/* eslint-disable no-unused-expressions */
import * as BetterSqlite3 from '@signalapp/better-sqlite3' ;
import {
ContactsConfigWrapperNode ,
ConvoInfoVolatileWrapperNode ,
UserConfigWrapperNode ,
UserGroupsWrapperNode ,
} from 'libsession_util_nodejs' ;
import { compact , isArray , isEmpty , isFinite , isNumber , isString , map , pick } from 'lodash' ;
import {
CONVERSATION_PRIORITIES ,
ConversationAttributes ,
} from '../../models/conversationAttributes' ;
import { HexKeyPair } from '../../receiver/keypairs' ;
import { fromHexToArray } from '../../session/utils/String' ;
import {
CONFIG_DUMP_TABLE ,
getCommunityInfoFromDBValues ,
getContactInfoFromDBValues ,
getLegacyGroupInfoFromDBValues ,
} from '../../types/sqlSharedTypes' ;
import {
CLOSED_GROUP_V2_KEY_PAIRS_TABLE ,
CONVERSATIONS_TABLE ,
GUARD_NODE_TABLE ,
LAST_HASHES_TABLE ,
MESSAGES_TABLE ,
NODES_FOR_PUBKEY_TABLE ,
OPEN_GROUP_ROOMS_V2_TABLE ,
dropFtsAndTriggers ,
objectToJSON ,
rebuildFtsTable ,
toSqliteBoolean ,
} from '../database_utility' ;
import { getIdentityKeys , sqlNode } from '../sql' ;
import { sleepFor } from '../../session/utils/Promise' ;
// import updateToSessionSchemaVersion33 from './session/migrationV33';
export const hasDebugEnvVariable = Boolean ( process . env . SESSION_DEBUG ) ;
// eslint:disable: quotemark one-variable-per-declaration no-unused-expression
function getSessionSchemaVersion ( db : BetterSqlite3.Database ) {
const result = db
. prepare (
`
SELECT MAX ( version ) as version FROM loki_schema ;
`
)
. get ( ) ;
if ( ! result || ! result . version ) {
return 0 ;
}
return result . version ;
}
function createSessionSchemaTable ( db : BetterSqlite3.Database ) {
db . transaction ( ( ) = > {
db . exec ( `
CREATE TABLE loki_schema (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ,
version INTEGER
) ;
INSERT INTO loki_schema (
version
) values (
0
) ;
` );
} ) ( ) ;
}
const LOKI_SCHEMA_VERSIONS = [
updateToSessionSchemaVersion1 ,
updateToSessionSchemaVersion2 ,
updateToSessionSchemaVersion3 ,
updateToSessionSchemaVersion4 ,
updateToSessionSchemaVersion5 ,
updateToSessionSchemaVersion6 ,
updateToSessionSchemaVersion7 ,
updateToSessionSchemaVersion8 ,
updateToSessionSchemaVersion9 ,
updateToSessionSchemaVersion10 ,
updateToSessionSchemaVersion11 ,
updateToSessionSchemaVersion12 ,
updateToSessionSchemaVersion13 ,
updateToSessionSchemaVersion14 ,
updateToSessionSchemaVersion15 ,
updateToSessionSchemaVersion16 ,
updateToSessionSchemaVersion17 ,
updateToSessionSchemaVersion18 ,
updateToSessionSchemaVersion19 ,
updateToSessionSchemaVersion20 ,
updateToSessionSchemaVersion21 ,
updateToSessionSchemaVersion22 ,
updateToSessionSchemaVersion23 ,
updateToSessionSchemaVersion24 ,
updateToSessionSchemaVersion25 ,
updateToSessionSchemaVersion26 ,
updateToSessionSchemaVersion27 ,
updateToSessionSchemaVersion28 ,
updateToSessionSchemaVersion29 ,
updateToSessionSchemaVersion30 ,
updateToSessionSchemaVersion31 ,
updateToSessionSchemaVersion32 ,
// updateToSessionSchemaVersion33,
] ;
function updateToSessionSchemaVersion1 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 1 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( `
ALTER TABLE $ { MESSAGES_TABLE }
ADD COLUMN serverId INTEGER ;
CREATE TABLE servers (
serverUrl STRING PRIMARY KEY ASC ,
token TEXT
) ;
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion2 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 2 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( `
CREATE TABLE pairingAuthorisations (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ,
primaryDevicePubKey VARCHAR ( 255 ) ,
secondaryDevicePubKey VARCHAR ( 255 ) ,
isGranted BOOLEAN ,
json TEXT ,
UNIQUE ( primaryDevicePubKey , secondaryDevicePubKey )
) ;
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion3 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 3 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( `
CREATE TABLE $ { GUARD_NODE_TABLE } (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ,
ed25519PubKey VARCHAR ( 64 )
) ;
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion4 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 4 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( `
DROP TABLE $ { LAST_HASHES_TABLE } ;
CREATE TABLE $ { LAST_HASHES_TABLE } (
id TEXT ,
snode TEXT ,
hash TEXT ,
expiresAt INTEGER ,
PRIMARY KEY ( id , snode )
) ;
-- Add senderIdentity field to unprocessed needed for medium size groups
ALTER TABLE unprocessed ADD senderIdentity TEXT ;
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion5 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 5 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( `
CREATE TABLE $ { NODES_FOR_PUBKEY_TABLE } (
pubkey TEXT PRIMARY KEY ,
json TEXT
) ;
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion6 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 6 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( `
-- Remove RSS Feed conversations
DELETE FROM $ { CONVERSATIONS_TABLE } WHERE
type = 'group' AND
id LIKE 'rss://%' ;
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion7 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 7 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( `
-- Remove multi device data
DELETE FROM pairingAuthorisations ;
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion8 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 8 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( `
ALTER TABLE $ { MESSAGES_TABLE }
ADD COLUMN serverTimestamp INTEGER ;
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion9 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 9 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
const rows = db
. prepare (
` SELECT * FROM ${ CONVERSATIONS_TABLE } WHERE
type = 'group' AND
id LIKE '__textsecure_group__!%' ;
`
)
. all ( ) ;
const conversationIdRows = db
. prepare ( ` SELECT id FROM ${ CONVERSATIONS_TABLE } ORDER BY id ASC; ` )
. all ( ) ;
const allOldConversationIds = map ( conversationIdRows , row = > row . id ) ;
rows . forEach ( o = > {
const oldId = o . id ;
const newId = oldId . replace ( '__textsecure_group__!' , '' ) ;
console . log ( ` migrating conversation, ${ oldId } to ${ newId } ` ) ;
if ( allOldConversationIds . includes ( newId ) ) {
console . log (
'Found a duplicate conversation after prefix removing. We need to take care of it'
) ;
// We have another conversation with the same future name.
// We decided to keep only the conversation with the higher number of messages
const countMessagesOld = sqlNode . getMessagesCountByConversation ( oldId , db ) ;
const countMessagesNew = sqlNode . getMessagesCountByConversation ( newId , db ) ;
console . log ( ` countMessagesOld: ${ countMessagesOld } , countMessagesNew: ${ countMessagesNew } ` ) ;
const deleteId = countMessagesOld > countMessagesNew ? newId : oldId ;
db . prepare ( ` DELETE FROM ${ CONVERSATIONS_TABLE } WHERE id = $ deleteId; ` ) . run ( { deleteId } ) ;
}
const morphedObject = {
. . . o ,
id : newId ,
} ;
db . prepare (
` UPDATE ${ CONVERSATIONS_TABLE } SET
id = $newId ,
json = $json
WHERE id = $oldId ; `
) . run ( {
newId ,
json : objectToJSON ( morphedObject ) ,
oldId ,
} ) ;
} ) ;
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion10 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 10 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( `
CREATE TABLE $ { CLOSED_GROUP_V2_KEY_PAIRS_TABLE } (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ,
groupPublicKey TEXT ,
timestamp NUMBER ,
json TEXT
) ;
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion11 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 11 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
function remove05PrefixFromStringIfNeeded ( str : string ) {
if ( str . length === 66 && str . startsWith ( '05' ) ) {
return str . substr ( 2 ) ;
}
return str ;
}
db . transaction ( ( ) = > {
// the migration is called only once, so all current groups not being open groups are v1 closed group.
const allClosedGroupV1Ids = db
. prepare (
` SELECT id FROM ${ CONVERSATIONS_TABLE } WHERE
type = 'group' AND
id NOT LIKE 'publicChat:%' ; `
)
. all ( )
. map ( m = > m . id ) as Array < string > ;
allClosedGroupV1Ids . forEach ( groupV1Id = > {
try {
console . log ( 'Migrating closed group v1 to v2: pubkey' , groupV1Id ) ;
const groupV1IdentityKey = sqlNode . getIdentityKeyById ( groupV1Id , db ) ;
if ( ! groupV1IdentityKey ) {
return ;
}
const encryptionPubKeyWithoutPrefix = remove05PrefixFromStringIfNeeded (
groupV1IdentityKey . id
) ;
// Note:
// this is what we get from getIdentityKeyById:
// {
// id: string;
// secretKey?: string;
// }
// and this is what we want saved in db:
// {
// publicHex: string; // without prefix
// privateHex: string;
// }
const keyPair = {
publicHex : encryptionPubKeyWithoutPrefix ,
privateHex : groupV1IdentityKey.secretKey ,
} ;
sqlNode . addClosedGroupEncryptionKeyPair ( groupV1Id , keyPair , db ) ;
} catch ( e ) {
console . error ( e ) ;
}
} ) ;
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion12 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 12 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( `
CREATE TABLE $ { OPEN_GROUP_ROOMS_V2_TABLE } (
serverUrl TEXT NOT NULL ,
roomId TEXT NOT NULL ,
conversationId TEXT ,
json TEXT ,
PRIMARY KEY ( serverUrl , roomId )
) ;
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion13 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 13 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
// Clear any already deleted db entries.
// secure_delete = ON will make sure next deleted entries are overwritten with 0 right away
db . transaction ( ( ) = > {
db . pragma ( 'secure_delete = ON' ) ;
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion14 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 14 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( `
DROP TABLE IF EXISTS servers ;
DROP TABLE IF EXISTS sessions ;
DROP TABLE IF EXISTS preKeys ;
DROP TABLE IF EXISTS contactPreKeys ;
DROP TABLE IF EXISTS contactSignedPreKeys ;
DROP TABLE IF EXISTS signedPreKeys ;
DROP TABLE IF EXISTS senderKeys ;
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion15 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 15 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( `
DROP TABLE pairingAuthorisations ;
DROP TRIGGER IF EXISTS messages_on_delete ;
DROP TRIGGER IF EXISTS messages_on_update ;
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion16 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 16 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( `
ALTER TABLE $ { MESSAGES_TABLE } ADD COLUMN serverHash TEXT ;
ALTER TABLE $ { MESSAGES_TABLE } ADD COLUMN isDeleted BOOLEAN ;
CREATE INDEX messages_serverHash ON $ { MESSAGES_TABLE } (
serverHash
) WHERE serverHash IS NOT NULL ;
CREATE INDEX messages_isDeleted ON $ { MESSAGES_TABLE } (
isDeleted
) WHERE isDeleted IS NOT NULL ;
ALTER TABLE unprocessed ADD serverHash TEXT ;
CREATE INDEX messages_messageHash ON unprocessed (
serverHash
) WHERE serverHash IS NOT NULL ;
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion17 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 17 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( `
UPDATE $ { CONVERSATIONS_TABLE } SET
json = json_set ( json , '$.isApproved' , 1 )
` );
// remove the moderators field. As it was only used for opengroups a long time ago and whatever is there is probably unused
db . exec ( `
UPDATE $ { CONVERSATIONS_TABLE } SET
json = json_remove ( json , '$.moderators' , '$.dataMessage' , '$.accessKey' , '$.profileSharing' , '$.sessionRestoreSeen' )
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion18 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 18 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
// Dropping all pre-existing schema relating to message searching.
// Recreating the full text search and related triggers
db . transaction ( ( ) = > {
dropFtsAndTriggers ( db ) ;
rebuildFtsTable ( db ) ;
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion19 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 19 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( `
DROP INDEX messages_schemaVersion ;
ALTER TABLE $ { MESSAGES_TABLE } DROP COLUMN schemaVersion ;
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion20 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 20 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
// First we want to drop the column friendRequestStatus if it is there, otherwise the transaction fails
const rows = db . pragma ( ` table_info( ${ CONVERSATIONS_TABLE } ); ` ) ;
if ( rows . some ( ( m : any ) = > m . name === 'friendRequestStatus' ) ) {
console . info ( 'found column friendRequestStatus. Dropping it' ) ;
db . exec ( ` ALTER TABLE ${ CONVERSATIONS_TABLE } DROP COLUMN friendRequestStatus; ` ) ;
}
// disable those updates as sqlNode.saveConversation will break if called without the right type of arguments.
// and when called during a migration we won't have the expected arguments. Plus, this migration is almost a year old already
// looking for all private conversations, with a nickname set
// const rowsToUpdate = db
// .prepare(
// `SELECT * FROM ${CONVERSATIONS_TABLE} WHERE type = 'private' AND (name IS NULL or name = '') AND json_extract(json, '$.nickname') <> '';`
// )
// .all();
// (rowsToUpdate || []).forEach(r => {
// const obj = jsonToObject(r.json);
// // obj.profile.displayName is the display as this user set it.
// if (obj?.nickname?.length && obj?.profile?.displayName?.length) {
// // this one has a nickname set, but name is unset, set it to the displayName in the lokiProfile if it's exisitng
// obj.name = obj.profile.displayName;
// sqlNode.saveConversation(obj as ConversationAttributes, db);
// }
// });
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion21 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 21 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( `
UPDATE $ { CONVERSATIONS_TABLE } SET
json = json_set ( json , '$.didApproveMe' , 1 , '$.isApproved' , 1 )
WHERE type = 'private' ;
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion22 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 22 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( ` DROP INDEX messages_duplicate_check; ` ) ;
db . exec ( `
ALTER TABLE $ { MESSAGES_TABLE } DROP sourceDevice ;
` );
db . exec ( `
ALTER TABLE unprocessed DROP sourceDevice ;
` );
db . exec ( `
CREATE INDEX messages_duplicate_check ON $ { MESSAGES_TABLE } (
source ,
sent_at
) ;
` );
dropFtsAndTriggers ( db ) ;
// we also want to remove the read_by it could have 20 times the same value set in the array
// we do this once, and updated the code to not allow multiple entries in read_by as we do not care about multiple entries
// (read_by is only used in private chats)
db . exec ( `
UPDATE $ { MESSAGES_TABLE } SET
json = json_remove ( json , '$.schemaVersion' , '$.recipients' , '$.decrypted_at' , '$.sourceDevice' , '$.read_by' )
` );
rebuildFtsTable ( db ) ;
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion23 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 23 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec (
`
ALTER TABLE $ { LAST_HASHES_TABLE } RENAME TO $ { LAST_HASHES_TABLE } _old ;
CREATE TABLE $ { LAST_HASHES_TABLE } (
id TEXT ,
snode TEXT ,
hash TEXT ,
expiresAt INTEGER ,
namespace INTEGER NOT NULL DEFAULT 0 ,
PRIMARY KEY ( id , snode , namespace )
) ; `
) ;
db . exec (
` INSERT INTO ${ LAST_HASHES_TABLE } (id, snode, hash, expiresAt) SELECT id, snode, hash, expiresAt FROM ${ LAST_HASHES_TABLE } _old; `
) ;
db . exec ( ` DROP TABLE ${ LAST_HASHES_TABLE } _old; ` ) ;
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion24 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 24 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
// it's unlikely there is still a publicChat v1 convo in the db, but run this in a migration to be 100% sure (previously, run on app start instead)
db . prepare (
` DELETE FROM ${ CONVERSATIONS_TABLE } WHERE
type = 'group' AND
id LIKE 'publicChat:1@%' ; `
) . run ( ) ;
db . exec ( `
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN zombies TEXT DEFAULT "[]" ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN left INTEGER ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN expireTimer INTEGER ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN mentionedUs INTEGER ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN unreadCount INTEGER ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN lastMessageStatus TEXT ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN lastMessage TEXT ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN lastJoinedTimestamp INTEGER ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN groupAdmins TEXT DEFAULT "[]" ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN isKickedFromGroup INTEGER ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN subscriberCount INTEGER ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN is_medium_group INTEGER ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN avatarPointer TEXT ; -- this is the url of the avatar for that conversation
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN avatarHash TEXT ; -- only used for opengroup avatar .
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN nickname TEXT ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN profileKey TEXT ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN triggerNotificationsFor TEXT DEFAULT "all" ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN isTrustedForAttachmentDownload INTEGER DEFAULT "FALSE" ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN isPinned INTEGER DEFAULT "FALSE" ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN isApproved INTEGER DEFAULT "FALSE" ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN didApproveMe INTEGER DEFAULT "FALSE" ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN avatarInProfile TEXT ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN avatarPathInAvatar TEXT ; -- this is very temporary , removed right below
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN displayNameInProfile TEXT ;
UPDATE $ { CONVERSATIONS_TABLE } SET
zombies = json_extract ( json , '$.zombies' ) ,
members = json_extract ( json , '$.members' ) ,
left = json_extract ( json , '$.left' ) ,
expireTimer = json_extract ( json , '$.expireTimer' ) ,
mentionedUs = json_extract ( json , '$.mentionedUs' ) ,
unreadCount = json_extract ( json , '$.unreadCount' ) ,
lastMessageStatus = json_extract ( json , '$.lastMessageStatus' ) ,
lastMessage = json_extract ( json , '$.lastMessage' ) ,
lastJoinedTimestamp = json_extract ( json , '$.lastJoinedTimestamp' ) ,
groupAdmins = json_extract ( json , '$.groupAdmins' ) ,
isKickedFromGroup = json_extract ( json , '$.isKickedFromGroup' ) ,
subscriberCount = json_extract ( json , '$.subscriberCount' ) ,
is_medium_group = json_extract ( json , '$.is_medium_group' ) ,
avatarPointer = json_extract ( json , '$.avatarPointer' ) ,
avatarHash = json_extract ( json , '$.avatarHash' ) ,
nickname = json_extract ( json , '$.nickname' ) ,
profileKey = json_extract ( json , '$.profileKey' ) ,
triggerNotificationsFor = json_extract ( json , '$.triggerNotificationsFor' ) ,
isTrustedForAttachmentDownload = json_extract ( json , '$.isTrustedForAttachmentDownload' ) ,
isPinned = json_extract ( json , '$.isPinned' ) ,
isApproved = json_extract ( json , '$.isApproved' ) ,
didApproveMe = json_extract ( json , '$.didApproveMe' ) ,
avatarInProfile = json_extract ( json , '$.profile.avatar' ) , -- profile . avatar is no longer used . We rely on avatarInProfile only ( for private chats and opengroups )
avatarPathInAvatar = json_extract ( json , '$.avatar.path' ) , -- this is very temporary
displayNameInProfile = json_extract ( json , '$.profile.displayName' ) ;
UPDATE $ { CONVERSATIONS_TABLE } SET json = json_remove ( json ,
'$.zombies' ,
'$.members' ,
'$.left' ,
'$.expireTimer' ,
'$.mentionedUs' ,
'$.unreadCount' ,
'$.lastMessageStatus' ,
'$.lastJoinedTimestamp' ,
'$.lastMessage' ,
'$.groupAdmins' ,
'$.isKickedFromGroup' ,
'$.subscriberCount' ,
'$.is_medium_group' ,
'$.avatarPointer' ,
'$.avatarHash' ,
'$.nickname' ,
'$.profileKey' ,
'$.triggerNotificationsFor' ,
'$.isTrustedForAttachmentDownload' ,
'$.isPinned' ,
'$.isApproved' ,
'$.type' ,
'$.version' ,
'$.isMe' ,
'$.didApproveMe' ,
'$.active_at' ,
'$.id' ,
'$.moderators' ,
'$.sessionRestoreSeen' ,
'$.profileName' ,
'$.timestamp' ,
'$.profile' ,
'$.name' ,
'$.profileAvatar' ,
' $ . avatarPath
' ) ;
ALTER TABLE $ { CONVERSATIONS_TABLE } DROP COLUMN json ;
UPDATE $ { CONVERSATIONS_TABLE } SET displayNameInProfile = name WHERE
type = 'group' AND
id NOT LIKE 'publicChat:%' ;
ALTER TABLE $ { CONVERSATIONS_TABLE } DROP COLUMN profileName ;
ALTER TABLE $ { CONVERSATIONS_TABLE } DROP COLUMN name ;
-- we want to rely on avatarInProfile only , but it can be set either in avatarInProfile or in avatarPathInAvatar .
-- make sure to override avatarInProfile with the value from avatarPathInAvatar if avatarInProfile is unset
UPDATE $ { CONVERSATIONS_TABLE } SET avatarInProfile = avatarPathInAvatar WHERE avatarInProfile IS NULL ;
ALTER TABLE $ { CONVERSATIONS_TABLE } DROP COLUMN avatarPathInAvatar ;
CREATE INDEX conversation_nickname ON $ { CONVERSATIONS_TABLE } (
nickname
) ;
CREATE INDEX conversation_displayNameInProfile ON $ { CONVERSATIONS_TABLE } (
displayNameInProfile
) ;
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion25 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 25 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
// mark all conversation as read/write/upload capability to be true on migration.
// the next batch poll will update them if needed
db . exec ( `
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN readCapability INTEGER DEFAULT 1 ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN writeCapability INTEGER DEFAULT 1 ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN uploadCapability INTEGER DEFAULT 1 ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN conversationIdOrigin TEXT ;
ALTER TABLE $ { CONVERSATIONS_TABLE } DROP COLUMN avatarHash ;
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN avatarImageId INTEGER ;
CREATE INDEX messages_convo_serverID ON $ { MESSAGES_TABLE } (
serverId ,
conversationId
) ;
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion26 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 26 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( `
ALTER TABLE $ { CONVERSATIONS_TABLE } ADD COLUMN groupModerators TEXT DEFAULT "[]" ; -- those are for sogs only ( for closed groups we only need the groupAdmins )
` );
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion27 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 27 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
const domainNameToUse = 'open.getsession.org' ;
const urlToUse = ` https:// ${ domainNameToUse } ` ;
const ipToRemove = '116.203.70.33' ;
// defining these functions here as this is very specific to this migration and used in a few places
function getNewConvoId ( oldConvoId? : string ) {
if ( ! oldConvoId ) {
return null ;
}
return oldConvoId
? . replace ( ` https:// ${ ipToRemove } ` , urlToUse )
? . replace ( ` http:// ${ ipToRemove } ` , urlToUse )
? . replace ( ipToRemove , urlToUse ) ;
}
function getAllOpenGroupV2Conversations ( instance : BetterSqlite3.Database ) {
// first _ matches all opengroupv1 (they are completely removed in a migration now),
// second _ force a second char to be there, so it can only be opengroupv2 convos
const rows = instance
. prepare (
` SELECT * FROM ${ CONVERSATIONS_TABLE } WHERE
type = 'group' AND
id LIKE 'publicChat:__%@%'
ORDER BY id ASC ; `
)
. all ( ) ;
return rows || [ ] ;
}
function getRoomIdFromConversationAttributes ( attributes? : ConversationAttributes | null ) {
if ( ! attributes ) {
return null ;
}
const indexSemiColon = attributes . id . indexOf ( ':' ) ;
const indexAt = attributes . id . indexOf ( '@' ) ;
if ( indexSemiColon < 0 || indexAt < 0 || indexSemiColon >= indexAt ) {
return null ;
}
const roomId = attributes . id . substring ( indexSemiColon , indexAt ) ;
if ( roomId . length <= 0 ) {
return null ;
}
return roomId ;
}
db . transaction ( ( ) = > {
// First we want to drop the column friendRequestStatus if it is there, otherwise the transaction fails
const rows = db . pragma ( ` table_info( ${ CONVERSATIONS_TABLE } ); ` ) ;
if ( rows . some ( ( m : any ) = > m . name === 'friendRequestStatus' ) ) {
console . info ( 'found column friendRequestStatus. Dropping it' ) ;
db . exec ( ` ALTER TABLE ${ CONVERSATIONS_TABLE } DROP COLUMN friendRequestStatus; ` ) ;
}
// We want to replace all the occurrences of the sogs server ip url (116.203.70.33 || http://116.203.70.33 || https://116.203.70.33) by its hostname: https://open.getsession.org
// This includes change the conversationTable, the openGroupRooms tables and every single message associated with them.
// Because the conversationId is used to link messages to conversation includes the ip/url in it...
/ * *
* First , remove duplicates for the v2 opengroup table , and replace the one without duplicates with their dns name syntax
* /
// rooms to rename are: crypto, lokinet, oxen, session, session-updates
const allSessionV2RoomsIp = sqlNode
. getAllV2OpenGroupRooms ( db )
. filter ( m = > m . serverUrl . includes ( ipToRemove ) ) ;
const allSessionV2RoomsDns = sqlNode
. getAllV2OpenGroupRooms ( db )
. filter ( m = > m . serverUrl . includes ( domainNameToUse ) ) ;
const duplicatesRoomsIpAndDns = allSessionV2RoomsIp . filter ( ip = >
allSessionV2RoomsDns . some ( dns = > dns . roomId === ip . roomId )
) ;
const withIpButNotDuplicateRoom = allSessionV2RoomsIp . filter ( ip = > {
return ! duplicatesRoomsIpAndDns . some ( dns = > dns . roomId === ip . roomId ) ;
} ) ;
console . info (
'allSessionV2RoomsIp' ,
allSessionV2RoomsIp . map ( m = > pick ( m , [ 'serverUrl' , 'roomId' ] ) )
) ;
console . info (
'allSessionV2RoomsDns' ,
allSessionV2RoomsDns . map ( m = > pick ( m , [ 'serverUrl' , 'roomId' ] ) )
) ;
console . info (
'duplicatesRoomsIpAndDns' ,
duplicatesRoomsIpAndDns . map ( m = > pick ( m , [ 'serverUrl' , 'roomId' ] ) )
) ;
console . info (
'withIpButNotDuplicateRoom' ,
withIpButNotDuplicateRoom . map ( m = > pick ( m , [ 'serverUrl' , 'roomId' ] ) )
) ;
console . info (
'========> before room update:' ,
sqlNode
. getAllV2OpenGroupRooms ( db )
. filter ( m = > m . serverUrl . includes ( domainNameToUse ) || m . serverUrl . includes ( ipToRemove ) )
. map ( m = > pick ( m , [ 'conversationId' , 'serverUrl' , 'roomId' ] ) )
) ;
// for those with duplicates, delete the one with the IP as we want to rely on the one with the DNS only now
// remove the ip ones completely which are duplicated.
// Note: this also removes the ones not duplicated, but we are recreating them just below with `saveV2OpenGroupRoom`
db . exec ( ` DELETE FROM ${ OPEN_GROUP_ROOMS_V2_TABLE } WHERE serverUrl LIKE '% ${ ipToRemove } %'; ` ) ;
// for those without duplicates, override the value with the Domain Name
withIpButNotDuplicateRoom . forEach ( r = > {
const newConvoId = getNewConvoId ( r . conversationId ) ;
if ( ! newConvoId ) {
return ;
}
console . info (
` withIpButNotDuplicateRoom: renaming room old: ${ r . conversationId } with saveV2OpenGroupRoom() new- conversationId: ${ newConvoId } : serverUrl: ${ urlToUse } `
) ;
sqlNode . saveV2OpenGroupRoom (
{
. . . r ,
serverUrl : urlToUse ,
conversationId : newConvoId ,
} ,
db
) ;
} ) ;
console . info (
'<======== after room update:' ,
sqlNode
. getAllV2OpenGroupRooms ( db )
. filter ( m = > m . serverUrl . includes ( domainNameToUse ) || m . serverUrl . includes ( ipToRemove ) )
. map ( m = > pick ( m , [ 'conversationId' , 'serverUrl' , 'roomId' ] ) )
) ;
/ * *
* Then , update the conversations table by doing the same thing
* /
const allSessionV2ConvosIp = compact (
getAllOpenGroupV2Conversations ( db ) . filter ( m = > m ? . id . includes ( ipToRemove ) )
) ;
const allSessionV2ConvosDns = compact (
getAllOpenGroupV2Conversations ( db ) . filter ( m = > m ? . id . includes ( domainNameToUse ) )
) ;
const withIpButNotDuplicateConvo = allSessionV2ConvosIp . filter ( ip = > {
const roomId = getRoomIdFromConversationAttributes ( ip ) ;
if ( ! roomId ) {
return false ;
}
return ! allSessionV2ConvosDns . some ( dns = > {
return getRoomIdFromConversationAttributes ( dns ) === roomId ;
} ) ;
} ) ;
// for those with duplicates, delete the one with the IP as we want to rely on the one with the DNS only now
// remove the ip ones completely which are duplicated.
// Note: this also removes the ones not duplicated, but we are recreating them just below with `saveConversation`
db . exec ( ` DELETE FROM ${ CONVERSATIONS_TABLE } WHERE id LIKE '% ${ ipToRemove } %'; ` ) ;
// for those without duplicates, override the value with the DNS
const convoIdsToMigrateFromIpToDns : Map < string , string > = new Map ( ) ;
withIpButNotDuplicateConvo . forEach ( r = > {
if ( ! r ) {
return ;
}
const newConvoId = getNewConvoId ( r . id ) ;
if ( ! newConvoId ) {
return ;
}
console . info (
` withIpButNotDuplicateConvo: renaming convo old: ${ r . id } with saveConversation() new- conversationId: ${ newConvoId } `
) ;
convoIdsToMigrateFromIpToDns . set ( r . id , newConvoId ) ;
// commenting this as saveConversation should not be called during migration.
// I actually suspect that this code was not working at all.
// sqlNode.saveConversation(
// {
// ...r,
// id: newConvoId,
// },
// db
// );
} ) ;
/ * *
* Lastly , we need to take care of messages .
* For duplicated rooms , we drop all the messages from the IP one . ( Otherwise we
* would need to compare each message id to not break the PRIMARY_KEY on the messageID and those are just sogs messages ) .
* For non duplicated rooms which got renamed to their dns ID , we override the stored conversationId in the message with the new conversationID
* /
dropFtsAndTriggers ( db ) ;
// let's start with the non duplicateD ones, as doing so will make the duplicated one process easier
console . info ( 'convoIdsToMigrateFromIpToDns' , [ . . . convoIdsToMigrateFromIpToDns . entries ( ) ] ) ;
[ . . . convoIdsToMigrateFromIpToDns . keys ( ) ] . forEach ( oldConvoId = > {
const newConvoId = convoIdsToMigrateFromIpToDns . get ( oldConvoId ) ;
if ( ! newConvoId ) {
return ;
}
console . info ( ` About to migrate messages of ${ oldConvoId } to ${ newConvoId } ` ) ;
db . prepare (
` UPDATE ${ MESSAGES_TABLE } SET
conversationId = $newConvoId ,
json = json_set ( json , '$.conversationId' , $newConvoId )
WHERE conversationId = $oldConvoId ; `
) . run ( { oldConvoId , newConvoId } ) ;
} ) ;
// now, the duplicated ones. We just need to move every message with a convoId matching that ip, because we already took care of the one to migrate to the dns before
console . log (
'Count of messages to be migrated: ' ,
db
. prepare (
` SELECT COUNT(*) FROM ${ MESSAGES_TABLE } WHERE conversationId LIKE '% ${ ipToRemove } %'; `
)
. get ( )
) ;
const messageWithIdsToUpdate = db
. prepare (
` SELECT DISTINCT conversationId FROM ${ MESSAGES_TABLE } WHERE conversationID LIKE '% ${ ipToRemove } %' `
)
. all ( ) ;
console . info ( 'messageWithConversationIdsToUpdate' , messageWithIdsToUpdate ) ;
messageWithIdsToUpdate . forEach ( oldConvo = > {
const newConvoId = getNewConvoId ( oldConvo . conversationId ) ;
if ( ! newConvoId ) {
return ;
}
console . info ( 'oldConvo.conversationId' , oldConvo . conversationId , newConvoId ) ;
db . prepare (
` UPDATE ${ MESSAGES_TABLE } SET
conversationId = $newConvoId ,
json = json_set ( json , '$.conversationId' , $newConvoId )
WHERE conversationId = $oldConvoId ; `
) . run ( { oldConvoId : oldConvo.conversationId , newConvoId } ) ;
} ) ;
rebuildFtsTable ( db ) ;
console . info (
'removing lastMessageDeletedServerID & lastMessageFetchedServerID from rooms table'
) ;
db . exec (
` UPDATE ${ OPEN_GROUP_ROOMS_V2_TABLE } SET
json = json_remove ( json , '$.lastMessageDeletedServerID' , '$.lastMessageFetchedServerID' , '$.token' ) ; `
) ;
console . info (
'removing lastMessageDeletedServerID & lastMessageFetchedServerID from rooms table. done'
) ;
writeSessionSchemaVersion ( targetVersion , db ) ;
console . log ( '... done' ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion28 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 28 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
// Keeping this empty migration because some people updated to this already, even if it is not needed anymore
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function updateToSessionSchemaVersion29 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 29 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
dropFtsAndTriggers ( db ) ;
db . exec ( ` CREATE INDEX messages_unread_by_conversation ON ${ MESSAGES_TABLE } (
unread ,
conversationId
) ; ` );
rebuildFtsTable ( db ) ;
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
function insertContactIntoContactWrapper (
contact : any ,
blockedNumbers : Array < string > ,
contactsConfigWrapper : ContactsConfigWrapperNode | null , // set this to null to only insert into the convo volatile wrapper (i.e. for ourConvo case)
volatileConfigWrapper : ConvoInfoVolatileWrapperNode ,
db : BetterSqlite3.Database
) {
if ( contactsConfigWrapper !== null ) {
const dbApproved = ! ! contact . isApproved || false ;
const dbApprovedMe = ! ! contact . didApproveMe || false ;
const dbBlocked = blockedNumbers . includes ( contact . id ) ;
const priority = contact . priority || CONVERSATION_PRIORITIES . default ;
const wrapperContact = getContactInfoFromDBValues ( {
id : contact.id ,
dbApproved ,
dbApprovedMe ,
dbBlocked ,
dbName : contact.displayNameInProfile || undefined ,
dbNickname : contact.nickname || undefined ,
dbProfileKey : contact.profileKey || undefined ,
dbProfileUrl : contact.avatarPointer || undefined ,
priority ,
dbCreatedAtSeconds : Math.floor ( ( contact . active_at || Date . now ( ) ) / 1000 ) ,
} ) ;
try {
hasDebugEnvVariable && console . info ( 'Inserting contact into wrapper: ' , wrapperContact ) ;
contactsConfigWrapper . set ( wrapperContact ) ;
} catch ( e ) {
console . error (
` contactsConfigWrapper.set during migration failed with ${ e . message } for id: ${ contact . id } `
) ;
// the wrapper did not like something. Try again with just the boolean fields as it's most likely the issue is with one of the strings (which could be recovered)
try {
hasDebugEnvVariable && console . info ( 'Inserting edited contact into wrapper: ' , contact . id ) ;
contactsConfigWrapper . set (
getContactInfoFromDBValues ( {
id : contact.id ,
dbApproved ,
dbApprovedMe ,
dbBlocked ,
dbName : undefined ,
dbNickname : undefined ,
dbProfileKey : undefined ,
dbProfileUrl : undefined ,
priority : CONVERSATION_PRIORITIES.default ,
dbCreatedAtSeconds : Math.floor ( Date . now ( ) / 1000 ) ,
} )
) ;
} catch ( err2 ) {
// there is nothing else we can do here
console . error (
` contactsConfigWrapper.set during migration failed with ${ err2 . message } for id: ${ contact . id } . Skipping contact entirely `
) ;
}
}
}
try {
const rows = db
. prepare (
`
SELECT MAX ( COALESCE ( sent_at , 0 ) ) AS max_sent_at
FROM $ { MESSAGES_TABLE } WHERE
conversationId = $conversationId AND
unread = $unread ;
`
)
. get ( {
conversationId : contact.id ,
unread : toSqliteBoolean ( false ) , // we want to find the message read with the higher sentAt timestamp
} ) ;
const maxRead = rows ? . max_sent_at ;
const lastRead = isNumber ( maxRead ) && isFinite ( maxRead ) ? maxRead : 0 ;
hasDebugEnvVariable &&
console . info ( ` Inserting contact into volatile wrapper maxread: ${ contact . id } : ${ lastRead } ` ) ;
volatileConfigWrapper . set1o1 ( contact . id , lastRead , false ) ;
} catch ( e ) {
console . error (
` volatileConfigWrapper.set1o1 during migration failed with ${ e . message } for id: ${ contact . id } . skipping `
) ;
}
}
function insertCommunityIntoWrapper (
community : { id : string ; priority : number } ,
userGroupConfigWrapper : UserGroupsWrapperNode ,
volatileConfigWrapper : ConvoInfoVolatileWrapperNode ,
db : BetterSqlite3.Database
) {
const priority = community . priority ;
const convoId = community . id ; // the id of a conversation has the prefix, the serverUrl and the roomToken already present, but not the pubkey
const roomDetails = sqlNode . getV2OpenGroupRoom ( convoId , db ) ;
// hasDebugEnvVariable && console.info('insertCommunityIntoWrapper: ', community);
if (
! roomDetails ||
isEmpty ( roomDetails ) ||
isEmpty ( roomDetails . serverUrl ) ||
isEmpty ( roomDetails . roomId ) ||
isEmpty ( roomDetails . serverPublicKey )
) {
console . info (
'insertCommunityIntoWrapper did not find corresponding room details' ,
convoId ,
roomDetails
) ;
return ;
}
hasDebugEnvVariable ? ?
console . info (
` building fullUrl from serverUrl:" ${ roomDetails . serverUrl } " roomId:" ${ roomDetails . roomId } " pubkey:" ${ roomDetails . serverPublicKey } " `
) ;
const fullUrl = userGroupConfigWrapper . buildFullUrlFromDetails (
roomDetails . serverUrl ,
roomDetails . roomId ,
roomDetails . serverPublicKey
) ;
const wrapperComm = getCommunityInfoFromDBValues ( {
fullUrl ,
priority ,
} ) ;
try {
hasDebugEnvVariable && console . info ( 'Inserting community into group wrapper: ' , wrapperComm ) ;
userGroupConfigWrapper . setCommunityByFullUrl ( wrapperComm . fullUrl , wrapperComm . priority ) ;
const rows = db
. prepare (
`
SELECT MAX ( COALESCE ( serverTimestamp , 0 ) ) AS max_sent_at
FROM $ { MESSAGES_TABLE } WHERE
conversationId = $conversationId AND
unread = $unread ;
`
)
. get ( {
conversationId : convoId ,
unread : toSqliteBoolean ( false ) , // we want to find the message read with the higher serverTimestamp timestamp
} ) ;
const maxRead = rows ? . max_sent_at ;
const lastRead = isNumber ( maxRead ) && isFinite ( maxRead ) ? maxRead : 0 ;
hasDebugEnvVariable &&
console . info (
` Inserting community into volatile wrapper: ${ wrapperComm . fullUrl } : ${ lastRead } `
) ;
volatileConfigWrapper . setCommunityByFullUrl ( wrapperComm . fullUrl , lastRead , false ) ;
} catch ( e ) {
console . error (
` userGroupConfigWrapper.set during migration failed with ${ e . message } for fullUrl: " ${ wrapperComm . fullUrl } ". Skipping community entirely `
) ;
}
}
function insertLegacyGroupIntoWrapper (
legacyGroup : Pick <
ConversationAttributes ,
'id' | 'priority' | 'displayNameInProfile' | 'lastJoinedTimestamp' | 'expireTimer'
> & { members : string ; groupAdmins : string } , // members and groupAdmins are still stringified here
userGroupConfigWrapper : UserGroupsWrapperNode ,
volatileInfoConfigWrapper : ConvoInfoVolatileWrapperNode ,
db : BetterSqlite3.Database
) {
const {
priority ,
id ,
// expireTimer,
groupAdmins ,
members ,
displayNameInProfile ,
lastJoinedTimestamp ,
} = legacyGroup ;
const latestEncryptionKeyPairHex = sqlNode . getLatestClosedGroupEncryptionKeyPair (
legacyGroup . id ,
db
) as HexKeyPair | undefined ;
const wrapperLegacyGroup = getLegacyGroupInfoFromDBValues ( {
id ,
priority ,
// expireTimer, // FIXME WILL add expirationMode here
groupAdmins ,
members ,
displayNameInProfile ,
encPubkeyHex : latestEncryptionKeyPairHex?.publicHex || '' ,
encSeckeyHex : latestEncryptionKeyPairHex?.privateHex || '' ,
lastJoinedTimestamp ,
} ) ;
try {
hasDebugEnvVariable &&
console . info ( 'Inserting legacy group into wrapper: ' , wrapperLegacyGroup ) ;
userGroupConfigWrapper . setLegacyGroup ( wrapperLegacyGroup ) ;
const rows = db
. prepare (
`
SELECT MAX ( COALESCE ( sent_at , 0 ) ) AS max_sent_at
FROM $ { MESSAGES_TABLE } WHERE
conversationId = $conversationId AND
unread = $unread ;
`
)
. get ( {
conversationId : id ,
unread : toSqliteBoolean ( false ) , // we want to find the message read with the higher sentAt timestamp
} ) ;
const maxRead = rows ? . max_sent_at ;
const lastRead = isNumber ( maxRead ) && isFinite ( maxRead ) ? maxRead : 0 ;
hasDebugEnvVariable &&
console . info ( ` Inserting legacy group into volatile wrapper maxread: ${ id } : ${ lastRead } ` ) ;
volatileInfoConfigWrapper . setLegacyGroup ( id , lastRead , false ) ;
} catch ( e ) {
console . error (
` userGroupConfigWrapper.set during migration failed with ${ e . message } for legacyGroup.id: " ${ legacyGroup . id } ". Skipping that legacy group entirely `
) ;
}
}
export function getBlockedNumbersDuringMigration ( db : BetterSqlite3.Database ) {
try {
const blockedItem = sqlNode . getItemById ( 'blocked' , db ) ;
if ( ! blockedItem ) {
return [ ] ;
}
const foundBlocked = blockedItem ? . value ;
hasDebugEnvVariable && console . info ( 'foundBlockedNumbers during migration' , foundBlocked ) ;
if ( isArray ( foundBlocked ) ) {
return foundBlocked ;
}
return [ ] ;
} catch ( e ) {
console . info ( 'failed to read blocked numbers. Considering no blocked numbers' , e . stack ) ;
return [ ] ;
}
}
function updateToSessionSchemaVersion30 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 30 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
/ * *
* Make all the changes required to the database structure to handle the user configs , in the next migration .
* I made two migrations because it was easier to separate
* - the part needed a user to be logged in ( creating the user dumps needs a private ed25519 key )
* - from the part not requiring a change of user , but which absolutely needed to be happening nevertheless ( database structure changes )
*
* /
db . transaction ( ( ) = > {
// drop unused readCapability & uploadCapability columns. Also move `writeCapability` to memory only value.
db . exec ( `
ALTER TABLE $ { CONVERSATIONS_TABLE } DROP COLUMN readCapability ; -- stored in a redux slice now
ALTER TABLE $ { CONVERSATIONS_TABLE } DROP COLUMN writeCapability ; -- stored in a redux slice now
ALTER TABLE $ { CONVERSATIONS_TABLE } DROP COLUMN uploadCapability ; -- stored in a redux slice now
ALTER TABLE $ { CONVERSATIONS_TABLE } DROP COLUMN subscriberCount ; -- stored in a redux slice now
ALTER TABLE $ { CONVERSATIONS_TABLE } DROP COLUMN groupModerators ; -- stored in a redux slice now
ALTER TABLE $ { CONVERSATIONS_TABLE } RENAME COLUMN isPinned TO priority ; -- isPinned was 0 for false and 1 for true , which matches our way of handling the priority
ALTER TABLE $ { CONVERSATIONS_TABLE } DROP COLUMN is_medium_group ; -- a medium group starts with 05 and has a type of group . We cache everything renderer side so there is no need for that field
` );
// Didn't find any reference to this serverTimestamp in the unprocessed table needed, so let's clean it up
db . exec ( `
ALTER TABLE unprocessed DROP COLUMN serverTimestamp ;
` );
// for manually flagging conversations as unread"
db . exec ( ` ALTER TABLE ${ CONVERSATIONS_TABLE } ADD COLUMN markedAsUnread BOOLEAN; ` ) ;
// after the rename of isPinned to priority, we also need to hide any private conversation that is not active at all.
// as they might be contacts, we did delete from the app already.
// The release of message requests from other platforms (17 april 2022) created a bunch of active, but not "real contacts" conversation.
// This `UPDATE` command makes sure to make any private conversation which have was inactive since the 1st may 2022 as inactive
db . prepare (
` UPDATE ${ CONVERSATIONS_TABLE } SET
active_at = 0
WHERE type = 'private' AND active_at > 0 AND active_at < $ { 1000 * 1651363200 } ; ` // 1st may 2022 GMT
) . run ( { } ) ;
db . prepare (
` UPDATE ${ CONVERSATIONS_TABLE } SET
priority = $ { CONVERSATION_PRIORITIES . hidden }
WHERE type = 'private' AND ( active_at IS NULL OR active_at = 0 ) ; `
) . run ( { } ) ;
// create the table which is going to handle the wrappers, without any content in this migration.
db . exec ( ` CREATE TABLE ${ CONFIG_DUMP_TABLE } (
variant TEXT NOT NULL ,
publicKey TEXT NOT NULL ,
data BLOB ,
PRIMARY KEY ( publicKey , variant )
) ;
` );
/ * *
* Remove the ` publicChat ` prefix from the communities , instead keep the full url + room in it , with the corresponding http or https prefix .
* This is easier to handle with the libsession wrappers
* /
const allOpengroupsConvo = db
. prepare (
` SELECT id FROM ${ CONVERSATIONS_TABLE } WHERE
type = 'group' AND
id LIKE 'publicChat:%'
ORDER BY id ASC ; `
)
. all ( ) ;
const allValidOpengroupsDetails = allOpengroupsConvo
. filter ( m = > isString ( m . id ) && m . id . indexOf ( '@' ) > 0 )
. map ( row = > {
const roomNameStart = ( row . id . indexOf ( ':' ) as number ) + 1 ;
const roomNameEnd = row . id . indexOf ( '@' ) ;
const roomName = row . id . substring ( roomNameStart , roomNameEnd ) ;
const baseUrl = row . id . substring ( ( roomNameEnd as number ) + 1 ) ;
return { roomName , baseUrl , oldConvoId : row.id } ;
} ) ;
allValidOpengroupsDetails . forEach ( convoDetails = > {
const newId = ` ${ convoDetails . baseUrl } / ${ convoDetails . roomName } ` ;
db . prepare (
` UPDATE ${ CONVERSATIONS_TABLE } SET
id = $newId
WHERE id = $oldId ; `
) . run ( {
newId ,
oldId : convoDetails.oldConvoId ,
} ) ;
// do the same for messages
db . prepare (
` UPDATE ${ MESSAGES_TABLE } SET
conversationId = $newId ,
json = json_set ( json , '$.conversationId' , $newId )
WHERE conversationId = $oldConvoId ; `
) . run ( { oldConvoId : convoDetails.oldConvoId , newId } ) ;
db . prepare (
` UPDATE ${ OPEN_GROUP_ROOMS_V2_TABLE } SET
conversationId = $newId ,
json = json_set ( json , '$.conversationId' , $newId )
WHERE conversationId = $oldConvoId ; `
) . run ( { newId , oldConvoId : convoDetails.oldConvoId } ) ;
} ) ;
// priority was isPinned before. Make sure that it was set to something, rather than allowing null values.
db . prepare (
` UPDATE ${ CONVERSATIONS_TABLE } SET
priority = $ { CONVERSATION_PRIORITIES . default }
WHERE priority IS NULL ; `
) . run ( { } ) ;
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
/ * *
* Get 's the user' s private and public keys from the database
* @param db the database
* @returns the keys { privateEd25519 : string , publicEd25519 : string }
* /
export function getOurAccountKeys ( db : BetterSqlite3.Database ) {
const keys = getIdentityKeys ( db ) ;
return keys ;
}
function updateToSessionSchemaVersion31 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 31 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
// In the migration 30, we made all the changes which didn't require the user to be logged in yet.
// in this one, we check if a user is logged in, and if yes we build and save the config dumps for the current state of the database.
try {
const keys = getOurAccountKeys ( db ) ;
const userAlreadyCreated = ! ! keys && ! isEmpty ( keys . privateEd25519 ) ;
if ( ! userAlreadyCreated ) {
throw new Error ( 'privateEd25519 was empty. Considering no users are logged in' ) ;
}
const blockedNumbers = getBlockedNumbersDuringMigration ( db ) ;
const { privateEd25519 , publicKeyHex } = keys ;
const userProfileWrapper = new UserConfigWrapperNode ( privateEd25519 , null ) ;
const contactsConfigWrapper = new ContactsConfigWrapperNode ( privateEd25519 , null ) ;
const userGroupsConfigWrapper = new UserGroupsWrapperNode ( privateEd25519 , null ) ;
const volatileInfoConfigWrapper = new ConvoInfoVolatileWrapperNode ( privateEd25519 , null ) ;
/ * *
* Setup up the User profile wrapper with what is stored in our own conversation
* /
const ourConversation = db
. prepare ( ` SELECT * FROM ${ CONVERSATIONS_TABLE } WHERE id = $ id; ` )
. get ( {
id : publicKeyHex ,
} ) as Record < string , any > | undefined ;
if ( ! ourConversation ) {
throw new Error ( 'Failed to find our logged in conversation while migrating' ) ;
}
// Insert the user profile into the userWrapper
const ourDbName = ourConversation . displayNameInProfile || '' ;
const ourDbProfileUrl = ourConversation . avatarPointer || '' ;
const ourDbProfileKey = fromHexToArray ( ourConversation . profileKey || '' ) ;
const ourConvoPriority = ourConversation . priority ;
if ( ourDbProfileUrl && ! isEmpty ( ourDbProfileKey ) ) {
userProfileWrapper . setUserInfo ( ourDbName , ourConvoPriority , {
url : ourDbProfileUrl ,
key : ourDbProfileKey ,
} ) ;
}
insertContactIntoContactWrapper (
ourConversation ,
blockedNumbers ,
null ,
volatileInfoConfigWrapper ,
db
) ;
// dump the user wrapper content and save it to the DB
const userDump = userProfileWrapper . dump ( ) ;
db . prepare (
` INSERT OR REPLACE INTO ${ CONFIG_DUMP_TABLE } (
publicKey ,
variant ,
data
) values (
$publicKey ,
$variant ,
$data
) ; `
) . run ( {
publicKey : publicKeyHex ,
variant : 'UserConfig' ,
data : userDump ,
} ) ;
/ * *
* Setup up the Contacts Wrapper with all the contact details which needs to be stored in it .
* /
// this filter is based on the `isContactToStoreInWrapper` function. Note, blocked contacts won't be added to the wrapper at first, but will on the first start
const contactsToWriteInWrapper = db
. prepare (
` SELECT * FROM ${ CONVERSATIONS_TABLE } WHERE type = 'private' AND active_at > 0 AND priority <> ${ CONVERSATION_PRIORITIES . hidden } AND (didApproveMe OR isApproved) AND id <> ' $ us' AND id NOT LIKE '15%' AND id NOT LIKE '25%' ; `
)
. all ( {
us : publicKeyHex ,
} ) ;
if ( isArray ( contactsToWriteInWrapper ) && contactsToWriteInWrapper . length ) {
console . info (
` ===================== Starting contact inserting into wrapper ${ contactsToWriteInWrapper ? . length } ======================= `
) ;
contactsToWriteInWrapper . forEach ( contact = > {
insertContactIntoContactWrapper (
contact ,
blockedNumbers ,
contactsConfigWrapper ,
volatileInfoConfigWrapper ,
db
) ;
} ) ;
console . info ( '===================== Done with contact inserting =======================' ) ;
}
const contactsDump = contactsConfigWrapper . dump ( ) ;
db . prepare (
` INSERT OR REPLACE INTO ${ CONFIG_DUMP_TABLE } (
publicKey ,
variant ,
data
) values (
$publicKey ,
$variant ,
$data
) ; `
) . run ( {
publicKey : publicKeyHex ,
variant : 'ContactsConfig' ,
data : contactsDump ,
} ) ;
/ * *
* Setup up the UserGroups Wrapper with all the comunities details which needs to be stored in it .
* /
// this filter is based on the `isCommunityToStoreInWrapper` function.
const communitiesToWriteInWrapper = db
. prepare (
` SELECT * FROM ${ CONVERSATIONS_TABLE } WHERE type = 'group' AND active_at > 0 AND id LIKE 'http%' ; `
)
. all ( { } ) ;
if ( isArray ( communitiesToWriteInWrapper ) && communitiesToWriteInWrapper . length ) {
console . info (
` ===================== Starting communities inserting into wrapper ${ communitiesToWriteInWrapper ? . length } ======================= `
) ;
communitiesToWriteInWrapper . forEach ( community = > {
try {
insertCommunityIntoWrapper (
community ,
userGroupsConfigWrapper ,
volatileInfoConfigWrapper ,
db
) ;
} catch ( e ) {
console . info ( ` failed to insert community with ${ e . message } ` , community ) ;
}
} ) ;
console . info (
'===================== Done with communinities inserting ======================='
) ;
}
// this filter is based on the `isLegacyGroupToStoreInWrapper` function.
const legacyGroupsToWriteInWrapper = db
. prepare (
` SELECT * FROM ${ CONVERSATIONS_TABLE } WHERE type = 'group' AND active_at > 0 AND id LIKE '05%' AND NOT isKickedFromGroup AND NOT left ; `
)
. all ( { } ) ;
if ( isArray ( legacyGroupsToWriteInWrapper ) && legacyGroupsToWriteInWrapper . length ) {
console . info (
` ===================== Starting legacy group inserting into wrapper length: ${ legacyGroupsToWriteInWrapper ? . length } ======================= `
) ;
legacyGroupsToWriteInWrapper . forEach ( legacyGroup = > {
try {
hasDebugEnvVariable &&
console . info ( 'Writing legacy group: ' , JSON . stringify ( legacyGroup ) ) ;
insertLegacyGroupIntoWrapper (
legacyGroup ,
userGroupsConfigWrapper ,
volatileInfoConfigWrapper ,
db
) ;
} catch ( e ) {
console . info ( ` failed to insert legacy group with ${ e . message } ` , legacyGroup ) ;
}
} ) ;
console . info (
'===================== Done with legacy group inserting ======================='
) ;
}
const userGroupsDump = userGroupsConfigWrapper . dump ( ) ;
db . prepare (
` INSERT OR REPLACE INTO ${ CONFIG_DUMP_TABLE } (
publicKey ,
variant ,
data
) values (
$publicKey ,
$variant ,
$data
) ; `
) . run ( {
publicKey : publicKeyHex ,
variant : 'UserGroupsConfig' ,
data : userGroupsDump ,
} ) ;
const convoVolatileDump = volatileInfoConfigWrapper . dump ( ) ;
db . prepare (
` INSERT OR REPLACE INTO ${ CONFIG_DUMP_TABLE } (
publicKey ,
variant ,
data
) values (
$publicKey ,
$variant ,
$data
) ; `
) . run ( {
publicKey : publicKeyHex ,
variant : 'ConvoInfoVolatileConfig' ,
data : convoVolatileDump ,
} ) ;
// we've just created the initial dumps. A ConfSyncJob is run when the app starts after 20 seconds
} catch ( e ) {
console . error (
` failed to create initial wrapper. Might just not have a logged in user yet? ` ,
e . message ,
e . stack ,
e
) ;
// if we get an exception here, most likely no users are logged in yet. We can just continue the transaction and the wrappers will be created when a user creates a new account.
}
// still, we update the schema version
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
}
function updateToSessionSchemaVersion32 ( currentVersion : number , db : BetterSqlite3.Database ) {
const targetVersion = 32 ;
if ( currentVersion >= targetVersion ) {
return ;
}
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : starting... ` ) ;
db . transaction ( ( ) = > {
db . exec ( ` CREATE INDEX messages_conversationId ON ${ MESSAGES_TABLE } (
conversationId
) ; ` );
dropFtsAndTriggers ( db ) ;
rebuildFtsTable ( db ) ;
writeSessionSchemaVersion ( targetVersion , db ) ;
} ) ( ) ;
console . log ( ` updateToSessionSchemaVersion ${ targetVersion } : success! ` ) ;
}
export function printTableColumns ( table : string , db : BetterSqlite3.Database ) {
console . info ( db . pragma ( ` table_info(' ${ table } '); ` ) ) ;
}
export function writeSessionSchemaVersion ( newVersion : number , db : BetterSqlite3.Database ) {
db . prepare (
` INSERT INTO loki_schema(
version
) values (
$newVersion
) `
) . run ( { newVersion } ) ;
}
export async function updateSessionSchema ( db : BetterSqlite3.Database ) {
const result = db
. prepare ( ` SELECT name FROM sqlite_master WHERE type = 'table' AND name='loki_schema'; ` )
. get ( ) ;
if ( ! result ) {
createSessionSchemaTable ( db ) ;
}
const lokiSchemaVersion = getSessionSchemaVersion ( db ) ;
console . log (
'updateSessionSchema:' ,
` Current loki schema version: ${ lokiSchemaVersion } ; ` ,
` Most recent schema version: ${ LOKI_SCHEMA_VERSIONS . length } ; `
) ;
for ( let index = 0 , max = LOKI_SCHEMA_VERSIONS . length ; index < max ; index += 1 ) {
const runSchemaUpdate = LOKI_SCHEMA_VERSIONS [ index ] ;
runSchemaUpdate ( lokiSchemaVersion , db ) ;
if ( index > lokiSchemaVersion && index - lokiSchemaVersion <= 3 ) {
/ * * W h e n r u n n i n g m i g r a t i o n s , w e b l o c k t h e n o d e p r o c e s s .
* This causes the app to be in a Not responding state when we have a lot of data .
* To avoid this , we add a ` sleep ` between the run of the last 3 migrations .
* This "only for the last 3 migrations" serves 2 purposes :
* - we don ' t wait for ` 200ms * total number of migrations ` when starting from schemaVersion 0
* - we do have some time between the last 3 migrations , at most .
*
* This means that this sleepFor will only sleep for at most 600 ms , even if we need to run 30 migrations .
* /
// eslint-disable-next-line no-await-in-loop
await sleepFor ( 200 ) ; // give some time for the UI to not freeze between 2 migrations
}
}
}