Files
EJClaw/src/db/database-lifecycle.ts

117 lines
3.6 KiB
TypeScript

import { Database } from 'bun:sqlite';
import { DATA_DIR, normalizeServiceId, SERVICE_ID } from '../config.js';
import { listUnexpectedDataStateFiles } from '../data-state-files.js';
import { StartupPreconditionError } from '../startup-preconditions.js';
import {
openDatabaseFromFile,
openInMemoryDatabase,
openPersistentDatabase,
initializeDatabaseSchema,
} from './bootstrap.js';
import {
clearExpiredPairedTaskExecutionLeasesInDatabase,
clearPairedTaskExecutionLeasesForServiceInDatabase,
} from './paired-state.js';
import { getUnsupportedRouterStateKeysFromDatabase } from './router-state.js';
function finalizeDatabaseInitialization(database: Database): void {
clearPairedTaskExecutionLeasesForServiceInDatabase(
database,
normalizeServiceId(SERVICE_ID),
);
clearExpiredPairedTaskExecutionLeasesInDatabase(database);
}
function getUnexpectedLegacyRoomBindingTables(database: Database): string[] {
const tableRows = database
.prepare(
`SELECT name
FROM sqlite_master
WHERE type = 'table'
AND name NOT LIKE 'sqlite_%'
ORDER BY name`,
)
.all() as Array<{ name: string }>;
const matches: string[] = [];
for (const row of tableRows) {
const columns = database
.prepare(`PRAGMA table_info("${row.name.replaceAll('"', '""')}")`)
.all() as Array<{ name: string }>;
const columnNames = new Set(columns.map((column) => column.name));
const looksLikeLegacyRoomBindingTable =
columnNames.has('jid') &&
columnNames.has('name') &&
columnNames.has('folder') &&
columnNames.has('trigger_pattern') &&
columnNames.has('added_at');
const isBackupTable = columnNames.has('backup_at');
if (looksLikeLegacyRoomBindingTable && !isBackupTable) {
matches.push(row.name);
}
}
return matches;
}
function assertNoPendingLegacyRoomMigration(database: Database): void {
const pendingTables = getUnexpectedLegacyRoomBindingTables(database);
if (pendingTables.length === 0) {
return;
}
throw new StartupPreconditionError(
`Legacy room migration required before startup (tables=${pendingTables.join(',')})`,
);
}
function assertNoUnexpectedDataStateFiles(): void {
const pendingFiles = listUnexpectedDataStateFiles(DATA_DIR);
if (pendingFiles.length === 0) {
return;
}
throw new StartupPreconditionError(
`Unexpected data state files detected before startup (files=${pendingFiles.join(',')})`,
);
}
function assertNoUnsupportedRouterStateDbKeys(database: Database): void {
const unsupportedKeys = getUnsupportedRouterStateKeysFromDatabase(database);
if (unsupportedKeys.length === 0) {
return;
}
throw new StartupPreconditionError(
`Unsupported router_state DB keys remain before startup (keys=${unsupportedKeys.join(',')})`,
);
}
export function openInitializedPersistentDatabase(): Database {
const database = openPersistentDatabase();
initializeDatabaseSchema(database);
assertNoPendingLegacyRoomMigration(database);
assertNoUnexpectedDataStateFiles();
assertNoUnsupportedRouterStateDbKeys(database);
finalizeDatabaseInitialization(database);
return database;
}
export function openInitializedInMemoryDatabase(): Database {
const database = openInMemoryDatabase();
initializeDatabaseSchema(database);
finalizeDatabaseInitialization(database);
return database;
}
export function openInitializedDatabaseFromFile(dbPath: string): Database {
const database = openDatabaseFromFile(dbPath);
initializeDatabaseSchema(database);
assertNoPendingLegacyRoomMigration(database);
assertNoUnsupportedRouterStateDbKeys(database);
finalizeDatabaseInitialization(database);
return database;
}