From e1f5eebf6fbf5cf937d94b0d487e0a0d85b32018 Mon Sep 17 00:00:00 2001 From: Max Radermacher Date: Tue, 23 Dec 2025 09:09:55 -0600 Subject: [PATCH] Always run the same migration code --- .github/workflows/main.yml | 10 + Gemfile | 1 - Gemfile.lock | 2 - Scripts/build-and-test.sh | 6 +- Scripts/schema_dump | 100 - Signal.xcodeproj/project.pbxproj | 4 - SignalServiceKit/Resources/schema.sql | 2520 ----------------- .../Storage/Database/DatabaseRecovery.swift | 122 +- .../Storage/Database/GRDBSchemaMigrator.swift | 787 ++++- .../Storage/Database/InMemoryDB.swift | 41 +- .../SDSDatabaseStorage.swift | 2 + .../TestUtils/MockKeychainStorage.swift | 8 +- .../TestUtils/MockSSKEnvironment.swift | 62 +- .../Database/DatabaseRecoveryTest.swift | 101 +- .../Database/GRDBSchemaMigratorTest.swift | 771 +---- 15 files changed, 967 insertions(+), 3570 deletions(-) delete mode 100755 Scripts/schema_dump delete mode 100644 SignalServiceKit/Resources/schema.sql diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 037df103c3..d5ef40f38c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 diff --git a/Gemfile b/Gemfile index 378e6c04ed..bfad32647a 100644 --- a/Gemfile +++ b/Gemfile @@ -2,5 +2,4 @@ source 'https://rubygems.org' gem 'cocoapods' gem 'fastlane' -gem 'anbt-sql-formatter' gem 'xcode-install' diff --git a/Gemfile.lock b/Gemfile.lock index 98091c2aae..71d2e81a0f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/Scripts/build-and-test.sh b/Scripts/build-and-test.sh index cfac39c5df..92e40813dc 100755 --- a/Scripts/build-and-test.sh +++ b/Scripts/build-and-test.sh @@ -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" \ diff --git a/Scripts/schema_dump b/Scripts/schema_dump deleted file mode 100755 index d11fc50916..0000000000 --- a/Scripts/schema_dump +++ /dev/null @@ -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) diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index be565a503a..b7ad41f901 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -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 = ""; }; F9A8ACC6280A175E00AFC6A7 /* DonationSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonationSettingsViewController.swift; sourceTree = ""; }; F9AE695228F046E40012E9C9 /* OWSFingerprintTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSFingerprintTest.swift; sourceTree = ""; }; - F9B0DC3C28948656004E07B7 /* schema.sql */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = schema.sql; sourceTree = ""; }; F9B0DC4128948656004E07B7 /* isrgrootx1.crt */ = {isa = PBXFileReference; lastKnownFileType = file; path = isrgrootx1.crt; sourceTree = ""; }; F9B0DC4328948656004E07B7 /* GSR2.crt */ = {isa = PBXFileReference; lastKnownFileType = file; path = GSR2.crt; sourceTree = ""; }; F9B0DC4528948656004E07B7 /* GSR4.crt */ = {isa = PBXFileReference; lastKnownFileType = file; path = GSR4.crt; sourceTree = ""; }; @@ -14149,7 +14147,6 @@ isa = PBXGroup; children = ( F9B0DC3D28948656004E07B7 /* Certificates */, - F9B0DC3C28948656004E07B7 /* schema.sql */, ); path = Resources; sourceTree = ""; @@ -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; diff --git a/SignalServiceKit/Resources/schema.sql b/SignalServiceKit/Resources/schema.sql deleted file mode 100644 index 4a99ec9898..0000000000 --- a/SignalServiceKit/Resources/schema.sql +++ /dev/null @@ -1,2520 +0,0 @@ -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 - ,"isMarkedUnread" BOOLEAN NOT NULL DEFAULT 0 - ,"lastVisibleSortIdOnScreenPercentage" DOUBLE NOT NULL DEFAULT 0 - ,"lastVisibleSortId" INTEGER NOT NULL DEFAULT 0 - ,"messageDraftBodyRanges" BLOB - ,"mentionNotificationMode" INTEGER NOT NULL DEFAULT 0 - ,"mutedUntilTimestamp" INTEGER NOT NULL DEFAULT 0 - ,"allowsReplies" BOOLEAN DEFAULT 0 - ,"lastSentStoryTimestamp" INTEGER - ,"name" TEXT - ,"addresses" BLOB - ,"storyViewMode" INTEGER DEFAULT 0 - ,"editTargetTimestamp" INTEGER - ,"lastDraftInteractionRowId" INTEGER DEFAULT 0 - ,"lastDraftUpdateTimestamp" INTEGER DEFAULT 0 - ) -; - -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 - ,"deprecated_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 - ,"wasNotCreatedLocally" 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 - ,"deprecated_sourceDeviceId" INTEGER - ,"storedMessageState" INTEGER - ,"storedShouldStartExpireTimer" INTEGER - ,"unregisteredAddress" BLOB - ,"verificationState" INTEGER - ,"wasReceivedByUD" INTEGER - ,"infoMessageUserInfo" BLOB - ,"wasRemotelyDeleted" BOOLEAN - ,"bodyRanges" BLOB - ,"offerType" INTEGER - ,"serverDeliveryTimestamp" INTEGER - ,"eraId" TEXT - ,"hasEnded" BOOLEAN - ,"creatorUuid" TEXT - ,"joinedMemberUuids" BLOB - ,"wasIdentityVerified" BOOLEAN - ,"paymentCancellation" BLOB - ,"paymentNotification" BLOB - ,"paymentRequest" BLOB - ,"viewed" BOOLEAN - ,"serverGuid" TEXT - ,"storyAuthorUuidString" TEXT - ,"storyTimestamp" INTEGER - ,"isGroupStoryReply" BOOLEAN DEFAULT 0 - ,"storyReactionEmoji" TEXT - ,"giftBadge" BLOB - ,"editState" INTEGER DEFAULT 0 - ,"archivedPaymentInfo" BLOB - ,"expireTimerVersion" INTEGER - ,"isSmsMessageRestoredFromBackup" BOOLEAN DEFAULT 0 - ,"isPoll" BOOLEAN DEFAULT 0 - ) -; - -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 - 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 - ,"contentType" TEXT - ) -; - -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 - ,"contactThreadId" TEXT - ,"envelopeData" BLOB - ,"invisibleMessage" BLOB - ,"messageId" TEXT - ,"removeMessageAfterSending" INTEGER - ,"threadId" TEXT - ,"isMediaMessage" BOOLEAN - ,"serverDeliveryTimestamp" INTEGER - ,"exclusiveProcessIdentifier" TEXT - ,"isHighPriority" BOOLEAN - ,"receiptCredentailRequest" BLOB - ,"receiptCredentailRequestContext" BLOB - ,"priorSubscriptionLevel" INTEGER - ,"subscriberID" BLOB - ,"targetSubscriptionLevel" INTEGER - ,"boostPaymentIntentID" TEXT - ,"isBoost" BOOLEAN - ,"receiptCredentialPresentation" BLOB - ,"amount" NUMERIC - ,"currencyCode" TEXT - ,"messageText" TEXT - ,"paymentIntentClientSecret" TEXT - ,"paymentMethodId" TEXT - ,"replacementAdminUuid" TEXT - ,"waitForMessageProcessing" BOOLEAN - ,"isCompleteContactSync" BOOLEAN DEFAULT 0 - ,"paymentProcessor" TEXT - ,"paypalPayerId" TEXT - ,"paypalPaymentId" TEXT - ,"paypalPaymentToken" TEXT - ,"paymentMethod" TEXT - ,"isNewSubscription" BOOLEAN - ,"shouldSuppressPaymentAlreadyRedeemed" BOOLEAN - ,"CRDAJR_sendDeleteAllSyncMessage" BOOLEAN - ,"CRDAJR_deleteAllBeforeTimestamp" INTEGER - ,"CRDAJR_deleteAllBeforeCallId" TEXT - ,"CRDAJR_deleteAllBeforeConversationId" BLOB - ,"ICSJR_cdnNumber" INTEGER - ,"ICSJR_cdnKey" TEXT - ,"ICSJR_encryptionKey" BLOB - ,"ICSJR_digest" BLOB - ,"ICSJR_plaintextLength" INTEGER - ,"BDIJR_anchorMessageRowId" INTEGER - ,"BDIJR_fullThreadDeletionAnchorMessageRowId" INTEGER - ,"BDIJR_threadUniqueId" TEXT - ,"receiptCredential" BLOB - ,"BRCRJR_state" BLOB - ) -; - -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 - 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 - ,"timerVersion" INTEGER NOT NULL DEFAULT 1 - ) -; - -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 - ,"unregisteredAtTimestamp" INTEGER - ,"pni" TEXT - ,"isPhoneNumberDiscoverable" BOOLEAN - ) -; - -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 - ,"familyName" TEXT - ,"lastFetchDate" DOUBLE - ,"lastMessagingDate" DOUBLE - ,"bio" TEXT - ,"bioEmoji" TEXT - ,"profileBadgeInfo" BLOB - ,"isStoriesCapable" BOOLEAN NOT NULL DEFAULT 0 - ,"canReceiveGiftBadges" BOOLEAN NOT NULL DEFAULT 0 - ,"isPniCapable" BOOLEAN NOT NULL DEFAULT 0 - ,"isPhoneNumberShared" BOOLEAN - ) -; - -CREATE - INDEX "index_model_OWSUserProfile_on_lastFetchDate_and_lastMessagingDate" - ON "model_OWSUserProfile"("lastFetchDate" - ,"lastMessagingDate" -) -; - -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_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_thread_on_contactPhoneNumber" - ON "model_TSThread"("contactPhoneNumber" -) -; - -CREATE - INDEX "index_thread_on_contactUUID" - ON "model_TSThread"("contactUUID" -) -; - -CREATE - INDEX "index_user_profiles_on_recipientPhoneNumber" - ON "model_OWSUserProfile"("recipientPhoneNumber" -) -; - -CREATE - INDEX "index_user_profiles_on_recipientUUID" - ON "model_OWSUserProfile"("recipientUUID" -) -; - -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 - ,"contactAvatarHash" BLOB - ,"contactAvatarJpegData" BLOB - ,"multipleAccountLabelText" TEXT NOT NULL - ,"recipientPhoneNumber" TEXT - ,"recipientUUID" TEXT - ,"cnContactId" TEXT - ,"givenName" TEXT NOT NULL DEFAULT '' - ,"familyName" TEXT NOT NULL DEFAULT '' - ,"nickname" TEXT NOT NULL DEFAULT '' - ,"fullName" TEXT NOT NULL DEFAULT '' - ) -; - -CREATE - INDEX "index_signal_accounts_on_recipientPhoneNumber" - ON "model_SignalAccount"("recipientPhoneNumber" -) -; - -CREATE - INDEX "index_signal_accounts_on_recipientUUID" - ON "model_SignalAccount"("recipientUUID" -) -; - -CREATE - TABLE - IF NOT EXISTS "model_OWSReaction" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL - ,"recordType" INTEGER NOT NULL - ,"uniqueId" TEXT NOT NULL UNIQUE - ON CONFLICT FAIL - ,"emoji" TEXT NOT NULL - ,"reactorE164" TEXT - ,"reactorUUID" TEXT - ,"receivedAtTimestamp" INTEGER NOT NULL - ,"sentAtTimestamp" INTEGER NOT NULL - ,"uniqueMessageId" TEXT NOT NULL - ,"read" BOOLEAN NOT NULL DEFAULT 0 - ) -; - -CREATE - INDEX "index_model_OWSReaction_on_uniqueMessageId_and_reactorE164" - ON "model_OWSReaction"("uniqueMessageId" - ,"reactorE164" -) -; - -CREATE - INDEX "index_model_OWSReaction_on_uniqueMessageId_and_reactorUUID" - ON "model_OWSReaction"("uniqueMessageId" - ,"reactorUUID" -) -; - -CREATE - UNIQUE INDEX "index_signal_recipients_on_recipientPhoneNumber" - ON "model_SignalRecipient"("recipientPhoneNumber" -) -; - -CREATE - UNIQUE INDEX "index_signal_recipients_on_recipientUUID" - ON "model_SignalRecipient"("recipientUUID" -) -; - -CREATE - TABLE - IF NOT EXISTS "indexable_text" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL - ,"collection" TEXT NOT NULL - ,"uniqueId" TEXT NOT NULL - ,"ftsIndexableContent" TEXT NOT NULL - ) -; - -CREATE - UNIQUE INDEX "index_indexable_text_on_collection_and_uniqueId" - ON "indexable_text"("collection" - ,"uniqueId" -) -; - -CREATE - VIRTUAL TABLE - "indexable_text_fts" - USING fts5 ( - ftsIndexableContent - ,tokenize = 'unicode61' - ,content = 'indexable_text' - ,content_rowid = 'id' - ) /* indexable_text_fts(ftsIndexableContent) */ -; - -CREATE - TRIGGER "__indexable_text_fts_ai" AFTER INSERT - ON "indexable_text" BEGIN INSERT - INTO - "indexable_text_fts"("rowid" - ,"ftsIndexableContent" -) -VALUES ( -new. "id" -,new. "ftsIndexableContent" -) -; - -END -; - -CREATE - TRIGGER "__indexable_text_fts_ad" AFTER DELETE - ON "indexable_text" BEGIN INSERT - INTO - "indexable_text_fts"("indexable_text_fts" - ,"rowid" - ,"ftsIndexableContent" -) -VALUES ( -'delete' -,old. "id" -,old. "ftsIndexableContent" -) -; - -END -; - -CREATE - TRIGGER "__indexable_text_fts_au" AFTER UPDATE - ON "indexable_text" BEGIN INSERT - INTO - "indexable_text_fts"("indexable_text_fts" - ,"rowid" - ,"ftsIndexableContent" -) -VALUES ( -'delete' -,old. "id" -,old. "ftsIndexableContent" -) -; - -INSERT - INTO - "indexable_text_fts"("rowid" - ,"ftsIndexableContent" -) -VALUES ( -new. "id" -,new. "ftsIndexableContent" -) -; - -END -; - -CREATE - INDEX "index_interaction_on_storedMessageState" - ON "model_TSInteraction"("storedMessageState" -) -; - -CREATE - INDEX "index_interaction_on_recordType_and_callType" - ON "model_TSInteraction"("recordType" - ,"callType" -) -; - -CREATE - TABLE - IF NOT EXISTS "model_IncomingGroupsV2MessageJob" ( - "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 - ,"groupId" BLOB - ,"serverDeliveryTimestamp" INTEGER NOT NULL DEFAULT 0 - ) -; - -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 - ,"firstViewedTimestamp" DOUBLE NOT NULL - ,"lastSnoozedTimestamp" DOUBLE NOT NULL - ,"isComplete" BOOLEAN NOT NULL - ,"manifest" BLOB - ,"snoozeCount" INTEGER NOT NULL DEFAULT 0 - ) -; - -CREATE - TABLE - IF NOT EXISTS "pending_read_receipts" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT - ,"threadId" INTEGER NOT NULL - ,"messageTimestamp" INTEGER NOT NULL - ,"authorPhoneNumber" TEXT - ,"authorUuid" TEXT - ,"messageUniqueId" TEXT - ) -; - -CREATE - INDEX "index_pending_read_receipts_on_threadId" - ON "pending_read_receipts"("threadId" -) -; - -CREATE - INDEX "index_model_IncomingGroupsV2MessageJob_on_groupId_and_id" - ON "model_IncomingGroupsV2MessageJob"("groupId" - ,"id" -) -; - -CREATE - INDEX "index_model_OWSReaction_on_uniqueMessageId_and_read" - ON "model_OWSReaction"("uniqueMessageId" - ,"read" -) -; - -CREATE - INDEX "index_model_TSInteraction_on_uniqueThreadId_recordType_messageType" - ON "model_TSInteraction"("uniqueThreadId" - ,"recordType" - ,"messageType" -) -; - -CREATE - TABLE - IF NOT EXISTS "model_TSMention" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL - ,"recordType" INTEGER NOT NULL - ,"uniqueId" TEXT NOT NULL UNIQUE - ON CONFLICT FAIL - ,"uniqueMessageId" TEXT NOT NULL - ,"uniqueThreadId" TEXT NOT NULL - ,"uuidString" TEXT NOT NULL - ,"creationTimestamp" DOUBLE NOT NULL - ) -; - -CREATE - INDEX "index_model_TSMention_on_uuidString_and_uniqueThreadId" - ON "model_TSMention"("uuidString" - ,"uniqueThreadId" -) -; - -CREATE - UNIQUE INDEX "index_model_TSMention_on_uniqueMessageId_and_uuidString" - ON "model_TSMention"("uniqueMessageId" - ,"uuidString" -) -; - -CREATE - TABLE - IF NOT EXISTS "model_TSPaymentModel" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT - ,"recordType" INTEGER NOT NULL - ,"uniqueId" TEXT NOT NULL UNIQUE - ON CONFLICT FAIL - ,"addressUuidString" TEXT - ,"createdTimestamp" INTEGER NOT NULL - ,"isUnread" BOOLEAN NOT NULL - ,"mcLedgerBlockIndex" INTEGER NOT NULL - ,"mcReceiptData" BLOB - ,"mcTransactionData" BLOB - ,"memoMessage" TEXT - ,"mobileCoin" BLOB - ,"paymentAmount" BLOB - ,"paymentFailure" INTEGER NOT NULL - ,"paymentState" INTEGER NOT NULL - ,"paymentType" INTEGER NOT NULL - ,"requestUuidString" TEXT - ,"interactionUniqueId" TEXT - ) -; - -CREATE - INDEX "index_model_TSPaymentModel_on_paymentState" - ON "model_TSPaymentModel"("paymentState" -) -; - -CREATE - INDEX "index_model_TSPaymentModel_on_mcLedgerBlockIndex" - ON "model_TSPaymentModel"("mcLedgerBlockIndex" -) -; - -CREATE - INDEX "index_model_TSPaymentModel_on_mcReceiptData" - ON "model_TSPaymentModel"("mcReceiptData" -) -; - -CREATE - INDEX "index_model_TSPaymentModel_on_mcTransactionData" - ON "model_TSPaymentModel"("mcTransactionData" -) -; - -CREATE - INDEX "index_model_TSPaymentModel_on_isUnread" - ON "model_TSPaymentModel"("isUnread" -) -; - -CREATE - TABLE - IF NOT EXISTS "model_TSGroupMember" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL - ,"recordType" INTEGER NOT NULL - ,"uniqueId" TEXT NOT NULL UNIQUE - ON CONFLICT FAIL - ,"groupThreadId" TEXT NOT NULL - ,"phoneNumber" TEXT - ,"uuidString" TEXT - ,"lastInteractionTimestamp" INTEGER NOT NULL DEFAULT 0 - ) -; - -CREATE - INDEX "index_model_TSGroupMember_on_groupThreadId" - ON "model_TSGroupMember"("groupThreadId" -) -; - -CREATE - UNIQUE INDEX "index_model_TSGroupMember_on_uuidString_and_groupThreadId" - ON "model_TSGroupMember"("uuidString" - ,"groupThreadId" -) -; - -CREATE - UNIQUE INDEX "index_model_TSGroupMember_on_phoneNumber_and_groupThreadId" - ON "model_TSGroupMember"("phoneNumber" - ,"groupThreadId" -) -; - -CREATE - TABLE - IF NOT EXISTS "pending_viewed_receipts" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT - ,"threadId" INTEGER NOT NULL - ,"messageTimestamp" INTEGER NOT NULL - ,"authorPhoneNumber" TEXT - ,"authorUuid" TEXT - ,"messageUniqueId" TEXT - ) -; - -CREATE - INDEX "index_pending_viewed_receipts_on_threadId" - ON "pending_viewed_receipts"("threadId" -) -; - -CREATE - TABLE - IF NOT EXISTS "thread_associated_data" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT - ,"threadUniqueId" TEXT NOT NULL UNIQUE - ON CONFLICT FAIL - ,"isArchived" BOOLEAN NOT NULL DEFAULT 0 - ,"isMarkedUnread" BOOLEAN NOT NULL DEFAULT 0 - ,"mutedUntilTimestamp" INTEGER NOT NULL DEFAULT 0 - ,"audioPlaybackRate" DOUBLE NOT NULL DEFAULT 1 - ) -; - -CREATE - INDEX "index_thread_associated_data_on_threadUniqueId_and_isMarkedUnread" - ON "thread_associated_data"("threadUniqueId" - ,"isMarkedUnread" -) -; - -CREATE - INDEX "index_thread_associated_data_on_threadUniqueId_and_isArchived" - ON "thread_associated_data"("threadUniqueId" - ,"isArchived" -) -; - -CREATE - TABLE - IF NOT EXISTS "MessageSendLog_Payload" ( - "payloadId" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL - ,"plaintextContent" BLOB NOT NULL - ,"contentHint" INTEGER NOT NULL - ,"sentTimestamp" INTEGER NOT NULL - ,"uniqueThreadId" TEXT NOT NULL - ,"sendComplete" BOOLEAN NOT NULL DEFAULT 0 - ) -; - -CREATE - TABLE - IF NOT EXISTS "MessageSendLog_Message" ( - "payloadId" INTEGER NOT NULL - ,"uniqueId" TEXT NOT NULL - ,PRIMARY KEY ( - "payloadId" - ,"uniqueId" - ) - ,FOREIGN KEY ("payloadId") REFERENCES "MessageSendLog_Payload"("payloadId" - ) - ON DELETE - CASCADE - ON UPDATE - CASCADE -) -; - -CREATE - TABLE - IF NOT EXISTS "MessageSendLog_Recipient" ( - "payloadId" INTEGER NOT NULL - ,"recipientUUID" TEXT NOT NULL - ,"recipientDeviceId" INTEGER NOT NULL - ,PRIMARY KEY ( - "payloadId" - ,"recipientUUID" - ,"recipientDeviceId" - ) - ,FOREIGN KEY ("payloadId") REFERENCES "MessageSendLog_Payload"("payloadId" - ) - ON DELETE - CASCADE - ON UPDATE - CASCADE -) -; - -CREATE - INDEX "MSLPayload_sentTimestampIndex" - ON "MessageSendLog_Payload"("sentTimestamp" -) -; - -CREATE - INDEX "MSLMessage_relatedMessageId" - ON "MessageSendLog_Message"("uniqueId" -) -; - -CREATE - TABLE - IF NOT EXISTS "model_ProfileBadgeTable" ( - "id" TEXT PRIMARY KEY - ,"rawCategory" TEXT NOT NULL - ,"localizedName" TEXT NOT NULL - ,"localizedDescriptionFormatString" TEXT NOT NULL - ,"resourcePath" TEXT NOT NULL - ,"badgeVariant" TEXT NOT NULL - ,"localization" TEXT NOT NULL - ,"duration" NUMERIC - ) -; - -CREATE - TABLE - IF NOT EXISTS "model_StoryMessage" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL - ,"recordType" INTEGER NOT NULL - ,"uniqueId" TEXT NOT NULL UNIQUE - ON CONFLICT FAIL - ,"timestamp" INTEGER NOT NULL - ,"authorUuid" TEXT NOT NULL - ,"groupId" BLOB - ,"direction" INTEGER NOT NULL - ,"manifest" BLOB NOT NULL - ,"attachment" BLOB NOT NULL - ,"replyCount" INTEGER NOT NULL DEFAULT 0 - ) -; - -CREATE - INDEX "index_model_StoryMessage_on_timestamp_and_authorUuid" - ON "model_StoryMessage"("timestamp" - ,"authorUuid" -) -; - -CREATE - INDEX "index_model_StoryMessage_on_direction" - ON "model_StoryMessage"("direction" -) -; - -CREATE - INDEX "index_model_TSInteraction_UnreadMessages" - ON "model_TSInteraction" ( - "read" - ,"uniqueThreadId" - ,"id" - ,"isGroupStoryReply" - ,"editState" - ,"recordType" - ) -; - -CREATE - TABLE - IF NOT EXISTS "model_DonationReceipt" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL - ,"uniqueId" TEXT NOT NULL UNIQUE - ON CONFLICT FAIL - ,"timestamp" INTEGER NOT NULL - ,"subscriptionLevel" INTEGER - ,"amount" NUMERIC NOT NULL - ,"currencyCode" TEXT NOT NULL - ,"receiptType" NUMERIC - ) -; - -CREATE - INDEX "index_model_TSThread_on_storyViewMode" - ON "model_TSThread"("storyViewMode" - ,"lastSentStoryTimestamp" - ,"allowsReplies" -) -; - -CREATE - INDEX index_model_StoryMessage_on_incoming_receivedState_viewedTimestamp - ON model_StoryMessage ( - json_extract ( - manifest - ,'$.incoming.receivedState.viewedTimestamp' - ) - ) -; - -CREATE - TABLE - IF NOT EXISTS "model_StoryContextAssociatedData" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL - ,"recordType" INTEGER NOT NULL - ,"uniqueId" NOT NULL UNIQUE - ON CONFLICT FAIL - ,"contactUuid" TEXT - ,"groupId" BLOB - ,"isHidden" BOOLEAN NOT NULL DEFAULT 0 - ,"latestUnexpiredTimestamp" INTEGER - ,"lastReceivedTimestamp" INTEGER - ,"lastViewedTimestamp" INTEGER - ,"lastReadTimestamp" INTEGER - ) -; - -CREATE - INDEX "index_story_context_associated_data_contact_on_contact_uuid" - ON "model_StoryContextAssociatedData"("contactUuid" -) -; - -CREATE - INDEX "index_story_context_associated_data_contact_on_group_id" - ON "model_StoryContextAssociatedData"("groupId" -) -; - -CREATE - TABLE - IF NOT EXISTS "cancelledGroupRing" ( - "id" INTEGER PRIMARY KEY NOT NULL - ,"timestamp" INTEGER NOT NULL - ) -; - -CREATE - TABLE - IF NOT EXISTS "CdsPreviousE164" ( - "id" INTEGER PRIMARY KEY NOT NULL - ,"e164" TEXT NOT NULL - ) -; - -CREATE - TABLE - IF NOT EXISTS "spamReportingTokenRecords" ( - "sourceUuid" BLOB PRIMARY KEY NOT NULL - ,"spamReportingToken" BLOB NOT NULL - ) -; - -CREATE - TABLE - IF NOT EXISTS "UsernameLookupRecord" ( - "aci" BLOB PRIMARY KEY NOT NULL - ,"username" TEXT NOT NULL - ) -; - -CREATE - TABLE - IF NOT EXISTS "EditRecord" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT - ,"latestRevisionId" INTEGER NOT NULL REFERENCES "model_TSInteraction"("id" - ) - ON DELETE - RESTRICT - ,"pastRevisionId" INTEGER NOT NULL REFERENCES "model_TSInteraction"("id" -) - ON DELETE - RESTRICT - ,"read" BOOLEAN NOT NULL DEFAULT 0 -) -; - -CREATE - INDEX "index_edit_record_on_latest_revision_id" - ON "EditRecord"("latestRevisionId" -) -; - -CREATE - INDEX "index_edit_record_on_past_revision_id" - ON "EditRecord"("pastRevisionId" -) -; - -CREATE - TABLE - IF NOT EXISTS "HiddenRecipient" ( - "recipientId" INTEGER PRIMARY KEY NOT NULL - ,"inKnownMessageRequestState" BOOLEAN NOT NULL DEFAULT 0 - ,FOREIGN KEY ("recipientId") REFERENCES "model_SignalRecipient"("id" - ) - ON DELETE - CASCADE -) -; - -CREATE - TABLE - IF NOT EXISTS "TSPaymentsActivationRequestModel" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT - ,"threadUniqueId" TEXT NOT NULL - ,"senderAci" BLOB NOT NULL - ) -; - -CREATE - INDEX "index_TSPaymentsActivationRequestModel_on_threadUniqueId" - ON "TSPaymentsActivationRequestModel"("threadUniqueId" -) -; - -CREATE - UNIQUE INDEX "index_signal_recipients_on_pni" - ON "model_SignalRecipient"("pni" -) -; - -CREATE - TABLE - IF NOT EXISTS "SearchableName" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL - ,"threadId" INTEGER UNIQUE - ,"signalAccountId" INTEGER UNIQUE - ,"userProfileId" INTEGER UNIQUE - ,"signalRecipientId" INTEGER UNIQUE - ,"usernameLookupRecordId" BLOB UNIQUE - ,"value" TEXT NOT NULL - ,"nicknameRecordRecipientId" INTEGER REFERENCES "NicknameRecord"("recipientRowID" - ) - ON DELETE - CASCADE - ,FOREIGN KEY ("threadId") REFERENCES "model_TSThread"("id" -) - ON DELETE - CASCADE - ON UPDATE - CASCADE - ,FOREIGN KEY ("signalAccountId") REFERENCES "model_SignalAccount"("id" -) - ON DELETE - CASCADE - ON UPDATE - CASCADE - ,FOREIGN KEY ("userProfileId") REFERENCES "model_OWSUserProfile"("id" -) - ON DELETE - CASCADE - ON UPDATE - CASCADE - ,FOREIGN KEY ("signalRecipientId") REFERENCES "model_SignalRecipient"("id" -) - ON DELETE - CASCADE - ON UPDATE - CASCADE - ,FOREIGN KEY ("usernameLookupRecordId") REFERENCES "UsernameLookupRecord"("aci" -) - ON DELETE - CASCADE - ON UPDATE - CASCADE -) -; - -CREATE - VIRTUAL TABLE - "SearchableNameFTS" - USING fts5 ( - VALUE - ,tokenize = 'unicode61' - ,content = 'SearchableName' - ,content_rowid = 'id' - ) /* SearchableNameFTS(value) */ -; - -CREATE - TRIGGER "__SearchableNameFTS_ai" AFTER INSERT - ON "SearchableName" BEGIN INSERT - INTO - "SearchableNameFTS"("rowid" - ,"value" -) -VALUES ( -new. "id" -,new. "value" -) -; - -END -; - -CREATE - TRIGGER "__SearchableNameFTS_ad" AFTER DELETE - ON "SearchableName" BEGIN INSERT - INTO - "SearchableNameFTS"("SearchableNameFTS" - ,"rowid" - ,"value" -) -VALUES ( -'delete' -,old. "id" -,old. "value" -) -; - -END -; - -CREATE - TRIGGER "__SearchableNameFTS_au" AFTER UPDATE - ON "SearchableName" BEGIN INSERT - INTO - "SearchableNameFTS"("SearchableNameFTS" - ,"rowid" - ,"value" -) -VALUES ( -'delete' -,old. "id" -,old. "value" -) -; - -INSERT - INTO - "SearchableNameFTS"("rowid" - ,"value" -) -VALUES ( -new. "id" -,new. "value" -) -; - -END -; - -CREATE - TABLE - IF NOT EXISTS "NicknameRecord" ( - "recipientRowID" INTEGER PRIMARY KEY NOT NULL REFERENCES "model_SignalRecipient"("id" - ) - ON DELETE - CASCADE - ,"givenName" TEXT - ,"familyName" TEXT - ,"note" TEXT -) -; - -CREATE - TABLE - IF NOT EXISTS "Attachment" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL - ,"blurHash" TEXT - ,"sha256ContentHash" BLOB UNIQUE - ,"mediaName" TEXT UNIQUE - ,"encryptedByteCount" INTEGER - ,"unencryptedByteCount" INTEGER - ,"mimeType" TEXT NOT NULL - ,"encryptionKey" BLOB NOT NULL - ,"digestSHA256Ciphertext" BLOB - ,"localRelativeFilePath" TEXT - ,"contentType" INTEGER - ,"transitCdnNumber" INTEGER - ,"transitCdnKey" TEXT - ,"transitUploadTimestamp" INTEGER - ,"transitEncryptionKey" BLOB - ,"transitDigestSHA256Ciphertext" BLOB - ,"lastTransitDownloadAttemptTimestamp" INTEGER - ,"mediaTierCdnNumber" INTEGER - ,"mediaTierUploadEra" TEXT - ,"lastMediaTierDownloadAttemptTimestamp" INTEGER - ,"thumbnailCdnNumber" INTEGER - ,"thumbnailUploadEra" TEXT - ,"lastThumbnailDownloadAttemptTimestamp" INTEGER - ,"localRelativeFilePathThumbnail" TEXT - ,"cachedAudioDurationSeconds" DOUBLE - ,"cachedMediaHeightPixels" INTEGER - ,"cachedMediaWidthPixels" INTEGER - ,"cachedVideoDurationSeconds" DOUBLE - ,"audioWaveformRelativeFilePath" TEXT - ,"videoStillFrameRelativeFilePath" TEXT - ,"originalAttachmentIdForQuotedReply" INTEGER REFERENCES "Attachment"("id" - ) - ON DELETE - SET - NULL - ,"transitUnencryptedByteCount" INTEGER - ,"mediaTierUnencryptedByteCount" INTEGER - ,"mediaTierIncrementalMac" BLOB - ,"mediaTierIncrementalMacChunkSize" INTEGER - ,"transitTierIncrementalMac" BLOB - ,"transitTierIncrementalMacChunkSize" INTEGER - ,"lastFullscreenViewTimestamp" INTEGER - ,"originalTransitCdnNumber" INTEGER - ,"originalTransitCdnKey" TEXT - ,"originalTransitUploadTimestamp" INTEGER - ,"originalTransitUnencryptedByteCount" INTEGER - ,"originalTransitDigestSHA256Ciphertext" BLOB - ,"originalTransitTierIncrementalMac" BLOB - ,"originalTransitTierIncrementalMacChunkSize" INTEGER -) -; - -CREATE - INDEX "index_attachment_on_contentType_and_mimeType" - ON "Attachment"("contentType" - ,"mimeType" -) -; - -CREATE - TABLE - IF NOT EXISTS "MessageAttachmentReference" ( - "ownerType" INTEGER NOT NULL - ,"ownerRowId" INTEGER NOT NULL REFERENCES "model_TSInteraction"("id" - ) - ON DELETE - CASCADE - ,"attachmentRowId" INTEGER NOT NULL REFERENCES "Attachment"("id" -) - ON DELETE - CASCADE - ,"receivedAtTimestamp" INTEGER NOT NULL - ,"contentType" INTEGER - ,"renderingFlag" INTEGER NOT NULL - ,"idInMessage" TEXT - ,"orderInMessage" INTEGER - ,"threadRowId" INTEGER NOT NULL REFERENCES "model_TSThread"("id" -) - ON DELETE - CASCADE - ,"caption" TEXT - ,"sourceFilename" TEXT - ,"sourceUnencryptedByteCount" INTEGER - ,"sourceMediaHeightPixels" INTEGER - ,"sourceMediaWidthPixels" INTEGER - ,"stickerPackId" BLOB - ,"stickerId" INTEGER - ,isVisualMediaContentType AS ( - contentType = 2 - OR contentType = 3 - OR contentType = 4 - ) VIRTUAL - ,isInvalidOrFileContentType AS ( - contentType = 0 - OR contentType = 1 - ) VIRTUAL - ,"isViewOnce" BOOLEAN NOT NULL DEFAULT 0 - ,"ownerIsPastEditRevision" BOOLEAN DEFAULT 0 -) -; - -CREATE - INDEX "index_message_attachment_reference_on_ownerRowId_and_ownerType" - ON "MessageAttachmentReference"("ownerRowId" - ,"ownerType" -) -; - -CREATE - INDEX "index_message_attachment_reference_on_attachmentRowId" - ON "MessageAttachmentReference"("attachmentRowId" -) -; - -CREATE - INDEX "index_message_attachment_reference_on_ownerRowId_and_idInMessage" - ON "MessageAttachmentReference"("ownerRowId" - ,"idInMessage" -) -; - -CREATE - INDEX "index_message_attachment_reference_on_stickerPackId_and_stickerId" - ON "MessageAttachmentReference"("stickerPackId" - ,"stickerId" -) -; - -CREATE - TABLE - IF NOT EXISTS "StoryMessageAttachmentReference" ( - "ownerType" INTEGER NOT NULL - ,"ownerRowId" INTEGER NOT NULL REFERENCES "model_StoryMessage"("id" - ) - ON DELETE - CASCADE - ,"attachmentRowId" INTEGER NOT NULL REFERENCES "Attachment"("id" -) - ON DELETE - CASCADE - ,"shouldLoop" BOOLEAN NOT NULL - ,"caption" TEXT - ,"captionBodyRanges" BLOB - ,"sourceFilename" TEXT - ,"sourceUnencryptedByteCount" INTEGER - ,"sourceMediaHeightPixels" INTEGER - ,"sourceMediaWidthPixels" INTEGER -) -; - -CREATE - INDEX "index_story_message_attachment_reference_on_ownerRowId_and_ownerType" - ON "StoryMessageAttachmentReference"("ownerRowId" - ,"ownerType" -) -; - -CREATE - INDEX "index_story_message_attachment_reference_on_attachmentRowId" - ON "StoryMessageAttachmentReference"("attachmentRowId" -) -; - -CREATE - TABLE - IF NOT EXISTS "ThreadAttachmentReference" ( - "ownerRowId" INTEGER UNIQUE REFERENCES "model_TSThread"("id" - ) - ON DELETE - CASCADE - ,"attachmentRowId" INTEGER NOT NULL REFERENCES "Attachment"("id" -) - ON DELETE - CASCADE - ,"creationTimestamp" INTEGER NOT NULL -) -; - -CREATE - INDEX "index_thread_attachment_reference_on_attachmentRowId" - ON "ThreadAttachmentReference"("attachmentRowId" -) -; - -CREATE - TABLE - IF NOT EXISTS "OrphanedAttachment" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL - ,"localRelativeFilePath" TEXT - ,"localRelativeFilePathThumbnail" TEXT - ,"localRelativeFilePathAudioWaveform" TEXT - ,"localRelativeFilePathVideoStillFrame" TEXT - ,"isPendingAttachment" BOOLEAN NOT NULL DEFAULT 0 - ) -; - -CREATE - TRIGGER __Attachment_contentType_au AFTER UPDATE - OF contentType - ON Attachment BEGIN UPDATE - MessageAttachmentReference - SET - contentType = NEW.contentType - WHERE - attachmentRowId = OLD.id -; - -END -; - -CREATE - TRIGGER "__MessageAttachmentReference_ad" AFTER DELETE - ON "MessageAttachmentReference" BEGIN DELETE - FROM - Attachment - WHERE - id = OLD.attachmentRowId - AND NOT EXISTS ( - SELECT - 1 - FROM - MessageAttachmentReference - WHERE - attachmentRowId = OLD.attachmentRowId - ) - AND NOT EXISTS ( - SELECT - 1 - FROM - StoryMessageAttachmentReference - WHERE - attachmentRowId = OLD.attachmentRowId - ) - AND NOT EXISTS ( - SELECT - 1 - FROM - ThreadAttachmentReference - WHERE - attachmentRowId = OLD.attachmentRowId - ) -; - -END -; - -CREATE - TRIGGER "__StoryMessageAttachmentReference_ad" AFTER DELETE - ON "StoryMessageAttachmentReference" BEGIN DELETE - FROM - Attachment - WHERE - id = OLD.attachmentRowId - AND NOT EXISTS ( - SELECT - 1 - FROM - MessageAttachmentReference - WHERE - attachmentRowId = OLD.attachmentRowId - ) - AND NOT EXISTS ( - SELECT - 1 - FROM - StoryMessageAttachmentReference - WHERE - attachmentRowId = OLD.attachmentRowId - ) - AND NOT EXISTS ( - SELECT - 1 - FROM - ThreadAttachmentReference - WHERE - attachmentRowId = OLD.attachmentRowId - ) -; - -END -; - -CREATE - TRIGGER "__ThreadAttachmentReference_ad" AFTER DELETE - ON "ThreadAttachmentReference" BEGIN DELETE - FROM - Attachment - WHERE - id = OLD.attachmentRowId - AND NOT EXISTS ( - SELECT - 1 - FROM - MessageAttachmentReference - WHERE - attachmentRowId = OLD.attachmentRowId - ) - AND NOT EXISTS ( - SELECT - 1 - FROM - StoryMessageAttachmentReference - WHERE - attachmentRowId = OLD.attachmentRowId - ) - AND NOT EXISTS ( - SELECT - 1 - FROM - ThreadAttachmentReference - WHERE - attachmentRowId = OLD.attachmentRowId - ) -; - -END -; - -CREATE - TRIGGER "__Attachment_ad" AFTER DELETE - ON "Attachment" BEGIN INSERT - INTO - OrphanedAttachment ( - localRelativeFilePath - ,localRelativeFilePathThumbnail - ,localRelativeFilePathAudioWaveform - ,localRelativeFilePathVideoStillFrame - ) - VALUES ( - OLD.localRelativeFilePath - ,OLD.localRelativeFilePathThumbnail - ,OLD.audioWaveformRelativeFilePath - ,OLD.videoStillFrameRelativeFilePath - ) -; - -END -; - -CREATE - INDEX "index_thread_on_shouldThreadBeVisible" - ON "model_TSThread" ( - "shouldThreadBeVisible" - ,"lastInteractionRowId" DESC - ) -; - -CREATE - INDEX "index_attachment_on_originalAttachmentIdForQuotedReply" - ON "Attachment"("originalAttachmentIdForQuotedReply" -) -; - -CREATE - INDEX "message_attachment_reference_media_gallery_single_content_type_index" - ON "MessageAttachmentReference"("threadRowId" - ,"ownerType" - ,"contentType" - ,"receivedAtTimestamp" - ,"ownerRowId" - ,"orderInMessage" -) -; - -CREATE - INDEX "message_attachment_reference_media_gallery_visualMedia_content_type_index" - ON "MessageAttachmentReference"("threadRowId" - ,"ownerType" - ,"isVisualMediaContentType" - ,"receivedAtTimestamp" - ,"ownerRowId" - ,"orderInMessage" -) -; - -CREATE - INDEX "message_attachment_reference_media_gallery_fileOrInvalid_content_type_index" - ON "MessageAttachmentReference"("threadRowId" - ,"ownerType" - ,"isInvalidOrFileContentType" - ,"receivedAtTimestamp" - ,"ownerRowId" - ,"orderInMessage" -) -; - -CREATE - TABLE - IF NOT EXISTS "AttachmentDownloadQueue" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL - ,"sourceType" INTEGER NOT NULL - ,"attachmentId" INTEGER NOT NULL REFERENCES "Attachment"("id" - ) - ON DELETE - CASCADE - ,"priority" INTEGER NOT NULL - ,"minRetryTimestamp" INTEGER - ,"retryAttempts" INTEGER NOT NULL - ,"localRelativeFilePath" TEXT NOT NULL -) -; - -CREATE - INDEX "index_AttachmentDownloadQueue_on_attachmentId_and_sourceType" - ON "AttachmentDownloadQueue"("attachmentId" - ,"sourceType" -) -; - -CREATE - INDEX "index_AttachmentDownloadQueue_on_priority" - ON "AttachmentDownloadQueue"("priority" -) -; - -CREATE - INDEX "partial_index_AttachmentDownloadQueue_on_priority_DESC_and_id_where_minRetryTimestamp_isNull" - ON "AttachmentDownloadQueue" ( - "priority" DESC - ,"id" - ) -WHERE - minRetryTimestamp IS NULL -; - -CREATE - INDEX "partial_index_AttachmentDownloadQueue_on_minRetryTimestamp_where_isNotNull" - ON "AttachmentDownloadQueue" ("minRetryTimestamp") -WHERE - minRetryTimestamp IS NOT NULL -; - -CREATE - TRIGGER "__AttachmentDownloadQueue_ad" AFTER DELETE - ON "AttachmentDownloadQueue" BEGIN INSERT - INTO - OrphanedAttachment (localRelativeFilePath) - VALUES (OLD.localRelativeFilePath) -; - -END -; - -CREATE - TABLE - IF NOT EXISTS "ArchivedPayment" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL - ,"amount" TEXT - ,"fee" TEXT - ,"note" TEXT - ,"mobileCoinIdentification" BLOB - ,"status" INTEGER - ,"failureReason" INTEGER - ,"timestamp" INTEGER - ,"blockIndex" INTEGER - ,"blockTimestamp" INTEGER - ,"transaction" BLOB - ,"receipt" BLOB - ,"direction" INTEGER - ,"senderOrRecipientAci" BLOB - ,"interactionUniqueId" TEXT - ) -; - -CREATE - INDEX "index_archived_payment_on_interaction_unique_id" - ON "ArchivedPayment"("interactionUniqueId" -) -; - -CREATE - INDEX "index_message_attachment_reference_on_receivedAtTimestamp" - ON "MessageAttachmentReference"("receivedAtTimestamp" -) -; - -CREATE - TABLE - IF NOT EXISTS "AttachmentUploadRecord" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL - ,"sourceType" INTEGER NOT NULL - ,"attachmentId" INTEGER NOT NULL - ,"uploadForm" BLOB - ,"uploadFormTimestamp" INTEGER - ,"localMetadata" BLOB - ,"uploadSessionUrl" BLOB - ,"attempt" INTEGER - ) -; - -CREATE - INDEX "index_attachment_upload_record_on_attachment_id" - ON "AttachmentUploadRecord"("attachmentId" -) -; - -CREATE - TABLE - IF NOT EXISTS "BlockedRecipient" ( - "recipientId" INTEGER PRIMARY KEY REFERENCES "model_SignalRecipient"("id" - ) - ON DELETE - CASCADE - ON UPDATE - CASCADE -) -; - -CREATE - TABLE - IF NOT EXISTS "AttachmentValidationBackfillQueue" ( - "attachmentId" INTEGER PRIMARY KEY - ON CONFLICT IGNORE NOT NULL REFERENCES "Attachment"("id" - ) - ON DELETE - CASCADE -) -; - -CREATE - TABLE - IF NOT EXISTS "BackupStickerPackDownloadQueue" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT - ,"packId" BLOB NOT NULL - ,"packKey" BLOB NOT NULL - ) -; - -CREATE - TABLE - IF NOT EXISTS "OrphanedBackupAttachment" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL - ,"cdnNumber" INTEGER NOT NULL - ,"mediaName" TEXT - ,"mediaId" BLOB - ,"type" INTEGER - ) -; - -CREATE - INDEX "index_OrphanedBackupAttachment_on_mediaName" - ON "OrphanedBackupAttachment"("mediaName" -) -; - -CREATE - INDEX "index_OrphanedBackupAttachment_on_mediaId" - ON "OrphanedBackupAttachment"("mediaId" -) -; - -CREATE - TRIGGER "__Attachment_ad_backup_fullsize" AFTER DELETE - ON "Attachment" WHEN ( - OLD.mediaTierCdnNumber IS NOT NULL - AND OLD.mediaName IS NOT NULL - ) BEGIN INSERT - INTO - OrphanedBackupAttachment ( - cdnNumber - ,mediaName - ,mediaId - ,type - ) - VALUES ( - OLD.mediaTierCdnNumber - ,OLD.mediaName - ,NULL - ,0 - ) -; - -END -; - -CREATE - TRIGGER "__Attachment_ad_backup_thumbnail" AFTER DELETE - ON "Attachment" WHEN ( - OLD.thumbnailCdnNumber IS NOT NULL - AND OLD.mediaName IS NOT NULL - ) BEGIN INSERT - INTO - OrphanedBackupAttachment ( - cdnNumber - ,mediaName - ,mediaId - ,type - ) - VALUES ( - OLD.thumbnailCdnNumber - ,OLD.mediaName - ,NULL - ,1 - ) -; - -END -; - -CREATE - TABLE - IF NOT EXISTS "CallLink" ( - "id" INTEGER PRIMARY KEY - ,"roomId" BLOB NOT NULL UNIQUE - ,"rootKey" BLOB NOT NULL - ,"adminPasskey" BLOB - ,"adminDeletedAtTimestampMs" INTEGER - ,"activeCallId" INTEGER - ,"isUpcoming" BOOLEAN - ,"pendingActionCounter" INTEGER NOT NULL DEFAULT 0 - ,"name" TEXT - ,"restrictions" INTEGER - ,"revoked" BOOLEAN - ,"expiration" INTEGER - ,CHECK ( - LENGTH( "roomId" ) IS 32 - ) - ,CHECK ( - LENGTH( "rootKey" ) IS 16 - ) - ,CHECK ( - LENGTH( "adminPasskey" ) > 0 - OR "adminPasskey" IS NULL - ) - ,CHECK ( - NOT ( - "isUpcoming" IS TRUE - AND "expiration" IS NULL - ) - ) - ) -; - -CREATE - INDEX "CallLink_Upcoming" - ON "CallLink"("expiration" -) -WHERE -"isUpcoming" = 1 -; - -CREATE - INDEX "CallLink_Pending" - ON "CallLink"("pendingActionCounter" -) -WHERE -"pendingActionCounter" > 0 -; - -CREATE - INDEX "CallLink_AdminDeleted" - ON "CallLink"("adminDeletedAtTimestampMs" -) -WHERE -"adminDeletedAtTimestampMs" IS NOT NULL -; - -CREATE - TABLE - IF NOT EXISTS "CallRecord" ( - "id" INTEGER PRIMARY KEY NOT NULL - ,"callId" TEXT NOT NULL - ,"interactionRowId" INTEGER UNIQUE REFERENCES "model_TSInteraction"("id" - ) - ON DELETE - RESTRICT - ON UPDATE - CASCADE - ,"threadRowId" INTEGER REFERENCES "model_TSThread"("id" -) - ON DELETE - RESTRICT - ON UPDATE - CASCADE - ,"callLinkRowId" INTEGER REFERENCES "CallLink"("id" -) - ON DELETE - RESTRICT - ON UPDATE - CASCADE - ,"type" INTEGER NOT NULL - ,"direction" INTEGER NOT NULL - ,"status" INTEGER NOT NULL - ,"unreadStatus" INTEGER NOT NULL - ,"callBeganTimestamp" INTEGER NOT NULL - ,"callEndedTimestamp" INTEGER NOT NULL - ,"groupCallRingerAci" BLOB - ,CHECK ( - IIF ( - "threadRowId" IS NOT NULL - ,"callLinkRowId" IS NULL - ,"callLinkRowId" IS NOT NULL - ) - ) - ,CHECK ( - IIF ( - "threadRowId" IS NOT NULL - ,"interactionRowId" IS NOT NULL - ,"interactionRowId" IS NULL - ) - ) -) -; - -CREATE - TABLE - IF NOT EXISTS "DeletedCallRecord" ( - "id" INTEGER PRIMARY KEY NOT NULL - ,"callId" TEXT NOT NULL - ,"threadRowId" INTEGER REFERENCES "model_TSThread"("id" - ) - ON DELETE - RESTRICT - ON UPDATE - CASCADE - ,"callLinkRowId" INTEGER REFERENCES "CallLink"("id" -) - ON DELETE - RESTRICT - ON UPDATE - CASCADE - ,"deletedAtTimestamp" INTEGER NOT NULL - ,CHECK ( - IIF ( - "threadRowId" IS NOT NULL - ,"callLinkRowId" IS NULL - ,"callLinkRowId" IS NOT NULL - ) - ) -) -; - -CREATE - UNIQUE INDEX "CallRecord_threadRowId_callId" - ON "CallRecord"("threadRowId" - ,"callId" -) -WHERE -"threadRowId" IS NOT NULL -; - -CREATE - UNIQUE INDEX "CallRecord_callLinkRowId_callId" - ON "CallRecord"("callLinkRowId" - ,"callId" -) -WHERE -"callLinkRowId" IS NOT NULL -; - -CREATE - INDEX "CallRecord_callBeganTimestamp" - ON "CallRecord"("callBeganTimestamp" -) -; - -CREATE - INDEX "CallRecord_status_callBeganTimestamp" - ON "CallRecord"("status" - ,"callBeganTimestamp" -) -; - -CREATE - INDEX "CallRecord_threadRowId_callBeganTimestamp" - ON "CallRecord"("threadRowId" - ,"callBeganTimestamp" -) -WHERE -"threadRowId" IS NOT NULL -; - -CREATE - INDEX "CallRecord_callLinkRowId_callBeganTimestamp" - ON "CallRecord"("callLinkRowId" - ,"callBeganTimestamp" -) -WHERE -"callLinkRowId" IS NOT NULL -; - -CREATE - INDEX "CallRecord_threadRowId_status_callBeganTimestamp" - ON "CallRecord"("threadRowId" - ,"status" - ,"callBeganTimestamp" -) -WHERE -"threadRowId" IS NOT NULL -; - -CREATE - INDEX "CallRecord_callStatus_unreadStatus_callBeganTimestamp" - ON "CallRecord"("status" - ,"unreadStatus" - ,"callBeganTimestamp" -) -; - -CREATE - INDEX "CallRecord_threadRowId_callStatus_unreadStatus_callBeganTimestamp" - ON "CallRecord"("threadRowId" - ,"status" - ,"unreadStatus" - ,"callBeganTimestamp" -) -WHERE -"threadRowId" IS NOT NULL -; - -CREATE - UNIQUE INDEX "DeletedCallRecord_threadRowId_callId" - ON "DeletedCallRecord"("threadRowId" - ,"callId" -) -WHERE -"threadRowId" IS NOT NULL -; - -CREATE - UNIQUE INDEX "DeletedCallRecord_callLinkRowId_callId" - ON "DeletedCallRecord"("callLinkRowId" - ,"callId" -) -WHERE -"callLinkRowId" IS NOT NULL -; - -CREATE - INDEX "DeletedCallRecord_deletedAtTimestamp" - ON "DeletedCallRecord"("deletedAtTimestamp" -) -; - -CREATE - TABLE - IF NOT EXISTS "MessageBackupAvatarFetchQueue" ( - "id" INTEGER PRIMARY KEY NOT NULL - ,"groupThreadRowId" INTEGER REFERENCES "model_TSThread"("id" - ) - ON DELETE - CASCADE - ,"groupAvatarUrl" TEXT - ,"serviceId" BLOB - ,"numRetries" INTEGER NOT NULL DEFAULT 0 - ,"nextRetryTimestamp" INTEGER NOT NULL DEFAULT 0 -) -; - -CREATE - INDEX "index_MessageBackupAvatarFetchQueue_on_nextRetryTimestamp" - ON "MessageBackupAvatarFetchQueue"("nextRetryTimestamp" -) -; - -CREATE - TABLE - IF NOT EXISTS "BlockedGroup" ( - "groupId" BLOB PRIMARY KEY NOT NULL - ) WITHOUT ROWID -; - -CREATE - TABLE - IF NOT EXISTS "CombinedGroupSendEndorsement" ( - "threadId" INTEGER PRIMARY KEY REFERENCES "model_TSThread"("id" - ) - ON DELETE - CASCADE - ON UPDATE - CASCADE - ,"endorsement" BLOB NOT NULL - ,"expiration" INTEGER NOT NULL -) -; - -CREATE - TABLE - IF NOT EXISTS "IndividualGroupSendEndorsement" ( - "threadId" INTEGER NOT NULL REFERENCES "CombinedGroupSendEndorsement"("threadId" - ) - ON DELETE - CASCADE - ON UPDATE - CASCADE - ,"recipientId" INTEGER NOT NULL REFERENCES "model_SignalRecipient"("id" -) - ON DELETE - CASCADE - ON UPDATE - CASCADE - ,"endorsement" BLOB NOT NULL - ,PRIMARY KEY ( - "threadId" - ,"recipientId" - ) -) -; - -CREATE - INDEX "IndividualGroupSendEndorsement_recipientId" - ON "IndividualGroupSendEndorsement"("recipientId" -) -; - -CREATE - INDEX "Interaction_incompleteViewOnce_partial" - ON "model_TSInteraction"("isViewOnceMessage" - ,"isViewOnceComplete" -) -WHERE -( - "isViewOnceMessage" = 1 -) -AND ( - "isViewOnceComplete" = 0 -) -; - -CREATE - INDEX "Interaction_disappearingMessages_partial" - ON "model_TSInteraction"("expiresAt" -) -WHERE -"expiresAt" > 0 -; - -CREATE - INDEX "Interaction_timestamp" - ON "model_TSInteraction"("timestamp" -) -; - -CREATE - INDEX "Interaction_unendedGroupCall_partial" - ON "model_TSInteraction"("recordType" - ,"hasEnded" - ,"uniqueThreadId" -) -WHERE -( - "recordType" = 65 -) -AND ( - "hasEnded" = 0 -) -; - -CREATE - INDEX "Interaction_groupCallEraId_partial" - ON "model_TSInteraction"("uniqueThreadId" - ,"recordType" - ,"eraId" -) -WHERE -"eraId" IS NOT NULL -; - -CREATE - INDEX "Interaction_storyReply_partial" - ON "model_TSInteraction"("storyAuthorUuidString" - ,"storyTimestamp" - ,"isGroupStoryReply" -) -WHERE -( - "storyAuthorUuidString" IS NOT NULL -) -AND ( - "storyTimestamp" IS NOT NULL -) -; - -CREATE - TABLE - IF NOT EXISTS "AvatarDefaultColor" ( - "recipientRowId" INTEGER UNIQUE REFERENCES "model_SignalRecipient"("id" - ) - ON DELETE - CASCADE - ON UPDATE - CASCADE - ,"groupId" BLOB UNIQUE - ,"defaultColorIndex" INTEGER NOT NULL -) -; - -CREATE - TABLE - IF NOT EXISTS "StoryRecipient" ( - "threadId" INTEGER NOT NULL - ,"recipientId" INTEGER NOT NULL - ,PRIMARY KEY ( - "threadId" - ,"recipientId" - ) - ,FOREIGN KEY ("threadId") REFERENCES "model_TSThread"("id" - ) - ON DELETE - CASCADE - ON UPDATE - CASCADE - ,FOREIGN KEY ("recipientId") REFERENCES "model_SignalRecipient"("id" -) - ON DELETE - CASCADE - ON UPDATE - CASCADE -) WITHOUT ROWID -; - -CREATE - INDEX "StoryRecipient_on_recipientId" - ON "StoryRecipient"("recipientId" -) -; - -CREATE - TABLE - IF NOT EXISTS "BackupAttachmentUploadQueue" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT - ,"attachmentRowId" INTEGER NOT NULL REFERENCES "Attachment"("id" - ) - ON DELETE - CASCADE - ,"maxOwnerTimestamp" INTEGER - ,"estimatedByteCount" INTEGER NOT NULL - ,"isFullsize" BOOLEAN NOT NULL - ,"numRetries" INTEGER NOT NULL DEFAULT 0 - ,"minRetryTimestamp" INTEGER NOT NULL DEFAULT 0 - ,"state" INTEGER DEFAULT 0 -) -; - -CREATE - INDEX "index_BackupAttachmentUploadQueue_on_attachmentRowId" - ON "BackupAttachmentUploadQueue"("attachmentRowId" -) -; - -CREATE - TABLE - IF NOT EXISTS "BackupAttachmentDownloadQueue" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT - ,"attachmentRowId" INTEGER NOT NULL REFERENCES "Attachment"("id" - ) - ON DELETE - CASCADE - ,"isThumbnail" BOOLEAN NOT NULL - ,"maxOwnerTimestamp" INTEGER - ,"canDownloadFromMediaTier" BOOLEAN NOT NULL - ,"minRetryTimestamp" INTEGER NOT NULL - ,"numRetries" INTEGER NOT NULL DEFAULT 0 - ,"state" INTEGER - ,"estimatedByteCount" INTEGER NOT NULL -) -; - -CREATE - INDEX "index_BackupAttachmentDownloadQueue_on_attachmentRowId" - ON "BackupAttachmentDownloadQueue"("attachmentRowId" -) -; - -CREATE - INDEX "index_BackupAttachmentDownloadQueue_on_state_isThumbnail_minRetryTimestamp" - ON "BackupAttachmentDownloadQueue"("state" - ,"isThumbnail" - ,"minRetryTimestamp" -) -; - -CREATE - INDEX "index_BackupAttachmentDownloadQueue_on_isThumbnail_canDownloadFromMediaTier_state_maxOwnerTimestamp" - ON "BackupAttachmentDownloadQueue"("isThumbnail" - ,"canDownloadFromMediaTier" - ,"state" - ,"maxOwnerTimestamp" -) -; - -CREATE - INDEX "index_BackupAttachmentDownloadQueue_on_state_estimatedByteCount" - ON "BackupAttachmentDownloadQueue"("state" - ,"estimatedByteCount" -) -; - -CREATE - TABLE - IF NOT EXISTS "ListedBackupMediaObject" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT - ,"mediaId" BLOB NOT NULL - ,"cdnNumber" INTEGER NOT NULL - ,"objectLength" INTEGER NOT NULL - ) -; - -CREATE - INDEX "index_ListedBackupMediaObject_on_mediaId" - ON "ListedBackupMediaObject"("mediaId" -) -; - -CREATE - TABLE - IF NOT EXISTS "BackupOversizeTextCache" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT - ,"attachmentRowId" INTEGER NOT NULL UNIQUE REFERENCES "Attachment"("id" - ) - ON DELETE - CASCADE - ,"text" TEXT NOT NULL CHECK ( - LENGTH( "text" ) <= 131072 - ) -) -; - -CREATE - TABLE - IF NOT EXISTS "OWSDevice" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT - ,"deviceId" INTEGER NOT NULL - ,"createdAt" DOUBLE NOT NULL - ,"lastSeenAt" DOUBLE NOT NULL - ,"name" TEXT - ) -; - -CREATE - TABLE - IF NOT EXISTS "Poll" ( - "id" INTEGER PRIMARY KEY NOT NULL - ,"interactionId" INTEGER NOT NULL REFERENCES "model_TSInteraction"("id" - ) - ON DELETE - CASCADE - ON UPDATE - CASCADE - ,"isEnded" BOOLEAN - ,"allowsMultiSelect" BOOLEAN -) -; - -CREATE - TABLE - IF NOT EXISTS "PollOption" ( - "id" INTEGER PRIMARY KEY NOT NULL - ,"pollId" INTEGER NOT NULL REFERENCES "Poll"("id" - ) - ON DELETE - CASCADE - ON UPDATE - CASCADE - ,"option" TEXT - ,"optionIndex" INTEGER -) -; - -CREATE - INDEX "index_poll_on_interactionId" - ON "Poll"("interactionId" -) -; - -CREATE - INDEX "index_polloption_on_pollId" - ON "PollOption"("pollId" -) -; - -CREATE - INDEX "index_BackupAttachmentUploadQueue_on_state_isFullsize_maxOwnerTimestamp" - ON "BackupAttachmentUploadQueue"("state" - ,"isFullsize" - ,"maxOwnerTimestamp" -) -; - -CREATE - TRIGGER __BackupAttachmentUploadQueue_au AFTER UPDATE - OF state - ON BackupAttachmentUploadQueue BEGIN DELETE - FROM - BackupAttachmentUploadQueue - WHERE - state = 1 - AND NOT EXISTS ( - SELECT - id - FROM - BackupAttachmentUploadQueue - WHERE - state = 0 - ) -; - -END -; - -CREATE - TABLE - IF NOT EXISTS "PollVote" ( - "id" INTEGER PRIMARY KEY NOT NULL - ,"optionId" INTEGER NOT NULL REFERENCES "PollOption"("id" - ) - ON DELETE - CASCADE - ON UPDATE - CASCADE - ,"voteAuthorId" INTEGER REFERENCES "model_SignalRecipient"("id" -) - ON DELETE - CASCADE - ON UPDATE - CASCADE - ,"voteCount" INTEGER - ,"voteState" INTEGER DEFAULT 0 -) -; - -CREATE - UNIQUE INDEX "index_pollVote_on_voteAuthorId_and_optionId_voteCount" - ON "PollVote"("voteAuthorId" - ,"optionId" - ,"voteCount" -) -; - -CREATE - TABLE - IF NOT EXISTS "PreKey" ( - "rowId" INTEGER PRIMARY KEY NOT NULL - ,"identity" INTEGER NOT NULL - ,"namespace" INTEGER NOT NULL - ,"keyId" INTEGER NOT NULL - ,"isOneTime" BOOLEAN NOT NULL - ,"replacedAt" DATE - ,"serializedRecord" BLOB - ,CHECK ( - CASE - "namespace" WHEN 0 - THEN "isOneTime" = 1 WHEN 2 - THEN "isOneTime" = 0 - ELSE TRUE - END - ) - ) -; - -CREATE - UNIQUE INDEX "PreKey_Unique" - ON "PreKey"("identity" - ,"namespace" - ,"keyId" -) -; - -CREATE - INDEX "PreKey_Obsolete" - ON "PreKey"("replacedAt" -) -WHERE -"replacedAt" IS NOT NULL -; - -CREATE - INDEX "PreKey_Replaced" - ON "PreKey"("identity" - ,"namespace" - ,"isOneTime" - ,"keyId" -) -WHERE -"replacedAt" IS NULL -; - -CREATE - TABLE - IF NOT EXISTS "KyberPreKeyUse" ( - "kyberRowId" INTEGER NOT NULL REFERENCES "PreKey"("rowId" - ) - ON DELETE - CASCADE - ON UPDATE - CASCADE - ,"signedPreKeyIdentity" INTEGER NOT NULL - ,"signedPreKeyId" INTEGER NOT NULL - ,"baseKey" BLOB NOT NULL - ,PRIMARY KEY ( - "kyberRowId" - ,"signedPreKeyIdentity" - ,"signedPreKeyId" - ,"baseKey" - ) -) WITHOUT ROWID -; - -CREATE - UNIQUE INDEX "UsernameLookupRecord_UniqueUsernames" - ON "UsernameLookupRecord"("username" COLLATE NOCASE -) -; - -CREATE - TABLE - IF NOT EXISTS "PinnedMessage" ( - "id" INTEGER PRIMARY KEY NOT NULL - ,"interactionId" INTEGER NOT NULL UNIQUE REFERENCES "model_TSInteraction"("id" - ) - ON DELETE - CASCADE - ON UPDATE - CASCADE - ,"threadId" INTEGER NOT NULL REFERENCES "model_TSThread"("id" -) - ON DELETE - CASCADE - ON UPDATE - CASCADE - ,"expiresAt" INTEGER - ,"sentTimestamp" INTEGER NOT NULL DEFAULT 0 - ,"receivedTimestamp" INTEGER NOT NULL DEFAULT 0 -) -; - -CREATE - INDEX "index_PinnedMessage_on_threadId" - ON "PinnedMessage"("threadId" -) -; - -CREATE - INDEX "partial_index_PinnedMessage_on_expiresAt" - ON "PinnedMessage"("expiresAt" -) -WHERE -"expiresAt" IS NOT NULL -; diff --git a/SignalServiceKit/Storage/Database/DatabaseRecovery.swift b/SignalServiceKit/Storage/Database/DatabaseRecovery.swift index c4b7e5a1d2..fca27e8b22 100644 --- a/SignalServiceKit/Storage/Database/DatabaseRecovery.swift +++ b/SignalServiceKit/Storage/Database/DatabaseRecovery.swift @@ -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[.. 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: - diff --git a/SignalServiceKit/Storage/Database/InMemoryDB.swift b/SignalServiceKit/Storage/Database/InMemoryDB.swift index 72bba99b16..be7cc71de3 100644 --- a/SignalServiceKit/Storage/Database/InMemoryDB.swift +++ b/SignalServiceKit/Storage/Database/InMemoryDB.swift @@ -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( diff --git a/SignalServiceKit/Storage/Database/SDSDatabaseStorage/SDSDatabaseStorage.swift b/SignalServiceKit/Storage/Database/SDSDatabaseStorage/SDSDatabaseStorage.swift index 6292162e61..45ca638188 100644 --- a/SignalServiceKit/Storage/Database/SDSDatabaseStorage/SDSDatabaseStorage.swift +++ b/SignalServiceKit/Storage/Database/SDSDatabaseStorage/SDSDatabaseStorage.swift @@ -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, diff --git a/SignalServiceKit/TestUtils/MockKeychainStorage.swift b/SignalServiceKit/TestUtils/MockKeychainStorage.swift index c1aa6101e2..8e25479ff4 100644 --- a/SignalServiceKit/TestUtils/MockKeychainStorage.swift +++ b/SignalServiceKit/TestUtils/MockKeychainStorage.swift @@ -7,7 +7,7 @@ import Foundation #if TESTABLE_BUILD -public class MockKeychainStorage: KeychainStorage { +public final class MockKeychainStorage: KeychainStorage { private let values = AtomicDictionary([:], 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 diff --git a/SignalServiceKit/TestUtils/MockSSKEnvironment.swift b/SignalServiceKit/TestUtils/MockSSKEnvironment.swift index 04c52302ef..887e2160fe 100644 --- a/SignalServiceKit/TestUtils/MockSSKEnvironment.swift +++ b/SignalServiceKit/TestUtils/MockSSKEnvironment.swift @@ -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 diff --git a/SignalServiceKit/tests/Storage/Database/DatabaseRecoveryTest.swift b/SignalServiceKit/tests/Storage/Database/DatabaseRecoveryTest.swift index d09751ac0d..c8b7570e1c 100644 --- a/SignalServiceKit/tests/Storage/Database/DatabaseRecoveryTest.swift +++ b/SignalServiceKit/tests/Storage/Database/DatabaseRecoveryTest.swift @@ -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 = 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 = 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 { - let db = transaction.database + func allNormalTableNames(tx: DBReadTransaction) throws -> Set { + 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 = ["grdb_migrations", "sqlite_sequence"] diff --git a/SignalServiceKit/tests/Storage/Database/GRDBSchemaMigratorTest.swift b/SignalServiceKit/tests/Storage/Database/GRDBSchemaMigratorTest.swift index 222777c3dd..b93d6cf196 100644 --- a/SignalServiceKit/tests/Storage/Database/GRDBSchemaMigratorTest.swift +++ b/SignalServiceKit/tests/Storage/Database/GRDBSchemaMigratorTest.swift @@ -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 -; -"""