From d3e71ffa96c332ffe493d850bc5c6d82f1940159 Mon Sep 17 00:00:00 2001 From: bq Date: Sun, 4 Jan 2026 14:03:33 +0100 Subject: [PATCH 01/14] fix building on windows --- scripts/gen-connectors.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/gen-connectors.ts b/scripts/gen-connectors.ts index be734deb..268b6e7a 100644 --- a/scripts/gen-connectors.ts +++ b/scripts/gen-connectors.ts @@ -38,7 +38,7 @@ async function getConnectorFiles(dir: string): Promise { const connectorFiles = await getConnectorFiles(connectorsDir); const connectorEntries = connectorFiles.map((file) => - file.replace(connectorsDir + "/", ""), + file.slice(connectorsDir.length + 1).replace(/\\/g, "/"), ); const connectors: { @@ -52,7 +52,7 @@ const connectors: { for (const entry of connectorEntries) { const pathName = entry.replace(/\.ts$/, ""); - const name = pathName.replace(/\/|\\/g, "-"); + const name = pathName.replace(/[/\\]/g, "-"); const subpath = `db0/connectors/${pathName}`; const fullPath = join(connectorsDir, `${pathName}.ts`); From 0d05cb71ecbc4ecb5546663b4acae5a72d45a966 Mon Sep 17 00:00:00 2001 From: bq Date: Sun, 4 Jan 2026 14:25:47 +0100 Subject: [PATCH 02/14] use `import.meta.dirname` instead of `new URL` --- scripts/gen-connectors.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/scripts/gen-connectors.ts b/scripts/gen-connectors.ts index 268b6e7a..e70ae090 100644 --- a/scripts/gen-connectors.ts +++ b/scripts/gen-connectors.ts @@ -1,15 +1,13 @@ import { readFile, readdir, writeFile } from "node:fs/promises"; -import { join } from "node:path"; -import { fileURLToPath } from "node:url"; +import { join, resolve, sep } from "node:path"; import { findTypeExports } from "mlly"; import { camelCase, upperFirst } from "scule"; -const connectorsDir = fileURLToPath( - new URL("../src/connectors", import.meta.url), -); +const connectorsDir = resolve(import.meta.dirname, "../src/connectors"); -const connectorsMetaFile = fileURLToPath( - new URL("../src/_connectors.ts", import.meta.url), +const connectorsMetaFile = resolve( + import.meta.dirname, + "../src/_connectors.ts", ); const aliases = { From 31ab9aa1e94d9721b6734fe4816fd79982cad992 Mon Sep 17 00:00:00 2001 From: bq Date: Mon, 5 Jan 2026 14:29:58 +0100 Subject: [PATCH 03/14] use pathe instead of replacing slashes --- package.json | 1 + pnpm-lock.yaml | 3 +++ scripts/gen-connectors.ts | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index af8793bd..6bb5f60f 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "mlly": "^1.8.0", "mysql2": "^3.15.3", "obuild": "^0.4.2", + "pathe": "^2.0.3", "pg": "^8.16.3", "prettier": "^3.6.2", "scule": "^1.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dff25f4d..56d5852b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,9 @@ importers: obuild: specifier: ^0.4.2 version: 0.4.2(magicast@0.5.1)(typescript@5.9.3) + pathe: + specifier: ^2.0.3 + version: 2.0.3 pg: specifier: ^8.16.3 version: 8.16.3 diff --git a/scripts/gen-connectors.ts b/scripts/gen-connectors.ts index e70ae090..fc4244a8 100644 --- a/scripts/gen-connectors.ts +++ b/scripts/gen-connectors.ts @@ -1,5 +1,5 @@ import { readFile, readdir, writeFile } from "node:fs/promises"; -import { join, resolve, sep } from "node:path"; +import { join, resolve } from "pathe"; import { findTypeExports } from "mlly"; import { camelCase, upperFirst } from "scule"; @@ -36,7 +36,7 @@ async function getConnectorFiles(dir: string): Promise { const connectorFiles = await getConnectorFiles(connectorsDir); const connectorEntries = connectorFiles.map((file) => - file.slice(connectorsDir.length + 1).replace(/\\/g, "/"), + file.slice(connectorsDir.length + 1), ); const connectors: { From c89546bd10c3258bbec261fd8a784984ead8362b Mon Sep 17 00:00:00 2001 From: bq Date: Thu, 8 Jan 2026 16:19:46 +0100 Subject: [PATCH 04/14] remove unnecessary character group from regex --- src/database.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database.ts b/src/database.ts index f94b69f9..69ae1f64 100644 --- a/src/database.ts +++ b/src/database.ts @@ -3,7 +3,7 @@ import type { Connector, Database, SQLDialect } from "./types.ts"; import type { Primitive } from "./types.ts"; const SQL_SELECT_RE = /^select/i; -const SQL_RETURNING_RE = /[\s]returning[\s]/i; +const SQL_RETURNING_RE = /\sreturning\s/i; const DIALECTS_WITH_RET: Set = new Set(["postgresql", "sqlite"]); const DISPOSED_ERR = From f61efef7c967c8da3a634161471bfb12d4e61018 Mon Sep 17 00:00:00 2001 From: bq Date: Thu, 8 Jan 2026 16:33:30 +0100 Subject: [PATCH 05/14] add `supportsPooling` --- src/connectors/better-sqlite3.ts | 1 + src/connectors/bun-sqlite.ts | 1 + src/connectors/node-sqlite.ts | 1 + src/connectors/postgresql.ts | 1 + src/connectors/sqlite3.ts | 1 + src/types.ts | 7 ++++++- 6 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/connectors/better-sqlite3.ts b/src/connectors/better-sqlite3.ts index 1e769680..c846b4cc 100644 --- a/src/connectors/better-sqlite3.ts +++ b/src/connectors/better-sqlite3.ts @@ -35,6 +35,7 @@ export default function sqliteConnector( return { name: "sqlite", dialect: "sqlite", + supportsPooling: false, getInstance: () => getDB(), exec: (sql) => getDB().exec(sql), prepare: (sql) => new StatementWrapper(() => getDB().prepare(sql)), diff --git a/src/connectors/bun-sqlite.ts b/src/connectors/bun-sqlite.ts index e265864d..86496ee6 100644 --- a/src/connectors/bun-sqlite.ts +++ b/src/connectors/bun-sqlite.ts @@ -34,6 +34,7 @@ export default function bunSqliteConnector( return { name: "sqlite", dialect: "sqlite", + supportsPooling: false, getInstance: () => getDB(), exec: (sql) => getDB().exec(sql), prepare: (sql) => new StatementWrapper(getDB().prepare(sql)), diff --git a/src/connectors/node-sqlite.ts b/src/connectors/node-sqlite.ts index bce7ef3f..6f1afce2 100644 --- a/src/connectors/node-sqlite.ts +++ b/src/connectors/node-sqlite.ts @@ -41,6 +41,7 @@ export default function nodeSqlite3Connector( return { name: "node-sqlite", dialect: "sqlite", + supportsPooling: false, getInstance: () => getDB(), exec(sql: string) { getDB().exec(sql); diff --git a/src/connectors/postgresql.ts b/src/connectors/postgresql.ts index c7457be8..a6c8c8d0 100644 --- a/src/connectors/postgresql.ts +++ b/src/connectors/postgresql.ts @@ -35,6 +35,7 @@ export default function postgresqlConnector( return { name: "postgresql", dialect: "postgresql", + supportsPooling: false, getInstance: () => getClient(), exec: (sql) => query(sql), prepare: (sql) => new StatementWrapper(sql, query), diff --git a/src/connectors/sqlite3.ts b/src/connectors/sqlite3.ts index 7c9f249d..533da5b5 100644 --- a/src/connectors/sqlite3.ts +++ b/src/connectors/sqlite3.ts @@ -47,6 +47,7 @@ export default function nodeSqlite3Connector( return { name: "sqlite3", dialect: "sqlite", + supportsPooling: false, getInstance: () => getDB(), exec: (sql: string) => query(sql), prepare: (sql) => { diff --git a/src/types.ts b/src/types.ts index c80bcc0f..2aad78bc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -81,6 +81,11 @@ export type Connector = { */ dialect: SQLDialect; + /** + * Whether the connector supports connection pooling. + */ + supportsPooling: boolean; + /** * The client instance used internally. */ @@ -96,7 +101,7 @@ export type Connector = { /** * Prepares an SQL statement for execution. * @param {string} sql - The SQL string to prepare. - * @returns {statement} The prepared SQL statement. + * @returns {Statement} The prepared SQL statement. */ prepare: (sql: string) => Statement; From 516b9311ff7414a45ab74f8b3c1c48ba9da03974 Mon Sep 17 00:00:00 2001 From: bq Date: Thu, 8 Jan 2026 16:35:05 +0100 Subject: [PATCH 06/14] add acquireConnection to types --- src/types.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/types.ts b/src/types.ts index 2aad78bc..d1a115dc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -91,6 +91,15 @@ export type Connector = { */ getInstance: () => TInstance | Promise; + /** + * Acquires a separate connection from the database. + * + * Will block all other queries until disposed unless the library supports connection pooling. + */ + acquireConnection: () => Promise< + Pick, "exec" | "prepare" | "dispose"> + >; + /** * Executes an SQL query directly and returns the result. * @param {string} sql - The SQL string to execute. @@ -139,6 +148,15 @@ export interface Database */ getInstance: () => Promise>>; + /** + * Acquires a separate connection from the database. + * + * Will block all other queries until disposed unless the library supports connection pooling. + */ + acquireConnection: () => Promise< + Omit, "getInstance" | "acquireConnection"> + >; + /** * Executes a raw SQL string. * @param {string} sql - The SQL string to execute. From 9488b1984cf67a33b0f102202f211b67f3a6d69c Mon Sep 17 00:00:00 2001 From: bq Date: Fri, 9 Jan 2026 14:51:52 +0100 Subject: [PATCH 07/14] implement pg-pool connector using acquireConnection --- src/_connectors.ts | 5 ++- src/connectors/pg-pool.ts | 81 +++++++++++++++++++++++++++++++++++++++ src/database.ts | 57 ++++++++++++++++++--------- src/types.ts | 22 +++++++---- 4 files changed, 138 insertions(+), 27 deletions(-) create mode 100644 src/connectors/pg-pool.ts diff --git a/src/_connectors.ts b/src/_connectors.ts index e0171c60..38d236a5 100644 --- a/src/_connectors.ts +++ b/src/_connectors.ts @@ -11,12 +11,13 @@ import type { ConnectorOptions as LibSQLNodeOptions } from "db0/connectors/libsq import type { ConnectorOptions as LibSQLWebOptions } from "db0/connectors/libsql/web"; import type { ConnectorOptions as MySQL2Options } from "db0/connectors/mysql2"; import type { ConnectorOptions as NodeSQLiteOptions } from "db0/connectors/node-sqlite"; +import type { ConnectorOptions as PgPoolOptions } from "db0/connectors/pg-pool"; import type { ConnectorOptions as PgliteOptions } from "db0/connectors/pglite"; import type { ConnectorOptions as PlanetscaleOptions } from "db0/connectors/planetscale"; import type { ConnectorOptions as PostgreSQLOptions } from "db0/connectors/postgresql"; import type { ConnectorOptions as SQLite3Options } from "db0/connectors/sqlite3"; -export type ConnectorName = "better-sqlite3" | "bun-sqlite" | "bun" | "cloudflare-d1" | "cloudflare-hyperdrive-mysql" | "cloudflare-hyperdrive-postgresql" | "libsql-core" | "libsql-http" | "libsql-node" | "libsql" | "libsql-web" | "mysql2" | "node-sqlite" | "sqlite" | "pglite" | "planetscale" | "postgresql" | "sqlite3"; +export type ConnectorName = "better-sqlite3" | "bun-sqlite" | "bun" | "cloudflare-d1" | "cloudflare-hyperdrive-mysql" | "cloudflare-hyperdrive-postgresql" | "libsql-core" | "libsql-http" | "libsql-node" | "libsql" | "libsql-web" | "mysql2" | "node-sqlite" | "sqlite" | "pg-pool" | "pglite" | "planetscale" | "postgresql" | "sqlite3"; export type ConnectorOptions = { "better-sqlite3": BetterSQLite3Options; @@ -36,6 +37,7 @@ export type ConnectorOptions = { "node-sqlite": NodeSQLiteOptions; /** alias of node-sqlite */ "sqlite": NodeSQLiteOptions; + "pg-pool": PgPoolOptions; "pglite": PgliteOptions; "planetscale": PlanetscaleOptions; "postgresql": PostgreSQLOptions; @@ -60,6 +62,7 @@ export const connectors: Record = Object.freeze({ "node-sqlite": "db0/connectors/node-sqlite", /** alias of node-sqlite */ "sqlite": "db0/connectors/node-sqlite", + "pg-pool": "db0/connectors/pg-pool", "pglite": "db0/connectors/pglite", "planetscale": "db0/connectors/planetscale", "postgresql": "db0/connectors/postgresql", diff --git a/src/connectors/pg-pool.ts b/src/connectors/pg-pool.ts new file mode 100644 index 00000000..4875cd74 --- /dev/null +++ b/src/connectors/pg-pool.ts @@ -0,0 +1,81 @@ +import pg from "pg"; +import type { Connector, Primitive } from "db0"; +import { BoundableStatement } from "./_internal/statement.ts"; +import type { Connection } from "../types.ts"; + +export type ConnectorOptions = pg.PoolConfig; + +type InternalQuery = ( + sql: string, + params?: Primitive[], +) => Promise; + +export default function postgresqlPoolConnector( + opts: ConnectorOptions, +): Connector { + let _pool: undefined | pg.Pool; + const getPool = () => { + _pool ??= new pg.Pool(opts); + return _pool; + }; + + const acquireConnection = async (): Promise> => { + const client = await getPool().connect(); + return { + dialect: "postgresql", + exec: (sql) => client.query(normalizeParams(sql)), + prepare: (sql) => new StatementWrapper(sql, client.query), + dispose: async () => client.release(), + [Symbol.asyncDispose]: async () => client.release(), + }; + }; + + return { + name: "pg-pool", + dialect: "postgresql", + supportsPooling: true, + getInstance: () => getPool(), + exec: (sql) => getPool().query(normalizeParams(sql)), + prepare: (sql) => new StatementWrapper(sql, getPool().query), + acquireConnection, + dispose: async () => { + await _pool?.end?.(); + _pool = undefined; + }, + }; +} + +// https://www.postgresql.org/docs/9.3/sql-prepare.html +function normalizeParams(sql: string) { + let i = 0; + return sql.replace(/\?/g, () => `$${++i}`); +} + +class StatementWrapper extends BoundableStatement { + #query: InternalQuery; + #sql: string; + + constructor(sql: string, query: InternalQuery) { + super(); + this.#sql = sql; + this.#query = query; + } + + async all(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return res.rows; + } + + async run(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return { + success: true, + ...res, + }; + } + + async get(...params: Primitive[]) { + const res = await this.#query(this.#sql, params); + return res.rows[0]; + } +} diff --git a/src/database.ts b/src/database.ts index 69ae1f64..f5aa2235 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,5 +1,11 @@ import { sqlTemplate } from "./template.ts"; -import type { Connector, Database, SQLDialect } from "./types.ts"; +import type { + Connection, + Connector, + Database, + DefaultSQLResult, + SQLDialect, +} from "./types.ts"; import type { Primitive } from "./types.ts"; const SQL_SELECT_RE = /^select/i; @@ -29,6 +35,30 @@ export function createDatabase( } }; + const createExecutor = + (connector: Omit) => + async ( + strings: TemplateStringsArray, + ...values: Primitive[] + ): Promise => { + checkDisposed(); + const [sql, params] = sqlTemplate(strings, ...values); + if ( + SQL_SELECT_RE.test(sql) /* select */ || + // prettier-ignore + (DIALECTS_WITH_RET.has(connector.dialect) && SQL_RETURNING_RE.test(sql)) /* returning */ + ) { + const rows = await connector.prepare(sql).all(...params); + return { + rows, + success: true, + } as never; + } else { + const res = await connector.prepare(sql).run(...params); + return res as never; + } + }; + return >{ get dialect() { return connector.dialect; @@ -43,6 +73,12 @@ export function createDatabase( return connector.getInstance(); }, + async acquireConnection(fn) { + const connection = await connector.acquireConnection(); + await fn({ ...connection, sql: createExecutor(connection) }); + await connection.dispose?.(); + }, + exec: (sql: string) => { checkDisposed(); return Promise.resolve(connector.exec(sql)); @@ -53,24 +89,7 @@ export function createDatabase( return connector.prepare(sql); }, - sql: async (strings: TemplateStringsArray, ...values: Primitive[]) => { - checkDisposed(); - const [sql, params] = sqlTemplate(strings, ...values); - if ( - SQL_SELECT_RE.test(sql) /* select */ || - // prettier-ignore - (DIALECTS_WITH_RET.has(connector.dialect) && SQL_RETURNING_RE.test(sql)) /* returning */ - ) { - const rows = await connector.prepare(sql).all(...params); - return { - rows, - success: true, - }; - } else { - const res = await connector.prepare(sql).run(...params); - return res; - } - }, + sql: createExecutor(connector), dispose: () => { if (_disposed) { diff --git a/src/types.ts b/src/types.ts index d1a115dc..2c9f4554 100644 --- a/src/types.ts +++ b/src/types.ts @@ -96,9 +96,7 @@ export type Connector = { * * Will block all other queries until disposed unless the library supports connection pooling. */ - acquireConnection: () => Promise< - Pick, "exec" | "prepare" | "dispose"> - >; + acquireConnection: () => Promise>; /** * Executes an SQL query directly and returns the result. @@ -124,7 +122,7 @@ export type Connector = { /** * Represents default SQL results, including any error messages, row changes and rows returned. */ -type DefaultSQLResult = { +export type DefaultSQLResult = { lastInsertRowid?: number; changes?: number; error?: string; @@ -132,6 +130,16 @@ type DefaultSQLResult = { success?: boolean; }; +export type Connection = Pick< + Database, + | "dialect" + | "exec" + | "prepare" + | "sql" + | "dispose" + | typeof Symbol.asyncDispose +>; + export interface Database extends AsyncDisposable { readonly dialect: SQLDialect; @@ -153,9 +161,9 @@ export interface Database * * Will block all other queries until disposed unless the library supports connection pooling. */ - acquireConnection: () => Promise< - Omit, "getInstance" | "acquireConnection"> - >; + acquireConnection: ( + fn: (connection: Connection) => void | Promise, + ) => Promise; /** * Executes a raw SQL string. From 701e0adb63567d21643343b02befeef89dd2212e Mon Sep 17 00:00:00 2001 From: bq Date: Fri, 9 Jan 2026 16:36:09 +0100 Subject: [PATCH 08/14] add tests for pg-pool, fix normalizeParams not being applied everywhere --- src/connectors/pg-pool.ts | 14 ++++++++------ test/connectors/_tests.ts | 6 +++--- test/connectors/pg-pool.test.ts | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 test/connectors/pg-pool.test.ts diff --git a/src/connectors/pg-pool.ts b/src/connectors/pg-pool.ts index 4875cd74..5b0f9783 100644 --- a/src/connectors/pg-pool.ts +++ b/src/connectors/pg-pool.ts @@ -3,7 +3,7 @@ import type { Connector, Primitive } from "db0"; import { BoundableStatement } from "./_internal/statement.ts"; import type { Connection } from "../types.ts"; -export type ConnectorOptions = pg.PoolConfig; +export type ConnectorOptions = { url: string } | pg.PoolConfig; type InternalQuery = ( sql: string, @@ -15,7 +15,9 @@ export default function postgresqlPoolConnector( ): Connector { let _pool: undefined | pg.Pool; const getPool = () => { - _pool ??= new pg.Pool(opts); + _pool ??= new pg.Pool( + "url" in opts ? { connectionString: opts.url } : opts, + ); return _pool; }; @@ -36,7 +38,7 @@ export default function postgresqlPoolConnector( supportsPooling: true, getInstance: () => getPool(), exec: (sql) => getPool().query(normalizeParams(sql)), - prepare: (sql) => new StatementWrapper(sql, getPool().query), + prepare: (sql) => new StatementWrapper(sql, getPool().query.bind(_pool)), acquireConnection, dispose: async () => { await _pool?.end?.(); @@ -52,12 +54,12 @@ function normalizeParams(sql: string) { } class StatementWrapper extends BoundableStatement { - #query: InternalQuery; - #sql: string; + readonly #query: InternalQuery; + readonly #sql: string; constructor(sql: string, query: InternalQuery) { super(); - this.#sql = sql; + this.#sql = normalizeParams(sql); this.#query = query; } diff --git a/test/connectors/_tests.ts b/test/connectors/_tests.ts index 7f42d784..01a7ffef 100644 --- a/test/connectors/_tests.ts +++ b/test/connectors/_tests.ts @@ -1,10 +1,10 @@ import { beforeAll, expect, it } from "vitest"; import { - Connector, - Database, + type Connector, + type Database, createDatabase, type SQLDialect, -} from "../../src"; +} from "../../src/index.ts"; export function testConnector(opts: { connector: TConnector; diff --git a/test/connectors/pg-pool.test.ts b/test/connectors/pg-pool.test.ts new file mode 100644 index 00000000..58750ac1 --- /dev/null +++ b/test/connectors/pg-pool.test.ts @@ -0,0 +1,15 @@ +import { describe } from "vitest"; +import connector from "../../src/connectors/pg-pool.ts"; +import { testConnector } from "./_tests.ts"; + +describe.runIf(process.env.POSTGRESQL_URL)( + "connectors: pg-pool.test", + () => { + testConnector({ + dialect: "postgresql", + connector: connector({ + url: process.env.POSTGRESQL_URL!, + }), + }); + }, +); From cb7c062e0996d91166a2b192f2308ba00654cc5e Mon Sep 17 00:00:00 2001 From: bq Date: Fri, 9 Jan 2026 17:04:22 +0100 Subject: [PATCH 09/14] add tests for acquireConnection --- src/connectors/pg-pool.ts | 2 +- test/connectors/_tests.ts | 43 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/connectors/pg-pool.ts b/src/connectors/pg-pool.ts index 5b0f9783..e9886cd4 100644 --- a/src/connectors/pg-pool.ts +++ b/src/connectors/pg-pool.ts @@ -26,7 +26,7 @@ export default function postgresqlPoolConnector( return { dialect: "postgresql", exec: (sql) => client.query(normalizeParams(sql)), - prepare: (sql) => new StatementWrapper(sql, client.query), + prepare: (sql) => new StatementWrapper(sql, client.query.bind(client)), dispose: async () => client.release(), [Symbol.asyncDispose]: async () => client.release(), }; diff --git a/test/connectors/_tests.ts b/test/connectors/_tests.ts index 01a7ffef..0afc9180 100644 --- a/test/connectors/_tests.ts +++ b/test/connectors/_tests.ts @@ -1,4 +1,4 @@ -import { beforeAll, expect, it } from "vitest"; +import { beforeAll, expect, describe, it } from "vitest"; import { type Connector, type Database, @@ -71,6 +71,47 @@ export function testConnector(opts: { expect(rows).toMatchInlineSnapshot(userSnapshot); }); + describe("acquireConnection", () => { + it("should return a connection that can execute queries", async () => { + await db.acquireConnection(async (connection) => { + const { rows } = await connection.sql`SELECT * FROM users`; + expect(rows).toMatchInlineSnapshot(userSnapshot); + }); + }); + + it.runIf(opts.connector.supportsPooling)( + "should not block other queries", + async () => { + const { promise, resolve } = Promise.withResolvers(); + void db.acquireConnection(async () => { + await promise; + }); + + await expect(db.sql`SELECT * FROM users`).resolves.not.toThrow(); + resolve(); + }, + ); + + it.runIf(!opts.connector.supportsPooling)( + "should block other queries until the connection is released", + async () => { + const { promise, resolve } = Promise.withResolvers(); + void db.acquireConnection(async () => { + await promise; + }); + + let secondPromiseResolved = false; + const secondQueryPromise = db.sql`SELECT * FROM users`.then(() => { + secondPromiseResolved = true; + }); + expect(secondPromiseResolved).toBe(false); + resolve(); + await secondQueryPromise; + expect(secondPromiseResolved).toBe(true); + }, + ); + }); + it("deferred prepare errors", async () => { await expect( db.prepare("SELECT * FROM non_existing_table").all(), From 9f4ff767264750222bc1f4127fbc13463127cf92 Mon Sep 17 00:00:00 2001 From: bq Date: Thu, 5 Feb 2026 15:30:41 +0100 Subject: [PATCH 10/14] use named exports --- src/connectors/pg-pool.ts | 14 ++++++-------- src/connectors/postgresql.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/connectors/pg-pool.ts b/src/connectors/pg-pool.ts index e9886cd4..13ce75f1 100644 --- a/src/connectors/pg-pool.ts +++ b/src/connectors/pg-pool.ts @@ -1,23 +1,21 @@ -import pg from "pg"; +import { Pool, type PoolConfig, type QueryResult } from "pg"; import type { Connector, Primitive } from "db0"; import { BoundableStatement } from "./_internal/statement.ts"; import type { Connection } from "../types.ts"; -export type ConnectorOptions = { url: string } | pg.PoolConfig; +export type ConnectorOptions = { url: string } | PoolConfig; type InternalQuery = ( sql: string, params?: Primitive[], -) => Promise; +) => Promise; export default function postgresqlPoolConnector( opts: ConnectorOptions, -): Connector { - let _pool: undefined | pg.Pool; +): Connector { + let _pool: undefined | Pool; const getPool = () => { - _pool ??= new pg.Pool( - "url" in opts ? { connectionString: opts.url } : opts, - ); + _pool ??= new Pool("url" in opts ? { connectionString: opts.url } : opts); return _pool; }; diff --git a/src/connectors/postgresql.ts b/src/connectors/postgresql.ts index a6c8c8d0..a601172b 100644 --- a/src/connectors/postgresql.ts +++ b/src/connectors/postgresql.ts @@ -1,25 +1,25 @@ -import pg from "pg"; +import { type ClientConfig, Client, type QueryResult } from "pg"; import type { Connector, Primitive } from "db0"; import { BoundableStatement } from "./_internal/statement.ts"; -export type ConnectorOptions = { url: string } | pg.ClientConfig; +export type ConnectorOptions = { url: string } | ClientConfig; type InternalQuery = ( sql: string, params?: Primitive[], -) => Promise; +) => Promise; export default function postgresqlConnector( opts: ConnectorOptions, -): Connector { - let _client: undefined | pg.Client | Promise; +): Connector { + let _client: undefined | Client | Promise; function getClient() { if (_client) { return _client; } - const client = new pg.Client("url" in opts ? opts.url : opts); + const client = new Client("url" in opts ? opts.url : opts); _client = client.connect().then(() => { _client = client; return _client; From 0b8965afb443fb2ec348f7b00de83a3f8728192f Mon Sep 17 00:00:00 2001 From: bq Date: Sat, 7 Feb 2026 14:17:38 +0100 Subject: [PATCH 11/14] extract common postgres util to own file --- src/connectors/_internal/postgresql.ts | 5 +++++ src/connectors/cloudflare-hyperdrive-postgresql.ts | 7 +------ src/connectors/pg-pool.ts | 7 +------ src/connectors/pglite.ts | 7 +------ src/connectors/postgresql.ts | 7 +------ 5 files changed, 9 insertions(+), 24 deletions(-) create mode 100644 src/connectors/_internal/postgresql.ts diff --git a/src/connectors/_internal/postgresql.ts b/src/connectors/_internal/postgresql.ts new file mode 100644 index 00000000..82c71e4f --- /dev/null +++ b/src/connectors/_internal/postgresql.ts @@ -0,0 +1,5 @@ +// https://www.postgresql.org/docs/9.3/sql-prepare.html +export function normalizeParams(sql: string): string { + let i = 0; + return sql.replace(/\?/g, () => `$${++i}`); +} diff --git a/src/connectors/cloudflare-hyperdrive-postgresql.ts b/src/connectors/cloudflare-hyperdrive-postgresql.ts index 01bf2fe2..1f6d6c45 100644 --- a/src/connectors/cloudflare-hyperdrive-postgresql.ts +++ b/src/connectors/cloudflare-hyperdrive-postgresql.ts @@ -2,6 +2,7 @@ import pg from "pg"; import type { Connector, Primitive } from "db0"; +import { normalizeParams } from "./_internal/postgresql.ts"; import { BoundableStatement } from "./_internal/statement.ts"; import { getHyperdrive } from "./_internal/cloudflare.ts"; @@ -56,12 +57,6 @@ export default function cloudflareHyperdrivePostgresqlConnector( }; } -// https://www.postgresql.org/docs/9.3/sql-prepare.html -function normalizeParams(sql: string) { - let i = 0; - return sql.replace(/\?/g, () => `$${++i}`); -} - class StatementWrapper extends BoundableStatement { #query: InternalQuery; #sql: string; diff --git a/src/connectors/pg-pool.ts b/src/connectors/pg-pool.ts index 13ce75f1..35fc1217 100644 --- a/src/connectors/pg-pool.ts +++ b/src/connectors/pg-pool.ts @@ -1,5 +1,6 @@ import { Pool, type PoolConfig, type QueryResult } from "pg"; import type { Connector, Primitive } from "db0"; +import { normalizeParams } from "./_internal/postgresql.ts"; import { BoundableStatement } from "./_internal/statement.ts"; import type { Connection } from "../types.ts"; @@ -45,12 +46,6 @@ export default function postgresqlPoolConnector( }; } -// https://www.postgresql.org/docs/9.3/sql-prepare.html -function normalizeParams(sql: string) { - let i = 0; - return sql.replace(/\?/g, () => `$${++i}`); -} - class StatementWrapper extends BoundableStatement { readonly #query: InternalQuery; readonly #sql: string; diff --git a/src/connectors/pglite.ts b/src/connectors/pglite.ts index 0d08e175..b8681791 100644 --- a/src/connectors/pglite.ts +++ b/src/connectors/pglite.ts @@ -5,6 +5,7 @@ import type { } from "@electric-sql/pglite"; import { PGlite } from "@electric-sql/pglite"; import type { Connector, Primitive } from "db0"; +import { normalizeParams } from "./_internal/postgresql.ts"; import { BoundableStatement } from "./_internal/statement.ts"; export type ConnectorOptions = PGliteOptions; @@ -46,12 +47,6 @@ export default function pgliteConnector( }; } -// https://www.postgresql.org/docs/9.3/sql-prepare.html -function normalizeParams(sql: string) { - let i = 0; - return sql.replace(/\?/g, () => `$${++i}`); -} - class StatementWrapper extends BoundableStatement { #query: InternalQuery; #sql: string; diff --git a/src/connectors/postgresql.ts b/src/connectors/postgresql.ts index a601172b..3bf51b44 100644 --- a/src/connectors/postgresql.ts +++ b/src/connectors/postgresql.ts @@ -2,6 +2,7 @@ import { type ClientConfig, Client, type QueryResult } from "pg"; import type { Connector, Primitive } from "db0"; +import { normalizeParams } from "./_internal/postgresql.ts"; import { BoundableStatement } from "./_internal/statement.ts"; export type ConnectorOptions = { url: string } | ClientConfig; @@ -46,12 +47,6 @@ export default function postgresqlConnector( }; } -// https://www.postgresql.org/docs/9.3/sql-prepare.html -function normalizeParams(sql: string) { - let i = 0; - return sql.replace(/\?/g, () => `$${++i}`); -} - class StatementWrapper extends BoundableStatement { #query: InternalQuery; #sql: string; From 0bca195ce7f75b4c080c08832b8acce338888ddb Mon Sep 17 00:00:00 2001 From: bq Date: Sat, 7 Feb 2026 14:44:30 +0100 Subject: [PATCH 12/14] restructure types --- src/connectors/pg-pool.ts | 15 +++-- src/database.ts | 4 +- src/index.ts | 1 + src/types.ts | 119 +++++++++++++++----------------------- 4 files changed, 60 insertions(+), 79 deletions(-) diff --git a/src/connectors/pg-pool.ts b/src/connectors/pg-pool.ts index 35fc1217..cfe19c6c 100644 --- a/src/connectors/pg-pool.ts +++ b/src/connectors/pg-pool.ts @@ -2,7 +2,7 @@ import { Pool, type PoolConfig, type QueryResult } from "pg"; import type { Connector, Primitive } from "db0"; import { normalizeParams } from "./_internal/postgresql.ts"; import { BoundableStatement } from "./_internal/statement.ts"; -import type { Connection } from "../types.ts"; +import type { ConnectorConnection } from "../types.ts"; export type ConnectorOptions = { url: string } | PoolConfig; @@ -20,7 +20,7 @@ export default function postgresqlPoolConnector( return _pool; }; - const acquireConnection = async (): Promise> => { + const acquireConnection = async (): Promise => { const client = await getPool().connect(); return { dialect: "postgresql", @@ -31,6 +31,11 @@ export default function postgresqlPoolConnector( }; }; + const dispose = async () => { + await _pool?.end?.(); + _pool = undefined; + }; + return { name: "pg-pool", dialect: "postgresql", @@ -39,10 +44,8 @@ export default function postgresqlPoolConnector( exec: (sql) => getPool().query(normalizeParams(sql)), prepare: (sql) => new StatementWrapper(sql, getPool().query.bind(_pool)), acquireConnection, - dispose: async () => { - await _pool?.end?.(); - _pool = undefined; - }, + dispose, + [Symbol.asyncDispose]: dispose, }; } diff --git a/src/database.ts b/src/database.ts index f5aa2235..26df48d0 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,6 +1,6 @@ import { sqlTemplate } from "./template.ts"; import type { - Connection, + ConnectorConnection, Connector, Database, DefaultSQLResult, @@ -36,7 +36,7 @@ export function createDatabase( }; const createExecutor = - (connector: Omit) => + (connector: ConnectorConnection) => async ( strings: TemplateStringsArray, ...values: Primitive[] diff --git a/src/index.ts b/src/index.ts index 2ce54c19..33e76b0e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ export { createDatabase } from "./database.ts"; export { connectors } from "./_connectors.ts"; export type { + Connection, Connector, Database, ExecResult, diff --git a/src/types.ts b/src/types.ts index 2c9f4554..0fde00cc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -67,10 +67,37 @@ export type PreparedStatement = { */ export type ExecResult = unknown; +export type ConnectorConnection = AsyncDisposable & { + /** + * The SQL dialect used by the connector. + */ + dialect: SQLDialect; + + /** + * Executes a raw SQL string. + * @param {string} sql - The SQL string to execute. + * @returns {Promise} A promise that resolves with the execution result. + */ + exec: (sql: string) => Promise; + + /** + * Prepares an SQL statement from a raw SQL string. + * @param {string} sql - The SQL string to prepare. + * @returns {statement} The prepared SQL statement. + */ + prepare: (sql: string) => Statement; + + /** + * Closes the connection and cleans up resources. + * @returns {Promise} A promise that resolves when the connection is closed. + */ + dispose: () => Promise; +}; + /** * Defines a database connector for executing SQL queries and preparing statements. */ -export type Connector = { +export type Connector = ConnectorConnection & { /** * The name of the connector. */ @@ -94,29 +121,9 @@ export type Connector = { /** * Acquires a separate connection from the database. * - * Will block all other queries until disposed unless the library supports connection pooling. + * Will block all other queries until disposed if the connector doesn't support connection pooling. */ - acquireConnection: () => Promise>; - - /** - * Executes an SQL query directly and returns the result. - * @param {string} sql - The SQL string to execute. - * @returns {ExecResult | Promise} The result of the execution. - */ - exec: (sql: string) => ExecResult | Promise; - - /** - * Prepares an SQL statement for execution. - * @param {string} sql - The SQL string to prepare. - * @returns {Statement} The prepared SQL statement. - */ - prepare: (sql: string) => Statement; - - /** - * Closes the database connection and cleans up resources. - * @returns {void | Promise} A promise that resolves when the connection is closed. - */ - dispose?: () => void | Promise; + acquireConnection: () => Promise; }; /** @@ -130,18 +137,26 @@ export type DefaultSQLResult = { success?: boolean; }; -export type Connection = Pick< - Database, - | "dialect" - | "exec" - | "prepare" - | "sql" - | "dispose" - | typeof Symbol.asyncDispose ->; +/** + * A connection to a database. + * Pulled from connection pool if available on the connector, otherwise blocking. + */ +export type Connection = ConnectorConnection & { + /** + * Executes SQL queries using tagged template literals. + * @template T The expected type of query result. + * @param {TemplateStringsArray} strings - The segments of the SQL string. + * @param {...Primitive[]} values - The values to interpolate into the SQL string. + * @returns {Promise} A promise that resolves with the typed result of the query. + */ + sql: ( + strings: TemplateStringsArray, + ...values: Primitive[] + ) => Promise; +}; export interface Database - extends AsyncDisposable { + extends Connection { readonly dialect: SQLDialect; /** @@ -164,42 +179,4 @@ export interface Database acquireConnection: ( fn: (connection: Connection) => void | Promise, ) => Promise; - - /** - * Executes a raw SQL string. - * @param {string} sql - The SQL string to execute. - * @returns {Promise} A promise that resolves with the execution result. - */ - exec: (sql: string) => Promise; - - /** - * Prepares an SQL statement from a raw SQL string. - * @param {string} sql - The SQL string to prepare. - * @returns {statement} The prepared SQL statement. - */ - prepare: (sql: string) => Statement; - - /** - * Executes SQL queries using tagged template literals. - * @template T The expected type of query result. - * @param {TemplateStringsArray} strings - The segments of the SQL string. - * @param {...Primitive[]} values - The values to interpolate into the SQL string. - * @returns {Promise} A promise that resolves with the typed result of the query. - */ - sql: ( - strings: TemplateStringsArray, - ...values: Primitive[] - ) => Promise; - - /** - * Closes the database connection and cleans up resources. - * @returns {Promise} A promise that resolves when the connection is closed. - */ - dispose: () => Promise; - - /** - * AsyncDisposable implementation for using syntax support. - * @returns {Promise} A promise that resolves when the connection is disposed. - */ - [Symbol.asyncDispose]: () => Promise; } From 1d876458d6e9c5461e3862a12e9477f5dae8de15 Mon Sep 17 00:00:00 2001 From: bq Date: Sat, 7 Feb 2026 15:25:28 +0100 Subject: [PATCH 13/14] update pg connector --- package.json | 1 + pnpm-lock.yaml | 8 +++++++ src/connectors/pg-pool.ts | 6 ++++++ src/connectors/postgresql.ts | 42 ++++++++++++++++++++++++++---------- 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 6bb5f60f..8c8d908c 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "automd": "^0.4.2", "better-sqlite3": "^12.4.1", "changelogen": "^0.6.2", + "ciorent": "^1.0.10", "db0": "link:.", "dotenv": "^17.2.3", "drizzle-orm": "^0.44.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56d5852b..9cb07a87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,9 @@ importers: changelogen: specifier: ^0.6.2 version: 0.6.2(magicast@0.5.1) + ciorent: + specifier: ^1.0.10 + version: 1.0.10 db0: specifier: link:. version: 'link:' @@ -1980,6 +1983,9 @@ packages: resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} + ciorent@1.0.10: + resolution: {integrity: sha512-TsZFWkzboexCn0IMVgdzZdaaiRGt2pv3kum5YJz2K4q5muFEyJVS67r3Gdlu+wlczMwZEx5OMglMwS/rFkt8aw==} + citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} @@ -5191,6 +5197,8 @@ snapshots: ci-info@4.3.1: {} + ciorent@1.0.10: {} + citty@0.1.6: dependencies: consola: 3.4.2 diff --git a/src/connectors/pg-pool.ts b/src/connectors/pg-pool.ts index cfe19c6c..d72729d1 100644 --- a/src/connectors/pg-pool.ts +++ b/src/connectors/pg-pool.ts @@ -15,6 +15,12 @@ export default function postgresqlPoolConnector( opts: ConnectorOptions, ): Connector { let _pool: undefined | Pool; + /* + TODO: decide what behavior to use here, the non-pooling connector connects before + proceeding with calling functions, while `pg` wants you to run queries + and let the library handle creating connections via the pool when it needs to. + should the non-pooling connector do the same? should it just be removed altogether? + */ const getPool = () => { _pool ??= new Pool("url" in opts ? { connectionString: opts.url } : opts); return _pool; diff --git a/src/connectors/postgresql.ts b/src/connectors/postgresql.ts index 3bf51b44..c23edb89 100644 --- a/src/connectors/postgresql.ts +++ b/src/connectors/postgresql.ts @@ -1,9 +1,11 @@ import { type ClientConfig, Client, type QueryResult } from "pg"; +import * as mutex from "ciorent/mutex"; import type { Connector, Primitive } from "db0"; import { normalizeParams } from "./_internal/postgresql.ts"; import { BoundableStatement } from "./_internal/statement.ts"; +import type { ConnectorConnection } from "../types.ts"; export type ConnectorOptions = { url: string } | ClientConfig; @@ -28,9 +30,28 @@ export default function postgresqlConnector( return _client; } - const query: InternalQuery = async (sql, params) => { + const connectionMutex = mutex.init(); + const acquireConnection = async (): Promise => { + const releaseMutex = await mutex.acquire(connectionMutex); + const client = await getClient(); - return client.query(normalizeParams(sql), params); + return { + dialect: "postgresql", + exec: (sql) => client.query(normalizeParams(sql)), + prepare: (sql) => new StatementWrapper(sql, client.query.bind(client)), + dispose: async () => releaseMutex(), + [Symbol.asyncDispose]: async () => releaseMutex(), + }; + }; + + const buildQueryFn = + (getClient: () => Client | Promise): InternalQuery => + async (...params) => + (await getClient()).query(...params); + + const dispose = async () => { + (await _client)?.end?.(); + _client = undefined; }; return { @@ -38,22 +59,21 @@ export default function postgresqlConnector( dialect: "postgresql", supportsPooling: false, getInstance: () => getClient(), - exec: (sql) => query(sql), - prepare: (sql) => new StatementWrapper(sql, query), - dispose: async () => { - await (await _client)?.end?.(); - _client = undefined; - }, + exec: buildQueryFn(getClient), + prepare: (sql) => new StatementWrapper(sql, buildQueryFn(getClient)), + acquireConnection, + dispose, + [Symbol.asyncDispose]: dispose, }; } class StatementWrapper extends BoundableStatement { - #query: InternalQuery; - #sql: string; + readonly #query: InternalQuery; + readonly #sql: string; constructor(sql: string, query: InternalQuery) { super(); - this.#sql = sql; + this.#sql = normalizeParams(sql); this.#query = query; } From 744fa31904a25b3cf9687f0273128f5bbd9053bd Mon Sep 17 00:00:00 2001 From: bq Date: Sat, 7 Feb 2026 15:25:57 +0100 Subject: [PATCH 14/14] format files --- examples/drizzle/index.ts | 2 +- test/connectors/pg-pool.test.ts | 19 ++++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/examples/drizzle/index.ts b/examples/drizzle/index.ts index c7d5919b..a5af10ce 100644 --- a/examples/drizzle/index.ts +++ b/examples/drizzle/index.ts @@ -1,7 +1,7 @@ import { sqliteTable, text, numeric } from "drizzle-orm/sqlite-core"; import { createDatabase } from "../../src"; -import { drizzle } from "../../src/integrations/drizzle" +import { drizzle } from "../../src/integrations/drizzle"; import sqlite from "../../src/connectors/better-sqlite3"; diff --git a/test/connectors/pg-pool.test.ts b/test/connectors/pg-pool.test.ts index 58750ac1..b4068ff0 100644 --- a/test/connectors/pg-pool.test.ts +++ b/test/connectors/pg-pool.test.ts @@ -2,14 +2,11 @@ import { describe } from "vitest"; import connector from "../../src/connectors/pg-pool.ts"; import { testConnector } from "./_tests.ts"; -describe.runIf(process.env.POSTGRESQL_URL)( - "connectors: pg-pool.test", - () => { - testConnector({ - dialect: "postgresql", - connector: connector({ - url: process.env.POSTGRESQL_URL!, - }), - }); - }, -); +describe.runIf(process.env.POSTGRESQL_URL)("connectors: pg-pool.test", () => { + testConnector({ + dialect: "postgresql", + connector: connector({ + url: process.env.POSTGRESQL_URL!, + }), + }); +});