Files
BizGaze_Remote/server/db/pg.js
T
Sravan ad48829337 Retire the SQLite backend — Postgres is the only engine
The dual backend (SQLite via db.js + Postgres via schema.pg.sql) was a
maintenance foot-gun: a schema change could land on the SQLite path only and
silently 500 every read on prod (it just did, with #18/#13). Production has run
on Postgres for weeks, so SQLite is retired: ONE schema source of truth
(db/schema.pg.sql), no drift possible.

- dbx.js: default DB_BACKEND=pg; an unknown backend now fails loudly at require
  time instead of silently selecting a stale engine.
- Deleted server/db.js, server/db/sqlite.js, server/db/migrate-sqlite-to-pg.js,
  server/scripts/migrate-bizgaze-only.js (all SQLite-only, none in the runtime
  path — the running server loads db/pg.js).
- Tests (e2e, db-smoke) target Postgres now and fail-fast (skip) unless
  DATABASE_URL points at a disposable test DB — never SQLite, never prod.
- Removed the dead DB_PATH env + fixed misleading SQLite comments in the
  Dockerfile / docker-compose (kept the /data volume: it holds
  uploads/recordings/transcripts/downloads, not just the old data.db).
- CLAUDE.md: stack + repo-layout + run-locally updated for Postgres-only.

Runtime is unaffected (prod already sets DB_BACKEND=pg and pg is a prod dep);
this only removes the unused SQLite path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 08:20:26 +05:30

60 lines
2.6 KiB
JavaScript

// PostgreSQL backend for the async DB adapter — the ONLY backend (SQLite retired 2026-08-12). Implements
// prepare(sql).{get,all,run}, exec(sql), tx(fn), init() so repos/app code stay engine-agnostic (the facade
// in dbx.js keeps the door open for future backends). Connection string from DATABASE_URL.
const { Pool, types } = require('pg');
const fs = require('fs');
const path = require('path');
// BIGINT (int8, OID 20) defaults to STRING in node-postgres to avoid precision loss. Every BIGINT here is
// an epoch-ms timestamp or a byte size — all far below Number.MAX_SAFE_INTEGER — so parse them as numbers
// to match the SQLite backend. Otherwise `expires_at < Date.now()` would compare a string to a number.
types.setTypeParser(20, (v) => (v === null ? null : parseInt(v, 10)));
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10 });
// Repos use '?' placeholders (SQLite style); Postgres wants $1,$2,… — replace positionally. Safe because
// no literal '?' appears inside any SQL string literal in this codebase.
function toPg(sql) { let i = 0; return sql.replace(/\?/g, () => '$' + (++i)); }
function prepare(sql) {
const q = toPg(sql);
return {
get: (...p) => pool.query(q, p).then((r) => r.rows[0]),
all: (...p) => pool.query(q, p).then((r) => r.rows),
run: (...p) => pool.query(q, p).then((r) => ({ changes: r.rowCount, lastInsertRowid: undefined })),
};
}
function exec(sql) { return pool.query(sql).then(() => {}); }
// Transaction on ONE pooled client (a pool would scatter BEGIN/COMMIT across connections). Same runner
// shape the sqlite backend's tx() exposes, so repos.mergeInto is identical on both engines.
async function tx(fn) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const t = {
run: (sql, ...p) => client.query(toPg(sql), p).then((r) => ({ changes: r.rowCount })),
get: (sql, ...p) => client.query(toPg(sql), p).then((r) => r.rows[0]),
all: (sql, ...p) => client.query(toPg(sql), p).then((r) => r.rows),
};
const out = await fn(t);
await client.query('COMMIT');
return out;
} catch (e) {
try { await client.query('ROLLBACK'); } catch (_) {}
throw e;
} finally {
client.release();
}
}
// Apply the schema (all CREATE ... IF NOT EXISTS — idempotent). Multi-statement, no params, so it runs via
// the simple-query protocol in one call. MUST be awaited before serving (server.js boot).
async function init() {
const sql = fs.readFileSync(path.join(__dirname, 'schema.pg.sql'), 'utf8');
await pool.query(sql);
}
module.exports = { prepare, exec, tx, init, name: 'pg', _pool: pool };