diff --git a/build.sh b/build.sh index c70ec225351bfa5fb8b8083a20004e223056d5c2..0b635ead72a2b4e5dd13297b32da8531fa1c039b 100755 --- a/build.sh +++ b/build.sh @@ -144,9 +144,15 @@ function install_jdbc() if [ ! -d "${OUTPUT_DIR}" ]; then mkdir ${OUTPUT_DIR} fi - mv ${JDBC_DIR}/jdbc/target/postgresql-42.2.5.jar ${OUTPUT_DIR}/postgresql.jar + cd ${OUTPUT_DIR} + rm -rf *.jar + version=`awk '/[^<]+<\/version>/{gsub(/|<\/version>/,"",$1);print $1;exit;}' ${JDBC_DIR}/jdbc/pom.xml` + cp ${JDBC_DIR}/jdbc/target/opengauss-jdbc-${version}.jar . + mv ${JDBC_DIR}/jdbc/target/opengauss-jdbc-${version}.jar ./postgresql.jar echo "Successfully make postgresql.jar" - + cd ${OUTPUT_DIR}/ + tar -zcvf ${JDBC_DIR}/openGauss-${version}-JDBC.tar.gz *.jar + echo "Successfully make jdbc jar package" } function clean() @@ -199,25 +205,11 @@ function make_package() } function registerJars() { - cd $third_part_lib/common/commons-logging - cp *.jar $libs - cd $third_part_lib/common/commons-codec - cp *.jar $libs - cd $third_part_lib/common/httpclient - cp *.jar $libs - cd $third_part_lib/common/httpcore - cp *.jar $libs - cd $third_part_lib/common/fastjson - cp *.jar $libs - cd $third_part_lib/common/joda-time - cp *.jar $libs - cd $third_part_lib/common/jackson - cp *.jar $libs - cd $third_part_lib/common/slf4j - cp *.jar $libs - cd $third_part_lib/common/javasdkcore - cp *.jar $libs - + for src in `find $third_part_lib -name '*.jar'` + do + cp $src $libs/ + done + echo "copy finished" cd $libs prepare_env mvn install:install-file -Dfile=./commons-logging-1.2.jar -DgroupId=commons-logging -DartifactId=commons-logging -Dversion=1.2 -Dpackaging=jar @@ -226,7 +218,7 @@ function registerJars() mvn install:install-file -Dfile=./httpcore-4.4.13.jar -DgroupId=org.apache.httpcomponents -DartifactId=httpcore -Dversion=4.4.13 -Dpackaging=jar mvn install:install-file -Dfile=./fastjson-1.2.70.jar -DgroupId=com.alibaba -DartifactId=fastjson -Dversion=1.2.70 -Dpackaging=jar mvn install:install-file -Dfile=./joda-time-2.10.6.jar -DgroupId=joda-time -DartifactId=joda-time -Dversion=2.10.6 -Dpackaging=jar - mvn install:install-file -Dfile=./jackson-databind-2.11.2.jar -DgroupId=com.fasterxml.jackson.core -DartifactId=joda-time -Dversion=2.11.2 -Dpackaging=jar + mvn install:install-file -Dfile=./jackson-databind-2.11.2.jar -DgroupId=com.fasterxml.jackson.core -DartifactId=jackson-databind -Dversion=2.11.2 -Dpackaging=jar mvn install:install-file -Dfile=./jackson-core-2.11.2.jar -DgroupId=com.fasterxml.jackson.core -DartifactId=jackson-core -Dversion=2.11.2 -Dpackaging=jar mvn install:install-file -Dfile=./jackson-annotations-2.11.2.jar -DgroupId=com.fasterxml.jackson.core -DartifactId=jackson-annotations -Dversion=2.11.2 -Dpackaging=jar mvn install:install-file -Dfile=./slf4j-api-1.7.30.jar -DgroupId=org.slf4j -DartifactId=slf4j-api -Dversion=1.7.30 -Dpackaging=jar diff --git a/open_source/httpclient/httpclient-4.5.9.jar b/open_source/httpclient/httpclient-4.5.13.jar similarity index 70% rename from open_source/httpclient/httpclient-4.5.9.jar rename to open_source/httpclient/httpclient-4.5.13.jar index 83bc29dbe41c1af0d67f68a7c4dec8452a37ebda..218ee25f2b11b65c83c16dbea9d47f652c4993b0 100644 Binary files a/open_source/httpclient/httpclient-4.5.9.jar and b/open_source/httpclient/httpclient-4.5.13.jar differ diff --git a/open_source/httpcore/httpcore-4.4.11.jar b/open_source/httpcore/httpcore-4.4.13.jar similarity index 77% rename from open_source/httpcore/httpcore-4.4.11.jar rename to open_source/httpcore/httpcore-4.4.13.jar index c31d40143830a0fec9e905cbae65e1c3869d6bff..163dc438cbb4ff9302855d40976ba8d02bf7450c 100644 Binary files a/open_source/httpcore/httpcore-4.4.11.jar and b/open_source/httpcore/httpcore-4.4.13.jar differ diff --git a/open_source/jackson/jackson-annotations-2.10.2.jar b/open_source/jackson/jackson-annotations-2.11.2.jar similarity index 67% rename from open_source/jackson/jackson-annotations-2.10.2.jar rename to open_source/jackson/jackson-annotations-2.11.2.jar index cf617f207709a866086c915bcee65726d605e3a4..8a6f1884aa56518bdcce622c52e4898c340ccddb 100644 Binary files a/open_source/jackson/jackson-annotations-2.10.2.jar and b/open_source/jackson/jackson-annotations-2.11.2.jar differ diff --git a/open_source/jackson/jackson-core-2.10.2.jar b/open_source/jackson/jackson-core-2.11.2.jar similarity index 43% rename from open_source/jackson/jackson-core-2.10.2.jar rename to open_source/jackson/jackson-core-2.11.2.jar index b9163fc9e2c35dbb5d4b75316c32b80b10a9a129..b31deebf49255030bd872570217b013d102c53c8 100644 Binary files a/open_source/jackson/jackson-core-2.10.2.jar and b/open_source/jackson/jackson-core-2.11.2.jar differ diff --git a/open_source/jackson/jackson-databind-2.10.2.jar b/open_source/jackson/jackson-databind-2.11.2.jar similarity index 51% rename from open_source/jackson/jackson-databind-2.10.2.jar rename to open_source/jackson/jackson-databind-2.11.2.jar index 4a8c353ea2bc13527461d1272a58f1ec0fe69088..92e080a5898de9594e9a00de09d195173f1d4064 100644 Binary files a/open_source/jackson/jackson-databind-2.10.2.jar and b/open_source/jackson/jackson-databind-2.11.2.jar differ diff --git a/open_source/java-sdk-core/java-sdk-core-3.0.12.jar b/open_source/java-sdk-core/java-sdk-core-3.0.12.jar new file mode 100644 index 0000000000000000000000000000000000000000..eb9916e24f0fa5d34d19720b101dffa8ae459fea Binary files /dev/null and b/open_source/java-sdk-core/java-sdk-core-3.0.12.jar differ diff --git a/open_source/joda-time/joda-time-2.10.jar b/open_source/joda-time/joda-time-2.10.6.jar similarity index 69% rename from open_source/joda-time/joda-time-2.10.jar rename to open_source/joda-time/joda-time-2.10.6.jar index 1909c2f07cab5ff5f78c4361b916e7b0a2301540..90b6e7b08da0b8b85fec1ffac0f09e331c20f47c 100644 Binary files a/open_source/joda-time/joda-time-2.10.jar and b/open_source/joda-time/joda-time-2.10.6.jar differ diff --git a/pgjdbc/pom.xml b/pgjdbc/pom.xml index afaf1b9063f1042f85fa36367bcbe0324973f380..fa2d3a75e24702790c1f73611a51be9e0e523ff8 100644 --- a/pgjdbc/pom.xml +++ b/pgjdbc/pom.xml @@ -1,11 +1,11 @@ 4.0.0 - test - postgresql - PostgreSQL JDBC Driver - JDBC 4.2 - 42.2.5 - Java JDBC 4.2 (JRE 8+) driver for PostgreSQL database - https://github.com/pgjdbc/pgjdbc + org.opengauss + opengauss-jdbc + openGauss JDBC Driver + 2.0.0 + Java JDBC driver for openGauss + https://gitee.com/opengauss/openGauss-connector-jdbc @@ -15,8 +15,8 @@ - PostgreSQL Global Development Group - https://jdbc.postgresql.org/ + openGauss + https://opengauss.org/ diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java index 4c77349bd513cbe3f15f831d490bcab355a3678d..592f2c994ce2fd7981257b738e5735c563dff1c8 100644 --- a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java +++ b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java @@ -224,7 +224,7 @@ public class ConnectionFactoryImpl extends ConnectionFactory { try { newStream = tryConnect(user, database, info, socketFactory, hostSpec, SslMode.DISABLE); - LOGGER.debug("Downgraded to non-encrypted connection for host " + hostSpec); + LOGGER.debug("Downgraded to non-encrypted connection for host " + hostSpec); } catch (SQLException ee) { ex = ee; } catch (IOException ee) { @@ -300,7 +300,7 @@ public class ConnectionFactoryImpl extends ConnectionFactory { GlobalHostStatusTracker.reportHostStatus(hostSpec, HostStatus.ConnectFail); knownStates.put(hostSpec, HostStatus.ConnectFail); if (hostIter.hasNext()) { - LOGGER.debug("ConnectException occured while connecting to {0}" + hostSpec, cex); + LOGGER.info("ConnectException occured while connecting to {0}" + hostSpec, cex); // still more addresses to try continue; } @@ -312,7 +312,7 @@ public class ConnectionFactoryImpl extends ConnectionFactory { GlobalHostStatusTracker.reportHostStatus(hostSpec, HostStatus.ConnectFail); knownStates.put(hostSpec, HostStatus.ConnectFail); if (hostIter.hasNext()) { - LOGGER.debug("IOException occured while connecting to " + hostSpec, ioe); + LOGGER.info("IOException occured while connecting to " + hostSpec, ioe); // still more addresses to try continue; } @@ -323,7 +323,7 @@ public class ConnectionFactoryImpl extends ConnectionFactory { GlobalHostStatusTracker.reportHostStatus(hostSpec, HostStatus.ConnectFail); knownStates.put(hostSpec, HostStatus.ConnectFail); if (hostIter.hasNext()) { - LOGGER.debug("SQLException occured while connecting to " + hostSpec, se); + LOGGER.info("SQLException occured while connecting to " + hostSpec, se); // still more addresses to try continue; } diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java index 6c802ce34856974c72c3b33beae7e9231cdd983e..14f580cf9cb83a70c9165a5617c4e773e0783666 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java @@ -40,6 +40,7 @@ import java.io.OutputStreamWriter; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.io.Writer; +import java.lang.reflect.Type; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; @@ -59,6 +60,10 @@ import java.sql.SQLXML; import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Calendar; import java.util.Map; @@ -182,6 +187,8 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement { sqlTypeToOid.put(Types.DATE, Oid.DATE); sqlTypeToOid.put(Types.TIME, Oid.UNSPECIFIED); sqlTypeToOid.put(Types.TIMESTAMP, Oid.UNSPECIFIED); + sqlTypeToOid.put(Types.TIME_WITH_TIMEZONE, Oid.UNSPECIFIED); + sqlTypeToOid.put(Types.TIMESTAMP_WITH_TIMEZONE, Oid.UNSPECIFIED); sqlTypeToOid.put(Types.BOOLEAN, Oid.BOOL); sqlTypeToOid.put(Types.BIT, Oid.BOOL); sqlTypeToOid.put(Types.BINARY, Oid.BYTEA); @@ -555,6 +562,9 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement { java.sql.Date tmpd; if (in instanceof java.util.Date) { tmpd = new java.sql.Date(((java.util.Date) in).getTime()); + } else if (in instanceof LocalDate) { + setDate(parameterIndex, (LocalDate) in); + break; } else { tmpd = connection.getTimestampUtils().toDate(getDefaultCalendar(), in.toString()); } @@ -568,6 +578,9 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement { java.sql.Time tmpt; if (in instanceof java.util.Date) { tmpt = new java.sql.Time(((java.util.Date) in).getTime()); + } else if (in instanceof LocalTime) { + setTime(parameterIndex, (LocalTime) in); + break; } else { tmpt = connection.getTimestampUtils().toTime(getDefaultCalendar(), in.toString()); } @@ -583,12 +596,27 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement { java.sql.Timestamp tmpts; if (in instanceof java.util.Date) { tmpts = new java.sql.Timestamp(((java.util.Date) in).getTime()); + } else if (in instanceof LocalDateTime) { + setTimestamp(parameterIndex, (LocalDateTime) in); + break; } else { tmpts = connection.getTimestampUtils().toTimestamp(getDefaultCalendar(), in.toString()); } setTimestamp(parameterIndex, tmpts); } break; + case Types.TIMESTAMP_WITH_TIMEZONE: + if (in instanceof OffsetDateTime) { + setTimestamp(parameterIndex, (OffsetDateTime) in); + } else if (in instanceof PGTimestamp) { + setObject(parameterIndex, in); + } else { + throw new PSQLException( + GT.tr("Cannot cast an instance of {0} to type {1}", + in.getClass().getName(), "Types.TIMESTAMP_WITH_TIMEZONE"), + PSQLState.INVALID_PARAMETER_TYPE); + } + break; case Types.BOOLEAN: case Types.BIT: setBoolean(parameterIndex, BooleanTypeUtil.castToBoolean(in)); @@ -923,6 +951,14 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement { setObjectOfByte(parameterIndex, x); } else if (x instanceof java.sql.Date || x instanceof Time || x instanceof Timestamp) { setObjectOfDate(parameterIndex, x); + } else if (x instanceof LocalDate) { + setDate(parameterIndex, (LocalDate) x); + } else if (x instanceof LocalTime) { + setTime(parameterIndex, (LocalTime) x); + } else if (x instanceof LocalDateTime) { + setTimestamp(parameterIndex, (LocalDateTime) x); + } else if (x instanceof OffsetDateTime) { + setTimestamp(parameterIndex, (OffsetDateTime) x); } else if (x instanceof Boolean) { setBoolean(parameterIndex, (Boolean) x); } else if (x instanceof Blob) { @@ -1318,6 +1354,28 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement { bindString(i, connection.getTimestampUtils().toString(cal, t), oid); } + private void setDate(int i, LocalDate localDate) throws SQLException { + int oid = Oid.DATE; + bindString(i, connection.getTimestampUtils().toString(localDate), oid); + } + + private void setTime(int i, LocalTime localTime) throws SQLException { + int oid = Oid.TIME; + bindString(i, connection.getTimestampUtils().toString(localTime), oid); + } + + private void setTimestamp(int i, LocalDateTime localDateTime) + throws SQLException { + int oid = Oid.TIMESTAMP; + bindString(i, connection.getTimestampUtils().toString(localDateTime), oid); + } + + private void setTimestamp(int i, OffsetDateTime offsetDateTime) + throws SQLException { + int oid = Oid.TIMESTAMPTZ; + bindString(i, connection.getTimestampUtils().toString(offsetDateTime), oid); + } + public ParameterMetaData createParameterMetaData(BaseConnection conn, int[] oids) throws SQLException { return new PgParameterMetaData(conn, oids); diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java index e772605e98dd066a976a48c7a86670ea38b0bb79..39f9d817d35bba46bdf041864944c3b389f272e3 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java @@ -562,6 +562,29 @@ public class PgResultSet implements ResultSet, org.postgresql.PGRefCursorResultS return connection.getTimestampUtils().toTime(cal, string); } + private java.time.LocalTime getLocalTime(int i) throws SQLException { + byte[] value = getRawValue(i); + if (value == null) { + return null; + } + + if (isBinary(i)) { + int col = i - 1; + int oid = fields[col].getOID(); + if (oid == Oid.TIME) { + return connection.getTimestampUtils().toLocalTimeBin(value); + } else { + throw new PSQLException( + GT.tr("Cannot convert the column of type {0} to requested type {1}.", + Oid.toString(oid), "time"), + PSQLState.DATA_TYPE_MISMATCH); + } + } + + String string = getString(i); + return connection.getTimestampUtils().toLocalTime(string); + } + @Override public Timestamp getTimestamp(int i, java.util.Calendar cal) throws SQLException { checkResultSet(i); @@ -607,6 +630,69 @@ public class PgResultSet implements ResultSet, org.postgresql.PGRefCursorResultS return connection.getTimestampUtils().toTimestamp(cal, string); } + private java.time.OffsetDateTime getOffsetDateTime(int i) throws SQLException { + byte[] value = getRawValue(i); + if (value == null) { + return null; + } + + int col = i - 1; + int oid = fields[col].getOID(); + + if (isBinary(i)) { + if (oid == Oid.TIMESTAMPTZ || oid == Oid.TIMESTAMP) { + return connection.getTimestampUtils().toOffsetDateTimeBin(value); + } else if (oid == Oid.TIMETZ) { + // JDBC spec says timetz must be supported + Time time = getTime(i); + if (time == null) { + return null; + } + return connection.getTimestampUtils().toOffsetDateTime(time); + } else { + throw new PSQLException( + GT.tr("Cannot convert the column of type {0} to requested type {1}.", + Oid.toString(oid), "timestamptz"), + PSQLState.DATA_TYPE_MISMATCH); + } + } + + // If this is actually a timestamptz, the server-provided timezone will override + // the one we pass in, which is the desired behaviour. Otherwise, we'll + // interpret the timezone-less value in the provided timezone. + String string = getString(i); + if (oid == Oid.TIMETZ) { + // JDBC spec says timetz must be supported + // If server sends us a TIMETZ, we ensure java counterpart has date of 1970-01-01 + Calendar cal = getDefaultCalendar(); + Time time = connection.getTimestampUtils().toTime(cal, string); + return connection.getTimestampUtils().toOffsetDateTime(time); + } + return connection.getTimestampUtils().toOffsetDateTime(string); + } + + private java.time.LocalDateTime getLocalDateTime(int i) throws SQLException { + byte[] value = getRawValue(i); + if (value == null) { + return null; + } + + int col = i - 1; + int oid = fields[col].getOID(); + if (oid != Oid.TIMESTAMP) { + throw new PSQLException( + GT.tr("Cannot convert the column of type {0} to requested type {1}.", + Oid.toString(oid), "timestamp"), + PSQLState.DATA_TYPE_MISMATCH); + } + if (isBinary(i)) { + return connection.getTimestampUtils().toLocalDateTimeBin(value); + } + + String string = getString(i); + return connection.getTimestampUtils().toLocalDateTime(string); + } + public java.sql.Date getDate(String c, java.util.Calendar cal) throws SQLException { return getDate(findColumn(c), cal); } @@ -2722,6 +2808,27 @@ public class PgResultSet implements ResultSet, org.postgresql.PGRefCursorResultS } } + /** + * Checks that the result set is not closed, it's positioned on a valid row and that the given + * column number is valid. Also updates the {@link #wasNullFlag} to correct value. + * + * @param column The column number to check. Range starts from 1. + * @return byte[] value or null + * @throws SQLException If state or column is invalid. + */ + protected byte[] getRawValue(int column) throws SQLException { + checkClosed(); + if (this_row == null) { + throw new PSQLException( + GT.tr("ResultSet not positioned properly, perhaps you need to call next."), + PSQLState.INVALID_CURSOR_STATE); + } + checkColumnIndex(column); + byte[] bytes = this_row[column - 1]; + wasNullFlag = bytes == null; + return bytes; + } + /** * Checks that the result set is not closed, it's positioned on a valid row and that the given * column number is valid. Also updates the {@link #wasNullFlag} to correct value. @@ -3246,14 +3353,16 @@ public class PgResultSet implements ResultSet, org.postgresql.PGRefCursorResultS PSQLState.INVALID_PARAMETER_VALUE); } } else if (type == Timestamp.class) { - if (sqlType == Types.TIMESTAMP) { + if (sqlType == Types.TIMESTAMP + || sqlType == Types.TIME_WITH_TIMEZONE) { return type.cast(getTimestamp(columnIndex)); } else { throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, sqlType), PSQLState.INVALID_PARAMETER_VALUE); } } else if (type == Calendar.class) { - if (sqlType == Types.TIMESTAMP) { + if (sqlType == Types.TIMESTAMP + || sqlType == Types.TIME_WITH_TIMEZONE) { Timestamp timestampValue = getTimestamp(columnIndex); if (wasNull()) { return null; @@ -3316,6 +3425,53 @@ public class PgResultSet implements ResultSet, org.postgresql.PGRefCursorResultS } catch (UnknownHostException e) { throw new SQLException("could not create inet address from string '" + addressString + "'"); } + // JSR-310 support + } else if (type == java.time.LocalDate.class) { + if (sqlType == Types.DATE) { + Date dateValue = getDate(columnIndex); + if (dateValue == null) { + return null; + } + long time = dateValue.getTime(); + if (time == PGStatement.DATE_POSITIVE_INFINITY) { + return type.cast(java.time.LocalDate.MAX); + } + if (time == PGStatement.DATE_NEGATIVE_INFINITY) { + return type.cast(java.time.LocalDate.MIN); + } + return type.cast(dateValue.toLocalDate()); + } else if (sqlType == Types.TIMESTAMP) { + java.time.LocalDateTime localDateTimeValue = getLocalDateTime(columnIndex); + if (localDateTimeValue == null) { + return null; + } + return type.cast(localDateTimeValue.toLocalDate()); + } else { + throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)), + PSQLState.INVALID_PARAMETER_VALUE); + } + } else if (type == java.time.LocalTime.class) { + if (sqlType == Types.TIME) { + return type.cast(getLocalTime(columnIndex)); + } else { + throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)), + PSQLState.INVALID_PARAMETER_VALUE); + } + } else if (type == java.time.LocalDateTime.class) { + if (sqlType == Types.TIMESTAMP) { + return type.cast(getLocalDateTime(columnIndex)); + } else { + throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)), + PSQLState.INVALID_PARAMETER_VALUE); + } + } else if (type == java.time.OffsetDateTime.class) { + if (sqlType == Types.TIMESTAMP_WITH_TIMEZONE || sqlType == Types.TIMESTAMP) { + java.time.OffsetDateTime offsetDateTime = getOffsetDateTime(columnIndex); + return type.cast(offsetDateTime); + } else { + throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)), + PSQLState.INVALID_PARAMETER_VALUE); + } } else if (PGobject.class.isAssignableFrom(type)) { Object object; if (isBinary(columnIndex)) { diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/TimestampUtils.java b/pgjdbc/src/main/java/org/postgresql/jdbc/TimestampUtils.java index 28720d2ee705cdcf34d6f189e4ba2c3bf613f202..377818c5698a636457e5fea9a1de1e1993a6b7ab 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/TimestampUtils.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/TimestampUtils.java @@ -21,6 +21,10 @@ import java.sql.Date; import java.sql.SQLException; import java.sql.Time; import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.HashMap; @@ -40,6 +44,16 @@ public class TimestampUtils { private static final char[][] NUMBERS; private static final HashMap GMT_ZONES = new HashMap(); private static final int MAX_NANOS_BEFORE_WRAP_ON_ROUND = 999999500; + private static final java.time.Duration ONE_MICROSECOND = java.time.Duration.ofNanos(1000); + // LocalTime.MAX is 23:59:59.999_999_999, and it wraps to 24:00:00 when nanos exceed 999_999_499 + // since PostgreSQL has microsecond resolution only + private static final LocalTime MAX_TIME = LocalTime.MAX.minus(java.time.Duration.ofNanos(500)); + private static final OffsetDateTime MAX_OFFSET_DATETIME = OffsetDateTime.MAX.minus(java.time.Duration.ofMillis(500)); + private static final LocalDateTime MAX_LOCAL_DATETIME = LocalDateTime.MAX.minus(java.time.Duration.ofMillis(500)); + // low value for dates is 4713 BC + private static final LocalDate MIN_LOCAL_DATE = LocalDate.of(4713, 1, 1).with(java.time.temporal.ChronoField.ERA, java.time.chrono.IsoEra.BCE.getValue()); + private static final LocalDateTime MIN_LOCAL_DATETIME = MIN_LOCAL_DATE.atStartOfDay(); + private static final OffsetDateTime MIN_OFFSET_DATETIME = MIN_LOCAL_DATETIME.atOffset(java.time.ZoneOffset.UTC); private static final Field DEFAULT_TIME_ZONE_FIELD; private static Log LOGGER = Logger.getLogger(TimestampUtils.class.getName()); @@ -112,7 +126,7 @@ public class TimestampUtils { private final boolean usesDouble; private final Provider timeZoneProvider; - TimestampUtils(boolean usesDouble, Provider timeZoneProvider) { + public TimestampUtils(boolean usesDouble, Provider timeZoneProvider) { this.usesDouble = usesDouble; this.timeZoneProvider = timeZoneProvider; } @@ -391,6 +405,143 @@ public class TimestampUtils { return result; } + /** + * Parse a string and return a LocalTime representing its value. + * + * @param s The ISO formated time string to parse. + * @return null if s is null or a LocalTime of the parsed string s. + * @throws SQLException if there is a problem parsing s. + */ + public LocalTime toLocalTime(String s) throws SQLException { + if (s == null) { + return null; + } + + if (s.equals("24:00:00")) { + return LocalTime.MAX; + } + + try { + return LocalTime.parse(s); + } catch (java.time.format.DateTimeParseException nfe) { + throw new PSQLException( + GT.tr("Bad value for type timestamp/date/time: {1}", s), + PSQLState.BAD_DATETIME_FORMAT, nfe); + } + + } + + /** + * Parse a string and return a LocalDateTime representing its value. + * + * @param s The ISO formated date string to parse. + * @return null if s is null or a LocalDateTime of the parsed string s. + * @throws SQLException if there is a problem parsing s. + */ + public LocalDateTime toLocalDateTime(String s) throws SQLException { + if (s == null) { + return null; + } + + int slen = s.length(); + + // convert postgres's infinity values to internal infinity magic value + if (slen == 8 && s.equals("infinity")) { + return LocalDateTime.MAX; + } + + if (slen == 9 && s.equals("-infinity")) { + return LocalDateTime.MIN; + } + + ParsedTimestamp ts = parseBackendTimestamp(s); + + // intentionally ignore time zone + // 2004-10-19 10:23:54+03:00 is 2004-10-19 10:23:54 locally + LocalDateTime result = LocalDateTime.of(ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second, ts.nanos); + if (ts.era == GregorianCalendar.BC) { + return result.with(java.time.temporal.ChronoField.ERA, java.time.chrono.IsoEra.BCE.getValue()); + } else { + return result; + } + } + + /** + * Parse a string and return a LocalDateTime representing its value. + * + * @param s The ISO formated date string to parse. + * @return null if s is null or a LocalDateTime of the parsed string s. + * @throws SQLException if there is a problem parsing s. + */ + public OffsetDateTime toOffsetDateTime( + String s) throws SQLException { + if (s == null) { + return null; + } + + int slen = s.length(); + + // convert postgres's infinity values to internal infinity magic value + if (slen == 8 && s.equals("infinity")) { + return OffsetDateTime.MAX; + } + + if (slen == 9 && s.equals("-infinity")) { + return OffsetDateTime.MIN; + } + + ParsedTimestamp ts = parseBackendTimestamp(s); + + Calendar tz = ts.tz; + int offsetSeconds; + if (tz == null) { + offsetSeconds = 0; + } else { + offsetSeconds = tz.get(Calendar.ZONE_OFFSET) / 1000; + } + java.time.ZoneOffset zoneOffset = java.time.ZoneOffset.ofTotalSeconds(offsetSeconds); + // Postgres is always UTC + OffsetDateTime result = OffsetDateTime.of(ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second, ts.nanos, zoneOffset) + .withOffsetSameInstant(java.time.ZoneOffset.UTC); + if (ts.era == GregorianCalendar.BC) { + return result.with(java.time.temporal.ChronoField.ERA, java.time.chrono.IsoEra.BCE.getValue()); + } else { + return result; + } + } + + /** + * Returns the offset date time object matching the given bytes with Oid#TIMETZ. + * + * @param t the time value + * @return the matching offset date time + */ + public OffsetDateTime toOffsetDateTime(Time t) { + // hardcode utc because the backend does not provide us the timezone + // hardcode UNIX epoch, JDBC requires OffsetDateTime but doesn't describe what date should be used + return t.toLocalTime().atDate(LocalDate.of(1970, 1, 1)).atOffset(java.time.ZoneOffset.UTC); + } + + /** + * Returns the offset date time object matching the given bytes with Oid#TIMESTAMPTZ. + * + * @param bytes The binary encoded local date time value. + * @return The parsed local date time object. + * @throws PSQLException If binary format could not be parsed. + */ + public OffsetDateTime toOffsetDateTimeBin(byte[] bytes) throws PSQLException { + ParsedBinaryTimestamp parsedTimestamp = this.toProlepticParsedTimestampBin(bytes); + if (parsedTimestamp.infinity == Infinity.POSITIVE) { + return OffsetDateTime.MAX; + } else if (parsedTimestamp.infinity == Infinity.NEGATIVE) { + return OffsetDateTime.MIN; + } + + // hardcode utc because the backend does not provide us the timezone + // Postgres is always UTC + java.time.Instant instant = java.time.Instant.ofEpochSecond(parsedTimestamp.millis / 1000L, parsedTimestamp.nanos); + return OffsetDateTime.ofInstant(instant, java.time.ZoneOffset.UTC); + } public synchronized Time toTime(Calendar cal, String s) throws SQLException { // 1) Parse backend string @@ -675,6 +826,111 @@ public class TimestampUtils { } } + public synchronized String toString(LocalDate localDate) { + if (LocalDate.MAX.equals(localDate)) { + return "infinity"; + } else if (localDate.isBefore(MIN_LOCAL_DATE)) { + return "-infinity"; + } + + sbuf.setLength(0); + + appendDate(sbuf, localDate); + appendEra(sbuf, localDate); + + return sbuf.toString(); + } + + public synchronized String toString(LocalTime localTime) { + + sbuf.setLength(0); + + if (localTime.isAfter(MAX_TIME)) { + return "24:00:00"; + } + + int nano = localTime.getNano(); + if (nanosExceed499(nano)) { + // Technically speaking this is not a proper rounding, however + // it relies on the fact that appendTime just truncates 000..999 nanosecond part + localTime = localTime.plus(ONE_MICROSECOND); + } + appendTime(sbuf, localTime); + + return sbuf.toString(); + } + + public synchronized String toString(OffsetDateTime offsetDateTime) { + if (offsetDateTime.isAfter(MAX_OFFSET_DATETIME)) { + return "infinity"; + } else if (offsetDateTime.isBefore(MIN_OFFSET_DATETIME)) { + return "-infinity"; + } + + sbuf.setLength(0); + + int nano = offsetDateTime.getNano(); + if (nanosExceed499(nano)) { + // Technically speaking this is not a proper rounding, however + // it relies on the fact that appendTime just truncates 000..999 nanosecond part + offsetDateTime = offsetDateTime.plus(ONE_MICROSECOND); + } + LocalDateTime localDateTime = offsetDateTime.toLocalDateTime(); + LocalDate localDate = localDateTime.toLocalDate(); + appendDate(sbuf, localDate); + sbuf.append(' '); + appendTime(sbuf, localDateTime.toLocalTime()); + appendTimeZone(sbuf, offsetDateTime.getOffset()); + appendEra(sbuf, localDate); + + return sbuf.toString(); + } + + /** + * Formats {@link java.time.LocalDateTime} to be sent to the backend, thus it adds time zone. + * Do not use this method in {@link java.sql.ResultSet#getString(int)} + * @param localDateTime The local date to format as a String + * @return The formatted local date + */ + public synchronized String toString(LocalDateTime localDateTime) { + if (localDateTime.isAfter(MAX_LOCAL_DATETIME)) { + return "infinity"; + } else if (localDateTime.isBefore(MIN_LOCAL_DATETIME)) { + return "-infinity"; + } + + // LocalDateTime is always passed with time zone so backend can decide between timestamp and timestamptz + java.time.ZonedDateTime zonedDateTime = localDateTime.atZone(getDefaultTz().toZoneId()); + return toString(zonedDateTime.toOffsetDateTime()); + } + + private static void appendDate(StringBuilder sb, LocalDate localDate) { + int year = localDate.get(java.time.temporal.ChronoField.YEAR_OF_ERA); + int month = localDate.getMonthValue(); + int day = localDate.getDayOfMonth(); + appendDate(sb, year, month, day); + } + + private static void appendTime(StringBuilder sb, LocalTime localTime) { + int hours = localTime.getHour(); + int minutes = localTime.getMinute(); + int seconds = localTime.getSecond(); + int nanos = localTime.getNano(); + appendTime(sb, hours, minutes, seconds, nanos); + } + + private void appendTimeZone(StringBuilder sb, java.time.ZoneOffset offset) { + int offsetSeconds = offset.getTotalSeconds(); + + appendTimeZone(sb, offsetSeconds); + } + + private static void appendEra(StringBuilder sb, LocalDate localDate) { + if (localDate.get(java.time.temporal.ChronoField.ERA) == java.time.chrono.IsoEra.BCE.getValue()) { + sb.append(" BC"); + } + } + private static int skipWhitespace(char[] s, int start) { int slen = s.length; for (int i = start; i < slen; i++) { @@ -816,6 +1072,32 @@ public class TimestampUtils { return convertToTime(millis, tz); // Ensure date part is 1970-01-01 } + /** + * Returns the SQL Time object matching the given bytes with {@link Oid#TIME}. + * + * @param bytes The binary encoded time value. + * @return The parsed time object. + * @throws PSQLException If binary format could not be parsed. + */ + public LocalTime toLocalTimeBin(byte[] bytes) throws PSQLException { + if (bytes.length != 8) { + throw new PSQLException(GT.tr("Unsupported binary encoding of {0}.", "time"), + PSQLState.BAD_DATETIME_FORMAT); + } + + long micros; + + if (usesDouble) { + double seconds = ByteConverter.float8(bytes, 0); + + micros = (long) (seconds * 1000000d); + } else { + micros = ByteConverter.int8(bytes, 0); + } + + return LocalTime.ofNanoOfDay(micros * 1000); + } + /** * Returns the SQL Timestamp object matching the given bytes with {@link Oid#TIMESTAMP} or * {@link Oid#TIMESTAMPTZ}. @@ -1174,4 +1456,102 @@ public class TimestampUtils { } return TimeZone.getTimeZone(timeZone); } + + private ParsedBinaryTimestamp toParsedTimestampBinPlain(byte[] bytes) + throws PSQLException { + + if (bytes.length != 8) { + throw new PSQLException(GT.tr("Unsupported binary encoding of {0}.", "timestamp"), + PSQLState.BAD_DATETIME_FORMAT); + } + + long secs; + int nanos; + + if (usesDouble) { + double time = ByteConverter.float8(bytes, 0); + if (time == Double.POSITIVE_INFINITY) { + ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp(); + ts.infinity = Infinity.POSITIVE; + return ts; + } else if (time == Double.NEGATIVE_INFINITY) { + ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp(); + ts.infinity = Infinity.NEGATIVE; + return ts; + } + + secs = (long) time; + nanos = (int) ((time - secs) * 1000000); + } else { + long time = ByteConverter.int8(bytes, 0); + + // compatibility with text based receiving, not strictly necessary + // and can actually be confusing because there are timestamps + // that are larger than infinite + if (time == Long.MAX_VALUE) { + ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp(); + ts.infinity = Infinity.POSITIVE; + return ts; + } else if (time == Long.MIN_VALUE) { + ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp(); + ts.infinity = Infinity.NEGATIVE; + return ts; + } + + secs = time / 1000000; + nanos = (int) (time - secs * 1000000); + } + if (nanos < 0) { + secs--; + nanos += 1000000; + } + nanos *= 1000; + + long millis = secs * 1000L; + + ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp(); + ts.millis = millis; + ts.nanos = nanos; + return ts; + } + + private ParsedBinaryTimestamp toProlepticParsedTimestampBin(byte[] bytes) + throws PSQLException { + + ParsedBinaryTimestamp ts = toParsedTimestampBinPlain(bytes); + if (ts.infinity != null) { + return ts; + } + + long secs = ts.millis / 1000L; + + // postgres epoc to java epoc + secs += 946684800L; + long millis = secs * 1000L; + + ts.millis = millis; + return ts; + } + + /** + * Returns the local date time object matching the given bytes with {@link Oid#TIMESTAMP} or + * {@link Oid#TIMESTAMPTZ}. + * @param bytes The binary encoded local date time value. + * + * @return The parsed local date time object. + * @throws PSQLException If binary format could not be parsed. + */ + public LocalDateTime toLocalDateTimeBin(byte[] bytes) throws PSQLException { + + ParsedBinaryTimestamp parsedTimestamp = this.toProlepticParsedTimestampBin(bytes); + if (parsedTimestamp.infinity == Infinity.POSITIVE) { + return LocalDateTime.MAX; + } else if (parsedTimestamp.infinity == Infinity.NEGATIVE) { + return LocalDateTime.MIN; + } + + // hardcode utc because the backend does not provide us the timezone + // Postgres is always UTC + return LocalDateTime.ofEpochSecond(parsedTimestamp.millis / 1000L, parsedTimestamp.nanos, java.time.ZoneOffset.UTC); + } } diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/TypeInfoCache.java b/pgjdbc/src/main/java/org/postgresql/jdbc/TypeInfoCache.java index 04002da0da7e742033f5b630ae59dab0672d35b1..6ddcc44f0662d22a3e4bb8a5bd107513d2beb084 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/TypeInfoCache.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/TypeInfoCache.java @@ -263,7 +263,7 @@ public class TypeInfoCache implements TypeInfo { ResultSet rs = oidStatement.getResultSet(); if (rs.next()) { oid = (int) rs.getLong(1); - String internalName = rs.getString(2); + String internalName = pgTypeName.toLowerCase(); _oidToPgName.put(oid, internalName); _pgNameToOid.put(internalName, oid); } diff --git a/pgjdbc/src/test/java/org/postgresql/localtimedate/BaseTest4.java b/pgjdbc/src/test/java/org/postgresql/localtimedate/BaseTest4.java new file mode 100644 index 0000000000000000000000000000000000000000..fe9f7c7b9d0999d26c7655123836db7aa30838ac --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/localtimedate/BaseTest4.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2004, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.localtimedate; + +import org.postgresql.PGConnection; +import org.postgresql.PGProperty; +import org.postgresql.core.Version; +import org.postgresql.jdbc.PreferQueryMode; + +import org.junit.After; +import org.junit.Assume; +import org.junit.Before; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Properties; + +public class BaseTest4 { + + public enum BinaryMode { + REGULAR, FORCE + } + + public enum ReWriteBatchedInserts { + YES, NO + } + + public enum AutoCommit { + YES, NO + } + + public enum StringType { + UNSPECIFIED, VARCHAR; + } + + protected Connection con; + private BinaryMode binaryMode; + private ReWriteBatchedInserts reWriteBatchedInserts; + protected PreferQueryMode preferQueryMode; + private StringType stringType; + + protected void updateProperties(Properties props) { + if (binaryMode == BinaryMode.FORCE) { + forceBinary(props); + } + if (reWriteBatchedInserts == ReWriteBatchedInserts.YES) { + PGProperty.REWRITE_BATCHED_INSERTS.set(props, true); + } + if (stringType != null) { + PGProperty.STRING_TYPE.set(props, stringType.name().toLowerCase()); + } + } + + protected void forceBinary(Properties props) { + PGProperty.PREPARE_THRESHOLD.set(props, -1); + } + + public final void setBinaryMode(BinaryMode binaryMode) { + this.binaryMode = binaryMode; + } + + public StringType getStringType() { + return stringType; + } + + public void setStringType(StringType stringType) { + this.stringType = stringType; + } + + public void setReWriteBatchedInserts( + ReWriteBatchedInserts reWriteBatchedInserts) { + this.reWriteBatchedInserts = reWriteBatchedInserts; + } + + @After + public void tearDown() throws SQLException { + TestUtil.closeDB(con); + } + + @Before + public void setUp() throws Exception { + Properties props = new Properties(); + updateProperties(props); + con = TestUtil.openDB(props); + PGConnection pg = con.unwrap(PGConnection.class); + preferQueryMode = pg == null ? PreferQueryMode.EXTENDED : pg.getPreferQueryMode(); + } + + public void assumeByteaSupported() { + Assume.assumeTrue("bytea is not supported in simple protocol execution mode", + preferQueryMode.compareTo(PreferQueryMode.EXTENDED) >= 0); + } + + public void assumeCallableStatementsSupported() { + Assume.assumeTrue("callable statements are not fully supported in simple protocol execution mode", + preferQueryMode.compareTo(PreferQueryMode.EXTENDED) >= 0); + } + + public void assumeBinaryModeRegular() { + Assume.assumeTrue(binaryMode == BinaryMode.REGULAR); + } + + public void assumeBinaryModeForce() { + Assume.assumeTrue(binaryMode == BinaryMode.FORCE); + Assume.assumeTrue(preferQueryMode != PreferQueryMode.SIMPLE); + } + + /** + * Shorthand for {@code Assume.assumeTrue(TestUtil.haveMinimumServerVersion(conn, version)}. + */ + public void assumeMinimumServerVersion(String message, Version version) throws SQLException { + Assume.assumeTrue(message, TestUtil.haveMinimumServerVersion(con, version)); + } + + /** + * Shorthand for {@code Assume.assumeTrue(TestUtil.haveMinimumServerVersion(conn, version)}. + */ + public void assumeMinimumServerVersion(Version version) throws SQLException { + Assume.assumeTrue(TestUtil.haveMinimumServerVersion(con, version)); + } + +} diff --git a/pgjdbc/src/test/java/org/postgresql/localtimedate/GetObject310InfinityTests.java b/pgjdbc/src/test/java/org/postgresql/localtimedate/GetObject310InfinityTests.java new file mode 100644 index 0000000000000000000000000000000000000000..52c3ee345ac251a2eea43c1f3055057d225464b8 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/localtimedate/GetObject310InfinityTests.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2017, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.localtimedate; +import org.postgresql.core.ServerVersion; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.lang.reflect.Field; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +@RunWith(Parameterized.class) +public class GetObject310InfinityTests extends BaseTest4 { + private final String expression; + private final String pgType; + private final Class klass; + private final Object expectedValue; + + public GetObject310InfinityTests(BinaryMode binaryMode, String expression, + String pgType, Class klass, Object expectedValue) { + setBinaryMode(binaryMode); + this.expression = expression; + this.pgType = pgType; + this.klass = klass; + this.expectedValue = expectedValue; + } + + @Override + public void setUp() throws Exception { + super.setUp(); + Assume.assumeTrue("PostgreSQL 8.3 does not support 'infinity' for 'date'", + !"date".equals(pgType) || TestUtil.haveMinimumServerVersion(con, ServerVersion.v8_4)); + } + + @Parameterized.Parameters(name = "binary = {0}, expr = {1}, pgType = {2}, klass = {3}") + public static Iterable data() throws IllegalAccessException { + Collection ids = new ArrayList(); + for (BinaryMode binaryMode : BinaryMode.values()) { + for (String expression : Arrays.asList("-infinity", "infinity")) { + for (String pgType : Arrays.asList("date", "timestamp", + "timestamp with time zone")) { + for (Class klass : Arrays.asList(LocalDate.class, LocalDateTime.class, + OffsetDateTime.class)) { + if (klass.equals(LocalDate.class) && !pgType.equals("date")) { + continue; + } + if (klass.equals(LocalDateTime.class) && !pgType.startsWith("timestamp")) { + continue; + } + if (klass.equals(OffsetDateTime.class) && !pgType.startsWith("timestamp")) { + continue; + } + if (klass.equals(LocalDateTime.class) && pgType.equals("timestamp with time zone")) { + // org.postgresql.util.PSQLException: Cannot convert the column of type TIMESTAMPTZ to requested type timestamp. + continue; + } + Field field = null; + try { + field = klass.getField(expression.startsWith("-") ? "MIN" : "MAX"); + } catch (NoSuchFieldException e) { + throw new IllegalStateException("No min/max field in " + klass, e); + } + Object expected = field.get(null); + ids.add(new Object[]{binaryMode, expression, pgType, klass, expected}); + } + } + } + } + return ids; + } + + @Test + public void test() throws SQLException { + PreparedStatement stmt = con.prepareStatement("select '" + expression + "'::" + pgType); + ResultSet rs = stmt.executeQuery(); + rs.next(); + Object res = rs.getObject(1, klass); + Assert.assertEquals(expectedValue, res); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/localtimedate/GetObject310Test.java b/pgjdbc/src/test/java/org/postgresql/localtimedate/GetObject310Test.java new file mode 100644 index 0000000000000000000000000000000000000000..e1d547d0893e40947aea5baf70cc40657d504223 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/localtimedate/GetObject310Test.java @@ -0,0 +1,372 @@ +/* + * Copyright (c) 2004, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.localtimedate; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import org.postgresql.core.ServerVersion; +import org.postgresql.util.PSQLException; +import org.postgresql.util.PSQLState; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.chrono.IsoEra; +import java.time.temporal.ChronoField; +import java.time.temporal.Temporal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.TimeZone; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@RunWith(Parameterized.class) +public class GetObject310Test extends BaseTest4 { + + private static final TimeZone saveTZ = TimeZone.getDefault(); + + private static final ZoneOffset UTC = ZoneOffset.UTC; // +0000 always + private static final ZoneOffset GMT03 = ZoneOffset.of("+03:00"); // +0300 always + private static final ZoneOffset GMT05 = ZoneOffset.of("-05:00"); // -0500 always + private static final ZoneOffset GMT13 = ZoneOffset.of("+13:00"); // +1300 always + + public GetObject310Test(BinaryMode binaryMode) { + setBinaryMode(binaryMode); + } + + @Parameterized.Parameters(name = "binary = {0}") + public static Iterable data() { + Collection ids = new ArrayList(); + for (BinaryMode binaryMode : BinaryMode.values()) { + ids.add(new Object[]{binaryMode}); + } + return ids; + } + + @Override + public void setUp() throws Exception { + super.setUp(); + TestUtil.createTable(con, "table1", "timestamp_without_time_zone_column timestamp without time zone," + + "timestamp_with_time_zone_column timestamp with time zone," + + "date_column date," + + "time_without_time_zone_column time without time zone," + + "time_with_time_zone_column time with time zone" + ); + } + + @Override + public void tearDown() throws SQLException { + TimeZone.setDefault(saveTZ); + TestUtil.dropTable(con, "table1"); + super.tearDown(); + } + + /** + * Test the behavior getObject for date columns. + */ + @Test + public void testGetLocalDate() throws SQLException { + Statement stmt = con.createStatement(); + stmt.executeUpdate(TestUtil.insertSQL("table1","date_column","DATE '1999-01-08'")); + + ResultSet rs = stmt.executeQuery(TestUtil.selectSQL("table1", "date_column")); + try { + assertTrue(rs.next()); + LocalDate localDate = LocalDate.of(1999, 1, 8); + assertEquals(localDate, rs.getObject("date_column", LocalDate.class)); + assertEquals(localDate, rs.getObject(1, LocalDate.class)); + } finally { + rs.close(); + } + } + + /** + * Test the behavior getObject for time columns. + */ + @Test + public void testGetLocalTime() throws SQLException { + Statement stmt = con.createStatement(); + stmt.executeUpdate(TestUtil.insertSQL("table1","time_without_time_zone_column","TIME '04:05:06.123456'")); + + ResultSet rs = stmt.executeQuery(TestUtil.selectSQL("table1", "time_without_time_zone_column")); + try { + assertTrue(rs.next()); + LocalTime localTime = LocalTime.of(4, 5, 6, 123456000); + assertEquals(localTime, rs.getObject("time_without_time_zone_column", LocalTime.class)); + assertEquals(localTime, rs.getObject(1, LocalTime.class)); + } finally { + rs.close(); + } + } + + /** + * Test the behavior getObject for time columns with null. + */ + @Test + public void testGetLocalTimeNull() throws SQLException { + Statement stmt = con.createStatement(); + stmt.executeUpdate(TestUtil.insertSQL("table1","time_without_time_zone_column","NULL")); + + ResultSet rs = stmt.executeQuery(TestUtil.selectSQL("table1", "time_without_time_zone_column")); + try { + assertTrue(rs.next()); + assertNull(rs.getObject("time_without_time_zone_column", LocalTime.class)); + assertNull(rs.getObject(1, LocalTime.class)); + } finally { + rs.close(); + } + } + + /** + * Test the behavior getObject for time columns with null. + */ + @Test + public void testGetLocalTimeInvalidType() throws SQLException { + Statement stmt = con.createStatement(); + stmt.executeUpdate(TestUtil.insertSQL("table1","time_with_time_zone_column", "TIME '04:05:06.123456-08:00'")); + + ResultSet rs = stmt.executeQuery(TestUtil.selectSQL("table1", "time_with_time_zone_column")); + try { + assertTrue(rs.next()); + try { + assertNull(rs.getObject("time_with_time_zone_column", LocalTime.class)); + } catch (PSQLException e) { + assertTrue(e.getSQLState().equals(PSQLState.DATA_TYPE_MISMATCH.getState()) + || e.getSQLState().equals(PSQLState.BAD_DATETIME_FORMAT.getState())); + } + try { + assertNull(rs.getObject(1, LocalTime.class)); + } catch (PSQLException e) { + assertTrue(e.getSQLState().equals(PSQLState.DATA_TYPE_MISMATCH.getState()) + || e.getSQLState().equals(PSQLState.BAD_DATETIME_FORMAT.getState())); + } + } finally { + rs.close(); + } + } + + /** + * Test the behavior getObject for timestamp columns. + */ + @Test + public void testGetLocalDateTime() throws SQLException { + assumeTrue(TestUtil.haveIntegerDateTimes(con)); + + List zoneIdsToTest = new ArrayList(); + zoneIdsToTest.add("Africa/Casablanca"); // It is something like GMT+0..GMT+1 + zoneIdsToTest.add("America/Adak"); // It is something like GMT-10..GMT-9 + zoneIdsToTest.add("Atlantic/Azores"); // It is something like GMT-1..GMT+0 + zoneIdsToTest.add("Europe/Moscow"); // It is something like GMT+3..GMT+4 for 2000s + zoneIdsToTest.add("Pacific/Apia"); // It is something like GMT+13..GMT+14 + zoneIdsToTest.add("Pacific/Niue"); // It is something like GMT-11..GMT-11 + for (int i = -12; i <= 13; i++) { + zoneIdsToTest.add(String.format("GMT%+02d", i)); + } + + List datesToTest = Arrays.asList("2015-09-03T12:00:00", "2015-06-30T23:59:58", + "1997-06-30T23:59:59", "1997-07-01T00:00:00", "2012-06-30T23:59:59", "2012-07-01T00:00:00", + "2015-06-30T23:59:59", "2015-07-01T00:00:00", "2005-12-31T23:59:59", "2006-01-01T00:00:00", + "2008-12-31T23:59:59", "2009-01-01T00:00:00", /* "2015-06-30T23:59:60", */ "2015-07-31T00:00:00", + "2015-07-31T00:00:01", "2015-07-31T00:00:00.000001", + + // On 2000-03-26 02:00:00 Moscow went to DST, thus local time became 03:00:00 + "2000-03-26T01:59:59", "2000-03-26T02:00:00", "2000-03-26T02:00:01", "2000-03-26T02:59:59", + "2000-03-26T03:00:00", "2000-03-26T03:00:01", "2000-03-26T03:59:59", "2000-03-26T04:00:00", + "2000-03-26T04:00:01", "2000-03-26T04:00:00.000001", + + // This is a pre-1970 date, so check if it is rounded properly + "1950-07-20T02:00:00", + + // Ensure the calendar is proleptic + "1582-09-30T00:00:00", "1582-10-16T00:00:00", + + // On 2000-10-29 03:00:00 Moscow went to regular time, thus local time became 02:00:00 + "2000-10-29T01:59:59", "2000-10-29T02:00:00", "2000-10-29T02:00:01", "2000-10-29T02:59:59", + "2000-10-29T03:00:00", "2000-10-29T03:00:01", "2000-10-29T03:59:59", "2000-10-29T04:00:00", + "2000-10-29T04:00:01", "2000-10-29T04:00:00.000001"); + + for (String zoneId : zoneIdsToTest) { + ZoneId zone = ZoneId.of(zoneId); + for (String date : datesToTest) { + localTimestamps(zone, date); + } + } + } + + public void localTimestamps(ZoneId zoneId, String timestamp) throws SQLException { + TimeZone.setDefault(TimeZone.getTimeZone(zoneId)); + Statement stmt = con.createStatement(); + try { + stmt.executeUpdate(TestUtil.insertSQL("table1","timestamp_without_time_zone_column","TIMESTAMP '" + timestamp + "'")); + + ResultSet rs = stmt.executeQuery(TestUtil.selectSQL("table1", "timestamp_without_time_zone_column")); + try { + assertTrue(rs.next()); + LocalDateTime localDateTime = LocalDateTime.parse(timestamp); + assertEquals(localDateTime, rs.getObject("timestamp_without_time_zone_column", LocalDateTime.class)); + assertEquals(localDateTime, rs.getObject(1, LocalDateTime.class)); + + //Also test that we get the correct values when retrieving the data as LocalDate objects + assertEquals(localDateTime.toLocalDate(), rs.getObject("timestamp_without_time_zone_column", LocalDate.class)); + assertEquals(localDateTime.toLocalDate(), rs.getObject(1, LocalDate.class)); + } finally { + rs.close(); + } + stmt.executeUpdate("DELETE FROM table1"); + } finally { + stmt.close(); + } + } + + /** + * Test the behavior getObject for timestamp with time zone columns. + */ + @Test + public void testGetTimestampWithTimeZone() throws SQLException { + runGetOffsetDateTime(UTC); + runGetOffsetDateTime(GMT03); + runGetOffsetDateTime(GMT05); + runGetOffsetDateTime(GMT13); + } + + private void runGetOffsetDateTime(ZoneOffset offset) throws SQLException { + Statement stmt = con.createStatement(); + try { + stmt.executeUpdate(TestUtil.insertSQL("table1","timestamp_with_time_zone_column","TIMESTAMP WITH TIME ZONE '2004-10-19 10:23:54.123456" + offset.toString() + "'")); + + ResultSet rs = stmt.executeQuery(TestUtil.selectSQL("table1", "timestamp_with_time_zone_column")); + try { + assertTrue(rs.next()); + LocalDateTime localDateTime = LocalDateTime.of(2004, 10, 19, 10, 23, 54, 123456000); + + OffsetDateTime offsetDateTime = localDateTime.atOffset(offset).withOffsetSameInstant(ZoneOffset.UTC); + assertEquals(offsetDateTime, rs.getObject("timestamp_with_time_zone_column", OffsetDateTime.class)); + assertEquals(offsetDateTime, rs.getObject(1, OffsetDateTime.class)); + } finally { + rs.close(); + } + stmt.executeUpdate("DELETE FROM table1"); + } finally { + stmt.close(); + } + } + + @Test + public void testBcTimestamp() throws SQLException { + + Statement stmt = con.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT '1582-09-30 12:34:56 BC'::timestamp"); + try { + assertTrue(rs.next()); + LocalDateTime expected = LocalDateTime.of(1582, 9, 30, 12, 34, 56) + .with(ChronoField.ERA, IsoEra.BCE.getValue()); + LocalDateTime actual = rs.getObject(1, LocalDateTime.class); + assertEquals(expected, actual); + assertFalse(rs.next()); + } finally { + rs.close(); + stmt.close(); + } + } + + @Test + public void testBcTimestamptz() throws SQLException { + + Statement stmt = con.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT '1582-09-30 12:34:56Z BC'::timestamp"); + try { + assertTrue(rs.next()); + OffsetDateTime expected = OffsetDateTime.of(1582, 9, 30, 12, 34, 56, 0, UTC) + .with(ChronoField.ERA, IsoEra.BCE.getValue()); + OffsetDateTime actual = rs.getObject(1, OffsetDateTime.class); + assertEquals(expected, actual); + assertFalse(rs.next()); + } finally { + rs.close(); + stmt.close(); + } + } + + @Test + public void testProlepticCalendarTimestamp() throws SQLException { + // date time ranges and CTEs are both new with 8.4 + assumeMinimumServerVersion(ServerVersion.v8_4); + LocalDateTime start = LocalDate.of(1582, 9, 30).atStartOfDay(); + LocalDateTime end = LocalDate.of(1582, 10, 16).atStartOfDay(); + long numberOfDays = Duration.between(start, end).toDays() + 1L; + List range = Stream.iterate(start, new LocalDateTimePlusOneDay()) + .limit(numberOfDays) + .collect(Collectors.toList()); + + runProlepticTests(LocalDateTime.class, "'1582-09-30 00:00'::timestamp, '1582-10-16 00:00'::timestamp", range); + } + + @Test + public void testProlepticCalendarTimestamptz() throws SQLException { + // date time ranges and CTEs are both new with 8.4 + assumeMinimumServerVersion(ServerVersion.v8_4); + OffsetDateTime start = LocalDate.of(1582, 9, 30).atStartOfDay().atOffset(UTC); + OffsetDateTime end = LocalDate.of(1582, 10, 16).atStartOfDay().atOffset(UTC); + long numberOfDays = Duration.between(start, end).toDays() + 1L; + List range = Stream.iterate(start, new OffsetDateTimePlusOneDay()) + .limit(numberOfDays) + .collect(Collectors.toList()); + + runProlepticTests(OffsetDateTime.class, "'1582-09-30 00:00:00 Z'::timestamptz, '1582-10-16 00:00:00 Z'::timestamptz", range); + } + + private void runProlepticTests(Class clazz, String selectRange, List range) throws SQLException { + List temporals = new ArrayList<>(range.size()); + + PreparedStatement stmt = con.prepareStatement("SELECT * FROM generate_series(" + selectRange + ", '1 day');"); + ResultSet rs = stmt.executeQuery(); + try { + while (rs.next()) { + T temporal = rs.getObject(1, clazz); + temporals.add(temporal); + } + assertEquals(range, temporals); + } finally { + rs.close(); + stmt.close(); + } + } + + private static class LocalDateTimePlusOneDay implements UnaryOperator { + + @Override + public LocalDateTime apply(LocalDateTime x) { + return x.plusDays(1); + } + } + + private static class OffsetDateTimePlusOneDay implements UnaryOperator { + + @Override + public OffsetDateTime apply(OffsetDateTime x) { + return x.plusDays(1); + } + } + +} diff --git a/pgjdbc/src/test/java/org/postgresql/localtimedate/Jdbc42TestSuite.java b/pgjdbc/src/test/java/org/postgresql/localtimedate/Jdbc42TestSuite.java new file mode 100644 index 0000000000000000000000000000000000000000..b0615dc1f5d4129612a744f9045c5e1961ec52f4 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/localtimedate/Jdbc42TestSuite.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2004, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.localtimedate; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +@RunWith(Suite.class) +@SuiteClasses({ + GetObject310InfinityTests.class, + GetObject310Test.class, + PreparedStatementTest.class, + SetObject310Test.class, + SetObject310InfinityTests.class, + TimestampUtilsTest.class +}) +public class Jdbc42TestSuite { + +} diff --git a/pgjdbc/src/test/java/org/postgresql/localtimedate/PreparedStatementTest.java b/pgjdbc/src/test/java/org/postgresql/localtimedate/PreparedStatementTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e56aa48a616478c66fc313cf525238ed46a08acc --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/localtimedate/PreparedStatementTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2004, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.localtimedate; +import org.postgresql.PGProperty; + +import org.junit.Assert; +import org.junit.Test; + +import java.math.BigDecimal; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.time.LocalTime; +import java.util.Properties; + +public class PreparedStatementTest extends BaseTest4 { + protected void updateProperties(Properties props) { + PGProperty.PREFER_QUERY_MODE.set(props, "simple"); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + TestUtil.createTable(con, "timestamptztable", "tstz timestamptz"); + TestUtil.createTable(con, "timetztable", "ttz timetz"); + TestUtil.createTable(con, "timetable", "id serial, tt time"); + } + + @Override + public void tearDown() throws SQLException { + TestUtil.dropTable(con, "timestamptztable"); + TestUtil.dropTable(con, "timetztable"); + TestUtil.dropTable(con, "timetable"); + super.tearDown(); + } + +// @Test + public void testSetNumber() throws SQLException { + PreparedStatement pstmt = con.prepareStatement("SELECT ? * 2"); + + pstmt.setBigDecimal(1, new BigDecimal("1.6")); + ResultSet rs = pstmt.executeQuery(); + rs.next(); + BigDecimal d = rs.getBigDecimal(1); + pstmt.close(); + + Assert.assertEquals(new BigDecimal("3.2"), d); + } + + @Test + public void testTimestampTzSetNull() throws SQLException { + PreparedStatement pstmt = con.prepareStatement("INSERT INTO timestamptztable (tstz) VALUES (?)"); + + // valid: fully qualified type to setNull() + pstmt.setNull(1, Types.TIMESTAMP_WITH_TIMEZONE); + pstmt.executeUpdate(); + + // valid: fully qualified type to setObject() + pstmt.setObject(1, null, Types.TIMESTAMP_WITH_TIMEZONE); + pstmt.executeUpdate(); + + pstmt.close(); + } + + @Test + public void testTimeTzSetNull() throws SQLException { + PreparedStatement pstmt = con.prepareStatement("INSERT INTO timetztable (ttz) VALUES (?)"); + + // valid: fully qualified type to setNull() + pstmt.setNull(1, Types.TIME_WITH_TIMEZONE); + pstmt.executeUpdate(); + + // valid: fully qualified type to setObject() + pstmt.setObject(1, null, Types.TIME_WITH_TIMEZONE); + pstmt.executeUpdate(); + + pstmt.close(); + } + + @Test + public void testLocalTimeMax() throws SQLException { + PreparedStatement pstmt = con.prepareStatement("INSERT INTO timetable (tt) VALUES (?)"); + + pstmt.setObject(1, LocalTime.MAX); + pstmt.executeUpdate(); + + pstmt.setObject(1, LocalTime.MIN); + pstmt.executeUpdate(); + + ResultSet rs = con.createStatement().executeQuery("select tt from timetable order by id asc"); + Assert.assertTrue(rs.next()); + LocalTime localTime = (LocalTime)rs.getObject(1,LocalTime.class); + Assert.assertEquals( LocalTime.MAX, localTime); + + Assert.assertTrue(rs.next()); + localTime = (LocalTime)rs.getObject(1, LocalTime.class); + Assert.assertEquals( LocalTime.MIN, localTime); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/localtimedate/SetObject310InfinityTests.java b/pgjdbc/src/test/java/org/postgresql/localtimedate/SetObject310InfinityTests.java new file mode 100644 index 0000000000000000000000000000000000000000..0c1526d46bb5e110525c62f76b43cb26adda8d24 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/localtimedate/SetObject310InfinityTests.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2017, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.localtimedate; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.postgresql.core.ServerVersion; + +import org.junit.After; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collection; + +@RunWith(Parameterized.class) +public class SetObject310InfinityTests extends BaseTest4 { + + public SetObject310InfinityTests(BinaryMode binaryMode) { + setBinaryMode(binaryMode); + } + + @Parameterized.Parameters(name = "binary = {0}") + public static Iterable data() { + Collection ids = new ArrayList<>(2); + for (BinaryMode binaryMode : BinaryMode.values()) { + ids.add(new Object[]{binaryMode}); + } + return ids; + } + + @Override + public void setUp() throws Exception { + super.setUp(); + Assume.assumeTrue("PostgreSQL 8.3 does not support 'infinity' for 'date'", + TestUtil.haveMinimumServerVersion(con, ServerVersion.v8_4)); + super.setUp(); + TestUtil.createTable(con, "table1", "timestamp_without_time_zone_column timestamp without time zone," + + "timestamp_with_time_zone_column timestamp with time zone," + + "date_column date" + ); + } + + @After + public void tearDown() throws SQLException { + TestUtil.dropTable(con, "table1"); + super.tearDown(); + } + + @Test + public void testTimestamptz() throws SQLException { + runTestforType(OffsetDateTime.MAX, OffsetDateTime.MIN, "timestamp_without_time_zone_column", null); + } + + @Test + public void testTimestamp() throws SQLException { + runTestforType(LocalDateTime.MAX, LocalDateTime.MIN, "timestamp_without_time_zone_column", null); + } + + @Test + public void testDate() throws SQLException { + runTestforType(LocalDate.MAX, LocalDate.MIN, "date_column", null); + } + + private void runTestforType(Object max, Object min, String columnName, Integer type) throws SQLException { + insert(max, columnName, type); + String readback = readString(columnName); + assertEquals("infinity", readback); + delete(); + + insert(min, columnName, type); + readback = readString(columnName); + assertEquals("-infinity", readback); + delete(); + } + + private void insert(Object data, String columnName, Integer type) throws SQLException { + PreparedStatement ps = con.prepareStatement(TestUtil.insertSQL("table1", columnName, "?")); + try { + if (type != null) { + ps.setObject(1, data, type); + } else { + ps.setObject(1, data); + } + assertEquals(1, ps.executeUpdate()); + } finally { + ps.close(); + } + } + + private String readString(String columnName) throws SQLException { + Statement st = con.createStatement(); + try { + ResultSet rs = st.executeQuery(TestUtil.selectSQL("table1", columnName)); + try { + assertNotNull(rs); + assertTrue(rs.next()); + return rs.getString(1); + } finally { + rs.close(); + } + } finally { + st.close(); + } + } + + private void delete() throws SQLException { + Statement st = con.createStatement(); + try { + st.execute("DELETE FROM table1"); + } finally { + st.close(); + } + } + +} diff --git a/pgjdbc/src/test/java/org/postgresql/localtimedate/SetObject310Test.java b/pgjdbc/src/test/java/org/postgresql/localtimedate/SetObject310Test.java new file mode 100644 index 0000000000000000000000000000000000000000..11bfaa4d63c457decbe0dfe2cb88b14eb6deb952 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/localtimedate/SetObject310Test.java @@ -0,0 +1,437 @@ +/* + * Copyright (c) 2004, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.localtimedate; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Types; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.chrono.IsoChronology; +import java.time.chrono.IsoEra; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.ResolverStyle; +import java.time.format.SignStyle; +import java.time.temporal.ChronoField; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +@RunWith(Parameterized.class) +public class SetObject310Test extends BaseTest4 { + private static final TimeZone saveTZ = TimeZone.getDefault(); + + public static final DateTimeFormatter LOCAL_TIME_FORMATTER = + new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .appendValue(ChronoField.YEAR_OF_ERA, 4, 10, SignStyle.EXCEEDS_PAD) + .appendLiteral('-') + .appendValue(ChronoField.MONTH_OF_YEAR, 2) + .appendLiteral('-') + .appendValue(ChronoField.DAY_OF_MONTH, 2) + .appendLiteral(' ') + .append(DateTimeFormatter.ISO_LOCAL_TIME) + .optionalStart() + .appendOffset("+HH:mm", "+00") + .optionalEnd() + .optionalStart() + .appendLiteral(' ') + .appendPattern("GG") + .toFormatter(Locale.ROOT) + .withResolverStyle(ResolverStyle.LENIENT) + .withChronology(IsoChronology.INSTANCE); + + public SetObject310Test(BinaryMode binaryMode) { + setBinaryMode(binaryMode); + } + + @Parameterized.Parameters(name = "binary = {0}") + public static Iterable data() { + Collection ids = new ArrayList(); + for (BinaryMode binaryMode : BinaryMode.values()) { + ids.add(new Object[]{binaryMode}); + } + return ids; + } + + @Before + public void setUp() throws Exception { + super.setUp(); + TestUtil.createTable(con, "table1", "timestamp_without_time_zone_column timestamp without time zone," + + "timestamp_with_time_zone_column timestamp with time zone," + + "date_column date," + + "time_without_time_zone_column time without time zone," + + "time_with_time_zone_column time with time zone" + ); + } + + @After + public void tearDown() throws SQLException { + TimeZone.setDefault(saveTZ); + TestUtil.dropTable(con, "table1"); + super.tearDown(); + } + + private void insert(Object data, String columnName, Integer type) throws SQLException { + PreparedStatement ps = con.prepareStatement(TestUtil.insertSQL("table1", columnName, "?")); + try { + if (type != null) { + ps.setObject(1, data, type); + } else { + ps.setObject(1, data); + } + assertEquals(1, ps.executeUpdate()); + } finally { + ps.close(); + } + } + + private String readString(String columnName) throws SQLException { + Statement st = con.createStatement(); + try { + ResultSet rs = st.executeQuery(TestUtil.selectSQL("table1", columnName)); + try { + assertNotNull(rs); + assertTrue(rs.next()); + return rs.getString(1); + } finally { + rs.close(); + } + } finally { + st.close(); + } + } + + private String insertThenReadStringWithoutType(LocalDateTime data, String columnName) throws SQLException { + insert(data, columnName, null); + return readString(columnName); + } + + private String insertThenReadStringWithType(LocalDateTime data, String columnName) throws SQLException { + insert(data, columnName, Types.TIMESTAMP); + return readString(columnName); + } + + private void insertWithoutType(Object data, String columnName) throws SQLException { + insert(data, columnName, null); + } + + private T insertThenReadWithoutType(Object data, String columnName, Class expectedType) throws SQLException { + PreparedStatement ps = con.prepareStatement(TestUtil.insertSQL("table1", columnName, "?")); + try { + ps.setObject(1, data); + assertEquals(1, ps.executeUpdate()); + } finally { + ps.close(); + } + + Statement st = con.createStatement(); + try { + ResultSet rs = st.executeQuery(TestUtil.selectSQL("table1", columnName)); + try { + assertNotNull(rs); + + assertTrue(rs.next()); + return expectedType.cast(rs.getObject(1)); + } finally { + rs.close(); + } + } finally { + st.close(); + } + } + + private T insertThenReadWithType(Object data, int sqlType, String columnName, Class expectedType) throws SQLException { + PreparedStatement ps = con.prepareStatement(TestUtil.insertSQL("table1", columnName, "?")); + try { + ps.setObject(1, data, sqlType); + assertEquals(1, ps.executeUpdate()); + } finally { + ps.close(); + } + + Statement st = con.createStatement(); + try { + ResultSet rs = st.executeQuery(TestUtil.selectSQL("table1", columnName)); + try { + assertNotNull(rs); + + assertTrue(rs.next()); + return expectedType.cast(rs.getObject(1)); + } finally { + rs.close(); + } + } finally { + st.close(); + } + } + + private void deleteRows() throws SQLException { + Statement st = con.createStatement(); + try { + st.executeUpdate("DELETE FROM table1"); + } finally { + st.close(); + } + } + + /** + * Test the behavior of setObject for timestamp columns. + */ + @Test + public void testSetLocalDateTime() throws SQLException { + List zoneIdsToTest = getZoneIdsToTest(); + List datesToTest = getDatesToTest(); + + for (String zoneId : zoneIdsToTest) { + ZoneId zone = ZoneId.of(zoneId); + for (String date : datesToTest) { + LocalDateTime localDateTime = LocalDateTime.parse(date); + String expected = localDateTime.atZone(zone) + .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + .replace('T', ' '); + localTimestamps(zone, localDateTime, expected); + } + } + } + + /** + * Test the behavior of setObject for timestamp columns. + */ + @Test + public void testSetOffsetDateTime() throws SQLException { + List zoneIdsToTest = getZoneIdsToTest(); + List storeZones = new ArrayList(); + for (String zoneId : zoneIdsToTest) { + storeZones.add(TimeZone.getTimeZone(zoneId)); + } + List datesToTest = getDatesToTest(); + + for (TimeZone timeZone : storeZones) { + ZoneId zoneId = timeZone.toZoneId(); + for (String date : datesToTest) { + LocalDateTime localDateTime = LocalDateTime.parse(date); + String expected = date.replace('T', ' '); + offsetTimestamps(zoneId, localDateTime, expected, storeZones); + } + } + } + + private List getDatesToTest() { + return Arrays.asList("2015-09-03T12:00:00", "2015-06-30T23:59:58", + "1997-06-30T23:59:59", "1997-07-01T00:00:00", "2012-06-30T23:59:59", "2012-07-01T00:00:00", + "2015-06-30T23:59:59", "2015-07-01T00:00:00", "2005-12-31T23:59:59", "2006-01-01T00:00:00", + "2008-12-31T23:59:59", "2009-01-01T00:00:00", /* "2015-06-30T23:59:60", */ "2015-07-31T00:00:00", + "2015-07-31T00:00:01", "2015-07-31T00:00:00.000001", + + // On 2000-03-26 02:00:00 Moscow went to DST, thus local time became 03:00:00 + "2000-03-26T01:59:59", "2000-03-26T02:00:00", "2000-03-26T02:00:01", "2000-03-26T02:59:59", + "2000-03-26T03:00:00", "2000-03-26T03:00:01", "2000-03-26T03:59:59", "2000-03-26T04:00:00", + "2000-03-26T04:00:01", "2000-03-26T04:00:00.000001", + + // This is a pre-1970 date, so check if it is rounded properly + "1950-07-20T02:00:00", + + // Ensure the calendar is proleptic + "1582-09-30T00:00:00", "1582-10-16T00:00:00", + + // On 2000-10-29 03:00:00 Moscow went to regular time, thus local time became 02:00:00 + "2000-10-29T01:59:59", "2000-10-29T02:00:00", "2000-10-29T02:00:01", "2000-10-29T02:59:59", + "2000-10-29T03:00:00", "2000-10-29T03:00:01", "2000-10-29T03:59:59", "2000-10-29T04:00:00", + "2000-10-29T04:00:01", "2000-10-29T04:00:00.000001"); + } + + private List getZoneIdsToTest() { + List zoneIdsToTest = new ArrayList(); + zoneIdsToTest.add("Africa/Casablanca"); // It is something like GMT+0..GMT+1 + zoneIdsToTest.add("America/Adak"); // It is something like GMT-10..GMT-9 + zoneIdsToTest.add("Atlantic/Azores"); // It is something like GMT-1..GMT+0 + zoneIdsToTest.add("Europe/Moscow"); // It is something like GMT+3..GMT+4 for 2000s + zoneIdsToTest.add("Pacific/Apia"); // It is something like GMT+13..GMT+14 + zoneIdsToTest.add("Pacific/Niue"); // It is something like GMT-11..GMT-11 + for (int i = -12; i <= 13; i++) { + zoneIdsToTest.add(String.format("GMT%+02d", i)); + } + return zoneIdsToTest; + } + + private void localTimestamps(ZoneId zoneId, LocalDateTime localDateTime, String expected) throws SQLException { + TimeZone.setDefault(TimeZone.getTimeZone(zoneId)); + String readBack = insertThenReadStringWithoutType(localDateTime, "timestamp_without_time_zone_column"); + assertEquals( + "LocalDateTime=" + localDateTime + ", with TimeZone.default=" + zoneId + ", setObject(int, Object)", + expected, readBack); + deleteRows(); + + readBack = insertThenReadStringWithType(localDateTime, "timestamp_without_time_zone_column"); + assertEquals( + "LocalDateTime=" + localDateTime + ", with TimeZone.default=" + zoneId + ", setObject(int, Object, TIMESTAMP)", + expected, readBack); + deleteRows(); + } + + private void offsetTimestamps(ZoneId dataZone, LocalDateTime localDateTime, String expected, List storeZones) throws SQLException { + OffsetDateTime data = localDateTime.atZone(dataZone).toOffsetDateTime(); + try (PreparedStatement ps = con.prepareStatement( + "select ?::timestamp with time zone, ?::timestamp with time zone")) { + for (TimeZone storeZone : storeZones) { + TimeZone.setDefault(storeZone); + ps.setObject(1, data); + ps.setObject(2, data, Types.TIMESTAMP_WITH_TIMEZONE); + try (ResultSet rs = ps.executeQuery()) { + rs.next(); + String noType = rs.getString(1); + String noTypeAppend = ifAddAppendString(noType); + OffsetDateTime noTypeRes = OffsetDateTime.parse(noType.replace(' ', 'T') + noTypeAppend); + assertEquals( + "OffsetDateTime=" + data + " (with ZoneId=" + dataZone + "), with TimeZone.default=" + + storeZone + ", setObject(int, Object)", data.toInstant(), + noTypeRes.toInstant()); + String withType = rs.getString(2); + String withTypeAppend = ifAddAppendString(withType); + OffsetDateTime withTypeRes = OffsetDateTime.parse(withType.replace(' ', 'T') + withTypeAppend); + assertEquals( + "OffsetDateTime=" + data + " (with ZoneId=" + dataZone + "), with TimeZone.default=" + + storeZone + ", setObject(int, Object, TIMESTAMP_WITH_TIMEZONE)", + data.toInstant(), withTypeRes.toInstant()); + } + } + } + } + + private String ifAddAppendString(String offsetDateTimeString) { + String append = ""; + if (offsetDateTimeString.length() - offsetDateTimeString.indexOf("+") <= 5) { + append = ":00"; + } + return append; + } + + @Test + public void testLocalDateTimeRounding() throws SQLException { + LocalDateTime dateTime = LocalDateTime.parse("2018-12-31T23:59:59.999999500"); + localTimestamps(ZoneOffset.UTC, dateTime, "2019-01-01 00:00:00"); + } + + @Test + public void testTimeStampRounding() throws SQLException { + // TODO: fix for binary + assumeBinaryModeRegular(); + LocalTime time = LocalTime.parse("23:59:59.999999500"); + Time actual = insertThenReadWithoutType(time, "time_without_time_zone_column", Time.class); + assertEquals(Time.valueOf("24:00:00"), actual); + } + + @Test + public void testTimeStampRoundingWithType() throws SQLException { + // TODO: fix for binary + assumeBinaryModeRegular(); + LocalTime time = LocalTime.parse("23:59:59.999999500"); + Time actual = + insertThenReadWithType(time, Types.TIME, "time_without_time_zone_column", Time.class); + assertEquals(Time.valueOf("24:00:00"), actual); + } + + /** + * Test the behavior of setObject for timestamp columns. + */ + @Test + public void testSetLocalDateTimeBc() throws SQLException { + assumeTrue(TestUtil.haveIntegerDateTimes(con)); + + // use BC for funsies + List bcDates = new ArrayList(); + bcDates.add(LocalDateTime.parse("1997-06-30T23:59:59.999999").with(ChronoField.ERA, IsoEra.BCE.getValue())); + bcDates.add(LocalDateTime.parse("0997-06-30T23:59:59.999999").with(ChronoField.ERA, IsoEra.BCE.getValue())); + + for (LocalDateTime bcDate : bcDates) { + String expected = LOCAL_TIME_FORMATTER.format(bcDate); + localTimestamps(ZoneOffset.UTC, bcDate, expected); + } + } + + /** + * Test the behavior setObject for date columns. + */ + @Test + public void testSetLocalDateWithType() throws SQLException { + LocalDate data = LocalDate.parse("1971-12-15"); + java.sql.Date actual = insertThenReadWithType(data, Types.DATE, "date_column", java.sql.Date.class); + java.sql.Date expected = java.sql.Date.valueOf("1971-12-15"); + assertEquals(expected, actual); + } + + /** + * Test the behavior setObject for date columns. + */ + @Test + public void testSetLocalDateWithoutType() throws SQLException { + LocalDate data = LocalDate.parse("1971-12-15"); + java.sql.Date actual = insertThenReadWithoutType(data, "date_column", java.sql.Date.class); + java.sql.Date expected = java.sql.Date.valueOf("1971-12-15"); + assertEquals(expected, actual); + } + + /** + * Test the behavior setObject for time columns. + */ + @Test + public void testSetLocalTimeAndReadBack() throws SQLException { + // TODO: fix for binary mode. + // Avoid micros truncation in org.postgresql.jdbc.PgResultSet#internalGetObject + assumeBinaryModeRegular(); + LocalTime data = LocalTime.parse("16:21:51.123456"); + + insertWithoutType(data, "time_without_time_zone_column"); + + String readBack = readString("time_without_time_zone_column"); + assertEquals("16:21:51.123456", readBack); + } + + /** + * Test the behavior setObject for time columns. + */ + @Test + public void testSetLocalTimeWithType() throws SQLException { + LocalTime data = LocalTime.parse("16:21:51"); + Time actual = insertThenReadWithType(data, Types.TIME, "time_without_time_zone_column", Time.class); + Time expected = Time.valueOf("16:21:51"); + assertEquals(expected, actual); + } + + /** + * Test the behavior setObject for time columns. + */ + @Test + public void testSetLocalTimeWithoutType() throws SQLException { + LocalTime data = LocalTime.parse("16:21:51"); + Time actual = insertThenReadWithoutType(data, "time_without_time_zone_column", Time.class); + Time expected = Time.valueOf("16:21:51"); + assertEquals(expected, actual); + } + +} diff --git a/pgjdbc/src/test/java/org/postgresql/localtimedate/TestUtil.java b/pgjdbc/src/test/java/org/postgresql/localtimedate/TestUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..f8c3d18655f1a619d1e3537967f853a092f11bb8 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/localtimedate/TestUtil.java @@ -0,0 +1,1065 @@ +/* + * Copyright (c) 2004, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.localtimedate; +import org.postgresql.PGProperty; +import org.postgresql.core.BaseConnection; +import org.postgresql.core.ServerVersion; +import org.postgresql.core.TransactionState; +import org.postgresql.core.Version; +import org.postgresql.jdbc.PgConnection; +import org.postgresql.util.PSQLException; + +import org.junit.Assert; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Utility class for JDBC tests. + */ +public class TestUtil { + /* + * The case is as follows: + * 1. Typically the database and hostname are taken from System.properties or build.properties or build.local.properties + * That enables to override test DB via system property + * 2. There are tests where different DBs should be used (e.g. SSL tests), so we can't just use DB name from system property + * That is why _test_ properties exist: they overpower System.properties and build.properties + */ + public static final String SERVER_HOST_PORT_PROP = "_test_hostport"; + public static final String DATABASE_PROP = "_test_database"; + + /* + * Returns the Test database JDBC URL + */ + public static String getURL() { + return getURL(getServer(), + getPort()); + } + + public static String getURL(String server, int port) { + return getURL(server + ":" + port, getDatabase()); + } + + public static String getURL(String hostport, String database) { + String logLevel = ""; + if (getLogLevel() != null && !getLogLevel().equals("")) { + logLevel = "&loggerLevel=" + getLogLevel(); + } + + String logFile = ""; + if (getLogFile() != null && !getLogFile().equals("")) { + logFile = "&loggerFile=" + getLogFile(); + } + + String protocolVersion = ""; + if (getProtocolVersion() != 0) { + protocolVersion = "&protocolVersion=" + getProtocolVersion(); + } + + String options = ""; + if (getOptions() != null) { + options = "&options=" + getOptions(); + } + + String binaryTransfer = ""; + if (getBinaryTransfer() != null && !getBinaryTransfer().equals("")) { + binaryTransfer = "&binaryTransfer=" + getBinaryTransfer(); + } + + String receiveBufferSize = ""; + if (getReceiveBufferSize() != -1) { + receiveBufferSize = "&receiveBufferSize=" + getReceiveBufferSize(); + } + + String sendBufferSize = ""; + if (getSendBufferSize() != -1) { + sendBufferSize = "&sendBufferSize=" + getSendBufferSize(); + } + + String ssl = ""; + if (getSSL() != null) { + ssl = "&ssl=" + getSSL(); + } + + return "jdbc:postgresql://" + + hostport + "/" + + database + + "?ApplicationName=Driver Tests" + + logLevel + + logFile + + protocolVersion + + options + + binaryTransfer + + receiveBufferSize + + sendBufferSize + + ssl; + } + + /* + * Returns the Test server + */ + public static String getServer() { + return System.getProperty("server", "localhost"); + } + + /* + * Returns the Test port + */ + public static int getPort() { + return Integer.parseInt(System.getProperty("port", System.getProperty("def_pgport"))); + } + + /* + * Returns the server side prepared statement threshold. + */ + public static int getPrepareThreshold() { + return Integer.parseInt(System.getProperty("preparethreshold", "5")); + } + + public static int getProtocolVersion() { + return Integer.parseInt(System.getProperty("protocolVersion", "0")); + } + + public static String getOptions() { + return System.getProperty("options"); + } + + /* + * Returns the Test database + */ + public static String getDatabase() { + return System.getProperty("database"); + } + + /* + * Returns the Postgresql username + */ + public static String getUser() { + return System.getProperty("username"); + } + + /* + * Returns the user's password + */ + public static String getPassword() { + return System.getProperty("password"); + } + + /* + * Returns password for default callbackhandler + */ + public static String getSslPassword() { + return System.getProperty(PGProperty.SSL_PASSWORD.getName()); + } + +// /* +// * Return the GSSEncMode for the tests +// */ +// public static GSSEncMode getGSSEncMode() throws PSQLException { +// return GSSEncMode.of(System.getProperties()); +// } + + /* + * Returns the user for SSPI authentication tests + */ + public static String getSSPIUser() { + return System.getProperty("sspiusername"); + } + + /* + * postgres like user + */ + public static String getPrivilegedUser() { + return System.getProperty("privilegedUser"); + } + + public static String getPrivilegedPassword() { + return System.getProperty("privilegedPassword"); + } + + /* + * Returns the log level to use + */ + public static String getLogLevel() { + return System.getProperty("loggerLevel"); + } + + /* + * Returns the log file to use + */ + public static String getLogFile() { + return System.getProperty("loggerFile"); + } + + /* + * Returns the binary transfer mode to use + */ + public static String getBinaryTransfer() { + return System.getProperty("binaryTransfer"); + } + + public static int getSendBufferSize() { + return Integer.parseInt(System.getProperty("sendBufferSize", "-1")); + } + + public static int getReceiveBufferSize() { + return Integer.parseInt(System.getProperty("receiveBufferSize", "-1")); + } + + public static String getSSL() { + return System.getProperty("ssl"); + } + + static { + try { + initDriver(); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Unable to initialize driver", e); + } + } + + private static boolean initialized = false; + + public static Properties loadPropertyFiles(String... names) { + Properties p = new Properties(); + for (String name : names) { + for (int i = 0; i < 2; i++) { + // load x.properties, then x.local.properties + if (i == 1 && name.endsWith(".properties") && !name.endsWith(".local.properties")) { + name = name.replaceAll("\\.properties$", ".local.properties"); + } + File f = getFile(name); + if (!f.exists()) { + System.out.println("Configuration file " + f.getAbsolutePath() + + " does not exist. Consider adding it to specify test db host and login"); + continue; + } + try { + p.load(new FileInputStream(f)); + } catch (IOException ex) { + // ignore + } + } + } + return p; + } + + public static void initDriver() { + synchronized (TestUtil.class) { + if (initialized) { + return; + } + + Properties p = loadPropertyFiles("build.properties"); + p.putAll(System.getProperties()); + System.getProperties().putAll(p); + + initialized = true; + } + } + + /** + * Resolves file path with account of {@code build.properties.relative.path}. This is a bit tricky + * since during maven release, maven does a temporary checkout to {@code core/target/checkout} + * folder, so that script should somehow get {@code build.local.properties} + * + * @param name original name of the file, as if it was in the root pgjdbc folder + * @return actual location of the file + */ + public static File getFile(String name) { + if (name == null) { + throw new IllegalArgumentException("null file name is not expected"); + } + if (name.startsWith("/")) { + return new File(name); + } + return new File(System.getProperty("build.properties.relative.path", "../"), name); + } + + /** + * Get a connection using a privileged user mostly for tests that the ability to load C functions + * now as of 4/14. + * + * @return connection using a privileged user mostly for tests that the ability to load C + * functions now as of 4/14 + */ + public static Connection openPrivilegedDB() throws SQLException { + initDriver(); + Properties properties = new Properties(); + +// PGProperty.GSS_ENC_MODE.set(properties,getGSSEncMode().value); + properties.setProperty("user", getPrivilegedUser()); + properties.setProperty("password", getPrivilegedPassword()); + return DriverManager.getConnection(getURL(), properties); + + } + + /** + * Helper - opens a connection. + * + * @return connection + */ + public static Connection openDB() throws SQLException { + return openDB(new Properties()); + } + + /* + * Helper - opens a connection with the allowance for passing additional parameters, like + * "compatible". + */ + public static Connection openDB(Properties props) throws SQLException { + initDriver(); + + // Allow properties to override the user name. + String user = props.getProperty("username"); + if (user == null) { + user = getUser(); + } + if (user == null) { + throw new IllegalArgumentException( + "user name is not specified. Please specify 'username' property via -D or build.properties"); + } + props.setProperty("user", user); + String password = getPassword(); + if (password == null) { + password = ""; + } + props.setProperty("password", password); + String sslPassword = getSslPassword(); + if (sslPassword != null) { + PGProperty.SSL_PASSWORD.set(props, sslPassword); + } + + if (!props.containsKey(PGProperty.PREPARE_THRESHOLD.getName())) { + PGProperty.PREPARE_THRESHOLD.set(props, getPrepareThreshold()); + } + if (!props.containsKey(PGProperty.PREFER_QUERY_MODE.getName())) { + String value = System.getProperty(PGProperty.PREFER_QUERY_MODE.getName()); + if (value != null) { + props.put(PGProperty.PREFER_QUERY_MODE.getName(), value); + } + } + // Enable Base4 tests to override host,port,database + String hostport = props.getProperty(SERVER_HOST_PORT_PROP, getServer() + ":" + getPort()); + String database = props.getProperty(DATABASE_PROP, getDatabase()); + + // Set GSSEncMode for tests only in the case the property is already missing +// if (PGProperty.GSS_ENC_MODE.getSetString(props) == null) { +// PGProperty.GSS_ENC_MODE.set(props, getGSSEncMode().value); +// } + + return DriverManager.getConnection(getURL(hostport, database), props); + } + + /* + * Helper - closes an open connection. + */ + public static void closeDB(Connection con) throws SQLException { + if (con != null) { + con.close(); + } + } + + /* + * Helper - creates a test schema for use by a test + */ + public static void createSchema(Connection con, String schema) throws SQLException { + Statement st = con.createStatement(); + try { + // Drop the schema + dropSchema(con, schema); + + // Now create the schema + String sql = "CREATE SCHEMA " + schema; + + st.executeUpdate(sql); + } finally { + closeQuietly(st); + } + } + + /* + * Helper - drops a schema + */ + public static void dropSchema(Connection con, String schema) throws SQLException { + Statement stmt = con.createStatement(); + try { + if (con.getAutoCommit()) { + // Not in a transaction so ignore error for missing object + stmt.executeUpdate("DROP SCHEMA IF EXISTS " + schema + " CASCADE"); + } else { + // In a transaction so do not ignore errors for missing object + stmt.executeUpdate("DROP SCHEMA " + schema + " CASCADE"); + } + } finally { + closeQuietly(stmt); + } + } + + /* + * Helper - creates a test table for use by a test + */ + public static void createTable(Connection con, String table, String columns) throws SQLException { + Statement st = con.createStatement(); + try { + // Drop the table + dropTable(con, table); + + // Now create the table + String sql = "CREATE TABLE " + table + " (" + columns + ")"; + + st.executeUpdate(sql); + } finally { + closeQuietly(st); + } + } + + /** + * Helper creates a temporary table. + * + * @param con Connection + * @param table String + * @param columns String + */ + + public static void createTempTable(Connection con, String table, String columns) + throws SQLException { + Statement st = con.createStatement(); + try { + // Drop the table + dropTable(con, table); + + // Now create the table + st.executeUpdate("create temp table " + table + " (" + columns + ")"); + } finally { + closeQuietly(st); + } + } + + /* + * Helper - creates a unlogged table for use by a test. + * Unlogged tables works from PostgreSQL 9.1+ + */ + public static void createUnloggedTable(Connection con, String table, String columns) + throws SQLException { + Statement st = con.createStatement(); + try { + // Drop the table + dropTable(con, table); + + String unlogged = haveMinimumServerVersion(con, ServerVersion.v9_1) ? "UNLOGGED" : ""; + + // Now create the table + st.executeUpdate("CREATE " + unlogged + " TABLE " + table + " (" + columns + ")"); + } finally { + closeQuietly(st); + } + } + + /** + * Helper creates an enum type. + * + * @param con Connection + * @param name String + * @param values String + */ + + public static void createEnumType(Connection con, String name, String values) + throws SQLException { + Statement st = con.createStatement(); + try { + dropType(con, name); + + // Now create the table + st.executeUpdate("create type " + name + " as enum (" + values + ")"); + } finally { + closeQuietly(st); + } + } + + /** + * Helper creates an composite type. + * + * @param con Connection + * @param name String + * @param values String + */ + public static void createCompositeType(Connection con, String name, String values) throws SQLException { + createCompositeType(con, name, values, true); + } + + /** + * Helper creates an composite type. + * + * @param con Connection + * @param name String + * @param values String + */ + public static void createCompositeType(Connection con, String name, String values, boolean shouldDrop) + throws SQLException { + Statement st = con.createStatement(); + try { + if (shouldDrop) { + dropType(con, name); + } + // Now create the type + st.executeUpdate("CREATE TYPE " + name + " AS (" + values + ")"); + } finally { + closeQuietly(st); + } + } + + /** + * Drops a domain. + * + * @param con Connection + * @param name String + */ + public static void dropDomain(Connection con, String name) + throws SQLException { + Statement stmt = con.createStatement(); + try { + if (con.getAutoCommit()) { + // Not in a transaction so ignore error for missing object + stmt.executeUpdate("DROP DOMAIN IF EXISTS " + name + " CASCADE"); + } else { + // In a transaction so do not ignore errors for missing object + stmt.executeUpdate("DROP DOMAIN " + name + " CASCADE"); + } + } finally { + closeQuietly(stmt); + } + } + + /** + * Helper creates a domain. + * + * @param con Connection + * @param name String + * @param values String + */ + public static void createDomain(Connection con, String name, String values) + throws SQLException { + Statement st = con.createStatement(); + try { + dropDomain(con, name); + // Now create the table + st.executeUpdate("create domain " + name + " as " + values); + } finally { + closeQuietly(st); + } + } + + /* + * drop a sequence because older versions don't have dependency information for serials + */ + public static void dropSequence(Connection con, String sequence) throws SQLException { + Statement stmt = con.createStatement(); + try { + if (con.getAutoCommit()) { + // Not in a transaction so ignore error for missing object + stmt.executeUpdate("DROP SEQUENCE IF EXISTS " + sequence + " CASCADE"); + } else { + // In a transaction so do not ignore errors for missing object + stmt.executeUpdate("DROP SEQUENCE " + sequence + " CASCADE"); + } + } finally { + closeQuietly(stmt); + } + } + + /* + * Helper - drops a table + */ + public static void dropTable(Connection con, String table) throws SQLException { + Statement stmt = con.createStatement(); + try { + if (con.getAutoCommit()) { + // Not in a transaction so ignore error for missing object + stmt.executeUpdate("DROP TABLE IF EXISTS " + table + " CASCADE "); + } else { + // In a transaction so do not ignore errors for missing object + stmt.executeUpdate("DROP TABLE " + table + " CASCADE "); + } + } finally { + closeQuietly(stmt); + } + } + + /* + * Helper - drops a type + */ + public static void dropType(Connection con, String type) throws SQLException { + Statement stmt = con.createStatement(); + try { + if (con.getAutoCommit()) { + // Not in a transaction so ignore error for missing object + stmt.executeUpdate("DROP TYPE IF EXISTS " + type + " CASCADE"); + } else { + // In a transaction so do not ignore errors for missing object + stmt.executeUpdate("DROP TYPE " + type + " CASCADE"); + } + } finally { + closeQuietly(stmt); + } + } + + public static void assertNumberOfRows(Connection con, String tableName, int expectedRows, String message) + throws SQLException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + ps = con.prepareStatement("select count(*) from " + tableName + " as t"); + rs = ps.executeQuery(); + rs.next(); + Assert.assertEquals(message, expectedRows, rs.getInt(1)); + } finally { + closeQuietly(rs); + closeQuietly(ps); + } + } + + public static void assertTransactionState(String message, Connection con, TransactionState expected) { + TransactionState actual = TestUtil.getTransactionState(con); + Assert.assertEquals(message, expected, actual); + } + + /* + * Helper - generates INSERT SQL - very simple + */ + public static String insertSQL(String table, String values) { + return insertSQL(table, null, values); + } + + public static String insertSQL(String table, String columns, String values) { + String s = "INSERT INTO " + table; + + if (columns != null) { + s = s + " (" + columns + ")"; + } + + return s + " VALUES (" + values + ")"; + } + + /* + * Helper - generates SELECT SQL - very simple + */ + public static String selectSQL(String table, String columns) { + return selectSQL(table, columns, null, null); + } + + public static String selectSQL(String table, String columns, String where) { + return selectSQL(table, columns, where, null); + } + + public static String selectSQL(String table, String columns, String where, String other) { + String s = "SELECT " + columns + " FROM " + table; + + if (where != null) { + s = s + " WHERE " + where; + } + if (other != null) { + s = s + " " + other; + } + + return s; + } + + /* + * Helper to prefix a number with leading zeros - ugly but it works... + * + * @param v value to prefix + * + * @param l number of digits (0-10) + */ + public static String fix(int v, int l) { + String s = "0000000000".substring(0, l) + Integer.toString(v); + return s.substring(s.length() - l); + } + + public static String escapeString(Connection con, String value) throws SQLException { + if (con == null) { + throw new NullPointerException("Connection is null"); + } + if (con instanceof PgConnection) { + return ((PgConnection) con).escapeString(value); + } + return value; + } + + public static boolean getStandardConformingStrings(Connection con) { + if (con == null) { + throw new NullPointerException("Connection is null"); + } + if (con instanceof PgConnection) { + return ((PgConnection) con).getStandardConformingStrings(); + } + return false; + } + + /** + * Determine if the given connection is connected to a server with a version of at least the given + * version. This is convenient because we are working with a java.sql.Connection, not an Postgres + * connection. + */ + public static boolean haveMinimumServerVersion(Connection con, int version) throws SQLException { + if (con == null) { + throw new NullPointerException("Connection is null"); + } + if (con instanceof PgConnection) { + return ((PgConnection) con).haveMinimumServerVersion(version); + } + return false; + } + + public static boolean haveMinimumServerVersion(Connection con, Version version) + throws SQLException { + if (con == null) { + throw new NullPointerException("Connection is null"); + } + if (con instanceof PgConnection) { + return ((PgConnection) con).haveMinimumServerVersion(version); + } + return false; + } + + public static boolean haveMinimumJVMVersion(String version) { + String jvm = System.getProperty("java.version"); + return (jvm.compareTo(version) >= 0); + } + + public static boolean haveIntegerDateTimes(Connection con) { + if (con == null) { + throw new NullPointerException("Connection is null"); + } + if (con instanceof PgConnection) { + return ((PgConnection) con).getQueryExecutor().getIntegerDateTimes(); + } + return false; + } + + /** + * Print a ResultSet to System.out. This is useful for debugging tests. + */ + public static void printResultSet(ResultSet rs) throws SQLException { + ResultSetMetaData rsmd = rs.getMetaData(); + for (int i = 1; i <= rsmd.getColumnCount(); i++) { + if (i != 1) { + System.out.print(", "); + } + System.out.print(rsmd.getColumnName(i)); + } + System.out.println(); + while (rs.next()) { + for (int i = 1; i <= rsmd.getColumnCount(); i++) { + if (i != 1) { + System.out.print(", "); + } + System.out.print(rs.getString(i)); + } + System.out.println(); + } + } + + public static List resultSetToLines(ResultSet rs) throws SQLException { + List res = new ArrayList(); + ResultSetMetaData rsmd = rs.getMetaData(); + StringBuilder sb = new StringBuilder(); + while (rs.next()) { + sb.setLength(0); + for (int i = 1; i <= rsmd.getColumnCount(); i++) { + if (i != 1) { + sb.append(','); + } + sb.append(rs.getString(i)); + } + res.add(sb.toString()); + } + return res; + } + + public static String join(List list) { + StringBuilder sb = new StringBuilder(); + for (String s : list) { + if (sb.length() > 0) { + sb.append('\n'); + } + sb.append(s); + } + return sb.toString(); + } + + /* + * Find the column for the given label. Only SQLExceptions for system or set-up problems are + * thrown. The PSQLState.UNDEFINED_COLUMN type exception is consumed to allow cleanup. Relying on + * the caller to detect if the column lookup was successful. + */ + public static int findColumn(PreparedStatement query, String label) throws SQLException { + int returnValue = 0; + ResultSet rs = query.executeQuery(); + if (rs.next()) { + try { + returnValue = rs.findColumn(label); + } catch (SQLException sqle) { + } // consume exception to allow cleanup of resource. + } + rs.close(); + return returnValue; + } + + /** + * Close a resource and ignore any errors during closing. + */ + public static void closeQuietly(Closeable resource) { + if (resource != null) { + try { + resource.close(); + } catch (Exception ignore) { + } + } + } + + /** + * Close a Connection and ignore any errors during closing. + */ + public static void closeQuietly(Connection conn) { + if (conn != null) { + try { + conn.close(); + } catch (SQLException ignore) { + } + } + } + + /** + * Close a Statement and ignore any errors during closing. + */ + public static void closeQuietly(Statement stmt) { + if (stmt != null) { + try { + stmt.close(); + } catch (SQLException ignore) { + } + } + } + + /** + * Close a ResultSet and ignore any errors during closing. + */ + public static void closeQuietly(ResultSet rs) { + if (rs != null) { + try { + rs.close(); + } catch (SQLException ignore) { + } + } + } + + public static void recreateLogicalReplicationSlot(Connection connection, String slotName, String outputPlugin) + throws SQLException, InterruptedException, TimeoutException { + //drop previous slot + dropReplicationSlot(connection, slotName); + + PreparedStatement stm = null; + try { + stm = connection.prepareStatement("SELECT * FROM pg_create_logical_replication_slot(?, ?)"); + stm.setString(1, slotName); + stm.setString(2, outputPlugin); + stm.execute(); + } finally { + closeQuietly(stm); + } + } + + public static void recreatePhysicalReplicationSlot(Connection connection, String slotName) + throws SQLException, InterruptedException, TimeoutException { + //drop previous slot + dropReplicationSlot(connection, slotName); + + PreparedStatement stm = null; + try { + stm = connection.prepareStatement("SELECT * FROM pg_create_physical_replication_slot(?)"); + stm.setString(1, slotName); + stm.execute(); + } finally { + closeQuietly(stm); + } + } + + public static void dropReplicationSlot(Connection connection, String slotName) + throws SQLException, InterruptedException, TimeoutException { + if (haveMinimumServerVersion(connection, ServerVersion.v9_5)) { + PreparedStatement stm = null; + try { + stm = connection.prepareStatement( + "select pg_terminate_backend(active_pid) from pg_replication_slots " + + "where active = true and slot_name = ?"); + stm.setString(1, slotName); + stm.execute(); + } finally { + closeQuietly(stm); + } + } + + waitStopReplicationSlot(connection, slotName); + + PreparedStatement stm = null; + try { + stm = connection.prepareStatement( + "select pg_drop_replication_slot(slot_name) " + + "from pg_replication_slots where slot_name = ?"); + stm.setString(1, slotName); + stm.execute(); + } finally { + closeQuietly(stm); + } + } + + public static boolean isReplicationSlotActive(Connection connection, String slotName) + throws SQLException { + PreparedStatement stm = null; + ResultSet rs = null; + + try { + stm = + connection.prepareStatement("select active from pg_replication_slots where slot_name = ?"); + stm.setString(1, slotName); + rs = stm.executeQuery(); + return rs.next() && rs.getBoolean(1); + } finally { + closeQuietly(rs); + closeQuietly(stm); + } + } + + /** + * Execute a SQL query with a given connection and return whether any rows were + * returned. No column data is fetched. + */ + public static boolean executeQuery(Connection conn, String sql) throws SQLException { + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql); + boolean hasNext = rs.next(); + rs.close(); + stmt.close(); + return hasNext; + } + + /** + * Execute a SQL query with a given connection, fetch the first row, and return its + * string value. + */ + public static String queryForString(Connection conn, String sql) throws SQLException { + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql); + Assert.assertTrue("Query should have returned exactly one row but none was found: " + sql, rs.next()); + String value = rs.getString(1); + Assert.assertFalse("Query should have returned exactly one row but more than one found: " + sql, rs.next()); + rs.close(); + stmt.close(); + return value; + } + + /** + * Retrieve the backend process id for a given connection. + */ + public static int getBackendPid(Connection conn) throws SQLException { + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT pg_backend_pid()"); + rs.next(); + int pid = rs.getInt(1); + rs.close(); + stmt.close(); + return pid; + } + + /** + * Executed pg_terminate_backend(...) to terminate the server process for + * a given process id with the given connection. + */ + public static boolean terminateBackend(Connection conn, int backendPid) throws SQLException { + PreparedStatement stmt = conn.prepareStatement("SELECT pg_terminate_backend(?)"); + stmt.setInt(1, backendPid); + ResultSet rs = stmt.executeQuery(); + rs.next(); + boolean wasTerminated = rs.getBoolean(1); + rs.close(); + stmt.close(); + return wasTerminated; + } + + /** + * Create a new connection using the default test credentials and use it to + * attempt to terminate the specified backend process. + */ + public static boolean terminateBackend(int backendPid) throws SQLException { + Connection conn = TestUtil.openPrivilegedDB(); + try { + return terminateBackend(conn, backendPid); + } finally { + conn.close(); + } + } + + /** + * Retrieve the given connection backend process id, then create a new connection + * using the default test credentials and attempt to terminate the process. + */ + public static boolean terminateBackend(Connection conn) throws SQLException { + int pid = getBackendPid(conn); + return terminateBackend(pid); + } + + public static TransactionState getTransactionState(Connection conn) { + return ((BaseConnection) conn).getTransactionState(); + } + + private static void waitStopReplicationSlot(Connection connection, String slotName) + throws InterruptedException, TimeoutException, SQLException { + long startWaitTime = System.currentTimeMillis(); + boolean stillActive; + long timeInWait = 0; + + do { + stillActive = isReplicationSlotActive(connection, slotName); + if (stillActive) { + TimeUnit.MILLISECONDS.sleep(100L); + timeInWait = System.currentTimeMillis() - startWaitTime; + } + } while (stillActive && timeInWait <= 30000); + + if (stillActive) { + throw new TimeoutException("Wait stop replication slot " + timeInWait + " timeout occurs"); + } + } + + public static void execute(String sql, Connection connection) throws SQLException { + Statement stmt = connection.createStatement(); + try { + stmt.execute(sql); + } finally { + try { + stmt.close(); + } catch (SQLException e) { + } + } + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/localtimedate/TimestampUtilsTest.java b/pgjdbc/src/test/java/org/postgresql/localtimedate/TimestampUtilsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ca2cc4bee7bd6ed5415c9c747989c959b22b5d02 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/localtimedate/TimestampUtilsTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2019, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.localtimedate; +import static org.junit.Assert.assertEquals; + +import org.postgresql.core.Provider; +import org.postgresql.jdbc.TimestampUtils; + +import org.junit.Test; + +import java.sql.SQLException; +import java.time.LocalTime; +import java.util.TimeZone; + +public class TimestampUtilsTest { + @Test + public void testToStringOfLocalTime() { + TimestampUtils timestampUtils = createTimestampUtils(); + + assertEquals("00:00:00", timestampUtils.toString(LocalTime.parse("00:00:00"))); + assertEquals("00:00:00.1", timestampUtils.toString(LocalTime.parse("00:00:00.1"))); + assertEquals("00:00:00.12", timestampUtils.toString(LocalTime.parse("00:00:00.12"))); + assertEquals("00:00:00.123", timestampUtils.toString(LocalTime.parse("00:00:00.123"))); + assertEquals("00:00:00.1234", timestampUtils.toString(LocalTime.parse("00:00:00.1234"))); + assertEquals("00:00:00.12345", timestampUtils.toString(LocalTime.parse("00:00:00.12345"))); + assertEquals("00:00:00.123456", timestampUtils.toString(LocalTime.parse("00:00:00.123456"))); + + assertEquals("00:00:00.999999", timestampUtils.toString(LocalTime.parse("00:00:00.999999"))); + assertEquals("00:00:00.999999", timestampUtils.toString(LocalTime.parse("00:00:00.999999499"))); // 499 NanoSeconds + assertEquals("00:00:01", timestampUtils.toString(LocalTime.parse("00:00:00.999999500"))); // 500 NanoSeconds + + assertEquals("23:59:59", timestampUtils.toString(LocalTime.parse("23:59:59"))); + assertEquals("23:59:59.999999", timestampUtils.toString(LocalTime.parse("23:59:59.999999"))); // 0 NanoSeconds + assertEquals("23:59:59.999999", timestampUtils.toString(LocalTime.parse("23:59:59.999999499"))); // 499 NanoSeconds + assertEquals("24:00:00", timestampUtils.toString(LocalTime.parse("23:59:59.999999500")));// 500 NanoSeconds + assertEquals("24:00:00", timestampUtils.toString(LocalTime.parse("23:59:59.999999999")));// 999 NanoSeconds + } + + @Test + public void testToLocalTime() throws SQLException { + TimestampUtils timestampUtils = createTimestampUtils(); + + assertEquals(LocalTime.parse("00:00:00"), timestampUtils.toLocalTime("00:00:00")); + + assertEquals(LocalTime.parse("00:00:00.1"), timestampUtils.toLocalTime("00:00:00.1")); + assertEquals(LocalTime.parse("00:00:00.12"), timestampUtils.toLocalTime("00:00:00.12")); + assertEquals(LocalTime.parse("00:00:00.123"), timestampUtils.toLocalTime("00:00:00.123")); + assertEquals(LocalTime.parse("00:00:00.1234"), timestampUtils.toLocalTime("00:00:00.1234")); + assertEquals(LocalTime.parse("00:00:00.12345"), timestampUtils.toLocalTime("00:00:00.12345")); + assertEquals(LocalTime.parse("00:00:00.123456"), timestampUtils.toLocalTime("00:00:00.123456")); + assertEquals(LocalTime.parse("00:00:00.999999"), timestampUtils.toLocalTime("00:00:00.999999")); + + assertEquals(LocalTime.parse("23:59:59"), timestampUtils.toLocalTime("23:59:59")); + assertEquals(LocalTime.parse("23:59:59.999999"), timestampUtils.toLocalTime("23:59:59.999999")); // 0 NanoSeconds + assertEquals(LocalTime.parse("23:59:59.9999999"), timestampUtils.toLocalTime("23:59:59.9999999")); // 900 NanoSeconds + assertEquals(LocalTime.parse("23:59:59.99999999"), timestampUtils.toLocalTime("23:59:59.99999999")); // 990 NanoSeconds + assertEquals(LocalTime.parse("23:59:59.999999998"), timestampUtils.toLocalTime("23:59:59.999999998")); // 998 NanoSeconds + assertEquals(LocalTime.parse("23:59:59.999999999"), timestampUtils.toLocalTime("24:00:00")); + } + + private TimestampUtils createTimestampUtils() { + return new TimestampUtils(true, (Provider) TimeZone::getDefault); + } +} diff --git a/prepare_demo.sh b/prepare_demo.sh new file mode 100644 index 0000000000000000000000000000000000000000..5213d467418e768ed2d9dcdabd1db150e002aef6 --- /dev/null +++ b/prepare_demo.sh @@ -0,0 +1,9 @@ +cd shade +mvn clean install -Dmaven.test.skip=true +rm -rf temp +mkdir temp +cp target/demo-0.0.1-SNAPSHOT.jar ./temp/ +cd temp +jar -xf demo-0.0.1-SNAPSHOT.jar +find ./com -name "*" | sort | xargs zip demo-0.0.1-SNAPSHOT_new.jar +mvn install:install-file -Dfile=./demo-0.0.1-SNAPSHOT_new.jar -DgroupId=com.huawei -DartifactId=demo-0.0.1-SNAPSHOT -Dversion=0.0.1 -Dpackaging=jar diff --git a/prepare_maven.sh b/prepare_maven.sh new file mode 100644 index 0000000000000000000000000000000000000000..8cbef0f12a5cc45e1010d43f2629da9b698ac921 --- /dev/null +++ b/prepare_maven.sh @@ -0,0 +1,18 @@ +echo begin run +mkdir libs +for src in `find open_source -name '*.jar'` +do + cp $src ./libs/ +done +mvn install:install-file -Dfile=./libs/commons-logging-1.2.jar -DgroupId=commons-logging -DartifactId=commons-logging -Dversion=1.2 -Dpackaging=jar +mvn install:install-file -Dfile=./libs/commons-codec-1.11.jar -DgroupId=commons-codec -DartifactId=commons-codec -Dversion=1.11 -Dpackaging=jar +mvn install:install-file -Dfile=./libs/httpclient-4.5.13.jar -DgroupId=org.apache.httpcomponents -DartifactId=httpclient -Dversion=4.5.13 -Dpackaging=jar +mvn install:install-file -Dfile=./libs/httpcore-4.4.13.jar -DgroupId=org.apache.httpcomponents -DartifactId=httpcore -Dversion=4.4.13 -Dpackaging=jar +mvn install:install-file -Dfile=./libs/fastjson-1.2.70.jar -DgroupId=com.alibaba -DartifactId=fastjson -Dversion=1.2.70 -Dpackaging=jar +mvn install:install-file -Dfile=./libs/joda-time-2.10.6.jar -DgroupId=joda-time -DartifactId=joda-time -Dversion=2.10.6 -Dpackaging=jar +mvn install:install-file -Dfile=./libs/jackson-databind-2.11.2.jar -DgroupId=com.fasterxml.jackson.core -DartifactId=jackson-databind -Dversion=2.11.2 -Dpackaging=jar +mvn install:install-file -Dfile=./libs/jackson-core-2.11.2.jar -DgroupId=com.fasterxml.jackson.core -DartifactId=jackson-core -Dversion=2.11.2 -Dpackaging=jar +mvn install:install-file -Dfile=./libs/jackson-annotations-2.11.2.jar -DgroupId=com.fasterxml.jackson.core -DartifactId=jackson-annotations -Dversion=2.11.2 -Dpackaging=jar +mvn install:install-file -Dfile=./libs/slf4j-api-1.7.30.jar -DgroupId=org.slf4j -DartifactId=slf4j-api -Dversion=1.7.30 -Dpackaging=jar +mvn install:install-file -Dfile=./libs/java-sdk-core-3.0.12.jar -DgroupId=com.huawei.apigateway -DartifactId=hw-java-sdk-core -Dversion=3.0.12 -Dpackaging=jar +