/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ @file:JvmName("DBUtil") @file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) package androidx.room.util import android.database.AbstractWindowedCursor import android.database.Cursor import android.database.sqlite.SQLiteConstraintException import android.os.Build import android.os.CancellationSignal import androidx.annotation.RestrictTo import androidx.room.RoomDatabase import androidx.sqlite.db.SupportSQLiteCompat import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteQuery import java.io.File import java.io.FileInputStream import java.io.IOException import java.nio.ByteBuffer /** * Performs the SQLiteQuery on the given database. * * This util method encapsulates copying the cursor if the `maybeCopy` parameter is * `true` and either the api level is below a certain threshold or the full result of the * query does not fit in a single window. * * @param db The database to perform the query on. * @param sqLiteQuery The query to perform. * @param maybeCopy True if the result cursor should maybe be copied, false otherwise. * @return Result of the query. * */ @Deprecated( "This is only used in the generated code and shouldn't be called directly." ) fun query(db: RoomDatabase, sqLiteQuery: SupportSQLiteQuery, maybeCopy: Boolean): Cursor { return query(db, sqLiteQuery, maybeCopy, null) } /** * Performs the SQLiteQuery on the given database. * * This util method encapsulates copying the cursor if the `maybeCopy` parameter is * `true` and either the api level is below a certain threshold or the full result of the * query does not fit in a single window. * * @param db The database to perform the query on. * @param sqLiteQuery The query to perform. * @param maybeCopy True if the result cursor should maybe be copied, false otherwise. * @param signal The cancellation signal to be attached to the query. * @return Result of the query. */ fun query( db: RoomDatabase, sqLiteQuery: SupportSQLiteQuery, maybeCopy: Boolean, signal: CancellationSignal? ): Cursor { val cursor = db.query(sqLiteQuery, signal) if (maybeCopy && cursor is AbstractWindowedCursor) { val rowsInCursor = cursor.count // Should fill the window. val rowsInWindow = if (cursor.hasWindow()) { cursor.window.numRows } else { rowsInCursor } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || rowsInWindow < rowsInCursor) { return copyAndClose(cursor) } } return cursor } /** * Drops all FTS content sync triggers created by Room. * * FTS content sync triggers created by Room are those that are found in the sqlite_master table * who's names start with 'room_fts_content_sync_'. * * @param db The database. */ fun dropFtsSyncTriggers(db: SupportSQLiteDatabase) { val existingTriggers = buildList { db.query("SELECT name FROM sqlite_master WHERE type = 'trigger'").useCursor { cursor -> while (cursor.moveToNext()) { add(cursor.getString(0)) } } } existingTriggers.forEach { triggerName -> if (triggerName.startsWith("room_fts_content_sync_")) { db.execSQL("DROP TRIGGER IF EXISTS $triggerName") } } } /** * Checks for foreign key violations by executing a PRAGMA foreign_key_check. */ fun foreignKeyCheck( db: SupportSQLiteDatabase, tableName: String ) { db.query("PRAGMA foreign_key_check(`$tableName`)").useCursor { cursor -> if (cursor.count > 0) { val errorMsg = processForeignKeyCheckFailure(cursor) throw SQLiteConstraintException(errorMsg) } } } /** * Reads the user version number out of the database header from the given file. * * @param databaseFile the database file. * @return the database version * @throws IOException if something goes wrong reading the file, such as bad database header or * missing permissions. * * @see [User Version * Number](https://www.sqlite.org/fileformat.html.user_version_number). */ @Throws(IOException::class) fun readVersion(databaseFile: File): Int { FileInputStream(databaseFile).channel.use { input -> val buffer = ByteBuffer.allocate(4) input.tryLock(60, 4, true) input.position(60) val read = input.read(buffer) if (read != 4) { throw IOException("Bad database header, unable to read 4 bytes at offset 60") } buffer.rewind() return buffer.int // ByteBuffer is big-endian by default } } /** * CancellationSignal is only available from API 16 on. This function will create a new * instance of the Cancellation signal only if the current API > 16. * * @return A new instance of CancellationSignal or null. */ fun createCancellationSignal(): CancellationSignal? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { SupportSQLiteCompat.Api16Impl.createCancellationSignal() } else { null } } /** * Converts the [Cursor] returned in case of a foreign key violation into a detailed * error message for debugging. * * The foreign_key_check pragma returns one row output for each foreign key violation. * * The cursor received has four columns for each row output. The first column is the name of * the child table. The second column is the rowId of the row that contains the foreign key * violation (or NULL if the child table is a WITHOUT ROWID table). The third column is the * name of the parent table. The fourth column is the index of the specific foreign key * constraint that failed. * * @param cursor Cursor containing information regarding the FK violation * @return Error message generated containing debugging information */ private fun processForeignKeyCheckFailure(cursor: Cursor): String { return buildString { val rowCount = cursor.count val fkParentTables = mutableMapOf() while (cursor.moveToNext()) { if (cursor.isFirst) { append("Foreign key violation(s) detected in '") append(cursor.getString(0)).append("'.\n") } val constraintIndex = cursor.getString(3) if (!fkParentTables.containsKey(constraintIndex)) { fkParentTables[constraintIndex] = cursor.getString(2) } } append("Number of different violations discovered: ") append(fkParentTables.keys.size).append("\n") append("Number of rows in violation: ") append(rowCount).append("\n") append("Violation(s) detected in the following constraint(s):\n") for ((key, value) in fkParentTables) { append("\tParent Table = ") append(value) append(", Foreign Key Constraint Index = ") append(key).append("\n") } } }