@happyvertical/sql 0.105.11 → 0.106.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -0
- package/dist/aggregate-expression.d.ts +100 -0
- package/dist/aggregate-expression.d.ts.map +1 -0
- package/dist/aggregate.d.ts +23 -0
- package/dist/aggregate.d.ts.map +1 -1
- package/dist/chunks/{duckdb-D4x661Ux.js → duckdb-EvsYk7GK.js} +2 -2
- package/dist/chunks/{duckdb-D4x661Ux.js.map → duckdb-EvsYk7GK.js.map} +1 -1
- package/dist/chunks/{query-DubG7aF0.js → query-B6ATOWzc.js} +246 -39
- package/dist/chunks/{query-DubG7aF0.js.map → query-B6ATOWzc.js.map} +1 -1
- package/dist/chunks/{sqlite-DZkc-EBc.js → sqlite-DxgBiHFg.js} +3 -3
- package/dist/chunks/{sqlite-DZkc-EBc.js.map → sqlite-DxgBiHFg.js.map} +1 -1
- package/dist/chunks/{sqlite-native-79oQLrDp.js → sqlite-native-ytHS5F4a.js} +2 -2
- package/dist/chunks/{sqlite-native-79oQLrDp.js.map → sqlite-native-ytHS5F4a.js.map} +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/query.js +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-DubG7aF0.js","names":[],"sources":["../../src/aggregate.ts","../../src/shared/duckdb-schema-utils.ts","../../src/shared/types.ts","../../src/query.ts"],"sourcesContent":["import type { WhereClause } from './shared/types.js';\nimport {\n buildWhere,\n parseConditionKey,\n raw,\n type SqlAdapterType,\n unwrapRawKey,\n} from './shared/utils.js';\n\nexport type AggregateTimeBucketUnit =\n | 'minute'\n | 'hour'\n | 'day'\n | 'week'\n | 'month'\n | 'quarter'\n | 'year';\n\nexport type AggregateFunction =\n | 'count'\n | 'sum'\n | 'avg'\n | 'min'\n | 'max'\n | (string & {});\n\nexport type AggregateSelectExpr =\n | { column: string; as?: string }\n | {\n fn: AggregateFunction;\n column?: string;\n as: string;\n distinct?: boolean;\n }\n | {\n bucket: AggregateTimeBucketUnit;\n column: string;\n as: string;\n };\n\nexport interface AggregateSpec {\n from: string;\n select: AggregateSelectExpr[];\n where?: WhereClause;\n groupBy?: string[];\n having?: WhereClause;\n orderBy?: string | string[];\n limit?: number;\n offset?: number;\n}\n\nexport interface AggregateBuildResult {\n sql: string;\n values: any[];\n}\n\nconst VALID_DIRECTIONS = new Set(['ASC', 'DESC']);\nconst VALID_BUCKET_UNITS = new Set<AggregateTimeBucketUnit>([\n 'minute',\n 'hour',\n 'day',\n 'week',\n 'month',\n 'quarter',\n 'year',\n]);\n\nfunction validateSqlIdentifier(identifier: string): string {\n // Reject non-strings before the regex. `test` coerces via `toString`, so a\n // value whose `toString` differs across reads could pass here and interpolate\n // a different identifier into the aggregate SQL than the one validated.\n if (typeof identifier !== 'string' || !/^[a-zA-Z0-9_.]+$/.test(identifier)) {\n throw new Error(`Invalid SQL identifier: ${identifier}`);\n }\n return identifier;\n}\n\nfunction normalizeAdapter(adapterType?: SqlAdapterType): SqlAdapterType {\n return adapterType ?? 'sqlite';\n}\n\nfunction placeholder(index: number): string {\n return `$${index}`;\n}\n\nexport function bucketExpr(\n adapterType: SqlAdapterType | undefined,\n unit: AggregateTimeBucketUnit,\n column: string,\n): string {\n if (!VALID_BUCKET_UNITS.has(unit)) {\n throw new Error(`Invalid aggregate bucket unit: ${unit}`);\n }\n\n const col = validateSqlIdentifier(column);\n const adapter = normalizeAdapter(adapterType);\n\n if (adapter === 'postgres' || adapter === 'duckdb' || adapter === 'json') {\n return `date_trunc('${unit}', ${col})`;\n }\n\n switch (unit) {\n case 'minute':\n return `strftime('%Y-%m-%d %H:%M:00', ${col})`;\n case 'hour':\n return `strftime('%Y-%m-%d %H:00:00', ${col})`;\n case 'day':\n return `strftime('%Y-%m-%d 00:00:00', ${col})`;\n case 'week':\n return `strftime('%Y-%m-%d 00:00:00', date(${col}, printf('-%d days', (CAST(strftime('%w', ${col}) AS INTEGER) + 6) % 7)))`;\n case 'month':\n return `strftime('%Y-%m-01 00:00:00', ${col})`;\n case 'quarter':\n return `strftime('%Y-%m-%d 00:00:00', date(strftime('%Y-', ${col}) || printf('%02d', ((CAST(strftime('%m', ${col}) AS INTEGER) - 1) / 3) * 3 + 1) || '-01'))`;\n case 'year':\n return `strftime('%Y-01-01 00:00:00', ${col})`;\n }\n}\n\nfunction aggregateExpression(\n expr: Extract<AggregateSelectExpr, { fn: AggregateFunction }>,\n): string {\n const normalizedFn = String(expr.fn).toLowerCase();\n const fn = validateSqlIdentifier(String(expr.fn)).toUpperCase();\n if (normalizedFn === 'count' && !expr.column) {\n if (expr.distinct) {\n throw new Error(`Aggregate ${expr.fn} with distinct requires a column`);\n }\n return 'COUNT(*)';\n }\n if (!expr.column) {\n throw new Error(`Aggregate ${expr.fn} requires a column`);\n }\n const col = validateSqlIdentifier(expr.column);\n const distinct = expr.distinct ? 'DISTINCT ' : '';\n return `${fn}(${distinct}${col})`;\n}\n\nfunction selectExpression(\n expr: AggregateSelectExpr,\n adapterType: SqlAdapterType | undefined,\n): { expression: string; alias: string; groupingCandidate: boolean } {\n if ('column' in expr && !('fn' in expr) && !('bucket' in expr)) {\n const column = validateSqlIdentifier(expr.column);\n const alias = validateSqlIdentifier(expr.as ?? expr.column);\n return { expression: column, alias, groupingCandidate: true };\n }\n if ('bucket' in expr) {\n const expression = bucketExpr(adapterType, expr.bucket, expr.column);\n const alias = validateSqlIdentifier(expr.as);\n return { expression, alias, groupingCandidate: true };\n }\n\n const expression = aggregateExpression(expr);\n const alias = validateSqlIdentifier(expr.as);\n return { expression, alias, groupingCandidate: false };\n}\n\nfunction expandHavingKey(\n key: string,\n expressionByAlias: Map<string, string>,\n): string {\n // A key the caller already wrapped in raw() is used verbatim: raw() means\n // \"this exact SQL\", so it deliberately bypasses select-alias expansion.\n if (unwrapRawKey(key).isRaw) return key;\n\n const parsed = parseConditionKey(key);\n const expression =\n expressionByAlias.get(parsed.field) ?? validateSqlIdentifier(parsed.field);\n\n // The caller's key is validated above (as an alias or a plain identifier);\n // what comes out is a select expression such as `SUM(total) >`, which only\n // buildWhere's raw channel accepts.\n return raw(`${expression} ${parsed.operator}`);\n}\n\nfunction expandHaving(\n having: WhereClause,\n expressionByAlias: Map<string, string>,\n): WhereClause {\n const expandCondition = (condition: Record<string, any>) =>\n Object.fromEntries(\n Object.entries(condition).map(([key, value]) => [\n expandHavingKey(key, expressionByAlias),\n value,\n ]),\n );\n if (Array.isArray(having)) {\n return having.map((andGroup: Record<string, any>[]) =>\n andGroup.map((condition: Record<string, any>) =>\n expandCondition(condition),\n ),\n );\n }\n\n return expandCondition(having);\n}\n\nfunction buildHaving(\n having: AggregateSpec['having'],\n expressionByAlias: Map<string, string>,\n startIndex: number,\n adapterType: SqlAdapterType | undefined,\n): AggregateBuildResult {\n if (!having) return { sql: '', values: [] };\n const expanded = expandHaving(having, expressionByAlias);\n const built = buildWhere(expanded, startIndex, adapterType);\n\n return {\n sql: built.sql ? built.sql.replace(/^WHERE/, 'HAVING') : '',\n values: built.values,\n };\n}\n\nfunction buildOrderBy(orderBy: string | string[] | undefined): string {\n if (!orderBy) return '';\n const items = Array.isArray(orderBy) ? orderBy : [orderBy];\n const parts = items.map((item) => {\n const [field, direction = 'ASC'] = item.trim().split(/\\s+/);\n const normalizedDirection = direction.toUpperCase();\n if (!VALID_DIRECTIONS.has(normalizedDirection)) {\n throw new Error(`Invalid aggregate order direction: ${direction}`);\n }\n const orderExpr = validateSqlIdentifier(field);\n return `${orderExpr} ${normalizedDirection}`;\n });\n return `ORDER BY ${parts.join(', ')}`;\n}\n\nexport function buildAggregate(\n spec: AggregateSpec,\n startIndex = 1,\n adapterType?: SqlAdapterType,\n): AggregateBuildResult {\n const from = validateSqlIdentifier(spec.from);\n if (spec.select.length === 0) {\n throw new Error(\n 'AggregateSpec.select must contain at least one expression',\n );\n }\n\n const expressionByAlias = new Map<string, string>();\n const groupingAliases: string[] = [];\n const selectSql = spec.select.map((expr) => {\n const selected = selectExpression(expr, adapterType);\n expressionByAlias.set(selected.alias, selected.expression);\n if (selected.groupingCandidate) {\n groupingAliases.push(selected.alias);\n }\n return `${selected.expression} AS ${selected.alias}`;\n });\n\n const where = spec.where\n ? buildWhere(spec.where, startIndex, adapterType)\n : { sql: '', values: [] };\n let nextParamIndex = startIndex + where.values.length;\n\n const groupByItems = spec.groupBy ?? groupingAliases;\n const groupBySql =\n groupByItems.length > 0\n ? `GROUP BY ${groupByItems\n .map((item) =>\n expressionByAlias.has(item)\n ? expressionByAlias.get(item)\n : validateSqlIdentifier(item),\n )\n .join(', ')}`\n : '';\n\n const having = buildHaving(\n spec.having,\n expressionByAlias,\n nextParamIndex,\n adapterType,\n );\n nextParamIndex += having.values.length;\n\n const orderBySql = buildOrderBy(spec.orderBy);\n\n const pagingValues: any[] = [];\n const pagingSql: string[] = [];\n if (spec.limit !== undefined) {\n pagingSql.push(`LIMIT ${placeholder(nextParamIndex++)}`);\n pagingValues.push(spec.limit);\n } else if (\n spec.offset !== undefined &&\n normalizeAdapter(adapterType) === 'sqlite'\n ) {\n pagingSql.push('LIMIT -1');\n }\n if (spec.offset !== undefined) {\n pagingSql.push(`OFFSET ${placeholder(nextParamIndex++)}`);\n pagingValues.push(spec.offset);\n }\n\n const sql = [\n `SELECT ${selectSql.join(', ')}`,\n `FROM ${from}`,\n where.sql,\n groupBySql,\n having.sql,\n orderBySql,\n pagingSql.join(' '),\n ]\n .filter(Boolean)\n .join(' ');\n\n return {\n sql,\n values: [...where.values, ...having.values, ...pagingValues],\n };\n}\n","/**\n * DuckDB-specific schema transformation utilities\n *\n * DuckDB has a known limitation (issue #12684) where ON CONFLICT clauses\n * fail with UNIQUE INDEX but work with inline UNIQUE constraints.\n *\n * This module provides utilities to transform SMRT-generated schemas\n * (which use separate CREATE UNIQUE INDEX statements) into DuckDB-compatible\n * schemas (which use inline UNIQUE constraints).\n */\n\n/**\n * Index definition extracted from CREATE INDEX statement\n */\ninterface ParsedIndex {\n name: string;\n columns: string[];\n unique: boolean;\n sql: string;\n}\n\n/**\n * Parses a CREATE INDEX statement to extract index details\n *\n * @param indexSQL - CREATE INDEX statement\n * @returns Parsed index definition or null if parsing fails\n *\n * @example\n * ```typescript\n * const index = parseIndexStatement(\n * 'CREATE UNIQUE INDEX ds_slug_context_idx ON ds (slug, context)'\n * );\n * // Returns: {\n * // name: 'ds_slug_context_idx',\n * // columns: ['slug', 'context'],\n * // unique: true,\n * // sql: '...'\n * // }\n * ```\n */\nexport function parseIndexStatement(indexSQL: string): ParsedIndex | null {\n const trimmed = indexSQL.trim();\n\n // Match: CREATE [UNIQUE] INDEX [IF NOT EXISTS] name ON table (columns)\n // Support both quoted and unquoted identifiers: table, \"table\", columns, \"columns\"\n const match = trimmed.match(\n /CREATE\\s+(UNIQUE\\s+)?INDEX\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(\\w+)\\s+ON\\s+(?:\"?\\w+\"?)\\s*\\(([^)]+)\\)/i,\n );\n\n if (!match) {\n return null;\n }\n\n const [, uniqueKeyword, name, columnsStr] = match;\n\n // Remove quotes from column names\n const columns = columnsStr\n .split(',')\n .map((col) => col.trim().replace(/^\"|\"$/g, ''));\n\n return {\n name,\n columns,\n unique: Boolean(uniqueKeyword),\n sql: trimmed,\n };\n}\n\n/**\n * Converts UNIQUE INDEX statements to inline UNIQUE constraints for DuckDB compatibility\n *\n * DuckDB's ON CONFLICT clause requires inline UNIQUE constraints, not separate indexes.\n * This function transforms SMRT-generated schemas to work with DuckDB's limitations.\n *\n * @param ddl - CREATE TABLE DDL statement\n * @param indexes - Array of CREATE INDEX statements (optional)\n * @returns Transformed DDL with inline UNIQUE constraints and filtered index list\n *\n * @example\n * ```typescript\n * const ddl = `\n * CREATE TABLE ds (\n * id TEXT PRIMARY KEY,\n * slug TEXT NOT NULL,\n * context TEXT NOT NULL\n * )\n * `;\n * const indexes = [\n * 'CREATE UNIQUE INDEX ds_slug_context_idx ON ds (slug, context)'\n * ];\n *\n * const result = convertUniqueIndexesToInlineConstraints(ddl, indexes);\n * // result.ddl:\n * // CREATE TABLE ds (\n * // id TEXT PRIMARY KEY,\n * // slug TEXT NOT NULL,\n * // context TEXT NOT NULL,\n * // UNIQUE(slug, context)\n * // )\n * // result.indexes: [] (UNIQUE index removed, non-UNIQUE indexes preserved)\n * ```\n */\nexport function convertUniqueIndexesToInlineConstraints(\n ddl: string,\n indexes?: string[],\n): {\n ddl: string;\n indexes: string[];\n} {\n if (!indexes || indexes.length === 0) {\n return { ddl, indexes: [] };\n }\n\n // Parse all indexes\n const parsedIndexes = indexes\n .map(parseIndexStatement)\n .filter((idx): idx is ParsedIndex => idx !== null);\n\n // Separate UNIQUE indexes from regular indexes\n const uniqueIndexes = parsedIndexes.filter((idx) => idx.unique);\n const regularIndexes = parsedIndexes.filter((idx) => !idx.unique);\n\n // If no UNIQUE indexes, return unchanged\n if (uniqueIndexes.length === 0) {\n return { ddl, indexes };\n }\n\n // Find the closing parenthesis of the CREATE TABLE statement\n const lines = ddl.split('\\n');\n let closingParenIndex = -1;\n\n for (let i = lines.length - 1; i >= 0; i--) {\n const trimmed = lines[i].trim();\n if (trimmed === ')' || trimmed.startsWith(');')) {\n closingParenIndex = i;\n break;\n }\n }\n\n if (closingParenIndex === -1) {\n // Can't find closing paren - return unchanged\n console.warn(\n '[duckdb-schema-utils] Could not find closing parenthesis in CREATE TABLE statement',\n );\n return { ddl, indexes };\n }\n\n // Build inline UNIQUE constraints with quoted column names\n const uniqueConstraints = uniqueIndexes.map((idx) => {\n const columns = idx.columns.map((col) => `\"${col}\"`).join(', ');\n return ` UNIQUE(${columns})`;\n });\n\n // Insert UNIQUE constraints before closing parenthesis\n const modifiedLines = [...lines];\n const closingLine = modifiedLines[closingParenIndex].trim();\n\n // Add comma to previous line if it doesn't have one\n if (closingParenIndex > 0) {\n const prevLine = modifiedLines[closingParenIndex - 1].trim();\n if (!prevLine.endsWith(',')) {\n modifiedLines[closingParenIndex - 1] = modifiedLines[\n closingParenIndex - 1\n ].replace(/\\s*$/, ',');\n }\n }\n\n // Insert UNIQUE constraints\n const constraintLines = uniqueConstraints.map((constraint, idx) => {\n // Add comma after all but the last constraint\n return idx < uniqueConstraints.length - 1 ? `${constraint},` : constraint;\n });\n\n modifiedLines.splice(closingParenIndex, 0, ...constraintLines);\n\n const transformedDDL = modifiedLines.join('\\n');\n\n // Return regular (non-UNIQUE) indexes only\n const remainingIndexes = regularIndexes.map((idx) => idx.sql);\n\n return {\n ddl: transformedDDL,\n indexes: remainingIndexes,\n };\n}\n\n/**\n * Checks if a schema definition contains UNIQUE indexes that need conversion\n *\n * @param indexes - Array of CREATE INDEX statements\n * @returns True if any UNIQUE indexes are present\n */\nexport function hasUniqueIndexes(indexes?: string[]): boolean {\n if (!indexes || indexes.length === 0) {\n return false;\n }\n\n return indexes.some((indexSQL) => {\n const parsed = parseIndexStatement(indexSQL);\n return parsed?.unique === true;\n });\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 name contains the exact text '100% cotton'\n * const where = {\n * status: 'active',\n * 'name contains': '100% cotton',\n * };\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, JSON and PGlite. 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, JSON and PGlite. 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 * When automatic file registration is enabled, JSON adapters detect changed,\n * added, or removed `.json` and companion `.schema.sql` files on a later\n * acquisition. Use this option when a caller needs an unconditional reload\n * even if those sources are unchanged. Existing references point to the\n * closed adapter after replacement.\n *\n * @default false\n *\n * @example\n * // Initial connection - cached\n * const db1 = await getDatabase({ type: 'json', url: './data' });\n *\n * // Force an unconditional replacement...\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, JSON and PGlite drive one\n * connection, and 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","/**\n * Browser-safe entry for the driver-independent query helpers.\n *\n * Everything here is pure SQL-fragment construction or operates on an\n * already-open `DatabaseInterface`. Nothing on this import graph reaches `pg`,\n * the libsql or DuckDB adapters, or any `node:` built-in, so browser-bundled\n * packages can import it instead of the package root. The root entry\n * re-exports all of it.\n *\n * @example\n * ```typescript\n * import { buildWhere, validateColumnName } from '@happyvertical/sql/query';\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { DatabaseInterface } from './shared/types.js';\n\n/**\n * Checks if a table exists in the database\n *\n * @param db - Database interface to use\n * @param tableName - Name of the table to check\n * @returns Promise resolving to boolean indicating if the table exists\n */\nexport async function tableExists(db: DatabaseInterface, tableName: string) {\n return db.tableExists(tableName);\n}\n\n/**\n * Escapes and formats a value for use in SQL queries\n *\n * @param value - Value to escape\n * @returns String representation of the value safe for SQL use\n */\nexport function escapeSqlValue(value: any): string {\n if (value === null) {\n return 'NULL';\n }\n if (value instanceof Date) {\n return `'${value.toISOString()}'`;\n }\n if (typeof value === 'number') {\n return value.toString();\n }\n if (typeof value === 'boolean') {\n return value ? '1' : '0';\n }\n // Escape single quotes and wrap in quotes\n return `'${String(value).replace(/'/g, \"''\")}'`;\n}\n\n/**\n * Validates a column name for use in SQL queries\n *\n * @param column - Column name to validate\n * @returns The validated column name\n * @throws Error if the column name contains invalid characters\n */\nexport function validateColumnName(column: string): string {\n // Reject non-strings before the regex. `test` coerces via `toString`, so an\n // object whose `toString` differs between this read and a later interpolation\n // read would pass validation and then reach SQL as a different identifier.\n // Only allow alphanumeric characters, underscores, and dots (for table.column notation)\n if (typeof column !== 'string' || !/^[a-zA-Z0-9_.]+$/.test(column)) {\n throw new Error(`Invalid column name: ${column}`);\n }\n return column;\n}\n\n/**\n * Synchronizes a SQL schema definition with a database\n * Creates tables if they don't exist and adds missing columns to existing tables\n *\n * @param options - Object containing database and schema\n * @param options.db - Database interface to use\n * @param options.schema - SQL schema definition\n * @throws Error if db or schema are missing or if the database doesn't support syncSchema\n */\nexport async function syncSchema(options: {\n db: DatabaseInterface;\n schema: string;\n}) {\n const { db, schema } = options;\n if (!db || !schema) {\n throw new Error('db and schema are required');\n }\n\n // Delegate to the database adapter's syncSchema implementation\n if (db.syncSchema) {\n await db.syncSchema(schema);\n } else {\n throw new Error('Database adapter does not support schema synchronization');\n }\n}\n\nexport {\n type AggregateBuildResult,\n type AggregateFunction,\n type AggregateSelectExpr,\n type AggregateSpec,\n type AggregateTimeBucketUnit,\n bucketExpr,\n buildAggregate,\n} from './aggregate.js';\nexport type { SchemaInitializationResult } from './schema-manager.js';\nexport { DatabaseSchemaManager } from './schema-manager.js';\nexport { convertUniqueIndexesToInlineConstraints } from './shared/duckdb-schema-utils.js';\nexport * from './shared/types.js';\nexport type { RawSqlKey, SqlAdapterType } from './shared/utils.js';\nexport { buildWhere, formatDbError, raw } from './shared/utils.js';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"query-B6ATOWzc.js","names":[],"sources":["../../src/aggregate-expression.ts","../../src/aggregate.ts","../../src/shared/duckdb-schema-utils.ts","../../src/shared/types.ts","../../src/query.ts"],"sourcesContent":["import type { SqlAdapterType } from './shared/utils.js';\n\n/**\n * Closed, structured expression nodes for {@link buildAggregate}.\n *\n * An expression is data, never SQL text: every node is checked against a fixed\n * set of operators, column references are validated identifiers, and literals\n * are either inlined after validation (finite numbers, booleans, null) or\n * bound as parameters (strings, dates). There is no raw-SQL escape hatch.\n */\n\n/** A scalar a node may hold. Strings and dates are bound; the rest are inlined. */\nexport type AggregateLiteral = string | number | boolean | Date | null;\n\nexport type AggregateArithmeticOp = 'add' | 'sub' | 'mul' | 'div';\nexport type AggregateComparisonOp = 'eq' | 'ne' | 'lt' | 'lte' | 'gt' | 'gte';\n\nexport type AggregateOperand =\n /** A column, bare (`qty`) or qualified by a table alias (`wo.estimated_hours`). */\n | { col: string }\n /** A literal value. */\n | { value: AggregateLiteral }\n | AggregateExpression;\n\nexport type AggregateExpression =\n /**\n * `left op right`. `div` yields a fractional result on every dialect (integer\n * operands are not truncated) and NULL, not an error, for a zero divisor.\n */\n | {\n op: AggregateArithmeticOp;\n left: AggregateOperand;\n right: AggregateOperand;\n }\n /**\n * Seconds from `earlier` to `later`, both timestamps; fractional, NULL if\n * either is NULL. Positive when `later` is after `earlier`.\n */\n | { op: 'secondsBetween'; later: AggregateOperand; earlier: AggregateOperand }\n /**\n * A comparison. `eq`/`ne` against a NULL literal compare with IS [NOT] NULL;\n * any other comparison involving NULL is NULL (not true), as in SQL.\n */\n | {\n op: AggregateComparisonOp;\n left: AggregateOperand;\n right: AggregateOperand;\n }\n | { op: 'and' | 'or'; operands: AggregateOperand[] }\n | { op: 'not'; operand: AggregateOperand }\n | { op: 'isNull' | 'isNotNull'; operand: AggregateOperand }\n /** The first operand that is not NULL. */\n | { op: 'coalesce'; operands: AggregateOperand[] };\n\n/** A table joined to the aggregate's `from`. */\nexport interface AggregateJoin {\n /** The joined table. */\n table: string;\n /** Its alias in the query; referenced as `alias.column`. Unique, no dots. */\n alias: string;\n /** `inner` (default) or `left`. */\n type?: 'inner' | 'left';\n /**\n * The join condition. Callers put a tenant equality here (for example\n * `eq(a.tenant_id, b.tenant_id)`) so ids that collide across tenants never\n * match; the builder does not know what a tenant is.\n */\n on: AggregateExpression;\n}\n\nconst MAX_DEPTH = 24;\n/** Node budget for a whole spec (every select item, join, filter and predicate together). */\nconst MAX_NODES = 1024;\n\nconst ARITHMETIC: Record<AggregateArithmeticOp, string> = {\n add: '+',\n sub: '-',\n mul: '*',\n div: '/',\n};\nconst COMPARISON: Record<AggregateComparisonOp, string> = {\n eq: '=',\n ne: '<>',\n lt: '<',\n lte: '<=',\n gt: '>',\n gte: '>=',\n};\n\n/** Where an expression renders: its identifier checks and its parameter numbering. */\nexport interface ExpressionContext {\n adapterType: SqlAdapterType | undefined;\n /** The parameter index the next bound value takes. Advanced as values are bound. */\n nextIndex: number;\n /** Values bound so far, in index order. */\n values: unknown[];\n /** Validates (and returns) an identifier; throws on anything unsafe. */\n identifier: (identifier: string) => string;\n /** Called with each column reference so the caller can check its table alias. */\n onColumn?: (column: string) => void;\n /** Nodes rendered so far across every expression of one build; capped at {@link MAX_NODES}. */\n nodeCount?: number;\n}\n\ninterface RenderState {\n ctx: ExpressionContext;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction literal(value: unknown, state: RenderState): string {\n const { ctx } = state;\n if (value === null) return 'NULL';\n if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE';\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) {\n throw new Error('Aggregate expression numbers must be finite');\n }\n // Inlined: a finite number cannot carry SQL, and an inlined number avoids\n // an untyped parameter (`$1 + $2`) that Postgres cannot infer.\n return String(value);\n }\n if (value instanceof Date) {\n if (Number.isNaN(value.getTime())) {\n throw new Error('Aggregate expression dates must be valid');\n }\n const placeholder = `$${ctx.nextIndex++}`;\n ctx.values.push(value.toISOString());\n // DuckDB/JSON cannot infer a timestamp from a bound string (issue #540),\n // and Postgres cannot type two bare parameters (`$1 - $2`).\n if (ctx.adapterType === 'duckdb' || ctx.adapterType === 'json') {\n return `CAST(${placeholder} AS TIMESTAMP)`;\n }\n if (ctx.adapterType === 'postgres') {\n return `CAST(${placeholder} AS TIMESTAMPTZ)`;\n }\n return placeholder;\n }\n if (typeof value === 'string') {\n const placeholder = `$${ctx.nextIndex++}`;\n ctx.values.push(value);\n return placeholder;\n }\n throw new Error(\n 'Aggregate expression literal must be a string, number, boolean, Date or null',\n );\n}\n\nfunction secondsBetween(\n adapterType: SqlAdapterType | undefined,\n later: string,\n earlier: string,\n): string {\n if (adapterType === 'postgres') {\n // A float like the other dialects: EXTRACT returns numeric, which drivers hand back as a string.\n return `CAST(EXTRACT(EPOCH FROM (${later} - ${earlier})) AS DOUBLE PRECISION)`;\n }\n if (adapterType === 'duckdb' || adapterType === 'json') {\n return `epoch(${later}) - epoch(${earlier})`;\n }\n // SQLite stores timestamps as ISO text; julianday reads that and the\n // difference is in days.\n return `((julianday(${later}) - julianday(${earlier})) * 86400.0)`;\n}\n\nfunction renderOperand(\n node: unknown,\n state: RenderState,\n depth: number,\n): string {\n if (depth > MAX_DEPTH) {\n throw new Error(`Aggregate expression is nested deeper than ${MAX_DEPTH}`);\n }\n state.ctx.nodeCount = (state.ctx.nodeCount ?? 0) + 1;\n if (state.ctx.nodeCount > MAX_NODES) {\n throw new Error(`Aggregate expression has more than ${MAX_NODES} nodes`);\n }\n if (!isRecord(node)) {\n throw new Error('Aggregate expression node must be an object');\n }\n const { ctx } = state;\n\n if ('col' in node) {\n const column = ctx.identifier(node.col as string);\n ctx.onColumn?.(column);\n return column;\n }\n if ('value' in node) {\n return literal(node.value, state);\n }\n\n const op = node.op;\n const child = (value: unknown) => renderOperand(value, state, depth + 1);\n\n if (typeof op === 'string' && Object.hasOwn(ARITHMETIC, op)) {\n const arithmetic = op as AggregateArithmeticOp;\n const left = child(node.left);\n const right = child(node.right);\n if (arithmetic === 'div') {\n // `* 1.0` keeps integer operands fractional (SQLite and Postgres truncate\n // integer division); NULLIF turns a zero divisor into NULL.\n const quotient = `((${left} * 1.0) / NULLIF(${right}, 0))`;\n // A float on every dialect: Postgres would otherwise return numeric,\n // which drivers hand back as a string.\n if (ctx.adapterType === 'postgres') {\n return `CAST(${quotient} AS DOUBLE PRECISION)`;\n }\n if (ctx.adapterType === 'duckdb' || ctx.adapterType === 'json') {\n return `CAST(${quotient} AS DOUBLE)`;\n }\n return quotient;\n }\n return `(${left} ${ARITHMETIC[arithmetic]} ${right})`;\n }\n if (op === 'secondsBetween') {\n return secondsBetween(\n ctx.adapterType,\n child(node.later),\n child(node.earlier),\n );\n }\n if (typeof op === 'string' && Object.hasOwn(COMPARISON, op)) {\n const comparison = op as AggregateComparisonOp;\n const against = (side: unknown): boolean =>\n isRecord(side) && 'value' in side && side.value === null;\n if ((comparison === 'eq' || comparison === 'ne') && against(node.right)) {\n return `(${child(node.left)} IS ${comparison === 'eq' ? 'NULL' : 'NOT NULL'})`;\n }\n if ((comparison === 'eq' || comparison === 'ne') && against(node.left)) {\n return `(${child(node.right)} IS ${comparison === 'eq' ? 'NULL' : 'NOT NULL'})`;\n }\n return `(${child(node.left)} ${COMPARISON[comparison]} ${child(node.right)})`;\n }\n if (op === 'and' || op === 'or') {\n const operands = node.operands;\n if (!Array.isArray(operands) || operands.length === 0) {\n throw new Error(\n `Aggregate expression ${op} requires at least one operand`,\n );\n }\n return `(${operands.map(child).join(op === 'and' ? ' AND ' : ' OR ')})`;\n }\n if (op === 'not') {\n return `(NOT ${child(node.operand)})`;\n }\n if (op === 'isNull' || op === 'isNotNull') {\n return `(${child(node.operand)} IS ${op === 'isNull' ? 'NULL' : 'NOT NULL'})`;\n }\n if (op === 'coalesce') {\n const operands = node.operands;\n if (!Array.isArray(operands) || operands.length === 0) {\n throw new Error(\n 'Aggregate expression coalesce requires at least one operand',\n );\n }\n return `COALESCE(${operands.map(child).join(', ')})`;\n }\n throw new Error(`Unsupported aggregate expression operator: ${String(op)}`);\n}\n\n/**\n * Render an expression node to SQL, binding its string and date literals into\n * `ctx` as numbered parameters. Throws on any unknown operator, unsafe\n * identifier, non-finite number, or an expression too large to be legitimate.\n */\nexport function renderAggregateExpression(\n node: AggregateOperand,\n ctx: ExpressionContext,\n): string {\n return renderOperand(node, { ctx }, 0);\n}\n","import {\n type AggregateJoin,\n type AggregateOperand,\n type ExpressionContext,\n renderAggregateExpression,\n} from './aggregate-expression.js';\nimport type { WhereClause } from './shared/types.js';\nimport {\n buildWhere,\n parseConditionKey,\n raw,\n type SqlAdapterType,\n unwrapRawKey,\n} from './shared/utils.js';\n\nexport type AggregateTimeBucketUnit =\n | 'minute'\n | 'hour'\n | 'day'\n | 'week'\n | 'month'\n | 'quarter'\n | 'year';\n\nexport type AggregateFunction =\n | 'count'\n | 'sum'\n | 'avg'\n | 'min'\n | 'max'\n | (string & {});\n\nexport type AggregateSelectExpr =\n | { column: string; as?: string }\n /** A row expression used as a grouping key (a computed column of each row). */\n | { expr: AggregateOperand; as: string }\n | {\n fn: AggregateFunction;\n column?: string;\n /** A row expression to aggregate in place of `column` (not both). */\n expr?: AggregateOperand;\n /**\n * Aggregate only the rows this predicate holds for (a filtered count or\n * sum). Rows where it is false or NULL are skipped. With `count` and no\n * column or expression it counts the matching rows.\n */\n filter?: AggregateOperand;\n as: string;\n distinct?: boolean;\n }\n | {\n bucket: AggregateTimeBucketUnit;\n column: string;\n as: string;\n };\n\nexport interface AggregateSpec {\n from: string;\n /** Alias for `from`, so its columns can be qualified (`o.total`) beside joined tables. */\n fromAlias?: string;\n /**\n * Tables joined to `from`, in order. With joins, a column naming a table must\n * be qualified by `from`, `fromAlias` or a join alias.\n */\n joins?: AggregateJoin[];\n select: AggregateSelectExpr[];\n where?: WhereClause;\n /** A row predicate built from expression nodes, ANDed with `where`. */\n whereExpr?: AggregateOperand;\n groupBy?: string[];\n having?: WhereClause;\n orderBy?: string | string[];\n limit?: number;\n offset?: number;\n}\n\nexport interface AggregateBuildResult {\n sql: string;\n values: any[];\n}\n\nconst EXPRESSION_AGGREGATES = new Set(['count', 'sum', 'avg', 'min', 'max']);\nconst VALID_DIRECTIONS = new Set(['ASC', 'DESC']);\nconst VALID_BUCKET_UNITS = new Set<AggregateTimeBucketUnit>([\n 'minute',\n 'hour',\n 'day',\n 'week',\n 'month',\n 'quarter',\n 'year',\n]);\n\nfunction validateSqlIdentifier(identifier: string): string {\n // Reject non-strings before the regex. `test` coerces via `toString`, so a\n // value whose `toString` differs across reads could pass here and interpolate\n // a different identifier into the aggregate SQL than the one validated.\n if (typeof identifier !== 'string' || !/^[a-zA-Z0-9_.]+$/.test(identifier)) {\n throw new Error(`Invalid SQL identifier: ${identifier}`);\n }\n return identifier;\n}\n\nfunction normalizeAdapter(adapterType?: SqlAdapterType): SqlAdapterType {\n return adapterType ?? 'sqlite';\n}\n\nfunction placeholder(index: number): string {\n return `$${index}`;\n}\n\nexport function bucketExpr(\n adapterType: SqlAdapterType | undefined,\n unit: AggregateTimeBucketUnit,\n column: string,\n): string {\n if (!VALID_BUCKET_UNITS.has(unit)) {\n throw new Error(`Invalid aggregate bucket unit: ${unit}`);\n }\n\n const col = validateSqlIdentifier(column);\n const adapter = normalizeAdapter(adapterType);\n\n if (adapter === 'postgres' || adapter === 'duckdb' || adapter === 'json') {\n return `date_trunc('${unit}', ${col})`;\n }\n\n switch (unit) {\n case 'minute':\n return `strftime('%Y-%m-%d %H:%M:00', ${col})`;\n case 'hour':\n return `strftime('%Y-%m-%d %H:00:00', ${col})`;\n case 'day':\n return `strftime('%Y-%m-%d 00:00:00', ${col})`;\n case 'week':\n return `strftime('%Y-%m-%d 00:00:00', date(${col}, printf('-%d days', (CAST(strftime('%w', ${col}) AS INTEGER) + 6) % 7)))`;\n case 'month':\n return `strftime('%Y-%m-01 00:00:00', ${col})`;\n case 'quarter':\n return `strftime('%Y-%m-%d 00:00:00', date(strftime('%Y-', ${col}) || printf('%02d', ((CAST(strftime('%m', ${col}) AS INTEGER) - 1) / 3) * 3 + 1) || '-01'))`;\n case 'year':\n return `strftime('%Y-01-01 00:00:00', ${col})`;\n }\n}\n\n/** What a select expression, join and predicate share while one query is built. */\ninterface BuildState {\n ctx: ExpressionContext;\n /** Table names and aliases a qualified column may name; undefined without joins. */\n scope: Set<string> | undefined;\n}\n\nfunction checkColumn(state: BuildState, column: string): string {\n const { scope } = state;\n if (scope && column.includes('.')) {\n const parts = column.split('.');\n if (parts.length !== 2 || !scope.has(parts[0])) {\n throw new Error(\n `Aggregate column ${column} must be qualified by the from table or a join alias`,\n );\n }\n }\n return column;\n}\n\nfunction validateColumn(state: BuildState, column: string): string {\n return checkColumn(state, validateSqlIdentifier(column));\n}\n\nfunction renderExpression(state: BuildState, node: AggregateOperand): string {\n return renderAggregateExpression(node, state.ctx);\n}\n\nfunction aggregateExpression(\n expr: Extract<AggregateSelectExpr, { fn: AggregateFunction }>,\n state: BuildState,\n): string {\n const normalizedFn = String(expr.fn).toLowerCase();\n const fn = validateSqlIdentifier(String(expr.fn)).toUpperCase();\n if (expr.column !== undefined && expr.expr !== undefined) {\n throw new Error(`Aggregate ${expr.fn} takes a column or an expr, not both`);\n }\n // `fn` is free-form for plain columns (existing behaviour), but an expression\n // or filter can feed it bound values, so it must be a real aggregate there.\n if (\n (expr.expr !== undefined || expr.filter !== undefined) &&\n !EXPRESSION_AGGREGATES.has(normalizedFn)\n ) {\n throw new Error(\n `Aggregate ${expr.fn} cannot take an expr or filter; use count, sum, avg, min or max`,\n );\n }\n const hasArgument = Boolean(expr.column) || expr.expr !== undefined;\n // Rendered first so a filter's parameters are numbered before the argument's.\n const filter =\n expr.filter !== undefined\n ? renderExpression(state, expr.filter)\n : undefined;\n\n if (normalizedFn === 'count' && !hasArgument) {\n if (expr.distinct) {\n throw new Error(`Aggregate ${expr.fn} with distinct requires a column`);\n }\n return filter ? `COUNT(CASE WHEN ${filter} THEN 1 END)` : 'COUNT(*)';\n }\n if (!hasArgument) {\n throw new Error(`Aggregate ${expr.fn} requires a column`);\n }\n const argument =\n expr.expr !== undefined\n ? renderExpression(state, expr.expr)\n : validateColumn(state, expr.column as string);\n const distinct = expr.distinct ? 'DISTINCT ' : '';\n // CASE WHEN rather than FILTER (WHERE ...): it means the same on every\n // dialect and every SQLite version.\n const inner = filter ? `CASE WHEN ${filter} THEN ${argument} END` : argument;\n return `${fn}(${distinct}${inner})`;\n}\n\nfunction selectExpression(\n expr: AggregateSelectExpr,\n adapterType: SqlAdapterType | undefined,\n state: BuildState,\n): {\n expression: string;\n alias: string;\n groupingCandidate: boolean;\n computed: boolean;\n} {\n if (\n ['fn' in expr, 'expr' in expr && !('fn' in expr), 'bucket' in expr].filter(\n Boolean,\n ).length > 1\n ) {\n throw new Error('Aggregate select item mixes fn, bucket and expr');\n }\n if ('fn' in expr) {\n const expression = aggregateExpression(expr, state);\n const alias = validateSqlIdentifier(expr.as);\n return { expression, alias, groupingCandidate: false, computed: false };\n }\n if ('expr' in expr) {\n const expression = renderExpression(state, expr.expr);\n const alias = validateSqlIdentifier(expr.as);\n return { expression, alias, groupingCandidate: true, computed: true };\n }\n if ('column' in expr && !('bucket' in expr)) {\n const column = validateColumn(state, expr.column);\n // A qualified column (`wo.title`) is named by its column part, not the dotted path.\n const alias = validateSqlIdentifier(\n expr.as ?? expr.column.split('.').pop() ?? expr.column,\n );\n return {\n expression: column,\n alias,\n groupingCandidate: true,\n computed: false,\n };\n }\n const column = validateColumn(state, (expr as { column: string }).column);\n const bucket = (expr as { bucket: AggregateTimeBucketUnit }).bucket;\n const expression = bucketExpr(adapterType, bucket, column);\n const alias = validateSqlIdentifier((expr as { as: string }).as);\n return { expression, alias, groupingCandidate: true, computed: false };\n}\n\nfunction expandHavingKey(\n key: string,\n expressionByAlias: Map<string, string>,\n): string {\n // A key the caller already wrapped in raw() is used verbatim: raw() means\n // \"this exact SQL\", so it deliberately bypasses select-alias expansion.\n if (unwrapRawKey(key).isRaw) return key;\n\n const parsed = parseConditionKey(key);\n const expression =\n expressionByAlias.get(parsed.field) ?? validateSqlIdentifier(parsed.field);\n\n // The caller's key is validated above (as an alias or a plain identifier);\n // what comes out is a select expression such as `SUM(total) >`, which only\n // buildWhere's raw channel accepts.\n return raw(`${expression} ${parsed.operator}`);\n}\n\nfunction expandHaving(\n having: WhereClause,\n expressionByAlias: Map<string, string>,\n): WhereClause {\n const expandCondition = (condition: Record<string, any>) =>\n Object.fromEntries(\n Object.entries(condition).map(([key, value]) => [\n expandHavingKey(key, expressionByAlias),\n value,\n ]),\n );\n if (Array.isArray(having)) {\n return having.map((andGroup: Record<string, any>[]) =>\n andGroup.map((condition: Record<string, any>) =>\n expandCondition(condition),\n ),\n );\n }\n\n return expandCondition(having);\n}\n\nfunction buildHaving(\n having: AggregateSpec['having'],\n expressionByAlias: Map<string, string>,\n startIndex: number,\n adapterType: SqlAdapterType | undefined,\n): AggregateBuildResult {\n if (!having) return { sql: '', values: [] };\n const expanded = expandHaving(having, expressionByAlias);\n const built = buildWhere(expanded, startIndex, adapterType);\n\n return {\n sql: built.sql ? built.sql.replace(/^WHERE/, 'HAVING') : '',\n values: built.values,\n };\n}\n\nfunction buildOrderBy(\n orderBy: string | string[] | undefined,\n state: BuildState,\n aliases: ReadonlySet<string>,\n): string {\n if (!orderBy) return '';\n const items = Array.isArray(orderBy) ? orderBy : [orderBy];\n const parts = items.map((item) => {\n const [field, direction = 'ASC'] = item.trim().split(/\\s+/);\n const normalizedDirection = direction.toUpperCase();\n if (!VALID_DIRECTIONS.has(normalizedDirection)) {\n throw new Error(`Invalid aggregate order direction: ${direction}`);\n }\n const orderExpr = aliases.has(field)\n ? validateSqlIdentifier(field)\n : validateColumn(state, field);\n return `${orderExpr} ${normalizedDirection}`;\n });\n return `ORDER BY ${parts.join(', ')}`;\n}\n\nconst VALID_JOIN_TYPES = new Set(['inner', 'left']);\nconst JOIN_ALIAS = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nfunction buildScope(\n fromAlias: string | undefined,\n joins: AggregateJoin[],\n from: string,\n): Set<string> | undefined {\n if (joins.length === 0 && fromAlias === undefined) return undefined;\n const scope = new Set<string>();\n const claim = (name: string, what: string) => {\n // A non-string could pass the pattern once and render differently later.\n if (typeof name !== 'string' || !JOIN_ALIAS.test(name)) {\n throw new Error(`Invalid aggregate ${what}: ${name}`);\n }\n if (scope.has(name)) {\n throw new Error(`Duplicate aggregate table name or alias: ${name}`);\n }\n scope.add(name);\n };\n if (fromAlias !== undefined) {\n claim(fromAlias, 'from alias');\n } else {\n if (from.includes('.')) {\n throw new Error(\n 'AggregateSpec.fromAlias is required when joining a schema-qualified from table',\n );\n }\n claim(from, 'from table');\n }\n for (const join of joins) {\n claim(join.alias, 'join alias');\n }\n return scope;\n}\n\nfunction joinSql(state: BuildState, joins: AggregateJoin[]): string[] {\n return joins.map((join) => {\n const type = join.type ?? 'inner';\n if (!VALID_JOIN_TYPES.has(type)) {\n throw new Error(`Invalid aggregate join type: ${type}`);\n }\n if (join.on === undefined) {\n throw new Error(`Aggregate join ${join.alias} requires an on condition`);\n }\n const table = validateSqlIdentifier(join.table);\n const alias = join.alias;\n if (typeof alias !== 'string' || !JOIN_ALIAS.test(alias)) {\n throw new Error(`Invalid aggregate join alias: ${String(alias)}`);\n }\n const on = renderExpression(state, join.on);\n return `${type === 'left' ? 'LEFT' : 'INNER'} JOIN ${table} AS ${alias} ON ${on}`;\n });\n}\n\nexport function buildAggregate(\n spec: AggregateSpec,\n startIndex = 1,\n adapterType?: SqlAdapterType,\n): AggregateBuildResult {\n const from = validateSqlIdentifier(spec.from);\n if (spec.select.length === 0) {\n throw new Error(\n 'AggregateSpec.select must contain at least one expression',\n );\n }\n if (spec.joins !== undefined && !Array.isArray(spec.joins)) {\n throw new Error('AggregateSpec.joins must be an array');\n }\n\n // Every bound value is numbered in the order it is built (select, joins,\n // where, whereExpr, having, paging) and listed in that order in `values`.\n const values: any[] = [];\n const ctx: ExpressionContext = {\n adapterType,\n nextIndex: startIndex,\n values,\n identifier: validateSqlIdentifier,\n };\n // Read once: the validated string is the one rendered.\n const fromAlias = spec.fromAlias;\n const joins: AggregateJoin[] = (spec.joins ?? []).map((join) => ({\n table: join.table,\n alias: join.alias,\n type: join.type,\n on: join.on,\n }));\n const state: BuildState = {\n ctx,\n scope: buildScope(fromAlias, joins, from),\n };\n ctx.onColumn = (column) => {\n checkColumn(state, column);\n };\n\n const expressionByAlias = new Map<string, string>();\n const ordinalByAlias = new Map<string, number>();\n const groupingAliases: string[] = [];\n const selectSql = spec.select.map((expr, position) => {\n const selected = selectExpression(expr, adapterType, state);\n if (expressionByAlias.has(selected.alias)) {\n throw new Error(`Duplicate aggregate select alias: ${selected.alias}`);\n }\n expressionByAlias.set(selected.alias, selected.expression);\n // A computed key is grouped by its position in the select list, so a bound\n // parameter in it is not repeated in the GROUP BY.\n if (selected.computed) ordinalByAlias.set(selected.alias, position + 1);\n if (selected.groupingCandidate) {\n groupingAliases.push(selected.alias);\n }\n return `${selected.expression} AS ${selected.alias}`;\n });\n\n const joinParts = joinSql(state, joins);\n\n const where = spec.where\n ? buildWhere(spec.where, ctx.nextIndex, adapterType)\n : { sql: '', values: [] };\n values.push(...where.values);\n ctx.nextIndex += where.values.length;\n\n let whereSql = where.sql;\n if (spec.whereExpr !== undefined) {\n const predicate = renderExpression(state, spec.whereExpr);\n // `where` may hold OR groups; parenthesise it so whereExpr constrains all of them.\n whereSql = whereSql\n ? `WHERE (${whereSql.replace(/^WHERE /, '')}) AND ${predicate}`\n : `WHERE ${predicate}`;\n }\n\n const groupByItems = spec.groupBy ?? groupingAliases;\n const groupBySql =\n groupByItems.length > 0\n ? `GROUP BY ${groupByItems\n .map((item) =>\n ordinalByAlias.has(item)\n ? String(ordinalByAlias.get(item))\n : expressionByAlias.has(item)\n ? expressionByAlias.get(item)\n : validateColumn(state, item),\n )\n .join(', ')}`\n : '';\n\n const having = buildHaving(\n spec.having,\n expressionByAlias,\n ctx.nextIndex,\n adapterType,\n );\n values.push(...having.values);\n ctx.nextIndex += having.values.length;\n\n const orderBySql = buildOrderBy(\n spec.orderBy,\n state,\n new Set(expressionByAlias.keys()),\n );\n\n const pagingSql: string[] = [];\n if (spec.limit !== undefined) {\n pagingSql.push(`LIMIT ${placeholder(ctx.nextIndex++)}`);\n values.push(spec.limit);\n } else if (\n spec.offset !== undefined &&\n normalizeAdapter(adapterType) === 'sqlite'\n ) {\n pagingSql.push('LIMIT -1');\n }\n if (spec.offset !== undefined) {\n pagingSql.push(`OFFSET ${placeholder(ctx.nextIndex++)}`);\n values.push(spec.offset);\n }\n\n const fromSql =\n spec.fromAlias !== undefined ? `${from} AS ${fromAlias}` : from;\n const sql = [\n `SELECT ${selectSql.join(', ')}`,\n `FROM ${fromSql}`,\n ...joinParts,\n whereSql,\n groupBySql,\n having.sql,\n orderBySql,\n pagingSql.join(' '),\n ]\n .filter(Boolean)\n .join(' ');\n\n return { sql, values };\n}\n","/**\n * DuckDB-specific schema transformation utilities\n *\n * DuckDB has a known limitation (issue #12684) where ON CONFLICT clauses\n * fail with UNIQUE INDEX but work with inline UNIQUE constraints.\n *\n * This module provides utilities to transform SMRT-generated schemas\n * (which use separate CREATE UNIQUE INDEX statements) into DuckDB-compatible\n * schemas (which use inline UNIQUE constraints).\n */\n\n/**\n * Index definition extracted from CREATE INDEX statement\n */\ninterface ParsedIndex {\n name: string;\n columns: string[];\n unique: boolean;\n sql: string;\n}\n\n/**\n * Parses a CREATE INDEX statement to extract index details\n *\n * @param indexSQL - CREATE INDEX statement\n * @returns Parsed index definition or null if parsing fails\n *\n * @example\n * ```typescript\n * const index = parseIndexStatement(\n * 'CREATE UNIQUE INDEX ds_slug_context_idx ON ds (slug, context)'\n * );\n * // Returns: {\n * // name: 'ds_slug_context_idx',\n * // columns: ['slug', 'context'],\n * // unique: true,\n * // sql: '...'\n * // }\n * ```\n */\nexport function parseIndexStatement(indexSQL: string): ParsedIndex | null {\n const trimmed = indexSQL.trim();\n\n // Match: CREATE [UNIQUE] INDEX [IF NOT EXISTS] name ON table (columns)\n // Support both quoted and unquoted identifiers: table, \"table\", columns, \"columns\"\n const match = trimmed.match(\n /CREATE\\s+(UNIQUE\\s+)?INDEX\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(\\w+)\\s+ON\\s+(?:\"?\\w+\"?)\\s*\\(([^)]+)\\)/i,\n );\n\n if (!match) {\n return null;\n }\n\n const [, uniqueKeyword, name, columnsStr] = match;\n\n // Remove quotes from column names\n const columns = columnsStr\n .split(',')\n .map((col) => col.trim().replace(/^\"|\"$/g, ''));\n\n return {\n name,\n columns,\n unique: Boolean(uniqueKeyword),\n sql: trimmed,\n };\n}\n\n/**\n * Converts UNIQUE INDEX statements to inline UNIQUE constraints for DuckDB compatibility\n *\n * DuckDB's ON CONFLICT clause requires inline UNIQUE constraints, not separate indexes.\n * This function transforms SMRT-generated schemas to work with DuckDB's limitations.\n *\n * @param ddl - CREATE TABLE DDL statement\n * @param indexes - Array of CREATE INDEX statements (optional)\n * @returns Transformed DDL with inline UNIQUE constraints and filtered index list\n *\n * @example\n * ```typescript\n * const ddl = `\n * CREATE TABLE ds (\n * id TEXT PRIMARY KEY,\n * slug TEXT NOT NULL,\n * context TEXT NOT NULL\n * )\n * `;\n * const indexes = [\n * 'CREATE UNIQUE INDEX ds_slug_context_idx ON ds (slug, context)'\n * ];\n *\n * const result = convertUniqueIndexesToInlineConstraints(ddl, indexes);\n * // result.ddl:\n * // CREATE TABLE ds (\n * // id TEXT PRIMARY KEY,\n * // slug TEXT NOT NULL,\n * // context TEXT NOT NULL,\n * // UNIQUE(slug, context)\n * // )\n * // result.indexes: [] (UNIQUE index removed, non-UNIQUE indexes preserved)\n * ```\n */\nexport function convertUniqueIndexesToInlineConstraints(\n ddl: string,\n indexes?: string[],\n): {\n ddl: string;\n indexes: string[];\n} {\n if (!indexes || indexes.length === 0) {\n return { ddl, indexes: [] };\n }\n\n // Parse all indexes\n const parsedIndexes = indexes\n .map(parseIndexStatement)\n .filter((idx): idx is ParsedIndex => idx !== null);\n\n // Separate UNIQUE indexes from regular indexes\n const uniqueIndexes = parsedIndexes.filter((idx) => idx.unique);\n const regularIndexes = parsedIndexes.filter((idx) => !idx.unique);\n\n // If no UNIQUE indexes, return unchanged\n if (uniqueIndexes.length === 0) {\n return { ddl, indexes };\n }\n\n // Find the closing parenthesis of the CREATE TABLE statement\n const lines = ddl.split('\\n');\n let closingParenIndex = -1;\n\n for (let i = lines.length - 1; i >= 0; i--) {\n const trimmed = lines[i].trim();\n if (trimmed === ')' || trimmed.startsWith(');')) {\n closingParenIndex = i;\n break;\n }\n }\n\n if (closingParenIndex === -1) {\n // Can't find closing paren - return unchanged\n console.warn(\n '[duckdb-schema-utils] Could not find closing parenthesis in CREATE TABLE statement',\n );\n return { ddl, indexes };\n }\n\n // Build inline UNIQUE constraints with quoted column names\n const uniqueConstraints = uniqueIndexes.map((idx) => {\n const columns = idx.columns.map((col) => `\"${col}\"`).join(', ');\n return ` UNIQUE(${columns})`;\n });\n\n // Insert UNIQUE constraints before closing parenthesis\n const modifiedLines = [...lines];\n const closingLine = modifiedLines[closingParenIndex].trim();\n\n // Add comma to previous line if it doesn't have one\n if (closingParenIndex > 0) {\n const prevLine = modifiedLines[closingParenIndex - 1].trim();\n if (!prevLine.endsWith(',')) {\n modifiedLines[closingParenIndex - 1] = modifiedLines[\n closingParenIndex - 1\n ].replace(/\\s*$/, ',');\n }\n }\n\n // Insert UNIQUE constraints\n const constraintLines = uniqueConstraints.map((constraint, idx) => {\n // Add comma after all but the last constraint\n return idx < uniqueConstraints.length - 1 ? `${constraint},` : constraint;\n });\n\n modifiedLines.splice(closingParenIndex, 0, ...constraintLines);\n\n const transformedDDL = modifiedLines.join('\\n');\n\n // Return regular (non-UNIQUE) indexes only\n const remainingIndexes = regularIndexes.map((idx) => idx.sql);\n\n return {\n ddl: transformedDDL,\n indexes: remainingIndexes,\n };\n}\n\n/**\n * Checks if a schema definition contains UNIQUE indexes that need conversion\n *\n * @param indexes - Array of CREATE INDEX statements\n * @returns True if any UNIQUE indexes are present\n */\nexport function hasUniqueIndexes(indexes?: string[]): boolean {\n if (!indexes || indexes.length === 0) {\n return false;\n }\n\n return indexes.some((indexSQL) => {\n const parsed = parseIndexStatement(indexSQL);\n return parsed?.unique === true;\n });\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 name contains the exact text '100% cotton'\n * const where = {\n * status: 'active',\n * 'name contains': '100% cotton',\n * };\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, JSON and PGlite. 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, JSON and PGlite. 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 * When automatic file registration is enabled, JSON adapters detect changed,\n * added, or removed `.json` and companion `.schema.sql` files on a later\n * acquisition. Use this option when a caller needs an unconditional reload\n * even if those sources are unchanged. Existing references point to the\n * closed adapter after replacement.\n *\n * @default false\n *\n * @example\n * // Initial connection - cached\n * const db1 = await getDatabase({ type: 'json', url: './data' });\n *\n * // Force an unconditional replacement...\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, JSON and PGlite drive one\n * connection, and 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","/**\n * Browser-safe entry for the driver-independent query helpers.\n *\n * Everything here is pure SQL-fragment construction or operates on an\n * already-open `DatabaseInterface`. Nothing on this import graph reaches `pg`,\n * the libsql or DuckDB adapters, or any `node:` built-in, so browser-bundled\n * packages can import it instead of the package root. The root entry\n * re-exports all of it.\n *\n * @example\n * ```typescript\n * import { buildWhere, validateColumnName } from '@happyvertical/sql/query';\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { DatabaseInterface } from './shared/types.js';\n\n/**\n * Checks if a table exists in the database\n *\n * @param db - Database interface to use\n * @param tableName - Name of the table to check\n * @returns Promise resolving to boolean indicating if the table exists\n */\nexport async function tableExists(db: DatabaseInterface, tableName: string) {\n return db.tableExists(tableName);\n}\n\n/**\n * Escapes and formats a value for use in SQL queries\n *\n * @param value - Value to escape\n * @returns String representation of the value safe for SQL use\n */\nexport function escapeSqlValue(value: any): string {\n if (value === null) {\n return 'NULL';\n }\n if (value instanceof Date) {\n return `'${value.toISOString()}'`;\n }\n if (typeof value === 'number') {\n return value.toString();\n }\n if (typeof value === 'boolean') {\n return value ? '1' : '0';\n }\n // Escape single quotes and wrap in quotes\n return `'${String(value).replace(/'/g, \"''\")}'`;\n}\n\n/**\n * Validates a column name for use in SQL queries\n *\n * @param column - Column name to validate\n * @returns The validated column name\n * @throws Error if the column name contains invalid characters\n */\nexport function validateColumnName(column: string): string {\n // Reject non-strings before the regex. `test` coerces via `toString`, so an\n // object whose `toString` differs between this read and a later interpolation\n // read would pass validation and then reach SQL as a different identifier.\n // Only allow alphanumeric characters, underscores, and dots (for table.column notation)\n if (typeof column !== 'string' || !/^[a-zA-Z0-9_.]+$/.test(column)) {\n throw new Error(`Invalid column name: ${column}`);\n }\n return column;\n}\n\n/**\n * Synchronizes a SQL schema definition with a database\n * Creates tables if they don't exist and adds missing columns to existing tables\n *\n * @param options - Object containing database and schema\n * @param options.db - Database interface to use\n * @param options.schema - SQL schema definition\n * @throws Error if db or schema are missing or if the database doesn't support syncSchema\n */\nexport async function syncSchema(options: {\n db: DatabaseInterface;\n schema: string;\n}) {\n const { db, schema } = options;\n if (!db || !schema) {\n throw new Error('db and schema are required');\n }\n\n // Delegate to the database adapter's syncSchema implementation\n if (db.syncSchema) {\n await db.syncSchema(schema);\n } else {\n throw new Error('Database adapter does not support schema synchronization');\n }\n}\n\nexport {\n type AggregateBuildResult,\n type AggregateFunction,\n type AggregateSelectExpr,\n type AggregateSpec,\n type AggregateTimeBucketUnit,\n bucketExpr,\n buildAggregate,\n} from './aggregate.js';\nexport type { SchemaInitializationResult } from './schema-manager.js';\nexport { DatabaseSchemaManager } from './schema-manager.js';\nexport { convertUniqueIndexesToInlineConstraints } from './shared/duckdb-schema-utils.js';\nexport * from './shared/types.js';\nexport type { RawSqlKey, SqlAdapterType } from './shared/utils.js';\nexport { buildWhere, formatDbError, raw } from './shared/utils.js';\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { o as resolveSchemas } from "./query-B6ATOWzc.js";
|
|
1
2
|
import { c as wrapDatabaseError, n as buildWhere, o as resolveInsertColumns, r as formatDbError, t as DatabaseSchemaManager } from "./schema-manager-tfASrb_b.js";
|
|
2
|
-
import { o as resolveSchemas } from "./query-DubG7aF0.js";
|
|
3
3
|
import { _ as validateColumnNames, c as createTransactionLock, d as validateDatabaseCacheOptions, g as validateColumnName, h as generateCreateIndexStatement, m as generateAddColumnStatement, v as validateIndexName, y as validateTableName } from "./postgres-dialect-DIy5f5YR.js";
|
|
4
4
|
import { t as redactDatabaseUrl } from "./redact-database-url-CfMDvSdy.js";
|
|
5
5
|
import { t as getCachedSqliteDatabase } from "./sqlite-connection-cache-C_7rVisQ.js";
|
|
@@ -1267,7 +1267,7 @@ async function getDatabase(options = {}) {
|
|
|
1267
1267
|
root: secureOptions.root
|
|
1268
1268
|
})));
|
|
1269
1269
|
}
|
|
1270
|
-
if (hasNativeSqliteCapabilities(options)) return (await import("./sqlite-native-
|
|
1270
|
+
if (hasNativeSqliteCapabilities(options)) return (await import("./sqlite-native-ytHS5F4a.js")).getNativeSqliteDatabase(options);
|
|
1271
1271
|
return getCachedSqliteDatabase("libsql", options, () => createDatabase(options, url));
|
|
1272
1272
|
}
|
|
1273
1273
|
async function createDatabase(options, url, clientFactory = () => createLibSQLClient(options)) {
|
|
@@ -2661,4 +2661,4 @@ async function createDatabase(options, url, clientFactory = () => createLibSQLCl
|
|
|
2661
2661
|
//#endregion
|
|
2662
2662
|
export { getDatabase };
|
|
2663
2663
|
|
|
2664
|
-
//# sourceMappingURL=sqlite-
|
|
2664
|
+
//# sourceMappingURL=sqlite-DxgBiHFg.js.map
|