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