Fix legacy call-history messages without a callId
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
parent
6f0401b847
commit
ef0a3de636
17 changed files with 831 additions and 426 deletions
|
@ -1953,7 +1953,6 @@ function saveMessageSync(
|
|||
sourceServiceId,
|
||||
sourceDevice,
|
||||
storyId,
|
||||
callId,
|
||||
type,
|
||||
readStatus,
|
||||
expireTimer,
|
||||
|
@ -2011,7 +2010,6 @@ function saveMessageSync(
|
|||
sourceServiceId: sourceServiceId || null,
|
||||
sourceDevice: sourceDevice || null,
|
||||
storyId: storyId || null,
|
||||
callId: callId || null,
|
||||
type: type || null,
|
||||
readStatus: readStatus ?? null,
|
||||
seenStatus: seenStatus ?? SeenStatus.NotApplicable,
|
||||
|
@ -2044,7 +2042,6 @@ function saveMessageSync(
|
|||
sourceServiceId = $sourceServiceId,
|
||||
sourceDevice = $sourceDevice,
|
||||
storyId = $storyId,
|
||||
callId = $callId,
|
||||
type = $type,
|
||||
readStatus = $readStatus,
|
||||
seenStatus = $seenStatus
|
||||
|
@ -2090,7 +2087,6 @@ function saveMessageSync(
|
|||
sourceServiceId,
|
||||
sourceDevice,
|
||||
storyId,
|
||||
callId,
|
||||
type,
|
||||
readStatus,
|
||||
seenStatus
|
||||
|
@ -2117,7 +2113,6 @@ function saveMessageSync(
|
|||
$sourceServiceId,
|
||||
$sourceDevice,
|
||||
$storyId,
|
||||
$callId,
|
||||
$type,
|
||||
$readStatus,
|
||||
$seenStatus
|
||||
|
|
|
@ -1,215 +0,0 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { Database } from '@signalapp/better-sqlite3';
|
||||
|
||||
import { callIdFromEra } from '@signalapp/ringrtc';
|
||||
import Long from 'long';
|
||||
|
||||
import type { LoggerType } from '../../types/Logging';
|
||||
import { sql } from '../util';
|
||||
import { getOurUuid } from './41-uuid-keys';
|
||||
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||
import {
|
||||
DirectCallStatus,
|
||||
CallDirection,
|
||||
CallType,
|
||||
GroupCallStatus,
|
||||
callHistoryDetailsSchema,
|
||||
} from '../../types/CallDisposition';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
|
||||
export default function updateToSchemaVersion87(
|
||||
currentVersion: number,
|
||||
db: Database,
|
||||
logger: LoggerType
|
||||
): void {
|
||||
if (currentVersion >= 87) {
|
||||
return;
|
||||
}
|
||||
|
||||
db.transaction(() => {
|
||||
const ourUuid = getOurUuid(db);
|
||||
|
||||
const [createTable] = sql`
|
||||
DROP TABLE IF EXISTS callsHistory;
|
||||
|
||||
CREATE TABLE 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
|
||||
);
|
||||
|
||||
CREATE INDEX callsHistory_order on callsHistory (timestamp DESC);
|
||||
CREATE INDEX callsHistory_byConversation ON callsHistory (peerId);
|
||||
-- For 'getCallHistoryGroupData':
|
||||
-- This index should target the subqueries for 'possible_parent' and 'possible_children'
|
||||
CREATE INDEX callsHistory_callAndGroupInfo_optimize on callsHistory (
|
||||
direction,
|
||||
peerId,
|
||||
timestamp DESC,
|
||||
status
|
||||
);
|
||||
`;
|
||||
|
||||
db.exec(createTable);
|
||||
|
||||
const [selectQuery] = sql`
|
||||
SELECT * FROM messages
|
||||
WHERE type = 'call-history'
|
||||
AND callId IS NOT NULL; -- Older messages don't have callId
|
||||
`;
|
||||
|
||||
const rows = db.prepare(selectQuery).all();
|
||||
|
||||
const uniqueConstraint = new Set();
|
||||
|
||||
for (const row of rows) {
|
||||
const json = JSON.parse(row.json);
|
||||
const details = json.callHistoryDetails;
|
||||
|
||||
const { conversationId: peerId } = row;
|
||||
const { callMode } = details;
|
||||
|
||||
let callId: string;
|
||||
let type: CallType;
|
||||
let direction: CallDirection;
|
||||
let status: GroupCallStatus | DirectCallStatus;
|
||||
let timestamp: number;
|
||||
let ringerId: string | null = null;
|
||||
|
||||
if (details.callMode === CallMode.Direct) {
|
||||
callId = details.callId;
|
||||
type = details.wasVideoCall ? CallType.Video : CallType.Audio;
|
||||
direction = details.wasIncoming
|
||||
? CallDirection.Incoming
|
||||
: CallDirection.Outgoing;
|
||||
if (details.acceptedTime != null) {
|
||||
status = DirectCallStatus.Accepted;
|
||||
} else {
|
||||
status = details.wasDeclined
|
||||
? DirectCallStatus.Declined
|
||||
: DirectCallStatus.Missed;
|
||||
}
|
||||
timestamp = details.endedTime ?? details.acceptedTime ?? null;
|
||||
} else if (details.callMode === CallMode.Group) {
|
||||
callId = Long.fromValue(callIdFromEra(details.eraId)).toString();
|
||||
type = CallType.Group;
|
||||
direction =
|
||||
details.creatorUuid === ourUuid
|
||||
? CallDirection.Outgoing
|
||||
: CallDirection.Incoming;
|
||||
status = GroupCallStatus.GenericGroupCall;
|
||||
timestamp = details.startedTime;
|
||||
ringerId = details.creatorUuid;
|
||||
} else {
|
||||
logger.error(
|
||||
`updateToSchemaVersion87: unknown callMode: ${details.callMode}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (callId == null) {
|
||||
logger.error(
|
||||
"updateToSchemaVersion87: callId doesn't exist, too old, skipping"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const callHistory: CallHistoryDetails = {
|
||||
callId,
|
||||
peerId,
|
||||
ringerId,
|
||||
mode: callMode,
|
||||
type,
|
||||
direction,
|
||||
status,
|
||||
timestamp,
|
||||
};
|
||||
|
||||
const result = callHistoryDetailsSchema.safeParse(callHistory);
|
||||
|
||||
if (!result.success) {
|
||||
logger.error(
|
||||
`updateToSchemaVersion87: invalid callHistoryDetails (error: ${JSON.stringify(
|
||||
result.error.format()
|
||||
)}, input: ${JSON.stringify(json)}, output: ${JSON.stringify(
|
||||
callHistory
|
||||
)}))`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// We need to ensure a call with the same callId and peerId doesn't get
|
||||
// inserted twice because of the unique constraint on the table.
|
||||
const uniqueKey = `${callId} -> ${peerId}`;
|
||||
if (uniqueConstraint.has(uniqueKey)) {
|
||||
logger.error(
|
||||
`updateToSchemaVersion87: duplicate callId/peerId pair (${uniqueKey})`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
uniqueConstraint.add(uniqueKey);
|
||||
|
||||
const [insertQuery, insertParams] = sql`
|
||||
INSERT 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 [updateQuery, updateParams] = sql`
|
||||
UPDATE messages
|
||||
SET json = JSON_PATCH(json, ${JSON.stringify({
|
||||
callHistoryDetails: null, // delete
|
||||
callId,
|
||||
})})
|
||||
WHERE id = ${row.id}
|
||||
`;
|
||||
|
||||
db.prepare(updateQuery).run(updateParams);
|
||||
}
|
||||
|
||||
const [updateMessagesTable] = sql`
|
||||
DROP INDEX IF EXISTS messages_call;
|
||||
|
||||
ALTER TABLE messages
|
||||
DROP COLUMN callId;
|
||||
ALTER TABLE messages
|
||||
ADD COLUMN callId TEXT;
|
||||
ALTER TABLE messages
|
||||
DROP COLUMN callMode;
|
||||
`;
|
||||
|
||||
db.exec(updateMessagesTable);
|
||||
|
||||
db.pragma('user_version = 87');
|
||||
})();
|
||||
|
||||
logger.info('updateToSchemaVersion87: success!');
|
||||
}
|
369
ts/sql/migrations/89-call-history.ts
Normal file
369
ts/sql/migrations/89-call-history.ts
Normal file
|
@ -0,0 +1,369 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { Database } from '@signalapp/better-sqlite3';
|
||||
import { callIdFromEra } from '@signalapp/ringrtc';
|
||||
import Long from 'long';
|
||||
import { v4 as generateUuid } from 'uuid';
|
||||
import type { SetOptional } from 'type-fest';
|
||||
import type { LoggerType } from '../../types/Logging';
|
||||
import { jsonToObject, sql } from '../util';
|
||||
import { getOurUuid } from './41-uuid-keys';
|
||||
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||
import {
|
||||
DirectCallStatus,
|
||||
CallDirection,
|
||||
CallType,
|
||||
GroupCallStatus,
|
||||
callHistoryDetailsSchema,
|
||||
} from '../../types/CallDisposition';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import type { MessageType, ConversationType } from '../Interface';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import type { ServiceIdString } from '../../types/ServiceId';
|
||||
import { isAciString } from '../../types/ServiceId';
|
||||
|
||||
// Legacy type for calls that never had a call id
|
||||
type DirectCallHistoryDetailsType = {
|
||||
callId?: string;
|
||||
callMode: CallMode.Direct;
|
||||
wasIncoming: boolean;
|
||||
wasVideoCall: boolean;
|
||||
wasDeclined: boolean;
|
||||
acceptedTime?: number;
|
||||
endedTime?: number;
|
||||
};
|
||||
type GroupCallHistoryDetailsType = {
|
||||
callMode: CallMode.Group;
|
||||
creatorUuid: string;
|
||||
eraId: string;
|
||||
startedTime: number;
|
||||
};
|
||||
export type CallHistoryDetailsType =
|
||||
| DirectCallHistoryDetailsType
|
||||
| GroupCallHistoryDetailsType;
|
||||
|
||||
export type CallHistoryDetailsFromDiskType =
|
||||
// old messages weren't set with a callMode
|
||||
| SetOptional<DirectCallHistoryDetailsType, 'callMode'>
|
||||
| SetOptional<GroupCallHistoryDetailsType, 'callMode'>;
|
||||
|
||||
export type MessageWithCallHistoryDetails = MessageType & {
|
||||
callHistoryDetails: CallHistoryDetailsFromDiskType;
|
||||
};
|
||||
|
||||
function upcastCallHistoryDetailsFromDiskType(
|
||||
callDetails: CallHistoryDetailsFromDiskType
|
||||
): CallHistoryDetailsType {
|
||||
if (callDetails.callMode === CallMode.Direct) {
|
||||
return callDetails as DirectCallHistoryDetailsType;
|
||||
}
|
||||
if (callDetails.callMode === CallMode.Group) {
|
||||
return callDetails as GroupCallHistoryDetailsType;
|
||||
}
|
||||
// Some very old calls don't have a callMode, so we need to infer it from the
|
||||
// other fields. This is a best effort attempt to make sure we at least have
|
||||
// enough data to form the call history entry correctly.
|
||||
if (
|
||||
Object.hasOwn(callDetails, 'wasIncoming') &&
|
||||
Object.hasOwn(callDetails, 'wasVideoCall')
|
||||
) {
|
||||
return {
|
||||
callMode: CallMode.Direct,
|
||||
...callDetails,
|
||||
} as DirectCallHistoryDetailsType;
|
||||
}
|
||||
if (
|
||||
Object.hasOwn(callDetails, 'eraId') &&
|
||||
Object.hasOwn(callDetails, 'startedTime')
|
||||
) {
|
||||
return {
|
||||
callMode: CallMode.Group,
|
||||
...callDetails,
|
||||
} as GroupCallHistoryDetailsType;
|
||||
}
|
||||
throw new Error('Could not determine call mode');
|
||||
}
|
||||
|
||||
function getPeerIdFromConversation(
|
||||
conversation: Pick<ConversationType, 'type' | 'serviceId' | 'groupId'>
|
||||
): string {
|
||||
if (conversation.type === 'private') {
|
||||
strictAssert(
|
||||
isAciString(conversation.serviceId),
|
||||
'ACI must exist for direct chat'
|
||||
);
|
||||
return conversation.serviceId;
|
||||
}
|
||||
strictAssert(
|
||||
conversation.groupId != null,
|
||||
'groupId must exist for group chat'
|
||||
);
|
||||
return conversation.groupId;
|
||||
}
|
||||
|
||||
function convertLegacyCallDetails(
|
||||
ourUuid: string | undefined,
|
||||
peerId: string,
|
||||
message: MessageType,
|
||||
partialDetails: CallHistoryDetailsFromDiskType
|
||||
): CallHistoryDetails {
|
||||
const details = upcastCallHistoryDetailsFromDiskType(partialDetails);
|
||||
const { callMode: mode } = details;
|
||||
|
||||
let callId: string;
|
||||
let type: CallType;
|
||||
let direction: CallDirection;
|
||||
let status: GroupCallStatus | DirectCallStatus;
|
||||
let timestamp: number;
|
||||
let ringerId: string | null = null;
|
||||
|
||||
strictAssert(mode != null, 'mode must exist');
|
||||
|
||||
if (mode === CallMode.Direct) {
|
||||
// We don't have a callId for older calls, generating a uuid instead
|
||||
callId = details.callId ?? generateUuid();
|
||||
type = details.wasVideoCall ? CallType.Video : CallType.Audio;
|
||||
direction = details.wasIncoming
|
||||
? CallDirection.Incoming
|
||||
: CallDirection.Outgoing;
|
||||
if (details.acceptedTime != null) {
|
||||
status = DirectCallStatus.Accepted;
|
||||
} else {
|
||||
status = details.wasDeclined
|
||||
? DirectCallStatus.Declined
|
||||
: DirectCallStatus.Missed;
|
||||
}
|
||||
timestamp = details.acceptedTime ?? details.endedTime ?? message.timestamp;
|
||||
} else if (mode === CallMode.Group) {
|
||||
callId = Long.fromValue(callIdFromEra(details.eraId)).toString();
|
||||
type = CallType.Group;
|
||||
direction =
|
||||
details.creatorUuid === ourUuid
|
||||
? CallDirection.Outgoing
|
||||
: CallDirection.Incoming;
|
||||
status = GroupCallStatus.GenericGroupCall;
|
||||
timestamp = details.startedTime;
|
||||
ringerId = details.creatorUuid;
|
||||
} else {
|
||||
throw missingCaseError(mode);
|
||||
}
|
||||
|
||||
const callHistory: CallHistoryDetails = {
|
||||
callId,
|
||||
peerId,
|
||||
ringerId,
|
||||
mode,
|
||||
type,
|
||||
direction,
|
||||
status,
|
||||
timestamp,
|
||||
};
|
||||
|
||||
return callHistoryDetailsSchema.parse(callHistory);
|
||||
}
|
||||
|
||||
export default function updateToSchemaVersion89(
|
||||
currentVersion: number,
|
||||
db: Database,
|
||||
logger: LoggerType
|
||||
): void {
|
||||
if (currentVersion >= 89) {
|
||||
return;
|
||||
}
|
||||
|
||||
db.transaction(() => {
|
||||
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.type AS conversationType,
|
||||
conversations.serviceId AS conversationServiceId,
|
||||
conversations.groupId AS conversationGroupId
|
||||
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;
|
||||
conversationType: ConversationType['type'];
|
||||
conversationServiceId: ServiceIdString | undefined;
|
||||
conversationGroupId: string | undefined;
|
||||
};
|
||||
|
||||
const rows: Array<CallHistoryRow> = db.prepare(selectQuery).all();
|
||||
|
||||
for (const row of rows) {
|
||||
const {
|
||||
messageJson,
|
||||
conversationType,
|
||||
conversationServiceId,
|
||||
conversationGroupId,
|
||||
} = row;
|
||||
const message = jsonToObject<MessageWithCallHistoryDetails>(messageJson);
|
||||
const details = message.callHistoryDetails;
|
||||
|
||||
const peerId = getPeerIdFromConversation({
|
||||
type: conversationType,
|
||||
serviceId: conversationServiceId,
|
||||
groupId: conversationGroupId,
|
||||
});
|
||||
|
||||
const callHistory = convertLegacyCallDetails(
|
||||
ourUuid,
|
||||
peerId,
|
||||
message,
|
||||
details
|
||||
);
|
||||
|
||||
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`
|
||||
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);
|
||||
|
||||
db.pragma('user_version = 89');
|
||||
})();
|
||||
|
||||
logger.info('updateToSchemaVersion89: success!');
|
||||
}
|
|
@ -62,8 +62,8 @@ import updateToSchemaVersion83 from './83-mentions';
|
|||
import updateToSchemaVersion84 from './84-all-mentions';
|
||||
import updateToSchemaVersion85 from './85-add-kyber-keys';
|
||||
import updateToSchemaVersion86 from './86-story-replies-index';
|
||||
import updateToSchemaVersion87 from './87-calls-history-table';
|
||||
import updateToSchemaVersion88 from './88-service-ids';
|
||||
import updateToSchemaVersion89 from './89-call-history';
|
||||
|
||||
function updateToSchemaVersion1(
|
||||
currentVersion: number,
|
||||
|
@ -1996,8 +1996,9 @@ export const SCHEMA_VERSIONS = [
|
|||
updateToSchemaVersion84,
|
||||
updateToSchemaVersion85,
|
||||
updateToSchemaVersion86,
|
||||
updateToSchemaVersion87,
|
||||
(_v: number, _i: Database, _l: LoggerType): void => undefined, // version 87 was dropped
|
||||
updateToSchemaVersion88,
|
||||
updateToSchemaVersion89,
|
||||
];
|
||||
|
||||
export function updateSchema(db: Database, logger: LoggerType): void {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue