Always run the same migration code
This commit is contained in:
parent
6fb8b566b9
commit
e1f5eebf6f
10
.github/workflows/main.yml
vendored
10
.github/workflows/main.yml
vendored
@ -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
|
||||
|
||||
|
||||
1
Gemfile
1
Gemfile
@ -2,5 +2,4 @@ source 'https://rubygems.org'
|
||||
|
||||
gem 'cocoapods'
|
||||
gem 'fastlane'
|
||||
gem 'anbt-sql-formatter'
|
||||
gem 'xcode-install'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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" \
|
||||
|
||||
@ -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)
|
||||
@ -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
@ -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.
|
||||
///
|
||||
|
||||
@ -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: -
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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
|
||||
;
|
||||
"""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user