Skip to content

Commit

Permalink
Implement support for hibernate timestamp columns with timezone (#728)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
philipp-kleber-avelios authored Dec 26, 2024
1 parent 2a58b82 commit 3126d8c
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)));
Expand All @@ -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;
}
Expand Down
67 changes: 67 additions & 0 deletions src/test/java/com/example/timezone/Item.java
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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 <T> FeatureMatcher<DatabaseObject, T> hasDatabaseAttribute(String attribute, Class<T> type, Matcher<T> matcher) {
return new FeatureMatcher<>(matcher, attribute, attribute) {

@Override
protected T featureValueOf(DatabaseObject databaseObject) {
return databaseObject.getAttribute(attribute, type);
}

};
}

}

0 comments on commit 3126d8c

Please sign in to comment.