import type { DatabaseAdapter, DatabaseResult as DatabaseWriteResult, ColumnInfo, FieldDefinition } from "./types.js"; import { DatabaseResult } from "./databaseResult.js"; import { DatabaseUrl } from "./databaseUrl.js"; import { type CachedAdapterOptions } from "./cachedDatabase.js"; /** * v3.13.12 — strip trailing `;` and whitespace from user-supplied SQL * before the framework wraps it with COUNT(*) subqueries or appends * LIMIT/OFFSET clauses. Without this, `"SELECT * FROM t;"` becomes * `"SELECT * FROM t; LIMIT 100 OFFSET 0"` — a syntax error on every * engine. Internal semicolons (in string literals, between meaningful * statements) are left alone; drivers reject those if the engine * doesn't support multi-statement. * * Exported so adapters and external tooling can compose it. */ export declare function stripTrailingSemicolons(sql: string): string; /** * Adapter bridge helpers (v3.14.0, Option A). * * The public Database/BaseModel/QueryBuilder API is async so it works on the * async adapters (PostgreSQL/MySQL/MSSQL/Firebird/Mongo). SQLite implements * only the synchronous methods (`node:sqlite` is sync); the async adapters * implement only the `*Async` variants and make the sync methods throw. * * Each helper prefers the adapter's `*Async` method when present and awaits it, * otherwise falls back to the sync method. For SQLite the fallback resolves * instantly; for async adapters the awaited promise does the real work. This is * the single chokepoint every public read/write flows through. */ export declare function adapterFetch>(adapter: DatabaseAdapter, sql: string, params?: unknown[], limit?: number, skip?: number, noCache?: boolean): Promise; export declare function adapterQuery>(adapter: DatabaseAdapter, sql: string, params?: unknown[]): Promise; export declare function adapterFetchOne>(adapter: DatabaseAdapter, sql: string, params?: unknown[]): Promise; export declare function adapterExecute(adapter: DatabaseAdapter, sql: string, params?: unknown[]): Promise; /** * ADR-0044: the adapter-level batch primitive, called exactly once by * Database#executeMany (never looped) — one aggregate DatabaseResult for the * whole batch. Normalises whichever native shape an adapter returns: SQLite's * `{success, affectedRows, lastId}` (the shared write shape already used by * insert/update/delete) or an async-native adapter's `{totalAffected, lastId}` * (pre-ADR-0044 shape, not yet unified per-adapter — normalised HERE at the * one chokepoint every public write flows through, so the facade's contract * is uniform without touching each of the five adapter files' internals). */ export declare function adapterExecuteMany(adapter: DatabaseAdapter, sql: string, paramsList: unknown[][]): Promise; /** * Insert one row (or a batch) through the adapter's OWN native insert path * (each adapter's `buildInsert()`/`Dialect`, feature 3's SQL builder * consolidation) instead of hand-built SQL. This is the ONLY correct way to * insert into a caller-named table/columns: Firebird's Dialect quotes only * when it has to (an unquoted identifier folds to UPPERCASE, so quoting a * lower-case name makes it unfindable — SQL error -204 "Table unknown"), * while PostgreSQL/MSSQL/SQLite quote unconditionally. A caller that hand- * quotes with one fixed style (e.g. always `"col"`) works on three engines * and silently breaks on the fourth. seedTable()/seedOrm() route through * this so their engine portability matches insert()/insertAsync()'s, which * the write-path + provider contract suites already prove on all four real * engines (features 9/10/11/12). */ export declare function adapterInsert(adapter: DatabaseAdapter, table: string, data: Record): Promise; export declare function adapterStartTransaction(adapter: DatabaseAdapter): Promise; export declare function adapterCommit(adapter: DatabaseAdapter): Promise; export declare function adapterRollback(adapter: DatabaseAdapter): Promise; export declare function adapterTableExists(adapter: DatabaseAdapter, name: string): Promise; export declare function adapterTables(adapter: DatabaseAdapter): Promise; export declare function adapterColumns(adapter: DatabaseAdapter, table: string): Promise; export declare function adapterCreateTable(adapter: DatabaseAdapter, name: string, columns: Record): Promise; /** * The true row count for `sql`, ignoring the pagination the caller applied. * * `count` on a DatabaseResult is the TRUE TOTAL for the filter, not the number * of rows the page returned. Node and Ruby used to populate it with * `records.length` while Python and PHP populated it from a probe, so * `db.fetch(sql).count` answered 20 here and 250 there for one query against one * table, and every `toPaginate()` envelope built on it under-reported (ADR-0043). * MEASURED 2026-08-05 on a 250-row table read with limit=20: Node reported total * 20 over 2 pages against Python's 250 over 13. * * This is the single source of truth for that probe. Both read paths that build a * DatabaseResult — `Database.fetch()` and `QueryBuilder.get()` — call it, so the * two can never drift (QueryBuilder.get used to leave `count` at rows-returned, * diverging from db.fetch AND from Python/Ruby, whose get() routes through fetch). * * Only probed when a limit was actually applied. With no limit the rows returned * ARE the whole answer for this SQL, so `records.length` is already the true total * and a second round-trip would buy nothing — which is also what keeps * `fetchAll()` at one query. * * BEST EFFORT, and it can never mask a real failure: it runs AFTER the main query * (which has already thrown on bad SQL) and returns `undefined` on any error. * `undefined` — not 0 — is the miss value, so DatabaseResult falls back to * records.length, a true lower bound. Reporting 0 next to 100 real records would * be the same "states a wrong number authoritatively" defect this exists to remove. * * The closing paren goes on its OWN LINE: appended inline, a trailing * `-- comment` in the caller's SQL comments it out and the probe dies with * "incomplete input". Postgres, MySQL and MSSQL additionally require a name for * the derived table; SQLite and Firebird do not, and Firebird rejects `AS` there — * so the alias comes from the adapter, not an assumption. */ export declare function probeTotal(adapter: DatabaseAdapter, sql: string, params: unknown[] | undefined, limit: number | undefined): Promise; /** * Extract the engine-assigned auto-increment id from an `execute()` result. * * SQLite returns `{ lastInsertRowid }`. PostgreSQL (pg) returns a result whose * `rows[0].id` holds the value when the statement had a `RETURNING` clause * (insertAsync adds one). MySQL/MSSQL adapters set the adapter's lastId, * so callers fall back to `adapter.lastInsertId()` when the result has neither. */ export declare function extractLastInsertId(result: unknown): number | bigint | null; /** * The default row cap on every read path that advertises a `limit`. * * One number for the whole family (Python, PHP, Ruby and Node all default to * this). Pagination is a default principle: an un-paginated read of a table * that grew to a million rows is a production incident waiting to happen. A * caller who wants more passes a bigger limit. */ export declare const DEFAULT_ROW_CAP = 100; /** * Wrap a raw adapter with the query cache so BOTH `db.fetch()` (via the * Database wrapper) AND ORM reads (via `getAdapter()` / `getNamedAdapter()`) * are cached through the same store and counters. * * Idempotent: an already-wrapped adapter is returned as-is, so re-binding the * same adapter (or binding the adapter a Database wrapper already holds) never * double-wraps. `options.sharedCache` backs all pooled connections with one * store so a write on any connection invalidates reads cached by all of them. * * Caching is OFF by default — both layers are opt-in. Turn the request-scoped * layer on with TINA4_AUTO_CACHING=true (for read-heavy endpoints) and/or the * persistent cross-request layer with TINA4_DB_CACHE=true. With both unset the * wrapper passes everything straight through (no cached read-after-write footgun). */ export declare function wrapWithCache(adapter: DatabaseAdapter, options?: CachedAdapterOptions): DatabaseAdapter; /** * Resolve the underlying wrapped adapter for a given raw adapter — used so the * Database wrapper and `getAdapter()` end up holding the SAME * CachedDatabaseAdapter instance (one cache, one set of counters). */ export declare function setAdapter(adapter: DatabaseAdapter): DatabaseAdapter; /** * Clear the request-scoped query cache on every live connection at the start of * each HTTP request, so request-scoped caching never serves rows across * requests. Persistent-mode connections (TINA4_DB_CACHE=true) are untouched. * * The request dispatcher calls this. Mirrors Python's * `Database.reset_request_caches()`. */ export declare function resetRequestCaches(): void; /** * Public, user-facing API to bind a database connection. * * - No `name` → registers `adapter` as the global default connection * (what `getAdapter()` returns and what every model resolves to unless it * declares `static _db`). This is the manual equivalent of the auto-binding * that `initDatabase()` performs from `.env`/`TINA4_DATABASE_URL`. * - With `name` → registers `adapter` in the named registry. A model with * `static _db = name` resolves to it via `getNamedAdapter(name)`. * * Mirrors the Python master `bind_database(db, name=None)`. * * import { bindDatabase, createAdapterFromUrl } from "@tina4/orm"; * * // Default connection * bindDatabase(adapter); * * // Named secondary connection built from a URL (kept synchronous — * // build the adapter first, then bind it) * bindDatabase(await createAdapterFromUrl(url, user, pass), "analytics"); * * `bindDatabase` itself is synchronous: it takes an already-constructed * adapter. Use `createAdapterFromUrl()` to build a named secondary adapter * from a URL without making it the default. */ export declare function bindDatabase(adapter: DatabaseAdapter, name?: string): void; export declare function getAdapter(): DatabaseAdapter; /** * Register a named adapter for multi-database support. * Models reference it via `static _db = 'name'`. */ export declare function setNamedAdapter(name: string, adapter: DatabaseAdapter): void; /** * Get a named adapter previously registered via `bindDatabase(adapter, name)` * (or the lower-level `setNamedAdapter(name, adapter)`). * * Throws a clear error if the name isn't registered — a model that declares * `static _db = "name"` resolves through here, so a missing name means the * connection was never bound. The message tells the developer exactly how to * fix it rather than silently falling back to the default connection (which * would hide the mistake and write to the wrong database). */ export declare function getNamedAdapter(name: string): DatabaseAdapter; export declare function closeDatabase(): void; export interface DatabaseConfig { type?: "sqlite" | "postgres" | "mysql" | "mssql" | "sqlserver" | "firebird" | "mongodb" | "odbc"; path?: string; url?: string; host?: string; port?: number; user?: string; username?: string; password?: string; database?: string; /** ODBC-specific: full connection string, e.g. "DSN=MyDSN" or "DRIVER={SQL Server};SERVER=host;DATABASE=db" */ connectionString?: string; } /** * Parsed result from a TINA4_DATABASE_URL connection string. */ /** * Parse a connection URL into a `DatabaseUrl` value. * * Breaking (feature 5): this returned a `ParsedDatabaseUrl` struct whose fields * were `type`, `user` and `path`. It now returns a `DatabaseUrl`, whose fields * are `engine`, `username` and `database` - the same names PHP, Python and Ruby * use, and the same names as the TINA4_DATABASE_USERNAME env var they come from. * `ParsedDatabaseUrl` is gone rather than kept as an alias. * * The 43-CC body that used to live here - the worst function measured anywhere * in the audit - is now one small parser per engine inside the value type. */ export declare function parseDatabaseUrl(url: string, username?: string, password?: string): DatabaseUrl; /** * A wrapper class around a DatabaseAdapter that provides a clean, high-level API. * * Mirrors the Database class in Python/Ruby Tina4 implementations. * * Usage: * const db = await Database.create("sqlite:///path/to/db.sqlite"); * const rows = db.fetch("SELECT * FROM users WHERE active = ?", [true], 10, 0); * const user = db.fetchOne("SELECT * FROM users WHERE id = ?", [1]); * db.insert("users", { name: "Alice", email: "alice@example.com" }); * db.update("users", { name: "Bob" }, { id: 1 }); * db.delete("users", { id: 1 }); * db.close(); * * Connection pooling: * const db = await Database.create("sqlite:///data/app.db", undefined, undefined, 4); * // 4 connections, round-robin rotation */ export declare class Database { private adapter; /** Connection pool — array of adapters with lazy creation */ private pool; /** Pool size (0 = single connection) */ private _poolSize; /** Round-robin index */ private poolIndex; /** Factory for creating new adapters (used by pool) */ private adapterFactory; /** table -> primary-key column name (or null), introspected once */ private _pkCache; /** * Whether a standalone write auto-commits. ON by default — a write made * outside an explicit transaction commits on its own connection before * returning (so it's durable and visible across pooled connections). Inside * startTransaction()/commit()/rollback() the per-statement commit is * suppressed, so explicit transactions stay atomic. Set TINA4_AUTOCOMMIT=false * for strict manual-commit mode. */ private autoCommit; private lastError; /** Database engine type (sqlite, postgres, mysql, mssql, firebird) */ private dbType; /** * Async-local storage for the adapter pinned to the current transaction. * * With pooling enabled, ordinary calls round-robin through the pool. Inside * a transaction, however, all calls must land on the SAME adapter — otherwise * startTransaction(), execute() and commit() each rotate to a different * connection and the transaction is meaningless (executes autocommit on * whatever adapter they hit; the final commit lands on yet another adapter * that has nothing to commit; rollback() is silently no-op'd). * * AsyncLocalStorage is the Node analog of Python's threading.local. It pins * the adapter to the current async task tree so concurrent transactions on * the same Database don't clobber each other. startTransaction() sets the * pin via .enterWith(); commit()/rollback() clear it. */ private txStore; /** * Create a Database wrapping an existing adapter. * For creating a Database from a URL, use the async static factories: * Database.create(url) or Database.fromEnv() */ constructor(adapter: DatabaseAdapter); /** * Set the engine type ("sqlite" | "postgres" | "mysql" | "mssql" | * "firebird" | "mongodb"). The static `Database.create` factory assigns the * private `dbType` directly; `initDatabase()` (a free function, no private * access) routes through this setter so a URL connection is correctly typed. * Without it a `postgres://` connection kept the `"sqlite"` default and * `getNextId()` took the SQLite branch — hitting the non-existent * `tina4_sequences` table on PostgreSQL instead of native sequences (#255). */ setDbType(type: string): void; /** * Async factory: creates a Database from a connection URL. * Works with all adapter types (sqlite, postgres, mysql, mssql, firebird). * * @param url - Connection URL * @param username - Optional username * @param password - Optional password * @param pool - Number of pooled connections (0 = single, N>0 = round-robin) */ static create(url: string, username?: string, password?: string, pool?: number): Promise; /** * Create a Database from an environment variable. * @param envKey - Name of the env var holding the connection URL. Defaults to "TINA4_DATABASE_URL". * @param pool - Number of pooled connections (0 = single, N>0 = round-robin) */ static fromEnv(envKey?: string, pool?: number): Promise; /** * Get the next adapter — from pool (round-robin) or single connection. * * If a transaction is active (an adapter is pinned in async-local storage), * that adapter is returned for every call so the whole transaction is * atomic on one connection. Otherwise pooled mode round-robins. */ private getNextAdapter; /** Get the underlying adapter (for advanced / escape-hatch usage). */ getAdapter(): DatabaseAdapter; /** Get the pool size (0 = single connection mode). */ poolSize(): number; /** Alias for poolSize() — returns total pool size (0 = single connection mode). */ size(): number; /** Get the number of active (created) connections in the pool. */ activeCount(): number; /** * Borrow a connection from the pool (or the single adapter). * The caller is responsible for returning it via checkin(). */ checkout(): DatabaseAdapter; /** * Return a borrowed connection to the pool. * For round-robin pools this is a no-op (connections stay in the pool array), * but the method exists for API parity and future pooling strategies. */ checkin(_adapter: DatabaseAdapter): void; /** * Close all pooled connections and clear the pool. * Equivalent to close() but named for explicit pool teardown. */ closeAll(): void; /** Query rows with optional pagination. Returns a DatabaseResult wrapper. * * Async since v3.14.0 (Option A): the public API awaits the adapter's * `*Async` method when present (PostgreSQL/MySQL/MSSQL/Firebird/Mongo) and * falls back to the synchronous method for SQLite (`node:sqlite` is sync, so * the fallback resolves instantly). This is the breaking change that makes * the wrapper work uniformly across every engine. */ /** * Fetch rows with pagination, capped at DEFAULT_ROW_CAP (100) when the * caller does not pass a limit. * * The cap is the one row-cap number the whole family shares (Python, PHP and * Ruby all default `fetch` to 100). Node was the outlier: `limit` was * optional with NO default, so a bare `db.fetch("select * from big_table")` * returned every row. * * `fetchAll` deliberately does NOT inherit the cap — see below. */ fetch(sql: string, params?: unknown[], limit?: number, offset?: number, opts?: { noCache?: boolean; }): Promise; /** * The shared read body. `limit` is passed through VERBATIM: `undefined` * means "no LIMIT clause at all", which is how `fetchAll` stays uncapped. * * This exists because Node's adapters treat `limit: 0` as `LIMIT 0` (zero * rows), not as the "no truncation" sentinel Python and PHP use — so the cap * cannot live on the parameter default, or `fetchAll()` would silently * inherit it and stop returning every row. */ private _fetchWithLimit; /** * Fetch a single row or null. * * Pass `{ noCache: true }` as the trailing options object to bypass the * query cache for this one call — no lookup, no store, run directly * (mirrors the Python master's `no_cache`). Default preserves caching. */ fetchOne>(sql: string, params?: unknown[], opts?: { noCache?: boolean; }): Promise; /** * Fetch rows and return the records array directly. * * Symmetric with `fetchOne`. For the common case where you just want * the rows and don't need the `DatabaseResult` metadata, this is one * less attribute access than `fetch(...).records`. * * const rows = db.fetchAll("SELECT * FROM users WHERE active = ?", [true]); * for (const row of rows) console.log(row.name); * * Returns `[]` (not `null`) when no rows match. Cross-framework parity * with Python `db.fetch_all()`, PHP `$db->fetchAll()`, and Ruby `db.fetch_all`. * * Pass `{ noCache: true }` as the trailing options object to bypass the * query cache for this one call — no lookup, no store, run directly * (mirrors the Python master's `no_cache`). The options object is a * SEPARATE trailing argument, never the params array. */ fetchAll>(sql: string, params?: unknown[], limit?: number, offset?: number, opts?: { noCache?: boolean; }): Promise; /** * Execute a write statement. * * On SUCCESS returns `true` for simple writes, or the result set when the * SQL contains RETURNING / CALL / EXEC / SELECT. * * On a SQL error (bad SQL, constraint violation, dead/aborted connection, * missing driver) it FAILS LOUD: it records the cause on `lastError` * (readable via `getError()`) and then RE-THROWS — it never swallows the * error and returns `false`. This mirrors `fetch()`/`fetchOne()`, which * already raise. Callers that need a boolean (e.g. ORM `save()`, * `createTable()`, the migration runner, dev-admin/MCP DB tools) must * `try/catch` and convert, rather than testing the return value. */ execute(sql: string, params?: unknown[]): Promise; /** * Insert one row (object) or a batch of rows (array of objects) into a table. * * FAIL LOUD, matching update()/delete()/truncate(): a real driver failure * (e.g. a NOT NULL / UNIQUE constraint violation) throws rather than * resolving to `{ success: false, affectedRows: 0 }`. The async adapters * (Postgres/MySQL/MSSQL/Firebird) already throw directly from insertAsync(); * SQLiteAdapter.insert() is the one adapter that CATCHES the driver error * and returns a `{ success: false, error }` result instead (its own * documented contract for the synchronous path) — assertWrote is what * converts that into the same thrown DatabaseException every other engine * already produces, exactly as it already does for update/delete/truncate. */ insert(table: string, data: Record | Record[]): Promise; /** * The table's primary-key column, introspected once and cached. * * Uses the cross-engine getColumns() contract (v3.13.14, #48), which reports * primaryKey per column on every adapter. Resolves to null when the table has * no primary key or cannot be introspected. */ primaryKey(table: string): Promise; /** * A failed write must be loud. * * The adapters catch a SQL error and return { success: false, affectedRows: 0 }, * so a filterless update produced invalid SQL ("... WHERE ") and reported * nothing rather than raising. A caller who does not inspect the result * believes the write landed (audit feature 4, P1). */ private static assertWrote; /** * Update rows. A write with no filter is an error, not a full-table write. * * With no explicit filter the primary key is taken out of `data` and used as * the WHERE clause. With neither a filter nor a primary key in `data` this * throws rather than silently changing nothing (audit feature 4, P1). */ update(table: string, data: Record, filter?: Record | string, params?: unknown[]): Promise; /** Delete rows. A filterless delete throws; use truncate() to empty a table. */ delete(table: string, filter?: Record | string | Record[], params?: unknown[]): Promise; /** Remove every row. The explicit spelling of a whole-table delete. */ truncate(table: string): Promise; /** Close all database connections (pool or single). */ close(): void; /** * True while an explicit transaction is active on the current async context. * startTransaction() pins an adapter into txStore; commit()/rollback() clear * it. Standalone writes only auto-commit when this is false, so per-statement * commits never break the atomicity of an explicit transaction. */ private inExplicitTransaction; /** * Start a transaction. Pins the adapter to the current async context for * the whole transaction so executes and the final commit/rollback all run * on the same connection (critical when pool > 0). * * Nested-begin guard (DB-contract C): a second startTransaction() on a * context that already has a pinned adapter is a double-begin — the inner * BEGIN silently commits or no-ops on most engines, leaving the connection * mid-transaction with the caller none the wiser. We keep a depth counter and * log a clear warning instead of silently re-beginning; the pin stays on the * original adapter so the eventual commit/rollback still land on the right * connection, and the matching inner commit just unwinds the depth. */ startTransaction(): Promise; /** * Commit the current transaction. * * FAIL LOUD (DB-contract C): if the underlying commit raises, capture * lastError and RE-THROW — never swallow. On failure the transaction pin is * RETAINED so the caller's follow-up rollback() lands on the SAME connection * (clearing it would leak a dirty connection back into the pool and route the * rollback to a different one). The pin is cleared ONLY on a successful * commit. An inner commit of an ignored nested begin (depth > 1) just unwinds * the depth — the outer commit is the real one. */ commit(): Promise; /** * Rollback the current transaction — the terminal cleanup of a transaction, * so it ALWAYS clears the pin (and the depth counter), even after a failed * commit (it routes to the retained pinned connection and cleans it up). If * the underlying rollback itself raises, lastError is captured and the error * re-thrown, but the pin is still released so a poisoned connection doesn't * stay pinned to this context forever. */ rollback(): Promise; /** Check if a table exists. */ tableExists(name: string): Promise; /** List all tables in the database. */ getTables(): Promise; /** * Get column metadata for a table. * Uses the adapter's columns() method which handles engine-specific introspection * (PRAGMA table_info for SQLite, information_schema.columns for others). * * @param tableName - Name of the table to inspect. * @returns Array of column info objects: { name, type, nullable, default, primaryKey }. */ getColumns(tableName: string): Promise<{ name: string; type: string; nullable?: boolean; default?: unknown; primaryKey?: boolean; primaryKeyPosition?: number | null; }[]>; /** * Execute a SQL statement with multiple parameter sets as ONE aggregate * batch (ADR-0044). Wraps the single delegated call in a transaction for * atomicity — never loops #execute itself. * * BREAKING (ADR-0044, pre-3.14.0): used to return one result PER ROW * (`unknown[]`, callers indexed into it) built by the FACADE looping * execute()/adapterExecute() per chunk or per row. It now delegates to the * adapter's OWN executeMany/executeManyAsync exactly once (DBA-D02: facade * delegates once, never a facade row loop) and returns the SAME shared * DatabaseResult shape insert()/update()/delete() already return * ({success, affectedRows, lastId}) — affectedRows is the total ROW count, * never the number of chunks/statements. A caller that indexed into the old * per-row array must switch to inspecting the aggregate result. * * @param sql - The SQL statement with parameter placeholders. * @param paramSets - Array of parameter arrays, one per row. * @returns The aggregate DatabaseResult for the whole batch. */ executeMany(sql: string, paramSets?: unknown[][]): Promise; /** Return the last execute() error message, or null. */ getError(): string | null; /** * Return query cache statistics from the REAL cache backing this connection. * * The bound adapter is a CachedDatabaseAdapter (caching is OFF by default — * both layers opt-in: request-scoped via TINA4_AUTO_CACHING=true, persistent * via TINA4_DB_CACHE=true), so we read the live counters + size + mode from it. * Mirrors Python's `Database.cache_stats()`: `{ enabled, mode, hits, misses, size, ttl }`. */ cacheStats(): { enabled: boolean; mode: "persistent" | "request" | "off"; hits: number; misses: number; size: number; ttl: number; backend?: string; }; /** Flush the query cache and reset counters (mirrors Python `cache_clear()`). */ cacheClear(): void; /** * Clear the request-scoped cache at the START of an HTTP request on this * connection (no-op in persistent mode). Mirrors Python's * `Database.cache_new_request()`. */ cacheNewRequest(): void; /** Get the last auto-increment id. */ getLastId(): string | number; /** * Create the tina4_sequences table if it doesn't exist. * Used by sequenceNext() for race-safe ID generation on * SQLite, MySQL, MSSQL, and as a PostgreSQL fallback. */ private ensureSequenceTable; /** * Best-effort MAX(pk) seed for a new sequence row. 0 if the table is * missing/empty. Mirrors Python's `_sequence_seed_value`. */ private sequenceSeedValue; /** * Atomically increment and return the next value from the sequence table. * * DB-contract B (no duplicate primary keys under concurrency): the old path * was read-increment-read across several `await` points, so two concurrent * async callers could read the same `current_value` and return the same id. * This now uses a single atomic increment-and-return per engine, pinned to * ONE adapter so the two statements (where two are needed) land on the same * connection: * * * SQLite: the SQLiteAdapter does ensure-table + seed + the atomic * `UPDATE ... RETURNING current_value` (>= 3.35; else `+1` then `SELECT`) * as ONE synchronous burst — no `await` between read and write, so no * other async task can interleave (Node analog of Python's _write_lock). * * MySQL: `UPDATE ... SET current_value = LAST_INSERT_ID(current_value + 1)` * then `SELECT LAST_INSERT_ID()` on the SAME pinned connection * (LAST_INSERT_ID is per-connection → race-safe). * * MSSQL: `UPDATE ... SET current_value = current_value + 1 OUTPUT * inserted.current_value ...` — one atomic statement. * * Seeding is always a race-safe insert-if-absent (INSERT OR IGNORE / * INSERT IGNORE / INSERT ... WHERE NOT EXISTS) seeded from MAX(pk), run * BEFORE the increment — never a read-then-insert gap. On error we RAISE * (never silently fall back to 1). */ private sequenceNext; /** * MySQL atomic sequence step. LAST_INSERT_ID(expr) stashes `expr` in this * CONNECTION's session var and returns it, so the read-back is per-connection * and race-safe. Runs on the pinned adapter. */ private sequenceNextMysql; /** * MSSQL atomic sequence step. A single `UPDATE ... OUTPUT * inserted.current_value` increments and returns the new value in one * statement. Runs on the pinned adapter. */ private sequenceNextMssql; /** * Defensive generic atomic-ish path for any engine not otherwise special-cased * (and the SQLite fallback if the adapter lacks the synchronous helper). Seeds * if absent, then increments and reads on the pinned connection. */ private sequenceNextGeneric; /** * Pre-generate the next available primary key ID using engine-aware strategies. * * - Firebird: auto-creates a generator if missing, then increments via GEN_ID (atomic). * - PostgreSQL: tries nextval() first; if sequence missing, auto-creates it * seeded from MAX; falls through to sequence table on failure. * - SQLite/MySQL/MSSQL: uses tina4_sequences table with atomic UPDATE + SELECT * (race-safe, replaces old MAX+1). * - Returns 1 if the table is empty or does not exist. */ getNextId(table: string, pkColumn?: string, generatorName?: string): Promise; } /** * Build a connected `DatabaseAdapter` from a connection URL. * * Used internally by `initDatabase()` and `Database.create()`, and exported so * users can construct a NAMED secondary adapter without making it the default: * * bindDatabase(await createAdapterFromUrl(url, user, pass), "analytics"); * * Unlike `initDatabase()`, this does NOT call `setAdapter()` — it returns a * standalone adapter that the caller decides what to do with. For async engines * (Postgres/MySQL/MSSQL/Firebird/Mongo) the returned adapter is already * connected; SQLite connects lazily. */ export declare function createAdapterFromUrl(url: string, username?: string, password?: string): Promise; /** * Initialize the database from a config object or TINA4_DATABASE_URL env var. * Now returns a Database wrapper instance. * * Priority: * 1. config.url (explicit URL) * 2. process.env.TINA4_DATABASE_URL * 3. config.type + config.path (legacy) */ /** * Resolve the connection-pool size from `TINA4_DB_POOL`. * * Default: 0 (single-connection mode). Any positive integer enables * round-robin pooling with that many connections — Database.create() honours * this transparently. The env var is the simple deploy-time override; tests * and library users can still pass `pool` directly to Database.create(). */ export declare function resolveDbPool(): number; /** * Open a database connection — convention name matching SQLAlchemy * `engine.connect()` and the cross-framework Database.get_connection() * surface shipped in 3.13.x. * * Equivalent to `initDatabase({ url })` but with an opinionated, simpler * signature: pass a URL string directly, or omit for env-based defaults * (falls back to in-memory SQLite when nothing resolves). * * const db = await Database.getConnection(); // from env * const db = await Database.getConnection("sqlite://./app.db"); // explicit URL * const db = await Database.getConnection("postgres://localhost/x", { username: "u", password: "p" }); * * Cross-framework parity with Python `Database.get_connection()`, PHP * `\Tina4\Database::getConnection()`, and Ruby `Tina4::Database.get_connection`. */ export declare namespace Database { function getConnection(url?: string, opts?: { username?: string; password?: string; }): Promise; /** * Clear the request-scoped query cache on every live connection. * * Static convenience mirroring Python's `Database.reset_request_caches()` * classmethod. The request dispatcher calls this at the start of each HTTP * request so request-scoped caching never serves rows across requests. * Persistent-mode connections (TINA4_DB_CACHE=true) are left alone. */ function resetRequestCaches(): void; } export declare function initDatabase(config?: DatabaseConfig): Promise;