import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js"; /** * Turn a URL path component into a Firebird database identifier. * * Firebird is the awkward one — it needs either an absolute file path on the * server, a Windows drive-letter path, or an alias name. The classic URI form * uses a double-slash to keep the leading "/" of an absolute path through * URL parsing: * * firebird://host:port//firebird/data/app.fdb -> /firebird/data/app.fdb * * But that double slash is unintuitive to anyone used to the way * postgres / mysql / mssql encode the database name. We accept five * equivalent forms and normalise all of them: * * - `//abs/path/db.fdb` -> `/abs/path/db.fdb` (classic double-slash) * - `/abs/path/db.fdb` -> `/abs/path/db.fdb` (single-slash, what most people type) * - `/C:/Data/db.fdb` -> `C:/Data/db.fdb` (Windows, leading URL slash dropped) * - `/C%3A/Data/db.fdb` -> `C:/Data/db.fdb` (Windows with URL-encoded colon) * - `/employee` -> `employee` (alias — single token) * * Aliases are detected as the leftover case: a single token with no * slashes. Anything path-like is kept as a path. */ export declare function normalizeFirebirdDbIdentifier(rawPath: string): string; /** * Resolve the Firebird connection charset (php #160 / parity with the Python * master's `_resolve_firebird_charset`). * * The adapter used to pass NO charset, deferring to the driver's implicit * default, which double-encodes UTF-8 bytes stored under a legacy `NONE` * database. This resolves the charset from, in precedence order: * * 1. the connection URL query — `firebird://host:port/path?charset=NONE` * 2. an explicit `charset` on the FirebirdConfig object passed to the adapter * 3. the `TINA4_DATABASE_CHARSET` environment variable * 4. the `UTF8` default * * Pure config resolution over its inputs (URL string, explicit charset, env) — * it opens NO connection, so it is unit-testable without a live server. */ export declare function resolveFirebirdCharset(connectionString: string, explicitCharset?: string): string; export interface FirebirdConfig { host?: string; port?: number; user?: string; password?: string; database?: string; role?: string; pageSize?: number; /** Connection charset. Overridden by a `?charset=` URL query; see resolveFirebirdCharset. */ charset?: string; } /** * Quote an identifier the way Firebird actually stores it: UPPERCASE. * * Firebird folds an UNQUOTED identifier to upper case and treats a QUOTED one as * case-sensitive. So after the ordinary `CREATE TABLE probe_t (...)` the table is * PROBE_T, and `INSERT INTO "probe_t"` matches nothing: * * Dynamic SQL Error / Table unknown / probe_t * * That broke the insert path against every conventionally-created table, columns * included. A name the caller has ALREADY quoted is passed through untouched, * which is the escape hatch for a genuinely case-sensitive `CREATE TABLE "orders"`. */ export declare function fbQuote(name: string): string; /** * Firebird's stored column name, folded back only when it was folded. * * Firebird's identifier folding is ASYMMETRIC. An unquoted `AS x` is stored * UPPERCASE, so the driver hands back "X" where every other engine Tina4 * supports gives "x" — PostgreSQL folds to lower, and MySQL, SQLite and MSSQL * preserve what you wrote. Portable code reading row.x broke on Firebird alone. * * A QUOTED `AS "MyCol"` is stored exactly as written, and that case is * deliberate — the caller asked for it — so it is left alone. Folding * unconditionally makes a mixed-case key unreachable, the same asymmetric trap * that made tableExists miss quoted tables. * * So: fold back only a name carrying no lowercase letter, the only thing * unquoted folding can produce. A quoted ALL-CAPS name is genuinely * indistinguishable from a folded one and is lowercased too; that ambiguity is * Firebird's, and it is the one spelling this cannot round-trip. */ export declare function firebirdColumnName(raw: string): string; export declare class FirebirdAdapter implements DatabaseAdapter { private config; private db; private transaction; private _lastInsertId; /** Resolved node-firebird config, kept so a dead connection can re-attach. */ private fbConfig; private static readonly DEAD_CONN_MARKERS; /** Is this a dead-socket error worth a transparent reconnect (not a logical SQL error)? */ static isDeadConnection(err: unknown): boolean; constructor(config: FirebirdConfig | string); /** Connect to Firebird. Must be called before using the adapter. */ /** ADR-0044 required adapter capability. */ getDatabaseType(): string; /** ADR-0044: readable/writable native boolean. */ autocommit: boolean; /** * ADR-0044 / DBA-P02: every built-in adapter can guarantee an atomic * multi-row batch by default. A test-only deployment representing one * that cannot sets this false so executeMany rejects BEFORE the first * write rather than risking partial durability. */ supportsAtomicBatch: boolean; connect(): Promise; private attachOnce; /** * Attach with a BOUNDED retry (FB-DEC-03). node-firebird's SRP login over * WireCrypt is intermittently flaky (~12% measured historically), and a flake * surfaces as an auth/handshake error indistinguishable from a real one, so a * bounded retry-all is the robust, honest handling: a transient handshake * failure recovers, while a genuine bad credential still fails after the bound * -- never skipped, never papered over. */ private attachWithRetry; /** * Run a node-firebird op; on a DEAD-connection error (outside an explicit * transaction) re-attach once and retry (FB-DEC-01). Inside a transaction the * error surfaces -- atomicity beats resilience, and the caller rolls back. */ private withReconnect; private reconnectFirebird; private parseUrl; private ensureConnected; /** Translate SQL for Firebird dialect. */ translateSql(sql: string): string; /** * The handle every statement runs on. While an explicit transaction is open * (startTransactionAsync set `this.transaction`), statements MUST run on that * transaction object so they are undone by rollbackAsync() / persisted by * commitAsync() — node-firebird's transaction exposes the same * query()/execute() as the connection. With no transaction open we run on * `this.db`, whose per-statement work auto-commits on the connection. * * This matches the Python master's contract (tina4_python/database/firebird.py): * there, ALL statements run on the single connection and start_transaction() * merely suppresses the per-statement autocommit in execute() so the batch * stays open until commit()/rollback(). node-firebird has no such suppression * hook — its `db.query/execute` always auto-commit — so the equivalent is to * route statements through the transaction object instead. Same observable * behaviour: an open transaction is atomic and rolls back cleanly. * * Previously every statement ran on `this.db` unconditionally, so the * transaction created by startTransactionAsync() never saw a single statement * — rollbackAsync() rolled back an EMPTY transaction and the already * auto-committed write survived (silent no-op). Twin of the PHP pdo_firebird * bug fixed in 3.13.86. */ private statementHandle; private queryPromise; private executePromise; /** * The real affected-row count. node-firebird gives NO DML count of its own * (the callback result is undefined -- MEASURED), but Firebird 5 multi-row * RETURNING surfaces one row per affected row, so `... RETURNING 1` + the row * count IS the real count (FB-AFFECTED-FAB replaces the hardcoded 1). RETURNING * a constant, not `*`, so a large update/delete does not materialise full rows. */ private executeReturningCount; /** * Firebird has no generic last_insert_id -- read the GEN__ID generator * the row's BEFORE INSERT trigger drew from (FB-LASTID-GAP). Column-name- * independent, so correct for a non-`id` PK too. null when the table has no * such generator (GEN_ID then throws -> caught). */ private readGeneratorId; /** * Read a node-firebird BLOB column into a Buffer. A BLOB arrives as a STREAMING * FUNCTION (fn((err, name, emitter) => emitter.on('data'|'end'))), NOT a Buffer * -- MEASURED -- so the old decodeBlobs no-op leaked the function to the caller * and no bytes round-tripped (FB-BLOB-SRP-UNVERIFIED). */ private readBlob; execute(sql: string, params?: unknown[]): unknown; executeMany(sql: string, paramsList: unknown[][]): { totalAffected: number; lastId?: number | bigint; }; executeManyAsync(sql: string, paramsList: unknown[][]): Promise<{ totalAffected: number; lastId?: number | bigint; }>; executeAsync(sql: string, params?: unknown[]): Promise; query>(sql: string, params?: unknown[]): T[]; queryAsync>(sql: string, params?: unknown[]): Promise; /** * Read out any BLOB columns to Buffers. node-firebird returns a BLOB as a * STREAMING FUNCTION, not a Buffer (MEASURED), so a column whose value is a * function is read via readBlob(); everything else passes through unchanged * (FB-BLOB-SRP-UNVERIFIED -- the old no-op leaked the function to the caller). */ private decodeBlobs; fetch>(sql: string, params?: unknown[], limit?: number, skip?: number): T[]; fetchAsync>(sql: string, params?: unknown[], limit?: number, skip?: number): Promise; fetchOne>(sql: string, params?: unknown[]): T | null; fetchOneAsync>(sql: string, params?: unknown[]): Promise; insert(table: string, data: Record | Record[]): DatabaseResult; insertAsync(table: string, data: Record | Record[]): Promise; update(table: string, data: Record, filter: Record, params?: unknown[]): DatabaseResult; updateAsync(table: string, data: Record, filter: Record | string, params?: unknown[]): Promise; delete(table: string, filter: Record, params?: unknown[]): DatabaseResult; deleteAsync(table: string, filter: Record | string, params?: unknown[]): Promise; startTransaction(): void; startTransactionAsync(): Promise; commit(): void; commitAsync(): Promise; rollback(): void; rollbackAsync(): Promise; getTables(): string[]; tablesAsync(): Promise; getColumns(table: string): ColumnInfo[]; columnsAsync(table: string): Promise; lastInsertId(): number | bigint | null; close(): void; tableExists(name: string): boolean; /** * Is this table present, under either spelling Firebird could have stored? * * Firebird's folding rule is ASYMMETRIC: * CREATE TABLE foo -> stored as FOO (unquoted folds to UPPER) * CREATE TABLE "Foo" -> stored as Foo (quoted keeps its case) * * So upper-casing is CORRECT for the unquoted case - the common one - and * WRONG for a quoted mixed-case table, which is a real thing on Firebird. * Dropping the upper-case would not fix that, it would invert which half is * broken. * * tableExistsAsync("Foo") is genuinely AMBIGUOUS: the caller could mean the * quoted `Foo` or the unquoted `FOO`. Match EITHER. Do not "simplify" this * back to one comparison - that is the bug it replaces, where a quoted * mixed-case table read as absent and createTableAsync's idempotency guard * (below) never fired. */ tableExistsAsync(name: string): Promise; createTable(name: string, columns: Record): void; createTableAsync(name: string, columns: Record): Promise; }