@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
@@ -1,3692 +0,0 @@
1
- import { DatabaseError, loadEnvConfig } from "@happyvertical/utils";
2
- import { r as redactDatabaseUrl } from "./postgres-cli-Cr6bBZ3q.js";
3
- import { mkdir, readdir } from "node:fs/promises";
4
- import { join, extname, basename } from "node:path";
5
- import { Pool } from "pg";
6
- function formatDbError(error) {
7
- const dbError = error;
8
- const parts = [];
9
- if (dbError.message) parts.push(dbError.message);
10
- if (dbError.code) parts.push(`code=${dbError.code}`);
11
- if (dbError.detail) parts.push(`detail=${dbError.detail}`);
12
- if (dbError.hint) parts.push(`hint=${dbError.hint}`);
13
- if (dbError.severity) parts.push(`severity=${dbError.severity}`);
14
- if (dbError.errno !== void 0) parts.push(`errno=${dbError.errno}`);
15
- return parts.length > 0 ? parts.join(", ") : String(error);
16
- }
17
- const VALID_OPERATORS = {
18
- "=": "=",
19
- ">": ">",
20
- ">=": ">=",
21
- "<": "<",
22
- "<=": "<=",
23
- "!=": "!=",
24
- like: "LIKE",
25
- in: "IN",
26
- "not in": "NOT IN"
27
- };
28
- function isSimpleSqlIdentifier(field) {
29
- return /^[a-zA-Z0-9_.]+$/.test(field);
30
- }
31
- function parseConditionKey(fullKey) {
32
- const trimmed = fullKey.trim();
33
- const lower = trimmed.toLowerCase();
34
- const operators = ["not in", "like", "in", "!=", ">=", "<=", ">", "<", "="];
35
- for (const operator of operators) {
36
- if (lower.endsWith(` ${operator}`)) {
37
- return {
38
- field: trimmed.slice(0, -(operator.length + 1)).trim(),
39
- operator,
40
- explicitOperator: true
41
- };
42
- }
43
- }
44
- return { field: trimmed, operator: "=", explicitOperator: false };
45
- }
46
- const buildCondition = (fullKey, value, currIndex, adapterType) => {
47
- const { field, operator, explicitOperator } = parseConditionKey(fullKey);
48
- if (!explicitOperator && !isSimpleSqlIdentifier(field)) {
49
- throw new Error(`Invalid SQL identifier: ${field}`);
50
- }
51
- const sqlOperator = VALID_OPERATORS[operator] || "=";
52
- const values = [];
53
- let sql;
54
- if (value === null) {
55
- sql = `${field} IS ${sqlOperator === "=" ? "NULL" : "NOT NULL"}`;
56
- } else if ((sqlOperator === "IN" || sqlOperator === "NOT IN") && Array.isArray(value)) {
57
- if (value.length === 0) {
58
- throw new Error(`${sqlOperator} requires at least one value`);
59
- }
60
- const placeholders = value.map(() => `$${currIndex.value++}`).join(", ");
61
- sql = `${field} ${sqlOperator} (${placeholders})`;
62
- values.push(...value);
63
- } else if (value instanceof Date) {
64
- const needsCast = adapterType === "duckdb" || adapterType === "json";
65
- if (needsCast) {
66
- sql = `${field} ${sqlOperator} CAST($${currIndex.value++} AS TIMESTAMP)`;
67
- } else {
68
- sql = `${field} ${sqlOperator} $${currIndex.value++}`;
69
- }
70
- values.push(value.toISOString());
71
- } else {
72
- sql = `${field} ${sqlOperator} $${currIndex.value++}`;
73
- values.push(value);
74
- }
75
- return { sql, values };
76
- };
77
- const buildWhere = (where, startIndex = 1, adapterType) => {
78
- let sql = "";
79
- const values = [];
80
- const currIndex = { value: startIndex };
81
- if (Array.isArray(where)) {
82
- if (where.length === 0) {
83
- return { sql: "", values: [] };
84
- }
85
- const orGroups = [];
86
- for (const andGroup of where) {
87
- if (!Array.isArray(andGroup) || andGroup.length === 0) {
88
- continue;
89
- }
90
- const andConditions = [];
91
- for (const conditionObj of andGroup) {
92
- for (const [fullKey, value] of Object.entries(conditionObj)) {
93
- const result = buildCondition(fullKey, value, currIndex, adapterType);
94
- andConditions.push(result.sql);
95
- values.push(...result.values);
96
- }
97
- }
98
- if (andConditions.length > 0) {
99
- orGroups.push(`(${andConditions.join(" AND ")})`);
100
- }
101
- }
102
- if (orGroups.length > 0) {
103
- sql = `WHERE ${orGroups.join(" OR ")}`;
104
- }
105
- return { sql, values };
106
- }
107
- if (where && Object.keys(where).length > 0) {
108
- sql = "WHERE ";
109
- for (const [fullKey, value] of Object.entries(where)) {
110
- const result = buildCondition(fullKey, value, currIndex, adapterType);
111
- if (sql !== "WHERE ") {
112
- sql += " AND ";
113
- }
114
- sql += result.sql;
115
- values.push(...result.values);
116
- }
117
- }
118
- return { sql, values };
119
- };
120
- const VALID_DIRECTIONS = /* @__PURE__ */ new Set(["ASC", "DESC"]);
121
- const VALID_BUCKET_UNITS = /* @__PURE__ */ new Set([
122
- "minute",
123
- "hour",
124
- "day",
125
- "week",
126
- "month",
127
- "quarter",
128
- "year"
129
- ]);
130
- function validateSqlIdentifier(identifier) {
131
- if (!/^[a-zA-Z0-9_.]+$/.test(identifier)) {
132
- throw new Error(`Invalid SQL identifier: ${identifier}`);
133
- }
134
- return identifier;
135
- }
136
- function normalizeAdapter(adapterType) {
137
- return adapterType ?? "sqlite";
138
- }
139
- function placeholder(index2) {
140
- return `$${index2}`;
141
- }
142
- function bucketExpr(adapterType, unit, column) {
143
- if (!VALID_BUCKET_UNITS.has(unit)) {
144
- throw new Error(`Invalid aggregate bucket unit: ${unit}`);
145
- }
146
- const col = validateSqlIdentifier(column);
147
- const adapter = normalizeAdapter(adapterType);
148
- if (adapter === "postgres" || adapter === "duckdb" || adapter === "json") {
149
- return `date_trunc('${unit}', ${col})`;
150
- }
151
- switch (unit) {
152
- case "minute":
153
- return `strftime('%Y-%m-%d %H:%M:00', ${col})`;
154
- case "hour":
155
- return `strftime('%Y-%m-%d %H:00:00', ${col})`;
156
- case "day":
157
- return `strftime('%Y-%m-%d 00:00:00', ${col})`;
158
- case "week":
159
- return `strftime('%Y-%m-%d 00:00:00', date(${col}, printf('-%d days', (CAST(strftime('%w', ${col}) AS INTEGER) + 6) % 7)))`;
160
- case "month":
161
- return `strftime('%Y-%m-01 00:00:00', ${col})`;
162
- case "quarter":
163
- return `strftime('%Y-%m-%d 00:00:00', date(strftime('%Y-', ${col}) || printf('%02d', ((CAST(strftime('%m', ${col}) AS INTEGER) - 1) / 3) * 3 + 1) || '-01'))`;
164
- case "year":
165
- return `strftime('%Y-01-01 00:00:00', ${col})`;
166
- }
167
- }
168
- function aggregateExpression(expr) {
169
- const normalizedFn = String(expr.fn).toLowerCase();
170
- const fn = validateSqlIdentifier(String(expr.fn)).toUpperCase();
171
- if (normalizedFn === "count" && !expr.column) {
172
- if (expr.distinct) {
173
- throw new Error(`Aggregate ${expr.fn} with distinct requires a column`);
174
- }
175
- return "COUNT(*)";
176
- }
177
- if (!expr.column) {
178
- throw new Error(`Aggregate ${expr.fn} requires a column`);
179
- }
180
- const col = validateSqlIdentifier(expr.column);
181
- const distinct = expr.distinct ? "DISTINCT " : "";
182
- return `${fn}(${distinct}${col})`;
183
- }
184
- function selectExpression(expr, adapterType) {
185
- if ("column" in expr && !("fn" in expr) && !("bucket" in expr)) {
186
- const column = validateSqlIdentifier(expr.column);
187
- const alias2 = validateSqlIdentifier(expr.as ?? expr.column);
188
- return { expression: column, alias: alias2, groupingCandidate: true };
189
- }
190
- if ("bucket" in expr) {
191
- const expression2 = bucketExpr(adapterType, expr.bucket, expr.column);
192
- const alias2 = validateSqlIdentifier(expr.as);
193
- return { expression: expression2, alias: alias2, groupingCandidate: true };
194
- }
195
- const expression = aggregateExpression(expr);
196
- const alias = validateSqlIdentifier(expr.as);
197
- return { expression, alias, groupingCandidate: false };
198
- }
199
- function validateWhereShape(where) {
200
- if (!where) return;
201
- const validateKey = (key) => {
202
- const { field } = parseConditionKey(key);
203
- if (field) validateSqlIdentifier(field);
204
- };
205
- if (Array.isArray(where)) {
206
- for (const andGroup of where) {
207
- for (const condition of andGroup) {
208
- for (const key of Object.keys(condition)) validateKey(key);
209
- }
210
- }
211
- return;
212
- }
213
- for (const key of Object.keys(where)) validateKey(key);
214
- }
215
- function expandHavingKey(key, expressionByAlias) {
216
- const parsed = parseConditionKey(key);
217
- const expression = expressionByAlias.get(parsed.field) ?? validateSqlIdentifier(parsed.field);
218
- return `${expression} ${parsed.operator}`;
219
- }
220
- function expandHaving(having, expressionByAlias) {
221
- const expandCondition = (condition) => Object.fromEntries(
222
- Object.entries(condition).map(([key, value]) => [
223
- expandHavingKey(key, expressionByAlias),
224
- value
225
- ])
226
- );
227
- if (Array.isArray(having)) {
228
- return having.map(
229
- (andGroup) => andGroup.map(
230
- (condition) => expandCondition(condition)
231
- )
232
- );
233
- }
234
- return expandCondition(having);
235
- }
236
- function buildHaving(having, expressionByAlias, startIndex, adapterType) {
237
- if (!having) return { sql: "", values: [] };
238
- const expanded = expandHaving(having, expressionByAlias);
239
- const built = buildWhere(expanded, startIndex, adapterType);
240
- return {
241
- sql: built.sql ? built.sql.replace(/^WHERE/, "HAVING") : "",
242
- values: built.values
243
- };
244
- }
245
- function buildOrderBy(orderBy) {
246
- if (!orderBy) return "";
247
- const items = Array.isArray(orderBy) ? orderBy : [orderBy];
248
- const parts = items.map((item) => {
249
- const [field, direction = "ASC"] = item.trim().split(/\s+/);
250
- const normalizedDirection = direction.toUpperCase();
251
- if (!VALID_DIRECTIONS.has(normalizedDirection)) {
252
- throw new Error(`Invalid aggregate order direction: ${direction}`);
253
- }
254
- const orderExpr = validateSqlIdentifier(field);
255
- return `${orderExpr} ${normalizedDirection}`;
256
- });
257
- return `ORDER BY ${parts.join(", ")}`;
258
- }
259
- function buildAggregate(spec, startIndex = 1, adapterType) {
260
- const from = validateSqlIdentifier(spec.from);
261
- if (spec.select.length === 0) {
262
- throw new Error(
263
- "AggregateSpec.select must contain at least one expression"
264
- );
265
- }
266
- validateWhereShape(spec.where);
267
- const expressionByAlias = /* @__PURE__ */ new Map();
268
- const groupingAliases = [];
269
- const selectSql = spec.select.map((expr) => {
270
- const selected = selectExpression(expr, adapterType);
271
- expressionByAlias.set(selected.alias, selected.expression);
272
- if (selected.groupingCandidate) {
273
- groupingAliases.push(selected.alias);
274
- }
275
- return `${selected.expression} AS ${selected.alias}`;
276
- });
277
- const where = spec.where ? buildWhere(spec.where, startIndex, adapterType) : { sql: "", values: [] };
278
- let nextParamIndex = startIndex + where.values.length;
279
- const groupByItems = spec.groupBy ?? groupingAliases;
280
- const groupBySql = groupByItems.length > 0 ? `GROUP BY ${groupByItems.map(
281
- (item) => expressionByAlias.has(item) ? expressionByAlias.get(item) : validateSqlIdentifier(item)
282
- ).join(", ")}` : "";
283
- const having = buildHaving(
284
- spec.having,
285
- expressionByAlias,
286
- nextParamIndex,
287
- adapterType
288
- );
289
- nextParamIndex += having.values.length;
290
- const orderBySql = buildOrderBy(spec.orderBy);
291
- const pagingValues = [];
292
- const pagingSql = [];
293
- if (spec.limit !== void 0) {
294
- pagingSql.push(`LIMIT ${placeholder(nextParamIndex++)}`);
295
- pagingValues.push(spec.limit);
296
- } else if (spec.offset !== void 0 && normalizeAdapter(adapterType) === "sqlite") {
297
- pagingSql.push("LIMIT -1");
298
- }
299
- if (spec.offset !== void 0) {
300
- pagingSql.push(`OFFSET ${placeholder(nextParamIndex++)}`);
301
- pagingValues.push(spec.offset);
302
- }
303
- const sql = [
304
- `SELECT ${selectSql.join(", ")}`,
305
- `FROM ${from}`,
306
- where.sql,
307
- groupBySql,
308
- having.sql,
309
- orderBySql,
310
- pagingSql.join(" ")
311
- ].filter(Boolean).join(" ");
312
- return {
313
- sql,
314
- values: [...where.values, ...having.values, ...pagingValues]
315
- };
316
- }
317
- function parseIndexStatement(indexSQL) {
318
- const trimmed = indexSQL.trim();
319
- const match = trimmed.match(
320
- /CREATE\s+(UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\s+ON\s+(?:"?\w+"?)\s*\(([^)]+)\)/i
321
- );
322
- if (!match) {
323
- return null;
324
- }
325
- const [, uniqueKeyword, name, columnsStr] = match;
326
- const columns = columnsStr.split(",").map((col) => col.trim().replace(/^"|"$/g, ""));
327
- return {
328
- name,
329
- columns,
330
- unique: Boolean(uniqueKeyword),
331
- sql: trimmed
332
- };
333
- }
334
- function convertUniqueIndexesToInlineConstraints(ddl, indexes) {
335
- if (!indexes || indexes.length === 0) {
336
- return { ddl, indexes: [] };
337
- }
338
- const parsedIndexes = indexes.map(parseIndexStatement).filter((idx) => idx !== null);
339
- const uniqueIndexes = parsedIndexes.filter((idx) => idx.unique);
340
- const regularIndexes = parsedIndexes.filter((idx) => !idx.unique);
341
- if (uniqueIndexes.length === 0) {
342
- return { ddl, indexes };
343
- }
344
- const lines = ddl.split("\n");
345
- let closingParenIndex = -1;
346
- for (let i = lines.length - 1; i >= 0; i--) {
347
- const trimmed = lines[i].trim();
348
- if (trimmed === ")" || trimmed.startsWith(");")) {
349
- closingParenIndex = i;
350
- break;
351
- }
352
- }
353
- if (closingParenIndex === -1) {
354
- console.warn(
355
- "[duckdb-schema-utils] Could not find closing parenthesis in CREATE TABLE statement"
356
- );
357
- return { ddl, indexes };
358
- }
359
- const uniqueConstraints = uniqueIndexes.map((idx) => {
360
- const columns = idx.columns.map((col) => `"${col}"`).join(", ");
361
- return ` UNIQUE(${columns})`;
362
- });
363
- const modifiedLines = [...lines];
364
- modifiedLines[closingParenIndex].trim();
365
- if (closingParenIndex > 0) {
366
- const prevLine = modifiedLines[closingParenIndex - 1].trim();
367
- if (!prevLine.endsWith(",")) {
368
- modifiedLines[closingParenIndex - 1] = modifiedLines[closingParenIndex - 1].replace(/\s*$/, ",");
369
- }
370
- }
371
- const constraintLines = uniqueConstraints.map((constraint, idx) => {
372
- return idx < uniqueConstraints.length - 1 ? `${constraint},` : constraint;
373
- });
374
- modifiedLines.splice(closingParenIndex, 0, ...constraintLines);
375
- const transformedDDL = modifiedLines.join("\n");
376
- const remainingIndexes = regularIndexes.map((idx) => idx.sql);
377
- return {
378
- ddl: transformedDDL,
379
- indexes: remainingIndexes
380
- };
381
- }
382
- async function runDoctor(options) {
383
- assertPostgresAdapter(options.db);
384
- const tables = await listPublicTables(options.db);
385
- const issues = [];
386
- for (const check of options.checks) {
387
- const checkIssues = await check({ db: options.db, tables });
388
- issues.push(...checkIssues);
389
- }
390
- return {
391
- failures: issues.filter((issue) => issue.level === "fail"),
392
- warnings: issues.filter((issue) => issue.level === "warn"),
393
- issues
394
- };
395
- }
396
- function assertPostgresAdapter(db) {
397
- const url = String(db.url ?? "");
398
- if (!url.startsWith("postgres://") && !url.startsWith("postgresql://")) {
399
- throw new Error(
400
- `runDoctor requires a Postgres database connection. Got URL: ${url ? redactDatabaseUrl(url) : "(empty)"}. The bundled check builders use Postgres-specific syntax (pg_tables, ::int/::text casts).`
401
- );
402
- }
403
- }
404
- function checkExpectedTables(expected) {
405
- return async ({ tables }) => {
406
- const issues = [];
407
- for (const table of expected) {
408
- if (!tables.has(table)) {
409
- issues.push({
410
- level: "warn",
411
- message: `Missing expected table: ${table}`
412
- });
413
- }
414
- }
415
- return issues;
416
- };
417
- }
418
- function checkUniqueColumn(spec) {
419
- return async ({ db, tables }) => {
420
- if (!tables.has(spec.table)) return [];
421
- const result = await db.query(`
422
- SELECT ${quoteIdentifier(spec.column)} AS value, count(*)::text AS count
423
- FROM ${quoteIdentifier(spec.table)}
424
- WHERE NULLIF(trim(${quoteIdentifier(spec.column)}::text), '') IS NOT NULL
425
- GROUP BY ${quoteIdentifier(spec.column)}
426
- HAVING count(*) > 1
427
- ORDER BY count DESC, ${quoteIdentifier(spec.column)}
428
- `);
429
- return result.rows.map((row) => ({
430
- level: "fail",
431
- message: `Duplicate ${spec.label} "${String(row.value)}" in ${spec.table} (${countValueFromRow(
432
- row.count
433
- )} rows).`
434
- }));
435
- };
436
- }
437
- function checkRelationship(spec) {
438
- return async ({ db, tables }) => {
439
- if (!tables.has(spec.childTable) || !tables.has(spec.parentTable))
440
- return [];
441
- const parentColumn = spec.parentColumn ?? "id";
442
- const result = await db.query(`
443
- SELECT count(*)::text AS count
444
- FROM ${quoteIdentifier(spec.childTable)} child
445
- LEFT JOIN ${quoteIdentifier(spec.parentTable)} parent
446
- ON parent.${quoteIdentifier(parentColumn)} = child.${quoteIdentifier(spec.childColumn)}
447
- WHERE NULLIF(trim(child.${quoteIdentifier(spec.childColumn)}::text), '') IS NOT NULL
448
- AND parent.${quoteIdentifier(parentColumn)} IS NULL
449
- `);
450
- const orphanCount = countValueFromRow(result.rows[0]?.count);
451
- if (orphanCount === "0") return [];
452
- return [
453
- {
454
- level: "fail",
455
- message: `${spec.label}: ${orphanCount} orphaned row${orphanCount === "1" ? "" : "s"}.`
456
- }
457
- ];
458
- };
459
- }
460
- async function listPublicTables(db) {
461
- const result = await db.query(
462
- "SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename"
463
- );
464
- return new Set(result.rows.map((row) => String(row.tablename ?? "")));
465
- }
466
- function quoteIdentifier(value) {
467
- return `"${value.replace(/"/gu, '""')}"`;
468
- }
469
- function countValueFromRow(value) {
470
- if (typeof value === "string") return value;
471
- if (typeof value === "number" || typeof value === "bigint") {
472
- return String(value);
473
- }
474
- return "0";
475
- }
476
- class DatabaseSchemaManager {
477
- static initializationLock = /* @__PURE__ */ new Map();
478
- initializedSchemas = /* @__PURE__ */ new Set();
479
- schemaVersions = /* @__PURE__ */ new Map();
480
- /**
481
- * Initializes database schemas from a JSON manifest with dependency resolution
482
- *
483
- * Schemas are topologically sorted by their declared dependencies and
484
- * initialized in order. Supports legacy raw-SQL schemas via the `schema` option,
485
- * JSON manifests via the `manifest` option, and schema overrides for extending
486
- * base schemas.
487
- *
488
- * @param db - Database interface to execute DDL against
489
- * @param options - Schema initialization options (manifest, overrides, force, debug)
490
- * @returns Result with lists of initialized/skipped schemas, errors, and execution time
491
- */
492
- async initializeSchemas(db, options) {
493
- const startTime = Date.now();
494
- const result = {
495
- initialized: [],
496
- skipped: [],
497
- errors: [],
498
- executionTime: 0
499
- };
500
- try {
501
- let schemas = {};
502
- if (options.schema) {
503
- console.log(
504
- "[schema] Legacy SQL schema provided, converting to manifest format"
505
- );
506
- if (db.syncSchema) {
507
- await db.syncSchema(options.schema);
508
- result.initialized.push("legacy-sql");
509
- }
510
- result.executionTime = Date.now() - startTime;
511
- return result;
512
- }
513
- if (options.manifest) {
514
- schemas = { ...options.manifest.schemas };
515
- }
516
- if (options.overrides) {
517
- schemas = { ...schemas, ...options.overrides };
518
- }
519
- if (Object.keys(schemas).length === 0) {
520
- console.warn("[schema] No schemas provided for initialization");
521
- result.executionTime = Date.now() - startTime;
522
- return result;
523
- }
524
- const dependencyGraph = this.buildDependencyGraph(schemas);
525
- const initializationOrder = this.resolveDependencies(dependencyGraph);
526
- if (options.debug) {
527
- console.log("[schema] Initialization order:", initializationOrder);
528
- }
529
- for (const schemaName of initializationOrder) {
530
- const schema = schemas[schemaName];
531
- if (!schema) continue;
532
- try {
533
- await this.initializeSchema(db, schemaName, schema, {
534
- force: options.force,
535
- debug: options.debug
536
- });
537
- result.initialized.push(schemaName);
538
- } catch (error) {
539
- const errorMessage = error instanceof Error ? error.message : String(error);
540
- result.errors.push({ schema: schemaName, error: errorMessage });
541
- if (options.debug) {
542
- console.error(
543
- `[schema] Failed to initialize ${schemaName}:`,
544
- error
545
- );
546
- }
547
- }
548
- }
549
- result.executionTime = Date.now() - startTime;
550
- if (options.debug) {
551
- console.log("[schema] Initialization complete:", result);
552
- }
553
- return result;
554
- } catch (error) {
555
- result.executionTime = Date.now() - startTime;
556
- const errorMessage = error instanceof Error ? error.message : String(error);
557
- result.errors.push({
558
- schema: "dependency-resolution",
559
- error: errorMessage
560
- });
561
- return result;
562
- }
563
- }
564
- /**
565
- * Initialize a single schema
566
- */
567
- async initializeSchema(db, schemaName, schema, options) {
568
- const { tableName, version } = schema;
569
- const lockKey = `${schemaName}:${tableName}`;
570
- if (DatabaseSchemaManager.initializationLock.has(lockKey)) {
571
- await DatabaseSchemaManager.initializationLock.get(lockKey);
572
- return;
573
- }
574
- if (!options.force && this.isSchemaUpToDate(schemaName, version)) {
575
- if (options.debug) {
576
- console.log(`[schema] Skipping ${schemaName} - already up to date`);
577
- }
578
- return;
579
- }
580
- const initPromise = this.performSchemaInitialization(
581
- db,
582
- schemaName,
583
- schema,
584
- options
585
- );
586
- DatabaseSchemaManager.initializationLock.set(lockKey, initPromise);
587
- try {
588
- await initPromise;
589
- this.markSchemaInitialized(schemaName, version);
590
- } finally {
591
- DatabaseSchemaManager.initializationLock.delete(lockKey);
592
- }
593
- }
594
- /**
595
- * Perform the actual schema initialization
596
- */
597
- async performSchemaInitialization(db, schemaName, schema, options) {
598
- const { tableName, columns, indexes, triggers } = schema;
599
- if (options.debug) {
600
- console.log(`[schema] Initializing ${schemaName} (${tableName})`);
601
- }
602
- const tableExists2 = await db.tableExists(tableName);
603
- if (!tableExists2) {
604
- await this.createTable(db, schema);
605
- for (const index2 of indexes) {
606
- await this.createIndex(db, tableName, index2);
607
- }
608
- for (const trigger of triggers) {
609
- await this.createTrigger(db, trigger);
610
- }
611
- } else if (options.force) {
612
- await db.query(`DROP TABLE IF EXISTS ${tableName}`);
613
- await this.createTable(db, schema);
614
- for (const index2 of indexes) {
615
- await this.createIndex(db, tableName, index2);
616
- }
617
- for (const trigger of triggers) {
618
- await this.createTrigger(db, trigger);
619
- }
620
- } else {
621
- await this.updateSchemaIfNeeded(db, schema);
622
- }
623
- }
624
- /**
625
- * Create table from schema definition
626
- */
627
- async createTable(db, schema) {
628
- const { tableName, columns, foreignKeys } = schema;
629
- const columnDefinitions = [];
630
- for (const [columnName, columnDef] of Object.entries(columns)) {
631
- let def = `${columnName} ${columnDef.type}`;
632
- if (columnDef.primaryKey) def += " PRIMARY KEY";
633
- if (columnDef.unique && !columnDef.primaryKey) def += " UNIQUE";
634
- if (columnDef.notNull) def += " NOT NULL";
635
- if (columnDef.defaultValue !== void 0) {
636
- def += ` DEFAULT ${columnDef.defaultValue}`;
637
- }
638
- if (columnDef.check) def += ` CHECK (${columnDef.check})`;
639
- columnDefinitions.push(def);
640
- }
641
- for (const fk of foreignKeys) {
642
- let fkDef = `FOREIGN KEY (${fk.column}) REFERENCES ${fk.referencesTable}(${fk.referencesColumn})`;
643
- if (fk.onDelete) fkDef += ` ON DELETE ${fk.onDelete}`;
644
- if (fk.onUpdate) fkDef += ` ON UPDATE ${fk.onUpdate}`;
645
- columnDefinitions.push(fkDef);
646
- }
647
- const createTableSQL = `CREATE TABLE ${tableName} (
648
- ${columnDefinitions.join(",\n ")}
649
- )`;
650
- await db.query(createTableSQL);
651
- }
652
- /**
653
- * Create index from definition
654
- */
655
- async createIndex(db, tableName, index2) {
656
- const uniqueClause = index2.unique ? "UNIQUE " : "";
657
- const whereClause = index2.where ? ` WHERE ${index2.where}` : "";
658
- const createIndexSQL = `CREATE ${uniqueClause}INDEX ${index2.name} ON ${tableName} (${index2.columns.join(", ")})${whereClause}`;
659
- await db.query(createIndexSQL);
660
- }
661
- /**
662
- * Create trigger from definition
663
- */
664
- async createTrigger(db, trigger) {
665
- const conditionClause = trigger.condition ? ` WHEN ${trigger.condition}` : "";
666
- const createTriggerSQL = `CREATE TRIGGER ${trigger.name} ${trigger.when} ${trigger.event} ON ${trigger.table}${conditionClause} BEGIN ${trigger.body} END`;
667
- await db.query(createTriggerSQL);
668
- }
669
- /**
670
- * Update schema if changes are detected
671
- */
672
- async updateSchemaIfNeeded(_db, schema) {
673
- console.log(
674
- `[schema] Schema update logic not yet implemented for ${schema.tableName}`
675
- );
676
- }
677
- /**
678
- * Build dependency graph from schemas
679
- */
680
- buildDependencyGraph(schemas) {
681
- const graph = /* @__PURE__ */ new Map();
682
- for (const [schemaName, schema] of Object.entries(schemas)) {
683
- const dependencies = schema.dependencies.filter(
684
- (dep) => Object.values(schemas).some((s) => s.tableName === dep)
685
- );
686
- graph.set(
687
- schemaName,
688
- dependencies.map(
689
- (dep) => Object.entries(schemas).find(
690
- ([_, s]) => s.tableName === dep
691
- )?.[0] || dep
692
- )
693
- );
694
- }
695
- return graph;
696
- }
697
- /**
698
- * Resolve dependencies using topological sort
699
- */
700
- resolveDependencies(graph) {
701
- const resolved = [];
702
- const visited = /* @__PURE__ */ new Set();
703
- const visiting = /* @__PURE__ */ new Set();
704
- const visit = (node) => {
705
- if (visiting.has(node)) {
706
- throw new Error(`Circular dependency detected involving ${node}`);
707
- }
708
- if (visited.has(node)) return;
709
- visiting.add(node);
710
- const dependencies = graph.get(node) || [];
711
- for (const dep of dependencies) {
712
- if (graph.has(dep)) {
713
- visit(dep);
714
- }
715
- }
716
- visiting.delete(node);
717
- visited.add(node);
718
- resolved.push(node);
719
- };
720
- for (const node of graph.keys()) {
721
- if (!visited.has(node)) {
722
- visit(node);
723
- }
724
- }
725
- return resolved;
726
- }
727
- /**
728
- * Check if schema is up to date
729
- */
730
- isSchemaUpToDate(schemaName, version) {
731
- return this.initializedSchemas.has(schemaName) && this.schemaVersions.get(schemaName) === version;
732
- }
733
- /**
734
- * Mark schema as initialized
735
- */
736
- markSchemaInitialized(schemaName, version) {
737
- this.initializedSchemas.add(schemaName);
738
- this.schemaVersions.set(schemaName, version);
739
- }
740
- /**
741
- * Reset initialization state (for testing)
742
- */
743
- reset() {
744
- this.initializedSchemas.clear();
745
- this.schemaVersions.clear();
746
- DatabaseSchemaManager.initializationLock.clear();
747
- }
748
- }
749
- function resolveSchemas(schemas) {
750
- if (!schemas) return void 0;
751
- if (typeof schemas === "function") return schemas();
752
- return schemas;
753
- }
754
- globalThis.__haveSqlMemoryConnectionCache ??= /* @__PURE__ */ new Map();
755
- const memoryConnectionCache = globalThis.__haveSqlMemoryConnectionCache;
756
- globalThis.__haveSqlPendingConnections ??= /* @__PURE__ */ new Map();
757
- const pendingConnections$1 = globalThis.__haveSqlPendingConnections;
758
- function clearConnectionCache() {
759
- memoryConnectionCache.clear();
760
- pendingConnections$1.clear();
761
- }
762
- async function createJSONConnection(options) {
763
- const { url, autoRegister = true, eagerLoadTables = true } = options;
764
- const schemas = resolveSchemas(options.schemas) ?? {};
765
- if (!url) {
766
- throw new DatabaseError("url is required for JSON adapter", {
767
- options
768
- });
769
- }
770
- const pendingJSONFiles = /* @__PURE__ */ new Map();
771
- const inferredSchemaTables = /* @__PURE__ */ new Set();
772
- try {
773
- const duckdbModule = "@duckdb/node-api";
774
- const { DuckDBInstance } = await import(
775
- /* @vite-ignore */
776
- duckdbModule
777
- );
778
- const instance = await DuckDBInstance.create(":memory:");
779
- const connection = await instance.connect();
780
- try {
781
- await mkdir(url, { recursive: true });
782
- } catch (_error) {
783
- }
784
- if (autoRegister) {
785
- await loadJSONTables(
786
- connection,
787
- url,
788
- schemas,
789
- pendingJSONFiles,
790
- eagerLoadTables,
791
- inferredSchemaTables
792
- );
793
- }
794
- return { connection, pendingJSONFiles, inferredSchemaTables };
795
- } catch (error) {
796
- const errorMessage = error instanceof Error ? error.message : String(error);
797
- throw new DatabaseError(
798
- `Failed to create JSON database connection: ${errorMessage}`,
799
- {
800
- url,
801
- originalError: errorMessage
802
- }
803
- );
804
- }
805
- }
806
- async function loadJSONData(connection, tableName, filePath) {
807
- try {
808
- const { readFile } = await import("node:fs/promises");
809
- const jsonContent = await readFile(filePath, "utf-8");
810
- const records = JSON.parse(jsonContent);
811
- if (Array.isArray(records) && records.length > 0) {
812
- await insertRecordsWithCast(connection, tableName, records);
813
- console.log(
814
- `[json-adapter] Loaded ${records.length} records into ${tableName} from JSON`
815
- );
816
- }
817
- } catch (error) {
818
- console.warn(
819
- `[json-adapter] Could not load data for ${tableName}: ${error instanceof Error ? error.message : String(error)}`
820
- );
821
- }
822
- }
823
- async function loadJSONTables(connection, dataDir, providedSchemas = {}, pendingJSONFiles = /* @__PURE__ */ new Map(), eagerLoadTables = true, inferredSchemaTables = /* @__PURE__ */ new Set()) {
824
- try {
825
- const files = await readdir(dataDir);
826
- const jsonFiles = files.filter(
827
- (file) => extname(file).toLowerCase() === ".json"
828
- );
829
- for (const file of jsonFiles) {
830
- const filePath = join(dataDir, file);
831
- const tableName = basename(file, ".json");
832
- const schema = providedSchemas[tableName];
833
- if (schema) {
834
- console.log(
835
- `[json-adapter] Creating ${tableName} with provided schema definition`
836
- );
837
- await createTableFromSchema(connection, tableName, schema);
838
- await loadJSONData(connection, tableName, filePath);
839
- } else {
840
- const schemaPath = join(dataDir, `${tableName}.schema.sql`);
841
- const { readFile, access } = await import("node:fs/promises");
842
- const { constants } = await import("node:fs");
843
- let schemaExists = false;
844
- try {
845
- await access(schemaPath, constants.F_OK);
846
- schemaExists = true;
847
- } catch {
848
- schemaExists = false;
849
- }
850
- if (schemaExists) {
851
- try {
852
- const schemaDDL = await readFile(schemaPath, "utf-8");
853
- await connection.run(schemaDDL);
854
- await loadJSONData(connection, tableName, filePath);
855
- } catch (error) {
856
- console.warn(
857
- `[json-adapter] Could not load ${tableName} with schema file: ${error instanceof Error ? error.message : String(error)}`
858
- );
859
- }
860
- } else {
861
- if (eagerLoadTables) {
862
- try {
863
- await connection.run(
864
- `CREATE TABLE IF NOT EXISTS "${tableName}" AS SELECT * FROM read_json_auto('${filePath}')`
865
- );
866
- inferredSchemaTables.add(tableName);
867
- console.log(
868
- `[json-adapter] Eagerly created ${tableName} with inferred schema (no UNIQUE constraints)`
869
- );
870
- } catch (error) {
871
- console.warn(
872
- `[json-adapter] Could not eagerly load ${tableName}: ${error instanceof Error ? error.message : String(error)}`
873
- );
874
- }
875
- } else {
876
- console.log(
877
- `[json-adapter] Deferring ${tableName} - will load after syncSchema() creates table`
878
- );
879
- }
880
- pendingJSONFiles.set(tableName, filePath);
881
- }
882
- }
883
- }
884
- } catch (error) {
885
- if (error.code !== "ENOENT") {
886
- console.error("[json-adapter] Error loading JSON tables:", error);
887
- throw new DatabaseError(`Failed to load JSON tables from ${dataDir}`, {
888
- dataDir,
889
- originalError: error instanceof Error ? error.message : String(error),
890
- stack: error instanceof Error ? error.stack : void 0
891
- });
892
- }
893
- }
894
- }
895
- function convertBigInts(obj) {
896
- if (obj === null || obj === void 0) return obj;
897
- if (typeof obj === "bigint") return Number(obj);
898
- if (typeof obj === "object" && !Array.isArray(obj) && "hugeint" in obj && Object.keys(obj).length === 1) {
899
- return String(obj.hugeint);
900
- }
901
- if (typeof obj === "object" && !Array.isArray(obj) && "micros" in obj && Object.keys(obj).length === 1 && (typeof obj.micros === "number" || typeof obj.micros === "bigint")) {
902
- const micros = typeof obj.micros === "bigint" ? Number(obj.micros) : obj.micros;
903
- const milliseconds = micros / 1e3;
904
- return new Date(milliseconds);
905
- }
906
- if (Array.isArray(obj)) return obj.map(convertBigInts);
907
- if (typeof obj === "object") {
908
- const result = {};
909
- for (const key in obj) {
910
- result[key] = convertBigInts(obj[key]);
911
- }
912
- return result;
913
- }
914
- return obj;
915
- }
916
- async function createTableFromSchema(connection, tableName, schema) {
917
- const ddlLines = schema.ddl.split("\n");
918
- const createTableEnd = ddlLines.findIndex((line) => {
919
- const trimmed = line.trim();
920
- return trimmed === ")" || trimmed.startsWith(");");
921
- });
922
- if (createTableEnd === -1) {
923
- throw new DatabaseError("Invalid schema DDL - no closing parenthesis", {
924
- tableName,
925
- ddl: schema.ddl
926
- });
927
- }
928
- let createTableSQL = ddlLines.slice(0, createTableEnd + 1).join("\n");
929
- createTableSQL = createTableSQL.replace(
930
- /\b(\w+)\s+(TEXT|VARCHAR)\s+DEFAULT\s+''/g,
931
- "$1 $2 DEFAULT CAST('' AS $2)"
932
- );
933
- createTableSQL = createTableSQL.replace(
934
- /\b(\w+)\s+(TEXT|VARCHAR)\s+DEFAULT\s+NULL/g,
935
- "$1 $2 DEFAULT CAST(NULL AS $2)"
936
- );
937
- const transformed = convertUniqueIndexesToInlineConstraints(
938
- createTableSQL,
939
- schema.indexes
940
- );
941
- createTableSQL = transformed.ddl;
942
- const indexesToCreate = transformed.indexes;
943
- try {
944
- await connection.run(createTableSQL);
945
- let existingColumns = /* @__PURE__ */ new Set();
946
- try {
947
- const schemaInfo = await connection.runAndReadAll(
948
- `DESCRIBE ${tableName}`
949
- );
950
- const columns = schemaInfo.getRowObjects();
951
- existingColumns = new Set(
952
- columns.map((col) => col.column_name.toLowerCase())
953
- );
954
- console.log(`[json-adapter] Created ${tableName} with schema:`, {
955
- columns: columns.map((col) => ({
956
- name: col.column_name,
957
- type: col.column_type,
958
- null: col.null
959
- }))
960
- });
961
- } catch (error) {
962
- console.warn(
963
- `[json-adapter] Could not verify schema for ${tableName}: ${error instanceof Error ? error.message : String(error)}`
964
- );
965
- }
966
- if (schema.fields && existingColumns.size > 0) {
967
- const fieldEntries = schema.fields instanceof Map ? Array.from(schema.fields.entries()) : Object.entries(schema.fields);
968
- for (const [fieldName, fieldDef] of fieldEntries) {
969
- const columnName = fieldName.toLowerCase();
970
- if (!existingColumns.has(columnName)) {
971
- const sqlType = fieldDef?.type || "TEXT";
972
- try {
973
- await connection.run(
974
- `ALTER TABLE ${tableName} ADD COLUMN "${fieldName}" ${sqlType}`
975
- );
976
- console.log(
977
- `[json-adapter] Added missing column '${fieldName}' (${sqlType}) to ${tableName}`
978
- );
979
- } catch (alterError) {
980
- console.warn(
981
- `[json-adapter] Could not add column '${fieldName}' to ${tableName}: ${alterError instanceof Error ? alterError.message : String(alterError)}`
982
- );
983
- }
984
- }
985
- }
986
- }
987
- if (indexesToCreate && indexesToCreate.length > 0) {
988
- for (const indexSQL of indexesToCreate) {
989
- try {
990
- await connection.run(indexSQL);
991
- } catch (error) {
992
- console.warn(
993
- `[json-adapter] Failed to create index: ${error instanceof Error ? error.message : String(error)}`
994
- );
995
- }
996
- }
997
- }
998
- } catch (error) {
999
- throw new DatabaseError("Failed to create table from schema", {
1000
- tableName,
1001
- sql: createTableSQL,
1002
- originalError: error instanceof Error ? error.message : String(error)
1003
- });
1004
- }
1005
- }
1006
- async function insertRecordsWithCast(connection, tableName, records) {
1007
- if (records.length === 0) return;
1008
- const keys = Object.keys(records[0]);
1009
- const values = [];
1010
- let paramIdx = 1;
1011
- const placeholders = records.map((record) => {
1012
- const rowPlaceholders = keys.map((key) => {
1013
- const value = record[key];
1014
- if (value === null) {
1015
- return "NULL";
1016
- } else if (value === "" && typeof value === "string") {
1017
- values.push(value);
1018
- return `CAST($${paramIdx++} AS TEXT)`;
1019
- } else if (value instanceof Date) {
1020
- values.push(value.toISOString());
1021
- return `CAST($${paramIdx++} AS TIMESTAMP)`;
1022
- } else if (Array.isArray(value)) {
1023
- values.push(JSON.stringify(value));
1024
- return `CAST($${paramIdx++} AS JSON)`;
1025
- } else if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
1026
- values.push(JSON.stringify(value));
1027
- return `CAST($${paramIdx++} AS JSON)`;
1028
- } else {
1029
- values.push(value);
1030
- return `$${paramIdx++}`;
1031
- }
1032
- });
1033
- return `(${rowPlaceholders.join(", ")})`;
1034
- }).join(", ");
1035
- const sql = `INSERT INTO ${tableName} (${keys.join(", ")}) VALUES ${placeholders}`;
1036
- await connection.run(sql, values);
1037
- }
1038
- function validateTableName$1(table) {
1039
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) {
1040
- throw new DatabaseError(`Invalid table name: ${table}`, { table });
1041
- }
1042
- }
1043
- async function getDatabase$2(options) {
1044
- const cacheKey = options.dbid || `json:${options.url}:${options.writeStrategy || "immediate"}:${options.autoRegister !== false}`;
1045
- if (options.clearCache) {
1046
- memoryConnectionCache.delete(cacheKey);
1047
- pendingConnections$1.delete(cacheKey);
1048
- }
1049
- const cached = memoryConnectionCache.get(cacheKey);
1050
- if (cached) {
1051
- return cached;
1052
- }
1053
- const pending = pendingConnections$1.get(cacheKey);
1054
- if (pending) {
1055
- return pending;
1056
- }
1057
- const connectionPromise = (async () => {
1058
- const { connection, pendingJSONFiles, inferredSchemaTables } = await createJSONConnection(options);
1059
- const writeStrategy = options.writeStrategy || "immediate";
1060
- const { url } = options;
1061
- const insert = async (table2, data) => {
1062
- validateTableName$1(table2);
1063
- if (writeStrategy === "none") {
1064
- throw new DatabaseError(
1065
- "Cannot insert: write strategy is set to none (read-only mode)",
1066
- { table: table2, writeStrategy }
1067
- );
1068
- }
1069
- const records = Array.isArray(data) ? data : [data];
1070
- if (records.length === 0) {
1071
- return { operation: "insert", affected: 0 };
1072
- }
1073
- const keys = Object.keys(records[0]);
1074
- const values = [];
1075
- let paramIdx = 1;
1076
- const placeholders = records.map((record) => {
1077
- const rowPlaceholders = keys.map((key) => {
1078
- const value = record[key];
1079
- if (value === null) {
1080
- return "NULL";
1081
- } else if (value === "" && typeof value === "string") {
1082
- values.push(value);
1083
- return `CAST($${paramIdx++} AS TEXT)`;
1084
- } else if (value instanceof Date) {
1085
- values.push(value.toISOString());
1086
- return `CAST($${paramIdx++} AS TIMESTAMP)`;
1087
- } else if (Array.isArray(value)) {
1088
- values.push(JSON.stringify(value));
1089
- return `CAST($${paramIdx++} AS JSON)`;
1090
- } else if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
1091
- values.push(JSON.stringify(value));
1092
- return `CAST($${paramIdx++} AS JSON)`;
1093
- } else {
1094
- values.push(value);
1095
- return `$${paramIdx++}`;
1096
- }
1097
- });
1098
- return `(${rowPlaceholders.join(", ")})`;
1099
- }).join(", ");
1100
- const sql = `INSERT INTO "${table2}" (${keys.join(", ")}) VALUES ${placeholders}`;
1101
- try {
1102
- await connection.run(sql, values);
1103
- const affected = records.length;
1104
- if (writeStrategy === "immediate") {
1105
- await exportTableToJSON(connection, table2, url);
1106
- }
1107
- return { operation: "insert", affected };
1108
- } catch (e) {
1109
- throw new DatabaseError("Failed to insert records into table", {
1110
- table: table2,
1111
- sql,
1112
- values,
1113
- originalError: formatDbError(e)
1114
- });
1115
- }
1116
- };
1117
- const get = async (table2, where) => {
1118
- validateTableName$1(table2);
1119
- const { sql: whereClause, values } = buildWhere(where, 1, "json");
1120
- const sql = `SELECT * FROM "${table2}" ${whereClause} LIMIT 1`;
1121
- try {
1122
- const reader = await connection.runAndReadAll(sql, values);
1123
- const rows = reader.getRowObjects();
1124
- return rows.length > 0 ? convertBigInts(rows[0]) : null;
1125
- } catch (e) {
1126
- throw new DatabaseError("Failed to retrieve record from table", {
1127
- table: table2,
1128
- sql,
1129
- values,
1130
- originalError: formatDbError(e)
1131
- });
1132
- }
1133
- };
1134
- const list = async (table2, where) => {
1135
- validateTableName$1(table2);
1136
- const { sql: whereClause, values } = buildWhere(where, 1, "json");
1137
- const sql = `SELECT * FROM "${table2}" ${whereClause}`;
1138
- try {
1139
- const reader = await connection.runAndReadAll(sql, values);
1140
- return convertBigInts(reader.getRowObjects());
1141
- } catch (e) {
1142
- throw new DatabaseError("Failed to list records from table", {
1143
- table: table2,
1144
- sql,
1145
- values,
1146
- originalError: formatDbError(e)
1147
- });
1148
- }
1149
- };
1150
- const update = async (table2, where, data) => {
1151
- validateTableName$1(table2);
1152
- if (writeStrategy === "none") {
1153
- throw new DatabaseError(
1154
- "Cannot update: write strategy is set to none (read-only mode)",
1155
- { table: table2, writeStrategy }
1156
- );
1157
- }
1158
- const keys = Object.keys(data);
1159
- const setClause = keys.map((key, idx) => `${key} = $${idx + 1}`).join(", ");
1160
- const { sql: whereClause, values: whereValues } = buildWhere(
1161
- where,
1162
- keys.length + 1,
1163
- "json"
1164
- );
1165
- const dataValues = Object.values(data).map(
1166
- (value) => value instanceof Date ? value.toISOString() : value
1167
- );
1168
- const sql = `UPDATE "${table2}" SET ${setClause} ${whereClause}`;
1169
- const values = [...dataValues, ...whereValues];
1170
- try {
1171
- await connection.run(sql, values);
1172
- if (writeStrategy === "immediate") {
1173
- await exportTableToJSON(connection, table2, url);
1174
- }
1175
- return { operation: "update", affected: 1 };
1176
- } catch (e) {
1177
- throw new DatabaseError("Failed to update records in table", {
1178
- table: table2,
1179
- sql,
1180
- values,
1181
- originalError: formatDbError(e)
1182
- });
1183
- }
1184
- };
1185
- const buildJSONValueExpression = (value, values, paramIdx) => {
1186
- if (value === null) {
1187
- return "NULL";
1188
- }
1189
- if (value === "" && typeof value === "string") {
1190
- values.push(value);
1191
- return `CAST($${paramIdx.value++} AS TEXT)`;
1192
- }
1193
- if (value instanceof Date) {
1194
- values.push(value.toISOString());
1195
- return `CAST($${paramIdx.value++} AS TIMESTAMP)`;
1196
- }
1197
- if (Array.isArray(value)) {
1198
- values.push(JSON.stringify(value));
1199
- return `CAST($${paramIdx.value++} AS JSON)`;
1200
- }
1201
- if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
1202
- values.push(JSON.stringify(value));
1203
- return `CAST($${paramIdx.value++} AS JSON)`;
1204
- }
1205
- values.push(value);
1206
- return `$${paramIdx.value++}`;
1207
- };
1208
- const executeNullAwareJSONUpsert = async (table2, conflictColumns, data) => {
1209
- const conflictWhere = Object.fromEntries(
1210
- conflictColumns.map((col) => [col, data[col]])
1211
- );
1212
- const existing = await get(table2, conflictWhere);
1213
- if (!existing) {
1214
- return insert(table2, data);
1215
- }
1216
- const keys = Object.keys(data);
1217
- const values = [];
1218
- const paramIdx = { value: 1 };
1219
- const assignments = keys.map((key) => {
1220
- const valueExpr = buildJSONValueExpression(data[key], values, paramIdx);
1221
- return `"${key}" = ${valueExpr}`;
1222
- });
1223
- const { sql: whereClause, values: whereValues } = buildWhere(
1224
- conflictWhere,
1225
- paramIdx.value,
1226
- "json"
1227
- );
1228
- const sql = `UPDATE "${table2}" SET ${assignments.join(", ")} ${whereClause}`;
1229
- try {
1230
- await connection.run(sql, [...values, ...whereValues]);
1231
- if (writeStrategy === "immediate") {
1232
- await exportTableToJSON(connection, table2, url);
1233
- }
1234
- return { operation: "upsert", affected: 1 };
1235
- } catch (e) {
1236
- throw new DatabaseError("Failed to upsert record into table", {
1237
- table: table2,
1238
- sql,
1239
- values: [...values, ...whereValues],
1240
- conflictColumns,
1241
- originalError: formatDbError(e)
1242
- });
1243
- }
1244
- };
1245
- const upsert = async (table2, conflictColumns, data, options2) => {
1246
- validateTableName$1(table2);
1247
- if (writeStrategy === "none") {
1248
- throw new DatabaseError(
1249
- "Cannot upsert: write strategy is set to none (read-only mode)",
1250
- { table: table2, writeStrategy }
1251
- );
1252
- }
1253
- const missingColumns = conflictColumns.filter((col) => !(col in data));
1254
- if (missingColumns.length > 0) {
1255
- throw new DatabaseError("Conflict columns missing from data", {
1256
- table: table2,
1257
- conflictColumns,
1258
- missingColumns,
1259
- availableColumns: Object.keys(data),
1260
- 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."
1261
- });
1262
- }
1263
- if (!options2?.nullsDistinct && conflictColumns.some((col) => data[col] === null)) {
1264
- return executeNullAwareJSONUpsert(table2, conflictColumns, data);
1265
- }
1266
- const keys = Object.keys(data);
1267
- const dataValues = Object.values(data);
1268
- const placeholders = [];
1269
- const values = [];
1270
- let paramIdx = 1;
1271
- for (const value of dataValues) {
1272
- if (value === null) {
1273
- placeholders.push("NULL");
1274
- } else if (value === "" && typeof value === "string") {
1275
- placeholders.push(`CAST($${paramIdx} AS TEXT)`);
1276
- values.push(value);
1277
- paramIdx++;
1278
- } else if (value instanceof Date) {
1279
- placeholders.push(`CAST($${paramIdx} AS TIMESTAMP)`);
1280
- values.push(value.toISOString());
1281
- paramIdx++;
1282
- } else if (Array.isArray(value)) {
1283
- placeholders.push(`CAST($${paramIdx} AS JSON)`);
1284
- values.push(JSON.stringify(value));
1285
- paramIdx++;
1286
- } else if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
1287
- placeholders.push(`CAST($${paramIdx} AS JSON)`);
1288
- values.push(JSON.stringify(value));
1289
- paramIdx++;
1290
- } else {
1291
- placeholders.push(`$${paramIdx}`);
1292
- values.push(value);
1293
- paramIdx++;
1294
- }
1295
- }
1296
- const updateSetParts = [];
1297
- for (let i = 0; i < keys.length; i++) {
1298
- const key = keys[i];
1299
- const value = dataValues[i];
1300
- if (value === null) {
1301
- updateSetParts.push(`${key} = NULL`);
1302
- } else if (value === "" && typeof value === "string") {
1303
- updateSetParts.push(`${key} = CAST($${paramIdx} AS TEXT)`);
1304
- values.push(value);
1305
- paramIdx++;
1306
- } else if (value instanceof Date) {
1307
- updateSetParts.push(`${key} = CAST($${paramIdx} AS TIMESTAMP)`);
1308
- values.push(value.toISOString());
1309
- paramIdx++;
1310
- } else if (Array.isArray(value)) {
1311
- updateSetParts.push(`${key} = CAST($${paramIdx} AS JSON)`);
1312
- values.push(JSON.stringify(value));
1313
- paramIdx++;
1314
- } else if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
1315
- updateSetParts.push(`${key} = CAST($${paramIdx} AS JSON)`);
1316
- values.push(JSON.stringify(value));
1317
- paramIdx++;
1318
- } else {
1319
- updateSetParts.push(`${key} = $${paramIdx}`);
1320
- values.push(value);
1321
- paramIdx++;
1322
- }
1323
- }
1324
- const conflict = conflictColumns.map((col) => `"${col}"`).join(", ");
1325
- const quotedKeys = keys.map((key) => `"${key}"`).join(", ");
1326
- const quotedUpdateSetParts = keys.map((key, i) => {
1327
- const part = updateSetParts[i];
1328
- const valueExpr = part.substring(part.indexOf("=") + 1).trim();
1329
- return `"${key}" = ${valueExpr}`;
1330
- });
1331
- const sql = `INSERT INTO "${table2}" (${quotedKeys}) VALUES (${placeholders.join(", ")}) ON CONFLICT(${conflict}) DO UPDATE SET ${quotedUpdateSetParts.join(", ")}`;
1332
- try {
1333
- await connection.run(sql, values);
1334
- if (writeStrategy === "immediate") {
1335
- await exportTableToJSON(connection, table2, url);
1336
- }
1337
- return { operation: "upsert", affected: 1 };
1338
- } catch (e) {
1339
- console.error("UPSERT failed:", {
1340
- table: table2,
1341
- sql,
1342
- values,
1343
- valueTypes: values.map((v) => `${typeof v} (${v})`),
1344
- conflictColumns,
1345
- error: formatDbError(e)
1346
- });
1347
- throw new DatabaseError("Failed to upsert record into table", {
1348
- table: table2,
1349
- sql,
1350
- values,
1351
- conflictColumns,
1352
- originalError: formatDbError(e)
1353
- });
1354
- }
1355
- };
1356
- const getOrInsert = async (table2, where, data) => {
1357
- const result = await get(table2, where);
1358
- if (result) return result;
1359
- await insert(table2, data);
1360
- const inserted = await get(table2, where);
1361
- if (!inserted) {
1362
- throw new DatabaseError("Failed to insert and retrieve record", {
1363
- table: table2,
1364
- where,
1365
- data
1366
- });
1367
- }
1368
- return inserted;
1369
- };
1370
- const deleteRecords = async (table2, where) => {
1371
- validateTableName$1(table2);
1372
- if (writeStrategy === "none") {
1373
- throw new DatabaseError(
1374
- "Cannot delete: write strategy is set to none (read-only mode)",
1375
- { table: table2, writeStrategy }
1376
- );
1377
- }
1378
- const keys = Object.keys(where);
1379
- if (keys.length === 0) {
1380
- throw new DatabaseError(
1381
- "DELETE requires at least one WHERE condition to prevent accidental deletion of all records",
1382
- { table: table2 }
1383
- );
1384
- }
1385
- const { sql: whereClause, values } = buildWhere(where, 1, "json");
1386
- const sql = `DELETE FROM "${table2}" ${whereClause}`;
1387
- try {
1388
- await connection.run(sql, values);
1389
- if (writeStrategy === "immediate") {
1390
- await exportTableToJSON(connection, table2, url);
1391
- }
1392
- return { operation: "delete", affected: 1 };
1393
- } catch (e) {
1394
- throw new DatabaseError("Failed to delete records from table", {
1395
- table: table2,
1396
- sql,
1397
- values,
1398
- originalError: formatDbError(e)
1399
- });
1400
- }
1401
- };
1402
- const count = async (table2, where) => {
1403
- validateTableName$1(table2);
1404
- try {
1405
- if (!where || Object.keys(where).length === 0) {
1406
- const result2 = await connection.runAndReadAll(
1407
- `SELECT COUNT(*) as count FROM "${table2}"`
1408
- );
1409
- const rows2 = result2.getRowObjects();
1410
- return Number(rows2[0]?.count) || 0;
1411
- }
1412
- const { sql: whereClause, values } = buildWhere(where, 1, "json");
1413
- const sql = `SELECT COUNT(*) as count FROM "${table2}" ${whereClause}`;
1414
- const result = await connection.runAndReadAll(sql, values);
1415
- const rows = result.getRowObjects();
1416
- return Number(rows[0]?.count) || 0;
1417
- } catch (e) {
1418
- throw new DatabaseError("Failed to count records in table", {
1419
- table: table2,
1420
- where,
1421
- originalError: formatDbError(e)
1422
- });
1423
- }
1424
- };
1425
- const tableExists2 = async (tableName) => {
1426
- try {
1427
- await connection.runAndReadAll(`SELECT * FROM ${tableName} LIMIT 1`);
1428
- return true;
1429
- } catch {
1430
- return false;
1431
- }
1432
- };
1433
- const exportTableToJSON = async (connection2, table2, dataDir) => {
1434
- const filePath = join(dataDir, `${table2}.json`);
1435
- const schemaPath = join(dataDir, `${table2}.schema.sql`);
1436
- try {
1437
- const schemaResult = await connection2.runAndReadAll(
1438
- `SELECT sql FROM sqlite_master WHERE type='table' AND name='${table2}'`
1439
- );
1440
- const rows = schemaResult.getRowObjects();
1441
- if (rows.length > 0 && rows[0].sql) {
1442
- const { writeFile } = await import("node:fs/promises");
1443
- await writeFile(schemaPath, rows[0].sql, "utf-8");
1444
- }
1445
- } catch (error) {
1446
- console.warn(
1447
- `[json-adapter] Could not export schema for ${table2}: ${error instanceof Error ? error.message : String(error)}`
1448
- );
1449
- }
1450
- const columnsResult = await connection2.runAndReadAll(
1451
- `SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '${table2}' ORDER BY ordinal_position`
1452
- );
1453
- const columnInfo = columnsResult.getRowObjects();
1454
- if (columnInfo.length === 0) {
1455
- await connection2.run(
1456
- `COPY (SELECT * FROM "${table2}") TO '${filePath}' (FORMAT JSON, ARRAY true)`
1457
- );
1458
- return;
1459
- }
1460
- const selectList = columnInfo.map(({ column_name, data_type }) => {
1461
- const upperType = data_type.toUpperCase();
1462
- if (upperType === "JSON" || upperType.startsWith("STRUCT")) {
1463
- return `"${column_name}"`;
1464
- }
1465
- if ([
1466
- "DOUBLE",
1467
- "REAL",
1468
- "FLOAT",
1469
- "INTEGER",
1470
- "BIGINT",
1471
- "SMALLINT",
1472
- "TINYINT",
1473
- "DECIMAL",
1474
- "NUMERIC",
1475
- "HUGEINT",
1476
- "UBIGINT",
1477
- "UINTEGER",
1478
- "USMALLINT",
1479
- "UTINYINT"
1480
- ].some((t) => upperType.includes(t))) {
1481
- return `"${column_name}"`;
1482
- }
1483
- if (upperType === "BOOLEAN" || upperType === "BOOL") {
1484
- return `"${column_name}"`;
1485
- }
1486
- return `CAST("${column_name}" AS TEXT) AS "${column_name}"`;
1487
- }).join(", ");
1488
- const sql = `COPY (SELECT ${selectList} FROM "${table2}") TO '${filePath}' (FORMAT JSON, ARRAY true)`;
1489
- await connection2.run(sql);
1490
- };
1491
- const exportTable = async (table2) => {
1492
- if (writeStrategy === "none") {
1493
- throw new DatabaseError(
1494
- "Cannot export table: write strategy is set to none (read-only mode)",
1495
- { table: table2, writeStrategy }
1496
- );
1497
- }
1498
- await exportTableToJSON(connection, table2, url);
1499
- };
1500
- const inferSchemaFromJSON = async (tableName, jsonPath) => {
1501
- validateTableName$1(tableName);
1502
- const filePath = jsonPath || join(url, `${tableName}.json`);
1503
- try {
1504
- const { access } = await import("node:fs/promises");
1505
- const { constants } = await import("node:fs");
1506
- await access(filePath, constants.F_OK);
1507
- console.log(
1508
- `[json-adapter] Inferring schema for ${tableName} from JSON file`
1509
- );
1510
- await connection.run(
1511
- `CREATE TABLE "${tableName}" AS SELECT * FROM read_json_auto('${filePath}')`
1512
- );
1513
- pendingJSONFiles.delete(tableName);
1514
- console.log(
1515
- `[json-adapter] Created ${tableName} with inferred schema (no UNIQUE constraints)`
1516
- );
1517
- } catch (error) {
1518
- throw new DatabaseError(
1519
- `Failed to infer schema from JSON for table ${tableName}`,
1520
- {
1521
- tableName,
1522
- jsonPath: filePath,
1523
- originalError: error instanceof Error ? error.message : String(error)
1524
- }
1525
- );
1526
- }
1527
- };
1528
- const table = (tableName) => ({
1529
- insert: (data) => insert(tableName, data),
1530
- get: (where) => get(tableName, where),
1531
- list: (where) => list(tableName, where)
1532
- });
1533
- const parseTemplate = (strings, ...vars) => {
1534
- let sql = strings[0];
1535
- const values = [];
1536
- for (let i = 0; i < vars.length; i++) {
1537
- values.push(vars[i]);
1538
- sql += `$${i + 1}${strings[i + 1]}`;
1539
- }
1540
- return { sql, values };
1541
- };
1542
- const many = async (strings, ...vars) => {
1543
- const { sql, values } = parseTemplate(strings, ...vars);
1544
- try {
1545
- const reader = await connection.runAndReadAll(sql, values);
1546
- return convertBigInts(reader.getRowObjects());
1547
- } catch (e) {
1548
- throw new DatabaseError("Failed to execute many query", {
1549
- sql,
1550
- values,
1551
- originalError: formatDbError(e)
1552
- });
1553
- }
1554
- };
1555
- const single = async (strings, ...vars) => {
1556
- const { sql, values } = parseTemplate(strings, ...vars);
1557
- try {
1558
- const reader = await connection.runAndReadAll(sql, values);
1559
- const rows = reader.getRowObjects();
1560
- return rows[0] ? convertBigInts(rows[0]) : null;
1561
- } catch (e) {
1562
- throw new DatabaseError("Failed to execute single query", {
1563
- sql,
1564
- values,
1565
- originalError: formatDbError(e)
1566
- });
1567
- }
1568
- };
1569
- const pluck = async (strings, ...vars) => {
1570
- const { sql, values } = parseTemplate(strings, ...vars);
1571
- try {
1572
- const reader = await connection.runAndReadAll(sql, values);
1573
- const rows = reader.getRowObjects();
1574
- if (rows.length === 0) return null;
1575
- const firstRow = rows[0];
1576
- const firstKey = Object.keys(firstRow)[0];
1577
- return convertBigInts(firstRow[firstKey]);
1578
- } catch (e) {
1579
- throw new DatabaseError("Failed to execute pluck query", {
1580
- sql,
1581
- values,
1582
- originalError: formatDbError(e)
1583
- });
1584
- }
1585
- };
1586
- const execute = async (strings, ...vars) => {
1587
- const { sql, values } = parseTemplate(strings, ...vars);
1588
- try {
1589
- await connection.run(sql, values);
1590
- const createTableMatch = sql.match(
1591
- /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i
1592
- );
1593
- if (createTableMatch) {
1594
- const tableName = createTableMatch[1];
1595
- const pendingFile = pendingJSONFiles.get(tableName);
1596
- if (pendingFile) {
1597
- await loadJSONData(connection, tableName, pendingFile);
1598
- pendingJSONFiles.delete(tableName);
1599
- } else if (writeStrategy !== "none") {
1600
- try {
1601
- const { access } = await import("node:fs/promises");
1602
- const jsonFilePath = join(url, `${tableName}.json`);
1603
- try {
1604
- await access(jsonFilePath);
1605
- } catch {
1606
- await exportTableToJSON(connection, tableName, url);
1607
- }
1608
- } catch (error) {
1609
- console.warn(
1610
- `[json-adapter] Could not export newly created table ${tableName}: ${error instanceof Error ? error.message : String(error)}`
1611
- );
1612
- }
1613
- }
1614
- }
1615
- } catch (e) {
1616
- throw new DatabaseError("Failed to execute query", {
1617
- sql,
1618
- values,
1619
- originalError: formatDbError(e)
1620
- });
1621
- }
1622
- };
1623
- const query = async (str, ...values) => {
1624
- const sql = str;
1625
- const args = Array.isArray(values[0]) ? values[0] : values;
1626
- try {
1627
- const reader = await connection.runAndReadAll(sql, args);
1628
- const rows = convertBigInts(reader.getRowObjects());
1629
- return {
1630
- command: sql.split(" ")[0].toUpperCase(),
1631
- rowCount: rows.length,
1632
- oid: null,
1633
- fields: rows.length > 0 ? Object.keys(rows[0]).map((name) => ({
1634
- name,
1635
- tableID: 0,
1636
- columnID: 0,
1637
- dataTypeID: 0,
1638
- dataTypeSize: -1,
1639
- dataTypeModifier: -1,
1640
- format: "text"
1641
- })) : [],
1642
- rows
1643
- };
1644
- } catch (e) {
1645
- throw new DatabaseError("Failed to execute raw query", {
1646
- sql,
1647
- args,
1648
- originalError: formatDbError(e)
1649
- });
1650
- }
1651
- };
1652
- const oo = many;
1653
- const oO = single;
1654
- const ox = pluck;
1655
- const xx = execute;
1656
- const syncSchema2 = async (schema) => {
1657
- const statements = schema.trim().split(";").filter((stmt) => stmt.trim() !== "");
1658
- const createTableStatements = [];
1659
- const indexStatements = [];
1660
- const otherStatements = [];
1661
- for (const stmt of statements) {
1662
- const trimmed = stmt.trim();
1663
- if (!trimmed) continue;
1664
- const upperTrimmed = trimmed.toUpperCase();
1665
- if (upperTrimmed.startsWith("CREATE TRIGGER")) {
1666
- console.warn(
1667
- "[json-adapter] Skipping trigger creation - timestamps managed at application level"
1668
- );
1669
- continue;
1670
- }
1671
- if (upperTrimmed.startsWith("CREATE INDEX") || upperTrimmed.startsWith("CREATE UNIQUE INDEX")) {
1672
- indexStatements.push(trimmed);
1673
- } else if (upperTrimmed.startsWith("CREATE TABLE")) {
1674
- createTableStatements.push(trimmed);
1675
- } else {
1676
- otherStatements.push(trimmed);
1677
- }
1678
- }
1679
- for (const ddl of createTableStatements) {
1680
- const { ddl: transformedDDL, indexes: remainingIndexes } = convertUniqueIndexesToInlineConstraints(ddl, indexStatements);
1681
- const createTableMatch = transformedDDL.match(
1682
- /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i
1683
- );
1684
- const tableName = createTableMatch ? createTableMatch[1] : null;
1685
- if (tableName && inferredSchemaTables.has(tableName)) {
1686
- try {
1687
- await connection.run(`DROP TABLE IF EXISTS "${tableName}"`);
1688
- inferredSchemaTables.delete(tableName);
1689
- console.log(
1690
- `[json-adapter] Dropped inferred-schema table ${tableName} for schema upgrade`
1691
- );
1692
- } catch (error) {
1693
- console.warn(
1694
- `[json-adapter] Could not drop inferred-schema table ${tableName}: ${error instanceof Error ? error.message : String(error)}`
1695
- );
1696
- }
1697
- }
1698
- try {
1699
- await connection.run(transformedDDL);
1700
- if (tableName) {
1701
- const pendingFile = pendingJSONFiles.get(tableName);
1702
- if (pendingFile) {
1703
- await loadJSONData(connection, tableName, pendingFile);
1704
- pendingJSONFiles.delete(tableName);
1705
- } else if (writeStrategy !== "none") {
1706
- try {
1707
- const { access } = await import("node:fs/promises");
1708
- const jsonFilePath = join(url, `${tableName}.json`);
1709
- try {
1710
- await access(jsonFilePath);
1711
- } catch {
1712
- await exportTableToJSON(connection, tableName, url);
1713
- }
1714
- } catch (error) {
1715
- console.warn(
1716
- `[json-adapter] Could not export newly created table ${tableName}: ${error instanceof Error ? error.message : String(error)}`
1717
- );
1718
- }
1719
- }
1720
- }
1721
- } catch (e) {
1722
- console.error("Schema sync error (CREATE TABLE):", e);
1723
- }
1724
- for (const indexSQL of remainingIndexes) {
1725
- try {
1726
- await connection.run(indexSQL);
1727
- } catch (e) {
1728
- console.error("Schema sync error (CREATE INDEX):", e);
1729
- }
1730
- }
1731
- }
1732
- for (const stmt of otherStatements) {
1733
- try {
1734
- await connection.run(stmt);
1735
- } catch (e) {
1736
- console.error("Schema sync error:", e);
1737
- }
1738
- }
1739
- };
1740
- const initializeSchemas = async (options2) => {
1741
- const schemaManager = new DatabaseSchemaManager();
1742
- const currentDb = {
1743
- url,
1744
- client: connection,
1745
- query,
1746
- insert,
1747
- update,
1748
- upsert,
1749
- get,
1750
- list,
1751
- getOrInsert,
1752
- delete: deleteRecords,
1753
- count,
1754
- table,
1755
- tableExists: tableExists2,
1756
- many,
1757
- single,
1758
- pluck,
1759
- execute,
1760
- oo,
1761
- oO,
1762
- ox,
1763
- xx,
1764
- syncSchema: syncSchema2
1765
- };
1766
- await schemaManager.initializeSchemas(currentDb, options2);
1767
- };
1768
- const transaction = async (callback) => {
1769
- try {
1770
- await connection.run("BEGIN TRANSACTION");
1771
- const txDb = {
1772
- url,
1773
- client: connection,
1774
- insert,
1775
- get,
1776
- list,
1777
- update,
1778
- upsert,
1779
- getOrInsert,
1780
- delete: deleteRecords,
1781
- count,
1782
- table,
1783
- many,
1784
- single,
1785
- pluck,
1786
- execute,
1787
- query,
1788
- oo,
1789
- oO,
1790
- ox,
1791
- xx,
1792
- tableExists: tableExists2,
1793
- syncSchema: syncSchema2,
1794
- transaction
1795
- };
1796
- const result = await callback(txDb);
1797
- await connection.run("COMMIT");
1798
- return result;
1799
- } catch (error) {
1800
- await connection.run("ROLLBACK");
1801
- throw error;
1802
- }
1803
- };
1804
- const beginTransaction = async () => {
1805
- await connection.run("BEGIN TRANSACTION");
1806
- let active = true;
1807
- const commit = async () => {
1808
- if (!active) {
1809
- throw new DatabaseError("Transaction already ended", {});
1810
- }
1811
- await connection.run("COMMIT");
1812
- active = false;
1813
- };
1814
- const rollback = async () => {
1815
- if (!active) {
1816
- throw new DatabaseError("Transaction already ended", {});
1817
- }
1818
- await connection.run("ROLLBACK");
1819
- active = false;
1820
- };
1821
- const isActive = () => active;
1822
- const txHandle = {
1823
- url,
1824
- client: connection,
1825
- insert,
1826
- get,
1827
- list,
1828
- update,
1829
- upsert,
1830
- getOrInsert,
1831
- delete: deleteRecords,
1832
- count,
1833
- table,
1834
- many,
1835
- single,
1836
- pluck,
1837
- execute,
1838
- query,
1839
- oo,
1840
- oO,
1841
- ox,
1842
- xx,
1843
- tableExists: tableExists2,
1844
- syncSchema: syncSchema2,
1845
- transaction,
1846
- commit,
1847
- rollback,
1848
- isActive
1849
- };
1850
- return txHandle;
1851
- };
1852
- return {
1853
- url,
1854
- requiresSchemaCheck: true,
1855
- client: connection,
1856
- query,
1857
- insert,
1858
- update,
1859
- upsert,
1860
- get,
1861
- list,
1862
- getOrInsert,
1863
- delete: deleteRecords,
1864
- count,
1865
- table,
1866
- tableExists: tableExists2,
1867
- many,
1868
- single,
1869
- pluck,
1870
- execute,
1871
- oo,
1872
- oO,
1873
- ox,
1874
- xx,
1875
- syncSchema: syncSchema2,
1876
- initializeSchemas,
1877
- transaction,
1878
- beginTransaction,
1879
- // JSON-specific methods
1880
- exportTable,
1881
- inferSchemaFromJSON
1882
- };
1883
- })();
1884
- pendingConnections$1.set(cacheKey, connectionPromise);
1885
- try {
1886
- const db = await connectionPromise;
1887
- memoryConnectionCache.set(cacheKey, db);
1888
- return db;
1889
- } finally {
1890
- pendingConnections$1.delete(cacheKey);
1891
- }
1892
- }
1893
- const json = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1894
- __proto__: null,
1895
- clearConnectionCache,
1896
- getDatabase: getDatabase$2
1897
- }, Symbol.toStringTag, { value: "Module" }));
1898
- function generateAddColumnStatement(table, column, _dialect) {
1899
- const parts = [column.name, column.type];
1900
- if (column.primaryKey) {
1901
- parts.push("PRIMARY KEY");
1902
- }
1903
- if (column.notNull) {
1904
- parts.push("NOT NULL");
1905
- }
1906
- if (column.unique) {
1907
- parts.push("UNIQUE");
1908
- }
1909
- if (column.defaultValue !== void 0) {
1910
- const defaultClause = generateDefaultClause(
1911
- column.type,
1912
- column.defaultValue
1913
- );
1914
- parts.push(`DEFAULT ${defaultClause}`);
1915
- }
1916
- if (column.check) {
1917
- parts.push(`CHECK (${column.check})`);
1918
- }
1919
- if (column.foreignKey) {
1920
- const fk = column.foreignKey;
1921
- const fkParts = [`REFERENCES ${fk.table}(${fk.column})`];
1922
- if (fk.onDelete) {
1923
- fkParts.push(`ON DELETE ${fk.onDelete}`);
1924
- }
1925
- if (fk.onUpdate) {
1926
- fkParts.push(`ON UPDATE ${fk.onUpdate}`);
1927
- }
1928
- parts.push(fkParts.join(" "));
1929
- }
1930
- const columnDef = parts.join(" ");
1931
- return `ALTER TABLE ${table} ADD COLUMN ${columnDef}`;
1932
- }
1933
- function generateDefaultClause(columnType, defaultValue) {
1934
- if (defaultValue === null) {
1935
- return "NULL";
1936
- }
1937
- const typeUpper = columnType.toUpperCase();
1938
- if (typeUpper.includes("TEXT") || typeUpper.includes("CHAR") || typeUpper.includes("VARCHAR") || typeUpper.includes("STRING")) {
1939
- if (defaultValue === "") {
1940
- return "''";
1941
- }
1942
- const escaped = String(defaultValue).replace(/'/g, "''");
1943
- return `'${escaped}'`;
1944
- }
1945
- if (typeUpper.includes("BOOL")) {
1946
- return defaultValue ? "true" : "false";
1947
- }
1948
- if (typeUpper.includes("INT") || typeUpper.includes("REAL") || typeUpper.includes("FLOAT") || typeUpper.includes("DOUBLE") || typeUpper.includes("NUMERIC") || typeUpper.includes("DECIMAL")) {
1949
- return String(defaultValue);
1950
- }
1951
- if (typeUpper.includes("JSON")) {
1952
- if (typeof defaultValue === "string") {
1953
- return `'${defaultValue.replace(/'/g, "''")}'`;
1954
- }
1955
- return `'${JSON.stringify(defaultValue).replace(/'/g, "''")}'`;
1956
- }
1957
- if (typeUpper.includes("DATE") || typeUpper.includes("TIME") || typeUpper.includes("TIMESTAMP")) {
1958
- if (defaultValue instanceof Date) {
1959
- return `'${defaultValue.toISOString()}'`;
1960
- }
1961
- return `'${String(defaultValue).replace(/'/g, "''")}'`;
1962
- }
1963
- if (typeof defaultValue === "string") {
1964
- return `'${defaultValue.replace(/'/g, "''")}'`;
1965
- }
1966
- return String(defaultValue);
1967
- }
1968
- function generateCreateIndexStatement(table, index2) {
1969
- const parts = ["CREATE"];
1970
- if (index2.unique) {
1971
- parts.push("UNIQUE");
1972
- }
1973
- parts.push("INDEX");
1974
- parts.push(index2.name);
1975
- parts.push(`ON ${table}`);
1976
- const columns = index2.columns.join(", ");
1977
- parts.push(`(${columns})`);
1978
- if (index2.where) {
1979
- parts.push(`WHERE ${index2.where}`);
1980
- }
1981
- return parts.join(" ");
1982
- }
1983
- function validateTableName(tableName) {
1984
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
1985
- throw new Error(
1986
- `Invalid table name: ${tableName}. Table names must start with a letter or underscore and contain only alphanumeric characters and underscores.`
1987
- );
1988
- }
1989
- }
1990
- function validateColumnName$1(columnName) {
1991
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(columnName)) {
1992
- throw new Error(
1993
- `Invalid column name: ${columnName}. Column names must start with a letter or underscore and contain only alphanumeric characters and underscores.`
1994
- );
1995
- }
1996
- }
1997
- function validateIndexName(indexName) {
1998
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(indexName)) {
1999
- throw new Error(
2000
- `Invalid index name: ${indexName}. Index names must start with a letter or underscore and contain only alphanumeric characters and underscores.`
2001
- );
2002
- }
2003
- }
2004
- const connectionCache = /* @__PURE__ */ new Map();
2005
- const pendingConnections = /* @__PURE__ */ new Map();
2006
- function clearPostgresConnectionCache() {
2007
- connectionCache.clear();
2008
- pendingConnections.clear();
2009
- }
2010
- function vectorOperator(metric) {
2011
- switch (metric) {
2012
- case "cosine":
2013
- return "<=>";
2014
- case "l2":
2015
- return "<->";
2016
- case "ip":
2017
- return "<#>";
2018
- default:
2019
- return "<=>";
2020
- }
2021
- }
2022
- function getMaxPostgresParameterIndex(sql) {
2023
- let maxIndex = 0;
2024
- for (const match of sql.matchAll(/\$(\d+)/g)) {
2025
- const index2 = Number(match[1]);
2026
- if (Number.isSafeInteger(index2) && index2 > maxIndex) {
2027
- maxIndex = index2;
2028
- }
2029
- }
2030
- return maxIndex;
2031
- }
2032
- function usesSingleArrayParameter(sql) {
2033
- const identifier = String.raw`(?:"[^"]+"|[a-z_][\w$]*)`;
2034
- const qualifiedIdentifier = String.raw`${identifier}(?:\s*\.\s*${identifier})?`;
2035
- const arrayType = String.raw`${qualifiedIdentifier}(?:\s*\([^)]*\))?\s*\[\]`;
2036
- return new RegExp(String.raw`\$1\s*::\s*${arrayType}`, "i").test(sql) || new RegExp(
2037
- String.raw`\bCAST\s*\(\s*\$1\s+AS\s+${arrayType}\s*\)`,
2038
- "i"
2039
- ).test(sql) || /\b(?:ANY|ALL|SOME)\s*\(\s*\$1\s*\)/i.test(sql);
2040
- }
2041
- function normalizeRawQueryValues(sql, values) {
2042
- if (values.length !== 1 || !Array.isArray(values[0])) {
2043
- return values;
2044
- }
2045
- const valuesArray = values[0];
2046
- const maxParameterIndex = getMaxPostgresParameterIndex(sql);
2047
- if (maxParameterIndex === 1 && usesSingleArrayParameter(sql)) {
2048
- return values;
2049
- }
2050
- if (maxParameterIndex === 0 || maxParameterIndex === valuesArray.length) {
2051
- return valuesArray;
2052
- }
2053
- return values;
2054
- }
2055
- function rewriteQuestionMarkPlaceholders(sql) {
2056
- let output = "";
2057
- let count = 0;
2058
- let index2 = 0;
2059
- const copyQuoted = (quote, escaped = false) => {
2060
- output += quote;
2061
- index2 += 1;
2062
- while (index2 < sql.length) {
2063
- const char = sql[index2];
2064
- output += char;
2065
- index2 += 1;
2066
- if (escaped && char === "\\" && index2 < sql.length) {
2067
- output += sql[index2];
2068
- index2 += 1;
2069
- continue;
2070
- }
2071
- if (char === quote) {
2072
- if (sql[index2] === quote) {
2073
- output += sql[index2];
2074
- index2 += 1;
2075
- continue;
2076
- }
2077
- break;
2078
- }
2079
- }
2080
- };
2081
- const copyUntil = (end) => {
2082
- const endIndex = sql.indexOf(end, index2 + end.length);
2083
- if (endIndex === -1) {
2084
- output += sql.slice(index2);
2085
- index2 = sql.length;
2086
- return;
2087
- }
2088
- output += sql.slice(index2, endIndex + end.length);
2089
- index2 = endIndex + end.length;
2090
- };
2091
- while (index2 < sql.length) {
2092
- const char = sql[index2];
2093
- const next = sql[index2 + 1];
2094
- if ((char === "E" || char === "e") && next === "'") {
2095
- output += char;
2096
- index2 += 1;
2097
- copyQuoted("'", true);
2098
- continue;
2099
- }
2100
- if (char === "'") {
2101
- copyQuoted("'");
2102
- continue;
2103
- }
2104
- if (char === '"') {
2105
- copyQuoted('"');
2106
- continue;
2107
- }
2108
- if (char === "-" && next === "-") {
2109
- const endIndex = sql.indexOf("\n", index2 + 2);
2110
- if (endIndex === -1) {
2111
- output += sql.slice(index2);
2112
- index2 = sql.length;
2113
- } else {
2114
- output += sql.slice(index2, endIndex + 1);
2115
- index2 = endIndex + 1;
2116
- }
2117
- continue;
2118
- }
2119
- if (char === "/" && next === "*") {
2120
- copyUntil("*/");
2121
- continue;
2122
- }
2123
- if (char === "$") {
2124
- const tag = sql.slice(index2).match(/^\$[A-Za-z_][\w$]*\$|^\$\$/);
2125
- if (tag) {
2126
- copyUntil(tag[0]);
2127
- continue;
2128
- }
2129
- }
2130
- if (char === "?" && next !== "|" && next !== "&") {
2131
- count += 1;
2132
- output += `$${count}`;
2133
- index2 += 1;
2134
- continue;
2135
- }
2136
- output += char;
2137
- index2 += 1;
2138
- }
2139
- return { sql: output, count };
2140
- }
2141
- function usesSingleQuestionArrayParameter(sql) {
2142
- const identifier = String.raw`(?:"[^"]+"|[a-z_][\w$]*)`;
2143
- const qualifiedIdentifier = String.raw`${identifier}(?:\s*\.\s*${identifier})?`;
2144
- const arrayType = String.raw`${qualifiedIdentifier}(?:\s*\([^)]*\))?\s*\[\]`;
2145
- return new RegExp(String.raw`\?\s*::\s*${arrayType}`, "i").test(sql) || new RegExp(String.raw`\bCAST\s*\(\s*\?\s+AS\s+${arrayType}\s*\)`, "i").test(
2146
- sql
2147
- ) || /\b(?:ANY|ALL|SOME)\s*\(\s*\?\s*\)/i.test(sql);
2148
- }
2149
- function normalizeQuestionMarkQueryValues(sql, values, placeholderCount) {
2150
- if (values.length !== 1 || !Array.isArray(values[0])) {
2151
- return values;
2152
- }
2153
- if (placeholderCount === 1 && usesSingleQuestionArrayParameter(sql)) {
2154
- return values;
2155
- }
2156
- if (placeholderCount === values[0].length) {
2157
- return values[0];
2158
- }
2159
- return values;
2160
- }
2161
- function normalizePostgresRawQuery(sql, values) {
2162
- if (getMaxPostgresParameterIndex(sql) > 0) {
2163
- return { sql, values: normalizeRawQueryValues(sql, values) };
2164
- }
2165
- const rewritten = rewriteQuestionMarkPlaceholders(sql);
2166
- const placeholderCount = rewritten.count;
2167
- const queryValues = normalizeQuestionMarkQueryValues(
2168
- sql,
2169
- values,
2170
- placeholderCount
2171
- );
2172
- if (placeholderCount > 0 && placeholderCount === queryValues.length) {
2173
- return {
2174
- sql: rewritten.sql,
2175
- values: queryValues
2176
- };
2177
- }
2178
- return { sql, values: normalizeRawQueryValues(sql, values) };
2179
- }
2180
- function vectorOpsClass(metric) {
2181
- switch (metric) {
2182
- case "cosine":
2183
- return "vector_cosine_ops";
2184
- case "l2":
2185
- return "vector_l2_ops";
2186
- case "ip":
2187
- return "vector_ip_ops";
2188
- default:
2189
- return "vector_cosine_ops";
2190
- }
2191
- }
2192
- function toVectorLiteral(embedding) {
2193
- if (embedding.length === 0) {
2194
- throw new DatabaseError("Vector embedding must not be empty", {});
2195
- }
2196
- for (let i = 0; i < embedding.length; i++) {
2197
- if (!Number.isFinite(embedding[i])) {
2198
- throw new DatabaseError(
2199
- "Vector embedding contains invalid value (NaN or Infinity)",
2200
- { index: i, value: embedding[i] }
2201
- );
2202
- }
2203
- }
2204
- return `[${embedding.join(",")}]`;
2205
- }
2206
- function createVectorCapabilities(pool) {
2207
- return {
2208
- async ensureColumn(table, column, dimensions) {
2209
- if (!Number.isInteger(dimensions) || dimensions <= 0) {
2210
- throw new DatabaseError(
2211
- "Vector dimensions must be a positive integer",
2212
- { dimensions }
2213
- );
2214
- }
2215
- try {
2216
- await pool.query("CREATE EXTENSION IF NOT EXISTS vector");
2217
- await pool.query(
2218
- `ALTER TABLE "${table}" ADD COLUMN IF NOT EXISTS "${column}" vector(${dimensions})`
2219
- );
2220
- } catch (e) {
2221
- throw new DatabaseError("Failed to ensure vector column", {
2222
- table,
2223
- column,
2224
- dimensions,
2225
- originalError: formatDbError(e)
2226
- });
2227
- }
2228
- },
2229
- async ensureIndex(table, column, options) {
2230
- const metric = options?.metric || "cosine";
2231
- const indexType = options?.type || "hnsw";
2232
- const opsClass = vectorOpsClass(metric);
2233
- const indexName = `idx_${table}_${column}_${metric}_${indexType}`;
2234
- try {
2235
- await pool.query(
2236
- `CREATE INDEX IF NOT EXISTS "${indexName}" ON "${table}" USING ${indexType} ("${column}" ${opsClass})`
2237
- );
2238
- } catch (e) {
2239
- throw new DatabaseError("Failed to ensure vector index", {
2240
- table,
2241
- column,
2242
- indexName,
2243
- originalError: formatDbError(e)
2244
- });
2245
- }
2246
- },
2247
- async upsertVector(table, where, column, embedding) {
2248
- const keys = Object.keys(where);
2249
- const values = Object.values(where);
2250
- const conditions = keys.map((key, i) => `"${key}" IS NOT DISTINCT FROM $${i + 1}`).join(" AND ");
2251
- const vectorParam = `$${values.length + 1}`;
2252
- try {
2253
- await pool.query(
2254
- `UPDATE "${table}" SET "${column}" = ${vectorParam}::vector WHERE ${conditions}`,
2255
- [...values, toVectorLiteral(embedding)]
2256
- );
2257
- } catch (e) {
2258
- throw new DatabaseError("Failed to upsert vector", {
2259
- table,
2260
- column,
2261
- where,
2262
- originalError: formatDbError(e)
2263
- });
2264
- }
2265
- },
2266
- async search(table, column, embedding, options) {
2267
- const metric = options?.metric || "cosine";
2268
- const limit = options?.limit || 10;
2269
- const op = vectorOperator(metric);
2270
- const queryParams = [toVectorLiteral(embedding)];
2271
- let whereClause = `"${column}" IS NOT NULL`;
2272
- if (options?.where) {
2273
- whereClause += ` AND (${options.where})`;
2274
- if (options.params) {
2275
- queryParams.push(...options.params);
2276
- }
2277
- }
2278
- const limitParam = `$${queryParams.length + 1}`;
2279
- queryParams.push(limit);
2280
- const sql = `SELECT *, ("${column}" ${op} $1::vector) AS distance FROM "${table}" WHERE ${whereClause} ORDER BY "${column}" ${op} $1::vector LIMIT ${limitParam}`;
2281
- try {
2282
- const result = await pool.query(sql, queryParams);
2283
- return result.rows.map((row) => ({
2284
- ...row,
2285
- id: row.id,
2286
- distance: Number.parseFloat(row.distance)
2287
- }));
2288
- } catch (e) {
2289
- throw new DatabaseError("Failed to execute vector search", {
2290
- table,
2291
- column,
2292
- metric,
2293
- limit,
2294
- originalError: formatDbError(e)
2295
- });
2296
- }
2297
- }
2298
- };
2299
- }
2300
- async function getDatabase$1(options = {}) {
2301
- const envUrl = process.env.HAVE_SQL_URL || process.env.SQLOO_URL || null;
2302
- const envDatabase = process.env.HAVE_SQL_DATABASE || process.env.SQLOO_DATABASE || null;
2303
- const envHost = process.env.HAVE_SQL_HOST || process.env.SQLOO_HOST || "localhost";
2304
- const envUser = process.env.HAVE_SQL_USER || process.env.SQLOO_USER || "";
2305
- const envPortRaw = process.env.HAVE_SQL_PORT || process.env.SQLOO_PORT;
2306
- const envPort = envPortRaw ? Number(envPortRaw) || 5432 : 5432;
2307
- const derivedHost = options.host || envHost;
2308
- const derivedPort = options.port || envPort;
2309
- const derivedDatabase = options.database || envDatabase;
2310
- const derivedUser = options.user || envUser;
2311
- const cacheKey = options.dbid || options.url || (derivedDatabase ? `pg:${derivedUser ? `${derivedUser}@` : ""}${derivedHost}:${derivedPort}/${derivedDatabase}` : envUrl || null);
2312
- if (cacheKey) {
2313
- const cached = connectionCache.get(cacheKey);
2314
- if (cached) {
2315
- return cached;
2316
- }
2317
- const pending = pendingConnections.get(cacheKey);
2318
- if (pending) return pending;
2319
- }
2320
- const connectionPromise = createDatabase(options, cacheKey);
2321
- if (cacheKey) {
2322
- pendingConnections.set(cacheKey, connectionPromise);
2323
- connectionPromise.finally(() => {
2324
- if (cacheKey) pendingConnections.delete(cacheKey);
2325
- });
2326
- }
2327
- return connectionPromise;
2328
- }
2329
- async function createDatabase(options, cacheKey) {
2330
- const config = loadEnvConfig(options, {
2331
- packageName: "sql",
2332
- schema: {
2333
- url: "string",
2334
- database: "string",
2335
- host: "string",
2336
- port: "number",
2337
- user: "string",
2338
- password: "string"
2339
- }
2340
- });
2341
- if (!config.url && !config.host && !config.database && !config.user) {
2342
- const legacyConfig = loadEnvConfig(
2343
- {},
2344
- {
2345
- prefix: "SQLOO",
2346
- schema: {
2347
- url: "string",
2348
- database: "string",
2349
- host: "string",
2350
- port: "number",
2351
- user: "string",
2352
- password: "string"
2353
- }
2354
- }
2355
- );
2356
- for (const [key, value] of Object.entries(legacyConfig)) {
2357
- if (config[key] === void 0 && value !== void 0) {
2358
- config[key] = value;
2359
- }
2360
- }
2361
- }
2362
- const { database, host = "localhost", user, password, port = 5432 } = config;
2363
- const url = config.url || `postgresql://${user}${password ? `:${password}` : ""}@${host}:${port}/${database || "postgres"}`;
2364
- const poolMax = options.max ?? 20;
2365
- const pool = new Pool(
2366
- config.url ? { connectionString: config.url, max: poolMax } : {
2367
- host,
2368
- user,
2369
- password,
2370
- port,
2371
- database,
2372
- max: poolMax
2373
- }
2374
- );
2375
- const originalEnd = pool.end.bind(pool);
2376
- const evictFromCache = () => {
2377
- for (const [key, cached] of connectionCache.entries()) {
2378
- if (cached.client === pool) {
2379
- connectionCache.delete(key);
2380
- }
2381
- }
2382
- };
2383
- pool.end = (callback) => {
2384
- evictFromCache();
2385
- if (callback) {
2386
- return originalEnd(callback);
2387
- }
2388
- return originalEnd();
2389
- };
2390
- const client = pool;
2391
- const serializeValue = (value) => {
2392
- if (value === null || value === void 0) {
2393
- return value;
2394
- }
2395
- if (value instanceof Date) {
2396
- return value.toISOString();
2397
- }
2398
- if (typeof value === "object") {
2399
- return JSON.stringify(value);
2400
- }
2401
- return value;
2402
- };
2403
- const serializeRecord = (record) => {
2404
- const serialized = {};
2405
- for (const [key, value] of Object.entries(record)) {
2406
- if (value === void 0) {
2407
- continue;
2408
- }
2409
- serialized[key] = serializeValue(value);
2410
- }
2411
- return serialized;
2412
- };
2413
- let serverVersionNumPromise;
2414
- const nullsNotDistinctIndexCache = /* @__PURE__ */ new Map();
2415
- const hasNullConflictValue = (conflictColumns, data) => conflictColumns.some((col) => data[col] === null);
2416
- const validateUpsertConflictColumns = (table2, conflictColumns, serializedData) => {
2417
- const missingColumns = conflictColumns.filter(
2418
- (col) => !(col in serializedData)
2419
- );
2420
- if (missingColumns.length > 0) {
2421
- throw new DatabaseError("Conflict columns missing from data", {
2422
- table: table2,
2423
- conflictColumns,
2424
- missingColumns,
2425
- availableColumns: Object.keys(serializedData),
2426
- 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."
2427
- });
2428
- }
2429
- };
2430
- const getServerVersionNum = async () => {
2431
- serverVersionNumPromise ??= client.query("SHOW server_version_num").then((result) => Number(result.rows[0]?.server_version_num) || 0);
2432
- return serverVersionNumPromise;
2433
- };
2434
- const hasNativeNullsNotDistinctIndex = async (table2, conflictColumns) => {
2435
- const versionNum = await getServerVersionNum();
2436
- if (versionNum < 15e4) return false;
2437
- const cacheKey2 = `${table2}:${conflictColumns.join(",")}`;
2438
- let cached = nullsNotDistinctIndexCache.get(cacheKey2);
2439
- if (!cached) {
2440
- cached = client.query(
2441
- `
2442
- SELECT 1
2443
- FROM pg_index i
2444
- WHERE i.indrelid = to_regclass($1)
2445
- AND i.indisunique
2446
- AND i.indnullsnotdistinct
2447
- AND ARRAY(
2448
- SELECT a.attname::text
2449
- FROM unnest(i.indkey) WITH ORDINALITY AS cols(attnum, ord)
2450
- JOIN pg_attribute a
2451
- ON a.attrelid = i.indrelid
2452
- AND a.attnum = cols.attnum
2453
- ORDER BY cols.ord
2454
- ) = $2::text[]
2455
- LIMIT 1
2456
- `,
2457
- [table2, conflictColumns]
2458
- ).then((result) => result.rows.length > 0).catch(() => false);
2459
- nullsNotDistinctIndexCache.set(cacheKey2, cached);
2460
- }
2461
- return cached;
2462
- };
2463
- const buildPostgresUpsertParts = (conflictColumns, serializedData) => {
2464
- const keys = Object.keys(serializedData);
2465
- const values = Object.values(serializedData);
2466
- const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
2467
- const updateSet = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
2468
- const conflict = conflictColumns.join(", ");
2469
- return { keys, values, placeholders, updateSet, conflict };
2470
- };
2471
- const executeStandardPostgresUpsert = async (executor, table2, conflictColumns, serializedData) => {
2472
- const { keys, values, placeholders, updateSet, conflict } = buildPostgresUpsertParts(conflictColumns, serializedData);
2473
- const sql = `INSERT INTO ${table2} (${keys.join(", ")}) VALUES (${placeholders}) ON CONFLICT(${conflict}) DO UPDATE SET ${updateSet}`;
2474
- const result = await executor.query(sql, values);
2475
- return { operation: "upsert", affected: result.rowCount ?? 0 };
2476
- };
2477
- const executeNullAwarePostgresUpsert = async (executor, table2, conflictColumns, serializedData) => {
2478
- const { keys, values, placeholders } = buildPostgresUpsertParts(
2479
- conflictColumns,
2480
- serializedData
2481
- );
2482
- const lockKey = conflictColumns.map((col) => `${col}:${JSON.stringify(serializedData[col])}`).join("|");
2483
- await executor.query(
2484
- "SELECT pg_advisory_xact_lock(hashtext($1), hashtext($2))",
2485
- [`@happyvertical/sql.upsert:${table2}`, lockKey]
2486
- );
2487
- const updateSet = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
2488
- const whereStart = values.length + 1;
2489
- const whereClause = conflictColumns.map((col, i) => `${col} IS NOT DISTINCT FROM $${whereStart + i}`).join(" AND ");
2490
- const whereValues = conflictColumns.map((col) => serializedData[col]);
2491
- const updateSql = `UPDATE ${table2} SET ${updateSet} WHERE ${whereClause}`;
2492
- const updateResult = await executor.query(updateSql, [
2493
- ...values,
2494
- ...whereValues
2495
- ]);
2496
- if ((updateResult.rowCount ?? 0) > 0) {
2497
- return { operation: "upsert", affected: updateResult.rowCount ?? 0 };
2498
- }
2499
- const insertSql = `INSERT INTO ${table2} (${keys.join(", ")}) VALUES (${placeholders})`;
2500
- const insertResult = await executor.query(insertSql, values);
2501
- return { operation: "upsert", affected: insertResult.rowCount ?? 0 };
2502
- };
2503
- const executeNullAwarePostgresUpsertInTransaction = async (table2, conflictColumns, serializedData) => {
2504
- const txClient = await client.connect();
2505
- try {
2506
- await txClient.query("BEGIN");
2507
- const result = await executeNullAwarePostgresUpsert(
2508
- txClient,
2509
- table2,
2510
- conflictColumns,
2511
- serializedData
2512
- );
2513
- await txClient.query("COMMIT");
2514
- return result;
2515
- } catch (error) {
2516
- await txClient.query("ROLLBACK");
2517
- throw error;
2518
- } finally {
2519
- txClient.release();
2520
- }
2521
- };
2522
- const executePostgresUpsert = async (executor, table2, conflictColumns, data, options2, acquireTransaction) => {
2523
- const serializedData = serializeRecord(data);
2524
- validateUpsertConflictColumns(table2, conflictColumns, serializedData);
2525
- if (options2?.nullsDistinct || !hasNullConflictValue(conflictColumns, serializedData)) {
2526
- return executeStandardPostgresUpsert(
2527
- executor,
2528
- table2,
2529
- conflictColumns,
2530
- serializedData
2531
- );
2532
- }
2533
- if (await hasNativeNullsNotDistinctIndex(table2, conflictColumns)) {
2534
- return executeStandardPostgresUpsert(
2535
- executor,
2536
- table2,
2537
- conflictColumns,
2538
- serializedData
2539
- );
2540
- }
2541
- if (!acquireTransaction) {
2542
- return executeNullAwarePostgresUpsert(
2543
- executor,
2544
- table2,
2545
- conflictColumns,
2546
- serializedData
2547
- );
2548
- }
2549
- return executeNullAwarePostgresUpsertInTransaction(
2550
- table2,
2551
- conflictColumns,
2552
- serializedData
2553
- );
2554
- };
2555
- const insert = async (table2, data) => {
2556
- if (Array.isArray(data)) {
2557
- const serializedRecords = data.map((record) => serializeRecord(record));
2558
- const keys2 = Object.keys(serializedRecords[0]);
2559
- const placeholders2 = serializedRecords.map(
2560
- (_, i) => `(${keys2.map((_2, j) => `$${i * keys2.length + j + 1}`).join(", ")})`
2561
- ).join(", ");
2562
- const query3 = `INSERT INTO ${table2} (${keys2.join(
2563
- ", "
2564
- )}) VALUES ${placeholders2}`;
2565
- const values2 = serializedRecords.reduce(
2566
- (acc, row) => acc.concat(Object.values(row)),
2567
- []
2568
- );
2569
- const result2 = await client.query(query3, values2);
2570
- return { operation: "insert", affected: result2.rowCount ?? 0 };
2571
- }
2572
- const serializedData = serializeRecord(data);
2573
- const keys = Object.keys(serializedData);
2574
- const values = Object.values(serializedData);
2575
- const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
2576
- const query2 = `INSERT INTO ${table2} (${keys.join(
2577
- ", "
2578
- )}) VALUES (${placeholders})`;
2579
- const result = await client.query(query2, values);
2580
- return { operation: "insert", affected: result.rowCount ?? 0 };
2581
- };
2582
- const get = async (table2, where) => {
2583
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2584
- if (!whereClause) {
2585
- throw new DatabaseError(
2586
- "GET requires at least one WHERE condition to prevent returning an arbitrary record",
2587
- { table: table2 }
2588
- );
2589
- }
2590
- const query2 = `SELECT * FROM ${table2} ${whereClause}`;
2591
- try {
2592
- const result = await client.query(query2, values);
2593
- return result.rows[0] || null;
2594
- } catch (e) {
2595
- throw new DatabaseError("Failed to retrieve record from table", {
2596
- table: table2,
2597
- sql: query2,
2598
- values,
2599
- originalError: formatDbError(e)
2600
- });
2601
- }
2602
- };
2603
- const list = async (table2, where) => {
2604
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2605
- const query2 = `SELECT * FROM ${table2} ${whereClause}`;
2606
- try {
2607
- const result = await client.query(query2, values);
2608
- return result.rows;
2609
- } catch (e) {
2610
- throw new DatabaseError("Failed to list records from table", {
2611
- table: table2,
2612
- sql: query2,
2613
- values,
2614
- originalError: formatDbError(e)
2615
- });
2616
- }
2617
- };
2618
- const update = async (table2, where, data) => {
2619
- const serializedData = serializeRecord(data);
2620
- const keys = Object.keys(serializedData);
2621
- const values = Object.values(serializedData);
2622
- const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
2623
- const { sql: whereClause, values: whereValues } = buildWhere(
2624
- where,
2625
- values.length + 1,
2626
- "postgres"
2627
- );
2628
- if (!whereClause) {
2629
- throw new DatabaseError(
2630
- "UPDATE requires at least one WHERE condition to prevent accidental update of all records",
2631
- { table: table2 }
2632
- );
2633
- }
2634
- const sql = `UPDATE ${table2} SET ${setClause} ${whereClause}`;
2635
- try {
2636
- const result = await client.query(sql, [...values, ...whereValues]);
2637
- return { operation: "update", affected: result.rowCount ?? 0 };
2638
- } catch (e) {
2639
- throw new DatabaseError("Failed to update records in table", {
2640
- table: table2,
2641
- sql,
2642
- values: [...values, ...whereValues],
2643
- originalError: formatDbError(e)
2644
- });
2645
- }
2646
- };
2647
- const upsert = async (table2, conflictColumns, data, options2) => {
2648
- try {
2649
- return await executePostgresUpsert(
2650
- client,
2651
- table2,
2652
- conflictColumns,
2653
- data,
2654
- options2,
2655
- true
2656
- );
2657
- } catch (e) {
2658
- if (e instanceof DatabaseError && e.message === "Conflict columns missing from data") {
2659
- throw e;
2660
- }
2661
- throw new DatabaseError("Failed to upsert record into table", {
2662
- table: table2,
2663
- values: data,
2664
- conflictColumns,
2665
- originalError: formatDbError(e)
2666
- });
2667
- }
2668
- };
2669
- const getOrInsert = async (table2, where, data) => {
2670
- const result = await get(table2, where);
2671
- if (result) return result;
2672
- await insert(table2, data);
2673
- const inserted = await get(table2, where);
2674
- if (!inserted) {
2675
- throw new DatabaseError("Failed to insert and retrieve record", {
2676
- table: table2,
2677
- where,
2678
- data
2679
- });
2680
- }
2681
- return inserted;
2682
- };
2683
- const deleteRecords = async (table2, where) => {
2684
- validateTableName(table2);
2685
- const keys = Object.keys(where);
2686
- if (keys.length === 0) {
2687
- throw new DatabaseError(
2688
- "DELETE requires at least one WHERE condition to prevent accidental deletion of all records",
2689
- { table: table2 }
2690
- );
2691
- }
2692
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2693
- try {
2694
- const result = await client.query(
2695
- `DELETE FROM ${table2} ${whereClause}`,
2696
- values
2697
- );
2698
- return { operation: "delete", affected: result.rowCount ?? 0 };
2699
- } catch (e) {
2700
- throw new DatabaseError("Failed to delete records from table", {
2701
- table: table2,
2702
- where,
2703
- originalError: formatDbError(e)
2704
- });
2705
- }
2706
- };
2707
- const count = async (table2, where) => {
2708
- validateTableName(table2);
2709
- try {
2710
- if (!where || Object.keys(where).length === 0) {
2711
- const result2 = await client.query(
2712
- `SELECT COUNT(*) as count FROM ${table2}`
2713
- );
2714
- return Number(result2.rows[0]?.count) || 0;
2715
- }
2716
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2717
- const result = await client.query(
2718
- `SELECT COUNT(*) as count FROM ${table2} ${whereClause}`,
2719
- values
2720
- );
2721
- return Number(result.rows[0]?.count) || 0;
2722
- } catch (e) {
2723
- throw new DatabaseError("Failed to count records in table", {
2724
- table: table2,
2725
- where,
2726
- originalError: formatDbError(e)
2727
- });
2728
- }
2729
- };
2730
- const table = (tableName) => {
2731
- return {
2732
- insert: (data) => insert(tableName, data),
2733
- get: (data) => get(tableName, data),
2734
- list: (data) => list(tableName, data)
2735
- };
2736
- };
2737
- const parseTemplate = (strings, ...vars) => {
2738
- let sql = strings[0];
2739
- const values = [];
2740
- for (let i = 0; i < vars.length; i++) {
2741
- values.push(vars[i]);
2742
- sql += `$${i + 1}${strings[i + 1]}`;
2743
- }
2744
- return { sql, values };
2745
- };
2746
- const pluck = async (strings, ...vars) => {
2747
- const { sql, values } = parseTemplate(strings, ...vars);
2748
- try {
2749
- const result = await client.query(sql, values);
2750
- const firstRow = result.rows[0];
2751
- if (!firstRow) return null;
2752
- return Object.values(firstRow)[0];
2753
- } catch (e) {
2754
- throw new DatabaseError("Failed to execute pluck query", {
2755
- sql,
2756
- values,
2757
- originalError: formatDbError(e)
2758
- });
2759
- }
2760
- };
2761
- const single = async (strings, ...vars) => {
2762
- const { sql, values } = parseTemplate(strings, ...vars);
2763
- try {
2764
- const result = await client.query(sql, values);
2765
- return result.rows[0] || null;
2766
- } catch (e) {
2767
- throw new DatabaseError("Failed to execute single query", {
2768
- sql,
2769
- values,
2770
- originalError: formatDbError(e)
2771
- });
2772
- }
2773
- };
2774
- const many = async (strings, ...vars) => {
2775
- const { sql, values } = parseTemplate(strings, ...vars);
2776
- try {
2777
- const result = await client.query(sql, values);
2778
- return result.rows;
2779
- } catch (e) {
2780
- throw new DatabaseError("Failed to execute many query", {
2781
- sql,
2782
- values,
2783
- originalError: formatDbError(e)
2784
- });
2785
- }
2786
- };
2787
- const execute = async (strings, ...vars) => {
2788
- const { sql, values } = parseTemplate(strings, ...vars);
2789
- try {
2790
- await client.query(sql, values);
2791
- } catch (e) {
2792
- throw new DatabaseError("Failed to execute query", {
2793
- sql,
2794
- values,
2795
- originalError: formatDbError(e)
2796
- });
2797
- }
2798
- };
2799
- const query = async (sql, ...values) => {
2800
- const query2 = normalizePostgresRawQuery(sql, values);
2801
- try {
2802
- const result = await client.query(query2.sql, query2.values);
2803
- return {
2804
- rows: result.rows,
2805
- rowCount: result.rowCount ?? 0
2806
- };
2807
- } catch (e) {
2808
- throw new DatabaseError("Failed to execute raw query", {
2809
- sql: query2.sql,
2810
- values: query2.values,
2811
- originalError: formatDbError(e)
2812
- });
2813
- }
2814
- };
2815
- const acquireSession = async () => {
2816
- const sessionClient = await pool.connect();
2817
- let released = false;
2818
- let lost = false;
2819
- sessionClient.on("error", () => {
2820
- lost = true;
2821
- });
2822
- return {
2823
- query: async (sql, ...values) => {
2824
- if (released) {
2825
- throw new DatabaseError("Session has been released", {});
2826
- }
2827
- if (lost) {
2828
- throw new DatabaseError("Session connection was lost", {});
2829
- }
2830
- const normalized = normalizePostgresRawQuery(sql, values);
2831
- try {
2832
- const result = await sessionClient.query(
2833
- normalized.sql,
2834
- normalized.values
2835
- );
2836
- return {
2837
- rows: result.rows,
2838
- rowCount: result.rowCount ?? 0
2839
- };
2840
- } catch (e) {
2841
- throw new DatabaseError("Failed to execute session query", {
2842
- sql: normalized.sql,
2843
- values: normalized.values,
2844
- originalError: formatDbError(e)
2845
- });
2846
- }
2847
- },
2848
- isActive: () => !released && !lost,
2849
- release: async () => {
2850
- if (released) return;
2851
- released = true;
2852
- if (!lost) {
2853
- try {
2854
- await sessionClient.query("ROLLBACK");
2855
- } catch {
2856
- }
2857
- try {
2858
- await sessionClient.query("SELECT pg_advisory_unlock_all()");
2859
- } catch {
2860
- }
2861
- }
2862
- sessionClient.release(true);
2863
- }
2864
- };
2865
- };
2866
- const tableExists2 = async (tableName) => {
2867
- const result = await client.query(
2868
- `SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1 AND table_schema = 'public')`,
2869
- [tableName]
2870
- );
2871
- return result.rows[0].exists;
2872
- };
2873
- const syncSchema2 = async (schema) => {
2874
- const commands = schema.trim().split(";").filter((command) => command.trim() !== "");
2875
- const createIndexRegex = /CREATE (UNIQUE )?INDEX (IF NOT EXISTS )?"?(\w+)"? ON "?(\w+)"?\s*\(([^)]+)\)/i;
2876
- const indexNames = [];
2877
- for (const command of commands) {
2878
- const indexMatch = command.trim().match(createIndexRegex);
2879
- if (indexMatch) {
2880
- indexNames.push(indexMatch[3]);
2881
- }
2882
- }
2883
- const existingIndexes = /* @__PURE__ */ new Set();
2884
- if (indexNames.length > 0) {
2885
- const result = await client.query(
2886
- `SELECT indexname FROM pg_indexes
2887
- WHERE schemaname = 'public' AND indexname = ANY($1::text[])`,
2888
- [indexNames]
2889
- );
2890
- for (const row of result.rows) {
2891
- existingIndexes.add(row.indexname);
2892
- }
2893
- }
2894
- for (const command of commands) {
2895
- const trimmedCommand = command.trim();
2896
- const createTableRegex = /CREATE TABLE (IF NOT EXISTS )?"?(\w+)"? \(([\s\S]+)\)/i;
2897
- const tableMatch = trimmedCommand.match(createTableRegex);
2898
- if (tableMatch) {
2899
- const tableName = tableMatch[2];
2900
- const columns = tableMatch[3].trim().split(",\n");
2901
- const exists = await tableExists2(tableName);
2902
- if (!exists) {
2903
- await client.query(trimmedCommand);
2904
- } else {
2905
- for (const column of columns) {
2906
- const columnDef = column.trim();
2907
- const columnMatch = columnDef.match(/"?(\w+)"?\s+(\w+[^,]*)/);
2908
- if (columnMatch) {
2909
- const columnName = columnMatch[1];
2910
- if (columnName.toUpperCase() === "PRIMARY" || columnName.toUpperCase() === "FOREIGN" || columnName.toUpperCase() === "UNIQUE" || columnName.toUpperCase() === "CHECK" || columnName.toUpperCase() === "CONSTRAINT") {
2911
- continue;
2912
- }
2913
- try {
2914
- const columnExists = await client.query(
2915
- `SELECT EXISTS (
2916
- SELECT 1 FROM information_schema.columns
2917
- WHERE table_name = $1
2918
- AND column_name = $2
2919
- AND table_schema = 'public'
2920
- )`,
2921
- [tableName, columnName]
2922
- );
2923
- if (!columnExists.rows[0].exists) {
2924
- const alterCommand = `ALTER TABLE "${tableName}" ADD COLUMN ${columnDef}`;
2925
- await client.query(alterCommand);
2926
- }
2927
- } catch (error) {
2928
- console.error(
2929
- `Error adding column ${columnName} to ${tableName}:`,
2930
- error
2931
- );
2932
- }
2933
- }
2934
- }
2935
- }
2936
- continue;
2937
- }
2938
- const indexMatch = trimmedCommand.match(createIndexRegex);
2939
- if (indexMatch) {
2940
- const indexName = indexMatch[3];
2941
- const indexTableName = indexMatch[4];
2942
- if (!existingIndexes.has(indexName)) {
2943
- try {
2944
- await client.query(trimmedCommand);
2945
- existingIndexes.add(indexName);
2946
- } catch (error) {
2947
- console.warn(
2948
- `Warning: Failed to create index ${indexName} on ${indexTableName}:`,
2949
- error instanceof Error ? error.message : String(error)
2950
- );
2951
- }
2952
- }
2953
- continue;
2954
- }
2955
- if (trimmedCommand.toUpperCase().startsWith("CREATE ") && !trimmedCommand.toUpperCase().includes("CREATE TABLE")) {
2956
- try {
2957
- await client.query(trimmedCommand);
2958
- } catch (error) {
2959
- console.warn(
2960
- `Warning: DDL statement may have failed:`,
2961
- error instanceof Error ? error.message : String(error)
2962
- );
2963
- }
2964
- }
2965
- }
2966
- };
2967
- const transaction = async (callback) => {
2968
- const txClient = await client.connect();
2969
- try {
2970
- await txClient.query("BEGIN");
2971
- const txDb = {
2972
- url,
2973
- client: txClient,
2974
- insert: async (table2, data) => {
2975
- if (Array.isArray(data)) {
2976
- const keys2 = Object.keys(data[0]);
2977
- const placeholders2 = data.map(
2978
- (_, i) => `(${keys2.map((_2, j) => `$${i * keys2.length + j + 1}`).join(", ")})`
2979
- ).join(", ");
2980
- const query3 = `INSERT INTO ${table2} (${keys2.join(", ")}) VALUES ${placeholders2}`;
2981
- const values2 = data.reduce(
2982
- (acc, row) => acc.concat(Object.values(row)),
2983
- []
2984
- );
2985
- const result3 = await txClient.query(query3, values2);
2986
- return { operation: "insert", affected: result3.rowCount ?? 0 };
2987
- }
2988
- const keys = Object.keys(data);
2989
- const values = Object.values(data);
2990
- const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
2991
- const query2 = `INSERT INTO ${table2} (${keys.join(", ")}) VALUES (${placeholders})`;
2992
- const result2 = await txClient.query(query2, values);
2993
- return { operation: "insert", affected: result2.rowCount ?? 0 };
2994
- },
2995
- get: async (table2, where) => {
2996
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2997
- if (!whereClause) {
2998
- throw new DatabaseError(
2999
- "GET requires at least one WHERE condition to prevent returning an arbitrary record",
3000
- { table: table2 }
3001
- );
3002
- }
3003
- const query2 = `SELECT * FROM ${table2} ${whereClause}`;
3004
- const result2 = await txClient.query(query2, values);
3005
- return result2.rows[0] || null;
3006
- },
3007
- list: async (table2, where) => {
3008
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
3009
- const query2 = `SELECT * FROM ${table2} ${whereClause}`;
3010
- const result2 = await txClient.query(query2, values);
3011
- return result2.rows;
3012
- },
3013
- update: async (table2, where, data) => {
3014
- const keys = Object.keys(data);
3015
- const values = Object.values(data);
3016
- const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
3017
- const { sql: whereClause, values: whereValues } = buildWhere(
3018
- where,
3019
- values.length + 1,
3020
- "postgres"
3021
- );
3022
- if (!whereClause) {
3023
- throw new DatabaseError(
3024
- "UPDATE requires at least one WHERE condition to prevent accidental update of all records",
3025
- { table: table2 }
3026
- );
3027
- }
3028
- const sql = `UPDATE ${table2} SET ${setClause} ${whereClause}`;
3029
- const result2 = await txClient.query(sql, [...values, ...whereValues]);
3030
- return { operation: "update", affected: result2.rowCount ?? 0 };
3031
- },
3032
- upsert: async (table2, conflictColumns, data, options2) => executePostgresUpsert(
3033
- txClient,
3034
- table2,
3035
- conflictColumns,
3036
- data,
3037
- options2,
3038
- false
3039
- ),
3040
- getOrInsert: async (table2, where, data) => {
3041
- const result2 = await txDb.get(table2, where);
3042
- if (result2) return result2;
3043
- await txDb.insert(table2, data);
3044
- const inserted = await txDb.get(table2, where);
3045
- if (!inserted) {
3046
- throw new DatabaseError("Failed to insert and retrieve record", {
3047
- table: table2,
3048
- where,
3049
- data
3050
- });
3051
- }
3052
- return inserted;
3053
- },
3054
- delete: async (table2, where) => {
3055
- validateTableName(table2);
3056
- const keys = Object.keys(where);
3057
- if (keys.length === 0) {
3058
- throw new DatabaseError(
3059
- "DELETE requires at least one WHERE condition to prevent accidental deletion of all records",
3060
- { table: table2 }
3061
- );
3062
- }
3063
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
3064
- const result2 = await txClient.query(
3065
- `DELETE FROM ${table2} ${whereClause}`,
3066
- values
3067
- );
3068
- return { operation: "delete", affected: result2.rowCount ?? 0 };
3069
- },
3070
- count: async (table2, where) => {
3071
- validateTableName(table2);
3072
- if (!where || Object.keys(where).length === 0) {
3073
- const result3 = await txClient.query(
3074
- `SELECT COUNT(*) as count FROM ${table2}`
3075
- );
3076
- return Number(result3.rows[0]?.count) || 0;
3077
- }
3078
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
3079
- const result2 = await txClient.query(
3080
- `SELECT COUNT(*) as count FROM ${table2} ${whereClause}`,
3081
- values
3082
- );
3083
- return Number(result2.rows[0]?.count) || 0;
3084
- },
3085
- table: (tableName) => ({
3086
- insert: (data) => txDb.insert(tableName, data),
3087
- get: (data) => txDb.get(tableName, data),
3088
- list: (data) => txDb.list(tableName, data)
3089
- }),
3090
- many: async (strings, ...vars) => {
3091
- const { sql, values } = parseTemplate(strings, ...vars);
3092
- const result2 = await txClient.query(sql, values);
3093
- return result2.rows;
3094
- },
3095
- single: async (strings, ...vars) => {
3096
- const { sql, values } = parseTemplate(strings, ...vars);
3097
- const result2 = await txClient.query(sql, values);
3098
- return result2.rows[0] || null;
3099
- },
3100
- pluck: async (strings, ...vars) => {
3101
- const { sql, values } = parseTemplate(strings, ...vars);
3102
- const result2 = await txClient.query(sql, values);
3103
- const firstRow = result2.rows[0];
3104
- if (!firstRow) return null;
3105
- return Object.values(firstRow)[0];
3106
- },
3107
- execute: async (strings, ...vars) => {
3108
- const { sql, values } = parseTemplate(strings, ...vars);
3109
- await txClient.query(sql, values);
3110
- },
3111
- query: async (sql, ...values) => {
3112
- const query2 = normalizePostgresRawQuery(sql, values);
3113
- const result2 = await txClient.query(query2.sql, query2.values);
3114
- return {
3115
- rows: result2.rows,
3116
- rowCount: result2.rowCount ?? 0
3117
- };
3118
- },
3119
- oo: async (strings, ...vars) => {
3120
- const { sql, values } = parseTemplate(strings, ...vars);
3121
- const result2 = await txClient.query(sql, values);
3122
- return result2.rows;
3123
- },
3124
- oO: async (strings, ...vars) => {
3125
- const { sql, values } = parseTemplate(strings, ...vars);
3126
- const result2 = await txClient.query(sql, values);
3127
- return result2.rows[0] || null;
3128
- },
3129
- ox: async (strings, ...vars) => {
3130
- const { sql, values } = parseTemplate(strings, ...vars);
3131
- const result2 = await txClient.query(sql, values);
3132
- const firstRow = result2.rows[0];
3133
- if (!firstRow) return null;
3134
- return Object.values(firstRow)[0];
3135
- },
3136
- xx: async (strings, ...vars) => {
3137
- const { sql, values } = parseTemplate(strings, ...vars);
3138
- await txClient.query(sql, values);
3139
- },
3140
- tableExists: tableExists2,
3141
- syncSchema: syncSchema2,
3142
- transaction
3143
- };
3144
- const result = await callback(txDb);
3145
- await txClient.query("COMMIT");
3146
- return result;
3147
- } catch (error) {
3148
- await txClient.query("ROLLBACK");
3149
- throw error;
3150
- } finally {
3151
- txClient.release();
3152
- }
3153
- };
3154
- const beginTransaction = async () => {
3155
- const txClient = await client.connect();
3156
- await txClient.query("BEGIN");
3157
- let active = true;
3158
- const commit = async () => {
3159
- if (!active) {
3160
- throw new DatabaseError("Transaction already ended", {});
3161
- }
3162
- await txClient.query("COMMIT");
3163
- active = false;
3164
- txClient.release();
3165
- };
3166
- const rollback = async () => {
3167
- if (!active) {
3168
- throw new DatabaseError("Transaction already ended", {});
3169
- }
3170
- await txClient.query("ROLLBACK");
3171
- active = false;
3172
- txClient.release();
3173
- };
3174
- const isActive = () => active;
3175
- const txHandle = {
3176
- url,
3177
- client: txClient,
3178
- insert: async (table2, data) => {
3179
- if (Array.isArray(data)) {
3180
- const keys2 = Object.keys(data[0]);
3181
- const placeholders2 = data.map(
3182
- (_, i) => `(${keys2.map((_2, j) => `$${i * keys2.length + j + 1}`).join(", ")})`
3183
- ).join(", ");
3184
- const query3 = `INSERT INTO ${table2} (${keys2.join(", ")}) VALUES ${placeholders2}`;
3185
- const values2 = data.reduce(
3186
- (acc, row) => acc.concat(Object.values(row)),
3187
- []
3188
- );
3189
- const result2 = await txClient.query(query3, values2);
3190
- return { operation: "insert", affected: result2.rowCount ?? 0 };
3191
- }
3192
- const keys = Object.keys(data);
3193
- const values = Object.values(data);
3194
- const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
3195
- const query2 = `INSERT INTO ${table2} (${keys.join(", ")}) VALUES (${placeholders})`;
3196
- const result = await txClient.query(query2, values);
3197
- return { operation: "insert", affected: result.rowCount ?? 0 };
3198
- },
3199
- get: async (table2, where) => {
3200
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
3201
- if (!whereClause) {
3202
- throw new DatabaseError(
3203
- "GET requires at least one WHERE condition to prevent returning an arbitrary record",
3204
- { table: table2 }
3205
- );
3206
- }
3207
- const query2 = `SELECT * FROM ${table2} ${whereClause}`;
3208
- const result = await txClient.query(query2, values);
3209
- return result.rows[0] || null;
3210
- },
3211
- list: async (table2, where) => {
3212
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
3213
- const query2 = `SELECT * FROM ${table2} ${whereClause}`;
3214
- const result = await txClient.query(query2, values);
3215
- return result.rows;
3216
- },
3217
- update: async (table2, where, data) => {
3218
- const keys = Object.keys(data);
3219
- const values = Object.values(data);
3220
- const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
3221
- const { sql: whereClause, values: whereValues } = buildWhere(
3222
- where,
3223
- values.length + 1,
3224
- "postgres"
3225
- );
3226
- if (!whereClause) {
3227
- throw new DatabaseError(
3228
- "UPDATE requires at least one WHERE condition to prevent accidental update of all records",
3229
- { table: table2 }
3230
- );
3231
- }
3232
- const sql = `UPDATE ${table2} SET ${setClause} ${whereClause}`;
3233
- const result = await txClient.query(sql, [...values, ...whereValues]);
3234
- return { operation: "update", affected: result.rowCount ?? 0 };
3235
- },
3236
- upsert: async (table2, conflictColumns, data, options2) => executePostgresUpsert(
3237
- txClient,
3238
- table2,
3239
- conflictColumns,
3240
- data,
3241
- options2,
3242
- false
3243
- ),
3244
- getOrInsert: async (table2, where, data) => {
3245
- const result = await txHandle.get(table2, where);
3246
- if (result) return result;
3247
- await txHandle.insert(table2, data);
3248
- const inserted = await txHandle.get(table2, where);
3249
- if (!inserted) {
3250
- throw new DatabaseError("Failed to insert and retrieve record", {
3251
- table: table2,
3252
- where,
3253
- data
3254
- });
3255
- }
3256
- return inserted;
3257
- },
3258
- delete: async (table2, where) => {
3259
- validateTableName(table2);
3260
- const keys = Object.keys(where);
3261
- if (keys.length === 0) {
3262
- throw new DatabaseError(
3263
- "DELETE requires at least one WHERE condition to prevent accidental deletion of all records",
3264
- { table: table2 }
3265
- );
3266
- }
3267
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
3268
- const result = await txClient.query(
3269
- `DELETE FROM ${table2} ${whereClause}`,
3270
- values
3271
- );
3272
- return { operation: "delete", affected: result.rowCount ?? 0 };
3273
- },
3274
- count: async (table2, where) => {
3275
- validateTableName(table2);
3276
- if (!where || Object.keys(where).length === 0) {
3277
- const result2 = await txClient.query(
3278
- `SELECT COUNT(*) as count FROM ${table2}`
3279
- );
3280
- return Number(result2.rows[0]?.count) || 0;
3281
- }
3282
- const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
3283
- const result = await txClient.query(
3284
- `SELECT COUNT(*) as count FROM ${table2} ${whereClause}`,
3285
- values
3286
- );
3287
- return Number(result.rows[0]?.count) || 0;
3288
- },
3289
- table: (tableName) => ({
3290
- insert: (data) => txHandle.insert(tableName, data),
3291
- get: (data) => txHandle.get(tableName, data),
3292
- list: (data) => txHandle.list(tableName, data)
3293
- }),
3294
- many: async (strings, ...vars) => {
3295
- const { sql, values } = parseTemplate(strings, ...vars);
3296
- const result = await txClient.query(sql, values);
3297
- return result.rows;
3298
- },
3299
- single: async (strings, ...vars) => {
3300
- const { sql, values } = parseTemplate(strings, ...vars);
3301
- const result = await txClient.query(sql, values);
3302
- return result.rows[0] || null;
3303
- },
3304
- pluck: async (strings, ...vars) => {
3305
- const { sql, values } = parseTemplate(strings, ...vars);
3306
- const result = await txClient.query(sql, values);
3307
- const firstRow = result.rows[0];
3308
- if (!firstRow) return null;
3309
- return Object.values(firstRow)[0];
3310
- },
3311
- execute: async (strings, ...vars) => {
3312
- const { sql, values } = parseTemplate(strings, ...vars);
3313
- await txClient.query(sql, values);
3314
- },
3315
- query: async (sql, ...values) => {
3316
- const query2 = normalizePostgresRawQuery(sql, values);
3317
- const result = await txClient.query(query2.sql, query2.values);
3318
- return {
3319
- rows: result.rows,
3320
- rowCount: result.rowCount ?? 0
3321
- };
3322
- },
3323
- oo: async (strings, ...vars) => {
3324
- const { sql, values } = parseTemplate(strings, ...vars);
3325
- const result = await txClient.query(sql, values);
3326
- return result.rows;
3327
- },
3328
- oO: async (strings, ...vars) => {
3329
- const { sql, values } = parseTemplate(strings, ...vars);
3330
- const result = await txClient.query(sql, values);
3331
- return result.rows[0] || null;
3332
- },
3333
- ox: async (strings, ...vars) => {
3334
- const { sql, values } = parseTemplate(strings, ...vars);
3335
- const result = await txClient.query(sql, values);
3336
- const firstRow = result.rows[0];
3337
- if (!firstRow) return null;
3338
- return Object.values(firstRow)[0];
3339
- },
3340
- xx: async (strings, ...vars) => {
3341
- const { sql, values } = parseTemplate(strings, ...vars);
3342
- await txClient.query(sql, values);
3343
- },
3344
- tableExists: tableExists2,
3345
- syncSchema: syncSchema2,
3346
- transaction,
3347
- commit,
3348
- rollback,
3349
- isActive
3350
- };
3351
- return txHandle;
3352
- };
3353
- const oo = many;
3354
- const oO = single;
3355
- const ox = pluck;
3356
- const xx = execute;
3357
- const initializeSchemas = async (options2) => {
3358
- const schemaManager = new DatabaseSchemaManager();
3359
- const currentDb = {
3360
- url,
3361
- client,
3362
- insert,
3363
- update,
3364
- upsert,
3365
- get,
3366
- getOrInsert,
3367
- delete: deleteRecords,
3368
- count,
3369
- list,
3370
- table,
3371
- many,
3372
- single,
3373
- pluck,
3374
- execute,
3375
- query,
3376
- oo,
3377
- oO,
3378
- ox,
3379
- xx,
3380
- tableExists: tableExists2,
3381
- syncSchema: syncSchema2,
3382
- transaction
3383
- };
3384
- await schemaManager.initializeSchemas(currentDb, options2);
3385
- };
3386
- const getTableSchema = async (table2) => {
3387
- validateTableName(table2);
3388
- try {
3389
- const exists = await tableExists2(table2);
3390
- if (!exists) {
3391
- return null;
3392
- }
3393
- const columnRows = await many`
3394
- SELECT
3395
- column_name,
3396
- data_type,
3397
- is_nullable,
3398
- column_default
3399
- FROM information_schema.columns
3400
- WHERE table_name = ${table2}
3401
- AND table_schema = 'public'
3402
- ORDER BY ordinal_position
3403
- `;
3404
- const pkRows = await many`
3405
- SELECT kcu.column_name
3406
- FROM information_schema.table_constraints tc
3407
- JOIN information_schema.key_column_usage kcu
3408
- ON tc.constraint_name = kcu.constraint_name
3409
- AND tc.table_schema = kcu.table_schema
3410
- WHERE tc.table_name = ${table2}
3411
- AND tc.table_schema = 'public'
3412
- AND tc.constraint_type = 'PRIMARY KEY'
3413
- `;
3414
- const pkColumns = new Set(pkRows.map((row) => row.column_name));
3415
- const columns = {};
3416
- for (const row of columnRows) {
3417
- const colName = row.column_name;
3418
- columns[colName] = {
3419
- type: row.data_type,
3420
- primaryKey: pkColumns.has(colName),
3421
- notNull: row.is_nullable === "NO",
3422
- defaultValue: row.column_default
3423
- };
3424
- }
3425
- const indexRows = await many`
3426
- SELECT
3427
- indexname,
3428
- indexdef
3429
- FROM pg_indexes
3430
- WHERE tablename = ${table2}
3431
- AND schemaname = 'public'
3432
- AND indexname NOT LIKE '%_pkey'
3433
- `;
3434
- const indexes = [];
3435
- for (const row of indexRows) {
3436
- const indexName = row.indexname;
3437
- const indexDef = row.indexdef;
3438
- const colMatch = indexDef.match(/\(([^)]+)\)/);
3439
- const indexColumns = colMatch ? colMatch[1].split(",").map((col) => col.trim()) : [];
3440
- const isUnique = indexDef.toUpperCase().includes("UNIQUE");
3441
- indexes.push({
3442
- name: indexName,
3443
- columns: indexColumns,
3444
- unique: isUnique
3445
- });
3446
- }
3447
- const fkRows = await many`
3448
- SELECT
3449
- kcu.column_name,
3450
- ccu.table_name AS foreign_table_name,
3451
- ccu.column_name AS foreign_column_name,
3452
- rc.delete_rule,
3453
- rc.update_rule
3454
- FROM information_schema.table_constraints AS tc
3455
- JOIN information_schema.key_column_usage AS kcu
3456
- ON tc.constraint_name = kcu.constraint_name
3457
- AND tc.table_schema = kcu.table_schema
3458
- JOIN information_schema.constraint_column_usage AS ccu
3459
- ON ccu.constraint_name = tc.constraint_name
3460
- AND ccu.table_schema = tc.table_schema
3461
- JOIN information_schema.referential_constraints AS rc
3462
- ON rc.constraint_name = tc.constraint_name
3463
- AND rc.constraint_schema = tc.table_schema
3464
- WHERE tc.constraint_type = 'FOREIGN KEY'
3465
- AND tc.table_name = ${table2}
3466
- AND tc.table_schema = 'public'
3467
- `;
3468
- const foreignKeys = [];
3469
- for (const fkRow of fkRows) {
3470
- foreignKeys.push({
3471
- column: fkRow.column_name,
3472
- referencesTable: fkRow.foreign_table_name,
3473
- referencesColumn: fkRow.foreign_column_name,
3474
- onDelete: fkRow.delete_rule,
3475
- onUpdate: fkRow.update_rule
3476
- });
3477
- }
3478
- return {
3479
- tableName: table2,
3480
- columns,
3481
- indexes,
3482
- foreignKeys
3483
- };
3484
- } catch (e) {
3485
- throw new DatabaseError("Failed to retrieve table schema", {
3486
- table: table2,
3487
- originalError: formatDbError(e)
3488
- });
3489
- }
3490
- };
3491
- const alterTable = {
3492
- /**
3493
- * Adds a new column to an existing table
3494
- *
3495
- * @param table - Table name
3496
- * @param column - Column definition with name
3497
- * @returns Promise that resolves when column is added
3498
- * @throws Error if the alter operation fails
3499
- */
3500
- addColumn: async (table2, column) => {
3501
- validateTableName(table2);
3502
- validateColumnName$1(column.name);
3503
- try {
3504
- const sql = generateAddColumnStatement(table2, column, "postgres");
3505
- await client.query(sql);
3506
- } catch (e) {
3507
- throw new DatabaseError("Failed to add column to table", {
3508
- table: table2,
3509
- column: column.name,
3510
- originalError: formatDbError(e)
3511
- });
3512
- }
3513
- },
3514
- /**
3515
- * Adds a new index to an existing table
3516
- *
3517
- * @param table - Table name
3518
- * @param index - Index definition
3519
- * @returns Promise that resolves when index is created
3520
- * @throws Error if the create index operation fails
3521
- */
3522
- addIndex: async (table2, index2) => {
3523
- validateTableName(table2);
3524
- validateIndexName(index2.name);
3525
- for (const col of index2.columns) {
3526
- validateColumnName$1(col);
3527
- }
3528
- try {
3529
- const sql = generateCreateIndexStatement(table2, index2);
3530
- await client.query(sql);
3531
- } catch (e) {
3532
- throw new DatabaseError("Failed to create index on table", {
3533
- table: table2,
3534
- index: index2.name,
3535
- originalError: formatDbError(e)
3536
- });
3537
- }
3538
- }
3539
- };
3540
- const db = {
3541
- url,
3542
- client,
3543
- insert,
3544
- update,
3545
- upsert,
3546
- get,
3547
- getOrInsert,
3548
- delete: deleteRecords,
3549
- count,
3550
- list,
3551
- table,
3552
- many,
3553
- single,
3554
- pluck,
3555
- execute,
3556
- query,
3557
- oo,
3558
- oO,
3559
- ox,
3560
- xx,
3561
- tableExists: tableExists2,
3562
- syncSchema: syncSchema2,
3563
- initializeSchemas,
3564
- transaction,
3565
- beginTransaction,
3566
- acquireSession,
3567
- getTableSchema,
3568
- alterTable,
3569
- vector: createVectorCapabilities(pool)
3570
- };
3571
- const finalCacheKey = cacheKey || url;
3572
- connectionCache.set(finalCacheKey, db);
3573
- return db;
3574
- }
3575
- const postgres = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
3576
- __proto__: null,
3577
- clearPostgresConnectionCache,
3578
- getDatabase: getDatabase$1
3579
- }, Symbol.toStringTag, { value: "Module" }));
3580
- function isDatabaseInstance(value) {
3581
- return value && typeof value === "object" && typeof value.client !== "undefined" && typeof value.insert === "function" && typeof value.get === "function" && typeof value.query === "function";
3582
- }
3583
- async function getDatabase(options = {}) {
3584
- if (isDatabaseInstance(options)) {
3585
- return options;
3586
- }
3587
- Object.assign(
3588
- options,
3589
- loadEnvConfig(options, {
3590
- packageName: "sql",
3591
- schema: {
3592
- type: "string",
3593
- url: "string",
3594
- host: "string",
3595
- port: "number",
3596
- database: "string",
3597
- user: "string",
3598
- password: "string"
3599
- }
3600
- })
3601
- );
3602
- if (!options.type && (options.url?.startsWith("file:") || options.url === ":memory:")) {
3603
- options.type = "sqlite";
3604
- }
3605
- if (options.type === "postgres") {
3606
- const postgres$1 = await Promise.resolve().then(() => postgres);
3607
- return postgres$1.getDatabase(options);
3608
- }
3609
- if (options.type === "sqlite") {
3610
- const sqlite = await import("./sqlite-CPCNeJIH.js");
3611
- return sqlite.getDatabase(options);
3612
- }
3613
- if (options.type === "duckdb") {
3614
- const duckdb = await import("./duckdb-B6m78QdL.js");
3615
- return duckdb.getDatabase(options);
3616
- }
3617
- if (options.type === "json") {
3618
- const json$1 = await Promise.resolve().then(() => json);
3619
- return json$1.getDatabase(options);
3620
- }
3621
- throw new Error("Invalid database type");
3622
- }
3623
- async function syncSchema(options) {
3624
- const { db, schema } = options;
3625
- if (!db || !schema) {
3626
- throw new Error("db and schema are required");
3627
- }
3628
- if (db.syncSchema) {
3629
- await db.syncSchema(schema);
3630
- } else {
3631
- throw new Error("Database adapter does not support schema synchronization");
3632
- }
3633
- }
3634
- async function tableExists(db, tableName) {
3635
- return db.tableExists(tableName);
3636
- }
3637
- function escapeSqlValue(value) {
3638
- if (value === null) {
3639
- return "NULL";
3640
- }
3641
- if (value instanceof Date) {
3642
- return `'${value.toISOString()}'`;
3643
- }
3644
- if (typeof value === "number") {
3645
- return value.toString();
3646
- }
3647
- if (typeof value === "boolean") {
3648
- return value ? "1" : "0";
3649
- }
3650
- return `'${String(value).replace(/'/g, "''")}'`;
3651
- }
3652
- function validateColumnName(column) {
3653
- if (!/^[a-zA-Z0-9_.]+$/.test(column)) {
3654
- throw new Error(`Invalid column name: ${column}`);
3655
- }
3656
- return column;
3657
- }
3658
- const index = {
3659
- getDatabase,
3660
- syncSchema,
3661
- tableExists,
3662
- buildWhere
3663
- };
3664
- const PACKAGE_VERSION_INITIALIZED = true;
3665
- export {
3666
- DatabaseSchemaManager as D,
3667
- PACKAGE_VERSION_INITIALIZED as P,
3668
- validateIndexName as a,
3669
- validateColumnName$1 as b,
3670
- generateAddColumnStatement as c,
3671
- buildWhere as d,
3672
- convertUniqueIndexesToInlineConstraints as e,
3673
- formatDbError as f,
3674
- generateCreateIndexStatement as g,
3675
- bucketExpr as h,
3676
- buildAggregate as i,
3677
- checkExpectedTables as j,
3678
- checkRelationship as k,
3679
- checkUniqueColumn as l,
3680
- clearConnectionCache as m,
3681
- clearPostgresConnectionCache as n,
3682
- index as o,
3683
- escapeSqlValue as p,
3684
- getDatabase as q,
3685
- resolveSchemas as r,
3686
- runDoctor as s,
3687
- syncSchema as t,
3688
- tableExists as u,
3689
- validateTableName as v,
3690
- validateColumnName as w
3691
- };
3692
- //# sourceMappingURL=index-CzdYvRFp.js.map