npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

Firebird Logo

[![NPM version][npm-version-image]][npm-url] [![NPM downloads][npm-downloads-image]][npm-url] [![Mozilla License][license-image]][license-url]

NPM

Table of contents

  • Installation
  • Usage — including developing the driver
  • Promises and async/await — the *Async API plus withConnection / withTransaction helpers
  • 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

Installation

npm install node-firebird

The 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:3050 with SYSDBA/masterkey for the integration tests (CI tests against Firebird 3, 4, 5 and 6-snapshot); the unit tests under test/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:5

Layout

  • src/ — the TypeScript sources; this is what you edit
  • lib/ — compiled CommonJS + generated .d.ts declarations; build output, gitignored — never edit it by hand
  • test/ — vitest suite (integration tests at the top level, server-free tests in test/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 Injections
  • Firebird.attach(options, function(err, db)) attach a database
  • Firebird.create(options, function(err, db)) create a database
  • Firebird.attachOrCreate(options, function(err, db)) attach or create database
  • Firebird.pool(max, options) -> return {Object} create a connection pooling
  • Firebird.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:

  • moduleFirebird.attachAsync(options), createAsync, attachOrCreateAsync, dropAsync; resolve with a Database (or a ServiceManager when options.manager is true)
  • poolpool.getAsync(), pool.destroyAsync(), pool.withConnection(work)
  • databasedb.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?)
  • transactionqueryAsync, executeAsync, executeBatchAsync, sequentiallyAsync, newStatementAsync, commitAsync, rollbackAsync, commitRetainingAsync, rollbackRetainingAsync
  • statementexecuteAsync(transaction, params?, options?), executeBatchAsync(transaction, rows, options?), fetchAsync, fetchScrollAsync, fetchAllAsync, closeAsync, dropAsync, releaseAsync
  • service manager — every Service Manager function has an *Async counterpart (backupAsync, restoreAsync, getUsersAsync, addUserAsync, getFbserverInfosAsync, startTraceAsync, …); stream-producing functions resolve with the Readable, info functions with the info object

Notes:

  • Rejections are always Error instances carrying the usual Firebird properties (err.gdscode, err.gdsparams) — see Using GDS codes.
  • queryAsync / executeAsync resolve 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 the withConnection / withTransaction helpers, which guarantee cleanup on every path.
  • TypeScript: the async methods accept a row-shape generic, e.g. db.queryAsync<User>(sql, params) returns Promise<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
});
  • idleTimeoutMillis closes connections that sat idle in the pool for that long, never shrinking below min — 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).
  • maxUses and maxLifetimeMillis recycle 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 below min) and a replacement is created on demand. Recycling bounds server-side resource drift on long-lived connections. Both default to 0 (off).
  • error is a background-error channel (idle eviction failures and the like); unlike a plain EventEmitter, 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 pool

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

  1. Connection Timeout: Use options.connectTimeout to 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).
  2. Pool Destruction: Calling pool.destroy() now immediately drains the pending queue, notifying all waiting callers with an error. It also prevents any further pool.get() calls.
  3. 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.
  4. Idle Reaping & Health: idleTimeoutMillis/min shrink 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 Object
  • db.execute(query, [params], function(err, result), options) - classic query, returns Array of Array
  • db.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 query
  • db.detach(function(err)) detach a database
  • db.transaction(options, function(err, transaction)) create transaction
  • db.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 Object
  • transaction.execute(query, [params], function(err, result), options) - classic query, returns Array of Array
  • transaction.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 query
  • transaction.commit(function(err)) commit current transaction
  • transaction.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 === 1

Sharp 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_NAMEfirstName; 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 metadatawithMeta 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. Calling String(next()) cannot recover digits already lost by lossy numeric decoding; select SAFE or STRING when exact coefficients matter.
  • Non-text BLOB columns reach the hook as the usual asynchronous fetch function; text BLOBs with blobAsText: true reach 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.typeName early.
  • 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 namedPlaceholders rewrite), so use parametrized queries to get hits.
  • The legacy cacheQuery: true / maxCachedQuery options 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.queryStream runs in its own transaction (like db.query); transaction.queryStream runs 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 query accepts (e.g. signal), plus highWaterMark (rows buffered before fetching pauses, default 16) and asObject: false for array rows.
  • Rows go through the regular decode path, so typeCast, blobAsText and jsonAsObject all 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 sequentially for 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 a Promise from on) 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.execute for 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_UNCOMMITTED
  • Firebird.ISOLATION_READ_COMMITTED
  • Firebird.ISOLATION_REPEATABLE_READ
  • Firebird.ISOLATION_SERIALIZABLE
  • Firebird.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's RELEASE SAVEPOINT also 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 0x800x9F. | | 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):

WIN1250WIN1258 (Central European, Cyrillic, Greek, Turkish, Hebrew, Arabic, Baltic, Vietnamese), ISO8859_2ISO8859_9, ISO8859_13, KOI8R, KOI8U, DOS866

const options = { /* ... */ encoding: 'WIN1251' };
await db.queryAsync('INSERT INTO T VALUES (?)', ['Привет']); // encoded as cp1251

The codecs are built from Node's ICU tables at first use (present in every offici