Skip to content

Commit 16828e8

Browse files
feat: support Spanner PostgreSQL dialect (#485)
* chore: add getDialect() method (#465) * add support postgesql Dialect * add support postgesql Dialect * add Liquibase system tables for PostgreSQL dialect * fix tests * fix formating * resolve comments * Add PostgreSQL dialect support for test configuration (#468) * Add PostgreSQL dialect support for test configuration * Add PostgreSQL dialect support for test configuration * add changelog pg-sql file * remove temporarily solution * feat: PostgreSQL data type mappings (#478) * Implement Cloud Spanner PostgreSQL data type mappings * Implement Cloud Spanner PostgreSQL data type mappings * resolve comments * resolve comments * fix timestamp test * chore: re-throw original exception --------- Co-authored-by: Knut Olav Løite <[email protected]> * Refactor Spanner Data Types and Add PostgreSQL Dialect Support for Data Type Handling and SQL Generation (#480) * add sql generation all type, default value and etc. * add comments * reformat * add session management commands (PostgreSQL) and fix: INSERT OR UPDATE instead of INSERT OR IGNORE. * feat: add sequence min/max validation and reformat * fix: add explicit type casting for literals in PostgreSQL insert generation * Enhance Mock Server Support for PostgreSQL Dialect in Liquibase-Spanner Tests (#513) * Implement Posgresql dialect for unit tests * resolve comments * Add Liquibase Harness Tests for PostgreSQL Dialect (#519) * add liquibase harness test for posgresql dialect * resolve comments * chore: try to downgrade JDBC version --------- Co-authored-by: Vladimir Evdokimov <[email protected]>
1 parent b9efb13 commit 16828e8

File tree

265 files changed

+6173
-1399
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

265 files changed

+6173
-1399
lines changed

build.gradle

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ dependencies {
7878
}
7979
}
8080
// Cloud Spanner related
81+
implementation("com.google.cloud:google-cloud-spanner-jdbc:2.33.3")
8182
implementation platform('com.google.cloud:libraries-bom:26.71.0')
82-
implementation("com.google.cloud:google-cloud-spanner-jdbc")
8383

8484
// Liquibase Core - needed for testing and docker container
8585
implementation("org.liquibase:liquibase-core:4.33.0")
@@ -88,6 +88,7 @@ dependencies {
8888

8989
// Testing
9090
testImplementation("org.junit.jupiter:junit-jupiter-api:5.13.4")
91+
testImplementation("org.junit.jupiter:junit-jupiter-params:5.13.4")
9192
testImplementation("org.testcontainers:testcontainers:1.21.3")
9293
testImplementation("net.java.dev.jna:jna:5.17.0")
9394
testImplementation("com.google.truth:truth:1.4.4") {
@@ -96,7 +97,7 @@ dependencies {
9697

9798
// For using the Liquibase test harness
9899
testImplementation 'junit:junit:4.13.2'
99-
testImplementation ('org.liquibase:liquibase-test-harness:1.0.10'){
100+
testImplementation ('org.liquibase:liquibase-test-harness:1.0.11'){
100101
exclude group: 'org.firebirdsql.jdbc', module: 'jaybird'
101102
}
102103
testImplementation('org.apache.groovy:groovy-all:4.0.28') {
@@ -127,6 +128,12 @@ test {
127128
excludeTags "integration"
128129
exclude '**/CloudSpannerBaseHarnessSuiteTest*'
129130
exclude '**/CloudSpannerAdvancedHarnessSuiteTest*'
131+
exclude '**/CloudSpannerFoundationalHarnessSuiteTest*'
132+
}
133+
134+
testLogging {
135+
events "failed"
136+
exceptionFormat "full"
130137
}
131138
}
132139

@@ -157,6 +164,7 @@ tasks.register('integrationTest', Test) {
157164
includeTags "integration"
158165
exclude '**/CloudSpannerBaseHarnessSuiteTest*'
159166
exclude '**/CloudSpannerAdvancedHarnessSuiteTest*'
167+
exclude '**/CloudSpannerFoundationalHarnessSuiteTest*'
160168
}
161169
}
162170

pom.xml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
<dependency>
8585
<groupId>com.google.cloud</groupId>
8686
<artifactId>google-cloud-spanner-jdbc</artifactId>
87+
<version>2.33.3</version>
8788
</dependency>
8889

8990
<!-- Liquibase test dependencies -->
@@ -150,6 +151,12 @@
150151
<version>${jupiter.version}</version>
151152
<scope>test</scope>
152153
</dependency>
154+
<dependency>
155+
<groupId>org.junit.jupiter</groupId>
156+
<artifactId>junit-jupiter-params</artifactId>
157+
<version>${jupiter.version}</version>
158+
<scope>test</scope>
159+
</dependency>
153160
<dependency>
154161
<groupId>org.testcontainers</groupId>
155162
<artifactId>testcontainers</artifactId>
@@ -393,7 +400,9 @@
393400
<configuration>
394401
<excludedGroups>integration</excludedGroups>
395402
<excludes>
396-
<exclude>**/CloudSpannerHarnessTest.java</exclude>
403+
<exclude>**/CloudSpannerAdvancedHarnessSuiteTest*</exclude>
404+
<exclude>**/CloudSpannerBaseHarnessSuiteTest*</exclude>
405+
<exclude>**/CloudSpannerFoundationalHarnessSuiteTest*</exclude>
397406
</excludes>
398407
</configuration>
399408
</plugin>
@@ -439,6 +448,18 @@
439448
</execution>
440449
</executions>
441450
</plugin>
451+
<plugin>
452+
<groupId>org.apache.maven.plugins</groupId>
453+
<artifactId>maven-surefire-plugin</artifactId>
454+
<configuration>
455+
<excludedGroups>integration</excludedGroups>
456+
<excludes>
457+
<exclude>**/CloudSpannerAdvancedHarnessSuiteTest*</exclude>
458+
<exclude>**/CloudSpannerBaseHarnessSuiteTest*</exclude>
459+
<exclude>**/CloudSpannerFoundationalHarnessSuiteTest*</exclude>
460+
</excludes>
461+
</configuration>
462+
</plugin>
442463
</plugins>
443464
</build>
444465
</profile>

src/main/java/liquibase/ext/spanner/CloudSpanner.java

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,18 @@
1616
import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
1717
import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME;
1818

19+
import com.google.cloud.spanner.Dialect;
1920
import com.google.cloud.spanner.Type;
2021
import com.google.cloud.spanner.jdbc.CloudSpannerJdbcConnection;
22+
import java.sql.Connection;
2123
import java.sql.DriverManager;
2224
import java.sql.SQLException;
2325
import java.text.ParseException;
2426
import java.time.Instant;
2527
import java.time.OffsetDateTime;
2628
import java.time.ZoneOffset;
2729
import java.util.Date;
30+
import java.util.Objects;
2831
import liquibase.Scope;
2932
import liquibase.database.AbstractJdbcDatabase;
3033
import liquibase.database.DatabaseConnection;
@@ -59,24 +62,37 @@ public boolean dataTypeIsNotModifiable(final String typeName) {
5962
@Override
6063
public String getDateLiteral(final String isoDate) {
6164
// Construct the literal based on whether it is a DATE or TIMESTAMP
62-
if (isDateTime(isoDate)) {
63-
try {
65+
if (isoDate == null || isoDate.trim().equalsIgnoreCase("null")) {
66+
return this.getDialect() == Dialect.POSTGRESQL ? "CAST(NULL AS timestamptz)" : "NULL";
67+
}
68+
try {
69+
if (isDateTime(isoDate)) {
6470
Date date = new ISODateFormat().parse(isoDate);
6571
Instant instant = date.toInstant();
6672
OffsetDateTime utcDateTime = instant.atOffset(ZoneOffset.UTC);
6773
String formattedDate = utcDateTime.format(ISO_LOCAL_DATE);
6874
String formattedTime = utcDateTime.format(ISO_LOCAL_TIME);
69-
return "TIMESTAMP '" + formattedDate + "T" + formattedTime + "Z'";
70-
} catch (ParseException e) {
71-
return "BAD_DATE_FORMAT:" + isoDate;
75+
return this.getDialect() == Dialect.POSTGRESQL
76+
? "'" + formattedDate + "T" + formattedTime + "Z'" + "::timestamptz"
77+
: "TIMESTAMP '" + formattedDate + "T" + formattedTime + "Z'";
78+
79+
} else {
80+
String formattedDate = super.getDateLiteral(isoDate);
81+
return this.getDialect() == Dialect.POSTGRESQL
82+
? formattedDate + "::date"
83+
: "DATE " + formattedDate;
7284
}
73-
} else {
74-
return "DATE " + super.getDateLiteral(isoDate);
85+
} catch (ParseException e) {
86+
return "BAD_DATE_FORMAT:" + isoDate;
7587
}
7688
}
7789

7890
@Override
7991
public String getCurrentDateTimeFunction() {
92+
Dialect dialect = this.getDialect();
93+
if (Objects.requireNonNull(dialect) == Dialect.POSTGRESQL) {
94+
return "CURRENT_TIMESTAMP";
95+
}
8096
return "CURRENT_TIMESTAMP()";
8197
}
8298

@@ -243,21 +259,46 @@ public boolean supportsPrimaryKeyNames() {
243259

244260
@Override
245261
protected String getQuotingStartCharacter() {
246-
return "`";
262+
Dialect dialect = this.getDialect();
263+
return dialect == Dialect.POSTGRESQL ? "\"" : "`";
247264
}
248265

249266
@Override
250267
protected String getQuotingEndCharacter() {
251-
return "`";
268+
Dialect dialect = this.getDialect();
269+
return dialect == Dialect.POSTGRESQL ? "\"" : "`";
252270
}
253271

254272
@Override
255273
protected String getQuotingEndReplacement() {
256-
return "\\`";
274+
Dialect dialect = this.getDialect();
275+
return dialect == Dialect.POSTGRESQL ? "\"" : "\\`";
257276
}
258277

259278
@Override
260279
public String escapeStringForDatabase(String string) {
280+
Dialect dialect = this.getDialect();
281+
if (dialect == Dialect.POSTGRESQL) {
282+
return string == null ? null : string.replace("'", "''");
283+
}
261284
return string == null ? null : string.replace("'", "\\'");
262285
}
286+
287+
@Override
288+
public Dialect getDialect() {
289+
try {
290+
DatabaseConnection conn = getConnection();
291+
292+
if (conn instanceof JdbcConnection) {
293+
Connection underlying = ((JdbcConnection) conn).getUnderlyingConnection();
294+
if (underlying.isWrapperFor(CloudSpannerJdbcConnection.class)) {
295+
return underlying.unwrap(CloudSpannerJdbcConnection.class).getDialect();
296+
}
297+
}
298+
} catch (SQLException e) {
299+
throw new RuntimeException("Failed to get dialect from connection", e);
300+
}
301+
302+
return null;
303+
}
263304
}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package liquibase.ext.spanner;
22

3+
import com.google.cloud.spanner.Dialect;
34
import liquibase.database.Database;
45

5-
public interface ICloudSpanner extends Database {}
6+
public interface ICloudSpanner extends Database {
7+
Dialect getDialect();
8+
}

src/main/java/liquibase/ext/spanner/change/AddLookupTableChangeSpanner.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414
package liquibase.ext.spanner.change;
1515

16+
import com.google.cloud.spanner.Dialect;
1617
import java.util.ArrayList;
1718
import java.util.Arrays;
1819
import java.util.List;
@@ -21,6 +22,8 @@
2122
import liquibase.change.core.AddForeignKeyConstraintChange;
2223
import liquibase.change.core.AddLookupTableChange;
2324
import liquibase.database.Database;
25+
import liquibase.datatype.DataTypeFactory;
26+
import liquibase.datatype.LiquibaseDataType;
2427
import liquibase.ext.spanner.ICloudSpanner;
2528
import liquibase.statement.SqlStatement;
2629
import liquibase.statement.core.RawSqlStatement;
@@ -48,6 +51,11 @@ public SqlStatement[] generateStatements(Database database) {
4851
String existingTableCatalogName = getExistingTableCatalogName();
4952
String existingTableSchemaName = getExistingTableSchemaName();
5053

54+
Dialect dialect = ((ICloudSpanner) database).getDialect();
55+
String rawType = getNewColumnDataType();
56+
LiquibaseDataType liquibaseType =
57+
DataTypeFactory.getInstance().fromDescription(rawType, database);
58+
String actualType = liquibaseType.toDatabaseDataType(database).toString();
5159
SqlStatement[] createTablesSQL =
5260
new SqlStatement[] {
5361
new RawSqlStatement(
@@ -57,10 +65,12 @@ public SqlStatement[] generateStatements(Database database) {
5765
+ " ("
5866
+ database.escapeObjectName(getNewColumnName(), Column.class)
5967
+ " "
60-
+ getNewColumnDataType()
61-
+ " NOT NULL) PRIMARY KEY ("
68+
+ actualType
69+
+ (dialect == Dialect.POSTGRESQL
70+
? " NOT NULL, PRIMARY KEY ("
71+
: " NOT NULL) PRIMARY KEY (")
6272
+ database.escapeObjectName(getNewColumnName(), Column.class)
63-
+ ")"),
73+
+ (dialect == Dialect.POSTGRESQL ? "))" : ")")),
6474
new RawSqlStatement(
6575
"INSERT INTO "
6676
+ database.escapeTableName(

src/main/java/liquibase/ext/spanner/change/DropAllForeignKeyConstraintsChangeSpanner.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,18 @@ public SqlStatement[] generateStatements(Database database) {
3737
try (PreparedStatement ps =
3838
connection.prepareStatement(
3939
"SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_CATALOG=? AND TABLE_SCHEMA=? AND TABLE_NAME=? AND CONSTRAINT_TYPE='FOREIGN KEY'")) {
40-
ps.setString(1, MoreObjects.firstNonNull(getBaseTableCatalogName(), ""));
41-
ps.setString(2, MoreObjects.firstNonNull(getBaseTableSchemaName(), ""));
40+
String catalogName =
41+
MoreObjects.firstNonNull(getBaseTableCatalogName(), database.getDefaultCatalogName());
42+
String schemaName =
43+
MoreObjects.firstNonNull(getBaseTableSchemaName(), database.getDefaultSchemaName());
44+
ps.setString(1, catalogName);
45+
ps.setString(2, schemaName);
4246
ps.setString(3, getBaseTableName());
4347
try (ResultSet rs = ps.executeQuery()) {
4448
while (rs.next()) {
4549
DropForeignKeyConstraintChange drop = new DropForeignKeyConstraintChange();
46-
drop.setBaseTableCatalogName(getBaseTableCatalogName());
47-
drop.setBaseTableSchemaName(getBaseTableSchemaName());
50+
drop.setBaseTableCatalogName(catalogName);
51+
drop.setBaseTableSchemaName(schemaName);
4852
drop.setBaseTableName(getBaseTableName());
4953
drop.setConstraintName(rs.getString(1));
5054
sqlStatements.addAll(Arrays.asList(drop.generateStatements(database)));

src/main/java/liquibase/ext/spanner/change/MergeColumnsChangeSpanner.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414
package liquibase.ext.spanner.change;
1515

16+
import com.google.cloud.spanner.Dialect;
1617
import java.util.ArrayList;
1718
import java.util.Arrays;
1819
import java.util.List;
@@ -23,6 +24,7 @@
2324
import liquibase.change.core.DropColumnChange;
2425
import liquibase.change.core.MergeColumnChange;
2526
import liquibase.database.Database;
27+
import liquibase.ext.spanner.ICloudSpanner;
2628
import liquibase.statement.SqlStatement;
2729
import liquibase.statement.core.RawSqlStatement;
2830
import liquibase.structure.core.Column;
@@ -32,7 +34,7 @@
3234
* UPDATE and DELETE statements to include a WHERE clause, even when all rows should be
3335
* updated/deleted. This feature is a safety precaution against accidental updates/deletes.
3436
*
35-
* <p>{@link SpannerMergeColumnsChange} will use a Partitioned DML statement to fill the data in the
37+
* <p>{@link MergeColumnsChangeSpanner} will use a Partitioned DML statement to fill the data in the
3638
* new column.
3739
*/
3840
@DatabaseChange(
@@ -44,6 +46,7 @@ public class MergeColumnsChangeSpanner extends MergeColumnChange {
4446

4547
@Override
4648
public SqlStatement[] generateStatements(final Database database) {
49+
Dialect dialect = ((ICloudSpanner) database).getDialect();
4750
List<SqlStatement> statements = new ArrayList<>();
4851

4952
AddColumnChange addNewColumnChange = new AddColumnChange();
@@ -55,9 +58,14 @@ public SqlStatement[] generateStatements(final Database database) {
5558
columnConfig.setType(getFinalColumnType());
5659
addNewColumnChange.addColumn(columnConfig);
5760
statements.addAll(Arrays.asList(addNewColumnChange.generateStatements(database)));
58-
59-
statements.add(new RawSqlStatement("SET AUTOCOMMIT=TRUE"));
60-
statements.add(new RawSqlStatement("SET AUTOCOMMIT_DML_MODE='PARTITIONED_NON_ATOMIC'"));
61+
if (dialect == Dialect.POSTGRESQL) {
62+
statements.add(new RawSqlStatement("set autocommit=true"));
63+
statements.add(
64+
new RawSqlStatement("set spanner.autocommit_dml_mode='partitioned_non_atomic'"));
65+
} else {
66+
statements.add(new RawSqlStatement("SET AUTOCOMMIT=TRUE"));
67+
statements.add(new RawSqlStatement("SET AUTOCOMMIT_DML_MODE='PARTITIONED_NON_ATOMIC'"));
68+
}
6169
String updateStatement =
6270
"UPDATE "
6371
+ database.escapeTableName(getCatalogName(), getSchemaName(), getTableName())
@@ -70,8 +78,11 @@ public SqlStatement[] generateStatements(final Database database) {
7078
database.escapeObjectName(getColumn2Name(), Column.class))
7179
+ " WHERE TRUE";
7280
statements.add(new RawSqlStatement(updateStatement));
73-
statements.add(new RawSqlStatement("SET AUTOCOMMIT_DML_MODE='TRANSACTIONAL'"));
74-
81+
if (dialect == Dialect.POSTGRESQL) {
82+
statements.add(new RawSqlStatement("set spanner.autocommit_dml_mode='transactional'"));
83+
} else {
84+
statements.add(new RawSqlStatement("SET AUTOCOMMIT_DML_MODE='TRANSACTIONAL'"));
85+
}
7586
DropColumnChange dropColumn1Change = new DropColumnChange();
7687
dropColumn1Change.setCatalogName(getCatalogName());
7788
dropColumn1Change.setSchemaName(getSchemaName());

0 commit comments

Comments
 (0)