fix(sqlite): added migrations for older storage schema

This commit is contained in:
alina 🌸 2024-02-08 02:07:47 +03:00
parent fc9dd07211
commit cc5cb3150d
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
8 changed files with 116 additions and 13 deletions

View file

@ -43,6 +43,10 @@ class SqliteStateRepository implements IStateRepository {
this._deleteRl = _driver.db.prepare('delete from rl_state where key = ?') this._deleteRl = _driver.db.prepare('delete from rl_state where key = ?')
this._deleteOldRl = _driver.db.prepare('delete from rl_state where reset < ?') this._deleteOldRl = _driver.db.prepare('delete from rl_state where reset < ?')
}) })
_driver.registerLegacyMigration('state', (db) => {
// not too important information, just drop the table
db.exec('drop table state')
})
} }
private _setState!: Statement private _setState!: Statement

View file

@ -21,6 +21,7 @@
}, },
"dependencies": { "dependencies": {
"@mtcute/core": "workspace:^", "@mtcute/core": "workspace:^",
"@mtcute/tl": "*",
"@mtcute/tl-runtime": "workspace:^", "@mtcute/tl-runtime": "workspace:^",
"better-sqlite3": "9.2.2" "better-sqlite3": "9.2.2"
}, },

View file

@ -1,6 +1,6 @@
import sqlite3, { Database, Options, Statement } from 'better-sqlite3' import sqlite3, { Database, Options, Statement } from 'better-sqlite3'
import { BaseStorageDriver, MtUnsupportedError } from '@mtcute/core' import { BaseStorageDriver } from '@mtcute/core'
import { beforeExit } from '@mtcute/core/utils.js' import { beforeExit } from '@mtcute/core/utils.js'
export interface SqliteStorageDriverOptions { export interface SqliteStorageDriverOptions {
@ -49,6 +49,17 @@ export class SqliteStorageDriver extends BaseStorageDriver {
private _migrations: Map<string, Map<number, MigrationFunction>> = new Map() private _migrations: Map<string, Map<number, MigrationFunction>> = new Map()
private _maxVersion: Map<string, number> = new Map() private _maxVersion: Map<string, number> = new Map()
// todo: remove in 1.0.0 + remove direct dep on @mtcute/tl
private _legacyMigrations: Map<string, MigrationFunction> = new Map()
registerLegacyMigration(repo: string, migration: MigrationFunction): void {
if (this.loaded) {
throw new Error('Cannot register migrations after loading')
}
this._legacyMigrations.set(repo, migration)
}
registerMigration(repo: string, version: number, migration: MigrationFunction): void { registerMigration(repo: string, version: number, migration: MigrationFunction): void {
if (this.loaded) { if (this.loaded) {
throw new Error('Cannot register migrations after loading') throw new Error('Cannot register migrations after loading')
@ -88,16 +99,16 @@ export class SqliteStorageDriver extends BaseStorageDriver {
this._pending.push([stmt, params]) this._pending.push([stmt, params])
} }
private _runLegacyMigrations = false
_initialize(): void { _initialize(): void {
const hasLegacyTables = this.db const hasLegacyTables = this.db
.prepare("select name from sqlite_master where type = 'table' and name = 'kv'") .prepare("select name from sqlite_master where type = 'table' and name = 'kv'")
.get() .get()
if (hasLegacyTables) { if (hasLegacyTables) {
throw new MtUnsupportedError( this._log.info('legacy tables detected, will run migrations')
'This database was created with an older version of mtcute, and cannot be used anymore. ' + this._runLegacyMigrations = true
'Please delete the database and try again.',
)
} }
this.db.exec(MIGRATIONS_TABLE_SQL) this.db.exec(MIGRATIONS_TABLE_SQL)
@ -154,12 +165,24 @@ export class SqliteStorageDriver extends BaseStorageDriver {
}) })
}) })
this._initialize() this.db.transaction(() => this._initialize())()
this._cleanup = beforeExit(() => { this._cleanup = beforeExit(() => {
this._save() this._save()
this._destroy() this._destroy()
}) })
for (const cb of this._onLoad) cb(this.db) for (const cb of this._onLoad) cb(this.db)
if (this._runLegacyMigrations) {
this.db.transaction(() => {
for (const migration of this._legacyMigrations.values()) {
migration(this.db)
}
// in case _writeLater was used
this._runMany(this._pending)
})()
}
} }
_save(): void { _save(): void {

View file

@ -18,11 +18,11 @@ export class SqliteAuthKeysRepository implements IAuthKeysRepository {
constructor(readonly _driver: SqliteStorageDriver) { constructor(readonly _driver: SqliteStorageDriver) {
_driver.registerMigration('auth_keys', 1, (db) => { _driver.registerMigration('auth_keys', 1, (db) => {
db.exec(` db.exec(`
create table auth_keys ( create table if not exists auth_keys (
dc integer primary key, dc integer primary key,
key blob not null key blob not null
); );
create table temp_auth_keys ( create table if not exists temp_auth_keys (
dc integer not null, dc integer not null,
idx integer not null, idx integer not null,
key blob not null, key blob not null,
@ -36,7 +36,9 @@ export class SqliteAuthKeysRepository implements IAuthKeysRepository {
this._getTemp = db.prepare('select key from temp_auth_keys where dc = ? and idx = ? and expires > ?') this._getTemp = db.prepare('select key from temp_auth_keys where dc = ? and idx = ? and expires > ?')
this._set = db.prepare('insert or replace into auth_keys (dc, key) values (?, ?)') this._set = db.prepare('insert or replace into auth_keys (dc, key) values (?, ?)')
this._setTemp = this._driver.db.prepare('insert or replace into temp_auth_keys (dc, idx, key, expires) values (?, ?, ?, ?)') this._setTemp = this._driver.db.prepare(
'insert or replace into temp_auth_keys (dc, idx, key, expires) values (?, ?, ?, ?)',
)
this._del = db.prepare('delete from auth_keys where dc = ?') this._del = db.prepare('delete from auth_keys where dc = ?')
this._delTemp = db.prepare('delete from temp_auth_keys where dc = ? and idx = ?') this._delTemp = db.prepare('delete from temp_auth_keys where dc = ? and idx = ?')

View file

@ -1,6 +1,12 @@
import { Statement } from 'better-sqlite3' import { Statement } from 'better-sqlite3'
import { IKeyValueRepository } from '@mtcute/core' import { IKeyValueRepository } from '@mtcute/core'
import { CurrentUserService } from '@mtcute/core/src/highlevel/storage/service/current-user.js'
import { UpdatesStateService } from '@mtcute/core/src/highlevel/storage/service/updates.js'
import { ServiceOptions } from '@mtcute/core/src/storage/service/base.js'
import { DefaultDcsService } from '@mtcute/core/src/storage/service/default-dcs.js'
import { __tlReaderMap } from '@mtcute/tl/binary/reader.js'
import { __tlWriterMap } from '@mtcute/tl/binary/writer.js'
import { SqliteStorageDriver } from '../driver.js' import { SqliteStorageDriver } from '../driver.js'
@ -25,6 +31,64 @@ export class SqliteKeyValueRepository implements IKeyValueRepository {
this._del = db.prepare('delete from key_value where key = ?') this._del = db.prepare('delete from key_value where key = ?')
this._delAll = db.prepare('delete from key_value') this._delAll = db.prepare('delete from key_value')
}) })
// awkward dependencies, unsafe code, awful crutches
// all in the name of backwards compatibility
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-floating-promises */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
_driver.registerLegacyMigration('kv', (db) => {
// fetch all values from the old table
const all = db.prepare('select key, value from kv').all() as { key: string; value: string }[]
const obj: Record<string, any> = {}
for (const { key, value } of all) {
obj[key] = JSON.parse(value)
}
db.exec('drop table kv')
// lol
const options: ServiceOptions = {
driver: this._driver,
readerMap: __tlReaderMap,
writerMap: __tlWriterMap,
// eslint-disable-next-line dot-notation
log: this._driver['_log'],
}
if (obj.self) {
new CurrentUserService(this, options).store({
userId: obj.self.userId,
isBot: obj.self.isBot,
isPremium: false,
usernames: [],
})
}
if (obj.pts) {
const svc = new UpdatesStateService(this, options)
svc.setPts(obj.pts)
if (obj.qts) svc.setQts(obj.qts)
if (obj.date) svc.setDate(obj.date)
if (obj.seq) svc.setSeq(obj.seq)
// also fetch channel states. they were moved to kv from a separate table
const channels = db.prepare('select * from pts').all() as any[]
for (const channel of channels) {
svc.setChannelPts(channel.channel_id, channel.pts)
}
}
db.exec('drop table pts')
if (obj.def_dc) {
new DefaultDcsService(this, options).store(obj.def_dc)
}
})
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any */
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-floating-promises */
/* eslint-enable @typescript-eslint/no-unsafe-argument */
} }
private _set!: Statement private _set!: Statement

View file

@ -54,6 +54,10 @@ export class SqlitePeersRepository implements IPeersRepository {
this._delAll = db.prepare('delete from peers') this._delAll = db.prepare('delete from peers')
}) })
_driver.registerLegacyMigration('peers', (db) => {
// not too important information, just drop the table
db.exec('drop table entities')
})
} }
private _store!: Statement private _store!: Statement

View file

@ -14,17 +14,19 @@ export class SqliteRefMessagesRepository implements IReferenceMessagesRepository
constructor(readonly _driver: SqliteStorageDriver) { constructor(readonly _driver: SqliteStorageDriver) {
_driver.registerMigration('ref_messages', 1, (db) => { _driver.registerMigration('ref_messages', 1, (db) => {
db.exec(` db.exec(`
create table message_refs ( create table if not exists message_refs (
peer_id integer not null, peer_id integer not null,
chat_id integer not null, chat_id integer not null,
msg_id integer not null msg_id integer not null
); );
create index idx_message_refs_peer on message_refs (peer_id); create index if not exists idx_message_refs_peer on message_refs (peer_id);
create index idx_message_refs on message_refs (chat_id, msg_id); create index if not exists idx_message_refs on message_refs (chat_id, msg_id);
`) `)
}) })
_driver.onLoad(() => { _driver.onLoad(() => {
this._store = this._driver.db.prepare('insert or replace into message_refs (peer_id, chat_id, msg_id) values (?, ?, ?)') this._store = this._driver.db.prepare(
'insert or replace into message_refs (peer_id, chat_id, msg_id) values (?, ?, ?)',
)
this._getByPeer = this._driver.db.prepare('select chat_id, msg_id from message_refs where peer_id = ?') this._getByPeer = this._driver.db.prepare('select chat_id, msg_id from message_refs where peer_id = ?')

View file

@ -279,6 +279,9 @@ importers:
'@mtcute/core': '@mtcute/core':
specifier: workspace:^ specifier: workspace:^
version: link:../core version: link:../core
'@mtcute/tl':
specifier: '*'
version: link:../tl
'@mtcute/tl-runtime': '@mtcute/tl-runtime':
specifier: workspace:^ specifier: workspace:^
version: link:../tl-runtime version: link:../tl-runtime