import type { AdminDatabaseLike, DbParams, DbTransaction, PreparedStatement, } from "@suaveplan/api-types/db"; interface Row { [k: string]: unknown; } /** * Tiny SQL-shape-aware in-memory admin adapter for unit tests in * `@suaveplan/api-db-adapter`. It does NOT implement real SQL; it parses * the small set of statements emitted by the tenant-scope wrapper and the * migration runner. * * Supported: * - CREATE TABLE IF NOT EXISTS (...columns...) * - INSERT INTO (cols) VALUES (?,?,?) * - INSERT INTO (cols) VALUES (?,?,?), (?,?,?), ... (multi-row not required) * - SELECT * FROM [WHERE col = ? [AND col = ?]*] * - SELECT FROM WHERE plugin = ? * - DELETE FROM [WHERE ...] * - UPDATE SET col = ? [, ...] [WHERE ...] * - ALTER TABLE ADD COLUMN * - arbitrary "fail-me" sentinel SQL via the `failOn` predicate */ export interface MemoryAdminOptions { /** When called with the SQL, return `true` to throw. Used for failure tests. */ failOn?: (sql: string) => boolean; /** Disable transaction support to exercise the no-tx code path. */ noTransaction?: boolean; } export function createMemoryAdmin( options: MemoryAdminOptions = {} ): AdminDatabaseLike & { readonly tables: ReadonlyMap; lastSql(): string | undefined; sqlLog(): ReadonlyArray<{ sql: string; params: ReadonlyArray }>; } { const tables = new Map(); let isClosed = false; const log: { sql: string; params: ReadonlyArray }[] = []; const trip = (sql: string): void => { if (options.failOn?.(sql) === true) { throw new Error(`memory-admin: failOn triggered for: ${sql}`); } }; const normParams = (p?: DbParams): unknown[] => { if (p === undefined) return []; if (Array.isArray(p)) return [...p]; return []; }; const runStatement = ( sql: string, params: unknown[] ): { rows?: Row[]; rowsAffected: number } => { log.push({ sql, params }); trip(sql); const trimmed = sql.trim(); // CREATE TABLE IF NOT EXISTS ... const createM = trimmed.match( /^create\s+table\s+if\s+not\s+exists\s+(\w+)/i ); if (createM !== null) { const name = createM[1] ?? ""; if (!tables.has(name)) tables.set(name, []); return { rowsAffected: 0 }; } // ALTER TABLE ADD COLUMN const alterM = trimmed.match( /^alter\s+table\s+(\w+)\s+add\s+column\s+(\w+)/i ); if (alterM !== null) { return { rowsAffected: 0 }; } // INSERT INTO (cols) VALUES (?, ?, ...) const insertM = trimmed.match( /^insert\s+into\s+(\w+)\s*\(([^)]+)\)\s*values\s*\(([^)]+)\)/i ); if (insertM !== null) { const name = insertM[1] ?? ""; const cols = (insertM[2] ?? "").split(",").map((c) => c.trim()); const vals = (insertM[3] ?? "").split(",").map((v) => v.trim()); const row: Row = {}; let pi = 0; for (let i = 0; i < cols.length; i++) { const v = vals[i] ?? "?"; if (v === "?") { row[cols[i] ?? ""] = params[pi++]; } else if (/^\$\d+$/.test(v)) { const idx = Number(v.slice(1)) - 1; row[cols[i] ?? ""] = params[idx]; } else { row[cols[i] ?? ""] = v.replace(/^['"]|['"]$/g, ""); } } if (!tables.has(name)) tables.set(name, []); tables.get(name)?.push(row); return { rowsAffected: 1 }; } // SELECT ... FROM [WHERE col = ? [AND col = ?]*] const selectM = trimmed.match( /^select\s+(.+?)\s+from\s+(\w+)(?:\s+where\s+(.+?))?(?:\s+order\s+by\s+.+)?(?:\s+limit\s+\d+)?;?\s*$/i ); if (selectM !== null) { const select = selectM[1] ?? "*"; const name = selectM[2] ?? ""; const whereClause = selectM[3]; const rows = tables.get(name) ?? []; let filtered = rows; if (whereClause !== undefined) { const preds = whereClause.split(/\s+and\s+/i).map((p) => { const m = p.match( /(\w+)\s*=\s*(\?|\$\d+|'[^']*'|"[^"]*"|\d+)/ ); return m === null ? null : { col: m[1] ?? "", val: m[2] ?? "" }; }); let pi = 0; const resolved = preds.map((pr) => { if (pr === null) return null; let v: unknown; if (pr.val === "?") { v = params[pi++]; } else if (/^\$\d+$/.test(pr.val)) { v = params[Number(pr.val.slice(1)) - 1]; } else { v = pr.val.replace(/^['"]|['"]$/g, ""); } return { col: pr.col, v }; }); filtered = rows.filter((r) => resolved.every( (pr) => pr === null || String(r[pr.col]) === String(pr.v) ) ); } if (select.trim() === "*") { return { rows: filtered, rowsAffected: filtered.length }; } const cols = select.split(",").map((c) => c.trim()); return { rows: filtered.map((r) => { const out: Row = {}; for (const c of cols) out[c] = r[c]; return out; }), rowsAffected: filtered.length, }; } // UPDATE — used by some tests const updateM = trimmed.match( /^update\s+(\w+)\s+set\s+(.+?)(?:\s+where\s+(.+))?$/i ); if (updateM !== null) { // For tests we only need to know that the rewriter appended a // WHERE clause; we don't have to actually mutate rows. return { rowsAffected: 0 }; } // DELETE const deleteM = trimmed.match(/^delete\s+from\s+(\w+)/i); if (deleteM !== null) { return { rowsAffected: 0 }; } throw new Error(`memory-admin: unsupported SQL: ${sql}`); }; const query = async ( sql: string, params?: DbParams ): Promise => { if (isClosed) throw new Error("memory-admin: closed"); const r = runStatement(sql, normParams(params)); return (r.rows ?? []) as unknown as R; }; const execute = async ( sql: string, params?: DbParams ): Promise<{ rowsAffected: number }> => { if (isClosed) throw new Error("memory-admin: closed"); const r = runStatement(sql, normParams(params)); return { rowsAffected: r.rowsAffected }; }; const prepare = async ( sql: string ): Promise> => ({ id: `stmt:${sql}`, execute: async (params) => (await query(sql, params)) as R, finalize: async () => undefined, }); const transaction = async ( fn: (tx: DbTransaction) => Promise ): Promise => { const snapshot = new Map(); for (const [k, v] of tables) snapshot.set(k, [...v]); const tx: DbTransaction = { query, execute, commit: async () => undefined, rollback: async () => undefined, }; try { return await fn(tx); } catch (e) { tables.clear(); for (const [k, v] of snapshot) tables.set(k, v); throw e; } }; const noTx = options.noTransaction === true; return { engine: "sqlite", query, execute, prepare, ...(noTx ? {} : { transaction }), close: async () => { isClosed = true; }, get tables() { return tables; }, lastSql: () => log.at(-1)?.sql, sqlLog: () => log, }; }