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