diff --git a/hibernate-dialect/CHANGELOG.md b/hibernate-dialect/CHANGELOG.md index 420dd90..33f1184 100644 --- a/hibernate-dialect/CHANGELOG.md +++ b/hibernate-dialect/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.0 ## + +- Fixed: data time type converters + ## 0.9.5 ## - Added query hint for view index for "select * from ... where" queries diff --git a/hibernate-dialect/pom.xml b/hibernate-dialect/pom.xml index 7f470f6..c3495e8 100644 --- a/hibernate-dialect/pom.xml +++ b/hibernate-dialect/pom.xml @@ -6,7 +6,7 @@ tech.ydb.dialects hibernate-ydb-dialect - 0.9.5 + 1.0.0 jar @@ -42,7 +42,7 @@ 2.17.2 2.2.6 - 2.1.5 + 2.2.4 diff --git a/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/YdbDialect.java b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/YdbDialect.java index ab13edb..4e63f14 100644 --- a/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/YdbDialect.java +++ b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/YdbDialect.java @@ -43,20 +43,27 @@ import static org.hibernate.type.SqlTypes.NVARCHAR; import static org.hibernate.type.SqlTypes.REAL; import static org.hibernate.type.SqlTypes.SMALLINT; -import static org.hibernate.type.SqlTypes.TIME; import static org.hibernate.type.SqlTypes.TIMESTAMP; +import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.TINYINT; import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; +import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; import tech.ydb.hibernate.dialect.exporter.EmptyExporter; import tech.ydb.hibernate.dialect.exporter.YdbIndexExporter; import tech.ydb.hibernate.dialect.hint.IndexQueryHintHandler; import tech.ydb.hibernate.dialect.translator.YdbSqlAstTranslatorFactory; +import tech.ydb.hibernate.dialect.types.InstantJavaType; +import tech.ydb.hibernate.dialect.types.InstantJdbcType; +import tech.ydb.hibernate.dialect.types.LocalDateJavaType; +import tech.ydb.hibernate.dialect.types.LocalDateJdbcType; import tech.ydb.hibernate.dialect.types.LocalDateTimeJavaType; import tech.ydb.hibernate.dialect.types.LocalDateTimeJdbcType; +import static tech.ydb.hibernate.dialect.types.LocalDateTimeJdbcType.JDBC_TYPE_DATETIME_CODE; /** * @author Kirill Kurdyukov @@ -82,9 +89,9 @@ protected String columnType(int sqlTypeCode) { case DOUBLE -> "Double"; case NUMERIC, DECIMAL -> "Decimal (22,9)"; // Fixed case DATE -> "Date"; - case TIME -> "Datetime"; + case JDBC_TYPE_DATETIME_CODE -> "Datetime"; case TIME_WITH_TIMEZONE -> "TzDateTime"; - case TIMESTAMP -> "Timestamp"; + case TIMESTAMP, TIMESTAMP_UTC -> "Timestamp"; case TIMESTAMP_WITH_TIMEZONE -> "TzTimestamp"; case CHAR, VARCHAR, CLOB, NCHAR, NVARCHAR, NCLOB, LONG32VARCHAR, LONG32NVARCHAR, LONGVARCHAR, LONGNVARCHAR -> "Text"; @@ -100,6 +107,19 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry typeContributions.contributeJavaType(LocalDateTimeJavaType.INSTANCE); typeContributions.contributeJdbcType(LocalDateTimeJdbcType.INSTANCE); + typeContributions.contributeJavaType(LocalDateJavaType.INSTANCE); + typeContributions.contributeJdbcType(LocalDateJdbcType.INSTANCE); + typeContributions.contributeJavaType(InstantJavaType.INSTANCE); + typeContributions.contributeJdbcType(InstantJdbcType.INSTANCE); + } + + @Override + protected void registerColumnTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.registerColumnTypes(typeContributions, serviceRegistry); + + final DdlTypeRegistry ddlTypeRegistry = typeContributions.getTypeConfiguration().getDdlTypeRegistry(); + + ddlTypeRegistry.addDescriptor(new DdlTypeImpl(JDBC_TYPE_DATETIME_CODE, "Datetime", "Datetime", this)); } @Override diff --git a/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/InstantJavaType.java b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/InstantJavaType.java new file mode 100644 index 0000000..b7108fc --- /dev/null +++ b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/InstantJavaType.java @@ -0,0 +1,17 @@ +package tech.ydb.hibernate.dialect.types; + +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; + +/** + * @author Kirill Kurdyukov + */ +public class InstantJavaType extends org.hibernate.type.descriptor.java.InstantJavaType { + + public static final InstantJavaType INSTANCE = new InstantJavaType(); + + @Override + public JdbcType getRecommendedJdbcType(JdbcTypeIndicators context) { + return InstantJdbcType.INSTANCE; + } +} diff --git a/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/InstantJdbcType.java b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/InstantJdbcType.java new file mode 100644 index 0000000..0ce6eb0 --- /dev/null +++ b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/InstantJdbcType.java @@ -0,0 +1,70 @@ +package tech.ydb.hibernate.dialect.types; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.time.Instant; +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 org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.TimestampUtcAsJdbcTimestampJdbcType; + +/** + * @author Kirill Kurdyukov + */ +public class InstantJdbcType extends TimestampUtcAsJdbcTimestampJdbcType { + + public static final InstantJdbcType INSTANCE = new InstantJdbcType(); + + @Override + public ValueBinder getBinder(final JavaType javaType) { + return new BasicBinder<>(javaType, this) { + @Override + protected void doBind( + PreparedStatement st, + X value, + int index, + WrapperOptions wrapperOptions) throws SQLException { + final Instant instant = javaType.unwrap(value, Instant.class, wrapperOptions); + + st.setObject(index, instant, Types.TIMESTAMP); + } + + @Override + protected void doBind( + CallableStatement st, + X value, + String name, + WrapperOptions wrapperOptions) + throws SQLException { + final Instant instant = javaType.unwrap(value, Instant.class, wrapperOptions); + + st.setObject(name, instant, Types.TIMESTAMP); + } + }; + } + + @Override + public ValueExtractor getExtractor(final JavaType javaType) { + return new ValueExtractor<>() { + @Override + public X extract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return javaType.wrap(rs.getObject(paramIndex), options); + } + + @Override + public X extract(CallableStatement statement, int paramIndex, WrapperOptions options) throws SQLException { + return javaType.wrap(statement.getObject(paramIndex), options); + } + + @Override + public X extract(CallableStatement statement, String paramName, WrapperOptions options) throws SQLException { + return javaType.wrap(statement.getObject(paramName), options); + } + }; + } +} diff --git a/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/LocalDateJavaType.java b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/LocalDateJavaType.java new file mode 100644 index 0000000..55cfba1 --- /dev/null +++ b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/LocalDateJavaType.java @@ -0,0 +1,17 @@ +package tech.ydb.hibernate.dialect.types; + +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; + +/** + * @author Kirill Kurdyukov + */ +public class LocalDateJavaType extends org.hibernate.type.descriptor.java.LocalDateJavaType { + + public static final LocalDateJavaType INSTANCE = new LocalDateJavaType(); + + @Override + public JdbcType getRecommendedJdbcType(JdbcTypeIndicators context) { + return LocalDateJdbcType.INSTANCE; + } +} diff --git a/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/LocalDateJdbcType.java b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/LocalDateJdbcType.java new file mode 100644 index 0000000..02f4fb3 --- /dev/null +++ b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/LocalDateJdbcType.java @@ -0,0 +1,74 @@ +package tech.ydb.hibernate.dialect.types; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.time.LocalDate; +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 org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.DateJdbcType; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * @author Kirill Kurdyukov + */ +public class LocalDateJdbcType extends DateJdbcType { + + public static final LocalDateJdbcType INSTANCE = new LocalDateJdbcType(); + + @Override + public Class getPreferredJavaTypeClass(WrapperOptions options) { + return LocalDate.class; + } + + @Override + public JavaType getJdbcRecommendedJavaTypeMapping(Integer length, Integer scale, + TypeConfiguration typeConfiguration) { + return typeConfiguration.getJavaTypeRegistry().getDescriptor(LocalDate.class); + } + + @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 LocalDate date = javaType.unwrap(value, LocalDate.class, options); + + st.setObject(index, date, Types.DATE); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final LocalDate date = javaType.unwrap(value, LocalDate.class, options); + + st.setObject(name, date, Types.DATE); + } + }; + } + + @Override + public ValueExtractor getExtractor(final JavaType javaType) { + return new ValueExtractor<>() { + @Override + public X extract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return javaType.wrap(rs.getObject(paramIndex), options); + } + + @Override + public X extract(CallableStatement statement, int paramIndex, WrapperOptions options) throws SQLException { + return javaType.wrap(statement.getObject(paramIndex), options); + } + + @Override + public X extract(CallableStatement statement, String paramName, WrapperOptions options) throws SQLException { + return javaType.wrap(statement.getObject(paramName), options); + } + }; + } +} diff --git a/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/LocalDateTimeJavaType.java b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/LocalDateTimeJavaType.java index 60b3d22..c9689ca 100644 --- a/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/LocalDateTimeJavaType.java +++ b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/LocalDateTimeJavaType.java @@ -1,11 +1,7 @@ package tech.ydb.hibernate.dialect.types; -import jakarta.persistence.TemporalType; -import java.sql.Types; -import org.hibernate.type.descriptor.java.TemporalJavaType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; -import org.hibernate.type.spi.TypeConfiguration; /** * @author Kirill Kurdyukov @@ -14,25 +10,8 @@ public class LocalDateTimeJavaType extends org.hibernate.type.descriptor.java.Lo public static final LocalDateTimeJavaType INSTANCE = new LocalDateTimeJavaType(); - @Override - public TemporalType getPrecision() { - return TemporalType.TIME; - } - @Override public JdbcType getRecommendedJdbcType(JdbcTypeIndicators context) { - return context.getJdbcType(Types.TIME); - } - - @Override - @SuppressWarnings("unchecked") - protected TemporalJavaType forTimePrecision(TypeConfiguration typeConfiguration) { - return (TemporalJavaType) this; - } - - protected TemporalJavaType forTimestampPrecision(TypeConfiguration typeConfiguration) { - throw new UnsupportedOperationException( - this + " as `jakarta.persistence.TemporalType.TIMESTAMP` not supported" - ); + return LocalDateTimeJdbcType.INSTANCE; } } diff --git a/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/LocalDateTimeJdbcType.java b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/LocalDateTimeJdbcType.java index af2fd23..8153320 100644 --- a/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/LocalDateTimeJdbcType.java +++ b/hibernate-dialect/src/main/java/tech/ydb/hibernate/dialect/types/LocalDateTimeJdbcType.java @@ -10,13 +10,14 @@ import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.jdbc.BasicBinder; -import org.hibernate.type.descriptor.jdbc.TimeJdbcType; +import org.hibernate.type.descriptor.jdbc.TimestampJdbcType; +import org.hibernate.type.spi.TypeConfiguration; /** * @author Kirill Kurdyukov */ -public class LocalDateTimeJdbcType extends TimeJdbcType { - +public class LocalDateTimeJdbcType extends TimestampJdbcType { + public static final int JDBC_TYPE_DATETIME_CODE = 10017; public static final LocalDateTimeJdbcType INSTANCE = new LocalDateTimeJdbcType(); @Override @@ -24,31 +25,42 @@ public String toString() { return "LocalDateTimeTypeDescriptor"; } + @Override + public int getJdbcTypeCode() { + return JDBC_TYPE_DATETIME_CODE; + } + + @Override + public String getFriendlyName() { + return "DATETIME"; + } + + @Override + public Class getPreferredJavaTypeClass(WrapperOptions options) { + return LocalDateTime.class; + } + + @Override + public JavaType getJdbcRecommendedJavaTypeMapping(Integer length, Integer scale, + TypeConfiguration typeConfiguration) { + return typeConfiguration.getJavaTypeRegistry().getDescriptor(LocalDateTime.class); + } + @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 { + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { final LocalDateTime localDateTime = javaType.unwrap(value, LocalDateTime.class, options); - st.setObject(index, localDateTime); + st.setObject(index, localDateTime, JDBC_TYPE_DATETIME_CODE); } @Override - protected void doBind( - CallableStatement st, - X value, - String name, - WrapperOptions options - ) throws SQLException { + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) throws SQLException { final LocalDateTime localDateTime = javaType.unwrap(value, LocalDateTime.class, options); - st.setObject(name, localDateTime); + st.setObject(name, localDateTime, JDBC_TYPE_DATETIME_CODE); } }; } @@ -57,29 +69,17 @@ protected void doBind( public ValueExtractor getExtractor(JavaType javaType) { return new ValueExtractor<>() { @Override - public X extract( - ResultSet rs, - int paramIndex, - WrapperOptions options - ) throws SQLException { + public X extract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { return javaType.wrap(rs.getObject(paramIndex), options); } @Override - public X extract( - CallableStatement statement, - int paramIndex, - WrapperOptions options - ) throws SQLException { + public X extract(CallableStatement statement, int paramIndex, WrapperOptions options) throws SQLException { return javaType.wrap(statement.getObject(paramIndex), options); } @Override - public X extract( - CallableStatement statement, - String paramName, - WrapperOptions options - ) throws SQLException { + public X extract(CallableStatement statement, String paramName, WrapperOptions options) throws SQLException { return javaType.wrap(statement.getObject(paramName), options); } }; diff --git a/hibernate-dialect/src/test/java/tech/ydb/hibernate/datetime/DataTimeTests.java b/hibernate-dialect/src/test/java/tech/ydb/hibernate/datetime/DataTimeTests.java new file mode 100644 index 0000000..45695a6 --- /dev/null +++ b/hibernate-dialect/src/test/java/tech/ydb/hibernate/datetime/DataTimeTests.java @@ -0,0 +1,60 @@ +package tech.ydb.hibernate.datetime; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import org.hibernate.cfg.AvailableSettings; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import static tech.ydb.hibernate.TestUtils.SESSION_FACTORY; +import static tech.ydb.hibernate.TestUtils.basedConfiguration; +import static tech.ydb.hibernate.TestUtils.inTransaction; +import static tech.ydb.hibernate.TestUtils.jdbcUrl; +import tech.ydb.test.junit5.YdbHelperExtension; + +/** + * @author Kirill Kurdyukov + */ +public class DataTimeTests { + + @RegisterExtension + private static final YdbHelperExtension ydb = new YdbHelperExtension(); + + @Test + void integrationTest() { + SESSION_FACTORY = basedConfiguration() + .addAnnotatedClass(TestEntity.class) + .setProperty(AvailableSettings.URL, jdbcUrl(ydb)) + .buildSessionFactory(); + + var date = LocalDate.now(); + var datetime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); + var timestamp = Instant.now().truncatedTo(ChronoUnit.MICROS); + + var expected = new TestEntity(); + expected.setId(1); + expected.setDate(date); + expected.setDatetime(datetime); + expected.setTimestamp(timestamp); + + inTransaction(session -> session.persist(expected)); + inTransaction(session -> assertEquals(expected, session.find(TestEntity.class, 1))); + + expected.setDatetime(datetime.plusDays(1)); + + inTransaction(session -> session.merge(expected)); + inTransaction(session -> assertEquals(expected, session.find(TestEntity.class, 1))); + + expected.setDate(date.plusDays(1)); + + inTransaction(session -> session.merge(expected)); + inTransaction(session -> assertEquals(expected, session.find(TestEntity.class, 1))); + + expected.setTimestamp(timestamp.plus(1, ChronoUnit.DAYS)); + + inTransaction(session -> session.merge(expected)); + inTransaction(session -> assertEquals(expected, session.find(TestEntity.class, 1))); + } +} diff --git a/hibernate-dialect/src/test/java/tech/ydb/hibernate/datetime/TestEntity.java b/hibernate-dialect/src/test/java/tech/ydb/hibernate/datetime/TestEntity.java new file mode 100644 index 0000000..a9b83e0 --- /dev/null +++ b/hibernate-dialect/src/test/java/tech/ydb/hibernate/datetime/TestEntity.java @@ -0,0 +1,31 @@ +package tech.ydb.hibernate.datetime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import lombok.Data; + +/** + * @author Kirill Kurdyukov + */ +@Entity +@Data +@Table(name = "hibernate_test") +public class TestEntity { + + @Id + private Integer id; + + @Column(name = "c_Date", nullable = false) + private LocalDate date; + + @Column(name = "c_Datetime", nullable = false) + private LocalDateTime datetime; + + @Column(name = "c_Timestamp", nullable = false) + private Instant timestamp; +} diff --git a/hibernate-dialect/src/test/java/tech/ydb/hibernate/types/Employee.java b/hibernate-dialect/src/test/java/tech/ydb/hibernate/types/Employee.java index 465095a..f6aed09 100644 --- a/hibernate-dialect/src/test/java/tech/ydb/hibernate/types/Employee.java +++ b/hibernate-dialect/src/test/java/tech/ydb/hibernate/types/Employee.java @@ -47,8 +47,8 @@ public class Employee { @Column private int age; - @Column(name = "limit_domain_password") - private LocalDateTime limitDomainPassword; + @Column(name = "expired_domain_password") + private LocalDateTime expiredDomainPassword; @Column private byte[] bytes; diff --git a/hibernate-dialect/src/test/java/tech/ydb/hibernate/types/TypesTest.java b/hibernate-dialect/src/test/java/tech/ydb/hibernate/types/TypesTest.java index 02d5d1b..ee39eed 100644 --- a/hibernate-dialect/src/test/java/tech/ydb/hibernate/types/TypesTest.java +++ b/hibernate-dialect/src/test/java/tech/ydb/hibernate/types/TypesTest.java @@ -3,7 +3,6 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.TimeZone; import org.hibernate.cfg.AvailableSettings; import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.BeforeAll; @@ -33,8 +32,6 @@ static void beforeAll() { @Test void integrationTypesTest() { - TimeZone.setDefault(TimeZone.getTimeZone("UTC")); - Employee employee = new Employee( 1, "Kirill Kurdyukov", diff --git a/liquibase-dialect/src/test/resources/changelogs/update/update.xml b/liquibase-dialect/src/test/resources/changelogs/update/update.xml index 9be3c9a..7bcff8c 100644 --- a/liquibase-dialect/src/test/resources/changelogs/update/update.xml +++ b/liquibase-dialect/src/test/resources/changelogs/update/update.xml @@ -7,7 +7,9 @@ - update test set token= true where code = 'A'; + update test + set token= true + where code = 'A'; update test set token=NULL where code='A';