@happyvertical/sql 0.80.6 → 0.81.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +49 -0
  2. package/dist/chunks/{schema-manager-a8RlxIE2.js → alter-utils-B900pfK_.js} +246 -2
  3. package/dist/chunks/alter-utils-B900pfK_.js.map +1 -0
  4. package/dist/chunks/{duckdb-D_Wu36JL.js → duckdb-CWy7Nziq.js} +45 -21
  5. package/dist/chunks/duckdb-CWy7Nziq.js.map +1 -0
  6. package/dist/chunks/{sqlite-B4ussRTS.js → sqlite-kBmA-x_C.js} +68 -29
  7. package/dist/chunks/sqlite-kBmA-x_C.js.map +1 -0
  8. package/dist/chunks/{sqlite-native-BgiF4301.js → sqlite-native-Dnlo3Tux.js} +73 -21
  9. package/dist/chunks/sqlite-native-Dnlo3Tux.js.map +1 -0
  10. package/dist/chunks/types-uVFHgaSU.js +133 -0
  11. package/dist/chunks/types-uVFHgaSU.js.map +1 -0
  12. package/dist/duckdb.d.ts.map +1 -1
  13. package/dist/index.js +715 -733
  14. package/dist/index.js.map +1 -1
  15. package/dist/json.d.ts.map +1 -1
  16. package/dist/postgres.d.ts.map +1 -1
  17. package/dist/shared/alter-utils.d.ts +48 -0
  18. package/dist/shared/alter-utils.d.ts.map +1 -1
  19. package/dist/shared/transaction-lock.d.ts +54 -0
  20. package/dist/shared/transaction-lock.d.ts.map +1 -0
  21. package/dist/shared/types.d.ts +69 -7
  22. package/dist/shared/types.d.ts.map +1 -1
  23. package/dist/shared/utils.d.ts +31 -0
  24. package/dist/shared/utils.d.ts.map +1 -1
  25. package/dist/sqlite-native.d.ts.map +1 -1
  26. package/dist/sqlite.d.ts +20 -0
  27. package/dist/sqlite.d.ts.map +1 -1
  28. package/package.json +5 -5
  29. package/dist/chunks/alter-utils-K7tiuj5L.js +0 -135
  30. package/dist/chunks/alter-utils-K7tiuj5L.js.map +0 -1
  31. package/dist/chunks/duckdb-D_Wu36JL.js.map +0 -1
  32. package/dist/chunks/schema-manager-a8RlxIE2.js.map +0 -1
  33. package/dist/chunks/sqlite-B4ussRTS.js.map +0 -1
  34. package/dist/chunks/sqlite-native-BgiF4301.js.map +0 -1
package/README.md CHANGED
@@ -111,6 +111,14 @@ await postsTable.insert({ id: 'p4', title: 'Scoped' });
111
111
  const p = await postsTable.get({ id: 'p4' });
112
112
  ```
113
113
 
114
+ A batch `insert()` writes one column list for every row, taken from the first
115
+ record, so every record in the batch must have the same keys. Key *order* does
116
+ not matter — each record is projected through the column list — but a record
117
+ with an extra or missing key is rejected with a `DatabaseError` rather than
118
+ silently dropping the extra or writing `NULL` for the missing one. Split records
119
+ of differing shapes into separate `insert()` calls, or fill the gaps with an
120
+ explicit `null`.
121
+
114
122
  `upsert()` treats `NULL` values in conflict columns as matching existing `NULL`
115
123
  values so nullable composite keys update the existing row instead of inserting a
116
124
  duplicate. Pass `{ nullsDistinct: true }` as the fourth argument to preserve the
@@ -136,6 +144,47 @@ try {
136
144
  }
137
145
  ```
138
146
 
147
+ On the single-connection adapters — SQLite (both the LibSQL and native paths),
148
+ DuckDB and JSON — a connection can only be in one transaction at a time, so
149
+ **transactions are serialized per connection**: an overlapping `transaction()`
150
+ waits for the one in progress instead of interleaving with it. A call that waits
151
+ longer than `transactionQueueTimeout` (30s by default) rejects rather than
152
+ stalling indefinitely.
153
+
154
+ ```typescript
155
+ const db = await getDatabase({
156
+ type: 'sqlite',
157
+ url: 'file:app.db',
158
+ transactionQueueTimeout: 60_000, // longer transactions, or heavier bursts
159
+ });
160
+ ```
161
+
162
+ Two consequences worth knowing:
163
+
164
+ - A `beginTransaction()` handle owns the connection until you commit or roll it
165
+ back. End it in a `finally` — a handle that is never ended holds the
166
+ connection for the life of the process, and every later transaction on it
167
+ fails with the queue timeout.
168
+ - Inside a `transaction()` callback, use the `tx` you were handed. Calling a
169
+ top-level `db.*` method that opens its own transaction makes it wait on the
170
+ connection its own caller is holding.
171
+
172
+ PostgreSQL pools its connections, so transactions there run concurrently and
173
+ never queue.
174
+
175
+ ### Identifiers
176
+
177
+ Table and column names are interpolated into SQL rather than bound as
178
+ parameters, so every CRUD method validates them: an identifier must match
179
+ `[a-zA-Z_][a-zA-Z0-9_]*`. Qualified names (`schema.table`), quoted names and
180
+ anything containing whitespace are rejected. Values are always parameterized and
181
+ are unaffected.
182
+
183
+ Note that `buildWhere` treats a condition key carrying an explicit operator
184
+ suffix (`'price >'`, `'name like'`) as SQL expression text and does **not**
185
+ validate it. That is deliberate, and it means those keys must stay
186
+ developer-controlled — never build them from request input.
187
+
139
188
  ### WHERE Clause Building
140
189
 
141
190
  ```typescript
@@ -1,5 +1,9 @@
1
+ import { DatabaseError } from "@happyvertical/utils";
1
2
  //#region src/shared/utils.ts
2
3
  /**
4
+ * Shared SQL utilities that work in both browser and Node.js environments
5
+ */
6
+ /**
3
7
  * Formats a database error to include all relevant details
4
8
  *
5
9
  * Database drivers often include additional properties beyond just `message`:
@@ -25,6 +29,58 @@ function formatDbError(error) {
25
29
  return parts.length > 0 ? parts.join(", ") : String(error);
26
30
  }
27
31
  /**
32
+ * Resolves the column list for a multi-row INSERT and rejects batches whose
33
+ * records disagree on it.
34
+ *
35
+ * A batch INSERT emits one column list for the whole statement, so it can only
36
+ * come from the first record. That leaves two ways for a later record to be
37
+ * written wrongly, both of them silent:
38
+ *
39
+ * - Its keys are the same but in a different insertion order. Callers that
40
+ * bound values with `Object.values(record)` bound them by position, so the
41
+ * values cross-assigned into each other's columns. Projecting each record
42
+ * through the returned column list — `keys.map((key) => record[key])` — is
43
+ * what makes key order irrelevant, and every caller must do it.
44
+ * - Its key set genuinely differs. An extra key has no column to go to and a
45
+ * missing one has no value, so the record cannot be expressed by this
46
+ * statement at all. Guessing (dropping the extra, writing NULL for the
47
+ * missing) is what makes the failure invisible, so this throws instead.
48
+ *
49
+ * A key whose value is `undefined` counts as absent. Adapters that serialize
50
+ * records first drop such keys before this point while the rest pass them
51
+ * through, so comparing raw key lists would make the same batch throw on some
52
+ * adapters and silently write NULL on others. Only the comparison ignores them;
53
+ * the returned column list is untouched, so each adapter keeps its own
54
+ * single-record handling of `undefined`.
55
+ *
56
+ * @param table - Table being inserted into, for error context
57
+ * @param records - Non-empty batch, as the adapter will bind it
58
+ * @returns Column list taken from the first record
59
+ * @throws DatabaseError if any later record's key set differs from the first's
60
+ */
61
+ function resolveInsertColumns(table, records) {
62
+ const definedKeys = (record) => new Set(Object.keys(record).filter((key) => record[key] !== void 0));
63
+ const keys = Object.keys(records[0]);
64
+ const expected = definedKeys(records[0]);
65
+ for (let index = 1; index < records.length; index++) {
66
+ const recordKeys = definedKeys(records[index]);
67
+ const missing = [...expected].filter((key) => !recordKeys.has(key));
68
+ const extra = [...recordKeys].filter((key) => !expected.has(key));
69
+ if (missing.length === 0 && extra.length === 0) continue;
70
+ const problems = [missing.length > 0 ? `missing ${missing.join(", ")}` : "", extra.length > 0 ? `unexpected ${extra.join(", ")}` : ""].filter(Boolean);
71
+ throw new DatabaseError(`Batch insert records must all have the same keys; record ${index} has ${problems.join(" and ")} relative to record 0 (${[...expected].join(", ")})`, {
72
+ table,
73
+ recordIndex: index,
74
+ expectedKeys: keys,
75
+ recordKeys: [...recordKeys],
76
+ missing,
77
+ extra,
78
+ hint: "A batch INSERT writes one column list for every row, taken from the first record, so record 0 defines the columns even when it is the one that looks wrong. A key whose value is undefined counts as absent. Split records with differing keys into separate insert() calls, or fill the gaps explicitly with null."
79
+ });
80
+ }
81
+ return keys;
82
+ }
83
+ /**
28
84
  * Map of valid SQL operators for use in WHERE clauses
29
85
  */
30
86
  var VALID_OPERATORS = {
@@ -445,6 +501,194 @@ var DatabaseSchemaManager = class DatabaseSchemaManager {
445
501
  }
446
502
  };
447
503
  //#endregion
448
- export { parseConditionKey as i, buildWhere as n, formatDbError as r, DatabaseSchemaManager as t };
504
+ //#region src/shared/alter-utils.ts
505
+ /**
506
+ * Generates an ALTER TABLE ADD COLUMN statement
507
+ *
508
+ * @param table - Table name
509
+ * @param column - Column definition with name
510
+ * @param dialect - Database dialect
511
+ * @returns SQL statement string
512
+ *
513
+ * @example
514
+ * ```typescript
515
+ * const sql = generateAddColumnStatement('users', {
516
+ * name: 'email',
517
+ * type: 'TEXT',
518
+ * unique: true,
519
+ * notNull: false
520
+ * }, 'sqlite');
521
+ * // Returns: "ALTER TABLE users ADD COLUMN email TEXT UNIQUE"
522
+ * ```
523
+ */
524
+ function generateAddColumnStatement(table, column, _dialect) {
525
+ const parts = [column.name, column.type];
526
+ if (column.primaryKey) parts.push("PRIMARY KEY");
527
+ if (column.notNull) parts.push("NOT NULL");
528
+ if (column.unique) parts.push("UNIQUE");
529
+ if (column.defaultValue !== void 0) {
530
+ const defaultClause = generateDefaultClause(column.type, column.defaultValue);
531
+ parts.push(`DEFAULT ${defaultClause}`);
532
+ }
533
+ if (column.check) parts.push(`CHECK (${column.check})`);
534
+ if (column.foreignKey) {
535
+ const fk = column.foreignKey;
536
+ const fkParts = [`REFERENCES ${fk.table}(${fk.column})`];
537
+ if (fk.onDelete) fkParts.push(`ON DELETE ${fk.onDelete}`);
538
+ if (fk.onUpdate) fkParts.push(`ON UPDATE ${fk.onUpdate}`);
539
+ parts.push(fkParts.join(" "));
540
+ }
541
+ return `ALTER TABLE ${table} ADD COLUMN ${parts.join(" ")}`;
542
+ }
543
+ /**
544
+ * Generates a DEFAULT clause for a column definition
545
+ *
546
+ * Handles proper formatting and escaping of default values based on column type.
547
+ *
548
+ * @param columnType - SQL column type (TEXT, INTEGER, BOOLEAN, etc.)
549
+ * @param defaultValue - Default value to format
550
+ * @returns Formatted default value string
551
+ *
552
+ * @example
553
+ * ```typescript
554
+ * generateDefaultClause('TEXT', 'hello'); // Returns: "'hello'"
555
+ * generateDefaultClause('INTEGER', 42); // Returns: "42"
556
+ * generateDefaultClause('BOOLEAN', true); // Returns: "true"
557
+ * generateDefaultClause('TEXT', null); // Returns: "NULL"
558
+ * ```
559
+ */
560
+ function generateDefaultClause(columnType, defaultValue) {
561
+ if (defaultValue === null) return "NULL";
562
+ const typeUpper = columnType.toUpperCase();
563
+ if (typeUpper.includes("TEXT") || typeUpper.includes("CHAR") || typeUpper.includes("VARCHAR") || typeUpper.includes("STRING")) {
564
+ if (defaultValue === "") return "''";
565
+ return `'${String(defaultValue).replace(/'/g, "''")}'`;
566
+ }
567
+ if (typeUpper.includes("BOOL")) return defaultValue ? "true" : "false";
568
+ if (typeUpper.includes("INT") || typeUpper.includes("REAL") || typeUpper.includes("FLOAT") || typeUpper.includes("DOUBLE") || typeUpper.includes("NUMERIC") || typeUpper.includes("DECIMAL")) return String(defaultValue);
569
+ if (typeUpper.includes("JSON")) {
570
+ if (typeof defaultValue === "string") return `'${defaultValue.replace(/'/g, "''")}'`;
571
+ return `'${JSON.stringify(defaultValue).replace(/'/g, "''")}'`;
572
+ }
573
+ if (typeUpper.includes("DATE") || typeUpper.includes("TIME") || typeUpper.includes("TIMESTAMP")) {
574
+ if (defaultValue instanceof Date) return `'${defaultValue.toISOString()}'`;
575
+ return `'${String(defaultValue).replace(/'/g, "''")}'`;
576
+ }
577
+ if (typeof defaultValue === "string") return `'${defaultValue.replace(/'/g, "''")}'`;
578
+ return String(defaultValue);
579
+ }
580
+ /**
581
+ * Generates a CREATE INDEX statement
582
+ *
583
+ * @param table - Table name
584
+ * @param index - Index definition
585
+ * @returns SQL CREATE INDEX statement
586
+ *
587
+ * @example
588
+ * ```typescript
589
+ * const sql = generateCreateIndexStatement('users', {
590
+ * name: 'idx_users_email',
591
+ * columns: ['email'],
592
+ * unique: true
593
+ * });
594
+ * // Returns: "CREATE UNIQUE INDEX idx_users_email ON users (email)"
595
+ * ```
596
+ */
597
+ function generateCreateIndexStatement(table, index) {
598
+ const parts = ["CREATE"];
599
+ if (index.unique) parts.push("UNIQUE");
600
+ parts.push("INDEX");
601
+ parts.push(index.name);
602
+ parts.push(`ON ${table}`);
603
+ const columns = index.columns.join(", ");
604
+ parts.push(`(${columns})`);
605
+ if (index.where) parts.push(`WHERE ${index.where}`);
606
+ return parts.join(" ");
607
+ }
608
+ /**
609
+ * Validates a table name to prevent SQL injection
610
+ *
611
+ * @param tableName - Table name to validate
612
+ * @throws Error if table name contains invalid characters
613
+ */
614
+ function validateTableName(tableName) {
615
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) throw new Error(`Invalid table name: ${tableName}. Table names must start with a letter or underscore and contain only alphanumeric characters and underscores.`);
616
+ }
617
+ /**
618
+ * Validates a column name to prevent SQL injection
619
+ *
620
+ * @param columnName - Column name to validate
621
+ * @throws Error if column name contains invalid characters
622
+ */
623
+ function validateColumnName(columnName) {
624
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(columnName)) throw new Error(`Invalid column name: ${columnName}. Column names must start with a letter or underscore and contain only alphanumeric characters and underscores.`);
625
+ }
626
+ /**
627
+ * Validates every identifier in a column list to prevent SQL injection
628
+ *
629
+ * Column names reaching the CRUD methods come from `Object.keys(data)` and the
630
+ * caller's `conflictColumns`, both of which are interpolated into SQL. They are
631
+ * checked rather than quoted deliberately: the accepted shape has no character
632
+ * that can end an identifier, so quoting adds nothing to safety, and quoting a
633
+ * name on PostgreSQL makes it case-sensitive, which would silently change which
634
+ * column an existing caller addresses.
635
+ *
636
+ * @param columnNames - Column names to validate
637
+ * @throws Error if any column name contains invalid characters
638
+ */
639
+ function validateColumnNames(columnNames) {
640
+ for (const columnName of columnNames) validateColumnName(columnName);
641
+ }
642
+ /**
643
+ * Escapes an identifier for interpolation inside double quotes
644
+ *
645
+ * For the positions that already quote the identifier — the DuckDB and JSON
646
+ * upsert statements, which quote to match their schema generator — quoting is
647
+ * what makes the identifier safe, so the only thing that has to be handled is
648
+ * a quote character inside the name. Doubling it is the SQL escape.
649
+ *
650
+ * This is deliberately more permissive than {@link validateColumnName}: those
651
+ * adapters accept column names that are not plain identifiers (`Full Name`,
652
+ * `user-id`, `2024_total`), which matters most for the JSON adapter, whose
653
+ * columns come straight from the keys of whatever JSON it was pointed at.
654
+ * Rejecting them would break working code to fix an injection that quoting
655
+ * already prevents.
656
+ *
657
+ * @param identifier - Identifier to place inside double quotes
658
+ * @returns The identifier with embedded quotes doubled
659
+ * @throws Error if the identifier is empty or contains a NUL byte
660
+ */
661
+ function escapeQuotedIdentifier(identifier) {
662
+ if (identifier.length === 0 || identifier.includes("\0")) throw new Error(`Invalid column name: ${JSON.stringify(identifier)}. Column names must be non-empty and must not contain a NUL byte.`);
663
+ return identifier.replaceAll("\"", "\"\"");
664
+ }
665
+ /**
666
+ * Escapes a value for interpolation inside a single-quoted SQL string literal
667
+ *
668
+ * The DuckDB-backed adapters read JSON through `read_json_auto('<path>')`, and
669
+ * a file path is not something an identifier validator can check — paths
670
+ * legitimately contain spaces, dots and, on Linux and macOS, quotes. Doubling
671
+ * the quote is the SQL escape, and it is what keeps a file named `x'.json`
672
+ * from ending the literal and starting a statement.
673
+ *
674
+ * @param value - Value to place inside single quotes
675
+ * @returns The value with embedded single quotes doubled
676
+ * @throws Error if the value contains a NUL byte
677
+ */
678
+ function escapeStringLiteral(value) {
679
+ if (value.includes("\0")) throw new Error(`Invalid SQL string literal: ${JSON.stringify(value)}. Values must not contain a NUL byte.`);
680
+ return value.replaceAll("'", "''");
681
+ }
682
+ /**
683
+ * Validates an index name to prevent SQL injection
684
+ *
685
+ * @param indexName - Index name to validate
686
+ * @throws Error if index name contains invalid characters
687
+ */
688
+ function validateIndexName(indexName) {
689
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(indexName)) throw new Error(`Invalid index name: ${indexName}. Index names must start with a letter or underscore and contain only alphanumeric characters and underscores.`);
690
+ }
691
+ //#endregion
692
+ export { validateColumnName as a, validateTableName as c, formatDbError as d, parseConditionKey as f, generateCreateIndexStatement as i, DatabaseSchemaManager as l, escapeStringLiteral as n, validateColumnNames as o, resolveInsertColumns as p, generateAddColumnStatement as r, validateIndexName as s, escapeQuotedIdentifier as t, buildWhere as u };
449
693
 
450
- //# sourceMappingURL=schema-manager-a8RlxIE2.js.map
694
+ //# sourceMappingURL=alter-utils-B900pfK_.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alter-utils-B900pfK_.js","names":[],"sources":["../../src/shared/utils.ts","../../src/schema-manager.ts","../../src/shared/alter-utils.ts"],"sourcesContent":["/**\n * Shared SQL utilities that work in both browser and Node.js environments\n */\n\nimport { DatabaseError } from '@happyvertical/utils';\nimport type { WhereClause } from './types.js';\n\n/**\n * Formats a database error to include all relevant details\n *\n * Database drivers often include additional properties beyond just `message`:\n * - PostgreSQL: code, detail, hint, severity\n * - DuckDB: may include additional context\n * - SQLite/LibSQL: may include errno, code\n *\n * This function extracts all available error properties to provide\n * better debugging information.\n *\n * @param error - The caught error object\n * @returns Formatted error string with all available error details\n */\nexport function formatDbError(error: unknown): string {\n const dbError = error as {\n message?: string;\n code?: string;\n detail?: string;\n hint?: string;\n severity?: string;\n errno?: number;\n cause?: unknown;\n };\n\n const parts: string[] = [];\n\n if (dbError.message) parts.push(dbError.message);\n if (dbError.code) parts.push(`code=${dbError.code}`);\n if (dbError.detail) parts.push(`detail=${dbError.detail}`);\n if (dbError.hint) parts.push(`hint=${dbError.hint}`);\n if (dbError.severity) parts.push(`severity=${dbError.severity}`);\n if (dbError.errno !== undefined) parts.push(`errno=${dbError.errno}`);\n\n return parts.length > 0 ? parts.join(', ') : String(error);\n}\n\n/**\n * Resolves the column list for a multi-row INSERT and rejects batches whose\n * records disagree on it.\n *\n * A batch INSERT emits one column list for the whole statement, so it can only\n * come from the first record. That leaves two ways for a later record to be\n * written wrongly, both of them silent:\n *\n * - Its keys are the same but in a different insertion order. Callers that\n * bound values with `Object.values(record)` bound them by position, so the\n * values cross-assigned into each other's columns. Projecting each record\n * through the returned column list — `keys.map((key) => record[key])` — is\n * what makes key order irrelevant, and every caller must do it.\n * - Its key set genuinely differs. An extra key has no column to go to and a\n * missing one has no value, so the record cannot be expressed by this\n * statement at all. Guessing (dropping the extra, writing NULL for the\n * missing) is what makes the failure invisible, so this throws instead.\n *\n * A key whose value is `undefined` counts as absent. Adapters that serialize\n * records first drop such keys before this point while the rest pass them\n * through, so comparing raw key lists would make the same batch throw on some\n * adapters and silently write NULL on others. Only the comparison ignores them;\n * the returned column list is untouched, so each adapter keeps its own\n * single-record handling of `undefined`.\n *\n * @param table - Table being inserted into, for error context\n * @param records - Non-empty batch, as the adapter will bind it\n * @returns Column list taken from the first record\n * @throws DatabaseError if any later record's key set differs from the first's\n */\nexport function resolveInsertColumns(\n table: string,\n records: Record<string, any>[],\n): string[] {\n const definedKeys = (record: Record<string, any>) =>\n new Set(Object.keys(record).filter((key) => record[key] !== undefined));\n\n const keys = Object.keys(records[0]);\n const expected = definedKeys(records[0]);\n\n for (let index = 1; index < records.length; index++) {\n const recordKeys = definedKeys(records[index]);\n const missing = [...expected].filter((key) => !recordKeys.has(key));\n const extra = [...recordKeys].filter((key) => !expected.has(key));\n\n if (missing.length === 0 && extra.length === 0) {\n continue;\n }\n\n const problems = [\n missing.length > 0 ? `missing ${missing.join(', ')}` : '',\n extra.length > 0 ? `unexpected ${extra.join(', ')}` : '',\n ].filter(Boolean);\n\n throw new DatabaseError(\n `Batch insert records must all have the same keys; record ${index} has ${problems.join(\n ' and ',\n )} relative to record 0 (${[...expected].join(', ')})`,\n {\n table,\n recordIndex: index,\n expectedKeys: keys,\n recordKeys: [...recordKeys],\n missing,\n extra,\n hint: 'A batch INSERT writes one column list for every row, taken from the first record, so record 0 defines the columns even when it is the one that looks wrong. A key whose value is undefined counts as absent. Split records with differing keys into separate insert() calls, or fill the gaps explicitly with null.',\n },\n );\n }\n\n return keys;\n}\n\n/**\n * Map of valid SQL operators for use in WHERE clauses\n */\nconst VALID_OPERATORS = {\n '=': '=',\n '>': '>',\n '>=': '>=',\n '<': '<',\n '<=': '<=',\n '!=': '!=',\n like: 'LIKE',\n in: 'IN',\n 'not in': 'NOT IN',\n} as const;\n\nfunction isSimpleSqlIdentifier(field: string): boolean {\n return /^[a-zA-Z0-9_.]+$/.test(field);\n}\n\nexport function parseConditionKey(fullKey: string): {\n field: string;\n operator: string;\n explicitOperator: boolean;\n} {\n const trimmed = fullKey.trim();\n const lower = trimmed.toLowerCase();\n const operators = ['not in', 'like', 'in', '!=', '>=', '<=', '>', '<', '='];\n\n for (const operator of operators) {\n if (lower.endsWith(` ${operator}`)) {\n return {\n field: trimmed.slice(0, -(operator.length + 1)).trim(),\n operator,\n explicitOperator: true,\n };\n }\n }\n\n return { field: trimmed, operator: '=', explicitOperator: false };\n}\n\n/**\n * SQL adapter type for adapter-specific query generation\n */\nexport type SqlAdapterType = 'sqlite' | 'postgres' | 'duckdb' | 'json';\n\n/**\n * Builds a single condition for a WHERE clause\n * @internal\n */\nconst buildCondition = (\n fullKey: string,\n value: any,\n currIndex: { value: number },\n adapterType?: SqlAdapterType,\n): { sql: string; values: any[] } => {\n const { field, operator, explicitOperator } = parseConditionKey(fullKey);\n if (!explicitOperator && !isSimpleSqlIdentifier(field)) {\n throw new Error(`Invalid SQL identifier: ${field}`);\n }\n const sqlOperator =\n VALID_OPERATORS[operator as keyof typeof VALID_OPERATORS] || '=';\n const values: any[] = [];\n\n let sql: string;\n\n if (value === null) {\n sql = `${field} IS ${sqlOperator === '=' ? 'NULL' : 'NOT NULL'}`;\n } else if (\n (sqlOperator === 'IN' || sqlOperator === 'NOT IN') &&\n Array.isArray(value)\n ) {\n if (value.length === 0) {\n throw new Error(`${sqlOperator} requires at least one value`);\n }\n const placeholders = value.map(() => `$${currIndex.value++}`).join(', ');\n sql = `${field} ${sqlOperator} (${placeholders})`;\n values.push(...value);\n } else if (value instanceof Date) {\n // DuckDB/JSON need CAST to TIMESTAMP to prevent ANY type inference (issue #540)\n // SQLite/PostgreSQL handle ISO strings natively without CAST\n const needsCast = adapterType === 'duckdb' || adapterType === 'json';\n if (needsCast) {\n sql = `${field} ${sqlOperator} CAST($${currIndex.value++} AS TIMESTAMP)`;\n } else {\n sql = `${field} ${sqlOperator} $${currIndex.value++}`;\n }\n values.push(value.toISOString());\n } else {\n sql = `${field} ${sqlOperator} $${currIndex.value++}`;\n values.push(value);\n }\n\n return { sql, values };\n};\n\n/**\n * Builds a SQL WHERE clause with parameterized values and flexible operators\n *\n * @param where - Conditions as object (AND-only) or 2D array (OR/AND compound)\n * @param startIndex - Starting index for parameter numbering (default: 1)\n * @param adapterType - Database adapter type. Controls Date handling:\n * - `'duckdb'` / `'json'`: Wraps Date values in `CAST($N AS TIMESTAMP)` to\n * prevent DuckDB ANY-type inference issues.\n * - `'sqlite'` / `'postgres'` / `undefined`: Passes ISO strings directly\n * (these adapters handle ISO timestamp strings natively).\n *\n * Keys with explicit operator suffixes are treated as SQL field/expression\n * text. Keep those keys developer-controlled; do not pass end-user input as a\n * condition key.\n *\n * @returns Object containing the SQL clause and array of values\n *\n * @example Basic Usage (Object format - AND-only):\n * ```typescript\n * buildWhere({\n * 'status': 'active', // equals operator is default\n * 'price >': 100, // greater than\n * 'stock <=': 5, // less than or equal\n * 'category in': ['A', 'B'], // IN clause for arrays\n * 'name like': '%shirt%' // LIKE for pattern matching\n * });\n * ```\n *\n * @example 2D Array Format (OR/AND compound logic):\n * ```typescript\n * // WHERE (status = 'active' AND price > 100) OR (status = 'pending' AND priority = 'high')\n * buildWhere([\n * [{ status: 'active' }, { 'price >': 100 }],\n * [{ status: 'pending' }, { priority: 'high' }]\n * ]);\n * ```\n *\n * @example NULL Handling:\n * ```typescript\n * buildWhere({\n * 'deleted_at': null, // becomes \"deleted_at IS NULL\"\n * 'updated_at !=': null, // becomes \"updated_at IS NOT NULL\"\n * 'status': 'active' // regular comparison\n * });\n * ```\n *\n * @example Common Patterns:\n * ```typescript\n * // Price range\n * buildWhere({\n * 'price >=': 10,\n * 'price <': 100\n * });\n *\n * // Date filtering\n * buildWhere({\n * 'created_at >': startDate,\n * 'created_at <=': endDate,\n * 'deleted_at': null\n * });\n *\n * // Search with LIKE\n * buildWhere({\n * 'title like': '%search%',\n * 'description like': '%search%',\n * 'status': 'published'\n * });\n *\n * // Multiple values with IN\n * buildWhere({\n * 'role in': ['admin', 'editor'],\n * 'active': true,\n * 'last_login !=': null\n * });\n * ```\n *\n * The function handles:\n * - Standard comparisons (=, >, >=, <, <=, !=)\n * - NULL checks (IS NULL, IS NOT NULL)\n * - IN clauses for arrays\n * - LIKE for pattern matching\n * - Object format: Multiple conditions combined with AND\n * - 2D array format: Inner arrays ANDed, outer array ORed\n */\nexport const buildWhere = (\n where: WhereClause,\n startIndex = 1,\n adapterType?: SqlAdapterType,\n) => {\n let sql = '';\n const values: any[] = [];\n const currIndex = { value: startIndex };\n\n // Handle 2D array format for OR/AND compound logic\n if (Array.isArray(where)) {\n if (where.length === 0) {\n return { sql: '', values: [] };\n }\n\n const orGroups: string[] = [];\n\n for (const andGroup of where) {\n if (!Array.isArray(andGroup) || andGroup.length === 0) {\n continue;\n }\n\n const andConditions: string[] = [];\n\n for (const conditionObj of andGroup) {\n for (const [fullKey, value] of Object.entries(conditionObj)) {\n const result = buildCondition(fullKey, value, currIndex, adapterType);\n andConditions.push(result.sql);\n values.push(...result.values);\n }\n }\n\n if (andConditions.length > 0) {\n // Wrap AND group in parentheses\n orGroups.push(`(${andConditions.join(' AND ')})`);\n }\n }\n\n if (orGroups.length > 0) {\n sql = `WHERE ${orGroups.join(' OR ')}`;\n }\n\n return { sql, values };\n }\n\n // Handle object format (original behavior - AND-only)\n if (where && Object.keys(where).length > 0) {\n sql = 'WHERE ';\n for (const [fullKey, value] of Object.entries(where)) {\n const result = buildCondition(fullKey, value, currIndex, adapterType);\n\n if (sql !== 'WHERE ') {\n sql += ' AND ';\n }\n\n sql += result.sql;\n values.push(...result.values);\n }\n }\n\n return { sql, values };\n};\n","/**\n * Schema management and dependency resolution for JSON manifests\n * Moved from SMRT package to SQL package for proper separation of concerns\n */\n\nimport type {\n DatabaseInterface,\n IndexDefinition,\n SchemaDefinition,\n SchemaInitializationOptions,\n TriggerDefinition,\n} from './shared/types.js';\n\n/**\n * Result returned by DatabaseSchemaManager.initializeSchemas()\n *\n * Contains the list of schemas that were successfully initialized,\n * any that were skipped (already up to date), errors encountered,\n * and the total execution time in milliseconds.\n */\nexport interface SchemaInitializationResult {\n initialized: string[];\n skipped: string[];\n errors: Array<{ schema: string; error: string }>;\n executionTime: number;\n}\n\n/**\n * Manages schema initialization from JSON manifests with dependency resolution\n *\n * Handles topological sorting of schema dependencies, table creation,\n * index and trigger setup, and version tracking to avoid redundant re-initialization.\n */\nexport class DatabaseSchemaManager {\n private static initializationLock = new Map<string, Promise<void>>();\n private initializedSchemas = new Set<string>();\n private schemaVersions = new Map<string, string>();\n\n /**\n * Initializes database schemas from a JSON manifest with dependency resolution\n *\n * Schemas are topologically sorted by their declared dependencies and\n * initialized in order. Supports legacy raw-SQL schemas via the `schema` option,\n * JSON manifests via the `manifest` option, and schema overrides for extending\n * base schemas.\n *\n * @param db - Database interface to execute DDL against\n * @param options - Schema initialization options (manifest, overrides, force, debug)\n * @returns Result with lists of initialized/skipped schemas, errors, and execution time\n */\n async initializeSchemas(\n db: DatabaseInterface,\n options: SchemaInitializationOptions,\n ): Promise<SchemaInitializationResult> {\n const startTime = Date.now();\n const result: SchemaInitializationResult = {\n initialized: [],\n skipped: [],\n errors: [],\n executionTime: 0,\n };\n\n try {\n let schemas: Record<string, SchemaDefinition> = {};\n\n // Handle legacy SQL schema format\n if (options.schema) {\n console.log(\n '[schema] Legacy SQL schema provided, converting to manifest format',\n );\n // For legacy support, we'll still use the original syncSchema method\n if (db.syncSchema) {\n await db.syncSchema(options.schema);\n result.initialized.push('legacy-sql');\n }\n result.executionTime = Date.now() - startTime;\n return result;\n }\n\n // Handle JSON manifest format\n if (options.manifest) {\n schemas = { ...options.manifest.schemas };\n }\n\n // Apply schema overrides\n if (options.overrides) {\n schemas = { ...schemas, ...options.overrides };\n }\n\n if (Object.keys(schemas).length === 0) {\n console.warn('[schema] No schemas provided for initialization');\n result.executionTime = Date.now() - startTime;\n return result;\n }\n\n // Build dependency graph\n const dependencyGraph = this.buildDependencyGraph(schemas);\n\n // Get initialization order\n const initializationOrder = this.resolveDependencies(dependencyGraph);\n\n if (options.debug) {\n console.log('[schema] Initialization order:', initializationOrder);\n }\n\n // Initialize schemas in dependency order\n for (const schemaName of initializationOrder) {\n const schema = schemas[schemaName];\n if (!schema) continue;\n\n try {\n await this.initializeSchema(db, schemaName, schema, {\n force: options.force,\n debug: options.debug,\n });\n result.initialized.push(schemaName);\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n result.errors.push({ schema: schemaName, error: errorMessage });\n\n if (options.debug) {\n console.error(\n `[schema] Failed to initialize ${schemaName}:`,\n error,\n );\n }\n }\n }\n\n result.executionTime = Date.now() - startTime;\n\n if (options.debug) {\n console.log('[schema] Initialization complete:', result);\n }\n\n return result;\n } catch (error) {\n result.executionTime = Date.now() - startTime;\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n result.errors.push({\n schema: 'dependency-resolution',\n error: errorMessage,\n });\n return result;\n }\n }\n\n /**\n * Initialize a single schema\n */\n private async initializeSchema(\n db: DatabaseInterface,\n schemaName: string,\n schema: SchemaDefinition,\n options: { force?: boolean; debug?: boolean },\n ): Promise<void> {\n const { tableName, version } = schema;\n const lockKey = `${schemaName}:${tableName}`;\n\n // Check if already being initialized\n if (DatabaseSchemaManager.initializationLock.has(lockKey)) {\n await DatabaseSchemaManager.initializationLock.get(lockKey);\n return;\n }\n\n // Check if already initialized and up to date\n if (!options.force && this.isSchemaUpToDate(schemaName, version)) {\n if (options.debug) {\n console.log(`[schema] Skipping ${schemaName} - already up to date`);\n }\n return;\n }\n\n // Create initialization promise\n const initPromise = this.performSchemaInitialization(\n db,\n schemaName,\n schema,\n options,\n );\n DatabaseSchemaManager.initializationLock.set(lockKey, initPromise);\n\n try {\n await initPromise;\n this.markSchemaInitialized(schemaName, version);\n } finally {\n DatabaseSchemaManager.initializationLock.delete(lockKey);\n }\n }\n\n /**\n * Perform the actual schema initialization\n */\n private async performSchemaInitialization(\n db: DatabaseInterface,\n schemaName: string,\n schema: SchemaDefinition,\n options: { force?: boolean; debug?: boolean },\n ): Promise<void> {\n const { tableName, columns, indexes, triggers } = schema;\n\n if (options.debug) {\n console.log(`[schema] Initializing ${schemaName} (${tableName})`);\n }\n\n // Check if table exists\n const tableExists = await db.tableExists(tableName);\n\n if (!tableExists) {\n // Create table\n await this.createTable(db, schema);\n\n // Create indexes\n for (const index of indexes) {\n await this.createIndex(db, tableName, index);\n }\n\n // Create triggers\n for (const trigger of triggers) {\n await this.createTrigger(db, trigger);\n }\n } else if (options.force) {\n // Recreate table if forced\n await db.query(`DROP TABLE IF EXISTS ${tableName}`);\n await this.createTable(db, schema);\n\n for (const index of indexes) {\n await this.createIndex(db, tableName, index);\n }\n\n for (const trigger of triggers) {\n await this.createTrigger(db, trigger);\n }\n } else {\n // Table exists, check for schema changes\n await this.updateSchemaIfNeeded(db, schema);\n }\n }\n\n /**\n * Create table from schema definition\n */\n private async createTable(\n db: DatabaseInterface,\n schema: SchemaDefinition,\n ): Promise<void> {\n const { tableName, columns, foreignKeys } = schema;\n\n const columnDefinitions: string[] = [];\n\n // Add column definitions\n for (const [columnName, columnDef] of Object.entries(columns)) {\n let def = `${columnName} ${columnDef.type}`;\n\n if (columnDef.primaryKey) def += ' PRIMARY KEY';\n if (columnDef.unique && !columnDef.primaryKey) def += ' UNIQUE';\n if (columnDef.notNull) def += ' NOT NULL';\n if (columnDef.defaultValue !== undefined) {\n def += ` DEFAULT ${columnDef.defaultValue}`;\n }\n if (columnDef.check) def += ` CHECK (${columnDef.check})`;\n\n columnDefinitions.push(def);\n }\n\n // Add foreign key constraints\n for (const fk of foreignKeys) {\n let fkDef = `FOREIGN KEY (${fk.column}) REFERENCES ${fk.referencesTable}(${fk.referencesColumn})`;\n if (fk.onDelete) fkDef += ` ON DELETE ${fk.onDelete}`;\n if (fk.onUpdate) fkDef += ` ON UPDATE ${fk.onUpdate}`;\n columnDefinitions.push(fkDef);\n }\n\n const createTableSQL = `CREATE TABLE ${tableName} (\\n ${columnDefinitions.join(',\\n ')}\\n)`;\n await db.query(createTableSQL);\n }\n\n /**\n * Create index from definition\n */\n private async createIndex(\n db: DatabaseInterface,\n tableName: string,\n index: IndexDefinition,\n ): Promise<void> {\n const uniqueClause = index.unique ? 'UNIQUE ' : '';\n const whereClause = index.where ? ` WHERE ${index.where}` : '';\n const createIndexSQL = `CREATE ${uniqueClause}INDEX ${index.name} ON ${tableName} (${index.columns.join(', ')})${whereClause}`;\n\n await db.query(createIndexSQL);\n }\n\n /**\n * Create trigger from definition\n */\n private async createTrigger(\n db: DatabaseInterface,\n trigger: TriggerDefinition,\n ): Promise<void> {\n const conditionClause = trigger.condition\n ? ` WHEN ${trigger.condition}`\n : '';\n const createTriggerSQL = `CREATE TRIGGER ${trigger.name} ${trigger.when} ${trigger.event} ON ${trigger.table}${conditionClause} BEGIN ${trigger.body} END`;\n\n await db.query(createTriggerSQL);\n }\n\n /**\n * Update schema if changes are detected\n */\n private async updateSchemaIfNeeded(\n _db: DatabaseInterface,\n schema: SchemaDefinition,\n ): Promise<void> {\n // For now, just log that update is needed\n // In future, implement ALTER TABLE logic\n console.log(\n `[schema] Schema update logic not yet implemented for ${schema.tableName}`,\n );\n }\n\n /**\n * Build dependency graph from schemas\n */\n private buildDependencyGraph(\n schemas: Record<string, SchemaDefinition>,\n ): Map<string, string[]> {\n const graph = new Map<string, string[]>();\n\n for (const [schemaName, schema] of Object.entries(schemas)) {\n const dependencies = schema.dependencies.filter((dep) =>\n Object.values(schemas).some((s) => s.tableName === dep),\n );\n graph.set(\n schemaName,\n dependencies.map(\n (dep) =>\n Object.entries(schemas).find(\n ([_, s]) => s.tableName === dep,\n )?.[0] || dep,\n ),\n );\n }\n\n return graph;\n }\n\n /**\n * Resolve dependencies using topological sort\n */\n private resolveDependencies(graph: Map<string, string[]>): string[] {\n const resolved: string[] = [];\n const visited = new Set<string>();\n const visiting = new Set<string>();\n\n const visit = (node: string) => {\n if (visiting.has(node)) {\n throw new Error(`Circular dependency detected involving ${node}`);\n }\n if (visited.has(node)) return;\n\n visiting.add(node);\n const dependencies = graph.get(node) || [];\n\n for (const dep of dependencies) {\n if (graph.has(dep)) {\n visit(dep);\n }\n }\n\n visiting.delete(node);\n visited.add(node);\n resolved.push(node);\n };\n\n for (const node of graph.keys()) {\n if (!visited.has(node)) {\n visit(node);\n }\n }\n\n return resolved;\n }\n\n /**\n * Check if schema is up to date\n */\n private isSchemaUpToDate(schemaName: string, version: string): boolean {\n return (\n this.initializedSchemas.has(schemaName) &&\n this.schemaVersions.get(schemaName) === version\n );\n }\n\n /**\n * Mark schema as initialized\n */\n private markSchemaInitialized(schemaName: string, version: string): void {\n this.initializedSchemas.add(schemaName);\n this.schemaVersions.set(schemaName, version);\n }\n\n /**\n * Reset initialization state (for testing)\n */\n reset(): void {\n this.initializedSchemas.clear();\n this.schemaVersions.clear();\n DatabaseSchemaManager.initializationLock.clear();\n }\n}\n","/**\n * Shared utilities for ALTER TABLE operations\n *\n * Provides database-agnostic utilities for generating ALTER TABLE statements,\n * handling column definitions, and creating indexes.\n */\n\nimport type { ColumnDefinitionWithName, IndexDefinition } from './types.js';\n\n/**\n * Database dialect type for SQL generation\n */\nexport type DatabaseDialect = 'sqlite' | 'postgres' | 'duckdb';\n\n/**\n * Generates an ALTER TABLE ADD COLUMN statement\n *\n * @param table - Table name\n * @param column - Column definition with name\n * @param dialect - Database dialect\n * @returns SQL statement string\n *\n * @example\n * ```typescript\n * const sql = generateAddColumnStatement('users', {\n * name: 'email',\n * type: 'TEXT',\n * unique: true,\n * notNull: false\n * }, 'sqlite');\n * // Returns: \"ALTER TABLE users ADD COLUMN email TEXT UNIQUE\"\n * ```\n */\nexport function generateAddColumnStatement(\n table: string,\n column: ColumnDefinitionWithName,\n _dialect: DatabaseDialect,\n): string {\n const parts: string[] = [column.name, column.type];\n\n // Primary key\n if (column.primaryKey) {\n parts.push('PRIMARY KEY');\n }\n\n // NOT NULL constraint\n if (column.notNull) {\n parts.push('NOT NULL');\n }\n\n // UNIQUE constraint\n if (column.unique) {\n parts.push('UNIQUE');\n }\n\n // DEFAULT clause\n if (column.defaultValue !== undefined) {\n const defaultClause = generateDefaultClause(\n column.type,\n column.defaultValue,\n );\n parts.push(`DEFAULT ${defaultClause}`);\n }\n\n // CHECK constraint\n if (column.check) {\n parts.push(`CHECK (${column.check})`);\n }\n\n // Foreign key (inline for some databases)\n if (column.foreignKey) {\n const fk = column.foreignKey;\n const fkParts = [`REFERENCES ${fk.table}(${fk.column})`];\n\n if (fk.onDelete) {\n fkParts.push(`ON DELETE ${fk.onDelete}`);\n }\n\n if (fk.onUpdate) {\n fkParts.push(`ON UPDATE ${fk.onUpdate}`);\n }\n\n parts.push(fkParts.join(' '));\n }\n\n const columnDef = parts.join(' ');\n return `ALTER TABLE ${table} ADD COLUMN ${columnDef}`;\n}\n\n/**\n * Generates a DEFAULT clause for a column definition\n *\n * Handles proper formatting and escaping of default values based on column type.\n *\n * @param columnType - SQL column type (TEXT, INTEGER, BOOLEAN, etc.)\n * @param defaultValue - Default value to format\n * @returns Formatted default value string\n *\n * @example\n * ```typescript\n * generateDefaultClause('TEXT', 'hello'); // Returns: \"'hello'\"\n * generateDefaultClause('INTEGER', 42); // Returns: \"42\"\n * generateDefaultClause('BOOLEAN', true); // Returns: \"true\"\n * generateDefaultClause('TEXT', null); // Returns: \"NULL\"\n * ```\n */\nexport function generateDefaultClause(\n columnType: string,\n defaultValue: any,\n): string {\n if (defaultValue === null) {\n return 'NULL';\n }\n\n const typeUpper = columnType.toUpperCase();\n\n // String types - need quotes\n if (\n typeUpper.includes('TEXT') ||\n typeUpper.includes('CHAR') ||\n typeUpper.includes('VARCHAR') ||\n typeUpper.includes('STRING')\n ) {\n // Handle empty strings\n if (defaultValue === '') {\n return \"''\";\n }\n // Escape single quotes\n const escaped = String(defaultValue).replace(/'/g, \"''\");\n return `'${escaped}'`;\n }\n\n // Boolean types\n if (typeUpper.includes('BOOL')) {\n return defaultValue ? 'true' : 'false';\n }\n\n // Numeric types - no quotes\n if (\n typeUpper.includes('INT') ||\n typeUpper.includes('REAL') ||\n typeUpper.includes('FLOAT') ||\n typeUpper.includes('DOUBLE') ||\n typeUpper.includes('NUMERIC') ||\n typeUpper.includes('DECIMAL')\n ) {\n return String(defaultValue);\n }\n\n // JSON types\n if (typeUpper.includes('JSON')) {\n if (typeof defaultValue === 'string') {\n return `'${defaultValue.replace(/'/g, \"''\")}'`;\n }\n return `'${JSON.stringify(defaultValue).replace(/'/g, \"''\")}'`;\n }\n\n // Date/timestamp types\n if (\n typeUpper.includes('DATE') ||\n typeUpper.includes('TIME') ||\n typeUpper.includes('TIMESTAMP')\n ) {\n if (defaultValue instanceof Date) {\n return `'${defaultValue.toISOString()}'`;\n }\n return `'${String(defaultValue).replace(/'/g, \"''\")}'`;\n }\n\n // Default: treat as string\n if (typeof defaultValue === 'string') {\n return `'${defaultValue.replace(/'/g, \"''\")}'`;\n }\n\n return String(defaultValue);\n}\n\n/**\n * Generates a CREATE INDEX statement\n *\n * @param table - Table name\n * @param index - Index definition\n * @returns SQL CREATE INDEX statement\n *\n * @example\n * ```typescript\n * const sql = generateCreateIndexStatement('users', {\n * name: 'idx_users_email',\n * columns: ['email'],\n * unique: true\n * });\n * // Returns: \"CREATE UNIQUE INDEX idx_users_email ON users (email)\"\n * ```\n */\nexport function generateCreateIndexStatement(\n table: string,\n index: IndexDefinition,\n): string {\n const parts: string[] = ['CREATE'];\n\n // UNIQUE modifier\n if (index.unique) {\n parts.push('UNIQUE');\n }\n\n parts.push('INDEX');\n\n // Index name\n parts.push(index.name);\n\n // ON table\n parts.push(`ON ${table}`);\n\n // Column list\n const columns = index.columns.join(', ');\n parts.push(`(${columns})`);\n\n // WHERE clause (partial index)\n if (index.where) {\n parts.push(`WHERE ${index.where}`);\n }\n\n return parts.join(' ');\n}\n\n/**\n * Validates a table name to prevent SQL injection\n *\n * @param tableName - Table name to validate\n * @throws Error if table name contains invalid characters\n */\nexport function validateTableName(tableName: string): void {\n if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {\n throw new Error(\n `Invalid table name: ${tableName}. Table names must start with a letter or underscore and contain only alphanumeric characters and underscores.`,\n );\n }\n}\n\n/**\n * Validates a column name to prevent SQL injection\n *\n * @param columnName - Column name to validate\n * @throws Error if column name contains invalid characters\n */\nexport function validateColumnName(columnName: string): void {\n if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(columnName)) {\n throw new Error(\n `Invalid column name: ${columnName}. Column names must start with a letter or underscore and contain only alphanumeric characters and underscores.`,\n );\n }\n}\n\n/**\n * Validates every identifier in a column list to prevent SQL injection\n *\n * Column names reaching the CRUD methods come from `Object.keys(data)` and the\n * caller's `conflictColumns`, both of which are interpolated into SQL. They are\n * checked rather than quoted deliberately: the accepted shape has no character\n * that can end an identifier, so quoting adds nothing to safety, and quoting a\n * name on PostgreSQL makes it case-sensitive, which would silently change which\n * column an existing caller addresses.\n *\n * @param columnNames - Column names to validate\n * @throws Error if any column name contains invalid characters\n */\nexport function validateColumnNames(columnNames: string[]): void {\n for (const columnName of columnNames) {\n validateColumnName(columnName);\n }\n}\n\n/**\n * Escapes an identifier for interpolation inside double quotes\n *\n * For the positions that already quote the identifier — the DuckDB and JSON\n * upsert statements, which quote to match their schema generator — quoting is\n * what makes the identifier safe, so the only thing that has to be handled is\n * a quote character inside the name. Doubling it is the SQL escape.\n *\n * This is deliberately more permissive than {@link validateColumnName}: those\n * adapters accept column names that are not plain identifiers (`Full Name`,\n * `user-id`, `2024_total`), which matters most for the JSON adapter, whose\n * columns come straight from the keys of whatever JSON it was pointed at.\n * Rejecting them would break working code to fix an injection that quoting\n * already prevents.\n *\n * @param identifier - Identifier to place inside double quotes\n * @returns The identifier with embedded quotes doubled\n * @throws Error if the identifier is empty or contains a NUL byte\n */\nexport function escapeQuotedIdentifier(identifier: string): string {\n if (identifier.length === 0 || identifier.includes('\\0')) {\n throw new Error(\n `Invalid column name: ${JSON.stringify(identifier)}. Column names must be non-empty and must not contain a NUL byte.`,\n );\n }\n return identifier.replaceAll('\"', '\"\"');\n}\n\n/**\n * Escapes a value for interpolation inside a single-quoted SQL string literal\n *\n * The DuckDB-backed adapters read JSON through `read_json_auto('<path>')`, and\n * a file path is not something an identifier validator can check — paths\n * legitimately contain spaces, dots and, on Linux and macOS, quotes. Doubling\n * the quote is the SQL escape, and it is what keeps a file named `x'.json`\n * from ending the literal and starting a statement.\n *\n * @param value - Value to place inside single quotes\n * @returns The value with embedded single quotes doubled\n * @throws Error if the value contains a NUL byte\n */\nexport function escapeStringLiteral(value: string): string {\n if (value.includes('\\0')) {\n throw new Error(\n `Invalid SQL string literal: ${JSON.stringify(value)}. Values must not contain a NUL byte.`,\n );\n }\n return value.replaceAll(\"'\", \"''\");\n}\n\n/**\n * Validates an index name to prevent SQL injection\n *\n * @param indexName - Index name to validate\n * @throws Error if index name contains invalid characters\n */\nexport function validateIndexName(indexName: string): void {\n if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(indexName)) {\n throw new Error(\n `Invalid index name: ${indexName}. Index names must start with a letter or underscore and contain only alphanumeric characters and underscores.`,\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,cAAc,OAAwB;CACpD,MAAM,UAAU;CAUhB,MAAM,QAAkB,CAAC;CAEzB,IAAI,QAAQ,SAAS,MAAM,KAAK,QAAQ,OAAO;CAC/C,IAAI,QAAQ,MAAM,MAAM,KAAK,QAAQ,QAAQ,MAAM;CACnD,IAAI,QAAQ,QAAQ,MAAM,KAAK,UAAU,QAAQ,QAAQ;CACzD,IAAI,QAAQ,MAAM,MAAM,KAAK,QAAQ,QAAQ,MAAM;CACnD,IAAI,QAAQ,UAAU,MAAM,KAAK,YAAY,QAAQ,UAAU;CAC/D,IAAI,QAAQ,UAAU,KAAA,GAAW,MAAM,KAAK,SAAS,QAAQ,OAAO;CAEpE,OAAO,MAAM,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,KAAK;AAC3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,qBACd,OACA,SACU;CACV,MAAM,eAAe,WACnB,IAAI,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,QAAQ,QAAQ,OAAO,SAAS,KAAA,CAAS,CAAC;CAExE,MAAM,OAAO,OAAO,KAAK,QAAQ,EAAE;CACnC,MAAM,WAAW,YAAY,QAAQ,EAAE;CAEvC,KAAK,IAAI,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS;EACnD,MAAM,aAAa,YAAY,QAAQ,MAAM;EAC7C,MAAM,UAAU,CAAC,GAAG,QAAQ,CAAC,CAAC,QAAQ,QAAQ,CAAC,WAAW,IAAI,GAAG,CAAC;EAClE,MAAM,QAAQ,CAAC,GAAG,UAAU,CAAC,CAAC,QAAQ,QAAQ,CAAC,SAAS,IAAI,GAAG,CAAC;EAEhE,IAAI,QAAQ,WAAW,KAAK,MAAM,WAAW,GAC3C;EAGF,MAAM,WAAW,CACf,QAAQ,SAAS,IAAI,WAAW,QAAQ,KAAK,IAAI,MAAM,IACvD,MAAM,SAAS,IAAI,cAAc,MAAM,KAAK,IAAI,MAAM,EACxD,CAAC,CAAC,OAAO,OAAO;EAEhB,MAAM,IAAI,cACR,4DAA4D,MAAM,OAAO,SAAS,KAChF,OACF,EAAE,yBAAyB,CAAC,GAAG,QAAQ,CAAC,CAAC,KAAK,IAAI,EAAE,IACpD;GACE;GACA,aAAa;GACb,cAAc;GACd,YAAY,CAAC,GAAG,UAAU;GAC1B;GACA;GACA,MAAM;EACR,CACF;CACF;CAEA,OAAO;AACT;;;;AAKA,IAAM,kBAAkB;CACtB,KAAK;CACL,KAAK;CACL,MAAM;CACN,KAAK;CACL,MAAM;CACN,MAAM;CACN,MAAM;CACN,IAAI;CACJ,UAAU;AACZ;AAEA,SAAS,sBAAsB,OAAwB;CACrD,OAAO,mBAAmB,KAAK,KAAK;AACtC;AAEA,SAAgB,kBAAkB,SAIhC;CACA,MAAM,UAAU,QAAQ,KAAK;CAC7B,MAAM,QAAQ,QAAQ,YAAY;CAGlC,KAAK,MAAM,YAAY;EAFJ;EAAU;EAAQ;EAAM;EAAM;EAAM;EAAM;EAAK;EAAK;CAEhD,GACrB,IAAI,MAAM,SAAS,IAAI,UAAU,GAC/B,OAAO;EACL,OAAO,QAAQ,MAAM,GAAG,EAAE,SAAS,SAAS,EAAE,CAAC,CAAC,KAAK;EACrD;EACA,kBAAkB;CACpB;CAIJ,OAAO;EAAE,OAAO;EAAS,UAAU;EAAK,kBAAkB;CAAM;AAClE;;;;;AAWA,IAAM,kBACJ,SACA,OACA,WACA,gBACmC;CACnC,MAAM,EAAE,OAAO,UAAU,qBAAqB,kBAAkB,OAAO;CACvE,IAAI,CAAC,oBAAoB,CAAC,sBAAsB,KAAK,GACnD,MAAM,IAAI,MAAM,2BAA2B,OAAO;CAEpD,MAAM,cACJ,gBAAgB,aAA6C;CAC/D,MAAM,SAAgB,CAAC;CAEvB,IAAI;CAEJ,IAAI,UAAU,MACZ,MAAM,GAAG,MAAM,MAAM,gBAAgB,MAAM,SAAS;MAC/C,KACJ,gBAAgB,QAAQ,gBAAgB,aACzC,MAAM,QAAQ,KAAK,GACnB;EACA,IAAI,MAAM,WAAW,GACnB,MAAM,IAAI,MAAM,GAAG,YAAY,6BAA6B;EAG9D,MAAM,GAAG,MAAM,GAAG,YAAY,IADT,MAAM,UAAU,IAAI,UAAU,SAAS,CAAC,CAAC,KAAK,IACjC,EAAa;EAC/C,OAAO,KAAK,GAAG,KAAK;CACtB,OAAO,IAAI,iBAAiB,MAAM;EAIhC,IADkB,gBAAgB,YAAY,gBAAgB,QAE5D,MAAM,GAAG,MAAM,GAAG,YAAY,SAAS,UAAU,QAAQ;OAEzD,MAAM,GAAG,MAAM,GAAG,YAAY,IAAI,UAAU;EAE9C,OAAO,KAAK,MAAM,YAAY,CAAC;CACjC,OAAO;EACL,MAAM,GAAG,MAAM,GAAG,YAAY,IAAI,UAAU;EAC5C,OAAO,KAAK,KAAK;CACnB;CAEA,OAAO;EAAE;EAAK;CAAO;AACvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsFA,IAAa,cACX,OACA,aAAa,GACb,gBACG;CACH,IAAI,MAAM;CACV,MAAM,SAAgB,CAAC;CACvB,MAAM,YAAY,EAAE,OAAO,WAAW;CAGtC,IAAI,MAAM,QAAQ,KAAK,GAAG;EACxB,IAAI,MAAM,WAAW,GACnB,OAAO;GAAE,KAAK;GAAI,QAAQ,CAAC;EAAE;EAG/B,MAAM,WAAqB,CAAC;EAE5B,KAAK,MAAM,YAAY,OAAO;GAC5B,IAAI,CAAC,MAAM,QAAQ,QAAQ,KAAK,SAAS,WAAW,GAClD;GAGF,MAAM,gBAA0B,CAAC;GAEjC,KAAK,MAAM,gBAAgB,UACzB,KAAK,MAAM,CAAC,SAAS,UAAU,OAAO,QAAQ,YAAY,GAAG;IAC3D,MAAM,SAAS,eAAe,SAAS,OAAO,WAAW,WAAW;IACpE,cAAc,KAAK,OAAO,GAAG;IAC7B,OAAO,KAAK,GAAG,OAAO,MAAM;GAC9B;GAGF,IAAI,cAAc,SAAS,GAEzB,SAAS,KAAK,IAAI,cAAc,KAAK,OAAO,EAAE,EAAE;EAEpD;EAEA,IAAI,SAAS,SAAS,GACpB,MAAM,SAAS,SAAS,KAAK,MAAM;EAGrC,OAAO;GAAE;GAAK;EAAO;CACvB;CAGA,IAAI,SAAS,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,GAAG;EAC1C,MAAM;EACN,KAAK,MAAM,CAAC,SAAS,UAAU,OAAO,QAAQ,KAAK,GAAG;GACpD,MAAM,SAAS,eAAe,SAAS,OAAO,WAAW,WAAW;GAEpE,IAAI,QAAQ,UACV,OAAO;GAGT,OAAO,OAAO;GACd,OAAO,KAAK,GAAG,OAAO,MAAM;EAC9B;CACF;CAEA,OAAO;EAAE;EAAK;CAAO;AACvB;;;;;;;;;ACrUA,IAAa,wBAAb,MAAa,sBAAsB;CACjC,OAAe,qCAAqB,IAAI,IAA2B;CACnE,qCAA6B,IAAI,IAAY;CAC7C,iCAAyB,IAAI,IAAoB;;;;;;;;;;;;;CAcjD,MAAM,kBACJ,IACA,SACqC;EACrC,MAAM,YAAY,KAAK,IAAI;EAC3B,MAAM,SAAqC;GACzC,aAAa,CAAC;GACd,SAAS,CAAC;GACV,QAAQ,CAAC;GACT,eAAe;EACjB;EAEA,IAAI;GACF,IAAI,UAA4C,CAAC;GAGjD,IAAI,QAAQ,QAAQ;IAClB,QAAQ,IACN,oEACF;IAEA,IAAI,GAAG,YAAY;KACjB,MAAM,GAAG,WAAW,QAAQ,MAAM;KAClC,OAAO,YAAY,KAAK,YAAY;IACtC;IACA,OAAO,gBAAgB,KAAK,IAAI,IAAI;IACpC,OAAO;GACT;GAGA,IAAI,QAAQ,UACV,UAAU,EAAE,GAAG,QAAQ,SAAS,QAAQ;GAI1C,IAAI,QAAQ,WACV,UAAU;IAAE,GAAG;IAAS,GAAG,QAAQ;GAAU;GAG/C,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,WAAW,GAAG;IACrC,QAAQ,KAAK,iDAAiD;IAC9D,OAAO,gBAAgB,KAAK,IAAI,IAAI;IACpC,OAAO;GACT;GAGA,MAAM,kBAAkB,KAAK,qBAAqB,OAAO;GAGzD,MAAM,sBAAsB,KAAK,oBAAoB,eAAe;GAEpE,IAAI,QAAQ,OACV,QAAQ,IAAI,kCAAkC,mBAAmB;GAInE,KAAK,MAAM,cAAc,qBAAqB;IAC5C,MAAM,SAAS,QAAQ;IACvB,IAAI,CAAC,QAAQ;IAEb,IAAI;KACF,MAAM,KAAK,iBAAiB,IAAI,YAAY,QAAQ;MAClD,OAAO,QAAQ;MACf,OAAO,QAAQ;KACjB,CAAC;KACD,OAAO,YAAY,KAAK,UAAU;IACpC,SAAS,OAAO;KACd,MAAM,eACJ,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;KACvD,OAAO,OAAO,KAAK;MAAE,QAAQ;MAAY,OAAO;KAAa,CAAC;KAE9D,IAAI,QAAQ,OACV,QAAQ,MACN,iCAAiC,WAAW,IAC5C,KACF;IAEJ;GACF;GAEA,OAAO,gBAAgB,KAAK,IAAI,IAAI;GAEpC,IAAI,QAAQ,OACV,QAAQ,IAAI,qCAAqC,MAAM;GAGzD,OAAO;EACT,SAAS,OAAO;GACd,OAAO,gBAAgB,KAAK,IAAI,IAAI;GACpC,MAAM,eACJ,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GACvD,OAAO,OAAO,KAAK;IACjB,QAAQ;IACR,OAAO;GACT,CAAC;GACD,OAAO;EACT;CACF;;;;CAKA,MAAc,iBACZ,IACA,YACA,QACA,SACe;EACf,MAAM,EAAE,WAAW,YAAY;EAC/B,MAAM,UAAU,GAAG,WAAW,GAAG;EAGjC,IAAI,sBAAsB,mBAAmB,IAAI,OAAO,GAAG;GACzD,MAAM,sBAAsB,mBAAmB,IAAI,OAAO;GAC1D;EACF;EAGA,IAAI,CAAC,QAAQ,SAAS,KAAK,iBAAiB,YAAY,OAAO,GAAG;GAChE,IAAI,QAAQ,OACV,QAAQ,IAAI,qBAAqB,WAAW,sBAAsB;GAEpE;EACF;EAGA,MAAM,cAAc,KAAK,4BACvB,IACA,YACA,QACA,OACF;EACA,sBAAsB,mBAAmB,IAAI,SAAS,WAAW;EAEjE,IAAI;GACF,MAAM;GACN,KAAK,sBAAsB,YAAY,OAAO;EAChD,UAAU;GACR,sBAAsB,mBAAmB,OAAO,OAAO;EACzD;CACF;;;;CAKA,MAAc,4BACZ,IACA,YACA,QACA,SACe;EACf,MAAM,EAAE,WAAW,SAAS,SAAS,aAAa;EAElD,IAAI,QAAQ,OACV,QAAQ,IAAI,yBAAyB,WAAW,IAAI,UAAU,EAAE;EAMlE,IAAI,CAAC,MAFqB,GAAG,YAAY,SAAS,GAEhC;GAEhB,MAAM,KAAK,YAAY,IAAI,MAAM;GAGjC,KAAK,MAAM,SAAS,SAClB,MAAM,KAAK,YAAY,IAAI,WAAW,KAAK;GAI7C,KAAK,MAAM,WAAW,UACpB,MAAM,KAAK,cAAc,IAAI,OAAO;EAExC,OAAO,IAAI,QAAQ,OAAO;GAExB,MAAM,GAAG,MAAM,wBAAwB,WAAW;GAClD,MAAM,KAAK,YAAY,IAAI,MAAM;GAEjC,KAAK,MAAM,SAAS,SAClB,MAAM,KAAK,YAAY,IAAI,WAAW,KAAK;GAG7C,KAAK,MAAM,WAAW,UACpB,MAAM,KAAK,cAAc,IAAI,OAAO;EAExC,OAEE,MAAM,KAAK,qBAAqB,IAAI,MAAM;CAE9C;;;;CAKA,MAAc,YACZ,IACA,QACe;EACf,MAAM,EAAE,WAAW,SAAS,gBAAgB;EAE5C,MAAM,oBAA8B,CAAC;EAGrC,KAAK,MAAM,CAAC,YAAY,cAAc,OAAO,QAAQ,OAAO,GAAG;GAC7D,IAAI,MAAM,GAAG,WAAW,GAAG,UAAU;GAErC,IAAI,UAAU,YAAY,OAAO;GACjC,IAAI,UAAU,UAAU,CAAC,UAAU,YAAY,OAAO;GACtD,IAAI,UAAU,SAAS,OAAO;GAC9B,IAAI,UAAU,iBAAiB,KAAA,GAC7B,OAAO,YAAY,UAAU;GAE/B,IAAI,UAAU,OAAO,OAAO,WAAW,UAAU,MAAM;GAEvD,kBAAkB,KAAK,GAAG;EAC5B;EAGA,KAAK,MAAM,MAAM,aAAa;GAC5B,IAAI,QAAQ,gBAAgB,GAAG,OAAO,eAAe,GAAG,gBAAgB,GAAG,GAAG,iBAAiB;GAC/F,IAAI,GAAG,UAAU,SAAS,cAAc,GAAG;GAC3C,IAAI,GAAG,UAAU,SAAS,cAAc,GAAG;GAC3C,kBAAkB,KAAK,KAAK;EAC9B;EAEA,MAAM,iBAAiB,gBAAgB,UAAU,QAAQ,kBAAkB,KAAK,OAAO,EAAE;EACzF,MAAM,GAAG,MAAM,cAAc;CAC/B;;;;CAKA,MAAc,YACZ,IACA,WACA,OACe;EACf,MAAM,eAAe,MAAM,SAAS,YAAY;EAChD,MAAM,cAAc,MAAM,QAAQ,UAAU,MAAM,UAAU;EAC5D,MAAM,iBAAiB,UAAU,aAAa,QAAQ,MAAM,KAAK,MAAM,UAAU,IAAI,MAAM,QAAQ,KAAK,IAAI,EAAE,GAAG;EAEjH,MAAM,GAAG,MAAM,cAAc;CAC/B;;;;CAKA,MAAc,cACZ,IACA,SACe;EACf,MAAM,kBAAkB,QAAQ,YAC5B,SAAS,QAAQ,cACjB;EACJ,MAAM,mBAAmB,kBAAkB,QAAQ,KAAK,GAAG,QAAQ,KAAK,GAAG,QAAQ,MAAM,MAAM,QAAQ,QAAQ,gBAAgB,SAAS,QAAQ,KAAK;EAErJ,MAAM,GAAG,MAAM,gBAAgB;CACjC;;;;CAKA,MAAc,qBACZ,KACA,QACe;EAGf,QAAQ,IACN,wDAAwD,OAAO,WACjE;CACF;;;;CAKA,qBACE,SACuB;EACvB,MAAM,wBAAQ,IAAI,IAAsB;EAExC,KAAK,MAAM,CAAC,YAAY,WAAW,OAAO,QAAQ,OAAO,GAAG;GAC1D,MAAM,eAAe,OAAO,aAAa,QAAQ,QAC/C,OAAO,OAAO,OAAO,CAAC,CAAC,MAAM,MAAM,EAAE,cAAc,GAAG,CACxD;GACA,MAAM,IACJ,YACA,aAAa,KACV,QACC,OAAO,QAAQ,OAAO,CAAC,CAAC,MACrB,CAAC,GAAG,OAAO,EAAE,cAAc,GAC9B,CAAC,GAAG,MAAM,GACd,CACF;EACF;EAEA,OAAO;CACT;;;;CAKA,oBAA4B,OAAwC;EAClE,MAAM,WAAqB,CAAC;EAC5B,MAAM,0BAAU,IAAI,IAAY;EAChC,MAAM,2BAAW,IAAI,IAAY;EAEjC,MAAM,SAAS,SAAiB;GAC9B,IAAI,SAAS,IAAI,IAAI,GACnB,MAAM,IAAI,MAAM,0CAA0C,MAAM;GAElE,IAAI,QAAQ,IAAI,IAAI,GAAG;GAEvB,SAAS,IAAI,IAAI;GACjB,MAAM,eAAe,MAAM,IAAI,IAAI,KAAK,CAAC;GAEzC,KAAK,MAAM,OAAO,cAChB,IAAI,MAAM,IAAI,GAAG,GACf,MAAM,GAAG;GAIb,SAAS,OAAO,IAAI;GACpB,QAAQ,IAAI,IAAI;GAChB,SAAS,KAAK,IAAI;EACpB;EAEA,KAAK,MAAM,QAAQ,MAAM,KAAK,GAC5B,IAAI,CAAC,QAAQ,IAAI,IAAI,GACnB,MAAM,IAAI;EAId,OAAO;CACT;;;;CAKA,iBAAyB,YAAoB,SAA0B;EACrE,OACE,KAAK,mBAAmB,IAAI,UAAU,KACtC,KAAK,eAAe,IAAI,UAAU,MAAM;CAE5C;;;;CAKA,sBAA8B,YAAoB,SAAuB;EACvE,KAAK,mBAAmB,IAAI,UAAU;EACtC,KAAK,eAAe,IAAI,YAAY,OAAO;CAC7C;;;;CAKA,QAAc;EACZ,KAAK,mBAAmB,MAAM;EAC9B,KAAK,eAAe,MAAM;EAC1B,sBAAsB,mBAAmB,MAAM;CACjD;AACF;;;;;;;;;;;;;;;;;;;;;;AC3XA,SAAgB,2BACd,OACA,QACA,UACQ;CACR,MAAM,QAAkB,CAAC,OAAO,MAAM,OAAO,IAAI;CAGjD,IAAI,OAAO,YACT,MAAM,KAAK,aAAa;CAI1B,IAAI,OAAO,SACT,MAAM,KAAK,UAAU;CAIvB,IAAI,OAAO,QACT,MAAM,KAAK,QAAQ;CAIrB,IAAI,OAAO,iBAAiB,KAAA,GAAW;EACrC,MAAM,gBAAgB,sBACpB,OAAO,MACP,OAAO,YACT;EACA,MAAM,KAAK,WAAW,eAAe;CACvC;CAGA,IAAI,OAAO,OACT,MAAM,KAAK,UAAU,OAAO,MAAM,EAAE;CAItC,IAAI,OAAO,YAAY;EACrB,MAAM,KAAK,OAAO;EAClB,MAAM,UAAU,CAAC,cAAc,GAAG,MAAM,GAAG,GAAG,OAAO,EAAE;EAEvD,IAAI,GAAG,UACL,QAAQ,KAAK,aAAa,GAAG,UAAU;EAGzC,IAAI,GAAG,UACL,QAAQ,KAAK,aAAa,GAAG,UAAU;EAGzC,MAAM,KAAK,QAAQ,KAAK,GAAG,CAAC;CAC9B;CAGA,OAAO,eAAe,MAAM,cADV,MAAM,KAAK,GACa;AAC5C;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,sBACd,YACA,cACQ;CACR,IAAI,iBAAiB,MACnB,OAAO;CAGT,MAAM,YAAY,WAAW,YAAY;CAGzC,IACE,UAAU,SAAS,MAAM,KACzB,UAAU,SAAS,MAAM,KACzB,UAAU,SAAS,SAAS,KAC5B,UAAU,SAAS,QAAQ,GAC3B;EAEA,IAAI,iBAAiB,IACnB,OAAO;EAIT,OAAO,IADS,OAAO,YAAY,CAAC,CAAC,QAAQ,MAAM,IACxC,EAAQ;CACrB;CAGA,IAAI,UAAU,SAAS,MAAM,GAC3B,OAAO,eAAe,SAAS;CAIjC,IACE,UAAU,SAAS,KAAK,KACxB,UAAU,SAAS,MAAM,KACzB,UAAU,SAAS,OAAO,KAC1B,UAAU,SAAS,QAAQ,KAC3B,UAAU,SAAS,SAAS,KAC5B,UAAU,SAAS,SAAS,GAE5B,OAAO,OAAO,YAAY;CAI5B,IAAI,UAAU,SAAS,MAAM,GAAG;EAC9B,IAAI,OAAO,iBAAiB,UAC1B,OAAO,IAAI,aAAa,QAAQ,MAAM,IAAI,EAAE;EAE9C,OAAO,IAAI,KAAK,UAAU,YAAY,CAAC,CAAC,QAAQ,MAAM,IAAI,EAAE;CAC9D;CAGA,IACE,UAAU,SAAS,MAAM,KACzB,UAAU,SAAS,MAAM,KACzB,UAAU,SAAS,WAAW,GAC9B;EACA,IAAI,wBAAwB,MAC1B,OAAO,IAAI,aAAa,YAAY,EAAE;EAExC,OAAO,IAAI,OAAO,YAAY,CAAC,CAAC,QAAQ,MAAM,IAAI,EAAE;CACtD;CAGA,IAAI,OAAO,iBAAiB,UAC1B,OAAO,IAAI,aAAa,QAAQ,MAAM,IAAI,EAAE;CAG9C,OAAO,OAAO,YAAY;AAC5B;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,6BACd,OACA,OACQ;CACR,MAAM,QAAkB,CAAC,QAAQ;CAGjC,IAAI,MAAM,QACR,MAAM,KAAK,QAAQ;CAGrB,MAAM,KAAK,OAAO;CAGlB,MAAM,KAAK,MAAM,IAAI;CAGrB,MAAM,KAAK,MAAM,OAAO;CAGxB,MAAM,UAAU,MAAM,QAAQ,KAAK,IAAI;CACvC,MAAM,KAAK,IAAI,QAAQ,EAAE;CAGzB,IAAI,MAAM,OACR,MAAM,KAAK,SAAS,MAAM,OAAO;CAGnC,OAAO,MAAM,KAAK,GAAG;AACvB;;;;;;;AAQA,SAAgB,kBAAkB,WAAyB;CACzD,IAAI,CAAC,2BAA2B,KAAK,SAAS,GAC5C,MAAM,IAAI,MACR,uBAAuB,UAAU,+GACnC;AAEJ;;;;;;;AAQA,SAAgB,mBAAmB,YAA0B;CAC3D,IAAI,CAAC,2BAA2B,KAAK,UAAU,GAC7C,MAAM,IAAI,MACR,wBAAwB,WAAW,gHACrC;AAEJ;;;;;;;;;;;;;;AAeA,SAAgB,oBAAoB,aAA6B;CAC/D,KAAK,MAAM,cAAc,aACvB,mBAAmB,UAAU;AAEjC;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,uBAAuB,YAA4B;CACjE,IAAI,WAAW,WAAW,KAAK,WAAW,SAAS,IAAI,GACrD,MAAM,IAAI,MACR,wBAAwB,KAAK,UAAU,UAAU,EAAE,kEACrD;CAEF,OAAO,WAAW,WAAW,MAAK,MAAI;AACxC;;;;;;;;;;;;;;AAeA,SAAgB,oBAAoB,OAAuB;CACzD,IAAI,MAAM,SAAS,IAAI,GACrB,MAAM,IAAI,MACR,+BAA+B,KAAK,UAAU,KAAK,EAAE,sCACvD;CAEF,OAAO,MAAM,WAAW,KAAK,IAAI;AACnC;;;;;;;AAQA,SAAgB,kBAAkB,WAAyB;CACzD,IAAI,CAAC,2BAA2B,KAAK,SAAS,GAC5C,MAAM,IAAI,MACR,uBAAuB,UAAU,+GACnC;AAEJ"}
@@ -1,7 +1,6 @@
1
- import { n as buildWhere, r as formatDbError, t as DatabaseSchemaManager } from "./schema-manager-a8RlxIE2.js";
1
+ import { a as validateColumnName, c as validateTableName, d as formatDbError, i as generateCreateIndexStatement, l as DatabaseSchemaManager, n as escapeStringLiteral, o as validateColumnNames, p as resolveInsertColumns, r as generateAddColumnStatement, s as validateIndexName, t as escapeQuotedIdentifier, u as buildWhere } from "./alter-utils-B900pfK_.js";
2
2
  import { t as convertUniqueIndexesToInlineConstraints } from "./duckdb-schema-utils-B-XBP8QF.js";
3
- import { a as validateTableName, i as validateIndexName, n as generateCreateIndexStatement, r as validateColumnName, t as generateAddColumnStatement } from "./alter-utils-K7tiuj5L.js";
4
- import { NestedTransactionError, resolveSchemas } from "../index.js";
3
+ import { n as resolveSchemas, r as createTransactionLock, t as NestedTransactionError } from "./types-uVFHgaSU.js";
5
4
  import { DatabaseError } from "@happyvertical/utils";
6
5
  import { readdir } from "node:fs/promises";
7
6
  import { basename, extname, join } from "node:path";
@@ -105,7 +104,8 @@ async function registerJSONFiles(connection, dataDir) {
105
104
  for (const file of jsonFiles) {
106
105
  const filePath = join(dataDir, file);
107
106
  const tableName = basename(file, ".json");
108
- await connection.run(`CREATE OR REPLACE VIEW ${tableName} AS SELECT * FROM read_json('${filePath}', auto_detect=true, format='auto', timestampformat='iso', dateformat='iso')`);
107
+ validateTableName(tableName);
108
+ await connection.run(`CREATE OR REPLACE VIEW ${tableName} AS SELECT * FROM read_json('${escapeStringLiteral(filePath)}', auto_detect=true, format='auto', timestampformat='iso', dateformat='iso')`);
109
109
  }
110
110
  } catch (error) {
111
111
  if (error.code !== "ENOENT") throw new DatabaseError(`Failed to register JSON files from ${dataDir}`, {
@@ -154,6 +154,7 @@ async function getDatabase(options = {}) {
154
154
  const writeStrategy = options.writeStrategy || "none";
155
155
  const dataDir = options.dataDir || "./data";
156
156
  const url = resolveDuckDBUrl(options.url || ":memory:");
157
+ const connectionLock = createTransactionLock("duckdb", options.transactionQueueTimeout);
157
158
  /**
158
159
  * Inserts one or more records into a table
159
160
  *
@@ -163,12 +164,14 @@ async function getDatabase(options = {}) {
163
164
  * @throws Error if the insert operation fails
164
165
  */
165
166
  const insert = async (table, data) => {
167
+ validateTableName(table);
166
168
  const records = Array.isArray(data) ? data : [data];
167
169
  if (records.length === 0) return {
168
170
  operation: "insert",
169
171
  affected: 0
170
172
  };
171
- const keys = Object.keys(records[0]);
173
+ const keys = resolveInsertColumns(table, records);
174
+ validateColumnNames(keys);
172
175
  const values = [];
173
176
  let paramIdx = 1;
174
177
  const placeholders = records.map((record) => {
@@ -220,6 +223,7 @@ async function getDatabase(options = {}) {
220
223
  * @throws Error if the query fails
221
224
  */
222
225
  const get = async (table, where) => {
226
+ validateTableName(table);
223
227
  const { sql: whereClause, values } = buildWhere(where, 1, "duckdb");
224
228
  const sql = `SELECT * FROM ${table} ${whereClause} LIMIT 1`;
225
229
  try {
@@ -243,6 +247,7 @@ async function getDatabase(options = {}) {
243
247
  * @throws Error if the query fails
244
248
  */
245
249
  const list = async (table, where) => {
250
+ validateTableName(table);
246
251
  const { sql: whereClause, values } = buildWhere(where, 1, "duckdb");
247
252
  const sql = `SELECT * FROM ${table} ${whereClause}`;
248
253
  try {
@@ -266,7 +271,9 @@ async function getDatabase(options = {}) {
266
271
  * @throws Error if the update operation fails
267
272
  */
268
273
  const update = async (table, where, data) => {
274
+ validateTableName(table);
269
275
  const keys = Object.keys(data);
276
+ validateColumnNames(keys);
270
277
  const setClause = keys.map((key, idx) => `${key} = $${idx + 1}`).join(", ");
271
278
  const { sql: whereClause, values: whereValues } = buildWhere(where, keys.length + 1, "duckdb");
272
279
  const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
@@ -315,7 +322,8 @@ async function getDatabase(options = {}) {
315
322
  const values = [];
316
323
  const paramIdx = { value: 1 };
317
324
  const assignments = keys.map((key) => {
318
- return `"${key}" = ${buildDuckDBValueExpression(data[key], values, paramIdx)}`;
325
+ const valueExpr = buildDuckDBValueExpression(data[key], values, paramIdx);
326
+ return `"${escapeQuotedIdentifier(key)}" = ${valueExpr}`;
319
327
  });
320
328
  const { sql: whereClause, values: whereValues } = buildWhere(conflictWhere, paramIdx.value, "duckdb");
321
329
  const sql = `UPDATE ${table} SET ${assignments.join(", ")} ${whereClause}`;
@@ -346,6 +354,7 @@ async function getDatabase(options = {}) {
346
354
  * @throws Error if the upsert operation fails
347
355
  */
348
356
  const upsert = async (table, conflictColumns, data, options) => {
357
+ validateTableName(table);
349
358
  const missingColumns = conflictColumns.filter((col) => !(col in data));
350
359
  if (missingColumns.length > 0) throw new DatabaseError("Conflict columns missing from data", {
351
360
  table,
@@ -409,11 +418,12 @@ async function getDatabase(options = {}) {
409
418
  paramIdx++;
410
419
  }
411
420
  }
412
- const conflict = conflictColumns.map((col) => `"${col}"`).join(", ");
413
- const quotedKeys = keys.map((key) => `"${key}"`).join(", ");
421
+ const conflict = conflictColumns.map((col) => `"${escapeQuotedIdentifier(col)}"`).join(", ");
422
+ const quotedKeys = keys.map((key) => `"${escapeQuotedIdentifier(key)}"`).join(", ");
414
423
  const quotedUpdateSetParts = keys.map((key, i) => {
415
424
  const part = updateSetParts[i];
416
- return `"${key}" = ${part.substring(part.indexOf("=") + 1).trim()}`;
425
+ const valueExpr = part.substring(part.indexOf("=") + 1).trim();
426
+ return `"${escapeQuotedIdentifier(key)}" = ${valueExpr}`;
417
427
  });
418
428
  const sql = `INSERT INTO ${table} (${quotedKeys}) VALUES (${placeholders.join(", ")}) ON CONFLICT(${conflict}) DO UPDATE SET ${quotedUpdateSetParts.join(", ")}`;
419
429
  try {
@@ -517,6 +527,7 @@ async function getDatabase(options = {}) {
517
527
  * @returns Promise resolving to boolean indicating if the table exists
518
528
  */
519
529
  const tableExists = async (tableName) => {
530
+ validateTableName(tableName);
520
531
  try {
521
532
  await connection.runAndReadAll(`SELECT * FROM ${tableName} LIMIT 1`);
522
533
  return true;
@@ -532,6 +543,7 @@ async function getDatabase(options = {}) {
532
543
  * @param dataDir - Directory to write JSON file
533
544
  */
534
545
  const exportTableToJSON = async (connection, table, dataDir) => {
546
+ validateTableName(table);
535
547
  const filePath = join(dataDir, `${table}.json`);
536
548
  await connection.run(`COPY (SELECT * FROM ${table}) TO '${filePath}' (FORMAT JSON, ARRAY true)`);
537
549
  };
@@ -932,7 +944,7 @@ async function getDatabase(options = {}) {
932
944
  * @param callback - Function to execute within transaction
933
945
  * @returns Promise resolving to callback result
934
946
  */
935
- const transaction = async (callback) => {
947
+ const transaction = async (callback) => connectionLock.run(async () => {
936
948
  try {
937
949
  await connection.run("BEGIN TRANSACTION");
938
950
  const result = await callback(createTransactionScope());
@@ -944,7 +956,7 @@ async function getDatabase(options = {}) {
944
956
  } catch {}
945
957
  throw error;
946
958
  }
947
- };
959
+ });
948
960
  /**
949
961
  * Builds the transaction-scoped interface handed to a transaction callback.
950
962
  *
@@ -993,18 +1005,30 @@ async function getDatabase(options = {}) {
993
1005
  * @returns Promise resolving to a TransactionHandle
994
1006
  */
995
1007
  const beginTransaction = async () => {
996
- await connection.run("BEGIN TRANSACTION");
1008
+ const releaseConnection = await connectionLock.acquire();
1009
+ try {
1010
+ await connection.run("BEGIN TRANSACTION");
1011
+ } catch (error) {
1012
+ releaseConnection();
1013
+ throw error;
1014
+ }
997
1015
  let active = true;
998
- const commit = async () => {
1016
+ const end = async (command) => {
999
1017
  if (!active) throw new DatabaseError("Transaction already ended", {});
1000
- await connection.run("COMMIT");
1001
- active = false;
1002
- };
1003
- const rollback = async () => {
1004
- if (!active) throw new DatabaseError("Transaction already ended", {});
1005
- await connection.run("ROLLBACK");
1006
- active = false;
1018
+ try {
1019
+ await connection.run(command);
1020
+ } catch (error) {
1021
+ try {
1022
+ await connection.run("ROLLBACK");
1023
+ } catch {}
1024
+ throw error;
1025
+ } finally {
1026
+ active = false;
1027
+ releaseConnection();
1028
+ }
1007
1029
  };
1030
+ const commit = () => end("COMMIT");
1031
+ const rollback = () => end("ROLLBACK");
1008
1032
  const isActive = () => active;
1009
1033
  return {
1010
1034
  ...createTransactionScope(),
@@ -1048,4 +1072,4 @@ async function getDatabase(options = {}) {
1048
1072
  //#endregion
1049
1073
  export { getDatabase };
1050
1074
 
1051
- //# sourceMappingURL=duckdb-D_Wu36JL.js.map
1075
+ //# sourceMappingURL=duckdb-CWy7Nziq.js.map