1 /* 2 * Copyright 2024 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.kotlintestapp.test 18 19 import androidx.kruth.assertThat 20 import androidx.kruth.assertThrows 21 import androidx.room.Room 22 import androidx.room.integration.kotlintestapp.TestDatabase 23 import androidx.room.integration.kotlintestapp.vo.Email 24 import androidx.room.integration.kotlintestapp.vo.User 25 import androidx.sqlite.db.SupportSQLiteDatabase 26 import androidx.sqlite.db.SupportSQLiteOpenHelper 27 import androidx.sqlite.db.SupportSQLiteStatement 28 import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory 29 import androidx.test.ext.junit.runners.AndroidJUnit4 30 import androidx.test.filters.SmallTest 31 import androidx.test.platform.app.InstrumentationRegistry 32 import java.util.IdentityHashMap 33 import java.util.concurrent.atomic.AtomicBoolean 34 import org.junit.Before 35 import org.junit.Test 36 import org.junit.runner.RunWith 37 38 @SmallTest 39 @RunWith(AndroidJUnit4::class) 40 class MissingMasterTableTest { 41 42 @Before deleteDbnull43 fun deleteDb() { 44 InstrumentationRegistry.getInstrumentation().targetContext.deleteDatabase("existingDb.db") 45 } 46 47 @Test openDatabaseWithMissingRoomMasterTablenull48 fun openDatabaseWithMissingRoomMasterTable() { 49 val allowVersionUpdates = AtomicBoolean(true) 50 val factory = MyOpenHelperFactory(allowVersionUpdates) 51 val user = User("u1", Email("e1", "email address 1"), Email("e2", "email address 2")) 52 53 fun openDb(): TestDatabase { 54 return Room.databaseBuilder( 55 InstrumentationRegistry.getInstrumentation().targetContext, 56 klass = TestDatabase::class.java, 57 name = "existingDb.db" 58 ) 59 .openHelperFactory(factory) 60 .build() 61 } 62 // Open first version of the database, and remove the room_master_table. This will cause 63 // a failure next time we open the database, due to a verification failure. 64 val db1 = openDb() 65 db1.usersDao().insertUser(user) 66 // Delete the room_master_table 67 db1.compileStatement("DROP TABLE room_master_table;").execute() 68 db1.close() 69 allowVersionUpdates.set(false) 70 71 // Second version of the database fails at open, the failed transaction should be rolled 72 // back, allowing the third version to open successfully. 73 val db2 = openDb() 74 assertThrows<Throwable> { assertThat(db2.usersDao().getUsers()).containsExactly(user) } 75 .hasMessageThat() 76 .contains("no-version-updates") 77 db2.close() 78 allowVersionUpdates.set(true) 79 80 // Third version of the database is expected to open with no failures. 81 val db3 = openDb() 82 assertThat(db3.usersDao().getUsers()).containsExactly(user) 83 db3.close() 84 } 85 86 private class MyOpenHelperFactory(private val allowVersionUpdates: AtomicBoolean) : 87 SupportSQLiteOpenHelper.Factory { 88 private val delegate = FrameworkSQLiteOpenHelperFactory() 89 createnull90 override fun create( 91 configuration: SupportSQLiteOpenHelper.Configuration 92 ): SupportSQLiteOpenHelper { 93 val newConfig = 94 SupportSQLiteOpenHelper.Configuration( 95 context = configuration.context, 96 name = configuration.name, 97 callback = MyCallback(allowVersionUpdates, configuration.callback), 98 useNoBackupDirectory = configuration.useNoBackupDirectory, 99 allowDataLossOnRecovery = configuration.allowDataLossOnRecovery 100 ) 101 return MyOpenHelper(allowVersionUpdates, delegate.create(newConfig)) 102 } 103 } 104 105 private class MyOpenHelper( 106 private val allowVersionUpdates: AtomicBoolean, 107 private val delegate: SupportSQLiteOpenHelper, 108 ) : SupportSQLiteOpenHelper { 109 override val databaseName: String? 110 get() = delegate.databaseName 111 setWriteAheadLoggingEnablednull112 override fun setWriteAheadLoggingEnabled(enabled: Boolean) { 113 delegate.setWriteAheadLoggingEnabled(enabled) 114 } 115 <lambda>null116 override val writableDatabase: SupportSQLiteDatabase by lazy { 117 MySupportSQLiteDatabase(allowVersionUpdates, delegate.writableDatabase) 118 } <lambda>null119 override val readableDatabase: SupportSQLiteDatabase by lazy { 120 MySupportSQLiteDatabase(allowVersionUpdates, delegate.writableDatabase) 121 } 122 closenull123 override fun close() { 124 delegate.close() 125 } 126 } 127 128 private class MySupportSQLiteDatabase( 129 private val allowVersionUpdates: AtomicBoolean, 130 private val delegate: SupportSQLiteDatabase <lambda>null131 ) : SupportSQLiteDatabase by delegate { 132 override fun compileStatement(sql: String): SupportSQLiteStatement { 133 // The "insert" SQL should not go through if version updates are disabled. 134 // This mimics app halt scenario, where a master table is created but not initialized. 135 if ( 136 sql.startsWith("INSERT OR REPLACE INTO room_master_table") && 137 !allowVersionUpdates.get() 138 ) { 139 throw RuntimeException("no-version-updates") 140 } 141 return delegate.compileStatement(sql) 142 } 143 } 144 145 private class MyCallback( 146 private val allowVersionUpdates: AtomicBoolean, 147 private val delegate: SupportSQLiteOpenHelper.Callback 148 ) : SupportSQLiteOpenHelper.Callback(delegate.version) { 149 private val wrappers = IdentityHashMap<SupportSQLiteDatabase, MySupportSQLiteDatabase>() 150 SupportSQLiteDatabasenull151 private fun SupportSQLiteDatabase.wrap(): MySupportSQLiteDatabase { 152 if (this is MySupportSQLiteDatabase) return this 153 return wrappers.getOrPut(this) { MySupportSQLiteDatabase(allowVersionUpdates, this) } 154 } 155 onConfigurenull156 override fun onConfigure(db: SupportSQLiteDatabase) { 157 delegate.onConfigure(db.wrap()) 158 } 159 onDowngradenull160 override fun onDowngrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) { 161 delegate.onDowngrade(db.wrap(), oldVersion, newVersion) 162 } 163 onOpennull164 override fun onOpen(db: SupportSQLiteDatabase) { 165 delegate.onOpen(db.wrap()) 166 } 167 onCorruptionnull168 override fun onCorruption(db: SupportSQLiteDatabase) { 169 delegate.onCorruption(db.wrap()) 170 } 171 onCreatenull172 override fun onCreate(db: SupportSQLiteDatabase) { 173 delegate.onCreate(db.wrap()) 174 } 175 onUpgradenull176 override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) { 177 delegate.onUpgrade(db.wrap(), oldVersion, newVersion) 178 } 179 } 180 } 181