FirebirdDialect) is implemented as a SQL-generation layer: it extendsPostgresDialect to reuse its query-builder scaffolding (WHERE/ORDER BYinclude handling, CTEs, index DDL, etc.)| Property | Value |
|---|---|
| Dialect name | firebird |
| Extends | PostgresDialect (postgres) |
| Driver | pg |
| Cloud SDK | none |
| Default port | 3050 |
| Local runtime | self-hosted; no image in this repo |
query() stubbed, so no livenode-firebird) to actually execute these statements. The connectionPostgresDialect and is not a Firebird wireconstructor(config: FirebirdDialectOptions = {}) {
super({ port: 3050, ...config });
}FirebirdDialectOptions is identical in shape to PostgresDialectOptions —$ npm install ts-prorm-orm pg$ pnpm add ts-prorm-orm pg$ yarn add ts-prorm-orm pg$ bun add ts-prorm-orm pgpg is the same driver the postgres dialect uses. pg-copy-streams is optional: only the features that need it load it.[!CAUTION] A SQL-generation layer
The driver the catalog lists,pg, is the one the parent dialect loads; it does not speak Firebird's own wire protocol, so connecting to a live server is unverified. The builders — everything under Generated SQL — are what this dialect is for. See Caveats.
import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'firebird',
host: 'localhost',
database: 'app',
username: 'SYSDBA',
password: process.env.DB_PASSWORD,
});import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'firebird',
url: 'firebird://SYSDBA:secret@localhost:3050/app',
});host, port, database, username and password before the dialect is built; any of those passed alongside it wins.import { DataTypes } from 'ts-prorm-orm';
const User = db.define('User', {
id: { type: DataTypes.INTEGER(), primaryKey: true, autoIncrement: true },
email: { type: DataTypes.STRING(255), allowNull: false, unique: true },
name: { type: DataTypes.STRING(100) },
});
await db.sync(); // CREATE TABLE — see Generated SQL belowimport { Op } from 'ts-prorm-orm';
await User.create({ email: 'ada@example.com', name: 'Ada' });
const page = await User.findAll({
where: { name: { [Op.like]: 'A%' } },
order: [['id', 'ASC']],
limit: 20,
offset: 40,
});query(), captured with the driver stubbed out. The model layer inlines the values.db.sync() for the model in the Quick start:CREATE TABLEawait User.create({ email: 'ada@example.com', name: 'Ada' });
await User.update({ name: 'Grace' }, { where: { email: 'ada@example.com' } });
await User.destroy({ where: { id: 1 } });await User.upsert({ id: 1, email: 'ada@example.com', name: 'Ada Lovelace' });upsert() also reads the row by primary key before and after the write, to return the instance.await User.findAll({
where: { name: { [Op.like]: 'A%' } },
order: [['id', 'ASC']],
limit: 20,
offset: 40,
});await db.transaction(async (t) => {
await User.create({ email: 'grace@example.com', name: 'Grace' }, { transaction: t });
// A nested transaction is a SAVEPOINT; its failure undoes only its own work.
await db
.transaction(async (sp) => {
await User.destroy({ where: { id: 1 }, transaction: sp });
throw new Error('keep the row');
}, { transaction: t })
.catch(() => {});
});STRING → VARCHAR(255), auto-increment INTEGER → INTEGER GENERATED BY DEFAULT AS IDENTITY, BOOLEAN → BOOLEAN, DATE → TIMESTAMP, UUID → CHAR(36), JSON → BLOB SUB_TYPE TEXT. The type comparison lines the major engines up side by side.import { Prorm } from 'ts-prorm-orm';
const orm = new Prorm({
dialect: 'firebird',
host: 'localhost',
port: 3050, // default for the Firebird dialect; may be omitted
database: 'app',
username: 'sysdba',
password: 'masterkey',
});import { FirebirdDialect, createFirebirdDialect } from 'prorm/dialects/firebird';
const dialect = new FirebirdDialect({ host: 'localhost', database: 'app' });
// or:
const dialect2 = createFirebirdDialect({ host: 'localhost', database: 'app' });PostgresDialect, but the PostgreSQL-only parts of itsSELECT are not Firebird SQL, so they are rendered natively or refused.lock: 'UPDATE' renders Firebird's own clause, and the other lock levels andskipLocked / nowait are refused:await User.findAll({ where: { name: 'Ada' }, lock: 'UPDATE', transaction: t });Firebird does not support SKIP LOCKED with a UPDATE lock. Remove lock.skipLocked, or use a lock level that supports it.Op.contains as@>, Op.overlap as &&, ?, range operators, @@) are refused with anUnsupportedWhereError, and the statement is not sent:Cannot render WHERE condition at tags.$contains for firebird: the operator '$contains' is not implemented by its WHERE builder. The statement was not sent, because running it without the condition would match rows it should not.[!NOTE]
Before 4.9.2 this dialect sent PostgreSQL'sFOR SHARE/SKIP LOCKED
clauses and the@>/&&operators unchanged, and the server rejected
them.
FirebirdDialect extends PostgresDialect (postgres) andObject.getOwnPropertyNames(FirebirdDialect.prototype),| Area | Overridden |
|---|---|
| DML builders | buildDeleteQuery, buildInsertQuery, buildLimitOffset, buildReturningClause, buildSelectQuery, buildUpdateQuery, buildUpsertQuery |
| Types | canonicalTypeSql, ensureEnumTypes, getDataTypeSql, getFirebirdColumnSql, getIdentityColumnSql, legacyFirebirdTypeSql, mapNativeType, unsupportedType |
| DDL and schema | addColumn, buildCreateTableSQL, createExtension, createGenerator, createMaterializedView, createPolicy, createSequence, createTable, disableRLS, dropExtension, dropMaterializedView, dropPolicy, dropSequence, enableRLS, refreshMaterializedView, removeColumn |
| Introspection | hasMaterializedView, hasPolicy, hasSequence |
| Other | buildExecuteBlock, buildFirebirdConstraintSql, buildFirstSkip, genId, lockClauseSpec, mainSelectInsertIndex, nextSequenceValue, nextValueFor, unsupported, unsupportedWhereOperators |
FIRST n SKIP m (not LIMIT/OFFSET)LIMIT ... OFFSET ... clause. Instead it placesFIRST n SKIP m immediately after the SELECT keyword. The dialectbuildLimitOffset (it returns an empty string) andFIRST/SKIP fragment right after the top-level SELECT.dialect.buildSelectQuery({ tableName: 'users', limit: 10, offset: 20 });SKIP:dialect.buildSelectQuery({ tableName: 'users', limit: 5 });SELECT at parenthesis depth 0, so itinclude) and leaves CTE inner SELECTs untouched:dialect.buildSelectQuery({
tableName: 'users',
limit: 3,
cte: [{ name: 'recent', query: 'SELECT id FROM logs' }],
});
// The main SELECT gets `FIRST 3`; the CTE's inner SELECT does not.ROWS nLIMIT n). The base builder's LIMIT n fragment is rewritten in place:dialect.buildUpdateQuery('users', { name: 'Bob' }, { active: true }, { limit: 5 });
// ... ROWS 5 (no LIMIT)
dialect.buildDeleteQuery('users', { active: false }, { limit: 3 });
// ... ROWS 3 (no LIMIT)GENERATED BY DEFAULT AS IDENTITYGENERATED BY DEFAULT AS IDENTITY clauseSERIAL. Auto-increment INTEGER/BIGINT columnsgetDataTypeSql and the column/DDL builders.dialect.getDataTypeSql({ key: 'INTEGER', autoIncrement: true });
// "INTEGER GENERATED BY DEFAULT AS IDENTITY"getIdentityColumnSql builds a full identity-column definition:dialect.getIdentityColumnSql('id', { primaryKey: true });
// '"id" INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY'
dialect.getIdentityColumnSql('id', { type: 'BIGINT' });
// '"id" BIGINT GENERATED BY DEFAULT AS IDENTITY'CREATE TABLE emits identity columns (never SERIAL):dialect.buildCreateTableSQL('users', {
id: { type: { key: 'INTEGER' }, autoIncrement: true, primaryKey: true },
name: { type: { key: 'STRING', length: 100 }, allowNull: false },
});createGeneratorcreateGenerator(name) helper emits the modern, SQL-standard CREATE SEQUENCEdialect.createGenerator('order_gen');
// 'CREATE SEQUENCE "order_gen"'UPDATE OR INSERT ... MATCHINGINSERT ... ON CONFLICT. It usesUPDATE OR INSERT INTO t (...) VALUES (...) MATCHING (...), where MATCHINGbuildUpsertQuery emits exactly this form:dialect.buildUpsertQuery('users', { id: 1, name: 'Ann' }, { conflictFields: ['id'] });conflictFields is omitted, the MATCHING clause is dropped and Firebirddialect.buildUpsertQuery('users', { id: 1, name: 'Ann' });RETURNING is supported on upserts:dialect.buildUpsertQuery('users', { id: 1, name: 'Ann' },
{ conflictFields: ['id'], returning: ['id'] });
// ... MATCHING ("id") RETURNING "id"buildInsertQuery with { upsert: true } produces the same UPDATE OR INSERTINSERT:dialect.buildInsertQuery('users', { id: 1, name: 'Ann' },
{ upsert: true, conflictFields: ['id'] });
// UPDATE OR INSERT INTO "users" (...) VALUES (...) MATCHING ("id")Because UPDATE OR INSERTalways overwrites every listed column on a
match,updateOnDuplicate(a PostgreSQL-style subset of columns to update)
has no Firebird equivalent and is ignored.
INSERT is unchanged and supports RETURNING and inline Literaldialect.buildInsertQuery('users', { name: 'Ann', age: 30 });
// INSERT INTO "users" ("name", "age") VALUES ($1, $2)
dialect.buildInsertQuery('users', { name: 'Ann', created_at: new Literal('CURRENT_TIMESTAMP') });
// INSERT INTO "users" ("name", "created_at") VALUES ($1, CURRENT_TIMESTAMP)COLUMN keywordALTER TABLE omits the COLUMN keyword that PostgreSQL uses.ADD col type; dropping is DROP col:await dialect.addColumn('users', 'nickname', { type: { key: 'STRING', length: 50 } });
// ALTER TABLE "users" ADD "nickname" VARCHAR(50) (not ADD COLUMN)
await dialect.removeColumn('users', 'nickname');
// ALTER TABLE "users" DROP "nickname" (not DROP COLUMN)DEFAULT before NOT NULL (a Firebird requirement),FOREIGN KEY and CHECK table constraints are supported:Note: Firebird does not support CREATE TABLE IF NOT EXISTS, so theifNotExiststable option is ignored bybuildCreateTableSQL.
getDataTypeSql overrides the mappings that differ from PostgreSQL andORM type (key) | Firebird SQL | Notes |
|---|---|---|
STRING | VARCHAR(n) | length defaults to 255 |
CHAR | CHAR(n) | length defaults to 1 |
TEXT | BLOB SUB_TYPE TEXT | Firebird has no TEXT type |
INTEGER | INTEGER / INTEGER GENERATED BY DEFAULT AS IDENTITY | identity when autoIncrement |
BIGINT | BIGINT / BIGINT GENERATED BY DEFAULT AS IDENTITY | identity when autoIncrement |
FLOAT | FLOAT | |
DOUBLE | DOUBLE PRECISION | |
DECIMAL | DECIMAL(precision,scale) | defaults to DECIMAL(10,0) |
BOOLEAN | BOOLEAN | |
DATE | TIMESTAMP / TIMESTAMP(precision) | ORM DATE is a timestamp |
DATEONLY | DATE | |
TIME | TIME / TIME(precision) | |
BLOB | BLOB | |
JSON / JSONB | BLOB SUB_TYPE TEXT | no native JSON type |
PostgresDialect.getDataTypeSql."identifier"), which preserves case asRETURNING is supported on singleton INSERT / UPDATE / DELETE /UPDATE OR INSERT statements, so the inherited RETURNING handling is keptRETURNING * or RETURNING "col", ...).updateOnDuplicate is ignored for upserts — Firebird'sUPDATE OR INSERT overwrites every listed column (see above).tests/dialects/firebird.test.ts andtests/dialects/firebird-crud.test.ts), which stub query() and assert oncapabilitiesOf() (src/transaction-support.ts),whereSupportOf() (src/query-builders/expression-render.ts),sqlObjectCapabilitiesOf() (src/sql-objects/capabilities.ts) and the| Capability | firebird |
|---|---|
| Transactions atomic | yes |
| Savepoints | yes |
| Isolation | up to SERIALIZABLE |
| Transaction modes | read-only yes; deferrable yes; constraint deferral yes |
| RETURNING | RETURNING on INSERT |
| Upsert | a plain INSERT: the engine has no conflict clause (see the page) |
| JSON column | BLOB SUB_TYPE TEXT (JSONB → BLOB SUB_TYPE TEXT) |
| Vector column | refused — Firebird has no column type for DataTypes.VECTOR: Firebird has no vector type. |
| Array column | refused — Firebird has no column type for DataTypes.ARRAY: store the value as JSON (BLOB SUB_TYPE TEXT), or use a child table. |
| WHERE builder | 46 operator keys; $and/$or/$not; $exists: yes; $in subquery: yes; expression operands: yes |
| SQL objects | family unknown: triggers no; procedures no; functions no; materialized views no; sequences no; enums check; RLS no |
| Streaming | queryStream() present; LISTEN present; createExtension() present |
FindOptions becomes SQL