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