From 713d6fb8f9d6913b1fd11326c390fb8ed164b3de Mon Sep 17 00:00:00 2001
From: Maxime Wiewiora <48218208+maximevw@users.noreply.github.com>
Date: Wed, 26 Jun 2024 16:35:10 +0200
Subject: [PATCH] Add compatibility mode for AWS Keyspaces (#303)
---
README.md | 8 +-
.../cassandra/database/CassandraDatabase.java | 44 +++++++-
.../database/CassandraDatabaseConnection.java | 2 +-
.../lockservice/LockServiceCassandra.java | 36 +++++--
...eChangeLogLockTableGeneratorCassandra.java | 14 ++-
.../DeleteGeneratorCassandra.java | 79 +++++++++++++-
...eChangeLogLockTableGeneratorCassandra.java | 33 +++++-
.../TagDatabaseGeneratorCassandra.java | 101 +++++++++++++-----
.../database/CassandraDatabaseTest.groovy | 7 ++
9 files changed, 265 insertions(+), 59 deletions(-)
diff --git a/README.md b/README.md
index fab6bb96..7a975153 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-liquibase-cassandra[![Build and Test Extention](https://github.com/liquibase/liquibase-cassandra/actions/workflows/build.yml/badge.svg)](https://github.com/liquibase/liquibase-cassandra/actions/workflows/build.yml)
+liquibase-cassandra [![Build and Test Extension](https://github.com/liquibase/liquibase-cassandra/actions/workflows/test.yml/badge.svg)](https://github.com/liquibase/liquibase-cassandra/actions/workflows/test.yml)
===================
Liquibase extension for Cassandra Support.
@@ -88,19 +88,19 @@ INSERT INTO posts(id, author_id, title, description, content, inserted_date) VAL
INSERT INTO posts(id, author_id, title, description, content, inserted_date) VALUES
(4,4,'itaque','deleniti','Magni nam optio id recusandae.','2010-07-28');
INSERT INTO posts(id, author_id, title, description, content, inserted_date) VALUES
-(5,5,'ad','similique','Rerum tempore quis ut nesciunt qui excepturi est.','2006-10-09');;
+(5,5,'ad','similique','Rerum tempore quis ut nesciunt qui excepturi est.','2006-10-09');
```
#### Executing the tests
First you need to build project - `mvn package` will do the job.
##### from IDE
-From your IDE, right click on the `liquibase.ext.cassandra.LiquibaseHarnessSuiteIT` test class present in `src/test/groovy` directory.
+From your IDE, right-click on the `liquibase.ext.cassandra.LiquibaseHarnessSuiteIT` test class present in `src/test/groovy` directory.
Doing so, will allow you to execute all the standard change object tests in the liquibase-test-harness as well as the
Cassandra specific change objects tests created exclusively to test this extension (You can find this in the
`src/test/resources/liquibase/harness/change/changelogs/cassandra` directory).
-To run single test case, let's say `addColumn`, create JUit configuration for `liquibase.harness.change.ChangeObjectTests` with arg `-DchangeObjects=addColumn`
+To run single test case, let's say `addColumn`, create JUnit configuration for `liquibase.harness.change.ChangeObjectTests` with arg `-DchangeObjects=addColumn`
More details about different options can be found in [liquibase-test-harness readme](https://github.com/liquibase/liquibase-test-harness)
##### from command line
diff --git a/src/main/java/liquibase/ext/cassandra/database/CassandraDatabase.java b/src/main/java/liquibase/ext/cassandra/database/CassandraDatabase.java
index 1f3c50f2..0a5cffa8 100644
--- a/src/main/java/liquibase/ext/cassandra/database/CassandraDatabase.java
+++ b/src/main/java/liquibase/ext/cassandra/database/CassandraDatabase.java
@@ -2,20 +2,25 @@
import com.ing.data.cassandra.jdbc.CassandraConnection;
import liquibase.Scope;
+import liquibase.configuration.ConfigurationDefinition;
+import liquibase.configuration.LiquibaseConfiguration;
import liquibase.database.AbstractJdbcDatabase;
import liquibase.database.DatabaseConnection;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.DatabaseException;
import liquibase.structure.core.Index;
+import java.sql.PreparedStatement;
import java.sql.Statement;
/**
- * Cassandra 1.2.0 NoSQL database support.
- * Javadocs for ING Cassandra JDBC Wrapper: https://javadoc.io/doc/com.ing.data/cassandra-jdbc-wrapper/latest/index.html
- *
- * Javadocs for DataStax OSS Driver: https://javadoc.io/doc/com.datastax.oss/java-driver-core/latest/index.html
- * Jar file for DataStax OSS Driver: https://search.maven.org/search?q=com.DataStax.oss
+ * Cassandra NoSQL database support.
+ * Javadocs for ING Cassandra
+ * JDBC Wrapper
+ * Javadocs for Apache Cassandra
+ * OSS Java driver
+ * Apache Cassandra OSS Java
+ * driver
*/
public class CassandraDatabase extends AbstractJdbcDatabase {
public static final String PRODUCT_NAME = "Cassandra";
@@ -24,6 +29,31 @@ public class CassandraDatabase extends AbstractJdbcDatabase {
public static final String DEFAULT_DRIVER = "com.ing.data.cassandra.jdbc.CassandraDriver";
private String keyspace;
+ /**
+ * When running on AWS Keyspaces, a specific compatibility mode has to be activated for Liquibase because some
+ * behaviors need to be modified since AWS Keyspaces does not fully support CQL syntax.
+ * See: Issue #297
+ * and: Support Cassandra APIs
+ * in AWS Keyspaces
+ */
+ public static final ConfigurationDefinition AWS_KEYSPACES_COMPATIBILITY_MODE;
+ static {
+ final ConfigurationDefinition.Builder builder = new ConfigurationDefinition.Builder("liquibase.cassandra");
+
+ AWS_KEYSPACES_COMPATIBILITY_MODE = builder.define("awsKeyspacesCompatibilityModeEnabled", Boolean.class)
+ .setDescription("Whether the compatibility mode for AWS Keyspaces must be enabled")
+ .addAliasKey("liquibase.cassandra.awsKeyspacesCompatibilityModeEnabled")
+ .setDefaultValue(false)
+ .build();
+
+ Scope.getCurrentScope().getSingleton(LiquibaseConfiguration.class)
+ .registerDefinition(AWS_KEYSPACES_COMPATIBILITY_MODE);
+ }
+
+ public static boolean isAwsKeyspacesCompatibilityModeEnabled() {
+ return AWS_KEYSPACES_COMPATIBILITY_MODE.getCurrentValue();
+ }
+
@Override
public String getShortName() {
return SHORT_PRODUCT_NAME;
@@ -143,6 +173,10 @@ public Statement getStatement() throws DatabaseException {
return ((JdbcConnection) super.getConnection()).createStatement();
}
+ public PreparedStatement prepareStatement(String query) throws DatabaseException {
+ return ((JdbcConnection) super.getConnection()).prepareStatement(query);
+ }
+
@Override
public boolean jdbcCallsCatalogsSchemas() {
return true;
diff --git a/src/main/java/liquibase/ext/cassandra/database/CassandraDatabaseConnection.java b/src/main/java/liquibase/ext/cassandra/database/CassandraDatabaseConnection.java
index 4432f971..3a6c7463 100644
--- a/src/main/java/liquibase/ext/cassandra/database/CassandraDatabaseConnection.java
+++ b/src/main/java/liquibase/ext/cassandra/database/CassandraDatabaseConnection.java
@@ -13,7 +13,7 @@ public class CassandraDatabaseConnection extends JdbcConnection {
@Override
public int getPriority() {
- return 201;
+ return PRIORITY_DATABASE + 200;
}
@Override
diff --git a/src/main/java/liquibase/ext/cassandra/lockservice/LockServiceCassandra.java b/src/main/java/liquibase/ext/cassandra/lockservice/LockServiceCassandra.java
index 8910eb43..1e385f9c 100644
--- a/src/main/java/liquibase/ext/cassandra/lockservice/LockServiceCassandra.java
+++ b/src/main/java/liquibase/ext/cassandra/lockservice/LockServiceCassandra.java
@@ -21,6 +21,8 @@
import java.util.List;
import java.util.Map;
+import static liquibase.ext.cassandra.database.CassandraDatabase.isAwsKeyspacesCompatibilityModeEnabled;
+
public class LockServiceCassandra extends StandardLockService {
private boolean isDatabaseChangeLogLockTableInitialized;
@@ -149,7 +151,7 @@ public boolean isDatabaseChangeLogLockTableInitialized(final boolean tableJustCr
Executor executor = Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor("jdbc", database);
try {
- isDatabaseChangeLogLockTableInitialized = executeCountQueryWithAlternative(executor,
+ isDatabaseChangeLogLockTableInitialized = executeCountQuery(executor,
"SELECT COUNT(*) FROM " + getChangeLogLockTableName()) > 0;
} catch (LiquibaseException e) {
if (executor.updatesDatabase()) {
@@ -170,7 +172,7 @@ private boolean isLocked(Executor executor) throws DatabaseException {
private boolean isLockedByCurrentInstance(Executor executor) throws DatabaseException {
final String lockedBy = NetUtil.getLocalHostName() + " (" + NetUtil.getLocalHostAddress() + ")";
- return executeCountQueryWithAlternative(executor,
+ return executeCountQuery(executor,
"SELECT COUNT(*) FROM " + getChangeLogLockTableName() + " WHERE " +
"LOCKED = TRUE AND LOCKEDBY = '" + lockedBy + "' ALLOW FILTERING") > 0;
}
@@ -183,20 +185,32 @@ private String getChangeLogLockTableName() {
}
}
- private int executeCountQueryWithAlternative(final Executor executor, final String query) throws DatabaseException {
+ /**
+ * Execute a count query using an alternative if the AWS Keyspaces compatibility mode is enabled.
+ *
+ * @implNote Since aggregate functions like COUNT are not supported by AWS Keyspaces (see
+ *
+ * Cassandra functions in AWS Keyspaces), this method tries to execute the same query without the COUNT
+ * function then programmatically count returned rows, when the AWS Keyspaces compatibility mode is enabled.
+ *
+ * @param executor The query executor.
+ * @param query The query to execute.
+ * @return The result of the count query.
+ * @throws DatabaseException in case something goes wrong during the query execution or if the provided query is
+ * not a count query.
+ */
+ private int executeCountQuery(final Executor executor, final String query) throws DatabaseException {
if (!query.contains("SELECT COUNT(*)")) {
throw new UnexpectedLiquibaseException("Invalid count query: " + query);
}
- try {
- return executor.queryForInt(new RawSqlStatement(query));
- } catch (DatabaseException e) {
- // If the count query failed (for example, because counting rows is not implemented - see issue #289 with
- // AWS Keyspaces where aggregate functions like COUNT are not supported:
- // https://docs.aws.amazon.com/keyspaces/latest/devguide/cassandra-apis.html#cassandra-functions), try to
- // execute the same query without the COUNT function then programmatically count returned rows.
- final String altQuery = query.replace("SELECT COUNT(*)", "SELECT *");
+ if (isAwsKeyspacesCompatibilityModeEnabled()) {
+ Scope.getCurrentScope().getLog(LockServiceCassandra.class)
+ .fine("AWS Keyspaces compatibility mode enabled: using alternative count query");
+ final String altQuery = query.replaceAll("(?i)SELECT COUNT\\(\\*\\)", "SELECT *");
final List