1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.room; 18 19 import android.database.Cursor; 20 21 import androidx.annotation.NonNull; 22 import androidx.annotation.Nullable; 23 import androidx.annotation.RestrictTo; 24 import androidx.room.migration.Migration; 25 import androidx.sqlite.db.SimpleSQLiteQuery; 26 import androidx.sqlite.db.SupportSQLiteDatabase; 27 import androidx.sqlite.db.SupportSQLiteOpenHelper; 28 29 import java.util.List; 30 31 /** 32 * An open helper that holds a reference to the configuration until the database is opened. 33 * 34 * @hide 35 */ 36 @SuppressWarnings("unused") 37 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 38 public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback { 39 @Nullable 40 private DatabaseConfiguration mConfiguration; 41 @NonNull 42 private final Delegate mDelegate; 43 @NonNull 44 private final String mIdentityHash; 45 /** 46 * Room v1 had a bug where the hash was not consistent if fields are reordered. 47 * The new has fixes it but we still need to accept the legacy hash. 48 */ 49 @NonNull // b/64290754 50 private final String mLegacyHash; 51 52 public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate, 53 @NonNull String identityHash, @NonNull String legacyHash) { 54 super(delegate.version); 55 mConfiguration = configuration; 56 mDelegate = delegate; 57 mIdentityHash = identityHash; 58 mLegacyHash = legacyHash; 59 } 60 61 public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate, 62 @NonNull String legacyHash) { 63 this(configuration, delegate, null, legacyHash); 64 } 65 66 @Override 67 public void onConfigure(SupportSQLiteDatabase db) { 68 super.onConfigure(db); 69 } 70 71 @Override 72 public void onCreate(SupportSQLiteDatabase db) { 73 updateIdentity(db); 74 mDelegate.createAllTables(db); 75 mDelegate.onCreate(db); 76 } 77 78 @Override 79 public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) { 80 boolean migrated = false; 81 if (mConfiguration != null) { 82 List<Migration> migrations = mConfiguration.migrationContainer.findMigrationPath( 83 oldVersion, newVersion); 84 if (migrations != null) { 85 for (Migration migration : migrations) { 86 migration.migrate(db); 87 } 88 mDelegate.validateMigration(db); 89 updateIdentity(db); 90 migrated = true; 91 } 92 } 93 if (!migrated) { 94 if (mConfiguration != null && !mConfiguration.isMigrationRequiredFrom(oldVersion)) { 95 mDelegate.dropAllTables(db); 96 mDelegate.createAllTables(db); 97 } else { 98 throw new IllegalStateException("A migration from " + oldVersion + " to " 99 + newVersion + " was required but not found. Please provide the " 100 + "necessary Migration path via " 101 + "RoomDatabase.Builder.addMigration(Migration ...) or allow for " 102 + "destructive migrations via one of the " 103 + "RoomDatabase.Builder.fallbackToDestructiveMigration* methods."); 104 } 105 } 106 } 107 108 @Override 109 public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) { 110 onUpgrade(db, oldVersion, newVersion); 111 } 112 113 @Override 114 public void onOpen(SupportSQLiteDatabase db) { 115 super.onOpen(db); 116 checkIdentity(db); 117 mDelegate.onOpen(db); 118 // there might be too many configurations etc, just clear it. 119 mConfiguration = null; 120 } 121 122 private void checkIdentity(SupportSQLiteDatabase db) { 123 String identityHash = null; 124 if (hasRoomMasterTable(db)) { 125 Cursor cursor = db.query(new SimpleSQLiteQuery(RoomMasterTable.READ_QUERY)); 126 //noinspection TryFinallyCanBeTryWithResources 127 try { 128 if (cursor.moveToFirst()) { 129 identityHash = cursor.getString(0); 130 } 131 } finally { 132 cursor.close(); 133 } 134 } 135 if (!mIdentityHash.equals(identityHash) && !mLegacyHash.equals(identityHash)) { 136 throw new IllegalStateException("Room cannot verify the data integrity. Looks like" 137 + " you've changed schema but forgot to update the version number. You can" 138 + " simply fix this by increasing the version number."); 139 } 140 } 141 142 private void updateIdentity(SupportSQLiteDatabase db) { 143 createMasterTableIfNotExists(db); 144 db.execSQL(RoomMasterTable.createInsertQuery(mIdentityHash)); 145 } 146 147 private void createMasterTableIfNotExists(SupportSQLiteDatabase db) { 148 db.execSQL(RoomMasterTable.CREATE_QUERY); 149 } 150 151 private static boolean hasRoomMasterTable(SupportSQLiteDatabase db) { 152 Cursor cursor = db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name='" 153 + RoomMasterTable.TABLE_NAME + "'"); 154 //noinspection TryFinallyCanBeTryWithResources 155 try { 156 return cursor.moveToFirst() && cursor.getInt(0) != 0; 157 } finally { 158 cursor.close(); 159 } 160 } 161 162 /** 163 * @hide 164 */ 165 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 166 public abstract static class Delegate { 167 public final int version; 168 169 public Delegate(int version) { 170 this.version = version; 171 } 172 173 protected abstract void dropAllTables(SupportSQLiteDatabase database); 174 175 protected abstract void createAllTables(SupportSQLiteDatabase database); 176 177 protected abstract void onOpen(SupportSQLiteDatabase database); 178 179 protected abstract void onCreate(SupportSQLiteDatabase database); 180 181 /** 182 * Called after a migration run to validate database integrity. 183 * 184 * @param db The SQLite database. 185 */ 186 protected abstract void validateMigration(SupportSQLiteDatabase db); 187 } 188 189 } 190