Android Open Source - SQLiteMigrationManager S Q Lite Migration Manager






From Project

Back to project page SQLiteMigrationManager.

License

The source code is released under:

Apache License

If you think the Android project SQLiteMigrationManager listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/**
 * Layer Android SDK//from w  ww  .j  av  a 2s  .c  o  m
 *
 * Created by Steven Jones on 6/4/14
 * Copyright (c) 2013 Layer. All rights reserved.
 */
package com.layer.sqlite;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;

import com.layer.sqlite.datasource.DataSource;
import com.layer.sqlite.migrations.Migration;
import com.layer.sqlite.schema.Schema;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

public class SQLiteMigrationManager {
    public static final long NO_VERSIONS = -1;

    /**
     * `BootstrapAction` tells SQLiteMigrationManager which action to take when no
     * `schema_migrations` table is found during a call to manageSchema().
     * <p><ul>
     * <li>NONE: Do nothing.  A SQLException will get thrown if no `schema_migrations` table is
     * created by the first migration.</li>
     * <li>APPLY_SCHEMA: Load and apply a Schema from the DataSource set.</li>
     * <li>CREATE_MIGRATIONS_TABLE: Create the `schema_migrations` table.</li>
     * </ul></p>
     */
    public static enum BootstrapAction {
        NONE,
        APPLY_SCHEMA,
        CREATE_MIGRATIONS_TABLE
    }

    /**
     * DataSources from which to find Schemas and Migrations
     */
    private final Set<DataSource> mDataSources = new HashSet<DataSource>();

    /**
     * Applies pending Migrations in order.  If a migration throws an SQLException, the process is
     * halted at that point, but all previous migrations remain applied.  Behavior when no
     * migrations table is present is controlled by the `BootstrapAction action` parameter.
     *
     * @param db     Database on which to operate.
     * @param action NoSchemaAction action to take when hasMigrationsTable() returns false.
     * @return The number of migrations applied.
     * @see #getOriginVersion(android.database.sqlite.SQLiteDatabase)
     * @see #getMigrations()
     * @see #getAppliedVersions(android.database.sqlite.SQLiteDatabase)
     * @see com.layer.sqlite.SQLiteMigrationManager.BootstrapAction
     */
    public int manageSchema(SQLiteDatabase db, BootstrapAction action) throws IOException {
        int numApplied = 0;

        // Begin an outer transaction.
        db.beginTransaction();

        // Bootstrap if no `schema_migrations` is present.
        if (!hasMigrationsTable(db)) {
            switch (action) {
                case APPLY_SCHEMA:
                    applySchema(db);
                    break;
                case CREATE_MIGRATIONS_TABLE:
                    createMigrationsTable(db);
                    break;
                case NONE:
                default:
                    break;
            }
        }

        // Apply Migrations.
        try {
            for (Migration migration : getPendingMigrations(db)) {
                try {
                    // Begin an inner Migration transaction.
                    db.beginTransaction();

                    // Apply the Migration.
                    SQLParser.execute(db, migration);
                    insertVersion(db, migration.getVersion());
                    numApplied++;

                    // Set the inner Migration transaction successful.
                    db.setTransactionSuccessful();
                } catch (SQLException e) {
                    // Halt the migration process on error.
                    e.printStackTrace();
                    break;
                } finally {
                    // End the inner Migration transaction.
                    db.endTransaction();
                }
            }
            // Set the outer transaction successful.
            db.setTransactionSuccessful();
        } finally {
            // End the outer transaction.
            db.endTransaction();
        }
        return numApplied;
    }

    /**
     * Adds a DataSource to the set of available sources for providing Schema and Migrations.
     *
     * @param dataSource DataSource to add to the set of managed sources.
     * @return `this` for chaining.
     */
    public SQLiteMigrationManager addDataSource(DataSource... dataSource) {
        mDataSources.addAll(Arrays.asList(dataSource));
        return this;
    }

    /**
     * Returns true if the `schema_migrations` table exists.
     *
     * @param db Database to query for the `schema_migrations` table.
     * @return true if the `schema_migrations` table exists.
     */
    public boolean hasMigrationsTable(SQLiteDatabase db) {
        Cursor c = null;
        try {
            c = db.rawQuery("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?",
                    new String[]{"schema_migrations"});
            return (c.getCount() > 0);
        } finally {
            if (c != null) {
                c.close();
            }
        }
    }

    /**
     * Creates the managed `schema_migrations` table used for tracking applied migrations.
     *
     * @param db Database to create the `schema_migrations` table in.
     * @return `this` for chaining.
     */
    public SQLiteMigrationManager createMigrationsTable(SQLiteDatabase db) {
        db.execSQL("CREATE TABLE IF NOT EXISTS schema_migrations (" +
                "version INTEGER UNIQUE NOT NULL)");
        return this;
    }

    /**
     * Returns true if any DataSource has a Schema.
     *
     * @return True if any DataSource has a Schema.
     */
    public boolean hasSchema() {
        for (DataSource dataSource : mDataSources) {
            if (dataSource.hasSchema()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns the first available Schema contained within the DataSource set.
     *
     * @return The first available Schema contained within the DataSource set.
     * @throws java.lang.IllegalStateException When no DataSources have been added.
     */
    public Schema getSchema() throws IllegalStateException {
        if (mDataSources.isEmpty()) {
            throw new IllegalStateException("No DataSources added");
        }

        for (DataSource dataSource : mDataSources) {
            if (dataSource.hasSchema()) {
                return dataSource.getSchema();
            }
        }
        return null;
    }

    /**
     * Bootstraps from the first available Schema found in the DataSource set.
     *
     * @param db Database to bootstrap.
     * @return `this` for chaining.
     * @throws IllegalStateException         If no schemas were found in the DataSource set.
     * @throws java.io.IOException
     * @throws android.database.SQLException
     */
    public SQLiteMigrationManager applySchema(SQLiteDatabase db) throws IOException {
        if (!hasSchema()) {
            throw new IllegalStateException("No schemas in DataSource set.");
        }
        SQLParser.execute(db, getSchema());
        return this;
    }

    /**
     * Generates a sorted list of Migration objects from all DataSources.
     *
     * @return A sorted list of Migrations from the set of DataSources.
     * @throws java.lang.IllegalStateException When no DataSources have been added.
     */
    public List<Migration> getMigrations() throws IllegalStateException {
        if (mDataSources.isEmpty()) {
            throw new IllegalStateException("No DataSources added");
        }

        // Use Sets to prevent duplicate Migrations.
        Set<Migration> migrations = new HashSet<Migration>();
        for (DataSource dataSource : mDataSources) {
            migrations.addAll(dataSource.getMigrations());
        }

        // Return a sorted List.
        List<Migration> sortedMigrations = new LinkedList<Migration>(migrations);
        Collections.sort(sortedMigrations);
        return sortedMigrations;
    }

    /**
     * Returns a sorted list of Migrations available in the set of DataSources which have not been
     * applied to the given SQLiteDatabase.  The list is generated as follows:
     *
     * 0) If the database isn't managed, return the entire list of available migrations; else:
     * 1) Get the origin version with getOrigin().
     * 2) Get the list of applied versions with getAppliedVersions().
     * 3) Loop through available Migrations with getMigrations(), and:
     * 3.1) If the migration has a version less than or equal to origin, skip;
     * 3.2) If the migration is in the applied list, skip;
     * 3.3) Otherwise, the migration is pending.
     *
     * @param db Database on which to compare migration versions.
     * @return The list of available Migrations which have not been applied.
     */
    public List<Migration> getPendingMigrations(SQLiteDatabase db) throws IOException {
        // If this database isn't yet managed, just return the list of available Migrations.
        if (!hasMigrationsTable(db)) {
            return getMigrations();
        }

        // (1) Get the origin version of this database.
        long originVersion = getOriginVersion(db);

        // (2) Get a list of currently-applied versions.
        HashSet<Long> appliedVersions = getAppliedVersions(db);

        // (3) Generate the list of pending migrations.
        List<Migration> pendingMigrations = new LinkedList<Migration>();
        for (Migration migration : getMigrations()) {
            long version = migration.getVersion();

            if (version <= originVersion) {
                // Our origin already had this migration applied, continue.
                continue;
            }

            if (appliedVersions.contains(version)) {
                // We've already applied this migration, continue.
                continue;
            }

            // This Migration is pending.
            pendingMigrations.add(migration);
        }
        Collections.sort(pendingMigrations);
        return pendingMigrations;
    }

    /**
     * Loads the lowest version number from the `schema_migrations` table, returns NO_VERSIONS if
     * the migrations table is empty, or throws an SQLException if the table isn't present.  The
     * origin version number tells the manageSchema() method which old migrations it can safely
     * ignore (because they were already applied on the bootstrapped schema deployed on this
     * device).
     *
     * @param db Database from which to load the origin version.
     * @return The lowest version present or NO_VERSIONS of the `schema_migrations` table is empty.
     * @throws android.database.SQLException When no `schema_migrations` table is present.
     */
    public long getOriginVersion(SQLiteDatabase db) throws SQLException {
        Cursor cursor = null;
        try {
            cursor = db.rawQuery("SELECT MIN(version) FROM schema_migrations", null);
            cursor.moveToNext();
            if (cursor.isNull(0)) {
                return NO_VERSIONS;
            }
            return cursor.getLong(0);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

    /**
     * Loads the current version number from the `schema_migrations` table, returns NO_VERSIONS if
     * the migrations table is empty, or throws an SQLException if the table isn't present.  The
     * current version number is the maximum version in the `schema_migrations` table.
     *
     * @param db Database from which to load the current version.
     * @return The highest version present or NO_VERSIONS of the `schema_migrations` table is
     * empty.
     * @throws android.database.SQLException When no `schema_migrations` table is present.
     */
    public long getCurrentVersion(SQLiteDatabase db) throws SQLException {
        Cursor cursor = null;
        try {
            cursor = db.rawQuery("SELECT MAX(version) FROM schema_migrations", null);
            cursor.moveToNext();
            if (cursor.isNull(0)) {
                return NO_VERSIONS;
            }
            return cursor.getLong(0);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

    /**
     * Loads an ordered set of all versions currently applied on this database.  This set is used
     * by manageSchema() to determine which available migrations have already been applied.
     *
     * @param db Database from which to load versions.
     * @return An ordered set of all versions applied.
     * @throws android.database.SQLException When no `schema_migrations` table is present.
     */
    public LinkedHashSet<Long> getAppliedVersions(SQLiteDatabase db) throws SQLException {
        Cursor cursor = null;
        try {
            LinkedHashSet<Long> versions = new LinkedHashSet<Long>();
            cursor = db.rawQuery("SELECT version FROM schema_migrations ORDER BY version", null);
            while (cursor.moveToNext()) {
                versions.add(cursor.getLong(0));
            }
            return versions;
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

    /**
     * Records a successfully-applied migration in the `schema_migrations` table.
     *
     * @param db      Database to record a successful version in.
     * @param version Migration version to record.
     * @throws SQLException
     */
    public void insertVersion(SQLiteDatabase db, Long version) throws SQLException {
        ContentValues values = new ContentValues();
        values.put("version", version);
        db.insert("schema_migrations", null, values);
    }

    /**
     * Returns true if the provided database's current version is not contained in the migrations,
     * or false if it does.
     *
     * @param db Database to check for downgrading.
     * @return true if the provided database has a higher version than the known migrations.
     */
    public boolean isDowngrade(SQLiteDatabase db) {
        if (!hasMigrationsTable(db)) {
            return false;
        }
        
        Long max = getCurrentVersion(db);
        List<Migration> migrations = getMigrations();

        boolean isCurrentVersionInMigrations = false;
        for (Migration migration : migrations) {
            if (migration.getVersion().equals(max)) {
                isCurrentVersionInMigrations = true;
            }
        }
        return !isCurrentVersionInMigrations;
    }
}




Java Source Code List

com.layer.sqlite.Fixtures.java
com.layer.sqlite.ResourceDataSourceTests.java
com.layer.sqlite.SQLParserTests.java
com.layer.sqlite.SQLParser.java
com.layer.sqlite.SQLiteMigrationManagerTests.java
com.layer.sqlite.SQLiteMigrationManager.java
com.layer.sqlite.datasource.DataSource.java
com.layer.sqlite.datasource.ResourceDataSource.java
com.layer.sqlite.migrations.Migration.java
com.layer.sqlite.migrations.ResourceMigration.java
com.layer.sqlite.schema.ResourceSchema.java
com.layer.sqlite.schema.Schema.java