- 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
- 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
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).
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>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)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
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[]>.
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.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)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:masterkey@db.example.com: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.
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.
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();
});
});// 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();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.
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.
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).
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 --> [*]
}
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)
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)
const options = {
autoCommit: false,
autoUndo: true,
isolation: Firebird.ISOLATION_READ_COMMITTED,
ignoreLimbo: false,
readOnly: false,
wait: true,
waitTimeout: 0,
};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
const options = {
timeout: 1000, // Statement timeout in ms, default is 0 (no timeout)
}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();
}
);
}
);
});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.
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 });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).
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.
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.
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);
}
// BIGINT columns as strings
if (column.type === Firebird.SQL_TYPES.SQL_INT64 && !column.scale) {
return String(next());
}
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:
- 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.
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.
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.
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();
});
});
});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();
}
);
}
);
});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.
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();
}
);
});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();
}
);
});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();
});
});
});
});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();
});
});
});
});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();
});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();
}
);
});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 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.
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();
});
}
);
}
);
});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 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 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) { ... });
});
});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);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]);
/*...*/
}
/*...*/
}
);- 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();
}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());
}
);
});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();})
});
});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);
}
);
});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 automatically mapped to their corresponding Node.js Buffer encodings:
| Firebird Character Set | Node.js Buffer Encoding | Description / Notes |
|---|---|---|
UTF8, UNICODE_FSS |
utf8 |
Unicode. Handles character-level truncation automatically based on charset width. |
WIN1252, ISO8859_1, LATIN1 |
latin1 |
8-bit European encodings. Safely decodes special accented characters. |
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
official Node build). attachOrCreate/create honour options.encoding
for the new database's default charset too. Accented characters and
fixed-length CHAR(N) whitespace/truncation are handled automatically per
the charset width — and single-byte columns (including charset NONE) are
readable in full under the default UTF8 connection (the declared fetch
lengths are widened per the charset-width ratio, fixing the
string right truncation errors of issue #422).
var options = {
host: '127.0.0.1',
port: 3050,
database: 'win1252_db.fdb',
user: 'SYSDBA',
password: 'masterkey',
encoding: 'WIN1252' // Automatically maps to 'latin1' under the hood
};
Firebird.attach(options, function (err, db) {
if (err) throw err;
// Writes 'Ç Ã É Ú Ñ' correctly using Windows-1252 encoding
db.query('INSERT INTO ACCENTED_TEST (ID, NAME) VALUES (?, ?)', [1, 'Ç Ã É Ú Ñ'], function (err) {
if (err) throw err;
db.query('SELECT NAME FROM ACCENTED_TEST WHERE ID = 1', function (err, rows) {
if (err) throw err;
console.log(rows[0].name); // 'Ç Ã É Ú Ñ' (perfectly decoded)
db.detach();
});
});
});Firebird 3.0 wire protocol versions 14 and 15 are now supported, including:
- Srp256 authentication (SHA-256) — preferred by default, alongside Srp (SHA-1) and Legacy_Auth
- Wire encryption (Arc4/RC4) — enabled by default via
wireCrypt - Wire compression — supported for protocol version 13+ (set
wireCompression: true) - Database encryption callback — support for encrypted databases via
dbCryptConfigoption
No server-side configuration changes are required for Firebird 3.0 with default settings.
Firebird.attach({
host: '127.0.0.1',
port: 3050,
database: '/path/to/db.fdb',
user: 'SYSDBA',
password: 'masterkey',
wireCrypt: Firebird.WIRE_CRYPT_ENABLE, // default, can set WIRE_CRYPT_DISABLE
pluginName: Firebird.AUTH_PLUGIN_SRP256, // optional, auto-negotiated
}, function(err, db) {
if (err) throw err;
// ...
db.detach();
});For encrypted databases, provide the encryption key via the dbCryptConfig option:
Firebird.attach({
host: '127.0.0.1',
database: '/path/to/encrypted.fdb',
user: 'SYSDBA',
password: 'masterkey',
dbCryptConfig: 'base64:bXlTZWNyZXRLZXkxMjM0NTY=', // base64-encoded key
// or dbCryptConfig: 'myPlainTextKey' // plain text key (UTF-8 encoded)
}, function(err, db) {
if (err) throw err;
// ...
db.detach();
});Notes:
- The
dbCryptConfigvalue can be prefixed withbase64:for base64-encoded keys - Plain text values are encoded as UTF-8
- Empty or undefined values send an empty response to the callback
- This feature requires Firebird 3.0.1+ (protocol 14/15) for encrypted databases
Firebird 4.0+ wire protocol (versions 16 and 17) is fully supported, including:
- Protocol versions 16 and 17 — Full support for Firebird 4.0+ and 5.0+ wire protocols (automatic fallback/negotiation).
- DECFLOAT data types — Production-ready support for
DECFLOAT(16)(Decimal64, 8 bytes) andDECFLOAT(34)(Decimal128, 16 bytes) complying with the full IEEE 754-2008 standard using BID (Binary Integer Decimal) encoding. Supports special values such asNaN,+Infinity,-Infinity,+0, and-0. - INT128 data type — Native support for 128-bit integers using Node.js
BigInt. - Statement Timeout — Support for query and statement-level execution timeouts (Protocol 16+).
- Time Zone Support — Native support for
TIME WITH TIME ZONEandTIMESTAMP WITH TIME ZONE(represented as JavaScriptDateobjects). - Extended metadata identifiers — Support for identifiers up to 63 characters.
No configuration changes are required for Firebird 4.0 or 5.0 servers. The driver will automatically negotiate the best protocol version supported by both the client and server.
Firebird.attach({
host: '127.0.0.1',
port: 3050,
database: '/path/to/fb4.fdb',
user: 'SYSDBA',
password: 'masterkey',
}, function(err, db) {
if (err) throw err;
// DECFLOAT and INT128 types are automatically supported
db.query('SELECT CAST(123.456 AS DECFLOAT(16)) AS df16, CAST(9876543210 AS INT128) AS i128 FROM RDB$DATABASE', function(err, result) {
console.log(result); // { df16: 123.456, i128: 9876543210n }
db.detach();
});
});Columns of type TIMESTAMP WITH TIME ZONE and TIME WITH TIME ZONE are automatically mapped to JavaScript Date objects. Values are read as UTC and represented in the local timezone of the Node.js process.
// Select timezone columns
db.query('SELECT TS_TZ_COL, T_TZ_COL FROM FB4_TABLE', function(err, result) {
console.log(result[0].ts_tz_col); // JavaScript Date object
});
// Insert using Date objects
db.query('INSERT INTO FB4_TABLE (TS_TZ_COL) VALUES (?)', [new Date()], function(err) {
// ...
});For legacy Firebird 4 servers with SRP authentication only, use the following configuration in firebird.conf:
AuthServer = Srp256, Srp
WireCrypt = EnabledFor more details see:
- Firebird 3 release notes — new authentication
- Firebird 4 release notes — Srp256
- Firebird 4 release notes — DECFLOAT
- Firebird 4 migration guide — authorization
- Firebird 5 migration guide — authorization
Firebird.attach({
host: '127.0.0.1',
database: '/path/to/fb4.fdb',
user: 'SYSDBA',
password: 'masterkey',
}, function(err, db) {
if (err) throw err;
// Insert DECFLOAT and INT128 types
db.query(
'INSERT INTO INVENTORY (ID, PRICE, SERIAL_NUMBER) VALUES (?, ?, ?)',
[1n, '12.34567890123456', 987654321098765432109876543210n],
function(err) {
if (err) throw err;
// Select them back
db.query('SELECT PRICE, SERIAL_NUMBER FROM INVENTORY WHERE ID = 1', function(err, result) {
if (err) throw err;
console.log(typeof result[0].price); // 'string' (e.g. '12.34567890123456')
console.log(typeof result[0].serial_number); // 'bigint' (e.g. 987654321098765432109876543210n)
db.detach();
});
}
);
});Any query can be cancelled while it is executing on the server. Pass an
AbortSignal in the query options — when it fires, the driver sends an
out-of-band op_cancel packet and the running statement fails with
err.gdscode === GDSCode.CANCELLED. The connection itself stays healthy and
can run further queries immediately.
const { GDSCode } = require('node-firebird/lib/gdscodes');
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000); // or req.on('close', ...) in Express
try {
const rows = await db.queryAsync('SELECT /* expensive */ ...', [], { signal: controller.signal });
} catch (err) {
if (err.gdscode === GDSCode.CANCELLED) {
// cancelled server-side; the connection remains usable
} else if (err.name === 'AbortError') {
// the signal was already aborted — the query was never sent
} else {
throw err;
}
}The option works with the callback API as well (db.query(sql, params, cb, { signal })) and on transaction-level queries. A running operation can also
be cancelled manually from anywhere with db.cancel() / await db.cancelAsync().
Notes:
- Cancellation is per attachment (that is how the Firebird protocol defines it): it cancels whatever is currently executing on that connection. With a pool this is naturally scoped to the request holding the connection.
- A signal that is already aborted rejects immediately with an
AbortErrorwithout contacting the server; cancelling an idle connection is harmless.
executeBatch sends many parameter rows for one statement in a single
round-trip using the Firebird 4 wire batch API (op_batch_create /
op_batch_msg / op_batch_exec) — typically 5–10× faster than executing
row by row. Available on the database, transaction and statement objects, in
callback and promise flavours.
const rows = [
[1, 'Alice', 10.50, new Date(), true, 100n],
[2, 'Bob', null, new Date(), false, null], // NULLs per column
[3, 'Carol', 7.25, new Date(), true, 300n],
];
// database level: own transaction, all-or-nothing (rollback if any record fails)
const res = await db.executeBatchAsync(
'INSERT INTO t (id, name, amount, created, active, big) VALUES (?, ?, ?, ?, ?, ?)',
rows);
console.log(res.recordCount, res.updateCounts, res.success);
// transaction level: partial success — failed records are reported,
// the rest can still be committed
const tr = await db.transactionAsync();
const r = await tr.executeBatchAsync('INSERT INTO t (...) VALUES (?, ?, ?, ?, ?, ?)', rows);
if (!r.success) {
console.log('failed record indexes:', r.errorRecordNumbers); // 0-based
console.log('first error:', r.errors[0].error.message);
}
await tr.commitAsync();The result object contains recordCount, updateCounts (one entry per
record), errors ({ recordNumber, error } with full gdscode details),
errorRecordNumbers and success. At the database level a failed batch
rejects with the first record error, carrying the full completion state as
err.batchCompletion.
Options (last argument): chunkSize (rows per op_batch_msg packet,
default 500), multiError (default true — collect all record errors
instead of stopping at the first), bufferSize (server-side batch buffer
in bytes).
Notes:
- Requires wire protocol 16+ (Firebird 4.0 or newer server).
- Values are encoded from the statement's own parameter metadata, so
NUMERIC/DECIMAL scale,
BIGINT/INT128(passBigInt),BOOLEAN,TIMESTAMP/DATE/TIME,FLOAT/DOUBLEandDECFLOATall round-trip exactly. BLOBcolumns accept Buffers, strings, JSON-able objects, or pre-created blob quad ids: values are uploaded as transaction blobs first — all initiated back-to-back so the blob ops pipeline on the wire — and the batch messages reference their ids.ARRAYparameters are not supported.- Oversized
CHAR/VARCHARvalues fail the batch client-side before anything is sent; server-side record errors (constraint violations, truncation…) are reported per record.
db.batchStream(sql, options) is the COPY FROM analogue: an object-mode
Writable that flushes parameter-array rows in chunks through one
prepared statement using the batch API. The Database form runs its own
transaction — committed on finish, rolled back on error or destroy,
all-or-nothing for the whole stream:
const { pipeline } = require('stream/promises');
const stream = db.batchStream('INSERT INTO EVENTS VALUES (?, ?, ?)', {
flushRows: 1000, // rows buffered per batch flush (default 1000)
});
await pipeline(mySourceOfRowArrays, stream); // e.g. a CSV parser
console.log(stream.recordCount, stream.affectedRows); // totals after 'finish'Backpressure is the Writable machinery itself: writes pause while a
chunk is in flight, so an arbitrarily large source never accumulates in
memory beyond flushRows. BLOB columns accept Buffers/strings per the
batch rules above. transaction.batchStream(sql, options) runs inside an
existing transaction and leaves commit/rollback to you. The remaining
options (chunkSize, bufferSize, …) pass through to
executeBatch.
Setting a statement timeout allows the client to automatically abort queries that take too long on the server.
Firebird.attach(options, function(err, db) {
if (err) throw err;
// Specify a statement-level execution timeout of 1000ms
db.query(
'SELECT * FROM MY_LARGE_TABLE',
[],
function(err, result) {
if (err) {
if (err.message.includes('timeout')) {
console.error('Query timed out!');
} else {
console.error('Error:', err);
}
}
db.detach();
},
{ timeout: 1000 } // timeout option passed to query/execute
);
});Firebird 5.0 introduced native support for scrollable cursors, enabling bi-directional result set traversal on the server side. You can execute a statement with { scrollable: true } and navigate with statement.fetchScroll().
db.transaction(function(err, tx) {
tx.newStatement('SELECT ID, VAL FROM MY_TABLE ORDER BY ID', function(err, statement) {
if (err) throw err;
// Execute the statement and request a scrollable cursor on the server
statement.execute(tx, [], function(err) {
if (err) throw err;
// 1. Fetch the first row
statement.fetchScroll(tx, 'FIRST', 0, 1, function(err, res) {
console.log('First:', res.data); // Row 1
// 2. Fetch the next row
statement.fetchScroll(tx, 'NEXT', 0, 1, function(err, res) {
console.log('Next:', res.data); // Row 2
// 3. Fetch the prior row
statement.fetchScroll(tx, 'PRIOR', 0, 1, function(err, res) {
console.log('Prior:', res.data); // Row 1 again
// 4. Fetch the absolute 3rd row
statement.fetchScroll(tx, 'ABSOLUTE', 3, 1, function(err, res) {
console.log('Absolute 3rd:', res.data); // Row 3
statement.release();
tx.commit();
db.detach();
});
});
});
});
}, { scrollable: true });
});
});Supported directions are: 'NEXT' (0), 'PRIOR' (1), 'FIRST' (2), 'LAST' (3), 'ABSOLUTE' (4), and 'RELATIVE' (5).
In Firebird 5.0, DML statements like UPDATE, DELETE, and INSERT ... SELECT with a RETURNING clause can return multiple rows. When executing these statements, the driver receives an array of objects representing all the affected rows:
db.query(
'UPDATE MY_TABLE SET VAL = VAL || \'!\' WHERE ID > 1 RETURNING ID, VAL',
[],
function(err, rows) {
if (err) throw err;
console.log(rows); // Array of updated rows: [{ id: 2, val: 'two!' }, { id: 3, val: 'three!' }]
}
);Firebird 5.0 supports the SKIP LOCKED clause with SELECT ... WITH LOCK, UPDATE, and DELETE statements. This allows transactions to skip rows currently locked by other transactions instead of waiting or raising lock conflict errors, making it ideal for concurrency queues:
// Selects unlocked rows, skipping any locked by concurrent processes
db.query(
'SELECT * FROM QUEUE_TASK WHERE STATUS = \'PENDING\' WITH LOCK SKIP LOCKED',
[],
function(err, result) {
if (err) throw err;
console.log(result);
}
);var pool = Firebird.pool(10, {
host: '127.0.0.1',
database: 'db.fdb',
user: 'SYSDBA',
password: 'masterkey',
connectTimeout: 5000 // 5 seconds connect timeout for pool.get()
});
// Retrieve a connection
pool.get(function(err, db) {
if (err) {
console.error('Could not get connection from pool:', err);
return;
}
db.query('SELECT * FROM TABLE', function(err, result) {
// Return connection back to the pool
db.detach();
});
});
// Close all pool connections and reject pending requests
process.on('SIGTERM', function() {
pool.destroy();
});node-firebird works well with Express, but because the driver is connection/pool based rather than an ORM with automatic connection management, a few request-lifecycle patterns keep connections from leaking under load. This section documents the recommended architecture, referenced from the roadmap.
The driver now ships native
pool.withConnection()anddb.withTransaction()helpers (see Promises and async/await) that supersede the hand-rolledwithConnection/transactionalhelpers shown in the examples below. The examples remain valid and show what the helpers do under the hood.
Create a single Firebird.pool(...) when the app boots and reuse it for the lifetime of the process. Do not call Firebird.pool() or Firebird.attach() inside a request handler — that opens a new socket (or an entire new pool) on every request and will exhaust server and database resources under load.
// db.js
const Firebird = require('node-firebird');
const options = {
host: '127.0.0.1',
database: '/var/lib/firebird/data/app.fdb',
user: 'SYSDBA',
password: 'masterkey',
connectTimeout: 5000,
};
// Size the pool to your expected concurrency, not to the number of requests.
const pool = Firebird.pool(10, options);
module.exports = pool;// app.js
const express = require('express');
const pool = require('./db');
const app = express();
// ...routes...
const server = app.listen(3000);
// Close the pool (and reject any in-flight pool.get() calls) on shutdown.
process.on('SIGTERM', function () {
server.close(function () {
pool.destroy();
});
});Every request that touches the database must release its connection back to the pool exactly once, on every code path: success, query error, or a synchronous throw. There is no native finally across async callbacks, so a small helper plays that role:
// withConnection.js
function withConnection(pool, handler) {
return function (req, res, next) {
pool.get(function (err, db) {
if (err) return next(err);
var finished = false;
// Safe to call more than once; only the first call has any effect.
function done(err) {
if (finished) return;
finished = true;
db.detach();
if (err) next(err);
}
try {
handler(db, req, res, done);
} catch (err) {
done(err);
}
});
};
}
module.exports = withConnection;app.get('/users', withConnection(pool, function (db, req, res, done) {
db.query('SELECT ID, ALIAS FROM USERS', function (err, rows) {
if (err) return done(err);
res.json(rows);
done();
});
}));For write endpoints, acquire a connection and start a transaction in middleware, expose it as req.tx, and let the route handler decide whether to commit or roll back:
function transactional(pool) {
return function (req, res, next) {
pool.get(function (err, db) {
if (err) return next(err);
db.transaction(function (err, tx) {
if (err) {
db.detach();
return next(err);
}
req.tx = tx;
// Call on success — commits, releases the connection, then responds.
req.commit = function (payload) {
tx.commit(function (err) {
db.detach();
if (err) return next(err);
res.json(payload);
});
};
// Call on failure — rolls back, releases the connection, then
// forwards the error to Express's error-handling middleware.
req.rollbackOnError = function (err) {
tx.rollback(function () {
db.detach();
next(err);
});
};
next();
});
});
};
}
app.post('/orders', transactional(pool), function (req, res, next) {
req.tx.query(
'INSERT INTO ORDERS (CUSTOMER, TOTAL) VALUES (?, ?) RETURNING ID',
[req.body.customer, req.body.total],
function (err, result) {
if (err) return req.rollbackOnError(err);
req.commit(result);
}
);
});Firebird errors surface as a plain Error with .message, .gdscode, and .gdsparams (see lib/callback.js). Map the codes you care about — exported as named constants from node-firebird/lib/gdscodes — to HTTP status codes in your error-handling middleware, and log the original error server-side only. Don't forward err.message as-is to clients: it can contain SQL text, table/column names, or file paths.
const { GDSCode } = require('node-firebird/lib/gdscodes');
function mapFirebirdErrorToStatus(err) {
switch (err.gdscode) {
case GDSCode.UNIQUE_KEY_VIOLATION:
return 409; // Conflict
case GDSCode.LOCK_CONFLICT:
return 423; // Locked
default:
return 500;
}
}
// Express error-handling middleware (register last, with 4 arguments).
app.use(function (err, req, res, next) {
console.error(err); // full detail goes to server logs only
res.status(mapFirebirdErrorToStatus(err)).json({
error: err.gdscode === GDSCode.UNIQUE_KEY_VIOLATION ? 'Duplicate record' : 'Request failed',
});
});Blob columns come back from db.query/transaction.query as functions (see Reading Blobs). Call the column function to get an EventEmitter you can .pipe() straight into res, and release the connection once streaming ends, errors, or the client disconnects early.
app.get('/users/:id/picture', withConnection(pool, function (db, req, res, done) {
db.query(
'SELECT USERPICTURE FROM USERS WHERE ID = ?',
[req.params.id],
function (err, rows) {
if (err) return done(err);
if (!rows.length) {
res.sendStatus(404);
return done();
}
rows[0].userpicture(function (err, name, blobStream) {
if (err) return done(err);
res.type('application/octet-stream');
// done() is idempotent — whichever of these fires first releases
// the connection; the rest are no-ops.
blobStream.once('end', function () { done(); });
blobStream.once('error', done);
res.once('close', function () { done(); });
blobStream.pipe(res);
});
}
);
}));Answers to recurring questions from the issue tracker.
Text comes back as ������ with a WIN1250/1251/1253/1257 database (issue #319)
Resolved — the driver now ships codepage codecs for the single-byte
charsets (WIN1250–WIN1258, ISO8859_2–9/13, KOI8R/KOI8U,
DOS866): just set the matching connection encoding and both reads and
writes transcode correctly, including parameters, SQL literals and
blobAsText blobs:
const options = { /* ... */ encoding: 'WIN1253' };See § Character Set & Encoding Support.
For a charset outside that list (e.g. DOS437/DOS850), the
iconv-lite escape hatch still
works: connect with encoding: 'NONE', read raw bytes via latin1 in a
typeCast hook and decode with the real
codepage; write already-encoded bytes as Buffer parameters.
Yes — LIST() is plain SQL and needs no special driver support. The error happens because LIST() returns a text blob (subtype 1), and blob columns come back from db.query/transaction.query as async reader functions bound to the transaction the query ran in (see Reading Blobs). Calling that function without a transaction — or with a different one — is what throws "no database to handle".
The simplest fix is blobAsText: true, which returns subtype-1 blobs as plain strings automatically, no callback needed:
const options = { /* ...other options... */ blobAsText: true };
Firebird.attach(options, function (err, db) {
if (err) throw err;
db.query('SELECT LIST(RDB$FIELD_NAME) AS COLUMNS FROM RDB$RELATION_FIELDS', function (err, rows) {
if (err) throw err;
console.log(rows[0].columns); // plain string, not a function
db.detach();
});
});If you need to read it manually instead, pass the same transaction the query used:
db.transaction(function (err, tx) {
tx.query('SELECT LIST(RDB$FIELD_NAME) AS COLUMNS FROM RDB$RELATION_FIELDS', function (err, rows) {
rows[0].columns(tx, function (err, name, e) {
let collected = '';
e.on('data', (chunk) => (collected += chunk));
e.on('end', () => console.log(collected));
});
});
});No. node-firebird negotiates the highest protocol version both the client and server support — up to Protocol 20 (Firebird 6.0) by default; see Protocol Implementation Status in the roadmap for the full version table. Servers ignore protocol versions they do not know, so offering the full list is safe on old servers too. To cap negotiation at an older protocol, limit how many versions are offered (the list is ordered oldest first):
options.maxNegotiatedProtocols = 10; // stop at Protocol 19 (pre-Firebird 6 behavior)Historical note: Protocol 20 used to be excluded by default because of a query-preparation hang — the client did not send the p_sqlst_flags field that protocol 20 added to op_prepare_statement, leaving the server blocked mid-packet. The field is now sent and Protocol 20 is fully negotiated.
This is almost always network round-trips, not the database. By default, blobs are streamed in small (1024-byte) segments; for a multi-megabyte file over a non-local connection that's thousands of round-trips. Raise blobChunkSize / blobReadChunkSize (up to the protocol max of 65535) to fetch/send far larger segments per round-trip — see Optimizing BLOB Read/Write Chunk Sizes:
options.blobChunkSize = 65535;
options.blobReadChunkSize = 65535;If your server and client are on the same host, this won't matter much — the slowdown is latency-bound, not throughput-bound.
Set options.encoding — no source changes required (see Character Set & Encoding Support for the full mapping table):
options.encoding = 'WIN1252'; // or 'ISO8859_1', 'LATIN1', 'ASCII', 'NONE'options.encoding must match the character set the database itself was created with. If you set an encoding that doesn't match the database's charset, Firebird will reject transliteration of characters that don't exist in both charsets with an error like Cannot transliterate character between character sets — that's the server protecting you from silent data corruption, not a driver bug. When in doubt, check the database's RDB$CHARACTER_SETS / connection charset rather than guessing.
If your database uses charset NONE (no transliteration at all — the server stores whatever bytes it's given) and you need full control over the byte encoding yourself, pass a Buffer instead of a string for text-column parameters. node-firebird writes Buffer values through unchanged, bypassing Node's UTF-8 string handling entirely — combine with a transliteration library like iconv-lite to produce the bytes:
const iconv = require('iconv-lite');
options.encoding = 'NONE';
db.query(
'INSERT INTO NOTES (ID, BODY) VALUES (?, ?)',
[1, iconv.encode('Café', 'win1252')], // pre-encoded bytes, written as-is
function (err) { /* ... */ }
);SELECT * FROM ACTORS WHERE NAME LIKE 'James Wick%' -- NAME is VARCHAR(10), literal is 11 chars
On some Firebird server versions, the optimizer infers a literal's type from the column it's compared against and can raise a truncation error at compile time if an inline string literal is longer than the column's declared length — even though the same query works fine as a parameterized query. This is server-side SQL compilation behavior, not something node-firebird controls; it did not reproduce against the Firebird 6.0 server used by this project's own test suite, so it appears to be limited to certain older engine versions/optimizer paths.
The reliable fix, which also happens to be the general best practice (see Parametrized Queries), is to pass the pattern as a parameter instead of inlining it:
// Instead of embedding the literal in the SQL text:
db.query("SELECT * FROM ACTORS WHERE NAME LIKE 'James Wick%'", ...);
// Bind it as a parameter:
db.query('SELECT * FROM ACTORS WHERE NAME LIKE ?', ['James Wick%'], function (err, rows) {
if (err) throw err;
console.log(rows);
});attach() sometimes fails with gdscode 335544472 ("Your user name and password are not defined") even though the credentials are correct
If the failure is intermittent — the same code with the same credentials succeeds on most attempts and fails on others — update the driver: versions before 2.8.1 had two serialization mismatches in the SRP (Srp/Srp256/384/512) proof-of-password computation that made roughly 1 in 80 attaches fail with exactly this error (whenever the ephemeral SRP values happened to have a leading zero byte). Fixed in 2.8.1; see issue #421 and issue #347. Switching the user to Legacy_UserManager/Legacy_Auth "fixed" it on older versions only because that avoids the SRP code path entirely — with 2.8.1+ this workaround is no longer needed.
If the failure is consistent, the credentials really don't match an account for the authentication plugin in use: check AuthServer/UserManager in firebird.conf and remember that SRP and Legacy user managers keep separate password stores — a user created under one plugin does not automatically exist for the other.
Contributions are welcome — code, documentation, and bug reports alike.
- Found a bug or have a question? Open a GitHub issue. Please include your Node.js version, your Firebird server version, and a minimal reproduction (SQL plus code).
- Want to write code? See Developing the driver for the build and test setup, and the ROADMAP for ideas on what to pick up. Pull requests against
mastershould come with tests and passnpm test,npm run typecheckandnpm run lint. - Improving the documentation is just as valuable — clearer examples and FAQ entries help everyone.
node-firebird was created by Henri Gourvest and is maintained by Popa Marius Adrian, with major early contributions from Peter Širka (pooling, sequential selects, driver events).
- Henri Gourvest — author, https://github.com/hgourvest
- Popa Marius Adrian — maintainer, https://github.com/mariuz
- Peter Širka — https://github.com/petersirka
…and many more — see the full list of contributors.

