fix(db): pin the tenant scope to one connection (coreci-chat#14)
ci / test-pglite (PGlite, default) (push) Failing after 1m50s
ci / test-postgres (Postgres 16, G-022 RLS) (push) Failing after 1m32s
ci / test-pglite (PGlite, default) (pull_request) Failing after 1m50s
ci / test-postgres (Postgres 16, G-022 RLS) (pull_request) Failing after 1m33s

This commit is contained in:
2026-10-27 08:32:00 +00:00
parent d9da54b3bf
commit 53532fbdb5
+52 -1
View File
@@ -49,13 +49,44 @@ export async function createDb(opts: CreateDbOptions = {}): Promise<DbClient> {
client.release();
}
},
// One connection for the whole transaction (coreci-chat#14): a pool
// hands out a different connection per statement, so a transaction-local
// setting (the tenant scope SET LOCAL app.tenant_id) would otherwise
// be set on one connection and read on another.
async transaction<T>(fn: (client: DbClient) => Promise<T>): Promise<T> {
const client = await pool.connect();
const tx: DbClient = {
async query<R = Record<string, unknown>>(text: string, params?: unknown[]) {
const res = await client.query(text, params);
return { rows: res.rows as R[], rowCount: res.rowCount ?? 0 };
},
async exec(text: string) {
await client.query(text);
},
};
try {
await client.query("BEGIN");
const out = await fn(tx);
await client.query("COMMIT");
return out;
} catch (err) {
try {
await client.query("ROLLBACK");
} catch {
/* ignore rollback failure; the original error is more important */
}
throw err;
} finally {
client.release();
}
},
};
}
// PGlite (dev/test). Real Postgres in WASM, supports RLS + triggers.
const { PGlite } = await import("@electric-sql/pglite");
const pg = await PGlite.create();
return {
const single: DbClient = {
async query<T = Record<string, unknown>>(text: string, params?: unknown[]) {
const res = await pg.query(text, params);
return { rows: res.rows as T[], rowCount: (res.affectedRows ?? res.rows.length) };
@@ -64,4 +95,24 @@ export async function createDb(opts: CreateDbOptions = {}): Promise<DbClient> {
await pg.exec(text);
},
};
return {
...single,
// PGlite is a single in-process connection: the transaction and the
// tenant scope always land on it.
async transaction<T>(fn: (client: DbClient) => Promise<T>): Promise<T> {
await pg.query("BEGIN");
try {
const out = await fn(single);
await pg.query("COMMIT");
return out;
} catch (err) {
try {
await pg.query("ROLLBACK");
} catch {
/* ignore rollback failure; the original error is more important */
}
throw err;
}
},
};
}