All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hhandoko.cassandra.migration.internal.dbsupport.SchemaVersionDAO.kt Maven / Gradle / Ivy

/**
 * File     : SchemaVersionDAO.kt
 * License  :
 *   Original   - Copyright (c) 2015 - 2016 Contrast Security
 *   Derivative - Copyright (c) 2016 - 2018 cassandra-migration Contributors
 *
 *   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.hhandoko.cassandra.migration.internal.dbsupport

import com.datastax.driver.core.*
import com.datastax.driver.core.exceptions.InvalidQueryException
import com.datastax.driver.core.querybuilder.QueryBuilder
import com.datastax.driver.core.querybuilder.QueryBuilder.eq
import com.datastax.driver.core.querybuilder.Select
import com.hhandoko.cassandra.migration.api.MigrationType
import com.hhandoko.cassandra.migration.api.MigrationVersion
import com.hhandoko.cassandra.migration.api.configuration.KeyspaceConfiguration
import com.hhandoko.cassandra.migration.internal.metadatatable.AppliedMigration
import com.hhandoko.cassandra.migration.internal.util.CachePrepareStatement
import com.hhandoko.cassandra.migration.internal.util.logging.LogFactory
import java.util.*

/**
 * Schema migrations table Data Access Object.
 *
 * @param session The Cassandra session connection to use to execute the migration.
 * @param keyspaceConfig The Cassandra keyspace to connect to.
 * @param tableName The Cassandra migration version table name.
 */
open class SchemaVersionDAO(private val session: Session, val keyspaceConfig: KeyspaceConfiguration, val tableName: String) {

    private val cachePs: CachePrepareStatement
    private val consistencyLevel: ConsistencyLevel

    // TODO: Break SchemaVersionDAO into service and table-specific mappings.
    // NOTE: SchemaVersionDAO might be able to be broken down further into service (i.e. logic) and actual data access /
    //       persistence. Right now it seems to be doing too much.
    //       Once it is refactored, the query and statements build methods can be kept inside the lazy val bodies rather
    //       than separate private methods.
    private val createSchemaMigrationTableStmt: SimpleStatement by lazy { buildCreateSchemaMigrationTableStmt() }
    private val createSchemaMigrationCounterTableStmt: SimpleStatement by lazy { buildCreateSchemaMigrationCounterTableStmt() }
    private val countSchemaMigrationTableQuery: Select by lazy { buildCountSchemaMigrationTableQuery() }
    private val countSchemaMigrationCounterTableQuery: Select by lazy { buildCountSchemaMigrationCounterTableQuery() }
    private val insertSchemaMigrationTableStmt: PreparedStatement by lazy { buildInsertSchemaMigrationRecordStmt() }
    private val findAppliedMigrationsQuery: Select by lazy { buildFindAppliedMigrationsQuery() }
    private val incrementInstalledRankStmt: SimpleStatement by lazy { buildIncrementInstalledRankStmt() }
    private val findInstalledRankCountColQuery: Select by lazy { buildFindInstalledRankCountColQuery() }
    private val findVersionRankQuery: Select by lazy { buildFindVersionRankQuery() }
    private val updateVersionRankStmt: PreparedStatement by lazy { buildUpdateVersionRankStmt() }

    init {
        this.cachePs = CachePrepareStatement(session)

        // If running on a single host, don't force ConsistencyLevel.ALL
        val isClustered = session.cluster.metadata.allHosts.size > 1
        // Use configuration consistency if provided, otherwise default to `ALL` on cluster or `ONE` on single host
        val consistencyDefined = keyspaceConfig.consistency != null

        this.consistencyLevel = when {
            consistencyDefined -> keyspaceConfig.consistency!!
            isClustered        -> ConsistencyLevel.ALL
            else               -> ConsistencyLevel.ONE
        }
    }

    /**
     * Create schema migration version table if it does not exists.
     */
    fun createTablesIfNotExist() {
        // GUARD: Skip table creation if already exists
        if (tablesExist()) return

        session.execute(createSchemaMigrationTableStmt)
        session.execute(createSchemaMigrationCounterTableStmt)
    }

    /**
     * Check if schema migration version table has already been created.
     *
     * @return `true` if schema migration version table exists in the keyspace.
     */
    open fun tablesExist(): Boolean {
        var schemaVersionTableExists = false
        var schemaVersionCountsTableExists = false

        try {
            val resultsSchemaVersion = session.execute(countSchemaMigrationTableQuery)
            if (resultsSchemaVersion.one() != null) {
                schemaVersionTableExists = true
            }
        } catch (e: InvalidQueryException) {
            LOG.debug("No schema version table found with a name of " + tableName)
        }

        try {
            val resultsSchemaVersionCounts = session.execute(countSchemaMigrationCounterTableQuery)
            if (resultsSchemaVersionCounts.one() != null) {
                schemaVersionCountsTableExists = true
            }
        } catch (e: InvalidQueryException) {
            LOG.debug("No schema version counts table found with a name of " + tableName + COUNTS_TABLE_NAME_SUFFIX)
        }

        return schemaVersionTableExists && schemaVersionCountsTableExists
    }

    /**
     * Add applied migration record into the schema migration version table.
     *
     * @param appliedMigration The applied migration.
     */
    fun addAppliedMigration(appliedMigration: AppliedMigration) {
        createTablesIfNotExist()

        val versionRank = calculateVersionRank(appliedMigration.version!!)
        val installedRank = calculateInstalledRank()

        val statement = boundInsertSchemaMigrationRecordStmt(versionRank, installedRank, appliedMigration)

        session.execute(statement)

        LOG.debug("Schema version table $tableName successfully updated to reflect changes")
    }

    /**
     * Retrieve the applied migrations from the schema migration version table.
     *
     * @return The applied migrations.
     */
    open fun findAppliedMigrations(): List {
        // GUARD: Return empty array if tables does not exists
        if (!tablesExist()) return ArrayList()

        val results = session.execute(findAppliedMigrationsQuery)
        // TODO: Refactor to idiomatic Kotlin collections method
        val resultsList = ArrayList()
        for (row in results) {
            resultsList.add(
                    AppliedMigration(
                            row.getInt("version_rank"),
                            row.getInt("installed_rank"),
                            MigrationVersion.fromVersion(row.getString("version")),
                            row.getString("description"),
                            MigrationType.valueOf(row.getString("type")),
                            row.getString("script"),
                            if (row.isNull("checksum")) null else 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
    }

    /**
     * Retrieve the applied migrations from the metadata table.
     *
     * @param migrationTypes The migration types to find.
     * @return The applied migrations.
     */
    open fun findAppliedMigrations(vararg migrationTypes: MigrationType): List {
        // GUARD: Return empty array if tables does not exists
        if (!tablesExist()) return ArrayList()

        val results = session.execute(findAppliedMigrationsQuery)
        // TODO: Refactor to idiomatic Kotlin collections method
        val resultsList = ArrayList()
        val migTypeList = Arrays.asList(*migrationTypes)
        for (row in results) {
            val migType = MigrationType.valueOf(row.getString("type"))
            if (migTypeList.contains(migType)) {
                resultsList.add(
                        AppliedMigration(
                                row.getInt("version_rank"),
                                row.getInt("installed_rank"),
                                MigrationVersion.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 `true` if the keyspace has applied migrations.
     */
    open fun hasAppliedMigrations(): Boolean {
        // GUARD: Mark as no migrations if tables don't exists
        if (!tablesExist()) return false

        createTablesIfNotExist()

        // TODO: Refactor to idiomatic Kotlin collections method
        val filteredMigrations = ArrayList()
        val appliedMigrations = findAppliedMigrations()
        for (appliedMigration in appliedMigrations) {
            if (appliedMigration.type != MigrationType.BASELINE) {
                filteredMigrations.add(appliedMigration)
            }
        }
        return filteredMigrations.isNotEmpty()
    }

    /**
     * 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.
     */
    fun addBaselineMarker(baselineVersion: MigrationVersion, baselineDescription: String, user: String) {
        addAppliedMigration(
                AppliedMigration(
                        baselineVersion,
                        baselineDescription,
                        MigrationType.BASELINE,
                        baselineDescription,
                        checksum = 0,
                        installedBy = user,
                        executionTime = 0,
                        success = true
                )
        )
    }

    /**
     * Get the baseline marker's applied migration.
     *
     * @return The baseline marker's applied migration.
     */
    val baselineMarker: AppliedMigration?
        get() {
            val appliedMigrations = findAppliedMigrations(MigrationType.BASELINE)
            return if (appliedMigrations.isEmpty()) null else appliedMigrations[0]
        }

    /**
     * Check if schema migration version table has a baseline marker.
     *
     * @return `true` if the schema migration version table has a baseline marker.
     */
    open fun hasBaselineMarker(): Boolean {
        // GUARD: Mark as no baseline marker if tables don't exists
        if (!tablesExist()) return false

        createTablesIfNotExist()

        return findAppliedMigrations(MigrationType.BASELINE).isNotEmpty()
    }

    /**
     * Calculates the installed rank for the new migration to be inserted.
     *
     * @return The installed rank.
     */
    private fun calculateInstalledRank(): Int {
        session.execute(incrementInstalledRankStmt)
        val result = session.execute(findInstalledRankCountColQuery)

        return result.one().getLong("count").toInt()
    }

    /**
     * Calculate the rank for this new version about to be inserted.
     *
     * @param version The version to calculated for.
     * @return The rank.
     */
    private fun calculateVersionRank(version: MigrationVersion): Int {
        val versionRows = session.execute(findVersionRankQuery)

        // TODO: Refactor to idiomatic Kotlin collections method
        val migrationVersions = ArrayList()
        val migrationMetaHolders = HashMap()
        for (versionRow in versionRows) {
            migrationVersions.add(MigrationVersion.fromVersion(versionRow.getString("version")))
            migrationMetaHolders.put(
                    versionRow.getString("version"),
                    MigrationMetaHolder(versionRow.getInt("version_rank"))
            )
        }

        if (migrationVersions.size == 0) {
            return 1;
        }

        Collections.sort(migrationVersions)

        // TODO: Refactor for loop with idiomatic Kotlin collection methods
        for (i in migrationVersions.indices) {
            if (version.compareTo(migrationVersions[i]) < 0) {
                return i + 1
            }
        }

        return migrationVersions.size + 1
    }

    /**
     * Schema Migration table CQL statement builder.
     *
     * @return Schema Migration table create statement.
     */
    private fun buildCreateSchemaMigrationTableStmt(): SimpleStatement {
        val stmt = SimpleStatement(
                """
                 | CREATE TABLE IF NOT EXISTS "${keyspaceConfig.name}"."${tableName}"
                 | (
                 |   version_rank   INT,
                 |   installed_rank INT,
                 |   version        TEXT,
                 |   description    TEXT,
                 |   script         TEXT,
                 |   checksum       INT,
                 |   type           TEXT,
                 |   installed_by   TEXT,
                 |   installed_on   TIMESTAMP,
                 |   execution_time INT,
                 |   success        BOOLEAN,
                 |   PRIMARY KEY (version)
                 | );
                """.trimMargin()
        )
        stmt.consistencyLevel = this.consistencyLevel
        return stmt
    }

    /**
     * Schema Migration Counter table CQL statement builder.
     *
     * @return Schema Migration Counter table create statement.
     */
    private fun buildCreateSchemaMigrationCounterTableStmt(): SimpleStatement {
        val stmt = SimpleStatement(
                """
                 | CREATE TABLE IF NOT EXISTS "${keyspaceConfig.name}"."${tableName}${COUNTS_TABLE_NAME_SUFFIX}"
                 | (
                 |   name  TEXT,
                 |   count COUNTER,
                 |   PRIMARY KEY (name)
                 | );
                """.trimMargin()
        )
        stmt.consistencyLevel = this.consistencyLevel
        return stmt
    }

    // ISSUE #17
    // ~~~~~~
    // Ref    : https://github.com/hhandoko/cassandra-migration/issues/17
    // Summary:
    //   Table check query fails following a `DROP TABLE` statement when run against embedded Cassandra and Apache
    //   Cassandra 3.7 and earlier.
    // Fix    :
    //   Use `SELECT *` rather than `SELECT count(*)` as a workaround, less efficient but universal.
    // Notes  :
    //   Can be reverted (to use count) once the affected Cassandra version has been superseded by another major
    //   version (e.g. 4.x).
    // ~~~~~~

    /**
     * Schema Migration table count / exist CQL query builder.
     *
     * @return Schema Migration table count query.
     */
    private fun buildCountSchemaMigrationTableQuery(): Select {
        val query = QueryBuilder
                .select()
                //.countAll()
                .from(keyspaceConfig.name, tableName)
        query.consistencyLevel = this.consistencyLevel
        return query
    }

    /**
     * Schema Migration Counter table count / exist CQL query builder.
     *
     * @return Schema Migration Counter table count query.
     */
    private fun buildCountSchemaMigrationCounterTableQuery(): Select {
        val query = QueryBuilder
                .select()
                //.countAll()
                .from(keyspaceConfig.name, tableName + COUNTS_TABLE_NAME_SUFFIX)
        query.consistencyLevel = this.consistencyLevel
        return query
    }

    /**
     * Insert Schema Migration record CQL statement builder.
     *
     * @return Schema Migration record insert statement.
     */
    private fun buildInsertSchemaMigrationRecordStmt(): PreparedStatement {
        val stmt = this.cachePs.prepare(
                """
                 | INSERT INTO "${keyspaceConfig.name}"."${tableName}"
                 | (
                 |   version_rank, installed_rank, version,
                 |   description, type, script,
                 |   checksum, installed_on, installed_by,
                 |   execution_time, success
                 | ) VALUES (
                 |   ?, ?, ?,
                 |   ?, ?, ?,
                 |   ?, dateOf(now()), ?,
                 |   ?, ?
                 | );
                """.trimMargin()
        )
        stmt.consistencyLevel = this.consistencyLevel
        return stmt
    }

    /**
     * Bind Schema Migration table insert CQL statement with the given params.
     *
     * @param versionRank The current version rank.
     * @param installedRank The current installed rank.
     * @param appliedMigration The current applied migration.
     * @return Bound Schema Migration record insert statement.
     */
    private fun boundInsertSchemaMigrationRecordStmt(versionRank: Int, installedRank: Int, appliedMigration: AppliedMigration): BoundStatement {
        return insertSchemaMigrationTableStmt.bind(
                versionRank,
                installedRank,
                appliedMigration.version.toString(),
                appliedMigration.description,
                appliedMigration.type!!.name,
                appliedMigration.script,
                appliedMigration.checksum,
                appliedMigration.installedBy,
                appliedMigration.executionTime,
                appliedMigration.isSuccess
        )
    }

    /**
     * Find Schema Migration table applied migrations CQL query.
     *
     * @return Schema Migration table applied migrations select query.
     */
    private fun buildFindAppliedMigrationsQuery(): Select {
        val query = 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(keyspaceConfig.name, tableName)

        query.consistencyLevel = this.consistencyLevel
        return query
    }

    /**
     * Increment Schema Migration table installed rank CQL statement.
     *
     * @return Schema Migration table increment installed rank update statement.
     */
    private fun buildIncrementInstalledRankStmt(): SimpleStatement {
        val stmt = SimpleStatement(
                """
                 | UPDATE "${keyspaceConfig.name}"."${tableName}${COUNTS_TABLE_NAME_SUFFIX}"
                 |    SET count = count + 1
                 |  WHERE name = 'installed_rank';
                """.trimMargin()
        )
        stmt.consistencyLevel = this.consistencyLevel
        return stmt
    }

    /**
     * Find Schema Migration table installed rank count CQL query.
     *
     * @return Schema Migration table installed rank count select query.
     */
    private fun buildFindInstalledRankCountColQuery(): Select {
        val query = QueryBuilder
                .select("count")
                .from(keyspaceConfig.name, tableName + COUNTS_TABLE_NAME_SUFFIX)
        query.where(eq("name", "installed_rank"))

        query.consistencyLevel = this.consistencyLevel
        return query
    }

    /**
     * Find Schema Migration table version rank CQL query.
     *
     * @return Schema Migration table version rank select query.
     */
    private fun buildFindVersionRankQuery(): Select {
        val query = QueryBuilder
                .select()
                .column("version")
                .column("version_rank")
                .from(keyspaceConfig.name, tableName)

        query.consistencyLevel = this.consistencyLevel
        return query
    }

    /**
     * Update Schema Migration table version rank CQL query.
     *
     * @return Schema Migration table version rank update query.
     */
    private fun buildUpdateVersionRankStmt(): PreparedStatement {
        val stmt = this.cachePs.prepare(
                """
                 | UPDATE "${keyspaceConfig.name}"."${tableName}"
                 |    SET version_rank = ?
                 |  WHERE version = ?;
                """.trimMargin()
        )
        stmt.consistencyLevel = this.consistencyLevel
        return stmt
    }

    /**
     * Bind Schema Migration table version update CQL statement with the given params.
     *
     * @param versionRank The current version rank.
     * @param version The current version.
     * @return Bound Schema Migration version record update statement.
     */
    private fun boundUpdateVersionRankStmt(versionRank: Int, version: String?): BoundStatement {
        return updateVersionRankStmt.bind(versionRank, version)
    }

    /**
     * Schema migration (transient) metadata.
     *
     * @param versionRank The applied migrations version rank.
     */
    internal inner class MigrationMetaHolder(val versionRank: Int)

    /**
     * SchemaVersionDAO companion object.
     */
    companion object {
        private val LOG = LogFactory.getLog(SchemaVersionDAO::class.java)
        private val COUNTS_TABLE_NAME_SUFFIX = "_counts"
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy