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
+ * - 3 (milliseconds)
+ * - 6 (microseconds)
+ * - 9 (nanoseconds)
+ *
+ */
+ 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;
+
+ }
+}