diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java index 80dc0affc598..483b6b5dcb77 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java @@ -72,6 +72,7 @@ import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; import org.hibernate.type.JavaObjectType; +import org.hibernate.type.descriptor.DateTimeUtils; import org.hibernate.type.descriptor.jdbc.BlobJdbcType; import org.hibernate.type.descriptor.jdbc.ClobJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; @@ -129,11 +130,10 @@ import static org.hibernate.type.SqlTypes.UUID; import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDateWithEraSuffix; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTime; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMicros; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillis; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMicrosAndEraSuffix; /** * A {@linkplain Dialect SQL dialect} for CockroachDB. @@ -742,7 +742,7 @@ public void appendDateTimeLiteral( switch ( precision ) { case DATE: appender.appendSql( "date '" ); - appendAsDate( appender, temporalAccessor ); + appendAsDateWithEraSuffix( appender, temporalAccessor ); appender.appendSql( '\'' ); break; case TIME: @@ -759,12 +759,12 @@ public void appendDateTimeLiteral( case TIMESTAMP: if ( supportsTemporalLiteralOffset() && temporalAccessor.isSupported( ChronoField.OFFSET_SECONDS ) ) { appender.appendSql( "timestamp with time zone '" ); - appendAsTimestampWithMicros( appender, temporalAccessor, true, jdbcTimeZone ); + appendAsTimestampWithMicrosAndEraSuffix( appender, temporalAccessor, true, jdbcTimeZone ); appender.appendSql( '\'' ); } else { appender.appendSql( "timestamp '" ); - appendAsTimestampWithMicros( appender, temporalAccessor, false, jdbcTimeZone ); + appendAsTimestampWithMicrosAndEraSuffix( appender, temporalAccessor, false, jdbcTimeZone ); appender.appendSql( '\'' ); } break; @@ -778,7 +778,7 @@ public void appendDateTimeLiteral(SqlAppender appender, Date date, TemporalType switch ( precision ) { case DATE: appender.appendSql( "date '" ); - appendAsDate( appender, date ); + appendAsDateWithEraSuffix( appender, date ); appender.appendSql( '\'' ); break; case TIME: @@ -788,7 +788,7 @@ public void appendDateTimeLiteral(SqlAppender appender, Date date, TemporalType break; case TIMESTAMP: appender.appendSql( "timestamp with time zone '" ); - appendAsTimestampWithMicros( appender,date, jdbcTimeZone ); + appendAsTimestampWithMicrosAndEraSuffix( appender,date, jdbcTimeZone ); appender.appendSql( '\'' ); break; default: @@ -805,7 +805,7 @@ public void appendDateTimeLiteral( switch ( precision ) { case DATE: appender.appendSql( "date '" ); - appendAsDate( appender, calendar ); + appendAsDateWithEraSuffix( appender, calendar ); appender.appendSql( '\'' ); break; case TIME: @@ -815,7 +815,7 @@ public void appendDateTimeLiteral( break; case TIMESTAMP: appender.appendSql( "timestamp with time zone '" ); - appendAsTimestampWithMillis( appender, calendar, jdbcTimeZone ); + DateTimeUtils.appendAsTimestampWithMillisAndEraSuffix( appender, calendar, jdbcTimeZone ); appender.appendSql( '\'' ); break; default: diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java index 3ff01b0ed31d..da3f0c4b2f0b 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java @@ -78,6 +78,8 @@ import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorNoOpImpl; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.type.descriptor.jdbc.EnumJdbcType; +import org.hibernate.type.descriptor.jdbc.GregorianEpochBasedDateJdbcType; +import org.hibernate.type.descriptor.jdbc.GregorianEpochBasedTimestampJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.OrdinalEnumJdbcType; import org.hibernate.type.descriptor.jdbc.TimeAsTimestampWithTimeZoneJdbcType; @@ -294,6 +296,8 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry jdbcTypeRegistry.addDescriptor( TimeUtcAsOffsetTimeJdbcType.INSTANCE ); } jdbcTypeRegistry.addDescriptor( TIMESTAMP_UTC, TimestampUtcAsInstantJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( GregorianEpochBasedDateJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( GregorianEpochBasedTimestampJdbcType.INSTANCE ); if ( getVersion().isSameOrAfter( 1, 4, 197 ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( UUIDJdbcType.INSTANCE ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java index 36bc428ee2d8..3b17a277b862 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java @@ -11,6 +11,7 @@ import org.hibernate.Locking; import org.hibernate.StaleObjectStateException; import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.boot.model.TypeContributions; import org.hibernate.community.dialect.pagination.LegacyHSQLLimitHandler; import org.hibernate.dialect.BooleanDecoder; import org.hibernate.dialect.DatabaseVersion; @@ -67,6 +68,7 @@ import org.hibernate.query.sqm.mutation.spi.BeforeUseAction; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; +import org.hibernate.service.ServiceRegistry; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; @@ -76,6 +78,10 @@ import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorHSQLDBDatabaseImpl; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; +import org.hibernate.type.descriptor.DateTimeUtils; +import org.hibernate.type.descriptor.jdbc.GregorianEpochBasedDateJdbcType; +import org.hibernate.type.descriptor.jdbc.GregorianEpochBasedTimestampJdbcType; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.spi.TypeConfiguration; import org.jboss.logging.Logger; @@ -83,6 +89,10 @@ import java.sql.DatabaseMetaData; import java.sql.SQLException; import java.sql.Types; +import java.time.temporal.TemporalAccessor; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate; import static org.hibernate.type.SqlTypes.BLOB; @@ -90,6 +100,9 @@ import static org.hibernate.type.SqlTypes.DOUBLE; import static org.hibernate.type.SqlTypes.NCLOB; import static org.hibernate.type.SqlTypes.NUMERIC; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDateWithEraPrefix; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillisAndEraPrefix; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithNanosAndEraPrefix; /** * A {@linkplain Dialect SQL dialect} for HSQLDB (HyperSQL) 1.8 up to (but not including) 2.6.1. @@ -294,6 +307,85 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio ) ); } + @Override + public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.contributeTypes( typeContributions, serviceRegistry ); + final JdbcTypeRegistry jdbcTypeRegistry = + typeContributions.getTypeConfiguration().getJdbcTypeRegistry(); + + jdbcTypeRegistry.addDescriptor( GregorianEpochBasedDateJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( GregorianEpochBasedTimestampJdbcType.INSTANCE ); + } + + @Override + public void appendDateTimeLiteral(SqlAppender appender, TemporalAccessor temporalAccessor, TemporalType precision, TimeZone jdbcTimeZone) { + if ( precision != TemporalType.TIME && DateTimeUtils.isBcEra( temporalAccessor ) ) { + switch ( precision ) { + case DATE: + appender.appendSql( "to_date('" ); + appendAsDateWithEraPrefix( appender, temporalAccessor ); + appender.appendSql( "','BC YYYY-MM-DD')" ); + break; + case TIMESTAMP: + appender.appendSql( "to_timestamp('" ); + appendAsTimestampWithNanosAndEraPrefix( appender, temporalAccessor, supportsTemporalLiteralOffset(), jdbcTimeZone ); + appender.appendSql( "','BC YYYY-MM-DD HH:MI:SS.FF')" ); + break; + default: + throw new IllegalArgumentException(); + } + } + else { + super.appendDateTimeLiteral( appender, temporalAccessor, precision, jdbcTimeZone ); + } + } + + @Override + public void appendDateTimeLiteral(SqlAppender appender, Date date, TemporalType precision, TimeZone jdbcTimeZone) { + if ( precision != TemporalType.TIME && DateTimeUtils.isBcEra( date ) ) { + switch ( precision ) { + case DATE: + appender.appendSql( "to_date('" ); + appendAsDateWithEraPrefix( appender, date ); + appender.appendSql( "','BC YYYY-MM-DD')" ); + break; + case TIMESTAMP: + appender.appendSql( "to_timestamp('" ); + appendAsTimestampWithNanosAndEraPrefix( appender, date, jdbcTimeZone ); + appender.appendSql( "','BC YYYY-MM-DD HH:MI:SS.FF')" ); + break; + default: + throw new IllegalArgumentException(); + } + } + else { + super.appendDateTimeLiteral( appender, date, precision, jdbcTimeZone ); + } + } + + @Override + public void appendDateTimeLiteral(SqlAppender appender, Calendar calendar, TemporalType precision, TimeZone jdbcTimeZone) { + if ( precision != TemporalType.TIME && DateTimeUtils.isBcEra( calendar ) ) { + switch ( precision ) { + case DATE: + appender.appendSql( "to_date('" ); + appendAsDateWithEraPrefix( appender, calendar ); + appender.appendSql( "','BC YYYY-MM-DD')" ); + break; + case TIMESTAMP: + appender.appendSql( "to_timestamp('" ); + appendAsTimestampWithMillisAndEraPrefix( appender, calendar, jdbcTimeZone ); + appender.appendSql( "','BC YYYY-MM-DD HH:MI:SS.FF')" ); + break; + default: + throw new IllegalArgumentException(); + } + } + else { + super.appendDateTimeLiteral( appender, calendar, precision, jdbcTimeZone ); + } + } + /** * HSQLDB doesn't support the {@code generate_series} function or {@code lateral} recursive CTEs, * so it has to be emulated with a top level recursive CTE which requires an upper bound on the amount diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index 91c59b8af375..99fe5589f7d5 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java @@ -9,8 +9,9 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; -import java.time.temporal.ChronoField; import java.time.temporal.TemporalAccessor; +import java.util.Calendar; +import java.util.Date; import java.util.List; import java.util.Locale; import java.util.TimeZone; @@ -158,7 +159,11 @@ import static org.hibernate.type.SqlTypes.TINYINT; import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithNanos; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDateWithoutYear0; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTime; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillisWithoutYear0; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithNanosWithoutYear0; /** * A {@linkplain Dialect SQL dialect} for Oracle 8i and above. @@ -1469,27 +1474,80 @@ public String getReadLockString(String aliases, int timeout) { @Override public boolean supportsTemporalLiteralOffset() { - // Oracle *does* support offsets, but only - // in the ANSI syntax, not in the JDBC - // escape-based syntax, which we use in - // almost all circumstances (see below) - return false; + return true; } @Override - public void appendDateTimeLiteral(SqlAppender appender, TemporalAccessor temporalAccessor, TemporalType precision, TimeZone jdbcTimeZone) { - // we usually use the JDBC escape-based syntax - // because we want to let the JDBC driver handle - // TIME (a concept which does not exist in Oracle) - // but for the special case of timestamps with an - // offset we need to use the ANSI syntax - if ( precision == TemporalType.TIMESTAMP && temporalAccessor.isSupported( ChronoField.OFFSET_SECONDS ) ) { - appender.appendSql( "timestamp '" ); - appendAsTimestampWithNanos( appender, temporalAccessor, true, jdbcTimeZone, false ); - appender.appendSql( '\'' ); + public void appendDateTimeLiteral( + SqlAppender appender, + TemporalAccessor temporalAccessor, + @SuppressWarnings("deprecation") + TemporalType precision, + TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "date '" ); + appendAsDateWithoutYear0( appender, temporalAccessor ); + appender.appendSql( '\'' ); + break; + case TIME: + appender.appendSql( "time '" ); + appendAsTime( appender, temporalAccessor, supportsTemporalLiteralOffset(), jdbcTimeZone ); + appender.appendSql( '\'' ); + break; + case TIMESTAMP: + appender.appendSql( "timestamp '" ); + appendAsTimestampWithNanosWithoutYear0( appender, temporalAccessor, supportsTemporalLiteralOffset(), jdbcTimeZone, false ); + appender.appendSql( '\'' ); + break; + default: + throw new IllegalArgumentException(); } - else { - super.appendDateTimeLiteral( appender, temporalAccessor, precision, jdbcTimeZone ); + } + + @Override + public void appendDateTimeLiteral(SqlAppender appender, Date date, TemporalType precision, TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "date '" ); + appendAsDateWithoutYear0( appender, date ); + appender.appendSql( '\'' ); + break; + case TIME: + appender.appendSql( "time '" ); + appendAsLocalTime( appender, date ); + appender.appendSql( '\'' ); + break; + case TIMESTAMP: + appender.appendSql( "timestamp '" ); + appendAsTimestampWithNanosWithoutYear0( appender, date, jdbcTimeZone ); + appender.appendSql( '\'' ); + break; + default: + throw new IllegalArgumentException(); + } + } + + @Override + public void appendDateTimeLiteral(SqlAppender appender, Calendar calendar, TemporalType precision, TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "date '" ); + appendAsDateWithoutYear0( appender, calendar ); + appender.appendSql( '\'' ); + break; + case TIME: + appender.appendSql( "time '" ); + appendAsLocalTime( appender, calendar ); + appender.appendSql( '\'' ); + break; + case TIMESTAMP: + appender.appendSql( "timestamp '" ); + appendAsTimestampWithMillisWithoutYear0( appender, calendar, jdbcTimeZone ); + appender.appendSql( '\'' ); + break; + default: + throw new IllegalArgumentException(); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java index 91debe50d7dd..9efc1f84b45a 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java @@ -98,6 +98,7 @@ import org.hibernate.tool.schema.internal.StandardTableExporter; import org.hibernate.tool.schema.spi.Exporter; import org.hibernate.type.JavaObjectType; +import org.hibernate.type.descriptor.DateTimeUtils; import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; import org.hibernate.type.descriptor.jdbc.BlobJdbcType; import org.hibernate.type.descriptor.jdbc.ClobJdbcType; @@ -158,11 +159,10 @@ import static org.hibernate.type.SqlTypes.UUID; import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDateWithEraSuffix; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTime; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMicros; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillis; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMicrosAndEraSuffix; /** * A {@linkplain Dialect SQL dialect} for PostgreSQL 8 and above. @@ -1253,7 +1253,7 @@ public void appendDateTimeLiteral( switch ( precision ) { case DATE: appender.appendSql( "date '" ); - appendAsDate( appender, temporalAccessor ); + appendAsDateWithEraSuffix( appender, temporalAccessor ); appender.appendSql( '\'' ); break; case TIME: @@ -1270,12 +1270,12 @@ public void appendDateTimeLiteral( case TIMESTAMP: if ( supportsTemporalLiteralOffset() && temporalAccessor.isSupported( ChronoField.OFFSET_SECONDS ) ) { appender.appendSql( "timestamp with time zone '" ); - appendAsTimestampWithMicros( appender, temporalAccessor, true, jdbcTimeZone ); + appendAsTimestampWithMicrosAndEraSuffix( appender, temporalAccessor, true, jdbcTimeZone ); appender.appendSql( '\'' ); } else { appender.appendSql( "timestamp '" ); - appendAsTimestampWithMicros( appender, temporalAccessor, false, jdbcTimeZone ); + appendAsTimestampWithMicrosAndEraSuffix( appender, temporalAccessor, false, jdbcTimeZone ); appender.appendSql( '\'' ); } break; @@ -1289,7 +1289,7 @@ public void appendDateTimeLiteral(SqlAppender appender, Date date, TemporalType switch ( precision ) { case DATE: appender.appendSql( "date '" ); - appendAsDate( appender, date ); + appendAsDateWithEraSuffix( appender, date ); appender.appendSql( '\'' ); break; case TIME: @@ -1299,7 +1299,7 @@ public void appendDateTimeLiteral(SqlAppender appender, Date date, TemporalType break; case TIMESTAMP: appender.appendSql( "timestamp with time zone '" ); - appendAsTimestampWithMicros( appender, date, jdbcTimeZone ); + appendAsTimestampWithMicrosAndEraSuffix( appender, date, jdbcTimeZone ); appender.appendSql( '\'' ); break; default: @@ -1316,7 +1316,7 @@ public void appendDateTimeLiteral( switch ( precision ) { case DATE: appender.appendSql( "date '" ); - appendAsDate( appender, calendar ); + appendAsDateWithEraSuffix( appender, calendar ); appender.appendSql( '\'' ); break; case TIME: @@ -1326,7 +1326,7 @@ public void appendDateTimeLiteral( break; case TIMESTAMP: appender.appendSql( "timestamp with time zone '" ); - appendAsTimestampWithMillis( appender, calendar, jdbcTimeZone ); + DateTimeUtils.appendAsTimestampWithMillisAndEraSuffix( appender, calendar, jdbcTimeZone ); appender.appendSql( '\'' ); break; default: diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java index 440cc014f3c6..8ce9b309dec6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -64,6 +64,7 @@ import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; import org.hibernate.type.JavaObjectType; +import org.hibernate.type.descriptor.DateTimeUtils; import org.hibernate.type.descriptor.jdbc.BlobJdbcType; import org.hibernate.type.descriptor.jdbc.ClobJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; @@ -124,11 +125,10 @@ import static org.hibernate.type.SqlTypes.UUID; import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDateWithEraSuffix; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTime; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMicros; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillis; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMicrosAndEraSuffix; /** * A {@linkplain Dialect SQL dialect} for CockroachDB 23.1 and above. @@ -709,7 +709,7 @@ public void appendDateTimeLiteral( switch ( precision ) { case DATE: appender.appendSql( "date '" ); - appendAsDate( appender, temporalAccessor ); + appendAsDateWithEraSuffix( appender, temporalAccessor ); appender.appendSql( '\'' ); break; case TIME: @@ -726,12 +726,12 @@ public void appendDateTimeLiteral( case TIMESTAMP: if ( supportsTemporalLiteralOffset() && temporalAccessor.isSupported( ChronoField.OFFSET_SECONDS ) ) { appender.appendSql( "timestamp with time zone '" ); - appendAsTimestampWithMicros( appender, temporalAccessor, true, jdbcTimeZone ); + appendAsTimestampWithMicrosAndEraSuffix( appender, temporalAccessor, true, jdbcTimeZone ); appender.appendSql( '\'' ); } else { appender.appendSql( "timestamp '" ); - appendAsTimestampWithMicros( appender, temporalAccessor, false, jdbcTimeZone ); + appendAsTimestampWithMicrosAndEraSuffix( appender, temporalAccessor, false, jdbcTimeZone ); appender.appendSql( '\'' ); } break; @@ -750,7 +750,7 @@ public void appendDateTimeLiteral( switch ( precision ) { case DATE: appender.appendSql( "date '" ); - appendAsDate( appender, date ); + appendAsDateWithEraSuffix( appender, date ); appender.appendSql( '\'' ); break; case TIME: @@ -760,7 +760,7 @@ public void appendDateTimeLiteral( break; case TIMESTAMP: appender.appendSql( "timestamp with time zone '" ); - appendAsTimestampWithMicros( appender,date, jdbcTimeZone ); + appendAsTimestampWithMicrosAndEraSuffix( appender,date, jdbcTimeZone ); appender.appendSql( '\'' ); break; default: @@ -778,7 +778,7 @@ public void appendDateTimeLiteral( switch ( precision ) { case DATE: appender.appendSql( "date '" ); - appendAsDate( appender, calendar ); + appendAsDateWithEraSuffix( appender, calendar ); appender.appendSql( '\'' ); break; case TIME: @@ -788,7 +788,7 @@ public void appendDateTimeLiteral( break; case TIMESTAMP: appender.appendSql( "timestamp with time zone '" ); - appendAsTimestampWithMillis( appender, calendar, jdbcTimeZone ); + DateTimeUtils.appendAsTimestampWithMillisAndEraSuffix( appender, calendar, jdbcTimeZone ); appender.appendSql( '\'' ); break; default: diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index 05da7444be3f..4b3fdddd34b6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -67,6 +67,8 @@ import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorLegacyImpl; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.type.descriptor.jdbc.EnumJdbcType; +import org.hibernate.type.descriptor.jdbc.GregorianEpochBasedDateJdbcType; +import org.hibernate.type.descriptor.jdbc.GregorianEpochBasedTimestampJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.OrdinalEnumJdbcType; import org.hibernate.type.descriptor.jdbc.TimeUtcAsOffsetTimeJdbcType; @@ -253,6 +255,8 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry .getJdbcTypeRegistry(); jdbcTypeRegistry.addDescriptor( TimeUtcAsOffsetTimeJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptor( TimestampUtcAsInstantJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( GregorianEpochBasedDateJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( GregorianEpochBasedTimestampJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( UUIDJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( H2DurationIntervalSecondJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( H2JsonJdbcType.INSTANCE ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java index b30dcdc651c9..f2b05ed06951 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java @@ -8,6 +8,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.LockOptions; import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.boot.model.TypeContributions; import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.function.TrimFunction; import org.hibernate.dialect.identity.HSQLIdentityColumnSupport; @@ -47,6 +48,7 @@ import org.hibernate.query.sqm.mutation.spi.BeforeUseAction; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; +import org.hibernate.service.ServiceRegistry; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; @@ -60,17 +62,28 @@ import org.hibernate.sql.model.internal.OptionalTableUpdate; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorHSQLDBDatabaseImpl; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; +import org.hibernate.type.descriptor.DateTimeUtils; +import org.hibernate.type.descriptor.jdbc.GregorianEpochBasedDateJdbcType; +import org.hibernate.type.descriptor.jdbc.GregorianEpochBasedTimestampJdbcType; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.spi.TypeConfiguration; import java.sql.DatabaseMetaData; import java.sql.SQLException; import java.sql.Types; +import java.time.temporal.TemporalAccessor; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate; import static org.hibernate.internal.util.JdbcExceptionHelper.extractErrorCode; import static org.hibernate.sql.ast.internal.NonLockingClauseStrategy.NON_CLAUSE_STRATEGY; import static org.hibernate.type.SqlTypes.DOUBLE; import static org.hibernate.type.SqlTypes.NCLOB; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDateWithEraPrefix; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillisAndEraPrefix; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithNanosAndEraPrefix; /** * A {@linkplain Dialect SQL dialect} for HSQLDB (HyperSQL) 2.6.1 and above. @@ -241,6 +254,85 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.hex( "hex(?1)" ); } + @Override + public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.contributeTypes( typeContributions, serviceRegistry ); + final JdbcTypeRegistry jdbcTypeRegistry = + typeContributions.getTypeConfiguration().getJdbcTypeRegistry(); + + jdbcTypeRegistry.addDescriptor( GregorianEpochBasedDateJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( GregorianEpochBasedTimestampJdbcType.INSTANCE ); + } + + @Override + public void appendDateTimeLiteral(SqlAppender appender, TemporalAccessor temporalAccessor, TemporalType precision, TimeZone jdbcTimeZone) { + if ( precision != TemporalType.TIME && DateTimeUtils.isBcEra( temporalAccessor ) ) { + switch ( precision ) { + case DATE: + appender.appendSql( "to_date('" ); + appendAsDateWithEraPrefix( appender, temporalAccessor ); + appender.appendSql( "','BC YYYY-MM-DD')" ); + break; + case TIMESTAMP: + appender.appendSql( "to_timestamp('" ); + appendAsTimestampWithNanosAndEraPrefix( appender, temporalAccessor, supportsTemporalLiteralOffset(), jdbcTimeZone ); + appender.appendSql( "','BC YYYY-MM-DD HH:MI:SS.FF')" ); + break; + default: + throw new IllegalArgumentException(); + } + } + else { + super.appendDateTimeLiteral( appender, temporalAccessor, precision, jdbcTimeZone ); + } + } + + @Override + public void appendDateTimeLiteral(SqlAppender appender, Date date, TemporalType precision, TimeZone jdbcTimeZone) { + if ( precision != TemporalType.TIME && DateTimeUtils.isBcEra( date ) ) { + switch ( precision ) { + case DATE: + appender.appendSql( "to_date('" ); + appendAsDateWithEraPrefix( appender, date ); + appender.appendSql( "','BC YYYY-MM-DD')" ); + break; + case TIMESTAMP: + appender.appendSql( "to_timestamp('" ); + appendAsTimestampWithNanosAndEraPrefix( appender, date, jdbcTimeZone ); + appender.appendSql( "','BC YYYY-MM-DD HH:MI:SS.FF')" ); + break; + default: + throw new IllegalArgumentException(); + } + } + else { + super.appendDateTimeLiteral( appender, date, precision, jdbcTimeZone ); + } + } + + @Override + public void appendDateTimeLiteral(SqlAppender appender, Calendar calendar, TemporalType precision, TimeZone jdbcTimeZone) { + if ( precision != TemporalType.TIME && DateTimeUtils.isBcEra( calendar ) ) { + switch ( precision ) { + case DATE: + appender.appendSql( "to_date('" ); + appendAsDateWithEraPrefix( appender, calendar ); + appender.appendSql( "','BC YYYY-MM-DD')" ); + break; + case TIMESTAMP: + appender.appendSql( "to_timestamp('" ); + appendAsTimestampWithMillisAndEraPrefix( appender, calendar, jdbcTimeZone ); + appender.appendSql( "','BC YYYY-MM-DD HH:MI:SS.FF')" ); + break; + default: + throw new IllegalArgumentException(); + } + } + else { + super.appendDateTimeLiteral( appender, calendar, precision, jdbcTimeZone ); + } + } + /** * HSQLDB doesn't support the {@code generate_series} function or {@code lateral} recursive CTEs, * so it has to be emulated with a top level recursive CTE which requires an upper bound on the amount diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index e9e75adcc0ef..da38f66068e7 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -116,8 +116,9 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; -import java.time.temporal.ChronoField; import java.time.temporal.TemporalAccessor; +import java.util.Calendar; +import java.util.Date; import java.util.List; import java.util.TimeZone; import java.util.regex.Matcher; @@ -165,7 +166,13 @@ import static org.hibernate.type.SqlTypes.TINYINT; import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithNanos; +import static org.hibernate.type.descriptor.DateTimeUtils.JDBC_ESCAPE_END; +import static org.hibernate.type.descriptor.DateTimeUtils.JDBC_ESCAPE_START_TIME; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDateWithoutYear0; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTime; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillisWithoutYear0; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithNanosWithoutYear0; /** * A {@linkplain Dialect SQL dialect} for Oracle 19c and above. @@ -1548,11 +1555,7 @@ public String getReadLockString(String aliases, int timeout) { @Override public boolean supportsTemporalLiteralOffset() { - // Oracle *does* support offsets, but only - // in the ANSI syntax, not in the JDBC - // escape-based syntax, which we use in - // almost all circumstances (see below) - return false; + return true; } @Override @@ -1562,19 +1565,70 @@ public void appendDateTimeLiteral( @SuppressWarnings("deprecation") TemporalType precision, TimeZone jdbcTimeZone) { - // we usually use the JDBC escape-based syntax - // because we want to let the JDBC driver handle - // TIME (a concept which does not exist in Oracle) - // but for the special case of timestamps with an - // offset we need to use the ANSI syntax - if ( precision == TemporalType.TIMESTAMP - && temporalAccessor.isSupported( ChronoField.OFFSET_SECONDS ) ) { - appender.appendSql( "timestamp '" ); - appendAsTimestampWithNanos( appender, temporalAccessor, true, jdbcTimeZone, false ); - appender.appendSql( '\'' ); + switch ( precision ) { + case DATE: + appender.appendSql( "date '" ); + appendAsDateWithoutYear0( appender, temporalAccessor ); + appender.appendSql( '\'' ); + break; + case TIME: + appender.appendSql( "time '" ); + appendAsTime( appender, temporalAccessor, supportsTemporalLiteralOffset(), jdbcTimeZone ); + appender.appendSql( '\'' ); + break; + case TIMESTAMP: + appender.appendSql( "timestamp '" ); + appendAsTimestampWithNanosWithoutYear0( appender, temporalAccessor, supportsTemporalLiteralOffset(), jdbcTimeZone, false ); + appender.appendSql( '\'' ); + break; + default: + throw new IllegalArgumentException(); } - else { - super.appendDateTimeLiteral( appender, temporalAccessor, precision, jdbcTimeZone ); + } + + @Override + public void appendDateTimeLiteral(SqlAppender appender, Date date, TemporalType precision, TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "date '" ); + appendAsDateWithoutYear0( appender, date ); + appender.appendSql( '\'' ); + break; + case TIME: + appender.appendSql( "time '" ); + appendAsLocalTime( appender, date ); + appender.appendSql( '\'' ); + break; + case TIMESTAMP: + appender.appendSql( "timestamp '" ); + appendAsTimestampWithNanosWithoutYear0( appender, date, jdbcTimeZone ); + appender.appendSql( '\'' ); + break; + default: + throw new IllegalArgumentException(); + } + } + + @Override + public void appendDateTimeLiteral(SqlAppender appender, Calendar calendar, TemporalType precision, TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "date '" ); + appendAsDateWithoutYear0( appender, calendar ); + appender.appendSql( '\'' ); + break; + case TIME: + appender.appendSql( JDBC_ESCAPE_START_TIME ); + appendAsLocalTime( appender, calendar ); + appender.appendSql( JDBC_ESCAPE_END ); + break; + case TIMESTAMP: + appender.appendSql( "timestamp '" ); + appendAsTimestampWithMillisWithoutYear0( appender, calendar, jdbcTimeZone ); + appender.appendSql( '\'' ); + break; + default: + throw new IllegalArgumentException(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index 54fd00b75940..33f3139b51fc 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -151,11 +151,11 @@ import static org.hibernate.type.SqlTypes.UUID; import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDateWithEraSuffix; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTime; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMicros; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillis; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMicrosAndEraSuffix; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillisAndEraSuffix; /** * A {@linkplain Dialect SQL dialect} for PostgreSQL 13 and above. @@ -1212,7 +1212,7 @@ public void appendDateTimeLiteral( switch ( precision ) { case DATE: appender.appendSql( "date '" ); - appendAsDate( appender, temporalAccessor ); + appendAsDateWithEraSuffix( appender, temporalAccessor ); appender.appendSql( '\'' ); break; case TIME: @@ -1229,12 +1229,12 @@ public void appendDateTimeLiteral( case TIMESTAMP: if ( supportsTemporalLiteralOffset() && temporalAccessor.isSupported( ChronoField.OFFSET_SECONDS ) ) { appender.appendSql( "timestamp with time zone '" ); - appendAsTimestampWithMicros( appender, temporalAccessor, true, jdbcTimeZone ); + appendAsTimestampWithMicrosAndEraSuffix( appender, temporalAccessor, true, jdbcTimeZone ); appender.appendSql( '\'' ); } else { appender.appendSql( "timestamp '" ); - appendAsTimestampWithMicros( appender, temporalAccessor, false, jdbcTimeZone ); + appendAsTimestampWithMicrosAndEraSuffix( appender, temporalAccessor, false, jdbcTimeZone ); appender.appendSql( '\'' ); } break; @@ -1253,7 +1253,7 @@ public void appendDateTimeLiteral( switch ( precision ) { case DATE: appender.appendSql( "date '" ); - appendAsDate( appender, date ); + appendAsDateWithEraSuffix( appender, date ); appender.appendSql( '\'' ); break; case TIME: @@ -1263,7 +1263,7 @@ public void appendDateTimeLiteral( break; case TIMESTAMP: appender.appendSql( "timestamp with time zone '" ); - appendAsTimestampWithMicros( appender, date, jdbcTimeZone ); + appendAsTimestampWithMicrosAndEraSuffix( appender, date, jdbcTimeZone ); appender.appendSql( '\'' ); break; default: @@ -1281,7 +1281,7 @@ public void appendDateTimeLiteral( switch ( precision ) { case DATE: appender.appendSql( "date '" ); - appendAsDate( appender, calendar ); + appendAsDateWithEraSuffix( appender, calendar ); appender.appendSql( '\'' ); break; case TIME: @@ -1291,7 +1291,7 @@ public void appendDateTimeLiteral( break; case TIMESTAMP: appender.appendSql( "timestamp with time zone '" ); - appendAsTimestampWithMillis( appender, calendar, jdbcTimeZone ); + appendAsTimestampWithMillisAndEraSuffix( appender, calendar, jdbcTimeZone ); appender.appendSql( '\'' ); break; default: diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/type/AbstractPostgreSQLStructJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/type/AbstractPostgreSQLStructJdbcType.java index 5a28ee9ef16d..71ccdade0331 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/type/AbstractPostgreSQLStructJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/type/AbstractPostgreSQLStructJdbcType.java @@ -43,13 +43,13 @@ import org.hibernate.type.descriptor.jdbc.StructuredJdbcType; import org.hibernate.type.spi.TypeConfiguration; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDateWithEraSuffix; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMicrosAndEraSuffix; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillisAndEraSuffix; import static org.hibernate.type.descriptor.jdbc.StructHelper.getSubPart; import static org.hibernate.type.descriptor.jdbc.StructHelper.instantiate; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTime; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMicros; -import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillis; /** * Implementation for serializing/deserializing an embeddable aggregate to/from the PostgreSQL component format. @@ -1396,16 +1396,16 @@ private void appendTemporal(SqlAppender appender, JdbcMapping jdbcMapping, Objec switch ( jdbcMapping.getJdbcType().getJdbcTypeCode() ) { case SqlTypes.DATE: if ( value instanceof java.util.Date date ) { - appendAsDate( appender, date ); + appendAsDateWithEraSuffix( appender, date ); } else if ( value instanceof java.util.Calendar calendar ) { - appendAsDate( appender, calendar ); + appendAsDateWithEraSuffix( appender, calendar ); } else if ( value instanceof TemporalAccessor temporalAccessor ) { - appendAsDate( appender, temporalAccessor ); + appendAsDateWithEraSuffix( appender, temporalAccessor ); } else { - appendAsDate( + appendAsDateWithEraSuffix( appender, javaType.unwrap( value, java.util.Date.class, options ) ); @@ -1440,16 +1440,16 @@ else if ( value instanceof TemporalAccessor temporalAccessor ) { case SqlTypes.TIMESTAMP_WITH_TIMEZONE: case SqlTypes.TIMESTAMP_UTC: if ( value instanceof java.util.Date date ) { - appendAsTimestampWithMicros( appender, date, jdbcTimeZone ); + appendAsTimestampWithMicrosAndEraSuffix( appender, date, jdbcTimeZone ); } else if ( value instanceof java.util.Calendar calendar ) { - appendAsTimestampWithMillis( appender, calendar, jdbcTimeZone ); + appendAsTimestampWithMillisAndEraSuffix( appender, calendar, jdbcTimeZone ); } else if ( value instanceof TemporalAccessor temporalAccessor ) { - appendAsTimestampWithMicros( appender, temporalAccessor, temporalAccessor.isSupported( ChronoField.OFFSET_SECONDS ), jdbcTimeZone ); + appendAsTimestampWithMicrosAndEraSuffix( appender, temporalAccessor, temporalAccessor.isSupported( ChronoField.OFFSET_SECONDS ), jdbcTimeZone ); } else { - appendAsTimestampWithMicros( + appendAsTimestampWithMicrosAndEraSuffix( appender, javaType.unwrap( value, java.util.Date.class, options ), jdbcTimeZone diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java index 854107d85be9..e2ffde82bfae 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java @@ -7,8 +7,12 @@ import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.time.Instant; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.chrono.IsoEra; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; @@ -23,6 +27,7 @@ import org.hibernate.Internal; import org.hibernate.dialect.Dialect; import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.spi.StringBuilderSqlAppender; import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME; @@ -39,51 +44,121 @@ private DateTimeUtils() { } public static final String FORMAT_STRING_DATE = "yyyy-MM-dd"; + public static final String FORMAT_STRING_DATE_WITH_ERA = "uuuu-MM-dd"; + public static final String FORMAT_STRING_DATE_WITH_ERA_SUFFIX = "yyyy-MM-dd G"; + public static final String FORMAT_STRING_DATE_WITH_ERA_PREFIX = "G yyyy-MM-dd"; public static final String FORMAT_STRING_TIME_WITH_OFFSET = "HH:mm:ssXXX"; public static final String FORMAT_STRING_TIME = "HH:mm:ss"; public static final String FORMAT_STRING_TIMESTAMP = "yyyy-MM-dd HH:mm:ss"; + private static final String FORMAT_STRING_TIMESTAMP_WITH_ERA = "uuuu-MM-dd HH:mm:ss"; public static final String FORMAT_STRING_TIMESTAMP_WITH_MILLIS = FORMAT_STRING_TIMESTAMP + ".SSS"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_MILLIS_AND_ERA = FORMAT_STRING_TIMESTAMP_WITH_ERA + ".SSS"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_MILLIS_AND_ERA_SUFFIX = FORMAT_STRING_TIMESTAMP_WITH_MILLIS + " G"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_MILLIS_AND_ERA_PREFIX = "G " + FORMAT_STRING_TIMESTAMP_WITH_MILLIS; public static final String FORMAT_STRING_TIMESTAMP_WITH_MICROS = FORMAT_STRING_TIMESTAMP + ".SSSSSS"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_ERA = FORMAT_STRING_TIMESTAMP_WITH_ERA + ".SSSSSS"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_ERA_SUFFIX = FORMAT_STRING_TIMESTAMP_WITH_MICROS + " G"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_MICROS_PADDED = FORMAT_STRING_TIMESTAMP + ".SSS000"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_ERA_SUFFIX_PADDED = FORMAT_STRING_TIMESTAMP_WITH_MICROS_PADDED + " G"; public static final String FORMAT_STRING_TIMESTAMP_WITH_NANOS = FORMAT_STRING_TIMESTAMP + ".SSSSSSSSS"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_ERA = FORMAT_STRING_TIMESTAMP_WITH_ERA + ".SSSSSSSSS"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_ERA_PREFIX = "G " + FORMAT_STRING_TIMESTAMP_WITH_NANOS; + public static final String FORMAT_STRING_TIMESTAMP_WITH_NANOS_PADDED = FORMAT_STRING_TIMESTAMP + ".SSS000000"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_ERA_PREFIX_PADDED = "G " + FORMAT_STRING_TIMESTAMP_WITH_NANOS_PADDED; + public static final String FORMAT_STRING_TIMESTAMP_WITH_NANOS_IN_BC = "-" + FORMAT_STRING_TIMESTAMP_WITH_NANOS; public static final String FORMAT_STRING_TIMESTAMP_WITH_MILLIS_AND_OFFSET = FORMAT_STRING_TIMESTAMP_WITH_MILLIS + "XXX"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_MILLIS_AND_OFFSET_AND_ERA = FORMAT_STRING_TIMESTAMP_WITH_MILLIS_AND_ERA + "XXX"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_MILLIS_AND_OFFSET_AND_ERA_SUFFIX = FORMAT_STRING_TIMESTAMP_WITH_MILLIS_AND_OFFSET + " G"; public static final String FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_OFFSET = FORMAT_STRING_TIMESTAMP_WITH_MICROS + "XXX"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_OFFSET_AND_ERA = FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_ERA + "XXX"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_OFFSET_AND_ERA_SUFFIX = FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_OFFSET + " G"; public static final String FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_OFFSET = FORMAT_STRING_TIMESTAMP_WITH_NANOS + "XXX"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_OFFSET_AND_ERA = FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_ERA + "XXX"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_OFFSET_AND_ERA_PREFIX = "G " + FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_OFFSET; + public static final String FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_OFFSET_IN_BC = "-" + FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_OFFSET; public static final String FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_OFFSET_NOZ = FORMAT_STRING_TIMESTAMP_WITH_MICROS + "xxx"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_OFFSET_NOZ_AND_ERA = FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_ERA + "xxx"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_OFFSET_NOZ_AND_ERA_SUFFIX = FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_OFFSET_NOZ + " G"; public static final String FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_OFFSET_NOZ = FORMAT_STRING_TIMESTAMP_WITH_NANOS + "xxx"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_OFFSET_NOZ_AND_ERA = FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_ERA + "xxx"; + public static final String FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_OFFSET_NOZ_IN_BC = "-" + FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_OFFSET_NOZ; public static final DateTimeFormatter DATE_TIME_FORMATTER_DATE = DateTimeFormatter.ofPattern( FORMAT_STRING_DATE, Locale.ENGLISH ); + public static final DateTimeFormatter DATE_TIME_FORMATTER_DATE_WITH_ERA = DateTimeFormatter.ofPattern( FORMAT_STRING_DATE_WITH_ERA, Locale.ENGLISH ); + public static final DateTimeFormatter DATE_TIME_FORMATTER_DATE_WITH_ERA_SUFFIX = DateTimeFormatter.ofPattern( FORMAT_STRING_DATE_WITH_ERA_SUFFIX, Locale.ENGLISH ); + public static final DateTimeFormatter DATE_TIME_FORMATTER_DATE_WITH_ERA_PREFIX = DateTimeFormatter.ofPattern( FORMAT_STRING_DATE_WITH_ERA_PREFIX, Locale.ENGLISH ); public static final DateTimeFormatter DATE_TIME_FORMATTER_TIME_WITH_OFFSET = DateTimeFormatter.ofPattern( FORMAT_STRING_TIME_WITH_OFFSET, Locale.ENGLISH ); public static final DateTimeFormatter DATE_TIME_FORMATTER_TIME = DateTimeFormatter.ofPattern( FORMAT_STRING_TIME, Locale.ENGLISH ); public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_MILLIS = DateTimeFormatter.ofPattern( - FORMAT_STRING_TIMESTAMP_WITH_MILLIS, + FORMAT_STRING_TIMESTAMP_WITH_MILLIS_AND_ERA, + Locale.ENGLISH + ); + public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_MILLIS_AND_ERA_SUFFIX = DateTimeFormatter.ofPattern( + FORMAT_STRING_TIMESTAMP_WITH_MILLIS_AND_ERA_SUFFIX, Locale.ENGLISH ); public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS = DateTimeFormatter.ofPattern( - FORMAT_STRING_TIMESTAMP_WITH_MICROS, + FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_ERA, + Locale.ENGLISH + ); + public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS_AND_ERA_SUFFIX = DateTimeFormatter.ofPattern( + FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_ERA_SUFFIX, Locale.ENGLISH ); public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS = DateTimeFormatter.ofPattern( - FORMAT_STRING_TIMESTAMP_WITH_NANOS, + FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_ERA, + Locale.ENGLISH + ); + public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_ERA_PREFIX = DateTimeFormatter.ofPattern( + FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_ERA_PREFIX, + Locale.ENGLISH + ); + public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_IN_BC = DateTimeFormatter.ofPattern( + FORMAT_STRING_TIMESTAMP_WITH_NANOS_IN_BC, Locale.ENGLISH ); public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_MILLIS_AND_OFFSET = DateTimeFormatter.ofPattern( - FORMAT_STRING_TIMESTAMP_WITH_MILLIS_AND_OFFSET, + FORMAT_STRING_TIMESTAMP_WITH_MILLIS_AND_OFFSET_AND_ERA, + Locale.ENGLISH + ); + public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_MILLIS_AND_OFFSET_AND_ERA_SUFFIX = DateTimeFormatter.ofPattern( + FORMAT_STRING_TIMESTAMP_WITH_MILLIS_AND_OFFSET_AND_ERA_SUFFIX, Locale.ENGLISH ); public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS_AND_OFFSET = DateTimeFormatter.ofPattern( - FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_OFFSET, + FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_OFFSET_AND_ERA, + Locale.ENGLISH + ); + public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS_AND_OFFSET_AND_ERA_SUFFIX = DateTimeFormatter.ofPattern( + FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_OFFSET_AND_ERA_SUFFIX, Locale.ENGLISH ); public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS_AND_OFFSET_NOZ = DateTimeFormatter.ofPattern( - FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_OFFSET_NOZ, + FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_OFFSET_NOZ_AND_ERA, + Locale.ENGLISH + ); + public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS_AND_OFFSET_NOZ_AND_ERA_SUFFIX = DateTimeFormatter.ofPattern( + FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_OFFSET_NOZ_AND_ERA_SUFFIX, Locale.ENGLISH ); public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_OFFSET = DateTimeFormatter.ofPattern( - FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_OFFSET, + FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_OFFSET_AND_ERA, + Locale.ENGLISH + ); + public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_OFFSET_AND_ERA_PREFIX = DateTimeFormatter.ofPattern( + FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_OFFSET_AND_ERA_PREFIX, + Locale.ENGLISH + ); + public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_OFFSET_IN_BC = DateTimeFormatter.ofPattern( + FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_OFFSET_IN_BC, Locale.ENGLISH ); public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_OFFSET_NOZ = DateTimeFormatter.ofPattern( - FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_OFFSET_NOZ, + FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_OFFSET_NOZ_AND_ERA, + Locale.ENGLISH + ); + public static final DateTimeFormatter DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_OFFSET_NOZ_IN_BC = DateTimeFormatter.ofPattern( + FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_OFFSET_NOZ_IN_BC, Locale.ENGLISH ); @@ -92,6 +167,14 @@ private DateTimeUtils() { public static final String JDBC_ESCAPE_START_TIMESTAMP = "{ts '"; public static final String JDBC_ESCAPE_END = "'}"; + public static final long YEAR_ONE_EPOCH_MILLIS = new java.sql.Timestamp( -1899, 0, 1, 0, 0, 0, 0 ).getTime(); + /** + * The millisecond value since the epoch at which the gregorian Calendar starts. + * Starting with this value, conversions between java.time and java.util.Date is safe, + * but before that, year, month and day based conversion is necessary. + */ + public static final long GREGORIAN_START_EPOCH_MILLIS = new java.sql.Timestamp( 1582-1900, 9, 5, 0, 0, 0, 0 ).getTime(); + /** * Pattern used for parsing literal datetimes in HQL. * @@ -112,12 +195,28 @@ private DateTimeUtils() { private static final ThreadLocal LOCAL_DATE_FORMAT = ThreadLocal.withInitial( () -> new SimpleDateFormat( FORMAT_STRING_DATE, Locale.ENGLISH ) ); + private static final ThreadLocal LOCAL_DATE_WITH_ERA_SUFFIX_FORMAT = + ThreadLocal.withInitial( () -> new SimpleDateFormat( FORMAT_STRING_DATE_WITH_ERA_SUFFIX, Locale.ENGLISH ) ); + private static final ThreadLocal LOCAL_DATE_WITH_ERA_PREFIX_FORMAT = + ThreadLocal.withInitial( () -> new SimpleDateFormat( FORMAT_STRING_DATE_WITH_ERA_PREFIX, Locale.ENGLISH ) ); private static final ThreadLocal LOCAL_TIME_FORMAT = ThreadLocal.withInitial( () -> new SimpleDateFormat( FORMAT_STRING_TIME, Locale.ENGLISH ) ); private static final ThreadLocal TIME_WITH_OFFSET_FORMAT = ThreadLocal.withInitial( () -> new SimpleDateFormat( FORMAT_STRING_TIME_WITH_OFFSET, Locale.ENGLISH ) ); private static final ThreadLocal TIMESTAMP_WITH_MILLIS_FORMAT = ThreadLocal.withInitial( () -> new SimpleDateFormat( FORMAT_STRING_TIMESTAMP_WITH_MILLIS, Locale.ENGLISH ) ); + private static final ThreadLocal TIMESTAMP_WITH_MILLIS_AND_ERA_SUFFIX_FORMAT = + ThreadLocal.withInitial( () -> new SimpleDateFormat( FORMAT_STRING_TIMESTAMP_WITH_MILLIS_AND_ERA_SUFFIX, Locale.ENGLISH ) ); + private static final ThreadLocal TIMESTAMP_WITH_MILLIS_AND_ERA_PREFIX_FORMAT = + ThreadLocal.withInitial( () -> new SimpleDateFormat( FORMAT_STRING_TIMESTAMP_WITH_MILLIS_AND_ERA_PREFIX, Locale.ENGLISH ) ); + private static final ThreadLocal TIMESTAMP_WITH_MICROS_FORMAT = + ThreadLocal.withInitial( () -> new SimpleDateFormat( FORMAT_STRING_TIMESTAMP_WITH_MICROS_PADDED, Locale.ENGLISH ) ); + private static final ThreadLocal TIMESTAMP_WITH_MICROS_AND_ERA_SUFFIX_FORMAT = + ThreadLocal.withInitial( () -> new SimpleDateFormat( FORMAT_STRING_TIMESTAMP_WITH_MICROS_AND_ERA_SUFFIX_PADDED, Locale.ENGLISH ) ); + private static final ThreadLocal TIMESTAMP_WITH_NANOS_FORMAT = + ThreadLocal.withInitial( () -> new SimpleDateFormat( FORMAT_STRING_TIMESTAMP_WITH_NANOS_PADDED, Locale.ENGLISH ) ); + private static final ThreadLocal TIMESTAMP_WITH_NANOS_AND_ERA_PREFIX_FORMAT = + ThreadLocal.withInitial( () -> new SimpleDateFormat( FORMAT_STRING_TIMESTAMP_WITH_NANOS_AND_ERA_PREFIX_PADDED, Locale.ENGLISH ) ); /** * Pattern used for parsing literal offset datetimes in HQL. @@ -151,6 +250,33 @@ public static void appendAsTimestampWithNanos( ); } + public static void appendAsTimestampWithNanosAndEraPrefix( + SqlAppender appender, + TemporalAccessor temporalAccessor, + boolean supportsOffset, + TimeZone jdbcTimeZone) { + if ( isBcEra( temporalAccessor ) ) { + appendAsTimestamp( + appender, + temporalAccessor, + supportsOffset, + jdbcTimeZone, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_ERA_PREFIX, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_OFFSET_AND_ERA_PREFIX + ); + } + else { + appendAsTimestamp( + appender, + temporalAccessor, + supportsOffset, + jdbcTimeZone, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_OFFSET + ); + } + } + public static void appendAsTimestampWithNanos( SqlAppender appender, TemporalAccessor temporalAccessor, @@ -162,13 +288,36 @@ public static void appendAsTimestampWithNanos( temporalAccessor, supportsOffset, jdbcTimeZone, - DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS, allowZforZeroOffset ? DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_OFFSET : DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_OFFSET_NOZ ); } + public static void appendAsTimestampWithNanosWithoutYear0( + SqlAppender appender, + TemporalAccessor temporalAccessor, + boolean supportsOffset, + TimeZone jdbcTimeZone, + boolean allowZforZeroOffset) { + if ( isBcEra( temporalAccessor ) ) { + appendAsTimestamp( + appender, + temporalAccessor, + supportsOffset, + jdbcTimeZone, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_IN_BC, + allowZforZeroOffset + ? DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_OFFSET_IN_BC + : DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_OFFSET_NOZ_IN_BC + ); + } + else { + appendAsTimestampWithNanos( appender, temporalAccessor, supportsOffset, jdbcTimeZone, allowZforZeroOffset ); + } + } + public static void appendAsTimestampWithMicros( SqlAppender appender, TemporalAccessor temporalAccessor, @@ -184,6 +333,33 @@ public static void appendAsTimestampWithMicros( ); } + public static void appendAsTimestampWithMicrosAndEraSuffix( + SqlAppender appender, + TemporalAccessor temporalAccessor, + boolean supportsOffset, + TimeZone jdbcTimeZone) { + if ( isBcEra( temporalAccessor ) ) { + appendAsTimestamp( + appender, + temporalAccessor, + supportsOffset, + jdbcTimeZone, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS_AND_ERA_SUFFIX, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS_AND_OFFSET_AND_ERA_SUFFIX + ); + } + else { + appendAsTimestamp( + appender, + temporalAccessor, + supportsOffset, + jdbcTimeZone, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS_AND_OFFSET + ); + } + } + public static void appendAsTimestampWithMicros( SqlAppender appender, TemporalAccessor temporalAccessor, @@ -202,6 +378,38 @@ public static void appendAsTimestampWithMicros( ); } + public static void appendAsTimestampWithMicrosAndEraSuffix( + SqlAppender appender, + TemporalAccessor temporalAccessor, + boolean supportsOffset, + TimeZone jdbcTimeZone, + boolean allowZforZeroOffset) { + if ( isBcEra( temporalAccessor ) ) { + appendAsTimestamp( + appender, + temporalAccessor, + supportsOffset, + jdbcTimeZone, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS_AND_ERA_SUFFIX, + allowZforZeroOffset + ? DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS_AND_OFFSET_AND_ERA_SUFFIX + : DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS_AND_OFFSET_NOZ_AND_ERA_SUFFIX + ); + } + else { + appendAsTimestamp( + appender, + temporalAccessor, + supportsOffset, + jdbcTimeZone, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS, + allowZforZeroOffset + ? DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS_AND_OFFSET + : DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS_AND_OFFSET_NOZ + ); + } + } + public static void appendAsTimestampWithMillis( SqlAppender appender, TemporalAccessor temporalAccessor, @@ -217,6 +425,33 @@ public static void appendAsTimestampWithMillis( ); } + public static void appendAsTimestampWithMillisAndEraSuffix( + SqlAppender appender, + TemporalAccessor temporalAccessor, + boolean supportsOffset, + TimeZone jdbcTimeZone) { + if ( isBcEra( temporalAccessor ) ) { + appendAsTimestamp( + appender, + temporalAccessor, + supportsOffset, + jdbcTimeZone, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_MILLIS_AND_ERA_SUFFIX, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_MILLIS_AND_OFFSET_AND_ERA_SUFFIX + ); + } + else { + appendAsTimestamp( + appender, + temporalAccessor, + supportsOffset, + jdbcTimeZone, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_MILLIS, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_MILLIS_AND_OFFSET + ); + } + } + private static void appendAsTimestamp( SqlAppender appender, TemporalAccessor temporalAccessor, @@ -258,9 +493,34 @@ else if ( temporalAccessor instanceof Instant instant ) { } public static void appendAsDate(SqlAppender appender, TemporalAccessor temporalAccessor) { + DATE_TIME_FORMATTER_DATE_WITH_ERA.formatTo( temporalAccessor, appender ); + } + + public static void appendAsDateWithoutYear0(SqlAppender appender, TemporalAccessor temporalAccessor) { + if ( isBcEra( temporalAccessor ) ) { + appender.appendSql( '-' );; + } DATE_TIME_FORMATTER_DATE.formatTo( temporalAccessor, appender ); } + public static void appendAsDateWithEraPrefix(SqlAppender appender, TemporalAccessor temporalAccessor) { + if ( isBcEra( temporalAccessor ) ) { + DATE_TIME_FORMATTER_DATE_WITH_ERA_PREFIX.formatTo( temporalAccessor, appender ); + } + else { + DATE_TIME_FORMATTER_DATE.formatTo( temporalAccessor, appender ); + } + } + + public static void appendAsDateWithEraSuffix(SqlAppender appender, TemporalAccessor temporalAccessor) { + if ( isBcEra( temporalAccessor ) ) { + DATE_TIME_FORMATTER_DATE_WITH_ERA_SUFFIX.formatTo( temporalAccessor, appender ); + } + else { + DATE_TIME_FORMATTER_DATE.formatTo( temporalAccessor, appender ); + } + } + public static void appendAsTime( SqlAppender appender, TemporalAccessor temporalAccessor, @@ -284,23 +544,24 @@ public static void appendAsLocalTime(SqlAppender appender, TemporalAccessor temp } public static void appendAsTimestampWithMillis(SqlAppender appender, java.util.Date date, TimeZone jdbcTimeZone) { - final SimpleDateFormat simpleDateFormat = TIMESTAMP_WITH_MILLIS_FORMAT.get(); - final TimeZone originalTimeZone = simpleDateFormat.getTimeZone(); - try { - simpleDateFormat.setTimeZone( jdbcTimeZone ); - appender.appendSql( simpleDateFormat.format( date ) ); + appendDateWithFormat( appender, date, jdbcTimeZone, TIMESTAMP_WITH_MILLIS_FORMAT.get() ); + } + + public static void appendAsTimestampWithMillisAndEraSuffix(SqlAppender appender, java.util.Date date, TimeZone jdbcTimeZone) { + if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appendDateWithFormatOnly( appender, date, jdbcTimeZone, TIMESTAMP_WITH_MILLIS_AND_ERA_SUFFIX_FORMAT.get() ); } - finally { - simpleDateFormat.setTimeZone( originalTimeZone ); + else { + appendDateWithFormatOnly( appender, date, jdbcTimeZone, TIMESTAMP_WITH_MILLIS_FORMAT.get() ); } } public static void appendAsTimestampWithMicros(SqlAppender appender, Date date, TimeZone jdbcTimeZone) { - if ( date instanceof Timestamp ) { + if ( date instanceof Timestamp timestamp ) { // java.sql.Timestamp supports nano sec appendAsTimestamp( appender, - date.toInstant().atZone( jdbcTimeZone.toZoneId() ), + toLocalDateTime( timestamp ).atZone( jdbcTimeZone.toZoneId() ), false, jdbcTimeZone, DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS, @@ -308,25 +569,49 @@ public static void appendAsTimestampWithMicros(SqlAppender appender, Date date, ); } else { - // java.util.Date supports only milli sec - final SimpleDateFormat simpleDateFormat = TIMESTAMP_WITH_MILLIS_FORMAT.get(); - final TimeZone originalTimeZone = simpleDateFormat.getTimeZone(); - try { - simpleDateFormat.setTimeZone( jdbcTimeZone ); - appender.appendSql( simpleDateFormat.format( date ) ); + appendDateWithFormat( appender, date, jdbcTimeZone, TIMESTAMP_WITH_MICROS_FORMAT.get() ); + } + } + + public static void appendAsTimestampWithMicrosAndEraSuffix(SqlAppender appender, Date date, TimeZone jdbcTimeZone) { + if ( date instanceof Timestamp timestamp ) { + // java.sql.Timestamp supports nano sec + final ZonedDateTime zonedDateTime = toLocalDateTime( timestamp ).atZone( jdbcTimeZone.toZoneId() ); + if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appendAsTimestamp( + appender, + zonedDateTime, + false, + jdbcTimeZone, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS_AND_ERA_SUFFIX, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS_AND_OFFSET_AND_ERA_SUFFIX + ); } - finally { - simpleDateFormat.setTimeZone( originalTimeZone ); + else { + appendAsTimestamp( + appender, + zonedDateTime, + false, + jdbcTimeZone, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_MICROS_AND_OFFSET + ); } } + else if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appendDateWithFormatOnly( appender, date, jdbcTimeZone, TIMESTAMP_WITH_MICROS_AND_ERA_SUFFIX_FORMAT.get() ); + } + else { + appendDateWithFormatOnly( appender, date, jdbcTimeZone, TIMESTAMP_WITH_MICROS_FORMAT.get() ); + } } public static void appendAsTimestampWithNanos(SqlAppender appender, Date date, TimeZone jdbcTimeZone) { - if ( date instanceof Timestamp ) { + if ( date instanceof Timestamp timestamp ) { // java.sql.Timestamp supports nano sec appendAsTimestamp( appender, - date.toInstant().atZone( jdbcTimeZone.toZoneId() ), + toLocalDateTime( timestamp ).atZone( jdbcTimeZone.toZoneId() ), false, jdbcTimeZone, DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS, @@ -334,23 +619,158 @@ public static void appendAsTimestampWithNanos(SqlAppender appender, Date date, T ); } else { - // java.util.Date supports only milli sec - final SimpleDateFormat simpleDateFormat = TIMESTAMP_WITH_MILLIS_FORMAT.get(); - final TimeZone originalTimeZone = simpleDateFormat.getTimeZone(); - try { - simpleDateFormat.setTimeZone( jdbcTimeZone ); - appender.appendSql( simpleDateFormat.format( date ) ); + appendDateWithFormat( appender, date, jdbcTimeZone, TIMESTAMP_WITH_NANOS_FORMAT.get() ); + } + } + + public static void appendAsTimestampWithNanosAndEraPrefix(SqlAppender appender, Date date, TimeZone jdbcTimeZone) { + if ( date instanceof Timestamp timestamp ) { + // java.sql.Timestamp supports nano sec + final ZonedDateTime zonedDateTime = toLocalDateTime( timestamp ).atZone( jdbcTimeZone.toZoneId() ); + if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appendAsTimestamp( + appender, + zonedDateTime, + false, + jdbcTimeZone, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_ERA_PREFIX, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_OFFSET_AND_ERA_PREFIX + ); + } + else { + appendAsTimestamp( + appender, + zonedDateTime, + false, + jdbcTimeZone, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_OFFSET + ); + } + } + else if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appendDateWithFormatOnly( appender, date, jdbcTimeZone, TIMESTAMP_WITH_NANOS_AND_ERA_PREFIX_FORMAT.get() ); + } + else { + appendDateWithFormatOnly( appender, date, jdbcTimeZone, TIMESTAMP_WITH_NANOS_FORMAT.get() ); + } + } + + public static void appendAsTimestampWithNanosWithoutYear0(SqlAppender appender, Date date, TimeZone jdbcTimeZone) { + if ( date instanceof Timestamp timestamp ) { + // java.sql.Timestamp supports nano sec + final ZonedDateTime zonedDateTime = toLocalDateTime( timestamp ).atZone( jdbcTimeZone.toZoneId() ); + if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appendAsTimestamp( + appender, + zonedDateTime, + false, + jdbcTimeZone, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_IN_BC, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_OFFSET_IN_BC + ); } - finally { - simpleDateFormat.setTimeZone( originalTimeZone ); + else { + appendAsTimestamp( + appender, + zonedDateTime, + false, + jdbcTimeZone, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS, + DATE_TIME_FORMATTER_TIMESTAMP_WITH_NANOS_AND_OFFSET + ); + } + } + else { + if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appender.appendSql( '-' ); } + appendDateWithFormatOnly( appender, date, jdbcTimeZone, TIMESTAMP_WITH_NANOS_FORMAT.get() ); } } + private static void appendDateWithFormat(SqlAppender appender, java.util.Date date, TimeZone jdbcTimeZone, SimpleDateFormat simpleDateFormat) { + if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appender.appendSql( '-' ); + date = addOneYear( date ); + } + appendDateWithFormatOnly( appender, date, jdbcTimeZone, simpleDateFormat ); + } + + private static void appendDateWithFormatOnly(SqlAppender appender, java.util.Date date, TimeZone jdbcTimeZone, SimpleDateFormat simpleDateFormat) { + final TimeZone originalTimeZone = simpleDateFormat.getTimeZone(); + try { + simpleDateFormat.setTimeZone( jdbcTimeZone ); + appender.appendSql( simpleDateFormat.format( date ) ); + } + finally { + simpleDateFormat.setTimeZone( originalTimeZone ); + } + } + + public static String dateToString(Date date) { + final StringBuilderSqlAppender appender = new StringBuilderSqlAppender(); + appendAsDate( appender, date ); + return appender.toString(); + } + public static void appendAsDate(SqlAppender appender, Date date) { + if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appender.appendSql( '-' ); + appender.appendSql( LOCAL_DATE_FORMAT.get().format( addOneYear( date ) ) ); + } + else { + appender.appendSql( LOCAL_DATE_FORMAT.get().format( date ) ); + } + } + + public static void appendAsDateWithEraPrefix(SqlAppender appender, Date date) { + if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appender.appendSql( LOCAL_DATE_WITH_ERA_PREFIX_FORMAT.get().format( date ) ); + } + else { + appender.appendSql( LOCAL_DATE_FORMAT.get().format( date ) ); + } + } + + public static void appendAsDateWithEraSuffix(SqlAppender appender, Date date) { + if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appender.appendSql( LOCAL_DATE_WITH_ERA_SUFFIX_FORMAT.get().format( date ) ); + } + else { + appender.appendSql( LOCAL_DATE_FORMAT.get().format( date ) ); + } + } + + public static void appendAsDateWithoutYear0(SqlAppender appender, Date date) { + if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appender.appendSql( '-' ); + } appender.appendSql( LOCAL_DATE_FORMAT.get().format( date ) ); } + public static String timestampToString(Date date) { + final StringBuilderSqlAppender appender = new StringBuilderSqlAppender(); + appendAsTimestampWithNanos( appender, date, TimeZone.getTimeZone( "UTC" ) ); + return appender.toString(); + } + + private static Date addOneYear(Date date) { + final Calendar calendar = Calendar.getInstance(); + calendar.setTime( date ); + calendar.add( Calendar.YEAR, 1 ); + if ( date instanceof Timestamp ) { + return new Timestamp( calendar.getTime().getTime() ); + } + else if ( date instanceof java.sql.Date ) { + return new java.sql.Date( calendar.getTime().getTime() ); + } + else { + assert date.getClass() == java.util.Date.class; + return new Date( calendar.getTime().getTime() ); + } + } + /** * @deprecated Use {@link #appendAsLocalTime(SqlAppender, Date)} instead */ @@ -360,15 +780,7 @@ public static void appendAsTime(SqlAppender appender, java.util.Date date) { } public static void appendAsTime(SqlAppender appender, java.util.Date date, TimeZone jdbcTimeZone) { - final SimpleDateFormat simpleDateFormat = TIME_WITH_OFFSET_FORMAT.get(); - final TimeZone originalTimeZone = simpleDateFormat.getTimeZone(); - try { - simpleDateFormat.setTimeZone( jdbcTimeZone ); - appender.appendSql( simpleDateFormat.format( date ) ); - } - finally { - simpleDateFormat.setTimeZone( originalTimeZone ); - } + appendDateWithFormatOnly( appender, date, jdbcTimeZone, TIME_WITH_OFFSET_FORMAT.get() ); } public static void appendAsLocalTime(SqlAppender appender, Date date) { @@ -379,29 +791,78 @@ public static void appendAsTimestampWithMillis( SqlAppender appender, java.util.Calendar calendar, TimeZone jdbcTimeZone) { - final SimpleDateFormat simpleDateFormat = TIMESTAMP_WITH_MILLIS_FORMAT.get(); - final TimeZone originalTimeZone = simpleDateFormat.getTimeZone(); - try { - simpleDateFormat.setTimeZone( jdbcTimeZone ); - appender.appendSql( simpleDateFormat.format( calendar.getTime() ) ); + appendDateWithFormat( appender, calendar.getTime(), jdbcTimeZone, TIMESTAMP_WITH_MILLIS_FORMAT.get() ); + } + + public static void appendAsTimestampWithMillisAndEraPrefix( + SqlAppender appender, + java.util.Calendar calendar, + TimeZone jdbcTimeZone) { + final Date date = calendar.getTime(); + if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appendDateWithFormatOnly( appender, date, jdbcTimeZone, TIMESTAMP_WITH_MILLIS_AND_ERA_PREFIX_FORMAT.get() ); } - finally { - simpleDateFormat.setTimeZone( originalTimeZone ); + else { + appendDateWithFormatOnly( appender, date, jdbcTimeZone, TIMESTAMP_WITH_MILLIS_FORMAT.get() ); } } + public static void appendAsTimestampWithMillisAndEraSuffix( + SqlAppender appender, + java.util.Calendar calendar, + TimeZone jdbcTimeZone) { + final Date date = calendar.getTime(); + if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appendDateWithFormatOnly( appender, date, jdbcTimeZone, TIMESTAMP_WITH_MILLIS_AND_ERA_SUFFIX_FORMAT.get() ); + } + else { + appendDateWithFormatOnly( appender, date, jdbcTimeZone, TIMESTAMP_WITH_MILLIS_FORMAT.get() ); + } + } + + public static void appendAsTimestampWithMillisWithoutYear0( + SqlAppender appender, + java.util.Calendar calendar, + TimeZone jdbcTimeZone) { + final Date date = calendar.getTime(); + if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appender.appendSql( '-' ); + } + appendDateWithFormatOnly( appender, date, jdbcTimeZone, TIMESTAMP_WITH_MILLIS_FORMAT.get() ); + } + public static void appendAsDate(SqlAppender appender, java.util.Calendar calendar) { - final SimpleDateFormat simpleDateFormat = LOCAL_DATE_FORMAT.get(); - final TimeZone originalTimeZone = simpleDateFormat.getTimeZone(); - try { - simpleDateFormat.setTimeZone( calendar.getTimeZone() ); - appender.appendSql( simpleDateFormat.format( calendar.getTime() ) ); + appendDateWithFormat( appender, calendar.getTime(), calendar.getTimeZone(), LOCAL_DATE_FORMAT.get() ); + } + + public static void appendAsDateWithEraPrefix(SqlAppender appender, java.util.Calendar calendar) { + final Date date = calendar.getTime(); + if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appendDateWithFormatOnly( appender, date, calendar.getTimeZone(), LOCAL_DATE_WITH_ERA_PREFIX_FORMAT.get() ); } - finally { - simpleDateFormat.setTimeZone( originalTimeZone ); + else { + appendDateWithFormatOnly( appender, date, calendar.getTimeZone(), LOCAL_DATE_FORMAT.get() ); } } + public static void appendAsDateWithEraSuffix(SqlAppender appender, java.util.Calendar calendar) { + final Date date = calendar.getTime(); + if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appendDateWithFormatOnly( appender, date, calendar.getTimeZone(), LOCAL_DATE_WITH_ERA_SUFFIX_FORMAT.get() ); + } + else { + appendDateWithFormatOnly( appender, date, calendar.getTimeZone(), LOCAL_DATE_FORMAT.get() ); + } + } + + public static void appendAsDateWithoutYear0(SqlAppender appender, java.util.Calendar calendar) { + final Date date = calendar.getTime(); + if ( date.getTime() < YEAR_ONE_EPOCH_MILLIS ) { + appender.appendSql( '-' ); + } + appendDateWithFormatOnly( appender, date, calendar.getTimeZone(), LOCAL_DATE_FORMAT.get() ); + } + /** * @deprecated Use {@link #appendAsLocalTime(SqlAppender, Calendar)} instead */ @@ -411,15 +872,7 @@ public static void appendAsTime(SqlAppender appender, java.util.Calendar calenda } public static void appendAsTime(SqlAppender appender, java.util.Calendar calendar, TimeZone jdbcTimeZone) { - final SimpleDateFormat simpleDateFormat = TIME_WITH_OFFSET_FORMAT.get(); - final TimeZone originalTimeZone = simpleDateFormat.getTimeZone(); - try { - simpleDateFormat.setTimeZone( jdbcTimeZone ); - appender.appendSql( simpleDateFormat.format( calendar.getTime() ) ); - } - finally { - simpleDateFormat.setTimeZone( originalTimeZone ); - } + appendDateWithFormatOnly( appender, calendar.getTime(), jdbcTimeZone, TIME_WITH_OFFSET_FORMAT.get() ); } public static void appendAsLocalTime(SqlAppender appender, Calendar calendar) { @@ -514,4 +967,119 @@ private static int pow10(int exponent) { default -> (int) Math.pow( 10, exponent ); }; } + + public static Timestamp toTimestamp(Instant instant) { + /* + * This works around two bugs: + * - HHH-13266 (JDK-8061577): around and before 1900, + * the number of milliseconds since the epoch does not mean the same thing + * for java.util and java.time, so conversion must be done using the year, month, day, hour, etc. + * - HHH-13379 (JDK-4312621): after 1908 (approximately), + * Daylight Saving Time introduces ambiguity in the year/month/day/hour/etc representation once a year + * (on DST end), so conversion must be done using the number of milliseconds since the epoch. + * - around 1905, both methods are equally valid, so we don't really care which one is used. + */ + final ZonedDateTime zonedDateTime = instant.atZone( ZoneId.systemDefault() ); + if ( zonedDateTime.getYear() < 1905 ) { + return Timestamp.valueOf( zonedDateTime.toLocalDateTime() ); + } + else { + return Timestamp.from( instant ); + } + } + + public static java.sql.Date toSqlDate(Instant instant) { + /* + * This works around two bugs: + * - HHH-13266 (JDK-8061577): around and before 1900, + * the number of milliseconds since the epoch does not mean the same thing + * for java.util and java.time, so conversion must be done using the year, month, day, hour, etc. + * - HHH-13379 (JDK-4312621): after 1908 (approximately), + * Daylight Saving Time introduces ambiguity in the year/month/day/hour/etc representation once a year + * (on DST end), so conversion must be done using the number of milliseconds since the epoch. + * - around 1905, both methods are equally valid, so we don't really care which one is used. + */ + final ZonedDateTime zonedDateTime = instant.atZone( ZoneId.systemDefault() ); + if ( zonedDateTime.getYear() < 1905 ) { + return java.sql.Date.valueOf( zonedDateTime.toLocalDate() ); + } + else { + return new java.sql.Date( instant.toEpochMilli() ); + } + } + + public static Instant toInstant(Timestamp timestamp) { + /* + * This works around two bugs: + * - HHH-13266 (JDK-8061577): around and before 1900, + * the number of milliseconds since the epoch does not mean the same thing + * for java.util and java.time, so conversion must be done using the year, month, day, hour, etc. + * - HHH-13379 (JDK-4312621): after 1908 (approximately), + * Daylight Saving Time introduces ambiguity in the year/month/day/hour/etc representation once a year + * (on DST end), so conversion must be done using the number of milliseconds since the epoch. + * - around 1905, both methods are equally valid, so we don't really care which one is used. + */ + if ( timestamp.getYear() < 5 ) { // Timestamp year 0 is 1900 + return toLocalDateTime( timestamp ).atZone( ZoneId.systemDefault() ).toInstant(); + } + else { + return timestamp.toInstant(); + } + } + + public static Instant toInstant(Date date) { + if ( date instanceof Timestamp timestamp ) { + return toInstant( timestamp ); + } + else { + return date.toInstant(); + } + } + + public static LocalDate toLocalDate(java.sql.Date sqlDate) { + final LocalDate localDate = sqlDate.toLocalDate(); + // Workaround the JDK-8269590 bug in java.sql.Date.toLocalDate(), which will use the positive BCE year + // instead of a negative year when constructing LocalDate + if ( sqlDate.getTime() < DateTimeUtils.YEAR_ONE_EPOCH_MILLIS && localDate.getYear() > 0 ) { + return localDate.withYear( -localDate.getYear() + 1 ); + } + else { + return localDate; + } + } + + public static LocalDateTime toLocalDateTime(Timestamp timestamp) { + final LocalDateTime localDateTime = timestamp.toLocalDateTime(); + // Workaround the JDK-8269590 bug in java.sql.Date.toLocalDate(), which will use the positive BCE year + // instead of a negative year when constructing LocalDate + if ( timestamp.getTime() < DateTimeUtils.YEAR_ONE_EPOCH_MILLIS && localDateTime.getYear() > 0 ) { + return localDateTime.withYear( -localDateTime.getYear() + 1 ); + } + else { + return localDateTime; + } + } + + public static boolean isBcEra(TemporalAccessor temporalAccessor) { + if ( temporalAccessor.isSupported( ChronoField.ERA ) ) { + return temporalAccessor.get( ChronoField.ERA ) == IsoEra.BCE.getValue(); + } + else if ( temporalAccessor instanceof Instant instant ) { + return instant.toEpochMilli() < YEAR_ONE_EPOCH_MILLIS; + } + else if ( temporalAccessor.isSupported( ChronoField.EPOCH_DAY ) ) { + return temporalAccessor.get( ChronoField.EPOCH_DAY ) < YEAR_ONE_EPOCH_MILLIS / 1000; + } + else { + return false; + } + } + + public static boolean isBcEra(Date date) { + return date.getTime() < YEAR_ONE_EPOCH_MILLIS; + } + + public static boolean isBcEra(Calendar calendar) { + return calendar.getTime().getTime() < YEAR_ONE_EPOCH_MILLIS; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CalendarDateJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CalendarDateJavaType.java index db5446e701ca..29d0cf895e5a 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CalendarDateJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CalendarDateJavaType.java @@ -5,6 +5,7 @@ package org.hibernate.type.descriptor.java; import java.sql.Types; +import java.time.Instant; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; @@ -13,6 +14,7 @@ import org.hibernate.dialect.Dialect; import org.hibernate.internal.util.compare.CalendarComparator; +import org.hibernate.type.descriptor.DateTimeUtils; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; @@ -123,6 +125,11 @@ else if ( value instanceof Date date ) { cal.setTime( date ); return cal; } + else if ( value instanceof Instant instant ) { + final Calendar cal = new GregorianCalendar(); + cal.setTime( DateTimeUtils.toSqlDate( instant ) ); + return cal; + } else { throw unknownWrap( value.getClass() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CalendarJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CalendarJavaType.java index 3b23b21b162f..2c322ef7c9de 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CalendarJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/CalendarJavaType.java @@ -5,6 +5,7 @@ package org.hibernate.type.descriptor.java; import java.sql.Types; +import java.time.Instant; import java.time.ZonedDateTime; import java.util.Calendar; import java.util.GregorianCalendar; @@ -14,6 +15,7 @@ import org.hibernate.dialect.Dialect; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.compare.CalendarComparator; +import org.hibernate.type.descriptor.DateTimeUtils; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; @@ -140,6 +142,11 @@ else if ( value instanceof java.util.Date date ) { cal.setTime( date ); return cal; } + else if ( value instanceof Instant instant ) { + final Calendar cal = new GregorianCalendar(); + cal.setTime( DateTimeUtils.toTimestamp( instant ) ); + return cal; + } else { throw unknownWrap( value.getClass() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/InstantJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/InstantJavaType.java index 3d1b49220345..d12b75adb64d 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/InstantJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/InstantJavaType.java @@ -7,7 +7,6 @@ import java.sql.Timestamp; import java.time.Instant; import java.time.OffsetDateTime; -import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -19,6 +18,7 @@ import org.hibernate.dialect.Dialect; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.type.descriptor.DateTimeUtils; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; @@ -107,27 +107,11 @@ public X unwrap(Instant instant, Class type, WrapperOptions options) { } if ( Timestamp.class.isAssignableFrom( type ) ) { - /* - * This works around two bugs: - * - HHH-13266 (JDK-8061577): around and before 1900, - * the number of milliseconds since the epoch does not mean the same thing - * for java.util and java.time, so conversion must be done using the year, month, day, hour, etc. - * - HHH-13379 (JDK-4312621): after 1908 (approximately), - * Daylight Saving Time introduces ambiguity in the year/month/day/hour/etc representation once a year - * (on DST end), so conversion must be done using the number of milliseconds since the epoch. - * - around 1905, both methods are equally valid, so we don't really care which one is used. - */ - ZonedDateTime zonedDateTime = instant.atZone( ZoneId.systemDefault() ); - if ( zonedDateTime.getYear() < 1905 ) { - return (X) Timestamp.valueOf( zonedDateTime.toLocalDateTime() ); - } - else { - return (X) Timestamp.from( instant ); - } + return (X) DateTimeUtils.toTimestamp( instant ); } if ( java.sql.Date.class.isAssignableFrom( type ) ) { - return (X) new java.sql.Date( instant.toEpochMilli() ); + return (X) DateTimeUtils.toSqlDate( instant ); } if ( java.sql.Time.class.isAssignableFrom( type ) ) { @@ -160,22 +144,7 @@ public Instant wrap(X value, WrapperOptions options) { } if ( value instanceof Timestamp timestamp ) { - /* - * This works around two bugs: - * - HHH-13266 (JDK-8061577): around and before 1900, - * the number of milliseconds since the epoch does not mean the same thing - * for java.util and java.time, so conversion must be done using the year, month, day, hour, etc. - * - HHH-13379 (JDK-4312621): after 1908 (approximately), - * Daylight Saving Time introduces ambiguity in the year/month/day/hour/etc representation once a year - * (on DST end), so conversion must be done using the number of milliseconds since the epoch. - * - around 1905, both methods are equally valid, so we don't really care which one is used. - */ - if ( timestamp.getYear() < 5 ) { // Timestamp year 0 is 1900 - return timestamp.toLocalDateTime().atZone( ZoneId.systemDefault() ).toInstant(); - } - else { - return timestamp.toInstant(); - } + return DateTimeUtils.toInstant( timestamp ); } if ( value instanceof Long longValue ) { @@ -186,8 +155,8 @@ public Instant wrap(X value, WrapperOptions options) { return ZonedDateTime.ofInstant( calendar.toInstant(), calendar.getTimeZone().toZoneId() ).toInstant(); } - if ( value instanceof Date ) { - return ( (Date) value ).toInstant(); + if ( value instanceof Date date ) { + return date.toInstant(); } throw unknownWrap( value.getClass() ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcDateJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcDateJavaType.java index 58a990fec108..65e5d7522433 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcDateJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcDateJavaType.java @@ -5,9 +5,8 @@ package org.hibernate.type.descriptor.java; import java.sql.Types; +import java.time.Instant; import java.time.LocalDate; -import java.time.LocalTime; -import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeParseException; @@ -18,6 +17,7 @@ import org.hibernate.HibernateException; import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.descriptor.DateTimeUtils; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; @@ -207,17 +207,21 @@ public Date wrap(Object value, WrapperOptions options) { return java.sql.Date.valueOf( localDate ); } + if ( value instanceof Instant instant ) { + return DateTimeUtils.toSqlDate( instant ); + } + throw unknownWrap( value.getClass() ); } @Override public String toString(Date value) { - if ( value instanceof java.sql.Date ) { - return LITERAL_FORMATTER.format( ( (java.sql.Date) value ).toLocalDate() ); - } - else { - return LITERAL_FORMATTER.format( LocalDate.ofInstant( value.toInstant(), ZoneOffset.systemDefault() ) ); - } + return DateTimeUtils.dateToString( value ); + } + + @Override + public String extractLoggableRepresentation(Date value) { + return value == null ? "null" : toString( value ); } @Override @@ -244,12 +248,7 @@ public Date fromEncodedString(CharSequence charSequence, int start, int end) { @Override public void appendEncodedString(SqlAppender sb, Date value) { - if ( value instanceof java.sql.Date ) { - LITERAL_FORMATTER.formatTo( ( (java.sql.Date) value ).toLocalDate(), sb ); - } - else { - LITERAL_FORMATTER.formatTo( LocalTime.ofInstant( value.toInstant(), ZoneOffset.systemDefault() ), sb ); - } + DateTimeUtils.appendAsDate( sb, value ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcTimestampJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcTimestampJavaType.java index ade64a05d7fd..cbc569de1949 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcTimestampJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/JdbcTimestampJavaType.java @@ -24,6 +24,7 @@ import org.hibernate.dialect.Dialect; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.descriptor.DateTimeUtils; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; @@ -42,7 +43,7 @@ public class JdbcTimestampJavaType extends AbstractTemporalJavaType implements VersionJavaType { public static final JdbcTimestampJavaType INSTANCE = new JdbcTimestampJavaType(); - public static final String TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss.SSSSSSSSS"; + public static final String TIMESTAMP_FORMAT = "uuuu-MM-dd HH:mm:ss.SSSSSSSSS"; /** * Intended for use in reading HQL literals and writing SQL literals @@ -186,6 +187,10 @@ public Date wrap(X value, WrapperOptions options) { return new Timestamp( calendar.getTimeInMillis() ); } + if ( value instanceof Instant instant ) { + return DateTimeUtils.toTimestamp( instant ); + } + throw unknownWrap( value.getClass() ); } @@ -199,7 +204,12 @@ public boolean isWider(JavaType javaType) { @Override public String toString(Date value) { - return LITERAL_FORMATTER.format( value.toInstant() ); + return DateTimeUtils.timestampToString( value ); + } + + @Override + public String extractLoggableRepresentation(Date value) { + return value == null ? "null" : toString( value ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/LocalDateJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/LocalDateJavaType.java index 2ace6939351f..54c61e6c570c 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/LocalDateJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/LocalDateJavaType.java @@ -19,6 +19,7 @@ import jakarta.persistence.TemporalType; import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.DateTimeUtils; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; @@ -157,7 +158,7 @@ public LocalDate wrap(X value, WrapperOptions options) { if (value instanceof Date date) { if (value instanceof java.sql.Date sqlDate) { - return sqlDate.toLocalDate(); + return DateTimeUtils.toLocalDate( sqlDate ); } else { return Instant.ofEpochMilli( date.getTime() ).atZone( ZoneId.systemDefault() ).toLocalDate(); diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/LocalDateTimeJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/LocalDateTimeJavaType.java index 1a6abfdbf8e1..623b82fcbced 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/LocalDateTimeJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/LocalDateTimeJavaType.java @@ -19,6 +19,7 @@ import org.hibernate.dialect.Dialect; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.DateTimeUtils; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; @@ -100,15 +101,13 @@ public X unwrap(LocalDateTime value, Class type, WrapperOptions options) return (X) Timestamp.valueOf( value ); } - if ( java.sql.Date.class.isAssignableFrom( type ) ) { - Instant instant = value.atZone( ZoneId.systemDefault() ).toInstant(); - return (X) java.sql.Date.from( instant ); - } - - if ( java.sql.Time.class.isAssignableFrom( type ) ) { - Instant instant = value.atZone( ZoneId.systemDefault() ).toInstant(); - return (X) java.sql.Time.from( instant ); - } +// if ( java.sql.Date.class.isAssignableFrom( type ) ) { +// return (X) java.sql.Date.valueOf( value.toLocalDate() ); +// } +// +// if ( java.sql.Time.class.isAssignableFrom( type ) ) { +// return (X) java.sql.Time.valueOf( value.toLocalTime() ); +// } if ( Date.class.isAssignableFrom( type ) ) { Instant instant = value.atZone( ZoneId.systemDefault() ).toInstant(); @@ -145,7 +144,7 @@ public LocalDateTime wrap(X value, WrapperOptions options) { * ts.toInstant() assumes the number of milliseconds since the epoch * means the same thing in Timestamp and Instant, but it doesn't, in particular before 1900. */ - return timestamp.toLocalDateTime(); + return DateTimeUtils.toLocalDateTime( timestamp ); } if (value instanceof Long longValue) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/OffsetDateTimeJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/OffsetDateTimeJavaType.java index 8e8b36cc1d29..1f9e78c30053 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/OffsetDateTimeJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/OffsetDateTimeJavaType.java @@ -24,6 +24,7 @@ import org.hibernate.dialect.Dialect; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.DateTimeUtils; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; @@ -200,22 +201,7 @@ public OffsetDateTime wrap(X value, WrapperOptions options) { } if (value instanceof Timestamp timestamp) { - /* - * This works around two bugs: - * - HHH-13266 (JDK-8061577): around and before 1900, - * the number of milliseconds since the epoch does not mean the same thing - * for java.util and java.time, so conversion must be done using the year, month, day, hour, etc. - * - HHH-13379 (JDK-4312621): after 1908 (approximately), - * Daylight Saving Time introduces ambiguity in the year/month/day/hour/etc representation once a year - * (on DST end), so conversion must be done using the number of milliseconds since the epoch. - * - around 1905, both methods are equally valid, so we don't really care which one is used. - */ - if ( timestamp.getYear() < 5 ) { // Timestamp year 0 is 1900 - return timestamp.toLocalDateTime().atZone( ZoneId.systemDefault() ).toOffsetDateTime(); - } - else { - return OffsetDateTime.ofInstant( timestamp.toInstant(), ZoneId.systemDefault() ); - } + return DateTimeUtils.toInstant( timestamp ).atZone( ZoneId.systemDefault() ).toOffsetDateTime(); } if (value instanceof Date date) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ZonedDateTimeJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ZonedDateTimeJavaType.java index 22cde050e664..be7e5650af09 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ZonedDateTimeJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ZonedDateTimeJavaType.java @@ -20,6 +20,7 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.ZonedDateTimeComparator; import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.DateTimeUtils; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; @@ -162,22 +163,7 @@ public ZonedDateTime wrap(X value, WrapperOptions options) { } if (value instanceof Timestamp timestamp) { - /* - * This works around two bugs: - * - HHH-13266 (JDK-8061577): around and before 1900, - * the number of milliseconds since the epoch does not mean the same thing - * for java.util and java.time, so conversion must be done using the year, month, day, hour, etc. - * - HHH-13379 (JDK-4312621): after 1908 (approximately), - * Daylight Saving Time introduces ambiguity in the year/month/day/hour/etc representation once a year - * (on DST end), so conversion must be done using the number of milliseconds since the epoch. - * - around 1905, both methods are equally valid, so we don't really care which one is used. - */ - if ( timestamp.getYear() < 5 ) { // Timestamp year 0 is 1900 - return timestamp.toLocalDateTime().atZone( ZoneId.systemDefault() ); - } - else { - return timestamp.toInstant().atZone( ZoneId.systemDefault() ); - } + return DateTimeUtils.toInstant( timestamp ).atZone( ZoneId.systemDefault() ); } if (value instanceof Date date) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/GregorianEpochBasedDateJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/GregorianEpochBasedDateJdbcType.java new file mode 100644 index 000000000000..ebdf239ddb05 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/GregorianEpochBasedDateJdbcType.java @@ -0,0 +1,111 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.type.descriptor.jdbc; + +import org.hibernate.type.descriptor.DateTimeUtils; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; + +import java.sql.CallableStatement; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.time.Instant; +import java.time.LocalTime; +import java.time.ZoneOffset; +import java.util.Calendar; + +/** + * Special descriptor for {@link Types#DATE DATE} handling where the driver interprets the epoch milliseconds + * as being Gregorian calendar based. java.util.Date uses the Julian calendar for dates before 15th October 1582 which + * leads to different dates for the same epoch when compared to the Gregorian calendar. + * Workaround this problem by converting Julian to Gregorian epoch milliseconds in bind and extract. + */ +public class GregorianEpochBasedDateJdbcType extends DateJdbcType { + public static final GregorianEpochBasedDateJdbcType INSTANCE = new GregorianEpochBasedDateJdbcType(); + + public GregorianEpochBasedDateJdbcType() { + } + + @Override + public ValueBinder getBinder(final JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + final Date date = getBindValue( value, options ); + if ( value instanceof Calendar calendar ) { + st.setDate( index, date, calendar ); + } + else { + st.setDate( index, date ); + } + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final Date date = getBindValue( value, options ); + if ( value instanceof Calendar calendar ) { + st.setDate( name, date, calendar ); + } + else { + st.setDate( name, date ); + } + } + + @Override + public Date getBindValue(X value, WrapperOptions options) { + final Date date = javaType.unwrap( value, Date.class, options ); + if ( value instanceof Calendar ) { + return date; + } + else if ( date.getTime() < DateTimeUtils.GREGORIAN_START_EPOCH_MILLIS ) { + final long epochSecond = + DateTimeUtils.toLocalDate( date ).toEpochSecond( LocalTime.MIN, ZoneOffset.UTC ); + return new java.sql.Date( epochSecond * 1000 ); + } + else { + return date; + } + } + }; + } + + @Override + public ValueExtractor getExtractor(final JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return getExtractValue( rs.getDate( paramIndex ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + return getExtractValue( statement.getDate( index ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + return getExtractValue( statement.getDate( name ), options ); + } + + private X getExtractValue(Date value, WrapperOptions options) { + if ( value != null && value.getTime() < DateTimeUtils.GREGORIAN_START_EPOCH_MILLIS ) { + final Date julianDate = Date.valueOf( + Instant.ofEpochMilli( value.getTime() ).atOffset( ZoneOffset.UTC ).toLocalDate() + ); + return javaType.wrap( julianDate, options ); + } + else { + return javaType.wrap( value, options ); + } + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/GregorianEpochBasedTimestampJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/GregorianEpochBasedTimestampJdbcType.java new file mode 100644 index 000000000000..33455e7d9ace --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/GregorianEpochBasedTimestampJdbcType.java @@ -0,0 +1,122 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.type.descriptor.jdbc; + +import org.hibernate.type.descriptor.DateTimeUtils; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.Calendar; + +/** + * Special descriptor for {@link Types#TIMESTAMP TIMESTAMP} handling where the driver interprets the epoch milliseconds + * as being Gregorian calendar based. java.util.Date uses the Julian calendar for dates before 15th October 1582 which + * leads to different dates for the same epoch when compared to the Gregorian calendar. + * Workaround this problem by converting Julian to Gregorian epoch milliseconds in bind and extract. + */ +public class GregorianEpochBasedTimestampJdbcType extends TimestampJdbcType { + public static final GregorianEpochBasedTimestampJdbcType INSTANCE = new GregorianEpochBasedTimestampJdbcType(); + + public GregorianEpochBasedTimestampJdbcType() { + } + + @Override + public ValueBinder getBinder(final JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + final Timestamp timestamp = getBindValue( value, options ); + if ( value instanceof Calendar calendar ) { + st.setTimestamp( index, timestamp, calendar ); + } + else if ( options.getJdbcTimeZone() != null ) { + st.setTimestamp( index, timestamp, Calendar.getInstance( options.getJdbcTimeZone() ) ); + } + else { + st.setTimestamp( index, timestamp ); + } + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final Timestamp timestamp = getBindValue( value, options ); + if ( value instanceof Calendar calendar ) { + st.setTimestamp( name, timestamp, calendar ); + } + else if ( options.getJdbcTimeZone() != null ) { + st.setTimestamp( name, timestamp, Calendar.getInstance( options.getJdbcTimeZone() ) ); + } + else { + st.setTimestamp( name, timestamp ); + } + } + + @Override + public Timestamp getBindValue(X value, WrapperOptions options) { + final Timestamp timestamp = javaType.unwrap( value, Timestamp.class, options ); + if ( value instanceof Calendar ) { + return timestamp; + } + else if ( timestamp.getTime() < DateTimeUtils.GREGORIAN_START_EPOCH_MILLIS ) { + final long epochSecond = + DateTimeUtils.toLocalDateTime( timestamp ).toEpochSecond( ZoneOffset.UTC ); + return new Timestamp( epochSecond * 1000 ); + } + else { + return timestamp; + } + } + }; + } + + @Override + public ValueExtractor getExtractor(final JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return options.getJdbcTimeZone() != null ? + getExtractValue( rs.getTimestamp( paramIndex, Calendar.getInstance( options.getJdbcTimeZone() ) ), options ) : + getExtractValue( rs.getTimestamp( paramIndex ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + return options.getJdbcTimeZone() != null ? + getExtractValue( statement.getTimestamp( index, Calendar.getInstance( options.getJdbcTimeZone() ) ), options ) : + getExtractValue( statement.getTimestamp( index ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + return options.getJdbcTimeZone() != null ? + getExtractValue( statement.getTimestamp( name, Calendar.getInstance( options.getJdbcTimeZone() ) ), options ) : + getExtractValue( statement.getTimestamp( name ), options ); + } + + private X getExtractValue(Timestamp value, WrapperOptions options) { + if ( value != null && value.getTime() < DateTimeUtils.GREGORIAN_START_EPOCH_MILLIS ) { + final Timestamp julianTimestamp = Timestamp.valueOf( + Instant.ofEpochMilli( value.getTime() ).atOffset( ZoneOffset.UTC ).toLocalDateTime() + ); + return javaType.wrap( julianTimestamp, options ); + } + else { + return javaType.wrap( value, options ); + } + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampUtcAsJdbcTimestampJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampUtcAsJdbcTimestampJdbcType.java index d0871a121e94..2876871196f2 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampUtcAsJdbcTimestampJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/TimestampUtcAsJdbcTimestampJdbcType.java @@ -15,6 +15,7 @@ import java.util.TimeZone; import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.DateTimeUtils; import org.hibernate.type.descriptor.ValueBinder; import org.hibernate.type.descriptor.ValueExtractor; import org.hibernate.type.descriptor.WrapperOptions; @@ -81,14 +82,14 @@ public ValueBinder getBinder(final JavaType javaType) { @Override protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { final Instant instant = javaType.unwrap( value, Instant.class, options ); - st.setTimestamp( index, Timestamp.from( instant ), UTC_CALENDAR ); + st.setTimestamp( index, DateTimeUtils.toTimestamp( instant ), UTC_CALENDAR ); } @Override protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) throws SQLException { final Instant instant = javaType.unwrap( value, Instant.class, options ); - st.setTimestamp( name, Timestamp.from( instant ), UTC_CALENDAR ); + st.setTimestamp( name, DateTimeUtils.toTimestamp( instant ), UTC_CALENDAR ); } }; } @@ -99,19 +100,19 @@ public ValueExtractor getExtractor(final JavaType javaType) { @Override protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { final Timestamp timestamp = rs.getTimestamp( paramIndex, UTC_CALENDAR ); - return javaType.wrap( timestamp == null ? null : timestamp.toInstant(), options ); + return javaType.wrap( timestamp == null ? null : DateTimeUtils.toInstant( timestamp ), options ); } @Override protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { final Timestamp timestamp = statement.getTimestamp( index, UTC_CALENDAR ); - return javaType.wrap( timestamp == null ? null : timestamp.toInstant(), options ); + return javaType.wrap( timestamp == null ? null : DateTimeUtils.toInstant( timestamp ), options ); } @Override protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { final Timestamp timestamp = statement.getTimestamp( name, UTC_CALENDAR ); - return javaType.wrap( timestamp == null ? null : timestamp.toInstant(), options ); + return javaType.wrap( timestamp == null ? null : DateTimeUtils.toInstant( timestamp ), options ); } }; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/BCDateTimeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/BCDateTimeTest.java new file mode 100644 index 000000000000..b2c59cd0153b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/BCDateTimeTest.java @@ -0,0 +1,187 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.type; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.HSQLDialect; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaQuery; +import org.hibernate.query.criteria.JpaRoot; +import org.hibernate.testing.orm.domain.StandardDomainModel; +import org.hibernate.testing.orm.domain.gambit.EntityOfBasics; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.hibernate.type.descriptor.java.CalendarDateJavaType; +import org.hibernate.type.descriptor.java.CalendarJavaType; +import org.hibernate.type.descriptor.java.JdbcDateJavaType; +import org.hibernate.type.descriptor.java.JdbcTimestampJavaType; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DomainModel(standardModels = StandardDomainModel.GAMBIT) +@SessionFactory +@ServiceRegistry(settings = { + @Setting(name = AvailableSettings.TIMEZONE_DEFAULT_STORAGE, value = "NORMALIZE") +}) +@Jira("https://hibernate.atlassian.net/browse/HHH-18589") +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsDatesInBCEra.class) +@SkipForDialect(dialectClass = HSQLDialect.class, reason = "Bug in HSQLDB https://sourceforge.net/p/hsqldb/bugs/1737/") +public class BCDateTimeTest { + + private static final Instant INSTANT = Instant.parse("-0001-01-01T00:00:00.0Z"); + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityOfBasics entity = new EntityOfBasics(); + entity.setId( 1 ); + // Transform the Instant to java.sql.Timestamp/java.sql.Date/Calendar through our JavaType implementations, + // since these will account for conversion between Gregorian and Julian epoch + entity.setTheTimestamp( JdbcTimestampJavaType.INSTANCE.wrap( INSTANT, session ) ); + entity.setTheDate( JdbcDateJavaType.INSTANCE.wrap( INSTANT, session ) ); + entity.setTheTimestampCalendar( CalendarJavaType.INSTANCE.wrap( INSTANT, session ) ); + entity.setTheDateCalendar( CalendarDateJavaType.INSTANCE.wrap( INSTANT, session ) ); + entity.setTheInstant( INSTANT ); + entity.setTheLocalDateTime( LocalDateTime.ofInstant( INSTANT, ZoneId.of( "UTC" ) ) ); + entity.setTheLocalDate( LocalDate.ofInstant( INSTANT, ZoneId.of( "UTC" ) ) ); + entity.setTheOffsetDateTime( OffsetDateTime.ofInstant( INSTANT, ZoneId.of( "UTC" ) ) ); + entity.setTheZonedDateTime( ZonedDateTime.ofInstant( INSTANT, ZoneId.of( "UTC" ) ) ); + session.persist( entity ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Test + public void testRead(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityOfBasics entity = session.find( EntityOfBasics.class, 1 ); + assertEquals( JdbcTimestampJavaType.INSTANCE.wrap( INSTANT, session ), entity.getTheTimestamp() ); + assertEquals( JdbcDateJavaType.INSTANCE.wrap( INSTANT, session ), entity.getTheDate() ); + assertEquals( CalendarJavaType.INSTANCE.wrap( INSTANT, session ), entity.getTheTimestampCalendar() ); + assertEquals( CalendarDateJavaType.INSTANCE.wrap( INSTANT, session ), entity.getTheDateCalendar() ); + assertEquals( INSTANT, entity.getTheInstant() ); + assertEquals( LocalDateTime.ofInstant( INSTANT, ZoneId.of( "UTC" ) ), entity.getTheLocalDateTime() ); + assertEquals( LocalDate.ofInstant( INSTANT, ZoneId.of( "UTC" ) ), entity.getTheLocalDate() ); + assertEquals( OffsetDateTime.ofInstant( INSTANT, ZoneId.of( "UTC" ) ), entity.getTheOffsetDateTime() ); + assertEquals( ZonedDateTime.ofInstant( INSTANT, ZoneId.of( "UTC" ) ), entity.getTheZonedDateTime() ); + }); + } + + @Test + public void testQueryTimestamp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityOfBasics entity = session.find( EntityOfBasics.class, 1 ); + assertExists( session, "theTimestamp", entity.getTheTimestamp(), true ); + assertExists( session, "theTimestamp", entity.getTheTimestamp(), false ); + }); + } + + @Test + public void testQueryDate(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityOfBasics entity = session.find( EntityOfBasics.class, 1 ); + assertExists( session, "theDate", entity.getTheDate(), true ); + assertExists( session, "theDate", entity.getTheDate(), false ); + }); + } + + @Test + public void testQueryCalendarTimestamp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityOfBasics entity = session.find( EntityOfBasics.class, 1 ); + assertExists( session, "theTimestampCalendar", entity.getTheTimestampCalendar(), true ); + assertExists( session, "theTimestampCalendar", entity.getTheTimestampCalendar(), false ); + }); + } + + @Test + public void testQueryCalendarDate(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityOfBasics entity = session.find( EntityOfBasics.class, 1 ); + assertExists( session, "theDateCalendar", entity.getTheDateCalendar(), true ); + assertExists( session, "theDateCalendar", entity.getTheDateCalendar(), false ); + }); + } + + @Test + public void testQueryInstant(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityOfBasics entity = session.find( EntityOfBasics.class, 1 ); + assertExists( session, "theInstant", entity.getTheInstant(), true ); + assertExists( session, "theInstant", entity.getTheInstant(), false ); + }); + } + + @Test + public void testQueryLocalDateTime(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityOfBasics entity = session.find( EntityOfBasics.class, 1 ); + assertExists( session, "theLocalDateTime", entity.getTheLocalDateTime(), true ); + assertExists( session, "theLocalDateTime", entity.getTheLocalDateTime(), false ); + }); + } + + @Test + public void testQueryLocalDate(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityOfBasics entity = session.find( EntityOfBasics.class, 1 ); + assertExists( session, "theLocalDate", entity.getTheLocalDate(), true ); + assertExists( session, "theLocalDate", entity.getTheLocalDate(), false ); + }); + } + + @Test + public void testQueryOffsetDateTime(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityOfBasics entity = session.find( EntityOfBasics.class, 1 ); + assertExists( session, "theOffsetDateTime", entity.getTheOffsetDateTime(), true ); + assertExists( session, "theOffsetDateTime", entity.getTheOffsetDateTime(), false ); + }); + } + + @Test + public void testQueryZonedDateTime(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityOfBasics entity = session.find( EntityOfBasics.class, 1 ); + assertExists( session, "theZonedDateTime", entity.getTheZonedDateTime(), true ); + assertExists( session, "theZonedDateTime", entity.getTheZonedDateTime(), false ); + }); + } + + private void assertExists(SessionImplementor session, String field, Object value, boolean parameter) { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaCriteriaQuery query = cb.createQuery( EntityOfBasics.class ); + final JpaRoot root = query.from( EntityOfBasics.class ); + if ( parameter ) { + query.where( cb.equal( root.get( field ), value ) ); + } + else { + query.where( cb.equal( root.get( field ), cb.literal( value ) ) ); + } + session.createQuery( query ).getSingleResult(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/InstantTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/InstantTest.java index 3ae11ca27fcb..c008e2370e8f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/type/InstantTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/InstantTest.java @@ -29,6 +29,7 @@ import org.hibernate.dialect.SybaseDialect; import org.hibernate.dialect.TimeZoneSupport; +import org.hibernate.type.descriptor.DateTimeUtils; import org.junit.runners.Parameterized; /** @@ -152,7 +153,7 @@ protected Instant getActualPropertyValue(EntityWithInstant entity) { protected void setJdbcValueForNonHibernateWrite(PreparedStatement statement, int parameterIndex) throws SQLException { if ( sessionFactory().getJdbcServices().getDialect().getTimeZoneSupport() == TimeZoneSupport.NATIVE ) { // Oracle and H2 require reading/writing through OffsetDateTime to avoid TZ related miscalculations - statement.setObject( parameterIndex, getExpectedJdbcValueAfterHibernateWrite().toInstant().atOffset( ZoneOffset.UTC ) ); + statement.setObject( parameterIndex, DateTimeUtils.toInstant( getExpectedJdbcValueAfterHibernateWrite() ).atOffset( ZoneOffset.UTC ) ); } else { statement.setTimestamp( @@ -165,14 +166,14 @@ protected void setJdbcValueForNonHibernateWrite(PreparedStatement statement, int @Override protected Timestamp getExpectedJdbcValueAfterHibernateWrite() { - return Timestamp.from( getExpectedPropertyValueAfterHibernateRead() ); + return DateTimeUtils.toTimestamp( getExpectedPropertyValueAfterHibernateRead() ); } @Override protected Object getActualJdbcValue(ResultSet resultSet, int columnIndex) throws SQLException { if ( sessionFactory().getJdbcServices().getDialect().getTimeZoneSupport() == TimeZoneSupport.NATIVE ) { // Oracle and H2 require reading/writing through OffsetDateTime to avoid TZ related miscalculations - return Timestamp.from( resultSet.getObject( columnIndex, OffsetDateTime.class ).toInstant() ); + return DateTimeUtils.toTimestamp( resultSet.getObject( columnIndex, OffsetDateTime.class ).toInstant() ); } else { return resultSet.getTimestamp( columnIndex, Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ) ); diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/gambit/EntityOfBasics.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/gambit/EntityOfBasics.java index e01965a31c31..7d99b2ad8b0c 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/gambit/EntityOfBasics.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/gambit/EntityOfBasics.java @@ -14,6 +14,7 @@ import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZonedDateTime; +import java.util.Calendar; import java.util.Date; import java.util.UUID; @@ -62,6 +63,9 @@ public enum Gender { private Date theDate; private Date theTime; private Date theTimestamp; + private Calendar theDateCalendar; + private Calendar theTimeCalendar; + private Calendar theTimestampCalendar; private Instant theInstant; private Gender gender; private Gender singleCharGender; @@ -228,6 +232,36 @@ public void setTheTimestamp(Date theTimestamp) { this.theTimestamp = theTimestamp; } + @Column(name = "the_date_calendar") + @Temporal( TemporalType.DATE ) + public Calendar getTheDateCalendar() { + return theDateCalendar; + } + + public void setTheDateCalendar(Calendar theDateCalendar) { + this.theDateCalendar = theDateCalendar; + } + + @Column(name = "the_time_calendar") + @Temporal( TemporalType.TIME ) + public Calendar getTheTimeCalendar() { + return theTimeCalendar; + } + + public void setTheTimeCalendar(Calendar theTimeCalendar) { + this.theTimeCalendar = theTimeCalendar; + } + + @Column(name = "the_timestamp_calendar") + @Temporal( TemporalType.TIMESTAMP ) + public Calendar getTheTimestampCalendar() { + return theTimestampCalendar; + } + + public void setTheTimestampCalendar(Calendar theTimestampCalendar) { + this.theTimestampCalendar = theTimestampCalendar; + } + @Column(name = "the_instant") @Temporal( TemporalType.TIMESTAMP ) public Instant getTheInstant() { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 7bb583f3801f..5e891f119b28 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -45,7 +45,10 @@ import org.hibernate.community.dialect.FirebirdDialect; import org.hibernate.community.dialect.GaussDBDialect; import org.hibernate.community.dialect.InformixDialect; +import org.hibernate.community.dialect.IngresDialect; +import org.hibernate.community.dialect.SQLiteDialect; import org.hibernate.community.dialect.TiDBDialect; +import org.hibernate.community.dialect.TimesTenDialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.Dialect; @@ -1172,6 +1175,19 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsDatesInBCEra implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return dialect instanceof H2Dialect + || dialect instanceof HSQLDialect + || dialect instanceof PostgreSQLDialect + || dialect instanceof CockroachDialect + || dialect instanceof OracleDialect + || dialect instanceof IngresDialect + || dialect instanceof SQLiteDialect + || dialect instanceof TimesTenDialect; + } + } + private static SqmFunctionRegistry getSqmFunctionRegistry(Dialect dialect) { SqmFunctionRegistry sqmFunctionRegistry = FUNCTION_REGISTRIES.get( dialect );