Prepared statements & the statement cache
Every connection keeps an LRU cache of prepared statements keyed by SQL text (statementCacheSize, default 64; 0 disables). Measured round trips, asserted by integration tests on FB 3/4/5/6:
| Operation (inside an open transaction) | Round trips |
|---|---|
| cold query (prepare → rows) | 2 |
| warm query, one batch | 1 (execute + fetch coalesced) |
| warm DML including affected count | 1 (execute + counts coalesced) |
For hot paths you can also pin a statement explicitly:
await using stmt = await db.prepare('select name from users where id = ?');
const rows = await stmt.query([42], tx); // 1 round trip per execution
const one = await stmt.queryOne([42], tx);Prepared statements also expose executeBatch — repeat batches skip the prepare round trip entirely:
const stmt = await db.prepare('insert into t (a, b) values (?, ?)');
await stmt.executeBatch(rowsA);
await stmt.executeBatch(rowsB);
await stmt.close();Metadata-lock caveat (standard Firebird behavior)
A live prepared statement — cached or pinned — makes its attachment refuse to yield the relation locks of the tables it references, so DDL on those tables from other connections blocks for its whole lock wait and fails with object … is in use. (With the driver's default 10 s lock wait that's a clear error; under Firebird's native unbounded wait it reads as a hang.) Freeing the statements flips the attachment to yielding on demand: a waiting DDL then proceeds in milliseconds — though a wait: false DDL never asks the holder to yield and may still see object … is in use.
What releases the handles:
- DDL executed through the same connection needs nothing — the driver clears that connection's cache automatically after any DDL it runs, and an attachment's own statements never block its own DDL.
db.clearStatementCache()/pool.clearStatementCaches()release them explicitly before external migrations.createPool({ statementCache: 'clear-on-release' })frees them at every pool release — see Connection pooling.- Disconnecting releases everything unconditionally (the pool's
idleTimeoutMseviction bounds pin lifetime on idle members).