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.integration.testapp.migration; 18 19 import static org.hamcrest.CoreMatchers.containsString; 20 import static org.hamcrest.CoreMatchers.endsWith; 21 import static org.hamcrest.CoreMatchers.instanceOf; 22 import static org.hamcrest.CoreMatchers.is; 23 import static org.hamcrest.CoreMatchers.not; 24 import static org.hamcrest.CoreMatchers.notNullValue; 25 import static org.hamcrest.CoreMatchers.nullValue; 26 import static org.hamcrest.CoreMatchers.startsWith; 27 import static org.hamcrest.MatcherAssert.assertThat; 28 29 import android.content.Context; 30 import android.support.test.InstrumentationRegistry; 31 import android.support.test.filters.SmallTest; 32 import android.support.test.runner.AndroidJUnit4; 33 34 import androidx.room.Room; 35 import androidx.room.migration.Migration; 36 import androidx.room.testing.MigrationTestHelper; 37 import androidx.room.util.TableInfo; 38 import androidx.sqlite.db.SupportSQLiteDatabase; 39 import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory; 40 41 import org.hamcrest.MatcherAssert; 42 import org.junit.Rule; 43 import org.junit.Test; 44 import org.junit.runner.RunWith; 45 46 import java.io.FileNotFoundException; 47 import java.io.IOException; 48 import java.util.List; 49 50 /** 51 * Test custom database migrations. 52 */ 53 @RunWith(AndroidJUnit4.class) 54 @SmallTest 55 public class MigrationTest { 56 private static final String TEST_DB = "migration-test"; 57 @Rule 58 public MigrationTestHelper helper; 59 60 public MigrationTest() { 61 helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), 62 MigrationDb.class.getCanonicalName()); 63 } 64 65 @Test 66 public void giveBadResource() throws IOException { 67 MigrationTestHelper helper = new MigrationTestHelper( 68 InstrumentationRegistry.getInstrumentation(), 69 "foo", new FrameworkSQLiteOpenHelperFactory()); 70 try { 71 helper.createDatabase(TEST_DB, 1); 72 throw new AssertionError("must have failed with missing file exception"); 73 } catch (FileNotFoundException exception) { 74 assertThat(exception.getMessage(), containsString("Cannot find")); 75 } 76 } 77 78 @Test 79 public void startInCurrentVersion() throws IOException { 80 SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 81 MigrationDb.LATEST_VERSION); 82 final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(db); 83 dao.insertIntoEntity1(2, "x"); 84 db.close(); 85 MigrationDb migrationDb = getLatestDb(); 86 List<MigrationDb.Entity1> items = migrationDb.dao().loadAllEntity1s(); 87 helper.closeWhenFinished(migrationDb); 88 assertThat(items.size(), is(1)); 89 } 90 91 @Test 92 public void addTable() throws IOException { 93 SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1); 94 final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(db); 95 dao.insertIntoEntity1(2, "foo"); 96 dao.insertIntoEntity1(3, "bar"); 97 db.close(); 98 db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2); 99 new MigrationDb.Dao_V2(db).insertIntoEntity2(3, "blah"); 100 db.close(); 101 MigrationDb migrationDb = getLatestDb(); 102 List<MigrationDb.Entity1> entity1s = migrationDb.dao().loadAllEntity1s(); 103 104 assertThat(entity1s.size(), is(2)); 105 MigrationDb.Entity2 entity2 = new MigrationDb.Entity2(); 106 entity2.id = 2; 107 entity2.name = "bar"; 108 // assert no error happens 109 migrationDb.dao().insert(entity2); 110 List<MigrationDb.Entity2> entity2s = migrationDb.dao().loadAllEntity2s(); 111 assertThat(entity2s.size(), is(2)); 112 } 113 114 private MigrationDb getLatestDb() { 115 MigrationDb db = Room.databaseBuilder( 116 InstrumentationRegistry.getInstrumentation().getTargetContext(), 117 MigrationDb.class, TEST_DB).addMigrations(ALL_MIGRATIONS).build(); 118 // trigger open 119 db.beginTransaction(); 120 db.endTransaction(); 121 helper.closeWhenFinished(db); 122 return db; 123 } 124 125 @Test 126 public void addTableFailure() throws IOException { 127 testFailure(1, 2); 128 } 129 130 @Test 131 public void addColumnFailure() throws IOException { 132 SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 2); 133 db.close(); 134 IllegalStateException caught = null; 135 try { 136 helper.runMigrationsAndValidate(TEST_DB, 3, true, new EmptyMigration(2, 3)); 137 } catch (IllegalStateException ex) { 138 caught = ex; 139 } 140 assertThat(caught, instanceOf(IllegalStateException.class)); 141 } 142 143 @Test 144 public void addColumn() throws IOException { 145 SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 2); 146 MigrationDb.Dao_V2 v2Dao = new MigrationDb.Dao_V2(db); 147 v2Dao.insertIntoEntity2(7, "blah"); 148 db.close(); 149 helper.runMigrationsAndValidate(TEST_DB, 3, true, MIGRATION_2_3); 150 // trigger open. 151 MigrationDb migrationDb = getLatestDb(); 152 List<MigrationDb.Entity2> entity2s = migrationDb.dao().loadAllEntity2s(); 153 assertThat(entity2s.size(), is(1)); 154 assertThat(entity2s.get(0).name, is("blah")); 155 assertThat(entity2s.get(0).addedInV3, is(nullValue())); 156 157 List<MigrationDb.Entity2Pojo> entity2Pojos = migrationDb.dao().loadAllEntity2sAsPojo(); 158 assertThat(entity2Pojos.size(), is(1)); 159 assertThat(entity2Pojos.get(0).name, is("blah")); 160 assertThat(entity2Pojos.get(0).addedInV3, is(nullValue())); 161 } 162 163 @Test 164 public void failedToRemoveColumn() throws IOException { 165 testFailure(4, 5); 166 } 167 168 @Test 169 public void removeColumn() throws IOException { 170 helper.createDatabase(TEST_DB, 4); 171 final SupportSQLiteDatabase db = helper.runMigrationsAndValidate(TEST_DB, 172 5, true, MIGRATION_4_5); 173 final TableInfo info = TableInfo.read(db, MigrationDb.Entity3.TABLE_NAME); 174 assertThat(info.columns.size(), is(2)); 175 } 176 177 @Test 178 public void dropTable() throws IOException { 179 helper.createDatabase(TEST_DB, 5); 180 final SupportSQLiteDatabase db = helper.runMigrationsAndValidate(TEST_DB, 181 6, true, MIGRATION_5_6); 182 final TableInfo info = TableInfo.read(db, MigrationDb.Entity3.TABLE_NAME); 183 assertThat(info.columns.size(), is(0)); 184 } 185 186 @Test 187 public void failedToDropTable() throws IOException { 188 testFailure(5, 6); 189 } 190 191 @Test 192 public void failedToDropTableDontVerify() throws IOException { 193 helper.createDatabase(TEST_DB, 5); 194 final SupportSQLiteDatabase db = helper.runMigrationsAndValidate(TEST_DB, 195 6, false, new EmptyMigration(5, 6)); 196 final TableInfo info = TableInfo.read(db, MigrationDb.Entity3.TABLE_NAME); 197 assertThat(info.columns.size(), is(2)); 198 } 199 200 @Test 201 public void failedForeignKey() throws IOException { 202 final SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 6); 203 db.close(); 204 Throwable throwable = null; 205 try { 206 helper.runMigrationsAndValidate(TEST_DB, 207 7, false, new Migration(6, 7) { 208 @Override 209 public void migrate(SupportSQLiteDatabase database) { 210 database.execSQL("CREATE TABLE Entity4 (`id` INTEGER NOT NULL," 211 + " `name` TEXT, PRIMARY KEY(`id`))"); 212 } 213 }); 214 } catch (Throwable t) { 215 throwable = t; 216 } 217 assertThat(throwable, instanceOf(IllegalStateException.class)); 218 //noinspection ConstantConditions 219 assertThat(throwable.getMessage(), containsString("Migration failed")); 220 } 221 222 @Test 223 public void newTableWithForeignKey() throws IOException { 224 helper.createDatabase(TEST_DB, 6); 225 final SupportSQLiteDatabase db = helper.runMigrationsAndValidate(TEST_DB, 226 7, false, MIGRATION_6_7); 227 final TableInfo info = TableInfo.read(db, MigrationDb.Entity4.TABLE_NAME); 228 assertThat(info.foreignKeys.size(), is(1)); 229 } 230 231 @Test 232 public void missingMigration() throws IOException { 233 SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 1); 234 database.close(); 235 try { 236 Context targetContext = InstrumentationRegistry.getTargetContext(); 237 MigrationDb db = Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB) 238 .build(); 239 db.dao().loadAllEntity1s(); 240 throw new AssertionError("Should've failed :/"); 241 } catch (IllegalStateException ignored) { 242 } 243 } 244 245 @Test 246 public void missingMigrationNuke() throws IOException { 247 SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 1); 248 final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(database); 249 dao.insertIntoEntity1(2, "foo"); 250 dao.insertIntoEntity1(3, "bar"); 251 database.close(); 252 253 Context targetContext = InstrumentationRegistry.getTargetContext(); 254 MigrationDb db = Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB) 255 .fallbackToDestructiveMigration() 256 .build(); 257 assertThat(db.dao().loadAllEntity1s().size(), is(0)); 258 db.close(); 259 } 260 261 @Test 262 public void failWithIdentityCheck() throws IOException { 263 for (int i = 1; i < MigrationDb.LATEST_VERSION; i++) { 264 String name = "test_" + i; 265 helper.createDatabase(name, i).close(); 266 IllegalStateException exception = null; 267 try { 268 MigrationDb db = Room.databaseBuilder( 269 InstrumentationRegistry.getInstrumentation().getTargetContext(), 270 MigrationDb.class, name).build(); 271 db.runInTransaction(new Runnable() { 272 @Override 273 public void run() { 274 // do nothing 275 } 276 }); 277 } catch (IllegalStateException ex) { 278 exception = ex; 279 } 280 MatcherAssert.assertThat("identity detection should've failed", 281 exception, notNullValue()); 282 } 283 } 284 285 @Test 286 public void fallbackToDestructiveMigrationFrom_destructiveMigrationOccursForSuppliedVersion() 287 throws IOException { 288 SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 6); 289 final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(database); 290 dao.insertIntoEntity1(2, "foo"); 291 dao.insertIntoEntity1(3, "bar"); 292 database.close(); 293 Context targetContext = InstrumentationRegistry.getTargetContext(); 294 295 MigrationDb db = Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB) 296 .fallbackToDestructiveMigrationFrom(6) 297 .build(); 298 299 assertThat(db.dao().loadAllEntity1s().size(), is(0)); 300 } 301 302 @Test 303 public void fallbackToDestructiveMigrationFrom_suppliedValueIsMigrationStartVersion_exception() 304 throws IOException { 305 SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 6); 306 database.close(); 307 Context targetContext = InstrumentationRegistry.getTargetContext(); 308 309 Throwable throwable = null; 310 try { 311 Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB) 312 .addMigrations(MIGRATION_6_7) 313 .fallbackToDestructiveMigrationFrom(6) 314 .build(); 315 } catch (Throwable t) { 316 throwable = t; 317 } 318 319 assertThat(throwable, is(not(nullValue()))); 320 //noinspection ConstantConditions 321 assertThat(throwable.getMessage(), 322 startsWith("Inconsistency detected. A Migration was supplied to")); 323 assertThat(throwable.getMessage(), endsWith("6")); 324 } 325 326 @Test 327 public void fallbackToDestructiveMigrationFrom_suppliedValueIsMigrationEndVersion_exception() 328 throws IOException { 329 SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 5); 330 database.close(); 331 Context targetContext = InstrumentationRegistry.getTargetContext(); 332 333 Throwable throwable = null; 334 try { 335 Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB) 336 .addMigrations(MIGRATION_5_6) 337 .fallbackToDestructiveMigrationFrom(6) 338 .build(); 339 } catch (Throwable t) { 340 throwable = t; 341 } 342 343 assertThat(throwable, is(not(nullValue()))); 344 //noinspection ConstantConditions 345 assertThat(throwable.getMessage(), 346 startsWith("Inconsistency detected. A Migration was supplied to")); 347 assertThat(throwable.getMessage(), endsWith("6")); 348 } 349 350 private void testFailure(int startVersion, int endVersion) throws IOException { 351 final SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, startVersion); 352 db.close(); 353 Throwable throwable = null; 354 try { 355 helper.runMigrationsAndValidate(TEST_DB, endVersion, true, 356 new EmptyMigration(startVersion, endVersion)); 357 } catch (Throwable t) { 358 throwable = t; 359 } 360 assertThat(throwable, instanceOf(IllegalStateException.class)); 361 //noinspection ConstantConditions 362 assertThat(throwable.getMessage(), containsString("Migration failed")); 363 } 364 365 private static final Migration MIGRATION_1_2 = new Migration(1, 2) { 366 @Override 367 public void migrate(SupportSQLiteDatabase database) { 368 database.execSQL("CREATE TABLE IF NOT EXISTS `Entity2` (" 369 + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," 370 + " `name` TEXT)"); 371 } 372 }; 373 374 private static final Migration MIGRATION_2_3 = new Migration(2, 3) { 375 @Override 376 public void migrate(SupportSQLiteDatabase database) { 377 database.execSQL("ALTER TABLE " + MigrationDb.Entity2.TABLE_NAME 378 + " ADD COLUMN addedInV3 TEXT"); 379 } 380 }; 381 382 private static final Migration MIGRATION_3_4 = new Migration(3, 4) { 383 @Override 384 public void migrate(SupportSQLiteDatabase database) { 385 database.execSQL("CREATE TABLE IF NOT EXISTS `Entity3` (`id` INTEGER NOT NULL," 386 + " `removedInV5` TEXT, `name` TEXT, PRIMARY KEY(`id`))"); 387 } 388 }; 389 390 private static final Migration MIGRATION_4_5 = new Migration(4, 5) { 391 @Override 392 public void migrate(SupportSQLiteDatabase database) { 393 database.execSQL("CREATE TABLE IF NOT EXISTS `Entity3_New` (`id` INTEGER NOT NULL," 394 + " `name` TEXT, PRIMARY KEY(`id`))"); 395 database.execSQL("INSERT INTO Entity3_New(`id`, `name`) " 396 + "SELECT `id`, `name` FROM Entity3"); 397 database.execSQL("DROP TABLE Entity3"); 398 database.execSQL("ALTER TABLE Entity3_New RENAME TO Entity3"); 399 } 400 }; 401 402 private static final Migration MIGRATION_5_6 = new Migration(5, 6) { 403 @Override 404 public void migrate(SupportSQLiteDatabase database) { 405 database.execSQL("DROP TABLE " + MigrationDb.Entity3.TABLE_NAME); 406 } 407 }; 408 409 private static final Migration MIGRATION_6_7 = new Migration(6, 7) { 410 @Override 411 public void migrate(SupportSQLiteDatabase database) { 412 database.execSQL("CREATE TABLE IF NOT EXISTS " + MigrationDb.Entity4.TABLE_NAME 413 + " (`id` INTEGER NOT NULL, `name` TEXT COLLATE NOCASE, PRIMARY KEY(`id`)," 414 + " FOREIGN KEY(`name`) REFERENCES `Entity1`(`name`)" 415 + " ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED)"); 416 database.execSQL("CREATE UNIQUE INDEX `index_entity1` ON " 417 + MigrationDb.Entity1.TABLE_NAME + " (`name`)"); 418 } 419 }; 420 421 private static final Migration[] ALL_MIGRATIONS = new Migration[]{MIGRATION_1_2, 422 MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7}; 423 424 static final class EmptyMigration extends Migration { 425 EmptyMigration(int startVersion, int endVersion) { 426 super(startVersion, endVersion); 427 } 428 429 @Override 430 public void migrate(SupportSQLiteDatabase database) { 431 // do nothing 432 } 433 } 434 } 435