@happyvertical/sql 0.86.1 → 0.86.3

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.
Files changed (43) hide show
  1. package/README.md +41 -0
  2. package/dist/chunks/{alter-utils-DeDDAATm.js → connection-cache-6EMfKWpo.js} +196 -2
  3. package/dist/chunks/connection-cache-6EMfKWpo.js.map +1 -0
  4. package/dist/chunks/{duckdb-jSSa2MbC.js → duckdb-D2Nah7NF.js} +22 -10
  5. package/dist/chunks/duckdb-D2Nah7NF.js.map +1 -0
  6. package/dist/chunks/{duckdb-schema-utils-B-XBP8QF.js → duckdb-resources-Cci6Lo6H.js} +43 -2
  7. package/dist/chunks/duckdb-resources-Cci6Lo6H.js.map +1 -0
  8. package/dist/chunks/redact-database-url-CfMDvSdy.js +33 -0
  9. package/dist/chunks/redact-database-url-CfMDvSdy.js.map +1 -0
  10. package/dist/chunks/{sqlite-Crx5PQ3G.js → sqlite-B2bnGN2x.js} +106 -85
  11. package/dist/chunks/sqlite-B2bnGN2x.js.map +1 -0
  12. package/dist/chunks/sqlite-connection-cache-0iFbrrI3.js +65 -0
  13. package/dist/chunks/sqlite-connection-cache-0iFbrrI3.js.map +1 -0
  14. package/dist/chunks/{sqlite-native-DCAaADpP.js → sqlite-native-Dc8eFStu.js} +44 -31
  15. package/dist/chunks/sqlite-native-Dc8eFStu.js.map +1 -0
  16. package/dist/chunks/types-uVFHgaSU.js.map +1 -1
  17. package/dist/duckdb.d.ts.map +1 -1
  18. package/dist/index.js +119 -118
  19. package/dist/index.js.map +1 -1
  20. package/dist/json.d.ts +5 -5
  21. package/dist/json.d.ts.map +1 -1
  22. package/dist/postgres.d.ts +18 -5
  23. package/dist/postgres.d.ts.map +1 -1
  24. package/dist/shared/connection-cache.d.ts +13 -0
  25. package/dist/shared/connection-cache.d.ts.map +1 -0
  26. package/dist/shared/duckdb-resources.d.ts +8 -0
  27. package/dist/shared/duckdb-resources.d.ts.map +1 -0
  28. package/dist/shared/redact-database-url.d.ts +3 -0
  29. package/dist/shared/redact-database-url.d.ts.map +1 -0
  30. package/dist/shared/sqlite-connection-cache.d.ts +4 -0
  31. package/dist/shared/sqlite-connection-cache.d.ts.map +1 -0
  32. package/dist/shared/types.d.ts +37 -4
  33. package/dist/shared/types.d.ts.map +1 -1
  34. package/dist/sqlite-native.d.ts +8 -0
  35. package/dist/sqlite-native.d.ts.map +1 -1
  36. package/dist/sqlite.d.ts +4 -4
  37. package/dist/sqlite.d.ts.map +1 -1
  38. package/package.json +2 -2
  39. package/dist/chunks/alter-utils-DeDDAATm.js.map +0 -1
  40. package/dist/chunks/duckdb-jSSa2MbC.js.map +0 -1
  41. package/dist/chunks/duckdb-schema-utils-B-XBP8QF.js.map +0 -1
  42. package/dist/chunks/sqlite-Crx5PQ3G.js.map +0 -1
  43. package/dist/chunks/sqlite-native-DCAaADpP.js.map +0 -1
@@ -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 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"}
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 DatabaseCacheOptions {\n /**\n * Stable, caller-supplied cache identity. When omitted, adapters derive an\n * identity from their effective connection configuration.\n */\n dbid?: string;\n\n /**\n * Whether this request may reuse and retain a shared adapter.\n * Set to `false` for a distinct connection that is never inserted into the\n * shared cache. Existing cached connections are left untouched.\n *\n * @default true for adapters that support caching\n */\n cache?: boolean;\n\n /**\n * Evict and close the matching cached adapter before serving this request.\n * Eviction waits for both resource closure and any in-flight initializer.\n * The replacement is cached unless `cache` is also `false`.\n *\n * @default false\n */\n clearCache?: boolean;\n}\n\nexport interface DatabaseOptions extends DatabaseCacheOptions {\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 * Accepted for uniform configuration. DuckDB creates a fresh adapter on\n * every call, so this option does not change its behavior.\n */\n cache?: boolean;\n\n /**\n * Accepted for uniform configuration. DuckDB has no shared cache to evict,\n * so this option does not change its behavior.\n */\n clearCache?: 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 extends DatabaseOptions {\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 * Evict and close the existing database connection before creating a fresh one\n *\n * The new adapter is cached unless `cache` is also false. Eviction is awaited.\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;;;;;;;;AAubA,SAAgB,eACd,SAC4C;CAC5C,IAAI,CAAC,SAAS,OAAO,KAAA;CACrB,IAAI,OAAO,YAAY,YAAY,OAAO,QAAQ;CAClD,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"duckdb.d.ts","sourceRoot":"","sources":["../src/duckdb.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAGV,iBAAiB,EACjB,aAAa,EAQd,MAAM,gBAAgB,CAAC;AA6PxB;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,iBAAiB,CAAC,CAuyC5B"}
1
+ {"version":3,"file":"duckdb.d.ts","sourceRoot":"","sources":["../src/duckdb.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAGV,iBAAiB,EACjB,aAAa,EAQd,MAAM,gBAAgB,CAAC;AAqQxB;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,iBAAiB,CAAC,CA4yC5B"}
package/dist/index.js CHANGED
@@ -1,11 +1,13 @@
1
- import { a as validateColumnName$1, c as validateTableName$1, d as formatDbError, f as parseConditionKey, h as unwrapRawKey, i as generateCreateIndexStatement, l as DatabaseSchemaManager, m as resolveInsertColumns, n as escapeStringLiteral, o as validateColumnNames, p as raw, r as generateAddColumnStatement, s as validateIndexName, t as escapeQuotedIdentifier, u as buildWhere } from "./chunks/alter-utils-DeDDAATm.js";
2
- import { t as convertUniqueIndexesToInlineConstraints } from "./chunks/duckdb-schema-utils-B-XBP8QF.js";
1
+ import { _ as unwrapRawKey, a as generateAddColumnStatement, c as validateColumnNames, d as DatabaseSchemaManager, f as buildWhere, g as resolveInsertColumns, h as raw, i as escapeStringLiteral, l as validateIndexName, m as parseConditionKey, n as validateDatabaseCacheOptions, o as generateCreateIndexStatement, p as formatDbError, r as escapeQuotedIdentifier, s as validateColumnName$1, t as ConnectionCache, u as validateTableName$1 } from "./chunks/connection-cache-6EMfKWpo.js";
2
+ import { n as throwWithDuckDBCleanup, r as convertUniqueIndexesToInlineConstraints, t as createDuckDBResourceCloser } from "./chunks/duckdb-resources-Cci6Lo6H.js";
3
3
  import { a as dropPostgresDatabase, c as postgresEnvFromUrl, i as databaseNameFromUrl, l as redactDatabaseUrl, n as assertCanImportDatabase, o as dumpPostgresDatabase, r as createPostgresDatabase, s as isLocalDatabaseUrl, t as assertCanExportDatabase, u as restorePostgresDatabase } from "./chunks/postgres-cli-DzZcXSIG.js";
4
+ import { t as redactDatabaseUrl$1 } from "./chunks/redact-database-url-CfMDvSdy.js";
4
5
  import { n as resolveSchemas, r as createTransactionLock, t as NestedTransactionError } from "./chunks/types-uVFHgaSU.js";
5
6
  import { DatabaseError, loadEnvConfig } from "@happyvertical/utils";
6
7
  import { mkdir, readdir } from "node:fs/promises";
7
8
  import { basename, extname, join } from "node:path";
8
9
  import { AsyncLocalStorage } from "node:async_hooks";
10
+ import { webcrypto } from "node:crypto";
9
11
  import { Pool } from "pg";
10
12
  //#region \0rolldown/runtime.js
11
13
  var __defProp = Object.defineProperty;
@@ -312,25 +314,17 @@ var json_exports = /* @__PURE__ */ __exportAll({
312
314
  *
313
315
  * @see https://github.com/happyvertical/sdk/issues/678
314
316
  */
315
- globalThis.__haveSqlMemoryConnectionCache ??= /* @__PURE__ */ new Map();
316
- var memoryConnectionCache = globalThis.__haveSqlMemoryConnectionCache;
317
+ globalThis.__haveSqlJSONConnectionCache ??= new ConnectionCache();
318
+ var memoryConnectionCache = globalThis.__haveSqlJSONConnectionCache;
317
319
  /**
318
- * Pending connection promises to handle concurrent getDatabase() calls
319
- * Prevents creating duplicate connections when parallel calls happen.
320
- *
321
- * Uses globalThis to ensure cache is shared across all module instances.
322
- *
323
- * @see https://github.com/happyvertical/sdk/issues/678
320
+ * Clears and closes all cached connections.
321
+ * Callers must await completion before opening a replacement connection.
324
322
  */
325
- globalThis.__haveSqlPendingConnections ??= /* @__PURE__ */ new Map();
326
- var pendingConnections$1 = globalThis.__haveSqlPendingConnections;
327
- /**
328
- * Clears all cached connections
329
- * Useful for testing to ensure test isolation
330
- */
331
- function clearConnectionCache() {
332
- memoryConnectionCache.clear();
333
- pendingConnections$1.clear();
323
+ async function clearConnectionCache() {
324
+ await memoryConnectionCache.clear(closeJSONDatabase);
325
+ }
326
+ async function closeJSONDatabase(db) {
327
+ await db.close?.();
334
328
  }
335
329
  /**
336
330
  * Creates a JSON database connection using DuckDB in-memory engine
@@ -357,17 +351,21 @@ async function createJSONConnection(options) {
357
351
  console.error("[json-adapter] onTableLoadError callback threw:", callbackError);
358
352
  }
359
353
  };
354
+ let instance;
355
+ let connection;
360
356
  try {
361
357
  const { DuckDBInstance } = await import(
362
358
  /* @vite-ignore */
363
359
  "@duckdb/node-api"
364
360
  );
365
- const connection = await (await DuckDBInstance.create(":memory:")).connect();
361
+ instance = await DuckDBInstance.create(":memory:");
362
+ connection = await instance.connect();
366
363
  try {
367
364
  await mkdir(url, { recursive: true });
368
365
  } catch (_error) {}
369
366
  if (autoRegister) await loadJSONTables(connection, url, schemas, pendingJSONFiles, eagerLoadTables, inferredSchemaTables, reportLoadError);
370
367
  return {
368
+ instance,
371
369
  connection,
372
370
  pendingJSONFiles,
373
371
  inferredSchemaTables,
@@ -375,11 +373,11 @@ async function createJSONConnection(options) {
375
373
  reportLoadError
376
374
  };
377
375
  } catch (error) {
378
- const errorMessage = error instanceof Error ? error.message : String(error);
379
- throw new DatabaseError(`Failed to create JSON database connection: ${errorMessage}`, {
380
- url,
376
+ const errorMessage = redactDatabaseUrl$1(error instanceof Error ? error.message : String(error));
377
+ return throwWithDuckDBCleanup(new DatabaseError(`Failed to create JSON database connection: ${errorMessage}`, {
378
+ url: redactDatabaseUrl$1(url),
381
379
  originalError: errorMessage
382
- });
380
+ }), createDuckDBResourceCloser(connection, instance));
383
381
  }
384
382
  }
385
383
  /**
@@ -748,16 +746,11 @@ function validateTableName(table) {
748
746
  */
749
747
  async function getDatabase$2(options) {
750
748
  const cacheKey = options.dbid || `json:${options.url}:${options.writeStrategy || "immediate"}:${options.autoRegister !== false}`;
751
- if (options.clearCache) {
752
- memoryConnectionCache.delete(cacheKey);
753
- pendingConnections$1.delete(cacheKey);
754
- }
755
- const cached = memoryConnectionCache.get(cacheKey);
756
- if (cached) return cached;
757
- const pending = pendingConnections$1.get(cacheKey);
758
- if (pending) return pending;
759
- const connectionPromise = (async () => {
760
- const { connection, pendingJSONFiles, inferredSchemaTables, tableLoadErrors, reportLoadError } = await createJSONConnection(options);
749
+ return memoryConnectionCache.getOrCreate(cacheKey, options, () => createJSONDatabase(options), closeJSONDatabase);
750
+ }
751
+ async function createJSONDatabase(options) {
752
+ return (async () => {
753
+ const { instance, connection, pendingJSONFiles, inferredSchemaTables, tableLoadErrors, reportLoadError } = await createJSONConnection(options);
761
754
  const writeStrategy = options.writeStrategy || "immediate";
762
755
  const { url } = options;
763
756
  const connectionLock = createTransactionLock("json", options.transactionQueueTimeout);
@@ -1680,7 +1673,8 @@ async function getDatabase$2(options) {
1680
1673
  isActive
1681
1674
  };
1682
1675
  };
1683
- return {
1676
+ const closeResources = createDuckDBResourceCloser(connection, instance);
1677
+ const db = {
1684
1678
  url,
1685
1679
  requiresSchemaCheck: true,
1686
1680
  client: connection,
@@ -1709,17 +1703,14 @@ async function getDatabase$2(options) {
1709
1703
  beginTransaction,
1710
1704
  exportTable,
1711
1705
  inferSchemaFromJSON,
1712
- getTableLoadErrors
1706
+ getTableLoadErrors,
1707
+ close: async () => {
1708
+ memoryConnectionCache.forget(db);
1709
+ await closeResources();
1710
+ }
1713
1711
  };
1714
- })();
1715
- pendingConnections$1.set(cacheKey, connectionPromise);
1716
- try {
1717
- const db = await connectionPromise;
1718
- memoryConnectionCache.set(cacheKey, db);
1719
1712
  return db;
1720
- } finally {
1721
- pendingConnections$1.delete(cacheKey);
1722
- }
1713
+ })();
1723
1714
  }
1724
1715
  //#endregion
1725
1716
  //#region src/postgres.ts
@@ -1727,20 +1718,69 @@ var postgres_exports = /* @__PURE__ */ __exportAll({
1727
1718
  clearPostgresConnectionCache: () => clearPostgresConnectionCache,
1728
1719
  getDatabase: () => getDatabase$1
1729
1720
  });
1730
- /**
1731
- * Module-level cache for PostgreSQL connections.
1732
- * Keyed by dbid (or derived from connection URL).
1733
- * Prevents creating multiple pg.Pool instances for the same database.
1734
- */
1735
- var connectionCache = /* @__PURE__ */ new Map();
1736
- var pendingConnections = /* @__PURE__ */ new Map();
1721
+ globalThis.__haveSqlPostgresCacheHmacKey ??= webcrypto.subtle.generateKey({
1722
+ name: "HMAC",
1723
+ hash: "SHA-256"
1724
+ }, false, ["sign"]);
1725
+ globalThis.__haveSqlPostgresConnectionCache ??= new ConnectionCache();
1726
+ var connectionCache = globalThis.__haveSqlPostgresConnectionCache;
1737
1727
  /**
1738
1728
  * Clears the PostgreSQL connection cache.
1739
1729
  * Useful for test isolation and reconnection scenarios.
1740
1730
  */
1741
- function clearPostgresConnectionCache() {
1742
- connectionCache.clear();
1743
- pendingConnections.clear();
1731
+ async function clearPostgresConnectionCache() {
1732
+ await connectionCache.clear(closePostgresDatabase);
1733
+ }
1734
+ async function closePostgresDatabase(db) {
1735
+ if (db.close) {
1736
+ await db.close();
1737
+ return;
1738
+ }
1739
+ await db.client.end();
1740
+ }
1741
+ function resolvePostgresConfig(options) {
1742
+ const preferred = (option, environment) => option !== void 0 ? option : environment;
1743
+ const envPort = process.env.HAVE_SQL_PORT;
1744
+ const primary = {
1745
+ url: preferred(options.url, process.env.HAVE_SQL_URL),
1746
+ database: preferred(options.database, process.env.HAVE_SQL_DATABASE),
1747
+ host: preferred(options.host, process.env.HAVE_SQL_HOST),
1748
+ user: preferred(options.user, process.env.HAVE_SQL_USER),
1749
+ password: preferred(options.password, process.env.HAVE_SQL_PASSWORD),
1750
+ port: preferred(options.port, envPort === void 0 ? void 0 : Number(envPort))
1751
+ };
1752
+ const useLegacy = !primary.url && !primary.host && !primary.database && !primary.user;
1753
+ const legacy = (value, environment) => value !== void 0 || !useLegacy ? value : environment;
1754
+ const legacyPort = process.env.SQLOO_PORT;
1755
+ const port = legacy(primary.port, legacyPort === void 0 ? void 0 : Number(legacyPort)) ?? 5432;
1756
+ const max = options.max ?? 20;
1757
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("PostgreSQL port must be an integer between 1 and 65535");
1758
+ if (!Number.isInteger(max) || max < 1) throw new Error("PostgreSQL pool max must be a positive integer");
1759
+ return {
1760
+ url: legacy(primary.url, process.env.SQLOO_URL),
1761
+ database: legacy(primary.database, process.env.SQLOO_DATABASE),
1762
+ host: legacy(primary.host, process.env.SQLOO_HOST),
1763
+ user: legacy(primary.user, process.env.SQLOO_USER),
1764
+ password: legacy(primary.password, process.env.SQLOO_PASSWORD),
1765
+ port,
1766
+ max
1767
+ };
1768
+ }
1769
+ async function derivePostgresConnectionCacheKey(options, effective) {
1770
+ if (options.dbid) return options.dbid;
1771
+ const identity = JSON.stringify({
1772
+ url: effective.url ?? null,
1773
+ host: effective.host ?? "localhost",
1774
+ port: effective.port,
1775
+ database: effective.database ?? null,
1776
+ user: effective.user ?? null,
1777
+ password: effective.password ?? null,
1778
+ max: effective.max
1779
+ });
1780
+ const hmacKey = await globalThis.__haveSqlPostgresCacheHmacKey;
1781
+ if (!hmacKey) throw new Error("PostgreSQL cache identity key was not initialized");
1782
+ const signature = await webcrypto.subtle.sign("HMAC", hmacKey, new TextEncoder().encode(`@happyvertical/sql:postgres-cache:v1\0${identity}`));
1783
+ return `pg:hmac-sha256:v1:${Buffer.from(signature).toString("hex")}`;
1744
1784
  }
1745
1785
  /**
1746
1786
  * Operator for the given distance metric in pgvector
@@ -2065,64 +2105,18 @@ function createVectorCapabilities(pool) {
2065
2105
  * @returns Database interface for PostgreSQL
2066
2106
  */
2067
2107
  async function getDatabase$1(options = {}) {
2068
- const envUrl = process.env.HAVE_SQL_URL || process.env.SQLOO_URL || null;
2069
- const envDatabase = process.env.HAVE_SQL_DATABASE || process.env.SQLOO_DATABASE || null;
2070
- const envHost = process.env.HAVE_SQL_HOST || process.env.SQLOO_HOST || "localhost";
2071
- const envUser = process.env.HAVE_SQL_USER || process.env.SQLOO_USER || "";
2072
- const envPortRaw = process.env.HAVE_SQL_PORT || process.env.SQLOO_PORT;
2073
- const envPort = envPortRaw ? Number(envPortRaw) || 5432 : 5432;
2074
- const derivedHost = options.host || envHost;
2075
- const derivedPort = options.port || envPort;
2076
- const derivedDatabase = options.database || envDatabase;
2077
- const derivedUser = options.user || envUser;
2078
- const cacheKey = options.dbid || options.url || (derivedDatabase ? `pg:${derivedUser ? `${derivedUser}@` : ""}${derivedHost}:${derivedPort}/${derivedDatabase}` : envUrl || null);
2079
- if (cacheKey) {
2080
- const cached = connectionCache.get(cacheKey);
2081
- if (cached) return cached;
2082
- const pending = pendingConnections.get(cacheKey);
2083
- if (pending) return pending;
2084
- }
2085
- const connectionPromise = createDatabase(options, cacheKey);
2086
- if (cacheKey) {
2087
- pendingConnections.set(cacheKey, connectionPromise);
2088
- connectionPromise.finally(() => {
2089
- if (cacheKey) pendingConnections.delete(cacheKey);
2090
- });
2091
- }
2092
- return connectionPromise;
2108
+ validateDatabaseCacheOptions(options);
2109
+ const effective = resolvePostgresConfig(options);
2110
+ const cacheKey = await derivePostgresConnectionCacheKey(options, effective);
2111
+ return connectionCache.getOrCreate(cacheKey, options, () => createDatabase(effective), closePostgresDatabase);
2093
2112
  }
2094
2113
  /**
2095
2114
  * Internal: creates the actual database connection and caches it.
2096
2115
  */
2097
- async function createDatabase(options, cacheKey) {
2098
- const config = loadEnvConfig(options, {
2099
- packageName: "sql",
2100
- schema: {
2101
- url: "string",
2102
- database: "string",
2103
- host: "string",
2104
- port: "number",
2105
- user: "string",
2106
- password: "string"
2107
- }
2108
- });
2109
- if (!config.url && !config.host && !config.database && !config.user) {
2110
- const legacyConfig = loadEnvConfig({}, {
2111
- prefix: "SQLOO",
2112
- schema: {
2113
- url: "string",
2114
- database: "string",
2115
- host: "string",
2116
- port: "number",
2117
- user: "string",
2118
- password: "string"
2119
- }
2120
- });
2121
- for (const [key, value] of Object.entries(legacyConfig)) if (config[key] === void 0 && value !== void 0) config[key] = value;
2122
- }
2116
+ async function createDatabase(config) {
2123
2117
  const { database, host = "localhost", user, password, port = 5432 } = config;
2124
2118
  const url = config.url || `postgresql://${user}${password ? `:${password}` : ""}@${host}:${port}/${database || "postgres"}`;
2125
- const poolMax = options.max ?? 20;
2119
+ const poolMax = config.max;
2126
2120
  const pool = new Pool(config.url ? {
2127
2121
  connectionString: config.url,
2128
2122
  max: poolMax
@@ -2138,13 +2132,19 @@ async function createDatabase(options, cacheKey) {
2138
2132
  console.warn("Warning: PostgreSQL pool client error (connection discarded):", error instanceof Error ? error.message : String(error));
2139
2133
  });
2140
2134
  const originalEnd = pool.end.bind(pool);
2141
- const evictFromCache = () => {
2142
- for (const [key, cached] of connectionCache.entries()) if (cached.client === pool) connectionCache.delete(key);
2143
- };
2135
+ let db;
2136
+ let endPromise;
2137
+ const evictFromCache = () => connectionCache.forget(db);
2144
2138
  pool.end = (callback) => {
2145
- evictFromCache();
2146
- if (callback) return originalEnd(callback);
2147
- return originalEnd();
2139
+ if (!endPromise) {
2140
+ evictFromCache();
2141
+ endPromise = originalEnd();
2142
+ }
2143
+ if (callback) {
2144
+ endPromise.then(() => callback(), (error) => callback(error instanceof Error ? error : new Error(String(error))));
2145
+ return;
2146
+ }
2147
+ return endPromise;
2148
2148
  };
2149
2149
  const client = pool;
2150
2150
  /**
@@ -3213,7 +3213,7 @@ async function createDatabase(options, cacheKey) {
3213
3213
  });
3214
3214
  }
3215
3215
  };
3216
- const db = {
3216
+ db = {
3217
3217
  url,
3218
3218
  client,
3219
3219
  insert,
@@ -3288,10 +3288,11 @@ async function createDatabase(options, cacheKey) {
3288
3288
  }
3289
3289
  }
3290
3290
  },
3291
- vector: createVectorCapabilities(pool)
3291
+ vector: createVectorCapabilities(pool),
3292
+ close: async () => {
3293
+ await pool.end();
3294
+ }
3292
3295
  };
3293
- const finalCacheKey = cacheKey || url;
3294
- connectionCache.set(finalCacheKey, db);
3295
3296
  return db;
3296
3297
  }
3297
3298
  //#endregion
@@ -3355,8 +3356,8 @@ async function getDatabase(options = {}) {
3355
3356
  }));
3356
3357
  if (!options.type && (options.url?.startsWith("file:") || options.url === ":memory:")) options.type = "sqlite";
3357
3358
  if (options.type === "postgres") return (await Promise.resolve().then(() => postgres_exports)).getDatabase(options);
3358
- if (options.type === "sqlite") return (await import("./chunks/sqlite-Crx5PQ3G.js")).getDatabase(options);
3359
- if (options.type === "duckdb") return (await import("./chunks/duckdb-jSSa2MbC.js")).getDatabase(options);
3359
+ if (options.type === "sqlite") return (await import("./chunks/sqlite-B2bnGN2x.js")).getDatabase(options);
3360
+ if (options.type === "duckdb") return (await import("./chunks/duckdb-D2Nah7NF.js")).getDatabase(options);
3360
3361
  if (options.type === "json") return (await Promise.resolve().then(() => json_exports)).getDatabase(options);
3361
3362
  throw new Error("Invalid database type");
3362
3363
  }