import { Pool, PoolClient } from "pg"; import Database from "better-sqlite3"; import fs from "fs"; import path from "path"; /** * Common interface for database operations to support both Postgres and SQLite */ export interface DBAdapter { query(text: string, params?: unknown[]): Promise<{ rows: unknown[], rowCount?: number }>; transaction(callback: (client: DBAdapter) => Promise): Promise; } export class PostgresAdapter implements DBAdapter { constructor(private pool: Pool) { } async query(text: string, params?: unknown[]) { try { // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = await this.pool.query(text, params as any[]); return { rows: result.rows, rowCount: result.rowCount ?? undefined }; } catch (error) { console.error("Postgres Query Error:", error); throw error; } } async transaction(callback: (client: DBAdapter) => Promise): Promise { const client = await this.pool.connect(); try { await client.query("BEGIN"); const connectedAdapter = new PostgresConnectedAdapter(client); const result = await callback(connectedAdapter); await client.query("COMMIT"); return result; } catch (e) { await client.query("ROLLBACK"); throw e; } finally { client.release(); } } } class PostgresConnectedAdapter implements DBAdapter { constructor(private client: PoolClient) { } async query(text: string, params?: unknown[]) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = await this.client.query(text, params as any[]); return { rows: result.rows, rowCount: result.rowCount ?? undefined }; } async transaction(callback: (client: DBAdapter) => Promise): Promise { // Nested transactions not explicitly supported, just pass through return callback(this); } } export class SqliteAdapter implements DBAdapter { private db: Database.Database; constructor(filePath: string) { // Ensure directory exists const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { try { fs.mkdirSync(dir, { recursive: true }); console.log(`Created directory for SQLite: ${dir}`); } catch (e) { console.error(`Failed to create directory ${dir}:`, e); } } this.db = new Database(filePath); // Enable WAL mode for better concurrency this.db.pragma('journal_mode = WAL'); } async query(text: string, params?: unknown[]) { // Convert Postgres $n params to SQLite placeholders. // We separately reorder params to match placeholder order. const convertedSql = text.replace(/\$(\d+)/g, "?"); // Reorder params array to match the order they appear in the SQL // SQLite expects params in the order they appear, not by their $n number const orderedParams: unknown[] = []; if (params && params.length > 0) { // Extract parameter numbers in order of appearance const paramNumbers: number[] = []; const regex = /\$(\d+)/g; let match; while ((match = regex.exec(text)) !== null) { paramNumbers.push(parseInt(match[1], 10)); } // Map to actual values for (const num of paramNumbers) { orderedParams.push(params[num - 1]); // $1 maps to params[0] } } try { const stmt = this.db.prepare(convertedSql); const safeParams = orderedParams.length > 0 ? orderedParams : []; const lowerSql = convertedSql.trim().toLowerCase(); // If it's a SELECT or RETURNING, we use .all() or .get() if (lowerSql.startsWith("select") || /returning\s+/.test(lowerSql)) { const rows = stmt.all(...safeParams); return { rows, rowCount: rows.length }; } else { // INSERT/UPDATE/DELETE with no RETURNING const info = stmt.run(...safeParams); return { rows: [], rowCount: info.changes }; } } catch (error) { console.error("SQLite Query Error:", error); throw error; } } async transaction(callback: (client: DBAdapter) => Promise): Promise { // Manual transaction management // better-sqlite3 is synchronous, but we simulate async interface this.db.exec("BEGIN"); try { const result = await callback(this); this.db.exec("COMMIT"); return result; } catch (e) { this.db.exec("ROLLBACK"); throw e; } } }