Connection pooling
import { createPool } from '@fast-firebird/core';
const pool = await createPool({ host, database, user, password, min: 2, max: 10 });
const rows = await pool.query('select * from users where id = ?', [1]); // acquire→run→release
const one = await pool.queryOne('select * from users where id = ?', [1]);
await pool.transaction(async (tx) => { /* … */ });
await pool.use(async (conn) => { /* borrow explicitly */ });
await pool.close(); // or: await using pool = await createPool({ … })Each pooled connection is a full Attachment with its own statement cache, so warm statements survive across acquire/release. Acquire is LIFO — the most recently released (warmest) connection is handed out next, so consecutive checkouts hit the same hot statement set. The pool enforces max concurrency, validates connections with op_ping on borrow, evicts idle connections down to min, and times out acquire when saturated.
Statement cache vs. DDL: statementCache: 'clear-on-release'
A live prepared statement — cached or pinned — makes its attachment refuse to yield the relation locks of the tables it references, so DDL from any other connection blocks for its whole lock wait and fails with object … is in use. For pools that must coexist with schema changes (apps that run their own migrations, admin tools), opt into:
const pool = await createPool({ …, statementCache: 'clear-on-release' });Every release then frees the connection's cached statement handles, which flips the attachment to yield on demand: a waiting DDL (any wait, including the driver's 10 s default) proceeds in milliseconds. Two honest caveats: a wait: false DDL never asks the holder to yield and may still see object … is in use (only disconnection releases locks unconditionally), and every statement re-prepares on its next checkout — keep the default 'keep' for hot OLTP pools and use the explicit pool.clearStatementCaches() before planned migrations instead.
DDL executed through the pool needs no ceremony: the driver clears a connection's own cache automatically after any DDL statement it runs, and an attachment's own statements never block its own DDL.
The pool also exposes executeBatch directly:
await pool.executeBatch('insert into t (a) values (?)', rows);Parallel work: pool.map
For parallel work, pool.map runs a function over items across connections with bounded concurrency (results in input order):
const parts = await pool.map(idRanges, (conn, range) =>
conn.query('select * from big where id between ? and ?', [range.lo, range.hi]),
{ concurrency: 4 },
);(A lazy Blob handle is bound to the connection+transaction that produced it, so parallelize by running the query per partition — not by sharing handles.)