diff --git a/src/main/java/liquibase/ext/hibernate/snapshot/ColumnSnapshotGenerator.java b/src/main/java/liquibase/ext/hibernate/snapshot/ColumnSnapshotGenerator.java index 777e6630..74e443f1 100644 --- a/src/main/java/liquibase/ext/hibernate/snapshot/ColumnSnapshotGenerator.java +++ b/src/main/java/liquibase/ext/hibernate/snapshot/ColumnSnapshotGenerator.java @@ -34,6 +34,9 @@ */ public class ColumnSnapshotGenerator extends HibernateSnapshotGenerator { + private static final String SQL_TIMEZONE_SUFFIX = "with time zone"; + private static final String LIQUIBASE_TIMEZONE_SUFFIX = "with timezone"; + private final static Pattern pattern = Pattern.compile("([^\\(]*)\\s*\\(?\\s*(\\d*)?\\s*,?\\s*(\\d*)?\\s*([^\\(]*?)\\)?"); public ColumnSnapshotGenerator() { @@ -183,7 +186,24 @@ protected DataType toDataType(String hibernateType, Integer sqlTypeCode) throws if (!matcher.matches()) { return null; } - DataType dataType = new DataType(matcher.group(1)); + + String typeName = matcher.group(1); + + // Liquibase seems to use 'with timezone' instead of 'with time zone', + // so we remove any 'with time zone' suffixes here. + // The corresponding 'with timezone' suffix will then be added below, + // because in that case hibernateType also ends with 'with time zone'. + if (typeName.toLowerCase().endsWith(SQL_TIMEZONE_SUFFIX)) { + typeName = typeName.substring(0, typeName.length() - SQL_TIMEZONE_SUFFIX.length()).stripTrailing(); + } + + // If hibernateType ends with 'with time zone' we need to add the corresponding + // 'with timezone' suffix to the Liquibase type. + if (hibernateType.toLowerCase().endsWith(SQL_TIMEZONE_SUFFIX)) { + typeName += (" " + LIQUIBASE_TIMEZONE_SUFFIX); + } + + DataType dataType = new DataType(typeName); if (matcher.group(3).isEmpty()) { if (!matcher.group(2).isEmpty()) { dataType.setColumnSize(Integer.parseInt(matcher.group(2))); @@ -200,6 +220,8 @@ protected DataType toDataType(String hibernateType, Integer sqlTypeCode) throws } } + Scope.getCurrentScope().getLog(getClass()).info("Converted column data type - hibernate type: " + hibernateType + ", SQL type: " + sqlTypeCode + ", type name: " + typeName); + dataType.setDataTypeId(sqlTypeCode); return dataType; } diff --git a/src/test/java/com/example/timezone/Item.java b/src/test/java/com/example/timezone/Item.java new file mode 100644 index 00000000..8d9aacc6 --- /dev/null +++ b/src/test/java/com/example/timezone/Item.java @@ -0,0 +1,67 @@ +package com.example.timezone; + +import jakarta.persistence.*; + +import java.time.Instant; +import java.time.LocalDateTime; + +@Entity +public class Item { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private long id; + + @Column + private Instant timestamp1; + + @Column + private LocalDateTime timestamp2; + + @Column(columnDefinition = "timestamp") + private Instant timestamp3; + + @Column(columnDefinition = "TIMESTAMP WITH TIME ZONE") + private LocalDateTime timestamp4; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Instant getTimestamp1() { + return timestamp1; + } + + public void setTimestamp1(Instant timestamp1) { + this.timestamp1 = timestamp1; + } + + public LocalDateTime getTimestamp2() { + return timestamp2; + } + + public void setTimestamp2(LocalDateTime timestamp2) { + this.timestamp2 = timestamp2; + } + + public Instant getTimestamp3() { + return timestamp3; + } + + public void setTimestamp3(Instant timestamp3) { + this.timestamp3 = timestamp3; + } + + public LocalDateTime getTimestamp4() { + return timestamp4; + } + + public void setTimestamp4(LocalDateTime timestamp4) { + this.timestamp4 = timestamp4; + } + +} diff --git a/src/test/java/liquibase/ext/hibernate/snapshot/TimezoneSnapshotTest.java b/src/test/java/liquibase/ext/hibernate/snapshot/TimezoneSnapshotTest.java new file mode 100644 index 00000000..615f3dc7 --- /dev/null +++ b/src/test/java/liquibase/ext/hibernate/snapshot/TimezoneSnapshotTest.java @@ -0,0 +1,66 @@ +package liquibase.ext.hibernate.snapshot; + +import liquibase.CatalogAndSchema; +import liquibase.database.Database; +import liquibase.integration.commandline.CommandLineUtils; +import liquibase.resource.ClassLoaderResourceAccessor; +import liquibase.snapshot.DatabaseSnapshot; +import liquibase.snapshot.SnapshotControl; +import liquibase.snapshot.SnapshotGeneratorFactory; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.Column; +import liquibase.structure.core.DataType; +import org.hamcrest.FeatureMatcher; +import org.hamcrest.Matcher; +import org.junit.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class TimezoneSnapshotTest { + + @Test + public void testTimezoneColumns() throws Exception { + Database database = CommandLineUtils.createDatabaseObject(new ClassLoaderResourceAccessor(this.getClass().getClassLoader()), "hibernate:spring:com.example.timezone?dialect=org.hibernate.dialect.H2Dialect", null, null, null, null, null, false, false, null, null, null, null, null, null, null); + + DatabaseSnapshot snapshot = SnapshotGeneratorFactory.getInstance().createSnapshot(CatalogAndSchema.DEFAULT, database, new SnapshotControl(database)); + + assertThat( + snapshot.get(Column.class), + hasItems( + // Instant column should result in 'timestamp with timezone' type + allOf( + hasProperty("name", equalTo("timestamp1")), + hasDatabaseAttribute("type", DataType.class, hasProperty("typeName", equalTo("timestamp with timezone"))) + ), + // LocalDateTime column should result in 'timestamp' type + allOf( + hasProperty("name", equalTo("timestamp2")), + hasDatabaseAttribute("type", DataType.class, hasProperty("typeName", equalTo("timestamp"))) + ), + // Instant column with explicit definition 'timestamp' should result in 'timestamp' type + allOf( + hasProperty("name", equalTo("timestamp3")), + hasDatabaseAttribute("type", DataType.class, hasProperty("typeName", equalTo("timestamp"))) + ), + // LocalDateTime Colum with explicit definition 'TIMESTAMP WITH TIME ZONE' should result in 'TIMESTAMP with timezone' type + allOf( + hasProperty("name", equalTo("timestamp4")), + hasDatabaseAttribute("type", DataType.class, hasProperty("typeName", equalToIgnoringCase("timestamp with timezone"))) + ) + ) + ); + } + + private static FeatureMatcher hasDatabaseAttribute(String attribute, Class type, Matcher matcher) { + return new FeatureMatcher<>(matcher, attribute, attribute) { + + @Override + protected T featureValueOf(DatabaseObject databaseObject) { + return databaseObject.getAttribute(attribute, type); + } + + }; + } + +}