mirror of https://github.com/M66B/FairEmail.git
214 lines
7.4 KiB
Kotlin
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")
|
||
|
}
|
||
|
}
|
||
|
}
|