1 /*
<lambda>null2  * Copyright 2023 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 androidx.annotation.RestrictTo
20 import androidx.room.RoomDatabase.JournalMode.TRUNCATE
21 import androidx.room.RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING
22 import androidx.room.concurrent.ExclusiveLock
23 import androidx.room.util.findMigrationPath
24 import androidx.room.util.isMigrationRequired
25 import androidx.sqlite.SQLiteConnection
26 import androidx.sqlite.SQLiteDriver
27 import androidx.sqlite.execSQL
28 
29 /** Expect implementation declaration of Room's connection manager. */
30 internal expect class RoomConnectionManager
31 
32 /**
33  * Base class for Room's database connection manager, responsible for opening and managing such
34  * connections, including performing migrations if necessary and validating schema.
35  */
36 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
37 abstract class BaseRoomConnectionManager {
38 
39     protected abstract val configuration: DatabaseConfiguration
40     protected abstract val openDelegate: RoomOpenDelegate
41     protected abstract val callbacks: List<RoomDatabase.Callback>
42 
43     // Flag indicating that the database was configured, i.e. at least one connection has been
44     // opened, configured and schema validated.
45     private var isConfigured = false
46     // Flag set during initialization to prevent recursive initialization.
47     private var isInitializing = false
48 
49     abstract suspend fun <R> useConnection(isReadOnly: Boolean, block: suspend (Transactor) -> R): R
50 
51     // Lets impl class resolve driver file name if necessary.
52     internal open fun resolveFileName(fileName: String): String = fileName
53 
54     /* A driver wrapper that configures opened connections per the manager. */
55     protected inner class DriverWrapper(private val actual: SQLiteDriver) : SQLiteDriver {
56         override fun open(fileName: String): SQLiteConnection {
57             return openLocked(resolveFileName(fileName))
58         }
59 
60         private fun openLocked(filename: String) =
61             ExclusiveLock(
62                     filename = filename,
63                     useFileLock = !isConfigured && !isInitializing && filename != ":memory:"
64                 )
65                 .withLock(
66                     onLocked = {
67                         check(!isInitializing) {
68                             "Recursive database initialization detected. Did you try to use the " +
69                                 "database instance during initialization? Maybe in one of the " +
70                                 "callbacks?"
71                         }
72                         val connection = actual.open(filename)
73                         if (!isConfigured) {
74                             // Perform initial connection configuration
75                             try {
76                                 isInitializing = true
77                                 configureDatabase(connection)
78                             } finally {
79                                 isInitializing = false
80                             }
81                         } else {
82                             // Perform other non-initial connection configuration
83                             configurationConnection(connection)
84                         }
85                         return@withLock connection
86                     },
87                     onLockError = { error ->
88                         throw IllegalStateException(
89                             "Unable to open database '$filename'. Was a proper path / " +
90                                 "name used in Room's database builder?",
91                             error
92                         )
93                     }
94                 )
95     }
96 
97     /**
98      * Performs initial database connection configuration and opening procedure, such as running
99      * migrations if necessary, validating schema and invoking configured callbacks if any.
100      */
101     // TODO(b/316944352): Retry mechanism
102     private fun configureDatabase(connection: SQLiteConnection) {
103         configureJournalMode(connection)
104         configureSynchronousFlag(connection)
105         configureBusyTimeout(connection)
106         val version =
107             connection.prepare("PRAGMA user_version").use { statement ->
108                 statement.step()
109                 statement.getLong(0).toInt()
110             }
111         if (version != openDelegate.version) {
112             connection.execSQL("BEGIN EXCLUSIVE TRANSACTION")
113             runCatching {
114                     if (version == 0) {
115                         onCreate(connection)
116                     } else {
117                         onMigrate(connection, version, openDelegate.version)
118                     }
119                     connection.execSQL("PRAGMA user_version = ${openDelegate.version}")
120                 }
121                 .onSuccess { connection.execSQL("END TRANSACTION") }
122                 .onFailure {
123                     connection.execSQL("ROLLBACK TRANSACTION")
124                     throw it
125                 }
126         }
127         onOpen(connection)
128     }
129 
130     /**
131      * Performs non-initial database connection configuration, specifically executing any
132      * per-connection PRAGMA.
133      */
134     private fun configurationConnection(connection: SQLiteConnection) {
135         configureSynchronousFlag(connection)
136         configureBusyTimeout(connection)
137         openDelegate.onOpen(connection)
138     }
139 
140     private fun configureJournalMode(connection: SQLiteConnection) {
141         val wal = configuration.journalMode == WRITE_AHEAD_LOGGING
142         if (wal) {
143             connection.execSQL("PRAGMA journal_mode = WAL")
144         } else {
145             connection.execSQL("PRAGMA journal_mode = TRUNCATE")
146         }
147     }
148 
149     private fun configureSynchronousFlag(connection: SQLiteConnection) {
150         // Use NORMAL in WAL mode and FULL for non-WAL as recommended in
151         // https://www.sqlite.org/pragma.html#pragma_synchronous
152         val wal = configuration.journalMode == WRITE_AHEAD_LOGGING
153         if (wal) {
154             connection.execSQL("PRAGMA synchronous = NORMAL")
155         } else {
156             connection.execSQL("PRAGMA synchronous = FULL")
157         }
158     }
159 
160     private fun configureBusyTimeout(connection: SQLiteConnection) {
161         // Set a busy timeout if no timeout is set to avoid SQLITE_BUSY during slow I/O or during
162         // an auto-checkpoint.
163         val currentBusyTimeout =
164             connection.prepare("PRAGMA busy_timeout").use {
165                 it.step()
166                 it.getLong(0)
167             }
168         if (currentBusyTimeout < BUSY_TIMEOUT_MS) {
169             connection.execSQL("PRAGMA busy_timeout = $BUSY_TIMEOUT_MS")
170         }
171     }
172 
173     protected fun onCreate(connection: SQLiteConnection) {
174         val isEmptyDatabase = hasEmptySchema(connection)
175         openDelegate.createAllTables(connection)
176         if (!isEmptyDatabase) {
177             // A 0 version pre-populated database goes through the create path, Room only allows
178             // for versions greater than 0, so if we find the database not to be empty, then it is
179             // a pre-populated, we must validate it to see if its suitable for usage.
180             val result = openDelegate.onValidateSchema(connection)
181             if (!result.isValid) {
182                 error("Pre-packaged database has an invalid schema: ${result.expectedFoundMsg}")
183             }
184         }
185         updateIdentity(connection)
186         openDelegate.onCreate(connection)
187         invokeCreateCallback(connection)
188     }
189 
190     private fun hasEmptySchema(connection: SQLiteConnection): Boolean =
191         connection
192             .prepare("SELECT count(*) FROM sqlite_master WHERE name != 'android_metadata'")
193             .use { it.step() && it.getLong(0) == 0L }
194 
195     private fun updateIdentity(connection: SQLiteConnection) {
196         createMasterTableIfNotExists(connection)
197         connection.execSQL(RoomMasterTable.createInsertQuery(openDelegate.identityHash))
198     }
199 
200     private fun createMasterTableIfNotExists(connection: SQLiteConnection) {
201         connection.execSQL(RoomMasterTable.CREATE_QUERY)
202     }
203 
204     protected fun onMigrate(connection: SQLiteConnection, oldVersion: Int, newVersion: Int) {
205         var migrated = false
206         val migrations = configuration.migrationContainer.findMigrationPath(oldVersion, newVersion)
207         if (migrations != null) {
208             openDelegate.onPreMigrate(connection)
209             migrations.forEach { it.migrate(connection) }
210             val result = openDelegate.onValidateSchema(connection)
211             if (!result.isValid) {
212                 error("Migration didn't properly handle: ${result.expectedFoundMsg}")
213             }
214             openDelegate.onPostMigrate(connection)
215             updateIdentity(connection)
216             migrated = true
217         }
218         if (!migrated) {
219             if (configuration.isMigrationRequired(oldVersion, newVersion)) {
220                 error(
221                     "A migration from $oldVersion to $newVersion was required but not found. " +
222                         "Please provide the necessary Migration path via " +
223                         "RoomDatabase.Builder.addMigration(...) or allow for " +
224                         "destructive migrations via one of the " +
225                         "RoomDatabase.Builder.fallbackToDestructiveMigration* functions."
226                 )
227             }
228             dropAllTables(connection)
229             invokeDestructiveMigrationCallback(connection)
230             openDelegate.createAllTables(connection)
231         }
232     }
233 
234     private fun dropAllTables(connection: SQLiteConnection) {
235         if (configuration.allowDestructiveMigrationForAllTables) {
236             // Drops all tables and views (excluding special ones)
237             connection
238                 .prepare(
239                     "SELECT name, type FROM sqlite_master WHERE type = 'table' OR type = 'view'"
240                 )
241                 .use { statement ->
242                     buildList {
243                         while (statement.step()) {
244                             val name = statement.getText(0)
245                             if (name.startsWith("sqlite_") || name == "android_metadata") {
246                                 continue
247                             }
248                             val isView = statement.getText(1) == "view"
249                             add(name to isView)
250                         }
251                     }
252                 }
253                 .forEach { (name, isView) ->
254                     if (isView) {
255                         connection.execSQL("DROP VIEW IF EXISTS $name")
256                     } else {
257                         connection.execSQL("DROP TABLE IF EXISTS $name")
258                     }
259                 }
260         } else {
261             // Drops known tables (Room entity tables)
262             openDelegate.dropAllTables(connection)
263         }
264     }
265 
266     protected fun onOpen(connection: SQLiteConnection) {
267         checkIdentity(connection)
268         openDelegate.onOpen(connection)
269         invokeOpenCallback(connection)
270         isConfigured = true
271     }
272 
273     private fun checkIdentity(connection: SQLiteConnection) {
274         if (hasRoomMasterTable(connection)) {
275             val identityHash: String? =
276                 connection.prepare(RoomMasterTable.READ_QUERY).use {
277                     if (it.step()) {
278                         it.getText(0)
279                     } else {
280                         null
281                     }
282                 }
283             if (
284                 openDelegate.identityHash != identityHash &&
285                     openDelegate.legacyIdentityHash != identityHash
286             ) {
287                 error(
288                     "Room cannot verify the data integrity. Looks like" +
289                         " you've changed schema but forgot to update the version number. You can" +
290                         " simply fix this by increasing the version number. Expected identity" +
291                         " hash: ${openDelegate.identityHash}, found: $identityHash"
292                 )
293             }
294         } else {
295             connection.execSQL("BEGIN EXCLUSIVE TRANSACTION")
296             runCatching {
297                     // No room_master_table, this might an a pre-populated DB, we must validate to
298                     // see
299                     // if it's suitable for usage.
300                     val result = openDelegate.onValidateSchema(connection)
301                     if (!result.isValid) {
302                         error(
303                             "Pre-packaged database has an invalid schema: ${result.expectedFoundMsg}"
304                         )
305                     }
306                     openDelegate.onPostMigrate(connection)
307                     updateIdentity(connection)
308                 }
309                 .onSuccess { connection.execSQL("END TRANSACTION") }
310                 .onFailure {
311                     connection.execSQL("ROLLBACK TRANSACTION")
312                     throw it
313                 }
314         }
315     }
316 
317     private fun hasRoomMasterTable(connection: SQLiteConnection): Boolean =
318         connection
319             .prepare(
320                 "SELECT 1 FROM sqlite_master " +
321                     "WHERE type = 'table' AND name = '${RoomMasterTable.TABLE_NAME}'"
322             )
323             .use { it.step() && it.getLong(0) != 0L }
324 
325     @Suppress("REDUNDANT_ELSE_IN_WHEN") // Redundant in common but not in Android
326     protected fun RoomDatabase.JournalMode.getMaxNumberOfReaders() =
327         when (this) {
328             TRUNCATE -> 1
329             WRITE_AHEAD_LOGGING -> 4
330             else -> error("Can't get max number of reader for journal mode '$this'")
331         }
332 
333     @Suppress("REDUNDANT_ELSE_IN_WHEN") // Redundant in common but not in Android
334     protected fun RoomDatabase.JournalMode.getMaxNumberOfWriters() =
335         when (this) {
336             TRUNCATE -> 1
337             WRITE_AHEAD_LOGGING -> 1
338             else -> error("Can't get max number of writers for journal mode '$this'")
339         }
340 
341     private fun invokeCreateCallback(connection: SQLiteConnection) {
342         callbacks.forEach { it.onCreate(connection) }
343     }
344 
345     private fun invokeDestructiveMigrationCallback(connection: SQLiteConnection) {
346         callbacks.forEach { it.onDestructiveMigration(connection) }
347     }
348 
349     private fun invokeOpenCallback(connection: SQLiteConnection) {
350         callbacks.forEach { it.onOpen(connection) }
351     }
352 
353     companion object {
354         /*
355          * Busy timeout amount. This wait time is relevant to same-process connections, if a
356          * database is used across multiple processes, it is recommended that the developer sets a
357          * higher timeout.
358          */
359         const val BUSY_TIMEOUT_MS = 3000
360     }
361 }
362