Skip to content

Commit

Permalink
[0.11.0-SNAPSHOT]
Browse files Browse the repository at this point in the history
Cassandra Keyspace by default support added
Cassandra Cognitor migration library support added
Cassandra Migration#dropMode TRUNCATE/DROP support added
  • Loading branch information
GoodforGod committed May 21, 2024
1 parent 97142fa commit 2fa3d80
Show file tree
Hide file tree
Showing 27 changed files with 369 additions and 139 deletions.
47 changes: 33 additions & 14 deletions cassandra/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ Features:

**Gradle**
```groovy
testImplementation "io.goodforgod:testcontainers-extensions-cassandra:0.10.2"
testImplementation "io.goodforgod:testcontainers-extensions-cassandra:0.11.0"
```

**Maven**
```xml
<dependency>
<groupId>io.goodforgod</groupId>
<artifactId>testcontainers-extensions-cassandra</artifactId>
<version>0.10.2</version>
<version>0.11.0</version>
<scope>test</scope>
</dependency>
```
Expand Down Expand Up @@ -78,8 +78,8 @@ class ExampleTests {

@Test
void test(@ConnectionCassandra CassandraConnection connection) {
connection.execute("INSERT INTO cassandra.users(id) VALUES(1);");
var usersFound = connection.queryMany("SELECT * FROM cassandra.users;", r -> r.getInt(0));
connection.execute("INSERT INTO users(id) VALUES(1);");
var usersFound = connection.queryMany("SELECT * FROM users;", r -> r.getInt(0));
assertEquals(1, usersFound.size());
}
}
Expand Down Expand Up @@ -111,6 +111,8 @@ class ExampleTests {
}
```

Keyspace is created automatically.

### Connection Migration

`Migrations` allow easily migrate database between test executions and drop after tests.
Expand All @@ -124,11 +126,18 @@ class ExampleTests {
void test(@ConnectionCassandra CassandraConnection connection) {
connection.migrationEngine(Migration.Engines.SCRIPTS).apply("migration/setup.cql");
connection.execute("INSERT INTO users VALUES(1);");
connection.migrationEngine(Migration.Engines.SCRIPTS).drop("migration/setup.cql");
connection.migrationEngine(Migration.Engines.SCRIPTS).drop("migration/setup.cql", Migration.DropMode.TRUNCATE);
}
}
```

Keyspace is created automatically.

It is recommended to **always** use construction `CREATE IF NOT EXISTS` for migration scripts,
cause migration drop when using `TRUNCATE TABLE` on all tables in keyspace is a lot faster compared to using `DROP TABLE`.

Default strategy is to use `TRUNCATE`, if you want to change it use `Migration.DropMode.DROP`.

## Annotation

Library provides annotation based approach for creating container.
Expand Down Expand Up @@ -192,6 +201,7 @@ class ExampleTests {
@Test
void test(@ConnectionCassandra CassandraConnection connection) {
assertEquals("mydc", connection.params().datacenter());
assertEquals("cassandra", connection.params().keyspace());
}
}
```
Expand Down Expand Up @@ -249,8 +259,8 @@ class ExampleTests {
@Test
void test() {
connection.execute("INSERT INTO cassandra.users(id) VALUES(1);");
connection.execute("INSERT INTO cassandra.users(id) VALUES(2);");
var usersFound = connection.queryMany("SELECT * FROM cassandra.users;", r -> r.getInt(0));
connection.execute("INSERT INTO users(id) VALUES(2);");
var usersFound = connection.queryMany("SELECT * FROM users;", r -> r.getInt(0));
assertEquals(2, usersFound.size());
}
}
Expand All @@ -264,16 +274,23 @@ Annotation parameters:
- `engine` - to use for migration.
- `apply` - parameter configures migration mode.
- `drop` - configures when to reset/drop/clear database.
- `dropMode` - configures what strategy to use for migration drop (`TRUNCATE` or `DROP`)
- `locations` - configures locations where migrations are placed.

Keyspace is created automatically.

It is recommended to **always** use construction `CREATE IF NOT EXISTS` for migration scripts,
cause migration drop when using `TRUNCATE TABLE` on all tables in keyspace is a lot faster compared to using `DROP TABLE`.

Default strategy is to use `TRUNCATE`, if you want to change it use `Migration.DropMode.DROP`.

Available migration engines:
- Scripts - For `apply` load scripts from specified paths or directories and execute in ASC order, for `drop` clean all Non System tables in all cassandra
- [Cognitor](https://github.com/patka/cassandra-migration) - For `apply` uses Cognitor Cassandra migration library, for `drop` clean all Non System tables in all cassandra

Given engine is Scripts and migration file named `setup.sql` is in resource directory `migration`:
Given engine is Scripts and migration file named `1_setup.sql` is in resource directory `migration`:
```sql
CREATE KEYSPACE IF NOT EXISTS cassandra WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};

CREATE TABLE IF NOT EXISTS cassandra.users
CREATE TABLE IF NOT EXISTS users
(
id INT,
PRIMARY KEY (id)
Expand All @@ -287,15 +304,16 @@ Test with container and migration per method will look like:
engine = Migration.Engines.SCRIPTS,
apply = Migration.Mode.PER_METHOD,
drop = Migration.Mode.PER_METHOD,
migrations = { "migration/setup.cql" }
dropMode = Migration.DropMode.TRUNCATE,
migrations = { "migration" }
))
class ExampleTests {

@Test
void test(@ConnectionCassandra CassandraConnection connection) {
connection.execute("INSERT INTO cassandra.users(id) VALUES(1);");
connection.execute("INSERT INTO cassandra.users(id) VALUES(2);");
var usersFound = connection.queryMany("SELECT * FROM cassandra.users;", r -> r.getInt(0));
connection.execute("INSERT INTO users(id) VALUES(2);");
var usersFound = connection.queryMany("SELECT * FROM users;", r -> r.getInt(0));
assertEquals(2, usersFound.size());
}
}
Expand All @@ -312,6 +330,7 @@ Special environment variables:
- `EXTERNAL_TEST_CASSANDRA_HOST` - Cassandra instance host.
- `EXTERNAL_TEST_CASSANDRA_PORT` - Cassandra instance port.
- `EXTERNAL_TEST_CASSANDRA_DATACENTER` - Cassandra instance database (`datacenter1` by default).
- `EXTERNAL_TEST_CASSANDRA_KEYSPACE` - Cassandra keyspace (`cassandra` by default).
-
## License

Expand Down
1 change: 0 additions & 1 deletion cassandra/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ dependencies {
exclude group: "com.datastax.cassandra"
}
implementation "org.cognitor.cassandra:cassandra-migration:2.6.1_v4"

implementation libs.slf4j.api

implementation libs.junit.launcher
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.goodforgod.testcontainers.extensions.cassandra;

import java.util.*;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

abstract class AbstractDropCassandraMigrationEngine implements CassandraMigrationEngine {

private static final Set<String> SYSTEM_KEYSPACES = Set.of("system", "system_auth", "system_schema", "system_distributed",
"system_traces");

protected final Logger logger = LoggerFactory.getLogger(getClass());

private static class Table {

private final String keyspace;
private final String name;

private Table(String keyspace, String name) {
this.keyspace = keyspace;
this.name = name;
}

public String keyspace() {
return keyspace;
}

public String name() {
return name;
}
}

protected final CassandraConnection connection;

public AbstractDropCassandraMigrationEngine(CassandraConnection connection) {
this.connection = connection;
}

@Override
public void drop(@NotNull List<String> locations, Migration.DropMode mode) {
if (locations.isEmpty()) {
logger.warn("Empty locations for schema migration for engine '{}' for connection: {}",
getClass().getSimpleName(), connection);
return;
}

logger.debug("Starting schema dropping for engine '{}' for connection: {}",
getClass().getSimpleName(), connection);

var tables = connection.queryMany(
"SELECT keyspace_name, table_name FROM system_schema.tables;",
r -> new Table(r.getString(0), r.getString(1)));

for (Table table : tables) {
if (table.keyspace().equals(connection.params().keyspace()) && !SYSTEM_KEYSPACES.contains(table.keyspace())) {
// always try to use TRUNCATE cause DROP is SUPER slow, drop keyspace is even slower
if (mode == Migration.DropMode.TRUNCATE) {
connection.execute("TRUNCATE TABLE " + table.keyspace() + "." + table.name());
} else {
connection.execute("DROP TABLE " + table.keyspace() + "." + table.name());
}
}
}

logger.info("Finished schema dropping for engine '{}' for connection: {}",
getClass().getSimpleName(), connection);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ interface Params {
@NotNull
String datacenter();

@NotNull
String keyspace();

String username();

String password();
Expand Down Expand Up @@ -113,31 +116,31 @@ <T, E extends Throwable> List<T> queryMany(@NotNull @Language("CQL") String cql,
throws E;

/**
* @param table example: mykeyspace.mytable
* @param table example: mytable
* @return SELECT COUNT(*) from specified table
*/
long count(@NotNull String table);

/**
* Asserts that SELECT COUNT(*) in specified table counts 0 rows
*
* @param table example: mykeyspace.mytable
* @param table example: mytable
*/
void assertCountsNone(@NotNull String table);

/**
* Asserts that SELECT COUNT(*) in specified table counts at least minimal number expectedAtLeast
* rows
*
* @param table example: mykeyspace.mytable
* @param table example: mytable
* @param expectedAtLeast at least minimal number of rows expected
*/
void assertCountsAtLeast(long expectedAtLeast, @NotNull String table);

/**
* Asserts that SELECT COUNT(*) in specified table counts exact number expected rows
*
* @param table example: mykeyspace.mytable
* @param table example: mytable
* @param expected exact number of rows expected
*/
void assertCountsEquals(long expected, @NotNull String table);
Expand Down Expand Up @@ -189,25 +192,30 @@ <T, E extends Throwable> List<T> queryMany(@NotNull @Language("CQL") String cql,
void close();

static CassandraConnection forContainer(CassandraContainer<?> container) {
return forContainer(container, "cassandra");
}

static CassandraConnection forContainer(CassandraContainer<?> container, String keyspace) {
if (!container.isRunning()) {
throw new IllegalStateException(container.getClass().getSimpleName() + " container is not running");
}

var params = new CassandraConnectionImpl.ParamsImpl(container.getHost(),
container.getMappedPort(CassandraContainer.CQL_PORT),
container.getLocalDatacenter(), container.getUsername(), container.getPassword());
keyspace, container.getLocalDatacenter(), container.getUsername(), container.getPassword());
final Params network = new CassandraConnectionImpl.ParamsImpl(container.getNetworkAliases().get(0),
CassandraContainer.CQL_PORT,
container.getLocalDatacenter(), container.getUsername(), container.getPassword());
keyspace, container.getLocalDatacenter(), container.getUsername(), container.getPassword());
return new CassandraConnectionClosableImpl(params, network);
}

static CassandraConnection forParams(String host,
int port,
String datacenter,
String keyspace,
String username,
String password) {
var params = new CassandraConnectionImpl.ParamsImpl(host, port, datacenter, username, password);
var params = new CassandraConnectionImpl.ParamsImpl(host, port, keyspace, datacenter, username, password);
return new CassandraConnectionClosableImpl(params, null);
}
}
Loading

0 comments on commit 2fa3d80

Please sign in to comment.