Files
WRNexusJS/packages/db/src/adapters/bunsql.ts
T

81 lines
2.8 KiB
TypeScript

/**
* Postgres + MySQL adapters built on Bun's native SQL client (`Bun.SQL`) — no
* external driver dependency. Both speak the same `Driver` interface; only the
* dialect (and thus the DDL types + placeholder style) differ.
*
* `Bun.SQL` pools connections, so transactions use its managed `begin(fn)` to
* keep BEGIN/…/COMMIT on one reserved connection.
*/
import type { Dialect } from "../sql.ts";
import type { Driver, Row, TxHandle } from "../driver.ts";
interface BunSqlClient {
unsafe(query: string, params?: unknown[]): Promise<unknown>;
begin<T>(fn: (tx: BunSqlClient) => Promise<T>): Promise<T>;
close(): Promise<void>;
}
interface ExecMeta {
count?: number;
affectedRows?: number;
lastInsertRowid?: number | bigint;
insertId?: number | bigint;
}
/** Convert the framework's portable positional placeholders for PostgreSQL. */
export function sqlForDialect(sql: string, dialect: Dialect): string {
if (dialect !== "postgres" || !sql.includes("?")) return sql;
let index = 0;
return sql.replace(/\?/g, () => `$${++index}`);
}
function runnerFor(client: BunSqlClient, dialect: Dialect): TxHandle {
return {
async query(sql, params = []): Promise<Row[]> {
const rows = (await client.unsafe(sqlForDialect(sql, dialect), params)) as Iterable<Row>;
return Array.from(rows);
},
async exec(sql, params = []) {
const meta = (await client.unsafe(sqlForDialect(sql, dialect), params)) as ExecMeta;
const id = meta.lastInsertRowid ?? meta.insertId;
return {
changes: Number(meta.affectedRows ?? meta.count ?? 0),
lastInsertId: id != null ? Number(id) : undefined,
};
},
};
}
/** Create a Bun.sql-backed driver for the given connection URL + dialect. */
export function bunSql(url: string, dialect: Dialect): Driver {
const max = Number(process.env.WRN_DB_POOL_MAX || 10);
const idleTimeout = Number(process.env.WRN_DB_IDLE_TIMEOUT || 30);
if (!Number.isInteger(max) || max < 1 || max > 100 || !Number.isInteger(idleTimeout) || idleTimeout < 0)
throw new Error('Invalid WRN_DB_POOL_MAX or WRN_DB_IDLE_TIMEOUT');
const Ctor = (Bun as unknown as { SQL: new (u: {url:string;max:number;idleTimeout:number}) => BunSqlClient }).SQL;
const client = new Ctor({url,max,idleTimeout});
const runner = runnerFor(client, dialect);
return {
dialect,
query: runner.query,
exec: runner.exec,
transaction(fn) {
return client.begin((tx) => fn(runnerFor(tx, dialect)));
},
close() {
return client.close();
},
};
}
/** PostgreSQL adapter (`postgres://user:pass@host:5432/db`). Placeholders: `$N`. */
export function postgres(url: string): Driver {
return bunSql(url, "postgres");
}
/** MySQL adapter (`mysql://user:pass@host:3306/db`). Placeholders: `?`. */
export function mysql(url: string): Driver {
return bunSql(url, "mysql");
}