Files
ChatLab/apps/desktop/main/database/migrations.ts
T

129 lines
4.1 KiB
TypeScript

/**
* Database migration — Electron adapter layer.
*
* Thin wrapper around @openchatlab/node-runtime shared migration definitions.
* Keeps Electron-only concerns: i18n MigrationInfo, better-sqlite3 type bridging.
*/
import type Database from 'better-sqlite3'
import {
CURRENT_SCHEMA_VERSION,
getSchemaVersion,
runMigrations,
needsMigration as coreNeedsMigration,
} from '@openchatlab/core'
import type { DatabaseAdapter, PathProvider } from '@openchatlab/core'
import { BetterSqliteAdapter } from '@openchatlab/node-runtime/src/better-sqlite3-adapter'
import {
CHAT_DB_COMPATIBILITY_RAISES,
getChatDbMigrations,
} from '@openchatlab/node-runtime/src/migrations/chat-db-migrations'
import { raiseDataDirMinRuntimeVersion, type RuntimeIdentity } from '@openchatlab/node-runtime/src/data-dir-compat'
import { t } from '../i18n'
import { tokenizeForFts } from '@openchatlab/node-runtime/src/nlp/fts-tokenizer'
export { CURRENT_SCHEMA_VERSION }
export interface MigrationInfo {
version: number
description: string
userMessage: string
}
export interface DesktopMigrationOptions {
pathProvider?: PathProvider
runtime?: RuntimeIdentity
}
const i18nKeys: Record<number, { descriptionKey: string; userMessageKey: string }> = Object.fromEntries(
Array.from({ length: CURRENT_SCHEMA_VERSION }, (_, index) => {
const version = index + 1
return [
version,
{
descriptionKey: `database.migrationV${version}Desc`,
userMessageKey: `database.migrationV${version}Message`,
},
]
})
)
function checkDatabaseIntegrity(db: DatabaseAdapter): { valid: boolean; error?: string } {
try {
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='meta'").all() as Array<{
name: string
}>
if (tables.length === 0) {
return { valid: false, error: t('database.integrityError') }
}
return { valid: true }
} catch (error) {
return {
valid: false,
error: t('database.checkFailed', { error: error instanceof Error ? error.message : String(error) }),
}
}
}
/**
* Execute database migrations.
* Wraps better-sqlite3 Database in a DatabaseAdapter and delegates to core runMigrations
* with shared migration definitions from @openchatlab/node-runtime.
*/
export function migrateDatabase(
db: Database.Database,
forceRepair = false,
options: DesktopMigrationOptions = {}
): boolean {
const adapter = new BetterSqliteAdapter(db)
const integrity = checkDatabaseIntegrity(adapter)
if (!integrity.valid) {
throw new Error(integrity.error)
}
const beforeVersion = getSchemaVersion(adapter)
const migrations = getChatDbMigrations({ tokenizeForFts })
const migrated = runMigrations(adapter, migrations, forceRepair)
if (!migrated || !options.pathProvider || !options.runtime) return migrated
const afterVersion = getSchemaVersion(adapter)
for (const compatibilityRaise of CHAT_DB_COMPATIBILITY_RAISES) {
if (beforeVersion >= compatibilityRaise.migrationVersion || afterVersion < compatibilityRaise.migrationVersion) {
continue
}
raiseDataDirMinRuntimeVersion(options.pathProvider, {
minRuntimeVersion: compatibilityRaise.minRuntimeVersion,
dataCompatibilityVersion: compatibilityRaise.dataCompatibilityVersion,
reason: compatibilityRaise.reason,
runtime: options.runtime,
module: compatibilityRaise.module,
})
}
return migrated
}
export function needsMigration(db: Database.Database): boolean {
const adapter = new BetterSqliteAdapter(db)
return coreNeedsMigration(adapter, CURRENT_SCHEMA_VERSION)
}
/**
* Get pending migration info for UI display (with i18n).
*/
export function getPendingMigrationInfos(fromVersion = 0): MigrationInfo[] {
const migrations = getChatDbMigrations({ tokenizeForFts })
return migrations
.filter((m) => m.version > fromVersion)
.map((m) => {
const keys = i18nKeys[m.version]
return {
version: m.version,
description: (keys && t(keys.descriptionKey)) || m.description,
userMessage: (keys && t(keys.userMessageKey)) || m.description,
}
})
}