Skip to content

Commit 3126d8c

Browse files
Implement support for hibernate timestamp columns with timezone (#728)
* Implement support for hibernate timestamp columns with the 'with time zone' suffix. * Add unit test for timestamp columns with or without timezones * Fix timezone handling for timestamp columns with an explicit columnDefinition
1 parent 2a58b82 commit 3126d8c

File tree

3 files changed

+156
-1
lines changed

3 files changed

+156
-1
lines changed

src/main/java/liquibase/ext/hibernate/snapshot/ColumnSnapshotGenerator.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
*/
3535
public class ColumnSnapshotGenerator extends HibernateSnapshotGenerator {
3636

37+
private static final String SQL_TIMEZONE_SUFFIX = "with time zone";
38+
private static final String LIQUIBASE_TIMEZONE_SUFFIX = "with timezone";
39+
3740
private final static Pattern pattern = Pattern.compile("([^\\(]*)\\s*\\(?\\s*(\\d*)?\\s*,?\\s*(\\d*)?\\s*([^\\(]*?)\\)?");
3841

3942
public ColumnSnapshotGenerator() {
@@ -183,7 +186,24 @@ protected DataType toDataType(String hibernateType, Integer sqlTypeCode) throws
183186
if (!matcher.matches()) {
184187
return null;
185188
}
186-
DataType dataType = new DataType(matcher.group(1));
189+
190+
String typeName = matcher.group(1);
191+
192+
// Liquibase seems to use 'with timezone' instead of 'with time zone',
193+
// so we remove any 'with time zone' suffixes here.
194+
// The corresponding 'with timezone' suffix will then be added below,
195+
// because in that case hibernateType also ends with 'with time zone'.
196+
if (typeName.toLowerCase().endsWith(SQL_TIMEZONE_SUFFIX)) {
197+
typeName = typeName.substring(0, typeName.length() - SQL_TIMEZONE_SUFFIX.length()).stripTrailing();
198+
}
199+
200+
// If hibernateType ends with 'with time zone' we need to add the corresponding
201+
// 'with timezone' suffix to the Liquibase type.
202+
if (hibernateType.toLowerCase().endsWith(SQL_TIMEZONE_SUFFIX)) {
203+
typeName += (" " + LIQUIBASE_TIMEZONE_SUFFIX);
204+
}
205+
206+
DataType dataType = new DataType(typeName);
187207
if (matcher.group(3).isEmpty()) {
188208
if (!matcher.group(2).isEmpty()) {
189209
dataType.setColumnSize(Integer.parseInt(matcher.group(2)));
@@ -200,6 +220,8 @@ protected DataType toDataType(String hibernateType, Integer sqlTypeCode) throws
200220
}
201221
}
202222

223+
Scope.getCurrentScope().getLog(getClass()).info("Converted column data type - hibernate type: " + hibernateType + ", SQL type: " + sqlTypeCode + ", type name: " + typeName);
224+
203225
dataType.setDataTypeId(sqlTypeCode);
204226
return dataType;
205227
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.example.timezone;
2+
3+
import jakarta.persistence.*;
4+
5+
import java.time.Instant;
6+
import java.time.LocalDateTime;
7+
8+
@Entity
9+
public class Item {
10+
11+
@Id
12+
@GeneratedValue(strategy = GenerationType.SEQUENCE)
13+
private long id;
14+
15+
@Column
16+
private Instant timestamp1;
17+
18+
@Column
19+
private LocalDateTime timestamp2;
20+
21+
@Column(columnDefinition = "timestamp")
22+
private Instant timestamp3;
23+
24+
@Column(columnDefinition = "TIMESTAMP WITH TIME ZONE")
25+
private LocalDateTime timestamp4;
26+
27+
public long getId() {
28+
return id;
29+
}
30+
31+
public void setId(long id) {
32+
this.id = id;
33+
}
34+
35+
public Instant getTimestamp1() {
36+
return timestamp1;
37+
}
38+
39+
public void setTimestamp1(Instant timestamp1) {
40+
this.timestamp1 = timestamp1;
41+
}
42+
43+
public LocalDateTime getTimestamp2() {
44+
return timestamp2;
45+
}
46+
47+
public void setTimestamp2(LocalDateTime timestamp2) {
48+
this.timestamp2 = timestamp2;
49+
}
50+
51+
public Instant getTimestamp3() {
52+
return timestamp3;
53+
}
54+
55+
public void setTimestamp3(Instant timestamp3) {
56+
this.timestamp3 = timestamp3;
57+
}
58+
59+
public LocalDateTime getTimestamp4() {
60+
return timestamp4;
61+
}
62+
63+
public void setTimestamp4(LocalDateTime timestamp4) {
64+
this.timestamp4 = timestamp4;
65+
}
66+
67+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package liquibase.ext.hibernate.snapshot;
2+
3+
import liquibase.CatalogAndSchema;
4+
import liquibase.database.Database;
5+
import liquibase.integration.commandline.CommandLineUtils;
6+
import liquibase.resource.ClassLoaderResourceAccessor;
7+
import liquibase.snapshot.DatabaseSnapshot;
8+
import liquibase.snapshot.SnapshotControl;
9+
import liquibase.snapshot.SnapshotGeneratorFactory;
10+
import liquibase.structure.DatabaseObject;
11+
import liquibase.structure.core.Column;
12+
import liquibase.structure.core.DataType;
13+
import org.hamcrest.FeatureMatcher;
14+
import org.hamcrest.Matcher;
15+
import org.junit.Test;
16+
17+
import static org.hamcrest.MatcherAssert.assertThat;
18+
import static org.hamcrest.Matchers.*;
19+
20+
public class TimezoneSnapshotTest {
21+
22+
@Test
23+
public void testTimezoneColumns() throws Exception {
24+
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);
25+
26+
DatabaseSnapshot snapshot = SnapshotGeneratorFactory.getInstance().createSnapshot(CatalogAndSchema.DEFAULT, database, new SnapshotControl(database));
27+
28+
assertThat(
29+
snapshot.get(Column.class),
30+
hasItems(
31+
// Instant column should result in 'timestamp with timezone' type
32+
allOf(
33+
hasProperty("name", equalTo("timestamp1")),
34+
hasDatabaseAttribute("type", DataType.class, hasProperty("typeName", equalTo("timestamp with timezone")))
35+
),
36+
// LocalDateTime column should result in 'timestamp' type
37+
allOf(
38+
hasProperty("name", equalTo("timestamp2")),
39+
hasDatabaseAttribute("type", DataType.class, hasProperty("typeName", equalTo("timestamp")))
40+
),
41+
// Instant column with explicit definition 'timestamp' should result in 'timestamp' type
42+
allOf(
43+
hasProperty("name", equalTo("timestamp3")),
44+
hasDatabaseAttribute("type", DataType.class, hasProperty("typeName", equalTo("timestamp")))
45+
),
46+
// LocalDateTime Colum with explicit definition 'TIMESTAMP WITH TIME ZONE' should result in 'TIMESTAMP with timezone' type
47+
allOf(
48+
hasProperty("name", equalTo("timestamp4")),
49+
hasDatabaseAttribute("type", DataType.class, hasProperty("typeName", equalToIgnoringCase("timestamp with timezone")))
50+
)
51+
)
52+
);
53+
}
54+
55+
private static <T> FeatureMatcher<DatabaseObject, T> hasDatabaseAttribute(String attribute, Class<T> type, Matcher<T> matcher) {
56+
return new FeatureMatcher<>(matcher, attribute, attribute) {
57+
58+
@Override
59+
protected T featureValueOf(DatabaseObject databaseObject) {
60+
return databaseObject.getAttribute(attribute, type);
61+
}
62+
63+
};
64+
}
65+
66+
}

0 commit comments

Comments
 (0)