@happyvertical/sql 0.85.5 → 0.86.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/chunks/types-uVFHgaSU.js.map +1 -1
- package/dist/index.js +28 -5
- package/dist/index.js.map +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/shared/types.d.ts +3 -1
- package/dist/shared/types.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -170,7 +170,9 @@ Two consequences worth knowing:
|
|
|
170
170
|
connection its own caller is holding.
|
|
171
171
|
|
|
172
172
|
PostgreSQL pools its connections, so transactions there run concurrently and
|
|
173
|
-
never queue.
|
|
173
|
+
never queue. Nested scopes on one PostgreSQL transaction use savepoints; if
|
|
174
|
+
sibling nested scopes are started concurrently, they serialize so PostgreSQL's
|
|
175
|
+
stack-ordered savepoint lifecycle remains intact.
|
|
174
176
|
|
|
175
177
|
### Identifiers
|
|
176
178
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types-uVFHgaSU.js","names":[],"sources":["../../src/shared/transaction-lock.ts","../../src/shared/types.ts"],"sourcesContent":["import { DatabaseError } from '@happyvertical/utils';\n\n/**\n * How long a queued transaction waits for the connection before giving up.\n *\n * A transaction that legitimately runs longer than this will make the ones\n * behind it fail rather than stall indefinitely. Raise it with the adapter's\n * `transactionQueueTimeout` option if that is the wrong trade for a workload.\n */\nexport const DEFAULT_TRANSACTION_QUEUE_TIMEOUT_MS = 30_000;\n\n/**\n * Serializes transactions on a connection that cannot hold more than one.\n */\nexport interface TransactionLock {\n /**\n * Takes the connection and returns the function that gives it back.\n *\n * Calling the returned function more than once is a no-op, so a teardown\n * path that runs twice cannot hand the connection to two callers at once.\n */\n acquire(): Promise<() => void>;\n\n /**\n * Runs `work` with exclusive use of the connection, releasing it however\n * `work` finishes.\n */\n run<T>(work: () => Promise<T>): Promise<T>;\n}\n\n/**\n * Creates a lock that serializes transactions on one connection.\n *\n * SQLite, DuckDB and the JSON adapter each drive a single connection, and a\n * connection can only be in one transaction at a time. `transaction()` used to\n * issue `BEGIN` on that shared connection with no serialization and no record\n * of whether one was already open, so two overlapping calls corrupted each\n * other: the second `BEGIN` threw, its `catch` ran `ROLLBACK`, and that\n * rollback landed on the *first* transaction — committing part of its work,\n * discarding the rest, and rejecting its promise. Nesting was not required;\n * two concurrent requests were enough, which made it reachable from any server\n * handling more than one at a time.\n *\n * The lock spans the whole `BEGIN` … `COMMIT`/`ROLLBACK` sequence, so a queued\n * transaction starts only once the previous one has ended. It is a\n * promise-chain mutex, the same shape as the `withNullAwareUpsertLock` the\n * SQLite adapter already used for the narrower upsert case.\n *\n * Re-entrant calls must not come through here. A nested `tx.transaction()`\n * arrives via the transaction-scoped interface, which re-enters under a\n * `SAVEPOINT` (SQLite) or refuses (DuckDB, JSON) without ever reaching the\n * top-level `transaction()`; routing one here instead would make it wait on a\n * lock its own caller is holding.\n *\n * @param adapter - Adapter name, for the timeout error\n * @param timeoutMs - Ceiling on how long to wait for a turn\n */\nexport function createTransactionLock(\n adapter: string,\n timeoutMs: number = DEFAULT_TRANSACTION_QUEUE_TIMEOUT_MS,\n): TransactionLock {\n // The timer is measured from enqueue, not from when the lock frees, so this\n // bounds the total wait rather than any one holder. A caller that expects\n // sustained bursts on one connection should raise it: a hundred queued\n // 400ms transactions exceed 30s even though none of them is stuck.\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n throw new DatabaseError(\n 'transactionQueueTimeout must be a positive, finite number of milliseconds',\n { adapter, timeoutMs },\n );\n }\n // Resolves when the transaction currently holding the connection ends.\n let tail: Promise<void> = Promise.resolve();\n\n const acquire = async (): Promise<() => void> => {\n const previous = tail;\n let release: () => void = () => {};\n const current = new Promise<void>((resolve) => {\n release = resolve;\n });\n\n // Publish our turn before awaiting, so a caller that arrives while we are\n // suspended queues behind us rather than racing us for the connection.\n tail = previous.then(\n () => current,\n () => current,\n );\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n await new Promise<void>((resolve, reject) => {\n timer = setTimeout(() => {\n reject(\n new DatabaseError(\n `Timed out after ${timeoutMs}ms waiting for the ${adapter} connection's current transaction to finish. This connection runs one transaction at a time. Usually one of: a beginTransaction() handle that was never committed or rolled back; a top-level db.* call that opens its own transaction being made inside a transaction callback, which waits on the connection its own caller holds (use the tx-scoped method instead); or simply more queued transactions than transactionQueueTimeout allows for.`,\n { adapter, timeoutMs },\n ),\n );\n }, timeoutMs);\n previous.then(resolve, resolve);\n });\n } catch (error) {\n // Drop out of the queue rather than wedge it: `tail` still chains through\n // `current`, so resolving it lets the callers behind us take their turn.\n release();\n throw error;\n } finally {\n if (timer) clearTimeout(timer);\n }\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n release();\n };\n };\n\n const run = async <T>(work: () => Promise<T>): Promise<T> => {\n const release = await acquire();\n try {\n return await work();\n } finally {\n release();\n }\n };\n\n return { acquire, run };\n}\n","import { DatabaseError } from '@happyvertical/utils';\n\n/**\n * Error thrown when `transaction()` is called on a transaction-scoped interface\n * belonging to an adapter whose engine has no savepoints.\n *\n * DuckDB — and therefore the JSON adapter built on it — does not implement\n * `SAVEPOINT`, so there is no way to re-enter the current transaction. Adapters\n * that can re-enter (PostgreSQL, SQLite) do so under a savepoint and never\n * throw this.\n *\n * The alternative would be to open a second, independent transaction, which is\n * what these adapters used to do; on a shared connection that silently\n * destroyed the enclosing transaction's uncommitted work.\n *\n * @example Ensuring a helper works on every adapter\n * ```typescript\n * // Take the transaction as a parameter rather than opening a nested one.\n * async function recordClick(db: DatabaseInterface, id: string) {\n * await db.update('links', { id }, { clicks: 1 });\n * }\n * await db.transaction(async (tx) => recordClick(tx, id));\n * ```\n */\nexport class NestedTransactionError extends DatabaseError {\n constructor(message: string, context?: Record<string, unknown>) {\n super(message, context);\n this.name = 'NestedTransactionError';\n }\n}\n\n/**\n * WHERE clause input for database queries\n *\n * Supports two formats:\n * 1. Object format (AND-only): `{ status: 'active', 'price >': 100 }`\n * - All conditions are AND-joined\n *\n * 2. 2D array format (OR/AND compound logic): `[[cond1, cond2], [cond3, cond4]]`\n * - Inner arrays are AND-joined: `(cond1 AND cond2)`\n * - Outer array is OR-joined: `(cond1 AND cond2) OR (cond3 AND cond4)`\n *\n * @example Object format (backward compatible)\n * ```typescript\n * // WHERE status = 'active' AND price > 100\n * const where = { status: 'active', 'price >': 100 };\n * ```\n *\n * @example 2D array format for OR/AND compound logic\n * ```typescript\n * // WHERE (status = 'active' AND price > 100) OR (status = 'pending' AND priority = 'high')\n * const where = [\n * [{ status: 'active' }, { 'price >': 100 }],\n * [{ status: 'pending' }, { priority: 'high' }]\n * ];\n * ```\n */\nexport type WhereClause = Record<string, any> | Record<string, any>[][];\n\n/**\n * Common database connection options\n */\nexport interface DatabaseOptions {\n /**\n * Database connection URL\n */\n url?: string;\n\n /**\n * Authentication token for the database connection\n */\n authToken?: string;\n\n /**\n * How long a queued transaction waits for the connection, in milliseconds.\n *\n * Applies to the single-connection adapters — SQLite (both the LibSQL and\n * native paths), DuckDB and JSON. Those drive one connection, so transactions\n * run one at a time and an overlapping `transaction()` waits its turn.\n * PostgreSQL pools and ignores this.\n *\n * The clock starts when the call queues, not when the connection frees, so\n * this bounds the total wait rather than any single transaction: raise it for\n * workloads with long transactions *or* with sustained bursts on one\n * connection. Must be positive and finite.\n *\n * Read once, when the connection is created. Adapters that cache connections\n * hand a later `getDatabase()` for the same database the existing one, which\n * keeps the timeout the first caller asked for.\n *\n * @default 30000\n */\n transactionQueueTimeout?: number;\n}\n\n/**\n * DuckDB-specific connection options\n */\nexport interface DuckDBOptions extends DatabaseOptions {\n /**\n * Database type identifier\n */\n type?: 'duckdb';\n\n /**\n * Path to directory containing JSON files to auto-register as tables\n * @default './data'\n */\n dataDir?: string;\n\n /**\n * Automatically register JSON files in dataDir as queryable tables\n * @default true\n */\n autoRegisterJSON?: boolean;\n\n /**\n * Strategy for writing data back to JSON files\n * - 'immediate': Write changes to JSON files immediately\n * - 'manual': Require explicit export calls\n * - 'none': Read-only mode, no writes to JSON files\n * @default 'none'\n */\n writeStrategy?: 'immediate' | 'manual' | 'none';\n\n /**\n * Whether to use a persistent DuckDB file for caching/indexes\n * If true, uses url or defaults to ':memory:'\n * @default false\n */\n persistent?: boolean;\n\n /**\n * Explicit schema definitions for tables\n * When provided, these schemas will be used for table creation\n */\n schemas?: SchemasOption;\n}\n\n/**\n * A JSON data file that parsed but whose records could not be loaded into the\n * table — e.g. a renamed or dropped column, a NOT NULL / PRIMARY KEY violation,\n * or a file whose fields match no column on the table.\n *\n * The JSON adapter keeps the table (empty) and continues loading every other\n * table, logging the failure to stderr. This record makes the same failure\n * observable to calling code so a table that silently stays empty can be\n * detected. See issue #1139.\n */\nexport interface JSONTableLoadError {\n /** Name of the table whose data file failed to load */\n table: string;\n /** Path to the JSON data file that could not be loaded */\n filePath: string;\n /**\n * The underlying failure. Typically a `DatabaseError` whose `context`\n * carries the engine-level cause (e.g. the DuckDB constraint message).\n */\n error: unknown;\n}\n\n/**\n * JSON database adapter options (DuckDB-backed)\n *\n * Uses DuckDB's in-memory engine to query JSON files directly.\n * No WAL files or persistent database files are created.\n */\nexport interface JSONOptions {\n /**\n * Database type identifier\n */\n type: 'json';\n\n /**\n * Path to directory containing JSON files (required)\n * JSON files in this directory will be loaded as queryable tables\n */\n url: string;\n\n /**\n * Automatically load all JSON files in dataDir as tables\n * @default true\n */\n autoRegister?: boolean;\n\n /**\n * Strategy for writing changes back to JSON files\n * - 'immediate': Auto-save after every insert/update (default)\n * - 'manual': Require explicit exportTable() calls\n * - 'none': Read-only mode, throws error on writes\n * @default 'immediate'\n */\n writeStrategy?: 'immediate' | 'manual' | 'none';\n\n /**\n * How long a queued transaction waits for the connection, in milliseconds.\n *\n * Applies to the single-connection adapters — SQLite (both the LibSQL and\n * native paths), DuckDB and JSON. Those drive one connection, so transactions\n * run one at a time and an overlapping `transaction()` waits its turn.\n * PostgreSQL pools and ignores this.\n *\n * The clock starts when the call queues, not when the connection frees, so\n * this bounds the total wait rather than any single transaction: raise it for\n * workloads with long transactions *or* with sustained bursts on one\n * connection. Must be positive and finite.\n *\n * Read once, when the connection is created. Adapters that cache connections\n * hand a later `getDatabase()` for the same database the existing one, which\n * keeps the timeout the first caller asked for.\n *\n * @default 30000\n */\n transactionQueueTimeout?: number;\n\n /**\n * Explicit schema definitions for tables\n *\n * When provided, these schemas will be used instead of auto-detection.\n * This allows frameworks to provide explicit schemas via dependency injection.\n *\n * @example\n * ```typescript\n * const db = await getDatabase({\n * type: 'json',\n * url: './data',\n * schemas: {\n * users: {\n * tableName: 'users',\n * ddl: 'CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT)',\n * indexes: ['CREATE INDEX idx_users_name ON users(name)']\n * }\n * }\n * });\n * ```\n */\n schemas?: SchemasOption;\n\n /**\n * Unique identifier for in-memory DuckDB instances to enable connection sharing\n * When multiple getDatabase() calls use the same dbid, they receive\n * the same database connection instance.\n *\n * Auto-generated if not provided, enabling connection sharing by default.\n * This solves the issue where multiple collections create separate in-memory\n * databases that can't see each other's tables.\n */\n dbid?: string;\n\n /**\n * Eagerly load all JSON files as tables at connection time\n *\n * When true (default), creates tables with inferred schemas for all JSON files\n * in the data directory, even those without explicit schemas or .schema.sql files.\n * This enables cross-table queries (JOINs, NOT EXISTS subqueries) before\n * syncSchema() is called.\n *\n * Note: Inferred schemas don't include UNIQUE constraints. UPSERT operations\n * require syncSchema() to be called first to establish proper constraints.\n * When syncSchema() is called, tables with inferred schemas are dropped and\n * recreated with the proper schema.\n *\n * @default true\n */\n eagerLoadTables?: boolean;\n\n /**\n * Force a fresh database connection, bypassing the connection cache\n *\n * When true, clears any cached connection for this URL before creating a new one.\n * Useful when you need to reload the database from disk after external changes.\n *\n * @default false\n *\n * @example\n * // Initial connection - cached\n * const db1 = await getDatabase({ type: 'json', url: './data' });\n *\n * // External process adds JSON files...\n *\n * // Force reload from disk\n * const db2 = await getDatabase({\n * type: 'json',\n * url: './data',\n * clearCache: true\n * });\n */\n clearCache?: boolean;\n\n /**\n * Called when a JSON data file parses but its records cannot be loaded into a\n * table that was created for it — from a provided schema, a `.schema.sql`\n * file, or deferred `syncSchema()` / `execute()` DDL — leaving that table\n * present but empty (renamed/dropped column, NOT NULL / PRIMARY KEY violation,\n * or a file whose fields match no column). Every other table still loads.\n *\n * This targets the *silent* failure: a present-but-empty table a query cannot\n * tell apart from a legitimately empty one. A file with no schema at all is\n * loaded by DuckDB's own inference; if that fails the table is simply not\n * created, which already surfaces as an error when you query it, so it is not\n * reported here.\n *\n * Fires for both connection-time and deferred (`syncSchema()` / `execute()`)\n * loads. Without it, such a failure is only visible as a `console.error` on\n * stderr. The same failures are also retrievable after the fact via the\n * adapter's `getTableLoadErrors()` method.\n *\n * Registered once, on the `getDatabase()` call that creates the connection.\n * The JSON adapter caches connections per URL, so a later `getDatabase()` for\n * the same directory returns the existing connection and does *not* re-wire a\n * different `onTableLoadError` — read `getTableLoadErrors()` on the returned\n * adapter instead, which reflects the shared connection's failures.\n *\n * A callback that throws is caught and ignored so it cannot break loading of\n * the remaining tables. See issue #1139.\n */\n onTableLoadError?: (info: JSONTableLoadError) => void;\n}\n\n/**\n * Result of a database operation that modifies data\n */\nexport interface QueryResult {\n /**\n * Type of operation performed (e.g., \"insert\", \"update\", \"delete\")\n */\n operation: string;\n\n /**\n * Number of rows affected by the operation\n */\n affected: number;\n}\n\n/**\n * Options for upsert operations.\n */\nexport interface UpsertOptions {\n /**\n * Preserve database-native NULL-distinct conflict behavior.\n *\n * By default, upsert treats NULL values in conflict columns as matching\n * existing NULL values. Set this to true to opt out and use the underlying\n * database's native ON CONFLICT behavior where NULLs are distinct.\n */\n nullsDistinct?: boolean;\n}\n\n/**\n * Schema definition that can be provided to database adapters\n *\n * Allows frameworks like SMRT to provide explicit schemas instead of\n * relying on auto-detection or circular dependencies.\n *\n * This is a simpler, DDL-based alternative to SchemaDefinition which is\n * used for structured JSON manifests.\n */\nexport interface SchemaProvider {\n /**\n * Table name in snake_case plural form\n */\n tableName: string;\n\n /**\n * DDL statement(s) to create the table\n * Can be a single CREATE TABLE statement or multiple statements\n */\n ddl: string;\n\n /**\n * Index creation statements (optional)\n * Each string should be a complete CREATE INDEX statement\n */\n indexes?: string[];\n\n /**\n * Trigger creation statements (optional, adapter-specific support)\n * Each string should be a complete CREATE TRIGGER statement\n */\n triggers?: string[];\n\n /**\n * Schema version for migration tracking (optional)\n */\n version?: string;\n\n /**\n * Field metadata (optional, for advanced use cases)\n * Can be used by frameworks for runtime type information and validation\n */\n fields?: Map<string, any>;\n}\n\n/**\n * Schema definitions for getDatabase(), either eagerly built or lazy.\n *\n * Callers can pass schemas as a lazy function so that building the schema\n * map (potentially 200+ objects from ObjectRegistry) is deferred until an\n * adapter actually needs it. Adapters that manage tables via migrations\n * (Postgres, SQLite) never call the function, making the cost zero.\n *\n * @example Lazy — only resolved by JSON/DuckDB adapters\n * ```typescript\n * await getDatabase({\n * type: 'postgres',\n * url: '...',\n * schemas: () => ObjectRegistry.getAllSchemas(), // never called\n * });\n * ```\n *\n * @example Eager — built upfront (fine for JSON/DuckDB)\n * ```typescript\n * await getDatabase({\n * type: 'json',\n * url: './data',\n * schemas: { users: { tableName: 'users', ddl: '...' } },\n * });\n * ```\n */\nexport type SchemasOption =\n | Record<string, SchemaProvider>\n | (() => Record<string, SchemaProvider>);\n\n/**\n * Resolves a SchemasOption to its concrete value.\n *\n * Adapters that need schemas (JSON, DuckDB) call this to unwrap the\n * lazy function. Adapters that don't (Postgres, SQLite) simply ignore\n * the `schemas` option without calling this.\n */\nexport function resolveSchemas(\n schemas: SchemasOption | undefined,\n): Record<string, SchemaProvider> | undefined {\n if (!schemas) return undefined;\n if (typeof schemas === 'function') return schemas();\n return schemas;\n}\n\n/**\n * Schema manifest structure for JSON-based schema management\n */\nexport interface SchemaManifest {\n version: string;\n timestamp: number;\n packageName: string;\n schemas: Record<string, SchemaDefinition>;\n dependencies: string[];\n}\n\n/**\n * Schema definition for individual tables\n */\nexport interface SchemaDefinition {\n tableName: string;\n columns: Record<string, ColumnDefinition>;\n indexes: IndexDefinition[];\n triggers: TriggerDefinition[];\n foreignKeys: ForeignKeyDefinition[];\n dependencies: string[];\n version: string;\n packageName: string;\n baseClass?: string;\n}\n\n/**\n * Column definition structure\n */\nexport interface ColumnDefinition {\n type: string;\n primaryKey?: boolean;\n unique?: boolean;\n notNull?: boolean;\n defaultValue?: any;\n check?: string;\n foreignKey?: {\n table: string;\n column: string;\n onDelete?: string;\n onUpdate?: string;\n };\n description?: string;\n}\n\n/**\n * Column definition with name for ALTER TABLE operations\n */\nexport interface ColumnDefinitionWithName extends ColumnDefinition {\n name: string;\n}\n\n/**\n * Index definition structure\n */\nexport interface IndexDefinition {\n name: string;\n columns: string[];\n unique?: boolean;\n where?: string;\n description?: string;\n}\n\n/**\n * Trigger definition structure\n */\nexport interface TriggerDefinition {\n name: string;\n when: string;\n event: string;\n table: string;\n condition?: string;\n body: string;\n description?: string;\n}\n\n/**\n * Foreign key constraint definition\n */\nexport interface ForeignKeyDefinition {\n column: string;\n referencesTable: string;\n referencesColumn: string;\n onDelete?: string;\n onUpdate?: string;\n}\n\n/**\n * Options for schema initialization and management\n */\nexport interface SchemaInitializationOptions {\n /** Schema manifest containing table definitions */\n manifest?: SchemaManifest;\n /** Raw SQL DDL schema string (legacy support) */\n schema?: string;\n /** Schema overrides for extending base schemas */\n overrides?: Record<string, SchemaDefinition>;\n /** Force recreation of tables */\n force?: boolean;\n /** Enable debug logging */\n debug?: boolean;\n}\n\n/**\n * Table schema information returned by getTableSchema()\n */\nexport interface TableSchemaInfo {\n /** Table name */\n tableName: string;\n /** Columns in the table */\n columns: Record<string, ColumnDefinition>;\n /** Indexes on the table */\n indexes: IndexDefinition[];\n /** Foreign key constraints */\n foreignKeys: ForeignKeyDefinition[];\n}\n\n/**\n * Options for vector similarity search\n */\nexport interface VectorSearchOptions {\n /** Maximum number of results to return */\n limit?: number;\n /** Distance metric for similarity comparison @default 'cosine' */\n metric?: 'cosine' | 'l2' | 'ip';\n /**\n * Additional SQL WHERE clause.\n * Use $2, $3, etc. for parameter placeholders ($1 is reserved for the query vector).\n * @example `category = $2`\n */\n where?: string;\n /** Parameters for the WHERE clause, bound to $2, $3, etc. */\n params?: any[];\n}\n\n/**\n * Result of a vector similarity search\n */\nexport interface VectorSearchResult {\n /** Row primary key */\n id: string;\n /**\n * Distance from query vector (lower = more similar).\n * Range varies by metric: cosine [0, 2], L2 [0, Infinity), ip (-Infinity, Infinity).\n */\n distance: number;\n /** Additional columns from the row */\n [key: string]: any;\n}\n\n/**\n * Options for creating a vector index\n */\nexport interface VectorIndexOptions {\n /** Number of dimensions (informational only, dimensions are set via ensureColumn) */\n dimensions?: number;\n /** Distance metric @default 'cosine' */\n metric?: 'cosine' | 'l2' | 'ip';\n /** Index type @default 'hnsw' */\n type?: 'hnsw' | 'ivfflat';\n}\n\n/**\n * Vector operations capability for database adapters\n *\n * Provides native vector similarity search, storage, and indexing.\n * Implemented by PostgreSQL via pgvector and optionally by SQLite via\n * sqlite-vector.\n */\nexport interface VectorCapabilities {\n /**\n * Search for similar vectors\n *\n * @param table - Table containing vector column\n * @param column - Name of the vector column\n * @param embedding - Query vector\n * @param options - Search options\n * @returns Ranked results by similarity\n */\n search(\n table: string,\n column: string,\n embedding: number[],\n options?: VectorSearchOptions,\n ): Promise<VectorSearchResult[]>;\n\n /**\n * Ensure a vector column exists on a table\n *\n * @param table - Table name\n * @param column - Column name\n * @param dimensions - Number of vector dimensions\n */\n ensureColumn(\n table: string,\n column: string,\n dimensions: number,\n ): Promise<void>;\n\n /**\n * Ensure a vector index exists on a column\n *\n * @param table - Table name\n * @param column - Column name\n * @param options - Index options\n */\n ensureIndex(\n table: string,\n column: string,\n options?: VectorIndexOptions,\n ): Promise<void>;\n\n /**\n * Store or update a vector in a specific row\n *\n * @param table - Table name\n * @param where - Conditions to identify the row\n * @param column - Vector column name\n * @param embedding - Vector data\n */\n upsertVector(\n table: string,\n where: Record<string, any>,\n column: string,\n embedding: number[],\n ): Promise<void>;\n}\n\n/**\n * SQLite notification delivered by optional notification capabilities.\n */\nexport interface DatabaseNotification {\n id: number;\n channel: string;\n payload: any;\n createdAt?: number | string | Date | null;\n}\n\n/**\n * Options for notification listeners.\n */\nexport interface NotificationListenOptions {\n /**\n * Fallback poll interval in milliseconds when the native watcher does not\n * receive an update signal.\n */\n fallbackPollMs?: number | null;\n}\n\n/**\n * Options for waiting on the next database update.\n */\nexport interface NotificationWaitOptions {\n /**\n * Maximum time to wait in milliseconds. When omitted, waits indefinitely.\n */\n timeoutMs?: number;\n\n /**\n * Optional abort signal to cancel the wait.\n */\n signal?: AbortSignal;\n}\n\n/**\n * Options for pruning persisted notification rows.\n */\nexport interface NotificationPruneOptions {\n /** Delete notifications older than this many seconds. */\n olderThanS?: number | null;\n /** Keep at most this many most-recent notifications. */\n maxKeep?: number | null;\n}\n\n/**\n * Optional notification/pub-sub capabilities for database adapters.\n */\nexport interface NotificationCapabilities {\n notify(channel: string, payload: any): Promise<number>;\n listen(\n channel: string,\n options?: NotificationListenOptions,\n ): AsyncIterable<DatabaseNotification>;\n waitForUpdate(options?: NotificationWaitOptions): Promise<boolean>;\n prune(options?: NotificationPruneOptions): Promise<number>;\n}\n\n/**\n * Options for SQLite native notification support.\n */\nexport interface SqliteNotificationCapabilityOptions {\n watcherBackend?: 'polling' | 'kernel' | 'shm';\n maxReaders?: number;\n}\n\n/**\n * Options for SQLite native vector support.\n */\nexport interface SqliteVectorCapabilityOptions {\n preload?: boolean;\n quantization?: 'turbo4' | 'turbo3' | 'turbo2' | 'uint8' | 'int8' | '1bit';\n maxMemory?: string;\n}\n\n/**\n * Optional SQLite-only native capabilities.\n */\nexport interface SqliteCapabilitiesOptions {\n notifications?: boolean | SqliteNotificationCapabilityOptions;\n vector?: boolean | SqliteVectorCapabilityOptions;\n}\n\n/**\n * Common interface for database adapters\n * Provides a unified API for different database backends\n */\nexport interface DatabaseInterface {\n /**\n * Database location identifier\n * For file-based databases: file path\n * For directory-based databases (JSON/DuckDB with JSON): directory path\n * For in-memory databases: ':memory:'\n * For remote databases: connection URL\n */\n url: string;\n\n /**\n * Underlying database client instance\n */\n client: any;\n\n /**\n * Inserts one or more records into a table\n *\n * @param table - Table name\n * @param data - Single record or array of records to insert\n * @returns Promise resolving to operation result\n */\n insert: (\n table: string,\n data: Record<string, any> | Record<string, any>[],\n ) => Promise<QueryResult>;\n\n /**\n * Retrieves a single record matching the where criteria\n *\n * @param table - Table name\n * @param where - Criteria to match records (object for AND-only, 2D array for OR/AND)\n * @returns Promise resolving to matching record or null if not found\n */\n get: (\n table: string,\n where: WhereClause,\n ) => Promise<Record<string, any> | null>;\n\n /**\n * Retrieves multiple records matching the where criteria\n *\n * @param table - Table name\n * @param where - Criteria to match records (object for AND-only, 2D array for OR/AND)\n * @returns Promise resolving to array of matching records\n */\n list: (table: string, where: WhereClause) => Promise<Record<string, any>[]>;\n\n /**\n * Updates records matching the where criteria\n *\n * @param table - Table name\n * @param where - Criteria to match records to update (object for AND-only, 2D array for OR/AND)\n * @param data - New data to set\n * @returns Promise resolving to operation result\n */\n update: (\n table: string,\n where: WhereClause,\n data: Record<string, any>,\n ) => Promise<QueryResult>;\n\n /**\n * Inserts a record or updates it if it already exists (UPSERT)\n * Uses database-specific ON CONFLICT / ON DUPLICATE KEY syntax\n *\n * @param table - Table name\n * @param conflictColumns - Columns that define the uniqueness constraint\n * @param data - Data to insert or update\n * @returns Promise resolving to operation result\n *\n * @example\n * ```typescript\n * // Upsert a user by email\n * await db.upsert('users', ['email'], {\n * email: 'user@example.com',\n * name: 'John Doe',\n * updated_at: new Date().toISOString()\n * });\n *\n * // Upsert with composite key\n * await db.upsert('settings', ['user_id', 'key'], {\n * user_id: '123',\n * key: 'theme',\n * value: 'dark'\n * });\n * ```\n */\n upsert: (\n table: string,\n conflictColumns: string[],\n data: Record<string, any>,\n options?: UpsertOptions,\n ) => Promise<QueryResult>;\n\n /**\n * Gets a record matching the where criteria or inserts it if not found\n *\n * @param table - Table name\n * @param where - Criteria to match existing record (object for AND-only, 2D array for OR/AND)\n * @param data - Data to insert if no record found\n * @returns Promise resolving to the record (either retrieved or newly inserted)\n */\n getOrInsert: (\n table: string,\n where: WhereClause,\n data: Record<string, any>,\n ) => Promise<Record<string, any>>;\n\n /**\n * Deletes records from a table matching the where criteria\n *\n * @param table - Table name\n * @param where - Criteria to match records for deletion (object for AND-only, 2D array for OR/AND)\n * @returns Promise resolving to operation result with count of deleted rows\n *\n * @example\n * ```typescript\n * // Delete by ID\n * await db.delete('products', { id: 'prod-123' });\n *\n * // Delete by criteria\n * await db.delete('orders', { status: 'cancelled' });\n *\n * // Delete with multiple conditions\n * await db.delete('sessions', {\n * user_id: 'user-123',\n * expired: true\n * });\n * ```\n */\n delete: (table: string, where: WhereClause) => Promise<QueryResult>;\n\n /**\n * Counts records in a table matching the where criteria\n *\n * @param table - Table name\n * @param where - Criteria to match records (optional, counts all if omitted; object for AND-only, 2D array for OR/AND)\n * @returns Promise resolving to count of matching records\n *\n * @example\n * ```typescript\n * // Count all records\n * const total = await db.count('products');\n *\n * // Count with single condition\n * const activeCount = await db.count('products', { active: true });\n *\n * // Count with multiple conditions\n * const filteredCount = await db.count('orders', {\n * status: 'pending',\n * priority: 'high'\n * });\n * ```\n */\n count: (table: string, where?: WhereClause) => Promise<number>;\n\n /**\n * Creates a table-specific interface for simplified table operations\n *\n * @param table - Table name\n * @returns TableInterface for the specified table\n */\n table: (table: string) => TableInterface;\n\n /**\n * Checks if a table exists in the database\n *\n * @param table - Table name\n * @returns Promise resolving to boolean indicating existence\n */\n tableExists: (table: string) => Promise<boolean>;\n\n /**\n * Whether this adapter requires schema existence checks at runtime.\n *\n * Set to `true` on adapters where tables may not exist yet and need\n * to be verified before use (e.g. JSON/DuckDB with auto-created tables).\n *\n * When absent or `false`, frameworks skip `tableExists()` calls during\n * collection initialization — tables are assumed to exist because\n * schema is managed by migrations (e.g. `smrt db:migrate`).\n *\n * - Postgres adapter: not set (migration-managed)\n * - SQLite adapter: not set (migration-managed)\n * - JSON/DuckDB adapter: `true` (tables may be auto-created)\n */\n requiresSchemaCheck?: boolean;\n\n /**\n * Executes a SQL query using template literals and returns multiple rows\n *\n * @param strings - Template strings\n * @param vars - Variables to interpolate into the query\n * @returns Promise resolving to array of result records\n */\n many: (\n strings: TemplateStringsArray,\n ...vars: any[]\n ) => Promise<Record<string, any>[]>;\n\n /**\n * Executes a SQL query using template literals and returns a single row\n *\n * @param strings - Template strings\n * @param vars - Variables to interpolate into the query\n * @returns Promise resolving to a single result record or null\n */\n single: (\n strings: TemplateStringsArray,\n ...vars: any[]\n ) => Promise<Record<string, any> | null>;\n\n /**\n * Executes a SQL query using template literals and returns a single value\n *\n * @param strings - Template strings\n * @param vars - Variables to interpolate into the query\n * @returns Promise resolving to a single value (first column of first row)\n */\n pluck: (strings: TemplateStringsArray, ...vars: any[]) => Promise<any>;\n\n /**\n * Executes a SQL query using template literals without returning results\n *\n * @param strings - Template strings\n * @param vars - Variables to interpolate into the query\n * @returns Promise that resolves when the query completes\n */\n execute: (strings: TemplateStringsArray, ...vars: any[]) => Promise<void>;\n\n /**\n * Alias for many() - Executes a SQL query and returns multiple rows\n */\n oo: (\n strings: TemplateStringsArray,\n ...vars: any[]\n ) => Promise<Record<string, any>[]>;\n\n /**\n * Alias for single() - Executes a SQL query and returns a single row\n */\n oO: (\n strings: TemplateStringsArray,\n ...vars: any[]\n ) => Promise<Record<string, any> | null>;\n\n /**\n * Alias for pluck() - Executes a SQL query and returns a single value\n */\n ox: (strings: TemplateStringsArray, ...vars: any[]) => Promise<any>;\n\n /**\n * Alias for execute() - Executes a SQL query without returning results\n */\n xx: (strings: TemplateStringsArray, ...vars: any[]) => Promise<void>;\n\n /**\n * Executes a raw SQL query with parameterized values\n *\n * Raw queries use the adapter's native placeholder syntax.\n * Parameters may be passed as rest arguments or as one values array.\n * For adapter-specific array parameter caveats, see the package docs.\n *\n * @param str - SQL query string\n * @param vars - Variables to use as parameters\n * @returns Promise resolving to query result with rows and count\n */\n query: (\n str: string,\n ...vars: any[]\n ) => Promise<{ rows: Record<string, any>[]; rowCount: number }>;\n\n /**\n * Synchronizes database schema with provided SQL DDL\n * Creates tables if they don't exist and adds missing columns\n *\n * @param schema - SQL schema definition with CREATE TABLE statements\n * @returns Promise that resolves when schema is synchronized\n */\n syncSchema?: (schema: string) => Promise<void>;\n\n /**\n * Initialize database schemas from JSON manifest\n * Supports dependency resolution and schema overrides\n *\n * @param options - Schema initialization options\n * @returns Promise that resolves when schemas are initialized\n */\n initializeSchemas?: (options: SchemaInitializationOptions) => Promise<void>;\n\n /**\n * Executes a callback within a database transaction\n * Automatically commits on success or rolls back on error\n *\n * Calling `transaction()` again on the `tx` handed to a callback is\n * adapter-specific — do not assume one behaviour across adapters:\n *\n * - **SQLite** (both the libsql and native-capabilities paths) — the nested\n * scope re-enters the transaction already in progress under a `SAVEPOINT`.\n * It reads the enclosing transaction's uncommitted rows, and if it throws,\n * only its own work is rolled back; the enclosing transaction stays usable.\n * - **DuckDB and JSON** — the engine has no `SAVEPOINT`, so nesting throws\n * {@link NestedTransactionError} without touching the connection. The\n * enclosing transaction is unaffected and can continue.\n * - **PostgreSQL** — the nested scope re-enters the enclosing transaction\n * under a `SAVEPOINT` on the same pooled connection, like SQLite. It used\n * to check out a second connection and begin an independent transaction,\n * which could not see the enclosing transaction's uncommitted rows and\n * could deadlock against it undetectably; that was fixed in #1108.\n *\n * Code that must run on every adapter should take the transaction as a\n * parameter rather than opening a nested one.\n *\n * **Transactions are serialized per connection on the single-connection\n * adapters** — SQLite (both paths), DuckDB and JSON drive one connection, and\n * a connection can only be in one transaction at a time, so a concurrent\n * `transaction()` waits for the one in progress rather than corrupting it. A\n * queued call that waits longer than `transactionQueueTimeout` (30s by\n * default) rejects rather than stalling indefinitely. Re-entrant calls do not\n * queue — they take the savepoint or refusal path described above.\n * PostgreSQL pools, so its transactions run concurrently and never queue.\n *\n * @param callback - Function to execute within transaction\n * @returns Promise resolving to callback result\n * @throws {NestedTransactionError} When nesting on an adapter without\n * savepoint support\n *\n * @example Portable across all adapters\n * ```typescript\n * async function recordClick(db: DatabaseInterface, id: string) {\n * await db.update('links', { id }, { clicks: 1 });\n * }\n *\n * await db.transaction(async (tx) => {\n * await tx.query('SELECT * FROM links WHERE id = $1 FOR UPDATE', id);\n * await recordClick(tx, id); // reuses tx; never opens its own\n * });\n * ```\n */\n transaction?: <T>(\n callback: (tx: DatabaseInterface) => Promise<T>,\n ) => Promise<T>;\n\n /**\n * Begins a new transaction and returns a handle for manual control\n *\n * Unlike transaction(), this gives you explicit control over commit/rollback.\n * Ideal for test isolation where you want to rollback after each test.\n *\n * On the single-connection adapters — SQLite (both paths), DuckDB and JSON —\n * the handle owns the connection until {@link TransactionHandle.commit} or\n * {@link TransactionHandle.rollback} is called, so every other transaction on\n * that connection waits in the meantime. **A handle that is never ended holds\n * the connection for the life of the process**, and later transactions then\n * fail with the queue-timeout error instead of proceeding. End the handle in\n * a `finally`, and in per-test setup guard the teardown, because an\n * assertion that throws between `beginTransaction()` and the teardown would\n * otherwise strand it.\n *\n * @returns Promise resolving to a TransactionHandle\n *\n * @example Test isolation pattern\n * ```typescript\n * let tx: TransactionHandle | undefined;\n *\n * beforeEach(async () => {\n * tx = await db.beginTransaction();\n * });\n *\n * afterEach(async () => {\n * // Guarded: if beforeEach threw, there is no handle to end, and an\n * // unguarded rollback would mask that failure with its own.\n * await tx?.rollback(); // Discard all test changes\n * tx = undefined;\n * });\n *\n * it('creates user', async () => {\n * await tx.insert('users', { id: '1', name: 'Test' });\n * const user = await tx.get('users', { id: '1' });\n * expect(user).toBeDefined();\n * // Changes rolled back in afterEach\n * });\n * ```\n */\n beginTransaction?: () => Promise<TransactionHandle>;\n\n /**\n * Acquire a pinned single-connection session.\n *\n * Returns a {@link SessionHandle} whose queries all run on one underlying\n * connection, enabling session-scoped state (e.g. PostgreSQL session advisory\n * locks) that the pooled top-level {@link query} cannot hold reliably. The\n * caller must {@link SessionHandle.release} it.\n *\n * Only meaningful for engines with session-scoped state, so it is currently\n * implemented for PostgreSQL only; check for the method before calling\n * (`if (db.acquireSession)`). Single-connection engines (SQLite, DuckDB) have\n * no session-scoped locks to hold, so they leave it undefined.\n *\n * @returns Promise resolving to a SessionHandle\n *\n * @example Hold a Postgres session advisory lock\n * ```typescript\n * const session = await db.acquireSession();\n * try {\n * await session.query('SELECT pg_advisory_lock($1)', 42);\n * // ... lock is held for the life of this session ...\n * } finally {\n * await session.release(); // frees the lock and drops the connection\n * }\n * ```\n */\n acquireSession?: () => Promise<SessionHandle>;\n\n /**\n * Retrieves the schema information for a table\n *\n * @param table - Table name\n * @returns Promise resolving to table schema info or null if table doesn't exist\n */\n getTableSchema?: (table: string) => Promise<TableSchemaInfo | null>;\n\n /**\n * Vector operations for native similarity search\n *\n * When available, enables database-level vector operations (e.g., pgvector).\n * Consumers should check for this capability rather than checking the database type.\n */\n vector?: VectorCapabilities;\n\n /**\n * Optional notification/pub-sub operations.\n */\n notifications?: NotificationCapabilities;\n\n /**\n * Close the database adapter and release native resources when supported.\n */\n close?: () => Promise<void>;\n\n /**\n * ALTER TABLE operations for schema evolution\n */\n alterTable?: {\n /**\n * Adds a new column to an existing table\n *\n * @param table - Table name\n * @param column - Column definition with name\n * @returns Promise that resolves when column is added\n */\n addColumn: (\n table: string,\n column: ColumnDefinitionWithName,\n ) => Promise<void>;\n\n /**\n * Adds a new index to an existing table\n *\n * @param table - Table name\n * @param index - Index definition\n * @returns Promise that resolves when index is created\n */\n addIndex: (table: string, index: IndexDefinition) => Promise<void>;\n };\n}\n\n/**\n * Transaction handle for manual transaction control\n *\n * Extends DatabaseInterface with commit() and rollback() methods.\n * Use beginTransaction() to obtain a handle, then explicitly\n * commit or rollback when done.\n *\n * @example Test isolation with rollback\n * ```typescript\n * const tx = await db.beginTransaction();\n * try {\n * // All operations happen in the transaction\n * await tx.insert('users', { id: '1', name: 'Test' });\n * const user = await tx.get('users', { id: '1' });\n *\n * // Rollback discards all changes (useful for test isolation)\n * await tx.rollback();\n * } catch (error) {\n * await tx.rollback();\n * throw error;\n * }\n * ```\n *\n * @example Manual commit\n * ```typescript\n * const tx = await db.beginTransaction();\n * try {\n * await tx.insert('orders', { id: '1', total: 100 });\n * await tx.insert('order_items', { order_id: '1', product: 'Widget' });\n * await tx.commit(); // Persist changes\n * } catch (error) {\n * await tx.rollback();\n * throw error;\n * }\n * ```\n */\nexport interface TransactionHandle extends DatabaseInterface {\n /**\n * Commits the transaction, persisting all changes\n *\n * After commit, the transaction handle should not be used.\n */\n commit: () => Promise<void>;\n\n /**\n * Rolls back the transaction, discarding all changes\n *\n * After rollback, the transaction handle should not be used.\n * This is the key method for test isolation - rollback after each test.\n */\n rollback: () => Promise<void>;\n\n /**\n * Whether the transaction is still active (not committed or rolled back)\n */\n isActive: () => boolean;\n}\n\n/**\n * Pinned single-connection session handle.\n *\n * Obtained via {@link DatabaseInterface.acquireSession}. Unlike the pooled\n * top-level `query` (which may run each statement on a different connection),\n * every call on a session handle runs on the *same* underlying connection for\n * the handle's lifetime. This is required for session-scoped state such as\n * PostgreSQL session advisory locks (`pg_advisory_lock`), which must be\n * acquired, observed, and released on one connection — and which release\n * automatically if the connection drops (e.g. the process dies).\n *\n * A session holds one connection out of the pool (default max 20) for its\n * entire lifetime, so it is meant to be held one-per-process, not per-operation.\n * Always {@link SessionHandle.release} the handle when done, and release it\n * *before* calling `db.client.end()` — a pinned connection keeps `pool.end()`\n * from resolving.\n */\nexport interface SessionHandle {\n /**\n * Execute a raw query on this session's pinned connection.\n * Same placeholder/return contract as {@link DatabaseInterface.query}.\n * Throws if the session has been released or its connection was lost.\n */\n query: (\n sql: string,\n ...vars: any[]\n ) => Promise<{ rows: Record<string, any>[]; rowCount: number }>;\n\n /**\n * Whether the session is still usable — not released and its connection has\n * not errored/dropped. A long-lived lock holder can poll this without\n * issuing a query.\n */\n isActive: () => boolean;\n\n /**\n * Release the session. Idempotent. After release the handle must not be used.\n * Best-effort frees session-scoped locks, then drops the underlying\n * connection (it is NOT returned to the pool) so the database releases any\n * remaining session state — returning it to the pool would leak that state\n * onto a later, unrelated checkout.\n */\n release: () => Promise<void>;\n}\n\n/**\n * Simplified interface for table-specific operations\n */\nexport interface TableInterface {\n /**\n * Inserts one or more records into the table\n *\n * @param data - Single record or array of records to insert\n * @returns Promise resolving to operation result\n */\n insert: (\n data: Record<string, any> | Record<string, any>[],\n ) => Promise<QueryResult>;\n\n /**\n * Retrieves a single record from the table matching the where criteria\n *\n * @param where - Criteria to match records (object for AND-only, 2D array for OR/AND)\n * @returns Promise resolving to matching record or null if not found\n */\n get: (where: WhereClause) => Promise<Record<string, any> | null>;\n\n /**\n * Retrieves multiple records from the table matching the where criteria\n *\n * @param where - Criteria to match records (object for AND-only, 2D array for OR/AND)\n * @returns Promise resolving to array of matching records\n */\n list: (where: WhereClause) => Promise<Record<string, any>[]>;\n}\n"],"mappings":";;;;;;;;;AASA,IAAa,uCAAuC;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDpD,SAAgB,sBACd,SACA,YAAoB,sCACH;CAKjB,IAAI,CAAC,OAAO,SAAS,SAAS,KAAK,aAAa,GAC9C,MAAM,IAAI,cACR,6EACA;EAAE;EAAS;CAAU,CACvB;CAGF,IAAI,OAAsB,QAAQ,QAAQ;CAE1C,MAAM,UAAU,YAAiC;EAC/C,MAAM,WAAW;EACjB,IAAI,gBAA4B,CAAC;EACjC,MAAM,UAAU,IAAI,SAAe,YAAY;GAC7C,UAAU;EACZ,CAAC;EAID,OAAO,SAAS,WACR,eACA,OACR;EAEA,IAAI;EACJ,IAAI;GACF,MAAM,IAAI,SAAe,SAAS,WAAW;IAC3C,QAAQ,iBAAiB;KACvB,OACE,IAAI,cACF,mBAAmB,UAAU,qBAAqB,QAAQ,sbAC1D;MAAE;MAAS;KAAU,CACvB,CACF;IACF,GAAG,SAAS;IACZ,SAAS,KAAK,SAAS,OAAO;GAChC,CAAC;EACH,SAAS,OAAO;GAGd,QAAQ;GACR,MAAM;EACR,UAAU;GACR,IAAI,OAAO,aAAa,KAAK;EAC/B;EAEA,IAAI,WAAW;EACf,aAAa;GACX,IAAI,UAAU;GACd,WAAW;GACX,QAAQ;EACV;CACF;CAEA,MAAM,MAAM,OAAU,SAAuC;EAC3D,MAAM,UAAU,MAAM,QAAQ;EAC9B,IAAI;GACF,OAAO,MAAM,KAAK;EACpB,UAAU;GACR,QAAQ;EACV;CACF;CAEA,OAAO;EAAE;EAAS;CAAI;AACxB;;;;;;;;;;;;;;;;;;;;;;;;;ACxGA,IAAa,yBAAb,cAA4C,cAAc;CACxD,YAAY,SAAiB,SAAmC;EAC9D,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;CACd;AACF;;;;;;;;AAiZA,SAAgB,eACd,SAC4C;CAC5C,IAAI,CAAC,SAAS,OAAO,KAAA;CACrB,IAAI,OAAO,YAAY,YAAY,OAAO,QAAQ;CAClD,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"types-uVFHgaSU.js","names":[],"sources":["../../src/shared/transaction-lock.ts","../../src/shared/types.ts"],"sourcesContent":["import { DatabaseError } from '@happyvertical/utils';\n\n/**\n * How long a queued transaction waits for the connection before giving up.\n *\n * A transaction that legitimately runs longer than this will make the ones\n * behind it fail rather than stall indefinitely. Raise it with the adapter's\n * `transactionQueueTimeout` option if that is the wrong trade for a workload.\n */\nexport const DEFAULT_TRANSACTION_QUEUE_TIMEOUT_MS = 30_000;\n\n/**\n * Serializes transactions on a connection that cannot hold more than one.\n */\nexport interface TransactionLock {\n /**\n * Takes the connection and returns the function that gives it back.\n *\n * Calling the returned function more than once is a no-op, so a teardown\n * path that runs twice cannot hand the connection to two callers at once.\n */\n acquire(): Promise<() => void>;\n\n /**\n * Runs `work` with exclusive use of the connection, releasing it however\n * `work` finishes.\n */\n run<T>(work: () => Promise<T>): Promise<T>;\n}\n\n/**\n * Creates a lock that serializes transactions on one connection.\n *\n * SQLite, DuckDB and the JSON adapter each drive a single connection, and a\n * connection can only be in one transaction at a time. `transaction()` used to\n * issue `BEGIN` on that shared connection with no serialization and no record\n * of whether one was already open, so two overlapping calls corrupted each\n * other: the second `BEGIN` threw, its `catch` ran `ROLLBACK`, and that\n * rollback landed on the *first* transaction — committing part of its work,\n * discarding the rest, and rejecting its promise. Nesting was not required;\n * two concurrent requests were enough, which made it reachable from any server\n * handling more than one at a time.\n *\n * The lock spans the whole `BEGIN` … `COMMIT`/`ROLLBACK` sequence, so a queued\n * transaction starts only once the previous one has ended. It is a\n * promise-chain mutex, the same shape as the `withNullAwareUpsertLock` the\n * SQLite adapter already used for the narrower upsert case.\n *\n * Re-entrant calls must not come through here. A nested `tx.transaction()`\n * arrives via the transaction-scoped interface, which re-enters under a\n * `SAVEPOINT` (SQLite) or refuses (DuckDB, JSON) without ever reaching the\n * top-level `transaction()`; routing one here instead would make it wait on a\n * lock its own caller is holding.\n *\n * @param adapter - Adapter name, for the timeout error\n * @param timeoutMs - Ceiling on how long to wait for a turn\n */\nexport function createTransactionLock(\n adapter: string,\n timeoutMs: number = DEFAULT_TRANSACTION_QUEUE_TIMEOUT_MS,\n): TransactionLock {\n // The timer is measured from enqueue, not from when the lock frees, so this\n // bounds the total wait rather than any one holder. A caller that expects\n // sustained bursts on one connection should raise it: a hundred queued\n // 400ms transactions exceed 30s even though none of them is stuck.\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n throw new DatabaseError(\n 'transactionQueueTimeout must be a positive, finite number of milliseconds',\n { adapter, timeoutMs },\n );\n }\n // Resolves when the transaction currently holding the connection ends.\n let tail: Promise<void> = Promise.resolve();\n\n const acquire = async (): Promise<() => void> => {\n const previous = tail;\n let release: () => void = () => {};\n const current = new Promise<void>((resolve) => {\n release = resolve;\n });\n\n // Publish our turn before awaiting, so a caller that arrives while we are\n // suspended queues behind us rather than racing us for the connection.\n tail = previous.then(\n () => current,\n () => current,\n );\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n await new Promise<void>((resolve, reject) => {\n timer = setTimeout(() => {\n reject(\n new DatabaseError(\n `Timed out after ${timeoutMs}ms waiting for the ${adapter} connection's current transaction to finish. This connection runs one transaction at a time. Usually one of: a beginTransaction() handle that was never committed or rolled back; a top-level db.* call that opens its own transaction being made inside a transaction callback, which waits on the connection its own caller holds (use the tx-scoped method instead); or simply more queued transactions than transactionQueueTimeout allows for.`,\n { adapter, timeoutMs },\n ),\n );\n }, timeoutMs);\n previous.then(resolve, resolve);\n });\n } catch (error) {\n // Drop out of the queue rather than wedge it: `tail` still chains through\n // `current`, so resolving it lets the callers behind us take their turn.\n release();\n throw error;\n } finally {\n if (timer) clearTimeout(timer);\n }\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n release();\n };\n };\n\n const run = async <T>(work: () => Promise<T>): Promise<T> => {\n const release = await acquire();\n try {\n return await work();\n } finally {\n release();\n }\n };\n\n return { acquire, run };\n}\n","import { DatabaseError } from '@happyvertical/utils';\n\n/**\n * Error thrown when `transaction()` is called on a transaction-scoped interface\n * belonging to an adapter whose engine has no savepoints.\n *\n * DuckDB — and therefore the JSON adapter built on it — does not implement\n * `SAVEPOINT`, so there is no way to re-enter the current transaction. Adapters\n * that can re-enter (PostgreSQL, SQLite) do so under a savepoint and never\n * throw this.\n *\n * The alternative would be to open a second, independent transaction, which is\n * what these adapters used to do; on a shared connection that silently\n * destroyed the enclosing transaction's uncommitted work.\n *\n * @example Ensuring a helper works on every adapter\n * ```typescript\n * // Take the transaction as a parameter rather than opening a nested one.\n * async function recordClick(db: DatabaseInterface, id: string) {\n * await db.update('links', { id }, { clicks: 1 });\n * }\n * await db.transaction(async (tx) => recordClick(tx, id));\n * ```\n */\nexport class NestedTransactionError extends DatabaseError {\n constructor(message: string, context?: Record<string, unknown>) {\n super(message, context);\n this.name = 'NestedTransactionError';\n }\n}\n\n/**\n * WHERE clause input for database queries\n *\n * Supports two formats:\n * 1. Object format (AND-only): `{ status: 'active', 'price >': 100 }`\n * - All conditions are AND-joined\n *\n * 2. 2D array format (OR/AND compound logic): `[[cond1, cond2], [cond3, cond4]]`\n * - Inner arrays are AND-joined: `(cond1 AND cond2)`\n * - Outer array is OR-joined: `(cond1 AND cond2) OR (cond3 AND cond4)`\n *\n * @example Object format (backward compatible)\n * ```typescript\n * // WHERE status = 'active' AND price > 100\n * const where = { status: 'active', 'price >': 100 };\n * ```\n *\n * @example 2D array format for OR/AND compound logic\n * ```typescript\n * // WHERE (status = 'active' AND price > 100) OR (status = 'pending' AND priority = 'high')\n * const where = [\n * [{ status: 'active' }, { 'price >': 100 }],\n * [{ status: 'pending' }, { priority: 'high' }]\n * ];\n * ```\n */\nexport type WhereClause = Record<string, any> | Record<string, any>[][];\n\n/**\n * Common database connection options\n */\nexport interface DatabaseOptions {\n /**\n * Database connection URL\n */\n url?: string;\n\n /**\n * Authentication token for the database connection\n */\n authToken?: string;\n\n /**\n * How long a queued transaction waits for the connection, in milliseconds.\n *\n * Applies to the single-connection adapters — SQLite (both the LibSQL and\n * native paths), DuckDB and JSON. Those drive one connection, so transactions\n * run one at a time and an overlapping `transaction()` waits its turn.\n * PostgreSQL pools and ignores this.\n *\n * The clock starts when the call queues, not when the connection frees, so\n * this bounds the total wait rather than any single transaction: raise it for\n * workloads with long transactions *or* with sustained bursts on one\n * connection. Must be positive and finite.\n *\n * Read once, when the connection is created. Adapters that cache connections\n * hand a later `getDatabase()` for the same database the existing one, which\n * keeps the timeout the first caller asked for.\n *\n * @default 30000\n */\n transactionQueueTimeout?: number;\n}\n\n/**\n * DuckDB-specific connection options\n */\nexport interface DuckDBOptions extends DatabaseOptions {\n /**\n * Database type identifier\n */\n type?: 'duckdb';\n\n /**\n * Path to directory containing JSON files to auto-register as tables\n * @default './data'\n */\n dataDir?: string;\n\n /**\n * Automatically register JSON files in dataDir as queryable tables\n * @default true\n */\n autoRegisterJSON?: boolean;\n\n /**\n * Strategy for writing data back to JSON files\n * - 'immediate': Write changes to JSON files immediately\n * - 'manual': Require explicit export calls\n * - 'none': Read-only mode, no writes to JSON files\n * @default 'none'\n */\n writeStrategy?: 'immediate' | 'manual' | 'none';\n\n /**\n * Whether to use a persistent DuckDB file for caching/indexes\n * If true, uses url or defaults to ':memory:'\n * @default false\n */\n persistent?: boolean;\n\n /**\n * Explicit schema definitions for tables\n * When provided, these schemas will be used for table creation\n */\n schemas?: SchemasOption;\n}\n\n/**\n * A JSON data file that parsed but whose records could not be loaded into the\n * table — e.g. a renamed or dropped column, a NOT NULL / PRIMARY KEY violation,\n * or a file whose fields match no column on the table.\n *\n * The JSON adapter keeps the table (empty) and continues loading every other\n * table, logging the failure to stderr. This record makes the same failure\n * observable to calling code so a table that silently stays empty can be\n * detected. See issue #1139.\n */\nexport interface JSONTableLoadError {\n /** Name of the table whose data file failed to load */\n table: string;\n /** Path to the JSON data file that could not be loaded */\n filePath: string;\n /**\n * The underlying failure. Typically a `DatabaseError` whose `context`\n * carries the engine-level cause (e.g. the DuckDB constraint message).\n */\n error: unknown;\n}\n\n/**\n * JSON database adapter options (DuckDB-backed)\n *\n * Uses DuckDB's in-memory engine to query JSON files directly.\n * No WAL files or persistent database files are created.\n */\nexport interface JSONOptions {\n /**\n * Database type identifier\n */\n type: 'json';\n\n /**\n * Path to directory containing JSON files (required)\n * JSON files in this directory will be loaded as queryable tables\n */\n url: string;\n\n /**\n * Automatically load all JSON files in dataDir as tables\n * @default true\n */\n autoRegister?: boolean;\n\n /**\n * Strategy for writing changes back to JSON files\n * - 'immediate': Auto-save after every insert/update (default)\n * - 'manual': Require explicit exportTable() calls\n * - 'none': Read-only mode, throws error on writes\n * @default 'immediate'\n */\n writeStrategy?: 'immediate' | 'manual' | 'none';\n\n /**\n * How long a queued transaction waits for the connection, in milliseconds.\n *\n * Applies to the single-connection adapters — SQLite (both the LibSQL and\n * native paths), DuckDB and JSON. Those drive one connection, so transactions\n * run one at a time and an overlapping `transaction()` waits its turn.\n * PostgreSQL pools and ignores this.\n *\n * The clock starts when the call queues, not when the connection frees, so\n * this bounds the total wait rather than any single transaction: raise it for\n * workloads with long transactions *or* with sustained bursts on one\n * connection. Must be positive and finite.\n *\n * Read once, when the connection is created. Adapters that cache connections\n * hand a later `getDatabase()` for the same database the existing one, which\n * keeps the timeout the first caller asked for.\n *\n * @default 30000\n */\n transactionQueueTimeout?: number;\n\n /**\n * Explicit schema definitions for tables\n *\n * When provided, these schemas will be used instead of auto-detection.\n * This allows frameworks to provide explicit schemas via dependency injection.\n *\n * @example\n * ```typescript\n * const db = await getDatabase({\n * type: 'json',\n * url: './data',\n * schemas: {\n * users: {\n * tableName: 'users',\n * ddl: 'CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT)',\n * indexes: ['CREATE INDEX idx_users_name ON users(name)']\n * }\n * }\n * });\n * ```\n */\n schemas?: SchemasOption;\n\n /**\n * Unique identifier for in-memory DuckDB instances to enable connection sharing\n * When multiple getDatabase() calls use the same dbid, they receive\n * the same database connection instance.\n *\n * Auto-generated if not provided, enabling connection sharing by default.\n * This solves the issue where multiple collections create separate in-memory\n * databases that can't see each other's tables.\n */\n dbid?: string;\n\n /**\n * Eagerly load all JSON files as tables at connection time\n *\n * When true (default), creates tables with inferred schemas for all JSON files\n * in the data directory, even those without explicit schemas or .schema.sql files.\n * This enables cross-table queries (JOINs, NOT EXISTS subqueries) before\n * syncSchema() is called.\n *\n * Note: Inferred schemas don't include UNIQUE constraints. UPSERT operations\n * require syncSchema() to be called first to establish proper constraints.\n * When syncSchema() is called, tables with inferred schemas are dropped and\n * recreated with the proper schema.\n *\n * @default true\n */\n eagerLoadTables?: boolean;\n\n /**\n * Force a fresh database connection, bypassing the connection cache\n *\n * When true, clears any cached connection for this URL before creating a new one.\n * Useful when you need to reload the database from disk after external changes.\n *\n * @default false\n *\n * @example\n * // Initial connection - cached\n * const db1 = await getDatabase({ type: 'json', url: './data' });\n *\n * // External process adds JSON files...\n *\n * // Force reload from disk\n * const db2 = await getDatabase({\n * type: 'json',\n * url: './data',\n * clearCache: true\n * });\n */\n clearCache?: boolean;\n\n /**\n * Called when a JSON data file parses but its records cannot be loaded into a\n * table that was created for it — from a provided schema, a `.schema.sql`\n * file, or deferred `syncSchema()` / `execute()` DDL — leaving that table\n * present but empty (renamed/dropped column, NOT NULL / PRIMARY KEY violation,\n * or a file whose fields match no column). Every other table still loads.\n *\n * This targets the *silent* failure: a present-but-empty table a query cannot\n * tell apart from a legitimately empty one. A file with no schema at all is\n * loaded by DuckDB's own inference; if that fails the table is simply not\n * created, which already surfaces as an error when you query it, so it is not\n * reported here.\n *\n * Fires for both connection-time and deferred (`syncSchema()` / `execute()`)\n * loads. Without it, such a failure is only visible as a `console.error` on\n * stderr. The same failures are also retrievable after the fact via the\n * adapter's `getTableLoadErrors()` method.\n *\n * Registered once, on the `getDatabase()` call that creates the connection.\n * The JSON adapter caches connections per URL, so a later `getDatabase()` for\n * the same directory returns the existing connection and does *not* re-wire a\n * different `onTableLoadError` — read `getTableLoadErrors()` on the returned\n * adapter instead, which reflects the shared connection's failures.\n *\n * A callback that throws is caught and ignored so it cannot break loading of\n * the remaining tables. See issue #1139.\n */\n onTableLoadError?: (info: JSONTableLoadError) => void;\n}\n\n/**\n * Result of a database operation that modifies data\n */\nexport interface QueryResult {\n /**\n * Type of operation performed (e.g., \"insert\", \"update\", \"delete\")\n */\n operation: string;\n\n /**\n * Number of rows affected by the operation\n */\n affected: number;\n}\n\n/**\n * Options for upsert operations.\n */\nexport interface UpsertOptions {\n /**\n * Preserve database-native NULL-distinct conflict behavior.\n *\n * By default, upsert treats NULL values in conflict columns as matching\n * existing NULL values. Set this to true to opt out and use the underlying\n * database's native ON CONFLICT behavior where NULLs are distinct.\n */\n nullsDistinct?: boolean;\n}\n\n/**\n * Schema definition that can be provided to database adapters\n *\n * Allows frameworks like SMRT to provide explicit schemas instead of\n * relying on auto-detection or circular dependencies.\n *\n * This is a simpler, DDL-based alternative to SchemaDefinition which is\n * used for structured JSON manifests.\n */\nexport interface SchemaProvider {\n /**\n * Table name in snake_case plural form\n */\n tableName: string;\n\n /**\n * DDL statement(s) to create the table\n * Can be a single CREATE TABLE statement or multiple statements\n */\n ddl: string;\n\n /**\n * Index creation statements (optional)\n * Each string should be a complete CREATE INDEX statement\n */\n indexes?: string[];\n\n /**\n * Trigger creation statements (optional, adapter-specific support)\n * Each string should be a complete CREATE TRIGGER statement\n */\n triggers?: string[];\n\n /**\n * Schema version for migration tracking (optional)\n */\n version?: string;\n\n /**\n * Field metadata (optional, for advanced use cases)\n * Can be used by frameworks for runtime type information and validation\n */\n fields?: Map<string, any>;\n}\n\n/**\n * Schema definitions for getDatabase(), either eagerly built or lazy.\n *\n * Callers can pass schemas as a lazy function so that building the schema\n * map (potentially 200+ objects from ObjectRegistry) is deferred until an\n * adapter actually needs it. Adapters that manage tables via migrations\n * (Postgres, SQLite) never call the function, making the cost zero.\n *\n * @example Lazy — only resolved by JSON/DuckDB adapters\n * ```typescript\n * await getDatabase({\n * type: 'postgres',\n * url: '...',\n * schemas: () => ObjectRegistry.getAllSchemas(), // never called\n * });\n * ```\n *\n * @example Eager — built upfront (fine for JSON/DuckDB)\n * ```typescript\n * await getDatabase({\n * type: 'json',\n * url: './data',\n * schemas: { users: { tableName: 'users', ddl: '...' } },\n * });\n * ```\n */\nexport type SchemasOption =\n | Record<string, SchemaProvider>\n | (() => Record<string, SchemaProvider>);\n\n/**\n * Resolves a SchemasOption to its concrete value.\n *\n * Adapters that need schemas (JSON, DuckDB) call this to unwrap the\n * lazy function. Adapters that don't (Postgres, SQLite) simply ignore\n * the `schemas` option without calling this.\n */\nexport function resolveSchemas(\n schemas: SchemasOption | undefined,\n): Record<string, SchemaProvider> | undefined {\n if (!schemas) return undefined;\n if (typeof schemas === 'function') return schemas();\n return schemas;\n}\n\n/**\n * Schema manifest structure for JSON-based schema management\n */\nexport interface SchemaManifest {\n version: string;\n timestamp: number;\n packageName: string;\n schemas: Record<string, SchemaDefinition>;\n dependencies: string[];\n}\n\n/**\n * Schema definition for individual tables\n */\nexport interface SchemaDefinition {\n tableName: string;\n columns: Record<string, ColumnDefinition>;\n indexes: IndexDefinition[];\n triggers: TriggerDefinition[];\n foreignKeys: ForeignKeyDefinition[];\n dependencies: string[];\n version: string;\n packageName: string;\n baseClass?: string;\n}\n\n/**\n * Column definition structure\n */\nexport interface ColumnDefinition {\n type: string;\n primaryKey?: boolean;\n unique?: boolean;\n notNull?: boolean;\n defaultValue?: any;\n check?: string;\n foreignKey?: {\n table: string;\n column: string;\n onDelete?: string;\n onUpdate?: string;\n };\n description?: string;\n}\n\n/**\n * Column definition with name for ALTER TABLE operations\n */\nexport interface ColumnDefinitionWithName extends ColumnDefinition {\n name: string;\n}\n\n/**\n * Index definition structure\n */\nexport interface IndexDefinition {\n name: string;\n columns: string[];\n unique?: boolean;\n where?: string;\n description?: string;\n}\n\n/**\n * Trigger definition structure\n */\nexport interface TriggerDefinition {\n name: string;\n when: string;\n event: string;\n table: string;\n condition?: string;\n body: string;\n description?: string;\n}\n\n/**\n * Foreign key constraint definition\n */\nexport interface ForeignKeyDefinition {\n column: string;\n referencesTable: string;\n referencesColumn: string;\n onDelete?: string;\n onUpdate?: string;\n}\n\n/**\n * Options for schema initialization and management\n */\nexport interface SchemaInitializationOptions {\n /** Schema manifest containing table definitions */\n manifest?: SchemaManifest;\n /** Raw SQL DDL schema string (legacy support) */\n schema?: string;\n /** Schema overrides for extending base schemas */\n overrides?: Record<string, SchemaDefinition>;\n /** Force recreation of tables */\n force?: boolean;\n /** Enable debug logging */\n debug?: boolean;\n}\n\n/**\n * Table schema information returned by getTableSchema()\n */\nexport interface TableSchemaInfo {\n /** Table name */\n tableName: string;\n /** Columns in the table */\n columns: Record<string, ColumnDefinition>;\n /** Indexes on the table */\n indexes: IndexDefinition[];\n /** Foreign key constraints */\n foreignKeys: ForeignKeyDefinition[];\n}\n\n/**\n * Options for vector similarity search\n */\nexport interface VectorSearchOptions {\n /** Maximum number of results to return */\n limit?: number;\n /** Distance metric for similarity comparison @default 'cosine' */\n metric?: 'cosine' | 'l2' | 'ip';\n /**\n * Additional SQL WHERE clause.\n * Use $2, $3, etc. for parameter placeholders ($1 is reserved for the query vector).\n * @example `category = $2`\n */\n where?: string;\n /** Parameters for the WHERE clause, bound to $2, $3, etc. */\n params?: any[];\n}\n\n/**\n * Result of a vector similarity search\n */\nexport interface VectorSearchResult {\n /** Row primary key */\n id: string;\n /**\n * Distance from query vector (lower = more similar).\n * Range varies by metric: cosine [0, 2], L2 [0, Infinity), ip (-Infinity, Infinity).\n */\n distance: number;\n /** Additional columns from the row */\n [key: string]: any;\n}\n\n/**\n * Options for creating a vector index\n */\nexport interface VectorIndexOptions {\n /** Number of dimensions (informational only, dimensions are set via ensureColumn) */\n dimensions?: number;\n /** Distance metric @default 'cosine' */\n metric?: 'cosine' | 'l2' | 'ip';\n /** Index type @default 'hnsw' */\n type?: 'hnsw' | 'ivfflat';\n}\n\n/**\n * Vector operations capability for database adapters\n *\n * Provides native vector similarity search, storage, and indexing.\n * Implemented by PostgreSQL via pgvector and optionally by SQLite via\n * sqlite-vector.\n */\nexport interface VectorCapabilities {\n /**\n * Search for similar vectors\n *\n * @param table - Table containing vector column\n * @param column - Name of the vector column\n * @param embedding - Query vector\n * @param options - Search options\n * @returns Ranked results by similarity\n */\n search(\n table: string,\n column: string,\n embedding: number[],\n options?: VectorSearchOptions,\n ): Promise<VectorSearchResult[]>;\n\n /**\n * Ensure a vector column exists on a table\n *\n * @param table - Table name\n * @param column - Column name\n * @param dimensions - Number of vector dimensions\n */\n ensureColumn(\n table: string,\n column: string,\n dimensions: number,\n ): Promise<void>;\n\n /**\n * Ensure a vector index exists on a column\n *\n * @param table - Table name\n * @param column - Column name\n * @param options - Index options\n */\n ensureIndex(\n table: string,\n column: string,\n options?: VectorIndexOptions,\n ): Promise<void>;\n\n /**\n * Store or update a vector in a specific row\n *\n * @param table - Table name\n * @param where - Conditions to identify the row\n * @param column - Vector column name\n * @param embedding - Vector data\n */\n upsertVector(\n table: string,\n where: Record<string, any>,\n column: string,\n embedding: number[],\n ): Promise<void>;\n}\n\n/**\n * SQLite notification delivered by optional notification capabilities.\n */\nexport interface DatabaseNotification {\n id: number;\n channel: string;\n payload: any;\n createdAt?: number | string | Date | null;\n}\n\n/**\n * Options for notification listeners.\n */\nexport interface NotificationListenOptions {\n /**\n * Fallback poll interval in milliseconds when the native watcher does not\n * receive an update signal.\n */\n fallbackPollMs?: number | null;\n}\n\n/**\n * Options for waiting on the next database update.\n */\nexport interface NotificationWaitOptions {\n /**\n * Maximum time to wait in milliseconds. When omitted, waits indefinitely.\n */\n timeoutMs?: number;\n\n /**\n * Optional abort signal to cancel the wait.\n */\n signal?: AbortSignal;\n}\n\n/**\n * Options for pruning persisted notification rows.\n */\nexport interface NotificationPruneOptions {\n /** Delete notifications older than this many seconds. */\n olderThanS?: number | null;\n /** Keep at most this many most-recent notifications. */\n maxKeep?: number | null;\n}\n\n/**\n * Optional notification/pub-sub capabilities for database adapters.\n */\nexport interface NotificationCapabilities {\n notify(channel: string, payload: any): Promise<number>;\n listen(\n channel: string,\n options?: NotificationListenOptions,\n ): AsyncIterable<DatabaseNotification>;\n waitForUpdate(options?: NotificationWaitOptions): Promise<boolean>;\n prune(options?: NotificationPruneOptions): Promise<number>;\n}\n\n/**\n * Options for SQLite native notification support.\n */\nexport interface SqliteNotificationCapabilityOptions {\n watcherBackend?: 'polling' | 'kernel' | 'shm';\n maxReaders?: number;\n}\n\n/**\n * Options for SQLite native vector support.\n */\nexport interface SqliteVectorCapabilityOptions {\n preload?: boolean;\n quantization?: 'turbo4' | 'turbo3' | 'turbo2' | 'uint8' | 'int8' | '1bit';\n maxMemory?: string;\n}\n\n/**\n * Optional SQLite-only native capabilities.\n */\nexport interface SqliteCapabilitiesOptions {\n notifications?: boolean | SqliteNotificationCapabilityOptions;\n vector?: boolean | SqliteVectorCapabilityOptions;\n}\n\n/**\n * Common interface for database adapters\n * Provides a unified API for different database backends\n */\nexport interface DatabaseInterface {\n /**\n * Database location identifier\n * For file-based databases: file path\n * For directory-based databases (JSON/DuckDB with JSON): directory path\n * For in-memory databases: ':memory:'\n * For remote databases: connection URL\n */\n url: string;\n\n /**\n * Underlying database client instance\n */\n client: any;\n\n /**\n * Inserts one or more records into a table\n *\n * @param table - Table name\n * @param data - Single record or array of records to insert\n * @returns Promise resolving to operation result\n */\n insert: (\n table: string,\n data: Record<string, any> | Record<string, any>[],\n ) => Promise<QueryResult>;\n\n /**\n * Retrieves a single record matching the where criteria\n *\n * @param table - Table name\n * @param where - Criteria to match records (object for AND-only, 2D array for OR/AND)\n * @returns Promise resolving to matching record or null if not found\n */\n get: (\n table: string,\n where: WhereClause,\n ) => Promise<Record<string, any> | null>;\n\n /**\n * Retrieves multiple records matching the where criteria\n *\n * @param table - Table name\n * @param where - Criteria to match records (object for AND-only, 2D array for OR/AND)\n * @returns Promise resolving to array of matching records\n */\n list: (table: string, where: WhereClause) => Promise<Record<string, any>[]>;\n\n /**\n * Updates records matching the where criteria\n *\n * @param table - Table name\n * @param where - Criteria to match records to update (object for AND-only, 2D array for OR/AND)\n * @param data - New data to set\n * @returns Promise resolving to operation result\n */\n update: (\n table: string,\n where: WhereClause,\n data: Record<string, any>,\n ) => Promise<QueryResult>;\n\n /**\n * Inserts a record or updates it if it already exists (UPSERT)\n * Uses database-specific ON CONFLICT / ON DUPLICATE KEY syntax\n *\n * @param table - Table name\n * @param conflictColumns - Columns that define the uniqueness constraint\n * @param data - Data to insert or update\n * @returns Promise resolving to operation result\n *\n * @example\n * ```typescript\n * // Upsert a user by email\n * await db.upsert('users', ['email'], {\n * email: 'user@example.com',\n * name: 'John Doe',\n * updated_at: new Date().toISOString()\n * });\n *\n * // Upsert with composite key\n * await db.upsert('settings', ['user_id', 'key'], {\n * user_id: '123',\n * key: 'theme',\n * value: 'dark'\n * });\n * ```\n */\n upsert: (\n table: string,\n conflictColumns: string[],\n data: Record<string, any>,\n options?: UpsertOptions,\n ) => Promise<QueryResult>;\n\n /**\n * Gets a record matching the where criteria or inserts it if not found\n *\n * @param table - Table name\n * @param where - Criteria to match existing record (object for AND-only, 2D array for OR/AND)\n * @param data - Data to insert if no record found\n * @returns Promise resolving to the record (either retrieved or newly inserted)\n */\n getOrInsert: (\n table: string,\n where: WhereClause,\n data: Record<string, any>,\n ) => Promise<Record<string, any>>;\n\n /**\n * Deletes records from a table matching the where criteria\n *\n * @param table - Table name\n * @param where - Criteria to match records for deletion (object for AND-only, 2D array for OR/AND)\n * @returns Promise resolving to operation result with count of deleted rows\n *\n * @example\n * ```typescript\n * // Delete by ID\n * await db.delete('products', { id: 'prod-123' });\n *\n * // Delete by criteria\n * await db.delete('orders', { status: 'cancelled' });\n *\n * // Delete with multiple conditions\n * await db.delete('sessions', {\n * user_id: 'user-123',\n * expired: true\n * });\n * ```\n */\n delete: (table: string, where: WhereClause) => Promise<QueryResult>;\n\n /**\n * Counts records in a table matching the where criteria\n *\n * @param table - Table name\n * @param where - Criteria to match records (optional, counts all if omitted; object for AND-only, 2D array for OR/AND)\n * @returns Promise resolving to count of matching records\n *\n * @example\n * ```typescript\n * // Count all records\n * const total = await db.count('products');\n *\n * // Count with single condition\n * const activeCount = await db.count('products', { active: true });\n *\n * // Count with multiple conditions\n * const filteredCount = await db.count('orders', {\n * status: 'pending',\n * priority: 'high'\n * });\n * ```\n */\n count: (table: string, where?: WhereClause) => Promise<number>;\n\n /**\n * Creates a table-specific interface for simplified table operations\n *\n * @param table - Table name\n * @returns TableInterface for the specified table\n */\n table: (table: string) => TableInterface;\n\n /**\n * Checks if a table exists in the database\n *\n * @param table - Table name\n * @returns Promise resolving to boolean indicating existence\n */\n tableExists: (table: string) => Promise<boolean>;\n\n /**\n * Whether this adapter requires schema existence checks at runtime.\n *\n * Set to `true` on adapters where tables may not exist yet and need\n * to be verified before use (e.g. JSON/DuckDB with auto-created tables).\n *\n * When absent or `false`, frameworks skip `tableExists()` calls during\n * collection initialization — tables are assumed to exist because\n * schema is managed by migrations (e.g. `smrt db:migrate`).\n *\n * - Postgres adapter: not set (migration-managed)\n * - SQLite adapter: not set (migration-managed)\n * - JSON/DuckDB adapter: `true` (tables may be auto-created)\n */\n requiresSchemaCheck?: boolean;\n\n /**\n * Executes a SQL query using template literals and returns multiple rows\n *\n * @param strings - Template strings\n * @param vars - Variables to interpolate into the query\n * @returns Promise resolving to array of result records\n */\n many: (\n strings: TemplateStringsArray,\n ...vars: any[]\n ) => Promise<Record<string, any>[]>;\n\n /**\n * Executes a SQL query using template literals and returns a single row\n *\n * @param strings - Template strings\n * @param vars - Variables to interpolate into the query\n * @returns Promise resolving to a single result record or null\n */\n single: (\n strings: TemplateStringsArray,\n ...vars: any[]\n ) => Promise<Record<string, any> | null>;\n\n /**\n * Executes a SQL query using template literals and returns a single value\n *\n * @param strings - Template strings\n * @param vars - Variables to interpolate into the query\n * @returns Promise resolving to a single value (first column of first row)\n */\n pluck: (strings: TemplateStringsArray, ...vars: any[]) => Promise<any>;\n\n /**\n * Executes a SQL query using template literals without returning results\n *\n * @param strings - Template strings\n * @param vars - Variables to interpolate into the query\n * @returns Promise that resolves when the query completes\n */\n execute: (strings: TemplateStringsArray, ...vars: any[]) => Promise<void>;\n\n /**\n * Alias for many() - Executes a SQL query and returns multiple rows\n */\n oo: (\n strings: TemplateStringsArray,\n ...vars: any[]\n ) => Promise<Record<string, any>[]>;\n\n /**\n * Alias for single() - Executes a SQL query and returns a single row\n */\n oO: (\n strings: TemplateStringsArray,\n ...vars: any[]\n ) => Promise<Record<string, any> | null>;\n\n /**\n * Alias for pluck() - Executes a SQL query and returns a single value\n */\n ox: (strings: TemplateStringsArray, ...vars: any[]) => Promise<any>;\n\n /**\n * Alias for execute() - Executes a SQL query without returning results\n */\n xx: (strings: TemplateStringsArray, ...vars: any[]) => Promise<void>;\n\n /**\n * Executes a raw SQL query with parameterized values\n *\n * Raw queries use the adapter's native placeholder syntax.\n * Parameters may be passed as rest arguments or as one values array.\n * For adapter-specific array parameter caveats, see the package docs.\n *\n * @param str - SQL query string\n * @param vars - Variables to use as parameters\n * @returns Promise resolving to query result with rows and count\n */\n query: (\n str: string,\n ...vars: any[]\n ) => Promise<{ rows: Record<string, any>[]; rowCount: number }>;\n\n /**\n * Synchronizes database schema with provided SQL DDL\n * Creates tables if they don't exist and adds missing columns\n *\n * @param schema - SQL schema definition with CREATE TABLE statements\n * @returns Promise that resolves when schema is synchronized\n */\n syncSchema?: (schema: string) => Promise<void>;\n\n /**\n * Initialize database schemas from JSON manifest\n * Supports dependency resolution and schema overrides\n *\n * @param options - Schema initialization options\n * @returns Promise that resolves when schemas are initialized\n */\n initializeSchemas?: (options: SchemaInitializationOptions) => Promise<void>;\n\n /**\n * Executes a callback within a database transaction\n * Automatically commits on success or rolls back on error\n *\n * Calling `transaction()` again on the `tx` handed to a callback is\n * adapter-specific — do not assume one behaviour across adapters:\n *\n * - **SQLite** (both the libsql and native-capabilities paths) — the nested\n * scope re-enters the transaction already in progress under a `SAVEPOINT`.\n * It reads the enclosing transaction's uncommitted rows, and if it throws,\n * only its own work is rolled back; the enclosing transaction stays usable.\n * - **DuckDB and JSON** — the engine has no `SAVEPOINT`, so nesting throws\n * {@link NestedTransactionError} without touching the connection. The\n * enclosing transaction is unaffected and can continue.\n * - **PostgreSQL** — the nested scope re-enters the enclosing transaction\n * under a `SAVEPOINT` on the same pooled connection, like SQLite. It used\n * to check out a second connection and begin an independent transaction,\n * which could not see the enclosing transaction's uncommitted rows and\n * could deadlock against it undetectably; that was fixed in #1108.\n *\n * Code that must run on every adapter should take the transaction as a\n * parameter rather than opening a nested one.\n *\n * **Transactions are serialized per connection on the single-connection\n * adapters** — SQLite (both paths), DuckDB and JSON drive one connection, and\n * a connection can only be in one transaction at a time, so a concurrent\n * `transaction()` waits for the one in progress rather than corrupting it. A\n * queued call that waits longer than `transactionQueueTimeout` (30s by\n * default) rejects rather than stalling indefinitely. Re-entrant calls do not\n * queue — they take the savepoint or refusal path described above.\n * PostgreSQL pools, so top-level transactions run concurrently. Nested scopes\n * on one PostgreSQL transaction use a scope-local queue: concurrently started\n * siblings serialize, while each child gets its own queue for further nesting.\n *\n * @param callback - Function to execute within transaction\n * @returns Promise resolving to callback result\n * @throws {NestedTransactionError} When nesting on an adapter without\n * savepoint support\n *\n * @example Portable across all adapters\n * ```typescript\n * async function recordClick(db: DatabaseInterface, id: string) {\n * await db.update('links', { id }, { clicks: 1 });\n * }\n *\n * await db.transaction(async (tx) => {\n * await tx.query('SELECT * FROM links WHERE id = $1 FOR UPDATE', id);\n * await recordClick(tx, id); // reuses tx; never opens its own\n * });\n * ```\n */\n transaction?: <T>(\n callback: (tx: DatabaseInterface) => Promise<T>,\n ) => Promise<T>;\n\n /**\n * Begins a new transaction and returns a handle for manual control\n *\n * Unlike transaction(), this gives you explicit control over commit/rollback.\n * Ideal for test isolation where you want to rollback after each test.\n *\n * On the single-connection adapters — SQLite (both paths), DuckDB and JSON —\n * the handle owns the connection until {@link TransactionHandle.commit} or\n * {@link TransactionHandle.rollback} is called, so every other transaction on\n * that connection waits in the meantime. **A handle that is never ended holds\n * the connection for the life of the process**, and later transactions then\n * fail with the queue-timeout error instead of proceeding. End the handle in\n * a `finally`, and in per-test setup guard the teardown, because an\n * assertion that throws between `beginTransaction()` and the teardown would\n * otherwise strand it.\n *\n * @returns Promise resolving to a TransactionHandle\n *\n * @example Test isolation pattern\n * ```typescript\n * let tx: TransactionHandle | undefined;\n *\n * beforeEach(async () => {\n * tx = await db.beginTransaction();\n * });\n *\n * afterEach(async () => {\n * // Guarded: if beforeEach threw, there is no handle to end, and an\n * // unguarded rollback would mask that failure with its own.\n * await tx?.rollback(); // Discard all test changes\n * tx = undefined;\n * });\n *\n * it('creates user', async () => {\n * await tx.insert('users', { id: '1', name: 'Test' });\n * const user = await tx.get('users', { id: '1' });\n * expect(user).toBeDefined();\n * // Changes rolled back in afterEach\n * });\n * ```\n */\n beginTransaction?: () => Promise<TransactionHandle>;\n\n /**\n * Acquire a pinned single-connection session.\n *\n * Returns a {@link SessionHandle} whose queries all run on one underlying\n * connection, enabling session-scoped state (e.g. PostgreSQL session advisory\n * locks) that the pooled top-level {@link query} cannot hold reliably. The\n * caller must {@link SessionHandle.release} it.\n *\n * Only meaningful for engines with session-scoped state, so it is currently\n * implemented for PostgreSQL only; check for the method before calling\n * (`if (db.acquireSession)`). Single-connection engines (SQLite, DuckDB) have\n * no session-scoped locks to hold, so they leave it undefined.\n *\n * @returns Promise resolving to a SessionHandle\n *\n * @example Hold a Postgres session advisory lock\n * ```typescript\n * const session = await db.acquireSession();\n * try {\n * await session.query('SELECT pg_advisory_lock($1)', 42);\n * // ... lock is held for the life of this session ...\n * } finally {\n * await session.release(); // frees the lock and drops the connection\n * }\n * ```\n */\n acquireSession?: () => Promise<SessionHandle>;\n\n /**\n * Retrieves the schema information for a table\n *\n * @param table - Table name\n * @returns Promise resolving to table schema info or null if table doesn't exist\n */\n getTableSchema?: (table: string) => Promise<TableSchemaInfo | null>;\n\n /**\n * Vector operations for native similarity search\n *\n * When available, enables database-level vector operations (e.g., pgvector).\n * Consumers should check for this capability rather than checking the database type.\n */\n vector?: VectorCapabilities;\n\n /**\n * Optional notification/pub-sub operations.\n */\n notifications?: NotificationCapabilities;\n\n /**\n * Close the database adapter and release native resources when supported.\n */\n close?: () => Promise<void>;\n\n /**\n * ALTER TABLE operations for schema evolution\n */\n alterTable?: {\n /**\n * Adds a new column to an existing table\n *\n * @param table - Table name\n * @param column - Column definition with name\n * @returns Promise that resolves when column is added\n */\n addColumn: (\n table: string,\n column: ColumnDefinitionWithName,\n ) => Promise<void>;\n\n /**\n * Adds a new index to an existing table\n *\n * @param table - Table name\n * @param index - Index definition\n * @returns Promise that resolves when index is created\n */\n addIndex: (table: string, index: IndexDefinition) => Promise<void>;\n };\n}\n\n/**\n * Transaction handle for manual transaction control\n *\n * Extends DatabaseInterface with commit() and rollback() methods.\n * Use beginTransaction() to obtain a handle, then explicitly\n * commit or rollback when done.\n *\n * @example Test isolation with rollback\n * ```typescript\n * const tx = await db.beginTransaction();\n * try {\n * // All operations happen in the transaction\n * await tx.insert('users', { id: '1', name: 'Test' });\n * const user = await tx.get('users', { id: '1' });\n *\n * // Rollback discards all changes (useful for test isolation)\n * await tx.rollback();\n * } catch (error) {\n * await tx.rollback();\n * throw error;\n * }\n * ```\n *\n * @example Manual commit\n * ```typescript\n * const tx = await db.beginTransaction();\n * try {\n * await tx.insert('orders', { id: '1', total: 100 });\n * await tx.insert('order_items', { order_id: '1', product: 'Widget' });\n * await tx.commit(); // Persist changes\n * } catch (error) {\n * await tx.rollback();\n * throw error;\n * }\n * ```\n */\nexport interface TransactionHandle extends DatabaseInterface {\n /**\n * Commits the transaction, persisting all changes\n *\n * After commit, the transaction handle should not be used.\n */\n commit: () => Promise<void>;\n\n /**\n * Rolls back the transaction, discarding all changes\n *\n * After rollback, the transaction handle should not be used.\n * This is the key method for test isolation - rollback after each test.\n */\n rollback: () => Promise<void>;\n\n /**\n * Whether the transaction is still active (not committed or rolled back)\n */\n isActive: () => boolean;\n}\n\n/**\n * Pinned single-connection session handle.\n *\n * Obtained via {@link DatabaseInterface.acquireSession}. Unlike the pooled\n * top-level `query` (which may run each statement on a different connection),\n * every call on a session handle runs on the *same* underlying connection for\n * the handle's lifetime. This is required for session-scoped state such as\n * PostgreSQL session advisory locks (`pg_advisory_lock`), which must be\n * acquired, observed, and released on one connection — and which release\n * automatically if the connection drops (e.g. the process dies).\n *\n * A session holds one connection out of the pool (default max 20) for its\n * entire lifetime, so it is meant to be held one-per-process, not per-operation.\n * Always {@link SessionHandle.release} the handle when done, and release it\n * *before* calling `db.client.end()` — a pinned connection keeps `pool.end()`\n * from resolving.\n */\nexport interface SessionHandle {\n /**\n * Execute a raw query on this session's pinned connection.\n * Same placeholder/return contract as {@link DatabaseInterface.query}.\n * Throws if the session has been released or its connection was lost.\n */\n query: (\n sql: string,\n ...vars: any[]\n ) => Promise<{ rows: Record<string, any>[]; rowCount: number }>;\n\n /**\n * Whether the session is still usable — not released and its connection has\n * not errored/dropped. A long-lived lock holder can poll this without\n * issuing a query.\n */\n isActive: () => boolean;\n\n /**\n * Release the session. Idempotent. After release the handle must not be used.\n * Best-effort frees session-scoped locks, then drops the underlying\n * connection (it is NOT returned to the pool) so the database releases any\n * remaining session state — returning it to the pool would leak that state\n * onto a later, unrelated checkout.\n */\n release: () => Promise<void>;\n}\n\n/**\n * Simplified interface for table-specific operations\n */\nexport interface TableInterface {\n /**\n * Inserts one or more records into the table\n *\n * @param data - Single record or array of records to insert\n * @returns Promise resolving to operation result\n */\n insert: (\n data: Record<string, any> | Record<string, any>[],\n ) => Promise<QueryResult>;\n\n /**\n * Retrieves a single record from the table matching the where criteria\n *\n * @param where - Criteria to match records (object for AND-only, 2D array for OR/AND)\n * @returns Promise resolving to matching record or null if not found\n */\n get: (where: WhereClause) => Promise<Record<string, any> | null>;\n\n /**\n * Retrieves multiple records from the table matching the where criteria\n *\n * @param where - Criteria to match records (object for AND-only, 2D array for OR/AND)\n * @returns Promise resolving to array of matching records\n */\n list: (where: WhereClause) => Promise<Record<string, any>[]>;\n}\n"],"mappings":";;;;;;;;;AASA,IAAa,uCAAuC;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDpD,SAAgB,sBACd,SACA,YAAoB,sCACH;CAKjB,IAAI,CAAC,OAAO,SAAS,SAAS,KAAK,aAAa,GAC9C,MAAM,IAAI,cACR,6EACA;EAAE;EAAS;CAAU,CACvB;CAGF,IAAI,OAAsB,QAAQ,QAAQ;CAE1C,MAAM,UAAU,YAAiC;EAC/C,MAAM,WAAW;EACjB,IAAI,gBAA4B,CAAC;EACjC,MAAM,UAAU,IAAI,SAAe,YAAY;GAC7C,UAAU;EACZ,CAAC;EAID,OAAO,SAAS,WACR,eACA,OACR;EAEA,IAAI;EACJ,IAAI;GACF,MAAM,IAAI,SAAe,SAAS,WAAW;IAC3C,QAAQ,iBAAiB;KACvB,OACE,IAAI,cACF,mBAAmB,UAAU,qBAAqB,QAAQ,sbAC1D;MAAE;MAAS;KAAU,CACvB,CACF;IACF,GAAG,SAAS;IACZ,SAAS,KAAK,SAAS,OAAO;GAChC,CAAC;EACH,SAAS,OAAO;GAGd,QAAQ;GACR,MAAM;EACR,UAAU;GACR,IAAI,OAAO,aAAa,KAAK;EAC/B;EAEA,IAAI,WAAW;EACf,aAAa;GACX,IAAI,UAAU;GACd,WAAW;GACX,QAAQ;EACV;CACF;CAEA,MAAM,MAAM,OAAU,SAAuC;EAC3D,MAAM,UAAU,MAAM,QAAQ;EAC9B,IAAI;GACF,OAAO,MAAM,KAAK;EACpB,UAAU;GACR,QAAQ;EACV;CACF;CAEA,OAAO;EAAE;EAAS;CAAI;AACxB;;;;;;;;;;;;;;;;;;;;;;;;;ACxGA,IAAa,yBAAb,cAA4C,cAAc;CACxD,YAAY,SAAiB,SAAmC;EAC9D,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;CACd;AACF;;;;;;;;AAiZA,SAAgB,eACd,SAC4C;CAC5C,IAAI,CAAC,SAAS,OAAO,KAAA;CACrB,IAAI,OAAO,YAAY,YAAY,OAAO,QAAQ;CAClD,OAAO;AACT"}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { n as resolveSchemas, r as createTransactionLock, t as NestedTransaction
|
|
|
5
5
|
import { DatabaseError, loadEnvConfig } from "@happyvertical/utils";
|
|
6
6
|
import { mkdir, readdir } from "node:fs/promises";
|
|
7
7
|
import { basename, extname, join } from "node:path";
|
|
8
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
9
|
import { Pool } from "pg";
|
|
9
10
|
//#region \0rolldown/runtime.js
|
|
10
11
|
var __defProp = Object.defineProperty;
|
|
@@ -2959,15 +2960,19 @@ async function createDatabase(options, cacheKey) {
|
|
|
2959
2960
|
* @param scopeFor - Builds the interface handed to the nested callback
|
|
2960
2961
|
*/
|
|
2961
2962
|
const createNestedTransaction = (txClient, scopeFor) => {
|
|
2962
|
-
|
|
2963
|
+
const nestedScope = new AsyncLocalStorage();
|
|
2964
|
+
const rootScope = { tail: Promise.resolve() };
|
|
2965
|
+
const runScope = async (scope, callback) => {
|
|
2963
2966
|
savepointSequence += 1;
|
|
2964
2967
|
const name = `hv_sp_${savepointSequence}`;
|
|
2965
2968
|
await txClient.query(`SAVEPOINT ${name}`);
|
|
2966
2969
|
try {
|
|
2967
2970
|
const result = await callback(scopeFor());
|
|
2971
|
+
await scope.tail;
|
|
2968
2972
|
await txClient.query(`RELEASE SAVEPOINT ${name}`);
|
|
2969
2973
|
return result;
|
|
2970
2974
|
} catch (error) {
|
|
2975
|
+
await scope.tail;
|
|
2971
2976
|
try {
|
|
2972
2977
|
await txClient.query(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
2973
2978
|
await txClient.query(`RELEASE SAVEPOINT ${name}`);
|
|
@@ -2975,6 +2980,15 @@ async function createDatabase(options, cacheKey) {
|
|
|
2975
2980
|
throw error;
|
|
2976
2981
|
}
|
|
2977
2982
|
};
|
|
2983
|
+
const transaction = async (callback) => {
|
|
2984
|
+
const parentScope = nestedScope.getStore() ?? rootScope;
|
|
2985
|
+
const scope = { tail: Promise.resolve() };
|
|
2986
|
+
const current = parentScope.tail.then(() => nestedScope.run(scope, () => runScope(scope, callback)));
|
|
2987
|
+
parentScope.tail = current.then(() => void 0, () => void 0);
|
|
2988
|
+
return current;
|
|
2989
|
+
};
|
|
2990
|
+
transaction.drain = () => rootScope.tail;
|
|
2991
|
+
return transaction;
|
|
2978
2992
|
};
|
|
2979
2993
|
/**
|
|
2980
2994
|
* Executes a callback within a database transaction
|
|
@@ -2985,19 +2999,24 @@ async function createDatabase(options, cacheKey) {
|
|
|
2985
2999
|
*/
|
|
2986
3000
|
const transaction = async (callback) => {
|
|
2987
3001
|
const txClient = await acquireTxClient();
|
|
3002
|
+
let nestedTransaction;
|
|
2988
3003
|
try {
|
|
2989
3004
|
await txClient.query("BEGIN");
|
|
2990
|
-
|
|
3005
|
+
let txDb;
|
|
3006
|
+
nestedTransaction = createNestedTransaction(txClient, () => txDb);
|
|
3007
|
+
txDb = {
|
|
2991
3008
|
url,
|
|
2992
3009
|
client: txClient,
|
|
2993
3010
|
...createClientMethods(txClient, true, passThroughRecord),
|
|
2994
|
-
transaction:
|
|
3011
|
+
transaction: nestedTransaction
|
|
2995
3012
|
};
|
|
2996
3013
|
const result = await callback(txDb);
|
|
3014
|
+
await nestedTransaction.drain();
|
|
2997
3015
|
await txClient.query("COMMIT");
|
|
2998
3016
|
releaseTxClient(txClient);
|
|
2999
3017
|
return result;
|
|
3000
3018
|
} catch (error) {
|
|
3019
|
+
await nestedTransaction?.drain();
|
|
3001
3020
|
const rollbackError = await discardTxClient(txClient);
|
|
3002
3021
|
if (rollbackError !== void 0 && error instanceof Error) error.cause ??= rollbackError;
|
|
3003
3022
|
throw error;
|
|
@@ -3020,9 +3039,11 @@ async function createDatabase(options, cacheKey) {
|
|
|
3020
3039
|
throw error;
|
|
3021
3040
|
}
|
|
3022
3041
|
let active = true;
|
|
3042
|
+
let nestedTransaction;
|
|
3023
3043
|
const end = async (command) => {
|
|
3024
3044
|
if (!active) throw new DatabaseError("Transaction already ended", {});
|
|
3025
3045
|
try {
|
|
3046
|
+
await nestedTransaction?.drain();
|
|
3026
3047
|
await txClient.query(command);
|
|
3027
3048
|
active = false;
|
|
3028
3049
|
releaseTxClient(txClient);
|
|
@@ -3036,11 +3057,13 @@ async function createDatabase(options, cacheKey) {
|
|
|
3036
3057
|
const commit = () => end("COMMIT");
|
|
3037
3058
|
const rollback = () => end("ROLLBACK");
|
|
3038
3059
|
const isActive = () => active;
|
|
3039
|
-
|
|
3060
|
+
let txHandle;
|
|
3061
|
+
nestedTransaction = createNestedTransaction(txClient, () => txHandle);
|
|
3062
|
+
txHandle = {
|
|
3040
3063
|
url,
|
|
3041
3064
|
client: txClient,
|
|
3042
3065
|
...createClientMethods(txClient, true, passThroughRecord),
|
|
3043
|
-
transaction:
|
|
3066
|
+
transaction: nestedTransaction,
|
|
3044
3067
|
commit,
|
|
3045
3068
|
rollback,
|
|
3046
3069
|
isActive
|