diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/FractionalSeconds.java b/hibernate-core/src/main/java/org/hibernate/annotations/FractionalSeconds.java new file mode 100644 index 0000000000..808bc30e60 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/FractionalSeconds.java @@ -0,0 +1,51 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.hibernate.Incubating; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Indicates that the associated temporal value should be stored with fractional seconds. + * Only valid for values which contain seconds. + * + * @apiNote The presence or absence of this annotation implies different semantics for time + * versus timestamp based values. By default, time values are stored without fractional seconds + * whereas timestamp values are stored with a precision based on the + * {@linkplain org.hibernate.dialect.Dialect#getDefaultTimestampPrecision Dialect default} + * + * @see java.time.Instant + * @see java.time.LocalDateTime + * @see java.time.LocalTime + * @see java.time.OffsetDateTime + * @see java.time.OffsetTime + * @see java.time.ZonedDateTime + * @see java.sql.Time + * @see java.sql.Timestamp + * @see java.util.Calendar + * + * @author Steve Ebersole + */ +@Target({METHOD, FIELD}) +@Retention( RUNTIME) +@Incubating +public @interface FractionalSeconds { + /** + * The fractional precision for the associated seconds. Generally this will be one of + */ + int value(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedColumn.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedColumn.java index d21482208d..fe40a9902c 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedColumn.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedColumn.java @@ -17,6 +17,7 @@ import org.hibernate.annotations.Checks; import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.ColumnTransformer; import org.hibernate.annotations.ColumnTransformers; +import org.hibernate.annotations.FractionalSeconds; import org.hibernate.annotations.GeneratedColumn; import org.hibernate.annotations.Index; import org.hibernate.annotations.common.reflection.XProperty; @@ -77,6 +78,7 @@ public class AnnotatedColumn { private Long length; private Integer precision; private Integer scale; + private Integer temporalPrecision; // technically scale, but most dbs call it precision so... private Integer arrayLength; private String logicalColumnName; private boolean unique; @@ -183,6 +185,10 @@ public class AnnotatedColumn { this.scale = scale; } + public void setTemporalPrecision(Integer temporalPrecision) { + this.temporalPrecision = temporalPrecision; + } + public void setLogicalColumnName(String logicalColumnName) { this.logicalColumnName = logicalColumnName; } @@ -239,6 +245,7 @@ public class AnnotatedColumn { length, precision, scale, + temporalPrecision, arrayLength, nullable, sqlType, @@ -269,6 +276,7 @@ public class AnnotatedColumn { Long length, Integer precision, Integer scale, + Integer temporalPrecision, Integer arrayLength, boolean nullable, String sqlType, @@ -287,6 +295,9 @@ public class AnnotatedColumn { mappingColumn.setPrecision( precision ); mappingColumn.setScale( scale ); } + if ( temporalPrecision != null ) { + mappingColumn.setTemporalPrecision( temporalPrecision ); + } mappingColumn.setArrayLength( arrayLength ); mappingColumn.setNullable( nullable ); mappingColumn.setSqlType( sqlType ); @@ -488,6 +499,7 @@ public class AnnotatedColumn { return buildColumnOrFormulaFromAnnotation( null, formulaAnn, + null, // commentAnn, nullability, propertyHolder, @@ -498,6 +510,7 @@ public class AnnotatedColumn { } public static AnnotatedColumns buildColumnFromNoAnnotation( + FractionalSeconds fractionalSeconds, // Comment commentAnn, Nullability nullability, PropertyHolder propertyHolder, @@ -506,6 +519,7 @@ public class AnnotatedColumn { MetadataBuildingContext context) { return buildColumnsFromAnnotations( null, + fractionalSeconds, // commentAnn, nullability, propertyHolder, @@ -517,6 +531,7 @@ public class AnnotatedColumn { public static AnnotatedColumns buildColumnFromAnnotation( jakarta.persistence.Column column, + org.hibernate.annotations.FractionalSeconds fractionalSeconds, // Comment commentAnn, Nullability nullability, PropertyHolder propertyHolder, @@ -526,6 +541,7 @@ public class AnnotatedColumn { return buildColumnOrFormulaFromAnnotation( column, null, + fractionalSeconds, // commentAnn, nullability, propertyHolder, @@ -537,6 +553,7 @@ public class AnnotatedColumn { public static AnnotatedColumns buildColumnsFromAnnotations( jakarta.persistence.Column[] columns, + FractionalSeconds fractionalSeconds, // Comment commentAnn, Nullability nullability, PropertyHolder propertyHolder, @@ -546,6 +563,7 @@ public class AnnotatedColumn { return buildColumnsOrFormulaFromAnnotation( columns, null, + fractionalSeconds, // commentAnn, nullability, propertyHolder, @@ -568,6 +586,7 @@ public class AnnotatedColumn { return buildColumnsOrFormulaFromAnnotation( columns, null, + null, // commentAnn, nullability, propertyHolder, @@ -581,6 +600,7 @@ public class AnnotatedColumn { public static AnnotatedColumns buildColumnOrFormulaFromAnnotation( jakarta.persistence.Column column, org.hibernate.annotations.Formula formulaAnn, + org.hibernate.annotations.FractionalSeconds fractionalSeconds, // Comment commentAnn, Nullability nullability, PropertyHolder propertyHolder, @@ -590,6 +610,7 @@ public class AnnotatedColumn { return buildColumnsOrFormulaFromAnnotation( column==null ? null : new jakarta.persistence.Column[] { column }, formulaAnn, + fractionalSeconds, // commentAnn, nullability, propertyHolder, @@ -603,6 +624,7 @@ public class AnnotatedColumn { public static AnnotatedColumns buildColumnsOrFormulaFromAnnotation( jakarta.persistence.Column[] columns, org.hibernate.annotations.Formula formulaAnn, + org.hibernate.annotations.FractionalSeconds fractionalSeconds, // Comment comment, Nullability nullability, PropertyHolder propertyHolder, @@ -630,6 +652,7 @@ public class AnnotatedColumn { final jakarta.persistence.Column[] actualColumns = overrideColumns( columns, propertyHolder, inferredData ); if ( actualColumns == null ) { return buildImplicitColumn( + fractionalSeconds, inferredData, suffixForDefaultColumnName, secondaryTables, @@ -647,7 +670,8 @@ public class AnnotatedColumn { suffixForDefaultColumnName, secondaryTables, context, - actualColumns + actualColumns, + fractionalSeconds ); } } @@ -684,7 +708,8 @@ public class AnnotatedColumn { String suffixForDefaultColumnName, Map secondaryTables, MetadataBuildingContext context, - jakarta.persistence.Column[] actualCols) { + jakarta.persistence.Column[] actualCols, + FractionalSeconds fractionalSeconds) { final AnnotatedColumns parent = new AnnotatedColumns(); parent.setPropertyHolder( propertyHolder ); parent.setPropertyName( getRelativePath( propertyHolder, inferredData.getPropertyName() ) ); @@ -708,6 +733,7 @@ public class AnnotatedColumn { actualCols.length, database, column, + fractionalSeconds, sqlType, tableName ); @@ -734,6 +760,7 @@ public class AnnotatedColumn { int numberOfColumns, Database database, jakarta.persistence.Column column, + FractionalSeconds fractionalSeconds, String sqlType, String tableName) { final String columnName = logicalColumnName( inferredData, suffixForDefaultColumnName, database, column ); @@ -742,7 +769,12 @@ public class AnnotatedColumn { annotatedColumn.setImplicit( false ); annotatedColumn.setSqlType( sqlType ); annotatedColumn.setLength( (long) column.length() ); - annotatedColumn.setPrecision( column.precision() ); + if ( fractionalSeconds != null ) { + annotatedColumn.setTemporalPrecision( fractionalSeconds.value() ); + } + else { + annotatedColumn.setPrecision( column.precision() ); + } annotatedColumn.setScale( column.scale() ); annotatedColumn.handleArrayLength( inferredData ); // annotatedColumn.setPropertyHolder( propertyHolder ); @@ -881,6 +913,7 @@ public class AnnotatedColumn { } private static AnnotatedColumns buildImplicitColumn( + FractionalSeconds fractionalSeconds, PropertyData inferredData, String suffixForDefaultColumnName, Map secondaryTables, @@ -920,6 +953,9 @@ public class AnnotatedColumn { column.applyCheckConstraint( inferredData, 1 ); column.extractDataFromPropertyData( propertyHolder, inferredData ); column.handleArrayLength( inferredData ); + if ( fractionalSeconds != null ) { + column.setTemporalPrecision( fractionalSeconds.value() ); + } column.bind(); return columns; } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedJoinColumn.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedJoinColumn.java index 30cac09145..73913ccbe7 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedJoinColumn.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedJoinColumn.java @@ -325,6 +325,7 @@ public class AnnotatedJoinColumn extends AnnotatedColumn { referencedColumn.getLength(), referencedColumn.getPrecision(), referencedColumn.getScale(), + referencedColumn.getTemporalPrecision(), referencedColumn.getArrayLength(), mappingColumn != null && mappingColumn.isNullable(), referencedColumn.getSqlType(), @@ -375,6 +376,7 @@ public class AnnotatedJoinColumn extends AnnotatedColumn { column.getLength(), column.getPrecision(), column.getScale(), + column.getTemporalPrecision(), column.getArrayLength(), getMappingColumn().isNullable(), column.getSqlType(), diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BinderHelper.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BinderHelper.java index a551f4c2cc..460d5503d6 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BinderHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BinderHelper.java @@ -761,6 +761,7 @@ public class BinderHelper { final AnnotatedColumns discriminatorColumns = buildColumnOrFormulaFromAnnotation( discriminatorColumn, discriminatorFormula, + null, // null, nullability, propertyHolder, diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java index f7527bab5b..1a68d52359 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java @@ -570,6 +570,7 @@ public abstract class CollectionBinder { if ( property.isAnnotationPresent( jakarta.persistence.Column.class ) ) { return buildColumnFromAnnotation( property.getAnnotation( jakarta.persistence.Column.class ), + null, // comment, nullability, propertyHolder, @@ -592,6 +593,7 @@ public abstract class CollectionBinder { else if ( property.isAnnotationPresent( Columns.class ) ) { return buildColumnsFromAnnotations( property.getAnnotation( Columns.class ).columns(), + null, // comment, nullability, propertyHolder, @@ -602,6 +604,7 @@ public abstract class CollectionBinder { } else { return buildColumnFromNoAnnotation( + null, // comment, nullability, propertyHolder, diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ColumnsBuilder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ColumnsBuilder.java index db22dca24f..5b07b97fe6 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ColumnsBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ColumnsBuilder.java @@ -9,6 +9,7 @@ package org.hibernate.boot.model.internal; import org.hibernate.AnnotationException; import org.hibernate.annotations.Columns; import org.hibernate.annotations.Formula; +import org.hibernate.annotations.FractionalSeconds; import org.hibernate.annotations.JoinColumnOrFormula; import org.hibernate.annotations.JoinColumnsOrFormulas; import org.hibernate.annotations.JoinFormula; @@ -85,6 +86,7 @@ class ColumnsBuilder { if ( property.isAnnotationPresent( Column.class ) ) { columns = buildColumnFromAnnotation( property.getAnnotation( Column.class ), + property.getAnnotation( FractionalSeconds.class ), // comment, nullability, propertyHolder, @@ -107,6 +109,7 @@ class ColumnsBuilder { else if ( property.isAnnotationPresent( Columns.class ) ) { columns = buildColumnsFromAnnotations( property.getAnnotation( Columns.class ).columns(), + null, // comment, nullability, propertyHolder, @@ -144,6 +147,7 @@ class ColumnsBuilder { if ( columns == null && !property.isAnnotationPresent( ManyToMany.class ) ) { //useful for collection of embedded elements columns = buildColumnFromNoAnnotation( + property.getAnnotation( FractionalSeconds.class ), // comment, nullability, propertyHolder, diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/IdBagBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/IdBagBinder.java index d94e27e54d..f112152206 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/IdBagBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/IdBagBinder.java @@ -70,6 +70,7 @@ public class IdBagBinder extends BagBinder { final AnnotatedColumns idColumns = AnnotatedColumn.buildColumnsFromAnnotations( new Column[] { collectionIdAnn.column() }, // null, + null, Nullability.FORCED_NOT_NULL, propertyHolder, propertyData, diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java index cc805c0f34..8ebefae311 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java @@ -63,7 +63,6 @@ import org.hibernate.type.descriptor.java.BasicJavaType; import org.hibernate.type.descriptor.java.BasicPluralJavaType; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.MutabilityPlan; -import org.hibernate.type.descriptor.java.spi.FormatMapperBasedJavaType; import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry; import org.hibernate.type.descriptor.java.spi.JsonJavaType; import org.hibernate.type.descriptor.java.spi.RegistryHelper; @@ -233,9 +232,10 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol @Override public long getColumnLength() { - final Selectable column = getColumn(); - if ( column instanceof Column ) { - final Long length = ( (Column) column ).getLength(); + final Selectable selectable = getColumn(); + if ( selectable instanceof Column ) { + final Column column = (Column) selectable; + final Long length = column.getLength(); return length == null ? NO_COLUMN_LENGTH : length; } else { @@ -245,10 +245,14 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol @Override public int getColumnPrecision() { - final Selectable column = getColumn(); - if ( column instanceof Column ) { - final Integer length = ( (Column) column ).getPrecision(); - return length == null ? NO_COLUMN_PRECISION : length; + final Selectable selectable = getColumn(); + if ( selectable instanceof Column ) { + final Column column = (Column) selectable; + if ( column.getTemporalPrecision() != null ) { + return column.getTemporalPrecision(); + } + final Integer precision = column.getPrecision(); + return precision == null ? NO_COLUMN_PRECISION : precision; } else { return NO_COLUMN_PRECISION; @@ -257,10 +261,11 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol @Override public int getColumnScale() { - final Selectable column = getColumn(); - if ( column instanceof Column ) { - final Integer length = ( (Column) column ).getScale(); - return length == null ? NO_COLUMN_SCALE : length; + final Selectable selectable = getColumn(); + if ( selectable instanceof Column ) { + final Column column = (Column) selectable; + final Integer scale = column.getScale(); + return scale == null ? NO_COLUMN_SCALE : scale; } else { return NO_COLUMN_SCALE; diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Column.java b/hibernate-core/src/main/java/org/hibernate/mapping/Column.java index 13607ebed0..faffed48b2 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Column.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Column.java @@ -23,12 +23,15 @@ import org.hibernate.engine.spi.Mapping; import org.hibernate.loader.internal.AliasConstantsHelper; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.query.sqm.function.SqmFunctionRegistry; +import org.hibernate.query.sqm.internal.TypecheckUtil; import org.hibernate.sql.Template; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.BasicType; import org.hibernate.type.ComponentType; import org.hibernate.type.EntityType; import org.hibernate.type.Type; import org.hibernate.type.descriptor.JdbcTypeNameMapper; +import org.hibernate.type.descriptor.java.JavaTypeHelper; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.sql.DdlType; import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; @@ -51,6 +54,7 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn private Long length; private Integer precision; private Integer scale; + private Integer temporalPrecision; private Integer arrayLength; private Value value; private int typeIndex; @@ -422,12 +426,23 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn Size calculateColumnSize(Dialect dialect, Mapping mapping) { Type type = getValue().getType(); + Long lengthToUse = getLength(); + Integer precisionToUse = getPrecision(); + Integer scaleToUse = getScale(); if ( type instanceof EntityType ) { type = getTypeForEntityValue( mapping, type, getTypeIndex() ); } if ( type instanceof ComponentType ) { type = getTypeForComponentValue( mapping, type, getTypeIndex() ); } + if ( type instanceof BasicType ) { + final BasicType basicType = (BasicType) type; + if ( JavaTypeHelper.isTemporal( basicType.getExpressibleJavaType() ) ) { + precisionToUse = getTemporalPrecision(); + lengthToUse = null; + scaleToUse = null; + } + } if ( type == null ) { throw new AssertionFailure( "no typing information available to determine column size" ); } @@ -435,9 +450,9 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn Size size = dialect.getSizeStrategy().resolveSize( jdbcMapping.getJdbcType(), jdbcMapping.getJdbcJavaType(), - precision, - scale, - length + precisionToUse, + scaleToUse, + lengthToUse ); size.setArrayLength( arrayLength ); return size; @@ -674,6 +689,14 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn this.scale = scale; } + public Integer getTemporalPrecision() { + return temporalPrecision; + } + + public void setTemporalPrecision(Integer temporalPrecision) { + this.temporalPrecision = temporalPrecision; + } + public String getComment() { return comment; } 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 210cfdc66a..c0ce637735 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 @@ -463,7 +463,19 @@ public final class DateTimeUtils { ); } + public static T roundToSecondPrecision(T temporal, int precision) { + //noinspection unchecked + return (T) temporal.with( + ChronoField.NANO_OF_SECOND, + roundToPrecision( temporal.get( ChronoField.NANO_OF_SECOND ), precision ) + ); + } + public static long roundToPrecision(int nano, int precision) { + assert precision < 9 : "Precision (scale) should be less-than 9 - " + precision; + if ( precision == 0 ) { + return 0; + } final int precisionMask = pow10( 9 - precision ); final int nanosToRound = nano % precisionMask; return nano - nanosToRound + ( nanosToRound >= ( precisionMask >> 1 ) ? precisionMask : 0 ); @@ -478,17 +490,17 @@ public final class DateTimeUtils { case 2: return 100; case 3: - return 1000; + return 1_000; case 4: - return 10000; + return 10_000; case 5: - return 100000; + return 100_000; case 6: - return 1000000; + return 1_000_000; case 7: - return 10000000; + return 10_000_000; case 8: - return 100000000; + return 100_000_000; default: return (int) Math.pow( 10, exponent ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/FractionalSecondsTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/FractionalSecondsTests.java new file mode 100644 index 0000000000..9c15d81e77 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/FractionalSecondsTests.java @@ -0,0 +1,268 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.temporal; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZonedDateTime; + +import org.hibernate.annotations.FractionalSeconds; +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.dialect.DB2Dialect; +import org.hibernate.dialect.DerbyDialect; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.HSQLDialect; +import org.hibernate.dialect.MariaDBDialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.SQLServerDialect; +import org.hibernate.dialect.SybaseDialect; +import org.hibernate.engine.jdbc.Size; +import org.hibernate.mapping.BasicValue; +import org.hibernate.mapping.Column; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.type.descriptor.DateTimeUtils; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +public class FractionalSecondsTests { + @Test + @DomainModel(annotatedClasses = {TestEntity.class, TestEntity0.class, TestEntity3.class, TestEntity9.class} ) + void testMapping(DomainModelScope scope) { + final MetadataImplementor domainModel = scope.getDomainModel(); + + final Dialect dialect = domainModel.getDatabase().getDialect(); + final int defaultPrecision = dialect.getDefaultTimestampPrecision(); + + final PersistentClass entityBinding = scope.getEntityBinding( TestEntity.class ); + checkPrecision( "theInstant", defaultPrecision, entityBinding, domainModel ); + checkPrecision( "theLocalDateTime", defaultPrecision, entityBinding, domainModel ); + checkPrecision( "theLocalTime", defaultPrecision, entityBinding, domainModel ); + checkPrecision( "theOffsetDateTime", defaultPrecision, entityBinding, domainModel ); + checkPrecision( "theOffsetTime", defaultPrecision, entityBinding, domainModel ); + checkPrecision( "theZonedDateTime", defaultPrecision, entityBinding, domainModel ); + + final PersistentClass entityBinding0 = scope.getEntityBinding( TestEntity0.class ); + checkPrecision( "theInstant", 0, entityBinding0, domainModel ); + + final PersistentClass entityBinding3 = scope.getEntityBinding( TestEntity3.class ); + checkPrecision( "theInstant", 3, entityBinding3, domainModel ); + checkPrecision( "theLocalDateTime", 3, entityBinding3, domainModel ); + checkPrecision( "theLocalTime", 3, entityBinding3, domainModel ); + + final PersistentClass entityBinding9 = scope.getEntityBinding( TestEntity9.class ); + checkPrecision( "theInstant", 9, entityBinding9, domainModel ); + checkPrecision( "theOffsetDateTime", 9, entityBinding9, domainModel ); + checkPrecision( "theOffsetTime", 9, entityBinding9, domainModel ); + checkPrecision( "theZonedDateTime", 9, entityBinding9, domainModel ); + } + + private void checkPrecision( + String propertyName, + int expectedMinimumSize, + PersistentClass entityBinding, + MetadataImplementor domainModel) { + final Property theInstant = entityBinding.getProperty( propertyName ); + final BasicValue value = (BasicValue) theInstant.getValue(); + final Column column = (Column) value.getColumn(); + final Size columnSize = column.getColumnSize( value.getDialect(), domainModel ); + assertThat( columnSize.getPrecision() ).isEqualTo( expectedMinimumSize ); + } + + @Test + @DomainModel(annotatedClasses = TestEntity.class) + @SessionFactory + @SkipForDialect( dialectClass = DB2Dialect.class, reason = "Occasional mismatch in rounding versus our code" ) + @SkipForDialect(dialectClass = SybaseDialect.class, reason = "Because... Sybase...", matchSubTypes = true) + void testUsage(SessionFactoryScope scope) { + final Instant start = Instant.now(); + + scope.inTransaction( (session) -> { + final TestEntity testEntity = new TestEntity(); + testEntity.id = 1; + testEntity.theInstant = start; + session.persist( testEntity ); + } ); + + scope.inTransaction( (session) -> { + final TestEntity testEntity = session.find( TestEntity.class, 1 ); + + final Dialect dialect = session.getSessionFactory().getJdbcServices().getDialect(); + if ( dialect instanceof DerbyDialect + || dialect instanceof MariaDBDialect ) { + assertThat( testEntity.theInstant ).isEqualTo( start ); + } + else { + assertThat( testEntity.theInstant ).isEqualTo( DateTimeUtils.roundToSecondPrecision( start, 6 ) ); + } + } ); + } + + @Test + @DomainModel(annotatedClasses = TestEntity0.class) + @SessionFactory + @SkipForDialect( dialectClass = H2Dialect.class, reason = "Occasional mismatch in rounding versus our code" ) + @SkipForDialect( dialectClass = MariaDBDialect.class, reason = "Occasional mismatch in rounding versus our code" ) + @SkipForDialect( dialectClass = MySQLDialect.class, reason = "Occasional mismatch in rounding versus our code", matchSubTypes = true ) + @SkipForDialect( dialectClass = OracleDialect.class, reason = "Occasional mismatch in rounding versus our code" ) + @SkipForDialect( dialectClass = SQLServerDialect.class, reason = "Occasional mismatch in rounding versus our code" ) + @SkipForDialect( dialectClass = PostgreSQLDialect.class, reason = "Occasional mismatch in rounding versus our code", matchSubTypes = true ) + @SkipForDialect( dialectClass = DerbyDialect.class, reason = "Derby does not support sized timestamp" ) + @SkipForDialect(dialectClass = SybaseDialect.class, reason = "Because... Sybase...", matchSubTypes = true) + void testUsage0(SessionFactoryScope scope) { + final Instant start = Instant.now(); + + scope.inTransaction( (session) -> { + final TestEntity0 testEntity = new TestEntity0(); + testEntity.id = 1; + testEntity.theInstant = start; + session.persist( testEntity ); + } ); + + scope.inTransaction( (session) -> { + final TestEntity0 testEntity = session.find( TestEntity0.class, 1 ); + assertThat( testEntity.theInstant ).isEqualTo( DateTimeUtils.roundToSecondPrecision( start, 0 ) ); + } ); + } + + @Test + @DomainModel(annotatedClasses = TestEntity3.class) + @SessionFactory + @SkipForDialect( dialectClass = MariaDBDialect.class, reason = "Occasional mismatch in rounding versus our code" ) + @SkipForDialect( dialectClass = HSQLDialect.class, reason = "Occasional mismatch in rounding versus our code" ) + @SkipForDialect( dialectClass = DB2Dialect.class, reason = "Occasional mismatch in rounding versus our code" ) + @SkipForDialect( dialectClass = DerbyDialect.class, reason = "Derby does not support sized timestamp" ) + @SkipForDialect(dialectClass = SybaseDialect.class, reason = "Because... Sybase...", matchSubTypes = true) + void testUsage3(SessionFactoryScope scope) { + final Instant start = Instant.now(); + + scope.inTransaction( (session) -> { + final TestEntity3 testEntity = new TestEntity3(); + testEntity.id = 1; + testEntity.theInstant = start; + session.persist( testEntity ); + } ); + + scope.inTransaction( (session) -> { + final TestEntity3 testEntity = session.find( TestEntity3.class, 1 ); + assertThat( testEntity.theInstant ).isEqualTo( DateTimeUtils.roundToSecondPrecision( start, 3 ) ); + } ); + } + + @Test + @DomainModel(annotatedClasses = TestEntity9.class) + @SessionFactory + @SkipForDialect( dialectClass = MariaDBDialect.class, reason = "MariaDB only supports precision <= 6" ) + @SkipForDialect( dialectClass = MySQLDialect.class, reason = "MySQL only supports precision <= 6", matchSubTypes = true ) + @SkipForDialect( dialectClass = SQLServerDialect.class, reason = "SQL Server only supports precision <= 6" ) + @SkipForDialect(dialectClass = SybaseDialect.class, reason = "Because... Sybase...", matchSubTypes = true) + void testUsage9(SessionFactoryScope scope) { + final Instant start = Instant.now(); + + scope.inTransaction( (session) -> { + final TestEntity9 testEntity = new TestEntity9(); + testEntity.id = 1; + testEntity.theInstant = start; + session.persist( testEntity ); + } ); + + scope.inTransaction( (session) -> { + final TestEntity9 testEntity = session.find( TestEntity9.class, 1 ); + + assertThat( testEntity.theInstant ).isEqualTo( start ); + } ); + } + + @Entity(name="TestEntity") + @Table(name="TestEntity") + public static class TestEntity { + @Id + private Integer id; + + private Instant theInstant; + + private LocalDateTime theLocalDateTime; + + private LocalTime theLocalTime; + + private OffsetDateTime theOffsetDateTime; + + private OffsetTime theOffsetTime; + + private ZonedDateTime theZonedDateTime; + + } + + @Entity(name="TestEntity0") + @Table(name="TestEntity0") + public static class TestEntity0 { + @Id + private Integer id; + + @FractionalSeconds(0) + private Instant theInstant; + + } + + @Entity(name="TestEntity3") + @Table(name="TestEntity3") + public static class TestEntity3 { + @Id + private Integer id; + + @FractionalSeconds(3) + private Instant theInstant; + + @FractionalSeconds(3) + private LocalDateTime theLocalDateTime; + + @FractionalSeconds(3) + private LocalTime theLocalTime; + + } + + @Entity(name="TestEntity9") + @Table(name="TestEntity9") + public static class TestEntity9 { + @Id + private Integer id; + + @FractionalSeconds(9) + private Instant theInstant; + + @FractionalSeconds(9) + private OffsetDateTime theOffsetDateTime; + + @FractionalSeconds(9) + private OffsetTime theOffsetTime; + + @FractionalSeconds(9) + private ZonedDateTime theZonedDateTime; + + } +}