diff --git a/.travis.yml b/.travis.yml index 83c1e6f..8c29aff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,18 @@ language: java -jdk: - - openjdk7 - - oraclejdk7 - - oraclejdk8 env: global: - secure: "q+fkZBCN8ET5RrgaG4RGt1t1aSjsL6LN6BSt/Yvx2H5a2DtGmNA/A/gcAEnKlyv0BgXAcrzAzCCIgXvt2P4om5DcBU/yOTEga+/46r7+iVnmfQGcW81NHQA1rlIYvuBqXGDo9yo1B3eRr8vTj3fzEE3K8jjchHQUlgRdUum3DNKeZwACodV2fpj9ZslyoX4HRpWg3ctqvB0R7/4NwtXnXrOw8hHDF8OrQK3JiCxAoZnA16/Fwc0d8yN4Or10N1XiWbNdLFek+Y3nVTdGRUZjsqp/VhvgIwzmtnuiCeF2iuMCpYy6C9SAFG4Tyn5VLmFzEqXMrbxuMBp8c2GCcFQb0jxEiWKsT0Nufqc1pYlUSl2S12D8yokEo5H9/NcH/2p2b5zqzcWzFe1c5YEn0Ktj8d/01GDYfkuPyoQ0UmjC6h68iozk5mogPT0t7eUf7i0wll72v4kGB2xOK3VY+53LA7DS+f/0HeDi47tXgPkA8bg2dGZTTD+JHXcqyMTt9Ey96a42cauLQ4PGfujc6fPJUw31sxx2IURj1USdxut/a5PEa+LL+xGrKKgOW4GMUwjrYMnLf9e3Y/uR4EoHmYYwsoNtD0g6bEt7C83JZrQd5Sp3mN6gEU0sjp2/iBPqS+tB3z6eRUur8ctnk6EC82WHmRwZHeoKLVOktAPCKumBnWY=" - secure: "04af3/9b67O5xd1U8GDhCqRQedHM3RP5HokdsOwAe8vN3EyyyKWXQafBkKsPvmDh5Uu/CYQppOYS4pQxB9ikweTfj34DbyyxpqJmjYE4KFvdQuFd0msyXhLCg6xfvS4KO6zjUQ8/c3rNQap4hx/icQ50/NES4rkUkxIZ/VKQ4jPXcBzPegEC6Le50Vw2tR8FT4erdNuABnGf1WnWGUUa3i6xdQQPyw8kdTIun08HxE6M9F+JJRH8jH3b7KizQhGdACAk4fnCOmFSgu7pm6ACXRJYqAfg055i5mr77yZXfeUIcIY3l45uY1uR8sxEbLUE/KwwlLGLVZWDI4xU6JIGisbrmMce+vz6YKUT9gHF3iAEJ5e4N18nJcRyHVrqcuRzv5Py0rFPZ70dr7aW/tk0JrTz6+FZ4FNIOdvIQe4qWy2TVns0EkERdtYGTdsigWfa/sKF/P5+/2foUOlnR06p55NHpIjaHRKy/XFVV1gyURUlRUGExVoIMX21bAMxGYMFMH7LfddRsly028lXwibRMkQGBeyVRYKQmqJvN3mTPbuAWmZZdaVpqn1jkgETlT6/qz43zv9y8jAOzZ22SeHEXe3NiexChqkAJWIH3cBYshMhy8H1fmYAIVYHvI+BPsbi+qDYSnAlAUDqoLWXPAvWUX89dAIXYRNcKplzpFsjWvM=" +matrix: + include: + - jdk: openjdk7 + script: mvn test + - jdk: oraclejdk7 + script: mvn test + - jdk: oraclejdk8 + script: + - mvn test + - mvn verify deploy: provider: script script: ./scripts/deploy.sh diff --git a/README.md b/README.md index d13a31e..32169e8 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ Import this library as a dependency (Maven example): ``` +***NOTE:** Integration test in Travis CI is only run against Oracle JDK 1.8, due to embedded Cassandra's dependencies on JDK 1.8* + ### Migration version table ``` shell @@ -140,7 +142,6 @@ Keyspace: ### Limitations - * Baselining not supported yet * The tool does not roll back the database upon migration failure. You're expected to manually restore backup. ## Project Rationale @@ -161,6 +162,14 @@ There are various reasons why Kotlin was chosen, but three main reasons are: * stronger `null` checks (enforced at the compiler level), and * better Java collection support (e.g. additional functional features) +## Testing + +Run `mvn test` to run the unit tests. + +Run `mvn verify` to run the integration tests. + +***NOTE:** The integration test might complain about some missing SIGAR binaries, this can be safely ignored. If you wish, you can download the missing binaries and set `java.library.path` parameter to point to the containing folder (e.g. `mvn verify -Djava.library.path=lib` where `lib` is the `/lib` folder relative to the project root).* + ## Contributing We follow the "[fork-and-pull]" Git workflow. @@ -207,4 +216,5 @@ https://github.com/builtamont/cassandra-migration/releases [Flyway]: https://flywaydb.org/ [Flyway's project license page]: https://github.com/flyway/flyway/blob/master/LICENSE [fork-and-pull]: https://help.github.com/articles/using-pull-requests -[LICENSE]: LICENSE \ No newline at end of file +[LICENSE]: LICENSE +[SIGAR]: https://support.hyperic.com/display/SIGAR/Home \ No newline at end of file diff --git a/pom.xml b/pom.xml index 158a13b..bedc6ba 100755 --- a/pom.xml +++ b/pom.xml @@ -194,7 +194,13 @@ org.apache.maven.plugins maven-failsafe-plugin - 2.19 + 2.19.1 + + + 3 + true + -Xmx1024m -XX:MaxPermSize=256m + diff --git a/src/main/java/com/builtamont/cassandra/migration/CassandraMigration.kt b/src/main/java/com/builtamont/cassandra/migration/CassandraMigration.kt index 893fcc3..6eca77d 100644 --- a/src/main/java/com/builtamont/cassandra/migration/CassandraMigration.kt +++ b/src/main/java/com/builtamont/cassandra/migration/CassandraMigration.kt @@ -25,6 +25,7 @@ import com.builtamont.cassandra.migration.api.configuration.CassandraMigrationCo import com.builtamont.cassandra.migration.api.configuration.MigrationConfigs import com.builtamont.cassandra.migration.api.resolver.MigrationResolver import com.builtamont.cassandra.migration.config.Keyspace +import com.builtamont.cassandra.migration.internal.command.Baseline import com.builtamont.cassandra.migration.internal.command.Initialize import com.builtamont.cassandra.migration.internal.command.Migrate import com.builtamont.cassandra.migration.internal.command.Validate @@ -61,6 +62,16 @@ class CassandraMigration : CassandraMigrationConfiguration { */ lateinit var configs: MigrationConfigs + /** + * The baseline version. + */ + private val baselineVersion = MigrationVersion.Companion.fromVersion("1") + + /** + * The baseline description. + */ + private val baselineDescription = "<< Cassandra Baseline >>" + /** * CassandraMigration initialization. */ @@ -104,7 +115,7 @@ class CassandraMigration : CassandraMigrationConfiguration { */ fun info(): MigrationInfoService { return execute(object : Action { - override fun execute(session: Session): MigrationInfoService? { + override fun execute(session: Session): MigrationInfoService { val migrationResolver = createMigrationResolver() val schemaVersionDAO = SchemaVersionDAO(session, keyspace, MigrationVersion.CURRENT.table) val migrationInfoService = MigrationInfoServiceImpl(migrationResolver, schemaVersionDAO, configs.target, false, true) @@ -128,8 +139,8 @@ class CassandraMigration : CassandraMigrationConfiguration { val validationError = execute(object : Action { override fun execute(session: Session): String? { val migrationResolver = createMigrationResolver() - val schemaVersionDao = SchemaVersionDAO(session, keyspace, MigrationVersion.CURRENT.table) - val validate = Validate(migrationResolver, configs.target, schemaVersionDao, true, false) + val schemaVersionDAO = SchemaVersionDAO(session, keyspace, MigrationVersion.CURRENT.table) + val validate = Validate(migrationResolver, configs.target, schemaVersionDAO, true, false) return validate.run() } }) @@ -143,8 +154,14 @@ class CassandraMigration : CassandraMigrationConfiguration { * Baselines an existing database, excluding all migrations up to and including baselineVersion. */ fun baseline() { - // TODO: Create the Cassandra migration implementation, refer to existing PR: https://github.com/Contrast-Security-OSS/cassandra-migration/pull/17 - throw NotImplementedException() + execute(object : Action { + override fun execute(session: Session): Unit { + val migrationResolver = createMigrationResolver() + val schemaVersionDAO = SchemaVersionDAO(session, keyspace, MigrationVersion.CURRENT.table) + val baseline = Baseline(migrationResolver, baselineVersion, schemaVersionDAO, baselineDescription, keyspace.cluster.username) + baseline.run() + } + }) } /** @@ -197,7 +214,7 @@ class CassandraMigration : CassandraMigrationConfiguration { else throw CassandraMigrationException("Keyspace: " + keyspace.name + " does not exist.") - result = action.execute(session)!! + result = action.execute(session) } finally { if (null != session && !session.isClosed) try { @@ -259,7 +276,7 @@ class CassandraMigration : CassandraMigrationConfiguration { * @param session The Cassandra session connection to use to execute the migration. * @return The action result. */ - fun execute(session: Session): T? + fun execute(session: Session): T } diff --git a/src/main/java/com/builtamont/cassandra/migration/CommandLine.kt b/src/main/java/com/builtamont/cassandra/migration/CommandLine.kt index 5c737dd..d94ca62 100644 --- a/src/main/java/com/builtamont/cassandra/migration/CommandLine.kt +++ b/src/main/java/com/builtamont/cassandra/migration/CommandLine.kt @@ -23,7 +23,6 @@ import com.builtamont.cassandra.migration.internal.util.logging.Log import com.builtamont.cassandra.migration.internal.util.logging.LogFactory import com.builtamont.cassandra.migration.internal.util.logging.console.ConsoleLog import com.builtamont.cassandra.migration.internal.util.logging.console.ConsoleLogCreator -import java.util.* /** * Cassandra migration command line runner. @@ -40,6 +39,11 @@ object CommandLine { */ val VALIDATE = "validate" + /** + * Command to trigger baseline action. + */ + val BASELINE = "baseline" + /** * Logging support. */ @@ -69,6 +73,8 @@ object CommandLine { cm.migrate() } else if (VALIDATE.equals(operation, ignoreCase = true)) { cm.validate() + } else if (BASELINE.equals(operation, ignoreCase = true)) { + cm.baseline() } } @@ -76,15 +82,7 @@ object CommandLine { * Get a list of applicable operations. */ private fun determineOperations(args: Array): List { - val operations = ArrayList() - - for (arg in args) { - if (!arg.startsWith("-")) { - operations.add(arg) - } - } - - return operations + return args.filterNot { it.startsWith("-") } } /** @@ -124,6 +122,7 @@ object CommandLine { LOG.info("========") LOG.info("migrate : Migrates the database") LOG.info("validate : Validates the applied migrations against the available ones") + LOG.info("baseline : Baselines an existing database, excluding all migrations up to, and including baselineVersion") LOG.info("") LOG.info("Add -X to print debug output") LOG.info("Add -q to suppress all output, except for errors and warnings") diff --git a/src/main/java/com/builtamont/cassandra/migration/internal/command/Baseline.kt b/src/main/java/com/builtamont/cassandra/migration/internal/command/Baseline.kt new file mode 100644 index 0000000..ded66e3 --- /dev/null +++ b/src/main/java/com/builtamont/cassandra/migration/internal/command/Baseline.kt @@ -0,0 +1,74 @@ +/** + * File : Baseline.kt + * License : + * Original - Copyright (c) 2015 - 2016 Contrast Security + * Derivative - Copyright (c) 2016 Citadel Technology Solutions Pte Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.builtamont.cassandra.migration.internal.command + +import com.builtamont.cassandra.migration.api.CassandraMigrationException +import com.builtamont.cassandra.migration.api.MigrationVersion +import com.builtamont.cassandra.migration.api.resolver.MigrationResolver +import com.builtamont.cassandra.migration.internal.dbsupport.SchemaVersionDAO + +/** + * Handles the baseline command. + * + * @param migrationResolver The Cassandra migration resolver. + * @param baselineVersion The baseline version of the migration. + * @param schemaVersionDAO The Cassandra migration schema version DAO. + * @param baselineDescription The baseline version description / comments. + * @param user The user to execute the migration as. + */ +class Baseline( + private val migrationResolver: MigrationResolver, + private val baselineVersion: MigrationVersion, + private val schemaVersionDAO: SchemaVersionDAO, + private val baselineDescription: String, + private val user: String +) { + + /** + * Runs the migration baselining. + * + * @return The number of successfully applied migration baselining. + * @throws CassandraMigrationException when migration baselining failed for any reason. + */ + @Throws(CassandraMigrationException::class) + fun run() { + val baselineMigration = schemaVersionDAO.baselineMarker + if (schemaVersionDAO.hasAppliedMigrations()) { + val msg = "Unable to baseline metadata table ${schemaVersionDAO.tableName} as it already contains migrations" + throw CassandraMigrationException(msg) + } + + if (schemaVersionDAO.hasBaselineMarker()) { + val isNotBaselineByVersion = !(baselineMigration.version?.equals(baselineVersion) ?: false) + val isNotBaselineByDescription = !baselineMigration.description.equals(baselineDescription) + if (isNotBaselineByVersion || isNotBaselineByDescription) { + val msg = "Unable to baseline metadata table ${schemaVersionDAO.tableName} with ($baselineVersion, $baselineDescription)" + + " as it has already been initialized with (${baselineMigration.version}, ${baselineMigration.description})" + throw CassandraMigrationException(msg) + } + } else { + if (baselineVersion.equals(MigrationVersion.fromVersion("0"))) { + val msg = "Unable to baseline metadata table ${schemaVersionDAO.tableName} with version 0 as this version was used for schema creation" + throw CassandraMigrationException(msg) + } + schemaVersionDAO.addBaselineMarker(baselineVersion, baselineDescription, user) + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/builtamont/cassandra/migration/internal/command/Migrate.kt b/src/main/java/com/builtamont/cassandra/migration/internal/command/Migrate.kt index e9ffbd6..f96ad98 100644 --- a/src/main/java/com/builtamont/cassandra/migration/internal/command/Migrate.kt +++ b/src/main/java/com/builtamont/cassandra/migration/internal/command/Migrate.kt @@ -219,7 +219,7 @@ class Migrate( * @return The migration success log message. */ fun successLogMsg(): String { - return "Successfully applied $count migration to keyspace $keyspaceName (execution time ${TimeFormat.format(executionTime)})" + return "Successfully applied $count migration(s) to keyspace $keyspaceName (execution time ${TimeFormat.format(executionTime)})" } when (count) { diff --git a/src/main/java/com/builtamont/cassandra/migration/internal/command/Validate.kt b/src/main/java/com/builtamont/cassandra/migration/internal/command/Validate.kt index 958d061..9204df4 100644 --- a/src/main/java/com/builtamont/cassandra/migration/internal/command/Validate.kt +++ b/src/main/java/com/builtamont/cassandra/migration/internal/command/Validate.kt @@ -72,7 +72,8 @@ class Validate( * @param executionTime The total time taken to perform this migration run (in ms). */ private fun logSummary(count: Int, executionTime: Long) { - LOG.info("Validated %d migrations (execution time %s)".format(count, TimeFormat.format(executionTime))) + val time = TimeFormat.format(executionTime) + LOG.info("Validated $count migrations (execution time $time)") } /** diff --git a/src/main/java/com/builtamont/cassandra/migration/internal/dbsupport/SchemaVersionDAO.java b/src/main/java/com/builtamont/cassandra/migration/internal/dbsupport/SchemaVersionDAO.java index ea26272..be6ae8e 100755 --- a/src/main/java/com/builtamont/cassandra/migration/internal/dbsupport/SchemaVersionDAO.java +++ b/src/main/java/com/builtamont/cassandra/migration/internal/dbsupport/SchemaVersionDAO.java @@ -30,13 +30,13 @@ import com.datastax.driver.core.querybuilder.QueryBuilder; import com.datastax.driver.core.querybuilder.Select; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; +import java.util.*; import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; +/** + * Schema migrations table Data Access Object. + */ // TODO: Convert to Kotlin code... Some challenges with Mockito mocking :) public class SchemaVersionDAO { @@ -49,20 +49,35 @@ public class SchemaVersionDAO { private CachePrepareStatement cachePs; private ConsistencyLevel consistencyLevel; + /** + * Creates a new schema version DAO. + * + * @param session The Cassandra session connection to use to execute the migration. + * @param keyspace The Cassandra keyspace to connect to. + * @param tableName The Cassandra migration version table name. + */ public SchemaVersionDAO(Session session, Keyspace keyspace, String tableName) { this.session = session; this.keyspace = keyspace; this.tableName = tableName; this.cachePs = new CachePrepareStatement(session); - //If running on a single host, don't force ConsistencyLevel.ALL - this.consistencyLevel = - session.getCluster().getMetadata().getAllHosts().size() > 1 ? ConsistencyLevel.ALL : ConsistencyLevel.ONE; + + // If running on a single host, don't force ConsistencyLevel.ALL + boolean isClustered = session.getCluster().getMetadata().getAllHosts().size() > 1; + this.consistencyLevel = isClustered ? ConsistencyLevel.ALL : ConsistencyLevel.ONE; + } + + public String getTableName() { + return tableName; } public Keyspace getKeyspace() { return this.keyspace; } + /** + * Create schema migration version table if it does not exists. + */ public void createTablesIfNotExist() { if (tablesExist()) { return; @@ -96,6 +111,11 @@ public void createTablesIfNotExist() { session.execute(statement); } + /** + * Check if schema migration version table has already been created. + * + * @return {@code true} if schema migration version table exists in the keyspace. + */ public boolean tablesExist() { boolean schemaVersionTableExists = false; boolean schemaVersionCountsTableExists = false; @@ -134,6 +154,11 @@ public boolean tablesExist() { return schemaVersionTableExists && schemaVersionCountsTableExists; } + /** + * Add applied migration record into the schema migration version table. + * + * @param appliedMigration The applied migration. + */ public void addAppliedMigration(AppliedMigration appliedMigration) { createTablesIfNotExist(); @@ -141,11 +166,17 @@ public void addAppliedMigration(AppliedMigration appliedMigration) { int versionRank = calculateVersionRank(version); PreparedStatement statement = cachePs.prepare( - "INSERT INTO " + keyspace.getName() + "." + tableName + - " (version_rank, installed_rank, version, description, type, script, checksum, installed_on," + - " installed_by, execution_time, success)" + - " VALUES" + - " (?, ?, ?, ?, ?, ?, ?, dateOf(now()), ?, ?, ?);" + "INSERT INTO " + keyspace.getName() + "." + tableName + "(" + + " version_rank, installed_rank, version," + + " description, type, script," + + " checksum, installed_on, installed_by," + + " execution_time, success" + + ") VALUES (" + + " ?, ?, ?," + + " ?, ?, ?," + + " ?, dateOf(now()), ?," + + " ?, ?" + + ");" ); statement.setConsistencyLevel(this.consistencyLevel); @@ -165,7 +196,7 @@ public void addAppliedMigration(AppliedMigration appliedMigration) { } /** - * Retrieve the applied migrations from the metadata table. + * Retrieve the applied migrations from the schema migration version table. * * @return The applied migrations. */ @@ -208,11 +239,133 @@ public List findAppliedMigrations() { )); } - //order by version_rank not necessary here as it eventually gets saved in TreeMap that uses natural ordering + // NOTE: Order by `version_rank` not necessary here, as it eventually gets saved in TreeMap + // that uses natural ordering + return resultsList; + } + + /** + * Retrieve the applied migrations from the metadata table. + * + * @param migrationTypes The migration types to find. + * @return The applied migrations. + */ + public List findAppliedMigrations(MigrationType... migrationTypes) { + if (!tablesExist()) { + return new ArrayList<>(); + } + + Select select = QueryBuilder + .select() + .column("version_rank") + .column("installed_rank") + .column("version") + .column("description") + .column("type") + .column("script") + .column("checksum") + .column("installed_on") + .column("installed_by") + .column("execution_time") + .column("success") + .from(keyspace.getName(), tableName); + + select.setConsistencyLevel(ConsistencyLevel.ALL); + ResultSet results = session.execute(select); + List resultsList = new ArrayList<>(); + List migTypeList = Arrays.asList(migrationTypes); + for (Row row : results) { + MigrationType migType = MigrationType.valueOf(row.getString("type")); + if(migTypeList.contains(migType)){ + resultsList.add(new AppliedMigration( + row.getInt("version_rank"), + row.getInt("installed_rank"), + MigrationVersion.Companion.fromVersion(row.getString("version")), + row.getString("description"), + migType, + row.getString("script"), + row.getInt("checksum"), + row.getTimestamp("installed_on"), + row.getString("installed_by"), + row.getInt("execution_time"), + row.getBool("success") + )); + } + } + // NOTE: Order by `version_rank` not necessary here, as it eventually gets saved in TreeMap + // that uses natural ordering return resultsList; } + /** + * Check if the keyspace has applied migrations. + * + * @return {@code true} if the keyspace has applied migrations. + */ + public boolean hasAppliedMigrations() { + if (!tablesExist()) { + return false; + } + + createTablesIfNotExist(); + List filteredMigrations = new ArrayList<>(); + List appliedMigrations = findAppliedMigrations(); + for (AppliedMigration appliedMigration : appliedMigrations) { + if (!appliedMigration.getType().equals(MigrationType.BASELINE)) { + filteredMigrations.add(appliedMigration); + } + } + return !filteredMigrations.isEmpty(); + } + + /** + * Add a baseline version marker. + * + * @param baselineVersion The baseline version. + * @param baselineDescription the baseline version description. + * @param user The user's username executing the baselining. + */ + public void addBaselineMarker(final MigrationVersion baselineVersion, final String baselineDescription, final String user) { + addAppliedMigration( + new AppliedMigration( + baselineVersion, + baselineDescription, + MigrationType.BASELINE, + baselineDescription, + 0, + user, + 0, + true + ) + ); + } + + /** + * Get the baseline marker's applied migration. + * + * @return The baseline marker's applied migration. + */ + public AppliedMigration getBaselineMarker() { + List appliedMigrations = findAppliedMigrations(MigrationType.BASELINE); + return appliedMigrations.isEmpty() ? null : appliedMigrations.get(0); + } + + /** + * Check if schema migration version table has a baseline marker. + * + * @return {@code true} if the schema migration version table has a baseline marker. + */ + public boolean hasBaselineMarker() { + if (!tablesExist()) { + return false; + } + + createTablesIfNotExist(); + + return !findAppliedMigrations(MigrationType.BASELINE).isEmpty(); + } + /** * Calculates the installed rank for the new migration to be inserted. * @@ -222,27 +375,20 @@ private int calculateInstalledRank() { Statement statement = new SimpleStatement( "UPDATE " + keyspace.getName() + "." + tableName + COUNTS_TABLE_NAME_SUFFIX + " SET count = count + 1" + - "WHERE name = 'installed_rank';"); + " WHERE name = 'installed_rank'" + + ";"); + session.execute(statement); + Select select = QueryBuilder .select("count") .from(tableName + COUNTS_TABLE_NAME_SUFFIX); select.where(eq("name", "installed_rank")); + select.setConsistencyLevel(this.consistencyLevel); ResultSet result = session.execute(select); - return (int) result.one().getLong("count"); - } - - class MigrationMetaHolder { - private int versionRank; - - public MigrationMetaHolder(int versionRank) { - this.versionRank = versionRank; - } - public int getVersionRank() { - return versionRank; - } + return (int) result.one().getLong("count"); } /** @@ -257,6 +403,7 @@ private int calculateVersionRank(MigrationVersion version) { .column("version") .column("version_rank") .from(keyspace.getName(), tableName); + statement.setConsistencyLevel(this.consistencyLevel); ResultSet versionRows = session.execute(statement); @@ -275,7 +422,8 @@ private int calculateVersionRank(MigrationVersion version) { PreparedStatement preparedStatement = cachePs.prepare( "UPDATE " + keyspace.getName() + "." + tableName + " SET version_rank = ?" + - " WHERE version = ?;"); + " WHERE version = ?" + + ";"); for (int i = 0; i < migrationVersions.size(); i++) { if (version.compareTo(migrationVersions.get(i)) < 0) { @@ -294,4 +442,19 @@ private int calculateVersionRank(MigrationVersion version) { return migrationVersions.size() + 1; } + /** + * Schema migration (transient) metadata. + */ + class MigrationMetaHolder { + private int versionRank; + + public MigrationMetaHolder(int versionRank) { + this.versionRank = versionRank; + } + + public int getVersionRank() { + return versionRank; + } + } + } diff --git a/src/main/java/com/builtamont/cassandra/migration/internal/resolver/java/JavaMigrationResolver.kt b/src/main/java/com/builtamont/cassandra/migration/internal/resolver/java/JavaMigrationResolver.kt index 985d07d..92e8991 100644 --- a/src/main/java/com/builtamont/cassandra/migration/internal/resolver/java/JavaMigrationResolver.kt +++ b/src/main/java/com/builtamont/cassandra/migration/internal/resolver/java/JavaMigrationResolver.kt @@ -86,9 +86,11 @@ class JavaMigrationResolver( */ @Throws(CassandraMigrationException::class) fun extractMigrationInfo(javaMigration: JavaMigration): ResolvedMigration { - var checksum: Int? = null + val checksum: Int? if (javaMigration is MigrationChecksumProvider) { checksum = javaMigration.checksum + } else { + checksum = 0 } val version: MigrationVersion diff --git a/src/main/java/com/builtamont/cassandra/migration/internal/util/VersionPrinter.java b/src/main/java/com/builtamont/cassandra/migration/internal/util/VersionPrinter.java index 9efd2d5..492c580 100644 --- a/src/main/java/com/builtamont/cassandra/migration/internal/util/VersionPrinter.java +++ b/src/main/java/com/builtamont/cassandra/migration/internal/util/VersionPrinter.java @@ -36,6 +36,6 @@ public static void printVersion(ClassLoader classLoader) { } printed = true; String version = new ClassPathResource("version.txt", classLoader).loadAsString("UTF-8"); - LOG.info("Cassandra Migration " + version + " by Contrast Security"); + LOG.info("Cassandra Migration " + version); } } diff --git a/src/test/java/com/builtamont/cassandra/migration/CassandraMigrationIT.java b/src/test/java/com/builtamont/cassandra/migration/CassandraMigrationIT.java index 66169d3..dfbc048 100644 --- a/src/test/java/com/builtamont/cassandra/migration/CassandraMigrationIT.java +++ b/src/test/java/com/builtamont/cassandra/migration/CassandraMigrationIT.java @@ -18,11 +18,10 @@ */ package com.builtamont.cassandra.migration; -import com.builtamont.cassandra.migration.api.CassandraMigrationException; -import com.builtamont.cassandra.migration.api.MigrationInfo; -import com.builtamont.cassandra.migration.api.MigrationInfoService; -import com.builtamont.cassandra.migration.api.MigrationType; +import com.builtamont.cassandra.migration.api.*; +import com.builtamont.cassandra.migration.internal.dbsupport.SchemaVersionDAO; import com.builtamont.cassandra.migration.internal.info.MigrationInfoDumper; +import com.builtamont.cassandra.migration.internal.metadatatable.AppliedMigration; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.Row; import com.datastax.driver.core.querybuilder.QueryBuilder; @@ -186,15 +185,18 @@ public void testValidate() { cm = new CassandraMigration(); cm.getConfigs().setScriptsLocations(scriptsLocations); cm.setKeyspace(getKeyspace()); + cm.validate(); cm = new CassandraMigration(); cm.getConfigs().setScriptsLocations(new String[] { "migration/integ/java" }); cm.setKeyspace(getKeyspace()); + try { cm.validate(); Assert.fail("The expected CassandraMigrationException was not raised"); } catch (CassandraMigrationException e) { + Assert.assertTrue("expected CassandraMigrationException", true); } } @@ -228,6 +230,33 @@ public void runCmdTest() throws IOException, InterruptedException { assertThat(runCmdTestSuccess, is(true)); } + @Test + public void testBaseLine(){ + String[] scriptsLocations = {"migration/integ", "migration/integ/java"}; + CassandraMigration cm = new CassandraMigration(); + cm.getConfigs().setScriptsLocations(scriptsLocations); + cm.setKeyspace(getKeyspace()); + cm.baseline(); + + SchemaVersionDAO schemaVersionDAO = new SchemaVersionDAO(getSession(), getKeyspace(), MigrationVersion.Companion.getCURRENT().getTable()); + AppliedMigration baselineMarker = schemaVersionDAO.getBaselineMarker(); + assertThat(baselineMarker.getVersion(), is(MigrationVersion.Companion.fromVersion("1"))); + } + + @Test(expected = CassandraMigrationException.class) + public void testBaseLineWithMigrations() { + String[] scriptsLocations = { "migration/integ", "migration/integ/java" }; + CassandraMigration cm = new CassandraMigration(); + cm.getConfigs().setScriptsLocations(scriptsLocations); + cm.setKeyspace(getKeyspace()); + cm.migrate(); + + cm = new CassandraMigration(); + cm.getConfigs().setScriptsLocations(scriptsLocations); + cm.setKeyspace(getKeyspace()); + cm.baseline(); + } + private static void watch(final Process process) { new Thread(new Runnable() { public void run() { @@ -235,7 +264,7 @@ public void run() { String line; try { while ((line = input.readLine()) != null) { - if (line.contains("Successfully applied 2 migrations")) + if (line.contains("Successfully applied 2 migration(s)")) runCmdTestSuccess = true; System.out.println(line); } diff --git a/src/test/java/com/builtamont/cassandra/migration/internal/resolver/java/JavaMigrationResolverTest.java b/src/test/java/com/builtamont/cassandra/migration/internal/resolver/java/JavaMigrationResolverTest.java index 0a33612..888949f 100644 --- a/src/test/java/com/builtamont/cassandra/migration/internal/resolver/java/JavaMigrationResolverTest.java +++ b/src/test/java/com/builtamont/cassandra/migration/internal/resolver/java/JavaMigrationResolverTest.java @@ -52,7 +52,7 @@ public void resolveMigrations() { ResolvedMigration migrationInfo = migrationList.get(0); Assert.assertEquals("2", migrationInfo.getVersion().toString()); Assert.assertEquals("InterfaceBasedMigration", migrationInfo.getDescription()); - assertNull(migrationInfo.getChecksum()); + Assert.assertEquals(new Integer(0), migrationInfo.getChecksum()); ResolvedMigration migrationInfo1 = migrationList.get(1); Assert.assertEquals("3.5", migrationInfo1.getVersion().toString()); @@ -69,7 +69,7 @@ public void conventionOverConfiguration() { ResolvedMigration migrationInfo = jdbcMigrationResolver.extractMigrationInfo(new V2__InterfaceBasedMigration()); Assert.assertEquals("2", migrationInfo.getVersion().toString()); Assert.assertEquals("InterfaceBasedMigration", migrationInfo.getDescription()); - assertNull(migrationInfo.getChecksum()); + Assert.assertEquals(new Integer(0), migrationInfo.getChecksum()); } @Test diff --git a/src/test/resources/cassandra-unit.yaml b/src/test/resources/cassandra-unit.yaml index 3c693c6..019cd75 100644 --- a/src/test/resources/cassandra-unit.yaml +++ b/src/test/resources/cassandra-unit.yaml @@ -37,6 +37,9 @@ hinted_handoff_throttle_in_kb: 1024 # Consider increasing this number when you have multi-dc deployments, since # cross-dc handoff tends to be slower max_hints_delivery_threads: 2 +# Directory where Cassandra should store hints +# If not set, the default directory is $CASSANDRA_HOMEdatahints +hints_directory: target/embeddedCassandra/hints # The following setting populates the page cache on memtable flush and compaction # WARNING: Enable this setting only when the whole node's data fits in memory.