Skip to content

Commit 6173844

Browse files
committed
merged conflicts
2 parents ede7238 + cfcea24 commit 6173844

17 files changed

+595
-14
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
language: java
2-
sudo: false
2+
sudo: true
3+
34
jdk:
45
- oraclejdk8
56
install: mvn install -Dgpg.skip=true

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,24 @@ This means that if you are using the driver version 2.1.9 you can add this depen
2727
or whatever the latest build version of the migration tool is. Check the version at the [Maven repository](http://mvnrepository.com/artifact/io.smartcat/cassandra-migration-tool).
2828

2929
# Examples
30-
In tests [MigrationEngineTest](src/test/java/io/smartcat/migration/MigrationEngineTest.java) can serve as an example of one use case which this tool can cover. There is already defined table in cassandra DB with production data in it and requirement is to add column and populate it with data.
30+
We have two test cases which explain common problems which migration tool can solve. This is only subset of use cases but we think these are most frequent once:
3131

32-
The initial table is simple (can be found in [init db.cql file](src/test/resources/db.cql)) and we use migration classes to do the following:
32+
**First use case** touches problem of adding new field and populating historic data with value. Cassandra does not have DDL `default` attribute, so you must populate data on application level. [MigrationEngineBooksTest](src/test/java/io/smartcat/migration/MigrationEngineBooksTest.java) can serve as an example of that use case which this tool can cover.
33+
34+
The initial table is simple (can be found in [books.cql file](src/test/resources/books.cql)) and we use migration classes to do the following:
3335

3436
1. Populate data initially with first `data` migration [InsertBooksMigration](src/test/java/io/smartcat/migration/migrations/data/InsertBooksMigration.java)
3537
2. Add `genre` column with `schema` migration [AddBookGenreFieldMigration](src/test/java/io/smartcat/migration/migrations/schema/AddBookGenreFieldMigration.java)
3638
3. Populate `genre` column with second `data` migration [AddGenreMigration](src/test/java/io/smartcat/migration/migrations/data/AddGenreMigration.java)
3739

40+
**Second use case** touches problem of query based modeling. Cassandra has good performance because you model your data as you will query it. Often after initial modeling you have request to read it based on different criteria. In Cassandra you do this with another table which is optimized for new requirements. You need to populate this new table with existing data and you can solve this with migration tool. [MigrationEngineItemsTest](src/test/java/io/smartcat/migration/MigrationEngineItemsTest.java) can serve as an example of that use case which this tool can cover.
41+
42+
The initial table is simple (can be found in [items.cql file](src/test/resources/items.cql)) and we use migration classes to do the following:
43+
44+
1. Populate data initially with first `data` migration [InsertInitialItemsMigration](src/test/java/io/smartcat/migration/migrations/data/InsertInitialItemsMigration.java)
45+
2. Add `items_by_number_external_id` table with `schema` migration [CreateItemByNumberAndExternalIdMigration](src/test/java/io/smartcat/migration/migrations/schema/CreateItemByNumberAndExternalIdMigration.java)
46+
3. Populate `items_by_number_external_id` table with second `data` migration [PopulateItemByNumberAndExternalIdMigration](src/test/java/io/smartcat/migration/migrations/data/PopulateItemByNumberAndExternalIdMigration.java)
47+
3848
# Schema agreement
3949
When executing schema migrations it is necessary to wait for cluster to propagate schema on all nodes. Schema agreement is implemented based on this [fix](https://datastax-oss.atlassian.net/browse/JAVA-669) and is exposed through [Migration](src/main/java/io/smartcat/migration/Migration.java) abstract class.
4050
To execute a statement with schema agreement you can use `executeWithSchemaAgreement` method.

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<guava.version>16.0.1</guava.version>
1717
<slf4j.version>1.7.7</slf4j.version>
1818
<junit.version>4.9</junit.version>
19+
<mockito.version>2.0.52-beta</mockito.version>
1920
</properties>
2021

2122
<dependencies>
@@ -35,6 +36,11 @@
3536
<version>${guava.version}</version>
3637
</dependency>
3738

39+
<dependency>
40+
<groupId>org.mockito</groupId>
41+
<artifactId>mockito-core</artifactId>
42+
<version>${mockito.version}</version>
43+
</dependency>
3844
<dependency>
3945
<groupId>org.cassandraunit</groupId>
4046
<artifactId>cassandra-unit</artifactId>

src/main/java/io/smartcat/migration/exceptions/MigrationException.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
public class MigrationException extends Exception {
44

5-
private static final long serialVersionUID = 939170349798471411L;
5+
private static final long serialVersionUID = 939170349798471411L;
66

7-
public MigrationException(final String message) {
7+
public MigrationException(final String message) {
88
super(message);
99
}
1010

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.smartcat.migration;
2+
3+
import com.datastax.driver.core.*;
4+
5+
import java.util.ArrayList;
6+
import java.util.List;
7+
8+
public class BaseTest {
9+
10+
public void truncateTables(final String keyspace, final Session session) {
11+
for (final String table : tables(keyspace, session)) {
12+
session.execute(String.format("TRUNCATE %s.%s;", keyspace, table));
13+
}
14+
}
15+
16+
private List<String> tables(final String keyspace, final Session session) {
17+
final List<String> tables = new ArrayList<>();
18+
final Cluster cluster = session.getCluster();
19+
final Metadata meta = cluster.getMetadata();
20+
final KeyspaceMetadata keyspaceMeta = meta.getKeyspace(keyspace);
21+
for (final TableMetadata tableMeta : keyspaceMeta.getTables()) {
22+
tables.add(tableMeta.getName());
23+
}
24+
25+
return tables;
26+
}
27+
28+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.smartcat.migration;
2+
3+
import static com.google.common.base.Preconditions.checkNotNull;
4+
import static com.google.common.collect.Iterables.tryFind;
5+
6+
import com.datastax.driver.core.ColumnMetadata;
7+
import com.datastax.driver.core.KeyspaceMetadata;
8+
import com.datastax.driver.core.Metadata;
9+
import com.datastax.driver.core.Session;
10+
import com.datastax.driver.core.TableMetadata;
11+
import com.google.common.base.Optional;
12+
13+
public class CassandraMetadataAnalyzer {
14+
15+
private Session session;
16+
17+
public CassandraMetadataAnalyzer(Session session) {
18+
checkNotNull(session, "Session cannot be null");
19+
checkNotNull(session.getLoggedKeyspace(), "Session must be logged into a keyspace");
20+
this.session = session;
21+
}
22+
23+
public boolean columnExistInTable(String columnName, String tableName) {
24+
TableMetadata table = getTableMetadata(this.session, tableName);
25+
Optional<ColumnMetadata> column = tryFind(table.getColumns(), new ColumnNameMatcher(columnName));
26+
return column.isPresent();
27+
}
28+
29+
private static TableMetadata getTableMetadata(Session session, String tableName) {
30+
Metadata metadata = session.getCluster().getMetadata();
31+
KeyspaceMetadata keyspaceMetadata = metadata.getKeyspace(session.getLoggedKeyspace());
32+
return keyspaceMetadata.getTable(tableName);
33+
}
34+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package io.smartcat.migration;
2+
3+
import static io.smartcat.migration.MigrationType.SCHEMA;
4+
import static org.hamcrest.CoreMatchers.is;
5+
import static org.junit.Assert.assertThat;
6+
import static org.mockito.Mockito.mock;
7+
import static org.mockito.Mockito.when;
8+
9+
import org.junit.Before;
10+
import org.junit.Test;
11+
import org.mockito.Mockito;
12+
import org.mockito.stubbing.OngoingStubbing;
13+
14+
import com.datastax.driver.core.ResultSet;
15+
import com.datastax.driver.core.Row;
16+
import com.datastax.driver.core.Session;
17+
import com.datastax.driver.core.Statement;
18+
19+
import io.smartcat.migration.migrations.schema.AddBookGenreFieldMigration;
20+
21+
public class CassandraVersionerTest {
22+
private CassandraVersioner versioner;
23+
private Session session;
24+
private ResultSet versionResultSet;
25+
26+
@Before
27+
public void setUp() throws Exception {
28+
session = mock(Session.class);
29+
versioner = new CassandraVersioner(session);
30+
versionResultSet = mock(ResultSet.class);
31+
}
32+
33+
@Test
34+
public void whenSchemaVersionTableIsEmptyThenCurrentVersionShouldBe0() throws Exception {
35+
expectRetrieveEmptyCurrentVersion();
36+
37+
int currentVersion = versioner.getCurrentVersion(SCHEMA);
38+
39+
assertThat(currentVersion, is(0));
40+
}
41+
42+
@Test
43+
public void whenSchemaVersionTableIsNotEmptyThenCurrentVersionShouldBeRetrievedFromTheTable() throws Exception {
44+
int expectedVersion = 1;
45+
46+
expectRetrieveCurrentVersion(expectedVersion);
47+
48+
int currentVersion = versioner.getCurrentVersion(SCHEMA);
49+
50+
assertThat(currentVersion, is(expectedVersion));
51+
}
52+
53+
@Test
54+
public void updateVersionSucess() throws Exception {
55+
versioner.updateVersion(new AddBookGenreFieldMigration(1));
56+
}
57+
58+
private void expectRetrieveEmptyCurrentVersion() {
59+
expectRetrieveVersionResultSetWithRow(null);
60+
}
61+
62+
private void expectRetrieveCurrentVersion(int expectedVersion) {
63+
Row row = expectRowWithVersion(expectedVersion);
64+
expectRetrieveVersionResultSetWithRow(row);
65+
}
66+
67+
private void expectRetrieveVersionResultSetWithRow(Row row) {
68+
whenSessionExecuteQuery().thenReturn(versionResultSet);
69+
whenRetrieveRowFromVersionResultSet().thenReturn(row);
70+
}
71+
72+
private Row expectRowWithVersion(int version) {
73+
Row row = mock(Row.class);
74+
when(row.getInt("version")).thenReturn(version);
75+
return row;
76+
}
77+
78+
private OngoingStubbing<ResultSet> whenSessionExecuteQuery() {
79+
return when(session.execute(Mockito.any(Statement.class)));
80+
}
81+
82+
private OngoingStubbing<Row> whenRetrieveRowFromVersionResultSet() {
83+
return when(versionResultSet.one());
84+
}
85+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.smartcat.migration;
2+
3+
import com.datastax.driver.core.ColumnMetadata;
4+
import com.google.common.base.Predicate;
5+
6+
public class ColumnNameMatcher implements Predicate<ColumnMetadata> {
7+
private String columnName;
8+
9+
public ColumnNameMatcher(String columnName) {
10+
this.columnName = columnName;
11+
}
12+
13+
public boolean apply(ColumnMetadata column) {
14+
return column.getName().equals(columnName);
15+
}
16+
}

src/test/java/io/smartcat/migration/MigrationEngineTest.java renamed to src/test/java/io/smartcat/migration/MigrationEngineBooksTest.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22

33
import static junit.framework.Assert.assertEquals;
44

5-
import java.util.HashMap;
6-
import java.util.Map;
7-
85
import org.cassandraunit.CQLDataLoader;
96
import org.cassandraunit.dataset.cql.ClassPathCQLDataSet;
107
import org.cassandraunit.utils.EmbeddedCassandraServerHelper;
@@ -25,14 +22,14 @@
2522
import io.smartcat.migration.migrations.data.InsertBooksMigration;
2623
import io.smartcat.migration.migrations.schema.AddBookGenreFieldMigration;
2724

28-
public class MigrationEngineTest {
25+
public class MigrationEngineBooksTest extends BaseTest {
2926

30-
private static final Logger LOGGER = LoggerFactory.getLogger(MigrationEngineTest.class);
27+
private static final Logger LOGGER = LoggerFactory.getLogger(MigrationEngineBooksTest.class);
3128

3229
private static final String CONTACT_POINT = "localhost";
3330
private static final int PORT = 9142;
34-
private static final String KEYSPACE = "migration_test";
35-
private static final String CQL = "db.cql";
31+
private static final String KEYSPACE = "migration_test_books";
32+
private static final String CQL = "books.cql";
3633

3734
private static Session session;
3835
private static Cluster cluster;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package io.smartcat.migration;
2+
3+
import static junit.framework.Assert.assertEquals;
4+
5+
import com.datastax.driver.core.*;
6+
import com.datastax.driver.core.querybuilder.QueryBuilder;
7+
import io.smartcat.migration.migrations.data.InsertInitialItemsMigration;
8+
import io.smartcat.migration.migrations.data.PopulateItemByNumberAndExternalIdMigration;
9+
import io.smartcat.migration.migrations.schema.CreateItemByNumberAndExternalIdMigration;
10+
import org.cassandraunit.CQLDataLoader;
11+
import org.cassandraunit.dataset.cql.ClassPathCQLDataSet;
12+
import org.cassandraunit.utils.EmbeddedCassandraServerHelper;
13+
import org.junit.After;
14+
import org.junit.AfterClass;
15+
import org.junit.BeforeClass;
16+
import org.junit.Test;
17+
import org.slf4j.Logger;
18+
import org.slf4j.LoggerFactory;
19+
20+
import java.util.List;
21+
22+
public class MigrationEngineItemsTest extends BaseTest {
23+
24+
private static final Logger LOGGER = LoggerFactory.getLogger(MigrationEngineItemsTest.class);
25+
26+
private static final String CONTACT_POINT = "localhost";
27+
private static final int PORT = 9142;
28+
private static final String KEYSPACE = "migration_test_items";
29+
private static final String CQL = "items.cql";
30+
31+
private static Session session;
32+
private static Cluster cluster;
33+
34+
@BeforeClass
35+
public static void init() throws Exception {
36+
LOGGER.info("Starting embedded cassandra server");
37+
EmbeddedCassandraServerHelper.startEmbeddedCassandra("another-cassandra.yaml");
38+
39+
LOGGER.info("Connect to embedded db");
40+
cluster = Cluster.builder().addContactPoints(CONTACT_POINT).withPort(PORT).build();
41+
session = cluster.connect();
42+
43+
LOGGER.info("Initialize keyspace");
44+
final CQLDataLoader cqlDataLoader = new CQLDataLoader(session);
45+
cqlDataLoader.load(new ClassPathCQLDataSet(CQL, false, true, KEYSPACE));
46+
}
47+
48+
@After
49+
public void cleanUp() {
50+
truncateTables(KEYSPACE, session);
51+
}
52+
53+
@AfterClass
54+
public static void tearDown() {
55+
if (cluster != null) {
56+
cluster.close();
57+
cluster = null;
58+
}
59+
}
60+
61+
@Test
62+
public void initial_insert_test() {
63+
final int count = 100;
64+
65+
final MigrationResources resources = new MigrationResources();
66+
resources.addMigration(new InsertInitialItemsMigration(count, 1));
67+
final boolean result = MigrationEngine.withSession(session).migrate(resources);
68+
69+
assertEquals(true, result);
70+
71+
final List<Row> rows = session.execute(QueryBuilder.select().from("items_by_id")).all();
72+
assertEquals(count, rows.size());
73+
}
74+
75+
@Test
76+
public void test_migrations() {
77+
final int count = 100;
78+
79+
final MigrationResources resources = new MigrationResources();
80+
resources.addMigration(new InsertInitialItemsMigration(count, 1));
81+
resources.addMigration(new CreateItemByNumberAndExternalIdMigration(1));
82+
resources.addMigration(new PopulateItemByNumberAndExternalIdMigration(2));
83+
final boolean result = MigrationEngine.withSession(session).migrate(resources);
84+
85+
assertEquals(true, result);
86+
87+
final List<Row> rows = session.execute(QueryBuilder.select().from("items_by_number_external_id")).all();
88+
assertEquals(count, rows.size());
89+
}
90+
91+
}

0 commit comments

Comments
 (0)