Always run the same migration code

This commit is contained in:
Max Radermacher 2025-12-23 09:09:55 -06:00 committed by GitHub
parent 6fb8b566b9
commit e1f5eebf6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 967 additions and 3570 deletions

View File

@ -71,6 +71,16 @@ jobs:
name: Logs
path: ~/Library/Logs/Signal-CI
- name: Normalize database schema
run: |
touch -d 2026-01-01T00:00:00 ~/Library/Signal-iOS-Schema/schema.json
- name: Upload database schema
uses: actions/upload-artifact@v4
with:
name: Database Schema
path: ~/Library/Signal-iOS-Schema
check_autogenstrings:
name: Check if strings file is outdated

View File

@ -2,5 +2,4 @@ source 'https://rubygems.org'
gem 'cocoapods'
gem 'fastlane'
gem 'anbt-sql-formatter'
gem 'xcode-install'

View File

@ -20,7 +20,6 @@ GEM
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
anbt-sql-formatter (0.1.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
@ -292,7 +291,6 @@ PLATFORMS
ruby
DEPENDENCIES
anbt-sql-formatter
cocoapods
fastlane
xcode-install

View File

@ -4,6 +4,10 @@ LOG_DIR="$HOME/Library/Logs/Signal-CI"
rm -rf "$LOG_DIR"
mkdir -p "$LOG_DIR"
SCHEMA_DIR="$HOME/Library/Signal-iOS-Schema"
rm -rf "$SCHEMA_DIR"
mkdir -p "$SCHEMA_DIR"
echo
echo "Available iOS Simulator runtimes:"
xcrun simctl list runtimes
@ -28,7 +32,7 @@ echo "Using simulator: $LATEST_IOS_SIM_ID"
echo
set -o pipefail \
&& NSUnbufferedIO=YES xcodebuild \
&& NSUnbufferedIO=YES TEST_RUNNER_SCHEMA_DUMP_PATH="$SCHEMA_DIR/schema.json" xcodebuild \
-workspace Signal.xcworkspace \
-scheme Signal \
-destination "platform=iOS Simulator,id=$LATEST_IOS_SIM_ID" \

View File

@ -1,100 +0,0 @@
#!/usr/bin/env python3
import argparse
import os
import re
import subprocess
SCHEMA_PATH = "SignalServiceKit/Resources/schema.sql"
TABLES_TO_IGNORE = [
"grdb_migrations",
"sqlite_sequence",
"indexable_text_fts_data",
"indexable_text_fts_idx",
"indexable_text_fts_docsize",
"indexable_text_fts_config",
"SearchableNameFTS_data",
"SearchableNameFTS_idx",
"SearchableNameFTS_docsize",
"SearchableNameFTS_config",
]
def main(ns):
repo_root = os.path.abspath(os.path.join(__file__, "../.."))
args = ["Scripts/sqlclient", "--quiet"]
if ns.staging:
args.extend(["--staging"])
if ns.path is not None:
args.extend(["--path", ns.path])
if ns.passphrase is not None:
args.extend(["--passphrase", ns.passphrase])
args.extend(["--", ".schema"])
schema = subprocess.run(
args,
check=True,
encoding="utf8",
capture_output=True,
cwd=repo_root,
).stdout
# Drop the "ok" from setting the passphrase.
assert schema.startswith("ok\n")
schema = schema[3:]
# Normalize the formatting.
schema = subprocess.run(
["bundle", "exec", "anbt-sql-formatter"],
check=True,
input=schema,
encoding="utf8",
capture_output=True,
).stdout
# Remove tables that don't need to be included. (Generally, some other
# mechanism creates these so that we don't need to.)
for table in TABLES_TO_IGNORE:
schema = re.sub(
r"CREATE\s+TABLE\s+(IF NOT EXISTS\s+)?'?" + table + r".*?;\n\n",
"",
schema,
flags=re.MULTILINE | re.DOTALL,
)
file_path = os.path.join(repo_root, SCHEMA_PATH)
with open(file_path, "r") as file:
old_schema = file.read()
if schema == old_schema:
return
with open(file_path, "w") as file:
file.write(schema)
def parse_args():
parser = argparse.ArgumentParser()
target = parser.add_mutually_exclusive_group()
target.add_argument(
"--staging",
action="store_true",
help="Target the staging database of the currently-booted simulator.",
)
target.add_argument(
"--path",
metavar="/a/b/c",
help="Target the database at the provided path.",
)
parser.add_argument(
"--passphrase",
metavar="abcdef0123456789",
help="Use the provided passphrase to decrypt the database. "
"(Or you can use “Settings” -> “Internal” -> “Misc” -> “Save plaintext database key”.)",
)
return parser.parse_args()
if __name__ == "__main__":
ns = parse_args()
main(ns)

View File

@ -3453,7 +3453,6 @@
F9A392B9297F2ED5007964E5 /* SpamReportingTokenRecordTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9A392B8297F2ED5007964E5 /* SpamReportingTokenRecordTest.swift */; };
F9A8ACC7280A175E00AFC6A7 /* DonationSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9A8ACC6280A175E00AFC6A7 /* DonationSettingsViewController.swift */; };
F9AE695328F046E40012E9C9 /* OWSFingerprintTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9AE695228F046E40012E9C9 /* OWSFingerprintTest.swift */; };
F9B0DC4F28948656004E07B7 /* schema.sql in Resources */ = {isa = PBXBuildFile; fileRef = F9B0DC3C28948656004E07B7 /* schema.sql */; };
F9B0DC5328948656004E07B7 /* isrgrootx1.crt in Resources */ = {isa = PBXBuildFile; fileRef = F9B0DC4128948656004E07B7 /* isrgrootx1.crt */; };
F9B0DC5528948656004E07B7 /* GSR2.crt in Resources */ = {isa = PBXBuildFile; fileRef = F9B0DC4328948656004E07B7 /* GSR2.crt */; };
F9B0DC5728948656004E07B7 /* GSR4.crt in Resources */ = {isa = PBXBuildFile; fileRef = F9B0DC4528948656004E07B7 /* GSR4.crt */; };
@ -7647,7 +7646,6 @@
F9A392B8297F2ED5007964E5 /* SpamReportingTokenRecordTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpamReportingTokenRecordTest.swift; sourceTree = "<group>"; };
F9A8ACC6280A175E00AFC6A7 /* DonationSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonationSettingsViewController.swift; sourceTree = "<group>"; };
F9AE695228F046E40012E9C9 /* OWSFingerprintTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSFingerprintTest.swift; sourceTree = "<group>"; };
F9B0DC3C28948656004E07B7 /* schema.sql */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = schema.sql; sourceTree = "<group>"; };
F9B0DC4128948656004E07B7 /* isrgrootx1.crt */ = {isa = PBXFileReference; lastKnownFileType = file; path = isrgrootx1.crt; sourceTree = "<group>"; };
F9B0DC4328948656004E07B7 /* GSR2.crt */ = {isa = PBXFileReference; lastKnownFileType = file; path = GSR2.crt; sourceTree = "<group>"; };
F9B0DC4528948656004E07B7 /* GSR4.crt */ = {isa = PBXFileReference; lastKnownFileType = file; path = GSR4.crt; sourceTree = "<group>"; };
@ -14149,7 +14147,6 @@
isa = PBXGroup;
children = (
F9B0DC3D28948656004E07B7 /* Certificates */,
F9B0DC3C28948656004E07B7 /* schema.sql */,
);
path = Resources;
sourceTree = "<group>";
@ -15873,7 +15870,6 @@
F9B0DC5C28948656004E07B7 /* GTSR3.crt in Resources */,
F9B0DC5F28948656004E07B7 /* GTSR4.crt in Resources */,
F9B0DC5328948656004E07B7 /* isrgrootx1.crt in Resources */,
F9B0DC4F28948656004E07B7 /* schema.sql in Resources */,
F9B0DC5928948656004E07B7 /* signal-messenger.cer in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;

File diff suppressed because it is too large Load Diff

View File

@ -79,6 +79,7 @@ public enum DatabaseRecovery {
private let unitCountForNewDatabaseCreation: Int64 = 1
private let unitCountForBestEffortCopy = Int64(DumpAndRestoreOperation.tablesToCopyWithBestEffort.count)
private let unitCountForFlawlessCopy = Int64(DumpAndRestoreOperation.tablesThatMustBeCopiedFlawlessly.count)
private let unitCountForMigrationIds: Int64 = 1
private let unitCountForNewDatabasePromotion: Int64 = 3
public let progress: Progress
@ -93,6 +94,7 @@ public enum DatabaseRecovery {
unitCountForNewDatabaseCreation +
unitCountForBestEffortCopy +
unitCountForFlawlessCopy +
unitCountForMigrationIds +
unitCountForNewDatabasePromotion
)
self.progress = Progress(totalUnitCount: totalUnitCount)
@ -125,7 +127,12 @@ public enum DatabaseRecovery {
}
progress.performAsCurrent(withPendingUnitCount: unitCountForOldDatabaseMigration) {
try? Self.runMigrationsOn(databaseStorage: oldDatabaseStorage, databaseIs: .old)
do {
logger.info("Running migrations on old database...")
try Self.runMigrationsOn(databaseStorage: oldDatabaseStorage)
} catch {
Logger.warn("Couldn't migrate existing database. Repair will probably fail because of an incompatible schema")
}
}
let newTemporaryDatabaseFileUrl = Self.temporaryDatabaseFileUrl()
@ -143,7 +150,9 @@ public enum DatabaseRecovery {
databaseFileUrl: newTemporaryDatabaseFileUrl,
keychainStorage: self.keychainStorage
)
try Self.runMigrationsOn(databaseStorage: newDatabaseStorage, databaseIs: .new)
logger.info("Running migrations on new database...")
try Self.runMigrationsOn(databaseStorage: newDatabaseStorage)
try Self.deleteEverythingFrom(databaseStorage: newDatabaseStorage)
} catch {
throw DatabaseRecoveryError.unrecoverablyCorrupted
}
@ -170,6 +179,14 @@ public enum DatabaseRecovery {
)
try copyTablesThatMustBeCopiedFlawlessly.run()
progress.performAsCurrent(withPendingUnitCount: unitCountForMigrationIds) {
do {
try Self.copyMigrationIds(oldDatabaseStorage: oldDatabaseStorage, newDatabaseStorage: newDatabaseStorage)
} catch {
Logger.warn("Continuing despite MigrationId copy error: \(error.grdbErrorForLogging)")
}
}
try progress.performAsCurrent(withPendingUnitCount: unitCountForNewDatabasePromotion) {
try Self.promoteNewDatabase(
oldDatabaseStorage: oldDatabaseStorage,
@ -180,7 +197,7 @@ public enum DatabaseRecovery {
logger.info("Dump and restore complete")
}
// MARK: Checkpoint old database to clear its WAL/SHM files (step 1)
// MARK: Checkpoint old database to clear its WAL/SHM files
private static func attemptToCheckpoint(oldDatabaseStorage: SDSDatabaseStorage) {
logger.info("Attempting to checkpoint the old database...")
@ -192,7 +209,7 @@ public enum DatabaseRecovery {
}
}
// MARK: Creating new database (step 2)
// MARK: Creating new database
private static func temporaryDatabaseFileUrl() -> URL {
logger.info("Creating temporary database file...")
@ -218,41 +235,58 @@ public enum DatabaseRecovery {
}
}
// MARK: Running schema migrations (steps 2 and 3)
// MARK: Running schema migrations
private enum MigrationsMode: CustomStringConvertible {
case old
case new
public var description: String {
switch self {
case .old: return "old"
case .new: return "new"
}
}
}
private static func runMigrationsOn(databaseStorage: SDSDatabaseStorage, databaseIs mode: MigrationsMode) throws {
logger.info("Running migrations on \(mode) database...")
private static func runMigrationsOn(databaseStorage: SDSDatabaseStorage) throws {
do {
let didPerformIncrementalMigrations = try GRDBSchemaMigrator.migrateDatabase(
_ = try GRDBSchemaMigrator.migrateDatabase(
databaseStorage: databaseStorage,
runDataMigrations: {
switch mode {
// We skip old data migrations because we suspect data is more likely to be corrupted.
case .old: return false
case .new: return true
}
}()
// We assume data migrations don't affect the schema of the tables, and
// we'll run them on the repaired database if everything else succeeds.
runDataMigrations: false,
)
logger.info("Ran migrations on \(mode) database. \(didPerformIncrementalMigrations ? "Performed" : "Did not perform") incremental migrations")
logger.info("Ran migrations")
} catch {
logger.warn("Failed to run migrations on \(mode) database. Error: \(error)")
logger.warn("Failed to run migrations: \(error.grdbErrorForLogging)")
throw error
}
}
// MARK: Copy tables with best effort (step 4)
/// Runs DELETE FROM on every non-sqlite, non-grdb, non-fts table.
private static func deleteEverythingFrom(databaseStorage: SDSDatabaseStorage) throws {
try databaseStorage.write { tx in
let tableNames = try String.fetchAll(tx.database, sql: "SELECT name FROM sqlite_master WHERE type = 'table'")
for tableName in tableNames {
let shouldSkip = (
Database.isSQLiteInternalTable(tableName)
|| Database.isGRDBInternalTable(tableName)
)
if shouldSkip {
continue
}
if
let ftsTableName = ftsTableName(forTableName: tableName),
tableNames.contains(ftsTableName)
{
continue
}
owsPrecondition(SqliteUtil.isSafe(sqlName: tableName))
logger.info("Deleting everything from \(tableName)")
try tx.database.execute(sql: "DELETE FROM \"\(tableName)\"")
}
}
}
private static func ftsTableName(forTableName tableName: String) -> String? {
for suffix in ["_config", "_data", "_docsize", "_idx"] {
if let matchingRange = tableName.range(of: suffix, options: [.anchored, .backwards]) {
return String(tableName[..<matchingRange.lowerBound])
}
}
return nil
}
// MARK: Copy tables with best effort
static let tablesToCopyWithBestEffort: [String] = [
// We should try to copy thread data.
@ -350,7 +384,7 @@ public enum DatabaseRecovery {
}
}
// MARK: Copy essential tables (step 5)
// MARK: Copy essential tables
static let tablesThatMustBeCopiedFlawlessly: [String] = [
// The app will be too unpredictable with strange key-value stores.
@ -384,6 +418,7 @@ public enum DatabaseRecovery {
}
switch result {
case let .totalFailure(error), let .copiedSomeButHadTrouble(error, _):
Logger.warn("Couldn't copy tables flawlessly: \(error.grdbErrorForLogging)")
let toThrow: DatabaseRecoveryError = error.isSqliteFullError ? .ranOutOfDiskSpace : .unrecoverablyCorrupted
throw toThrow
case .wentFlawlessly:
@ -415,7 +450,30 @@ public enum DatabaseRecovery {
return result
}
// MARK: Promote the old database (step 6)
// MARK: Copy migrations
/// Copies migrationIds (esp. data migrations) that were already performed.
///
/// After repairing, we want to skip data migrations we've already run, but
/// we want to execute the ones that haven't yet run.
private static func copyMigrationIds(
oldDatabaseStorage: SDSDatabaseStorage,
newDatabaseStorage: SDSDatabaseStorage,
) throws {
let migrationIds = try oldDatabaseStorage.read { tx in
return try String.fetchAll(tx.database, sql: "SELECT identifier FROM grdb_migrations")
}
try newDatabaseStorage.write { tx in
for migrationId in migrationIds {
try tx.database.execute(
sql: "INSERT OR IGNORE INTO grdb_migrations (identifier) VALUES (?)",
arguments: [migrationId],
)
}
}
}
// MARK: Promote the old database
/// "Promotes" the new database and clobbers the old one.
///

View File

@ -22,71 +22,52 @@ public class GRDBSchemaMigrator {
databaseStorage: SDSDatabaseStorage,
runDataMigrations: Bool = true,
) throws -> Bool {
let didPerformIncrementalMigrations: Bool
let grdbStorageAdapter = databaseStorage.grdbStorage
let hasCreatedInitialSchema = try grdbStorageAdapter.read {
try Self.hasCreatedInitialSchema(transaction: $0)
}
if hasCreatedInitialSchema {
do {
didPerformIncrementalMigrations = try runIncrementalMigrations(
databaseStorage: databaseStorage,
runDataMigrations: runDataMigrations
)
} catch {
owsFailDebug("Incremental migrations failed: \(error.grdbErrorForLogging)")
throw error
}
} else {
do {
try newUserMigrator().migrate(grdbStorageAdapter.pool)
didPerformIncrementalMigrations = false
} catch {
owsFailDebug("New user migrator failed: \(error.grdbErrorForLogging)")
throw error
}
}
return didPerformIncrementalMigrations
return try runIncrementalMigrations(databaseStorage: databaseStorage, runDataMigrations: runDataMigrations)
}
private static func runIncrementalMigrations(
databaseStorage: SDSDatabaseStorage,
runDataMigrations: Bool
) throws -> Bool {
let grdbStorageAdapter = databaseStorage.grdbStorage
return try _runIncrementalMigrations(
databaseWriter: databaseStorage.grdbStorage.pool,
runDataMigrations: runDataMigrations,
)
}
let previouslyAppliedMigrations = try grdbStorageAdapter.read { transaction in
try DatabaseMigrator().appliedIdentifiers(transaction.database)
#if TESTABLE_BUILD
static func runIncrementalMigrations(databaseWriter: some DatabaseWriter) throws {
_ = try _runIncrementalMigrations(databaseWriter: databaseWriter, runDataMigrations: false)
}
#endif
private static func _runIncrementalMigrations(
databaseWriter: some DatabaseWriter,
runDataMigrations: Bool,
) throws -> Bool {
let previouslyAppliedMigrations = try databaseWriter.read { database in
try DatabaseMigrator().appliedIdentifiers(database)
}
// First do the schema migrations. (See the comment within MigrationId for why schema and data
// migrations are separate.)
let incrementalMigrator = DatabaseMigratorWrapper()
registerSchemaMigrations(migrator: incrementalMigrator)
try incrementalMigrator.migrate(grdbStorageAdapter.pool)
try incrementalMigrator.migrate(databaseWriter)
if runDataMigrations {
// Finally, do data migrations.
registerDataMigrations(migrator: incrementalMigrator)
try incrementalMigrator.migrate(grdbStorageAdapter.pool)
try incrementalMigrator.migrate(databaseWriter)
}
let allAppliedMigrations = try grdbStorageAdapter.read { transaction in
try DatabaseMigrator().appliedIdentifiers(transaction.database)
let allAppliedMigrations = try databaseWriter.read { database in
try DatabaseMigrator().appliedIdentifiers(database)
}
return allAppliedMigrations != previouslyAppliedMigrations
}
private static func hasCreatedInitialSchema(transaction: DBReadTransaction) throws -> Bool {
let appliedMigrations = try DatabaseMigrator().appliedIdentifiers(transaction.database)
return appliedMigrations.contains(MigrationId.createInitialSchema.rawValue)
}
// MARK: -
private enum MigrationId: String, CaseIterable {
@ -458,40 +439,6 @@ public class GRDBSchemaMigrator {
public static let grdbSchemaVersionDefault: UInt = 0
public static let grdbSchemaVersionLatest: UInt = 137
// An optimization for new users, we have the first migration import the latest schema
// and mark any other migrations as "already run".
private static func newUserMigrator() -> DatabaseMigrator {
var migrator = DatabaseMigrator()
migrator.registerMigration(MigrationId.createInitialSchema.rawValue) { db in
// Within the transaction this migration opens, check that we haven't already run
// the initial schema migration, in case we are racing with another process that
// is also running migrations.
guard try hasCreatedInitialSchema(transaction: DBReadTransaction(database: db)).negated else {
// Already done!
return
}
Logger.info("importing latest schema")
guard let sqlFile = Bundle(for: GRDBSchemaMigrator.self).url(forResource: "schema", withExtension: "sql") else {
owsFail("sqlFile was unexpectedly nil")
}
let sql = try String(contentsOf: sqlFile)
try db.execute(sql: sql)
// This isn't enabled by schema.sql, so we need to explicitly turn it on
// for new databases.
try enableFts5SecureDelete(for: "indexable_text_fts", db: db)
try enableFts5SecureDelete(for: "SearchableNameFTS", db: db)
// After importing the initial schema, we want to skip the remaining
// incremental migrations, so we manually mark them as complete.
for migrationId in (MigrationId.allCases.filter { $0 != .createInitialSchema }) {
insertMigration(migrationId.rawValue, db: db)
}
}
return migrator
}
private class DatabaseMigratorWrapper {
var migrator = DatabaseMigrator()
@ -551,6 +498,7 @@ public class GRDBSchemaMigrator {
Logger.info("Running migration: \(identifier)")
let transaction = DBWriteTransaction(database: database)
defer { transaction.finalizeTransaction() }
let result = try migrate(transaction)
switch result {
case .success:
@ -560,7 +508,6 @@ public class GRDBSchemaMigrator {
case .failure(let error):
throw error
}
transaction.finalizeTransaction()
}
}
@ -571,8 +518,674 @@ public class GRDBSchemaMigrator {
private static func registerSchemaMigrations(migrator: DatabaseMigratorWrapper) {
migrator.registerMigration(.createInitialSchema) { _ in
owsFail("This migration should have already been run by the last YapDB migration.")
migrator.registerMigration(.createInitialSchema) { tx in
let sql = """
CREATE
TABLE
keyvalue (
KEY TEXT NOT NULL
,collection TEXT NOT NULL
,VALUE BLOB NOT NULL
,PRIMARY KEY (
KEY
,collection
)
)
;
CREATE
TABLE
IF NOT EXISTS "model_TSThread" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"conversationColorName" TEXT NOT NULL
,"creationDate" DOUBLE
,"isArchived" INTEGER NOT NULL
,"lastInteractionRowId" INTEGER NOT NULL
,"messageDraft" TEXT
,"mutedUntilDate" DOUBLE
,"shouldThreadBeVisible" INTEGER NOT NULL
,"contactPhoneNumber" TEXT
,"contactUUID" TEXT
,"groupModel" BLOB
,"hasDismissedOffers" INTEGER
)
;
CREATE
INDEX "index_model_TSThread_on_uniqueId"
ON "model_TSThread"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_TSInteraction" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"receivedAtTimestamp" INTEGER NOT NULL
,"timestamp" INTEGER NOT NULL
,"uniqueThreadId" TEXT NOT NULL
,"attachmentIds" BLOB
,"authorId" TEXT
,"authorPhoneNumber" TEXT
,"authorUUID" TEXT
,"body" TEXT
,"callType" INTEGER
,"configurationDurationSeconds" INTEGER
,"configurationIsEnabled" INTEGER
,"contactShare" BLOB
,"createdByRemoteName" TEXT
,"createdInExistingGroup" INTEGER
,"customMessage" TEXT
,"envelopeData" BLOB
,"errorType" INTEGER
,"expireStartedAt" INTEGER
,"expiresAt" INTEGER
,"expiresInSeconds" INTEGER
,"groupMetaMessage" INTEGER
,"hasLegacyMessageState" INTEGER
,"hasSyncedTranscript" INTEGER
,"isFromLinkedDevice" INTEGER
,"isLocalChange" INTEGER
,"isViewOnceComplete" INTEGER
,"isViewOnceMessage" INTEGER
,"isVoiceMessage" INTEGER
,"legacyMessageState" INTEGER
,"legacyWasDelivered" INTEGER
,"linkPreview" BLOB
,"messageId" TEXT
,"messageSticker" BLOB
,"messageType" INTEGER
,"mostRecentFailureText" TEXT
,"preKeyBundle" BLOB
,"protocolVersion" INTEGER
,"quotedMessage" BLOB
,"read" INTEGER
,"recipientAddress" BLOB
,"recipientAddressStates" BLOB
,"sender" BLOB
,"serverTimestamp" INTEGER
,"sourceDeviceId" INTEGER
,"storedMessageState" INTEGER
,"storedShouldStartExpireTimer" INTEGER
,"unregisteredAddress" BLOB
,"verificationState" INTEGER
,"wasReceivedByUD" INTEGER
)
;
CREATE
INDEX "index_model_TSInteraction_on_uniqueId"
ON "model_TSInteraction"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_StickerPack" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"author" TEXT
,"cover" BLOB NOT NULL
,"dateCreated" DOUBLE NOT NULL
,"info" BLOB NOT NULL
,"isInstalled" INTEGER NOT NULL
,"items" BLOB NOT NULL
,"title" TEXT
)
;
CREATE
INDEX "index_model_StickerPack_on_uniqueId"
ON "model_StickerPack"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_InstalledSticker" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"emojiString" TEXT
,"info" BLOB NOT NULL
)
;
CREATE
INDEX "index_model_InstalledSticker_on_uniqueId"
ON "model_InstalledSticker"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_KnownStickerPack" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"dateCreated" DOUBLE NOT NULL
,"info" BLOB NOT NULL
,"referenceCount" INTEGER NOT NULL
)
;
CREATE
INDEX "index_model_KnownStickerPack_on_uniqueId"
ON "model_KnownStickerPack"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_TSAttachment" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"albumMessageId" TEXT
,"attachmentType" INTEGER NOT NULL
,"blurHash" TEXT
,"byteCount" INTEGER NOT NULL
,"caption" TEXT
,"contentType" TEXT NOT NULL
,"encryptionKey" BLOB
,"serverId" INTEGER NOT NULL
,"sourceFilename" TEXT
,"cachedAudioDurationSeconds" DOUBLE
,"cachedImageHeight" DOUBLE
,"cachedImageWidth" DOUBLE
,"creationTimestamp" DOUBLE
,"digest" BLOB
,"isUploaded" INTEGER
,"isValidImageCached" INTEGER
,"isValidVideoCached" INTEGER
,"lazyRestoreFragmentId" TEXT
,"localRelativeFilePath" TEXT
,"mediaSize" BLOB
,"pointerType" INTEGER
,"state" INTEGER
)
;
CREATE
INDEX "index_model_TSAttachment_on_uniqueId"
ON "model_TSAttachment"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_SSKJobRecord" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"failureCount" INTEGER NOT NULL
,"label" TEXT NOT NULL
,"status" INTEGER NOT NULL
,"attachmentIdMap" BLOB
,"contactThreadId" TEXT
,"envelopeData" BLOB
,"invisibleMessage" BLOB
,"messageId" TEXT
,"removeMessageAfterSending" INTEGER
,"threadId" TEXT
)
;
CREATE
INDEX "index_model_SSKJobRecord_on_uniqueId"
ON "model_SSKJobRecord"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_OWSMessageContentJob" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"createdAt" DOUBLE NOT NULL
,"envelopeData" BLOB NOT NULL
,"plaintextData" BLOB
,"wasReceivedByUD" INTEGER NOT NULL
)
;
CREATE
INDEX "index_model_OWSMessageContentJob_on_uniqueId"
ON "model_OWSMessageContentJob"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_OWSRecipientIdentity" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"accountId" TEXT NOT NULL
,"createdAt" DOUBLE NOT NULL
,"identityKey" BLOB NOT NULL
,"isFirstKnownKey" INTEGER NOT NULL
,"verificationState" INTEGER NOT NULL
)
;
CREATE
INDEX "index_model_OWSRecipientIdentity_on_uniqueId"
ON "model_OWSRecipientIdentity"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_ExperienceUpgrade" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
)
;
CREATE
INDEX "index_model_ExperienceUpgrade_on_uniqueId"
ON "model_ExperienceUpgrade"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_OWSDisappearingMessagesConfiguration" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"durationSeconds" INTEGER NOT NULL
,"enabled" INTEGER NOT NULL
)
;
CREATE
INDEX "index_model_OWSDisappearingMessagesConfiguration_on_uniqueId"
ON "model_OWSDisappearingMessagesConfiguration"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_SignalRecipient" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"devices" BLOB NOT NULL
,"recipientPhoneNumber" TEXT
,"recipientUUID" TEXT
)
;
CREATE
INDEX "index_model_SignalRecipient_on_uniqueId"
ON "model_SignalRecipient"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_SignalAccount" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"contact" BLOB
,"multipleAccountLabelText" TEXT NOT NULL
,"recipientPhoneNumber" TEXT
,"recipientUUID" TEXT
)
;
CREATE
INDEX "index_model_SignalAccount_on_uniqueId"
ON "model_SignalAccount"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_OWSUserProfile" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"avatarFileName" TEXT
,"avatarUrlPath" TEXT
,"profileKey" BLOB
,"profileName" TEXT
,"recipientPhoneNumber" TEXT
,"recipientUUID" TEXT
,"username" TEXT
)
;
CREATE
INDEX "index_model_OWSUserProfile_on_uniqueId"
ON "model_OWSUserProfile"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_TSRecipientReadReceipt" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"recipientMap" BLOB NOT NULL
,"sentTimestamp" INTEGER NOT NULL
)
;
CREATE
INDEX "index_model_TSRecipientReadReceipt_on_uniqueId"
ON "model_TSRecipientReadReceipt"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_OWSLinkedDeviceReadReceipt" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"messageIdTimestamp" INTEGER NOT NULL
,"readTimestamp" INTEGER NOT NULL
,"senderPhoneNumber" TEXT
,"senderUUID" TEXT
)
;
CREATE
INDEX "index_model_OWSLinkedDeviceReadReceipt_on_uniqueId"
ON "model_OWSLinkedDeviceReadReceipt"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_OWSDevice" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"createdAt" DOUBLE NOT NULL
,"deviceId" INTEGER NOT NULL
,"lastSeenAt" DOUBLE NOT NULL
,"name" TEXT
)
;
CREATE
INDEX "index_model_OWSDevice_on_uniqueId"
ON "model_OWSDevice"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_OWSContactQuery" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"lastQueried" DOUBLE NOT NULL
,"nonce" BLOB NOT NULL
)
;
CREATE
INDEX "index_model_OWSContactQuery_on_uniqueId"
ON "model_OWSContactQuery"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_TestModel" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"dateValue" DOUBLE
,"doubleValue" DOUBLE NOT NULL
,"floatValue" DOUBLE NOT NULL
,"int64Value" INTEGER NOT NULL
,"nsIntegerValue" INTEGER NOT NULL
,"nsNumberValueUsingInt64" INTEGER
,"nsNumberValueUsingUInt64" INTEGER
,"nsuIntegerValue" INTEGER NOT NULL
,"uint64Value" INTEGER NOT NULL
)
;
CREATE
INDEX "index_model_TestModel_on_uniqueId"
ON "model_TestModel"("uniqueId"
)
;
CREATE
INDEX "index_interactions_on_threadUniqueId_and_id"
ON "model_TSInteraction"("uniqueThreadId"
,"id"
)
;
CREATE
INDEX "index_jobs_on_label_and_id"
ON "model_SSKJobRecord"("label"
,"id"
)
;
CREATE
INDEX "index_jobs_on_status_and_label_and_id"
ON "model_SSKJobRecord"("label"
,"status"
,"id"
)
;
CREATE
INDEX "index_interactions_on_view_once"
ON "model_TSInteraction"("isViewOnceMessage"
,"isViewOnceComplete"
)
;
CREATE
INDEX "index_key_value_store_on_collection_and_key"
ON "keyvalue"("collection"
,"key"
)
;
CREATE
INDEX "index_interactions_on_recordType_and_threadUniqueId_and_errorType"
ON "model_TSInteraction"("recordType"
,"uniqueThreadId"
,"errorType"
)
;
CREATE
INDEX "index_attachments_on_albumMessageId"
ON "model_TSAttachment"("albumMessageId"
,"recordType"
)
;
CREATE
INDEX "index_interactions_on_uniqueId_and_threadUniqueId"
ON "model_TSInteraction"("uniqueThreadId"
,"uniqueId"
)
;
CREATE
INDEX "index_signal_accounts_on_recipientPhoneNumber"
ON "model_SignalAccount"("recipientPhoneNumber"
)
;
CREATE
INDEX "index_signal_accounts_on_recipientUUID"
ON "model_SignalAccount"("recipientUUID"
)
;
CREATE
INDEX "index_signal_recipients_on_recipientPhoneNumber"
ON "model_SignalRecipient"("recipientPhoneNumber"
)
;
CREATE
INDEX "index_signal_recipients_on_recipientUUID"
ON "model_SignalRecipient"("recipientUUID"
)
;
CREATE
INDEX "index_thread_on_contactPhoneNumber"
ON "model_TSThread"("contactPhoneNumber"
)
;
CREATE
INDEX "index_thread_on_contactUUID"
ON "model_TSThread"("contactUUID"
)
;
CREATE
INDEX "index_thread_on_shouldThreadBeVisible"
ON "model_TSThread"("shouldThreadBeVisible"
,"isArchived"
,"lastInteractionRowId"
)
;
CREATE
INDEX "index_user_profiles_on_recipientPhoneNumber"
ON "model_OWSUserProfile"("recipientPhoneNumber"
)
;
CREATE
INDEX "index_user_profiles_on_recipientUUID"
ON "model_OWSUserProfile"("recipientUUID"
)
;
CREATE
INDEX "index_user_profiles_on_username"
ON "model_OWSUserProfile"("username"
)
;
CREATE
INDEX "index_linkedDeviceReadReceipt_on_senderPhoneNumberAndTimestamp"
ON "model_OWSLinkedDeviceReadReceipt"("senderPhoneNumber"
,"messageIdTimestamp"
)
;
CREATE
INDEX "index_linkedDeviceReadReceipt_on_senderUUIDAndTimestamp"
ON "model_OWSLinkedDeviceReadReceipt"("senderUUID"
,"messageIdTimestamp"
)
;
CREATE
INDEX "index_interactions_on_timestamp_sourceDeviceId_and_authorUUID"
ON "model_TSInteraction"("timestamp"
,"sourceDeviceId"
,"authorUUID"
)
;
CREATE
INDEX "index_interactions_on_timestamp_sourceDeviceId_and_authorPhoneNumber"
ON "model_TSInteraction"("timestamp"
,"sourceDeviceId"
,"authorPhoneNumber"
)
;
CREATE
INDEX "index_interactions_unread_counts"
ON "model_TSInteraction"("read"
,"uniqueThreadId"
,"recordType"
)
;
CREATE
INDEX "index_interactions_on_expiresInSeconds_and_expiresAt"
ON "model_TSInteraction"("expiresAt"
,"expiresInSeconds"
)
;
CREATE
INDEX "index_interactions_on_threadUniqueId_storedShouldStartExpireTimer_and_expiresAt"
ON "model_TSInteraction"("expiresAt"
,"expireStartedAt"
,"storedShouldStartExpireTimer"
,"uniqueThreadId"
)
;
CREATE
INDEX "index_contact_queries_on_lastQueried"
ON "model_OWSContactQuery"("lastQueried"
)
;
CREATE
INDEX "index_attachments_on_lazyRestoreFragmentId"
ON "model_TSAttachment"("lazyRestoreFragmentId"
)
;
CREATE
VIRTUAL TABLE
"signal_grdb_fts"
USING fts5 (
collection UNINDEXED
,uniqueId UNINDEXED
,ftsIndexableContent
,tokenize = 'unicode61'
) /* signal_grdb_fts(collection,uniqueId,ftsIndexableContent) */
;
"""
try tx.database.execute(sql: sql)
return .success(())
}
migrator.registerMigration(.signalAccount_add_contactAvatars) { transaction in
@ -4367,6 +4980,9 @@ public class GRDBSchemaMigrator {
}
migrator.registerMigration(.dataMigration_turnScreenSecurityOnForExistingUsers) { transaction in
guard try hasAnyAccountManagerState(tx: transaction) else {
return .success(())
}
// Declare the key value store here, since it's normally only
// available in SignalMessaging.Preferences.
let preferencesKeyValueStore = KeyValueStore(collection: "SignalPreferences")
@ -4397,6 +5013,9 @@ public class GRDBSchemaMigrator {
}
migrator.registerMigration(.dataMigration_disableSharingSuggestionsForExistingUsers) { transaction in
guard try hasAnyAccountManagerState(tx: transaction) else {
return .success(())
}
SSKPreferences.setAreIntentDonationsEnabled(false, transaction: transaction)
return .success(())
}
@ -4569,6 +5188,9 @@ public class GRDBSchemaMigrator {
}
migrator.registerMigration(.dataMigration_repairAvatar) { transaction in
guard try hasAnyAccountManagerState(tx: transaction) else {
return .success(())
}
// Declare the key value store here, since it's normally only
// available in SignalMessaging.Preferences.
let preferencesKeyValueStore = KeyValueStore(collection: Self.migrationSideEffectsCollectionName)
@ -6619,6 +7241,15 @@ public class GRDBSchemaMigrator {
"""
)
}
/// A very rough check for "is this a brand new installation?"
private static func hasAnyAccountManagerState(tx: DBReadTransaction) throws -> Bool {
return try Bool.fetchOne(
tx.database,
sql: "SELECT 1 FROM keyvalue WHERE collection = ?",
arguments: ["TSStorageUserAccountCollection"],
) ?? false
}
}
// MARK: -

View File

@ -16,37 +16,20 @@ public final class InMemoryDB: DB {
let databaseQueue: DatabaseQueue
public init() {
self.databaseQueue = DatabaseQueue()
let schemaUrl: URL
if
let urlInNormalPlace = Bundle(for: GRDBSchemaMigrator.self)
.url(forResource: "schema", withExtension: "sql")
{
schemaUrl = urlInNormalPlace
} else if
let urlInWonkyPlace = Bundle(for: GRDBSchemaMigrator.self)
.url(forResource: "schema", withExtension: "sql", subdirectory: "Frameworks/SignalServiceKit.framework")
{
/// There's what appears to be a bug in Xcode 16.3, in which for
/// Xcode Previews, sometimes, the Bundle returned for SSK types
/// refers to the top-level app bundle, not the SSK bundle.
/// Searching in that bundle will fail to find `schema.sql`; so
/// instead manually search in the SSK subdirectory.
///
/// If we go a while without noticing the warning print below, we
/// can remove this and see what happens.
///
/// Note that Logger doesn't work in Xcode Previews, hence print!
print("Warning: needed to use fallback schema.sql location!")
schemaUrl = urlInWonkyPlace
} else {
owsFail("Failed to find schema.sql in Bundle!")
}
try! databaseQueue.write { try $0.execute(sql: try String(contentsOf: schemaUrl)) }
var configuration = GRDB.Configuration()
configuration.acceptsDoubleQuotedStringLiterals = true
self.databaseQueue = DatabaseQueue(configuration: configuration)
try! Self.emptyDb.backup(to: self.databaseQueue)
}
private static let emptyDb: DatabaseQueue = {
var configuration = GRDB.Configuration()
configuration.acceptsDoubleQuotedStringLiterals = true
let databaseQueue = DatabaseQueue(configuration: configuration)
try! GRDBSchemaMigrator.runIncrementalMigrations(databaseWriter: databaseQueue)
return databaseQueue
}()
// MARK: - Protocol
public func add(

View File

@ -24,6 +24,7 @@ public class SDSDatabaseStorage: NSObject, DB {
private let _databaseChangeObserver: SDSDatabaseChangeObserver
public let databaseFileUrl: URL
public let keychainStorage: any KeychainStorage
public let keyFetcher: GRDBKeyFetcher
private(set) public var grdbStorage: GRDBDatabaseStorageAdapter
@ -33,6 +34,7 @@ public class SDSDatabaseStorage: NSObject, DB {
self.appReadiness = appReadiness
self._databaseChangeObserver = DatabaseChangeObserverImpl(appReadiness: appReadiness)
self.databaseFileUrl = databaseFileUrl
self.keychainStorage = keychainStorage
self.keyFetcher = GRDBKeyFetcher(keychainStorage: keychainStorage)
self.grdbStorage = try GRDBDatabaseStorageAdapter(
databaseChangeObserver: _databaseChangeObserver,

View File

@ -7,7 +7,7 @@ import Foundation
#if TESTABLE_BUILD
public class MockKeychainStorage: KeychainStorage {
public final class MockKeychainStorage: KeychainStorage {
private let values = AtomicDictionary<String, Data>([:], lock: .init())
private func buildKey(service: String, key: String) -> String {
@ -29,6 +29,12 @@ public class MockKeychainStorage: KeychainStorage {
public func removeValue(service: String, key: String) throws {
values[buildKey(service: service, key: key)] = nil
}
func clone() -> MockKeychainStorage {
let result = MockKeychainStorage()
result.values.set(self.values.get())
return result
}
}
#endif

View File

@ -16,8 +16,29 @@ public class MockSSKEnvironment {
callMessageHandler: any CallMessageHandler = NoopCallMessageHandler(),
currentCallProvider: any CurrentCallProvider = CurrentCallNoOpProvider(),
notificationPresenter: any NotificationPresenter = NoopNotificationPresenterImpl(),
testDependencies: AppSetup.TestDependencies? = nil
testDependencies: AppSetup.TestDependencies? = nil,
) async {
let sampleDatabase = await initializeSampleDatabase()
_ = await _activate(
appReadiness: appReadiness,
callMessageHandler: callMessageHandler,
currentCallProvider: currentCallProvider,
notificationPresenter: notificationPresenter,
testDependencies: testDependencies,
sampleDatabase: sampleDatabase,
)
}
@MainActor
private static func _activate(
appReadiness: any AppReadiness = AppReadinessImpl(),
callMessageHandler: any CallMessageHandler = NoopCallMessageHandler(),
currentCallProvider: any CurrentCallProvider = CurrentCallNoOpProvider(),
keychainStorage: MockKeychainStorage = MockKeychainStorage(),
notificationPresenter: any NotificationPresenter = NoopNotificationPresenterImpl(),
testDependencies: AppSetup.TestDependencies? = nil,
sampleDatabase: SampleDatabase?,
) async -> SampleDatabase {
owsPrecondition(!(CurrentAppContext() is TestAppContext))
owsPrecondition(!SSKEnvironment.hasShared)
owsPrecondition(!DependenciesBridge.hasShared)
@ -32,13 +53,22 @@ public class MockSSKEnvironment {
/// For a ``TestAppContext`` as configured above, this will be a
/// subdirectory of our temp directory unique to the instantiation of
/// the app context.
let databaseUrl = SDSDatabaseStorage.grdbDatabaseFileUrl
let keychainStorage: MockKeychainStorage
if let sampleDatabase {
sampleDatabase.copyTo(databaseUrl)
keychainStorage = sampleDatabase.keychainStorage.clone()
} else {
keychainStorage = MockKeychainStorage()
}
let finalContinuation = await AppSetup().start(
appContext: testAppContext,
databaseStorage: try! SDSDatabaseStorage(
appReadiness: appReadiness,
databaseFileUrl: SDSDatabaseStorage.grdbDatabaseFileUrl,
keychainStorage: MockKeychainStorage()
databaseFileUrl: databaseUrl,
keychainStorage: keychainStorage,
),
).migrateDatabaseSchema().initGlobals(
appReadiness: appReadiness,
@ -71,6 +101,32 @@ public class MockSSKEnvironment {
)
).migrateDatabaseData()
finalContinuation.runLaunchTasksIfNeededAndReloadCaches()
return SampleDatabase(fileUrl: databaseUrl, keychainStorage: keychainStorage)
}
struct SampleDatabase {
var fileUrl: URL
var keychainStorage: MockKeychainStorage
func copyTo(_ databaseUrl: URL) {
try! FileManager.default.copyItem(at: self.fileUrl, to: databaseUrl)
}
}
@MainActor
private static var sampleDatabase: SampleDatabase?
@MainActor
private static func initializeSampleDatabase() async -> SampleDatabase {
if let sampleDatabase {
return sampleDatabase
}
let oldContext = CurrentAppContext()
let result = await MockSSKEnvironment._activate(sampleDatabase: nil)
try! SSKEnvironment.shared.databaseStorageRef.grdbStorage.syncTruncatingCheckpoint()
self.sampleDatabase = result
await MockSSKEnvironment.deactivateAsync(oldContext: oldContext)
return result
}
@MainActor

View File

@ -12,11 +12,8 @@ import XCTest
final class DatabaseRecoveryTest: SSKBaseTest {
// MARK: - Setup
private var keychainStorage: MockKeychainStorage!
override func setUp() {
super.setUp()
self.keychainStorage = MockKeychainStorage()
SSKEnvironment.shared.databaseStorageRef.write { tx in
(DependenciesBridge.shared.registrationStateChangeManager as! RegistrationStateChangeManagerImpl).registerForTests(
localIdentifiers: .forUnitTests,
@ -29,32 +26,24 @@ final class DatabaseRecoveryTest: SSKBaseTest {
return try SDSDatabaseStorage(
appReadiness: AppReadinessMock(),
databaseFileUrl: databaseStorage.databaseFileUrl,
keychainStorage: keychainStorage
keychainStorage: databaseStorage.keychainStorage,
)
}
// MARK: - Reindex existing database
func testReindexExistingDatabase() throws {
let databaseStorage = try newDatabase(keychainStorage: keychainStorage)
let databaseStorage = try newDatabase()
try GRDBSchemaMigrator.migrateDatabase(databaseStorage: databaseStorage)
let oldRowCounts = try normalTableRowCounts(databaseStorage: databaseStorage)
try XCTUnwrap(databaseStorage.grdbStorage.pool.close())
DatabaseRecovery.reindex(databaseStorage: try cloneDatabaseStorage(databaseStorage))
// As a smoke test, ensure that the database is still empty.
// As a smoke test, ensure that the database has the same number of rows.
let finishedDatabaseStorage = try cloneDatabaseStorage(databaseStorage)
finishedDatabaseStorage.read { transaction in
let database = transaction.database
for tableName in allNormalTableNames(transaction: transaction) {
let sql = "SELECT EXISTS (SELECT 1 FROM \(tableName))"
guard let anyRowExists = try? XCTUnwrap(Bool.fetchOne(database, sql: sql)) else {
XCTFail("Could not fetch boolean from test query")
return
}
XCTAssertFalse(anyRowExists, "\(tableName) had at least one row, unexpectedly")
}
}
let newRowCounts = try normalTableRowCounts(databaseStorage: finishedDatabaseStorage)
XCTAssertEqual(newRowCounts, oldRowCounts)
}
// MARK: - Dump and restore
@ -73,18 +62,16 @@ final class DatabaseRecoveryTest: SSKBaseTest {
}
let expectedTableNames: Set<String> = try {
let databaseStorage = try newDatabase(keychainStorage: keychainStorage)
try GRDBSchemaMigrator.migrateDatabase(databaseStorage: databaseStorage)
return databaseStorage.read { allNormalTableNames(transaction: $0) }
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
return try databaseStorage.read { try allNormalTableNames(tx: $0) }
}()
XCTAssertEqual(allTableNamesSet, expectedTableNames)
}
func testColumnSafety() throws {
let databaseStorage = try newDatabase(keychainStorage: keychainStorage)
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
let tableNames: Set<String> = try {
try GRDBSchemaMigrator.migrateDatabase(databaseStorage: databaseStorage)
return databaseStorage.read { allNormalTableNames(transaction: $0) }
return try databaseStorage.read { try allNormalTableNames(tx: $0) }
}()
for tableName in tableNames {
@ -98,38 +85,38 @@ final class DatabaseRecoveryTest: SSKBaseTest {
}
}
private func normalTableRowCounts(databaseStorage: SDSDatabaseStorage) throws -> [String: Int] {
return try databaseStorage.read { (tx) throws -> [String: Int] in
var result = [String: Int]()
for tableName in try allNormalTableNames(tx: tx) {
let sql = "SELECT COUNT(*) FROM \(tableName)"
let rowCount = try XCTUnwrap(Int.fetchOne(tx.database, sql: sql))
result[tableName] = rowCount
}
return result
}
}
func testDumpAndRestoreOfEmptyDatabase() throws {
let databaseStorage = try newDatabase(keychainStorage: keychainStorage)
let databaseStorage = try newDatabase()
try GRDBSchemaMigrator.migrateDatabase(databaseStorage: databaseStorage)
let oldRowCounts = try normalTableRowCounts(databaseStorage: databaseStorage)
try XCTUnwrap(databaseStorage.grdbStorage.pool.close())
let dump = DatabaseRecovery.DumpAndRestoreOperation(
appReadiness: AppReadinessMock(),
corruptDatabaseStorage: try cloneDatabaseStorage(databaseStorage),
keychainStorage: keychainStorage
keychainStorage: databaseStorage.keychainStorage,
)
try XCTUnwrap(dump.run())
let finishedDatabaseStorage = try SDSDatabaseStorage(
appReadiness: AppReadinessMock(),
databaseFileUrl: databaseStorage.databaseFileUrl,
keychainStorage: keychainStorage
)
finishedDatabaseStorage.read { transaction in
let database = transaction.database
for tableName in allNormalTableNames(transaction: transaction) {
let sql = "SELECT EXISTS (SELECT 1 FROM \(tableName))"
guard let anyRowExists = try? XCTUnwrap(Bool.fetchOne(database, sql: sql)) else {
XCTFail("Could not fetch boolean from test query")
return
}
XCTAssertFalse(anyRowExists, "\(tableName) had at least one row, unexpectedly")
}
}
let finishedDatabaseStorage = try cloneDatabaseStorage(databaseStorage)
let newRowCounts = try normalTableRowCounts(databaseStorage: finishedDatabaseStorage)
XCTAssertEqual(newRowCounts, oldRowCounts)
}
func testDumpAndRestoreOnHappyDatabase() throws {
let databaseStorage = try newDatabase(keychainStorage: keychainStorage)
let databaseStorage = try newDatabase()
try GRDBSchemaMigrator.migrateDatabase(databaseStorage: databaseStorage)
let contactAci = Aci.randomForTesting()
@ -186,14 +173,14 @@ final class DatabaseRecoveryTest: SSKBaseTest {
let dump = DatabaseRecovery.DumpAndRestoreOperation(
appReadiness: AppReadinessMock(),
corruptDatabaseStorage: try cloneDatabaseStorage(databaseStorage),
keychainStorage: keychainStorage
keychainStorage: databaseStorage.keychainStorage,
)
try XCTUnwrap(dump.run())
let finishedDatabaseStorage = try SDSDatabaseStorage(
appReadiness: AppReadinessMock(),
databaseFileUrl: databaseStorage.databaseFileUrl,
keychainStorage: keychainStorage
keychainStorage: databaseStorage.keychainStorage,
)
finishedDatabaseStorage.read { transaction in
// Thread
@ -255,7 +242,7 @@ final class DatabaseRecoveryTest: SSKBaseTest {
}
func testDumpAndRestoreWithInvalidEssentialTable() throws {
let databaseStorage = try newDatabase(keychainStorage: keychainStorage)
let databaseStorage = try newDatabase()
try GRDBSchemaMigrator.migrateDatabase(databaseStorage: databaseStorage)
databaseStorage.write { transaction in
try! transaction.database.drop(table: KeyValueStore.tableName)
@ -265,7 +252,7 @@ final class DatabaseRecoveryTest: SSKBaseTest {
let dump = DatabaseRecovery.DumpAndRestoreOperation(
appReadiness: AppReadinessMock(),
corruptDatabaseStorage: try cloneDatabaseStorage(databaseStorage),
keychainStorage: keychainStorage
keychainStorage: databaseStorage.keychainStorage,
)
XCTAssertThrowsError(try dump.run()) { error in
XCTAssertEqual(
@ -276,7 +263,7 @@ final class DatabaseRecoveryTest: SSKBaseTest {
}
func testDumpAndRestoreWithInvalidNonessentialTable() throws {
let databaseStorage = try newDatabase(keychainStorage: keychainStorage)
let databaseStorage = try newDatabase()
try GRDBSchemaMigrator.migrateDatabase(databaseStorage: databaseStorage)
databaseStorage.write { transaction in
try! transaction.database.drop(table: OWSReaction.databaseTableName)
@ -286,14 +273,14 @@ final class DatabaseRecoveryTest: SSKBaseTest {
let dump = DatabaseRecovery.DumpAndRestoreOperation(
appReadiness: AppReadinessMock(),
corruptDatabaseStorage: try cloneDatabaseStorage(databaseStorage),
keychainStorage: keychainStorage
keychainStorage: databaseStorage.keychainStorage,
)
try XCTUnwrap(dump.run())
let finishedDatabaseStorage = try SDSDatabaseStorage(
appReadiness: AppReadinessMock(),
databaseFileUrl: databaseStorage.databaseFileUrl,
keychainStorage: keychainStorage
keychainStorage: databaseStorage.keychainStorage,
)
finishedDatabaseStorage.read { transaction in
let sql = "SELECT EXISTS (SELECT 1 FROM \(OWSReaction.databaseTableName))"
@ -309,7 +296,7 @@ final class DatabaseRecoveryTest: SSKBaseTest {
// MARK: - Manual restoration
func testFullTextSearchRestoration() throws {
let databaseStorage = try newDatabase(keychainStorage: keychainStorage)
let databaseStorage = try newDatabase()
try GRDBSchemaMigrator.migrateDatabase(databaseStorage: databaseStorage)
databaseStorage.write { transaction in
@ -335,14 +322,14 @@ final class DatabaseRecoveryTest: SSKBaseTest {
let dump = DatabaseRecovery.DumpAndRestoreOperation(
appReadiness: AppReadinessMock(),
corruptDatabaseStorage: try cloneDatabaseStorage(databaseStorage),
keychainStorage: keychainStorage
keychainStorage: databaseStorage.keychainStorage,
)
try XCTUnwrap(dump.run())
let finishedDatabaseStorage = try SDSDatabaseStorage(
appReadiness: AppReadinessMock(),
databaseFileUrl: databaseStorage.databaseFileUrl,
keychainStorage: keychainStorage
keychainStorage: databaseStorage.keychainStorage,
)
let recreateFTSIndex = DatabaseRecovery.RecreateFTSIndexOperation(databaseStorage: finishedDatabaseStorage)
@ -370,18 +357,18 @@ final class DatabaseRecoveryTest: SSKBaseTest {
let validTableOrColumnNameRegex = "^[a-zA-Z][a-zA-Z0-9_]+$"
func newDatabase(keychainStorage: any KeychainStorage) throws -> SDSDatabaseStorage {
func newDatabase() throws -> SDSDatabaseStorage {
return try SDSDatabaseStorage(
appReadiness: AppReadinessMock(),
databaseFileUrl: OWSFileSystem.temporaryFileUrl(),
keychainStorage: keychainStorage
keychainStorage: MockKeychainStorage(),
)
}
func allNormalTableNames(transaction: DBReadTransaction) -> Set<String> {
let db = transaction.database
func allNormalTableNames(tx: DBReadTransaction) throws -> Set<String> {
let db = tx.database
let sql = "SELECT name FROM sqlite_schema WHERE type IS 'table'"
let allTableNames = Set((try? String.fetchAll(db, sql: sql)) ?? [])
let allTableNames = Set(try String.fetchAll(db, sql: sql))
owsPrecondition(!allTableNames.isEmpty, "No tables were found!")
let tableNamesToSkip: Set<String> = ["grdb_migrations", "sqlite_sequence"]

View File

@ -10,44 +10,46 @@ import XCTest
@testable import SignalServiceKit
class GRDBSchemaMigratorTest: XCTestCase {
func testMigrateFromScratch() throws {
let databaseStorage = try SDSDatabaseStorage(
appReadiness: AppReadinessMock(),
databaseFileUrl: OWSFileSystem.temporaryFileUrl(),
keychainStorage: MockKeychainStorage()
)
try GRDBSchemaMigrator.migrateDatabase(databaseStorage: databaseStorage)
databaseStorage.read { transaction in
let db = transaction.database
let sql = "SELECT name FROM sqlite_schema WHERE type IS 'table'"
let allTableNames = (try? String.fetchAll(db, sql: sql)) ?? []
XCTAssert(allTableNames.contains(TSThread.table.tableName))
}
}
func testSchemaMigrations() throws {
// TODO: Reuse initializeSampleDatabase when it doesn't need globals.
let databaseStorage = try SDSDatabaseStorage(
appReadiness: AppReadinessMock(),
databaseFileUrl: OWSFileSystem.temporaryFileUrl(),
keychainStorage: MockKeychainStorage()
)
// Create the initial schema (the one from 2019).
databaseStorage.write { tx in
try! tx.database.execute(sql: sqlToCreateInitialSchema)
try! tx.database.execute(sql: """
CREATE TABLE IF NOT EXISTS grdb_migrations (identifier TEXT NOT NULL PRIMARY KEY);
INSERT INTO grdb_migrations (identifier) VALUES ('createInitialSchema');
"""
)
}
// Run all schema migrations. This should succeed without globals!
try GRDBSchemaMigrator.migrateDatabase(
databaseStorage: databaseStorage,
runDataMigrations: false,
)
try extractSchema(databaseStorage: databaseStorage)
}
/// Extracts the current database schema for documentation.
private func extractSchema(databaseStorage: SDSDatabaseStorage) throws {
struct SchemaEntry: Encodable {
var name: String
var sql: String
}
let schemaEntries = try databaseStorage.read { tx in
let rows = try Row.fetchAll(tx.database, sql: "SELECT * FROM sqlite_master")
return rows.compactMap { row -> SchemaEntry? in
let name: String = row["name"]
let sql: String? = row["sql"]
guard let sql else {
return nil
}
return SchemaEntry(name: name, sql: sql)
}
}
let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys
let encodedSchema = try encoder.encode(schemaEntries)
let schemaDumpPath = ProcessInfo.processInfo.environment["SCHEMA_DUMP_PATH"]
if let schemaDumpPath {
try encodedSchema.write(to: URL(fileURLWithPath: schemaDumpPath))
Logger.verbose("wrote schema to \(schemaDumpPath)")
}
}
private func keyedArchiverData(rootObject: Any) -> Data {
@ -1159,718 +1161,3 @@ class GRDBSchemaMigratorTest: XCTestCase {
}
}
}
// MARK: -
private let sqlToCreateInitialSchema = """
CREATE
TABLE
keyvalue (
KEY TEXT NOT NULL
,collection TEXT NOT NULL
,VALUE BLOB NOT NULL
,PRIMARY KEY (
KEY
,collection
)
)
;
CREATE
TABLE
IF NOT EXISTS "model_TSThread" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"conversationColorName" TEXT NOT NULL
,"creationDate" DOUBLE
,"isArchived" INTEGER NOT NULL
,"lastInteractionRowId" INTEGER NOT NULL
,"messageDraft" TEXT
,"mutedUntilDate" DOUBLE
,"shouldThreadBeVisible" INTEGER NOT NULL
,"contactPhoneNumber" TEXT
,"contactUUID" TEXT
,"groupModel" BLOB
,"hasDismissedOffers" INTEGER
)
;
CREATE
INDEX "index_model_TSThread_on_uniqueId"
ON "model_TSThread"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_TSInteraction" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"receivedAtTimestamp" INTEGER NOT NULL
,"timestamp" INTEGER NOT NULL
,"uniqueThreadId" TEXT NOT NULL
,"attachmentIds" BLOB
,"authorId" TEXT
,"authorPhoneNumber" TEXT
,"authorUUID" TEXT
,"body" TEXT
,"callType" INTEGER
,"configurationDurationSeconds" INTEGER
,"configurationIsEnabled" INTEGER
,"contactShare" BLOB
,"createdByRemoteName" TEXT
,"createdInExistingGroup" INTEGER
,"customMessage" TEXT
,"envelopeData" BLOB
,"errorType" INTEGER
,"expireStartedAt" INTEGER
,"expiresAt" INTEGER
,"expiresInSeconds" INTEGER
,"groupMetaMessage" INTEGER
,"hasLegacyMessageState" INTEGER
,"hasSyncedTranscript" INTEGER
,"isFromLinkedDevice" INTEGER
,"isLocalChange" INTEGER
,"isViewOnceComplete" INTEGER
,"isViewOnceMessage" INTEGER
,"isVoiceMessage" INTEGER
,"legacyMessageState" INTEGER
,"legacyWasDelivered" INTEGER
,"linkPreview" BLOB
,"messageId" TEXT
,"messageSticker" BLOB
,"messageType" INTEGER
,"mostRecentFailureText" TEXT
,"preKeyBundle" BLOB
,"protocolVersion" INTEGER
,"quotedMessage" BLOB
,"read" INTEGER
,"recipientAddress" BLOB
,"recipientAddressStates" BLOB
,"sender" BLOB
,"serverTimestamp" INTEGER
,"sourceDeviceId" INTEGER
,"storedMessageState" INTEGER
,"storedShouldStartExpireTimer" INTEGER
,"unregisteredAddress" BLOB
,"verificationState" INTEGER
,"wasReceivedByUD" INTEGER
)
;
CREATE
INDEX "index_model_TSInteraction_on_uniqueId"
ON "model_TSInteraction"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_StickerPack" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"author" TEXT
,"cover" BLOB NOT NULL
,"dateCreated" DOUBLE NOT NULL
,"info" BLOB NOT NULL
,"isInstalled" INTEGER NOT NULL
,"items" BLOB NOT NULL
,"title" TEXT
)
;
CREATE
INDEX "index_model_StickerPack_on_uniqueId"
ON "model_StickerPack"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_InstalledSticker" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"emojiString" TEXT
,"info" BLOB NOT NULL
)
;
CREATE
INDEX "index_model_InstalledSticker_on_uniqueId"
ON "model_InstalledSticker"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_KnownStickerPack" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"dateCreated" DOUBLE NOT NULL
,"info" BLOB NOT NULL
,"referenceCount" INTEGER NOT NULL
)
;
CREATE
INDEX "index_model_KnownStickerPack_on_uniqueId"
ON "model_KnownStickerPack"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_TSAttachment" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"albumMessageId" TEXT
,"attachmentType" INTEGER NOT NULL
,"blurHash" TEXT
,"byteCount" INTEGER NOT NULL
,"caption" TEXT
,"contentType" TEXT NOT NULL
,"encryptionKey" BLOB
,"serverId" INTEGER NOT NULL
,"sourceFilename" TEXT
,"cachedAudioDurationSeconds" DOUBLE
,"cachedImageHeight" DOUBLE
,"cachedImageWidth" DOUBLE
,"creationTimestamp" DOUBLE
,"digest" BLOB
,"isUploaded" INTEGER
,"isValidImageCached" INTEGER
,"isValidVideoCached" INTEGER
,"lazyRestoreFragmentId" TEXT
,"localRelativeFilePath" TEXT
,"mediaSize" BLOB
,"pointerType" INTEGER
,"state" INTEGER
)
;
CREATE
INDEX "index_model_TSAttachment_on_uniqueId"
ON "model_TSAttachment"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_SSKJobRecord" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"failureCount" INTEGER NOT NULL
,"label" TEXT NOT NULL
,"status" INTEGER NOT NULL
,"attachmentIdMap" BLOB
,"contactThreadId" TEXT
,"envelopeData" BLOB
,"invisibleMessage" BLOB
,"messageId" TEXT
,"removeMessageAfterSending" INTEGER
,"threadId" TEXT
)
;
CREATE
INDEX "index_model_SSKJobRecord_on_uniqueId"
ON "model_SSKJobRecord"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_OWSMessageContentJob" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"createdAt" DOUBLE NOT NULL
,"envelopeData" BLOB NOT NULL
,"plaintextData" BLOB
,"wasReceivedByUD" INTEGER NOT NULL
)
;
CREATE
INDEX "index_model_OWSMessageContentJob_on_uniqueId"
ON "model_OWSMessageContentJob"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_OWSRecipientIdentity" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"accountId" TEXT NOT NULL
,"createdAt" DOUBLE NOT NULL
,"identityKey" BLOB NOT NULL
,"isFirstKnownKey" INTEGER NOT NULL
,"verificationState" INTEGER NOT NULL
)
;
CREATE
INDEX "index_model_OWSRecipientIdentity_on_uniqueId"
ON "model_OWSRecipientIdentity"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_ExperienceUpgrade" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
)
;
CREATE
INDEX "index_model_ExperienceUpgrade_on_uniqueId"
ON "model_ExperienceUpgrade"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_OWSDisappearingMessagesConfiguration" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"durationSeconds" INTEGER NOT NULL
,"enabled" INTEGER NOT NULL
)
;
CREATE
INDEX "index_model_OWSDisappearingMessagesConfiguration_on_uniqueId"
ON "model_OWSDisappearingMessagesConfiguration"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_SignalRecipient" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"devices" BLOB NOT NULL
,"recipientPhoneNumber" TEXT
,"recipientUUID" TEXT
)
;
CREATE
INDEX "index_model_SignalRecipient_on_uniqueId"
ON "model_SignalRecipient"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_SignalAccount" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"contact" BLOB
,"multipleAccountLabelText" TEXT NOT NULL
,"recipientPhoneNumber" TEXT
,"recipientUUID" TEXT
)
;
CREATE
INDEX "index_model_SignalAccount_on_uniqueId"
ON "model_SignalAccount"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_OWSUserProfile" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"avatarFileName" TEXT
,"avatarUrlPath" TEXT
,"profileKey" BLOB
,"profileName" TEXT
,"recipientPhoneNumber" TEXT
,"recipientUUID" TEXT
,"username" TEXT
)
;
CREATE
INDEX "index_model_OWSUserProfile_on_uniqueId"
ON "model_OWSUserProfile"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_TSRecipientReadReceipt" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"recipientMap" BLOB NOT NULL
,"sentTimestamp" INTEGER NOT NULL
)
;
CREATE
INDEX "index_model_TSRecipientReadReceipt_on_uniqueId"
ON "model_TSRecipientReadReceipt"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_OWSLinkedDeviceReadReceipt" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"messageIdTimestamp" INTEGER NOT NULL
,"readTimestamp" INTEGER NOT NULL
,"senderPhoneNumber" TEXT
,"senderUUID" TEXT
)
;
CREATE
INDEX "index_model_OWSLinkedDeviceReadReceipt_on_uniqueId"
ON "model_OWSLinkedDeviceReadReceipt"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_OWSDevice" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"createdAt" DOUBLE NOT NULL
,"deviceId" INTEGER NOT NULL
,"lastSeenAt" DOUBLE NOT NULL
,"name" TEXT
)
;
CREATE
INDEX "index_model_OWSDevice_on_uniqueId"
ON "model_OWSDevice"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_OWSContactQuery" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"lastQueried" DOUBLE NOT NULL
,"nonce" BLOB NOT NULL
)
;
CREATE
INDEX "index_model_OWSContactQuery_on_uniqueId"
ON "model_OWSContactQuery"("uniqueId"
)
;
CREATE
TABLE
IF NOT EXISTS "model_TestModel" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recordType" INTEGER NOT NULL
,"uniqueId" TEXT NOT NULL UNIQUE
ON CONFLICT FAIL
,"dateValue" DOUBLE
,"doubleValue" DOUBLE NOT NULL
,"floatValue" DOUBLE NOT NULL
,"int64Value" INTEGER NOT NULL
,"nsIntegerValue" INTEGER NOT NULL
,"nsNumberValueUsingInt64" INTEGER
,"nsNumberValueUsingUInt64" INTEGER
,"nsuIntegerValue" INTEGER NOT NULL
,"uint64Value" INTEGER NOT NULL
)
;
CREATE
INDEX "index_model_TestModel_on_uniqueId"
ON "model_TestModel"("uniqueId"
)
;
CREATE
INDEX "index_interactions_on_threadUniqueId_and_id"
ON "model_TSInteraction"("uniqueThreadId"
,"id"
)
;
CREATE
INDEX "index_jobs_on_label_and_id"
ON "model_SSKJobRecord"("label"
,"id"
)
;
CREATE
INDEX "index_jobs_on_status_and_label_and_id"
ON "model_SSKJobRecord"("label"
,"status"
,"id"
)
;
CREATE
INDEX "index_interactions_on_view_once"
ON "model_TSInteraction"("isViewOnceMessage"
,"isViewOnceComplete"
)
;
CREATE
INDEX "index_key_value_store_on_collection_and_key"
ON "keyvalue"("collection"
,"key"
)
;
CREATE
INDEX "index_interactions_on_recordType_and_threadUniqueId_and_errorType"
ON "model_TSInteraction"("recordType"
,"uniqueThreadId"
,"errorType"
)
;
CREATE
INDEX "index_attachments_on_albumMessageId"
ON "model_TSAttachment"("albumMessageId"
,"recordType"
)
;
CREATE
INDEX "index_interactions_on_uniqueId_and_threadUniqueId"
ON "model_TSInteraction"("uniqueThreadId"
,"uniqueId"
)
;
CREATE
INDEX "index_signal_accounts_on_recipientPhoneNumber"
ON "model_SignalAccount"("recipientPhoneNumber"
)
;
CREATE
INDEX "index_signal_accounts_on_recipientUUID"
ON "model_SignalAccount"("recipientUUID"
)
;
CREATE
INDEX "index_signal_recipients_on_recipientPhoneNumber"
ON "model_SignalRecipient"("recipientPhoneNumber"
)
;
CREATE
INDEX "index_signal_recipients_on_recipientUUID"
ON "model_SignalRecipient"("recipientUUID"
)
;
CREATE
INDEX "index_thread_on_contactPhoneNumber"
ON "model_TSThread"("contactPhoneNumber"
)
;
CREATE
INDEX "index_thread_on_contactUUID"
ON "model_TSThread"("contactUUID"
)
;
CREATE
INDEX "index_thread_on_shouldThreadBeVisible"
ON "model_TSThread"("shouldThreadBeVisible"
,"isArchived"
,"lastInteractionRowId"
)
;
CREATE
INDEX "index_user_profiles_on_recipientPhoneNumber"
ON "model_OWSUserProfile"("recipientPhoneNumber"
)
;
CREATE
INDEX "index_user_profiles_on_recipientUUID"
ON "model_OWSUserProfile"("recipientUUID"
)
;
CREATE
INDEX "index_user_profiles_on_username"
ON "model_OWSUserProfile"("username"
)
;
CREATE
INDEX "index_linkedDeviceReadReceipt_on_senderPhoneNumberAndTimestamp"
ON "model_OWSLinkedDeviceReadReceipt"("senderPhoneNumber"
,"messageIdTimestamp"
)
;
CREATE
INDEX "index_linkedDeviceReadReceipt_on_senderUUIDAndTimestamp"
ON "model_OWSLinkedDeviceReadReceipt"("senderUUID"
,"messageIdTimestamp"
)
;
CREATE
INDEX "index_interactions_on_timestamp_sourceDeviceId_and_authorUUID"
ON "model_TSInteraction"("timestamp"
,"sourceDeviceId"
,"authorUUID"
)
;
CREATE
INDEX "index_interactions_on_timestamp_sourceDeviceId_and_authorPhoneNumber"
ON "model_TSInteraction"("timestamp"
,"sourceDeviceId"
,"authorPhoneNumber"
)
;
CREATE
INDEX "index_interactions_unread_counts"
ON "model_TSInteraction"("read"
,"uniqueThreadId"
,"recordType"
)
;
CREATE
INDEX "index_interactions_on_expiresInSeconds_and_expiresAt"
ON "model_TSInteraction"("expiresAt"
,"expiresInSeconds"
)
;
CREATE
INDEX "index_interactions_on_threadUniqueId_storedShouldStartExpireTimer_and_expiresAt"
ON "model_TSInteraction"("expiresAt"
,"expireStartedAt"
,"storedShouldStartExpireTimer"
,"uniqueThreadId"
)
;
CREATE
INDEX "index_contact_queries_on_lastQueried"
ON "model_OWSContactQuery"("lastQueried"
)
;
CREATE
INDEX "index_attachments_on_lazyRestoreFragmentId"
ON "model_TSAttachment"("lazyRestoreFragmentId"
)
;
CREATE
VIRTUAL TABLE
"signal_grdb_fts"
USING fts5 (
collection UNINDEXED
,uniqueId UNINDEXED
,ftsIndexableContent
,tokenize = 'unicode61'
) /* signal_grdb_fts(collection,uniqueId,ftsIndexableContent) */
;
CREATE
TABLE
IF NOT EXISTS 'signal_grdb_fts_data' (
id INTEGER PRIMARY KEY
,block BLOB
)
;
CREATE
TABLE
IF NOT EXISTS 'signal_grdb_fts_idx' (
segid
,term
,pgno
,PRIMARY KEY (
segid
,term
)
) WITHOUT ROWID
;
CREATE
TABLE
IF NOT EXISTS 'signal_grdb_fts_content' (
id INTEGER PRIMARY KEY
,c0
,c1
,c2
)
;
CREATE
TABLE
IF NOT EXISTS 'signal_grdb_fts_docsize' (
id INTEGER PRIMARY KEY
,sz BLOB
)
;
CREATE
TABLE
IF NOT EXISTS 'signal_grdb_fts_config' (
k PRIMARY KEY
,v
) WITHOUT ROWID
;
"""