Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for hibernate timestamp columns with timezone #728

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
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;
}

filipelautert marked this conversation as resolved.
Show resolved Hide resolved
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);
}

};
}

}
Loading