@happyvertical/sql 0.79.0 → 0.80.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/AGENT.md +1 -1
  2. package/dist/backup/index.js +210 -188
  3. package/dist/backup/index.js.map +1 -1
  4. package/dist/chunks/alter-utils-K7tiuj5L.js +135 -0
  5. package/dist/chunks/alter-utils-K7tiuj5L.js.map +1 -0
  6. package/dist/chunks/duckdb-DykBBzd6.js +1056 -0
  7. package/dist/chunks/duckdb-DykBBzd6.js.map +1 -0
  8. package/dist/chunks/duckdb-schema-utils-B-XBP8QF.js +115 -0
  9. package/dist/chunks/duckdb-schema-utils-B-XBP8QF.js.map +1 -0
  10. package/dist/chunks/postgres-cli-DzZcXSIG.js +272 -0
  11. package/dist/chunks/postgres-cli-DzZcXSIG.js.map +1 -0
  12. package/dist/chunks/schema-manager-a8RlxIE2.js +450 -0
  13. package/dist/chunks/schema-manager-a8RlxIE2.js.map +1 -0
  14. package/dist/chunks/sqlite-YgU9LCVy.js +1045 -0
  15. package/dist/chunks/sqlite-YgU9LCVy.js.map +1 -0
  16. package/dist/chunks/sqlite-native-BGtpx7Ro.js +902 -0
  17. package/dist/chunks/sqlite-native-BGtpx7Ro.js.map +1 -0
  18. package/dist/cli/claude-context.js +17 -17
  19. package/dist/cli/claude-context.js.map +1 -1
  20. package/dist/index.js +3244 -35
  21. package/dist/index.js.map +1 -1
  22. package/metadata.json +2 -2
  23. package/package.json +5 -5
  24. package/dist/chunks/duckdb-B6m78QdL.js +0 -924
  25. package/dist/chunks/duckdb-B6m78QdL.js.map +0 -1
  26. package/dist/chunks/index-CzdYvRFp.js +0 -3692
  27. package/dist/chunks/index-CzdYvRFp.js.map +0 -1
  28. package/dist/chunks/postgres-cli-Cr6bBZ3q.js +0 -209
  29. package/dist/chunks/postgres-cli-Cr6bBZ3q.js.map +0 -1
  30. package/dist/chunks/sqlite-CPCNeJIH.js +0 -937
  31. package/dist/chunks/sqlite-CPCNeJIH.js.map +0 -1
  32. package/dist/chunks/sqlite-native-DqVPKNUu.js +0 -1129
  33. package/dist/chunks/sqlite-native-DqVPKNUu.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duckdb-DykBBzd6.js","names":[],"sources":["../../src/duckdb.ts"],"sourcesContent":["import { readdir } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { basename, extname, join } from 'node:path';\nimport { DatabaseError } from '@happyvertical/utils';\nimport { DatabaseSchemaManager } from './schema-manager';\nimport {\n generateAddColumnStatement,\n generateCreateIndexStatement,\n validateColumnName,\n validateIndexName,\n validateTableName,\n} from './shared/alter-utils';\nimport { convertUniqueIndexesToInlineConstraints } from './shared/duckdb-schema-utils';\nimport type {\n ColumnDefinition,\n ColumnDefinitionWithName,\n DatabaseInterface,\n DuckDBOptions,\n IndexDefinition,\n QueryResult,\n SchemaInitializationOptions,\n TableInterface,\n TableSchemaInfo,\n TransactionHandle,\n UpsertOptions,\n} from './shared/types';\nimport { resolveSchemas } from './shared/types';\nimport { buildWhere, formatDbError } from './shared/utils';\n\n/**\n * Creates tables from provided schema definitions\n *\n * DuckDB has a known limitation (issue #12684) where ON CONFLICT clauses\n * fail with UNIQUE INDEX but work with inline UNIQUE constraints.\n * This function converts UNIQUE indexes to inline constraints for compatibility.\n *\n * @param connection - DuckDB connection\n * @param schemas - Schema definitions to create\n */\nasync function createTablesFromSchemas(\n connection: any,\n schemas: Record<string, import('./shared/types').SchemaProvider>,\n): Promise<void> {\n for (const [tableName, schema] of Object.entries(schemas)) {\n try {\n // Check if table already exists\n const result = await connection.runAndReadAll(\n `SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`,\n );\n const rows = result.getRowObjects();\n\n if (rows.length > 0) {\n console.log(`[duckdb] Table ${tableName} already exists, skipping`);\n continue;\n }\n\n console.log(`[duckdb] Creating table ${tableName} from provided schema`);\n\n // Convert UNIQUE indexes to inline UNIQUE constraints for DuckDB compatibility\n // DuckDB's ON CONFLICT requires inline constraints, not separate indexes\n const transformed = convertUniqueIndexesToInlineConstraints(\n schema.ddl,\n schema.indexes,\n );\n\n // Create table from transformed DDL\n await connection.run(transformed.ddl);\n\n // Create remaining indexes (non-UNIQUE indexes only)\n // UNIQUE indexes have been converted to inline constraints\n if (transformed.indexes && transformed.indexes.length > 0) {\n for (const indexSQL of transformed.indexes) {\n try {\n await connection.run(indexSQL);\n } catch (error) {\n console.warn(\n `[duckdb] Failed to create index for ${tableName}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n }\n\n // Create triggers (if supported)\n if (schema.triggers && schema.triggers.length > 0) {\n for (const triggerSQL of schema.triggers) {\n try {\n await connection.run(triggerSQL);\n } catch (error) {\n console.warn(\n `[duckdb] Failed to create trigger for ${tableName}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n }\n } catch (error) {\n throw new DatabaseError(\n `Failed to create table ${tableName} from schema`,\n {\n tableName,\n schema,\n originalError: error instanceof Error ? error.message : String(error),\n },\n );\n }\n }\n}\n\n/**\n * Resolves a DuckDB URL, handling :memory: variants to prevent file leakage.\n *\n * DuckDB interprets any string as a file path unless it's exactly ':memory:'.\n * URLs like ':memory:12345' create files named ':memory:12345' in the working directory.\n *\n * This function:\n * - Returns ':memory:' unchanged (pure in-memory database)\n * - Converts ':memory:*' patterns to temp files in os.tmpdir()\n * - Returns other URLs unchanged (file paths, etc.)\n *\n * @param url - The database URL to resolve\n * @returns The resolved URL safe for DuckDB\n */\nfunction resolveDuckDBUrl(url: string): string {\n // Pure in-memory: use as-is\n if (url === ':memory:') {\n return url;\n }\n\n // Pattern: :memory: followed by anything (e.g., :memory:12345-0.5678)\n // These would create files in CWD - redirect to temp directory instead\n if (url.startsWith(':memory:')) {\n const suffix = url.slice(':memory:'.length);\n const tempPath = join(tmpdir(), `duckdb-memory-${suffix}.db`);\n return tempPath;\n }\n\n // Other URLs (file paths, etc.): use as-is\n return url;\n}\n\n/**\n * Creates a DuckDB connection instance\n *\n * @param options - DuckDB connection options\n * @returns Promise resolving to a DuckDB connection\n */\nasync function createDuckDBConnection(options: DuckDBOptions) {\n const {\n url = ':memory:',\n dataDir = './data',\n autoRegisterJSON = true,\n } = options;\n const schemas = resolveSchemas(options.schemas) ?? {};\n\n // Resolve URL to prevent file leakage from :memory:* patterns\n const resolvedUrl = resolveDuckDBUrl(url);\n\n try {\n // Dynamic import to avoid bundling\n const duckdbModule = '@duckdb/node-api';\n const { DuckDBInstance } = await import(/* @vite-ignore */ duckdbModule);\n\n // Create DuckDB instance with resolved URL\n const instance = await DuckDBInstance.create(resolvedUrl);\n const connection = await instance.connect();\n\n // Create tables from provided schemas first\n if (schemas && Object.keys(schemas).length > 0) {\n await createTablesFromSchemas(connection, schemas);\n }\n\n // Auto-register JSON files if enabled\n if (autoRegisterJSON && dataDir) {\n await registerJSONFiles(connection, dataDir);\n }\n\n return connection;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n throw new DatabaseError(\n `Failed to create DuckDB connection: ${errorMessage}`,\n {\n url,\n originalError: errorMessage,\n },\n );\n }\n}\n\n/**\n * Scans the data directory and registers JSON files as queryable tables\n *\n * @param connection - DuckDB connection\n * @param dataDir - Directory containing JSON files\n */\nasync function registerJSONFiles(connection: any, dataDir: string) {\n try {\n const files = await readdir(dataDir);\n const jsonFiles = files.filter(\n (file) => extname(file).toLowerCase() === '.json',\n );\n\n for (const file of jsonFiles) {\n const filePath = join(dataDir, file);\n const tableName = basename(file, '.json');\n\n // Create view that reads from JSON file\n // DuckDB automatically infers schema with ISO date/timestamp parsing\n await connection.run(\n `CREATE OR REPLACE VIEW ${tableName} AS SELECT * FROM read_json('${filePath}', auto_detect=true, format='auto', timestampformat='iso', dateformat='iso')`,\n );\n }\n } catch (error) {\n // If directory doesn't exist, that's okay - user might not have JSON files yet\n if ((error as any).code !== 'ENOENT') {\n throw new DatabaseError(`Failed to register JSON files from ${dataDir}`, {\n dataDir,\n originalError: error instanceof Error ? error.message : String(error),\n });\n }\n }\n}\n\n/**\n * Converts DuckDB-specific type representations to JavaScript types\n *\n * Handles two cases:\n * 1. JavaScript BigInt values → converted to numbers\n * 2. DuckDB timestamp objects ({ micros: number }) → converted to Date objects\n *\n * DuckDB represents TIMESTAMP values as objects with a single property\n * called 'micros' containing microseconds since Unix epoch. This function\n * converts them to JavaScript Date objects for proper serialization.\n *\n * @param obj - Object that may contain BigInt or timestamp values\n * @returns Object with DuckDB types converted to JavaScript types\n */\nfunction convertBigInts(obj: any): any {\n if (obj === null || obj === undefined) return obj;\n if (typeof obj === 'bigint') return Number(obj);\n\n // Handle DuckDB timestamp objects: { micros: number | bigint }\n // Convert to JavaScript Date for proper serialization\n // Fixes issue #314: DuckDB returns dates as { micros } objects\n if (\n typeof obj === 'object' &&\n !Array.isArray(obj) &&\n 'micros' in obj &&\n Object.keys(obj).length === 1 &&\n (typeof obj.micros === 'number' || typeof obj.micros === 'bigint')\n ) {\n // Convert microseconds to milliseconds for Date constructor\n // DuckDB stores timestamps as microseconds since Unix epoch\n // Handle both number and bigint types (DuckDB can return either)\n const micros =\n typeof obj.micros === 'bigint' ? Number(obj.micros) : obj.micros;\n const milliseconds = micros / 1000;\n return new Date(milliseconds);\n }\n\n if (Array.isArray(obj)) return obj.map(convertBigInts);\n if (typeof obj === 'object') {\n const result: any = {};\n for (const key in obj) {\n result[key] = convertBigInts(obj[key]);\n }\n return result;\n }\n return obj;\n}\n\n/**\n * Creates a DuckDB database adapter\n *\n * @param options - DuckDB connection options\n * @returns Database interface for DuckDB\n */\nexport async function getDatabase(\n options: DuckDBOptions = {},\n): Promise<DatabaseInterface> {\n const connection = await createDuckDBConnection(options);\n const writeStrategy = options.writeStrategy || 'none';\n const dataDir = options.dataDir || './data';\n // Use resolved URL to reflect actual database location\n const url = resolveDuckDBUrl(options.url || ':memory:');\n\n /**\n * Inserts one or more records into a table\n *\n * @param table - Table name\n * @param data - Single record or array of records to insert\n * @returns Promise resolving to operation result\n * @throws Error if the insert operation fails\n */\n const insert = async (\n table: string,\n data: Record<string, any> | Record<string, any>[],\n ): Promise<QueryResult> => {\n const records = Array.isArray(data) ? data : [data];\n\n if (records.length === 0) {\n return { operation: 'insert', affected: 0 };\n }\n\n const keys = Object.keys(records[0]);\n const values: any[] = [];\n let paramIdx = 1;\n\n // Build placeholders with proper type casting for DuckDB\n const placeholders = records\n .map((record) => {\n const rowPlaceholders = keys.map((key) => {\n const value = record[key];\n\n if (value === null || value === undefined) {\n return 'NULL';\n } else if (value === '' && typeof value === 'string') {\n // CAST empty strings to TEXT to prevent DuckDB ANY type inference\n values.push(value);\n return `CAST($${paramIdx++} AS TEXT)`;\n } else if (value instanceof Date) {\n // Convert Date objects to ISO strings for DuckDB\n values.push(value.toISOString());\n return `$${paramIdx++}`;\n } else if (Array.isArray(value)) {\n // CAST arrays to JSON to prevent DuckDB ANY type inference\n values.push(JSON.stringify(value));\n return `CAST($${paramIdx++} AS JSON)`;\n } else if (\n typeof value === 'object' &&\n value !== null &&\n Object.getPrototypeOf(value) === Object.prototype\n ) {\n // CAST plain objects to JSON to prevent DuckDB ANY type inference\n values.push(JSON.stringify(value));\n return `CAST($${paramIdx++} AS JSON)`;\n } else {\n // Direct parameter binding for other values\n values.push(value);\n return `$${paramIdx++}`;\n }\n });\n return `(${rowPlaceholders.join(', ')})`;\n })\n .join(', ');\n\n const sql = `INSERT INTO ${table} (${keys.join(', ')}) VALUES ${placeholders}`;\n\n try {\n await connection.run(sql, values);\n const affected = records.length;\n\n // Handle write-back strategy\n if (writeStrategy === 'immediate') {\n await exportTableToJSON(connection, table, dataDir);\n }\n\n return { operation: 'insert', affected };\n } catch (e) {\n throw new DatabaseError('Failed to insert records into table', {\n table,\n sql,\n values,\n originalError: formatDbError(e),\n });\n }\n };\n\n /**\n * Retrieves a single record matching the where criteria\n *\n * @param table - Table name\n * @param where - Criteria to match records\n * @returns Promise resolving to matching record or null if not found\n * @throws Error if the query fails\n */\n const get = async (\n table: string,\n where: Record<string, any>,\n ): Promise<Record<string, any> | null> => {\n const { sql: whereClause, values } = buildWhere(where, 1, 'duckdb');\n const sql = `SELECT * FROM ${table} ${whereClause} LIMIT 1`;\n\n try {\n const reader = await connection.runAndReadAll(sql, values);\n const rows = reader.getRowObjects();\n return rows.length > 0 ? convertBigInts(rows[0]) : null;\n } catch (e) {\n throw new DatabaseError('Failed to retrieve record from table', {\n table,\n sql,\n values,\n originalError: formatDbError(e),\n });\n }\n };\n\n /**\n * Retrieves multiple records matching the where criteria\n *\n * @param table - Table name\n * @param where - Criteria to match records\n * @returns Promise resolving to array of matching records\n * @throws Error if the query fails\n */\n const list = async (\n table: string,\n where: Record<string, any>,\n ): Promise<Record<string, any>[]> => {\n const { sql: whereClause, values } = buildWhere(where, 1, 'duckdb');\n const sql = `SELECT * FROM ${table} ${whereClause}`;\n\n try {\n const reader = await connection.runAndReadAll(sql, values);\n return convertBigInts(reader.getRowObjects());\n } catch (e) {\n throw new DatabaseError('Failed to list records from table', {\n table,\n sql,\n values,\n originalError: formatDbError(e),\n });\n }\n };\n\n /**\n * Updates records matching the where criteria\n *\n * @param table - Table name\n * @param where - Criteria to match records to update\n * @param data - New data to set\n * @returns Promise resolving to operation result\n * @throws Error if the update operation fails\n */\n const update = async (\n table: string,\n where: Record<string, any>,\n data: Record<string, any>,\n ): Promise<QueryResult> => {\n const keys = Object.keys(data);\n const setClause = keys.map((key, idx) => `${key} = $${idx + 1}`).join(', ');\n const { sql: whereClause, values: whereValues } = buildWhere(\n where,\n keys.length + 1,\n 'duckdb',\n );\n\n const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;\n const values = [...Object.values(data), ...whereValues];\n\n try {\n await connection.run(sql, values);\n\n // Handle write-back strategy\n if (writeStrategy === 'immediate') {\n await exportTableToJSON(connection, table, dataDir);\n }\n\n // DuckDB doesn't return rowsAffected in the same way, estimate from where clause\n return { operation: 'update', affected: 1 };\n } catch (e) {\n throw new DatabaseError('Failed to update records in table', {\n table,\n sql,\n values,\n originalError: formatDbError(e),\n });\n }\n };\n\n const buildDuckDBValueExpression = (\n value: any,\n values: any[],\n paramIdx: { value: number },\n ): string => {\n if (value === null || value === undefined) {\n return 'NULL';\n }\n if (value === '' && typeof value === 'string') {\n values.push(value);\n return `CAST($${paramIdx.value++} AS TEXT)`;\n }\n if (value instanceof Date) {\n values.push(value.toISOString());\n return `$${paramIdx.value++}`;\n }\n if (Array.isArray(value)) {\n values.push(JSON.stringify(value));\n return `CAST($${paramIdx.value++} AS JSON)`;\n }\n if (\n typeof value === 'object' &&\n value !== null &&\n Object.getPrototypeOf(value) === Object.prototype\n ) {\n values.push(JSON.stringify(value));\n return `CAST($${paramIdx.value++} AS JSON)`;\n }\n\n values.push(value);\n return `$${paramIdx.value++}`;\n };\n\n const executeNullAwareDuckDBUpsert = async (\n table: string,\n conflictColumns: string[],\n data: Record<string, any>,\n ): Promise<QueryResult> => {\n const conflictWhere = Object.fromEntries(\n conflictColumns.map((col) => [col, data[col]]),\n );\n const existing = await get(table, conflictWhere);\n\n if (!existing) {\n return insert(table, data);\n }\n\n const keys = Object.keys(data);\n const values: any[] = [];\n const paramIdx = { value: 1 };\n const assignments = keys.map((key) => {\n const valueExpr = buildDuckDBValueExpression(data[key], values, paramIdx);\n return `\"${key}\" = ${valueExpr}`;\n });\n const { sql: whereClause, values: whereValues } = buildWhere(\n conflictWhere,\n paramIdx.value,\n 'duckdb',\n );\n const sql = `UPDATE ${table} SET ${assignments.join(', ')} ${whereClause}`;\n\n try {\n await connection.run(sql, [...values, ...whereValues]);\n\n if (writeStrategy === 'immediate') {\n await exportTableToJSON(connection, table, dataDir);\n }\n\n return { operation: 'upsert', affected: 1 };\n } catch (e) {\n throw new DatabaseError('Failed to upsert record into table', {\n table,\n sql,\n values: [...values, ...whereValues],\n conflictColumns,\n originalError: formatDbError(e),\n });\n }\n };\n\n /**\n * Inserts a record or updates it if it already exists (UPSERT)\n *\n * @param table - Table name\n * @param conflictColumns - Columns that define the uniqueness constraint\n * @param data - Data to insert or update\n * @returns Promise resolving to operation result\n * @throws Error if the upsert operation fails\n */\n const upsert = async (\n table: string,\n conflictColumns: string[],\n data: Record<string, any>,\n options?: UpsertOptions,\n ): Promise<QueryResult> => {\n // Validate that all conflict columns are present in the data\n const missingColumns = conflictColumns.filter((col) => !(col in data));\n\n if (missingColumns.length > 0) {\n throw new DatabaseError('Conflict columns missing from data', {\n table,\n conflictColumns,\n missingColumns,\n availableColumns: Object.keys(data),\n 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.',\n });\n }\n\n if (\n !options?.nullsDistinct &&\n conflictColumns.some((col) => data[col] === null)\n ) {\n return executeNullAwareDuckDBUpsert(table, conflictColumns, data);\n }\n\n const keys = Object.keys(data);\n const dataValues = Object.values(data);\n\n // Build placeholders and values with proper type handling for DuckDB\n // DuckDB cannot infer types from empty strings or certain values, so we need explicit CAST\n const placeholders: string[] = [];\n const values: any[] = [];\n let paramIdx = 1;\n\n for (const value of dataValues) {\n if (value === null || value === undefined) {\n // DuckDB requires explicit NULL for null/undefined values\n placeholders.push('NULL');\n } else if (value === '' && typeof value === 'string') {\n // CAST empty strings to TEXT to prevent DuckDB ANY type inference\n placeholders.push(`CAST($${paramIdx} AS TEXT)`);\n values.push(value);\n paramIdx++;\n } else if (value instanceof Date) {\n // Convert Date objects to ISO strings for DuckDB\n placeholders.push(`$${paramIdx}`);\n values.push(value.toISOString());\n paramIdx++;\n } else if (Array.isArray(value)) {\n // CAST arrays to JSON to prevent DuckDB ANY type inference\n // DuckDB cannot infer array element types from empty arrays or mixed types\n placeholders.push(`CAST($${paramIdx} AS JSON)`);\n values.push(JSON.stringify(value));\n paramIdx++;\n } else if (\n typeof value === 'object' &&\n value !== null &&\n Object.getPrototypeOf(value) === Object.prototype\n ) {\n // CAST plain objects to JSON to prevent DuckDB ANY type inference\n // Only applies to plain objects (not class instances)\n placeholders.push(`CAST($${paramIdx} AS JSON)`);\n values.push(JSON.stringify(value));\n paramIdx++;\n } else {\n // Direct parameter binding for other values\n placeholders.push(`$${paramIdx}`);\n values.push(value);\n paramIdx++;\n }\n }\n\n // Build UPDATE SET clause with same type handling\n // DO NOT reset paramIdx - parameters must be unique across entire query\n const updateSetParts: string[] = [];\n\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const value = dataValues[i];\n\n if (value === null || value === undefined) {\n // DuckDB requires explicit NULL for null/undefined values\n updateSetParts.push(`${key} = NULL`);\n } else if (value === '' && typeof value === 'string') {\n updateSetParts.push(`${key} = CAST($${paramIdx} AS TEXT)`);\n values.push(value);\n paramIdx++;\n } else if (value instanceof Date) {\n updateSetParts.push(`${key} = $${paramIdx}`);\n values.push(value.toISOString());\n paramIdx++;\n } else if (Array.isArray(value)) {\n // CAST arrays to JSON to prevent DuckDB ANY type inference\n updateSetParts.push(`${key} = CAST($${paramIdx} AS JSON)`);\n values.push(JSON.stringify(value));\n paramIdx++;\n } else if (\n typeof value === 'object' &&\n value !== null &&\n Object.getPrototypeOf(value) === Object.prototype\n ) {\n // CAST plain objects to JSON to prevent DuckDB ANY type inference\n updateSetParts.push(`${key} = CAST($${paramIdx} AS JSON)`);\n values.push(JSON.stringify(value));\n paramIdx++;\n } else {\n updateSetParts.push(`${key} = $${paramIdx}`);\n values.push(value);\n paramIdx++;\n }\n }\n\n // Quote ALL column names to match DuckDB's schema generation\n // SchemaGenerator always quotes column names, so UPSERT must match\n const conflict = conflictColumns.map((col) => `\"${col}\"`).join(', ');\n const quotedKeys = keys.map((key) => `\"${key}\"`).join(', ');\n\n // Quote column names in UPDATE SET clause to match schema and ON CONFLICT\n // Extract the value expression from each updateSetPart (everything after '=')\n const quotedUpdateSetParts = keys.map((key, i) => {\n const part = updateSetParts[i];\n const valueExpr = part.substring(part.indexOf('=') + 1).trim();\n return `\"${key}\" = ${valueExpr}`;\n });\n\n const sql = `INSERT INTO ${table} (${quotedKeys}) VALUES (${placeholders.join(', ')}) ON CONFLICT(${conflict}) DO UPDATE SET ${quotedUpdateSetParts.join(', ')}`;\n\n try {\n await connection.run(sql, values);\n\n // Handle write-back strategy\n if (writeStrategy === 'immediate') {\n await exportTableToJSON(connection, table, dataDir);\n }\n\n return { operation: 'upsert', affected: 1 };\n } catch (e) {\n throw new DatabaseError('Failed to upsert record into table', {\n table,\n sql,\n values,\n conflictColumns,\n originalError: formatDbError(e),\n });\n }\n };\n\n /**\n * Gets a record matching the where criteria or inserts it if not found\n *\n * @param table - Table name\n * @param where - Criteria to match existing record\n * @param data - Data to insert if no record found\n * @returns Promise resolving to the record (either retrieved or newly inserted)\n * @throws Error if the operation fails\n */\n const getOrInsert = async (\n table: string,\n where: Record<string, any>,\n data: Record<string, any>,\n ): Promise<Record<string, any>> => {\n const result = await get(table, where);\n if (result) return result;\n\n await insert(table, data);\n const inserted = await get(table, where);\n\n if (!inserted) {\n throw new DatabaseError('Failed to insert and retrieve record', {\n table,\n where,\n data,\n });\n }\n\n return inserted;\n };\n\n /**\n * Deletes records from a table matching the where criteria\n *\n * @param table - Table name\n * @param where - Criteria to match records for deletion\n * @returns Promise resolving to operation result with count of deleted rows\n * @throws Error if the delete operation fails\n */\n const deleteRecords = async (\n table: string,\n where: Record<string, any>,\n ): Promise<QueryResult> => {\n validateTableName(table);\n\n const keys = Object.keys(where);\n if (keys.length === 0) {\n throw new DatabaseError(\n 'DELETE requires at least one WHERE condition to prevent accidental deletion of all records',\n { table },\n );\n }\n\n const { sql: whereClause, values } = buildWhere(where, 1, 'duckdb');\n const sql = `DELETE FROM ${table} ${whereClause}`;\n\n try {\n await connection.run(sql, values);\n\n // Handle write-back strategy\n if (writeStrategy === 'immediate') {\n await exportTableToJSON(connection, table, dataDir);\n }\n\n return { operation: 'delete', affected: 1 };\n } catch (e) {\n throw new DatabaseError('Failed to delete records from table', {\n table,\n sql,\n values,\n originalError: formatDbError(e),\n });\n }\n };\n\n /**\n * Counts records in a table matching the where criteria\n *\n * @param table - Table name\n * @param where - Criteria to match records (optional, counts all if omitted)\n * @returns Promise resolving to count of matching records\n * @throws Error if the count operation fails\n */\n const count = async (\n table: string,\n where?: Record<string, any>,\n ): Promise<number> => {\n validateTableName(table);\n\n try {\n if (!where || Object.keys(where).length === 0) {\n // Count all records\n const result = await connection.runAndReadAll(\n `SELECT COUNT(*) as count FROM ${table}`,\n );\n const rows = result.getRowObjects();\n return Number(rows[0]?.count) || 0;\n }\n\n // Count with conditions\n const { sql: whereClause, values } = buildWhere(where, 1, 'duckdb');\n const sql = `SELECT COUNT(*) as count FROM ${table} ${whereClause}`;\n\n const result = await connection.runAndReadAll(sql, values);\n const rows = result.getRowObjects();\n\n return Number(rows[0]?.count) || 0;\n } catch (e) {\n throw new DatabaseError('Failed to count records in table', {\n table,\n where,\n originalError: formatDbError(e),\n });\n }\n };\n\n /**\n * Checks if a table exists in the database\n *\n * @param tableName - Name of the table to check\n * @returns Promise resolving to boolean indicating if the table exists\n */\n const tableExists = async (tableName: string): Promise<boolean> => {\n try {\n // Try to query the table - simpler and works for both tables and views\n await connection.runAndReadAll(`SELECT * FROM ${tableName} LIMIT 1`);\n return true;\n } catch {\n return false;\n }\n };\n\n /**\n * Exports a table to a JSON file (for write-back strategies)\n *\n * @param connection - DuckDB connection\n * @param table - Table name\n * @param dataDir - Directory to write JSON file\n */\n const exportTableToJSON = async (\n connection: any,\n table: string,\n dataDir: string,\n ): Promise<void> => {\n const filePath = join(dataDir, `${table}.json`);\n await connection.run(\n `COPY (SELECT * FROM ${table}) TO '${filePath}' (FORMAT JSON, ARRAY true)`,\n );\n };\n\n /**\n * Manual export method for 'manual' write strategy\n *\n * @param table - Table name to export\n * @returns Promise that resolves when export completes\n */\n const exportTable = async (table: string): Promise<void> => {\n if (writeStrategy === 'none') {\n throw new DatabaseError(\n 'Cannot export table: write strategy is set to none',\n { table, writeStrategy },\n );\n }\n await exportTableToJSON(connection, table, dataDir);\n };\n\n /**\n * Creates a table-specific interface for simplified table operations\n *\n * @param tableName - Table name\n * @returns TableInterface for the specified table\n */\n const table = (tableName: string): TableInterface => ({\n insert: (data) => insert(tableName, data),\n get: (where) => get(tableName, where),\n list: (where) => list(tableName, where),\n });\n\n /**\n * Parses a tagged template literal into a SQL query and values\n *\n * @param strings - Template strings\n * @param vars - Variables to interpolate into the query\n * @returns Object with SQL query and values array\n */\n const parseTemplate = (strings: TemplateStringsArray, ...vars: any[]) => {\n let sql = strings[0];\n const values = [];\n for (let i = 0; i < vars.length; i++) {\n values.push(vars[i]);\n sql += `$${i + 1}${strings[i + 1]}`;\n }\n return { sql, values };\n };\n\n /**\n * Executes a SQL query using template literals and returns multiple rows\n *\n * @param strings - Template strings\n * @param vars - Variables to interpolate into the query\n * @returns Promise resolving to array of result records\n * @throws Error if the query fails\n */\n const many = async (\n strings: TemplateStringsArray,\n ...vars: any[]\n ): Promise<Record<string, any>[]> => {\n const { sql, values } = parseTemplate(strings, ...vars);\n try {\n const reader = await connection.runAndReadAll(sql, values);\n return convertBigInts(reader.getRowObjects());\n } catch (e) {\n throw new DatabaseError('Failed to execute many query', {\n sql,\n values,\n originalError: formatDbError(e),\n });\n }\n };\n\n /**\n * Executes a SQL query using template literals and returns a single row\n *\n * @param strings - Template strings\n * @param vars - Variables to interpolate into the query\n * @returns Promise resolving to a single result record or null\n * @throws Error if the query fails\n */\n const single = async (\n strings: TemplateStringsArray,\n ...vars: any[]\n ): Promise<Record<string, any> | null> => {\n const { sql, values } = parseTemplate(strings, ...vars);\n try {\n const reader = await connection.runAndReadAll(sql, values);\n const rows = reader.getRowObjects();\n return rows[0] ? convertBigInts(rows[0]) : null;\n } catch (e) {\n throw new DatabaseError('Failed to execute single query', {\n sql,\n values,\n originalError: formatDbError(e),\n });\n }\n };\n\n /**\n * Executes a SQL query using template literals and returns a single value\n *\n * @param strings - Template strings\n * @param vars - Variables to interpolate into the query\n * @returns Promise resolving to a single value (first column of first row)\n * @throws Error if the query fails\n */\n const pluck = async (\n strings: TemplateStringsArray,\n ...vars: any[]\n ): Promise<any> => {\n const { sql, values } = parseTemplate(strings, ...vars);\n try {\n const reader = await connection.runAndReadAll(sql, values);\n const rows = reader.getRowObjects();\n if (rows.length === 0) return null;\n const firstRow = rows[0];\n const firstKey = Object.keys(firstRow)[0];\n return convertBigInts(firstRow[firstKey]);\n } catch (e) {\n throw new DatabaseError('Failed to execute pluck query', {\n sql,\n values,\n originalError: formatDbError(e),\n });\n }\n };\n\n /**\n * Executes a SQL query using template literals without returning results\n *\n * @param strings - Template strings\n * @param vars - Variables to interpolate into the query\n * @returns Promise that resolves when the query completes\n * @throws Error if the query fails\n */\n const execute = async (\n strings: TemplateStringsArray,\n ...vars: any[]\n ): Promise<void> => {\n const { sql, values } = parseTemplate(strings, ...vars);\n try {\n await connection.run(sql, values);\n } catch (e) {\n throw new DatabaseError('Failed to execute query', {\n sql,\n values,\n originalError: formatDbError(e),\n });\n }\n };\n\n /**\n * Executes a raw SQL query with parameterized values\n *\n * @param str - SQL query string\n * @param values - Variables to use as parameters\n * @returns Promise resolving to query result with rows and metadata\n * @throws Error if the query fails\n */\n const query = async (str: string, ...values: any[]) => {\n const sql = str;\n const args = Array.isArray(values[0]) ? values[0] : values;\n\n try {\n const reader = await connection.runAndReadAll(sql, args);\n const rows = convertBigInts(reader.getRowObjects());\n\n return {\n command: sql.split(' ')[0].toUpperCase(),\n rowCount: rows.length,\n oid: null,\n fields:\n rows.length > 0\n ? Object.keys(rows[0]).map((name) => ({\n name,\n tableID: 0,\n columnID: 0,\n dataTypeID: 0,\n dataTypeSize: -1,\n dataTypeModifier: -1,\n format: 'text',\n }))\n : [],\n rows,\n };\n } catch (e) {\n throw new DatabaseError('Failed to execute raw query', {\n sql,\n args,\n originalError: formatDbError(e),\n });\n }\n };\n\n // Shorthand aliases for query methods\n const oo = many; // (o)bjective-(o)bjects: returns multiple rows\n const oO = single; // (o)bjective-(O)bject: returns a single row\n const ox = pluck; // (o)bjective-(x): returns a single value\n const xx = execute; // e(x)ecute-e(x)ecute: executes without returning\n\n /**\n * Synchronizes database schema with provided SQL DDL\n *\n * DuckDB requires UNIQUE constraints to be defined **inline** in CREATE TABLE\n * statements for ON CONFLICT clauses to work. This method automatically\n * transforms UNIQUE indexes to inline constraints before executing the schema.\n *\n * @param schema - SQL schema definition with CREATE TABLE and CREATE INDEX statements\n * @returns Promise that resolves when schema is synchronized\n */\n const syncSchema = async (schema: string): Promise<void> => {\n const statements = schema\n .trim()\n .split(';')\n .filter((stmt) => stmt.trim() !== '');\n\n const createTableStatements: string[] = [];\n const indexStatements: string[] = [];\n\n // Separate CREATE TABLE from CREATE INDEX statements\n for (const stmt of statements) {\n const trimmed = stmt.trim();\n if (!trimmed) continue;\n\n if (\n trimmed.startsWith('CREATE INDEX') ||\n trimmed.startsWith('CREATE UNIQUE INDEX')\n ) {\n indexStatements.push(trimmed);\n } else if (trimmed.startsWith('CREATE TABLE')) {\n createTableStatements.push(trimmed);\n }\n }\n\n // Process each CREATE TABLE statement\n for (const ddl of createTableStatements) {\n // Transform UNIQUE indexes to inline constraints for DuckDB compatibility\n const { ddl: transformedDDL, indexes: remainingIndexes } =\n convertUniqueIndexesToInlineConstraints(ddl, indexStatements);\n\n // Execute transformed DDL\n try {\n await connection.run(transformedDDL);\n } catch (e) {\n console.error('Schema sync error (CREATE TABLE):', e);\n }\n\n // Execute remaining (non-UNIQUE) indexes for this table\n for (const indexSQL of remainingIndexes) {\n try {\n await connection.run(indexSQL);\n } catch (e) {\n console.error('Schema sync error (CREATE INDEX):', e);\n }\n }\n }\n };\n\n /**\n * Initialize database schemas from JSON manifest\n * Supports dependency resolution and schema overrides\n *\n * @param options - Schema initialization options\n * @returns Promise that resolves when schemas are initialized\n */\n const initializeSchemas = async (\n options: SchemaInitializationOptions,\n ): Promise<void> => {\n const schemaManager = new DatabaseSchemaManager();\n const currentDb: DatabaseInterface = {\n url,\n client: connection,\n query,\n insert,\n update,\n upsert,\n get,\n list,\n getOrInsert,\n delete: deleteRecords,\n count,\n table,\n tableExists,\n many,\n single,\n pluck,\n execute,\n oo,\n oO,\n ox,\n xx,\n syncSchema,\n };\n\n await schemaManager.initializeSchemas(currentDb, options);\n };\n\n /**\n * Retrieves the schema information for a table\n *\n * @param table - Table name\n * @returns Promise resolving to table schema info or null if table doesn't exist\n * @throws Error if the query fails\n */\n const getTableSchema = async (\n table: string,\n ): Promise<TableSchemaInfo | null> => {\n validateTableName(table);\n\n try {\n // Check if table exists\n const exists = await tableExists(table);\n if (!exists) {\n return null;\n }\n\n // Get column information from information_schema\n const columnRows = await many`\n SELECT\n column_name,\n data_type,\n is_nullable,\n column_default\n FROM information_schema.columns\n WHERE table_name = ${table}\n ORDER BY ordinal_position\n `;\n\n // Get primary key columns from DuckDB system tables\n const pkRows = await many`\n SELECT column_name\n FROM duckdb_constraints() con\n JOIN duckdb_columns() cols\n ON con.table_oid = cols.table_oid\n WHERE con.table_name = ${table}\n AND con.constraint_type = 'PRIMARY KEY'\n AND cols.column_name = ANY(con.constraint_column_names)\n `;\n\n const pkColumns = new Set(pkRows.map((row) => row.column_name));\n\n const columns: Record<string, ColumnDefinition> = {};\n for (const row of columnRows) {\n const colName = row.column_name as string;\n columns[colName] = {\n type: row.data_type as string,\n primaryKey: pkColumns.has(colName),\n notNull: row.is_nullable === 'NO',\n defaultValue: row.column_default,\n };\n }\n\n // Get index information from duckdb_indexes()\n const indexRows = await many`\n SELECT\n index_name,\n sql\n FROM duckdb_indexes()\n WHERE table_name = ${table}\n AND NOT is_primary\n `;\n\n const indexes: IndexDefinition[] = [];\n for (const row of indexRows) {\n const indexName = row.index_name as string;\n const indexDef = (row.sql as string) || '';\n\n // Parse column names from index definition\n const colMatch = indexDef.match(/\\(([^)]+)\\)/);\n const indexColumns = colMatch\n ? colMatch[1].split(',').map((col) => col.trim())\n : [];\n\n const isUnique = indexDef.toUpperCase().includes('UNIQUE');\n\n indexes.push({\n name: indexName,\n columns: indexColumns,\n unique: isUnique,\n });\n }\n\n // Get foreign key information\n const fkRows = await many`\n SELECT\n constraint_column_names,\n constraint_column_indexes,\n constraint_text\n FROM duckdb_constraints()\n WHERE table_name = ${table}\n AND constraint_type = 'FOREIGN KEY'\n `;\n\n const foreignKeys: Array<{\n column: string;\n referencesTable: string;\n referencesColumn: string;\n onDelete?: string;\n onUpdate?: string;\n }> = [];\n\n for (const fkRow of fkRows) {\n // Parse foreign key constraint text to extract referenced table and column\n const fkText = (fkRow.constraint_text as string) || '';\n const referencesMatch = fkText.match(\n /REFERENCES\\s+(\\w+)\\s*\\(([^)]+)\\)/i,\n );\n\n if (\n referencesMatch &&\n Array.isArray(fkRow.constraint_column_names) &&\n fkRow.constraint_column_names.length > 0\n ) {\n const columnName = fkRow.constraint_column_names[0];\n const referencedTable = referencesMatch[1];\n const referencedColumn = referencesMatch[2].trim();\n\n const onDeleteMatch = fkText.match(/ON DELETE\\s+(\\w+)/i);\n const onUpdateMatch = fkText.match(/ON UPDATE\\s+(\\w+)/i);\n\n foreignKeys.push({\n column: columnName,\n referencesTable: referencedTable,\n referencesColumn: referencedColumn,\n onDelete: onDeleteMatch ? onDeleteMatch[1] : undefined,\n onUpdate: onUpdateMatch ? onUpdateMatch[1] : undefined,\n });\n }\n }\n\n return {\n tableName: table,\n columns,\n indexes,\n foreignKeys,\n };\n } catch (e) {\n throw new DatabaseError('Failed to retrieve table schema', {\n table,\n originalError: formatDbError(e),\n });\n }\n };\n\n /**\n * ALTER TABLE operations for schema evolution\n */\n const alterTable = {\n /**\n * Adds a new column to an existing table\n *\n * @param table - Table name\n * @param column - Column definition with name\n * @returns Promise that resolves when column is added\n * @throws Error if the alter operation fails\n */\n addColumn: async (\n table: string,\n column: ColumnDefinitionWithName,\n ): Promise<void> => {\n validateTableName(table);\n validateColumnName(column.name);\n\n try {\n const sql = generateAddColumnStatement(table, column, 'duckdb');\n await connection.run(sql);\n } catch (e) {\n throw new DatabaseError('Failed to add column to table', {\n table,\n column: column.name,\n originalError: formatDbError(e),\n });\n }\n },\n\n /**\n * Adds a new index to an existing table\n *\n * @param table - Table name\n * @param index - Index definition\n * @returns Promise that resolves when index is created\n * @throws Error if the create index operation fails\n */\n addIndex: async (table: string, index: IndexDefinition): Promise<void> => {\n validateTableName(table);\n validateIndexName(index.name);\n\n for (const col of index.columns) {\n validateColumnName(col);\n }\n\n try {\n const sql = generateCreateIndexStatement(table, index);\n await connection.run(sql);\n } catch (e) {\n throw new DatabaseError('Failed to create index on table', {\n table,\n index: index.name,\n originalError: formatDbError(e),\n });\n }\n },\n };\n\n /**\n * Executes a callback within a database transaction\n * Automatically commits on success or rolls back on error\n *\n * @param callback - Function to execute within transaction\n * @returns Promise resolving to callback result\n */\n const transaction = async <T>(\n callback: (tx: DatabaseInterface) => Promise<T>,\n ): Promise<T> => {\n try {\n await connection.run('BEGIN TRANSACTION');\n\n // Create a transaction-scoped database interface\n const txDb: DatabaseInterface = {\n url,\n client: connection,\n insert,\n get,\n list,\n update,\n upsert,\n getOrInsert,\n delete: deleteRecords,\n count,\n table,\n many,\n single,\n pluck,\n execute,\n query,\n oo,\n oO,\n ox,\n xx,\n tableExists,\n syncSchema,\n transaction,\n };\n\n const result = await callback(txDb);\n await connection.run('COMMIT');\n return result;\n } catch (error) {\n await connection.run('ROLLBACK');\n throw error;\n }\n };\n\n /**\n * Begins a new transaction and returns a handle for manual control\n *\n * Unlike transaction(), this gives you explicit control over commit/rollback.\n * Ideal for test isolation where you want to rollback after each test.\n *\n * @returns Promise resolving to a TransactionHandle\n */\n const beginTransaction = async (): Promise<TransactionHandle> => {\n await connection.run('BEGIN TRANSACTION');\n\n let active = true;\n\n const commit = async (): Promise<void> => {\n if (!active) {\n throw new DatabaseError('Transaction already ended', {});\n }\n await connection.run('COMMIT');\n active = false;\n };\n\n const rollback = async (): Promise<void> => {\n if (!active) {\n throw new DatabaseError('Transaction already ended', {});\n }\n await connection.run('ROLLBACK');\n active = false;\n };\n\n const isActive = (): boolean => active;\n\n // Create a transaction-scoped database interface with commit/rollback\n const txHandle: TransactionHandle = {\n url,\n client: connection,\n insert,\n get,\n list,\n update,\n upsert,\n getOrInsert,\n delete: deleteRecords,\n count,\n table,\n many,\n single,\n pluck,\n execute,\n query,\n oo,\n oO,\n ox,\n xx,\n tableExists,\n syncSchema,\n transaction,\n commit,\n rollback,\n isActive,\n };\n\n return txHandle;\n };\n\n return {\n url,\n requiresSchemaCheck: true,\n client: connection,\n query,\n insert,\n update,\n upsert,\n get,\n list,\n getOrInsert,\n delete: deleteRecords,\n count,\n table,\n tableExists,\n many,\n single,\n pluck,\n execute,\n oo,\n oO,\n ox,\n xx,\n syncSchema,\n initializeSchemas,\n transaction,\n beginTransaction,\n getTableSchema,\n alterTable,\n // DuckDB-specific export method\n exportTable,\n } as DatabaseInterface & { exportTable: (table: string) => Promise<void> };\n}\n"],"mappings":"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AM,WAAW,IAAI,mBAAmB;EAExC,IAAI,SAAS;EAEb,MAAM,SAAS,YAA2B;GACxC,IAAI,CAAC,QACH,MAAM,IAAI,cAAc,6BAA6B,CAAC,CAAC;GAEzD,MAAM,WAAW,IAAI,QAAQ;GAC7B,SAAS;EACX;EAEA,MAAM,WAAW,YAA2B;GAC1C,IAAI,CAAC,QACH,MAAM,IAAI,cAAc,6BAA6B,CAAC,CAAC;GAEzD,MAAM,WAAW,IAAI,UAAU;GAC/B,SAAS;EACX;EAEA,MAAM,iBAA0B;EAgChC,OAAO;GA5BL;GACA,QAAQ;GACR;GACA;GACA;GACA;GACA;GACA;GACA,QAAQ;GACR;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EAGK;CACT;CAEA,OAAO;EACL;EACA,qBAAqB;EACrB,QAAQ;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;CACF;AACF"}
@@ -0,0 +1,115 @@
1
+ //#region src/shared/duckdb-schema-utils.ts
2
+ /**
3
+ * Parses a CREATE INDEX statement to extract index details
4
+ *
5
+ * @param indexSQL - CREATE INDEX statement
6
+ * @returns Parsed index definition or null if parsing fails
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const index = parseIndexStatement(
11
+ * 'CREATE UNIQUE INDEX ds_slug_context_idx ON ds (slug, context)'
12
+ * );
13
+ * // Returns: {
14
+ * // name: 'ds_slug_context_idx',
15
+ * // columns: ['slug', 'context'],
16
+ * // unique: true,
17
+ * // sql: '...'
18
+ * // }
19
+ * ```
20
+ */
21
+ function parseIndexStatement(indexSQL) {
22
+ const trimmed = indexSQL.trim();
23
+ const match = trimmed.match(/CREATE\s+(UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\s+ON\s+(?:"?\w+"?)\s*\(([^)]+)\)/i);
24
+ if (!match) return null;
25
+ const [, uniqueKeyword, name, columnsStr] = match;
26
+ return {
27
+ name,
28
+ columns: columnsStr.split(",").map((col) => col.trim().replace(/^"|"$/g, "")),
29
+ unique: Boolean(uniqueKeyword),
30
+ sql: trimmed
31
+ };
32
+ }
33
+ /**
34
+ * Converts UNIQUE INDEX statements to inline UNIQUE constraints for DuckDB compatibility
35
+ *
36
+ * DuckDB's ON CONFLICT clause requires inline UNIQUE constraints, not separate indexes.
37
+ * This function transforms SMRT-generated schemas to work with DuckDB's limitations.
38
+ *
39
+ * @param ddl - CREATE TABLE DDL statement
40
+ * @param indexes - Array of CREATE INDEX statements (optional)
41
+ * @returns Transformed DDL with inline UNIQUE constraints and filtered index list
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * const ddl = `
46
+ * CREATE TABLE ds (
47
+ * id TEXT PRIMARY KEY,
48
+ * slug TEXT NOT NULL,
49
+ * context TEXT NOT NULL
50
+ * )
51
+ * `;
52
+ * const indexes = [
53
+ * 'CREATE UNIQUE INDEX ds_slug_context_idx ON ds (slug, context)'
54
+ * ];
55
+ *
56
+ * const result = convertUniqueIndexesToInlineConstraints(ddl, indexes);
57
+ * // result.ddl:
58
+ * // CREATE TABLE ds (
59
+ * // id TEXT PRIMARY KEY,
60
+ * // slug TEXT NOT NULL,
61
+ * // context TEXT NOT NULL,
62
+ * // UNIQUE(slug, context)
63
+ * // )
64
+ * // result.indexes: [] (UNIQUE index removed, non-UNIQUE indexes preserved)
65
+ * ```
66
+ */
67
+ function convertUniqueIndexesToInlineConstraints(ddl, indexes) {
68
+ if (!indexes || indexes.length === 0) return {
69
+ ddl,
70
+ indexes: []
71
+ };
72
+ const parsedIndexes = indexes.map(parseIndexStatement).filter((idx) => idx !== null);
73
+ const uniqueIndexes = parsedIndexes.filter((idx) => idx.unique);
74
+ const regularIndexes = parsedIndexes.filter((idx) => !idx.unique);
75
+ if (uniqueIndexes.length === 0) return {
76
+ ddl,
77
+ indexes
78
+ };
79
+ const lines = ddl.split("\n");
80
+ let closingParenIndex = -1;
81
+ for (let i = lines.length - 1; i >= 0; i--) {
82
+ const trimmed = lines[i].trim();
83
+ if (trimmed === ")" || trimmed.startsWith(");")) {
84
+ closingParenIndex = i;
85
+ break;
86
+ }
87
+ }
88
+ if (closingParenIndex === -1) {
89
+ console.warn("[duckdb-schema-utils] Could not find closing parenthesis in CREATE TABLE statement");
90
+ return {
91
+ ddl,
92
+ indexes
93
+ };
94
+ }
95
+ const uniqueConstraints = uniqueIndexes.map((idx) => {
96
+ return ` UNIQUE(${idx.columns.map((col) => `"${col}"`).join(", ")})`;
97
+ });
98
+ const modifiedLines = [...lines];
99
+ modifiedLines[closingParenIndex].trim();
100
+ if (closingParenIndex > 0) {
101
+ if (!modifiedLines[closingParenIndex - 1].trim().endsWith(",")) modifiedLines[closingParenIndex - 1] = modifiedLines[closingParenIndex - 1].replace(/\s*$/, ",");
102
+ }
103
+ const constraintLines = uniqueConstraints.map((constraint, idx) => {
104
+ return idx < uniqueConstraints.length - 1 ? `${constraint},` : constraint;
105
+ });
106
+ modifiedLines.splice(closingParenIndex, 0, ...constraintLines);
107
+ return {
108
+ ddl: modifiedLines.join("\n"),
109
+ indexes: regularIndexes.map((idx) => idx.sql)
110
+ };
111
+ }
112
+ //#endregion
113
+ export { convertUniqueIndexesToInlineConstraints as t };
114
+
115
+ //# sourceMappingURL=duckdb-schema-utils-B-XBP8QF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duckdb-schema-utils-B-XBP8QF.js","names":[],"sources":["../../src/shared/duckdb-schema-utils.ts"],"sourcesContent":["/**\n * DuckDB-specific schema transformation utilities\n *\n * DuckDB has a known limitation (issue #12684) where ON CONFLICT clauses\n * fail with UNIQUE INDEX but work with inline UNIQUE constraints.\n *\n * This module provides utilities to transform SMRT-generated schemas\n * (which use separate CREATE UNIQUE INDEX statements) into DuckDB-compatible\n * schemas (which use inline UNIQUE constraints).\n */\n\n/**\n * Index definition extracted from CREATE INDEX statement\n */\ninterface ParsedIndex {\n name: string;\n columns: string[];\n unique: boolean;\n sql: string;\n}\n\n/**\n * Parses a CREATE INDEX statement to extract index details\n *\n * @param indexSQL - CREATE INDEX statement\n * @returns Parsed index definition or null if parsing fails\n *\n * @example\n * ```typescript\n * const index = parseIndexStatement(\n * 'CREATE UNIQUE INDEX ds_slug_context_idx ON ds (slug, context)'\n * );\n * // Returns: {\n * // name: 'ds_slug_context_idx',\n * // columns: ['slug', 'context'],\n * // unique: true,\n * // sql: '...'\n * // }\n * ```\n */\nexport function parseIndexStatement(indexSQL: string): ParsedIndex | null {\n const trimmed = indexSQL.trim();\n\n // Match: CREATE [UNIQUE] INDEX [IF NOT EXISTS] name ON table (columns)\n // Support both quoted and unquoted identifiers: table, \"table\", columns, \"columns\"\n const match = trimmed.match(\n /CREATE\\s+(UNIQUE\\s+)?INDEX\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(\\w+)\\s+ON\\s+(?:\"?\\w+\"?)\\s*\\(([^)]+)\\)/i,\n );\n\n if (!match) {\n return null;\n }\n\n const [, uniqueKeyword, name, columnsStr] = match;\n\n // Remove quotes from column names\n const columns = columnsStr\n .split(',')\n .map((col) => col.trim().replace(/^\"|\"$/g, ''));\n\n return {\n name,\n columns,\n unique: Boolean(uniqueKeyword),\n sql: trimmed,\n };\n}\n\n/**\n * Converts UNIQUE INDEX statements to inline UNIQUE constraints for DuckDB compatibility\n *\n * DuckDB's ON CONFLICT clause requires inline UNIQUE constraints, not separate indexes.\n * This function transforms SMRT-generated schemas to work with DuckDB's limitations.\n *\n * @param ddl - CREATE TABLE DDL statement\n * @param indexes - Array of CREATE INDEX statements (optional)\n * @returns Transformed DDL with inline UNIQUE constraints and filtered index list\n *\n * @example\n * ```typescript\n * const ddl = `\n * CREATE TABLE ds (\n * id TEXT PRIMARY KEY,\n * slug TEXT NOT NULL,\n * context TEXT NOT NULL\n * )\n * `;\n * const indexes = [\n * 'CREATE UNIQUE INDEX ds_slug_context_idx ON ds (slug, context)'\n * ];\n *\n * const result = convertUniqueIndexesToInlineConstraints(ddl, indexes);\n * // result.ddl:\n * // CREATE TABLE ds (\n * // id TEXT PRIMARY KEY,\n * // slug TEXT NOT NULL,\n * // context TEXT NOT NULL,\n * // UNIQUE(slug, context)\n * // )\n * // result.indexes: [] (UNIQUE index removed, non-UNIQUE indexes preserved)\n * ```\n */\nexport function convertUniqueIndexesToInlineConstraints(\n ddl: string,\n indexes?: string[],\n): {\n ddl: string;\n indexes: string[];\n} {\n if (!indexes || indexes.length === 0) {\n return { ddl, indexes: [] };\n }\n\n // Parse all indexes\n const parsedIndexes = indexes\n .map(parseIndexStatement)\n .filter((idx): idx is ParsedIndex => idx !== null);\n\n // Separate UNIQUE indexes from regular indexes\n const uniqueIndexes = parsedIndexes.filter((idx) => idx.unique);\n const regularIndexes = parsedIndexes.filter((idx) => !idx.unique);\n\n // If no UNIQUE indexes, return unchanged\n if (uniqueIndexes.length === 0) {\n return { ddl, indexes };\n }\n\n // Find the closing parenthesis of the CREATE TABLE statement\n const lines = ddl.split('\\n');\n let closingParenIndex = -1;\n\n for (let i = lines.length - 1; i >= 0; i--) {\n const trimmed = lines[i].trim();\n if (trimmed === ')' || trimmed.startsWith(');')) {\n closingParenIndex = i;\n break;\n }\n }\n\n if (closingParenIndex === -1) {\n // Can't find closing paren - return unchanged\n console.warn(\n '[duckdb-schema-utils] Could not find closing parenthesis in CREATE TABLE statement',\n );\n return { ddl, indexes };\n }\n\n // Build inline UNIQUE constraints with quoted column names\n const uniqueConstraints = uniqueIndexes.map((idx) => {\n const columns = idx.columns.map((col) => `\"${col}\"`).join(', ');\n return ` UNIQUE(${columns})`;\n });\n\n // Insert UNIQUE constraints before closing parenthesis\n const modifiedLines = [...lines];\n const closingLine = modifiedLines[closingParenIndex].trim();\n\n // Add comma to previous line if it doesn't have one\n if (closingParenIndex > 0) {\n const prevLine = modifiedLines[closingParenIndex - 1].trim();\n if (!prevLine.endsWith(',')) {\n modifiedLines[closingParenIndex - 1] = modifiedLines[\n closingParenIndex - 1\n ].replace(/\\s*$/, ',');\n }\n }\n\n // Insert UNIQUE constraints\n const constraintLines = uniqueConstraints.map((constraint, idx) => {\n // Add comma after all but the last constraint\n return idx < uniqueConstraints.length - 1 ? `${constraint},` : constraint;\n });\n\n modifiedLines.splice(closingParenIndex, 0, ...constraintLines);\n\n const transformedDDL = modifiedLines.join('\\n');\n\n // Return regular (non-UNIQUE) indexes only\n const remainingIndexes = regularIndexes.map((idx) => idx.sql);\n\n return {\n ddl: transformedDDL,\n indexes: remainingIndexes,\n };\n}\n\n/**\n * Checks if a schema definition contains UNIQUE indexes that need conversion\n *\n * @param indexes - Array of CREATE INDEX statements\n * @returns True if any UNIQUE indexes are present\n */\nexport function hasUniqueIndexes(indexes?: string[]): boolean {\n if (!indexes || indexes.length === 0) {\n return false;\n }\n\n return indexes.some((indexSQL) => {\n const parsed = parseIndexStatement(indexSQL);\n return parsed?.unique === true;\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAwCA,SAAgB,oBAAoB,UAAsC;CACxE,MAAM,UAAU,SAAS,KAAK;CAI9B,MAAM,QAAQ,QAAQ,MACpB,+FACF;CAEA,IAAI,CAAC,OACH,OAAO;CAGT,MAAM,GAAG,eAAe,MAAM,cAAc;CAO5C,OAAO;EACL;EACA,SANc,WACb,MAAM,GAAG,CAAC,CACV,KAAK,QAAQ,IAAI,KAAK,CAAC,CAAC,QAAQ,UAAU,EAAE,CAI7C;EACA,QAAQ,QAAQ,aAAa;EAC7B,KAAK;CACP;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,wCACd,KACA,SAIA;CACA,IAAI,CAAC,WAAW,QAAQ,WAAW,GACjC,OAAO;EAAE;EAAK,SAAS,CAAC;CAAE;CAI5B,MAAM,gBAAgB,QACnB,IAAI,mBAAmB,CAAC,CACxB,QAAQ,QAA4B,QAAQ,IAAI;CAGnD,MAAM,gBAAgB,cAAc,QAAQ,QAAQ,IAAI,MAAM;CAC9D,MAAM,iBAAiB,cAAc,QAAQ,QAAQ,CAAC,IAAI,MAAM;CAGhE,IAAI,cAAc,WAAW,GAC3B,OAAO;EAAE;EAAK;CAAQ;CAIxB,MAAM,QAAQ,IAAI,MAAM,IAAI;CAC5B,IAAI,oBAAoB;CAExB,KAAK,IAAI,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;EAC1C,MAAM,UAAU,MAAM,EAAE,CAAC,KAAK;EAC9B,IAAI,YAAY,OAAO,QAAQ,WAAW,IAAI,GAAG;GAC/C,oBAAoB;GACpB;EACF;CACF;CAEA,IAAI,sBAAsB,IAAI;EAE5B,QAAQ,KACN,oFACF;EACA,OAAO;GAAE;GAAK;EAAQ;CACxB;CAGA,MAAM,oBAAoB,cAAc,KAAK,QAAQ;EAEnD,OAAO,YADS,IAAI,QAAQ,KAAK,QAAQ,IAAI,IAAI,EAAE,CAAC,CAAC,KAAK,IACvC,EAAQ;CAC7B,CAAC;CAGD,MAAM,gBAAgB,CAAC,GAAG,KAAK;CACX,cAAc,kBAAkB,CAAC,KAAK;CAG1D,IAAI,oBAAoB;MAElB,CADa,cAAc,oBAAoB,EAAE,CAAC,KACjD,CAAA,CAAS,SAAS,GAAG,GACxB,cAAc,oBAAoB,KAAK,cACrC,oBAAoB,EACrB,CAAC,QAAQ,QAAQ,GAAG;CAAA;CAKzB,MAAM,kBAAkB,kBAAkB,KAAK,YAAY,QAAQ;EAEjE,OAAO,MAAM,kBAAkB,SAAS,IAAI,GAAG,WAAW,KAAK;CACjE,CAAC;CAED,cAAc,OAAO,mBAAmB,GAAG,GAAG,eAAe;CAO7D,OAAO;EACL,KANqB,cAAc,KAAK,IAMnC;EACL,SAJuB,eAAe,KAAK,QAAQ,IAAI,GAI9C;CACX;AACF"}
@@ -0,0 +1,272 @@
1
+ import { spawn } from "node:child_process";
2
+ import { chmod, mkdir } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ //#region src/postgres-cli.ts
5
+ /**
6
+ * Shell-out wrappers for the postgres command-line tools (`pg_dump`,
7
+ * `pg_restore`, `createdb`, `dropdb`) and the URL/locality helpers that
8
+ * surround them.
9
+ *
10
+ * These live in `@happyvertical/sql` because postgres URL parsing and pg
11
+ * binary invocation are SQL-layer concerns — no SMRT awareness or backup
12
+ * orchestration here. See `@happyvertical/sql/backup` for the higher-level
13
+ * snapshot helpers that compose these primitives with file storage.
14
+ */
15
+ /**
16
+ * Extract the database name from a postgres connection URL.
17
+ *
18
+ * Throws if the URL has no path/db segment (postgres rejects connections
19
+ * without one anyway).
20
+ */
21
+ function databaseNameFromUrl(databaseUrl) {
22
+ const url = new URL(databaseUrl);
23
+ const database = decodeURIComponent(url.pathname.replace(/^\/+/u, ""));
24
+ if (!database) throw new Error("Database URL must include a database name.");
25
+ return database;
26
+ }
27
+ /**
28
+ * Treat localhost / 127.0.0.1 / ::1 / empty-hostname URLs as "local".
29
+ *
30
+ * Used by the export/import safety guards to decide whether a command
31
+ * needs `--allow-production` to proceed.
32
+ */
33
+ function isLocalDatabaseUrl(databaseUrl) {
34
+ try {
35
+ const hostname = new URL(databaseUrl).hostname.toLowerCase().replace(/^\[|\]$/gu, "");
36
+ return hostname === "" || hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+ /**
42
+ * Strip credentials from a postgres URL for logging/manifest use.
43
+ * Returns `[invalid database url]` if parsing fails.
44
+ *
45
+ * Redacts both the userinfo (`user:password@`) and any credential-shaped
46
+ * query parameters (`?password=…`, `?passwd=…`, `?pass=…`) — cloud
47
+ * providers like Neon and Supabase emit URLs with `?password=` instead of
48
+ * (or in addition to) the userinfo form, and leaking those into logs or
49
+ * the backup manifest defeats the rest of the redaction.
50
+ */
51
+ function redactDatabaseUrl(databaseUrl) {
52
+ try {
53
+ const url = new URL(databaseUrl);
54
+ if (url.username) url.username = "***";
55
+ if (url.password) url.password = "***";
56
+ for (const param of [
57
+ "password",
58
+ "passwd",
59
+ "pass"
60
+ ]) if (url.searchParams.has(param)) url.searchParams.set(param, "***");
61
+ return url.toString();
62
+ } catch {
63
+ return "[invalid database url]";
64
+ }
65
+ }
66
+ /**
67
+ * Translate a postgres URL into the libpq env vars that pg_dump/pg_restore
68
+ * read. Passing credentials this way keeps them out of process argv (and
69
+ * therefore out of `ps`, audit logs, and error output).
70
+ *
71
+ * `databaseOverride` lets callers point pg tools at a different db on the
72
+ * same server (e.g. connecting to `postgres` to drop the real target).
73
+ */
74
+ function postgresEnvFromUrl(databaseUrl, databaseOverride) {
75
+ const url = new URL(databaseUrl);
76
+ if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") throw new Error(`Unsupported database URL protocol: ${url.protocol}`);
77
+ const env = {};
78
+ env.PGDATABASE = databaseOverride ?? databaseNameFromUrl(databaseUrl);
79
+ if (url.hostname) env.PGHOST = url.hostname.replace(/^\[|\]$/gu, "");
80
+ if (url.port) env.PGPORT = url.port;
81
+ if (url.username) env.PGUSER = decodeUserInfo(url.username);
82
+ const passwordFromUrl = url.password && decodeUserInfo(url.password) || url.searchParams.get("password") || url.searchParams.get("passwd") || url.searchParams.get("pass") || "";
83
+ if (passwordFromUrl) env.PGPASSWORD = passwordFromUrl;
84
+ for (const [param, envName] of [
85
+ ["sslmode", "PGSSLMODE"],
86
+ ["sslrootcert", "PGSSLROOTCERT"],
87
+ ["sslcert", "PGSSLCERT"],
88
+ ["sslkey", "PGSSLKEY"]
89
+ ]) {
90
+ const value = url.searchParams.get(param);
91
+ if (value) env[envName] = value;
92
+ }
93
+ return env;
94
+ }
95
+ /**
96
+ * Libpq env vars that determine **where** a pg connection lands. Scrubbed
97
+ * from the inherited parent environment by `runCommand` so a developer
98
+ * with `PGHOST=staging` in their shell can't silently redirect a
99
+ * pg_dump call that was supposed to target the URL the caller passed.
100
+ *
101
+ * SSL-related vars (PGSSLMODE/PGSSLROOTCERT/...) are intentionally NOT
102
+ * scrubbed — a caller may legitimately want to inherit a system-wide
103
+ * certificate path. They're explicitly overridden only when the URL
104
+ * supplies a value.
105
+ *
106
+ * Auth/identity vars (PGUSER / PGPASSWORD / PGPASSFILE) are
107
+ * intentionally NOT scrubbed: they don't change target routing and
108
+ * some environments intentionally supply credentials and the
109
+ * connecting role out-of-band while keeping DATABASE_URL itself
110
+ * minimal — the Heroku/Railway/Render-style "DATABASE_URL has the
111
+ * host and database; PGUSER + PGPASSWORD env carry the auth identity"
112
+ * pattern. When the URL DOES supply a username or password,
113
+ * `postgresEnvFromUrl` writes that value into the per-call env which
114
+ * then overrides the inherited one via the spawn-env merge.
115
+ *
116
+ * `PGDATABASE` is also kept out of the scrub list: `postgresEnvFromUrl`
117
+ * always sets it (either from the URL path or the explicit override),
118
+ * so the parent's value is overwritten in the merge regardless.
119
+ *
120
+ * **Trade-off this opens up:** on a developer machine, a stale
121
+ * `PGUSER`/`PGPASSWORD` set for an earlier task can flow into a later
122
+ * `pg_dump` targeting a different database. This typically surfaces as
123
+ * a clear authentication error rather than silent wrong-credential
124
+ * use, but callers in environments where this is a real concern should
125
+ * clear those env vars from `process.env` before invoking these
126
+ * helpers (or supply the credential via the URL itself, which
127
+ * overrides any inherited value).
128
+ */
129
+ var LIBPQ_INHERITANCE_VARS = [
130
+ "PGHOST",
131
+ "PGHOSTADDR",
132
+ "PGPORT",
133
+ "PGSERVICE",
134
+ "PGSERVICEFILE"
135
+ ];
136
+ /**
137
+ * Guard for export operations. Two distinct hazards:
138
+ * - running a "prod" export against a local DB by mistake (likely a
139
+ * developer running prod scripts on their laptop)
140
+ * - running a regular export against a non-local DB (likely a developer
141
+ * pointing at staging or prod when they meant to target their laptop)
142
+ *
143
+ * Each escape hatch is named so the override has to be deliberate.
144
+ */
145
+ function assertCanExportDatabase(databaseUrl, options) {
146
+ const local = isLocalDatabaseUrl(databaseUrl);
147
+ if (options.prod && local && !options.allowLocal) throw new Error("Refusing to run a production-flavored export against a local database. Pass allowLocal only when intentionally testing the production export path.");
148
+ if (!options.prod && !local && !options.allowProduction) throw new Error("Refusing to export a non-local database without allowProduction. Use the production export path, or pass allowProduction intentionally.");
149
+ }
150
+ /**
151
+ * Guard for import/restore operations against non-local databases.
152
+ * Restoring is destructive, so the default is "local only".
153
+ */
154
+ function assertCanImportDatabase(databaseUrl, options) {
155
+ if (!isLocalDatabaseUrl(databaseUrl) && !options.allowProduction) throw new Error("Refusing to import into a non-local database without allowProduction.");
156
+ }
157
+ /**
158
+ * Run `pg_dump` against `databaseUrl`, writing a custom-format archive to
159
+ * `dumpPath`. Defaults match the common "logical backup, no-owner, no-acl"
160
+ * recipe used for cross-environment restores.
161
+ *
162
+ * Credentials travel via env vars, never argv.
163
+ */
164
+ async function dumpPostgresDatabase(databaseUrl, dumpPath, options = {}) {
165
+ await mkdir(dirname(dumpPath), {
166
+ recursive: true,
167
+ mode: 448
168
+ });
169
+ await runCommand(options.binary ?? "pg_dump", [
170
+ "--format=custom",
171
+ "--no-owner",
172
+ "--no-acl",
173
+ "--file",
174
+ dumpPath,
175
+ ...options.extraArgs ?? []
176
+ ], { env: postgresEnvFromUrl(databaseUrl) });
177
+ await chmod(dumpPath, 384);
178
+ }
179
+ /**
180
+ * Run `pg_restore` against `databaseUrl` from a custom-format archive at
181
+ * `dumpPath`. Uses `--clean --if-exists` so the target db can be a previous
182
+ * state of the same schema.
183
+ */
184
+ async function restorePostgresDatabase(databaseUrl, dumpPath, options = {}) {
185
+ const singleTransaction = options.singleTransaction ?? true;
186
+ await runCommand(options.binary ?? "pg_restore", [
187
+ "--clean",
188
+ "--if-exists",
189
+ "--no-owner",
190
+ "--no-acl",
191
+ ...singleTransaction ? ["--single-transaction", "--exit-on-error"] : [],
192
+ "--dbname",
193
+ databaseNameFromUrl(databaseUrl),
194
+ ...options.extraArgs ?? [],
195
+ "--",
196
+ dumpPath
197
+ ], { env: postgresEnvFromUrl(databaseUrl) });
198
+ }
199
+ /**
200
+ * `createdb`-equivalent. Connects via the maintenance database (default
201
+ * `postgres`) and creates the database named in the URL, or in
202
+ * `options.database` if provided.
203
+ */
204
+ async function createPostgresDatabase(databaseUrl, options = {}) {
205
+ const database = options.database ?? databaseNameFromUrl(databaseUrl);
206
+ await runCommand(options.binary ?? "createdb", [database, ...options.extraArgs ?? []], { env: postgresEnvFromUrl(databaseUrl, options.maintenanceDatabase ?? "postgres") });
207
+ }
208
+ /**
209
+ * `dropdb`-equivalent, with `--if-exists` by default so callers can use
210
+ * this idempotently as part of a reset flow.
211
+ */
212
+ async function dropPostgresDatabase(databaseUrl, options = {}) {
213
+ const database = options.database ?? databaseNameFromUrl(databaseUrl);
214
+ await runCommand(options.binary ?? "dropdb", [
215
+ "--if-exists",
216
+ database,
217
+ ...options.extraArgs ?? []
218
+ ], { env: postgresEnvFromUrl(databaseUrl, options.maintenanceDatabase ?? "postgres") });
219
+ }
220
+ /**
221
+ * Spawn helper used by the wrappers above. Inherits stdio so progress and
222
+ * error output stream to the parent terminal (these are interactive ops),
223
+ * unless the caller passes `stdio: 'pipe'` for programmatic use — in
224
+ * which case stderr is captured and included in the thrown error so the
225
+ * failure isn't an opaque `"pg_dump exited with status 1"`.
226
+ *
227
+ * Libpq connection vars (PGHOST, PGSERVICE, ...) inherited from the
228
+ * parent process are scrubbed before merging with `options.env`. This is
229
+ * the half of "URL-determined connection target" that
230
+ * `postgresEnvFromUrl` can't do alone: without scrubbing, a developer
231
+ * with `PGHOST=staging` in their shell would silently redirect the
232
+ * child's connection regardless of what the URL specified.
233
+ */
234
+ async function runCommand(command, args, options = {}) {
235
+ const parentEnv = { ...process.env };
236
+ for (const key of LIBPQ_INHERITANCE_VARS) delete parentEnv[key];
237
+ await new Promise((resolvePromise, reject) => {
238
+ const child = spawn(command, args, {
239
+ cwd: options.cwd ?? process.cwd(),
240
+ env: {
241
+ ...parentEnv,
242
+ ...options.env
243
+ },
244
+ stdio: options.stdio ?? "inherit"
245
+ });
246
+ const stderrChunks = [];
247
+ if (options.stdio === "pipe" && child.stderr) child.stderr.on("data", (chunk) => {
248
+ stderrChunks.push(chunk);
249
+ });
250
+ child.on("error", reject);
251
+ child.on("close", (code) => {
252
+ if (code === 0) {
253
+ resolvePromise();
254
+ return;
255
+ }
256
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
257
+ const stderrSuffix = stderr ? `: ${stderr.slice(-2e3)}` : "";
258
+ reject(/* @__PURE__ */ new Error(`${command} exited with status ${code}${stderrSuffix}`));
259
+ });
260
+ });
261
+ }
262
+ function decodeUserInfo(value) {
263
+ try {
264
+ return decodeURIComponent(value);
265
+ } catch {
266
+ return value;
267
+ }
268
+ }
269
+ //#endregion
270
+ export { dropPostgresDatabase as a, postgresEnvFromUrl as c, databaseNameFromUrl as i, redactDatabaseUrl as l, assertCanImportDatabase as n, dumpPostgresDatabase as o, createPostgresDatabase as r, isLocalDatabaseUrl as s, assertCanExportDatabase as t, restorePostgresDatabase as u };
271
+
272
+ //# sourceMappingURL=postgres-cli-DzZcXSIG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres-cli-DzZcXSIG.js","names":[],"sources":["../../src/postgres-cli.ts"],"sourcesContent":["/**\n * Shell-out wrappers for the postgres command-line tools (`pg_dump`,\n * `pg_restore`, `createdb`, `dropdb`) and the URL/locality helpers that\n * surround them.\n *\n * These live in `@happyvertical/sql` because postgres URL parsing and pg\n * binary invocation are SQL-layer concerns — no SMRT awareness or backup\n * orchestration here. See `@happyvertical/sql/backup` for the higher-level\n * snapshot helpers that compose these primitives with file storage.\n */\nimport { spawn } from 'node:child_process';\nimport { chmod, mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\nexport interface PostgresLocalityOptions {\n /** Allow exporting from a non-local database without --allow-production. */\n allowProduction?: boolean;\n /** Allow exporting a local database when `prod: true` is set. */\n allowLocal?: boolean;\n /** True when the caller is running a production-flavored export. */\n prod?: boolean;\n}\n\nexport interface PostgresImportLocalityOptions {\n allowProduction?: boolean;\n}\n\nexport interface DumpOptions {\n /** Extra args appended after the built-in pg_dump flags. */\n extraArgs?: string[];\n /** Override the binary name (e.g. `/usr/local/pg17/bin/pg_dump`). */\n binary?: string;\n}\n\nexport interface RestoreOptions {\n extraArgs?: string[];\n binary?: string;\n /**\n * Wrap the restore in `--single-transaction --exit-on-error` so a\n * mid-restore failure rolls the target database back to its prior\n * state instead of leaving it partially-restored-but-failing.\n * Defaults to `true`. Set to `false` if you need parallel restore\n * (`--jobs N` in `extraArgs`); pg_restore rejects `--single-transaction`\n * combined with `--jobs`.\n */\n singleTransaction?: boolean;\n}\n\nexport interface CreateDropOptions {\n /** Database name to drop. Defaults to the one parsed from the URL. */\n database?: string;\n /**\n * The maintenance database to connect to when running create/drop\n * (the target DB itself can't be open). Defaults to 'postgres'.\n */\n maintenanceDatabase?: string;\n extraArgs?: string[];\n binary?: string;\n}\n\n/**\n * Extract the database name from a postgres connection URL.\n *\n * Throws if the URL has no path/db segment (postgres rejects connections\n * without one anyway).\n */\nexport function databaseNameFromUrl(databaseUrl: string): string {\n const url = new URL(databaseUrl);\n const database = decodeURIComponent(url.pathname.replace(/^\\/+/u, ''));\n if (!database) throw new Error('Database URL must include a database name.');\n return database;\n}\n\n/**\n * Treat localhost / 127.0.0.1 / ::1 / empty-hostname URLs as \"local\".\n *\n * Used by the export/import safety guards to decide whether a command\n * needs `--allow-production` to proceed.\n */\nexport function isLocalDatabaseUrl(databaseUrl: string): boolean {\n try {\n const url = new URL(databaseUrl);\n const hostname = url.hostname.toLowerCase().replace(/^\\[|\\]$/gu, '');\n return (\n hostname === '' ||\n hostname === 'localhost' ||\n hostname === '127.0.0.1' ||\n hostname === '::1'\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Strip credentials from a postgres URL for logging/manifest use.\n * Returns `[invalid database url]` if parsing fails.\n *\n * Redacts both the userinfo (`user:password@`) and any credential-shaped\n * query parameters (`?password=…`, `?passwd=…`, `?pass=…`) — cloud\n * providers like Neon and Supabase emit URLs with `?password=` instead of\n * (or in addition to) the userinfo form, and leaking those into logs or\n * the backup manifest defeats the rest of the redaction.\n */\nexport function redactDatabaseUrl(databaseUrl: string): string {\n try {\n const url = new URL(databaseUrl);\n if (url.username) url.username = '***';\n if (url.password) url.password = '***';\n for (const param of ['password', 'passwd', 'pass']) {\n if (url.searchParams.has(param)) {\n url.searchParams.set(param, '***');\n }\n }\n return url.toString();\n } catch {\n return '[invalid database url]';\n }\n}\n\n/**\n * Translate a postgres URL into the libpq env vars that pg_dump/pg_restore\n * read. Passing credentials this way keeps them out of process argv (and\n * therefore out of `ps`, audit logs, and error output).\n *\n * `databaseOverride` lets callers point pg tools at a different db on the\n * same server (e.g. connecting to `postgres` to drop the real target).\n */\nexport function postgresEnvFromUrl(\n databaseUrl: string,\n databaseOverride?: string,\n): Record<string, string> {\n const url = new URL(databaseUrl);\n if (url.protocol !== 'postgres:' && url.protocol !== 'postgresql:') {\n throw new Error(`Unsupported database URL protocol: ${url.protocol}`);\n }\n\n const env: Record<string, string> = {};\n const database = databaseOverride ?? databaseNameFromUrl(databaseUrl);\n\n // Only set vars we have URL-derived values for. Inherited libpq env\n // (PGHOST, PGSERVICE, etc.) is scrubbed separately by `runCommand`\n // via `LIBPQ_INHERITANCE_VARS` — see below. We cannot use empty\n // strings to \"unset\" these vars: libpq treats e.g. `PGSERVICE=''` as\n // a request for a service literally named empty-string, which fails\n // with `definition of service \"\" not found`. The right way to\n // \"unset\" is to remove the key entirely from the child's env, which\n // runCommand does for us before merging.\n env.PGDATABASE = database;\n if (url.hostname) {\n // Strip IPv6 brackets: `URL.hostname` returns `'[::1]'` for IPv6\n // literals, but libpq's `PGHOST` expects the bare address (`'::1'`).\n // Brackets are URI syntax only — passing them through means\n // pg_dump/pg_restore fail to resolve the host even though\n // `isLocalDatabaseUrl` correctly recognises the same URL as local.\n env.PGHOST = url.hostname.replace(/^\\[|\\]$/gu, '');\n }\n if (url.port) env.PGPORT = url.port;\n if (url.username) env.PGUSER = decodeUserInfo(url.username);\n\n // Password can come from userinfo OR query-string (`?password=...`,\n // `?passwd=...`, `?pass=...`) — cloud providers like Neon and\n // Supabase use the query-string form. `redactDatabaseUrl` already\n // masks all three in logs; the env builder honors all three for\n // connection.\n const passwordFromUrl =\n (url.password && decodeUserInfo(url.password)) ||\n url.searchParams.get('password') ||\n url.searchParams.get('passwd') ||\n url.searchParams.get('pass') ||\n '';\n if (passwordFromUrl) env.PGPASSWORD = passwordFromUrl;\n\n for (const [param, envName] of [\n ['sslmode', 'PGSSLMODE'],\n ['sslrootcert', 'PGSSLROOTCERT'],\n ['sslcert', 'PGSSLCERT'],\n ['sslkey', 'PGSSLKEY'],\n ] as const) {\n const value = url.searchParams.get(param);\n if (value) env[envName] = value;\n }\n\n return env;\n}\n\n/**\n * Libpq env vars that determine **where** a pg connection lands. Scrubbed\n * from the inherited parent environment by `runCommand` so a developer\n * with `PGHOST=staging` in their shell can't silently redirect a\n * pg_dump call that was supposed to target the URL the caller passed.\n *\n * SSL-related vars (PGSSLMODE/PGSSLROOTCERT/...) are intentionally NOT\n * scrubbed — a caller may legitimately want to inherit a system-wide\n * certificate path. They're explicitly overridden only when the URL\n * supplies a value.\n *\n * Auth/identity vars (PGUSER / PGPASSWORD / PGPASSFILE) are\n * intentionally NOT scrubbed: they don't change target routing and\n * some environments intentionally supply credentials and the\n * connecting role out-of-band while keeping DATABASE_URL itself\n * minimal — the Heroku/Railway/Render-style \"DATABASE_URL has the\n * host and database; PGUSER + PGPASSWORD env carry the auth identity\"\n * pattern. When the URL DOES supply a username or password,\n * `postgresEnvFromUrl` writes that value into the per-call env which\n * then overrides the inherited one via the spawn-env merge.\n *\n * `PGDATABASE` is also kept out of the scrub list: `postgresEnvFromUrl`\n * always sets it (either from the URL path or the explicit override),\n * so the parent's value is overwritten in the merge regardless.\n *\n * **Trade-off this opens up:** on a developer machine, a stale\n * `PGUSER`/`PGPASSWORD` set for an earlier task can flow into a later\n * `pg_dump` targeting a different database. This typically surfaces as\n * a clear authentication error rather than silent wrong-credential\n * use, but callers in environments where this is a real concern should\n * clear those env vars from `process.env` before invoking these\n * helpers (or supply the credential via the URL itself, which\n * overrides any inherited value).\n */\nconst LIBPQ_INHERITANCE_VARS = [\n 'PGHOST',\n 'PGHOSTADDR',\n 'PGPORT',\n 'PGSERVICE',\n 'PGSERVICEFILE',\n] as const;\n\n/**\n * Guard for export operations. Two distinct hazards:\n * - running a \"prod\" export against a local DB by mistake (likely a\n * developer running prod scripts on their laptop)\n * - running a regular export against a non-local DB (likely a developer\n * pointing at staging or prod when they meant to target their laptop)\n *\n * Each escape hatch is named so the override has to be deliberate.\n */\nexport function assertCanExportDatabase(\n databaseUrl: string,\n options: PostgresLocalityOptions,\n): void {\n const local = isLocalDatabaseUrl(databaseUrl);\n if (options.prod && local && !options.allowLocal) {\n throw new Error(\n 'Refusing to run a production-flavored export against a local database. Pass allowLocal only when intentionally testing the production export path.',\n );\n }\n if (!options.prod && !local && !options.allowProduction) {\n throw new Error(\n 'Refusing to export a non-local database without allowProduction. Use the production export path, or pass allowProduction intentionally.',\n );\n }\n}\n\n/**\n * Guard for import/restore operations against non-local databases.\n * Restoring is destructive, so the default is \"local only\".\n */\nexport function assertCanImportDatabase(\n databaseUrl: string,\n options: PostgresImportLocalityOptions,\n): void {\n if (!isLocalDatabaseUrl(databaseUrl) && !options.allowProduction) {\n throw new Error(\n 'Refusing to import into a non-local database without allowProduction.',\n );\n }\n}\n\n/**\n * Run `pg_dump` against `databaseUrl`, writing a custom-format archive to\n * `dumpPath`. Defaults match the common \"logical backup, no-owner, no-acl\"\n * recipe used for cross-environment restores.\n *\n * Credentials travel via env vars, never argv.\n */\nexport async function dumpPostgresDatabase(\n databaseUrl: string,\n dumpPath: string,\n options: DumpOptions = {},\n): Promise<void> {\n await mkdir(dirname(dumpPath), { recursive: true, mode: 0o700 });\n await runCommand(\n options.binary ?? 'pg_dump',\n [\n '--format=custom',\n '--no-owner',\n '--no-acl',\n '--file',\n dumpPath,\n ...(options.extraArgs ?? []),\n ],\n { env: postgresEnvFromUrl(databaseUrl) },\n );\n await chmod(dumpPath, 0o600);\n}\n\n/**\n * Run `pg_restore` against `databaseUrl` from a custom-format archive at\n * `dumpPath`. Uses `--clean --if-exists` so the target db can be a previous\n * state of the same schema.\n */\nexport async function restorePostgresDatabase(\n databaseUrl: string,\n dumpPath: string,\n options: RestoreOptions = {},\n): Promise<void> {\n const singleTransaction = options.singleTransaction ?? true;\n await runCommand(\n options.binary ?? 'pg_restore',\n [\n '--clean',\n '--if-exists',\n '--no-owner',\n '--no-acl',\n // Atomic-restore default: wrap the whole operation in one\n // transaction. Without this, a mid-restore failure (e.g. a unique\n // constraint conflict halfway through data load) leaves the\n // target database with the `--clean` DROPs applied AND part of\n // the new schema/data, while pg_restore still exits non-zero —\n // operationally the worst of both worlds. Caller can opt out via\n // `singleTransaction: false` when running parallel restores\n // (`--jobs N`) since pg_restore rejects the combination.\n ...(singleTransaction ? ['--single-transaction', '--exit-on-error'] : []),\n '--dbname',\n databaseNameFromUrl(databaseUrl),\n ...(options.extraArgs ?? []),\n // POSIX end-of-options marker: `dumpPath` is the final positional\n // and must be interpreted as a path, not a flag. Without `--`, a\n // path that starts with `-` (e.g. a temp file generated as\n // `-tmp-XXX.dump`) would be misparsed by pg_restore as an unknown\n // option.\n '--',\n dumpPath,\n ],\n { env: postgresEnvFromUrl(databaseUrl) },\n );\n}\n\n/**\n * `createdb`-equivalent. Connects via the maintenance database (default\n * `postgres`) and creates the database named in the URL, or in\n * `options.database` if provided.\n */\nexport async function createPostgresDatabase(\n databaseUrl: string,\n options: CreateDropOptions = {},\n): Promise<void> {\n const database = options.database ?? databaseNameFromUrl(databaseUrl);\n await runCommand(\n options.binary ?? 'createdb',\n [database, ...(options.extraArgs ?? [])],\n {\n env: postgresEnvFromUrl(\n databaseUrl,\n options.maintenanceDatabase ?? 'postgres',\n ),\n },\n );\n}\n\n/**\n * `dropdb`-equivalent, with `--if-exists` by default so callers can use\n * this idempotently as part of a reset flow.\n */\nexport async function dropPostgresDatabase(\n databaseUrl: string,\n options: CreateDropOptions = {},\n): Promise<void> {\n const database = options.database ?? databaseNameFromUrl(databaseUrl);\n await runCommand(\n options.binary ?? 'dropdb',\n ['--if-exists', database, ...(options.extraArgs ?? [])],\n {\n env: postgresEnvFromUrl(\n databaseUrl,\n options.maintenanceDatabase ?? 'postgres',\n ),\n },\n );\n}\n\n/**\n * Spawn helper used by the wrappers above. Inherits stdio so progress and\n * error output stream to the parent terminal (these are interactive ops),\n * unless the caller passes `stdio: 'pipe'` for programmatic use — in\n * which case stderr is captured and included in the thrown error so the\n * failure isn't an opaque `\"pg_dump exited with status 1\"`.\n *\n * Libpq connection vars (PGHOST, PGSERVICE, ...) inherited from the\n * parent process are scrubbed before merging with `options.env`. This is\n * the half of \"URL-determined connection target\" that\n * `postgresEnvFromUrl` can't do alone: without scrubbing, a developer\n * with `PGHOST=staging` in their shell would silently redirect the\n * child's connection regardless of what the URL specified.\n */\nexport async function runCommand(\n command: string,\n args: string[],\n options: {\n cwd?: string;\n env?: Record<string, string>;\n stdio?: 'inherit' | 'pipe';\n } = {},\n): Promise<void> {\n const parentEnv = { ...process.env };\n for (const key of LIBPQ_INHERITANCE_VARS) {\n delete parentEnv[key];\n }\n\n await new Promise<void>((resolvePromise, reject) => {\n const child = spawn(command, args, {\n cwd: options.cwd ?? process.cwd(),\n env: { ...parentEnv, ...options.env },\n stdio: options.stdio ?? 'inherit',\n });\n\n // When the caller pipes stdio, capture stderr so we can surface it\n // in the rejection. Default (`'inherit'`) streams stderr to the\n // parent terminal live; there's nothing to capture in that case.\n const stderrChunks: Buffer[] = [];\n if (options.stdio === 'pipe' && child.stderr) {\n child.stderr.on('data', (chunk: Buffer) => {\n stderrChunks.push(chunk);\n });\n }\n\n child.on('error', reject);\n child.on('close', (code) => {\n if (code === 0) {\n resolvePromise();\n return;\n }\n const stderr = Buffer.concat(stderrChunks).toString('utf8').trim();\n const stderrSuffix = stderr ? `: ${stderr.slice(-2000)}` : '';\n reject(new Error(`${command} exited with status ${code}${stderrSuffix}`));\n });\n });\n}\n\nfunction decodeUserInfo(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n"],"mappings":"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"}