@happyvertical/sql 0.83.0 → 0.84.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 CHANGED
@@ -175,9 +175,11 @@ never queue.
175
175
  ### Identifiers
176
176
 
177
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
178
+ parameters, so every CRUD method validates them: an identifier must be a string
179
+ matching `[a-zA-Z_][a-zA-Z0-9_]*`. Qualified names (`schema.table`), quoted names
180
+ and anything containing whitespace are rejected. A non-string — including an
181
+ object with a `toString` — is rejected outright rather than coerced, so the value
182
+ validated is always the value interpolated. Values are always parameterized and
181
183
  are unaffected.
182
184
 
183
185
  Note that `buildWhere` treats a condition key carrying an explicit operator
@@ -1 +1 @@
1
- {"version":3,"file":"aggregate.d.ts","sourceRoot":"","sources":["../src/aggregate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAIL,KAAK,cAAc,EAEpB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,uBAAuB,GAC/B,QAAQ,GACR,MAAM,GACN,KAAK,GACL,MAAM,GACN,OAAO,GACP,SAAS,GACT,MAAM,CAAC;AAEX,MAAM,MAAM,iBAAiB,GACzB,OAAO,GACP,KAAK,GACL,KAAK,GACL,KAAK,GACL,KAAK,GACL,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB,MAAM,MAAM,mBAAmB,GAC3B;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/B;IACE,EAAE,EAAE,iBAAiB,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,GACD;IACE,MAAM,EAAE,uBAAuB,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEN,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,GAAG,EAAE,CAAC;CACf;AA4BD,wBAAgB,UAAU,CACxB,WAAW,EAAE,cAAc,GAAG,SAAS,EACvC,IAAI,EAAE,uBAAuB,EAC7B,MAAM,EAAE,MAAM,GACb,MAAM,CA4BR;AAgHD,wBAAgB,cAAc,CAC5B,IAAI,EAAE,aAAa,EACnB,UAAU,SAAI,EACd,WAAW,CAAC,EAAE,cAAc,GAC3B,oBAAoB,CA8EtB"}
1
+ {"version":3,"file":"aggregate.d.ts","sourceRoot":"","sources":["../src/aggregate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAIL,KAAK,cAAc,EAEpB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,uBAAuB,GAC/B,QAAQ,GACR,MAAM,GACN,KAAK,GACL,MAAM,GACN,OAAO,GACP,SAAS,GACT,MAAM,CAAC;AAEX,MAAM,MAAM,iBAAiB,GACzB,OAAO,GACP,KAAK,GACL,KAAK,GACL,KAAK,GACL,KAAK,GACL,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB,MAAM,MAAM,mBAAmB,GAC3B;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/B;IACE,EAAE,EAAE,iBAAiB,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,GACD;IACE,MAAM,EAAE,uBAAuB,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEN,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,GAAG,EAAE,CAAC;CACf;AA+BD,wBAAgB,UAAU,CACxB,WAAW,EAAE,cAAc,GAAG,SAAS,EACvC,IAAI,EAAE,uBAAuB,EAC7B,MAAM,EAAE,MAAM,GACb,MAAM,CA4BR;AAgHD,wBAAgB,cAAc,CAC5B,IAAI,EAAE,aAAa,EACnB,UAAU,SAAI,EACd,WAAW,CAAC,EAAE,cAAc,GAC3B,oBAAoB,CA8EtB"}
@@ -689,20 +689,33 @@ function generateCreateIndexStatement(table, index) {
689
689
  /**
690
690
  * Validates a table name to prevent SQL injection
691
691
  *
692
+ * The argument must be a primitive string. `RegExp.prototype.test` coerces its
693
+ * argument with `toString`, so an object whose `toString` returns a valid name
694
+ * on the call the validator makes and a different one on the call the caller's
695
+ * `${table}` interpolation makes would pass here and then reach SQL. Rejecting
696
+ * non-strings up front closes that gap: the value validated is the value
697
+ * interpolated, because a primitive string cannot observe how many times it is
698
+ * read.
699
+ *
692
700
  * @param tableName - Table name to validate
693
- * @throws Error if table name contains invalid characters
701
+ * @throws Error if the value is not a string or contains invalid characters
694
702
  */
695
703
  function validateTableName(tableName) {
696
- 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.`);
704
+ if (typeof tableName !== "string" || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) throw new Error(`Invalid table name: ${tableName}. Table names must be a string that starts with a letter or underscore and contains only alphanumeric characters and underscores.`);
697
705
  }
698
706
  /**
699
707
  * Validates a column name to prevent SQL injection
700
708
  *
709
+ * Rejects non-strings for the same reason as {@link validateTableName}: a value
710
+ * whose `toString` differs between the validation read and the interpolation
711
+ * read would otherwise slip a different identifier into SQL than the one
712
+ * checked here.
713
+ *
701
714
  * @param columnName - Column name to validate
702
- * @throws Error if column name contains invalid characters
715
+ * @throws Error if the value is not a string or contains invalid characters
703
716
  */
704
717
  function validateColumnName(columnName) {
705
- 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.`);
718
+ if (typeof columnName !== "string" || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(columnName)) throw new Error(`Invalid column name: ${columnName}. Column names must be a string that starts with a letter or underscore and contains only alphanumeric characters and underscores.`);
706
719
  }
707
720
  /**
708
721
  * Validates every identifier in a column list to prevent SQL injection
@@ -740,8 +753,9 @@ function validateColumnNames(columnNames) {
740
753
  * @throws Error if the identifier is empty or contains a NUL byte
741
754
  */
742
755
  function escapeQuotedIdentifier(identifier) {
743
- 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.`);
744
- return identifier.replaceAll("\"", "\"\"");
756
+ const value = String(identifier);
757
+ if (value.length === 0 || value.includes("\0")) throw new Error(`Invalid column name: ${JSON.stringify(value)}. Column names must be non-empty and must not contain a NUL byte.`);
758
+ return value.replaceAll("\"", "\"\"");
745
759
  }
746
760
  /**
747
761
  * Escapes a value for interpolation inside a single-quoted SQL string literal
@@ -757,8 +771,9 @@ function escapeQuotedIdentifier(identifier) {
757
771
  * @throws Error if the value contains a NUL byte
758
772
  */
759
773
  function escapeStringLiteral(value) {
760
- if (value.includes("\0")) throw new Error(`Invalid SQL string literal: ${JSON.stringify(value)}. Values must not contain a NUL byte.`);
761
- return value.replaceAll("'", "''");
774
+ const text = String(value);
775
+ if (text.includes("\0")) throw new Error(`Invalid SQL string literal: ${JSON.stringify(text)}. Values must not contain a NUL byte.`);
776
+ return text.replaceAll("'", "''");
762
777
  }
763
778
  /**
764
779
  * Validates an index name to prevent SQL injection
@@ -767,9 +782,9 @@ function escapeStringLiteral(value) {
767
782
  * @throws Error if index name contains invalid characters
768
783
  */
769
784
  function validateIndexName(indexName) {
770
- 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.`);
785
+ if (typeof indexName !== "string" || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(indexName)) throw new Error(`Invalid index name: ${indexName}. Index names must be a string that starts with a letter or underscore and contains only alphanumeric characters and underscores.`);
771
786
  }
772
787
  //#endregion
773
788
  export { validateColumnName as a, validateTableName as c, formatDbError as d, parseConditionKey as f, unwrapRawKey as h, generateCreateIndexStatement as i, DatabaseSchemaManager as l, resolveInsertColumns as m, escapeStringLiteral as n, validateColumnNames as o, raw as p, generateAddColumnStatement as r, validateIndexName as s, escapeQuotedIdentifier as t, buildWhere as u };
774
789
 
775
- //# sourceMappingURL=alter-utils-F8JQn1LL.js.map
790
+ //# sourceMappingURL=alter-utils-DeDDAATm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"alter-utils-F8JQn1LL.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\ndeclare const rawSqlKeyBrand: unique symbol;\n\n/**\n * A WHERE-clause key produced by {@link raw}: SQL text the caller vouches for,\n * exempt from the identifier validation `buildWhere` applies to every other key.\n */\nexport type RawSqlKey = string & { readonly [rawSqlKeyBrand]: true };\n\n/**\n * Marks a condition key as caller-authored SQL text.\n *\n * A fixed, non-secret sentinel rather than a random per-process token. A random\n * token has to travel inside the key, which means it reaches every place a key\n * is echoed — error messages, logs, each adapter's `DatabaseError` context — and\n * one disclosure would let a caller mint raw keys for the life of the process.\n * A registry of minted expressions avoids that but is not referentially\n * transparent: it would make `raw('revenue >')` in one query change what the\n * plain key `'revenue >'` means in every other one.\n *\n * The NUL delimiters cannot appear in an identifier or in SQL a developer would\n * write, so the marker never collides with a legitimate key.\n */\nconst RAW_KEY_PREFIX = '\\u0000hv-sql-raw\\u0000';\n\n/**\n * Marks a WHERE-clause key as SQL expression text rather than an identifier.\n *\n * {@link buildWhere} validates every condition key as a plain identifier. Wrap a\n * key in `raw()` to opt one condition out of that check:\n *\n * ```typescript\n * buildWhere({\n * status: 'paid', // validated as an identifier\n * [raw('SUM(total) >')]: 100, // caller-authored SQL, not validated\n * });\n * ```\n *\n * Never build the argument from end-user input. `raw()` is an assertion that\n * the caller, not the request, authored this SQL.\n *\n * The returned key carries a marker, so it is a distinct string from the bare\n * expression: minting `raw('revenue >')` never changes what the plain key\n * `'revenue >'` means anywhere else.\n *\n * The marker is a fixed sentinel, not a secret. It stops an expression key being\n * used by accident — the shape of the key no longer grants raw access, a call to\n * `raw()` does — but a caller that controls a whole key string verbatim,\n * including the NUL-delimited marker, can still reproduce it. Validate at your\n * own trust boundary; do not rely on this as the only barrier against hostile\n * input.\n *\n * Enforcement is at runtime: `WhereClause` keys are plain `string`, so a\n * computed key widens and TypeScript will not reject an unmarked expression key\n * at the call site.\n *\n * A trailing operator is recognised only if it is one of `=`, `!=`, `>`, `>=`,\n * `<`, `<=`, `like`, `in`, `not in`. Any other trailing token is treated as part\n * of the expression and `=` is appended, so `raw('name ILIKE')` silently emits\n * `name ILIKE = $1`, which the database then rejects. Fold an unsupported\n * operator into the expression instead: `raw('LOWER(name) like')`.\n *\n * @param expression - SQL field or expression text, optionally ending in a\n * supported operator (for example `SUM(total) >`)\n * @returns A branded key for use in a {@link WhereClause}\n * @throws If the expression is empty\n */\nexport function raw(expression: string): RawSqlKey {\n // Unwrap first so raw(raw(x)) === raw(x) and a marker can never nest into the\n // emitted SQL.\n const trimmed = unwrapRawKey(expression).key.trim();\n if (!trimmed) {\n throw new Error('raw() requires a non-empty SQL expression');\n }\n return `${RAW_KEY_PREFIX}${trimmed}` as RawSqlKey;\n}\n\n/**\n * Splits the raw marker off a condition key.\n * @internal\n */\nexport function unwrapRawKey(fullKey: string): {\n key: string;\n isRaw: boolean;\n} {\n return fullKey.startsWith(RAW_KEY_PREFIX)\n ? { key: fullKey.slice(RAW_KEY_PREFIX.length), isRaw: true }\n : { key: fullKey, isRaw: false };\n}\n\nexport function parseConditionKey(fullKey: string): {\n field: string;\n operator: string;\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 };\n }\n }\n\n // Deliberately no `explicitOperator` flag: \"key ends in an operator\" was the\n // condition that used to suppress identifier validation, and that was the\n // vulnerability. Nothing should gate trust on the shape of the key again.\n return { field: trimmed, operator: '=' };\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 { key, isRaw } = unwrapRawKey(fullKey);\n const { field, operator } = parseConditionKey(key);\n if (!isRaw && !isSimpleSqlIdentifier(field)) {\n throw new Error(\n `Invalid SQL identifier: ${field}. Condition keys must be plain ` +\n 'identifiers; wrap developer-authored SQL expressions in raw().',\n );\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 * Every condition key is validated as a plain SQL identifier, with or without\n * an operator suffix. To use expression text as a key, wrap it in {@link raw}:\n *\n * ```typescript\n * buildWhere({ [raw('LOWER(status) =')]: 'paid' });\n * ```\n *\n * `raw()` is the only way to reach expression text, so mapping untrusted input\n * into a condition key throws rather than emitting attacker-controlled SQL.\n *\n * @returns Object containing the SQL clause and array of values\n * @throws If a key that is not wrapped in {@link raw} is not a plain identifier\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;;;;;;;;;;;;;;;AAwBA,IAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CvB,SAAgB,IAAI,YAA+B;CAGjD,MAAM,UAAU,aAAa,UAAU,CAAC,CAAC,IAAI,KAAK;CAClD,IAAI,CAAC,SACH,MAAM,IAAI,MAAM,2CAA2C;CAE7D,OAAO,GAAG,iBAAiB;AAC7B;;;;;AAMA,SAAgB,aAAa,SAG3B;CACA,OAAO,QAAQ,WAAW,cAAc,IACpC;EAAE,KAAK,QAAQ,MAAM,EAAqB;EAAG,OAAO;CAAK,IACzD;EAAE,KAAK;EAAS,OAAO;CAAM;AACnC;AAEA,SAAgB,kBAAkB,SAGhC;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;CACF;CAOJ,OAAO;EAAE,OAAO;EAAS,UAAU;CAAI;AACzC;;;;;AAWA,IAAM,kBACJ,SACA,OACA,WACA,gBACmC;CACnC,MAAM,EAAE,KAAK,UAAU,aAAa,OAAO;CAC3C,MAAM,EAAE,OAAO,aAAa,kBAAkB,GAAG;CACjD,IAAI,CAAC,SAAS,CAAC,sBAAsB,KAAK,GACxC,MAAM,IAAI,MACR,2BAA2B,MAAM,8FAEnC;CAEF,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FA,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;;;;;;;;;AC1aA,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
+ {"version":3,"file":"alter-utils-DeDDAATm.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\ndeclare const rawSqlKeyBrand: unique symbol;\n\n/**\n * A WHERE-clause key produced by {@link raw}: SQL text the caller vouches for,\n * exempt from the identifier validation `buildWhere` applies to every other key.\n */\nexport type RawSqlKey = string & { readonly [rawSqlKeyBrand]: true };\n\n/**\n * Marks a condition key as caller-authored SQL text.\n *\n * A fixed, non-secret sentinel rather than a random per-process token. A random\n * token has to travel inside the key, which means it reaches every place a key\n * is echoed — error messages, logs, each adapter's `DatabaseError` context — and\n * one disclosure would let a caller mint raw keys for the life of the process.\n * A registry of minted expressions avoids that but is not referentially\n * transparent: it would make `raw('revenue >')` in one query change what the\n * plain key `'revenue >'` means in every other one.\n *\n * The NUL delimiters cannot appear in an identifier or in SQL a developer would\n * write, so the marker never collides with a legitimate key.\n */\nconst RAW_KEY_PREFIX = '\\u0000hv-sql-raw\\u0000';\n\n/**\n * Marks a WHERE-clause key as SQL expression text rather than an identifier.\n *\n * {@link buildWhere} validates every condition key as a plain identifier. Wrap a\n * key in `raw()` to opt one condition out of that check:\n *\n * ```typescript\n * buildWhere({\n * status: 'paid', // validated as an identifier\n * [raw('SUM(total) >')]: 100, // caller-authored SQL, not validated\n * });\n * ```\n *\n * Never build the argument from end-user input. `raw()` is an assertion that\n * the caller, not the request, authored this SQL.\n *\n * The returned key carries a marker, so it is a distinct string from the bare\n * expression: minting `raw('revenue >')` never changes what the plain key\n * `'revenue >'` means anywhere else.\n *\n * The marker is a fixed sentinel, not a secret. It stops an expression key being\n * used by accident — the shape of the key no longer grants raw access, a call to\n * `raw()` does — but a caller that controls a whole key string verbatim,\n * including the NUL-delimited marker, can still reproduce it. Validate at your\n * own trust boundary; do not rely on this as the only barrier against hostile\n * input.\n *\n * Enforcement is at runtime: `WhereClause` keys are plain `string`, so a\n * computed key widens and TypeScript will not reject an unmarked expression key\n * at the call site.\n *\n * A trailing operator is recognised only if it is one of `=`, `!=`, `>`, `>=`,\n * `<`, `<=`, `like`, `in`, `not in`. Any other trailing token is treated as part\n * of the expression and `=` is appended, so `raw('name ILIKE')` silently emits\n * `name ILIKE = $1`, which the database then rejects. Fold an unsupported\n * operator into the expression instead: `raw('LOWER(name) like')`.\n *\n * @param expression - SQL field or expression text, optionally ending in a\n * supported operator (for example `SUM(total) >`)\n * @returns A branded key for use in a {@link WhereClause}\n * @throws If the expression is empty\n */\nexport function raw(expression: string): RawSqlKey {\n // Unwrap first so raw(raw(x)) === raw(x) and a marker can never nest into the\n // emitted SQL.\n const trimmed = unwrapRawKey(expression).key.trim();\n if (!trimmed) {\n throw new Error('raw() requires a non-empty SQL expression');\n }\n return `${RAW_KEY_PREFIX}${trimmed}` as RawSqlKey;\n}\n\n/**\n * Splits the raw marker off a condition key.\n * @internal\n */\nexport function unwrapRawKey(fullKey: string): {\n key: string;\n isRaw: boolean;\n} {\n return fullKey.startsWith(RAW_KEY_PREFIX)\n ? { key: fullKey.slice(RAW_KEY_PREFIX.length), isRaw: true }\n : { key: fullKey, isRaw: false };\n}\n\nexport function parseConditionKey(fullKey: string): {\n field: string;\n operator: string;\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 };\n }\n }\n\n // Deliberately no `explicitOperator` flag: \"key ends in an operator\" was the\n // condition that used to suppress identifier validation, and that was the\n // vulnerability. Nothing should gate trust on the shape of the key again.\n return { field: trimmed, operator: '=' };\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 { key, isRaw } = unwrapRawKey(fullKey);\n const { field, operator } = parseConditionKey(key);\n if (!isRaw && !isSimpleSqlIdentifier(field)) {\n throw new Error(\n `Invalid SQL identifier: ${field}. Condition keys must be plain ` +\n 'identifiers; wrap developer-authored SQL expressions in raw().',\n );\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 * Every condition key is validated as a plain SQL identifier, with or without\n * an operator suffix. To use expression text as a key, wrap it in {@link raw}:\n *\n * ```typescript\n * buildWhere({ [raw('LOWER(status) =')]: 'paid' });\n * ```\n *\n * `raw()` is the only way to reach expression text, so mapping untrusted input\n * into a condition key throws rather than emitting attacker-controlled SQL.\n *\n * @returns Object containing the SQL clause and array of values\n * @throws If a key that is not wrapped in {@link raw} is not a plain identifier\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 * The argument must be a primitive string. `RegExp.prototype.test` coerces its\n * argument with `toString`, so an object whose `toString` returns a valid name\n * on the call the validator makes and a different one on the call the caller's\n * `${table}` interpolation makes would pass here and then reach SQL. Rejecting\n * non-strings up front closes that gap: the value validated is the value\n * interpolated, because a primitive string cannot observe how many times it is\n * read.\n *\n * @param tableName - Table name to validate\n * @throws Error if the value is not a string or contains invalid characters\n */\nexport function validateTableName(tableName: string): void {\n if (\n typeof tableName !== 'string' ||\n !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)\n ) {\n throw new Error(\n `Invalid table name: ${tableName}. Table names must be a string that starts with a letter or underscore and contains only alphanumeric characters and underscores.`,\n );\n }\n}\n\n/**\n * Validates a column name to prevent SQL injection\n *\n * Rejects non-strings for the same reason as {@link validateTableName}: a value\n * whose `toString` differs between the validation read and the interpolation\n * read would otherwise slip a different identifier into SQL than the one\n * checked here.\n *\n * @param columnName - Column name to validate\n * @throws Error if the value is not a string or contains invalid characters\n */\nexport function validateColumnName(columnName: string): void {\n if (\n typeof columnName !== 'string' ||\n !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(columnName)\n ) {\n throw new Error(\n `Invalid column name: ${columnName}. Column names must be a string that starts with a letter or underscore and contains 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 // Coerce to a primitive string first. The escape's safety rests on\n // `replaceAll`/`includes` being the real `String` methods, but a crafted\n // object could supply its own that return unescaped SQL while its `toString`\n // reads as a benign name elsewhere. Coercing once runs its `toString` here\n // and pins an immutable primitive for the length check and the escape, so the\n // value escaped is the value returned.\n const value = String(identifier);\n if (value.length === 0 || value.includes('\\0')) {\n throw new Error(\n `Invalid column name: ${JSON.stringify(value)}. Column names must be non-empty and must not contain a NUL byte.`,\n );\n }\n return value.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 // Coerce to a primitive string first, for the same reason as\n // {@link escapeQuotedIdentifier}: the escape must run on an immutable\n // primitive, not on an object that could supply its own `replaceAll`.\n const text = String(value);\n if (text.includes('\\0')) {\n throw new Error(\n `Invalid SQL string literal: ${JSON.stringify(text)}. Values must not contain a NUL byte.`,\n );\n }\n return text.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 (\n typeof indexName !== 'string' ||\n !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(indexName)\n ) {\n throw new Error(\n `Invalid index name: ${indexName}. Index names must be a string that starts with a letter or underscore and contains only alphanumeric characters and underscores.`,\n );\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { a as validateColumnName, c as validateTableName, d as formatDbError, i as generateCreateIndexStatement, l as DatabaseSchemaManager, m as resolveInsertColumns, n as escapeStringLiteral, o as validateColumnNames, r as generateAddColumnStatement, s as validateIndexName, t as escapeQuotedIdentifier, u as buildWhere } from "./alter-utils-F8JQn1LL.js";
1
+ import { a as validateColumnName, c as validateTableName, d as formatDbError, i as generateCreateIndexStatement, l as DatabaseSchemaManager, m as resolveInsertColumns, n as escapeStringLiteral, o as validateColumnNames, r as generateAddColumnStatement, s as validateIndexName, t as escapeQuotedIdentifier, u as buildWhere } from "./alter-utils-DeDDAATm.js";
2
2
  import { t as convertUniqueIndexesToInlineConstraints } from "./duckdb-schema-utils-B-XBP8QF.js";
3
3
  import { n as resolveSchemas, r as createTransactionLock, t as NestedTransactionError } from "./types-uVFHgaSU.js";
4
4
  import { DatabaseError } from "@happyvertical/utils";
@@ -277,7 +277,7 @@ async function getDatabase(options = {}) {
277
277
  const setClause = keys.map((key, idx) => `${key} = $${idx + 1}`).join(", ");
278
278
  const { sql: whereClause, values: whereValues } = buildWhere(where, keys.length + 1, "duckdb");
279
279
  const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
280
- const values = [...Object.values(data), ...whereValues];
280
+ const values = [...keys.map((key) => data[key]), ...whereValues];
281
281
  try {
282
282
  await connection.run(sql, values);
283
283
  if (writeStrategy === "immediate") await exportTableToJSON(connection, table, dataDir);
@@ -355,17 +355,19 @@ async function getDatabase(options = {}) {
355
355
  */
356
356
  const upsert = async (table, conflictColumns, data, options) => {
357
357
  validateTableName(table);
358
- const missingColumns = conflictColumns.filter((col) => !(col in data));
358
+ const record = { ...data };
359
+ const conflictCols = conflictColumns.map((col) => String(col));
360
+ const missingColumns = conflictCols.filter((col) => !(col in record));
359
361
  if (missingColumns.length > 0) throw new DatabaseError("Conflict columns missing from data", {
360
362
  table,
361
- conflictColumns,
363
+ conflictColumns: conflictCols,
362
364
  missingColumns,
363
- availableColumns: Object.keys(data),
365
+ availableColumns: Object.keys(record),
364
366
  hint: "All columns specified in ON CONFLICT must be present in the data being inserted. Undefined values should be replaced with null or an appropriate default."
365
367
  });
366
- if (!options?.nullsDistinct && conflictColumns.some((col) => data[col] === null)) return executeNullAwareDuckDBUpsert(table, conflictColumns, data);
367
- const keys = Object.keys(data);
368
- const dataValues = Object.values(data);
368
+ if (!options?.nullsDistinct && conflictCols.some((col) => record[col] === null)) return executeNullAwareDuckDBUpsert(table, conflictCols, record);
369
+ const keys = Object.keys(record);
370
+ const dataValues = Object.values(record);
369
371
  const placeholders = [];
370
372
  const values = [];
371
373
  let paramIdx = 1;
@@ -418,7 +420,7 @@ async function getDatabase(options = {}) {
418
420
  paramIdx++;
419
421
  }
420
422
  }
421
- const conflict = conflictColumns.map((col) => `"${escapeQuotedIdentifier(col)}"`).join(", ");
423
+ const conflict = conflictCols.map((col) => `"${escapeQuotedIdentifier(col)}"`).join(", ");
422
424
  const quotedKeys = keys.map((key) => `"${escapeQuotedIdentifier(key)}"`).join(", ");
423
425
  const quotedUpdateSetParts = keys.map((key, i) => {
424
426
  const part = updateSetParts[i];
@@ -438,7 +440,7 @@ async function getDatabase(options = {}) {
438
440
  table,
439
441
  sql,
440
442
  values,
441
- conflictColumns,
443
+ conflictColumns: conflictCols,
442
444
  originalError: formatDbError(e)
443
445
  });
444
446
  }
@@ -1072,4 +1074,4 @@ async function getDatabase(options = {}) {
1072
1074
  //#endregion
1073
1075
  export { getDatabase };
1074
1076
 
1075
- //# sourceMappingURL=duckdb-SVvTBGT0.js.map
1077
+ //# sourceMappingURL=duckdb-jSSa2MbC.js.map