// Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { Database, RunResult } from '@signalapp/better-sqlite3'; import type { LoggerType } from '../../types/Logging'; import { sql } from '../util'; import type { PniString } from '../../types/ServiceId'; 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) { return; } db.transaction(() => { // 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(); logger.info(`updateToSchemaVersion91: Found ${totalKeys} keys`); if (totalKeys < 1000) { db.pragma('user_version = 91'); return; } // Grab our PNI let pni: PniString; const pniJson = db .prepare("SELECT json FROM items WHERE id IS 'pni'") .pluck() .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; } // Grab PNI-specific count 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`); // Create index to help us with all these queries 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'); // Fetch 500th-oldest timestamp for PNI 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( `updateToSchemaVersion91: Found 500th-oldest timestamp: ${oldBoundary}` ); // Fetch 500th-newest timestamp 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}` ); // 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} AND createdAt < ${newBoundary} 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!'); }