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/package.json b/package.json index af8793bd..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", @@ -62,6 +63,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..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:' @@ -72,6 +75,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 @@ -1977,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==} @@ -5188,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/scripts/gen-connectors.ts b/scripts/gen-connectors.ts index be734deb..fc4244a8 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 } from "pathe"; 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 = { @@ -38,7 +36,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), ); const connectors: { @@ -52,7 +50,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`); 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/_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/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/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/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/pg-pool.ts b/src/connectors/pg-pool.ts new file mode 100644 index 00000000..d72729d1 --- /dev/null +++ b/src/connectors/pg-pool.ts @@ -0,0 +1,85 @@ +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 { ConnectorConnection } from "../types.ts"; + +export type ConnectorOptions = { url: string } | PoolConfig; + +type InternalQuery = ( + sql: string, + params?: Primitive[], +) => Promise; + +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; + }; + + 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.bind(client)), + dispose: async () => client.release(), + [Symbol.asyncDispose]: async () => client.release(), + }; + }; + + const dispose = async () => { + await _pool?.end?.(); + _pool = undefined; + }; + + return { + name: "pg-pool", + dialect: "postgresql", + supportsPooling: true, + getInstance: () => getPool(), + exec: (sql) => getPool().query(normalizeParams(sql)), + prepare: (sql) => new StatementWrapper(sql, getPool().query.bind(_pool)), + acquireConnection, + dispose, + [Symbol.asyncDispose]: dispose, + }; +} + +class StatementWrapper extends BoundableStatement { + readonly #query: InternalQuery; + readonly #sql: string; + + constructor(sql: string, query: InternalQuery) { + super(); + this.#sql = normalizeParams(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/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 c7457be8..c23edb89 100644 --- a/src/connectors/postgresql.ts +++ b/src/connectors/postgresql.ts @@ -1,25 +1,28 @@ -import pg from "pg"; +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 } | 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; @@ -27,37 +30,50 @@ 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 { name: "postgresql", 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, }; } -// 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; + 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/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/database.ts b/src/database.ts index f94b69f9..26df48d0 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,9 +1,15 @@ import { sqlTemplate } from "./template.ts"; -import type { Connector, Database, SQLDialect } from "./types.ts"; +import type { + ConnectorConnection, + Connector, + Database, + DefaultSQLResult, + 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 = @@ -29,6 +35,30 @@ export function createDatabase( } }; + const createExecutor = + (connector: ConnectorConnection) => + 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/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 c80bcc0f..0fde00cc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -67,87 +67,81 @@ export type PreparedStatement = { */ export type ExecResult = unknown; -/** - * Defines a database connector for executing SQL queries and preparing statements. - */ -export type Connector = { - /** - * The name of the connector. - */ - name: string; - +export type ConnectorConnection = AsyncDisposable & { /** * The SQL dialect used by the connector. */ dialect: SQLDialect; /** - * The client instance used internally. - */ - getInstance: () => TInstance | Promise; - - /** - * Executes an SQL query directly and returns the result. + * Executes a raw SQL string. * @param {string} sql - The SQL string to execute. - * @returns {ExecResult | Promise} The result of the execution. + * @returns {Promise} A promise that resolves with the execution result. */ - exec: (sql: string) => ExecResult | Promise; + exec: (sql: string) => Promise; /** - * Prepares an SQL statement for execution. + * 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 database connection and cleans up resources. - * @returns {void | Promise} A promise that resolves when the connection is closed. + * Closes the connection and cleans up resources. + * @returns {Promise} A promise that resolves when the connection is closed. */ - dispose?: () => void | Promise; + dispose: () => Promise; }; /** - * Represents default SQL results, including any error messages, row changes and rows returned. + * Defines a database connector for executing SQL queries and preparing statements. */ -type DefaultSQLResult = { - lastInsertRowid?: number; - changes?: number; - error?: string; - rows?: { id?: string | number; [key: string]: unknown }[]; - success?: boolean; -}; - -export interface Database - extends AsyncDisposable { - readonly dialect: SQLDialect; +export type Connector = ConnectorConnection & { + /** + * The name of the connector. + */ + name: string; /** - * Indicates whether the database instance has been disposed/closed. - * @returns {boolean} True if the database has been disposed, false otherwise. + * The SQL dialect used by the connector. */ - readonly disposed: boolean; + dialect: SQLDialect; /** - * The client instance used internally. - * @returns {Promise} A promise that resolves with the client instance. + * Whether the connector supports connection pooling. */ - getInstance: () => Promise>>; + supportsPooling: boolean; /** - * Executes a raw SQL string. - * @param {string} sql - The SQL string to execute. - * @returns {Promise} A promise that resolves with the execution result. + * The client instance used internally. */ - exec: (sql: string) => Promise; + getInstance: () => TInstance | Promise; /** - * Prepares an SQL statement from a raw SQL string. - * @param {string} sql - The SQL string to prepare. - * @returns {statement} The prepared SQL statement. + * Acquires a separate connection from the database. + * + * Will block all other queries until disposed if the connector doesn't support connection pooling. */ - prepare: (sql: string) => Statement; + acquireConnection: () => Promise; +}; + +/** + * Represents default SQL results, including any error messages, row changes and rows returned. + */ +export type DefaultSQLResult = { + lastInsertRowid?: number; + changes?: number; + error?: string; + rows?: { id?: string | number; [key: string]: unknown }[]; + success?: boolean; +}; +/** + * 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. @@ -159,16 +153,30 @@ export interface Database strings: TemplateStringsArray, ...values: Primitive[] ) => Promise; +}; + +export interface Database + extends Connection { + readonly dialect: SQLDialect; /** - * Closes the database connection and cleans up resources. - * @returns {Promise} A promise that resolves when the connection is closed. + * Indicates whether the database instance has been disposed/closed. + * @returns {boolean} True if the database has been disposed, false otherwise. */ - dispose: () => Promise; + readonly disposed: boolean; + + /** + * The client instance used internally. + * @returns {Promise} A promise that resolves with the client instance. + */ + getInstance: () => Promise>>; /** - * AsyncDisposable implementation for using syntax support. - * @returns {Promise} A promise that resolves when the connection is disposed. + * Acquires a separate connection from the database. + * + * Will block all other queries until disposed unless the library supports connection pooling. */ - [Symbol.asyncDispose]: () => Promise; + acquireConnection: ( + fn: (connection: Connection) => void | Promise, + ) => Promise; } diff --git a/test/connectors/_tests.ts b/test/connectors/_tests.ts index 7f42d784..0afc9180 100644 --- a/test/connectors/_tests.ts +++ b/test/connectors/_tests.ts @@ -1,10 +1,10 @@ -import { beforeAll, expect, it } from "vitest"; +import { beforeAll, expect, describe, 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; @@ -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(), diff --git a/test/connectors/pg-pool.test.ts b/test/connectors/pg-pool.test.ts new file mode 100644 index 00000000..b4068ff0 --- /dev/null +++ b/test/connectors/pg-pool.test.ts @@ -0,0 +1,12 @@ +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!, + }), + }); +});