// ============================================================ // infrastructure/storage/db-core.ts — sql.js 数据库核心封装 // // 职责: // - 初始化 sql.js WASM 运行时 + 创建/加载 Database // - 执行 DDL 建表 / 迁移 // - 提供通用 CRUD 操作(execute / query / run / get) // - 导出数据库为 Uint8Array(供加密落盘) // // 安全: // - 数据库完全在内存中运行,不直接操作磁盘 // - 数据导出由 db-encrypt.ts 加密后写入磁盘 // ============================================================ import initSqlJs, { type Database, type SqlJsStatic, type SqlValue } from 'sql.js'; import { DDL_STATEMENTS, INIT_VERSION_SQL } from './schema'; import { logger } from '@/utils/logger'; // ---------- 内部状态 ---------- let SQL: SqlJsStatic | null = null; let db: Database | null = null; // ---------- 初始化 ---------- /** * 初始化 sql.js 运行时(WASM 加载),仅需调用一次。 */ async function ensureRuntime(): Promise { if (SQL) return SQL; try { SQL = await initSqlJs({ locateFile: (file: string) => { const url = import.meta.env.DEV ? `/${file}` : `./${file}`; console.log(`[storage] 加载 sql.js WASM:${url}`); return url; }, }); console.log('[storage] sql.js 运行时初始化成功'); return SQL; } catch (err) { const msg = err instanceof Error ? err.message : String(err); console.error(`[storage] sql.js WASM 加载失败:${msg}`); logger.error('storage', `sql.js WASM 加载失败:${msg}`, err instanceof Error ? err : undefined); throw err; } } /** * 打开或创建数据库。 * * @param encryptedData - 从磁盘读取的加密数据(由 db-encrypt.ts 解密后提供), * 未提供则创建空库。 * @returns true 表示成功 */ export async function openDatabase(encryptedData?: Uint8Array): Promise { try { const sql = await ensureRuntime(); db = new sql.Database(encryptedData ?? undefined); // 执行建表 DDL db.run('PRAGMA journal_mode=OFF'); db.run('PRAGMA synchronous=0'); for (const stmt of DDL_STATEMENTS) { db.run(stmt); } db.run(INIT_VERSION_SQL); const size = (db.export()?.byteLength ?? 0); console.log(`[storage] 数据库已打开(${encryptedData ? '从磁盘恢复' : '新建'}),${DDL_STATEMENTS.length} 条 DDL,${size} bytes`); logger.info('storage', `数据库已打开(${encryptedData ? '从磁盘恢复' : '新建'})`); return true; } catch (err) { const msg = err instanceof Error ? err.message : String(err); console.error(`[storage] 数据库打开失败:${msg}`); logger.error('storage', `数据库打开失败:${msg}`, err instanceof Error ? err : undefined); return false; } } // ---------- 导出 ---------- /** * 导出当前数据库为 Uint8Array(供加密后写入磁盘)。 */ export function exportDatabase(): Uint8Array | null { if (!db) return null; try { return db.export(); } catch (err) { logger.error('storage', '数据库导出失败', err instanceof Error ? err : undefined); return null; } } // ---------- 关闭 ---------- export function closeDatabase(): void { if (db) { db.close(); db = null; logger.info('storage', '数据库已关闭'); } } export function isOpen(): boolean { return db !== null; } // ---------- 查询 API ---------- /** 确保数据库已打开 */ function guard(): Database { if (!db) throw new Error('数据库未初始化 — 请确认已调用 initStorage() 并等待其完成'); return db; } /** 执行一条写操作 SQL(INSERT / UPDATE / DELETE),返回受影响行数 */ export function execute(sql: string, params?: SqlValue[]): number { try { guard().run(sql, params); return guard().getRowsModified(); } catch (err) { logger.error('storage', `SQL 执行失败:${sql.substring(0, 80)}`, err instanceof Error ? err : undefined); throw err; } } /** 查询多条记录 */ export function queryAll>(sql: string, params?: SqlValue[]): T[] { try { const stmt = guard().prepare(sql); if (params) stmt.bind(params); const rows: T[] = []; while (stmt.step()) { rows.push(stmt.getAsObject() as T); } stmt.free(); return rows; } catch (err) { logger.error('storage', `SQL 查询失败:${sql.substring(0, 80)}`, err instanceof Error ? err : undefined); return []; } } /** 查询单条记录 */ export function queryOne>(sql: string, params?: SqlValue[]): T | null { try { const stmt = guard().prepare(sql); if (params) stmt.bind(params); let row: T | null = null; if (stmt.step()) { row = stmt.getAsObject() as T; } stmt.free(); return row; } catch (err) { logger.error('storage', `SQL 查询失败:${sql.substring(0, 80)}`, err instanceof Error ? err : undefined); return null; } } /** 执行多条 SQL(事务包裹) */ export function executeBatch(statements: string[]): void { const d = guard(); try { d.run('BEGIN'); for (const stmt of statements) { d.run(stmt); } d.run('COMMIT'); } catch (err) { try { d.run('ROLLBACK'); } catch { /* ignore */ } throw err; } }