com.github.gfx.android.orma.migration.SchemaDiffMigration.java Source code

Java tutorial

Introduction

Here is the source code for com.github.gfx.android.orma.migration.SchemaDiffMigration.java

Source

/*
 * Copyright (c) 2015 FUJI Goro (gfx).
 *
 * 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.github.gfx.android.orma.migration;

import com.github.gfx.android.orma.core.Database;
import com.github.gfx.android.orma.migration.sqliteparser.CreateIndexStatement;
import com.github.gfx.android.orma.migration.sqliteparser.CreateTableStatement;
import com.github.gfx.android.orma.migration.sqliteparser.SQLiteComponent;
import com.github.gfx.android.orma.migration.sqliteparser.SQLiteParserUtils;

import org.json.JSONArray;

import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Pair;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

@SuppressLint("Assert")
public class SchemaDiffMigration extends AbstractMigrationEngine {

    public static final String TAG = "SchemaDiffMigration";

    public static final String MIGRATION_STEPS_TABLE_1 = "orma_schema_diff_migration_steps";

    public static final String MIGRATION_STEPS_TABLE = "orma_schema_diff_migration_2";

    static final String kId = "id";

    static final String kDbVersion = "db_version"; // PRAGMA schema_version

    static final String kVersionName = "version_name";

    static final String kVersionCode = "version_code";

    static final String kSchemaHash = "schema_hash";

    static final String kSql = "sql";

    static final String kArgs = "args";

    static final String kCreatedTimestamp = "created_timestamp";

    public static final String SCHEMA_DIFF_DDL = "CREATE TABLE IF NOT EXISTS " + MIGRATION_STEPS_TABLE + " (" + kId
            + " INTEGER PRIMARY KEY AUTOINCREMENT, " + kDbVersion + " INTEGER NOT NULL, " + kVersionName
            + " TEXT NOT NULL, " + kVersionCode + " INTEGER NOT NULL, " + kSchemaHash + " TEXT NOT NULL, " + kSql
            + " TEXT NULL, " + kArgs + " TEXT NULL, " + kCreatedTimestamp
            + " DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP)";

    final String versionName;

    final int versionCode;

    final String schemaHash;

    final SqliteDdlBuilder util = new SqliteDdlBuilder();

    private boolean tableCreated = false;

    public SchemaDiffMigration(@NonNull Context context, @NonNull String schemaHash,
            @NonNull TraceListener traceListener) {
        super(traceListener);
        this.versionName = extractVersionName(context);
        this.versionCode = extractVersionCode(context);
        this.schemaHash = schemaHash;
    }

    public SchemaDiffMigration(@NonNull Context context, @NonNull String schemaHash) {
        this(context, schemaHash, extractDebuggable(context) ? TraceListener.LOGCAT : TraceListener.EMPTY);
    }

    static private Map<CreateIndexStatement, String> parseIndexes(Collection<String> indexes) {
        Map<CreateIndexStatement, String> parsedIndexPairs = new LinkedHashMap<>();
        for (String createIndexStatement : indexes) {
            parsedIndexPairs.put(SQLiteParserUtils.parseIntoCreateIndexStatement(createIndexStatement),
                    createIndexStatement);
        }
        return parsedIndexPairs;
    }

    private static String serializeArgs(@NonNull Object[] args) {
        if (args.length == 0) {
            return "[]";
        }

        JSONArray array = new JSONArray();
        for (Object arg : args) {
            array.put(arg);
        }
        return array.toString();
    }

    public static Map<String, SQLiteMaster> loadMetadata(Database db, List<? extends MigrationSchema> schemas) {
        Map<String, SQLiteMaster> metadata = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

        Set<String> tableNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
        for (MigrationSchema schema : schemas) {
            tableNames.add(schema.getTableName());
        }

        for (Map.Entry<String, SQLiteMaster> entry : SQLiteMaster.loadTables(db).entrySet()) {
            if (tableNames.contains(entry.getKey())) {
                metadata.put(entry.getKey(), entry.getValue());
            }
        }
        return metadata;
    }

    @NonNull
    @Override
    public String getTag() {
        return TAG;
    }

    @Override
    public void start(@NonNull Database db, @NonNull List<? extends MigrationSchema> schemas) {
        if (isSchemaChanged(db)) {
            Map<String, ? extends MigrationSchema> masterSchemas = loadMetadata(db, schemas);
            List<String> statements = diffAll(masterSchemas, schemas);
            if (statements.isEmpty()) {
                int dbVersion = fetchDbVersion(db);
                saveStep(db, dbVersion, null); // just to save the db version
            } else {
                executeStatements(db, statements);
            }
        }
    }

    public boolean isSchemaChanged(Database db) {
        Pair<Integer, String> versions = fetchSchemaVersions(db);
        int dbVersion = fetchDbVersion(db);
        return !(dbVersion == versions.first && schemaHash.equals(versions.second));
    }

    @NonNull
    private Pair<Integer, String> fetchSchemaVersions(Database db) {
        ensureHistoryTableExists(db);
        Cursor cursor = db.query(MIGRATION_STEPS_TABLE, new String[] { kDbVersion, kSchemaHash }, null, null, null,
                null, kId + " DESC", "1");
        try {
            if (cursor.moveToFirst()) {
                return new Pair<>(cursor.getInt(0), cursor.getString(1));
            } else {
                return new Pair<>(0, "");
            }
        } finally {
            cursor.close();
        }
    }

    void ensureHistoryTableExists(Database db) {
        if (!tableCreated) {
            migrate(db);
            tableCreated = true;
        }
    }

    public void migrate(Database db) {
        if (SQLiteMaster.checkIfTableNameExists(db, MIGRATION_STEPS_TABLE_1)) {
            try {
                db.beginTransaction();

                db.execSQL(SCHEMA_DIFF_DDL);
                String[] src = { kId, "0", kVersionName, kVersionCode, kSchemaHash, kSql, kArgs,
                        kCreatedTimestamp };
                String[] dst = { kId, kDbVersion, kVersionName, kVersionCode, kSchemaHash, kSql, kArgs,
                        kCreatedTimestamp };
                db.execSQL("INSERT INTO " + MIGRATION_STEPS_TABLE + " (" + TextUtils.join(", ", dst) + ") SELECT "
                        + TextUtils.join(", ", src) + " FROM " + MIGRATION_STEPS_TABLE_1);
                db.execSQL("DROP TABLE " + MIGRATION_STEPS_TABLE_1);
                db.execSQL(SCHEMA_DIFF_DDL);

                db.setTransactionSuccessful();
            } finally {
                db.endTransaction();
            }
        } else {
            db.execSQL(SCHEMA_DIFF_DDL);
        }
    }

    @NonNull
    public List<String> diffAll(@NonNull Map<String, ? extends MigrationSchema> srsSchemas,
            @NonNull List<? extends MigrationSchema> dstSchemas) {
        List<String> statements = new ArrayList<>();

        // NOTE: ignore tables which exist only in database
        for (MigrationSchema dstSchema : dstSchemas) {
            MigrationSchema srcSchema = srsSchemas.get(dstSchema.getTableName());
            if (srcSchema == null) {
                statements.add(dstSchema.getCreateTableStatement());
                statements.addAll(dstSchema.getCreateIndexStatements());
            } else {
                List<String> tableDiffStatements = tableDiff(srcSchema.getCreateTableStatement(),
                        dstSchema.getCreateTableStatement());

                if (tableDiffStatements.isEmpty()) {
                    statements.addAll(
                            indexDiff(srcSchema.getCreateIndexStatements(), dstSchema.getCreateIndexStatements()));
                } else {
                    // This table needs re-create, where all the indexes are also dropped.
                    statements.addAll(tableDiffStatements);
                    statements.addAll(dstSchema.getCreateIndexStatements());
                }
            }
        }
        return statements;
    }

    /**
     * @param srcIndexes Set of "CREATE INDEX" statements which the DB has
     * @param dstIndexes Set of "CREATE INDEX" statements which the running code has
     * @return List of "CREATED INDEX" statements to apply to DB
     */
    @NonNull
    public List<String> indexDiff(@NonNull Collection<String> srcIndexes, @NonNull Collection<String> dstIndexes) {
        LinkedHashMap<CreateIndexStatement, String> unionIndexes = new LinkedHashMap<>();

        Map<CreateIndexStatement, String> srcIndexesPairs = parseIndexes(srcIndexes);
        unionIndexes.putAll(srcIndexesPairs);

        Map<CreateIndexStatement, String> dstIndexesPairs = parseIndexes(dstIndexes);
        unionIndexes.putAll(dstIndexesPairs);

        List<String> createIndexStatements = new ArrayList<>();
        for (Map.Entry<CreateIndexStatement, String> createIndexStatement : unionIndexes.entrySet()) {
            boolean existsInDst = dstIndexesPairs.containsKey(createIndexStatement.getKey());
            boolean existsInSrc = srcIndexesPairs.containsKey(createIndexStatement.getKey());

            if (existsInDst && existsInSrc) {
                // okay, nothing to do
            } else if (existsInDst) {
                createIndexStatements.add(createIndexStatement.getValue());
            } else { // existsInSrc
                createIndexStatements.add(buildDropIndexStatement(createIndexStatement.getKey()));
            }
        }
        return createIndexStatements;
    }

    @NonNull
    public List<String> tableDiff(String from, String to) {
        if (from.equals(to)) {
            return Collections.emptyList();
        }

        CreateTableStatement fromTable = SQLiteParserUtils.parseIntoCreateTableStatement(from);
        CreateTableStatement toTable = SQLiteParserUtils.parseIntoCreateTableStatement(to);

        Set<CreateTableStatement.ColumnDef> toColumns = new LinkedHashSet<>();
        Set<SQLiteComponent.Name> toColumnNames = new LinkedHashSet<>();
        List<CreateTableStatement.ColumnDef> intersectionColumns = new ArrayList<>();

        List<SQLiteComponent.Name> intersectionColumnNames = new ArrayList<>();

        for (CreateTableStatement.ColumnDef column : toTable.getColumns()) {
            toColumns.add(column);
            toColumnNames.add(column.getName());
        }

        for (CreateTableStatement.ColumnDef column : fromTable.getColumns()) {
            if (toColumns.contains(column)) {
                intersectionColumns.add(column);
            }

            if (toColumnNames.contains(column.getName())) {
                intersectionColumnNames.add(column.getName());
            }
        }

        if (intersectionColumns.size() != toTable.getColumns().size()
                || intersectionColumns.size() != fromTable.getColumns().size()
                || !fromTable.getConstraints().equals(toTable.getConstraints())) {
            trace("from: %s", from);
            trace("to:   %s", to);
            return util.buildRecreateTable(fromTable, toTable, intersectionColumnNames, intersectionColumnNames);
        } else {
            return Collections.emptyList();
        }
    }

    @NonNull
    public String buildDropIndexStatement(String createIndexStatement) {
        return buildDropIndexStatement(SQLiteParserUtils.parseIntoCreateIndexStatement(createIndexStatement));
    }

    @NonNull
    public String buildDropIndexStatement(CreateIndexStatement createIndexStatement) {
        return "DROP INDEX IF EXISTS " + createIndexStatement.getIndexName();
    }

    public void saveStep(@NonNull Database db, int dbVersion, @Nullable String sql, @NonNull Object... args) {
        ensureHistoryTableExists(db);

        ContentValues values = new ContentValues();
        values.put(kDbVersion, dbVersion);
        values.put(kVersionName, versionName);
        values.put(kVersionCode, versionCode);
        values.put(kSchemaHash, schemaHash);
        values.put(kSql, sql);
        values.put(kArgs, serializeArgs(args));
        db.insertOrThrow(MIGRATION_STEPS_TABLE, null, values);
    }

    public void executeStatements(final Database db, final List<String> statements) {
        if (statements.isEmpty()) {
            return;
        }

        transaction(db, new Runnable() {
            @Override
            public void run() {
                for (String statement : statements) {
                    trace("%s", statement);
                    db.execSQL(statement);
                }

                int dbVersion = fetchDbVersion(db);
                for (String statement : statements) {
                    saveStep(db, dbVersion, statement);
                }
            }
        });
    }

    private static int fetchDbVersion(Database db) {
        return (int) db.longForQuery("PRAGMA schema_version", null);
    }
}