Skip to content

Connection pooling ​

ts
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:

ts
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:

ts
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):

ts
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.)

MIT Licensed. Contains data derived from the Firebird project under IDPL/IPL.