diff --git a/ts/sql/migrations/1000-mark-unread-call-history-messages-as-unseen.ts b/ts/sql/migrations/1000-mark-unread-call-history-messages-as-unseen.ts index a5a40dc1145..8bb371f5be3 100644 --- a/ts/sql/migrations/1000-mark-unread-call-history-messages-as-unseen.ts +++ b/ts/sql/migrations/1000-mark-unread-call-history-messages-as-unseen.ts @@ -3,58 +3,41 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; import { ReadStatus } from '../../messages/MessageReadStatus'; import { SeenStatus } from '../../MessageSeenStatus'; import { strictAssert } from '../../util/assert'; import { sql, sqlConstant } from '../util'; -export const version = 1000; - const READ_STATUS_UNREAD = sqlConstant(ReadStatus.Unread); const READ_STATUS_READ = sqlConstant(ReadStatus.Read); const SEEN_STATUS_UNSEEN = sqlConstant(SeenStatus.Unseen); -export function updateToSchemaVersion1000( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1000) { - return; - } +export default function updateToSchemaVersion1000(db: Database): void { + const [selectQuery] = sql` + SELECT id + FROM messages + WHERE messages.type = 'call-history' + AND messages.readStatus IS ${READ_STATUS_UNREAD} + `; - db.transaction(() => { - const [selectQuery] = sql` - SELECT id - FROM messages - WHERE messages.type = 'call-history' - AND messages.readStatus IS ${READ_STATUS_UNREAD} + const rows = db.prepare(selectQuery).all(); + + for (const row of rows) { + const { id } = row; + strictAssert(id != null, 'message id must exist'); + + const [updateQuery, updateParams] = sql` + UPDATE messages + SET + json = JSON_PATCH(json, ${JSON.stringify({ + readStatus: ReadStatus.Read, + seenStatus: SeenStatus.Unseen, + })}), + readStatus = ${READ_STATUS_READ}, + seenStatus = ${SEEN_STATUS_UNSEEN} + WHERE id = ${id} `; - const rows = db.prepare(selectQuery).all(); - - for (const row of rows) { - const { id } = row; - strictAssert(id != null, 'message id must exist'); - - const [updateQuery, updateParams] = sql` - UPDATE messages - SET - json = JSON_PATCH(json, ${JSON.stringify({ - readStatus: ReadStatus.Read, - seenStatus: SeenStatus.Unseen, - })}), - readStatus = ${READ_STATUS_READ}, - seenStatus = ${SEEN_STATUS_UNSEEN} - WHERE id = ${id} - `; - - db.prepare(updateQuery).run(updateParams); - } - - db.pragma('user_version = 1000'); - })(); - - logger.info('updateToSchemaVersion1000: success!'); + db.prepare(updateQuery).run(updateParams); + } } diff --git a/ts/sql/migrations/1010-call-links-table.ts b/ts/sql/migrations/1010-call-links-table.ts index 07dc73a1f43..8e2645e400a 100644 --- a/ts/sql/migrations/1010-call-links-table.ts +++ b/ts/sql/migrations/1010-call-links-table.ts @@ -3,38 +3,21 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; import { sql } from '../util'; -export const version = 1010; +export default function updateToSchemaVersion1010(db: Database): void { + const [createTable] = sql` + CREATE TABLE callLinks ( + roomId TEXT NOT NULL PRIMARY KEY, + rootKey BLOB NOT NULL, + adminKey BLOB, + name TEXT NOT NULL, + -- Enum which stores CallLinkRestrictions from ringrtc + restrictions INTEGER NOT NULL, + revoked INTEGER NOT NULL, + expiration INTEGER + ) STRICT; + `; -export function updateToSchemaVersion1010( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1010) { - return; - } - - db.transaction(() => { - const [createTable] = sql` - CREATE TABLE callLinks ( - roomId TEXT NOT NULL PRIMARY KEY, - rootKey BLOB NOT NULL, - adminKey BLOB, - name TEXT NOT NULL, - -- Enum which stores CallLinkRestrictions from ringrtc - restrictions INTEGER NOT NULL, - revoked INTEGER NOT NULL, - expiration INTEGER - ) STRICT; - `; - - db.exec(createTable); - - db.pragma('user_version = 1010'); - })(); - - logger.info('updateToSchemaVersion1010: success!'); + db.exec(createTable); } diff --git a/ts/sql/migrations/1020-self-merges.ts b/ts/sql/migrations/1020-self-merges.ts index eacb8b6ddd0..a1b3efb9e83 100644 --- a/ts/sql/migrations/1020-self-merges.ts +++ b/ts/sql/migrations/1020-self-merges.ts @@ -6,54 +6,39 @@ import { sql } from '../util'; import type { WritableDB } from '../Interface'; import { getOurUuid } from './41-uuid-keys'; -export const version = 1020; - -export function updateToSchemaVersion1020( - currentVersion: number, +export default function updateToSchemaVersion1020( db: WritableDB, logger: LoggerType ): void { - if (currentVersion >= 1020) { + const ourAci = getOurUuid(db); + + if (ourAci == null) { + logger.info('not linked'); return; } - db.transaction(() => { - const ourAci = getOurUuid(db); + const [selectQuery, selectParams] = sql` + SELECT id FROM conversations + WHERE serviceId IS ${ourAci} + `; + const ourConversationId = db + .prepare(selectQuery, { + pluck: true, + }) + .get(selectParams); + if (ourConversationId == null) { + logger.error('no conversation'); + return; + } - if (ourAci == null) { - logger.info('updateToSchemaVersion1020: not linked'); - db.pragma('user_version = 1020'); - return; - } - - const [selectQuery, selectParams] = sql` - SELECT id FROM conversations - WHERE serviceId IS ${ourAci} - `; - const ourConversationId = db - .prepare(selectQuery, { - pluck: true, - }) - .get(selectParams); - if (ourConversationId == null) { - logger.error('updateToSchemaVersion1020: no conversation'); - db.pragma('user_version = 1020'); - return; - } - - const [deleteQuery, deleteParams] = sql` - DELETE FROM messages - WHERE - conversationId IS ${ourConversationId} AND - type IS 'conversation-merge' - `; - const { changes } = db.prepare(deleteQuery).run(deleteParams); - if (changes !== 0) { - logger.warn(`updateToSchemaVersion1020: removed ${changes} self merges`); - } - - db.pragma('user_version = 1020'); - })(); - - logger.info('updateToSchemaVersion1020: success!'); + const [deleteQuery, deleteParams] = sql` + DELETE FROM messages + WHERE + conversationId IS ${ourConversationId} AND + type IS 'conversation-merge' + `; + const { changes } = db.prepare(deleteQuery).run(deleteParams); + if (changes !== 0) { + logger.warn(`removed ${changes} self merges`); + } } diff --git a/ts/sql/migrations/1030-unblock-event.ts b/ts/sql/migrations/1030-unblock-event.ts index 6e85ce5e766..1fe1f5c3db8 100644 --- a/ts/sql/migrations/1030-unblock-event.ts +++ b/ts/sql/migrations/1030-unblock-event.ts @@ -3,85 +3,68 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; import { sql, sqlFragment } from '../util'; -export const version = 1030; +export default function updateToSchemaVersion1030(db: Database): void { + // From migration 81 + const shouldAffectActivityOrPreview = sqlFragment` + type IS NULL + OR + type NOT IN ( + 'change-number-notification', + 'contact-removed-notification', + 'conversation-merge', + 'group-v1-migration', + 'keychange', + 'message-history-unsynced', + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change' + ) + AND NOT ( + type IS 'message-request-response-event' + AND json_extract(json, '$.messageRequestResponseEvent') IN ('ACCEPT', 'BLOCK', 'UNBLOCK') + ) + `; -export function updateToSchemaVersion1030( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1030) { - return; - } + const [updateShouldAffectPreview] = sql` + --- These will be re-added below + DROP INDEX messages_preview; + DROP INDEX messages_preview_without_story; + DROP INDEX messages_activity; + DROP INDEX message_user_initiated; - db.transaction(() => { - // From migration 81 - const shouldAffectActivityOrPreview = sqlFragment` - type IS NULL - OR - type NOT IN ( - 'change-number-notification', - 'contact-removed-notification', - 'conversation-merge', - 'group-v1-migration', - 'keychange', - 'message-history-unsynced', - 'profile-change', - 'story', - 'universal-timer-notification', - 'verified-change' - ) - AND NOT ( - type IS 'message-request-response-event' - AND json_extract(json, '$.messageRequestResponseEvent') IN ('ACCEPT', 'BLOCK', 'UNBLOCK') - ) - `; + --- These will also be re-added below + ALTER TABLE messages DROP COLUMN shouldAffectActivity; + ALTER TABLE messages DROP COLUMN shouldAffectPreview; - const [updateShouldAffectPreview] = sql` - --- These will be re-added below - DROP INDEX messages_preview; - DROP INDEX messages_preview_without_story; - DROP INDEX messages_activity; - DROP INDEX message_user_initiated; + --- (change: added message-request-response-event->ACCEPT/BLOCK/UNBLOCK) + ALTER TABLE messages + ADD COLUMN shouldAffectPreview INTEGER + GENERATED ALWAYS AS (${shouldAffectActivityOrPreview}); + ALTER TABLE messages + ADD COLUMN shouldAffectActivity INTEGER + GENERATED ALWAYS AS (${shouldAffectActivityOrPreview}); - --- These will also be re-added below - ALTER TABLE messages DROP COLUMN shouldAffectActivity; - ALTER TABLE messages DROP COLUMN shouldAffectPreview; + --- From migration 88 + CREATE INDEX messages_preview ON messages + (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, + received_at, sent_at); - --- (change: added message-request-response-event->ACCEPT/BLOCK/UNBLOCK) - ALTER TABLE messages - ADD COLUMN shouldAffectPreview INTEGER - GENERATED ALWAYS AS (${shouldAffectActivityOrPreview}); - ALTER TABLE messages - ADD COLUMN shouldAffectActivity INTEGER - GENERATED ALWAYS AS (${shouldAffectActivityOrPreview}); + --- From migration 88 + CREATE INDEX messages_preview_without_story ON messages + (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, + received_at, sent_at) WHERE storyId IS NULL; - --- From migration 88 - CREATE INDEX messages_preview ON messages - (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, - received_at, sent_at); + --- From migration 88 + CREATE INDEX messages_activity ON messages + (conversationId, shouldAffectActivity, isTimerChangeFromSync, + isGroupLeaveEventFromOther, received_at, sent_at); - --- From migration 88 - CREATE INDEX messages_preview_without_story ON messages - (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, - received_at, sent_at) WHERE storyId IS NULL; + --- From migration 81 + CREATE INDEX message_user_initiated ON messages (conversationId, isUserInitiatedMessage); + `; - --- From migration 88 - CREATE INDEX messages_activity ON messages - (conversationId, shouldAffectActivity, isTimerChangeFromSync, - isGroupLeaveEventFromOther, received_at, sent_at); - - --- From migration 81 - CREATE INDEX message_user_initiated ON messages (conversationId, isUserInitiatedMessage); - `; - - db.exec(updateShouldAffectPreview); - - db.pragma('user_version = 1030'); - })(); - - logger.info('updateToSchemaVersion1030: success!'); + db.exec(updateShouldAffectPreview); } diff --git a/ts/sql/migrations/1040-undownloaded-backed-up-media.ts b/ts/sql/migrations/1040-undownloaded-backed-up-media.ts index c1dae770872..3d491eccc39 100644 --- a/ts/sql/migrations/1040-undownloaded-backed-up-media.ts +++ b/ts/sql/migrations/1040-undownloaded-backed-up-media.ts @@ -20,8 +20,6 @@ import { type JobManagerJobType, } from '../../jobs/JobManager'; -export const version = 1040; - export type _AttachmentDownloadJobTypeV1030 = { attachment: AttachmentType; attempts: number; @@ -56,192 +54,171 @@ export type _AttachmentDownloadJobTypeV1040 = Omit< 'attachmentSignature' | 'originalSource' > & { digest: string }; -export function updateToSchemaVersion1040( - currentVersion: number, +export default function updateToSchemaVersion1040( db: Database, logger: LoggerType ): void { - if (currentVersion >= 1040) { - return; + // 1. Load all existing rows into memory (shouldn't be many) + const existingJobs: Array<{ + id: string | null; + timestamp: number | null; + pending: number | null; + json: string | null; + }> = db + .prepare( + ` + SELECT id, timestamp, pending, json from attachment_downloads + ` + ) + .all(); + logger.info(`loaded ${existingJobs.length} existing jobs`); + + // 2. Create new temp table, with a couple new columns and stricter typing + db.exec(` + CREATE TABLE tmp_attachment_downloads ( + messageId TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE, + attachmentType TEXT NOT NULL, + digest TEXT NOT NULL, + receivedAt INTEGER NOT NULL, + sentAt INTEGER NOT NULL, + contentType TEXT NOT NULL, + size INTEGER NOT NULL, + attachmentJson TEXT NOT NULL, + active INTEGER NOT NULL, + attempts INTEGER NOT NULL, + retryAfter INTEGER, + lastAttemptTimestamp INTEGER, + + PRIMARY KEY (messageId, attachmentType, digest) + ) STRICT; + `); + + // 3. Drop existing table + db.exec('DROP TABLE attachment_downloads;'); + + // 4. Rename temp table + db.exec( + 'ALTER TABLE tmp_attachment_downloads RENAME TO attachment_downloads;' + ); + + // 5. Add new index on active & receivedAt. For most queries when there are lots of + // jobs (like during backup restore), many jobs will match the the WHERE clause, so + // the ORDER BY on receivedAt is probably the most expensive part. + db.exec(` + CREATE INDEX attachment_downloads_active_receivedAt + ON attachment_downloads ( + active, receivedAt + ); + `); + + // 6. Add new index on active & messageId. In order to prioritize visible messages, + // we'll also query for rows with a matching messageId. For these, the messageId + // matching is likely going to be the most expensive part. + db.exec(` + CREATE INDEX attachment_downloads_active_messageId + ON attachment_downloads ( + active, messageId + ); + `); + + // 7. Add new index just on messageId, for the ON DELETE CASCADE foreign key + // constraint + db.exec(` + CREATE INDEX attachment_downloads_messageId + ON attachment_downloads ( + messageId + ); + `); + + // 8. Rewrite old rows to match new schema + const rowsToTransfer: Array< + _AttachmentDownloadJobTypeV1040 & JobManagerJobType + > = []; + + for (const existingJob of existingJobs) { + try { + // Type this as partial in case there is missing data + const existingJobData: Partial<_AttachmentDownloadJobTypeV1030> = + jsonToObject(existingJob.json ?? ''); + + const updatedJob: Partial<_AttachmentDownloadJobTypeV1040> = { + messageId: existingJobData.messageId, + attachmentType: existingJobData.type, + attachment: existingJobData.attachment, + // The existing timestamp column works reasonably well in place of + // actually retrieving the message's receivedAt + receivedAt: existingJobData.timestamp ?? Date.now(), + sentAt: existingJobData.timestamp ?? Date.now(), + digest: existingJobData.attachment?.digest, + contentType: existingJobData.attachment?.contentType, + size: existingJobData.attachment?.size, + active: false, // all jobs are inactive on app start + attempts: existingJobData.attempts ?? 0, + retryAfter: null, + lastAttemptTimestamp: null, + // adding due to changes in the schema + source: AttachmentDownloadSource.STANDARD, + ciphertextSize: 0, + }; + + const parsed = parsePartial(attachmentDownloadJobSchemaV1040, updatedJob); + + rowsToTransfer.push(parsed); + } catch { + logger.warn( + `unable to transfer job ${existingJob.id} to new table; invalid data` + ); + } } - db.transaction(() => { - // 1. Load all existing rows into memory (shouldn't be many) - const existingJobs: Array<{ - id: string | null; - timestamp: number | null; - pending: number | null; - json: string | null; - }> = db - .prepare( - ` - SELECT id, timestamp, pending, json from attachment_downloads - ` - ) - .all(); - logger.info( - `updateToSchemaVersion1040: loaded ${existingJobs.length} existing jobs` - ); - - // 2. Create new temp table, with a couple new columns and stricter typing - db.exec(` - CREATE TABLE tmp_attachment_downloads ( - messageId TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE, - attachmentType TEXT NOT NULL, - digest TEXT NOT NULL, - receivedAt INTEGER NOT NULL, - sentAt INTEGER NOT NULL, - contentType TEXT NOT NULL, - size INTEGER NOT NULL, - attachmentJson TEXT NOT NULL, - active INTEGER NOT NULL, - attempts INTEGER NOT NULL, - retryAfter INTEGER, - lastAttemptTimestamp INTEGER, - - PRIMARY KEY (messageId, attachmentType, digest) - ) STRICT; - `); - - // 3. Drop existing table - db.exec('DROP TABLE attachment_downloads;'); - - // 4. Rename temp table - db.exec( - 'ALTER TABLE tmp_attachment_downloads RENAME TO attachment_downloads;' - ); - - // 5. Add new index on active & receivedAt. For most queries when there are lots of - // jobs (like during backup restore), many jobs will match the the WHERE clause, so - // the ORDER BY on receivedAt is probably the most expensive part. - db.exec(` - CREATE INDEX attachment_downloads_active_receivedAt - ON attachment_downloads ( - active, receivedAt - ); - `); - - // 6. Add new index on active & messageId. In order to prioritize visible messages, - // we'll also query for rows with a matching messageId. For these, the messageId - // matching is likely going to be the most expensive part. - db.exec(` - CREATE INDEX attachment_downloads_active_messageId - ON attachment_downloads ( - active, messageId - ); - `); - - // 7. Add new index just on messageId, for the ON DELETE CASCADE foreign key - // constraint - db.exec(` - CREATE INDEX attachment_downloads_messageId - ON attachment_downloads ( - messageId - ); - `); - - // 8. Rewrite old rows to match new schema - const rowsToTransfer: Array< - _AttachmentDownloadJobTypeV1040 & JobManagerJobType - > = []; - - for (const existingJob of existingJobs) { - try { - // Type this as partial in case there is missing data - const existingJobData: Partial<_AttachmentDownloadJobTypeV1030> = - jsonToObject(existingJob.json ?? ''); - - const updatedJob: Partial<_AttachmentDownloadJobTypeV1040> = { - messageId: existingJobData.messageId, - attachmentType: existingJobData.type, - attachment: existingJobData.attachment, - // The existing timestamp column works reasonably well in place of - // actually retrieving the message's receivedAt - receivedAt: existingJobData.timestamp ?? Date.now(), - sentAt: existingJobData.timestamp ?? Date.now(), - digest: existingJobData.attachment?.digest, - contentType: existingJobData.attachment?.contentType, - size: existingJobData.attachment?.size, - active: false, // all jobs are inactive on app start - attempts: existingJobData.attempts ?? 0, - retryAfter: null, - lastAttemptTimestamp: null, - // adding due to changes in the schema - source: AttachmentDownloadSource.STANDARD, - ciphertextSize: 0, - }; - - const parsed = parsePartial( - attachmentDownloadJobSchemaV1040, - updatedJob - ); - - rowsToTransfer.push(parsed); - } catch { - logger.warn( - `updateToSchemaVersion1040: unable to transfer job ${existingJob.id} to new table; invalid data` - ); - } - } - - let numTransferred = 0; - if (rowsToTransfer.length) { - logger.info( - `updateToSchemaVersion1040: transferring ${rowsToTransfer.length} rows` - ); - for (const row of rowsToTransfer) { - const [insertQuery, insertParams] = sql` - INSERT INTO attachment_downloads - ( - messageId, - attachmentType, - receivedAt, - sentAt, - digest, - contentType, - size, - attachmentJson, - active, - attempts, - retryAfter, - lastAttemptTimestamp - ) - VALUES - ( - ${row.messageId}, - ${row.attachmentType}, - ${row.receivedAt}, - ${row.sentAt}, - ${row.digest}, - ${row.contentType}, - ${row.size}, - ${objectToJSON(row.attachment)}, - ${row.active ? 1 : 0}, - ${row.attempts}, - ${row.retryAfter}, - ${row.lastAttemptTimestamp} - ); - `; - try { - db.prepare(insertQuery).run(insertParams); - numTransferred += 1; - } catch (error) { - logger.error( - 'updateToSchemaVersion1040: error when transferring row', - error + let numTransferred = 0; + if (rowsToTransfer.length) { + logger.info(`transferring ${rowsToTransfer.length} rows`); + for (const row of rowsToTransfer) { + const [insertQuery, insertParams] = sql` + INSERT INTO attachment_downloads + ( + messageId, + attachmentType, + receivedAt, + sentAt, + digest, + contentType, + size, + attachmentJson, + active, + attempts, + retryAfter, + lastAttemptTimestamp + ) + VALUES + ( + ${row.messageId}, + ${row.attachmentType}, + ${row.receivedAt}, + ${row.sentAt}, + ${row.digest}, + ${row.contentType}, + ${row.size}, + ${objectToJSON(row.attachment)}, + ${row.active ? 1 : 0}, + ${row.attempts}, + ${row.retryAfter}, + ${row.lastAttemptTimestamp} ); - } + `; + try { + db.prepare(insertQuery).run(insertParams); + numTransferred += 1; + } catch (error) { + logger.error('error when transferring row', error); } } + } - logger.info( - `updateToSchemaVersion1040: transferred ${numTransferred} rows, removed ${ - existingJobs.length - numTransferred - }` - ); - - db.pragma('user_version = 1040'); - })(); - - logger.info('updateToSchemaVersion1040: success!'); + logger.info( + `transferred ${numTransferred} rows, removed ${ + existingJobs.length - numTransferred + }` + ); } diff --git a/ts/sql/migrations/1050-group-send-endorsements.ts b/ts/sql/migrations/1050-group-send-endorsements.ts index 6f06e4dc768..1962e6807d8 100644 --- a/ts/sql/migrations/1050-group-send-endorsements.ts +++ b/ts/sql/migrations/1050-group-send-endorsements.ts @@ -3,48 +3,31 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; import { sql } from '../util'; -export const version = 1050; +export default function updateToSchemaVersion1050(db: Database): void { + const [createTables] = sql` + DROP TABLE IF EXISTS groupSendCombinedEndorsement; + DROP TABLE IF EXISTS groupSendMemberEndorsement; -export function updateToSchemaVersion1050( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1050) { - return; - } + -- From GroupSendEndorsementsResponse->ReceivedEndorsements in libsignal + -- this is the combined endorsement for all group members + CREATE TABLE groupSendCombinedEndorsement ( + groupId TEXT NOT NULL PRIMARY KEY, -- Only one endorsement per group + expiration INTEGER NOT NULL, -- Unix timestamp in seconds + endorsement BLOB NOT NULL + ) STRICT; - db.transaction(() => { - const [createTables] = sql` - DROP TABLE IF EXISTS groupSendCombinedEndorsement; - DROP TABLE IF EXISTS groupSendMemberEndorsement; + -- From GroupSendEndorsementsResponse->ReceivedEndorsements in libsignal + -- these are the individual endorsements for each group member + CREATE TABLE groupSendMemberEndorsement ( + groupId TEXT NOT NULL, + memberAci TEXT NOT NULL, + expiration INTEGER NOT NULL, -- Unix timestamp in seconds + endorsement BLOB NOT NULL, + PRIMARY KEY (groupId, memberAci) -- Only one endorsement per group member + ) STRICT; + `; - -- From GroupSendEndorsementsResponse->ReceivedEndorsements in libsignal - -- this is the combined endorsement for all group members - CREATE TABLE groupSendCombinedEndorsement ( - groupId TEXT NOT NULL PRIMARY KEY, -- Only one endorsement per group - expiration INTEGER NOT NULL, -- Unix timestamp in seconds - endorsement BLOB NOT NULL - ) STRICT; - - -- From GroupSendEndorsementsResponse->ReceivedEndorsements in libsignal - -- these are the individual endorsements for each group member - CREATE TABLE groupSendMemberEndorsement ( - groupId TEXT NOT NULL, - memberAci TEXT NOT NULL, - expiration INTEGER NOT NULL, -- Unix timestamp in seconds - endorsement BLOB NOT NULL, - PRIMARY KEY (groupId, memberAci) -- Only one endorsement per group member - ) STRICT; - `; - - db.exec(createTables); - - db.pragma('user_version = 1050'); - })(); - - logger.info('updateToSchemaVersion1050: success!'); + db.exec(createTables); } diff --git a/ts/sql/migrations/1060-addressable-messages-and-sync-tasks.ts b/ts/sql/migrations/1060-addressable-messages-and-sync-tasks.ts index 4c8ae15f64b..880b4248dea 100644 --- a/ts/sql/migrations/1060-addressable-messages-and-sync-tasks.ts +++ b/ts/sql/migrations/1060-addressable-messages-and-sync-tasks.ts @@ -3,54 +3,36 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; - -export const version = 1060; - -export function updateToSchemaVersion1060( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1060) { - return; - } - - db.transaction(() => { - db.exec(` - ALTER TABLE messages - ADD COLUMN isAddressableMessage INTEGER - GENERATED ALWAYS AS ( - type IS NULL - OR - type IN ( - 'incoming', - 'outgoing' - ) - ); - - CREATE INDEX messages_by_date_addressable - ON messages ( - conversationId, isAddressableMessage, received_at, sent_at +export default function updateToSchemaVersion1060(db: Database): void { + db.exec(` + ALTER TABLE messages + ADD COLUMN isAddressableMessage INTEGER + GENERATED ALWAYS AS ( + type IS NULL + OR + type IN ( + 'incoming', + 'outgoing' + ) ); - CREATE TABLE syncTasks( - id TEXT PRIMARY KEY NOT NULL, - attempts INTEGER NOT NULL, - createdAt INTEGER NOT NULL, - data TEXT NOT NULL, - envelopeId TEXT NOT NULL, - sentAt INTEGER NOT NULL, - type TEXT NOT NULL - ) STRICT; + CREATE INDEX messages_by_date_addressable + ON messages ( + conversationId, isAddressableMessage, received_at, sent_at + ); - CREATE INDEX syncTasks_order ON syncTasks ( - createdAt, sentAt, id - ) - `); + CREATE TABLE syncTasks( + id TEXT PRIMARY KEY NOT NULL, + attempts INTEGER NOT NULL, + createdAt INTEGER NOT NULL, + data TEXT NOT NULL, + envelopeId TEXT NOT NULL, + sentAt INTEGER NOT NULL, + type TEXT NOT NULL + ) STRICT; - db.pragma('user_version = 1060'); - })(); - - logger.info('updateToSchemaVersion1060: success!'); + CREATE INDEX syncTasks_order ON syncTasks ( + createdAt, sentAt, id + ) + `); } diff --git a/ts/sql/migrations/1070-attachment-backup.ts b/ts/sql/migrations/1070-attachment-backup.ts index d03bf01d791..c8f35fdf619 100644 --- a/ts/sql/migrations/1070-attachment-backup.ts +++ b/ts/sql/migrations/1070-attachment-backup.ts @@ -3,53 +3,35 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion1070(db: Database): void { + db.exec(` + CREATE TABLE attachment_backup_jobs ( + mediaName TEXT NOT NULL PRIMARY KEY, + type TEXT NOT NULL, + data TEXT NOT NULL, + receivedAt INTEGER NOT NULL, -export const version = 1070; + -- job manager fields + attempts INTEGER NOT NULL, + active INTEGER NOT NULL, + retryAfter INTEGER, + lastAttemptTimestamp INTEGER + ) STRICT; -export function updateToSchemaVersion1070( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1070) { - return; - } + CREATE INDEX attachment_backup_jobs_receivedAt + ON attachment_backup_jobs ( + receivedAt + ); - db.transaction(() => { - db.exec(` - CREATE TABLE attachment_backup_jobs ( - mediaName TEXT NOT NULL PRIMARY KEY, - type TEXT NOT NULL, - data TEXT NOT NULL, - receivedAt INTEGER NOT NULL, + CREATE INDEX attachment_backup_jobs_type_receivedAt + ON attachment_backup_jobs ( + type, receivedAt + ); - -- job manager fields - attempts INTEGER NOT NULL, - active INTEGER NOT NULL, - retryAfter INTEGER, - lastAttemptTimestamp INTEGER - ) STRICT; - - CREATE INDEX attachment_backup_jobs_receivedAt - ON attachment_backup_jobs ( - receivedAt - ); - - CREATE INDEX attachment_backup_jobs_type_receivedAt - ON attachment_backup_jobs ( - type, receivedAt - ); - - CREATE TABLE backup_cdn_object_metadata ( - mediaId TEXT NOT NULL PRIMARY KEY, - cdnNumber INTEGER NOT NULL, - sizeOnBackupCdn INTEGER - ) STRICT; - `); - - db.pragma('user_version = 1070'); - })(); - - logger.info('updateToSchemaVersion1070: success!'); + CREATE TABLE backup_cdn_object_metadata ( + mediaId TEXT NOT NULL PRIMARY KEY, + cdnNumber INTEGER NOT NULL, + sizeOnBackupCdn INTEGER + ) STRICT; + `); } diff --git a/ts/sql/migrations/1080-nondisappearing-addressable.ts b/ts/sql/migrations/1080-nondisappearing-addressable.ts index 38957cde806..8cd49e4cad5 100644 --- a/ts/sql/migrations/1080-nondisappearing-addressable.ts +++ b/ts/sql/migrations/1080-nondisappearing-addressable.ts @@ -3,29 +3,11 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; - -export const version = 1080; - -export function updateToSchemaVersion1080( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1080) { - return; - } - - db.transaction(() => { - db.exec(` - CREATE INDEX messages_by_date_addressable_nondisappearing - ON messages ( - conversationId, isAddressableMessage, received_at, sent_at - ) WHERE expireTimer IS NULL; - `); - - db.pragma('user_version = 1080'); - })(); - - logger.info('updateToSchemaVersion1080: success!'); +export default function updateToSchemaVersion1080(db: Database): void { + db.exec(` + CREATE INDEX messages_by_date_addressable_nondisappearing + ON messages ( + conversationId, isAddressableMessage, received_at, sent_at + ) WHERE expireTimer IS NULL; + `); } diff --git a/ts/sql/migrations/1090-message-delete-indexes.ts b/ts/sql/migrations/1090-message-delete-indexes.ts index da85ce564f9..2e7ac9da1c0 100644 --- a/ts/sql/migrations/1090-message-delete-indexes.ts +++ b/ts/sql/migrations/1090-message-delete-indexes.ts @@ -3,30 +3,12 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion1090(db: Database): void { + db.exec(` + CREATE INDEX reactions_messageId + ON reactions (messageId); -export const version = 1090; - -export function updateToSchemaVersion1090( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1090) { - return; - } - - db.transaction(() => { - db.exec(` - CREATE INDEX reactions_messageId - ON reactions (messageId); - - CREATE INDEX storyReads_storyId - ON storyReads (storyId); - `); - - db.pragma('user_version = 1090'); - })(); - - logger.info('updateToSchemaVersion1090: success!'); + CREATE INDEX storyReads_storyId + ON storyReads (storyId); + `); } diff --git a/ts/sql/migrations/1100-optimize-mark-call-history-read-in-conversation.ts b/ts/sql/migrations/1100-optimize-mark-call-history-read-in-conversation.ts index db27f9f9fc8..e9b1545807f 100644 --- a/ts/sql/migrations/1100-optimize-mark-call-history-read-in-conversation.ts +++ b/ts/sql/migrations/1100-optimize-mark-call-history-read-in-conversation.ts @@ -2,62 +2,45 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; import { sql } from '../util'; -export const version = 1100; +export default function updateToSchemaVersion1100(db: Database): void { + const [query] = sql` + -- Fix: Query went from readStatus to seenStatus but index wasn't updated + DROP INDEX IF EXISTS messages_callHistory_readStatus; + DROP INDEX IF EXISTS messages_callHistory_seenStatus; + CREATE INDEX messages_callHistory_seenStatus + ON messages (type, seenStatus) + WHERE type IS 'call-history'; -export function updateToSchemaVersion1100( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1100) { - return; - } + -- Update to index created in 89: add sent_at to make it covering, and where clause to make it smaller + DROP INDEX IF EXISTS messages_call; + CREATE INDEX messages_call ON messages + (type, conversationId, callId, sent_at) + WHERE type IS 'call-history'; - db.transaction(() => { - const [query] = sql` - -- Fix: Query went from readStatus to seenStatus but index wasn't updated - DROP INDEX IF EXISTS messages_callHistory_readStatus; - DROP INDEX IF EXISTS messages_callHistory_seenStatus; - CREATE INDEX messages_callHistory_seenStatus - ON messages (type, seenStatus) - WHERE type IS 'call-history'; + -- Update to index created in 89: add callId and peerId to make it covering + DROP INDEX IF EXISTS callsHistory_order; + CREATE INDEX callsHistory_order ON callsHistory + (timestamp DESC, callId, peerId); - -- Update to index created in 89: add sent_at to make it covering, and where clause to make it smaller - DROP INDEX IF EXISTS messages_call; - CREATE INDEX messages_call ON messages - (type, conversationId, callId, sent_at) - WHERE type IS 'call-history'; + -- Update to index created in 89: add timestamp for querying by order and callId to make it covering + DROP INDEX IF EXISTS callsHistory_byConversation; + DROP INDEX IF EXISTS callsHistory_byConversation_order; + CREATE INDEX callsHistory_byConversation_order ON callsHistory (peerId, timestamp DESC, callId); - -- Update to index created in 89: add callId and peerId to make it covering - DROP INDEX IF EXISTS callsHistory_order; - CREATE INDEX callsHistory_order ON callsHistory - (timestamp DESC, callId, peerId); + -- Optimize markAllCallHistoryRead + DROP INDEX IF EXISTS messages_callHistory_markReadBefore; + CREATE INDEX messages_callHistory_markReadBefore + ON messages (type, seenStatus, sent_at DESC) + WHERE type IS 'call-history'; - -- Update to index created in 89: add timestamp for querying by order and callId to make it covering - DROP INDEX IF EXISTS callsHistory_byConversation; - DROP INDEX IF EXISTS callsHistory_byConversation_order; - CREATE INDEX callsHistory_byConversation_order ON callsHistory (peerId, timestamp DESC, callId); + -- Optimize markAllCallHistoryReadInConversation + DROP INDEX IF EXISTS messages_callHistory_markReadByConversationBefore; + CREATE INDEX messages_callHistory_markReadByConversationBefore + ON messages (type, conversationId, seenStatus, sent_at DESC) + WHERE type IS 'call-history'; + `; - -- Optimize markAllCallHistoryRead - DROP INDEX IF EXISTS messages_callHistory_markReadBefore; - CREATE INDEX messages_callHistory_markReadBefore - ON messages (type, seenStatus, sent_at DESC) - WHERE type IS 'call-history'; - - -- Optimize markAllCallHistoryReadInConversation - DROP INDEX IF EXISTS messages_callHistory_markReadByConversationBefore; - CREATE INDEX messages_callHistory_markReadByConversationBefore - ON messages (type, conversationId, seenStatus, sent_at DESC) - WHERE type IS 'call-history'; - `; - - db.exec(query); - - db.pragma('user_version = 1100'); - })(); - - logger.info('updateToSchemaVersion1100: success!'); + db.exec(query); } diff --git a/ts/sql/migrations/1110-sticker-local-key.ts b/ts/sql/migrations/1110-sticker-local-key.ts index f55cf481aef..ff315d6681f 100644 --- a/ts/sql/migrations/1110-sticker-local-key.ts +++ b/ts/sql/migrations/1110-sticker-local-key.ts @@ -3,33 +3,15 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion1110(db: Database): void { + db.exec(` + ALTER TABLE stickers + ADD COLUMN version INTEGER NOT NULL DEFAULT 1; -export const version = 1110; + ALTER TABLE stickers + ADD COLUMN localKey TEXT; -export function updateToSchemaVersion1110( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1110) { - return; - } - - db.transaction(() => { - db.exec(` - ALTER TABLE stickers - ADD COLUMN version INTEGER NOT NULL DEFAULT 1; - - ALTER TABLE stickers - ADD COLUMN localKey TEXT; - - ALTER TABLE stickers - ADD COLUMN size INTEGER; - `); - - db.pragma('user_version = 1110'); - })(); - - logger.info('updateToSchemaVersion1110: success!'); + ALTER TABLE stickers + ADD COLUMN size INTEGER; + `); } diff --git a/ts/sql/migrations/1120-messages-foreign-keys-indexes.ts b/ts/sql/migrations/1120-messages-foreign-keys-indexes.ts index a6d2d33fe7f..3808f86278d 100644 --- a/ts/sql/migrations/1120-messages-foreign-keys-indexes.ts +++ b/ts/sql/migrations/1120-messages-foreign-keys-indexes.ts @@ -3,31 +3,13 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion1120(db: Database): void { + /** Adds indexes for all tables with foreign key relationships to messages(id) */ + db.exec(` + CREATE INDEX edited_messages_messageId + ON edited_messages(messageId); -export const version = 1120; - -export function updateToSchemaVersion1120( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1120) { - return; - } - - db.transaction(() => { - /** Adds indexes for all tables with foreign key relationships to messages(id) */ - db.exec(` - CREATE INDEX edited_messages_messageId - ON edited_messages(messageId); - - CREATE INDEX mentions_messageId - ON mentions(messageId); - `); - - db.pragma('user_version = 1120'); - })(); - - logger.info('updateToSchemaVersion1120: success!'); + CREATE INDEX mentions_messageId + ON mentions(messageId); + `); } diff --git a/ts/sql/migrations/1130-isStory-index.ts b/ts/sql/migrations/1130-isStory-index.ts index fc900899ab3..6d42f977a23 100644 --- a/ts/sql/migrations/1130-isStory-index.ts +++ b/ts/sql/migrations/1130-isStory-index.ts @@ -3,29 +3,11 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; - -export const version = 1130; - -export function updateToSchemaVersion1130( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1130) { - return; - } - - db.transaction(() => { - // This is to improve the performance of getAllStories - db.exec(` - CREATE INDEX messages_isStory - ON messages(received_at, sent_at) - WHERE isStory = 1; - `); - - db.pragma('user_version = 1130'); - })(); - - logger.info('updateToSchemaVersion1130: success!'); +export default function updateToSchemaVersion1130(db: Database): void { + // This is to improve the performance of getAllStories + db.exec(` + CREATE INDEX messages_isStory + ON messages(received_at, sent_at) + WHERE isStory = 1; + `); } diff --git a/ts/sql/migrations/1140-call-links-deleted-column.ts b/ts/sql/migrations/1140-call-links-deleted-column.ts index 79ecf37e094..53038d829fc 100644 --- a/ts/sql/migrations/1140-call-links-deleted-column.ts +++ b/ts/sql/migrations/1140-call-links-deleted-column.ts @@ -1,31 +1,15 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; -export const version = 1140; +export default function updateToSchemaVersion1140(db: Database): void { + db.exec(` + DROP INDEX IF EXISTS callLinks_deleted; -export function updateToSchemaVersion1140( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1140) { - return; - } + ALTER TABLE callLinks + ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0; - db.transaction(() => { - db.exec(` - DROP INDEX IF EXISTS callLinks_deleted; - - ALTER TABLE callLinks - ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0; - - CREATE INDEX callLinks_deleted - ON callLinks (deleted, roomId); - `); - - db.pragma('user_version = 1140'); - })(); - logger.info('updateToSchemaVersion1140: success!'); + CREATE INDEX callLinks_deleted + ON callLinks (deleted, roomId); + `); } diff --git a/ts/sql/migrations/1150-expire-timer-version.ts b/ts/sql/migrations/1150-expire-timer-version.ts index ec0650d4a7d..528d4b02819 100644 --- a/ts/sql/migrations/1150-expire-timer-version.ts +++ b/ts/sql/migrations/1150-expire-timer-version.ts @@ -1,30 +1,14 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; -export const version = 1150; +export default function updateToSchemaVersion1150(db: Database): void { + db.exec(` + -- All future conversations will start from '1' + ALTER TABLE conversations + ADD COLUMN expireTimerVersion INTEGER NOT NULL DEFAULT 1; -export function updateToSchemaVersion1150( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1150) { - return; - } - - db.transaction(() => { - db.exec(` - -- All future conversations will start from '1' - ALTER TABLE conversations - ADD COLUMN expireTimerVersion INTEGER NOT NULL DEFAULT 1; - - -- All current conversations will start from '2' - UPDATE conversations SET expireTimerVersion = 2; - `); - - db.pragma('user_version = 1150'); - })(); - logger.info('updateToSchemaVersion1150: success!'); + -- All current conversations will start from '2' + UPDATE conversations SET expireTimerVersion = 2; + `); } diff --git a/ts/sql/migrations/1160-optimize-calls-unread-count.ts b/ts/sql/migrations/1160-optimize-calls-unread-count.ts index ec4e04ae6d1..64de29914f7 100644 --- a/ts/sql/migrations/1160-optimize-calls-unread-count.ts +++ b/ts/sql/migrations/1160-optimize-calls-unread-count.ts @@ -1,36 +1,20 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; import { sql, sqlConstant } from '../util'; import { CallDirection, CallStatusValue } from '../../types/CallDisposition'; -export const version = 1160; - const CALL_STATUS_MISSED = sqlConstant(CallStatusValue.Missed); const CALL_DIRECTION_INCOMING = sqlConstant(CallDirection.Incoming); -export function updateToSchemaVersion1160( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1160) { - return; - } +export default function updateToSchemaVersion1160(db: Database): void { + const [query] = sql` + DROP INDEX IF EXISTS callsHistory_incoming_missed; - db.transaction(() => { - const [query] = sql` - DROP INDEX IF EXISTS callsHistory_incoming_missed; - - CREATE INDEX callsHistory_incoming_missed - ON callsHistory (callId, status, direction) - WHERE status IS ${CALL_STATUS_MISSED} - AND direction IS ${CALL_DIRECTION_INCOMING}; - `; - db.exec(query); - - db.pragma('user_version = 1160'); - })(); - logger.info('updateToSchemaVersion1160: success!'); + CREATE INDEX callsHistory_incoming_missed + ON callsHistory (callId, status, direction) + WHERE status IS ${CALL_STATUS_MISSED} + AND direction IS ${CALL_DIRECTION_INCOMING}; + `; + db.exec(query); } diff --git a/ts/sql/migrations/1170-update-call-history-unread-index.ts b/ts/sql/migrations/1170-update-call-history-unread-index.ts index f5a162d4408..e70bc262a14 100644 --- a/ts/sql/migrations/1170-update-call-history-unread-index.ts +++ b/ts/sql/migrations/1170-update-call-history-unread-index.ts @@ -1,29 +1,14 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; import { sql } from '../util'; -export const version = 1170; -export function updateToSchemaVersion1170( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1170) { - return; - } - - db.transaction(() => { - const [query] = sql` - DROP INDEX IF EXISTS messages_callHistory_markReadBefore; - CREATE INDEX messages_callHistory_markReadBefore - ON messages (type, seenStatus, received_at DESC) - WHERE type IS 'call-history'; - `; - db.exec(query); - - db.pragma('user_version = 1170'); - })(); - logger.info('updateToSchemaVersion1170: success!'); +export default function updateToSchemaVersion1170(db: Database): void { + const [query] = sql` + DROP INDEX IF EXISTS messages_callHistory_markReadBefore; + CREATE INDEX messages_callHistory_markReadBefore + ON messages (type, seenStatus, received_at DESC) + WHERE type IS 'call-history'; + `; + db.exec(query); } diff --git a/ts/sql/migrations/1180-add-attachment-download-source.ts b/ts/sql/migrations/1180-add-attachment-download-source.ts index e2fdac371a3..bbef0bbe339 100644 --- a/ts/sql/migrations/1180-add-attachment-download-source.ts +++ b/ts/sql/migrations/1180-add-attachment-download-source.ts @@ -1,37 +1,22 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; import { AttachmentDownloadSource } from '../Interface'; -export const version = 1180; -export function updateToSchemaVersion1180( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1180) { - return; - } +export default function updateToSchemaVersion1180(db: Database): void { + db.exec(` + ALTER TABLE attachment_downloads + ADD COLUMN source TEXT NOT NULL DEFAULT ${AttachmentDownloadSource.STANDARD}; - db.transaction(() => { - db.exec(` - ALTER TABLE attachment_downloads - ADD COLUMN source TEXT NOT NULL DEFAULT ${AttachmentDownloadSource.STANDARD}; - - ALTER TABLE attachment_downloads - -- this default value will be overridden by getNextAttachmentDownloadJobs - ADD COLUMN ciphertextSize INTEGER NOT NULL DEFAULT 0; - `); + ALTER TABLE attachment_downloads + -- this default value will be overridden by getNextAttachmentDownloadJobs + ADD COLUMN ciphertextSize INTEGER NOT NULL DEFAULT 0; + `); - db.exec(` - CREATE INDEX attachment_downloads_source_ciphertextSize - ON attachment_downloads ( - source, ciphertextSize - ); - `); - - db.pragma('user_version = 1180'); - })(); - logger.info('updateToSchemaVersion1180: success!'); + db.exec(` + CREATE INDEX attachment_downloads_source_ciphertextSize + ON attachment_downloads ( + source, ciphertextSize + ); + `); } diff --git a/ts/sql/migrations/1190-call-links-storage.ts b/ts/sql/migrations/1190-call-links-storage.ts index f86546dcdc4..9b8a04f0e4e 100644 --- a/ts/sql/migrations/1190-call-links-storage.ts +++ b/ts/sql/migrations/1190-call-links-storage.ts @@ -1,38 +1,22 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; -export const version = 1190; - -export function updateToSchemaVersion1190( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1190) { - return; - } - - db.transaction(() => { - db.exec(` - ALTER TABLE callLinks ADD COLUMN storageID TEXT; - ALTER TABLE callLinks ADD COLUMN storageVersion INTEGER; - ALTER TABLE callLinks ADD COLUMN storageUnknownFields BLOB; - ALTER TABLE callLinks ADD COLUMN storageNeedsSync INTEGER NOT NULL DEFAULT 0; - ALTER TABLE callLinks ADD COLUMN deletedAt INTEGER; - `); - db.prepare( - ` - UPDATE callLinks - SET deletedAt = $deletedAt - WHERE deleted = 1; - ` - ).run({ - deletedAt: new Date().getTime(), - }); - - db.pragma('user_version = 1190'); - })(); - logger.info('updateToSchemaVersion1190: success!'); +export default function updateToSchemaVersion1190(db: Database): void { + db.exec(` + ALTER TABLE callLinks ADD COLUMN storageID TEXT; + ALTER TABLE callLinks ADD COLUMN storageVersion INTEGER; + ALTER TABLE callLinks ADD COLUMN storageUnknownFields BLOB; + ALTER TABLE callLinks ADD COLUMN storageNeedsSync INTEGER NOT NULL DEFAULT 0; + ALTER TABLE callLinks ADD COLUMN deletedAt INTEGER; + `); + db.prepare( + ` + UPDATE callLinks + SET deletedAt = $deletedAt + WHERE deleted = 1; + ` + ).run({ + deletedAt: new Date().getTime(), + }); } diff --git a/ts/sql/migrations/1200-attachment-download-source-index.ts b/ts/sql/migrations/1200-attachment-download-source-index.ts index b6f8b2bc227..2bf89f2045f 100644 --- a/ts/sql/migrations/1200-attachment-download-source-index.ts +++ b/ts/sql/migrations/1200-attachment-download-source-index.ts @@ -1,29 +1,14 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; -export const version = 1200; -export function updateToSchemaVersion1200( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1200) { - return; - } - - db.transaction(() => { - // The standard getNextAttachmentDownloadJobs query uses active & source conditions, - // ordered by received_at - db.exec(` - CREATE INDEX attachment_downloads_active_source_receivedAt - ON attachment_downloads ( - active, source, receivedAt - ); - `); - - db.pragma('user_version = 1200'); - })(); - logger.info('updateToSchemaVersion1200: success!'); +export default function updateToSchemaVersion1200(db: Database): void { + // The standard getNextAttachmentDownloadJobs query uses active & source conditions, + // ordered by received_at + db.exec(` + CREATE INDEX attachment_downloads_active_source_receivedAt + ON attachment_downloads ( + active, source, receivedAt + ); + `); } diff --git a/ts/sql/migrations/1210-call-history-started-id.ts b/ts/sql/migrations/1210-call-history-started-id.ts index 3da56cd8f0e..f0093c0b99e 100644 --- a/ts/sql/migrations/1210-call-history-started-id.ts +++ b/ts/sql/migrations/1210-call-history-started-id.ts @@ -1,30 +1,15 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; -export const version = 1210; -export function updateToSchemaVersion1210( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1210) { - return; - } +export default function updateToSchemaVersion1210(db: Database): void { + // The standard getNextAttachmentDownloadJobs query uses active & source conditions, + // ordered by received_at + db.exec(` + ALTER TABLE callsHistory + ADD COLUMN startedById TEXT DEFAULT NULL; - db.transaction(() => { - // The standard getNextAttachmentDownloadJobs query uses active & source conditions, - // ordered by received_at - db.exec(` - ALTER TABLE callsHistory - ADD COLUMN startedById TEXT DEFAULT NULL; - - ALTER TABLE callsHistory - ADD COLUMN endedTimestamp INTEGER DEFAULT NULL; - `); - - db.pragma('user_version = 1210'); - })(); - logger.info('updateToSchemaVersion1210: success!'); + ALTER TABLE callsHistory + ADD COLUMN endedTimestamp INTEGER DEFAULT NULL; + `); } diff --git a/ts/sql/migrations/1220-blob-sessions.ts b/ts/sql/migrations/1220-blob-sessions.ts index 37312fc10e9..d49f6f97926 100644 --- a/ts/sql/migrations/1220-blob-sessions.ts +++ b/ts/sql/migrations/1220-blob-sessions.ts @@ -120,100 +120,84 @@ function migrateSession( throw missingCaseError(session.version); } -export function updateToSchemaVersion1220( - currentVersion: number, +export default function updateToSchemaVersion1220( db: Database, logger: LoggerType ): void { - if (currentVersion >= 1220) { + db.exec(` + ALTER TABLE sessions + RENAME TO old_sessions; + + CREATE TABLE sessions ( + id TEXT NOT NULL PRIMARY KEY, + ourServiceId TEXT NOT NULL, + serviceId TEXT NOT NULL, + conversationId TEXT NOT NULL, + deviceId INTEGER NOT NULL, + record BLOB NOT NULL + ) STRICT; + `); + + const getItem = db.prepare( + ` + SELECT json -> '$.value' FROM items WHERE id IS ? + `, + { + pluck: true, + } + ); + + const identityKeyMapJson = getItem.get(['identityKeyMap']); + const registrationIdMapJson = getItem.get(['registrationIdMap']); + + // If we don't have private keys - the sessions cannot be used anyway + if (!identityKeyMapJson || !registrationIdMapJson) { + logger.info('no identity/registration id'); + db.exec('DROP TABLE old_sessions'); return; } - db.transaction(() => { - db.exec(` - ALTER TABLE sessions - RENAME TO old_sessions; + const identityKeyMap = identityKeyMapSchema.parse( + JSON.parse(identityKeyMapJson) + ); + const registrationIdMap = registrationIdMapSchema.parse( + JSON.parse(registrationIdMapJson) + ); - CREATE TABLE sessions ( - id TEXT NOT NULL PRIMARY KEY, - ourServiceId TEXT NOT NULL, - serviceId TEXT NOT NULL, - conversationId TEXT NOT NULL, - deviceId INTEGER NOT NULL, - record BLOB NOT NULL - ) STRICT; - `); + const getSessionsPage = db.prepare( + 'DELETE FROM old_sessions RETURNING * LIMIT 1000' + ); + const insertSession = db.prepare(` + INSERT INTO sessions + (id, ourServiceId, serviceId, conversationId, deviceId, record) + VALUES + ($id, $ourServiceId, $serviceId, $conversationId, $deviceId, $record) + `); - const getItem = db.prepare( - ` - SELECT json -> '$.value' FROM items WHERE id IS ? - `, - { - pluck: true, - } - ); + let migrated = 0; + let failed = 0; - const identityKeyMapJson = getItem.get(['identityKeyMap']); - const registrationIdMapJson = getItem.get(['registrationIdMap']); - - // If we don't have private keys - the sessions cannot be used anyway - if (!identityKeyMapJson || !registrationIdMapJson) { - logger.info('updateToSchemaVersion1220: no identity/registration id'); - db.exec('DROP TABLE old_sessions'); - db.pragma('user_version = 1220'); - return; + // eslint-disable-next-line no-constant-condition + while (true) { + const rows: Array = getSessionsPage.all(); + if (rows.length === 0) { + break; } - const identityKeyMap = identityKeyMapSchema.parse( - JSON.parse(identityKeyMapJson) - ); - const registrationIdMap = registrationIdMapSchema.parse( - JSON.parse(registrationIdMapJson) - ); - - const getSessionsPage = db.prepare( - 'DELETE FROM old_sessions RETURNING * LIMIT 1000' - ); - const insertSession = db.prepare(` - INSERT INTO sessions - (id, ourServiceId, serviceId, conversationId, deviceId, record) - VALUES - ($id, $ourServiceId, $serviceId, $conversationId, $deviceId, $record) - `); - - let migrated = 0; - let failed = 0; - - // eslint-disable-next-line no-constant-condition - while (true) { - const rows: Array = getSessionsPage.all(); - if (rows.length === 0) { - break; - } - - for (const row of rows) { - try { - insertSession.run( - migrateSession(row, identityKeyMap, registrationIdMap, logger) - ); - migrated += 1; - } catch (error) { - failed += 1; - logger.error( - 'updateToSchemaVersion1220: failed to migrate session', - Errors.toLogFormat(error) - ); - } + for (const row of rows) { + try { + insertSession.run( + migrateSession(row, identityKeyMap, registrationIdMap, logger) + ); + migrated += 1; + } catch (error) { + failed += 1; + logger.error('failed to migrate session', Errors.toLogFormat(error)); } } + } - logger.info( - `updateToSchemaVersion1220: migrated ${migrated} sessions, ` + - `${failed} failed` - ); + logger.info(`migrated ${migrated} sessions, ${failed} failed`); - db.exec('DROP TABLE old_sessions'); - db.pragma('user_version = 1220'); - })(); - logger.info('updateToSchemaVersion1220: success!'); + db.exec('DROP TABLE old_sessions'); } diff --git a/ts/sql/migrations/1230-call-links-admin-key-index.ts b/ts/sql/migrations/1230-call-links-admin-key-index.ts index 7ab71a2fae1..3f380b26c41 100644 --- a/ts/sql/migrations/1230-call-links-admin-key-index.ts +++ b/ts/sql/migrations/1230-call-links-admin-key-index.ts @@ -1,28 +1,12 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; -export const version = 1230; +export default function updateToSchemaVersion1230(db: Database): void { + db.exec(` + DROP INDEX IF EXISTS callLinks_adminKey; -export function updateToSchemaVersion1230( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1230) { - return; - } - - db.transaction(() => { - db.exec(` - DROP INDEX IF EXISTS callLinks_adminKey; - - CREATE INDEX callLinks_adminKey - ON callLinks (adminKey); - `); - - db.pragma('user_version = 1230'); - })(); - logger.info('updateToSchemaVersion1230: success!'); + CREATE INDEX callLinks_adminKey + ON callLinks (adminKey); + `); } diff --git a/ts/sql/migrations/1240-defunct-call-links-table.ts b/ts/sql/migrations/1240-defunct-call-links-table.ts index e29778bd007..c52a6b1e64d 100644 --- a/ts/sql/migrations/1240-defunct-call-links-table.ts +++ b/ts/sql/migrations/1240-defunct-call-links-table.ts @@ -3,33 +3,16 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; import { sql } from '../util'; -export const version = 1240; +export default function updateToSchemaVersion1240(db: Database): void { + const [createTable] = sql` + CREATE TABLE defunctCallLinks ( + roomId TEXT NOT NULL PRIMARY KEY, + rootKey BLOB NOT NULL, + adminKey BLOB + ) STRICT; + `; -export function updateToSchemaVersion1240( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1240) { - return; - } - - db.transaction(() => { - const [createTable] = sql` - CREATE TABLE defunctCallLinks ( - roomId TEXT NOT NULL PRIMARY KEY, - rootKey BLOB NOT NULL, - adminKey BLOB - ) STRICT; - `; - - db.exec(createTable); - - db.pragma('user_version = 1240'); - })(); - - logger.info('updateToSchemaVersion1240: success!'); + db.exec(createTable); } diff --git a/ts/sql/migrations/1250-defunct-call-links-storage.ts b/ts/sql/migrations/1250-defunct-call-links-storage.ts index 398822caa91..d8b1c529427 100644 --- a/ts/sql/migrations/1250-defunct-call-links-storage.ts +++ b/ts/sql/migrations/1250-defunct-call-links-storage.ts @@ -1,28 +1,12 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; -export const version = 1250; - -export function updateToSchemaVersion1250( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1250) { - return; - } - - db.transaction(() => { - db.exec(` - ALTER TABLE defunctCallLinks ADD COLUMN storageID TEXT; - ALTER TABLE defunctCallLinks ADD COLUMN storageVersion INTEGER; - ALTER TABLE defunctCallLinks ADD COLUMN storageUnknownFields BLOB; - ALTER TABLE defunctCallLinks ADD COLUMN storageNeedsSync INTEGER NOT NULL DEFAULT 0; - `); - - db.pragma('user_version = 1250'); - })(); - logger.info('updateToSchemaVersion1250: success!'); +export default function updateToSchemaVersion1250(db: Database): void { + db.exec(` + ALTER TABLE defunctCallLinks ADD COLUMN storageID TEXT; + ALTER TABLE defunctCallLinks ADD COLUMN storageVersion INTEGER; + ALTER TABLE defunctCallLinks ADD COLUMN storageUnknownFields BLOB; + ALTER TABLE defunctCallLinks ADD COLUMN storageNeedsSync INTEGER NOT NULL DEFAULT 0; + `); } diff --git a/ts/sql/migrations/1260-sync-tasks-rowid.ts b/ts/sql/migrations/1260-sync-tasks-rowid.ts index 18aed510033..7e795717025 100644 --- a/ts/sql/migrations/1260-sync-tasks-rowid.ts +++ b/ts/sql/migrations/1260-sync-tasks-rowid.ts @@ -1,30 +1,13 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; import { sql } from '../util'; -export const version = 1260; +export default function updateToSchemaVersion1260(db: Database): void { + const [query] = sql` + DROP INDEX IF EXISTS syncTasks_order; + CREATE INDEX syncTasks_delete ON syncTasks (attempts DESC); + `; -export function updateToSchemaVersion1260( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1260) { - return; - } - - db.transaction(() => { - const [query] = sql` - DROP INDEX IF EXISTS syncTasks_order; - CREATE INDEX syncTasks_delete ON syncTasks (attempts DESC); - `; - - db.exec(query); - - db.pragma('user_version = 1260'); - })(); - - logger.info('updateToSchemaVersion1260: success!'); + db.exec(query); } diff --git a/ts/sql/migrations/1270-normalize-messages.ts b/ts/sql/migrations/1270-normalize-messages.ts index 9c2c8ff0cfe..3d3199a6c7b 100644 --- a/ts/sql/migrations/1270-normalize-messages.ts +++ b/ts/sql/migrations/1270-normalize-messages.ts @@ -1,53 +1,36 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; import { sql } from '../util'; -export const version = 1270; +export default function updateToSchemaVersion1270(db: Database): void { + const [query] = sql` + ALTER TABLE messages + ADD COLUMN timestamp INTEGER; + ALTER TABLE messages + ADD COLUMN received_at_ms INTEGER; + ALTER TABLE messages + ADD COLUMN unidentifiedDeliveryReceived INTEGER; + ALTER TABLE messages + ADD COLUMN serverTimestamp INTEGER; -export function updateToSchemaVersion1270( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1270) { - return; - } + ALTER TABLE messages + RENAME COLUMN source TO legacySource; + ALTER TABLE messages + ADD COLUMN source TEXT; - db.transaction(() => { - const [query] = sql` - ALTER TABLE messages - ADD COLUMN timestamp INTEGER; - ALTER TABLE messages - ADD COLUMN received_at_ms INTEGER; - ALTER TABLE messages - ADD COLUMN unidentifiedDeliveryReceived INTEGER; - ALTER TABLE messages - ADD COLUMN serverTimestamp INTEGER; + UPDATE messages SET + timestamp = json_extract(json, '$.timestamp'), + received_at_ms = json_extract(json, '$.received_at_ms'), + unidentifiedDeliveryReceived = + json_extract(json, '$.unidentifiedDeliveryReceived'), + serverTimestamp = + json_extract(json, '$.serverTimestamp'), + source = IFNULL(json_extract(json, '$.source'), '+' || legacySource); - ALTER TABLE messages - RENAME COLUMN source TO legacySource; - ALTER TABLE messages - ADD COLUMN source TEXT; + ALTER TABLE messages + DROP COLUMN legacySource; + `; - UPDATE messages SET - timestamp = json_extract(json, '$.timestamp'), - received_at_ms = json_extract(json, '$.received_at_ms'), - unidentifiedDeliveryReceived = - json_extract(json, '$.unidentifiedDeliveryReceived'), - serverTimestamp = - json_extract(json, '$.serverTimestamp'), - source = IFNULL(json_extract(json, '$.source'), '+' || legacySource); - - ALTER TABLE messages - DROP COLUMN legacySource; - `; - - db.exec(query); - - db.pragma('user_version = 1270'); - })(); - - logger.info('updateToSchemaVersion1270: success!'); + db.exec(query); } diff --git a/ts/sql/migrations/1280-blob-unprocessed.ts b/ts/sql/migrations/1280-blob-unprocessed.ts index c844a6cb0e5..5a3f360f885 100644 --- a/ts/sql/migrations/1280-blob-unprocessed.ts +++ b/ts/sql/migrations/1280-blob-unprocessed.ts @@ -14,166 +14,142 @@ import { sql } from '../util'; import type { WritableDB } from '../Interface'; import { getOurUuid } from './41-uuid-keys'; -export const version = 1280; - -export function updateToSchemaVersion1280( - currentVersion: number, +export default function updateToSchemaVersion1280( db: WritableDB, logger: LoggerType ): void { - if (currentVersion >= 1280) { - return; + const ourAci = getOurUuid(db); + + let rows = db.prepare('SELECT * FROM unprocessed').all(); + + const [query] = sql` + DROP TABLE unprocessed; + + CREATE TABLE unprocessed( + id TEXT NOT NULL PRIMARY KEY ASC, + type INTEGER NOT NULL, + timestamp INTEGER NOT NULL, + attempts INTEGER NOT NULL, + receivedAtCounter INTEGER NOT NULL, + urgent INTEGER NOT NULL, + story INTEGER NOT NULL, + serverGuid TEXT NOT NULL, + serverTimestamp INTEGER NOT NULL, + isEncrypted INTEGER NOT NULL, + content BLOB NOT NULL, + messageAgeSec INTEGER NOT NULL, + destinationServiceId TEXT NOT NULL, + + -- Not present for 1:1 messages and not sealed messages + groupId TEXT, + + -- Not present for sealed envelopes + reportingToken BLOB, + source TEXT, + sourceServiceId TEXT, + sourceDevice TEXT, + + -- Present only for PNP change number + updatedPni TEXT + ) STRICT; + + CREATE INDEX unprocessed_timestamp ON unprocessed + (timestamp); + + CREATE INDEX unprocessed_byReceivedAtCounter ON unprocessed + (receivedAtCounter); + `; + db.exec(query); + + const insertStmt = db.prepare(` + INSERT INTO unprocessed + (id, type, timestamp, attempts, receivedAtCounter, urgent, story, + serverGuid, serverTimestamp, isEncrypted, content, source, + messageAgeSec, sourceServiceId, sourceDevice, + destinationServiceId, reportingToken) + VALUES + ($id, $type, $timestamp, $attempts, $receivedAtCounter, $urgent, $story, + $serverGuid, $serverTimestamp, $isEncrypted, $content, $source, + $messageAgeSec, $sourceServiceId, $sourceDevice, + $destinationServiceId, $reportingToken); + `); + + let oldEnvelopes = 0; + + if (!ourAci) { + if (rows.length) { + logger.warn(`no aci, dropping ${rows.length} envelopes`); + rows = []; + } } - db.transaction(() => { - const ourAci = getOurUuid(db); + for (const row of rows) { + const { + id, + envelope, + decrypted, + timestamp, + attempts, + version: envelopeVersion, + receivedAtCounter, + urgent, + story, + serverGuid, + serverTimestamp, + ...rest + } = row; - let rows = db.prepare('SELECT * FROM unprocessed').all(); - - const [query] = sql` - DROP TABLE unprocessed; - - CREATE TABLE unprocessed( - id TEXT NOT NULL PRIMARY KEY ASC, - type INTEGER NOT NULL, - timestamp INTEGER NOT NULL, - attempts INTEGER NOT NULL, - receivedAtCounter INTEGER NOT NULL, - urgent INTEGER NOT NULL, - story INTEGER NOT NULL, - serverGuid TEXT NOT NULL, - serverTimestamp INTEGER NOT NULL, - isEncrypted INTEGER NOT NULL, - content BLOB NOT NULL, - messageAgeSec INTEGER NOT NULL, - destinationServiceId TEXT NOT NULL, - - -- Not present for 1:1 messages and not sealed messages - groupId TEXT, - - -- Not present for sealed envelopes - reportingToken BLOB, - source TEXT, - sourceServiceId TEXT, - sourceDevice TEXT, - - -- Present only for PNP change number - updatedPni TEXT - ) STRICT; - - CREATE INDEX unprocessed_timestamp ON unprocessed - (timestamp); - - CREATE INDEX unprocessed_byReceivedAtCounter ON unprocessed - (receivedAtCounter); - `; - db.exec(query); - - const insertStmt = db.prepare(` - INSERT INTO unprocessed - (id, type, timestamp, attempts, receivedAtCounter, urgent, story, - serverGuid, serverTimestamp, isEncrypted, content, source, - messageAgeSec, sourceServiceId, sourceDevice, - destinationServiceId, reportingToken) - VALUES - ($id, $type, $timestamp, $attempts, $receivedAtCounter, $urgent, $story, - $serverGuid, $serverTimestamp, $isEncrypted, $content, $source, - $messageAgeSec, $sourceServiceId, $sourceDevice, - $destinationServiceId, $reportingToken); - `); - - let oldEnvelopes = 0; - - if (!ourAci) { - if (rows.length) { - logger.warn( - `updateToSchemaVersion1280: no aci, dropping ${rows.length} envelopes` - ); - rows = []; - } + // Skip old and/or invalid rows + if (envelopeVersion !== 2 || !envelope) { + oldEnvelopes += 1; + continue; } - for (const row of rows) { - const { - id, - envelope, - decrypted, - timestamp, - attempts, - version: envelopeVersion, - receivedAtCounter, - urgent, - story, - serverGuid, - serverTimestamp, - ...rest - } = row; - - // Skip old and/or invalid rows - if (envelopeVersion !== 2 || !envelope) { - oldEnvelopes += 1; - continue; - } - - try { - const decoded = Proto.Envelope.decode( - Buffer.from(String(envelope), 'base64') - ); - if (!decoded.content) { - throw new Error('Missing envelope content'); - } - - const content = decrypted - ? Buffer.from(String(decrypted), 'base64') - : decoded.content; - - insertStmt.run({ - ...rest, - id, - type: decoded.type ?? Proto.Envelope.Type.UNKNOWN, - content: content ?? null, - isEncrypted: decrypted ? 0 : 1, - timestamp: timestamp || Date.now(), - attempts: attempts || 0, - receivedAtCounter: receivedAtCounter || 0, - urgent: urgent ? 1 : 0, - story: story ? 1 : 0, - serverGuid: serverGuid || getGuid(), - serverTimestamp: serverTimestamp || 0, - destinationServiceId: - normalizeServiceId( - decoded.destinationServiceId || ourAci, - 'Envelope.destinationServiceId' - ) ?? null, - updatedPni: isUntaggedPniString(decoded.updatedPni) - ? normalizePni( - toTaggedPni(decoded.updatedPni), - 'Envelope.updatedPni' - ) - : null, - // Sadly not captured previously - messageAgeSec: 0, - reportingToken: decoded.reportSpamToken?.length - ? decoded.reportSpamToken - : null, - }); - } catch (error) { - logger.warn( - 'updateToSchemaVersion1280: failed to migrate unprocessed', - id, - error - ); - } - } - - if (oldEnvelopes !== 0) { - logger.warn( - `updateToSchemaVersion1280: dropped ${oldEnvelopes} envelopes` + try { + const decoded = Proto.Envelope.decode( + Buffer.from(String(envelope), 'base64') ); + if (!decoded.content) { + throw new Error('Missing envelope content'); + } + + const content = decrypted + ? Buffer.from(String(decrypted), 'base64') + : decoded.content; + + insertStmt.run({ + ...rest, + id, + type: decoded.type ?? Proto.Envelope.Type.UNKNOWN, + content: content ?? null, + isEncrypted: decrypted ? 0 : 1, + timestamp: timestamp || Date.now(), + attempts: attempts || 0, + receivedAtCounter: receivedAtCounter || 0, + urgent: urgent ? 1 : 0, + story: story ? 1 : 0, + serverGuid: serverGuid || getGuid(), + serverTimestamp: serverTimestamp || 0, + destinationServiceId: + normalizeServiceId( + decoded.destinationServiceId || ourAci, + 'Envelope.destinationServiceId' + ) ?? null, + updatedPni: isUntaggedPniString(decoded.updatedPni) + ? normalizePni(toTaggedPni(decoded.updatedPni), 'Envelope.updatedPni') + : null, + // Sadly not captured previously + messageAgeSec: 0, + reportingToken: decoded.reportSpamToken?.length + ? decoded.reportSpamToken + : null, + }); + } catch (error) { + logger.warn('failed to migrate unprocessed', id, error); } + } - db.pragma('user_version = 1280'); - })(); - - logger.info('updateToSchemaVersion1280: success!'); + if (oldEnvelopes !== 0) { + logger.warn(`dropped ${oldEnvelopes} envelopes`); + } } diff --git a/ts/sql/migrations/1290-int-unprocessed-source-device.ts b/ts/sql/migrations/1290-int-unprocessed-source-device.ts index 1ac1d85e966..7534b654742 100644 --- a/ts/sql/migrations/1290-int-unprocessed-source-device.ts +++ b/ts/sql/migrations/1290-int-unprocessed-source-device.ts @@ -1,34 +1,17 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { LoggerType } from '../../types/Logging'; import { sql } from '../util'; import type { WritableDB } from '../Interface'; -export const version = 1290; +export default function updateToSchemaVersion1290(db: WritableDB): void { + const [query] = sql` + ALTER TABLE unprocessed RENAME COLUMN sourceDevice TO legacySourceDevice; + ALTER TABLE unprocessed ADD COLUMN sourceDevice INTEGER; -export function updateToSchemaVersion1290( - currentVersion: number, - db: WritableDB, - logger: LoggerType -): void { - if (currentVersion >= 1290) { - return; - } + UPDATE unprocessed + SET sourceDevice = legacySourceDevice; - db.transaction(() => { - const [query] = sql` - ALTER TABLE unprocessed RENAME COLUMN sourceDevice TO legacySourceDevice; - ALTER TABLE unprocessed ADD COLUMN sourceDevice INTEGER; - - UPDATE unprocessed - SET sourceDevice = legacySourceDevice; - - ALTER TABLE unprocessed DROP COLUMN legacySourceDevice; - `; - db.exec(query); - - db.pragma('user_version = 1290'); - })(); - - logger.info('updateToSchemaVersion1290: success!'); + ALTER TABLE unprocessed DROP COLUMN legacySourceDevice; + `; + db.exec(query); } diff --git a/ts/sql/migrations/1300-sticker-pack-refs.ts b/ts/sql/migrations/1300-sticker-pack-refs.ts index 7867fe28cca..03e4703ee7e 100644 --- a/ts/sql/migrations/1300-sticker-pack-refs.ts +++ b/ts/sql/migrations/1300-sticker-pack-refs.ts @@ -1,35 +1,18 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { LoggerType } from '../../types/Logging'; import { sql } from '../util'; import type { WritableDB } from '../Interface'; -export const version = 1300; +export default function updateToSchemaVersion1300(db: WritableDB): void { + const [query] = sql` + ALTER TABLE sticker_references + ADD COLUMN stickerId INTEGER NOT NULL DEFAULT -1; + ALTER TABLE sticker_references + ADD COLUMN isUnresolved INTEGER NOT NULL DEFAULT 0; -export function updateToSchemaVersion1300( - currentVersion: number, - db: WritableDB, - logger: LoggerType -): void { - if (currentVersion >= 1300) { - return; - } - - db.transaction(() => { - const [query] = sql` - ALTER TABLE sticker_references - ADD COLUMN stickerId INTEGER NOT NULL DEFAULT -1; - ALTER TABLE sticker_references - ADD COLUMN isUnresolved INTEGER NOT NULL DEFAULT 0; - - CREATE INDEX unresolved_sticker_refs - ON sticker_references (packId, stickerId) - WHERE isUnresolved IS 1; - `; - db.exec(query); - - db.pragma('user_version = 1300'); - })(); - - logger.info('updateToSchemaVersion1300: success!'); + CREATE INDEX unresolved_sticker_refs + ON sticker_references (packId, stickerId) + WHERE isUnresolved IS 1; + `; + db.exec(query); } diff --git a/ts/sql/migrations/1310-muted-fixup.ts b/ts/sql/migrations/1310-muted-fixup.ts index 63879cbd1ab..f4ff81b4ae4 100644 --- a/ts/sql/migrations/1310-muted-fixup.ts +++ b/ts/sql/migrations/1310-muted-fixup.ts @@ -4,37 +4,24 @@ import type { LoggerType } from '../../types/Logging'; import { sql } from '../util'; import type { WritableDB } from '../Interface'; -export const version = 1310; - // Value from ts/util/timestamp.ts at the time of creation of this migration const MAX_SAFE_DATE = 8640000000000000; -export function updateToSchemaVersion1310( - currentVersion: number, +export default function updateToSchemaVersion1310( db: WritableDB, logger: LoggerType ): void { - if (currentVersion >= 1310) { - return; + const [query, params] = sql` + UPDATE conversations + SET json = json_replace( + json, + '$.muteExpiresAt', + 9007199254740991 -- max safe integer + ) + WHERE json ->> '$.muteExpiresAt' IS ${MAX_SAFE_DATE}; + `; + const { changes } = db.prepare(query).run(params); + if (changes !== 0) { + logger.warn(`fixed ${changes} conversations`); } - - db.transaction(() => { - const [query, params] = sql` - UPDATE conversations - SET json = json_replace( - json, - '$.muteExpiresAt', - 9007199254740991 -- max safe integer - ) - WHERE json ->> '$.muteExpiresAt' IS ${MAX_SAFE_DATE}; - `; - const { changes } = db.prepare(query).run(params); - if (changes !== 0) { - logger.warn(`updateToSchemaVersion1310: fixed ${changes} conversations`); - } - - db.pragma('user_version = 1310'); - })(); - - logger.info('updateToSchemaVersion1310: success!'); } diff --git a/ts/sql/migrations/1320-unprocessed-received-at-date.ts b/ts/sql/migrations/1320-unprocessed-received-at-date.ts index 4a415620463..61f669a532f 100644 --- a/ts/sql/migrations/1320-unprocessed-received-at-date.ts +++ b/ts/sql/migrations/1320-unprocessed-received-at-date.ts @@ -1,38 +1,21 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { LoggerType } from '../../types/Logging'; import { sql } from '../util'; import type { WritableDB } from '../Interface'; -export const version = 1320; +export default function updateToSchemaVersion1320(db: WritableDB): void { + const [query] = sql` + DROP INDEX unprocessed_timestamp; -export function updateToSchemaVersion1320( - currentVersion: number, - db: WritableDB, - logger: LoggerType -): void { - if (currentVersion >= 1320) { - return; - } + ALTER TABLE unprocessed + ADD COLUMN receivedAtDate INTEGER DEFAULT 0 NOT NULL; - db.transaction(() => { - const [query] = sql` - DROP INDEX unprocessed_timestamp; + UPDATE unprocessed + SET receivedAtDate = timestamp; - ALTER TABLE unprocessed - ADD COLUMN receivedAtDate INTEGER DEFAULT 0 NOT NULL; - - UPDATE unprocessed - SET receivedAtDate = timestamp; - - CREATE INDEX unprocessed_byReceivedAtDate ON unprocessed - (receivedAtDate); - `; - db.exec(query); - - db.pragma('user_version = 1320'); - })(); - - logger.info('updateToSchemaVersion1320: success!'); + CREATE INDEX unprocessed_byReceivedAtDate ON unprocessed + (receivedAtDate); + `; + db.exec(query); } diff --git a/ts/sql/migrations/1330-sync-tasks-type-index.ts b/ts/sql/migrations/1330-sync-tasks-type-index.ts index 5b73bcf9859..99195bb648a 100644 --- a/ts/sql/migrations/1330-sync-tasks-type-index.ts +++ b/ts/sql/migrations/1330-sync-tasks-type-index.ts @@ -1,29 +1,12 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; import { sql } from '../util'; -export const version = 1330; +export default function updateToSchemaVersion1330(db: Database): void { + const [query] = sql` + CREATE INDEX syncTasks_type ON syncTasks (type); + `; -export function updateToSchemaVersion1330( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1330) { - return; - } - - db.transaction(() => { - const [query] = sql` - CREATE INDEX syncTasks_type ON syncTasks (type); - `; - - db.exec(query); - - db.pragma('user_version = 1330'); - })(); - - logger.info('updateToSchemaVersion1330: success!'); + db.exec(query); } diff --git a/ts/sql/migrations/1340-recent-gifs.ts b/ts/sql/migrations/1340-recent-gifs.ts index 4da06d8a69f..29a064cd4a2 100644 --- a/ts/sql/migrations/1340-recent-gifs.ts +++ b/ts/sql/migrations/1340-recent-gifs.ts @@ -1,44 +1,27 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; import { sql } from '../util'; -export const version = 1340; +export default function updateToSchemaVersion1340(db: Database): void { + const [query] = sql` + CREATE TABLE recentGifs ( + id TEXT NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + description TEXT NOT NULL, + previewMedia_url TEXT NOT NULL, + previewMedia_width INTEGER NOT NULL, + previewMedia_height INTEGER NOT NULL, + attachmentMedia_url TEXT NOT NULL, + attachmentMedia_width INTEGER NOT NULL, + attachmentMedia_height INTEGER NOT NULL, + lastUsedAt INTEGER NOT NULL + ) STRICT; -export function updateToSchemaVersion1340( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1340) { - return; - } + CREATE INDEX recentGifs_order ON recentGifs ( + lastUsedAt DESC + ); + `; - db.transaction(() => { - const [query] = sql` - CREATE TABLE recentGifs ( - id TEXT NOT NULL PRIMARY KEY, - title TEXT NOT NULL, - description TEXT NOT NULL, - previewMedia_url TEXT NOT NULL, - previewMedia_width INTEGER NOT NULL, - previewMedia_height INTEGER NOT NULL, - attachmentMedia_url TEXT NOT NULL, - attachmentMedia_width INTEGER NOT NULL, - attachmentMedia_height INTEGER NOT NULL, - lastUsedAt INTEGER NOT NULL - ) STRICT; - - CREATE INDEX recentGifs_order ON recentGifs ( - lastUsedAt DESC - ); - `; - - db.exec(query); - - db.pragma('user_version = 1340'); - })(); - - logger.info('updateToSchemaVersion1340: success!'); + db.exec(query); } diff --git a/ts/sql/migrations/1350-notification-profiles.ts b/ts/sql/migrations/1350-notification-profiles.ts index a36e56f4612..b7c13a2e535 100644 --- a/ts/sql/migrations/1350-notification-profiles.ts +++ b/ts/sql/migrations/1350-notification-profiles.ts @@ -1,58 +1,41 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; import { sql } from '../util'; -export const version = 1350; +export default function updateToSchemaVersion1350(db: Database): void { + const [query] = sql` + CREATE TABLE notificationProfiles( + id TEXT PRIMARY KEY NOT NULL, -export function updateToSchemaVersion1350( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1350) { - return; - } + name TEXT NOT NULL, + emoji TEXT, + /* A numeric representation of a color, like 0xAARRGGBB */ + color INTEGER NOT NULL, - db.transaction(() => { - const [query] = sql` - CREATE TABLE notificationProfiles( - id TEXT PRIMARY KEY NOT NULL, - - name TEXT NOT NULL, - emoji TEXT, - /* A numeric representation of a color, like 0xAARRGGBB */ - color INTEGER NOT NULL, - - createdAtMs INTEGER NOT NULL, - - allowAllCalls INTEGER NOT NULL, - allowAllMentions INTEGER NOT NULL, + createdAtMs INTEGER NOT NULL, - /* A JSON array of conversationId strings */ - allowedMembersJson TEXT NOT NULL, - scheduleEnabled INTEGER NOT NULL, - - /* 24-hour clock int, 0000-2359 (e.g., 15, 900, 1130, 2345) */ - scheduleStartTime INTEGER, - scheduleEndTime INTEGER, + allowAllCalls INTEGER NOT NULL, + allowAllMentions INTEGER NOT NULL, - /* A JSON object with true/false for each of the numbers in the Protobuf enum */ - scheduleDaysEnabledJson TEXT, - deletedAtTimestampMs INTEGER, + /* A JSON array of conversationId strings */ + allowedMembersJson TEXT NOT NULL, + scheduleEnabled INTEGER NOT NULL, - storageID TEXT, - storageVersion INTEGER, - storageUnknownFields BLOB, - storageNeedsSync INTEGER NOT NULL DEFAULT 0 - ) STRICT; - `; + /* 24-hour clock int, 0000-2359 (e.g., 15, 900, 1130, 2345) */ + scheduleStartTime INTEGER, + scheduleEndTime INTEGER, - db.exec(query); + /* A JSON object with true/false for each of the numbers in the Protobuf enum */ + scheduleDaysEnabledJson TEXT, + deletedAtTimestampMs INTEGER, - db.pragma('user_version = 1350'); - })(); + storageID TEXT, + storageVersion INTEGER, + storageUnknownFields BLOB, + storageNeedsSync INTEGER NOT NULL DEFAULT 0 + ) STRICT; + `; - logger.info('updateToSchemaVersion1350: success!'); + db.exec(query); } diff --git a/ts/sql/migrations/1360-attachments.ts b/ts/sql/migrations/1360-attachments.ts index fb071511c92..6321c3c6b2a 100644 --- a/ts/sql/migrations/1360-attachments.ts +++ b/ts/sql/migrations/1360-attachments.ts @@ -1,119 +1,102 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { LoggerType } from '../../types/Logging'; import type { WritableDB } from '../Interface'; -export const version = 1360; +export default function updateToSchemaVersion1360(db: WritableDB): void { + db.exec(` + DROP TABLE IF EXISTS message_attachments; + `); -export function updateToSchemaVersion1360( - currentVersion: number, - db: WritableDB, - logger: LoggerType -): void { - if (currentVersion >= 1360) { - return; - } + db.exec(` + CREATE TABLE message_attachments ( + messageId TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE, + -- For editHistoryIndex to be part of the primary key, it cannot be NULL in strict tables. + -- For that reason, we use a value of -1 to indicate that it is the root message (not in editHistory) + editHistoryIndex INTEGER NOT NULL, + attachmentType TEXT NOT NULL, -- 'long-message' | 'quote' | 'attachment' | 'preview' | 'contact' | 'sticker' + orderInMessage INTEGER NOT NULL, + conversationId TEXT NOT NULL, + sentAt INTEGER NOT NULL, + clientUuid TEXT, + size INTEGER NOT NULL, + contentType TEXT NOT NULL, + path TEXT, + plaintextHash TEXT, + localKey TEXT, + caption TEXT, + fileName TEXT, + blurHash TEXT, + height INTEGER, + width INTEGER, + digest TEXT, + key TEXT, + iv TEXT, + downloadPath TEXT, + version INTEGER, + incrementalMac TEXT, + incrementalMacChunkSize INTEGER, + transitCdnKey TEXT, + transitCdnNumber INTEGER, + transitCdnUploadTimestamp INTEGER, + backupMediaName TEXT, + backupCdnNumber INTEGER, + isReencryptableToSameDigest INTEGER, + reencryptionIv TEXT, + reencryptionKey TEXT, + reencryptionDigest TEXT, + thumbnailPath TEXT, + thumbnailSize INTEGER, + thumbnailContentType TEXT, + thumbnailLocalKey TEXT, + thumbnailVersion INTEGER, + screenshotPath TEXT, + screenshotSize INTEGER, + screenshotContentType TEXT, + screenshotLocalKey TEXT, + screenshotVersion INTEGER, + backupThumbnailPath TEXT, + backupThumbnailSize INTEGER, + backupThumbnailContentType TEXT, + backupThumbnailLocalKey TEXT, + backupThumbnailVersion INTEGER, + storyTextAttachmentJson TEXT, + localBackupPath TEXT, + flags INTEGER, + error INTEGER, + wasTooBig INTEGER, + isCorrupted INTEGER, + copiedFromQuotedAttachment INTEGER, + pending INTEGER, + backfillError INTEGER, + PRIMARY KEY (messageId, editHistoryIndex, attachmentType, orderInMessage) + ) STRICT; + `); - db.transaction(() => { - db.exec(` - DROP TABLE IF EXISTS message_attachments; - `); + // The following indexes were removed in migration 1370 - db.exec(` - CREATE TABLE message_attachments ( - messageId TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE, - -- For editHistoryIndex to be part of the primary key, it cannot be NULL in strict tables. - -- For that reason, we use a value of -1 to indicate that it is the root message (not in editHistory) - editHistoryIndex INTEGER NOT NULL, - attachmentType TEXT NOT NULL, -- 'long-message' | 'quote' | 'attachment' | 'preview' | 'contact' | 'sticker' - orderInMessage INTEGER NOT NULL, - conversationId TEXT NOT NULL, - sentAt INTEGER NOT NULL, - clientUuid TEXT, - size INTEGER NOT NULL, - contentType TEXT NOT NULL, - path TEXT, - plaintextHash TEXT, - localKey TEXT, - caption TEXT, - fileName TEXT, - blurHash TEXT, - height INTEGER, - width INTEGER, - digest TEXT, - key TEXT, - iv TEXT, - downloadPath TEXT, - version INTEGER, - incrementalMac TEXT, - incrementalMacChunkSize INTEGER, - transitCdnKey TEXT, - transitCdnNumber INTEGER, - transitCdnUploadTimestamp INTEGER, - backupMediaName TEXT, - backupCdnNumber INTEGER, - isReencryptableToSameDigest INTEGER, - reencryptionIv TEXT, - reencryptionKey TEXT, - reencryptionDigest TEXT, - thumbnailPath TEXT, - thumbnailSize INTEGER, - thumbnailContentType TEXT, - thumbnailLocalKey TEXT, - thumbnailVersion INTEGER, - screenshotPath TEXT, - screenshotSize INTEGER, - screenshotContentType TEXT, - screenshotLocalKey TEXT, - screenshotVersion INTEGER, - backupThumbnailPath TEXT, - backupThumbnailSize INTEGER, - backupThumbnailContentType TEXT, - backupThumbnailLocalKey TEXT, - backupThumbnailVersion INTEGER, - storyTextAttachmentJson TEXT, - localBackupPath TEXT, - flags INTEGER, - error INTEGER, - wasTooBig INTEGER, - isCorrupted INTEGER, - copiedFromQuotedAttachment INTEGER, - pending INTEGER, - backfillError INTEGER, - PRIMARY KEY (messageId, editHistoryIndex, attachmentType, orderInMessage) - ) STRICT; - `); - - // The following indexes were removed in migration 1370 - - // db.exec( - // 'CREATE INDEX message_attachments_messageId - // ON message_attachments (messageId);' - // ); - // db.exec( - // 'CREATE INDEX message_attachments_plaintextHash - // ON message_attachments (plaintextHash);' - // ); - // db.exec( - // 'CREATE INDEX message_attachments_path - // ON message_attachments (path);' - // ); - // db.exec( - // 'CREATE INDEX message_attachments_all_thumbnailPath - // ON message_attachments (thumbnailPath);' - // ); - // db.exec( - // 'CREATE INDEX message_attachments_all_screenshotPath - // ON message_attachments (screenshotPath);' - // ); - // db.exec( - // 'CREATE INDEX message_attachments_all_backupThumbnailPath - // ON message_attachments (backupThumbnailPath);' - // ); - - db.pragma('user_version = 1360'); - })(); - - logger.info('updateToSchemaVersion1360: success!'); + // db.exec( + // 'CREATE INDEX message_attachments_messageId + // ON message_attachments (messageId);' + // ); + // db.exec( + // 'CREATE INDEX message_attachments_plaintextHash + // ON message_attachments (plaintextHash);' + // ); + // db.exec( + // 'CREATE INDEX message_attachments_path + // ON message_attachments (path);' + // ); + // db.exec( + // 'CREATE INDEX message_attachments_all_thumbnailPath + // ON message_attachments (thumbnailPath);' + // ); + // db.exec( + // 'CREATE INDEX message_attachments_all_screenshotPath + // ON message_attachments (screenshotPath);' + // ); + // db.exec( + // 'CREATE INDEX message_attachments_all_backupThumbnailPath + // ON message_attachments (backupThumbnailPath);' + // ); } diff --git a/ts/sql/migrations/1370-message-attachment-indexes.ts b/ts/sql/migrations/1370-message-attachment-indexes.ts index 044ffe15df4..8ba33999878 100644 --- a/ts/sql/migrations/1370-message-attachment-indexes.ts +++ b/ts/sql/migrations/1370-message-attachment-indexes.ts @@ -1,31 +1,15 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { LoggerType } from '../../types/Logging'; import type { WritableDB } from '../Interface'; -export const version = 1370; - -export function updateToSchemaVersion1370( - currentVersion: number, - db: WritableDB, - logger: LoggerType -): void { - if (currentVersion >= 1370) { - return; - } - - db.transaction(() => { - db.exec(` - DROP INDEX IF EXISTS message_attachments_messageId; - DROP INDEX IF EXISTS message_attachments_plaintextHash; - DROP INDEX IF EXISTS message_attachments_path; - DROP INDEX IF EXISTS message_attachments_all_thumbnailPath; - DROP INDEX IF EXISTS message_attachments_all_screenshotPath; - DROP INDEX IF EXISTS message_attachments_all_backupThumbnailPath; - `); - db.pragma('user_version = 1370'); - })(); - - logger.info('updateToSchemaVersion1370: success!'); +export default function updateToSchemaVersion1370(db: WritableDB): void { + db.exec(` + DROP INDEX IF EXISTS message_attachments_messageId; + DROP INDEX IF EXISTS message_attachments_plaintextHash; + DROP INDEX IF EXISTS message_attachments_path; + DROP INDEX IF EXISTS message_attachments_all_thumbnailPath; + DROP INDEX IF EXISTS message_attachments_all_screenshotPath; + DROP INDEX IF EXISTS message_attachments_all_backupThumbnailPath; + `); } diff --git a/ts/sql/migrations/1380-donation-receipts.ts b/ts/sql/migrations/1380-donation-receipts.ts index 06af5722b72..2203d1b5306 100644 --- a/ts/sql/migrations/1380-donation-receipts.ts +++ b/ts/sql/migrations/1380-donation-receipts.ts @@ -1,35 +1,19 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { LoggerType } from '../../types/Logging'; import type { WritableDB } from '../Interface'; -export const version = 1380; +export default function updateToSchemaVersion1380(db: WritableDB): void { + db.exec(` + CREATE TABLE donationReceipts( + id TEXT NOT NULL PRIMARY KEY, + currencyType TEXT NOT NULL, + paymentAmount INTEGER NOT NULL, + paymentDetailJson TEXT NOT NULL, + paymentType TEXT NOT NULL, + timestamp INTEGER NOT NULL + ) STRICT; -export function updateToSchemaVersion1380( - currentVersion: number, - db: WritableDB, - logger: LoggerType -): void { - if (currentVersion >= 1380) { - return; - } - - db.transaction(() => { - db.exec(` - CREATE TABLE donationReceipts( - id TEXT NOT NULL PRIMARY KEY, - currencyType TEXT NOT NULL, - paymentAmount INTEGER NOT NULL, - paymentDetailJson TEXT NOT NULL, - paymentType TEXT NOT NULL, - timestamp INTEGER NOT NULL - ) STRICT; - - CREATE INDEX donationReceipts_byTimestamp on donationReceipts(timestamp); - `); - db.pragma('user_version = 1380'); - })(); - - logger.info('updateToSchemaVersion1380: success!'); + CREATE INDEX donationReceipts_byTimestamp on donationReceipts(timestamp); + `); } diff --git a/ts/sql/migrations/1390-attachment-download-keys.ts b/ts/sql/migrations/1390-attachment-download-keys.ts index 5e7314fb013..aaf6281fa2e 100644 --- a/ts/sql/migrations/1390-attachment-download-keys.ts +++ b/ts/sql/migrations/1390-attachment-download-keys.ts @@ -1,53 +1,36 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { LoggerType } from '../../types/Logging'; import { type WritableDB } from '../Interface'; -export const version = 1390; +export default function updateToSchemaVersion1390(db: WritableDB): void { + // TODO: DESKTOP-8879 Digest column is only used for deduplication purposes; here we + // genericize its name to attachmentSignature to allow jobs to be added with + // plaintextHash and no digest + db.exec(` + ALTER TABLE attachment_downloads + RENAME COLUMN digest TO attachmentSignature; + `); -export function updateToSchemaVersion1390( - currentVersion: number, - db: WritableDB, - logger: LoggerType -): void { - if (currentVersion >= 1390) { - return; - } + // We no longer these need columns due to the new mediaName derivation + db.exec(` + ALTER TABLE message_attachments + DROP COLUMN iv; + ALTER TABLE message_attachments + DROP COLUMN isReencryptableToSameDigest; + ALTER TABLE message_attachments + DROP COLUMN reencryptionIv; + ALTER TABLE message_attachments + DROP COLUMN reencryptionKey; + ALTER TABLE message_attachments + DROP COLUMN reencryptionDigest; + ALTER TABLE message_attachments + DROP COLUMN backupMediaName; + `); - db.transaction(() => { - // TODO: DESKTOP-8879 Digest column is only used for deduplication purposes; here we - // genericize its name to attachmentSignature to allow jobs to be added with - // plaintextHash and no digest - db.exec(` - ALTER TABLE attachment_downloads - RENAME COLUMN digest TO attachmentSignature; - `); - - // We no longer these need columns due to the new mediaName derivation - db.exec(` - ALTER TABLE message_attachments - DROP COLUMN iv; - ALTER TABLE message_attachments - DROP COLUMN isReencryptableToSameDigest; - ALTER TABLE message_attachments - DROP COLUMN reencryptionIv; - ALTER TABLE message_attachments - DROP COLUMN reencryptionKey; - ALTER TABLE message_attachments - DROP COLUMN reencryptionDigest; - ALTER TABLE message_attachments - DROP COLUMN backupMediaName; - `); - - // Because mediaName has changed, backupCdnNumber is no longer accurate - db.exec(` - UPDATE message_attachments - SET backupCdnNumber = NULL; - `); - - db.pragma('user_version = 1390'); - })(); - - logger.info('updateToSchemaVersion1390: success!'); + // Because mediaName has changed, backupCdnNumber is no longer accurate + db.exec(` + UPDATE message_attachments + SET backupCdnNumber = NULL; + `); } diff --git a/ts/sql/migrations/1400-simplify-receipts.ts b/ts/sql/migrations/1400-simplify-receipts.ts index 7d0ffa69b73..fb7b03f28a2 100644 --- a/ts/sql/migrations/1400-simplify-receipts.ts +++ b/ts/sql/migrations/1400-simplify-receipts.ts @@ -1,27 +1,11 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { LoggerType } from '../../types/Logging'; import type { WritableDB } from '../Interface'; -export const version = 1400; - -export function updateToSchemaVersion1400( - currentVersion: number, - db: WritableDB, - logger: LoggerType -): void { - if (currentVersion >= 1400) { - return; - } - - db.transaction(() => { - db.exec(` - ALTER TABLE donationReceipts DROP COLUMN paymentDetailJson; - ALTER TABLE donationReceipts DROP COLUMN paymentType; - `); - db.pragma('user_version = 1400'); - })(); - - logger.info('updateToSchemaVersion1400: success!'); +export default function updateToSchemaVersion1400(db: WritableDB): void { + db.exec(` + ALTER TABLE donationReceipts DROP COLUMN paymentDetailJson; + ALTER TABLE donationReceipts DROP COLUMN paymentType; + `); } diff --git a/ts/sql/migrations/1410-remove-wallpaper.ts b/ts/sql/migrations/1410-remove-wallpaper.ts index 0e91d48631a..be55616f500 100644 --- a/ts/sql/migrations/1410-remove-wallpaper.ts +++ b/ts/sql/migrations/1410-remove-wallpaper.ts @@ -1,41 +1,24 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { LoggerType } from '../../types/Logging'; import { type WritableDB } from '../Interface'; -export const version = 1410; +export default function updateToSchemaVersion1410(db: WritableDB): void { + db.exec(` + UPDATE conversations + SET json = json_remove(json, + '$.wallpaperPreset', + '$.wallpaperPhotoPointerBase64', + '$.dimWallpaperInDarkMode', + '$.autoBubbleColor' + ); -export function updateToSchemaVersion1410( - currentVersion: number, - db: WritableDB, - logger: LoggerType -): void { - if (currentVersion >= 1410) { - return; - } - - db.transaction(() => { - db.exec(` - UPDATE conversations - SET json = json_remove(json, - '$.wallpaperPreset', - '$.wallpaperPhotoPointerBase64', - '$.dimWallpaperInDarkMode', - '$.autoBubbleColor' - ); - - DELETE FROM items - WHERE id IN ( - 'defaultWallpaperPhotoPointer', - 'defaultWallpaperPreset', - 'defaultDimWallpaperInDarkMode', - 'defaultAutoBubbleColor' - ); - `); - - db.pragma('user_version = 1410'); - })(); - - logger.info('updateToSchemaVersion1410: success!'); + DELETE FROM items + WHERE id IN ( + 'defaultWallpaperPhotoPointer', + 'defaultWallpaperPreset', + 'defaultDimWallpaperInDarkMode', + 'defaultAutoBubbleColor' + ); + `); } diff --git a/ts/sql/migrations/1420-backup-downloads.ts b/ts/sql/migrations/1420-backup-downloads.ts index 7525353e0d9..392aac878a8 100644 --- a/ts/sql/migrations/1420-backup-downloads.ts +++ b/ts/sql/migrations/1420-backup-downloads.ts @@ -1,70 +1,53 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { LoggerType } from '../../types/Logging'; import { AttachmentDownloadSource, type WritableDB } from '../Interface'; -export const version = 1420; +export default function updateToSchemaVersion1420(db: WritableDB): void { + db.exec(` + ALTER TABLE attachment_downloads + ADD COLUMN originalSource TEXT NOT NULL DEFAULT ${AttachmentDownloadSource.STANDARD}; -export function updateToSchemaVersion1420( - currentVersion: number, - db: WritableDB, - logger: LoggerType -): void { - if (currentVersion >= 1420) { - return; - } + UPDATE attachment_downloads + SET originalSource = source; + `); - db.transaction(() => { - db.exec(` - ALTER TABLE attachment_downloads - ADD COLUMN originalSource TEXT NOT NULL DEFAULT ${AttachmentDownloadSource.STANDARD}; - - UPDATE attachment_downloads - SET originalSource = source; - `); + db.exec(` + CREATE TABLE attachment_downloads_backup_stats ( + id INTEGER PRIMARY KEY CHECK (id = 0), + totalBytes INTEGER NOT NULL, + completedBytes INTEGER NOT NULL + ) STRICT; - db.exec(` - CREATE TABLE attachment_downloads_backup_stats ( - id INTEGER PRIMARY KEY CHECK (id = 0), - totalBytes INTEGER NOT NULL, - completedBytes INTEGER NOT NULL - ) STRICT; + INSERT INTO attachment_downloads_backup_stats + (id, totalBytes, completedBytes) + VALUES + (0, 0, 0); - INSERT INTO attachment_downloads_backup_stats - (id, totalBytes, completedBytes) - VALUES - (0, 0, 0); + CREATE TRIGGER attachment_downloads_backup_job_insert + AFTER INSERT ON attachment_downloads + WHEN NEW.originalSource = 'backup_import' + BEGIN + UPDATE attachment_downloads_backup_stats SET + totalBytes = totalBytes + NEW.ciphertextSize; + END; - CREATE TRIGGER attachment_downloads_backup_job_insert - AFTER INSERT ON attachment_downloads - WHEN NEW.originalSource = 'backup_import' - BEGIN - UPDATE attachment_downloads_backup_stats SET - totalBytes = totalBytes + NEW.ciphertextSize; - END; - - CREATE TRIGGER attachment_downloads_backup_job_update - AFTER UPDATE OF ciphertextSize ON attachment_downloads - WHEN NEW.originalSource = 'backup_import' - BEGIN - UPDATE attachment_downloads_backup_stats SET - totalBytes = MAX(0, totalBytes - OLD.ciphertextSize + NEW.ciphertextSize) - WHERE id = 0; - END; + CREATE TRIGGER attachment_downloads_backup_job_update + AFTER UPDATE OF ciphertextSize ON attachment_downloads + WHEN NEW.originalSource = 'backup_import' + BEGIN + UPDATE attachment_downloads_backup_stats SET + totalBytes = MAX(0, totalBytes - OLD.ciphertextSize + NEW.ciphertextSize) + WHERE id = 0; + END; - CREATE TRIGGER attachment_downloads_backup_job_delete - AFTER DELETE ON attachment_downloads - WHEN OLD.originalSource = 'backup_import' - BEGIN - UPDATE attachment_downloads_backup_stats SET - completedBytes = completedBytes + OLD.ciphertextSize - WHERE id = 0; - END; - `); - - db.pragma('user_version = 1420'); - })(); - - logger.info('updateToSchemaVersion1420: success!'); + CREATE TRIGGER attachment_downloads_backup_job_delete + AFTER DELETE ON attachment_downloads + WHEN OLD.originalSource = 'backup_import' + BEGIN + UPDATE attachment_downloads_backup_stats SET + completedBytes = completedBytes + OLD.ciphertextSize + WHERE id = 0; + END; + `); } diff --git a/ts/sql/migrations/41-uuid-keys.ts b/ts/sql/migrations/41-uuid-keys.ts index 8d0d38c51d8..dce7a02881a 100644 --- a/ts/sql/migrations/41-uuid-keys.ts +++ b/ts/sql/migrations/41-uuid-keys.ts @@ -25,22 +25,17 @@ export function getOurUuid(db: ReadableDB): string | undefined { } export default function updateToSchemaVersion41( - currentVersion: number, db: WritableDB, logger: LoggerType ): void { - if (currentVersion >= 41) { - return; - } - const getConversationUuid = db.prepare( ` - SELECT uuid - FROM - conversations - WHERE - id = $conversationId - `, + SELECT uuid + FROM + conversations + WHERE + id = $conversationId + `, { pluck: true, } @@ -377,70 +372,61 @@ export default function updateToSchemaVersion41( logger.info(`Migrated ${migrated} identity keys`); }; - db.transaction(() => { - db.exec( - ` - -- Change type of 'id' column from INTEGER to STRING + db.exec( + ` + -- Change type of 'id' column from INTEGER to STRING - ALTER TABLE preKeys - RENAME TO old_preKeys; + ALTER TABLE preKeys + RENAME TO old_preKeys; - ALTER TABLE signedPreKeys - RENAME TO old_signedPreKeys; + ALTER TABLE signedPreKeys + RENAME TO old_signedPreKeys; - CREATE TABLE preKeys( - id STRING PRIMARY KEY ASC, - json TEXT - ); - CREATE TABLE signedPreKeys( - id STRING PRIMARY KEY ASC, - json TEXT - ); - - -- sqlite handles the type conversion - INSERT INTO preKeys SELECT * FROM old_preKeys; - INSERT INTO signedPreKeys SELECT * FROM old_signedPreKeys; - - DROP TABLE old_preKeys; - DROP TABLE old_signedPreKeys; - - -- Alter sessions - - ALTER TABLE sessions - ADD COLUMN ourUuid STRING; - - ALTER TABLE sessions - ADD COLUMN uuid STRING; - ` + CREATE TABLE preKeys( + id STRING PRIMARY KEY ASC, + json TEXT + ); + CREATE TABLE signedPreKeys( + id STRING PRIMARY KEY ASC, + json TEXT ); - const ourUuid = getOurUuid(db); + -- sqlite handles the type conversion + INSERT INTO preKeys SELECT * FROM old_preKeys; + INSERT INTO signedPreKeys SELECT * FROM old_signedPreKeys; - if (!isValidUuid(ourUuid)) { - const deleteCount = clearSessionsAndKeys(); + DROP TABLE old_preKeys; + DROP TABLE old_signedPreKeys; - if (deleteCount > 0) { - logger.error( - 'updateToSchemaVersion41: no uuid is available, ' + - `erased ${deleteCount} sessions/keys` - ); - } + -- Alter sessions - db.pragma('user_version = 41'); - return; + ALTER TABLE sessions + ADD COLUMN ourUuid STRING; + + ALTER TABLE sessions + ADD COLUMN uuid STRING; + ` + ); + + const ourUuid = getOurUuid(db); + + if (!isValidUuid(ourUuid)) { + const deleteCount = clearSessionsAndKeys(); + + if (deleteCount > 0) { + logger.error(`no uuid is available, erased ${deleteCount} sessions/keys`); } - prefixKeys(ourUuid); + return; + } - updateSenderKeys(ourUuid); + prefixKeys(ourUuid); - updateSessions(ourUuid); + updateSenderKeys(ourUuid); - moveIdentityKeyToMap(ourUuid); + updateSessions(ourUuid); - updateIdentityKeys(); + moveIdentityKeyToMap(ourUuid); - db.pragma('user_version = 41'); - })(); - logger.info('updateToSchemaVersion41: success!'); + updateIdentityKeys(); } diff --git a/ts/sql/migrations/42-stale-reactions.ts b/ts/sql/migrations/42-stale-reactions.ts index e74b01d533e..99b1c231fa2 100644 --- a/ts/sql/migrations/42-stale-reactions.ts +++ b/ts/sql/migrations/42-stale-reactions.ts @@ -6,75 +6,62 @@ import type { WritableDB } from '../Interface'; import type { LoggerType } from '../../types/Logging'; export default function updateToSchemaVersion42( - currentVersion: number, db: WritableDB, logger: LoggerType ): void { - if (currentVersion >= 42) { - return; + // First, recreate messages table delete trigger with reaction support + + db.exec(` + DROP TRIGGER messages_on_delete; + + CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN + DELETE FROM messages_fts WHERE rowid = old.rowid; + DELETE FROM sendLogPayloads WHERE id IN ( + SELECT payloadId FROM sendLogMessageIds + WHERE messageId = old.id + ); + DELETE FROM reactions WHERE rowid IN ( + SELECT rowid FROM reactions + WHERE messageId = old.id + ); + END; + `); + + // Then, delete previously-orphaned reactions + + // Note: we use `pluck` here to fetch only the first column of + // returned row. + const messageIdList: Array = db + .prepare('SELECT id FROM messages ORDER BY id ASC;', { + pluck: true, + }) + .all(); + const allReactions: Array<{ + rowid: number; + messageId: string; + }> = db.prepare('SELECT rowid, messageId FROM reactions;').all(); + + const messageIds = new Set(messageIdList); + const reactionsToDelete: Array = []; + + allReactions.forEach(reaction => { + if (!messageIds.has(reaction.messageId)) { + reactionsToDelete.push(reaction.rowid); + } + }); + + function deleteReactions(rowids: ReadonlyArray, persistent: boolean) { + db.prepare( + ` + DELETE FROM reactions + WHERE rowid IN ( ${rowids.map(() => '?').join(', ')} ); + `, + { persistent } + ).run(rowids); } - db.transaction(() => { - // First, recreate messages table delete trigger with reaction support - - db.exec(` - DROP TRIGGER messages_on_delete; - - CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN - DELETE FROM messages_fts WHERE rowid = old.rowid; - DELETE FROM sendLogPayloads WHERE id IN ( - SELECT payloadId FROM sendLogMessageIds - WHERE messageId = old.id - ); - DELETE FROM reactions WHERE rowid IN ( - SELECT rowid FROM reactions - WHERE messageId = old.id - ); - END; - `); - - // Then, delete previously-orphaned reactions - - // Note: we use `pluck` here to fetch only the first column of - // returned row. - const messageIdList: Array = db - .prepare('SELECT id FROM messages ORDER BY id ASC;', { - pluck: true, - }) - .all(); - const allReactions: Array<{ - rowid: number; - messageId: string; - }> = db.prepare('SELECT rowid, messageId FROM reactions;').all(); - - const messageIds = new Set(messageIdList); - const reactionsToDelete: Array = []; - - allReactions.forEach(reaction => { - if (!messageIds.has(reaction.messageId)) { - reactionsToDelete.push(reaction.rowid); - } - }); - - function deleteReactions( - rowids: ReadonlyArray, - persistent: boolean - ) { - db.prepare( - ` - DELETE FROM reactions - WHERE rowid IN ( ${rowids.map(() => '?').join(', ')} ); - `, - { persistent } - ).run(rowids); - } - - if (reactionsToDelete.length > 0) { - logger.info(`Deleting ${reactionsToDelete.length} orphaned reactions`); - batchMultiVarQuery(db, reactionsToDelete, deleteReactions); - } - - db.pragma('user_version = 42'); - })(); - logger.info('updateToSchemaVersion42: success!'); + if (reactionsToDelete.length > 0) { + logger.info(`Deleting ${reactionsToDelete.length} orphaned reactions`); + batchMultiVarQuery(db, reactionsToDelete, deleteReactions); + } } diff --git a/ts/sql/migrations/43-gv2-uuid.ts b/ts/sql/migrations/43-gv2-uuid.ts index bb8a476274a..a78919809c1 100644 --- a/ts/sql/migrations/43-gv2-uuid.ts +++ b/ts/sql/migrations/43-gv2-uuid.ts @@ -33,14 +33,9 @@ type ConversationType = Readonly<{ }>; export default function updateToSchemaVersion43( - currentVersion: number, db: WritableDB, logger: LoggerType ): void { - if (currentVersion >= 43) { - return; - } - type LegacyPendingMemberType = { addedByUserId?: string; conversationId: string; @@ -117,8 +112,7 @@ export default function updateToSchemaVersion43( }); if (!uuid) { logger.warn( - `updateToSchemaVersion43: ${logId}.${key} UUID not found ` + - `for ${member.conversationId}` + `${logId}.${key} UUID not found for ${member.conversationId}` ); return undefined; } @@ -158,15 +152,14 @@ export default function updateToSchemaVersion43( if (oldValue.length !== 0) { logger.info( - `updateToSchemaVersion43: migrated ${oldValue.length} ${key} ` + + `migrated ${oldValue.length} ${key} ` + `entries to ${newValue.length} for ${logId}` ); } if (addedByCount > 0) { logger.info( - `updateToSchemaVersion43: migrated ${addedByCount} addedByUserId ` + - `in ${key} for ${logId}` + `migrated ${addedByCount} addedByUserId in ${key} for ${logId}` ); } } @@ -286,7 +279,7 @@ export default function updateToSchemaVersion43( } if (!newValue) { logger.warn( - `updateToSchemaVersion43: ${id}.groupV2Change.details.${key} ` + + `${id}.groupV2Change.details.${key} ` + `UUID not found for ${oldValue}` ); return undefined; @@ -342,7 +335,7 @@ export default function updateToSchemaVersion43( if (!uuid) { logger.warn( - `updateToSchemaVersion43: ${id}.invitedGV2Members UUID ` + + `${id}.invitedGV2Members UUID ` + `not found for ${conversationId}` ); return undefined; @@ -390,44 +383,35 @@ export default function updateToSchemaVersion43( return true; }; - db.transaction(() => { - const allConversations = db - .prepare( - ` - SELECT json - FROM conversations - ORDER BY id ASC; - `, - { pluck: true } - ) - .all() - .map(json => jsonToObject(json)); + const allConversations = db + .prepare( + ` + SELECT json + FROM conversations + ORDER BY id ASC; + `, + { pluck: true } + ) + .all() + .map(json => jsonToObject(json)); - logger.info( - 'updateToSchemaVersion43: About to iterate through ' + - `${allConversations.length} conversations` - ); + logger.info( + `About to iterate through ${allConversations.length} conversations` + ); - for (const convo of allConversations) { - upgradeConversation(convo); + for (const convo of allConversations) { + upgradeConversation(convo); + } + + const messageCount = getCountFromTable(db, 'messages'); + logger.info(`About to iterate through ${messageCount} messages`); + + let updatedCount = 0; + for (const message of new TableIterator(db, 'messages')) { + if (upgradeMessage(message)) { + updatedCount += 1; } + } - const messageCount = getCountFromTable(db, 'messages'); - logger.info( - 'updateToSchemaVersion43: About to iterate through ' + - `${messageCount} messages` - ); - - let updatedCount = 0; - for (const message of new TableIterator(db, 'messages')) { - if (upgradeMessage(message)) { - updatedCount += 1; - } - } - - logger.info(`updateToSchemaVersion43: Updated ${updatedCount} messages`); - - db.pragma('user_version = 43'); - })(); - logger.info('updateToSchemaVersion43: success!'); + logger.info(`Updated ${updatedCount} messages`); } diff --git a/ts/sql/migrations/44-badges.ts b/ts/sql/migrations/44-badges.ts index 64a60a2cecb..2f7e506387c 100644 --- a/ts/sql/migrations/44-badges.ts +++ b/ts/sql/migrations/44-badges.ts @@ -3,41 +3,25 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; - -export default function updateToSchemaVersion44( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 44) { - return; - } - - db.transaction(() => { - db.exec( - ` - CREATE TABLE badges( - id TEXT PRIMARY KEY, - category TEXT NOT NULL, - name TEXT NOT NULL, - descriptionTemplate TEXT NOT NULL - ); - - CREATE TABLE badgeImageFiles( - badgeId TEXT REFERENCES badges(id) - ON DELETE CASCADE - ON UPDATE CASCADE, - 'order' INTEGER NOT NULL, - url TEXT NOT NULL, - localPath TEXT, - theme TEXT NOT NULL - ); - ` +export default function updateToSchemaVersion44(db: Database): void { + db.exec( + ` + CREATE TABLE badges( + id TEXT PRIMARY KEY, + category TEXT NOT NULL, + name TEXT NOT NULL, + descriptionTemplate TEXT NOT NULL ); - db.pragma('user_version = 44'); - })(); - - logger.info('updateToSchemaVersion44: success!'); + CREATE TABLE badgeImageFiles( + badgeId TEXT REFERENCES badges(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + 'order' INTEGER NOT NULL, + url TEXT NOT NULL, + localPath TEXT, + theme TEXT NOT NULL + ); + ` + ); } diff --git a/ts/sql/migrations/45-stories.ts b/ts/sql/migrations/45-stories.ts index c864536b348..05130483b7b 100644 --- a/ts/sql/migrations/45-stories.ts +++ b/ts/sql/migrations/45-stories.ts @@ -3,135 +3,119 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion45(db: Database): void { + db.exec( + ` + --- Add column to messages table -export default function updateToSchemaVersion45( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 45) { - return; - } + ALTER TABLE messages ADD COLUMN storyId STRING; - db.transaction(() => { - db.exec( - ` - --- Add column to messages table + --- Update important message indices - ALTER TABLE messages ADD COLUMN storyId STRING; + DROP INDEX messages_conversation; + CREATE INDEX messages_conversation ON messages + (conversationId, type, storyId, received_at); - --- Update important message indices + DROP INDEX messages_unread; + CREATE INDEX messages_unread ON messages + (conversationId, readStatus, type, storyId) WHERE readStatus IS NOT NULL; - DROP INDEX messages_conversation; - CREATE INDEX messages_conversation ON messages - (conversationId, type, storyId, received_at); + --- Update attachment indices for All Media views - DROP INDEX messages_unread; - CREATE INDEX messages_unread ON messages - (conversationId, readStatus, type, storyId) WHERE readStatus IS NOT NULL; + DROP INDEX messages_hasAttachments; + CREATE INDEX messages_hasAttachments + ON messages (conversationId, hasAttachments, received_at) + WHERE type IS NOT 'story' AND storyId IS NULL; - --- Update attachment indices for All Media views + DROP INDEX messages_hasFileAttachments; + CREATE INDEX messages_hasFileAttachments + ON messages (conversationId, hasFileAttachments, received_at) + WHERE type IS NOT 'story' AND storyId IS NULL; - DROP INDEX messages_hasAttachments; - CREATE INDEX messages_hasAttachments - ON messages (conversationId, hasAttachments, received_at) - WHERE type IS NOT 'story' AND storyId IS NULL; + DROP INDEX messages_hasVisualMediaAttachments; + CREATE INDEX messages_hasVisualMediaAttachments + ON messages (conversationId, hasVisualMediaAttachments, received_at) + WHERE type IS NOT 'story' AND storyId IS NULL; - DROP INDEX messages_hasFileAttachments; - CREATE INDEX messages_hasFileAttachments - ON messages (conversationId, hasFileAttachments, received_at) - WHERE type IS NOT 'story' AND storyId IS NULL; + --- Message insert/update triggers to exclude stories and story replies - DROP INDEX messages_hasVisualMediaAttachments; - CREATE INDEX messages_hasVisualMediaAttachments - ON messages (conversationId, hasVisualMediaAttachments, received_at) - WHERE type IS NOT 'story' AND storyId IS NULL; + DROP TRIGGER messages_on_insert; + -- Note: any changes to this trigger must be reflected in + -- Server.ts: enableMessageInsertTriggersAndBackfill + CREATE TRIGGER messages_on_insert AFTER INSERT ON messages + WHEN new.isViewOnce IS NOT 1 AND new.storyId IS NULL + BEGIN + INSERT INTO messages_fts + (rowid, body) + VALUES + (new.rowid, new.body); + END; - --- Message insert/update triggers to exclude stories and story replies + DROP TRIGGER messages_on_update; + CREATE TRIGGER messages_on_update AFTER UPDATE ON messages + WHEN + (new.body IS NULL OR old.body IS NOT new.body) AND + new.isViewOnce IS NOT 1 AND new.storyId IS NULL + BEGIN + DELETE FROM messages_fts WHERE rowid = old.rowid; + INSERT INTO messages_fts + (rowid, body) + VALUES + (new.rowid, new.body); + END; - DROP TRIGGER messages_on_insert; - -- Note: any changes to this trigger must be reflected in - -- Server.ts: enableMessageInsertTriggersAndBackfill - CREATE TRIGGER messages_on_insert AFTER INSERT ON messages - WHEN new.isViewOnce IS NOT 1 AND new.storyId IS NULL - BEGIN - INSERT INTO messages_fts - (rowid, body) - VALUES - (new.rowid, new.body); - END; + --- Update delete trigger to remove storyReads - DROP TRIGGER messages_on_update; - CREATE TRIGGER messages_on_update AFTER UPDATE ON messages - WHEN - (new.body IS NULL OR old.body IS NOT new.body) AND - new.isViewOnce IS NOT 1 AND new.storyId IS NULL - BEGIN - DELETE FROM messages_fts WHERE rowid = old.rowid; - INSERT INTO messages_fts - (rowid, body) - VALUES - (new.rowid, new.body); - END; - - --- Update delete trigger to remove storyReads - - --- Note: for future updates to this trigger, be sure to update Server.ts/removeAll() - --- (it deletes and re-adds this trigger for performance) - DROP TRIGGER messages_on_delete; - CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN - DELETE FROM messages_fts WHERE rowid = old.rowid; - DELETE FROM sendLogPayloads WHERE id IN ( - SELECT payloadId FROM sendLogMessageIds - WHERE messageId = old.id - ); - DELETE FROM reactions WHERE rowid IN ( - SELECT rowid FROM reactions - WHERE messageId = old.id - ); - DELETE FROM storyReads WHERE storyId = old.storyId; - END; - - --- Story Read History - - CREATE TABLE storyReads ( - authorId STRING NOT NULL, - conversationId STRING NOT NULL, - storyId STRING NOT NULL, - storyReadDate NUMBER NOT NULL, - - PRIMARY KEY (authorId, storyId) + --- Note: for future updates to this trigger, be sure to update Server.ts/removeAll() + --- (it deletes and re-adds this trigger for performance) + DROP TRIGGER messages_on_delete; + CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN + DELETE FROM messages_fts WHERE rowid = old.rowid; + DELETE FROM sendLogPayloads WHERE id IN ( + SELECT payloadId FROM sendLogMessageIds + WHERE messageId = old.id ); - - CREATE INDEX storyReads_data ON storyReads ( - storyReadDate, authorId, conversationId + DELETE FROM reactions WHERE rowid IN ( + SELECT rowid FROM reactions + WHERE messageId = old.id ); + DELETE FROM storyReads WHERE storyId = old.storyId; + END; - --- Story Distribution Lists + --- Story Read History - CREATE TABLE storyDistributions( - id STRING PRIMARY KEY NOT NULL, - name TEXT, + CREATE TABLE storyReads ( + authorId STRING NOT NULL, + conversationId STRING NOT NULL, + storyId STRING NOT NULL, + storyReadDate NUMBER NOT NULL, - avatarUrlPath TEXT, - avatarKey BLOB, - senderKeyInfoJson STRING - ); - - CREATE TABLE storyDistributionMembers( - listId STRING NOT NULL REFERENCES storyDistributions(id) - ON DELETE CASCADE - ON UPDATE CASCADE, - uuid STRING NOT NULL, - - PRIMARY KEY (listId, uuid) - ) - ` + PRIMARY KEY (authorId, storyId) ); - db.pragma('user_version = 45'); - })(); + CREATE INDEX storyReads_data ON storyReads ( + storyReadDate, authorId, conversationId + ); - logger.info('updateToSchemaVersion45: success!'); + --- Story Distribution Lists + + CREATE TABLE storyDistributions( + id STRING PRIMARY KEY NOT NULL, + name TEXT, + + avatarUrlPath TEXT, + avatarKey BLOB, + senderKeyInfoJson STRING + ); + + CREATE TABLE storyDistributionMembers( + listId STRING NOT NULL REFERENCES storyDistributions(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + uuid STRING NOT NULL, + + PRIMARY KEY (listId, uuid) + ) + ` + ); } diff --git a/ts/sql/migrations/46-optimize-stories.ts b/ts/sql/migrations/46-optimize-stories.ts index 12be9450969..1a840ccb804 100644 --- a/ts/sql/migrations/46-optimize-stories.ts +++ b/ts/sql/migrations/46-optimize-stories.ts @@ -3,38 +3,22 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion46(db: Database): void { + db.exec( + ` + --- Add column to messages table -export default function updateToSchemaVersion46( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 46) { - return; - } + ALTER TABLE messages + ADD COLUMN + isStory INTEGER + GENERATED ALWAYS + AS (type = 'story'); - db.transaction(() => { - db.exec( - ` - --- Add column to messages table + --- Update important message indices - ALTER TABLE messages - ADD COLUMN - isStory INTEGER - GENERATED ALWAYS - AS (type = 'story'); - - --- Update important message indices - - DROP INDEX messages_conversation; - CREATE INDEX messages_conversation ON messages - (conversationId, isStory, storyId, received_at, sent_at); - ` - ); - - db.pragma('user_version = 46'); - })(); - - logger.info('updateToSchemaVersion46: success!'); + DROP INDEX messages_conversation; + CREATE INDEX messages_conversation ON messages + (conversationId, isStory, storyId, received_at, sent_at); + ` + ); } diff --git a/ts/sql/migrations/47-further-optimize.ts b/ts/sql/migrations/47-further-optimize.ts index 752fe0774a7..df6a2a4d998 100644 --- a/ts/sql/migrations/47-further-optimize.ts +++ b/ts/sql/migrations/47-further-optimize.ts @@ -1,139 +1,128 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { LoggerType } from '../../types/Logging'; import { getOurUuid } from './41-uuid-keys'; +import type { LoggerType } from '../../types/Logging'; import type { WritableDB } from '../Interface'; export default function updateToSchemaVersion47( - currentVersion: number, db: WritableDB, logger: LoggerType ): void { - if (currentVersion >= 47) { - return; + db.exec( + ` + DROP INDEX messages_conversation; + + ALTER TABLE messages + DROP COLUMN isStory; + ALTER TABLE messages + ADD COLUMN isStory INTEGER + GENERATED ALWAYS AS (type IS 'story'); + + ALTER TABLE messages + ADD COLUMN isChangeCreatedByUs INTEGER NOT NULL DEFAULT 0; + + ALTER TABLE messages + ADD COLUMN shouldAffectActivity INTEGER + GENERATED ALWAYS AS ( + type IS NULL + OR + type NOT IN ( + 'change-number-notification', + 'group-v1-migration', + 'message-history-unsynced', + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change', + + 'keychange' + ) + ); + + ALTER TABLE messages + ADD COLUMN shouldAffectPreview INTEGER + GENERATED ALWAYS AS ( + type IS NULL + OR + type NOT IN ( + 'change-number-notification', + 'group-v1-migration', + 'message-history-unsynced', + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change' + ) + ); + + ALTER TABLE messages + ADD COLUMN isUserInitiatedMessage INTEGER + GENERATED ALWAYS AS ( + type IS NULL + OR + type NOT IN ( + 'change-number-notification', + 'group-v1-migration', + 'message-history-unsynced', + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change', + + 'group-v2-change', + 'keychange' + ) + ); + + ALTER TABLE messages + ADD COLUMN isTimerChangeFromSync INTEGER + GENERATED ALWAYS AS ( + json_extract(json, '$.expirationTimerUpdate.fromSync') IS 1 + ); + + ALTER TABLE messages + ADD COLUMN isGroupLeaveEvent INTEGER + GENERATED ALWAYS AS ( + type IS 'group-v2-change' AND + json_array_length(json_extract(json, '$.groupV2Change.details')) IS 1 AND + json_extract(json, '$.groupV2Change.details[0].type') IS 'member-remove' AND + json_extract(json, '$.groupV2Change.from') IS NOT NULL AND + json_extract(json, '$.groupV2Change.from') IS json_extract(json, '$.groupV2Change.details[0].uuid') + ); + + ALTER TABLE messages + ADD COLUMN isGroupLeaveEventFromOther INTEGER + GENERATED ALWAYS AS ( + isGroupLeaveEvent IS 1 + AND + isChangeCreatedByUs IS 0 + ); + + CREATE INDEX messages_conversation ON messages + (conversationId, isStory, storyId, received_at, sent_at); + + CREATE INDEX messages_preview ON messages + (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, expiresAt, received_at, sent_at); + + CREATE INDEX messages_activity ON messages + (conversationId, shouldAffectActivity, isTimerChangeFromSync, isGroupLeaveEventFromOther, received_at, sent_at); + + CREATE INDEX message_user_initiated ON messages (isUserInitiatedMessage); + ` + ); + + const ourUuid = getOurUuid(db); + if (!ourUuid) { + logger.info('our UUID not found'); + } else { + db.prepare( + ` + UPDATE messages SET + isChangeCreatedByUs = json_extract(json, '$.groupV2Change.from') IS $ourUuid; + ` + ).run({ + ourUuid, + }); } - - db.transaction(() => { - db.exec( - ` - DROP INDEX messages_conversation; - - ALTER TABLE messages - DROP COLUMN isStory; - ALTER TABLE messages - ADD COLUMN isStory INTEGER - GENERATED ALWAYS AS (type IS 'story'); - - ALTER TABLE messages - ADD COLUMN isChangeCreatedByUs INTEGER NOT NULL DEFAULT 0; - - ALTER TABLE messages - ADD COLUMN shouldAffectActivity INTEGER - GENERATED ALWAYS AS ( - type IS NULL - OR - type NOT IN ( - 'change-number-notification', - 'group-v1-migration', - 'message-history-unsynced', - 'profile-change', - 'story', - 'universal-timer-notification', - 'verified-change', - - 'keychange' - ) - ); - - ALTER TABLE messages - ADD COLUMN shouldAffectPreview INTEGER - GENERATED ALWAYS AS ( - type IS NULL - OR - type NOT IN ( - 'change-number-notification', - 'group-v1-migration', - 'message-history-unsynced', - 'profile-change', - 'story', - 'universal-timer-notification', - 'verified-change' - ) - ); - - ALTER TABLE messages - ADD COLUMN isUserInitiatedMessage INTEGER - GENERATED ALWAYS AS ( - type IS NULL - OR - type NOT IN ( - 'change-number-notification', - 'group-v1-migration', - 'message-history-unsynced', - 'profile-change', - 'story', - 'universal-timer-notification', - 'verified-change', - - 'group-v2-change', - 'keychange' - ) - ); - - ALTER TABLE messages - ADD COLUMN isTimerChangeFromSync INTEGER - GENERATED ALWAYS AS ( - json_extract(json, '$.expirationTimerUpdate.fromSync') IS 1 - ); - - ALTER TABLE messages - ADD COLUMN isGroupLeaveEvent INTEGER - GENERATED ALWAYS AS ( - type IS 'group-v2-change' AND - json_array_length(json_extract(json, '$.groupV2Change.details')) IS 1 AND - json_extract(json, '$.groupV2Change.details[0].type') IS 'member-remove' AND - json_extract(json, '$.groupV2Change.from') IS NOT NULL AND - json_extract(json, '$.groupV2Change.from') IS json_extract(json, '$.groupV2Change.details[0].uuid') - ); - - ALTER TABLE messages - ADD COLUMN isGroupLeaveEventFromOther INTEGER - GENERATED ALWAYS AS ( - isGroupLeaveEvent IS 1 - AND - isChangeCreatedByUs IS 0 - ); - - CREATE INDEX messages_conversation ON messages - (conversationId, isStory, storyId, received_at, sent_at); - - CREATE INDEX messages_preview ON messages - (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, expiresAt, received_at, sent_at); - - CREATE INDEX messages_activity ON messages - (conversationId, shouldAffectActivity, isTimerChangeFromSync, isGroupLeaveEventFromOther, received_at, sent_at); - - CREATE INDEX message_user_initiated ON messages (isUserInitiatedMessage); - ` - ); - - const ourUuid = getOurUuid(db); - if (!ourUuid) { - logger.info('updateToSchemaVersion47: our UUID not found'); - } else { - db.prepare( - ` - UPDATE messages SET - isChangeCreatedByUs = json_extract(json, '$.groupV2Change.from') IS $ourUuid; - ` - ).run({ - ourUuid, - }); - } - - db.pragma('user_version = 47'); - })(); - - logger.info('updateToSchemaVersion47: success!'); } diff --git a/ts/sql/migrations/48-fix-user-initiated-index.ts b/ts/sql/migrations/48-fix-user-initiated-index.ts index b9cd911f81e..3374eaf5a21 100644 --- a/ts/sql/migrations/48-fix-user-initiated-index.ts +++ b/ts/sql/migrations/48-fix-user-initiated-index.ts @@ -3,28 +3,12 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion48(db: Database): void { + db.exec( + ` + DROP INDEX message_user_initiated; -export default function updateToSchemaVersion48( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 48) { - return; - } - - db.transaction(() => { - db.exec( - ` - DROP INDEX message_user_initiated; - - CREATE INDEX message_user_initiated ON messages (conversationId, isUserInitiatedMessage); - ` - ); - - db.pragma('user_version = 48'); - })(); - - logger.info('updateToSchemaVersion48: success!'); + CREATE INDEX message_user_initiated ON messages (conversationId, isUserInitiatedMessage); + ` + ); } diff --git a/ts/sql/migrations/49-fix-preview-index.ts b/ts/sql/migrations/49-fix-preview-index.ts index d33aca5b3a1..240ef5b8b42 100644 --- a/ts/sql/migrations/49-fix-preview-index.ts +++ b/ts/sql/migrations/49-fix-preview-index.ts @@ -3,31 +3,15 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion49(db: Database): void { + db.exec( + ` + DROP INDEX messages_preview; -export default function updateToSchemaVersion49( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 49) { - return; - } - - db.transaction(() => { - db.exec( - ` - DROP INDEX messages_preview; - - -- Note the omitted 'expiresAt' column in the index. If it is present - -- sqlite can't ORDER BY received_at, sent_at using this index. - CREATE INDEX messages_preview ON messages - (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, received_at, sent_at); - ` - ); - - db.pragma('user_version = 49'); - })(); - - logger.info('updateToSchemaVersion49: success!'); + -- Note the omitted 'expiresAt' column in the index. If it is present + -- sqlite can't ORDER BY received_at, sent_at using this index. + CREATE INDEX messages_preview ON messages + (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, received_at, sent_at); + ` + ); } diff --git a/ts/sql/migrations/50-fix-messages-unread-index.ts b/ts/sql/migrations/50-fix-messages-unread-index.ts index 5f23e921fc5..81fd1c42aba 100644 --- a/ts/sql/migrations/50-fix-messages-unread-index.ts +++ b/ts/sql/migrations/50-fix-messages-unread-index.ts @@ -3,30 +3,14 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion50(db: Database): void { + db.exec( + ` + DROP INDEX messages_unread; -export default function updateToSchemaVersion50( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 50) { - return; - } - - db.transaction(() => { - db.exec( - ` - DROP INDEX messages_unread; - - -- Note: here we move to the modern isStory/storyId fields and add received_at/sent_at. - CREATE INDEX messages_unread ON messages - (conversationId, readStatus, isStory, storyId, received_at, sent_at) WHERE readStatus IS NOT NULL; - ` - ); - - db.pragma('user_version = 50'); - })(); - - logger.info('updateToSchemaVersion50: success!'); + -- Note: here we move to the modern isStory/storyId fields and add received_at/sent_at. + CREATE INDEX messages_unread ON messages + (conversationId, readStatus, isStory, storyId, received_at, sent_at) WHERE readStatus IS NOT NULL; + ` + ); } diff --git a/ts/sql/migrations/51-centralize-conversation-jobs.ts b/ts/sql/migrations/51-centralize-conversation-jobs.ts index 3cb1976db33..fb7afd301d1 100644 --- a/ts/sql/migrations/51-centralize-conversation-jobs.ts +++ b/ts/sql/migrations/51-centralize-conversation-jobs.ts @@ -7,102 +7,81 @@ import type { WritableDB } from '../Interface'; import { getJobsInQueue, insertJob } from '../Server'; export default function updateToSchemaVersion51( - currentVersion: number, db: WritableDB, logger: LoggerType ): void { - if (currentVersion >= 51) { - return; - } + const deleteJobsInQueue = db.prepare( + 'DELETE FROM jobs WHERE queueType = $queueType' + ); - db.transaction(() => { - const deleteJobsInQueue = db.prepare( - 'DELETE FROM jobs WHERE queueType = $queueType' - ); + // First, make sure that reactions job data has a type and conversationId + const reactionsJobs = getJobsInQueue(db, 'reactions'); + deleteJobsInQueue.run({ queueType: 'reactions' }); - // First, make sure that reactions job data has a type and conversationId - const reactionsJobs = getJobsInQueue(db, 'reactions'); - deleteJobsInQueue.run({ queueType: 'reactions' }); + const getMessageById = db.prepare( + 'SELECT conversationId FROM messages WHERE id IS ?' + ); - const getMessageById = db.prepare( - 'SELECT conversationId FROM messages WHERE id IS ?' - ); + reactionsJobs.forEach(job => { + const { data, id } = job; - reactionsJobs.forEach(job => { - const { data, id } = job; + if (!isRecord(data)) { + logger.warn(`reactions queue job ${id} was missing valid data`); + return; + } - if (!isRecord(data)) { - logger.warn( - `updateToSchemaVersion51: reactions queue job ${id} was missing valid data` - ); - return; - } + const { messageId } = data; + if (typeof messageId !== 'string') { + logger.warn(`reactions queue job ${id} had a non-string messageId`); + return; + } - const { messageId } = data; - if (typeof messageId !== 'string') { - logger.warn( - `updateToSchemaVersion51: reactions queue job ${id} had a non-string messageId` - ); - return; - } + const message = getMessageById.get([messageId]); + if (!message) { + logger.warn(`Unable to find message for reaction job ${id}`); + return; + } - const message = getMessageById.get([messageId]); - if (!message) { - logger.warn( - `updateToSchemaVersion51: Unable to find message for reaction job ${id}` - ); - return; - } + const { conversationId } = message; + if (typeof conversationId !== 'string') { + logger.warn(`reactions queue job ${id} had a non-string conversationId`); + return; + } - const { conversationId } = message; - if (typeof conversationId !== 'string') { - logger.warn( - `updateToSchemaVersion51: reactions queue job ${id} had a non-string conversationId` - ); - return; - } + const newJob = { + ...job, + queueType: 'conversation', + data: { + ...data, + type: 'Reaction', + conversationId, + }, + }; - const newJob = { - ...job, - queueType: 'conversation', - data: { - ...data, - type: 'Reaction', - conversationId, - }, - }; + insertJob(db, newJob); + }); - insertJob(db, newJob); - }); + // Then make sure all normal send job data has a type + const normalSendJobs = getJobsInQueue(db, 'normal send'); + deleteJobsInQueue.run({ queueType: 'normal send' }); - // Then make sure all normal send job data has a type - const normalSendJobs = getJobsInQueue(db, 'normal send'); - deleteJobsInQueue.run({ queueType: 'normal send' }); + normalSendJobs.forEach(job => { + const { data, id } = job; - normalSendJobs.forEach(job => { - const { data, id } = job; + if (!isRecord(data)) { + logger.warn(`normal send queue job ${id} was missing valid data`); + return; + } - if (!isRecord(data)) { - logger.warn( - `updateToSchemaVersion51: normal send queue job ${id} was missing valid data` - ); - return; - } + const newJob = { + ...job, + queueType: 'conversation', + data: { + ...data, + type: 'NormalMessage', + }, + }; - const newJob = { - ...job, - queueType: 'conversation', - data: { - ...data, - type: 'NormalMessage', - }, - }; - - insertJob(db, newJob); - }); - - db.pragma('user_version = 51'); - })(); - - logger.info('updateToSchemaVersion51: success!'); + insertJob(db, newJob); + }); } diff --git a/ts/sql/migrations/52-optimize-stories.ts b/ts/sql/migrations/52-optimize-stories.ts index a5987249fb2..47b3b98d762 100644 --- a/ts/sql/migrations/52-optimize-stories.ts +++ b/ts/sql/migrations/52-optimize-stories.ts @@ -3,36 +3,20 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion52(db: Database): void { + db.exec( + ` + -- Create indices that don't have storyId in them so that + -- '_storyIdPredicate' could be optimized. -export default function updateToSchemaVersion52( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 52) { - return; - } + -- See migration 47 + CREATE INDEX messages_conversation_no_story_id ON messages + (conversationId, isStory, received_at, sent_at); - db.transaction(() => { - db.exec( - ` - -- Create indices that don't have storyId in them so that - -- '_storyIdPredicate' could be optimized. - - -- See migration 47 - CREATE INDEX messages_conversation_no_story_id ON messages - (conversationId, isStory, received_at, sent_at); - - -- See migration 50 - CREATE INDEX messages_unread_no_story_id ON messages - (conversationId, readStatus, isStory, received_at, sent_at) - WHERE readStatus IS NOT NULL; - ` - ); - - db.pragma('user_version = 52'); - })(); - - logger.info('updateToSchemaVersion52: success!'); + -- See migration 50 + CREATE INDEX messages_unread_no_story_id ON messages + (conversationId, readStatus, isStory, received_at, sent_at) + WHERE readStatus IS NOT NULL; + ` + ); } diff --git a/ts/sql/migrations/53-gv2-banned-members.ts b/ts/sql/migrations/53-gv2-banned-members.ts index 801fe38a845..45a7971f035 100644 --- a/ts/sql/migrations/53-gv2-banned-members.ts +++ b/ts/sql/migrations/53-gv2-banned-members.ts @@ -7,14 +7,9 @@ import type { LoggerType } from '../../types/Logging'; import { jsonToObject } from '../util'; export default function updateToSchemaVersion53( - currentVersion: number, db: Database, logger: LoggerType ): void { - if (currentVersion >= 53) { - return; - } - type LegacyConversationType = { id: string; groupId: string; @@ -52,7 +47,7 @@ export default function updateToSchemaVersion53( }; logger.info( - `updateToSchemaVersion53: Updating ${logId} with ` + + `Updating ${logId} with ` + `${legacy.bannedMembersV2.length} banned members` ); @@ -64,33 +59,27 @@ export default function updateToSchemaVersion53( return true; }; - db.transaction(() => { - const allConversations = db - .prepare( - ` - SELECT json - FROM conversations - WHERE type = 'group' - ORDER BY id ASC; - `, - { pluck: true } - ) - .all() - .map(json => jsonToObject(json)); + const allConversations = db + .prepare( + ` + SELECT json + FROM conversations + WHERE type = 'group' + ORDER BY id ASC; + `, + { pluck: true } + ) + .all() + .map(json => jsonToObject(json)); - logger.info( - 'updateToSchemaVersion53: About to iterate through ' + - `${allConversations.length} conversations` - ); + logger.info( + `About to iterate through ${allConversations.length} conversations` + ); - let updated = 0; - for (const convo of allConversations) { - updated += upgradeConversation(convo) ? 1 : 0; - } + let updated = 0; + for (const convo of allConversations) { + updated += upgradeConversation(convo) ? 1 : 0; + } - logger.info(`updateToSchemaVersion53: Updated ${updated} conversations`); - - db.pragma('user_version = 53'); - })(); - logger.info('updateToSchemaVersion53: success!'); + logger.info(`Updated ${updated} conversations`); } diff --git a/ts/sql/migrations/54-unprocessed-received-at-counter.ts b/ts/sql/migrations/54-unprocessed-received-at-counter.ts index 2678deb6334..4039de6e205 100644 --- a/ts/sql/migrations/54-unprocessed-received-at-counter.ts +++ b/ts/sql/migrations/54-unprocessed-received-at-counter.ts @@ -3,25 +3,10 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; - -export default function updateToSchemaVersion54( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 54) { - return; - } - - db.transaction(() => { - db.exec( - ` - ALTER TABLE unprocessed ADD COLUMN receivedAtCounter INTEGER; - ` - ); - - db.pragma('user_version = 54'); - })(); - logger.info('updateToSchemaVersion54: success!'); +export default function updateToSchemaVersion54(db: Database): void { + db.exec( + ` + ALTER TABLE unprocessed ADD COLUMN receivedAtCounter INTEGER; + ` + ); } diff --git a/ts/sql/migrations/55-report-message-aci.ts b/ts/sql/migrations/55-report-message-aci.ts index e9aba0bd705..8dd55faf821 100644 --- a/ts/sql/migrations/55-report-message-aci.ts +++ b/ts/sql/migrations/55-report-message-aci.ts @@ -8,61 +8,45 @@ import { isRecord } from '../../util/isRecord'; import { isIterable } from '../../util/iterables'; export default function updateToSchemaVersion55( - currentVersion: number, db: WritableDB, logger: LoggerType ): void { - if (currentVersion >= 55) { - return; - } + const deleteJobsInQueue = db.prepare( + 'DELETE FROM jobs WHERE queueType = $queueType' + ); - db.transaction(() => { - const deleteJobsInQueue = db.prepare( - 'DELETE FROM jobs WHERE queueType = $queueType' - ); + // First, make sure that report spam job data has e164 and serverGuids + const reportSpamJobs = getJobsInQueue(db, 'report spam'); + deleteJobsInQueue.run({ queueType: 'report spam' }); - // First, make sure that report spam job data has e164 and serverGuids - const reportSpamJobs = getJobsInQueue(db, 'report spam'); - deleteJobsInQueue.run({ queueType: 'report spam' }); + reportSpamJobs.forEach(job => { + const { data, id } = job; - reportSpamJobs.forEach(job => { - const { data, id } = job; + if (!isRecord(data)) { + logger.warn(`report spam queue job ${id} was missing valid data`); + return; + } - if (!isRecord(data)) { - logger.warn( - `updateToSchemaVersion55: report spam queue job ${id} was missing valid data` - ); - return; - } + const { e164, serverGuids } = data; + if (typeof e164 !== 'string') { + logger.warn(`report spam queue job ${id} had a non-string e164`); + return; + } - const { e164, serverGuids } = data; - if (typeof e164 !== 'string') { - logger.warn( - `updateToSchemaVersion55: report spam queue job ${id} had a non-string e164` - ); - return; - } + if (!isIterable(serverGuids)) { + logger.warn(`report spam queue job ${id} had a non-iterable serverGuids`); + return; + } - if (!isIterable(serverGuids)) { - logger.warn( - `updateToSchemaVersion55: report spam queue job ${id} had a non-iterable serverGuids` - ); - return; - } + const newJob = { + ...job, + queueType: 'report spam', + data: { + uuid: e164, // this looks odd, but they are both strings and interchangeable in the server API + serverGuids, + }, + }; - const newJob = { - ...job, - queueType: 'report spam', - data: { - uuid: e164, // this looks odd, but they are both strings and interchangeable in the server API - serverGuids, - }, - }; - - insertJob(db, newJob); - }); - - db.pragma('user_version = 55'); - })(); - logger.info('updateToSchemaVersion55: success!'); + insertJob(db, newJob); + }); } diff --git a/ts/sql/migrations/56-add-unseen-to-message.ts b/ts/sql/migrations/56-add-unseen-to-message.ts index 4e79451ef46..45b74321eb3 100644 --- a/ts/sql/migrations/56-add-unseen-to-message.ts +++ b/ts/sql/migrations/56-add-unseen-to-message.ts @@ -5,69 +5,37 @@ import type { Database } from '@signalapp/sqlcipher'; import { ReadStatus } from '../../messages/MessageReadStatus'; import { SeenStatus } from '../../MessageSeenStatus'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion56(db: Database): void { + db.exec( + ` + --- Add column to messages table -export default function updateToSchemaVersion56( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 56) { - return; - } + ALTER TABLE messages ADD COLUMN seenStatus NUMBER default 0; - db.transaction(() => { - db.exec( - ` - --- Add column to messages table + --- Add index to make searching on this field easy - ALTER TABLE messages ADD COLUMN seenStatus NUMBER default 0; + CREATE INDEX messages_unseen_no_story ON messages + (conversationId, seenStatus, isStory, received_at, sent_at) + WHERE + seenStatus IS NOT NULL; - --- Add index to make searching on this field easy + CREATE INDEX messages_unseen_with_story ON messages + (conversationId, seenStatus, isStory, storyId, received_at, sent_at) + WHERE + seenStatus IS NOT NULL; - CREATE INDEX messages_unseen_no_story ON messages - (conversationId, seenStatus, isStory, received_at, sent_at) - WHERE - seenStatus IS NOT NULL; + --- Update seenStatus to UnseenStatus.Unseen for certain messages + --- (NULL included because 'timer-notification' in 1:1 convos had type = NULL) - CREATE INDEX messages_unseen_with_story ON messages - (conversationId, seenStatus, isStory, storyId, received_at, sent_at) - WHERE - seenStatus IS NOT NULL; - - --- Update seenStatus to UnseenStatus.Unseen for certain messages - --- (NULL included because 'timer-notification' in 1:1 convos had type = NULL) - - UPDATE messages - SET - seenStatus = ${SeenStatus.Unseen} - WHERE - readStatus = ${ReadStatus.Unread} AND - ( - type IS NULL - OR - type IN ( - 'call-history', - 'change-number-notification', - 'chat-session-refreshed', - 'delivery-issue', - 'group', - 'incoming', - 'keychange', - 'timer-notification', - 'verified-change' - ) - ); - - --- Set readStatus to ReadStatus.Read for all other message types - - UPDATE messages - SET - readStatus = ${ReadStatus.Read} - WHERE - readStatus = ${ReadStatus.Unread} AND - type IS NOT NULL AND - type NOT IN ( + UPDATE messages + SET + seenStatus = ${SeenStatus.Unseen} + WHERE + readStatus = ${ReadStatus.Unread} AND + ( + type IS NULL + OR + type IN ( 'call-history', 'change-number-notification', 'chat-session-refreshed', @@ -77,12 +45,28 @@ export default function updateToSchemaVersion56( 'keychange', 'timer-notification', 'verified-change' - ); - ` - ); + ) + ); - db.pragma('user_version = 56'); - })(); + --- Set readStatus to ReadStatus.Read for all other message types - logger.info('updateToSchemaVersion56: success!'); + UPDATE messages + SET + readStatus = ${ReadStatus.Read} + WHERE + readStatus = ${ReadStatus.Unread} AND + type IS NOT NULL AND + type NOT IN ( + 'call-history', + 'change-number-notification', + 'chat-session-refreshed', + 'delivery-issue', + 'group', + 'incoming', + 'keychange', + 'timer-notification', + 'verified-change' + ); + ` + ); } diff --git a/ts/sql/migrations/57-rm-message-history-unsynced.ts b/ts/sql/migrations/57-rm-message-history-unsynced.ts index fd0c51c6705..828f26ee34c 100644 --- a/ts/sql/migrations/57-rm-message-history-unsynced.ts +++ b/ts/sql/migrations/57-rm-message-history-unsynced.ts @@ -3,27 +3,11 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; - -export default function updateToSchemaVersion57( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 57) { - return; - } - - db.transaction(() => { - db.exec( - ` - DELETE FROM messages - WHERE type IS 'message-history-unsynced'; - ` - ); - - db.pragma('user_version = 57'); - })(); - - logger.info('updateToSchemaVersion57: success!'); +export default function updateToSchemaVersion57(db: Database): void { + db.exec( + ` + DELETE FROM messages + WHERE type IS 'message-history-unsynced'; + ` + ); } diff --git a/ts/sql/migrations/58-update-unread.ts b/ts/sql/migrations/58-update-unread.ts index 3c96dfa9eb4..eba540047e3 100644 --- a/ts/sql/migrations/58-update-unread.ts +++ b/ts/sql/migrations/58-update-unread.ts @@ -5,73 +5,43 @@ import type { Database } from '@signalapp/sqlcipher'; import { ReadStatus } from '../../messages/MessageReadStatus'; import { SeenStatus } from '../../MessageSeenStatus'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion58(db: Database): void { + db.exec( + ` + --- Promote unread status in JSON to SQL column -export default function updateToSchemaVersion58( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 58) { - return; - } + -- NOTE: This was disabled because the 'unread' json field was deprecated + -- in b0750e5f4e1f79f0f177b17cbe06d688431f948d, but the old value was kept + -- in the messages created before the release of that commit. + -- + -- UPDATE messages + -- SET + -- readStatus = ${ReadStatus.Unread}, + -- seenStatus = ${SeenStatus.Unseen} + -- WHERE + -- json_extract(json, '$.unread') IS true OR + -- json_extract(json, '$.unread') IS 1; - db.transaction(() => { - db.exec( - ` - --- Promote unread status in JSON to SQL column + --- Clean up all old messages that still have a null read status + --- Note: we don't need to update seenStatus, because that was defaulted to zero - -- NOTE: This was disabled because the 'unread' json field was deprecated - -- in b0750e5f4e1f79f0f177b17cbe06d688431f948d, but the old value was kept - -- in the messages created before the release of that commit. - -- - -- UPDATE messages - -- SET - -- readStatus = ${ReadStatus.Unread}, - -- seenStatus = ${SeenStatus.Unseen} - -- WHERE - -- json_extract(json, '$.unread') IS true OR - -- json_extract(json, '$.unread') IS 1; + UPDATE messages + SET + readStatus = ${ReadStatus.Read} + WHERE + readStatus IS NULL; - --- Clean up all old messages that still have a null read status - --- Note: we don't need to update seenStatus, because that was defaulted to zero + --- Re-run unseen/unread queries from migration 56 - UPDATE messages - SET - readStatus = ${ReadStatus.Read} - WHERE - readStatus IS NULL; - - --- Re-run unseen/unread queries from migration 56 - - UPDATE messages - SET - seenStatus = ${SeenStatus.Unseen} - WHERE - readStatus = ${ReadStatus.Unread} AND - ( - type IS NULL - OR - type IN ( - 'call-history', - 'change-number-notification', - 'chat-session-refreshed', - 'delivery-issue', - 'group', - 'incoming', - 'keychange', - 'timer-notification', - 'verified-change' - ) - ); - - UPDATE messages - SET - readStatus = ${ReadStatus.Read} - WHERE - readStatus = ${ReadStatus.Unread} AND - type IS NOT NULL AND - type NOT IN ( + UPDATE messages + SET + seenStatus = ${SeenStatus.Unseen} + WHERE + readStatus = ${ReadStatus.Unread} AND + ( + type IS NULL + OR + type IN ( 'call-history', 'change-number-notification', 'chat-session-refreshed', @@ -81,56 +51,70 @@ export default function updateToSchemaVersion58( 'keychange', 'timer-notification', 'verified-change' - ); - - --- (new) Ensure these message types are not unread, just unseen - - UPDATE messages - SET - readStatus = ${ReadStatus.Read} - WHERE - readStatus = ${ReadStatus.Unread} AND - ( - type IN ( - 'change-number-notification', - 'keychange' - ) - ); - - --- (new) Ensure that these message types are neither unseen nor unread - - UPDATE messages - SET - readStatus = ${ReadStatus.Read}, - seenStatus = ${SeenStatus.Seen} - WHERE - type IN ( - 'group-v1-migration', - 'message-history-unsynced', - 'outgoing', - 'profile-change', - 'universal-timer-notification' - ); - - --- Make sure JSON reflects SQL columns - - UPDATE messages - SET - json = json_patch( - json, - json_object( - 'readStatus', readStatus, - 'seenStatus', seenStatus - ) ) - WHERE - readStatus IS NOT NULL OR - seenStatus IS NOT 0; - ` - ); + ); - db.pragma('user_version = 58'); - })(); + UPDATE messages + SET + readStatus = ${ReadStatus.Read} + WHERE + readStatus = ${ReadStatus.Unread} AND + type IS NOT NULL AND + type NOT IN ( + 'call-history', + 'change-number-notification', + 'chat-session-refreshed', + 'delivery-issue', + 'group', + 'incoming', + 'keychange', + 'timer-notification', + 'verified-change' + ); - logger.info('updateToSchemaVersion58: success!'); + --- (new) Ensure these message types are not unread, just unseen + + UPDATE messages + SET + readStatus = ${ReadStatus.Read} + WHERE + readStatus = ${ReadStatus.Unread} AND + ( + type IN ( + 'change-number-notification', + 'keychange' + ) + ); + + --- (new) Ensure that these message types are neither unseen nor unread + + UPDATE messages + SET + readStatus = ${ReadStatus.Read}, + seenStatus = ${SeenStatus.Seen} + WHERE + type IN ( + 'group-v1-migration', + 'message-history-unsynced', + 'outgoing', + 'profile-change', + 'universal-timer-notification' + ); + + --- Make sure JSON reflects SQL columns + + UPDATE messages + SET + json = json_patch( + json, + json_object( + 'readStatus', readStatus, + 'seenStatus', seenStatus + ) + ) + WHERE + readStatus IS NOT NULL OR + seenStatus IS NOT 0; + ` + ); } diff --git a/ts/sql/migrations/59-unprocessed-received-at-counter-index.ts b/ts/sql/migrations/59-unprocessed-received-at-counter-index.ts index 61fd8ed98fa..fd558756894 100644 --- a/ts/sql/migrations/59-unprocessed-received-at-counter-index.ts +++ b/ts/sql/migrations/59-unprocessed-received-at-counter-index.ts @@ -3,26 +3,11 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; - -export default function updateToSchemaVersion59( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 59) { - return; - } - - db.transaction(() => { - db.exec( - ` - CREATE INDEX unprocessed_byReceivedAtCounter ON unprocessed - (receivedAtCounter) - ` - ); - - db.pragma('user_version = 59'); - })(); - logger.info('updateToSchemaVersion59: success!'); +export default function updateToSchemaVersion59(db: Database): void { + db.exec( + ` + CREATE INDEX unprocessed_byReceivedAtCounter ON unprocessed + (receivedAtCounter) + ` + ); } diff --git a/ts/sql/migrations/60-update-expiring-index.ts b/ts/sql/migrations/60-update-expiring-index.ts index 42ff0fce57c..833228f0a7b 100644 --- a/ts/sql/migrations/60-update-expiring-index.ts +++ b/ts/sql/migrations/60-update-expiring-index.ts @@ -3,38 +3,22 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; - // TODO: DESKTOP-3694 -export default function updateToSchemaVersion60( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 60) { - return; - } +export default function updateToSchemaVersion60(db: Database): void { + db.exec( + ` + DROP INDEX expiring_message_by_conversation_and_received_at; - db.transaction(() => { - db.exec( - ` - DROP INDEX expiring_message_by_conversation_and_received_at; - - CREATE INDEX expiring_message_by_conversation_and_received_at - ON messages - ( - conversationId, - storyId, - expirationStartTimestamp, - expireTimer, - received_at - ) - WHERE isStory IS 0 AND type IS 'incoming'; - ` - ); - - db.pragma('user_version = 60'); - })(); - - logger.info('updateToSchemaVersion60: success!'); + CREATE INDEX expiring_message_by_conversation_and_received_at + ON messages + ( + conversationId, + storyId, + expirationStartTimestamp, + expireTimer, + received_at + ) + WHERE isStory IS 0 AND type IS 'incoming'; + ` + ); } diff --git a/ts/sql/migrations/61-distribution-list-storage.ts b/ts/sql/migrations/61-distribution-list-storage.ts index badab217818..32911080d88 100644 --- a/ts/sql/migrations/61-distribution-list-storage.ts +++ b/ts/sql/migrations/61-distribution-list-storage.ts @@ -3,42 +3,26 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion61(db: Database): void { + db.exec( + ` + ALTER TABLE storyDistributions DROP COLUMN avatarKey; + ALTER TABLE storyDistributions DROP COLUMN avatarUrlPath; -export default function updateToSchemaVersion61( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 61) { - return; - } + ALTER TABLE storyDistributions ADD COLUMN deletedAtTimestamp INTEGER; + ALTER TABLE storyDistributions ADD COLUMN allowsReplies INTEGER; + ALTER TABLE storyDistributions ADD COLUMN isBlockList INTEGER; - db.transaction(() => { - db.exec( - ` - ALTER TABLE storyDistributions DROP COLUMN avatarKey; - ALTER TABLE storyDistributions DROP COLUMN avatarUrlPath; + ALTER TABLE storyDistributions ADD COLUMN storageID STRING; + ALTER TABLE storyDistributions ADD COLUMN storageVersion INTEGER; + ALTER TABLE storyDistributions ADD COLUMN storageUnknownFields BLOB; + ALTER TABLE storyDistributions ADD COLUMN storageNeedsSync INTEGER; - ALTER TABLE storyDistributions ADD COLUMN deletedAtTimestamp INTEGER; - ALTER TABLE storyDistributions ADD COLUMN allowsReplies INTEGER; - ALTER TABLE storyDistributions ADD COLUMN isBlockList INTEGER; + ALTER TABLE messages ADD COLUMN storyDistributionListId STRING; - ALTER TABLE storyDistributions ADD COLUMN storageID STRING; - ALTER TABLE storyDistributions ADD COLUMN storageVersion INTEGER; - ALTER TABLE storyDistributions ADD COLUMN storageUnknownFields BLOB; - ALTER TABLE storyDistributions ADD COLUMN storageNeedsSync INTEGER; - - ALTER TABLE messages ADD COLUMN storyDistributionListId STRING; - - CREATE INDEX messages_by_distribution_list - ON messages(storyDistributionListId, received_at) - WHERE storyDistributionListId IS NOT NULL; - ` - ); - - db.pragma('user_version = 61'); - })(); - - logger.info('updateToSchemaVersion61: success!'); + CREATE INDEX messages_by_distribution_list + ON messages(storyDistributionListId, received_at) + WHERE storyDistributionListId IS NOT NULL; + ` + ); } diff --git a/ts/sql/migrations/62-add-urgent-to-send-log.ts b/ts/sql/migrations/62-add-urgent-to-send-log.ts index b0a4b8b0614..ce552f3ec6e 100644 --- a/ts/sql/migrations/62-add-urgent-to-send-log.ts +++ b/ts/sql/migrations/62-add-urgent-to-send-log.ts @@ -3,26 +3,10 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; - -export default function updateToSchemaVersion62( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 62) { - return; - } - - db.transaction(() => { - db.exec( - ` - ALTER TABLE sendLogPayloads ADD COLUMN urgent INTEGER; - ` - ); - - db.pragma('user_version = 62'); - })(); - - logger.info('updateToSchemaVersion62: success!'); +export default function updateToSchemaVersion62(db: Database): void { + db.exec( + ` + ALTER TABLE sendLogPayloads ADD COLUMN urgent INTEGER; + ` + ); } diff --git a/ts/sql/migrations/63-add-urgent-to-unprocessed.ts b/ts/sql/migrations/63-add-urgent-to-unprocessed.ts index 5dadfb8616f..04673c6c9bf 100644 --- a/ts/sql/migrations/63-add-urgent-to-unprocessed.ts +++ b/ts/sql/migrations/63-add-urgent-to-unprocessed.ts @@ -3,26 +3,10 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; - -export default function updateToSchemaVersion63( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 63) { - return; - } - - db.transaction(() => { - db.exec( - ` - ALTER TABLE unprocessed ADD COLUMN urgent INTEGER; - ` - ); - - db.pragma('user_version = 63'); - })(); - - logger.info('updateToSchemaVersion63: success!'); +export default function updateToSchemaVersion63(db: Database): void { + db.exec( + ` + ALTER TABLE unprocessed ADD COLUMN urgent INTEGER; + ` + ); } diff --git a/ts/sql/migrations/64-uuid-column-for-pre-keys.ts b/ts/sql/migrations/64-uuid-column-for-pre-keys.ts index e0fae3462bd..24b0e1f0024 100644 --- a/ts/sql/migrations/64-uuid-column-for-pre-keys.ts +++ b/ts/sql/migrations/64-uuid-column-for-pre-keys.ts @@ -3,36 +3,20 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion64(db: Database): void { + db.exec( + ` + ALTER TABLE preKeys + ADD COLUMN ourUuid STRING + GENERATED ALWAYS AS (json_extract(json, '$.ourUuid')); -export default function updateToSchemaVersion64( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 64) { - return; - } + CREATE INDEX preKeys_ourUuid ON preKeys (ourUuid); - db.transaction(() => { - db.exec( - ` - ALTER TABLE preKeys - ADD COLUMN ourUuid STRING - GENERATED ALWAYS AS (json_extract(json, '$.ourUuid')); + ALTER TABLE signedPreKeys + ADD COLUMN ourUuid STRING + GENERATED ALWAYS AS (json_extract(json, '$.ourUuid')); - CREATE INDEX preKeys_ourUuid ON preKeys (ourUuid); - - ALTER TABLE signedPreKeys - ADD COLUMN ourUuid STRING - GENERATED ALWAYS AS (json_extract(json, '$.ourUuid')); - - CREATE INDEX signedPreKeys_ourUuid ON signedPreKeys (ourUuid); - ` - ); - - db.pragma('user_version = 64'); - })(); - - logger.info('updateToSchemaVersion64: success!'); + CREATE INDEX signedPreKeys_ourUuid ON signedPreKeys (ourUuid); + ` + ); } diff --git a/ts/sql/migrations/65-add-storage-id-to-stickers.ts b/ts/sql/migrations/65-add-storage-id-to-stickers.ts index c35ba4bb95b..f5baefad183 100644 --- a/ts/sql/migrations/65-add-storage-id-to-stickers.ts +++ b/ts/sql/migrations/65-add-storage-id-to-stickers.ts @@ -3,60 +3,44 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion65(db: Database): void { + db.exec( + ` + ALTER TABLE sticker_packs ADD COLUMN position INTEGER DEFAULT 0 NOT NULL; + ALTER TABLE sticker_packs ADD COLUMN storageID STRING; + ALTER TABLE sticker_packs ADD COLUMN storageVersion INTEGER; + ALTER TABLE sticker_packs ADD COLUMN storageUnknownFields BLOB; + ALTER TABLE sticker_packs + ADD COLUMN storageNeedsSync + INTEGER DEFAULT 0 NOT NULL; -export default function updateToSchemaVersion65( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 65) { - return; - } - - db.transaction(() => { - db.exec( - ` - ALTER TABLE sticker_packs ADD COLUMN position INTEGER DEFAULT 0 NOT NULL; - ALTER TABLE sticker_packs ADD COLUMN storageID STRING; - ALTER TABLE sticker_packs ADD COLUMN storageVersion INTEGER; - ALTER TABLE sticker_packs ADD COLUMN storageUnknownFields BLOB; - ALTER TABLE sticker_packs - ADD COLUMN storageNeedsSync - INTEGER DEFAULT 0 NOT NULL; - - CREATE TABLE uninstalled_sticker_packs ( - id STRING NOT NULL PRIMARY KEY, - uninstalledAt NUMBER NOT NULL, - storageID STRING, - storageVersion NUMBER, - storageUnknownFields BLOB, - storageNeedsSync INTEGER NOT NULL - ); - - -- Set initial position - - UPDATE sticker_packs - SET - position = (row_number - 1), - storageNeedsSync = 1 - FROM ( - SELECT id, row_number() OVER (ORDER BY lastUsed DESC) as row_number - FROM sticker_packs - ) as ordered_pairs - WHERE sticker_packs.id IS ordered_pairs.id; - - -- See: getAllStickerPacks - - CREATE INDEX sticker_packs_by_position_and_id ON sticker_packs ( - position ASC, - id ASC - ); - ` + CREATE TABLE uninstalled_sticker_packs ( + id STRING NOT NULL PRIMARY KEY, + uninstalledAt NUMBER NOT NULL, + storageID STRING, + storageVersion NUMBER, + storageUnknownFields BLOB, + storageNeedsSync INTEGER NOT NULL ); - db.pragma('user_version = 65'); - })(); + -- Set initial position - logger.info('updateToSchemaVersion65: success!'); + UPDATE sticker_packs + SET + position = (row_number - 1), + storageNeedsSync = 1 + FROM ( + SELECT id, row_number() OVER (ORDER BY lastUsed DESC) as row_number + FROM sticker_packs + ) as ordered_pairs + WHERE sticker_packs.id IS ordered_pairs.id; + + -- See: getAllStickerPacks + + CREATE INDEX sticker_packs_by_position_and_id ON sticker_packs ( + position ASC, + id ASC + ); + ` + ); } diff --git a/ts/sql/migrations/66-add-pni-signature-to-sent-protos.ts b/ts/sql/migrations/66-add-pni-signature-to-sent-protos.ts index a669fb619d3..c20e45ecdf4 100644 --- a/ts/sql/migrations/66-add-pni-signature-to-sent-protos.ts +++ b/ts/sql/migrations/66-add-pni-signature-to-sent-protos.ts @@ -3,27 +3,11 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; - -export default function updateToSchemaVersion66( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 66) { - return; - } - - db.transaction(() => { - db.exec( - ` - ALTER TABLE sendLogPayloads - ADD COLUMN hasPniSignatureMessage INTEGER DEFAULT 0 NOT NULL; - ` - ); - - db.pragma('user_version = 66'); - })(); - - logger.info('updateToSchemaVersion66: success!'); +export default function updateToSchemaVersion66(db: Database): void { + db.exec( + ` + ALTER TABLE sendLogPayloads + ADD COLUMN hasPniSignatureMessage INTEGER DEFAULT 0 NOT NULL; + ` + ); } diff --git a/ts/sql/migrations/67-add-story-to-unprocessed.ts b/ts/sql/migrations/67-add-story-to-unprocessed.ts index 3341d056baa..ee0a403ab60 100644 --- a/ts/sql/migrations/67-add-story-to-unprocessed.ts +++ b/ts/sql/migrations/67-add-story-to-unprocessed.ts @@ -3,26 +3,10 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; - -export default function updateToSchemaVersion67( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 67) { - return; - } - - db.transaction(() => { - db.exec( - ` - ALTER TABLE unprocessed ADD COLUMN story INTEGER; - ` - ); - - db.pragma('user_version = 67'); - })(); - - logger.info('updateToSchemaVersion67: success!'); +export default function updateToSchemaVersion67(db: Database): void { + db.exec( + ` + ALTER TABLE unprocessed ADD COLUMN story INTEGER; + ` + ); } diff --git a/ts/sql/migrations/68-drop-deprecated-columns.ts b/ts/sql/migrations/68-drop-deprecated-columns.ts index 9e0c37c2020..f3a4cca8db9 100644 --- a/ts/sql/migrations/68-drop-deprecated-columns.ts +++ b/ts/sql/migrations/68-drop-deprecated-columns.ts @@ -3,29 +3,13 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; - -export default function updateToSchemaVersion68( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 68) { - return; - } - - db.transaction(() => { - db.exec( - ` - ALTER TABLE messages - DROP COLUMN deprecatedSourceDevice; - ALTER TABLE unprocessed - DROP COLUMN deprecatedSourceDevice; - ` - ); - - db.pragma('user_version = 68'); - })(); - - logger.info('updateToSchemaVersion68: success!'); +export default function updateToSchemaVersion68(db: Database): void { + db.exec( + ` + ALTER TABLE messages + DROP COLUMN deprecatedSourceDevice; + ALTER TABLE unprocessed + DROP COLUMN deprecatedSourceDevice; + ` + ); } diff --git a/ts/sql/migrations/69-group-call-ring-cancellations.ts b/ts/sql/migrations/69-group-call-ring-cancellations.ts index 33f4af041dc..e8634eea141 100644 --- a/ts/sql/migrations/69-group-call-ring-cancellations.ts +++ b/ts/sql/migrations/69-group-call-ring-cancellations.ts @@ -3,31 +3,15 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion69(db: Database): void { + db.exec( + ` + DROP TABLE IF EXISTS groupCallRings; -export default function updateToSchemaVersion69( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 69) { - return; - } - - db.transaction(() => { - db.exec( - ` - DROP TABLE IF EXISTS groupCallRings; - - CREATE TABLE groupCallRingCancellations( - ringId INTEGER PRIMARY KEY, - createdAt INTEGER NOT NULL - ); - ` + CREATE TABLE groupCallRingCancellations( + ringId INTEGER PRIMARY KEY, + createdAt INTEGER NOT NULL ); - - db.pragma('user_version = 69'); - })(); - - logger.info('updateToSchemaVersion69: success!'); + ` + ); } diff --git a/ts/sql/migrations/70-story-reply-index.ts b/ts/sql/migrations/70-story-reply-index.ts index 0c63170d7a8..f35c1469e0e 100644 --- a/ts/sql/migrations/70-story-reply-index.ts +++ b/ts/sql/migrations/70-story-reply-index.ts @@ -3,27 +3,11 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; - -export default function updateToSchemaVersion70( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 70) { - return; - } - - db.transaction(() => { - // Used in `getAllStories`. - db.exec( - ` - CREATE INDEX messages_by_storyId ON messages (storyId); - ` - ); - - db.pragma('user_version = 70'); - })(); - - logger.info('updateToSchemaVersion70: success!'); +export default function updateToSchemaVersion70(db: Database): void { + // Used in `getAllStories`. + db.exec( + ` + CREATE INDEX messages_by_storyId ON messages (storyId); + ` + ); } diff --git a/ts/sql/migrations/71-merge-notifications.ts b/ts/sql/migrations/71-merge-notifications.ts index 70c92023d93..29ff2f340df 100644 --- a/ts/sql/migrations/71-merge-notifications.ts +++ b/ts/sql/migrations/71-merge-notifications.ts @@ -3,121 +3,105 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion71(db: Database): void { + db.exec( + ` + --- These will be re-added below + DROP INDEX messages_preview; + DROP INDEX messages_activity; + DROP INDEX message_user_initiated; -export default function updateToSchemaVersion71( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 71) { - return; - } + --- These will also be re-added below + ALTER TABLE messages DROP COLUMN shouldAffectActivity; + ALTER TABLE messages DROP COLUMN shouldAffectPreview; + ALTER TABLE messages DROP COLUMN isUserInitiatedMessage; - db.transaction(() => { - db.exec( - ` - --- These will be re-added below - DROP INDEX messages_preview; - DROP INDEX messages_activity; - DROP INDEX message_user_initiated; + --- Note: These generated columns were originally introduced in migration 47, and + --- are mostly the same + + --- Based on the current list (model-types.ts), the types which DO affect activity: + --- NULL (old, malformed data) + --- call-history + --- chat-session-refreshed (deprecated) + --- delivery-issue + --- group (deprecated) + --- group-v2-change + --- incoming + --- outgoing + --- timer-notification - --- These will also be re-added below - ALTER TABLE messages DROP COLUMN shouldAffectActivity; - ALTER TABLE messages DROP COLUMN shouldAffectPreview; - ALTER TABLE messages DROP COLUMN isUserInitiatedMessage; + --- (change: added conversation-merge, keychange, and phone-number-discovery) + ALTER TABLE messages + ADD COLUMN shouldAffectActivity INTEGER + GENERATED ALWAYS AS ( + type IS NULL + OR + type NOT IN ( + 'change-number-notification', + 'conversation-merge', + 'group-v1-migration', + 'keychange', + 'message-history-unsynced', + 'phone-number-discovery', + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change' + ) + ); - --- Note: These generated columns were originally introduced in migration 47, and - --- are mostly the same - - --- Based on the current list (model-types.ts), the types which DO affect activity: - --- NULL (old, malformed data) - --- call-history - --- chat-session-refreshed (deprecated) - --- delivery-issue - --- group (deprecated) - --- group-v2-change - --- incoming - --- outgoing - --- timer-notification + --- (change: added conversation-merge and phone-number-discovery + --- (now matches the above list) + ALTER TABLE messages + ADD COLUMN shouldAffectPreview INTEGER + GENERATED ALWAYS AS ( + type IS NULL + OR + type NOT IN ( + 'change-number-notification', + 'conversation-merge', + 'group-v1-migration', + 'keychange', + 'message-history-unsynced', + 'phone-number-discovery', + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change' + ) + ); - --- (change: added conversation-merge, keychange, and phone-number-discovery) - ALTER TABLE messages - ADD COLUMN shouldAffectActivity INTEGER - GENERATED ALWAYS AS ( - type IS NULL - OR - type NOT IN ( - 'change-number-notification', - 'conversation-merge', - 'group-v1-migration', - 'keychange', - 'message-history-unsynced', - 'phone-number-discovery', - 'profile-change', - 'story', - 'universal-timer-notification', - 'verified-change' - ) - ); + --- Note: This list only differs from the above on these types: + --- group-v2-change - --- (change: added conversation-merge and phone-number-discovery - --- (now matches the above list) - ALTER TABLE messages - ADD COLUMN shouldAffectPreview INTEGER - GENERATED ALWAYS AS ( - type IS NULL - OR - type NOT IN ( - 'change-number-notification', - 'conversation-merge', - 'group-v1-migration', - 'keychange', - 'message-history-unsynced', - 'phone-number-discovery', - 'profile-change', - 'story', - 'universal-timer-notification', - 'verified-change' - ) - ); + --- (change: added conversation-merge and phone-number-discovery + ALTER TABLE messages + ADD COLUMN isUserInitiatedMessage INTEGER + GENERATED ALWAYS AS ( + type IS NULL + OR + type NOT IN ( + 'change-number-notification', + 'conversation-merge', + 'group-v1-migration', + 'group-v2-change', + 'keychange', + 'message-history-unsynced', + 'phone-number-discovery', + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change' + ) + ); - --- Note: This list only differs from the above on these types: - --- group-v2-change + CREATE INDEX messages_preview ON messages + (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, expiresAt, received_at, sent_at); - --- (change: added conversation-merge and phone-number-discovery - ALTER TABLE messages - ADD COLUMN isUserInitiatedMessage INTEGER - GENERATED ALWAYS AS ( - type IS NULL - OR - type NOT IN ( - 'change-number-notification', - 'conversation-merge', - 'group-v1-migration', - 'group-v2-change', - 'keychange', - 'message-history-unsynced', - 'phone-number-discovery', - 'profile-change', - 'story', - 'universal-timer-notification', - 'verified-change' - ) - ); + CREATE INDEX messages_activity ON messages + (conversationId, shouldAffectActivity, isTimerChangeFromSync, isGroupLeaveEventFromOther, received_at, sent_at); - CREATE INDEX messages_preview ON messages - (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, expiresAt, received_at, sent_at); - - CREATE INDEX messages_activity ON messages - (conversationId, shouldAffectActivity, isTimerChangeFromSync, isGroupLeaveEventFromOther, received_at, sent_at); - - CREATE INDEX message_user_initiated ON messages (isUserInitiatedMessage); - ` - ); - - db.pragma('user_version = 71'); - })(); - - logger.info('updateToSchemaVersion71: success!'); + CREATE INDEX message_user_initiated ON messages (isUserInitiatedMessage); + ` + ); } diff --git a/ts/sql/migrations/72-optimize-call-id-message-lookup.ts b/ts/sql/migrations/72-optimize-call-id-message-lookup.ts index 4e82af86751..17a84343f71 100644 --- a/ts/sql/migrations/72-optimize-call-id-message-lookup.ts +++ b/ts/sql/migrations/72-optimize-call-id-message-lookup.ts @@ -2,37 +2,22 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; -export default function updateToSchemaVersion72( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 72) { - return; - } - - db.transaction(() => { - db.exec( - ` - ALTER TABLE messages - ADD COLUMN callId TEXT - GENERATED ALWAYS AS ( - json_extract(json, '$.callHistoryDetails.callId') - ); - ALTER TABLE messages - ADD COLUMN callMode TEXT - GENERATED ALWAYS AS ( - json_extract(json, '$.callHistoryDetails.callMode') - ); - CREATE INDEX messages_call ON messages - (conversationId, type, callMode, callId); - ` - ); - - db.pragma('user_version = 72'); - })(); - - logger.info('updateToSchemaVersion72: success!'); +export default function updateToSchemaVersion72(db: Database): void { + db.exec( + ` + ALTER TABLE messages + ADD COLUMN callId TEXT + GENERATED ALWAYS AS ( + json_extract(json, '$.callHistoryDetails.callId') + ); + ALTER TABLE messages + ADD COLUMN callMode TEXT + GENERATED ALWAYS AS ( + json_extract(json, '$.callHistoryDetails.callMode') + ); + CREATE INDEX messages_call ON messages + (conversationId, type, callMode, callId); + ` + ); } diff --git a/ts/sql/migrations/73-remove-phone-number-discovery.ts b/ts/sql/migrations/73-remove-phone-number-discovery.ts index 7508800ead5..008c2d63a5e 100644 --- a/ts/sql/migrations/73-remove-phone-number-discovery.ts +++ b/ts/sql/migrations/73-remove-phone-number-discovery.ts @@ -3,110 +3,94 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion73(db: Database): void { + db.exec( + ` + --- Delete deprecated notifications + DELETE FROM messages WHERE type IS 'phone-number-discovery'; -export default function updateToSchemaVersion73( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 73) { - return; - } + --- These will be re-added below + DROP INDEX messages_preview; + DROP INDEX messages_activity; + DROP INDEX message_user_initiated; - db.transaction(() => { - db.exec( - ` - --- Delete deprecated notifications - DELETE FROM messages WHERE type IS 'phone-number-discovery'; + --- These will also be re-added below + ALTER TABLE messages DROP COLUMN shouldAffectActivity; + ALTER TABLE messages DROP COLUMN shouldAffectPreview; + ALTER TABLE messages DROP COLUMN isUserInitiatedMessage; - --- These will be re-added below - DROP INDEX messages_preview; - DROP INDEX messages_activity; - DROP INDEX message_user_initiated; + --- Note: These generated columns were originally introduced in migration 71, and + --- are mostly the same - --- These will also be re-added below - ALTER TABLE messages DROP COLUMN shouldAffectActivity; - ALTER TABLE messages DROP COLUMN shouldAffectPreview; - ALTER TABLE messages DROP COLUMN isUserInitiatedMessage; + --- (change: removed phone-number-discovery) + ALTER TABLE messages + ADD COLUMN shouldAffectActivity INTEGER + GENERATED ALWAYS AS ( + type IS NULL + OR + type NOT IN ( + 'change-number-notification', + 'conversation-merge', + 'group-v1-migration', + 'keychange', + 'message-history-unsynced', + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change' + ) + ); - --- Note: These generated columns were originally introduced in migration 71, and - --- are mostly the same + --- (change: removed phone-number-discovery + --- (now matches the above list) + ALTER TABLE messages + ADD COLUMN shouldAffectPreview INTEGER + GENERATED ALWAYS AS ( + type IS NULL + OR + type NOT IN ( + 'change-number-notification', + 'conversation-merge', + 'group-v1-migration', + 'keychange', + 'message-history-unsynced', + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change' + ) + ); - --- (change: removed phone-number-discovery) - ALTER TABLE messages - ADD COLUMN shouldAffectActivity INTEGER - GENERATED ALWAYS AS ( - type IS NULL - OR - type NOT IN ( - 'change-number-notification', - 'conversation-merge', - 'group-v1-migration', - 'keychange', - 'message-history-unsynced', - 'profile-change', - 'story', - 'universal-timer-notification', - 'verified-change' - ) - ); + --- Note: This list only differs from the above on these types: + --- group-v2-change - --- (change: removed phone-number-discovery - --- (now matches the above list) - ALTER TABLE messages - ADD COLUMN shouldAffectPreview INTEGER - GENERATED ALWAYS AS ( - type IS NULL - OR - type NOT IN ( - 'change-number-notification', - 'conversation-merge', - 'group-v1-migration', - 'keychange', - 'message-history-unsynced', - 'profile-change', - 'story', - 'universal-timer-notification', - 'verified-change' - ) - ); + --- (change: removed phone-number-discovery + ALTER TABLE messages + ADD COLUMN isUserInitiatedMessage INTEGER + GENERATED ALWAYS AS ( + type IS NULL + OR + type NOT IN ( + 'change-number-notification', + 'conversation-merge', + 'group-v1-migration', + 'group-v2-change', + 'keychange', + 'message-history-unsynced', + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change' + ) + ); - --- Note: This list only differs from the above on these types: - --- group-v2-change + CREATE INDEX messages_preview ON messages + (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, expiresAt, received_at, sent_at); - --- (change: removed phone-number-discovery - ALTER TABLE messages - ADD COLUMN isUserInitiatedMessage INTEGER - GENERATED ALWAYS AS ( - type IS NULL - OR - type NOT IN ( - 'change-number-notification', - 'conversation-merge', - 'group-v1-migration', - 'group-v2-change', - 'keychange', - 'message-history-unsynced', - 'profile-change', - 'story', - 'universal-timer-notification', - 'verified-change' - ) - ); + CREATE INDEX messages_activity ON messages + (conversationId, shouldAffectActivity, isTimerChangeFromSync, isGroupLeaveEventFromOther, received_at, sent_at); - CREATE INDEX messages_preview ON messages - (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, expiresAt, received_at, sent_at); - - CREATE INDEX messages_activity ON messages - (conversationId, shouldAffectActivity, isTimerChangeFromSync, isGroupLeaveEventFromOther, received_at, sent_at); - - CREATE INDEX message_user_initiated ON messages (isUserInitiatedMessage); - ` - ); - - db.pragma('user_version = 73'); - })(); - - logger.info('updateToSchemaVersion73: success!'); + CREATE INDEX message_user_initiated ON messages (isUserInitiatedMessage); + ` + ); } diff --git a/ts/sql/migrations/74-optimize-convo-open.ts b/ts/sql/migrations/74-optimize-convo-open.ts index 83a284d23fc..a424b0d3b2a 100644 --- a/ts/sql/migrations/74-optimize-convo-open.ts +++ b/ts/sql/migrations/74-optimize-convo-open.ts @@ -3,37 +3,21 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion74(db: Database): void { + db.exec( + ` + -- Previously: (isUserInitiatedMessage) + DROP INDEX message_user_initiated; -export default function updateToSchemaVersion74( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 74) { - return; - } + CREATE INDEX message_user_initiated ON messages (conversationId, isUserInitiatedMessage); - db.transaction(() => { - db.exec( - ` - -- Previously: (isUserInitiatedMessage) - DROP INDEX message_user_initiated; + -- Previously: (unread, conversationId) + DROP INDEX reactions_unread; - CREATE INDEX message_user_initiated ON messages (conversationId, isUserInitiatedMessage); - - -- Previously: (unread, conversationId) - DROP INDEX reactions_unread; - - CREATE INDEX reactions_unread ON reactions ( - conversationId, - unread - ); - ` + CREATE INDEX reactions_unread ON reactions ( + conversationId, + unread ); - - db.pragma('user_version = 74'); - })(); - - logger.info('updateToSchemaVersion74: success!'); + ` + ); } diff --git a/ts/sql/migrations/75-noop.ts b/ts/sql/migrations/75-noop.ts index dd745220c89..dc83b42d606 100644 --- a/ts/sql/migrations/75-noop.ts +++ b/ts/sql/migrations/75-noop.ts @@ -1,25 +1,8 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { Database } from '@signalapp/sqlcipher'; - -import type { LoggerType } from '../../types/Logging'; - -export default function updateToSchemaVersion75( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 75) { - return; - } - - db.transaction(() => { - // This was previously a FTS5 migration, but we had to reorder the - // migrations for backports. - // See: migrations 76 and 77. - db.pragma('user_version = 75'); - })(); - - logger.info('updateToSchemaVersion75: success!'); +export default function updateToSchemaVersion75(): void { + // This was previously a FTS5 migration, but we had to reorder the + // migrations for backports. + // See: migrations 76 and 77. } diff --git a/ts/sql/migrations/76-optimize-convo-open-2.ts b/ts/sql/migrations/76-optimize-convo-open-2.ts index 192224e8f12..2bc694bd4ce 100644 --- a/ts/sql/migrations/76-optimize-convo-open-2.ts +++ b/ts/sql/migrations/76-optimize-convo-open-2.ts @@ -3,57 +3,41 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion76(db: Database): void { + db.exec( + ` + -- Re-created below + DROP INDEX IF EXISTS message_expires_at; + DROP INDEX IF EXISTS messages_preview; -export default function updateToSchemaVersion76( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 76) { - return; - } + -- Create non-null expiresAt column + ALTER TABLE messages + DROP COLUMN expiresAt; - db.transaction(() => { - db.exec( - ` - -- Re-created below - DROP INDEX IF EXISTS message_expires_at; - DROP INDEX IF EXISTS messages_preview; + ALTER TABLE messages + ADD COLUMN + expiresAt INT + GENERATED ALWAYS + AS (ifnull( + expirationStartTimestamp + (expireTimer * 1000), + ${Number.MAX_SAFE_INTEGER} + )); - -- Create non-null expiresAt column - ALTER TABLE messages - DROP COLUMN expiresAt; - - ALTER TABLE messages - ADD COLUMN - expiresAt INT - GENERATED ALWAYS - AS (ifnull( - expirationStartTimestamp + (expireTimer * 1000), - ${Number.MAX_SAFE_INTEGER} - )); - - -- Re-create indexes - -- Note the "s" at the end of "messages" - CREATE INDEX messages_expires_at ON messages ( - expiresAt - ); - - -- Note that expiresAt is intentionally dropped from the index since - -- expiresAt > $now is likely to be true so we just try selecting it - -- *after* ordering by received_at/sent_at. - CREATE INDEX messages_preview ON messages - (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, - received_at, sent_at); - CREATE INDEX messages_preview_without_story ON messages - (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, - received_at, sent_at) WHERE storyId IS NULL; - ` + -- Re-create indexes + -- Note the "s" at the end of "messages" + CREATE INDEX messages_expires_at ON messages ( + expiresAt ); - db.pragma('user_version = 76'); - })(); - - logger.info('updateToSchemaVersion76: success!'); + -- Note that expiresAt is intentionally dropped from the index since + -- expiresAt > $now is likely to be true so we just try selecting it + -- *after* ordering by received_at/sent_at. + CREATE INDEX messages_preview ON messages + (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, + received_at, sent_at); + CREATE INDEX messages_preview_without_story ON messages + (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, + received_at, sent_at) WHERE storyId IS NULL; + ` + ); } diff --git a/ts/sql/migrations/77-signal-tokenizer.ts b/ts/sql/migrations/77-signal-tokenizer.ts index cdc04f69d86..5e0a043d870 100644 --- a/ts/sql/migrations/77-signal-tokenizer.ts +++ b/ts/sql/migrations/77-signal-tokenizer.ts @@ -3,42 +3,26 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion77(db: Database): void { + db.exec( + ` + -- Create FTS table with custom tokenizer from + -- @signalapp/sqlcipher. -export default function updateToSchemaVersion77( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 77) { - return; - } + DROP TABLE messages_fts; - db.transaction(() => { - db.exec( - ` - -- Create FTS table with custom tokenizer from - -- @signalapp/sqlcipher. - - DROP TABLE messages_fts; - - CREATE VIRTUAL TABLE messages_fts USING fts5( - body, - tokenize = 'signal_tokenizer' - ); - - -- Reindex messages - -- Based on messages_on_insert trigger from migrations/45-stories.ts - - INSERT INTO messages_fts (rowid, body) - SELECT rowid, body - FROM messages - WHERE isViewOnce IS NOT 1 AND storyId IS NULL; - ` + CREATE VIRTUAL TABLE messages_fts USING fts5( + body, + tokenize = 'signal_tokenizer' ); - db.pragma('user_version = 77'); - })(); + -- Reindex messages + -- Based on messages_on_insert trigger from migrations/45-stories.ts - logger.info('updateToSchemaVersion77: success!'); + INSERT INTO messages_fts (rowid, body) + SELECT rowid, body + FROM messages + WHERE isViewOnce IS NOT 1 AND storyId IS NULL; + ` + ); } diff --git a/ts/sql/migrations/78-merge-receipt-jobs.ts b/ts/sql/migrations/78-merge-receipt-jobs.ts index 06abd1c418e..ea9c72d88d7 100644 --- a/ts/sql/migrations/78-merge-receipt-jobs.ts +++ b/ts/sql/migrations/78-merge-receipt-jobs.ts @@ -7,125 +7,112 @@ import type { WritableDB } from '../Interface'; import { getJobsInQueue, insertJob } from '../Server'; export default function updateToSchemaVersion78( - currentVersion: number, db: WritableDB, logger: LoggerType ): void { - if (currentVersion >= 78) { - return; + const deleteJobsInQueue = db.prepare( + 'DELETE FROM jobs WHERE queueType = $queueType' + ); + + const queues = [ + { + queueType: 'delivery receipts', + jobDataKey: 'deliveryReceipts', + jobDataIsArray: true, + newReceiptsType: 'deliveryReceipt', + }, + { + queueType: 'read receipts', + jobDataKey: 'readReceipts', + jobDataIsArray: true, + newReceiptsType: 'readReceipt', + }, + { + queueType: 'viewed receipts', + jobDataKey: 'viewedReceipt', + jobDataIsArray: false, + newReceiptsType: 'viewedReceipt', + }, + ]; + + const getMessageById = db.prepare( + 'SELECT conversationId FROM messages WHERE id IS ?' + ); + + for (const queue of queues) { + const prevJobs = getJobsInQueue(db, queue.queueType); + deleteJobsInQueue.run({ queueType: queue.queueType }); + + prevJobs.forEach(job => { + const { data, id } = job; + if (!isRecord(data)) { + logger.warn( + `${queue.queueType} queue job ${id} was missing valid data` + ); + return; + } + + const { messageId } = data; + if (typeof messageId !== 'string') { + logger.warn( + `${queue.queueType} queue job ${id} had a non-string messageId` + ); + return; + } + + const message = getMessageById.get([messageId]); + if (!message) { + logger.warn(`Unable to find message for ${queue.queueType} job ${id}`); + return; + } + + const { conversationId } = message; + if (typeof conversationId !== 'string') { + logger.warn( + `${queue.queueType} queue job ${id} had a non-string conversationId` + ); + return; + } + + const oldReceipts = queue.jobDataIsArray + ? data[queue.jobDataKey] + : [data[queue.jobDataKey]]; + + if (!Array.isArray(oldReceipts)) { + logger.warn( + `${queue.queueType} queue job ${id} had a non-array ${queue.jobDataKey}` + ); + return; + } + + const newReceipts = []; + + for (const receipt of oldReceipts) { + if (!isRecord(receipt)) { + logger.warn( + `${queue.queueType} queue job ${id} had a non-record receipt` + ); + continue; + } + + newReceipts.push({ + ...receipt, + conversationId, + }); + } + + const newJob = { + ...job, + queueType: 'conversation', + data: { + type: 'Receipts', + conversationId, + receiptsType: queue.newReceiptsType, + receipts: newReceipts, + }, + }; + + insertJob(db, newJob); + }); } - - db.transaction(() => { - const deleteJobsInQueue = db.prepare( - 'DELETE FROM jobs WHERE queueType = $queueType' - ); - - const queues = [ - { - queueType: 'delivery receipts', - jobDataKey: 'deliveryReceipts', - jobDataIsArray: true, - newReceiptsType: 'deliveryReceipt', - }, - { - queueType: 'read receipts', - jobDataKey: 'readReceipts', - jobDataIsArray: true, - newReceiptsType: 'readReceipt', - }, - { - queueType: 'viewed receipts', - jobDataKey: 'viewedReceipt', - jobDataIsArray: false, - newReceiptsType: 'viewedReceipt', - }, - ]; - - const getMessageById = db.prepare( - 'SELECT conversationId FROM messages WHERE id IS ?' - ); - - for (const queue of queues) { - const prevJobs = getJobsInQueue(db, queue.queueType); - deleteJobsInQueue.run({ queueType: queue.queueType }); - - prevJobs.forEach(job => { - const { data, id } = job; - if (!isRecord(data)) { - logger.warn( - `updateToSchemaVersion78: ${queue.queueType} queue job ${id} was missing valid data` - ); - return; - } - - const { messageId } = data; - if (typeof messageId !== 'string') { - logger.warn( - `updateToSchemaVersion78: ${queue.queueType} queue job ${id} had a non-string messageId` - ); - return; - } - - const message = getMessageById.get([messageId]); - if (!message) { - logger.warn( - `updateToSchemaVersion78: Unable to find message for ${queue.queueType} job ${id}` - ); - return; - } - - const { conversationId } = message; - if (typeof conversationId !== 'string') { - logger.warn( - `updateToSchemaVersion78: ${queue.queueType} queue job ${id} had a non-string conversationId` - ); - return; - } - - const oldReceipts = queue.jobDataIsArray - ? data[queue.jobDataKey] - : [data[queue.jobDataKey]]; - - if (!Array.isArray(oldReceipts)) { - logger.warn( - `updateToSchemaVersion78: ${queue.queueType} queue job ${id} had a non-array ${queue.jobDataKey}` - ); - return; - } - - const newReceipts = []; - - for (const receipt of oldReceipts) { - if (!isRecord(receipt)) { - logger.warn( - `updateToSchemaVersion78: ${queue.queueType} queue job ${id} had a non-record receipt` - ); - continue; - } - - newReceipts.push({ - ...receipt, - conversationId, - }); - } - - const newJob = { - ...job, - queueType: 'conversation', - data: { - type: 'Receipts', - conversationId, - receiptsType: queue.newReceiptsType, - receipts: newReceipts, - }, - }; - - insertJob(db, newJob); - }); - } - - db.pragma('user_version = 78'); - })(); - - logger.info('updateToSchemaVersion78: success!'); } diff --git a/ts/sql/migrations/79-paging-lightbox.ts b/ts/sql/migrations/79-paging-lightbox.ts index d5c4b3c95ca..49b641d90a0 100644 --- a/ts/sql/migrations/79-paging-lightbox.ts +++ b/ts/sql/migrations/79-paging-lightbox.ts @@ -3,30 +3,14 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; - -export default function updateToSchemaVersion79( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 79) { - return; - } - - db.transaction(() => { - db.exec(` - DROP INDEX messages_hasVisualMediaAttachments; - CREATE INDEX messages_hasVisualMediaAttachments - ON messages ( - conversationId, isStory, storyId, - hasVisualMediaAttachments, received_at, sent_at - ) - WHERE hasVisualMediaAttachments IS 1; - `); - - db.pragma('user_version = 79'); - })(); - - logger.info('updateToSchemaVersion79: success!'); +export default function updateToSchemaVersion79(db: Database): void { + db.exec(` + DROP INDEX messages_hasVisualMediaAttachments; + CREATE INDEX messages_hasVisualMediaAttachments + ON messages ( + conversationId, isStory, storyId, + hasVisualMediaAttachments, received_at, sent_at + ) + WHERE hasVisualMediaAttachments IS 1; + `); } diff --git a/ts/sql/migrations/80-edited-messages.ts b/ts/sql/migrations/80-edited-messages.ts index 3eb322e43a2..e2932cd7a90 100644 --- a/ts/sql/migrations/80-edited-messages.ts +++ b/ts/sql/migrations/80-edited-messages.ts @@ -3,32 +3,16 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion80(db: Database): void { + db.exec(` + CREATE TABLE edited_messages( + fromId STRING, + messageId STRING REFERENCES messages(id) + ON DELETE CASCADE, + sentAt INTEGER, + readStatus INTEGER + ); -export default function updateToSchemaVersion80( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 80) { - return; - } - - db.transaction(() => { - db.exec(` - CREATE TABLE edited_messages( - fromId STRING, - messageId STRING REFERENCES messages(id) - ON DELETE CASCADE, - sentAt INTEGER, - readStatus INTEGER - ); - - CREATE INDEX edited_messages_sent_at ON edited_messages (sentAt); - `); - - db.pragma('user_version = 80'); - })(); - - logger.info('updateToSchemaVersion80: success!'); + CREATE INDEX edited_messages_sent_at ON edited_messages (sentAt); + `); } diff --git a/ts/sql/migrations/81-contact-removed-notification.ts b/ts/sql/migrations/81-contact-removed-notification.ts index c0f96000606..07d50be656a 100644 --- a/ts/sql/migrations/81-contact-removed-notification.ts +++ b/ts/sql/migrations/81-contact-removed-notification.ts @@ -3,116 +3,100 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion81(db: Database): void { + db.exec( + ` + --- These will be re-added below + DROP INDEX messages_preview; + DROP INDEX messages_preview_without_story; + DROP INDEX messages_activity; + DROP INDEX message_user_initiated; -export default function updateToSchemaVersion81( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 81) { - return; - } + --- These will also be re-added below + ALTER TABLE messages DROP COLUMN shouldAffectActivity; + ALTER TABLE messages DROP COLUMN shouldAffectPreview; + ALTER TABLE messages DROP COLUMN isUserInitiatedMessage; - db.transaction(() => { - db.exec( - ` - --- These will be re-added below - DROP INDEX messages_preview; - DROP INDEX messages_preview_without_story; - DROP INDEX messages_activity; - DROP INDEX message_user_initiated; + --- Note: These generated columns were previously modified in + --- migration 73, and are mostly the same - --- These will also be re-added below - ALTER TABLE messages DROP COLUMN shouldAffectActivity; - ALTER TABLE messages DROP COLUMN shouldAffectPreview; - ALTER TABLE messages DROP COLUMN isUserInitiatedMessage; + --- (change: added contact-removed-notification) + ALTER TABLE messages + ADD COLUMN shouldAffectActivity INTEGER + GENERATED ALWAYS AS ( + type IS NULL + OR + type NOT IN ( + 'change-number-notification', + 'contact-removed-notification', + 'conversation-merge', + 'group-v1-migration', + 'keychange', + 'message-history-unsynced', + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change' + ) + ); - --- Note: These generated columns were previously modified in - --- migration 73, and are mostly the same + --- (change: added contact-removed-notification) + ALTER TABLE messages + ADD COLUMN shouldAffectPreview INTEGER + GENERATED ALWAYS AS ( + type IS NULL + OR + type NOT IN ( + 'change-number-notification', + 'contact-removed-notification', + 'conversation-merge', + 'group-v1-migration', + 'keychange', + 'message-history-unsynced', + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change' + ) + ); - --- (change: added contact-removed-notification) - ALTER TABLE messages - ADD COLUMN shouldAffectActivity INTEGER - GENERATED ALWAYS AS ( - type IS NULL - OR - type NOT IN ( - 'change-number-notification', - 'contact-removed-notification', - 'conversation-merge', - 'group-v1-migration', - 'keychange', - 'message-history-unsynced', - 'profile-change', - 'story', - 'universal-timer-notification', - 'verified-change' - ) - ); + --- (change: added contact-removed-notification) + ALTER TABLE messages + ADD COLUMN isUserInitiatedMessage INTEGER + GENERATED ALWAYS AS ( + type IS NULL + OR + type NOT IN ( + 'change-number-notification', + 'contact-removed-notification', + 'conversation-merge', + 'group-v1-migration', + 'group-v2-change', + 'keychange', + 'message-history-unsynced', + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change' + ) + ); - --- (change: added contact-removed-notification) - ALTER TABLE messages - ADD COLUMN shouldAffectPreview INTEGER - GENERATED ALWAYS AS ( - type IS NULL - OR - type NOT IN ( - 'change-number-notification', - 'contact-removed-notification', - 'conversation-merge', - 'group-v1-migration', - 'keychange', - 'message-history-unsynced', - 'profile-change', - 'story', - 'universal-timer-notification', - 'verified-change' - ) - ); + --- From migration 76 + CREATE INDEX messages_preview ON messages + (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, + received_at, sent_at); - --- (change: added contact-removed-notification) - ALTER TABLE messages - ADD COLUMN isUserInitiatedMessage INTEGER - GENERATED ALWAYS AS ( - type IS NULL - OR - type NOT IN ( - 'change-number-notification', - 'contact-removed-notification', - 'conversation-merge', - 'group-v1-migration', - 'group-v2-change', - 'keychange', - 'message-history-unsynced', - 'profile-change', - 'story', - 'universal-timer-notification', - 'verified-change' - ) - ); + --- From migration 76 + CREATE INDEX messages_preview_without_story ON messages + (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, + received_at, sent_at) WHERE storyId IS NULL; - --- From migration 76 - CREATE INDEX messages_preview ON messages - (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, - received_at, sent_at); + --- From migration 73 + CREATE INDEX messages_activity ON messages + (conversationId, shouldAffectActivity, isTimerChangeFromSync, isGroupLeaveEventFromOther, received_at, sent_at); - --- From migration 76 - CREATE INDEX messages_preview_without_story ON messages - (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, - received_at, sent_at) WHERE storyId IS NULL; - - --- From migration 73 - CREATE INDEX messages_activity ON messages - (conversationId, shouldAffectActivity, isTimerChangeFromSync, isGroupLeaveEventFromOther, received_at, sent_at); - - --- From migration 74 - CREATE INDEX message_user_initiated ON messages (conversationId, isUserInitiatedMessage); - ` - ); - - db.pragma('user_version = 81'); - })(); - - logger.info('updateToSchemaVersion81: success!'); + --- From migration 74 + CREATE INDEX message_user_initiated ON messages (conversationId, isUserInitiatedMessage); + ` + ); } diff --git a/ts/sql/migrations/82-edited-messages-read-index.ts b/ts/sql/migrations/82-edited-messages-read-index.ts index f95ba0bd636..328c9839d45 100644 --- a/ts/sql/migrations/82-edited-messages-read-index.ts +++ b/ts/sql/migrations/82-edited-messages-read-index.ts @@ -3,27 +3,11 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion82(db: Database): void { + db.exec(` + ALTER TABLE edited_messages DROP COLUMN fromId; + ALTER TABLE edited_messages ADD COLUMN conversationId STRING; -export default function updateToSchemaVersion82( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 82) { - return; - } - - db.transaction(() => { - db.exec(` - ALTER TABLE edited_messages DROP COLUMN fromId; - ALTER TABLE edited_messages ADD COLUMN conversationId STRING; - - CREATE INDEX edited_messages_unread ON edited_messages (readStatus, conversationId); - `); - - db.pragma('user_version = 82'); - })(); - - logger.info('updateToSchemaVersion82: success!'); + CREATE INDEX edited_messages_unread ON edited_messages (readStatus, conversationId); + `); } diff --git a/ts/sql/migrations/83-mentions.ts b/ts/sql/migrations/83-mentions.ts index 7209c286d7b..50a138e8877 100644 --- a/ts/sql/migrations/83-mentions.ts +++ b/ts/sql/migrations/83-mentions.ts @@ -2,37 +2,22 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; -export default function updateToSchemaVersion83( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 83) { - return; - } +export default function updateToSchemaVersion83(db: Database): void { + db.exec( + ` + ALTER TABLE messages + ADD COLUMN mentionsMe INTEGER NOT NULL DEFAULT 0; - db.transaction(() => { - db.exec( - ` - ALTER TABLE messages - ADD COLUMN mentionsMe INTEGER NOT NULL DEFAULT 0; - - -- one which includes story data... - CREATE INDEX messages_unread_mentions ON messages - (conversationId, readStatus, mentionsMe, isStory, storyId, received_at, sent_at) - WHERE readStatus IS NOT NULL; - - -- ...and one which doesn't, so storyPredicate works as expected - CREATE INDEX messages_unread_mentions_no_story_id ON messages - (conversationId, readStatus, mentionsMe, isStory, received_at, sent_at) - WHERE isStory IS 0 AND readStatus IS NOT NULL; - ` - ); + -- one which includes story data... + CREATE INDEX messages_unread_mentions ON messages + (conversationId, readStatus, mentionsMe, isStory, storyId, received_at, sent_at) + WHERE readStatus IS NOT NULL; - db.pragma('user_version = 83'); - })(); - - logger.info('updateToSchemaVersion83: success!'); + -- ...and one which doesn't, so storyPredicate works as expected + CREATE INDEX messages_unread_mentions_no_story_id ON messages + (conversationId, readStatus, mentionsMe, isStory, received_at, sent_at) + WHERE isStory IS 0 AND readStatus IS NOT NULL; + ` + ); } diff --git a/ts/sql/migrations/84-all-mentions.ts b/ts/sql/migrations/84-all-mentions.ts index 8e2067aed82..d7987c3f742 100644 --- a/ts/sql/migrations/84-all-mentions.ts +++ b/ts/sql/migrations/84-all-mentions.ts @@ -2,59 +2,44 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; -export default function updateToSchemaVersion84( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 84) { - return; - } +export default function updateToSchemaVersion84(db: Database): void { + const selectMentionsFromMessages = ` + SELECT messages.id, bodyRanges.value ->> 'mentionUuid' as mentionUuid, bodyRanges.value ->> 'start' as start, bodyRanges.value ->> 'length' as length + FROM messages, json_each(messages.json ->> 'bodyRanges') as bodyRanges + WHERE bodyRanges.value ->> 'mentionUuid' IS NOT NULL + `; - db.transaction(() => { - const selectMentionsFromMessages = ` - SELECT messages.id, bodyRanges.value ->> 'mentionUuid' as mentionUuid, bodyRanges.value ->> 'start' as start, bodyRanges.value ->> 'length' as length - FROM messages, json_each(messages.json ->> 'bodyRanges') as bodyRanges - WHERE bodyRanges.value ->> 'mentionUuid' IS NOT NULL - `; + db.exec(` + DROP TABLE IF EXISTS mentions; - db.exec(` - DROP TABLE IF EXISTS mentions; + CREATE TABLE mentions ( + messageId REFERENCES messages(id) ON DELETE CASCADE, + mentionUuid STRING, + start INTEGER, + length INTEGER + ); - CREATE TABLE mentions ( - messageId REFERENCES messages(id) ON DELETE CASCADE, - mentionUuid STRING, - start INTEGER, - length INTEGER - ); + CREATE INDEX mentions_uuid ON mentions (mentionUuid); - CREATE INDEX mentions_uuid ON mentions (mentionUuid); + INSERT INTO mentions (messageId, mentionUuid, start, length) + ${selectMentionsFromMessages}; + -- Note: any changes to this trigger must be reflected in + -- Server.ts: enableMessageInsertTriggersAndBackfill + CREATE TRIGGER messages_on_insert_insert_mentions AFTER INSERT ON messages + BEGIN INSERT INTO mentions (messageId, mentionUuid, start, length) - ${selectMentionsFromMessages}; + ${selectMentionsFromMessages} + AND messages.id = new.id; + END; - -- Note: any changes to this trigger must be reflected in - -- Server.ts: enableMessageInsertTriggersAndBackfill - CREATE TRIGGER messages_on_insert_insert_mentions AFTER INSERT ON messages - BEGIN - INSERT INTO mentions (messageId, mentionUuid, start, length) - ${selectMentionsFromMessages} - AND messages.id = new.id; - END; - - CREATE TRIGGER messages_on_update_update_mentions AFTER UPDATE ON messages - BEGIN - DELETE FROM mentions WHERE messageId = new.id; - INSERT INTO mentions (messageId, mentionUuid, start, length) - ${selectMentionsFromMessages} - AND messages.id = new.id; - END; - `); - - db.pragma('user_version = 84'); - })(); - - logger.info('updateToSchemaVersion84: success!'); + CREATE TRIGGER messages_on_update_update_mentions AFTER UPDATE ON messages + BEGIN + DELETE FROM mentions WHERE messageId = new.id; + INSERT INTO mentions (messageId, mentionUuid, start, length) + ${selectMentionsFromMessages} + AND messages.id = new.id; + END; + `); } diff --git a/ts/sql/migrations/85-add-kyber-keys.ts b/ts/sql/migrations/85-add-kyber-keys.ts index d49fd555fb2..e532ca5d496 100644 --- a/ts/sql/migrations/85-add-kyber-keys.ts +++ b/ts/sql/migrations/85-add-kyber-keys.ts @@ -3,40 +3,24 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion85(db: Database): void { + db.exec( + `CREATE TABLE kyberPreKeys( + id STRING PRIMARY KEY NOT NULL, + json TEXT NOT NULL, + ourUuid STRING + GENERATED ALWAYS AS (json_extract(json, '$.ourUuid')) + );` + ); -export default function updateToSchemaVersion85( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 85) { - return; - } + // To manage our ACI or PNI keys quickly + db.exec('CREATE INDEX kyberPreKeys_ourUuid ON kyberPreKeys (ourUuid);'); - db.transaction(() => { - db.exec( - `CREATE TABLE kyberPreKeys( - id STRING PRIMARY KEY NOT NULL, - json TEXT NOT NULL, - ourUuid STRING - GENERATED ALWAYS AS (json_extract(json, '$.ourUuid')) - );` - ); - - // To manage our ACI or PNI keys quickly - db.exec('CREATE INDEX kyberPreKeys_ourUuid ON kyberPreKeys (ourUuid);'); - - // Add time to all existing preKeys to allow us to expire them - const now = Date.now(); - db.exec( - `UPDATE preKeys SET - json = json_set(json, '$.createdAt', ${now}); - ` - ); - - db.pragma('user_version = 85'); - })(); - - logger.info('updateToSchemaVersion85: success!'); + // Add time to all existing preKeys to allow us to expire them + const now = Date.now(); + db.exec( + `UPDATE preKeys SET + json = json_set(json, '$.createdAt', ${now}); + ` + ); } diff --git a/ts/sql/migrations/86-story-replies-index.ts b/ts/sql/migrations/86-story-replies-index.ts index 2ca9723584b..e9fd0d76868 100644 --- a/ts/sql/migrations/86-story-replies-index.ts +++ b/ts/sql/migrations/86-story-replies-index.ts @@ -3,30 +3,14 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; - -export default function updateToSchemaVersion86( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 86) { - return; - } - - db.transaction(() => { - // The key reason for this new schema is that all of our previous schemas start with - // conversationId. This query is meant to find all replies to a given story, no - // matter the conversation. - db.exec( - `CREATE INDEX messages_story_replies - ON messages (storyId, received_at, sent_at) - WHERE isStory IS 0; - ` - ); - - db.pragma('user_version = 86'); - })(); - - logger.info('updateToSchemaVersion86: success!'); +export default function updateToSchemaVersion86(db: Database): void { + // The key reason for this new schema is that all of our previous schemas start with + // conversationId. This query is meant to find all replies to a given story, no + // matter the conversation. + db.exec( + `CREATE INDEX messages_story_replies + ON messages (storyId, received_at, sent_at) + WHERE isStory IS 0; + ` + ); } diff --git a/ts/sql/migrations/87-cleanup.ts b/ts/sql/migrations/87-cleanup.ts index 52f2ad138f8..d6e1c86caf2 100644 --- a/ts/sql/migrations/87-cleanup.ts +++ b/ts/sql/migrations/87-cleanup.ts @@ -3,8 +3,8 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; import { cleanKeys } from './920-clean-more-keys'; +import type { LoggerType } from '../../types/Logging'; import { sqlFragment } from '../util'; // Note: for many users, this is not what ran for them as migration 87. You can see that @@ -13,42 +13,38 @@ import { sqlFragment } from '../util'; // The goal of this migration is to ensure that key cleanup happens before migration 88. export default function updateToSchemaVersion87( - currentVersion: number, db: Database, - logger: LoggerType + logger: LoggerType, + startingVersion: number ): void { // We're checking for the version of the next migration here, not this version. We want // this to run if the user hasn't yet successfully run migration 88. - if (currentVersion >= 88) { + if (startingVersion >= 88) { return; } - db.transaction(() => { - cleanKeys( - db, - logger, - 'updateToSchemaVersion87(cleanup)/kyberPreKeys', - sqlFragment`kyberPreKeys`, - sqlFragment`createdAt`, - sqlFragment`ourUuid` - ); - cleanKeys( - db, - logger, - 'updateToSchemaVersion87(cleanup)/preKeys', - sqlFragment`preKeys`, - sqlFragment`createdAt`, - sqlFragment`ourUuid` - ); - cleanKeys( - db, - logger, - 'updateToSchemaVersion87(cleanup)/signedPreKeys', - sqlFragment`signedPreKeys`, - sqlFragment`created_at`, - sqlFragment`ourUuid` - ); - })(); - - logger.info('updateToSchemaVersion87(cleanup): success!'); + cleanKeys( + db, + logger, + '(cleanup)/kyberPreKeys', + sqlFragment`kyberPreKeys`, + sqlFragment`createdAt`, + sqlFragment`ourUuid` + ); + cleanKeys( + db, + logger, + '(cleanup)/preKeys', + sqlFragment`preKeys`, + sqlFragment`createdAt`, + sqlFragment`ourUuid` + ); + cleanKeys( + db, + logger, + '(cleanup)/signedPreKeys', + sqlFragment`signedPreKeys`, + sqlFragment`created_at`, + sqlFragment`ourUuid` + ); } diff --git a/ts/sql/migrations/88-service-ids.ts b/ts/sql/migrations/88-service-ids.ts index 0848057c05f..3268809af28 100644 --- a/ts/sql/migrations/88-service-ids.ts +++ b/ts/sql/migrations/88-service-ids.ts @@ -26,14 +26,9 @@ import { isNotNil } from '../../util/isNotNil'; // export default function updateToSchemaVersion88( - currentVersion: number, db: Database, logger: LoggerType ): void { - if (currentVersion >= 88) { - return; - } - // See updateToSchemaVersion84 const selectMentionsFromMessages = ` SELECT messages.id, bodyRanges.value ->> 'mentionAci' as mentionAci, @@ -43,205 +38,199 @@ export default function updateToSchemaVersion88( WHERE bodyRanges.value ->> 'mentionAci' IS NOT NULL `; - db.transaction(() => { - // Rename all columns and re-create all indexes first. - db.exec(` - -- - -- conversations - -- + // Rename all columns and re-create all indexes first. + db.exec(` + -- + -- conversations + -- - DROP INDEX conversations_uuid; + DROP INDEX conversations_uuid; - ALTER TABLE conversations - RENAME COLUMN uuid TO serviceId; + ALTER TABLE conversations + RENAME COLUMN uuid TO serviceId; - -- See: updateToSchemaVersion20 - CREATE INDEX conversations_serviceId ON conversations(serviceId); + -- See: updateToSchemaVersion20 + CREATE INDEX conversations_serviceId ON conversations(serviceId); - -- - -- sessions - -- + -- + -- sessions + -- - ALTER TABLE sessions - RENAME COLUMN ourUuid TO ourServiceId; - ALTER TABLE sessions - RENAME COLUMN uuid TO serviceId; + ALTER TABLE sessions + RENAME COLUMN ourUuid TO ourServiceId; + ALTER TABLE sessions + RENAME COLUMN uuid TO serviceId; - -- - -- messages - -- + -- + -- messages + -- - DROP INDEX messages_sourceUuid; - DROP INDEX messages_preview; - DROP INDEX messages_preview_without_story; - DROP INDEX messages_activity; + DROP INDEX messages_sourceUuid; + DROP INDEX messages_preview; + DROP INDEX messages_preview_without_story; + DROP INDEX messages_activity; - ALTER TABLE messages - DROP COLUMN isGroupLeaveEventFromOther; - ALTER TABLE messages - DROP COLUMN isGroupLeaveEvent; + ALTER TABLE messages + DROP COLUMN isGroupLeaveEventFromOther; + ALTER TABLE messages + DROP COLUMN isGroupLeaveEvent; - ALTER TABLE messages - RENAME COLUMN sourceUuid TO sourceServiceId; + ALTER TABLE messages + RENAME COLUMN sourceUuid TO sourceServiceId; - -- See: updateToSchemaVersion47 - ALTER TABLE messages - ADD COLUMN isGroupLeaveEvent INTEGER - GENERATED ALWAYS AS ( - type IS 'group-v2-change' AND - json_array_length(json_extract(json, '$.groupV2Change.details')) IS 1 AND - json_extract(json, '$.groupV2Change.details[0].type') IS 'member-remove' AND - json_extract(json, '$.groupV2Change.from') IS NOT NULL AND - json_extract(json, '$.groupV2Change.from') IS json_extract(json, '$.groupV2Change.details[0].aci') - ); - - ALTER TABLE messages - ADD COLUMN isGroupLeaveEventFromOther INTEGER - GENERATED ALWAYS AS ( - isGroupLeaveEvent IS 1 - AND - isChangeCreatedByUs IS 0 - ); - - -- See: updateToSchemaVersion25 - CREATE INDEX messages_sourceServiceId on messages(sourceServiceId); - - -- See: updateToSchemaVersion81 - CREATE INDEX messages_preview ON messages - (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, - received_at, sent_at); - CREATE INDEX messages_preview_without_story ON messages - (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, - received_at, sent_at) WHERE storyId IS NULL; - CREATE INDEX messages_activity ON messages - (conversationId, shouldAffectActivity, isTimerChangeFromSync, - isGroupLeaveEventFromOther, received_at, sent_at); - - -- - -- reactions - -- - - DROP INDEX reaction_identifier; - - ALTER TABLE reactions - RENAME COLUMN targetAuthorUuid TO targetAuthorAci; - - -- See: updateToSchemaVersion29 - CREATE INDEX reaction_identifier ON reactions ( - emoji, - targetAuthorAci, - targetTimestamp + -- See: updateToSchemaVersion47 + ALTER TABLE messages + ADD COLUMN isGroupLeaveEvent INTEGER + GENERATED ALWAYS AS ( + type IS 'group-v2-change' AND + json_array_length(json_extract(json, '$.groupV2Change.details')) IS 1 AND + json_extract(json, '$.groupV2Change.details[0].type') IS 'member-remove' AND + json_extract(json, '$.groupV2Change.from') IS NOT NULL AND + json_extract(json, '$.groupV2Change.from') IS json_extract(json, '$.groupV2Change.details[0].aci') ); - -- - -- unprocessed - -- + ALTER TABLE messages + ADD COLUMN isGroupLeaveEventFromOther INTEGER + GENERATED ALWAYS AS ( + isGroupLeaveEvent IS 1 + AND + isChangeCreatedByUs IS 0 + ); - ALTER TABLE unprocessed - RENAME COLUMN sourceUuid TO sourceServiceId; + -- See: updateToSchemaVersion25 + CREATE INDEX messages_sourceServiceId on messages(sourceServiceId); - -- - -- sendLogRecipients - -- + -- See: updateToSchemaVersion81 + CREATE INDEX messages_preview ON messages + (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, + received_at, sent_at); + CREATE INDEX messages_preview_without_story ON messages + (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, + received_at, sent_at) WHERE storyId IS NULL; + CREATE INDEX messages_activity ON messages + (conversationId, shouldAffectActivity, isTimerChangeFromSync, + isGroupLeaveEventFromOther, received_at, sent_at); - DROP INDEX sendLogRecipientsByRecipient; + -- + -- reactions + -- - ALTER TABLE sendLogRecipients - RENAME COLUMN recipientUuid TO recipientServiceId; + DROP INDEX reaction_identifier; - -- See: updateToSchemaVersion37 - CREATE INDEX sendLogRecipientsByRecipient - ON sendLogRecipients (recipientServiceId, deviceId); + ALTER TABLE reactions + RENAME COLUMN targetAuthorUuid TO targetAuthorAci; - -- - -- storyDistributionMembers - -- + -- See: updateToSchemaVersion29 + CREATE INDEX reaction_identifier ON reactions ( + emoji, + targetAuthorAci, + targetTimestamp + ); - ALTER TABLE storyDistributionMembers - RENAME COLUMN uuid TO serviceId; + -- + -- unprocessed + -- - -- - -- mentions - -- + ALTER TABLE unprocessed + RENAME COLUMN sourceUuid TO sourceServiceId; - DROP TRIGGER messages_on_update; - DROP TRIGGER messages_on_insert_insert_mentions; - DROP TRIGGER messages_on_update_update_mentions; - DROP INDEX mentions_uuid; + -- + -- sendLogRecipients + -- - ALTER TABLE mentions - RENAME COLUMN mentionUuid TO mentionAci; + DROP INDEX sendLogRecipientsByRecipient; - -- See: updateToSchemaVersion84 - CREATE INDEX mentions_aci ON mentions (mentionAci); + ALTER TABLE sendLogRecipients + RENAME COLUMN recipientUuid TO recipientServiceId; - -- - -- preKeys - -- + -- See: updateToSchemaVersion37 + CREATE INDEX sendLogRecipientsByRecipient + ON sendLogRecipients (recipientServiceId, deviceId); - DROP INDEX preKeys_ourUuid; - DROP INDEX signedPreKeys_ourUuid; - DROP INDEX kyberPreKeys_ourUuid; + -- + -- storyDistributionMembers + -- - ALTER TABLE preKeys - RENAME COLUMN ourUuid TO ourServiceId; - ALTER TABLE signedPreKeys - RENAME COLUMN ourUuid TO ourServiceId; - ALTER TABLE kyberPreKeys - RENAME COLUMN ourUuid TO ourServiceId; + ALTER TABLE storyDistributionMembers + RENAME COLUMN uuid TO serviceId; - -- See: updateToSchemaVersion64 - CREATE INDEX preKeys_ourServiceId ON preKeys (ourServiceId); - CREATE INDEX signedPreKeys_ourServiceId ON signedPreKeys (ourServiceId); - CREATE INDEX kyberPreKeys_ourServiceId ON kyberPreKeys (ourServiceId); - `); + -- + -- mentions + -- - // Migrate JSON fields - const { identifierToServiceId } = migrateConversations(db, logger); - const ourServiceIds = migrateItems(db, logger); - migrateSessions(db, ourServiceIds, logger); - migrateMessages(db, logger); - migratePreKeys(db, 'preKeys', ourServiceIds, logger); - migratePreKeys(db, 'signedPreKeys', ourServiceIds, logger); - migratePreKeys(db, 'kyberPreKeys', ourServiceIds, logger); - migrateJobs(db, identifierToServiceId, logger); + DROP TRIGGER messages_on_update; + DROP TRIGGER messages_on_insert_insert_mentions; + DROP TRIGGER messages_on_update_update_mentions; + DROP INDEX mentions_uuid; - // Re-create triggers after updating messages - db.exec(` - -- See: updateToSchemaVersion45 - CREATE TRIGGER messages_on_update AFTER UPDATE ON messages - WHEN - (new.body IS NULL OR old.body IS NOT new.body) AND - new.isViewOnce IS NOT 1 AND new.storyId IS NULL - BEGIN - DELETE FROM messages_fts WHERE rowid = old.rowid; - INSERT INTO messages_fts - (rowid, body) - VALUES - (new.rowid, new.body); - END; + ALTER TABLE mentions + RENAME COLUMN mentionUuid TO mentionAci; - -- See: updateToSchemaVersion84 - CREATE TRIGGER messages_on_insert_insert_mentions AFTER INSERT ON messages - BEGIN - INSERT INTO mentions (messageId, mentionAci, start, length) - ${selectMentionsFromMessages} - AND messages.id = new.id; - END; + -- See: updateToSchemaVersion84 + CREATE INDEX mentions_aci ON mentions (mentionAci); - CREATE TRIGGER messages_on_update_update_mentions AFTER UPDATE ON messages - BEGIN - DELETE FROM mentions WHERE messageId = new.id; - INSERT INTO mentions (messageId, mentionAci, start, length) - ${selectMentionsFromMessages} - AND messages.id = new.id; - END; - `); + -- + -- preKeys + -- - db.pragma('user_version = 88'); - })(); + DROP INDEX preKeys_ourUuid; + DROP INDEX signedPreKeys_ourUuid; + DROP INDEX kyberPreKeys_ourUuid; - logger.info('updateToSchemaVersion88: success!'); + ALTER TABLE preKeys + RENAME COLUMN ourUuid TO ourServiceId; + ALTER TABLE signedPreKeys + RENAME COLUMN ourUuid TO ourServiceId; + ALTER TABLE kyberPreKeys + RENAME COLUMN ourUuid TO ourServiceId; + + -- See: updateToSchemaVersion64 + CREATE INDEX preKeys_ourServiceId ON preKeys (ourServiceId); + CREATE INDEX signedPreKeys_ourServiceId ON signedPreKeys (ourServiceId); + CREATE INDEX kyberPreKeys_ourServiceId ON kyberPreKeys (ourServiceId); + `); + + // Migrate JSON fields + const { identifierToServiceId } = migrateConversations(db, logger); + const ourServiceIds = migrateItems(db, logger); + migrateSessions(db, ourServiceIds, logger); + migrateMessages(db, logger); + migratePreKeys(db, 'preKeys', ourServiceIds, logger); + migratePreKeys(db, 'signedPreKeys', ourServiceIds, logger); + migratePreKeys(db, 'kyberPreKeys', ourServiceIds, logger); + migrateJobs(db, identifierToServiceId, logger); + + // Re-create triggers after updating messages + db.exec(` + -- See: updateToSchemaVersion45 + CREATE TRIGGER messages_on_update AFTER UPDATE ON messages + WHEN + (new.body IS NULL OR old.body IS NOT new.body) AND + new.isViewOnce IS NOT 1 AND new.storyId IS NULL + BEGIN + DELETE FROM messages_fts WHERE rowid = old.rowid; + INSERT INTO messages_fts + (rowid, body) + VALUES + (new.rowid, new.body); + END; + + -- See: updateToSchemaVersion84 + CREATE TRIGGER messages_on_insert_insert_mentions AFTER INSERT ON messages + BEGIN + INSERT INTO mentions (messageId, mentionAci, start, length) + ${selectMentionsFromMessages} + AND messages.id = new.id; + END; + + CREATE TRIGGER messages_on_update_update_mentions AFTER UPDATE ON messages + BEGIN + DELETE FROM mentions WHERE messageId = new.id; + INSERT INTO mentions (messageId, mentionAci, start, length) + ${selectMentionsFromMessages} + AND messages.id = new.id; + END; + `); } // @@ -338,9 +327,7 @@ function migrateConversations( 'UPDATE conversations SET json = $json WHERE id IS $id' ); - logger.info( - `updateToSchemaVersion88: updating ${convos.length} conversations` - ); + logger.info(`updating ${convos.length} conversations`); // Build lookup map for senderKeyInfo const identifierToServiceId = new Map(); @@ -439,9 +426,7 @@ function migrateConversations( .map(({ identifier, ...rest }) => { const deviceServiceId = identifierToServiceId.get(identifier); if (!deviceServiceId) { - logger.warn( - `updateToSchemaVersion88: failed to resolve identifier ${identifier}` - ); + logger.warn(`failed to resolve identifier ${identifier}`); return undefined; } @@ -454,10 +439,7 @@ function migrateConversations( updateStmt.run({ id, json: JSON.stringify(modern) }); } catch (error) { - logger.warn( - `updateToSchemaVersion88: failed to parse convo ${id} json`, - error - ); + logger.warn(`failed to parse convo ${id} json`, error); continue; } } @@ -511,12 +493,9 @@ function migrateItems(db: Database, logger: LoggerType): OurServiceIds { [legacyAci] = JSON.parse(uuidIdJson ?? '').value.split('.', 2); } catch (error) { if (uuidIdJson) { - logger.warn( - 'updateToSchemaVersion88: failed to parse uuid_id item', - error - ); + logger.warn('failed to parse uuid_id item', error); } else { - logger.info('updateToSchemaVersion88: Our UUID not found'); + logger.info('Our UUID not found'); } } @@ -525,9 +504,9 @@ function migrateItems(db: Database, logger: LoggerType): OurServiceIds { legacyPni = JSON.parse(pniJson ?? '').value; } catch (error) { if (pniJson) { - logger.warn('updateToSchemaVersion88: failed to parse pni item', error); + logger.warn('failed to parse pni item', error); } else { - logger.info('updateToSchemaVersion88: Our PNI not found'); + logger.info('Our PNI not found'); } } @@ -573,7 +552,7 @@ function migrateItems(db: Database, logger: LoggerType): OurServiceIds { updateStmt.run({ id, json: JSON.stringify(data) }); } catch (error) { - logger.warn(`updateToSchemaVersion88: failed to parse ${id} item`, error); + logger.warn(`failed to parse ${id} item`, error); } } return { aci, pni, legacyAci, legacyPni }; @@ -611,21 +590,18 @@ function migrateSessions( ` ); - logger.info(`updateToSchemaVersion88: updating ${sessions.length} sessions`); + logger.info(`updating ${sessions.length} sessions`); for (const { id, serviceId, ourServiceId, json } of sessions) { const match = id.match(/^(.*):(.*)\.(.*)$/); if (!match) { - logger.warn(`updateToSchemaVersion88: invalid session id ${id}`); + logger.warn(`invalid session id ${id}`); continue; } let legacyData: JSONWithUnknownFields>; try { legacyData = JSON.parse(json); } catch (error) { - logger.warn( - `updateToSchemaVersion88: failed to parse session ${id}`, - error - ); + logger.warn(`failed to parse session ${id}`, error); continue; } @@ -642,7 +618,7 @@ function migrateSessions( ); if (!newServiceId || !newOurServiceId) { logger.warn( - 'updateToSchemaVersion88: failed to normalize session service ids', + 'failed to normalize session service ids', serviceId, ourServiceId ); @@ -753,7 +729,7 @@ function migrateMessages(db: Database, logger: LoggerType): void { WHERE rowid = $rowid `); - logger.info('updateToSchemaVersion88: updating messages'); + logger.info('updating messages'); let totalMessages = 0; // eslint-disable-next-line no-constant-condition @@ -832,15 +808,12 @@ function migrateMessages(db: Database, logger: LoggerType): void { json: JSON.stringify(updatedMessage), }); } catch (error) { - logger.warn( - `updateToSchemaVersion88: failed to parse message ${id} json`, - error - ); + logger.warn(`failed to parse message ${id} json`, error); } } } - logger.info(`updateToSchemaVersion88: updated ${totalMessages} messages`); + logger.info(`updated ${totalMessages} messages`); } // migratePreKeys works similarly to migrateSessions and does: @@ -867,11 +840,11 @@ function migratePreKeys( WHERE id = $id `); - logger.info(`updateToSchemaVersion88: updating ${preKeys.length} ${table}`); + logger.info(`updating ${preKeys.length} ${table}`); for (const { id, json } of preKeys) { const match = id.match(/^(.*):(.*)$/); if (!match) { - logger.warn(`updateToSchemaVersion88: invalid ${table} id ${id}`); + logger.warn(`invalid ${table} id ${id}`); continue; } @@ -879,10 +852,7 @@ function migratePreKeys( try { legacyData = JSON.parse(json); } catch (error) { - logger.warn( - `updateToSchemaVersion88: failed to parse ${table} ${id}`, - error - ); + logger.warn(`failed to parse ${table} ${id}`, error); continue; } @@ -1118,7 +1088,7 @@ function migrateJobs( const serviceId = identifierToServiceId.get(identifier); if (!serviceId) { logger.warn( - `updateToSchemaVersion88: failed to resolve identifier ${identifier} ` + + `failed to resolve identifier ${identifier} ` + `for job ${id}/${queueType}` ); continue; @@ -1137,14 +1107,11 @@ function migrateJobs( updateStmt.run({ id, data: JSON.stringify(updatedData) }); } } catch (error) { - logger.warn( - `updateToSchemaVersion88: failed to migrate job ${id}/${queueType} json`, - error - ); + logger.warn(`failed to migrate job ${id}/${queueType} json`, error); } } - logger.info(`updateToSchemaVersion88: updated ${updatedCount} jobs`); + logger.info(`updated ${updatedCount} jobs`); } // diff --git a/ts/sql/migrations/89-call-history.ts b/ts/sql/migrations/89-call-history.ts index 7e07bdbc271..ba6288de7d1 100644 --- a/ts/sql/migrations/89-call-history.ts +++ b/ts/sql/migrations/89-call-history.ts @@ -94,7 +94,7 @@ function getPeerIdFromConversation( if (conversation.type === 'private') { if (conversation.serviceId == null) { logger.warn( - `updateToSchemaVersion89: Private conversation (${conversation.id}) was missing serviceId (discoveredUnregisteredAt: ${conversation.discoveredUnregisteredAt})` + `Private conversation (${conversation.id}) was missing serviceId (discoveredUnregisteredAt: ${conversation.discoveredUnregisteredAt})` ); return conversation.id; } @@ -191,205 +191,194 @@ function convertLegacyCallDetails( } export default function updateToSchemaVersion89( - currentVersion: number, db: WritableDB, logger: LoggerType ): void { - if (currentVersion >= 89) { - return; + const ourUuid = getOurUuid(db); + + const [createTable] = sql` + -- This table may have already existed from migration 87 + CREATE TABLE IF NOT EXISTS callsHistory ( + callId TEXT PRIMARY KEY, + peerId TEXT NOT NULL, -- conversation id (legacy) | uuid | groupId | roomId + ringerId TEXT DEFAULT NULL, -- ringer uuid + mode TEXT NOT NULL, -- enum "Direct" | "Group" + type TEXT NOT NULL, -- enum "Audio" | "Video" | "Group" + direction TEXT NOT NULL, -- enum "Incoming" | "Outgoing + -- Direct: enum "Pending" | "Missed" | "Accepted" | "Deleted" + -- Group: enum "GenericGroupCall" | "OutgoingRing" | "Ringing" | "Joined" | "Missed" | "Declined" | "Accepted" | "Deleted" + status TEXT NOT NULL, + timestamp INTEGER NOT NULL, + UNIQUE (callId, peerId) ON CONFLICT FAIL + ); + + -- Update peerId to be uuid or groupId + UPDATE callsHistory + SET peerId = ( + SELECT + CASE + WHEN conversations.type = 'private' THEN conversations.serviceId + WHEN conversations.type = 'group' THEN conversations.groupId + END + FROM conversations + WHERE callsHistory.peerId IS conversations.id + AND callsHistory.peerId IS NOT conversations.serviceId + ) + WHERE EXISTS ( + SELECT 1 + FROM conversations + WHERE callsHistory.peerId IS conversations.id + AND callsHistory.peerId IS NOT conversations.serviceId + ); + + CREATE INDEX IF NOT EXISTS callsHistory_order on callsHistory (timestamp DESC); + CREATE INDEX IF NOT EXISTS callsHistory_byConversation ON callsHistory (peerId); + -- For 'getCallHistoryGroupData': + -- This index should target the subqueries for 'possible_parent' and 'possible_children' + CREATE INDEX IF NOT EXISTS callsHistory_callAndGroupInfo_optimize on callsHistory ( + direction, + peerId, + timestamp DESC, + status + ); + `; + + db.exec(createTable); + + const [selectQuery] = sql` + SELECT + messages.json AS messageJson, + conversations.id AS conversationId, + conversations.json AS conversationJson + FROM messages + LEFT JOIN conversations ON conversations.id = messages.conversationId + WHERE messages.type = 'call-history' + -- Some of these messages were already migrated + AND messages.json->'callHistoryDetails' IS NOT NULL + -- Sort from oldest to newest, so that newer messages can overwrite older + ORDER BY messages.received_at ASC, messages.sent_at ASC; + `; + + // Must match query above + type CallHistoryRow = { + messageJson: string; + conversationId: string; + conversationJson: string; + }; + + const rows: Array = db.prepare(selectQuery).all(); + + for (const row of rows) { + const { messageJson, conversationId, conversationJson } = row; + const message = jsonToObject(messageJson); + const conversation = jsonToObject(conversationJson); + + if (!isObject(conversation)) { + logger.warn( + `Private conversation (${conversationId}) ` + + 'has non-object json column' + ); + continue; + } + + const details = message.callHistoryDetails; + + const peerId = getPeerIdFromConversation(conversation, logger); + + const callHistory = convertLegacyCallDetails( + ourUuid, + peerId, + message, + details, + logger + ); + + const [insertQuery, insertParams] = sql` + -- Using 'OR REPLACE' because in some earlier versions of call history + -- we had a bug where we would insert duplicate call history entries + -- for the same callId and peerId. + -- We're assuming here that the latest call history entry is the most + -- accurate. + INSERT OR REPLACE INTO callsHistory ( + callId, + peerId, + ringerId, + mode, + type, + direction, + status, + timestamp + ) VALUES ( + ${callHistory.callId}, + ${callHistory.peerId}, + ${callHistory.ringerId}, + ${callHistory.mode}, + ${callHistory.type}, + ${callHistory.direction}, + ${callHistory.status}, + ${callHistory.timestamp} + ) + `; + + db.prepare(insertQuery).run(insertParams); + + const messageId = message.id; + strictAssert(messageId != null, 'message.id must exist'); + + const [updateQuery, updateParams] = sql` + UPDATE messages + SET json = JSON_PATCH(json, ${JSON.stringify({ + callHistoryDetails: null, // delete + callId: callHistory.callId, + })}) + WHERE id = ${messageId} + `; + + db.prepare(updateQuery).run(updateParams); } - db.transaction(() => { - const ourUuid = getOurUuid(db); + const [dropIndex] = sql` + DROP INDEX IF EXISTS messages_call; + `; + db.exec(dropIndex); - const [createTable] = sql` - -- This table may have already existed from migration 87 - CREATE TABLE IF NOT EXISTS callsHistory ( - callId TEXT PRIMARY KEY, - peerId TEXT NOT NULL, -- conversation id (legacy) | uuid | groupId | roomId - ringerId TEXT DEFAULT NULL, -- ringer uuid - mode TEXT NOT NULL, -- enum "Direct" | "Group" - type TEXT NOT NULL, -- enum "Audio" | "Video" | "Group" - direction TEXT NOT NULL, -- enum "Incoming" | "Outgoing - -- Direct: enum "Pending" | "Missed" | "Accepted" | "Deleted" - -- Group: enum "GenericGroupCall" | "OutgoingRing" | "Ringing" | "Joined" | "Missed" | "Declined" | "Accepted" | "Deleted" - status TEXT NOT NULL, - timestamp INTEGER NOT NULL, - UNIQUE (callId, peerId) ON CONFLICT FAIL - ); - - -- Update peerId to be uuid or groupId - UPDATE callsHistory - SET peerId = ( - SELECT - CASE - WHEN conversations.type = 'private' THEN conversations.serviceId - WHEN conversations.type = 'group' THEN conversations.groupId - END - FROM conversations - WHERE callsHistory.peerId IS conversations.id - AND callsHistory.peerId IS NOT conversations.serviceId - ) - WHERE EXISTS ( - SELECT 1 - FROM conversations - WHERE callsHistory.peerId IS conversations.id - AND callsHistory.peerId IS NOT conversations.serviceId - ); - - CREATE INDEX IF NOT EXISTS callsHistory_order on callsHistory (timestamp DESC); - CREATE INDEX IF NOT EXISTS callsHistory_byConversation ON callsHistory (peerId); - -- For 'getCallHistoryGroupData': - -- This index should target the subqueries for 'possible_parent' and 'possible_children' - CREATE INDEX IF NOT EXISTS callsHistory_callAndGroupInfo_optimize on callsHistory ( - direction, - peerId, - timestamp DESC, - status - ); - `; - - db.exec(createTable); - - const [selectQuery] = sql` - SELECT - messages.json AS messageJson, - conversations.id AS conversationId, - conversations.json AS conversationJson - FROM messages - LEFT JOIN conversations ON conversations.id = messages.conversationId - WHERE messages.type = 'call-history' - -- Some of these messages were already migrated - AND messages.json->'callHistoryDetails' IS NOT NULL - -- Sort from oldest to newest, so that newer messages can overwrite older - ORDER BY messages.received_at ASC, messages.sent_at ASC; - `; - - // Must match query above - type CallHistoryRow = { - messageJson: string; - conversationId: string; - conversationJson: string; - }; - - const rows: Array = db.prepare(selectQuery).all(); - - for (const row of rows) { - const { messageJson, conversationId, conversationJson } = row; - const message = jsonToObject(messageJson); - const conversation = jsonToObject(conversationJson); - - if (!isObject(conversation)) { - logger.warn( - `updateToSchemaVersion89: Private conversation (${conversationId}) ` + - 'has non-object json column' - ); - continue; - } - - const details = message.callHistoryDetails; - - const peerId = getPeerIdFromConversation(conversation, logger); - - const callHistory = convertLegacyCallDetails( - ourUuid, - peerId, - message, - details, - logger - ); - - const [insertQuery, insertParams] = sql` - -- Using 'OR REPLACE' because in some earlier versions of call history - -- we had a bug where we would insert duplicate call history entries - -- for the same callId and peerId. - -- We're assuming here that the latest call history entry is the most - -- accurate. - INSERT OR REPLACE INTO callsHistory ( - callId, - peerId, - ringerId, - mode, - type, - direction, - status, - timestamp - ) VALUES ( - ${callHistory.callId}, - ${callHistory.peerId}, - ${callHistory.ringerId}, - ${callHistory.mode}, - ${callHistory.type}, - ${callHistory.direction}, - ${callHistory.status}, - ${callHistory.timestamp} - ) - `; - - db.prepare(insertQuery).run(insertParams); - - const messageId = message.id; - strictAssert(messageId != null, 'message.id must exist'); - - const [updateQuery, updateParams] = sql` - UPDATE messages - SET json = JSON_PATCH(json, ${JSON.stringify({ - callHistoryDetails: null, // delete - callId: callHistory.callId, - })}) - WHERE id = ${messageId} - `; - - db.prepare(updateQuery).run(updateParams); - } - - const [dropIndex] = sql` - DROP INDEX IF EXISTS messages_call; - `; - db.exec(dropIndex); - - try { - const [dropColumnQuery] = sql` - ALTER TABLE messages - DROP COLUMN callMode; - `; - db.exec(dropColumnQuery); - } catch (error) { - if (!error.message.includes('no such column: "callMode"')) { - throw error; - } - } - - try { - const [dropColumnQuery] = sql` - ALTER TABLE messages - DROP COLUMN callId; - `; - db.exec(dropColumnQuery); - } catch (error) { - if (!error.message.includes('no such column: "callId"')) { - throw error; - } - } - - const [optimizeMessages] = sql` + try { + const [dropColumnQuery] = sql` ALTER TABLE messages - ADD COLUMN callId TEXT - GENERATED ALWAYS AS ( - json_extract(json, '$.callId') - ); - -- Optimize getCallHistoryMessageByCallId - CREATE INDEX messages_call ON messages - (conversationId, type, callId); - - CREATE INDEX messages_callHistory_readStatus ON messages - (type, readStatus) - WHERE type IS 'call-history'; + DROP COLUMN callMode; `; - db.exec(optimizeMessages); + db.exec(dropColumnQuery); + } catch (error) { + if (!error.message.includes('no such column: "callMode"')) { + throw error; + } + } - db.pragma('user_version = 89'); - })(); + try { + const [dropColumnQuery] = sql` + ALTER TABLE messages + DROP COLUMN callId; + `; + db.exec(dropColumnQuery); + } catch (error) { + if (!error.message.includes('no such column: "callId"')) { + throw error; + } + } - logger.info('updateToSchemaVersion89: success!'); + const [optimizeMessages] = sql` + ALTER TABLE messages + ADD COLUMN callId TEXT + GENERATED ALWAYS AS ( + json_extract(json, '$.callId') + ); + -- Optimize getCallHistoryMessageByCallId + CREATE INDEX messages_call ON messages + (conversationId, type, callId); + + CREATE INDEX messages_callHistory_readStatus ON messages + (type, readStatus) + WHERE type IS 'call-history'; + `; + db.exec(optimizeMessages); } diff --git a/ts/sql/migrations/90-delete-story-reply-screenshot.ts b/ts/sql/migrations/90-delete-story-reply-screenshot.ts index b7919237843..c0c4fdf23eb 100644 --- a/ts/sql/migrations/90-delete-story-reply-screenshot.ts +++ b/ts/sql/migrations/90-delete-story-reply-screenshot.ts @@ -7,39 +7,28 @@ import type { LoggerType } from '../../types/Logging'; import { sql } from '../util'; export default function updateToSchemaVersion90( - currentVersion: number, db: Database, logger: LoggerType ): void { - if (currentVersion >= 90) { - return; - } - let numChanges = 0; - db.transaction(() => { - const [updateQuery, updateParams] = sql` - UPDATE messages - SET json = json_remove(json, '$.storyReplyContext.attachment.screenshotData') - WHERE isStory = 0 + const [updateQuery, updateParams] = sql` + UPDATE messages + SET json = json_remove(json, '$.storyReplyContext.attachment.screenshotData') + WHERE isStory = 0 - /* we want to find all messages with a non-null storyId, but using string - comparison (instead of a non-null check) here causes Sqlite to use the - storyId index */ - AND storyId > '0' + /* we want to find all messages with a non-null storyId, but using string + comparison (instead of a non-null check) here causes Sqlite to use the + storyId index */ + AND storyId > '0' - AND json->'$.storyReplyContext.attachment.screenshotData' IS NOT NULL; - `; + AND json->'$.storyReplyContext.attachment.screenshotData' IS NOT NULL; + `; - const info = db.prepare(updateQuery).run(updateParams); - numChanges = info.changes; - - db.pragma('user_version = 90'); - })(); + const info = db.prepare(updateQuery).run(updateParams); + numChanges = info.changes; logger.info( - `updateToSchemaVersion90: removed screenshotData from ${numChanges} ` + + `removed screenshotData from ${numChanges} ` + `message${numChanges > 1 ? 's' : ''}` ); - - logger.info('updateToSchemaVersion90: success!'); } diff --git a/ts/sql/migrations/91-clean-keys.ts b/ts/sql/migrations/91-clean-keys.ts index 67534c2d626..3e13254c77e 100644 --- a/ts/sql/migrations/91-clean-keys.ts +++ b/ts/sql/migrations/91-clean-keys.ts @@ -10,202 +10,180 @@ import { normalizePni } from '../../types/ServiceId'; import * as Errors from '../../types/errors'; export default function updateToSchemaVersion91( - currentVersion: number, db: Database, logger: LoggerType ): void { - if (currentVersion >= 91) { + // Fix the ourServiceId column so it's generated from the right JSON field + + db.exec(` + --- First, prekeys + DROP INDEX preKeys_ourServiceId; + + ALTER TABLE preKeys + DROP COLUMN ourServiceId; + ALTER TABLE preKeys + ADD COLUMN ourServiceId NUMBER + GENERATED ALWAYS AS (json_extract(json, '$.ourServiceId')); + + CREATE INDEX preKeys_ourServiceId ON preKeys (ourServiceId); + + -- Second, kyber prekeys + + DROP INDEX kyberPreKeys_ourServiceId; + + ALTER TABLE kyberPreKeys + DROP COLUMN ourServiceId; + ALTER TABLE kyberPreKeys + ADD COLUMN ourServiceId NUMBER + GENERATED ALWAYS AS (json_extract(json, '$.ourServiceId')); + + CREATE INDEX kyberPreKeys_ourServiceId ON kyberPreKeys (ourServiceId); + + -- Finally, signed prekeys + + DROP INDEX signedPreKeys_ourServiceId; + + ALTER TABLE signedPreKeys + DROP COLUMN ourServiceId; + ALTER TABLE signedPreKeys + ADD COLUMN ourServiceId NUMBER + GENERATED ALWAYS AS (json_extract(json, '$.ourServiceId')); + + CREATE INDEX signedPreKeys_ourServiceId ON signedPreKeys (ourServiceId); + `); + + // Do overall count - if it's less than 1000, move on + + const totalKeys = + db + .prepare('SELECT count(*) FROM preKeys;', { + pluck: true, + }) + .get() ?? 0; + logger.info(`Found ${totalKeys} keys`); + if (totalKeys < 1000) { return; } - db.transaction(() => { - // Fix the ourServiceId column so it's generated from the right JSON field + // Grab our PNI - db.exec(` - --- First, prekeys - DROP INDEX preKeys_ourServiceId; - - ALTER TABLE preKeys - DROP COLUMN ourServiceId; - ALTER TABLE preKeys - ADD COLUMN ourServiceId NUMBER - GENERATED ALWAYS AS (json_extract(json, '$.ourServiceId')); - - CREATE INDEX preKeys_ourServiceId ON preKeys (ourServiceId); - - -- Second, kyber prekeys - - DROP INDEX kyberPreKeys_ourServiceId; - - ALTER TABLE kyberPreKeys - DROP COLUMN ourServiceId; - ALTER TABLE kyberPreKeys - ADD COLUMN ourServiceId NUMBER - GENERATED ALWAYS AS (json_extract(json, '$.ourServiceId')); - - CREATE INDEX kyberPreKeys_ourServiceId ON kyberPreKeys (ourServiceId); - - -- Finally, signed prekeys - - DROP INDEX signedPreKeys_ourServiceId; - - ALTER TABLE signedPreKeys - DROP COLUMN ourServiceId; - ALTER TABLE signedPreKeys - ADD COLUMN ourServiceId NUMBER - GENERATED ALWAYS AS (json_extract(json, '$.ourServiceId')); - - CREATE INDEX signedPreKeys_ourServiceId ON signedPreKeys (ourServiceId); - `); - - // Do overall count - if it's less than 1000, move on - - const totalKeys = - db - .prepare('SELECT count(*) FROM preKeys;', { - pluck: true, - }) - .get() ?? 0; - logger.info(`updateToSchemaVersion91: Found ${totalKeys} keys`); - if (totalKeys < 1000) { - db.pragma('user_version = 91'); - return; + let pni: PniString; + const pniJson = db + .prepare("SELECT json FROM items WHERE id IS 'pni'", { + pluck: true, + }) + .get(); + try { + const pniData = JSON.parse(pniJson ?? ''); + pni = normalizePni(pniData.value, 'updateToSchemaVersion91'); + } catch (error) { + if (pniJson) { + logger.warn('PNI found but did not parse', Errors.toLogFormat(error)); + } else { + logger.info('Our PNI not found'); } + return; + } - // Grab our PNI + // Grab PNI-specific count - let pni: PniString; - const pniJson = db - .prepare("SELECT json FROM items WHERE id IS 'pni'", { - pluck: true, - }) - .get(); - try { - const pniData = JSON.parse(pniJson ?? ''); - pni = normalizePni(pniData.value, 'updateToSchemaVersion91'); - } catch (error) { - db.pragma('user_version = 91'); - if (pniJson) { - logger.warn( - 'updateToSchemaVersion91: PNI found but did not parse', - Errors.toLogFormat(error) - ); - } else { - logger.info('updateToSchemaVersion91: Our PNI not found'); - } - return; - } + const [beforeQuery, beforeParams] = + sql`SELECT count(*) from preKeys WHERE ourServiceId = ${pni}`; + const beforeKeys = db + .prepare(beforeQuery, { + pluck: true, + }) + .get(beforeParams); + logger.info(`Found ${beforeKeys} preKeys for PNI`); - // Grab PNI-specific count + // Create index to help us with all these queries - const [beforeQuery, beforeParams] = - sql`SELECT count(*) from preKeys WHERE ourServiceId = ${pni}`; - const beforeKeys = db - .prepare(beforeQuery, { - pluck: true, - }) - .get(beforeParams); - logger.info(`updateToSchemaVersion91: Found ${beforeKeys} preKeys for PNI`); + db.exec(` + ALTER TABLE preKeys + ADD COLUMN createdAt NUMBER + GENERATED ALWAYS AS (json_extract(json, '$.createdAt')); + + CREATE INDEX preKeys_date + ON preKeys (ourServiceId, createdAt); + `); + logger.info('Temporary index created'); - // Create index to help us with all these queries + // Fetch 500th-oldest timestamp for PNI - db.exec(` - ALTER TABLE preKeys - ADD COLUMN createdAt NUMBER - GENERATED ALWAYS AS (json_extract(json, '$.createdAt')); - - CREATE INDEX preKeys_date - ON preKeys (ourServiceId, createdAt); - `); - logger.info('updateToSchemaVersion91: Temporary index created'); + const [oldQuery, oldParams] = sql` + SELECT createdAt + FROM preKeys + WHERE + createdAt IS NOT NULL AND + ourServiceId = ${pni} + ORDER BY createdAt ASC + LIMIT 1 + OFFSET 499 + `; + const oldBoundary = db + .prepare(oldQuery, { + pluck: true, + }) + .get(oldParams); + logger.info(`Found 500th-oldest timestamp: ${oldBoundary}`); - // Fetch 500th-oldest timestamp for PNI + // Fetch 500th-newest timestamp for PNI - const [oldQuery, oldParams] = sql` - SELECT createdAt - FROM preKeys - WHERE + const [newQuery, newParams] = sql` + SELECT createdAt + FROM preKeys + WHERE + createdAt IS NOT NULL AND + ourServiceId = ${pni} + ORDER BY createdAt DESC + LIMIT 1 + OFFSET 499 + `; + const newBoundary = db + .prepare(newQuery, { + pluck: true, + }) + .get(newParams); + logger.info(`Found 500th-newest timestamp: ${newBoundary}`); + + // Delete everything in between for PNI + + let result: RunResult; + const [deleteQuery, deleteParams] = sql` + DELETE FROM preKeys + WHERE rowid IN ( + SELECT rowid FROM preKeys + WHERE createdAt IS NOT NULL AND + createdAt > ${oldBoundary ?? null} AND + createdAt < ${newBoundary ?? null} AND ourServiceId = ${pni} - ORDER BY createdAt ASC - LIMIT 1 - OFFSET 499 - `; - const oldBoundary = db - .prepare(oldQuery, { - pluck: true, - }) - .get(oldParams); - logger.info( - `updateToSchemaVersion91: Found 500th-oldest timestamp: ${oldBoundary}` + LIMIT 10000 ); + `; + const preparedQuery = db.prepare(deleteQuery); + do { + result = preparedQuery.run(deleteParams); + logger.info(`Deleted ${result.changes} items`); + } while (result.changes > 0); + logger.info('Delete is complete!'); - // Fetch 500th-newest timestamp for PNI + // Get updated count for PNI - const [newQuery, newParams] = sql` - SELECT createdAt - FROM preKeys - WHERE - createdAt IS NOT NULL AND - ourServiceId = ${pni} - ORDER BY createdAt DESC - LIMIT 1 - OFFSET 499 - `; - const newBoundary = db - .prepare(newQuery, { - pluck: true, - }) - .get(newParams); - logger.info( - `updateToSchemaVersion91: Found 500th-newest timestamp: ${newBoundary}` - ); + const [afterQuery, afterParams] = sql` + SELECT count(*) + FROM preKeys + WHERE ourServiceId = ${pni}; + `; + const afterCount = db + .prepare(afterQuery, { + pluck: true, + }) + .get(afterParams); + logger.info(`Found ${afterCount} preKeys for PNI after delete`); - // Delete everything in between for PNI - - let result: RunResult; - const [deleteQuery, deleteParams] = sql` - DELETE FROM preKeys - WHERE rowid IN ( - SELECT rowid FROM preKeys - WHERE - createdAt IS NOT NULL AND - createdAt > ${oldBoundary ?? null} AND - createdAt < ${newBoundary ?? null} AND - ourServiceId = ${pni} - LIMIT 10000 - ); - `; - const preparedQuery = db.prepare(deleteQuery); - do { - result = preparedQuery.run(deleteParams); - logger.info(`updateToSchemaVersion91: Deleted ${result.changes} items`); - } while (result.changes > 0); - logger.info('updateToSchemaVersion91: Delete is complete!'); - - // Get updated count for PNI - - const [afterQuery, afterParams] = sql` - SELECT count(*) - FROM preKeys - WHERE ourServiceId = ${pni}; - `; - const afterCount = db - .prepare(afterQuery, { - pluck: true, - }) - .get(afterParams); - logger.info( - `updateToSchemaVersion91: Found ${afterCount} preKeys for PNI after delete` - ); - - db.exec(` - DROP INDEX preKeys_date; - ALTER TABLE preKeys DROP COLUMN createdAt; - `); - - db.pragma('user_version = 91'); - })(); - - logger.info('updateToSchemaVersion91: success!'); + db.exec(` + DROP INDEX preKeys_date; + ALTER TABLE preKeys DROP COLUMN createdAt; + `); } diff --git a/ts/sql/migrations/920-clean-more-keys.ts b/ts/sql/migrations/920-clean-more-keys.ts index 9adb0d19162..f461194c86f 100644 --- a/ts/sql/migrations/920-clean-more-keys.ts +++ b/ts/sql/migrations/920-clean-more-keys.ts @@ -11,47 +11,30 @@ import { sql, sqlFragment } from '../util'; import { normalizePni } from '../../types/ServiceId'; import * as Errors from '../../types/errors'; -export const version = 920; - -export function updateToSchemaVersion920( - currentVersion: number, +export default function updateToSchemaVersion920( db: Database, logger: LoggerType -): void { - if (currentVersion >= 920) { - return; - } - - db.transaction(() => { - cleanKeys( - db, - logger, - 'updateToSchemaVersion920/kyberPreKeys', - sqlFragment`kyberPreKeys`, - sqlFragment`createdAt`, - sqlFragment`ourServiceId` - ); - cleanKeys( - db, - logger, - 'updateToSchemaVersion920/signedPreKeys', - sqlFragment`signedPreKeys`, - sqlFragment`created_at`, - sqlFragment`ourServiceId` - ); - - logger.info('updateToSchemaVersion920: Done with deletions'); - - db.pragma('user_version = 920'); - })(); - - logger.info( - 'updateToSchemaVersion920: user_version set to 920. Starting vacuum...' +): 'vacuum' { + cleanKeys( + db, + logger, + 'kyberPreKeys', + sqlFragment`kyberPreKeys`, + sqlFragment`createdAt`, + sqlFragment`ourServiceId` + ); + cleanKeys( + db, + logger, + 'signedPreKeys', + sqlFragment`signedPreKeys`, + sqlFragment`created_at`, + sqlFragment`ourServiceId` ); - db.exec('VACUUM;'); - logger.info('updateToSchemaVersion920: Vacuum complete.'); - logger.info('updateToSchemaVersion920: success!'); + logger.info('Done with deletions, starting vacuum...'); + + return 'vacuum'; } export function cleanKeys( diff --git a/ts/sql/migrations/930-fts5-secure-delete.ts b/ts/sql/migrations/930-fts5-secure-delete.ts index 3b277e0a7c4..53e1885be93 100644 --- a/ts/sql/migrations/930-fts5-secure-delete.ts +++ b/ts/sql/migrations/930-fts5-secure-delete.ts @@ -1,26 +1,6 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { Database } from '@signalapp/sqlcipher'; - -import type { LoggerType } from '../../types/Logging'; - -export const version = 930; - -export function updateToSchemaVersion930( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 930) { - return; - } - - db.transaction(() => { - // This was a migration that enabled 'secure-delete' in FTS - - db.pragma('user_version = 930'); - })(); - - logger.info('updateToSchemaVersion930: success!'); +export default function updateToSchemaVersion930(): void { + // This was a migration that enabled 'secure-delete' in FTS } diff --git a/ts/sql/migrations/940-fts5-revert.ts b/ts/sql/migrations/940-fts5-revert.ts index 60f17a4a64b..388778b5615 100644 --- a/ts/sql/migrations/940-fts5-revert.ts +++ b/ts/sql/migrations/940-fts5-revert.ts @@ -1,25 +1,6 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { Database } from '@signalapp/sqlcipher'; - -import type { LoggerType } from '../../types/Logging'; - -export const version = 940; - -export function updateToSchemaVersion940( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 940) { - return; - } - - db.transaction(() => { - // This was a migration that disabled secure-delete and rebuilt the index - db.pragma('user_version = 940'); - })(); - - logger.info('updateToSchemaVersion940: success!'); +export default function updateToSchemaVersion940(): void { + // This was a migration that disabled secure-delete and rebuilt the index } diff --git a/ts/sql/migrations/950-fts5-secure-delete.ts b/ts/sql/migrations/950-fts5-secure-delete.ts index 6351a1fcc80..2a44abd1049 100644 --- a/ts/sql/migrations/950-fts5-secure-delete.ts +++ b/ts/sql/migrations/950-fts5-secure-delete.ts @@ -1,25 +1,6 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { Database } from '@signalapp/sqlcipher'; - -import type { LoggerType } from '../../types/Logging'; - -export const version = 950; - -export function updateToSchemaVersion950( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 950) { - return; - } - - db.transaction(() => { - // This was a migration that enable secure-delete - db.pragma('user_version = 950'); - })(); - - logger.info('updateToSchemaVersion950: success!'); +export default function updateToSchemaVersion950(): void { + // This was a migration that enable secure-delete } diff --git a/ts/sql/migrations/960-untag-pni.ts b/ts/sql/migrations/960-untag-pni.ts index 04a39cc2e99..b464ee8e277 100644 --- a/ts/sql/migrations/960-untag-pni.ts +++ b/ts/sql/migrations/960-untag-pni.ts @@ -13,44 +13,31 @@ import { normalizePni } from '../../types/ServiceId'; import { normalizeAci } from '../../util/normalizeAci'; import type { JSONWithUnknownFields } from '../../types/Util'; -export const version = 960; - -export function updateToSchemaVersion960( - currentVersion: number, +export default function updateToSchemaVersion960( db: Database, logger: LoggerType ): void { - if (currentVersion >= 960) { + const ourServiceIds = migratePni(db, logger); + if (!ourServiceIds) { + logger.info('not running, pni is normalized'); return; } - db.transaction(() => { - const ourServiceIds = migratePni(db, logger); - if (!ourServiceIds) { - logger.info('updateToSchemaVersion960: not running, pni is normalized'); - return; - } + // Migrate JSON fields + db.prepare( + ` + UPDATE conversations + SET json = json_set(json, '$.pni', $pni) + WHERE serviceId IS $aci + ` + ).run({ + aci: ourServiceIds.aci, + pni: ourServiceIds.pni, + }); - // Migrate JSON fields - db.prepare( - ` - UPDATE conversations - SET json = json_set(json, '$.pni', $pni) - WHERE serviceId IS $aci - ` - ).run({ - aci: ourServiceIds.aci, - pni: ourServiceIds.pni, - }); - - migratePreKeys(db, 'preKeys', ourServiceIds, logger); - migratePreKeys(db, 'signedPreKeys', ourServiceIds, logger); - migratePreKeys(db, 'kyberPreKeys', ourServiceIds, logger); - - db.pragma('user_version = 960'); - })(); - - logger.info('updateToSchemaVersion960: success!'); + migratePreKeys(db, 'preKeys', ourServiceIds, logger); + migratePreKeys(db, 'signedPreKeys', ourServiceIds, logger); + migratePreKeys(db, 'kyberPreKeys', ourServiceIds, logger); } // @@ -101,12 +88,9 @@ function migratePni( [aci] = JSON.parse(uuidIdJson ?? '').value.split('.', 2); } catch (error) { if (uuidIdJson) { - logger.warn( - 'updateToSchemaVersion960: failed to parse uuid_id item', - error - ); + logger.warn('failed to parse uuid_id item', error); } else { - logger.info('updateToSchemaVersion960: Our ACI not found'); + logger.info('Our ACI not found'); } } if (!aci) { @@ -118,9 +102,9 @@ function migratePni( legacyPni = JSON.parse(pniJson ?? '').value; } catch (error) { if (pniJson) { - logger.warn('updateToSchemaVersion960: failed to parse pni item', error); + logger.warn('failed to parse pni item', error); } else { - logger.info('updateToSchemaVersion960: Our PNI not found'); + logger.info('Our PNI not found'); } } if (!legacyPni) { @@ -164,10 +148,7 @@ function migratePni( updateStmt.run({ id, json: JSON.stringify(data) }); } catch (error) { - logger.warn( - `updateToSchemaVersion960: failed to parse ${id} item`, - error - ); + logger.warn(`failed to parse ${id} item`, error); } } return { @@ -200,11 +181,11 @@ function migratePreKeys( WHERE id = $id `); - logger.info(`updateToSchemaVersion960: updating ${preKeys.length} ${table}`); + logger.info(`updating ${preKeys.length} ${table}`); for (const { id, json } of preKeys) { const match = id.match(/^(.*):(.*)$/); if (!match) { - logger.warn(`updateToSchemaVersion960: invalid ${table} id ${id}`); + logger.warn(`invalid ${table} id ${id}`); continue; } @@ -212,20 +193,13 @@ function migratePreKeys( try { legacyData = JSON.parse(json); } catch (error) { - logger.warn( - `updateToSchemaVersion960: failed to parse ${table} ${id}`, - error - ); + logger.warn(`failed to parse ${table} ${id}`, error); continue; } const [, ourServiceId, keyId] = match; if (ourServiceId !== legacyPni) { - logger.warn( - 'updateToSchemaVersion960: unexpected ourServiceId', - ourServiceId, - legacyPni - ); + logger.warn('unexpected ourServiceId', ourServiceId, legacyPni); continue; } diff --git a/ts/sql/migrations/970-fts5-optimize.ts b/ts/sql/migrations/970-fts5-optimize.ts index d6d39794e9f..1a71f8e9e16 100644 --- a/ts/sql/migrations/970-fts5-optimize.ts +++ b/ts/sql/migrations/970-fts5-optimize.ts @@ -2,29 +2,9 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; -export const version = 970; - -export function updateToSchemaVersion970( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 970) { - return; - } - - const start = Date.now(); - db.transaction(() => { - db.exec(` - INSERT INTO messages_fts(messages_fts) VALUES ('optimize'); - `); - db.pragma('user_version = 970'); - })(); - - const duration = Date.now() - start; - logger.info( - `updateToSchemaVersion970: success! fts optimize took ${duration}ms` - ); +export default function updateToSchemaVersion970(db: Database): void { + db.exec(` + INSERT INTO messages_fts(messages_fts) VALUES ('optimize'); + `); } diff --git a/ts/sql/migrations/980-reaction-timestamp.ts b/ts/sql/migrations/980-reaction-timestamp.ts index 0c4455324d2..79b06e0aed2 100644 --- a/ts/sql/migrations/980-reaction-timestamp.ts +++ b/ts/sql/migrations/980-reaction-timestamp.ts @@ -3,30 +3,12 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; +export default function updateToSchemaVersion980(db: Database): void { + db.exec(` + ALTER TABLE reactions ADD COLUMN timestamp NUMBER; -export const version = 980; - -export function updateToSchemaVersion980( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 980) { - return; - } - - db.transaction(() => { - db.exec(` - ALTER TABLE reactions ADD COLUMN timestamp NUMBER; - - CREATE INDEX reactions_byTimestamp - ON reactions - (fromId, timestamp); - `); - - db.pragma('user_version = 980'); - })(); - - logger.info('updateToSchemaVersion980: success!'); + CREATE INDEX reactions_byTimestamp + ON reactions + (fromId, timestamp); + `); } diff --git a/ts/sql/migrations/990-phone-number-sharing.ts b/ts/sql/migrations/990-phone-number-sharing.ts index 3ec05b4384d..82a7c0eb647 100644 --- a/ts/sql/migrations/990-phone-number-sharing.ts +++ b/ts/sql/migrations/990-phone-number-sharing.ts @@ -3,43 +3,25 @@ import type { Database } from '@signalapp/sqlcipher'; -import type { LoggerType } from '../../types/Logging'; - -export const version = 990; - -export function updateToSchemaVersion990( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 990) { - return; - } - - db.transaction(() => { - db.exec(` - UPDATE conversations - SET json = json_remove( - json_insert( - json, - '$.sharingPhoneNumber', - iif( - json ->> '$.notSharingPhoneNumber', - -- We flip the value from false to true, and vice versa - json('false'), - json('true') - ) - ), - '$.notSharingPhoneNumber' - ) - -- Default value of '$.notSharingPhoneNumber' is true and - -- the default value of '$.sharingPhoneNumber' is false so we don't have - -- to do anything if the field wasn't present. - WHERE json ->> '$.notSharingPhoneNumber' IS NOT NULL; - `); - - db.pragma('user_version = 990'); - })(); - - logger.info('updateToSchemaVersion990: success!'); +export default function updateToSchemaVersion990(db: Database): void { + db.exec(` + UPDATE conversations + SET json = json_remove( + json_insert( + json, + '$.sharingPhoneNumber', + iif( + json ->> '$.notSharingPhoneNumber', + -- We flip the value from false to true, and vice versa + json('false'), + json('true') + ) + ), + '$.notSharingPhoneNumber' + ) + -- Default value of '$.notSharingPhoneNumber' is true and + -- the default value of '$.sharingPhoneNumber' is false so we don't have + -- to do anything if the field wasn't present. + WHERE json ->> '$.notSharingPhoneNumber' IS NOT NULL; + `); } diff --git a/ts/sql/migrations/index.ts b/ts/sql/migrations/index.ts index be39881ff88..43c25b48adb 100644 --- a/ts/sql/migrations/index.ts +++ b/ts/sql/migrations/index.ts @@ -67,2056 +67,1530 @@ import updateToSchemaVersion88 from './88-service-ids'; import updateToSchemaVersion89 from './89-call-history'; import updateToSchemaVersion90 from './90-delete-story-reply-screenshot'; import updateToSchemaVersion91 from './91-clean-keys'; -import { updateToSchemaVersion920 } from './920-clean-more-keys'; -import { updateToSchemaVersion930 } from './930-fts5-secure-delete'; -import { updateToSchemaVersion940 } from './940-fts5-revert'; -import { updateToSchemaVersion950 } from './950-fts5-secure-delete'; -import { updateToSchemaVersion960 } from './960-untag-pni'; -import { updateToSchemaVersion970 } from './970-fts5-optimize'; -import { updateToSchemaVersion980 } from './980-reaction-timestamp'; -import { updateToSchemaVersion990 } from './990-phone-number-sharing'; -import { updateToSchemaVersion1000 } from './1000-mark-unread-call-history-messages-as-unseen'; -import { updateToSchemaVersion1010 } from './1010-call-links-table'; -import { updateToSchemaVersion1020 } from './1020-self-merges'; -import { updateToSchemaVersion1030 } from './1030-unblock-event'; -import { updateToSchemaVersion1040 } from './1040-undownloaded-backed-up-media'; -import { updateToSchemaVersion1050 } from './1050-group-send-endorsements'; -import { updateToSchemaVersion1060 } from './1060-addressable-messages-and-sync-tasks'; -import { updateToSchemaVersion1070 } from './1070-attachment-backup'; -import { updateToSchemaVersion1080 } from './1080-nondisappearing-addressable'; -import { updateToSchemaVersion1090 } from './1090-message-delete-indexes'; -import { updateToSchemaVersion1100 } from './1100-optimize-mark-call-history-read-in-conversation'; -import { updateToSchemaVersion1110 } from './1110-sticker-local-key'; -import { updateToSchemaVersion1120 } from './1120-messages-foreign-keys-indexes'; -import { updateToSchemaVersion1130 } from './1130-isStory-index'; -import { updateToSchemaVersion1140 } from './1140-call-links-deleted-column'; -import { updateToSchemaVersion1150 } from './1150-expire-timer-version'; -import { updateToSchemaVersion1160 } from './1160-optimize-calls-unread-count'; -import { updateToSchemaVersion1170 } from './1170-update-call-history-unread-index'; -import { updateToSchemaVersion1180 } from './1180-add-attachment-download-source'; -import { updateToSchemaVersion1190 } from './1190-call-links-storage'; -import { updateToSchemaVersion1200 } from './1200-attachment-download-source-index'; -import { updateToSchemaVersion1210 } from './1210-call-history-started-id'; -import { updateToSchemaVersion1220 } from './1220-blob-sessions'; -import { updateToSchemaVersion1230 } from './1230-call-links-admin-key-index'; -import { updateToSchemaVersion1240 } from './1240-defunct-call-links-table'; -import { updateToSchemaVersion1250 } from './1250-defunct-call-links-storage'; -import { updateToSchemaVersion1260 } from './1260-sync-tasks-rowid'; -import { updateToSchemaVersion1270 } from './1270-normalize-messages'; -import { updateToSchemaVersion1280 } from './1280-blob-unprocessed'; -import { updateToSchemaVersion1290 } from './1290-int-unprocessed-source-device'; -import { updateToSchemaVersion1300 } from './1300-sticker-pack-refs'; -import { updateToSchemaVersion1310 } from './1310-muted-fixup'; -import { updateToSchemaVersion1320 } from './1320-unprocessed-received-at-date'; -import { updateToSchemaVersion1330 } from './1330-sync-tasks-type-index'; -import { updateToSchemaVersion1340 } from './1340-recent-gifs'; -import { updateToSchemaVersion1350 } from './1350-notification-profiles'; -import { updateToSchemaVersion1360 } from './1360-attachments'; -import { updateToSchemaVersion1370 } from './1370-message-attachment-indexes'; -import { updateToSchemaVersion1380 } from './1380-donation-receipts'; -import { updateToSchemaVersion1390 } from './1390-attachment-download-keys'; -import { updateToSchemaVersion1400 } from './1400-simplify-receipts'; -import { updateToSchemaVersion1410 } from './1410-remove-wallpaper'; -import { - updateToSchemaVersion1420, - version as MAX_VERSION, -} from './1420-backup-downloads'; +import updateToSchemaVersion920 from './920-clean-more-keys'; +import updateToSchemaVersion930 from './930-fts5-secure-delete'; +import updateToSchemaVersion940 from './940-fts5-revert'; +import updateToSchemaVersion950 from './950-fts5-secure-delete'; +import updateToSchemaVersion960 from './960-untag-pni'; +import updateToSchemaVersion970 from './970-fts5-optimize'; +import updateToSchemaVersion980 from './980-reaction-timestamp'; +import updateToSchemaVersion990 from './990-phone-number-sharing'; +import updateToSchemaVersion1000 from './1000-mark-unread-call-history-messages-as-unseen'; +import updateToSchemaVersion1010 from './1010-call-links-table'; +import updateToSchemaVersion1020 from './1020-self-merges'; +import updateToSchemaVersion1030 from './1030-unblock-event'; +import updateToSchemaVersion1040 from './1040-undownloaded-backed-up-media'; +import updateToSchemaVersion1050 from './1050-group-send-endorsements'; +import updateToSchemaVersion1060 from './1060-addressable-messages-and-sync-tasks'; +import updateToSchemaVersion1070 from './1070-attachment-backup'; +import updateToSchemaVersion1080 from './1080-nondisappearing-addressable'; +import updateToSchemaVersion1090 from './1090-message-delete-indexes'; +import updateToSchemaVersion1100 from './1100-optimize-mark-call-history-read-in-conversation'; +import updateToSchemaVersion1110 from './1110-sticker-local-key'; +import updateToSchemaVersion1120 from './1120-messages-foreign-keys-indexes'; +import updateToSchemaVersion1130 from './1130-isStory-index'; +import updateToSchemaVersion1140 from './1140-call-links-deleted-column'; +import updateToSchemaVersion1150 from './1150-expire-timer-version'; +import updateToSchemaVersion1160 from './1160-optimize-calls-unread-count'; +import updateToSchemaVersion1170 from './1170-update-call-history-unread-index'; +import updateToSchemaVersion1180 from './1180-add-attachment-download-source'; +import updateToSchemaVersion1190 from './1190-call-links-storage'; +import updateToSchemaVersion1200 from './1200-attachment-download-source-index'; +import updateToSchemaVersion1210 from './1210-call-history-started-id'; +import updateToSchemaVersion1220 from './1220-blob-sessions'; +import updateToSchemaVersion1230 from './1230-call-links-admin-key-index'; +import updateToSchemaVersion1240 from './1240-defunct-call-links-table'; +import updateToSchemaVersion1250 from './1250-defunct-call-links-storage'; +import updateToSchemaVersion1260 from './1260-sync-tasks-rowid'; +import updateToSchemaVersion1270 from './1270-normalize-messages'; +import updateToSchemaVersion1280 from './1280-blob-unprocessed'; +import updateToSchemaVersion1290 from './1290-int-unprocessed-source-device'; +import updateToSchemaVersion1300 from './1300-sticker-pack-refs'; +import updateToSchemaVersion1310 from './1310-muted-fixup'; +import updateToSchemaVersion1320 from './1320-unprocessed-received-at-date'; +import updateToSchemaVersion1330 from './1330-sync-tasks-type-index'; +import updateToSchemaVersion1340 from './1340-recent-gifs'; +import updateToSchemaVersion1350 from './1350-notification-profiles'; +import updateToSchemaVersion1360 from './1360-attachments'; +import updateToSchemaVersion1370 from './1370-message-attachment-indexes'; +import updateToSchemaVersion1380 from './1380-donation-receipts'; +import updateToSchemaVersion1390 from './1390-attachment-download-keys'; +import updateToSchemaVersion1400 from './1400-simplify-receipts'; +import updateToSchemaVersion1410 from './1410-remove-wallpaper'; +import updateToSchemaVersion1420 from './1420-backup-downloads'; import { DataWriter } from '../Server'; -function updateToSchemaVersion1( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 1) { - return; - } +function updateToSchemaVersion1(db: Database): void { + db.exec(` + CREATE TABLE messages( + id STRING PRIMARY KEY ASC, + json TEXT, - logger.info('updateToSchemaVersion1: starting...'); - - db.transaction(() => { - db.exec(` - CREATE TABLE messages( - id STRING PRIMARY KEY ASC, - json TEXT, - - unread INTEGER, - expires_at INTEGER, - sent_at INTEGER, - schemaVersion INTEGER, - conversationId STRING, - received_at INTEGER, - source STRING, - sourceDevice STRING, - hasAttachments INTEGER, - hasFileAttachments INTEGER, - hasVisualMediaAttachments INTEGER - ); - CREATE INDEX messages_unread ON messages ( - unread - ); - CREATE INDEX messages_expires_at ON messages ( - expires_at - ); - CREATE INDEX messages_receipt ON messages ( - sent_at - ); - CREATE INDEX messages_schemaVersion ON messages ( - schemaVersion - ); - CREATE INDEX messages_conversation ON messages ( - conversationId, - received_at - ); - CREATE INDEX messages_duplicate_check ON messages ( - source, - sourceDevice, - sent_at - ); - CREATE INDEX messages_hasAttachments ON messages ( - conversationId, - hasAttachments, - received_at - ); - CREATE INDEX messages_hasFileAttachments ON messages ( - conversationId, - hasFileAttachments, - received_at - ); - CREATE INDEX messages_hasVisualMediaAttachments ON messages ( - conversationId, - hasVisualMediaAttachments, - received_at - ); - CREATE TABLE unprocessed( - id STRING, - timestamp INTEGER, - json TEXT - ); - CREATE INDEX unprocessed_id ON unprocessed ( - id - ); - CREATE INDEX unprocessed_timestamp ON unprocessed ( - timestamp - ); - `); - - db.pragma('user_version = 1'); - })(); - - logger.info('updateToSchemaVersion1: success!'); + unread INTEGER, + expires_at INTEGER, + sent_at INTEGER, + schemaVersion INTEGER, + conversationId STRING, + received_at INTEGER, + source STRING, + sourceDevice STRING, + hasAttachments INTEGER, + hasFileAttachments INTEGER, + hasVisualMediaAttachments INTEGER + ); + CREATE INDEX messages_unread ON messages ( + unread + ); + CREATE INDEX messages_expires_at ON messages ( + expires_at + ); + CREATE INDEX messages_receipt ON messages ( + sent_at + ); + CREATE INDEX messages_schemaVersion ON messages ( + schemaVersion + ); + CREATE INDEX messages_conversation ON messages ( + conversationId, + received_at + ); + CREATE INDEX messages_duplicate_check ON messages ( + source, + sourceDevice, + sent_at + ); + CREATE INDEX messages_hasAttachments ON messages ( + conversationId, + hasAttachments, + received_at + ); + CREATE INDEX messages_hasFileAttachments ON messages ( + conversationId, + hasFileAttachments, + received_at + ); + CREATE INDEX messages_hasVisualMediaAttachments ON messages ( + conversationId, + hasVisualMediaAttachments, + received_at + ); + CREATE TABLE unprocessed( + id STRING, + timestamp INTEGER, + json TEXT + ); + CREATE INDEX unprocessed_id ON unprocessed ( + id + ); + CREATE INDEX unprocessed_timestamp ON unprocessed ( + timestamp + ); + `); } -function updateToSchemaVersion2( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 2) { - return; - } +function updateToSchemaVersion2(db: Database): void { + db.exec(` + ALTER TABLE messages + ADD COLUMN expireTimer INTEGER; - logger.info('updateToSchemaVersion2: starting...'); + ALTER TABLE messages + ADD COLUMN expirationStartTimestamp INTEGER; - db.transaction(() => { - db.exec(` - ALTER TABLE messages - ADD COLUMN expireTimer INTEGER; + ALTER TABLE messages + ADD COLUMN type STRING; - ALTER TABLE messages - ADD COLUMN expirationStartTimestamp INTEGER; - - ALTER TABLE messages - ADD COLUMN type STRING; - - CREATE INDEX messages_expiring ON messages ( - expireTimer, - expirationStartTimestamp, - expires_at - ); - - UPDATE messages SET - expirationStartTimestamp = json_extract(json, '$.expirationStartTimestamp'), - expireTimer = json_extract(json, '$.expireTimer'), - type = json_extract(json, '$.type'); - `); - db.pragma('user_version = 2'); - })(); - logger.info('updateToSchemaVersion2: success!'); -} - -function updateToSchemaVersion3( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 3) { - return; - } - - logger.info('updateToSchemaVersion3: starting...'); - - db.transaction(() => { - db.exec(` - DROP INDEX messages_expiring; - DROP INDEX messages_unread; - - CREATE INDEX messages_without_timer ON messages ( - expireTimer, - expires_at, - type - ) WHERE expires_at IS NULL AND expireTimer IS NOT NULL; - - CREATE INDEX messages_unread ON messages ( - conversationId, - unread - ) WHERE unread IS NOT NULL; - - ANALYZE; - `); - - db.pragma('user_version = 3'); - })(); - - logger.info('updateToSchemaVersion3: success!'); -} - -function updateToSchemaVersion4( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 4) { - return; - } - - logger.info('updateToSchemaVersion4: starting...'); - - db.transaction(() => { - db.exec(` - CREATE TABLE conversations( - id STRING PRIMARY KEY ASC, - json TEXT, - - active_at INTEGER, - type STRING, - members TEXT, - name TEXT, - profileName TEXT - ); - CREATE INDEX conversations_active ON conversations ( - active_at - ) WHERE active_at IS NOT NULL; - - CREATE INDEX conversations_type ON conversations ( - type - ) WHERE type IS NOT NULL; - `); - - db.pragma('user_version = 4'); - })(); - - logger.info('updateToSchemaVersion4: success!'); -} - -function updateToSchemaVersion6( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 6) { - return; - } - logger.info('updateToSchemaVersion6: starting...'); - - db.transaction(() => { - db.exec(` - -- key-value, ids are strings, one extra column - CREATE TABLE sessions( - id STRING PRIMARY KEY ASC, - number STRING, - json TEXT - ); - CREATE INDEX sessions_number ON sessions ( - number - ) WHERE number IS NOT NULL; - -- key-value, ids are strings - CREATE TABLE groups( - id STRING PRIMARY KEY ASC, - json TEXT - ); - CREATE TABLE identityKeys( - id STRING PRIMARY KEY ASC, - json TEXT - ); - CREATE TABLE items( - id STRING PRIMARY KEY ASC, - json TEXT - ); - -- key-value, ids are integers - CREATE TABLE preKeys( - id INTEGER PRIMARY KEY ASC, - json TEXT - ); - CREATE TABLE signedPreKeys( - id INTEGER PRIMARY KEY ASC, - json TEXT - ); - `); - - db.pragma('user_version = 6'); - })(); - - logger.info('updateToSchemaVersion6: success!'); -} - -function updateToSchemaVersion7( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 7) { - return; - } - logger.info('updateToSchemaVersion7: starting...'); - - db.transaction(() => { - db.exec(` - -- SQLite has been coercing our STRINGs into numbers, so we force it with TEXT - -- We create a new table then copy the data into it, since we can't modify columns - DROP INDEX sessions_number; - ALTER TABLE sessions RENAME TO sessions_old; - - CREATE TABLE sessions( - id TEXT PRIMARY KEY, - number TEXT, - json TEXT - ); - CREATE INDEX sessions_number ON sessions ( - number - ) WHERE number IS NOT NULL; - INSERT INTO sessions(id, number, json) - SELECT '+' || id, number, json FROM sessions_old; - DROP TABLE sessions_old; - `); - - db.pragma('user_version = 7'); - })(); - logger.info('updateToSchemaVersion7: success!'); -} - -function updateToSchemaVersion8( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 8) { - return; - } - logger.info('updateToSchemaVersion8: starting...'); - db.transaction(() => { - db.exec(` - -- First, we pull a new body field out of the message table's json blob - ALTER TABLE messages - ADD COLUMN body TEXT; - UPDATE messages SET body = json_extract(json, '$.body'); - - -- Then we create our full-text search table and populate it - CREATE VIRTUAL TABLE messages_fts - USING fts5(id UNINDEXED, body); - - INSERT INTO messages_fts(id, body) - SELECT id, body FROM messages; - - -- Then we set up triggers to keep the full-text search table up to date - CREATE TRIGGER messages_on_insert AFTER INSERT ON messages BEGIN - INSERT INTO messages_fts ( - id, - body - ) VALUES ( - new.id, - new.body - ); - END; - CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN - DELETE FROM messages_fts WHERE id = old.id; - END; - CREATE TRIGGER messages_on_update AFTER UPDATE ON messages BEGIN - DELETE FROM messages_fts WHERE id = old.id; - INSERT INTO messages_fts( - id, - body - ) VALUES ( - new.id, - new.body - ); - END; - `); - - // For formatting search results: - // https://sqlite.org/fts5.html#the_highlight_function - // https://sqlite.org/fts5.html#the_snippet_function - - db.pragma('user_version = 8'); - })(); - logger.info('updateToSchemaVersion8: success!'); -} - -function updateToSchemaVersion9( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 9) { - return; - } - logger.info('updateToSchemaVersion9: starting...'); - - db.transaction(() => { - db.exec(` - CREATE TABLE attachment_downloads( - id STRING primary key, - timestamp INTEGER, - pending INTEGER, - json TEXT - ); - - CREATE INDEX attachment_downloads_timestamp - ON attachment_downloads ( - timestamp - ) WHERE pending = 0; - CREATE INDEX attachment_downloads_pending - ON attachment_downloads ( - pending - ) WHERE pending != 0; - `); - - db.pragma('user_version = 9'); - })(); - - logger.info('updateToSchemaVersion9: success!'); -} - -function updateToSchemaVersion10( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 10) { - return; - } - logger.info('updateToSchemaVersion10: starting...'); - db.transaction(() => { - db.exec(` - DROP INDEX unprocessed_id; - DROP INDEX unprocessed_timestamp; - ALTER TABLE unprocessed RENAME TO unprocessed_old; - - CREATE TABLE unprocessed( - id STRING, - timestamp INTEGER, - version INTEGER, - attempts INTEGER, - envelope TEXT, - decrypted TEXT, - source TEXT, - sourceDevice TEXT, - serverTimestamp INTEGER - ); - - CREATE INDEX unprocessed_id ON unprocessed ( - id - ); - CREATE INDEX unprocessed_timestamp ON unprocessed ( - timestamp - ); - - INSERT INTO unprocessed ( - id, - timestamp, - version, - attempts, - envelope, - decrypted, - source, - sourceDevice, - serverTimestamp - ) SELECT - id, - timestamp, - json_extract(json, '$.version'), - json_extract(json, '$.attempts'), - json_extract(json, '$.envelope'), - json_extract(json, '$.decrypted'), - json_extract(json, '$.source'), - json_extract(json, '$.sourceDevice'), - json_extract(json, '$.serverTimestamp') - FROM unprocessed_old; - - DROP TABLE unprocessed_old; - `); - - db.pragma('user_version = 10'); - })(); - logger.info('updateToSchemaVersion10: success!'); -} - -function updateToSchemaVersion11( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 11) { - return; - } - logger.info('updateToSchemaVersion11: starting...'); - - db.transaction(() => { - db.exec(` - DROP TABLE groups; - `); - - db.pragma('user_version = 11'); - })(); - logger.info('updateToSchemaVersion11: success!'); -} - -function updateToSchemaVersion12( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 12) { - return; - } - - logger.info('updateToSchemaVersion12: starting...'); - db.transaction(() => { - db.exec(` - CREATE TABLE sticker_packs( - id TEXT PRIMARY KEY, - key TEXT NOT NULL, - - author STRING, - coverStickerId INTEGER, - createdAt INTEGER, - downloadAttempts INTEGER, - installedAt INTEGER, - lastUsed INTEGER, - status STRING, - stickerCount INTEGER, - title STRING - ); - - CREATE TABLE stickers( - id INTEGER NOT NULL, - packId TEXT NOT NULL, - - emoji STRING, - height INTEGER, - isCoverOnly INTEGER, - lastUsed INTEGER, - path STRING, - width INTEGER, - - PRIMARY KEY (id, packId), - CONSTRAINT stickers_fk - FOREIGN KEY (packId) - REFERENCES sticker_packs(id) - ON DELETE CASCADE - ); - - CREATE INDEX stickers_recents - ON stickers ( - lastUsed - ) WHERE lastUsed IS NOT NULL; - - CREATE TABLE sticker_references( - messageId STRING, - packId TEXT, - CONSTRAINT sticker_references_fk - FOREIGN KEY(packId) - REFERENCES sticker_packs(id) - ON DELETE CASCADE - ); - `); - - db.pragma('user_version = 12'); - })(); - logger.info('updateToSchemaVersion12: success!'); -} - -function updateToSchemaVersion13( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 13) { - return; - } - - logger.info('updateToSchemaVersion13: starting...'); - db.transaction(() => { - db.exec(` - ALTER TABLE sticker_packs ADD COLUMN attemptedStatus STRING; - `); - - db.pragma('user_version = 13'); - })(); - logger.info('updateToSchemaVersion13: success!'); -} - -function updateToSchemaVersion14( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 14) { - return; - } - - logger.info('updateToSchemaVersion14: starting...'); - db.transaction(() => { - db.exec(` - CREATE TABLE emojis( - shortName STRING PRIMARY KEY, - lastUsage INTEGER - ); - - CREATE INDEX emojis_lastUsage - ON emojis ( - lastUsage - ); - `); - - db.pragma('user_version = 14'); - })(); - - logger.info('updateToSchemaVersion14: success!'); -} - -function updateToSchemaVersion15( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 15) { - return; - } - - logger.info('updateToSchemaVersion15: starting...'); - db.transaction(() => { - db.exec(` - -- SQLite has again coerced our STRINGs into numbers, so we force it with TEXT - -- We create a new table then copy the data into it, since we can't modify columns - - DROP INDEX emojis_lastUsage; - ALTER TABLE emojis RENAME TO emojis_old; - - CREATE TABLE emojis( - shortName TEXT PRIMARY KEY, - lastUsage INTEGER - ); - CREATE INDEX emojis_lastUsage - ON emojis ( - lastUsage - ); - - DELETE FROM emojis WHERE shortName = 1; - INSERT INTO emojis(shortName, lastUsage) - SELECT shortName, lastUsage FROM emojis_old; - - DROP TABLE emojis_old; - `); - - db.pragma('user_version = 15'); - })(); - logger.info('updateToSchemaVersion15: success!'); -} - -function updateToSchemaVersion16( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 16) { - return; - } - - logger.info('updateToSchemaVersion16: starting...'); - db.transaction(() => { - db.exec(` - ALTER TABLE messages - ADD COLUMN messageTimer INTEGER; - ALTER TABLE messages - ADD COLUMN messageTimerStart INTEGER; - ALTER TABLE messages - ADD COLUMN messageTimerExpiresAt INTEGER; - ALTER TABLE messages - ADD COLUMN isErased INTEGER; - - CREATE INDEX messages_message_timer ON messages ( - messageTimer, - messageTimerStart, - messageTimerExpiresAt, - isErased - ) WHERE messageTimer IS NOT NULL; - - -- Updating full-text triggers to avoid anything with a messageTimer set - - DROP TRIGGER messages_on_insert; - DROP TRIGGER messages_on_delete; - DROP TRIGGER messages_on_update; - - CREATE TRIGGER messages_on_insert AFTER INSERT ON messages - WHEN new.messageTimer IS NULL - BEGIN - INSERT INTO messages_fts ( - id, - body - ) VALUES ( - new.id, - new.body - ); - END; - CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN - DELETE FROM messages_fts WHERE id = old.id; - END; - CREATE TRIGGER messages_on_update AFTER UPDATE ON messages - WHEN new.messageTimer IS NULL - BEGIN - DELETE FROM messages_fts WHERE id = old.id; - INSERT INTO messages_fts( - id, - body - ) VALUES ( - new.id, - new.body - ); - END; - `); - - db.pragma('user_version = 16'); - })(); - logger.info('updateToSchemaVersion16: success!'); -} - -function updateToSchemaVersion17( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 17) { - return; - } - - logger.info('updateToSchemaVersion17: starting...'); - db.transaction(() => { - try { - db.exec(` - ALTER TABLE messages - ADD COLUMN isViewOnce INTEGER; - - DROP INDEX messages_message_timer; - `); - } catch (error) { - logger.info( - 'updateToSchemaVersion17: Message table already had isViewOnce column' - ); - } - - db.exec('DROP INDEX IF EXISTS messages_view_once;'); - - db.exec(` - CREATE INDEX messages_view_once ON messages ( - isErased - ) WHERE isViewOnce = 1; - - -- Updating full-text triggers to avoid anything with isViewOnce = 1 - - DROP TRIGGER messages_on_insert; - DROP TRIGGER messages_on_update; - - CREATE TRIGGER messages_on_insert AFTER INSERT ON messages - WHEN new.isViewOnce != 1 - BEGIN - INSERT INTO messages_fts ( - id, - body - ) VALUES ( - new.id, - new.body - ); - END; - CREATE TRIGGER messages_on_update AFTER UPDATE ON messages - WHEN new.isViewOnce != 1 - BEGIN - DELETE FROM messages_fts WHERE id = old.id; - INSERT INTO messages_fts( - id, - body - ) VALUES ( - new.id, - new.body - ); - END; - `); - - db.pragma('user_version = 17'); - })(); - logger.info('updateToSchemaVersion17: success!'); -} - -function updateToSchemaVersion18( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 18) { - return; - } - - logger.info('updateToSchemaVersion18: starting...'); - db.transaction(() => { - db.exec(` - -- Delete and rebuild full-text search index to capture everything - - DELETE FROM messages_fts; - INSERT INTO messages_fts(messages_fts) VALUES('rebuild'); - - INSERT INTO messages_fts(id, body) - SELECT id, body FROM messages WHERE isViewOnce IS NULL OR isViewOnce != 1; - - -- Fixing full-text triggers - - DROP TRIGGER messages_on_insert; - DROP TRIGGER messages_on_update; - - CREATE TRIGGER messages_on_insert AFTER INSERT ON messages - WHEN new.isViewOnce IS NULL OR new.isViewOnce != 1 - BEGIN - INSERT INTO messages_fts ( - id, - body - ) VALUES ( - new.id, - new.body - ); - END; - CREATE TRIGGER messages_on_update AFTER UPDATE ON messages - WHEN new.isViewOnce IS NULL OR new.isViewOnce != 1 - BEGIN - DELETE FROM messages_fts WHERE id = old.id; - INSERT INTO messages_fts( - id, - body - ) VALUES ( - new.id, - new.body - ); - END; - `); - - db.pragma('user_version = 18'); - })(); - logger.info('updateToSchemaVersion18: success!'); -} - -function updateToSchemaVersion19( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 19) { - return; - } - - logger.info('updateToSchemaVersion19: starting...'); - db.transaction(() => { - db.exec(` - ALTER TABLE conversations - ADD COLUMN profileFamilyName TEXT; - ALTER TABLE conversations - ADD COLUMN profileFullName TEXT; - - -- Preload new field with the profileName we already have - UPDATE conversations SET profileFullName = profileName; - `); - - db.pragma('user_version = 19'); - })(); - - logger.info('updateToSchemaVersion19: success!'); -} - -function updateToSchemaVersion20( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 20) { - return; - } - - logger.info('updateToSchemaVersion20: starting...'); - db.transaction(() => { - // The triggers on the messages table slow down this migration - // significantly, so we drop them and recreate them later. - // Drop triggers - const triggers = db - .prepare( - "SELECT * FROM sqlite_master WHERE type = 'trigger' AND tbl_name = 'messages'" - ) - .all<{ name: string; sql: string }>(); - - for (const trigger of triggers) { - db.exec(`DROP TRIGGER ${trigger.name}`); - } - - // Create new columns and indices - db.exec(` - ALTER TABLE conversations ADD COLUMN e164 TEXT; - ALTER TABLE conversations ADD COLUMN uuid TEXT; - ALTER TABLE conversations ADD COLUMN groupId TEXT; - ALTER TABLE messages ADD COLUMN sourceUuid TEXT; - ALTER TABLE sessions RENAME COLUMN number TO conversationId; - CREATE INDEX conversations_e164 ON conversations(e164); - CREATE INDEX conversations_uuid ON conversations(uuid); - CREATE INDEX conversations_groupId ON conversations(groupId); - CREATE INDEX messages_sourceUuid on messages(sourceUuid); - - -- Migrate existing IDs - UPDATE conversations SET e164 = '+' || id WHERE type = 'private'; - UPDATE conversations SET groupId = id WHERE type = 'group'; - `); - - // Drop invalid groups and any associated messages - const maybeInvalidGroups = db - .prepare( - "SELECT * FROM conversations WHERE type = 'group' AND members IS NULL;" - ) - .all<{ id: string; json: string }>(); - for (const group of maybeInvalidGroups) { - const json: { id: string; members: Array } = JSON.parse( - group.json - ); - if (!json.members || !json.members.length) { - db.prepare('DELETE FROM conversations WHERE id = $id;').run({ - id: json.id, - }); - db.prepare('DELETE FROM messages WHERE conversationId = $id;').run({ - id: json.id, - }); - } - } - - // Generate new IDs and alter data - const allConversations = db.prepare('SELECT * FROM conversations;').all<{ - id: string; - type: string; - }>(); - const allConversationsByOldId = keyBy(allConversations, 'id'); - - for (const row of allConversations) { - const oldId = row.id; - const newId = generateUuid(); - allConversationsByOldId[oldId].id = newId; - const patchObj: { id: string; e164?: string; groupId?: string } = { - id: newId, - }; - if (row.type === 'private') { - patchObj.e164 = `+${oldId}`; - } else if (row.type === 'group') { - patchObj.groupId = oldId; - } - const patch = JSON.stringify(patchObj); - - db.prepare( - ` - UPDATE conversations - SET id = $newId, json = JSON_PATCH(json, $patch) - WHERE id = $oldId - ` - ).run({ - newId, - oldId, - patch, - }); - const messagePatch = JSON.stringify({ conversationId: newId }); - db.prepare( - ` - UPDATE messages - SET conversationId = $newId, json = JSON_PATCH(json, $patch) - WHERE conversationId = $oldId - ` - ).run({ newId, oldId, patch: messagePatch }); - } - - const groupConversations: Array<{ - id: string; - members: string; - json: string; - }> = db - .prepare( - ` - SELECT id, members, json FROM conversations WHERE type = 'group'; - ` - ) - .all(); - - // Update group conversations, point members at new conversation ids - groupConversations.forEach(groupRow => { - const members = groupRow.members.split(/\s?\+/).filter(Boolean); - const newMembers = []; - for (const m of members) { - const memberRow = allConversationsByOldId[m]; - - if (memberRow) { - newMembers.push(memberRow.id); - } else { - // We didn't previously have a private conversation for this member, - // we need to create one - const id = generateUuid(); - const updatedConversation = { - id, - e164: m, - type: 'private', - version: 2, - unreadCount: 0, - verified: 0, - - // Not directly used by saveConversation, but are necessary - // for conversation model - inbox_position: 0, - isPinned: false, - lastMessageDeletedForEveryone: false, - markedUnread: false, - messageCount: 0, - sentMessageCount: 0, - profileSharing: false, - }; - - db.prepare( - ` - UPDATE conversations - SET - json = $json, - e164 = $e164, - type = $type - WHERE - id = $id; - ` - ).run({ - id: updatedConversation.id, - json: objectToJSON(updatedConversation), - e164: updatedConversation.e164, - type: updatedConversation.type, - }); - - newMembers.push(id); - } - } - const json = { - ...jsonToObject>(groupRow.json), - members: newMembers, - }; - const newMembersValue = newMembers.join(' '); - db.prepare( - ` - UPDATE conversations - SET members = $newMembersValue, json = $newJsonValue - WHERE id = $id - ` - ).run({ - id: groupRow.id, - newMembersValue, - newJsonValue: objectToJSON(json), - }); - }); - - // Update sessions to stable IDs - const allSessions = db.prepare('SELECT * FROM sessions;').all<{ - id: string; - json: string; - }>(); - for (const session of allSessions) { - // Not using patch here so we can explicitly delete a property rather than - // implicitly delete via null - const newJson = JSON.parse(session.json); - const conversation = allConversationsByOldId[newJson.number.substr(1)]; - if (conversation) { - newJson.conversationId = conversation.id; - newJson.id = `${newJson.conversationId}.${newJson.deviceId}`; - } - delete newJson.number; - db.prepare( - ` - UPDATE sessions - SET id = $newId, json = $newJson, conversationId = $newConversationId - WHERE id = $oldId - ` - ).run({ - newId: newJson.id, - newJson: objectToJSON(newJson), - oldId: session.id, - newConversationId: newJson.conversationId, - }); - } - - // Update identity keys to stable IDs - const allIdentityKeys = db.prepare('SELECT * FROM identityKeys;').all<{ - json: string; - id: number; - }>(); - for (const identityKey of allIdentityKeys) { - const newJson = JSON.parse(identityKey.json); - newJson.id = allConversationsByOldId[newJson.id]; - db.prepare( - ` - UPDATE identityKeys - SET id = $newId, json = $newJson - WHERE id = $oldId - ` - ).run({ - newId: newJson.id, - newJson: objectToJSON(newJson), - oldId: identityKey.id, - }); - } - - // Recreate triggers - for (const trigger of triggers) { - db.exec(trigger.sql); - } - - db.pragma('user_version = 20'); - })(); - logger.info('updateToSchemaVersion20: success!'); -} - -function updateToSchemaVersion21( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 21) { - return; - } - - db.transaction(() => { - db.exec(` - UPDATE conversations - SET json = json_set( - json, - '$.messageCount', - (SELECT count(*) FROM messages WHERE messages.conversationId = conversations.id) - ); - UPDATE conversations - SET json = json_set( - json, - '$.sentMessageCount', - (SELECT count(*) FROM messages WHERE messages.conversationId = conversations.id AND messages.type = 'outgoing') - ); - `); - db.pragma('user_version = 21'); - })(); - logger.info('updateToSchemaVersion21: success!'); -} - -function updateToSchemaVersion22( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 22) { - return; - } - - db.transaction(() => { - db.exec(` - ALTER TABLE unprocessed - ADD COLUMN sourceUuid STRING; - `); - - db.pragma('user_version = 22'); - })(); - logger.info('updateToSchemaVersion22: success!'); -} - -function updateToSchemaVersion23( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 23) { - return; - } - - db.transaction(() => { - db.exec(` - -- Remove triggers which keep full-text search up to date - DROP TRIGGER messages_on_insert; - DROP TRIGGER messages_on_update; - DROP TRIGGER messages_on_delete; - `); - - db.pragma('user_version = 23'); - })(); - logger.info('updateToSchemaVersion23: success!'); -} - -function updateToSchemaVersion24( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 24) { - return; - } - - db.transaction(() => { - db.exec(` - ALTER TABLE conversations - ADD COLUMN profileLastFetchedAt INTEGER; - `); - - db.pragma('user_version = 24'); - })(); - logger.info('updateToSchemaVersion24: success!'); -} - -function updateToSchemaVersion25( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 25) { - return; - } - - db.transaction(() => { - db.exec(` - ALTER TABLE messages - RENAME TO old_messages - `); - - const indicesToDrop = [ - 'messages_expires_at', - 'messages_receipt', - 'messages_schemaVersion', - 'messages_conversation', - 'messages_duplicate_check', - 'messages_hasAttachments', - 'messages_hasFileAttachments', - 'messages_hasVisualMediaAttachments', - 'messages_without_timer', - 'messages_unread', - 'messages_view_once', - 'messages_sourceUuid', - ]; - for (const index of indicesToDrop) { - db.exec(`DROP INDEX IF EXISTS ${index};`); - } - - db.exec(` - -- - -- Create a new table with a different primary key - -- - - CREATE TABLE messages( - rowid INTEGER PRIMARY KEY ASC, - id STRING UNIQUE, - json TEXT, - unread INTEGER, - expires_at INTEGER, - sent_at INTEGER, - schemaVersion INTEGER, - conversationId STRING, - received_at INTEGER, - source STRING, - sourceDevice STRING, - hasAttachments INTEGER, - hasFileAttachments INTEGER, - hasVisualMediaAttachments INTEGER, - expireTimer INTEGER, - expirationStartTimestamp INTEGER, - type STRING, - body TEXT, - messageTimer INTEGER, - messageTimerStart INTEGER, - messageTimerExpiresAt INTEGER, - isErased INTEGER, - isViewOnce INTEGER, - sourceUuid TEXT); - - -- Create index in lieu of old PRIMARY KEY - CREATE INDEX messages_id ON messages (id ASC); - - -- - -- Recreate indices - -- - - CREATE INDEX messages_expires_at ON messages (expires_at); - - CREATE INDEX messages_receipt ON messages (sent_at); - - CREATE INDEX messages_schemaVersion ON messages (schemaVersion); - - CREATE INDEX messages_conversation ON messages - (conversationId, received_at); - - CREATE INDEX messages_duplicate_check ON messages - (source, sourceDevice, sent_at); - - CREATE INDEX messages_hasAttachments ON messages - (conversationId, hasAttachments, received_at); - - CREATE INDEX messages_hasFileAttachments ON messages - (conversationId, hasFileAttachments, received_at); - - CREATE INDEX messages_hasVisualMediaAttachments ON messages - (conversationId, hasVisualMediaAttachments, received_at); - - CREATE INDEX messages_without_timer ON messages - (expireTimer, expires_at, type) - WHERE expires_at IS NULL AND expireTimer IS NOT NULL; - - CREATE INDEX messages_unread ON messages - (conversationId, unread) WHERE unread IS NOT NULL; - - CREATE INDEX messages_view_once ON messages - (isErased) WHERE isViewOnce = 1; - - CREATE INDEX messages_sourceUuid on messages(sourceUuid); - - -- New index for searchMessages - CREATE INDEX messages_searchOrder on messages(received_at, sent_at); - - -- - -- Re-create messages_fts and add triggers - -- - - DROP TABLE messages_fts; - - CREATE VIRTUAL TABLE messages_fts USING fts5(body); - - CREATE TRIGGER messages_on_insert AFTER INSERT ON messages - WHEN new.isViewOnce IS NULL OR new.isViewOnce != 1 - BEGIN - INSERT INTO messages_fts - (rowid, body) - VALUES - (new.rowid, new.body); - END; - - CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN - DELETE FROM messages_fts WHERE rowid = old.rowid; - END; - - CREATE TRIGGER messages_on_update AFTER UPDATE ON messages - WHEN new.isViewOnce IS NULL OR new.isViewOnce != 1 - BEGIN - DELETE FROM messages_fts WHERE rowid = old.rowid; - INSERT INTO messages_fts - (rowid, body) - VALUES - (new.rowid, new.body); - END; - - -- - -- Copy data over - -- - - INSERT INTO messages - ( - id, json, unread, expires_at, sent_at, schemaVersion, conversationId, - received_at, source, sourceDevice, hasAttachments, hasFileAttachments, - hasVisualMediaAttachments, expireTimer, expirationStartTimestamp, type, - body, messageTimer, messageTimerStart, messageTimerExpiresAt, isErased, - isViewOnce, sourceUuid - ) - SELECT - id, json, unread, expires_at, sent_at, schemaVersion, conversationId, - received_at, source, sourceDevice, hasAttachments, hasFileAttachments, - hasVisualMediaAttachments, expireTimer, expirationStartTimestamp, type, - body, messageTimer, messageTimerStart, messageTimerExpiresAt, isErased, - isViewOnce, sourceUuid - FROM old_messages; - - -- Drop old database - DROP TABLE old_messages; - `); - - db.pragma('user_version = 25'); - })(); - logger.info('updateToSchemaVersion25: success!'); -} - -function updateToSchemaVersion26( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 26) { - return; - } - - db.transaction(() => { - db.exec(` - DROP TRIGGER messages_on_insert; - DROP TRIGGER messages_on_update; - - CREATE TRIGGER messages_on_insert AFTER INSERT ON messages - WHEN new.isViewOnce IS NULL OR new.isViewOnce != 1 - BEGIN - INSERT INTO messages_fts - (rowid, body) - VALUES - (new.rowid, new.body); - END; - - CREATE TRIGGER messages_on_update AFTER UPDATE ON messages - WHEN new.body != old.body AND - (new.isViewOnce IS NULL OR new.isViewOnce != 1) - BEGIN - DELETE FROM messages_fts WHERE rowid = old.rowid; - INSERT INTO messages_fts - (rowid, body) - VALUES - (new.rowid, new.body); - END; - `); - - db.pragma('user_version = 26'); - })(); - logger.info('updateToSchemaVersion26: success!'); -} - -function updateToSchemaVersion27( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 27) { - return; - } - - db.transaction(() => { - db.exec(` - DELETE FROM messages_fts WHERE rowid IN - (SELECT rowid FROM messages WHERE body IS NULL); - - DROP TRIGGER messages_on_update; - - CREATE TRIGGER messages_on_update AFTER UPDATE ON messages - WHEN - new.body IS NULL OR - ((old.body IS NULL OR new.body != old.body) AND - (new.isViewOnce IS NULL OR new.isViewOnce != 1)) - BEGIN - DELETE FROM messages_fts WHERE rowid = old.rowid; - INSERT INTO messages_fts - (rowid, body) - VALUES - (new.rowid, new.body); - END; - - CREATE TRIGGER messages_on_view_once_update AFTER UPDATE ON messages - WHEN - new.body IS NOT NULL AND new.isViewOnce = 1 - BEGIN - DELETE FROM messages_fts WHERE rowid = old.rowid; - END; - `); - - db.pragma('user_version = 27'); - })(); - logger.info('updateToSchemaVersion27: success!'); -} - -function updateToSchemaVersion28( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 28) { - return; - } - - db.transaction(() => { - db.exec(` - CREATE TABLE jobs( - id TEXT PRIMARY KEY, - queueType TEXT STRING NOT NULL, - timestamp INTEGER NOT NULL, - data STRING TEXT - ); - - CREATE INDEX jobs_timestamp ON jobs (timestamp); - `); - - db.pragma('user_version = 28'); - })(); - logger.info('updateToSchemaVersion28: success!'); -} - -function updateToSchemaVersion29( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 29) { - return; - } - - db.transaction(() => { - db.exec(` - CREATE TABLE reactions( - conversationId STRING, - emoji STRING, - fromId STRING, - messageReceivedAt INTEGER, - targetAuthorUuid STRING, - targetTimestamp INTEGER, - unread INTEGER - ); - - CREATE INDEX reactions_unread ON reactions ( - unread, - conversationId - ); - - CREATE INDEX reaction_identifier ON reactions ( - emoji, - targetAuthorUuid, - targetTimestamp - ); - `); - - db.pragma('user_version = 29'); - })(); - logger.info('updateToSchemaVersion29: success!'); -} - -function updateToSchemaVersion30( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 30) { - return; - } - - db.transaction(() => { - db.exec(` - CREATE TABLE senderKeys( - id TEXT PRIMARY KEY NOT NULL, - senderId TEXT NOT NULL, - distributionId TEXT NOT NULL, - data BLOB NOT NULL, - lastUpdatedDate NUMBER NOT NULL - ); - `); - - db.pragma('user_version = 30'); - })(); - logger.info('updateToSchemaVersion30: success!'); -} - -function updateToSchemaVersion31( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 31) { - return; - } - logger.info('updateToSchemaVersion31: starting...'); - db.transaction(() => { - db.exec(` - DROP INDEX unprocessed_id; - DROP INDEX unprocessed_timestamp; - ALTER TABLE unprocessed RENAME TO unprocessed_old; - - CREATE TABLE unprocessed( - id STRING PRIMARY KEY ASC, - timestamp INTEGER, - version INTEGER, - attempts INTEGER, - envelope TEXT, - decrypted TEXT, - source TEXT, - sourceDevice TEXT, - serverTimestamp INTEGER, - sourceUuid STRING - ); - - CREATE INDEX unprocessed_timestamp ON unprocessed ( - timestamp - ); - - INSERT OR REPLACE INTO unprocessed - (id, timestamp, version, attempts, envelope, decrypted, source, - sourceDevice, serverTimestamp, sourceUuid) - SELECT - id, timestamp, version, attempts, envelope, decrypted, source, - sourceDevice, serverTimestamp, sourceUuid - FROM unprocessed_old; - - DROP TABLE unprocessed_old; - `); - - db.pragma('user_version = 31'); - })(); - logger.info('updateToSchemaVersion31: success!'); -} - -function updateToSchemaVersion32( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 32) { - return; - } - - db.transaction(() => { - db.exec(` - ALTER TABLE messages - ADD COLUMN serverGuid STRING NULL; - - ALTER TABLE unprocessed - ADD COLUMN serverGuid STRING NULL; - `); - - db.pragma('user_version = 32'); - })(); - logger.info('updateToSchemaVersion32: success!'); -} - -function updateToSchemaVersion33( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 33) { - return; - } - - db.transaction(() => { - db.exec(` - -- These indexes should exist, but we add "IF EXISTS" for safety. - DROP INDEX IF EXISTS messages_expires_at; - DROP INDEX IF EXISTS messages_without_timer; - - ALTER TABLE messages - ADD COLUMN - expiresAt INT - GENERATED ALWAYS - AS (expirationStartTimestamp + (expireTimer * 1000)); - - CREATE INDEX message_expires_at ON messages ( - expiresAt - ); - - CREATE INDEX outgoing_messages_without_expiration_start_timestamp ON messages ( - expireTimer, expirationStartTimestamp, type - ) - WHERE expireTimer IS NOT NULL AND expirationStartTimestamp IS NULL; - `); - - db.pragma('user_version = 33'); - })(); - logger.info('updateToSchemaVersion33: success!'); -} - -function updateToSchemaVersion34( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 34) { - return; - } - - db.transaction(() => { - db.exec(` - -- This index should exist, but we add "IF EXISTS" for safety. - DROP INDEX IF EXISTS outgoing_messages_without_expiration_start_timestamp; - - CREATE INDEX messages_unexpectedly_missing_expiration_start_timestamp ON messages ( - expireTimer, expirationStartTimestamp, type - ) - WHERE expireTimer IS NOT NULL AND expirationStartTimestamp IS NULL; - `); - - db.pragma('user_version = 34'); - })(); - logger.info('updateToSchemaVersion34: success!'); -} - -function updateToSchemaVersion35( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 35) { - return; - } - - db.transaction(() => { - db.exec(` - CREATE INDEX expiring_message_by_conversation_and_received_at - ON messages - ( - expirationStartTimestamp, - expireTimer, - conversationId, - received_at - ); - `); - - db.pragma('user_version = 35'); - })(); - logger.info('updateToSchemaVersion35: success!'); -} - -// Reverted -function updateToSchemaVersion36( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 36) { - return; - } - - db.pragma('user_version = 36'); - logger.info('updateToSchemaVersion36: success!'); -} - -function updateToSchemaVersion37( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 37) { - return; - } - - db.transaction(() => { - db.exec(` - -- Create send log primary table - - CREATE TABLE sendLogPayloads( - id INTEGER PRIMARY KEY ASC, - - timestamp INTEGER NOT NULL, - contentHint INTEGER NOT NULL, - proto BLOB NOT NULL - ); - - CREATE INDEX sendLogPayloadsByTimestamp ON sendLogPayloads (timestamp); - - -- Create send log recipients table with foreign key relationship to payloads - - CREATE TABLE sendLogRecipients( - payloadId INTEGER NOT NULL, - - recipientUuid STRING NOT NULL, - deviceId INTEGER NOT NULL, - - PRIMARY KEY (payloadId, recipientUuid, deviceId), - - CONSTRAINT sendLogRecipientsForeignKey - FOREIGN KEY (payloadId) - REFERENCES sendLogPayloads(id) - ON DELETE CASCADE - ); - - CREATE INDEX sendLogRecipientsByRecipient - ON sendLogRecipients (recipientUuid, deviceId); - - -- Create send log messages table with foreign key relationship to payloads - - CREATE TABLE sendLogMessageIds( - payloadId INTEGER NOT NULL, - - messageId STRING NOT NULL, - - PRIMARY KEY (payloadId, messageId), - - CONSTRAINT sendLogMessageIdsForeignKey - FOREIGN KEY (payloadId) - REFERENCES sendLogPayloads(id) - ON DELETE CASCADE - ); - - CREATE INDEX sendLogMessageIdsByMessage - ON sendLogMessageIds (messageId); - - -- Recreate messages table delete trigger with send log support - - DROP TRIGGER messages_on_delete; - - CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN - DELETE FROM messages_fts WHERE rowid = old.rowid; - DELETE FROM sendLogPayloads WHERE id IN ( - SELECT payloadId FROM sendLogMessageIds - WHERE messageId = old.id - ); - END; - - --- Add messageId column to reactions table to properly track proto associations - - ALTER TABLE reactions ADD column messageId STRING; - `); - - db.pragma('user_version = 37'); - })(); - logger.info('updateToSchemaVersion37: success!'); -} - -function updateToSchemaVersion38( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 38) { - return; - } - - db.transaction(() => { - db.exec(` - DROP INDEX IF EXISTS messages_duplicate_check; - - ALTER TABLE messages - RENAME COLUMN sourceDevice TO deprecatedSourceDevice; - ALTER TABLE messages - ADD COLUMN sourceDevice INTEGER; - - UPDATE messages - SET - sourceDevice = CAST(deprecatedSourceDevice AS INTEGER), - deprecatedSourceDevice = NULL; - - ALTER TABLE unprocessed - RENAME COLUMN sourceDevice TO deprecatedSourceDevice; - ALTER TABLE unprocessed - ADD COLUMN sourceDevice INTEGER; - - UPDATE unprocessed - SET - sourceDevice = CAST(deprecatedSourceDevice AS INTEGER), - deprecatedSourceDevice = NULL; - `); - - db.pragma('user_version = 38'); - })(); - logger.info('updateToSchemaVersion38: success!'); -} - -function updateToSchemaVersion39( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 39) { - return; - } - - db.transaction(() => { - db.exec('ALTER TABLE messages RENAME COLUMN unread TO readStatus;'); - - db.pragma('user_version = 39'); - })(); - logger.info('updateToSchemaVersion39: success!'); -} - -function updateToSchemaVersion40( - currentVersion: number, - db: Database, - logger: LoggerType -): void { - if (currentVersion >= 40) { - return; - } - - db.transaction(() => { - db.exec( - ` - CREATE TABLE groupCallRings( - ringId INTEGER PRIMARY KEY, - isActive INTEGER NOT NULL, - createdAt INTEGER NOT NULL - ); - ` + CREATE INDEX messages_expiring ON messages ( + expireTimer, + expirationStartTimestamp, + expires_at ); - db.pragma('user_version = 40'); - })(); - logger.info('updateToSchemaVersion40: success!'); + UPDATE messages SET + expirationStartTimestamp = json_extract(json, '$.expirationStartTimestamp'), + expireTimer = json_extract(json, '$.expireTimer'), + type = json_extract(json, '$.type'); + `); } -export const SCHEMA_VERSIONS = [ - updateToSchemaVersion1, - updateToSchemaVersion2, - updateToSchemaVersion3, - updateToSchemaVersion4, +function updateToSchemaVersion3(db: Database): void { + db.exec(` + DROP INDEX messages_expiring; + DROP INDEX messages_unread; + + CREATE INDEX messages_without_timer ON messages ( + expireTimer, + expires_at, + type + ) WHERE expires_at IS NULL AND expireTimer IS NOT NULL; + + CREATE INDEX messages_unread ON messages ( + conversationId, + unread + ) WHERE unread IS NOT NULL; + + ANALYZE; + `); +} + +function updateToSchemaVersion4(db: Database): void { + db.exec(` + CREATE TABLE conversations( + id STRING PRIMARY KEY ASC, + json TEXT, + + active_at INTEGER, + type STRING, + members TEXT, + name TEXT, + profileName TEXT + ); + CREATE INDEX conversations_active ON conversations ( + active_at + ) WHERE active_at IS NOT NULL; + + CREATE INDEX conversations_type ON conversations ( + type + ) WHERE type IS NOT NULL; + `); +} + +function updateToSchemaVersion6(db: Database): void { + db.exec(` + -- key-value, ids are strings, one extra column + CREATE TABLE sessions( + id STRING PRIMARY KEY ASC, + number STRING, + json TEXT + ); + CREATE INDEX sessions_number ON sessions ( + number + ) WHERE number IS NOT NULL; + -- key-value, ids are strings + CREATE TABLE groups( + id STRING PRIMARY KEY ASC, + json TEXT + ); + CREATE TABLE identityKeys( + id STRING PRIMARY KEY ASC, + json TEXT + ); + CREATE TABLE items( + id STRING PRIMARY KEY ASC, + json TEXT + ); + -- key-value, ids are integers + CREATE TABLE preKeys( + id INTEGER PRIMARY KEY ASC, + json TEXT + ); + CREATE TABLE signedPreKeys( + id INTEGER PRIMARY KEY ASC, + json TEXT + ); + `); +} + +function updateToSchemaVersion7(db: Database): void { + db.exec(` + -- SQLite has been coercing our STRINGs into numbers, so we force it with TEXT + -- We create a new table then copy the data into it, since we can't modify columns + DROP INDEX sessions_number; + ALTER TABLE sessions RENAME TO sessions_old; + + CREATE TABLE sessions( + id TEXT PRIMARY KEY, + number TEXT, + json TEXT + ); + CREATE INDEX sessions_number ON sessions ( + number + ) WHERE number IS NOT NULL; + INSERT INTO sessions(id, number, json) + SELECT '+' || id, number, json FROM sessions_old; + DROP TABLE sessions_old; + `); +} + +function updateToSchemaVersion8(db: Database): void { + db.exec(` + -- First, we pull a new body field out of the message table's json blob + ALTER TABLE messages + ADD COLUMN body TEXT; + UPDATE messages SET body = json_extract(json, '$.body'); + + -- Then we create our full-text search table and populate it + CREATE VIRTUAL TABLE messages_fts + USING fts5(id UNINDEXED, body); + + INSERT INTO messages_fts(id, body) + SELECT id, body FROM messages; + + -- Then we set up triggers to keep the full-text search table up to date + CREATE TRIGGER messages_on_insert AFTER INSERT ON messages BEGIN + INSERT INTO messages_fts ( + id, + body + ) VALUES ( + new.id, + new.body + ); + END; + CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN + DELETE FROM messages_fts WHERE id = old.id; + END; + CREATE TRIGGER messages_on_update AFTER UPDATE ON messages BEGIN + DELETE FROM messages_fts WHERE id = old.id; + INSERT INTO messages_fts( + id, + body + ) VALUES ( + new.id, + new.body + ); + END; + `); + + // For formatting search results: + // https://sqlite.org/fts5.html#the_highlight_function + // https://sqlite.org/fts5.html#the_snippet_function +} + +function updateToSchemaVersion9(db: Database): void { + db.exec(` + CREATE TABLE attachment_downloads( + id STRING primary key, + timestamp INTEGER, + pending INTEGER, + json TEXT + ); + + CREATE INDEX attachment_downloads_timestamp + ON attachment_downloads ( + timestamp + ) WHERE pending = 0; + CREATE INDEX attachment_downloads_pending + ON attachment_downloads ( + pending + ) WHERE pending != 0; + `); +} + +function updateToSchemaVersion10(db: Database): void { + db.exec(` + DROP INDEX unprocessed_id; + DROP INDEX unprocessed_timestamp; + ALTER TABLE unprocessed RENAME TO unprocessed_old; + + CREATE TABLE unprocessed( + id STRING, + timestamp INTEGER, + version INTEGER, + attempts INTEGER, + envelope TEXT, + decrypted TEXT, + source TEXT, + sourceDevice TEXT, + serverTimestamp INTEGER + ); + + CREATE INDEX unprocessed_id ON unprocessed ( + id + ); + CREATE INDEX unprocessed_timestamp ON unprocessed ( + timestamp + ); + + INSERT INTO unprocessed ( + id, + timestamp, + version, + attempts, + envelope, + decrypted, + source, + sourceDevice, + serverTimestamp + ) SELECT + id, + timestamp, + json_extract(json, '$.version'), + json_extract(json, '$.attempts'), + json_extract(json, '$.envelope'), + json_extract(json, '$.decrypted'), + json_extract(json, '$.source'), + json_extract(json, '$.sourceDevice'), + json_extract(json, '$.serverTimestamp') + FROM unprocessed_old; + + DROP TABLE unprocessed_old; + `); +} + +function updateToSchemaVersion11(db: Database): void { + db.exec(` + DROP TABLE groups; + `); +} + +function updateToSchemaVersion12(db: Database): void { + db.exec(` + CREATE TABLE sticker_packs( + id TEXT PRIMARY KEY, + key TEXT NOT NULL, + + author STRING, + coverStickerId INTEGER, + createdAt INTEGER, + downloadAttempts INTEGER, + installedAt INTEGER, + lastUsed INTEGER, + status STRING, + stickerCount INTEGER, + title STRING + ); + + CREATE TABLE stickers( + id INTEGER NOT NULL, + packId TEXT NOT NULL, + + emoji STRING, + height INTEGER, + isCoverOnly INTEGER, + lastUsed INTEGER, + path STRING, + width INTEGER, + + PRIMARY KEY (id, packId), + CONSTRAINT stickers_fk + FOREIGN KEY (packId) + REFERENCES sticker_packs(id) + ON DELETE CASCADE + ); + + CREATE INDEX stickers_recents + ON stickers ( + lastUsed + ) WHERE lastUsed IS NOT NULL; + + CREATE TABLE sticker_references( + messageId STRING, + packId TEXT, + CONSTRAINT sticker_references_fk + FOREIGN KEY(packId) + REFERENCES sticker_packs(id) + ON DELETE CASCADE + ); + `); +} + +function updateToSchemaVersion13(db: Database): void { + db.exec(` + ALTER TABLE sticker_packs ADD COLUMN attemptedStatus STRING; + `); +} + +function updateToSchemaVersion14(db: Database): void { + db.exec(` + CREATE TABLE emojis( + shortName STRING PRIMARY KEY, + lastUsage INTEGER + ); + + CREATE INDEX emojis_lastUsage + ON emojis ( + lastUsage + ); + `); +} + +function updateToSchemaVersion15(db: Database): void { + db.exec(` + -- SQLite has again coerced our STRINGs into numbers, so we force it with TEXT + -- We create a new table then copy the data into it, since we can't modify columns + + DROP INDEX emojis_lastUsage; + ALTER TABLE emojis RENAME TO emojis_old; + + CREATE TABLE emojis( + shortName TEXT PRIMARY KEY, + lastUsage INTEGER + ); + CREATE INDEX emojis_lastUsage + ON emojis ( + lastUsage + ); + + DELETE FROM emojis WHERE shortName = 1; + INSERT INTO emojis(shortName, lastUsage) + SELECT shortName, lastUsage FROM emojis_old; + + DROP TABLE emojis_old; + `); +} + +function updateToSchemaVersion16(db: Database): void { + db.exec(` + ALTER TABLE messages + ADD COLUMN messageTimer INTEGER; + ALTER TABLE messages + ADD COLUMN messageTimerStart INTEGER; + ALTER TABLE messages + ADD COLUMN messageTimerExpiresAt INTEGER; + ALTER TABLE messages + ADD COLUMN isErased INTEGER; + + CREATE INDEX messages_message_timer ON messages ( + messageTimer, + messageTimerStart, + messageTimerExpiresAt, + isErased + ) WHERE messageTimer IS NOT NULL; + + -- Updating full-text triggers to avoid anything with a messageTimer set + + DROP TRIGGER messages_on_insert; + DROP TRIGGER messages_on_delete; + DROP TRIGGER messages_on_update; + + CREATE TRIGGER messages_on_insert AFTER INSERT ON messages + WHEN new.messageTimer IS NULL + BEGIN + INSERT INTO messages_fts ( + id, + body + ) VALUES ( + new.id, + new.body + ); + END; + CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN + DELETE FROM messages_fts WHERE id = old.id; + END; + CREATE TRIGGER messages_on_update AFTER UPDATE ON messages + WHEN new.messageTimer IS NULL + BEGIN + DELETE FROM messages_fts WHERE id = old.id; + INSERT INTO messages_fts( + id, + body + ) VALUES ( + new.id, + new.body + ); + END; + `); +} + +function updateToSchemaVersion17(db: Database, logger: LoggerType): void { + try { + db.exec(` + ALTER TABLE messages + ADD COLUMN isViewOnce INTEGER; + + DROP INDEX messages_message_timer; + `); + } catch (error) { + logger.info('Message table already had isViewOnce column'); + } + + db.exec('DROP INDEX IF EXISTS messages_view_once;'); + + db.exec(` + CREATE INDEX messages_view_once ON messages ( + isErased + ) WHERE isViewOnce = 1; + + -- Updating full-text triggers to avoid anything with isViewOnce = 1 + + DROP TRIGGER messages_on_insert; + DROP TRIGGER messages_on_update; + + CREATE TRIGGER messages_on_insert AFTER INSERT ON messages + WHEN new.isViewOnce != 1 + BEGIN + INSERT INTO messages_fts ( + id, + body + ) VALUES ( + new.id, + new.body + ); + END; + CREATE TRIGGER messages_on_update AFTER UPDATE ON messages + WHEN new.isViewOnce != 1 + BEGIN + DELETE FROM messages_fts WHERE id = old.id; + INSERT INTO messages_fts( + id, + body + ) VALUES ( + new.id, + new.body + ); + END; + `); +} + +function updateToSchemaVersion18(db: Database): void { + db.exec(` + -- Delete and rebuild full-text search index to capture everything + + DELETE FROM messages_fts; + INSERT INTO messages_fts(messages_fts) VALUES('rebuild'); + + INSERT INTO messages_fts(id, body) + SELECT id, body FROM messages WHERE isViewOnce IS NULL OR isViewOnce != 1; + + -- Fixing full-text triggers + + DROP TRIGGER messages_on_insert; + DROP TRIGGER messages_on_update; + + CREATE TRIGGER messages_on_insert AFTER INSERT ON messages + WHEN new.isViewOnce IS NULL OR new.isViewOnce != 1 + BEGIN + INSERT INTO messages_fts ( + id, + body + ) VALUES ( + new.id, + new.body + ); + END; + CREATE TRIGGER messages_on_update AFTER UPDATE ON messages + WHEN new.isViewOnce IS NULL OR new.isViewOnce != 1 + BEGIN + DELETE FROM messages_fts WHERE id = old.id; + INSERT INTO messages_fts( + id, + body + ) VALUES ( + new.id, + new.body + ); + END; + `); +} + +function updateToSchemaVersion19(db: Database): void { + db.exec(` + ALTER TABLE conversations + ADD COLUMN profileFamilyName TEXT; + ALTER TABLE conversations + ADD COLUMN profileFullName TEXT; + + -- Preload new field with the profileName we already have + UPDATE conversations SET profileFullName = profileName; + `); +} + +function updateToSchemaVersion20(db: Database): void { + // The triggers on the messages table slow down this migration + // significantly, so we drop them and recreate them later. + // Drop triggers + const triggers = db + .prepare( + "SELECT * FROM sqlite_master WHERE type = 'trigger' AND tbl_name = 'messages'" + ) + .all<{ name: string; sql: string }>(); + + for (const trigger of triggers) { + db.exec(`DROP TRIGGER ${trigger.name}`); + } + + // Create new columns and indices + db.exec(` + ALTER TABLE conversations ADD COLUMN e164 TEXT; + ALTER TABLE conversations ADD COLUMN uuid TEXT; + ALTER TABLE conversations ADD COLUMN groupId TEXT; + ALTER TABLE messages ADD COLUMN sourceUuid TEXT; + ALTER TABLE sessions RENAME COLUMN number TO conversationId; + CREATE INDEX conversations_e164 ON conversations(e164); + CREATE INDEX conversations_uuid ON conversations(uuid); + CREATE INDEX conversations_groupId ON conversations(groupId); + CREATE INDEX messages_sourceUuid on messages(sourceUuid); + + -- Migrate existing IDs + UPDATE conversations SET e164 = '+' || id WHERE type = 'private'; + UPDATE conversations SET groupId = id WHERE type = 'group'; + `); + + // Drop invalid groups and any associated messages + const maybeInvalidGroups = db + .prepare( + "SELECT * FROM conversations WHERE type = 'group' AND members IS NULL;" + ) + .all<{ id: string; json: string }>(); + for (const group of maybeInvalidGroups) { + const json: { id: string; members: Array } = JSON.parse( + group.json + ); + if (!json.members || !json.members.length) { + db.prepare('DELETE FROM conversations WHERE id = $id;').run({ + id: json.id, + }); + db.prepare('DELETE FROM messages WHERE conversationId = $id;').run({ + id: json.id, + }); + } + } + + // Generate new IDs and alter data + const allConversations = db.prepare('SELECT * FROM conversations;').all<{ + id: string; + type: string; + }>(); + const allConversationsByOldId = keyBy(allConversations, 'id'); + + for (const row of allConversations) { + const oldId = row.id; + const newId = generateUuid(); + allConversationsByOldId[oldId].id = newId; + const patchObj: { id: string; e164?: string; groupId?: string } = { + id: newId, + }; + if (row.type === 'private') { + patchObj.e164 = `+${oldId}`; + } else if (row.type === 'group') { + patchObj.groupId = oldId; + } + const patch = JSON.stringify(patchObj); + + db.prepare( + ` + UPDATE conversations + SET id = $newId, json = JSON_PATCH(json, $patch) + WHERE id = $oldId + ` + ).run({ + newId, + oldId, + patch, + }); + const messagePatch = JSON.stringify({ conversationId: newId }); + db.prepare( + ` + UPDATE messages + SET conversationId = $newId, json = JSON_PATCH(json, $patch) + WHERE conversationId = $oldId + ` + ).run({ newId, oldId, patch: messagePatch }); + } + + const groupConversations: Array<{ + id: string; + members: string; + json: string; + }> = db + .prepare( + ` + SELECT id, members, json FROM conversations WHERE type = 'group'; + ` + ) + .all(); + + // Update group conversations, point members at new conversation ids + groupConversations.forEach(groupRow => { + const members = groupRow.members.split(/\s?\+/).filter(Boolean); + const newMembers = []; + for (const m of members) { + const memberRow = allConversationsByOldId[m]; + + if (memberRow) { + newMembers.push(memberRow.id); + } else { + // We didn't previously have a private conversation for this member, + // we need to create one + const id = generateUuid(); + const updatedConversation = { + id, + e164: m, + type: 'private', + version: 2, + unreadCount: 0, + verified: 0, + + // Not directly used by saveConversation, but are necessary + // for conversation model + inbox_position: 0, + isPinned: false, + lastMessageDeletedForEveryone: false, + markedUnread: false, + messageCount: 0, + sentMessageCount: 0, + profileSharing: false, + }; + + db.prepare( + ` + UPDATE conversations + SET + json = $json, + e164 = $e164, + type = $type + WHERE + id = $id; + ` + ).run({ + id: updatedConversation.id, + json: objectToJSON(updatedConversation), + e164: updatedConversation.e164, + type: updatedConversation.type, + }); + + newMembers.push(id); + } + } + const json = { + ...jsonToObject>(groupRow.json), + members: newMembers, + }; + const newMembersValue = newMembers.join(' '); + db.prepare( + ` + UPDATE conversations + SET members = $newMembersValue, json = $newJsonValue + WHERE id = $id + ` + ).run({ + id: groupRow.id, + newMembersValue, + newJsonValue: objectToJSON(json), + }); + }); + + // Update sessions to stable IDs + const allSessions = db.prepare('SELECT * FROM sessions;').all<{ + id: string; + json: string; + }>(); + for (const session of allSessions) { + // Not using patch here so we can explicitly delete a property rather than + // implicitly delete via null + const newJson = JSON.parse(session.json); + const conversation = allConversationsByOldId[newJson.number.substr(1)]; + if (conversation) { + newJson.conversationId = conversation.id; + newJson.id = `${newJson.conversationId}.${newJson.deviceId}`; + } + delete newJson.number; + db.prepare( + ` + UPDATE sessions + SET id = $newId, json = $newJson, conversationId = $newConversationId + WHERE id = $oldId + ` + ).run({ + newId: newJson.id, + newJson: objectToJSON(newJson), + oldId: session.id, + newConversationId: newJson.conversationId, + }); + } + + // Update identity keys to stable IDs + const allIdentityKeys = db.prepare('SELECT * FROM identityKeys;').all<{ + json: string; + id: number; + }>(); + for (const identityKey of allIdentityKeys) { + const newJson = JSON.parse(identityKey.json); + newJson.id = allConversationsByOldId[newJson.id]; + db.prepare( + ` + UPDATE identityKeys + SET id = $newId, json = $newJson + WHERE id = $oldId + ` + ).run({ + newId: newJson.id, + newJson: objectToJSON(newJson), + oldId: identityKey.id, + }); + } + + // Recreate triggers + for (const trigger of triggers) { + db.exec(trigger.sql); + } +} + +function updateToSchemaVersion21(db: Database): void { + db.exec(` + UPDATE conversations + SET json = json_set( + json, + '$.messageCount', + (SELECT count(*) FROM messages WHERE messages.conversationId = conversations.id) + ); + UPDATE conversations + SET json = json_set( + json, + '$.sentMessageCount', + (SELECT count(*) FROM messages WHERE messages.conversationId = conversations.id AND messages.type = 'outgoing') + ); + `); +} + +function updateToSchemaVersion22(db: Database): void { + db.exec(` + ALTER TABLE unprocessed + ADD COLUMN sourceUuid STRING; + `); +} + +function updateToSchemaVersion23(db: Database): void { + db.exec(` + -- Remove triggers which keep full-text search up to date + DROP TRIGGER messages_on_insert; + DROP TRIGGER messages_on_update; + DROP TRIGGER messages_on_delete; + `); +} + +function updateToSchemaVersion24(db: Database): void { + db.exec(` + ALTER TABLE conversations + ADD COLUMN profileLastFetchedAt INTEGER; + `); +} + +function updateToSchemaVersion25(db: Database): void { + db.exec(` + ALTER TABLE messages + RENAME TO old_messages + `); + + const indicesToDrop = [ + 'messages_expires_at', + 'messages_receipt', + 'messages_schemaVersion', + 'messages_conversation', + 'messages_duplicate_check', + 'messages_hasAttachments', + 'messages_hasFileAttachments', + 'messages_hasVisualMediaAttachments', + 'messages_without_timer', + 'messages_unread', + 'messages_view_once', + 'messages_sourceUuid', + ]; + for (const index of indicesToDrop) { + db.exec(`DROP INDEX IF EXISTS ${index};`); + } + + db.exec(` + -- + -- Create a new table with a different primary key + -- + + CREATE TABLE messages( + rowid INTEGER PRIMARY KEY ASC, + id STRING UNIQUE, + json TEXT, + unread INTEGER, + expires_at INTEGER, + sent_at INTEGER, + schemaVersion INTEGER, + conversationId STRING, + received_at INTEGER, + source STRING, + sourceDevice STRING, + hasAttachments INTEGER, + hasFileAttachments INTEGER, + hasVisualMediaAttachments INTEGER, + expireTimer INTEGER, + expirationStartTimestamp INTEGER, + type STRING, + body TEXT, + messageTimer INTEGER, + messageTimerStart INTEGER, + messageTimerExpiresAt INTEGER, + isErased INTEGER, + isViewOnce INTEGER, + sourceUuid TEXT); + + -- Create index in lieu of old PRIMARY KEY + CREATE INDEX messages_id ON messages (id ASC); + + -- + -- Recreate indices + -- + + CREATE INDEX messages_expires_at ON messages (expires_at); + + CREATE INDEX messages_receipt ON messages (sent_at); + + CREATE INDEX messages_schemaVersion ON messages (schemaVersion); + + CREATE INDEX messages_conversation ON messages + (conversationId, received_at); + + CREATE INDEX messages_duplicate_check ON messages + (source, sourceDevice, sent_at); + + CREATE INDEX messages_hasAttachments ON messages + (conversationId, hasAttachments, received_at); + + CREATE INDEX messages_hasFileAttachments ON messages + (conversationId, hasFileAttachments, received_at); + + CREATE INDEX messages_hasVisualMediaAttachments ON messages + (conversationId, hasVisualMediaAttachments, received_at); + + CREATE INDEX messages_without_timer ON messages + (expireTimer, expires_at, type) + WHERE expires_at IS NULL AND expireTimer IS NOT NULL; + + CREATE INDEX messages_unread ON messages + (conversationId, unread) WHERE unread IS NOT NULL; + + CREATE INDEX messages_view_once ON messages + (isErased) WHERE isViewOnce = 1; + + CREATE INDEX messages_sourceUuid on messages(sourceUuid); + + -- New index for searchMessages + CREATE INDEX messages_searchOrder on messages(received_at, sent_at); + + -- + -- Re-create messages_fts and add triggers + -- + + DROP TABLE messages_fts; + + CREATE VIRTUAL TABLE messages_fts USING fts5(body); + + CREATE TRIGGER messages_on_insert AFTER INSERT ON messages + WHEN new.isViewOnce IS NULL OR new.isViewOnce != 1 + BEGIN + INSERT INTO messages_fts + (rowid, body) + VALUES + (new.rowid, new.body); + END; + + CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN + DELETE FROM messages_fts WHERE rowid = old.rowid; + END; + + CREATE TRIGGER messages_on_update AFTER UPDATE ON messages + WHEN new.isViewOnce IS NULL OR new.isViewOnce != 1 + BEGIN + DELETE FROM messages_fts WHERE rowid = old.rowid; + INSERT INTO messages_fts + (rowid, body) + VALUES + (new.rowid, new.body); + END; + + -- + -- Copy data over + -- + + INSERT INTO messages + ( + id, json, unread, expires_at, sent_at, schemaVersion, conversationId, + received_at, source, sourceDevice, hasAttachments, hasFileAttachments, + hasVisualMediaAttachments, expireTimer, expirationStartTimestamp, type, + body, messageTimer, messageTimerStart, messageTimerExpiresAt, isErased, + isViewOnce, sourceUuid + ) + SELECT + id, json, unread, expires_at, sent_at, schemaVersion, conversationId, + received_at, source, sourceDevice, hasAttachments, hasFileAttachments, + hasVisualMediaAttachments, expireTimer, expirationStartTimestamp, type, + body, messageTimer, messageTimerStart, messageTimerExpiresAt, isErased, + isViewOnce, sourceUuid + FROM old_messages; + + -- Drop old database + DROP TABLE old_messages; + `); +} + +function updateToSchemaVersion26(db: Database): void { + db.exec(` + DROP TRIGGER messages_on_insert; + DROP TRIGGER messages_on_update; + + CREATE TRIGGER messages_on_insert AFTER INSERT ON messages + WHEN new.isViewOnce IS NULL OR new.isViewOnce != 1 + BEGIN + INSERT INTO messages_fts + (rowid, body) + VALUES + (new.rowid, new.body); + END; + + CREATE TRIGGER messages_on_update AFTER UPDATE ON messages + WHEN new.body != old.body AND + (new.isViewOnce IS NULL OR new.isViewOnce != 1) + BEGIN + DELETE FROM messages_fts WHERE rowid = old.rowid; + INSERT INTO messages_fts + (rowid, body) + VALUES + (new.rowid, new.body); + END; + `); +} + +function updateToSchemaVersion27(db: Database): void { + db.exec(` + DELETE FROM messages_fts WHERE rowid IN + (SELECT rowid FROM messages WHERE body IS NULL); + + DROP TRIGGER messages_on_update; + + CREATE TRIGGER messages_on_update AFTER UPDATE ON messages + WHEN + new.body IS NULL OR + ((old.body IS NULL OR new.body != old.body) AND + (new.isViewOnce IS NULL OR new.isViewOnce != 1)) + BEGIN + DELETE FROM messages_fts WHERE rowid = old.rowid; + INSERT INTO messages_fts + (rowid, body) + VALUES + (new.rowid, new.body); + END; + + CREATE TRIGGER messages_on_view_once_update AFTER UPDATE ON messages + WHEN + new.body IS NOT NULL AND new.isViewOnce = 1 + BEGIN + DELETE FROM messages_fts WHERE rowid = old.rowid; + END; + `); +} + +function updateToSchemaVersion28(db: Database): void { + db.exec(` + CREATE TABLE jobs( + id TEXT PRIMARY KEY, + queueType TEXT STRING NOT NULL, + timestamp INTEGER NOT NULL, + data STRING TEXT + ); + + CREATE INDEX jobs_timestamp ON jobs (timestamp); + `); +} + +function updateToSchemaVersion29(db: Database): void { + db.exec(` + CREATE TABLE reactions( + conversationId STRING, + emoji STRING, + fromId STRING, + messageReceivedAt INTEGER, + targetAuthorUuid STRING, + targetTimestamp INTEGER, + unread INTEGER + ); + + CREATE INDEX reactions_unread ON reactions ( + unread, + conversationId + ); + + CREATE INDEX reaction_identifier ON reactions ( + emoji, + targetAuthorUuid, + targetTimestamp + ); + `); +} + +function updateToSchemaVersion30(db: Database): void { + db.exec(` + CREATE TABLE senderKeys( + id TEXT PRIMARY KEY NOT NULL, + senderId TEXT NOT NULL, + distributionId TEXT NOT NULL, + data BLOB NOT NULL, + lastUpdatedDate NUMBER NOT NULL + ); + `); +} + +function updateToSchemaVersion31(db: Database): void { + db.exec(` + DROP INDEX unprocessed_id; + DROP INDEX unprocessed_timestamp; + ALTER TABLE unprocessed RENAME TO unprocessed_old; + + CREATE TABLE unprocessed( + id STRING PRIMARY KEY ASC, + timestamp INTEGER, + version INTEGER, + attempts INTEGER, + envelope TEXT, + decrypted TEXT, + source TEXT, + sourceDevice TEXT, + serverTimestamp INTEGER, + sourceUuid STRING + ); + + CREATE INDEX unprocessed_timestamp ON unprocessed ( + timestamp + ); + + INSERT OR REPLACE INTO unprocessed + (id, timestamp, version, attempts, envelope, decrypted, source, + sourceDevice, serverTimestamp, sourceUuid) + SELECT + id, timestamp, version, attempts, envelope, decrypted, source, + sourceDevice, serverTimestamp, sourceUuid + FROM unprocessed_old; + + DROP TABLE unprocessed_old; + `); +} + +function updateToSchemaVersion32(db: Database): void { + db.exec(` + ALTER TABLE messages + ADD COLUMN serverGuid STRING NULL; + + ALTER TABLE unprocessed + ADD COLUMN serverGuid STRING NULL; + `); +} + +function updateToSchemaVersion33(db: Database): void { + db.exec(` + -- These indexes should exist, but we add "IF EXISTS" for safety. + DROP INDEX IF EXISTS messages_expires_at; + DROP INDEX IF EXISTS messages_without_timer; + + ALTER TABLE messages + ADD COLUMN + expiresAt INT + GENERATED ALWAYS + AS (expirationStartTimestamp + (expireTimer * 1000)); + + CREATE INDEX message_expires_at ON messages ( + expiresAt + ); + + CREATE INDEX outgoing_messages_without_expiration_start_timestamp ON messages ( + expireTimer, expirationStartTimestamp, type + ) + WHERE expireTimer IS NOT NULL AND expirationStartTimestamp IS NULL; + `); +} + +function updateToSchemaVersion34(db: Database): void { + db.exec(` + -- This index should exist, but we add "IF EXISTS" for safety. + DROP INDEX IF EXISTS outgoing_messages_without_expiration_start_timestamp; + + CREATE INDEX messages_unexpectedly_missing_expiration_start_timestamp ON messages ( + expireTimer, expirationStartTimestamp, type + ) + WHERE expireTimer IS NOT NULL AND expirationStartTimestamp IS NULL; + `); +} + +function updateToSchemaVersion35(db: Database): void { + db.exec(` + CREATE INDEX expiring_message_by_conversation_and_received_at + ON messages + ( + expirationStartTimestamp, + expireTimer, + conversationId, + received_at + ); + `); +} + +function updateToSchemaVersion36(): void { + // Reverted +} + +function updateToSchemaVersion37(db: Database): void { + db.exec(` + -- Create send log primary table + + CREATE TABLE sendLogPayloads( + id INTEGER PRIMARY KEY ASC, + + timestamp INTEGER NOT NULL, + contentHint INTEGER NOT NULL, + proto BLOB NOT NULL + ); + + CREATE INDEX sendLogPayloadsByTimestamp ON sendLogPayloads (timestamp); + + -- Create send log recipients table with foreign key relationship to payloads + + CREATE TABLE sendLogRecipients( + payloadId INTEGER NOT NULL, + + recipientUuid STRING NOT NULL, + deviceId INTEGER NOT NULL, + + PRIMARY KEY (payloadId, recipientUuid, deviceId), + + CONSTRAINT sendLogRecipientsForeignKey + FOREIGN KEY (payloadId) + REFERENCES sendLogPayloads(id) + ON DELETE CASCADE + ); + + CREATE INDEX sendLogRecipientsByRecipient + ON sendLogRecipients (recipientUuid, deviceId); + + -- Create send log messages table with foreign key relationship to payloads + + CREATE TABLE sendLogMessageIds( + payloadId INTEGER NOT NULL, + + messageId STRING NOT NULL, + + PRIMARY KEY (payloadId, messageId), + + CONSTRAINT sendLogMessageIdsForeignKey + FOREIGN KEY (payloadId) + REFERENCES sendLogPayloads(id) + ON DELETE CASCADE + ); + + CREATE INDEX sendLogMessageIdsByMessage + ON sendLogMessageIds (messageId); + + -- Recreate messages table delete trigger with send log support + + DROP TRIGGER messages_on_delete; + + CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN + DELETE FROM messages_fts WHERE rowid = old.rowid; + DELETE FROM sendLogPayloads WHERE id IN ( + SELECT payloadId FROM sendLogMessageIds + WHERE messageId = old.id + ); + END; + + --- Add messageId column to reactions table to properly track proto associations + + ALTER TABLE reactions ADD column messageId STRING; + `); +} + +function updateToSchemaVersion38(db: Database): void { + db.exec(` + DROP INDEX IF EXISTS messages_duplicate_check; + + ALTER TABLE messages + RENAME COLUMN sourceDevice TO deprecatedSourceDevice; + ALTER TABLE messages + ADD COLUMN sourceDevice INTEGER; + + UPDATE messages + SET + sourceDevice = CAST(deprecatedSourceDevice AS INTEGER), + deprecatedSourceDevice = NULL; + + ALTER TABLE unprocessed + RENAME COLUMN sourceDevice TO deprecatedSourceDevice; + ALTER TABLE unprocessed + ADD COLUMN sourceDevice INTEGER; + + UPDATE unprocessed + SET + sourceDevice = CAST(deprecatedSourceDevice AS INTEGER), + deprecatedSourceDevice = NULL; + `); +} + +function updateToSchemaVersion39(db: Database): void { + db.exec('ALTER TABLE messages RENAME COLUMN unread TO readStatus;'); +} + +function updateToSchemaVersion40(db: Database): void { + db.exec( + ` + CREATE TABLE groupCallRings( + ringId INTEGER PRIMARY KEY, + isActive INTEGER NOT NULL, + createdAt INTEGER NOT NULL + ); + ` + ); +} + +export type SchemaUpdateType = Readonly<{ + version: number; + update: ( + db: WritableDB, + logger: LoggerType, + startingVersion: number + ) => void | 'vacuum'; +}>; + +export const SCHEMA_VERSIONS: ReadonlyArray = [ + { version: 1, update: updateToSchemaVersion1 }, + { version: 2, update: updateToSchemaVersion2 }, + { version: 3, update: updateToSchemaVersion3 }, + { version: 4, update: updateToSchemaVersion4 }, // version 5 was dropped - updateToSchemaVersion6, - updateToSchemaVersion7, - updateToSchemaVersion8, - updateToSchemaVersion9, + { version: 6, update: updateToSchemaVersion6 }, + { version: 7, update: updateToSchemaVersion7 }, + { version: 8, update: updateToSchemaVersion8 }, + { version: 9, update: updateToSchemaVersion9 }, - updateToSchemaVersion10, - updateToSchemaVersion11, - updateToSchemaVersion12, - updateToSchemaVersion13, - updateToSchemaVersion14, - updateToSchemaVersion15, - updateToSchemaVersion16, - updateToSchemaVersion17, - updateToSchemaVersion18, - updateToSchemaVersion19, + { version: 10, update: updateToSchemaVersion10 }, + { version: 11, update: updateToSchemaVersion11 }, + { version: 12, update: updateToSchemaVersion12 }, + { version: 13, update: updateToSchemaVersion13 }, + { version: 14, update: updateToSchemaVersion14 }, + { version: 15, update: updateToSchemaVersion15 }, + { version: 16, update: updateToSchemaVersion16 }, + { version: 17, update: updateToSchemaVersion17 }, + { version: 18, update: updateToSchemaVersion18 }, + { version: 19, update: updateToSchemaVersion19 }, - updateToSchemaVersion20, - updateToSchemaVersion21, - updateToSchemaVersion22, - updateToSchemaVersion23, - updateToSchemaVersion24, - updateToSchemaVersion25, - updateToSchemaVersion26, - updateToSchemaVersion27, - updateToSchemaVersion28, - updateToSchemaVersion29, + { version: 20, update: updateToSchemaVersion20 }, + { version: 21, update: updateToSchemaVersion21 }, + { version: 22, update: updateToSchemaVersion22 }, + { version: 23, update: updateToSchemaVersion23 }, + { version: 24, update: updateToSchemaVersion24 }, + { version: 25, update: updateToSchemaVersion25 }, + { version: 26, update: updateToSchemaVersion26 }, + { version: 27, update: updateToSchemaVersion27 }, + { version: 28, update: updateToSchemaVersion28 }, + { version: 29, update: updateToSchemaVersion29 }, - updateToSchemaVersion30, - updateToSchemaVersion31, - updateToSchemaVersion32, - updateToSchemaVersion33, - updateToSchemaVersion34, - updateToSchemaVersion35, - updateToSchemaVersion36, - updateToSchemaVersion37, - updateToSchemaVersion38, - updateToSchemaVersion39, + { version: 30, update: updateToSchemaVersion30 }, + { version: 31, update: updateToSchemaVersion31 }, + { version: 32, update: updateToSchemaVersion32 }, + { version: 33, update: updateToSchemaVersion33 }, + { version: 34, update: updateToSchemaVersion34 }, + { version: 35, update: updateToSchemaVersion35 }, + { version: 36, update: updateToSchemaVersion36 }, + { version: 37, update: updateToSchemaVersion37 }, + { version: 38, update: updateToSchemaVersion38 }, + { version: 39, update: updateToSchemaVersion39 }, - updateToSchemaVersion40, - updateToSchemaVersion41, - updateToSchemaVersion42, - updateToSchemaVersion43, - updateToSchemaVersion44, - updateToSchemaVersion45, - updateToSchemaVersion46, - updateToSchemaVersion47, - updateToSchemaVersion48, - updateToSchemaVersion49, + { version: 40, update: updateToSchemaVersion40 }, + { version: 41, update: updateToSchemaVersion41 }, + { version: 42, update: updateToSchemaVersion42 }, + { version: 43, update: updateToSchemaVersion43 }, + { version: 44, update: updateToSchemaVersion44 }, + { version: 45, update: updateToSchemaVersion45 }, + { version: 46, update: updateToSchemaVersion46 }, + { version: 47, update: updateToSchemaVersion47 }, + { version: 48, update: updateToSchemaVersion48 }, + { version: 49, update: updateToSchemaVersion49 }, - updateToSchemaVersion50, - updateToSchemaVersion51, - updateToSchemaVersion52, - updateToSchemaVersion53, - updateToSchemaVersion54, - updateToSchemaVersion55, - updateToSchemaVersion56, - updateToSchemaVersion57, - updateToSchemaVersion58, - updateToSchemaVersion59, + { version: 50, update: updateToSchemaVersion50 }, + { version: 51, update: updateToSchemaVersion51 }, + { version: 52, update: updateToSchemaVersion52 }, + { version: 53, update: updateToSchemaVersion53 }, + { version: 54, update: updateToSchemaVersion54 }, + { version: 55, update: updateToSchemaVersion55 }, + { version: 56, update: updateToSchemaVersion56 }, + { version: 57, update: updateToSchemaVersion57 }, + { version: 58, update: updateToSchemaVersion58 }, + { version: 59, update: updateToSchemaVersion59 }, - updateToSchemaVersion60, - updateToSchemaVersion61, - updateToSchemaVersion62, - updateToSchemaVersion63, - updateToSchemaVersion64, - updateToSchemaVersion65, - updateToSchemaVersion66, - updateToSchemaVersion67, - updateToSchemaVersion68, - updateToSchemaVersion69, + { version: 60, update: updateToSchemaVersion60 }, + { version: 61, update: updateToSchemaVersion61 }, + { version: 62, update: updateToSchemaVersion62 }, + { version: 63, update: updateToSchemaVersion63 }, + { version: 64, update: updateToSchemaVersion64 }, + { version: 65, update: updateToSchemaVersion65 }, + { version: 66, update: updateToSchemaVersion66 }, + { version: 67, update: updateToSchemaVersion67 }, + { version: 68, update: updateToSchemaVersion68 }, + { version: 69, update: updateToSchemaVersion69 }, - updateToSchemaVersion70, - updateToSchemaVersion71, - updateToSchemaVersion72, - updateToSchemaVersion73, - updateToSchemaVersion74, - updateToSchemaVersion75, - updateToSchemaVersion76, - updateToSchemaVersion77, - updateToSchemaVersion78, - updateToSchemaVersion79, + { version: 70, update: updateToSchemaVersion70 }, + { version: 71, update: updateToSchemaVersion71 }, + { version: 72, update: updateToSchemaVersion72 }, + { version: 73, update: updateToSchemaVersion73 }, + { version: 74, update: updateToSchemaVersion74 }, + { version: 75, update: updateToSchemaVersion75 }, + { version: 76, update: updateToSchemaVersion76 }, + { version: 77, update: updateToSchemaVersion77 }, + { version: 78, update: updateToSchemaVersion78 }, + { version: 79, update: updateToSchemaVersion79 }, - updateToSchemaVersion80, - updateToSchemaVersion81, - updateToSchemaVersion82, - updateToSchemaVersion83, - updateToSchemaVersion84, - updateToSchemaVersion85, - updateToSchemaVersion86, - updateToSchemaVersion87, - updateToSchemaVersion88, - updateToSchemaVersion89, + { version: 80, update: updateToSchemaVersion80 }, + { version: 81, update: updateToSchemaVersion81 }, + { version: 82, update: updateToSchemaVersion82 }, + { version: 83, update: updateToSchemaVersion83 }, + { version: 84, update: updateToSchemaVersion84 }, + { version: 85, update: updateToSchemaVersion85 }, + { version: 86, update: updateToSchemaVersion86 }, + { version: 87, update: updateToSchemaVersion87 }, + { version: 88, update: updateToSchemaVersion88 }, + { version: 89, update: updateToSchemaVersion89 }, - updateToSchemaVersion90, - updateToSchemaVersion91, + { version: 90, update: updateToSchemaVersion90 }, + { version: 91, update: updateToSchemaVersion91 }, // From here forward, all migrations should be multiples of 10 - updateToSchemaVersion920, - updateToSchemaVersion930, - updateToSchemaVersion940, - updateToSchemaVersion950, - updateToSchemaVersion960, - updateToSchemaVersion970, - updateToSchemaVersion980, - updateToSchemaVersion990, + { version: 920, update: updateToSchemaVersion920 }, + { version: 930, update: updateToSchemaVersion930 }, + { version: 940, update: updateToSchemaVersion940 }, + { version: 950, update: updateToSchemaVersion950 }, + { version: 960, update: updateToSchemaVersion960 }, + { version: 970, update: updateToSchemaVersion970 }, + { version: 980, update: updateToSchemaVersion980 }, + { version: 990, update: updateToSchemaVersion990 }, - updateToSchemaVersion1000, - updateToSchemaVersion1010, - updateToSchemaVersion1020, - updateToSchemaVersion1030, - updateToSchemaVersion1040, - updateToSchemaVersion1050, - updateToSchemaVersion1060, - updateToSchemaVersion1070, - updateToSchemaVersion1080, - updateToSchemaVersion1090, + { version: 1000, update: updateToSchemaVersion1000 }, + { version: 1010, update: updateToSchemaVersion1010 }, + { version: 1020, update: updateToSchemaVersion1020 }, + { version: 1030, update: updateToSchemaVersion1030 }, + { version: 1040, update: updateToSchemaVersion1040 }, + { version: 1050, update: updateToSchemaVersion1050 }, + { version: 1060, update: updateToSchemaVersion1060 }, + { version: 1070, update: updateToSchemaVersion1070 }, + { version: 1080, update: updateToSchemaVersion1080 }, + { version: 1090, update: updateToSchemaVersion1090 }, - updateToSchemaVersion1100, - updateToSchemaVersion1110, - updateToSchemaVersion1120, - updateToSchemaVersion1130, - updateToSchemaVersion1140, - updateToSchemaVersion1150, - updateToSchemaVersion1160, - updateToSchemaVersion1170, - updateToSchemaVersion1180, - updateToSchemaVersion1190, + { version: 1100, update: updateToSchemaVersion1100 }, + { version: 1110, update: updateToSchemaVersion1110 }, + { version: 1120, update: updateToSchemaVersion1120 }, + { version: 1130, update: updateToSchemaVersion1130 }, + { version: 1140, update: updateToSchemaVersion1140 }, + { version: 1150, update: updateToSchemaVersion1150 }, + { version: 1160, update: updateToSchemaVersion1160 }, + { version: 1170, update: updateToSchemaVersion1170 }, + { version: 1180, update: updateToSchemaVersion1180 }, + { version: 1190, update: updateToSchemaVersion1190 }, - updateToSchemaVersion1200, - updateToSchemaVersion1210, - updateToSchemaVersion1220, - updateToSchemaVersion1230, - updateToSchemaVersion1240, - updateToSchemaVersion1250, - updateToSchemaVersion1260, - updateToSchemaVersion1270, - updateToSchemaVersion1280, - updateToSchemaVersion1290, + { version: 1200, update: updateToSchemaVersion1200 }, + { version: 1210, update: updateToSchemaVersion1210 }, + { version: 1220, update: updateToSchemaVersion1220 }, + { version: 1230, update: updateToSchemaVersion1230 }, + { version: 1240, update: updateToSchemaVersion1240 }, + { version: 1250, update: updateToSchemaVersion1250 }, + { version: 1260, update: updateToSchemaVersion1260 }, + { version: 1270, update: updateToSchemaVersion1270 }, + { version: 1280, update: updateToSchemaVersion1280 }, + { version: 1290, update: updateToSchemaVersion1290 }, - updateToSchemaVersion1300, - updateToSchemaVersion1310, - updateToSchemaVersion1320, - updateToSchemaVersion1330, - updateToSchemaVersion1340, - updateToSchemaVersion1350, - updateToSchemaVersion1360, - updateToSchemaVersion1370, - updateToSchemaVersion1380, - updateToSchemaVersion1390, + { version: 1300, update: updateToSchemaVersion1300 }, + { version: 1310, update: updateToSchemaVersion1310 }, + { version: 1320, update: updateToSchemaVersion1320 }, + { version: 1330, update: updateToSchemaVersion1330 }, + { version: 1340, update: updateToSchemaVersion1340 }, + { version: 1350, update: updateToSchemaVersion1350 }, + { version: 1360, update: updateToSchemaVersion1360 }, + { version: 1370, update: updateToSchemaVersion1370 }, + { version: 1380, update: updateToSchemaVersion1380 }, + { version: 1390, update: updateToSchemaVersion1390 }, - updateToSchemaVersion1400, - updateToSchemaVersion1410, - updateToSchemaVersion1420, + { version: 1400, update: updateToSchemaVersion1400 }, + { version: 1410, update: updateToSchemaVersion1410 }, + { version: 1420, update: updateToSchemaVersion1420 }, ]; export class DBVersionFromFutureError extends Error { @@ -2154,6 +1628,18 @@ export function updateSchema(db: WritableDB, logger: LoggerType): void { const startingVersion = getUserVersion(db); const schemaVersion = getSchemaVersion(db); + const MAX_VERSION = SCHEMA_VERSIONS[SCHEMA_VERSIONS.length - 1].version; + + for (let i = 1; i < SCHEMA_VERSIONS.length; i += 1) { + const prev = SCHEMA_VERSIONS[i - 1].version; + const next = SCHEMA_VERSIONS[i].version; + if (prev >= next) { + throw new Error( + `Migration versions are not monotonic: ${prev} >= ${next}` + ); + } + } + logger.info( 'updateSchema:\n', ` Current user_version: ${startingVersion};\n`, @@ -2170,10 +1656,40 @@ export function updateSchema(db: WritableDB, logger: LoggerType): void { ); } - for (let index = 0, max = SCHEMA_VERSIONS.length; index < max; index += 1) { - const runSchemaUpdate = SCHEMA_VERSIONS[index]; + // Try to run as many migrations as possible in a single transaction, but if + // one requires vacuum - commit, vacuum, and continue from where we stopped + let i = 0; + while (i < SCHEMA_VERSIONS.length) { + // eslint-disable-next-line no-loop-func + const needsVacuum = db.transaction(() => { + for (; i < SCHEMA_VERSIONS.length; i += 1) { + const { version, update } = SCHEMA_VERSIONS[i]; + if (version <= startingVersion) { + continue; + } - runSchemaUpdate(startingVersion, db, logger); + const schemaLogger = logger.child(`updateSchema(${version})`); + const result = update(db, schemaLogger, startingVersion); + if (result === 'vacuum') { + schemaLogger.info('success, needs vacuum'); + + db.pragma(`user_version = ${version}`); + i += 1; + return true; + } + + schemaLogger.info('success'); + } + + db.pragma(`user_version = ${MAX_VERSION}`); + return false; + })(); + + if (needsVacuum) { + logger.info('running vacuum'); + db.exec('VACUUM'); + logger.info('done running vacuum'); + } } DataWriter.ensureMessageInsertTriggersAreEnabled(db); diff --git a/ts/sql/sqlLogger.ts b/ts/sql/sqlLogger.ts index 73b56593ace..cde06aa26e8 100644 --- a/ts/sql/sqlLogger.ts +++ b/ts/sql/sqlLogger.ts @@ -8,43 +8,50 @@ import type { WrappedWorkerLogEntry, WrappedWorkerResponse } from './main'; import { consoleLogger } from '../util/consoleLogger'; import { strictAssert } from '../util/assert'; -const log = ( - level: WrappedWorkerLogEntry['level'], - args: Array -): void => { - if (parentPort) { - const wrappedResponse: WrappedWorkerResponse = { - type: 'log', - level, - args, - }; - parentPort.postMessage(wrappedResponse); - } else { - strictAssert(process.env.NODE_ENV === 'test', 'must be test environment'); - consoleLogger[level](format(...args)); - } -}; +class SQLLogger { + #msgPrefix: string; + + constructor(msgPrefix: string) { + this.#msgPrefix = msgPrefix; + } -export const sqlLogger: LoggerType = { fatal(...args: Array) { - log('fatal', args); - }, + this.#log('fatal', args); + } error(...args: Array) { - log('error', args); - }, + this.#log('error', args); + } warn(...args: Array) { - log('warn', args); - }, + this.#log('warn', args); + } info(...args: Array) { - log('info', args); - }, + this.#log('info', args); + } debug(...args: Array) { - log('debug', args); - }, + this.#log('debug', args); + } trace(...args: Array) { - log('trace', args); - }, - child() { - return sqlLogger; - }, -}; + this.#log('trace', args); + } + child(subsystem: string) { + return new SQLLogger(`${this.#msgPrefix}[${subsystem}] `); + } + + #log(level: WrappedWorkerLogEntry['level'], args: Array): void { + if (parentPort) { + const [fmt, ...rest] = args; + + const wrappedResponse: WrappedWorkerResponse = { + type: 'log', + level, + args: ([this.#msgPrefix + fmt] as Array).concat(rest), + }; + parentPort.postMessage(wrappedResponse); + } else { + strictAssert(process.env.NODE_ENV === 'test', 'must be test environment'); + consoleLogger[level](this.#msgPrefix + format(...args)); + } + } +} + +export const sqlLogger: LoggerType = new SQLLogger(''); diff --git a/ts/test-node/sql/helpers.ts b/ts/test-node/sql/helpers.ts index 373d21bb0b0..1315a120663 100644 --- a/ts/test-node/sql/helpers.ts +++ b/ts/test-node/sql/helpers.ts @@ -17,16 +17,24 @@ export function createDB(): WritableDB { export function updateToVersion(db: WritableDB, version: number): void { const startVersion = db.pragma('user_version', { simple: true }) as number; + if (startVersion === version) { + return; + } const silentLogger = { ...consoleLogger, info: noop, }; - for (const run of SCHEMA_VERSIONS) { - run(startVersion, db, silentLogger); + for (const { version: currentVersion, update } of SCHEMA_VERSIONS) { + if (currentVersion <= startVersion) { + continue; + } - const currentVersion = db.pragma('user_version', { simple: true }); + db.transaction(() => { + update(db, silentLogger, startVersion); + db.pragma(`user_version = ${version}`); + })(); if (currentVersion === version) { return; diff --git a/ts/util/consoleLogger.ts b/ts/util/consoleLogger.ts index 0d4d4aa8910..b99590de1d9 100644 --- a/ts/util/consoleLogger.ts +++ b/ts/util/consoleLogger.ts @@ -24,7 +24,7 @@ export const consoleLogger: LoggerType = { console.log(...args); }, child() { - throw new Error('Not supported'); + return consoleLogger; }, };