mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Add additional CDN reconciliations to BackupMediaSnapshotSyncJob.
Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
committed by
Cody Henthorne
parent
85647f1258
commit
f73d929feb
@@ -12,8 +12,10 @@ dependencies {
|
||||
|
||||
implementation(libs.androidx.sqlite)
|
||||
implementation(libs.androidx.documentfile)
|
||||
testImplementation(libs.androidx.sqlite.framework)
|
||||
|
||||
testImplementation(testLibs.junit.junit)
|
||||
testImplementation(testLibs.assertk)
|
||||
testImplementation(testLibs.robolectric.robolectric)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.core.content.contentValuesOf
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteQueryBuilder
|
||||
import androidx.sqlite.db.SupportSQLiteStatement
|
||||
import org.signal.core.util.SqlUtil.ForeignKeyViolation
|
||||
import org.signal.core.util.logging.Log
|
||||
import kotlin.time.Duration
|
||||
@@ -246,10 +247,34 @@ fun SupportSQLiteDatabase.deleteAll(tableName: String): Int {
|
||||
return this.delete(tableName, null, arrayOfNulls<String>(0))
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins an INSERT statement with a helpful builder pattern.
|
||||
*/
|
||||
fun SupportSQLiteDatabase.insertInto(tableName: String): InsertBuilderPart1 {
|
||||
return InsertBuilderPart1(this, tableName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind an arbitrary value to an index. It will handle calling the correct bind method based on the class type.
|
||||
* @param index The index you want to bind to. Important: Indexes start at 1, not 0.
|
||||
*/
|
||||
fun SupportSQLiteStatement.bindValue(index: Int, value: Any?) {
|
||||
when (value) {
|
||||
null -> this.bindNull(index)
|
||||
is DatabaseId -> this.bindString(index, value.serialize())
|
||||
is Boolean -> this.bindLong(index, value.toInt().toLong())
|
||||
is ByteArray -> this.bindBlob(index, value)
|
||||
is Number -> {
|
||||
if (value.toLong() == value) {
|
||||
this.bindLong(index, value.toLong())
|
||||
} else {
|
||||
this.bindDouble(index, value.toDouble())
|
||||
}
|
||||
}
|
||||
else -> this.bindString(index, value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
class SelectBuilderPart1(
|
||||
private val db: SupportSQLiteDatabase,
|
||||
private val columns: Array<String>
|
||||
@@ -422,7 +447,7 @@ class UpdateBuilderPart2(
|
||||
) {
|
||||
fun where(where: String, vararg whereArgs: Any): UpdateBuilderPart3 {
|
||||
require(where.isNotBlank())
|
||||
return UpdateBuilderPart3(db, tableName, values, where, SqlUtil.buildArgs(*whereArgs))
|
||||
return UpdateBuilderPart3(db, tableName, values, where, whereArgs.toArgs())
|
||||
}
|
||||
|
||||
fun where(where: String, whereArgs: Array<String>): UpdateBuilderPart3 {
|
||||
@@ -436,11 +461,45 @@ class UpdateBuilderPart3(
|
||||
private val tableName: String,
|
||||
private val values: ContentValues,
|
||||
private val where: String,
|
||||
private val whereArgs: Array<String>
|
||||
private val whereArgs: Array<out Any?>
|
||||
) {
|
||||
@JvmOverloads
|
||||
fun run(conflictStrategy: Int = SQLiteDatabase.CONFLICT_NONE): Int {
|
||||
return db.update(tableName, conflictStrategy, values, where, whereArgs)
|
||||
val query = StringBuilder("UPDATE $tableName SET ")
|
||||
|
||||
val contentValuesKeys = values.keySet()
|
||||
for ((index, column) in contentValuesKeys.withIndex()) {
|
||||
query.append(column).append(" = ?")
|
||||
if (index < contentValuesKeys.size - 1) {
|
||||
query.append(", ")
|
||||
}
|
||||
}
|
||||
|
||||
query.append(" WHERE ").append(where)
|
||||
|
||||
val conflictString = when (conflictStrategy) {
|
||||
SQLiteDatabase.CONFLICT_IGNORE -> " ON CONFLICT IGNORE"
|
||||
SQLiteDatabase.CONFLICT_ABORT -> " ON CONFLICT ABORT"
|
||||
SQLiteDatabase.CONFLICT_FAIL -> " ON CONFLICT FAIL"
|
||||
SQLiteDatabase.CONFLICT_ROLLBACK -> " ON CONFLICT ROLLBACK"
|
||||
SQLiteDatabase.CONFLICT_REPLACE -> " ON CONFLICT REPLACE"
|
||||
else -> ""
|
||||
}
|
||||
query.append(conflictString)
|
||||
|
||||
val statement = db.compileStatement(query.toString())
|
||||
var bindIndex = 1
|
||||
for (key in contentValuesKeys) {
|
||||
statement.bindValue(bindIndex, values.get(key))
|
||||
bindIndex++
|
||||
}
|
||||
|
||||
for (arg in whereArgs) {
|
||||
statement.bindValue(bindIndex, arg)
|
||||
bindIndex++
|
||||
}
|
||||
|
||||
return statement.use { it.executeUpdateDelete() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,6 +609,20 @@ class InsertBuilderPart2(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to massage passed-in arguments into a better form to give to the database.
|
||||
*/
|
||||
private fun Array<out Any?>.toArgs(): Array<Any?> {
|
||||
return this
|
||||
.map {
|
||||
when (it) {
|
||||
is DatabaseId -> it.serialize()
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
.toTypedArray()
|
||||
}
|
||||
|
||||
data class ForeignKeyConstraint(
|
||||
val table: String,
|
||||
val column: String,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.util
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
|
||||
/**
|
||||
* Helper to create an in-memory database used for testing SQLite stuff.
|
||||
*/
|
||||
object InMemorySqliteOpenHelper {
|
||||
fun create(
|
||||
onCreate: (db: SupportSQLiteDatabase) -> Unit,
|
||||
onUpgrade: (db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) -> Unit = { _, _, _ -> }
|
||||
): SupportSQLiteOpenHelper {
|
||||
val configuration = SupportSQLiteOpenHelper.Configuration(
|
||||
context = ApplicationProvider.getApplicationContext(),
|
||||
name = "test",
|
||||
callback = object : SupportSQLiteOpenHelper.Callback(1) {
|
||||
override fun onCreate(db: SupportSQLiteDatabase) = onCreate(db)
|
||||
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) = onUpgrade(db, oldVersion, newVersion)
|
||||
},
|
||||
useNoBackupDirectory = false,
|
||||
allowDataLossOnRecovery = true
|
||||
)
|
||||
|
||||
return FrameworkSQLiteOpenHelperFactory().create(configuration)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.util
|
||||
|
||||
import android.app.Application
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNotNull
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class SQLiteDatabaseExtensionsTest {
|
||||
|
||||
lateinit var db: SupportSQLiteOpenHelper
|
||||
|
||||
companion object {
|
||||
const val TABLE_NAME = "test"
|
||||
const val ID = "_id"
|
||||
const val STRING_COLUMN = "string_column"
|
||||
const val LONG_COLUMN = "long_column"
|
||||
const val DOUBLE_COLUMN = "double_column"
|
||||
const val BLOB_COLUMN = "blob_column"
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
db = InMemorySqliteOpenHelper.create(
|
||||
onCreate = { db ->
|
||||
db.execSQL("CREATE TABLE $TABLE_NAME ($ID INTEGER PRIMARY KEY AUTOINCREMENT, $STRING_COLUMN TEXT, $LONG_COLUMN INTEGER, $DOUBLE_COLUMN DOUBLE, $BLOB_COLUMN BLOB)")
|
||||
}
|
||||
)
|
||||
|
||||
db.writableDatabase.insertInto(TABLE_NAME)
|
||||
.values(
|
||||
STRING_COLUMN to "asdf",
|
||||
LONG_COLUMN to 1,
|
||||
DOUBLE_COLUMN to 0.5f,
|
||||
BLOB_COLUMN to byteArrayOf(1, 2, 3)
|
||||
)
|
||||
.run()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `update - content values work`() {
|
||||
val updateCount: Int = db.writableDatabase
|
||||
.update("test")
|
||||
.values(
|
||||
STRING_COLUMN to "asdf2",
|
||||
LONG_COLUMN to 2,
|
||||
DOUBLE_COLUMN to 1.5f,
|
||||
BLOB_COLUMN to byteArrayOf(4, 5, 6)
|
||||
)
|
||||
.where("$ID = ?", 1)
|
||||
.run()
|
||||
|
||||
val record = readRecord(1)
|
||||
|
||||
assertThat(updateCount).isEqualTo(1)
|
||||
assertThat(record).isNotNull()
|
||||
assertThat(record!!.id).isEqualTo(1)
|
||||
assertThat(record.stringColumn).isEqualTo("asdf2")
|
||||
assertThat(record.longColumn).isEqualTo(2)
|
||||
assertThat(record.doubleColumn).isEqualTo(1.5f)
|
||||
assertArrayEquals(record.blobColumn, byteArrayOf(4, 5, 6))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `update - querying by blob works`() {
|
||||
val updateCount: Int = db.writableDatabase
|
||||
.update("test")
|
||||
.values(
|
||||
STRING_COLUMN to "asdf2"
|
||||
)
|
||||
.where("$BLOB_COLUMN = ?", byteArrayOf(1, 2, 3))
|
||||
.run()
|
||||
|
||||
val record = readRecord(1)
|
||||
|
||||
assertThat(updateCount).isEqualTo(1)
|
||||
assertThat(record).isNotNull()
|
||||
assertThat(record!!.stringColumn).isEqualTo("asdf2")
|
||||
}
|
||||
|
||||
private fun readRecord(id: Long): TestRecord? {
|
||||
return db.readableDatabase
|
||||
.select()
|
||||
.from(TABLE_NAME)
|
||||
.where("$ID = ?", id)
|
||||
.run()
|
||||
.readToSingleObject {
|
||||
TestRecord(
|
||||
id = it.requireLong(ID),
|
||||
stringColumn = it.requireString(STRING_COLUMN),
|
||||
longColumn = it.requireLong(LONG_COLUMN),
|
||||
doubleColumn = it.requireFloat(DOUBLE_COLUMN),
|
||||
blobColumn = it.requireBlob(BLOB_COLUMN)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class TestRecord(
|
||||
val id: Long,
|
||||
val stringColumn: String?,
|
||||
val longColumn: Long,
|
||||
val doubleColumn: Float,
|
||||
val blobColumn: ByteArray?
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user