FairEmail/app/src/main/java/androidx/room/util/DBUtil.kt

214 lines
7.4 KiB
Kotlin

/*
* 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<String, String>()
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")
}
}
}