node-firebird
v2.15.0
Published
Pure JavaScript and Asynchronous Firebird client for Node.js.
Readme
Pure JavaScript and Asynchronous Firebird client for Node.js

[![NPM version][npm-version-image]][npm-url] [![NPM downloads][npm-downloads-image]][npm-url] [![Mozilla License][license-image]][license-url]
Table of contents
- Installation
- Usage — including developing the driver
- Promises and async/await — the
*AsyncAPI pluswithConnection/withTransactionhelpers - Connection types — connection options,
firebird://URIs and traditional connection strings, classic connections, pooling - Database object (db) — database, transaction and statement methods/options
- Examples — parametrized queries, tagged-template queries (sql), named placeholders, nested result tables (nestTables), row-key transforms (transformKeys), result metadata / affected rows (withMeta), custom type parsers (typeCast), BLOBs, streaming big data, transactions, driver events, database events (POST_EVENT), service manager, charsets/encoding, Firebird 3.0–6.0 features
- Extensive Examples — DECFLOAT/INT128, query cancellation (AbortSignal), batch execution (bulk inserts incl. BLOBs), bulk-insert stream (batchStream), statement timeouts, scrollable cursors, RETURNING multiple rows, SKIP LOCKED, advanced pooling
- Using node-firebird with Express.js
- FAQ
- Contributing · Contributors
Community & resources
- Firebird forum on Google Groups — questions and discussion
- GitHub issues — bug reports and feature requests
- ROADMAP — planned work and protocol implementation status
- Driver internals: SRP authentication protocol · database encryption key callback
- Firebird itself: documentation · limits and data types · Twitter/X · Facebook
Installation
npm install node-firebirdThe driver is pure JavaScript at runtime — no native addons and no runtime dependencies. Node.js 20 or newer is supported (CI runs on Node 20, 22, 24 and 26 against Firebird 3, 4, 5 and 6).
Usage
CommonJS and ESM are both first-class (conditional exports):
// CommonJS
const Firebird = require('node-firebird');
// ESM — default and named imports both work
import Firebird from 'node-firebird';
import { attach, pool, GDSCode, SQL_TYPES } from 'node-firebird';The documented subpaths keep working in both module systems
(require('node-firebird/lib/gdscodes'), …).
TypeScript is fully supported — the driver itself is written in TypeScript and ships its own type declarations, with generics on the query APIs:
import * as Firebird from 'node-firebird';
import type { Options, Database } from 'node-firebird';
interface Emp { ID: number; NAME: string }
const rows = await db.queryAsync<Emp>('SELECT ID, NAME FROM EMP'); // Emp[]
db.query<Emp>('SELECT ID, NAME FROM EMP', [], (err, rows) => { /* rows: Emp[] */ });
const r = await db.queryAsync<Emp>('SELECT ...', [], { withMeta: true }); // QueryResult<Emp>Developing the driver
Since v2.4.0 the driver is written in TypeScript and compiled with the native
TypeScript 7 compiler (tsc). The published package ships both the compiled
output and the sources.
Requirements
- Node.js >= 20 (CI matrix: 20, 22, 24, 26)
- npm (TypeScript 7 and all tooling are installed as devDependencies — no global installs needed)
- a Firebird server on
127.0.0.1:3050withSYSDBA/masterkeyfor the integration tests (CI tests against Firebird 3, 4, 5 and 6-snapshot); the unit tests undertest/unit/run without a server
The quickest way to get a test server is Docker:
docker run -d --name firebird -p 3050:3050 \
-e FIREBIRD_ROOT_PASSWORD=masterkey \
firebirdsql/firebird:5Layout
src/— the TypeScript sources; this is what you editlib/— compiled CommonJS + generated.d.tsdeclarations; build output, gitignored — never edit it by handtest/— vitest suite (integration tests at the top level, server-free tests intest/unit/)
Workflow
npm install # installs deps and builds lib/ via the prepare script
npm run build # compile src/ -> lib/
npm run typecheck # type-check sources and tests without emitting
npm run lint # oxlint
npm test # build + run the vitest suite (unit + integration)Methods
Firebird.escape(value) -> return {String}- prevent for SQL InjectionsFirebird.attach(options, function(err, db))attach a databaseFirebird.create(options, function(err, db))create a databaseFirebird.attachOrCreate(options, function(err, db))attach or create databaseFirebird.pool(max, options) -> return {Object}create a connection poolingFirebird.attachAsync(options) -> Promise<Database>,createAsync,attachOrCreateAsync,dropAsync— promise counterparts, see Promises and async/await
Promises and async/await
Every callback API has a promise-returning counterpart with an Async
suffix, plus two higher-level helpers: pool.withConnection() and
db.withTransaction(). The callback API is unchanged and the two styles can
be mixed freely, though sticking to one per project keeps code readable.
const Firebird = require('node-firebird');
const pool = Firebird.pool(5, options);
// acquire → work → always release, even when `work` throws
const users = await pool.withConnection((db) =>
db.queryAsync('SELECT id, name FROM users WHERE plan = ?', ['pro'])
);
// commit on success, rollback on error
await pool.withConnection((db) =>
db.withTransaction(async (transaction) => {
await transaction.executeAsync('INSERT INTO audit (msg) VALUES (?)', ['signup']);
await transaction.executeAsync('UPDATE stats SET signups = signups + 1');
})
);
await pool.destroyAsync();Available wrappers:
- module —
Firebird.attachAsync(options),createAsync,attachOrCreateAsync,dropAsync; resolve with aDatabase(or aServiceManagerwhenoptions.manageristrue) - pool —
pool.getAsync(),pool.destroyAsync(),pool.withConnection(work) - database —
db.queryAsync(sql, params?, options?),executeAsync,executeBatchAsync(sql, rows, options?),sequentiallyAsync(sql, params?, onRow, options?),transactionAsync(options?),newStatementAsync(sql),attachEventAsync(),detachAsync(),dropAsync(),db.withTransaction(work, options?) - transaction —
queryAsync,executeAsync,executeBatchAsync,sequentiallyAsync,newStatementAsync,commitAsync,rollbackAsync,commitRetainingAsync,rollbackRetainingAsync - statement —
executeAsync(transaction, params?, options?),executeBatchAsync(transaction, rows, options?),fetchAsync,fetchScrollAsync,fetchAllAsync,closeAsync,dropAsync,releaseAsync - service manager — every Service Manager function has an
*Asynccounterpart (backupAsync,restoreAsync,getUsersAsync,addUserAsync,getFbserverInfosAsync,startTraceAsync, …); stream-producing functions resolve with theReadable, info functions with the info object
Notes:
- Rejections are always
Errorinstances carrying the usual Firebird properties (err.gdscode,err.gdsparams) — see Using GDS codes. queryAsync/executeAsyncresolve with the rows only; column metadata is currently available through the callback API only.- An un-
awaited rejected promise becomes an unhandled rejection instead of a callback error — prefer thewithConnection/withTransactionhelpers, which guarantee cleanup on every path. - TypeScript: the async methods accept a row-shape generic, e.g.
db.queryAsync<User>(sql, params)returnsPromise<User[]>.
Connection types
Connection options
Settings you leave out fall back to environment variables first — using
Firebird's own conventions (ISC_USER and ISC_PASSWORD, the same
variables isql honours) plus FIREBIRD_HOST, FIREBIRD_PORT,
FIREBIRD_DATABASE and FIREBIRD_ROLE — and to the built-in defaults
below (SYSDBA / masterkey / 127.0.0.1) only after that. Explicitly
provided options always win, so credentials can stay out of code the
same way PGUSER/PGPASSWORD work with pg.
var options = {};
options.host = '127.0.0.1';
options.port = 3050;
options.database = 'database.fdb';
options.user = 'SYSDBA';
options.password = 'masterkey';
options.lowercase_keys = false; // set to true to lowercase keys
options.role = null; // default
options.pageSize = 4096; // default when creating database
options.retryConnectionInterval = 1000; // reconnect interval in case of connection drop
options.blobAsText = false; // set to true to get blob as text, only affects blob subtype 1
options.blobChunkSize = 1024; // segment size in bytes used when WRITING blobs (default 1024, max 65535)
options.blobReadChunkSize = 1024; // buffer size in bytes requested per op_get_segment when READING blobs (default 1024, max 65535)
options.encoding = 'UTF8'; // default encoding for connection is UTF-8
options.wireCompression = false; // set to true to enable firebird compression on the wire (works only on FB >= 3 and compression is enabled on server (WireCompression = true in firebird.conf))
options.wireCrypt = Firebird.WIRE_CRYPT_ENABLE; // default; set to Firebird.WIRE_CRYPT_DISABLE to disable wire encryption (FB >= 3)
options.pluginName = undefined; // optional, auto-negotiated; can be set to Firebird.AUTH_PLUGIN_SRP256, Firebird.AUTH_PLUGIN_SRP, or Firebird.AUTH_PLUGIN_LEGACY
options.dbCryptConfig = undefined; // optional; database encryption key for encrypted databases. Use 'base64:<value>' for base64-encoded keys or plain text
options.connectTimeout = 10000; // optional; timeout in ms for a single pool.get() attach operation (default: no timeout)
options.enableKeepAlive = true; // TCP keepalive probing to detect dead/stale connections (same option names as mysql2); set to false to disable
options.keepAliveInitialDelay = 60000; // ms a socket must be idle before the first keepalive probe (ignored when enableKeepAlive is false)
options.parallelWorkers = undefined; // optional; request multiple thread workers for maintenance/index tasks (FB >= 5)
options.maxInlineBlobSize = undefined; // optional; threshold size in bytes for inline blob transmission (default 65535, FB >= 5.0.3)
options.maxNegotiatedProtocols = undefined; // optional; cap how many protocol versions are offered, oldest first (default: all, up to Protocol 20; set to 10 to stop at Protocol 19)
options.defaultSchema = undefined; // optional; sets session CURRENT_SCHEMA at connect time by putting the schema first in the search path (FB >= 6.0)
options.searchPath = undefined; // optional; ordered list/array of schemas to resolve unqualified object references (FB >= 6.0)
options.owner = undefined; // optional; owner of a newly created database — lets a superuser create a database for another user (create only, FB >= 6.0)
options.jsonAsObject = false; // optional; automatically stringify parameters and parse query results that contain JSON (FB >= 6.0)
options.namedPlaceholders = false; // set to true to allow :name placeholders in SQL with a { name: value } params object (see Named placeholders)
options.nestTables = false; // true nests object rows by source table (row[table][column]); a string separator flattens to 'table<sep>column' keys — see Nested result tables (nestTables). Overridable per query
options.transformKeys = undefined; // 'camel' (FIRST_NAME → firstName) or a (key) => key mapper for object-row keys — see Transforming row keys (transformKeys). Overridable per query
options.numericMode = Firebird.NUMERIC_MODE_LOSSY; // INT64/INT128 result policy: LOSSY (default), SAFE, or STRING
options.typeCast = undefined; // optional; custom type parser called for every result column value (see Custom type parsers)
options.statementCacheSize = 0; // optional; per-connection LRU cache of prepared statements, 0 = disabled (see Prepared-statement cache)Connection URI strings
Everywhere an options object is accepted — attach, create,
attachOrCreate, drop, Firebird.pool() and their *Async
counterparts — a firebird:// URI string works too, which is handy for
12-factor apps and containers that configure the database via a single
environment variable:
const db = await Firebird.attachAsync(process.env.DATABASE_URL);
// e.g. DATABASE_URL=firebird://SYSDBA:[email protected]:3050//var/fb/prod.fdb?encoding=UTF8
const pool = Firebird.pool(10,
'firebird://app:secret@localhost/appdb?lowercase_keys=true&idleTimeoutMillis=30000');The database part after host[:port]/ can be:
| URI | database |
| :--- | :--- |
| firebird://host/employee | the alias employee |
| firebird://host//var/fb/prod.fdb | /var/fb/prod.fdb (explicit absolute path) |
| firebird://host/var/fb/prod.fdb | /var/fb/prod.fdb (a database part with / is a path — aliases cannot contain slashes) |
| firebird://host/C:/fbdata/prod.fdb | the Windows path C:/fbdata/prod.fdb |
Query parameters map 1:1 onto the connection options above and are coerced
to the right type (?pageSize=8192&lowercase_keys=true&wireCompression=1).
Credentials and paths are URL-decoded, so reserved characters can be
percent-encoded (p%40ss for p@ss); user/password may alternatively
be passed as query parameters. IPv6 hosts use brackets:
firebird://[::1]:3050/employee. The parser is exported as
Firebird.parseConnectionUri(uri) if you need the resulting options object.
Traditional connection strings (old style)
The classic Firebird connection string format — the same
[host[/port]:]{path | alias} strings isql and the other Firebird tools
use — is accepted everywhere too:
const db = await Firebird.attachAsync('db.example.com/3051:/var/fb/prod.fdb');| Connection string | meaning |
| :--- | :--- |
| employee | the alias employee on 127.0.0.1:3050 |
| /var/fb/prod.fdb | a path on 127.0.0.1:3050 |
| db.example.com:employee | the alias employee on db.example.com:3050 |
| db.example.com/3051:/var/fb/prod.fdb | host and explicit port |
| myserver:C:\fbdata\prod.fdb | a Windows path behind a host |
| C:\fbdata\prod.fdb | a single character before : is a drive letter, not a host (same rule as Firebird) |
| [::1]/3050:employee | IPv6 hosts use brackets |
Unlike firebird:// URIs, traditional strings carry no credentials or
options — the driver defaults apply (SYSDBA/masterkey, port 3050), and
the port must be numeric (/etc/services names are not resolved). Use the
URI form or an options object when you need to set anything else.
Firebird.parseConnectionString(str) parses both forms and is what
attach/create/pool use internally for string arguments.
Classic
Firebird.attach(options, function (err, db) {
if (err) throw err;
// db = DATABASE
db.query('SELECT * FROM TABLE', function (err, result) {
// IMPORTANT: close the connection
db.detach();
});
});Pooling
// 5 = the number is count of opened sockets
var pool = Firebird.pool(5, options);
// Get a free pool
pool.get(function (err, db) {
if (err) throw err;
// db = DATABASE
db.query('SELECT * FROM TABLE', function (err, result) {
// IMPORTANT: release the pool connection
db.detach();
});
});
// Destroy pool
pool.destroy();Pool events and metrics
The pool is an EventEmitter and exposes live counters, following the
pg.Pool conventions:
const pool = Firebird.pool(10, {
...options,
idleTimeoutMillis: 30000, // close connections idle for 30s…
min: 2, // …but always keep 2 alive
connectTimeout: 5000,
maxUses: 7500, // retire a connection after 7500 checkouts (pg's maxUses)
maxLifetimeMillis: 3600000, // …or 1h after creation (Postgres.js's max_lifetime)
});
pool.on('connect', (db) => console.log('new server connection'));
pool.on('acquire', (db) => console.log('connection handed to a caller'));
pool.on('release', (db) => console.log('connection returned to the pool'));
pool.on('remove', (db) => console.log('connection closed & removed'));
pool.on('error', (err, db) => console.error('background pool error', err));
// live metrics — e.g. for a /health endpoint or periodic monitoring
console.log({
total: pool.totalCount, // physical connections (idle + in use)
idle: pool.idleCount, // available in the pool
active: pool.activeCount, // handed out to callers
waiting: pool.waitingCount, // get() calls queued for a free slot
});idleTimeoutMilliscloses connections that sat idle in the pool for that long, never shrinking belowmin— long-lived pools no longer hold every connection they ever created (issue #329). The sweep also evicts idle connections whose socket has died, so callers don't receive them after a server restart (issue #343).maxUsesandmaxLifetimeMillisrecycle physical connections: a worn-out or over-age connection is closed for good when returned to the pool (lifetime is also enforced on idle connections by the sweep, even belowmin) and a replacement is created on demand. Recycling bounds server-side resource drift on long-lived connections. Both default to 0 (off).erroris a background-error channel (idle eviction failures and the like); unlike a plainEventEmitter, it is only emitted when a listener is attached, so existing applications keep working unchanged.- Metrics are plain getters — reading them has no side effects.
Multi-host pooling (PoolCluster)
For primary/replica topologies (Firebird 4+ logical replication) or plain
redundancy, Firebird.poolCluster manages one pool per named node with
pattern-based selection and automatic failover — the mysql2 PoolCluster
model:
const cluster = Firebird.poolCluster({
defaults: { user: 'SYSDBA', password: 'masterkey', database: '/data/app.fdb', connectTimeout: 5000 },
nodes: {
primary: { host: 'db-primary' },
replica1: { host: 'db-replica-1' },
replica2: { host: 'db-replica-2' },
},
max: 4, // per-node pool size
selector: 'rr', // 'rr' | 'random' | 'order' (first online match)
removeNodeErrorCount: 5, // offline a node after N consecutive connection failures
restoreNodeTimeout: 30000, // put it back into rotation after 30s (0 = manual restore())
});
// writes go to the primary, reads round-robin across replicas
await cluster.withConnection('primary', db => db.queryAsync('UPDATE ...'));
const rows = await cluster.withConnection('replica*', db => db.queryAsync('SELECT ...'));
// or bind a pattern once (mysql2's cluster.of)
const replicas = cluster.of('replica*', 'rr');
const db = await replicas.getAsync(); // release with db.detach(), as with a plain poolA failed connection attempt marks the node and fails over to the next
matching online node; only when every candidate has failed does the call
error (set connectTimeout in defaults so dead-but-routable hosts fail
fast). Nodes taken offline emit 'offline', restorations emit
'online', and cluster.status() returns per-node
{ online, errorCount, totalCount, idleCount, activeCount, waitingCount }
for monitoring. Each node's pool is a regular
connection pool — health checks, idle
reaping, maxUses/maxLifetimeMillis recycling and keepalive all apply
per node. add(name, overrides) / remove(name) manage nodes at
runtime; destroy() closes everything.
Advanced Pooling Features
The pool implementation includes several safeguards for reliability:
- Connection Timeout: Use
options.connectTimeoutto prevent the pool from hanging if a server accepts the TCP connection but fails to respond to the Firebird wire protocol (e.g., during high load or authentication stalls). - Pool Destruction: Calling
pool.destroy()now immediately drains thependingqueue, notifying all waiting callers with an error. It also prevents any furtherpool.get()calls. - Slot Recovery: If a connection attempt times out, the pool slot is correctly freed so subsequent requests can be served. Late-arriving connections are automatically discarded to prevent resource leaks.
- Idle Reaping & Health:
idleTimeoutMillis/minshrink the pool when traffic drops and evict dead idle connections (see Pool events and metrics).
Pool Lifecycle State Diagram
stateDiagram-v2
[*] --> Active
Active --> Destroying: pool.destroy()
Destroying --> Destroyed: all connections detached
Destroyed --> [*]
state Active {
[*] --> Idle
Idle --> Creating: pool.get() [no idle db]
Creating --> InUse: attach() success
Creating --> Idle: attach() failure/timeout
Idle --> InUse: pool.get() [idle db exists]
InUse --> Idle: db.detach()
}
state Destroying {
[*] --> DrainingPending
DrainingPending --> DetachingIdle
DetachingIdle --> WaitingForInUse
WaitingForInUse --> [*]
}Connect Timeout Sequence
sequenceDiagram
participant User
participant Pool
participant Firebird
User->>Pool: pool.get()
Pool->>Pool: increment _creating
Pool->>Pool: start timer (connectTimeout)
Pool->>Firebird: attach(options)
Note over Firebird: Server accepts TCP but hangs
Pool-->>Pool: timer expires
Pool->>Pool: decrement _creating
Pool-->>User: callback(Error: Connection timeout)
Note over Firebird: Server eventually responds
Firebird-->>Pool: attach callback(db)
Pool->>Firebird: db.detach() (discard late connection)Database object (db)
Database Methods
db.query(query, [params], function(err, result), options)- classic query, returns Array of Objectdb.execute(query, [params], function(err, result), options)- classic query, returns Array of Arraydb.executeBatch(query, rows, function(err, result), options)- bulk execution in one round-trip, all-or-nothing (FB >= 4.0, see Batch Execution)db.sequentially(query, [params], function(row, index), function(err), options)- sequentially querydb.detach(function(err))detach a databasedb.transaction(options, function(err, transaction))create transactiondb.createTablespace(name, filePath, function(err, result))- Create a physical tablespace (FB >= 6.0)db.alterTablespace(name, filePath, function(err, result))- Alter an existing tablespace physical location (FB >= 6.0)db.dropTablespace(name, function(err, result))- Drop a tablespace (FB >= 6.0)db.createSchema(schemaName, [tablespaceName], function(err, result))- Create a schema/namespace, optionally binding it to a tablespace (FB >= 6.0)
Transaction options
const options = {
autoCommit: false,
autoUndo: true,
isolation: Firebird.ISOLATION_READ_COMMITTED,
ignoreLimbo: false,
readOnly: false,
wait: true,
waitTimeout: 0,
};Transaction methods
transaction.query(query, [params], function(err, result), options)- classic query, returns Array of Objecttransaction.execute(query, [params], function(err, result), options)- classic query, returns Array of Arraytransaction.executeBatch(query, rows, function(err, result), options)- bulk execution with per-record errors (FB >= 4.0, see Batch Execution)transaction.sequentially(query, [params], function(row, index), function(err), options)- sequentially querytransaction.commit(function(err))commit current transactiontransaction.rollback(function(err))rollback current transaction
Statement options
const options = {
timeout: 1000, // Statement timeout in ms, default is 0 (no timeout)
}Examples
Parametrized Queries
Parameters
Firebird.attach(options, function (err, db) {
if (err) throw err;
// db = DATABASE
db.query(
'INSERT INTO USERS (ID, ALIAS, CREATED) VALUES(?, ?, ?) RETURNING ID',
[1, "Pe'ter", new Date()],
function (err, result) {
console.log(result[0].id);
db.query(
'SELECT * FROM USERS WHERE Alias=?',
['Peter'],
function (err, result) {
console.log(result);
db.detach();
}
);
}
);
});Tagged-template queries (sql)
db.sql / transaction.sql offer a Postgres.js-style tagged-template API
on top of the regular parameter machinery. Interpolated values are bound
as positional parameters — never concatenated into the SQL — so the API is
injection-safe by construction:
const id = 2;
const rows = await db.sql`SELECT NAME FROM EMP WHERE ID = ${id}`;
// → executes SELECT NAME FROM EMP WHERE ID = ? with params [2]The returned query is a lazy thenable: it runs when awaited (or via
.then/.catch/.finally), exactly once. Until then it can be embedded
in another tag as a fragment, splicing its text and parameters in
place:
const filter = db.sql`DEPT_ID = ${1} AND ACTIVE = ${true}`;
const rows = await db.sql`SELECT * FROM EMP WHERE ${filter} ORDER BY ID`;Arrays expand to placeholder lists (for IN), and calling the tag with a
string produces a safely quoted, dot-qualified identifier for dynamic
table/column names:
await db.sql`SELECT * FROM EMP WHERE ID IN (${[1, 2, 3]})`;
const col = 'NAME';
await db.sql`SELECT ${db.sql(col)} FROM ${db.sql('S1.EMP')}`;
// → SELECT "NAME" FROM "S1"."EMP".options({...}) attaches per-query options (timeout, signal,
nestTables, …), .withMeta() executes resolving the
full result object, and
.toQuery() returns the compiled { text, params } without executing —
handy for logging and tests:
const r = await db.sql`UPDATE EMP SET ACTIVE = false WHERE ID = ${9}`.withMeta();
// r.affectedRows === 1Sharp edges, made loud instead of silent: a query executes once, in the
shape of its first consumer — consuming it again in the other shape
(plain await after .withMeta(), or vice versa) throws, as does
.options() after execution. Interpolating an empty array throws
(it would compile to invalid SQL like IN ()), and circular fragments
are rejected instead of overflowing the stack. The compiled text is
positional-only, so the named placeholders
rewriter is disabled for tagged queries — PSQL :variable references
in an EXECUTE BLOCK template are safe even with namedPlaceholders:
true on the connection.
Named placeholders
With the namedPlaceholders: true connection option, SQL may use :name
markers and parameters may be passed as a values-by-name object instead of
a positional array. The rewrite happens client-side before the statement is
prepared, so it works on every Firebird version; positional ? arrays keep
working unchanged on the same connection.
const db = await Firebird.attachAsync({ ...options, namedPlaceholders: true });
// or: firebird://user:pass@host/db?namedPlaceholders=true
const rows = await db.queryAsync(
'SELECT * FROM USERS WHERE ALIAS = :alias AND CREATED > :since',
{ alias: 'Peter', since: new Date(2026, 0, 1) });
// A name may repeat — it binds once per occurrence:
await db.queryAsync(
'SELECT * FROM T WHERE A = :v OR B = :v', { v: 42 });
// Batch rows can be objects too (Firebird 4.0+):
await db.executeBatchAsync(
'INSERT INTO USERS (ID, ALIAS) VALUES (:id, :alias)',
[{ id: 1, alias: 'a' }, { id: 2, alias: 'b' }]);Placeholders inside string literals ('...'), quoted identifiers ("..."),
comments and q'{...}' alternative literals are left untouched. A key
present with value null binds SQL NULL; a missing key raises
Missing value for named placeholder(s): ....
The scanner has no SQL grammar, so inside an EXECUTE BLOCK body every
PSQL :variable reference looks like a placeholder too. The option is
therefore off by default — and can be disabled for a single statement with
the per-query option:
await db.queryAsync(execBlockSql, [], { namedPlaceholders: false });Nested result tables (nestTables)
In a JOIN, columns sharing a name overwrite each other in object rows —
SELECT EMP.ID, DEPT.ID ... leaves only one ID key. The nestTables
option (same as mysql2's) qualifies row keys by source table instead. It is
accepted at connection level and per query; the per-query value wins.
With nestTables: true each row nests one sub-object per table:
const rows = await db.queryAsync(
'SELECT EMP.ID, EMP.NAME, DEPT.ID, DEPT.NAME FROM EMP JOIN DEPT ON DEPT.ID = EMP.DEPT_ID',
[], { nestTables: true });
// rows[0] = { EMP: { ID: 10, NAME: 'Ada' }, DEPT: { ID: 1, NAME: 'Engineering' } }With a string separator the keys stay flat but qualified:
const rows = await db.queryAsync(sql, [], { nestTables: '_' });
// rows[0] = { EMP_ID: 10, EMP_NAME: 'Ada', DEPT_ID: 1, DEPT_NAME: 'Engineering' }The table qualifier is the query's relation alias when one is used, the table name otherwise — so self-joins nest cleanly:
const rows = await db.queryAsync(
'SELECT E.NAME, B.NAME FROM EMP E LEFT JOIN EMP B ON B.ID = E.BOSS_ID',
[], { nestTables: true });
// rows[0] = { E: { NAME: 'Grace' }, B: { NAME: 'Ada' } }Expression columns (no source table) qualify as '', exactly like mysql2:
they land under the '' key when nesting (row[''].ANSWER) and get the
bare separator prefix in separator mode (row._ANSWER) — always prefixing
keeps qualified keys collision-free (a bare expression alias could
otherwise collide with a real table<sep>column key). Keys honour
lowercase_keys, and the option composes with
typeCast, blobAsText and queryStream. Object rows only: db.execute
array rows are positional and need no qualification. Works on every
supported Firebird version (the source-table metadata comes from the
statement describe, available since Firebird 2.0).
Transforming row keys (transformKeys)
transformKeys rewrites object-row keys — the counterpart of
Postgres.js's transform. The built-in 'camel' maps FIRST_NAME →
firstName; a custom (key) => key mapper gives full control. Accepted
at connection level and per query (per-query wins):
const rows = await db.queryAsync('SELECT EMP_ID, FIRST_NAME FROM EMP_INFO', [],
{ transformKeys: 'camel' });
// rows[0] = { empId: 1, firstName: 'Ada' }The transform runs after lowercase_keys and applies to both parts of
nestTables keys (row.e.empId).
Column metadata — withMeta fields and the typeCast hook — keeps
the raw server aliases. A custom mapper that throws falls back to the
untransformed key (with a console warning) rather than corrupting the
row decode.
Result metadata and affected rows (withMeta)
By default, queries deliver bare rows and DML row counts are not reported.
The per-query withMeta: true option switches the result (callback and
promise APIs alike) to a full result object, the counterpart of pg's
{ rows, rowCount, fields } and mysql2's affectedRows:
const r = await db.queryAsync('UPDATE EMP SET ACTIVE = false WHERE DEPT_ID = ?', [1],
{ withMeta: true });
// r = {
// rows: undefined, // rows array (SELECT), row object (RETURNING), or undefined
// fields: [...], // per-column metadata (see below)
// affectedRows: 2, // what the server actually changed
// recordCounts: { selectCount: 0, insertCount: 0, updateCount: 2, deleteCount: 0 },
// warnings: [], // isc_arg_warning entries from the execute response
// }For DML (INSERT/UPDATE/DELETE, including ... RETURNING and
EXECUTE PROCEDURE), affectedRows is the server-reported count
(isc_info_sql_records) and recordCounts breaks it down per verb — this
costs one extra lightweight info request per statement, which is why the
option is opt-in. For SELECT, affectedRows is simply the number of rows
returned (pg's rowCount convention) with no extra round-trip, and
recordCounts is absent.
Each entry in fields describes one output column — the same vocabulary
the typeCast hook receives, plus
nullability and the relation alias/schema:
{ type: 448, typeName: 'VARYING', subType: 4, scale: 0, length: 80,
nullable: true, field: 'NAME', relation: 'EMP', relationAlias: '',
relationSchema: 'PUBLIC', alias: 'NAME' }(relationSchema is filled on Firebird 6.0+; subType of a text column is
its character-set id.) In TypeScript, queryAsync<T>(sql, params,
{ withMeta: true }) resolves to QueryResult<T> automatically. Server
warnings attached to any response — not just queries — are also emitted as
'warning' driver events.
The option is honoured by query/execute (and their *Async wrappers),
on databases and transactions alike. It is ignored by the streaming APIs
(sequentially, queryStream — rows bypass the result there) and by
executeBatch (which has its own completion shape). For EXECUTE
PROCEDURE, affectedRows reflects DML the procedure performed — a
procedure that only returns values reports 0 alongside its row.
Fixed-point numeric results (numericMode)
Firebird sends BIGINT, INT128, and the NUMERIC/DECIMAL types backed by
them as signed integer coefficients plus a decimal scale. JavaScript numbers
cannot represent every INT64 or INT128 coefficient exactly. The connection
option numericMode controls how those result values are exposed:
| Mode | Result policy |
| :--- | :--- |
| Firebird.NUMERIC_MODE_LOSSY | INT64-backed values are returned as number; INT128 uses a mixed number/string path. Unsafe coefficients may lose precision. |
| Firebird.NUMERIC_MODE_SAFE | Safe coefficients are returned as number; unsafe coefficients as exact scaled string. |
| Firebird.NUMERIC_MODE_STRING | All values are returned as exact scaled string. |
LOSSY decodes INT64-backed values through JavaScript Number. INT128 uses a
mixed number/string decoding path. For coefficients outside JavaScript's safe
integer range, the result type can depend on the Firebird wire type and value,
and numeric precision is not guaranteed. LOSSY remains the default so that
adding numericMode does not silently change result types for applications
upgrading from earlier node-firebird releases.
SAFE tests the raw integer coefficient against JavaScript's inclusive safe
range (Number.MIN_SAFE_INTEGER through Number.MAX_SAFE_INTEGER) before
applying its scale. STRING provides a stable result type and retains zeroes
implied by the declared scale:
const db = await Firebird.attachAsync({
...options,
numericMode: Firebird.NUMERIC_MODE_STRING,
});
// BIGINT 42 -> '42'
// DECIMAL coefficient 420000,-4 -> '42.0000'The string literals 'lossy', 'safe', and 'string' are accepted too,
including in connection URIs (?numericMode=safe). NULL remains null in
every mode. The option does not change FLOAT, DOUBLE, DECFLOAT, or input
parameter encoding. A SAFE result returned as a number still has the normal
IEEE-754 behavior of JavaScript fractional numbers; use STRING when the
decimal representation itself must remain exact.
Custom type parsers (typeCast)
The typeCast connection option lets you override how column values are
decoded, per SQL type or per column — the same idea as mysql2's typeCast
and pg's setTypeParser. The hook is called for every column value of
every result row (including NULLs); whatever it returns becomes the
value in the row. Call next() to get the value the driver would produce
by default (after blobAsText / jsonAsObject are applied).
const Firebird = require('node-firebird');
Firebird.attach({
...options,
typeCast: (column, next) => {
// dates as ISO strings instead of Date objects
if (column.typeName === 'DATE') {
const v = next();
return v === null ? null : v.toISOString().slice(0, 10);
}
return next(); // everything else: default decoding
},
}, (err, db) => { /* ... */ });column describes the result column:
| Property | Meaning |
| :--------- | :------------------------------------------------------------------ |
| type | Firebird SQL type code — compare against Firebird.SQL_TYPES.* |
| typeName | Friendly name: 'VARYING', 'INT64', 'DATE', 'BLOB', ... |
| subType | Column subtype (1 = text for BLOBs) |
| scale | Negative decimal scale for NUMERIC/DECIMAL (e.g. -2) |
| length | Declared length in bytes |
| field | Column name in the table |
| relation | Table name |
| alias | SELECT-list alias (the row key for object rows) |
Notes:
- The hook runs after
numericMode. CallingString(next())cannot recover digits already lost by lossy numeric decoding; selectSAFEorSTRINGwhen exact coefficients matter. - Non-text BLOB columns reach the hook as the usual asynchronous fetch
function; text BLOBs with
blobAsText: truereach it as the resolved string. - The hook must be a pure function of its inputs: when a response spans multiple TCP packets the affected rows can be decoded more than once, calling the hook again for the same value.
- The hook runs for every value on the hot row-decoding path — keep it
cheap, and prefer dispatching on
column.type/column.typeNameearly. - Exceptions thrown by the hook are caught: the default value is used and a warning is printed. A throw cannot be allowed to escape into the wire decoder, so validate inside the hook and encode failures in the value.
Prepared-statement cache
Setting statementCacheSize keeps a per-connection LRU cache of prepared
statements (like mysql2's statement cache): running the same SQL string
again transparently reuses the already-prepared server-side statement,
skipping the prepare round-trip. No API changes are needed — db.query,
tx.query, sequentially, executeBatch and the *Async wrappers all
benefit automatically.
Firebird.attach({ ...options, statementCacheSize: 100 }, (err, db) => {
// the second identical query reuses the prepared statement
db.query('SELECT * FROM t WHERE id = ?', [1], () => {
db.query('SELECT * FROM t WHERE id = ?', [2], () => { /* ... */ });
});
});How it works:
- The number is the maximum of idle statements kept per connection; the least-recently-used statement is dropped when the limit is exceeded.
- A cached statement leaves the cache while in use, so concurrent runs of the same SQL never share a server-side cursor — extra preparations run in parallel and only one goes back into the cache.
- Statements that failed and DDL statements are never cached.
- Cache keys are exact SQL strings (after the
namedPlaceholdersrewrite), so use parametrized queries to get hits. - The legacy
cacheQuery: true/maxCachedQueryoptions remain supported and now map onto the same LRU cache (with a default limit of 100 instead of the old unbounded map).
Note (DDL): a statement prepared before a metadata change (e.g.
ALTER TABLE) may fail when reused. If you mix DDL with hot queries on the same connection, keep the cache small or disabled.
Streaming rows with queryStream
db.queryStream(sql, params, options) returns an object-mode
Readable emitting one row per chunk — the counterpart of
pg-query-stream and mysql2's .stream(). It is built on
sequentially()'s backpressure: fetching from the server pauses while
the stream's buffer is full and resumes as the consumer drains it, so
constant memory is used regardless of the result size.
const { pipeline } = require('stream/promises');
// async iteration
for await (const row of db.queryStream('SELECT * FROM big_table')) {
console.log(row.ID);
}
// or piping into any Writable/Transform (HTTP response, CSV encoder, ...)
await pipeline(
db.queryStream('SELECT * FROM big_table WHERE grp = ?', [42]),
myCsvTransform,
res);db.queryStreamruns in its own transaction (likedb.query);transaction.queryStreamruns inside your transaction, which is not committed when the stream ends.- Destroying the stream early — including an error mid-
pipeline()— aborts the fetch and releases the statement; the connection stays usable. - Options: everything
queryaccepts (e.g.signal), plushighWaterMark(rows buffered before fetching pauses, default 16) andasObject: falsefor array rows. - Rows go through the regular decode path, so
typeCast,blobAsTextandjsonAsObjectall apply.
Tablespaces and Schema Partitioning (Firebird 6.0+)
For Firebird 6.0+ (Protocol 20+), you can create and manage physical tablespace locations and logical schema namespaces, optionally partitioning schemas into specific physical tablespaces.
Firebird.attach(options, function (err, db) {
if (err) throw err;
// 1. Create a physical tablespace mapping to a physical storage location
db.createTablespace('FAST_TS', '/ssd/fast_data.ts', function (err, result) {
if (err) throw err;
console.log('Tablespace FAST_TS created successfully');
// 2. Create a schema namespace and partition it into the FAST_TS tablespace
db.createSchema('MYSCHEMA', 'FAST_TS', function (err, result) {
if (err) throw err;
console.log('Schema MYSCHEMA partitioned to FAST_TS');
// 3. Drop tablespace when no longer needed
// db.dropTablespace('FAST_TS', function (err, result) { ... });
db.detach();
});
});
});Native JSON Data Type Support (Firebird 6.0+)
By enabling the jsonAsObject connection parameter, the driver will automatically serialize JavaScript objects/arrays passed as query parameters to JSON strings, and automatically parse returned JSON text/BLOB columns back into JavaScript objects/arrays.
const options = {
// ...other connection options
jsonAsObject: true,
blobAsText: true // recommended to read text BLOBs as strings
};
Firebird.attach(options, function (err, db) {
if (err) throw err;
const data = { name: 'Alice', age: 30, roles: ['admin', 'user'] };
db.query(
'INSERT INTO USERS (ID, PROFILE_JSON) VALUES (?, ?)',
[1, data],
function (err, result) {
if (err) throw err;
db.query(
'SELECT PROFILE_JSON FROM USERS WHERE ID = ?',
[1],
function (err, result) {
if (err) throw err;
// PROFILE_JSON is automatically parsed back to a JavaScript object
console.log(result[0].profile_json); // { name: 'Alice', age: 30, roles: ['admin', 'user'] }
db.detach();
}
);
}
);
});SQL-Standard ROW Type (Firebird 6.0+)
Firebird 6.0+ supports the SQL-standard ROW type representing composite records / tuples (e.g. ROW(id INT, name VARCHAR(20))). Since the database server compiles row value expressions into individual scalar columns/parameters at the wire interface, you can pass individual parameters or tuple arrays natively:
Firebird.attach(options, function (err, db) {
if (err) throw err;
// Use a row value expression / tuple comparison
db.query(
'SELECT * FROM USERS WHERE (ID, NAME) = (ROW(?, ?))',
[1, 'Alice'],
function (err, rows) {
if (err) throw err;
console.log(rows);
db.detach();
}
);
});For PSQL block declarations (triggers, procedures), you can declare and use ROW/RECORD variables (such as DECLARE VARIABLE myrow ROW(id INT, name VARCHAR(20))) directly within the compiled SQL strings executed via db.query or db.execute.
BLOB (stream)
Firebird.attach(options, function (err, db) {
if (err) throw err;
// db = DATABASE
// INSERT STREAM as BLOB
db.query(
'INSERT INTO USERS (ID, ALIAS, FILE) VALUES(?, ?, ?)',
[1, 'Peter', fs.createReadStream('/users/image.jpg')],
function (err, result) {
// IMPORTANT: close the connection
db.detach();
}
);
});BLOB (buffer)
Firebird.attach(options, function (err, db) {
if (err) throw err;
// db = DATABASE
// INSERT BUFFER as BLOB
db.query(
'INSERT INTO USERS (ID, ALIAS, FILE) VALUES(?, ?, ?)',
[1, 'Peter', fs.readFileSync('/users/image.jpg')],
function (err, result) {
// IMPORTANT: close the connection
db.detach();
}
);
});Reading Blobs (Asynchronous)
Firebird.attach(options, function (err, db) {
if (err) throw err;
// db = DATABASE
db.query('SELECT ID, ALIAS, USERPICTURE FROM USER', function (err, rows) {
if (err) throw err;
// first row
rows[0].userpicture(function (err, name, e) {
if (err) throw err;
// +v0.2.4
// e.pipe(writeStream/Response);
// e === EventEmitter
e.on('data', function (chunk) {
// reading data
});
e.on('end', function () {
// end reading
// IMPORTANT: close the connection
db.detach();
});
});
});
});Reading Multiples Blobs (Asynchronous)
Firebird.attach(options, (err, db) => {
if (err) throw err;
db.transaction(Firebird.ISOLATION_READ_COMMITTED, (err, transaction) => {
if (err) {
throw err;
}
transaction.query('SELECT FIRST 10 * FROM JOB', (err, result) => {
if (err) {
transaction.rollback();
return;
}
const arrBlob = [];
for (const item of result) {
const fields = Object.keys(item);
for (const key of fields) {
if (typeof item[key] === 'function') {
item[key] = new Promise((resolve, reject) => {
// the same transaction is used (better performance)
// this is optional
item[key](transaction, (error, name, event, row) => {
if (error) {
return reject(error);
}
// reading data
let value = '';
event.on('data', (chunk) => {
value += chunk.toString('binary');
});
event.on('end', () => {
resolve({ value, column: name, row });
});
});
});
arrBlob.push(item[key]);
}
}
}
Promise.all(arrBlob)
.then((blobs) => {
for (const blob of blobs) {
result[blob.row][blob.column] = blob.value;
}
transaction.commit((err) => {
if (err) {
transaction.rollback();
return;
}
db.detach();
console.log(result);
});
})
.catch((err) => {
transaction.rollback();
});
});
});
});Optimizing BLOB Read/Write Chunk Sizes
When working with large blobs (especially over remote or high-latency connections), you can configure the chunk/segment sizes to minimize the number of network round-trips:
- blobChunkSize: The segment size in bytes used when writing blobs (default:
1024, maximum:65535). - blobReadChunkSize: The buffer size in bytes requested per segment read operation when reading blobs (default:
1024, maximum:65535).
For example, setting blobReadChunkSize: 65535 requests 64KB segments at a time, resulting in up to 64x fewer network packets/round-trips when reading large blobs.
var options = {
host: '127.0.0.1',
port: 3050,
database: 'database.fdb',
user: 'SYSDBA',
password: 'masterkey',
blobChunkSize: 65535, // Minimize write round-trips
blobReadChunkSize: 65535 // Minimize read round-trips
};
Firebird.attach(options, function (err, db) {
if (err) throw err;
// Insert/Read operations will use the configured 64KB chunk sizes
db.detach();
});Streaming a big data
db.query / db.execute buffer the entire result set into memory as an array before your callback runs — fine for small/medium results, but a poor fit for big tables or unbounded exports (this is what causes "Sequential heap limit / allocation failed"-style errors on very large result sets). db.sequentially / transaction.sequentially stream rows one at a time to an on(row, index) callback instead: node-firebird never accumulates the rows itself, so memory use stays flat regardless of table size (the rows argument passed to the completion callback is always []).
Firebird.attach(options, function (err, db) {
if (err) throw err;
// db = DATABASE
db.sequentially(
'SELECT * FROM BIGTABLE',
function (row, index) {
// EXAMPLE
stream.write(JSON.stringify(row));
},
function (err) {
// END
// IMPORTANT: close the connection
db.detach();
}
);
});Backpressure
Rows are still fetched from the server in batches (200 rows per round-trip) as fast as your on callback returns. If you're forwarding each row to something that can fall behind (an HTTP response, a file write stream, a rate-limited API), declare on with a third next parameter — or return a Promise — and node-firebird will wait for you to call it (or for the promise to resolve) before fetching or processing the next row:
db.sequentially(
'SELECT * FROM BIGTABLE',
function (row, index, next) {
// Only ask for the next row once the downstream write has drained.
if (outputStream.write(JSON.stringify(row) + '\n')) {
next();
} else {
outputStream.once('drain', next);
}
},
function (err) {
db.detach();
}
);Do / Don't
- Do use
sequentiallyfor large tables, full-table exports, or any query whose row count you can't bound in advance. - Do use the 3-arg
on(row, index, next)form (or return aPromisefromon) when writing rows to something that applies its own backpressure, so unprocessed rows can't pile up faster than they're consumed. - Don't use
db.query/db.executefor big or unbounded result sets — both build the full array in memory before your callback ever runs. - Don't assume the 2-arg
on(row, index)form throttles you — it only guarantees node-firebird itself won't buffer rows; if your handler does async work without waiting on it (e.g. fire-and-forget writes), buffering can still build up on the consumer side.
Transactions
Transaction types:
Firebird.ISOLATION_READ_UNCOMMITTEDFirebird.ISOLATION_READ_COMMITTEDFirebird.ISOLATION_REPEATABLE_READFirebird.ISOLATION_SERIALIZABLEFirebird.ISOLATION_READ_COMMITTED_READ_ONLY
Firebird.attach(options, function (err, db) {
if (err) throw err;
// db = DATABASE
db.transaction(
Firebird.ISOLATION_READ_COMMITTED,
function (err, transaction) {
transaction.query(
'INSERT INTO users VALUE(?,?)',
[1, 'Janko'],
function (err, result) {
if (err) {
transaction.rollback();
return;
}
transaction.commit(function (err) {
if (err) transaction.rollback();
else db.detach();
});
}
);
}
);
});Savepoints
transaction.savepoint(work) runs work inside a savepoint (Firebird
1.5+): on resolve the savepoint is released, on reject the transaction
rolls back to the savepoint — undoing only work's changes — and the
error is rethrown while the transaction itself stays usable. Calls nest;
names are generated automatically. This is the counterpart of
Postgres.js's sql.savepoint() and mirrors db.withTransaction's style:
await db.withTransaction(async (tx) => {
await tx.sql`INSERT INTO ORDERS VALUES (${1}, ${'paid'})`;
try {
await tx.savepoint(async () => {
await tx.sql`INSERT INTO AUDIT VALUES (${1}, ${'optional enrichment'})`;
await maybeFailingStep(tx);
});
} catch (err) {
// the AUDIT insert is undone; the ORDERS insert survives and the
// transaction continues toward commit
}
});If the rollback-to itself fails (e.g. the connection died), the original
error is still thrown, with the rollback failure attached as
err.savepointRollbackError. A release failure does not roll the
work back — only a work failure does.
Note: do not run sibling savepoints concurrently on one transaction (
Promise.all): Firebird'sRELEASE SAVEPOINTalso releases every savepoint created after it, so interleaved siblings would release each other. Nested (awaited) savepoints are fine.
Driver Events
Driver events are synchronous notifications emitted on the Database object for connection-level operations. Subscribe with db.on(eventName, handler).
Firebird.attach(options, function (err, db) {
if (err) throw err;
db.on('attach', function () {
// fired once the database is attached
});
db.on('detach', function (isPoolConnection) {
// isPoolConnection === Boolean
});
db.on('reconnect', function () {
// fired after the driver reconnects a dropped socket
});
db.on('error', function (err) {
// connection-level errors (socket errors, closed connection, etc.).
// Delivered to listeners only: without one, background failures (e.g.
// a failed automatic reconnect) are NOT re-thrown as uncaught
// exceptions — the operations they affect still receive the error
// through their own callbacks/promises.
});
db.on('transaction', function (options) {
// fired when a transaction is started (before server response)
// options === resolved transaction options object
});
db.on('commit', function () {
// fired when a transaction commit is sent
});
db.on('rollback', function () {
// fired when a transaction rollback is sent
});
db.on('query', function (sql) {
// fired with the SQL string when a statement is prepared
});
db.on('row', function (row, index, isObject) {
// fired for each row decoded during a fetch
// index === Number, isObject === Boolean
});
db.on('result', function (rows) {
// fired with the full rows array once all rows are fetched
// rows === Array
});
db.on('warning', function (warning) {
// fired (next tick) for every isc_arg_warning the server attaches to a
// successful response — e.g. "parallel workers value capped" when
// parallelWorkers exceeds the server maximum. The listener above is
// registered inside the attach callback and still catches attach-time
// warnings, because emission is deferred by one tick.
// warning === { gdscode: Number, params?: Array, message: String }
});
db.detach();
});Firebird Database Events (POST_EVENT)
Firebird database events are asynchronous notifications triggered by POST_EVENT inside PSQL
triggers or stored procedures. They travel over a separate "aux" connection (opened via
db.attachEvent()) and are managed through a FbEventManager instance.
Firebird.attach(options, function (err, db) {
if (err) throw err;
// 1. Open the aux event connection and get a FbEventManager
db.attachEvent(function (err, evtmgr) {
if (err) throw err;
// 2. Subscribe to one or more named events (names must match POST_EVENT('name') in your
// PSQL triggers/procedures). Resolves once op_que_events is acknowledged by the server.
evtmgr.registerEvent(['MY_EVENT'], function (err) {
if (err) throw err;
// 3. Listen for POST_EVENT notifications
evtmgr.on('post_event', function (name, count) {
// name === event name string (e.g. 'MY_EVENT')
// count === cumulative trigger count since last notification
});
});
// 4. Unsubscribe from one or more events. Passing all currently registered names cancels
// the subscription (sends op_cancel_events); the manager re-subscribes automatically if
// other event names remain registered.
// evtmgr.unregisterEvent(['MY_EVENT'], function (err) { ... });
// 5. Inspect the current subscription state for debugging: returns
// { state, hasActiveSubscription, registeredEvents, eventId,
// isEventConnectionOpen, isDatabaseConnectionClosed }.
// state is one of 'IDLE' (aux connection open, no active subscription),
// 'SUBSCRIBED' (op_que_events acknowledged) or 'CLOSED'.
// const state = evtmgr.getState();
// 6. Release the aux connection when done. Cancels any active subscription first, then
// gracefully closes the aux socket.
// evtmgr.close(function (err) { ... });
});
});Escaping Query values
var sql1 = 'SELECT * FROM TBL_USER WHERE ID>' + Firebird.escape(1);
var sql2 = 'SELECT * FROM TBL_USER WHERE NAME=' + Firebird.escape("Pe'er");
var sql3 =
'SELECT * FROM TBL_USER WHERE CREATED<=' + Firebird.escape(new Date());
var sql4 = 'SELECT * FROM TBL_USER WHERE NEWSLETTER=' + Firebird.escape(true);
// or db.escape()
console.log(sql1);
console.log(sql2);
console.log(sql3);
console.log(sql4);Using GDS codes
var { GDSCode } = require('node-firebird/lib/gdscodes');
/*...*/
db.query(
'insert into my_table(id, name) values (?, ?)',
[1, 'John Doe'],
function (err) {
if (err.gdscode == GDSCode.UNIQUE_KEY_VIOLATION) {
console.log('constraint name:' + err.gdsparams[0]);
console.log('table name:' + err.gdsparams[0]);
/*...*/
}
/*...*/
}
);Service Manager functions
- backup
- restore
- fixproperties
- serverinfo
- database validation
- commit transaction
- rollback transaction
- recover transaction
- database stats
- users infos
- user actions (add modify remove)
- get firebird file log
- tracing
// each row : fctname : [params], typeofreturn
var fbsvc = {
"backup" : { [ "options"], "stream" },
"nbackup" : { [ "options"], "stream" },
"restore" : { [ "options"], "stream" },
"nrestore" : { [ "options"], "stream" },
"setDialect": { [ "database","dialect"], "stream" },
"setSweepinterval": { [ "database","sweepinterval"], "stream" },
"setCachebuffer" : { [ "database","nbpagebuffers"], "stream" },
"BringOnline" : { [ "database"], "stream" },
"Shutdown" : { [ "database","shutdown","shutdowndelay","shutdownmode"], "stream" },
"setShadow" : { [ "database","activateshadow"], "stream" },
"setForcewrite" : { [ "database","forcewrite"], "stream" },
"setReservespace" : { [ "database","reservespace"], "stream" },
"setReadonlyMode" : { [ "database"], "stream" },
"setReadwriteMode" : { [ "database"], "stream" },
"validate" : { [ "options"], "stream" },
"commit" : { [ "database", "transactid"], "stream" },
"rollback" : { [ "database", "transactid"], "stream" },
"recover" : { [ "database", "transactid"], "stream" },
"getStats" : { [ "options"], "stream" },
"getLog" : { [ "options"], "stream" },
"getUsers" : { [ "username"], "object" },
"addUser" : { [ "username", "password", "options"], "stream" },
"editUser" : { [ "username", "options"], "stream" },
"removeUser" : { [ "username","rolename"], "stream" },
"getFbserverInfos" : { [ "options", "options"], "object" },
"startTrace" : { [ "options"], "stream" },
"suspendTrace" : { [ "options"], "stream" },
"resumeTrace" : { [ "options"], "stream" },
"stopTrace" : { [ "options"], "stream" },
"getTraceList" : { [ "options"], "stream" },
"hasActionRunning" : { [ "options"], "object"}
}
Every function also has a promise-returning *Async counterpart (no
callback argument): stream-producing functions resolve with the
Readable, info functions with the info object.
const svc = await Firebird.attachAsync({ ...options, manager: true });
try {
const info = await svc.getFbserverInfosAsync();
console.log(info.fbversion);
const backup = await svc.backupAsync({
database: '/DB/MYDB.FDB',
files: [{ filename: '/DB/MYDB.FBK' }]
});
for await (const line of backup) console.log(line);
} finally {
await svc.detachAsync();
}Backup Service example
const options = {...}; // Classic configuration with manager = true
Firebird.attach(options, function(err, svc) {
if (err)
return;
svc.backup(
{
database:'/DB/MYDB.FDB',
files: [
{
filename:'/DB/MYDB.FBK',
sizefile:'0'
}
]
},
function(err, data) {
data.on('data', line => console.log(line));
data.on('end', () => svc.detach());
}
);
});Restore Service example
const config = {...}; // Classic configuration with manager = true
const RESTORE_OPTS = {
database: 'database.fdb',
files: ['backup.fbk']
};
Firebird.attach(config, (err, srv) => {
srv.restore(RESTORE_OPTS, (err, data) => {
data.on('data', () => {});
data.on('end', () =>{
srv.detach();})
});
});getLog and getFbserverInfos Service examples with use of stream and object return
fb.attach(_connection, function (err, svc) {
if (err) return;
// all function that return a stream take two optional parameter
// optread => byline or buffer byline use isc_info_svc_line and buffer use isc_info_svc_to_eof
// buffersize => is the buffer for service manager it can't exceed 8ko (i'm not sure)
svc.getLog({ optread: 'buffer', buffersize: 2048 }, function (err, data) {
// data is a readablestream that contain the firebird.log file
console.log(err);
data.on('data', function (data) {
console.log(data.toString());
});
data.on('end', function () {
console.log('finish');
});
});
// an other exemple to use function that return object
svc.getFbserverInfos(
{
dbinfo: true,
fbconfig: true,
svcversion: true,
fbversion: true,
fbimplementation: true,
fbcapatibilities: true,
pathsecuritydb: true,
fbenv: true,
fbenvlock: true,
fbenvmsg: true,
},
{},
function (err, data) {
console.log(err);
console.log(data);
}
);
});Character Set & Encoding Support
Node-Firebird defaults to UTF-8 for database connections, but fully supports custom client character sets. You can set the connection encoding by specifying options.encoding (e.g. 'UTF8', 'WIN1252', 'ISO8859_1', 'LATIN1', 'ASCII', or 'NONE').
Commonly used Firebird character sets are handled through the corresponding Node.js encoding or ICU codec:
| Firebird Character Set | Node.js encoding / ICU codec | Description / Notes |
| ---------------------- | ---------------------------- | ------------------- |
| UTF8, UNICODE_FSS | utf8 | Unicode. Handles character-level truncation automatically based on charset width. |
| WIN1252 | ICU windows-1252 codec | Windows Western European encoding, including the printable characters in bytes 0x80–0x9F. |
| ISO8859_1, LATIN1 | latin1 | ISO-8859-1-compatible byte mapping; intentionally distinct from Windows-1252. |
| ASCII | ascii | 7-bit ASCII. |
| NONE | latin1 | Raw/unspecified character set. Treated as binary-safe 8-bit characters. |
Beyond Node's native encodings, the driver ships codepage codecs for the
single-byte charsets (decode and encode — columns, parameters, SQL
literals and blobAsText blobs all transcode):
WIN1250–WIN1258(Central European, Cyrillic, Greek, Turkish, Hebrew, Arabic, Baltic, Vietnamese),ISO8859_2–ISO8859_9,ISO8859_13,KOI8R,KOI8U,DOS866
const options = { /* ... */ encoding: 'WIN1251' };
await db.queryAsync('INSERT INTO T VALUES (?)', ['Привет']); // encoded as cp1251The codecs are built from Node's ICU tables at first use (present in every offici

