@happyvertical/sql 0.80.6 → 0.81.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/chunks/{schema-manager-a8RlxIE2.js → alter-utils-B900pfK_.js} +246 -2
- package/dist/chunks/alter-utils-B900pfK_.js.map +1 -0
- package/dist/chunks/{duckdb-D_Wu36JL.js → duckdb-CWy7Nziq.js} +45 -21
- package/dist/chunks/duckdb-CWy7Nziq.js.map +1 -0
- package/dist/chunks/{sqlite-B4ussRTS.js → sqlite-kBmA-x_C.js} +68 -29
- package/dist/chunks/sqlite-kBmA-x_C.js.map +1 -0
- package/dist/chunks/{sqlite-native-BgiF4301.js → sqlite-native-Dnlo3Tux.js} +73 -21
- package/dist/chunks/sqlite-native-Dnlo3Tux.js.map +1 -0
- package/dist/chunks/types-uVFHgaSU.js +133 -0
- package/dist/chunks/types-uVFHgaSU.js.map +1 -0
- package/dist/duckdb.d.ts.map +1 -1
- package/dist/index.js +715 -733
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts.map +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/shared/alter-utils.d.ts +48 -0
- package/dist/shared/alter-utils.d.ts.map +1 -1
- package/dist/shared/transaction-lock.d.ts +54 -0
- package/dist/shared/transaction-lock.d.ts.map +1 -0
- package/dist/shared/types.d.ts +69 -7
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/utils.d.ts +31 -0
- package/dist/shared/utils.d.ts.map +1 -1
- package/dist/sqlite-native.d.ts.map +1 -1
- package/dist/sqlite.d.ts +20 -0
- package/dist/sqlite.d.ts.map +1 -1
- package/package.json +5 -5
- package/dist/chunks/alter-utils-K7tiuj5L.js +0 -135
- package/dist/chunks/alter-utils-K7tiuj5L.js.map +0 -1
- package/dist/chunks/duckdb-D_Wu36JL.js.map +0 -1
- package/dist/chunks/schema-manager-a8RlxIE2.js.map +0 -1
- package/dist/chunks/sqlite-B4ussRTS.js.map +0 -1
- package/dist/chunks/sqlite-native-BgiF4301.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as validateColumnName$1, c as validateTableName$1, d as formatDbError, f as parseConditionKey, i as generateCreateIndexStatement, l as DatabaseSchemaManager, n as escapeStringLiteral, o as validateColumnNames, p as resolveInsertColumns, r as generateAddColumnStatement, s as validateIndexName, t as escapeQuotedIdentifier, u as buildWhere } from "./chunks/alter-utils-B900pfK_.js";
|
|
2
2
|
import { t as convertUniqueIndexesToInlineConstraints } from "./chunks/duckdb-schema-utils-B-XBP8QF.js";
|
|
3
3
|
import { a as dropPostgresDatabase, c as postgresEnvFromUrl, i as databaseNameFromUrl, l as redactDatabaseUrl, n as assertCanImportDatabase, o as dumpPostgresDatabase, r as createPostgresDatabase, s as isLocalDatabaseUrl, t as assertCanExportDatabase, u as restorePostgresDatabase } from "./chunks/postgres-cli-DzZcXSIG.js";
|
|
4
|
-
import {
|
|
4
|
+
import { n as resolveSchemas, r as createTransactionLock, t as NestedTransactionError } from "./chunks/types-uVFHgaSU.js";
|
|
5
5
|
import { DatabaseError, loadEnvConfig } from "@happyvertical/utils";
|
|
6
6
|
import { mkdir, readdir } from "node:fs/promises";
|
|
7
7
|
import { basename, extname, join } from "node:path";
|
|
@@ -308,48 +308,6 @@ function countValueFromRow(value) {
|
|
|
308
308
|
return "0";
|
|
309
309
|
}
|
|
310
310
|
//#endregion
|
|
311
|
-
//#region src/shared/types.ts
|
|
312
|
-
/**
|
|
313
|
-
* Error thrown when `transaction()` is called on a transaction-scoped interface
|
|
314
|
-
* belonging to an adapter whose engine has no savepoints.
|
|
315
|
-
*
|
|
316
|
-
* DuckDB — and therefore the JSON adapter built on it — does not implement
|
|
317
|
-
* `SAVEPOINT`, so there is no way to re-enter the current transaction. Adapters
|
|
318
|
-
* that can re-enter (PostgreSQL, SQLite) do so under a savepoint and never
|
|
319
|
-
* throw this.
|
|
320
|
-
*
|
|
321
|
-
* The alternative would be to open a second, independent transaction, which is
|
|
322
|
-
* what these adapters used to do; on a shared connection that silently
|
|
323
|
-
* destroyed the enclosing transaction's uncommitted work.
|
|
324
|
-
*
|
|
325
|
-
* @example Ensuring a helper works on every adapter
|
|
326
|
-
* ```typescript
|
|
327
|
-
* // Take the transaction as a parameter rather than opening a nested one.
|
|
328
|
-
* async function recordClick(db: DatabaseInterface, id: string) {
|
|
329
|
-
* await db.update('links', { id }, { clicks: 1 });
|
|
330
|
-
* }
|
|
331
|
-
* await db.transaction(async (tx) => recordClick(tx, id));
|
|
332
|
-
* ```
|
|
333
|
-
*/
|
|
334
|
-
var NestedTransactionError = class extends DatabaseError {
|
|
335
|
-
constructor(message, context) {
|
|
336
|
-
super(message, context);
|
|
337
|
-
this.name = "NestedTransactionError";
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
/**
|
|
341
|
-
* Resolves a SchemasOption to its concrete value.
|
|
342
|
-
*
|
|
343
|
-
* Adapters that need schemas (JSON, DuckDB) call this to unwrap the
|
|
344
|
-
* lazy function. Adapters that don't (Postgres, SQLite) simply ignore
|
|
345
|
-
* the `schemas` option without calling this.
|
|
346
|
-
*/
|
|
347
|
-
function resolveSchemas(schemas) {
|
|
348
|
-
if (!schemas) return void 0;
|
|
349
|
-
if (typeof schemas === "function") return schemas();
|
|
350
|
-
return schemas;
|
|
351
|
-
}
|
|
352
|
-
//#endregion
|
|
353
311
|
//#region src/json.ts
|
|
354
312
|
var json_exports = /* @__PURE__ */ __exportAll({
|
|
355
313
|
clearConnectionCache: () => clearConnectionCache,
|
|
@@ -430,21 +388,36 @@ async function createJSONConnection(options) {
|
|
|
430
388
|
* This function only loads data - it assumes the table already exists with proper schema.
|
|
431
389
|
* Used after DDL creates the table to load persisted JSON data.
|
|
432
390
|
*
|
|
391
|
+
* An unreadable or malformed file is tolerated with a warning - the table
|
|
392
|
+
* structure is what matters and there is no data to lose.
|
|
393
|
+
*
|
|
394
|
+
* A failure to insert records that DID parse is logged loudly rather than
|
|
395
|
+
* swallowed with the old dismissive "that's okay" warning: the resulting empty
|
|
396
|
+
* table is a real surprise the caller should see (issue #1132). It is logged
|
|
397
|
+
* rather than thrown so that one unloadable table cannot abort the load of every
|
|
398
|
+
* other table sharing the same data directory or syncSchema() call - both of
|
|
399
|
+
* those paths already load each table independently.
|
|
400
|
+
*
|
|
433
401
|
* @param connection - DuckDB connection
|
|
434
402
|
* @param tableName - Name of the table to load data into
|
|
435
403
|
* @param filePath - Path to the JSON file
|
|
436
404
|
*/
|
|
437
405
|
async function loadJSONData(connection, tableName, filePath) {
|
|
406
|
+
let records;
|
|
438
407
|
try {
|
|
439
408
|
const { readFile } = await import("node:fs/promises");
|
|
440
409
|
const jsonContent = await readFile(filePath, "utf-8");
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
410
|
+
records = JSON.parse(jsonContent);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
console.warn(`[json-adapter] Could not read data for ${tableName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (!Array.isArray(records) || records.length === 0) return;
|
|
416
|
+
try {
|
|
417
|
+
const inserted = await insertRecordsWithCast(connection, tableName, records);
|
|
418
|
+
if (inserted > 0) console.log(`[json-adapter] Loaded ${inserted} records into ${tableName} from JSON`);
|
|
446
419
|
} catch (error) {
|
|
447
|
-
console.
|
|
420
|
+
console.error(`[json-adapter] Failed to load data for ${tableName}; the table will be empty:`, error);
|
|
448
421
|
}
|
|
449
422
|
}
|
|
450
423
|
/**
|
|
@@ -503,7 +476,7 @@ async function loadJSONTables(connection, dataDir, providedSchemas = {}, pending
|
|
|
503
476
|
}
|
|
504
477
|
else {
|
|
505
478
|
if (eagerLoadTables) try {
|
|
506
|
-
await connection.run(`CREATE TABLE IF NOT EXISTS "${tableName}" AS SELECT * FROM read_json_auto('${filePath}')`);
|
|
479
|
+
await connection.run(`CREATE TABLE IF NOT EXISTS "${escapeQuotedIdentifier(tableName)}" AS SELECT * FROM read_json_auto('${escapeStringLiteral(filePath)}')`);
|
|
507
480
|
inferredSchemaTables.add(tableName);
|
|
508
481
|
console.log(`[json-adapter] Eagerly created ${tableName} with inferred schema (no UNIQUE constraints)`);
|
|
509
482
|
} catch (error) {
|
|
@@ -635,25 +608,87 @@ async function createTableFromSchema(connection, tableName, schema) {
|
|
|
635
608
|
}
|
|
636
609
|
}
|
|
637
610
|
/**
|
|
611
|
+
* Reads the column names of an existing table, keyed by lowercase name
|
|
612
|
+
*
|
|
613
|
+
* The table-name predicate is case-insensitive: DuckDB resolves identifiers
|
|
614
|
+
* case-insensitively, so a data file named `Items.json` can back a table
|
|
615
|
+
* created as `items`. Matching table_name exactly would find no columns and
|
|
616
|
+
* make the loader wrongly report that every field is unmatched (issue #1132).
|
|
617
|
+
*
|
|
618
|
+
* @param connection - DuckDB connection
|
|
619
|
+
* @param tableName - Name of the table to describe
|
|
620
|
+
* @returns Map of lowercase column name to the column's declared name
|
|
621
|
+
*/
|
|
622
|
+
async function getTableColumns(connection, tableName) {
|
|
623
|
+
const reader = await connection.runAndReadAll("SELECT column_name FROM information_schema.columns WHERE LOWER(table_name) = LOWER($1) ORDER BY ordinal_position", [tableName]);
|
|
624
|
+
return new Map(reader.getRowObjects().map((row) => [String(row.column_name).toLowerCase(), String(row.column_name)]));
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
638
627
|
* Inserts records with proper CAST handling for empty strings
|
|
639
628
|
*
|
|
640
629
|
* This helper avoids using read_json() with auto_detect, which causes DuckDB
|
|
641
630
|
* to re-infer column types from data (causing ANY type for empty strings).
|
|
642
631
|
* Instead, it uses parameterized INSERT with CAST to maintain table schema.
|
|
643
632
|
*
|
|
633
|
+
* Used only for loading a JSON data file, where records having different key
|
|
634
|
+
* sets is normal input rather than a caller error: a document with an optional
|
|
635
|
+
* field omits that key entirely on the records that lack it. Columns are
|
|
636
|
+
* therefore unioned across ALL records and any record missing a key binds NULL.
|
|
637
|
+
* Deriving them from the first record alone bound `undefined` for a missing key
|
|
638
|
+
* (which DuckDB rejects, losing every row) and dropped keys the first record
|
|
639
|
+
* did not have (losing the column silently). See issue #1132.
|
|
640
|
+
*
|
|
641
|
+
* The caller-facing insert() API is a separate implementation (it resolves
|
|
642
|
+
* columns through `resolveInsertColumns`) and is not affected by this
|
|
643
|
+
* file-loading tolerance.
|
|
644
|
+
*
|
|
644
645
|
* @param connection - DuckDB connection
|
|
645
646
|
* @param tableName - Name of the table to insert into
|
|
646
647
|
* @param records - Array of records to insert
|
|
648
|
+
* @returns Number of records inserted (0 if there was nothing loadable)
|
|
649
|
+
* @throws DatabaseError if the resolved records cannot be inserted
|
|
647
650
|
*/
|
|
648
651
|
async function insertRecordsWithCast(connection, tableName, records) {
|
|
649
|
-
if (records.length === 0) return;
|
|
650
|
-
const
|
|
652
|
+
if (records.length === 0) return 0;
|
|
653
|
+
const objectRecords = records.filter((record) => record != null && typeof record === "object" && !Array.isArray(record));
|
|
654
|
+
if (objectRecords.length === 0) return 0;
|
|
655
|
+
const unionedKeys = [];
|
|
656
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
657
|
+
for (const record of objectRecords) for (const key of Object.keys(record)) if (!seenKeys.has(key)) {
|
|
658
|
+
seenKeys.add(key);
|
|
659
|
+
unionedKeys.push(key);
|
|
660
|
+
}
|
|
661
|
+
if (unionedKeys.length === 0) {
|
|
662
|
+
console.warn(`[json-adapter] Skipping data load for ${tableName}: records have no fields`);
|
|
663
|
+
return 0;
|
|
664
|
+
}
|
|
665
|
+
const tableColumns = await getTableColumns(connection, tableName);
|
|
666
|
+
const keys = [];
|
|
667
|
+
const columnNames = [];
|
|
668
|
+
const unmatchedKeys = [];
|
|
669
|
+
const claimedColumns = /* @__PURE__ */ new Set();
|
|
670
|
+
for (const key of unionedKeys) {
|
|
671
|
+
const column = tableColumns.get(key.toLowerCase());
|
|
672
|
+
if (column === void 0 || claimedColumns.has(column.toLowerCase())) unmatchedKeys.push(key);
|
|
673
|
+
else {
|
|
674
|
+
claimedColumns.add(column.toLowerCase());
|
|
675
|
+
keys.push(key);
|
|
676
|
+
columnNames.push(column);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (unmatchedKeys.length > 0) console.warn(`[json-adapter] Ignoring ${unmatchedKeys.length} field(s) in ${tableName} data with no matching column: ${unmatchedKeys.join(", ")}`);
|
|
680
|
+
if (keys.length === 0) throw new DatabaseError(`No fields in the ${tableName} data file match a column on the table`, {
|
|
681
|
+
tableName,
|
|
682
|
+
fields: unionedKeys,
|
|
683
|
+
columns: [...tableColumns.values()]
|
|
684
|
+
});
|
|
685
|
+
const insertableRecords = objectRecords.filter((record) => keys.some((key) => key in record));
|
|
651
686
|
const values = [];
|
|
652
687
|
let paramIdx = 1;
|
|
653
|
-
const placeholders =
|
|
688
|
+
const placeholders = insertableRecords.map((record) => {
|
|
654
689
|
return `(${keys.map((key) => {
|
|
655
690
|
const value = record[key];
|
|
656
|
-
if (value === null) return "NULL";
|
|
691
|
+
if (value === null || value === void 0) return "NULL";
|
|
657
692
|
else if (value === "" && typeof value === "string") {
|
|
658
693
|
values.push(value);
|
|
659
694
|
return `CAST($${paramIdx++} AS TEXT)`;
|
|
@@ -672,8 +707,18 @@ async function insertRecordsWithCast(connection, tableName, records) {
|
|
|
672
707
|
}
|
|
673
708
|
}).join(", ")})`;
|
|
674
709
|
}).join(", ");
|
|
675
|
-
const sql = `INSERT INTO ${tableName} (${
|
|
676
|
-
|
|
710
|
+
const sql = `INSERT INTO "${tableName}" (${columnNames.map((name) => `"${name}"`).join(", ")}) VALUES ${placeholders}`;
|
|
711
|
+
try {
|
|
712
|
+
await connection.run(sql, values);
|
|
713
|
+
} catch (e) {
|
|
714
|
+
throw new DatabaseError("Failed to load records into table", {
|
|
715
|
+
tableName,
|
|
716
|
+
recordCount: insertableRecords.length,
|
|
717
|
+
columns: columnNames,
|
|
718
|
+
originalError: formatDbError(e)
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
return insertableRecords.length;
|
|
677
722
|
}
|
|
678
723
|
/**
|
|
679
724
|
* Validates that a table name is valid (alphanumeric + underscores only)
|
|
@@ -708,6 +753,7 @@ async function getDatabase$2(options) {
|
|
|
708
753
|
const { connection, pendingJSONFiles, inferredSchemaTables } = await createJSONConnection(options);
|
|
709
754
|
const writeStrategy = options.writeStrategy || "immediate";
|
|
710
755
|
const { url } = options;
|
|
756
|
+
const connectionLock = createTransactionLock("json", options.transactionQueueTimeout);
|
|
711
757
|
/**
|
|
712
758
|
* Inserts one or more records into a table
|
|
713
759
|
*
|
|
@@ -727,13 +773,14 @@ async function getDatabase$2(options) {
|
|
|
727
773
|
operation: "insert",
|
|
728
774
|
affected: 0
|
|
729
775
|
};
|
|
730
|
-
const keys =
|
|
776
|
+
const keys = resolveInsertColumns(table, records);
|
|
777
|
+
validateColumnNames(keys);
|
|
731
778
|
const values = [];
|
|
732
779
|
let paramIdx = 1;
|
|
733
780
|
const placeholders = records.map((record) => {
|
|
734
781
|
return `(${keys.map((key) => {
|
|
735
782
|
const value = record[key];
|
|
736
|
-
if (value === null) return "NULL";
|
|
783
|
+
if (value === null || value === void 0) return "NULL";
|
|
737
784
|
else if (value === "" && typeof value === "string") {
|
|
738
785
|
values.push(value);
|
|
739
786
|
return `CAST($${paramIdx++} AS TEXT)`;
|
|
@@ -833,6 +880,7 @@ async function getDatabase$2(options) {
|
|
|
833
880
|
writeStrategy
|
|
834
881
|
});
|
|
835
882
|
const keys = Object.keys(data);
|
|
883
|
+
validateColumnNames(keys);
|
|
836
884
|
const setClause = keys.map((key, idx) => `${key} = $${idx + 1}`).join(", ");
|
|
837
885
|
const { sql: whereClause, values: whereValues } = buildWhere(where, keys.length + 1, "json");
|
|
838
886
|
const dataValues = Object.values(data).map((value) => value instanceof Date ? value.toISOString() : value);
|
|
@@ -882,7 +930,8 @@ async function getDatabase$2(options) {
|
|
|
882
930
|
const values = [];
|
|
883
931
|
const paramIdx = { value: 1 };
|
|
884
932
|
const assignments = keys.map((key) => {
|
|
885
|
-
|
|
933
|
+
const valueExpr = buildJSONValueExpression(data[key], values, paramIdx);
|
|
934
|
+
return `"${escapeQuotedIdentifier(key)}" = ${valueExpr}`;
|
|
886
935
|
});
|
|
887
936
|
const { sql: whereClause, values: whereValues } = buildWhere(conflictWhere, paramIdx.value, "json");
|
|
888
937
|
const sql = `UPDATE "${table}" SET ${assignments.join(", ")} ${whereClause}`;
|
|
@@ -981,11 +1030,12 @@ async function getDatabase$2(options) {
|
|
|
981
1030
|
paramIdx++;
|
|
982
1031
|
}
|
|
983
1032
|
}
|
|
984
|
-
const conflict = conflictColumns.map((col) => `"${col}"`).join(", ");
|
|
985
|
-
const quotedKeys = keys.map((key) => `"${key}"`).join(", ");
|
|
1033
|
+
const conflict = conflictColumns.map((col) => `"${escapeQuotedIdentifier(col)}"`).join(", ");
|
|
1034
|
+
const quotedKeys = keys.map((key) => `"${escapeQuotedIdentifier(key)}"`).join(", ");
|
|
986
1035
|
const quotedUpdateSetParts = keys.map((key, i) => {
|
|
987
1036
|
const part = updateSetParts[i];
|
|
988
|
-
|
|
1037
|
+
const valueExpr = part.substring(part.indexOf("=") + 1).trim();
|
|
1038
|
+
return `"${escapeQuotedIdentifier(key)}" = ${valueExpr}`;
|
|
989
1039
|
});
|
|
990
1040
|
const sql = `INSERT INTO "${table}" (${quotedKeys}) VALUES (${placeholders.join(", ")}) ON CONFLICT(${conflict}) DO UPDATE SET ${quotedUpdateSetParts.join(", ")}`;
|
|
991
1041
|
try {
|
|
@@ -1101,6 +1151,7 @@ async function getDatabase$2(options) {
|
|
|
1101
1151
|
* @returns Promise resolving to boolean indicating if the table exists
|
|
1102
1152
|
*/
|
|
1103
1153
|
const tableExists = async (tableName) => {
|
|
1154
|
+
validateTableName(tableName);
|
|
1104
1155
|
try {
|
|
1105
1156
|
await connection.runAndReadAll(`SELECT * FROM ${tableName} LIMIT 1`);
|
|
1106
1157
|
return true;
|
|
@@ -1124,6 +1175,7 @@ async function getDatabase$2(options) {
|
|
|
1124
1175
|
* @param dataDir - Directory to write files
|
|
1125
1176
|
*/
|
|
1126
1177
|
const exportTableToJSON = async (connection, table, dataDir) => {
|
|
1178
|
+
validateTableName(table);
|
|
1127
1179
|
const filePath = join(dataDir, `${table}.json`);
|
|
1128
1180
|
const schemaPath = join(dataDir, `${table}.schema.sql`);
|
|
1129
1181
|
try {
|
|
@@ -1199,7 +1251,7 @@ async function getDatabase$2(options) {
|
|
|
1199
1251
|
const { constants } = await import("node:fs");
|
|
1200
1252
|
await access(filePath, constants.F_OK);
|
|
1201
1253
|
console.log(`[json-adapter] Inferring schema for ${tableName} from JSON file`);
|
|
1202
|
-
await connection.run(`CREATE TABLE "${tableName}" AS SELECT * FROM read_json_auto('${filePath}')`);
|
|
1254
|
+
await connection.run(`CREATE TABLE "${tableName}" AS SELECT * FROM read_json_auto('${escapeStringLiteral(filePath)}')`);
|
|
1203
1255
|
pendingJSONFiles.delete(tableName);
|
|
1204
1256
|
console.log(`[json-adapter] Created ${tableName} with inferred schema (no UNIQUE constraints)`);
|
|
1205
1257
|
} catch (error) {
|
|
@@ -1497,7 +1549,7 @@ async function getDatabase$2(options) {
|
|
|
1497
1549
|
* @param callback - Function to execute within transaction
|
|
1498
1550
|
* @returns Promise resolving to callback result
|
|
1499
1551
|
*/
|
|
1500
|
-
const transaction = async (callback) => {
|
|
1552
|
+
const transaction = async (callback) => connectionLock.run(async () => {
|
|
1501
1553
|
try {
|
|
1502
1554
|
await connection.run("BEGIN TRANSACTION");
|
|
1503
1555
|
const result = await callback(createTransactionScope());
|
|
@@ -1509,7 +1561,7 @@ async function getDatabase$2(options) {
|
|
|
1509
1561
|
} catch {}
|
|
1510
1562
|
throw error;
|
|
1511
1563
|
}
|
|
1512
|
-
};
|
|
1564
|
+
});
|
|
1513
1565
|
/**
|
|
1514
1566
|
* Builds the transaction-scoped interface handed to a transaction callback.
|
|
1515
1567
|
*
|
|
@@ -1558,18 +1610,30 @@ async function getDatabase$2(options) {
|
|
|
1558
1610
|
* @returns Promise resolving to a TransactionHandle
|
|
1559
1611
|
*/
|
|
1560
1612
|
const beginTransaction = async () => {
|
|
1561
|
-
await
|
|
1613
|
+
const releaseConnection = await connectionLock.acquire();
|
|
1614
|
+
try {
|
|
1615
|
+
await connection.run("BEGIN TRANSACTION");
|
|
1616
|
+
} catch (error) {
|
|
1617
|
+
releaseConnection();
|
|
1618
|
+
throw error;
|
|
1619
|
+
}
|
|
1562
1620
|
let active = true;
|
|
1563
|
-
const
|
|
1564
|
-
if (!active) throw new DatabaseError("Transaction already ended", {});
|
|
1565
|
-
await connection.run("COMMIT");
|
|
1566
|
-
active = false;
|
|
1567
|
-
};
|
|
1568
|
-
const rollback = async () => {
|
|
1621
|
+
const end = async (command) => {
|
|
1569
1622
|
if (!active) throw new DatabaseError("Transaction already ended", {});
|
|
1570
|
-
|
|
1571
|
-
|
|
1623
|
+
try {
|
|
1624
|
+
await connection.run(command);
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
try {
|
|
1627
|
+
await connection.run("ROLLBACK");
|
|
1628
|
+
} catch {}
|
|
1629
|
+
throw error;
|
|
1630
|
+
} finally {
|
|
1631
|
+
active = false;
|
|
1632
|
+
releaseConnection();
|
|
1633
|
+
}
|
|
1572
1634
|
};
|
|
1635
|
+
const commit = () => end("COMMIT");
|
|
1636
|
+
const rollback = () => end("ROLLBACK");
|
|
1573
1637
|
const isActive = () => active;
|
|
1574
1638
|
return {
|
|
1575
1639
|
...createTransactionScope(),
|
|
@@ -1818,10 +1882,36 @@ function toVectorLiteral(embedding) {
|
|
|
1818
1882
|
* @param pool - PostgreSQL connection pool
|
|
1819
1883
|
* @returns VectorCapabilities implementation
|
|
1820
1884
|
*/
|
|
1885
|
+
/**
|
|
1886
|
+
* `type` is interpolated into `USING <type>`, which cannot be parameterized and
|
|
1887
|
+
* is not an identifier the quoting helpers cover. `VectorIndexOptions.type`
|
|
1888
|
+
* constrains it at compile time only, so anything reaching this at runtime —
|
|
1889
|
+
* a JavaScript caller, or a `filter as VectorIndexOptions` on parsed JSON — has
|
|
1890
|
+
* to be checked against the values pgvector actually offers.
|
|
1891
|
+
*/
|
|
1892
|
+
var VECTOR_INDEX_TYPES = /* @__PURE__ */ new Set(["hnsw", "ivfflat"]);
|
|
1893
|
+
/**
|
|
1894
|
+
* `metric` needs the same treatment as `type`, for a less obvious reason.
|
|
1895
|
+
*
|
|
1896
|
+
* It never reaches SQL directly — `vectorOpsClass`/`vectorOperator` map it
|
|
1897
|
+
* through a closed switch with a safe default, so an unknown metric produces a
|
|
1898
|
+
* valid operator class. But it is also concatenated into the *generated index
|
|
1899
|
+
* name*, which is then interpolated into a quoted identifier, so a metric
|
|
1900
|
+
* carrying a `"` still ends that identifier. The safe-default mapping is what
|
|
1901
|
+
* makes this easy to miss: the part of the metric that looks dangerous is
|
|
1902
|
+
* handled, and the part that is actually dangerous is somewhere else.
|
|
1903
|
+
*/
|
|
1904
|
+
var VECTOR_METRICS = /* @__PURE__ */ new Set([
|
|
1905
|
+
"cosine",
|
|
1906
|
+
"l2",
|
|
1907
|
+
"ip"
|
|
1908
|
+
]);
|
|
1821
1909
|
function createVectorCapabilities(pool) {
|
|
1822
1910
|
return {
|
|
1823
1911
|
async ensureColumn(table, column, dimensions) {
|
|
1824
1912
|
if (!Number.isInteger(dimensions) || dimensions <= 0) throw new DatabaseError("Vector dimensions must be a positive integer", { dimensions });
|
|
1913
|
+
validateTableName$1(table);
|
|
1914
|
+
validateColumnName$1(column);
|
|
1825
1915
|
try {
|
|
1826
1916
|
await pool.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
1827
1917
|
await pool.query(`ALTER TABLE "${table}" ADD COLUMN IF NOT EXISTS "${column}" vector(${dimensions})`);
|
|
@@ -1835,8 +1925,22 @@ function createVectorCapabilities(pool) {
|
|
|
1835
1925
|
}
|
|
1836
1926
|
},
|
|
1837
1927
|
async ensureIndex(table, column, options) {
|
|
1928
|
+
validateTableName$1(table);
|
|
1929
|
+
validateColumnName$1(column);
|
|
1838
1930
|
const metric = options?.metric || "cosine";
|
|
1839
1931
|
const indexType = options?.type || "hnsw";
|
|
1932
|
+
if (!VECTOR_INDEX_TYPES.has(indexType)) throw new DatabaseError("Unsupported vector index type", {
|
|
1933
|
+
table,
|
|
1934
|
+
column,
|
|
1935
|
+
type: indexType,
|
|
1936
|
+
supported: [...VECTOR_INDEX_TYPES]
|
|
1937
|
+
});
|
|
1938
|
+
if (!VECTOR_METRICS.has(metric)) throw new DatabaseError("Unsupported vector metric", {
|
|
1939
|
+
table,
|
|
1940
|
+
column,
|
|
1941
|
+
metric,
|
|
1942
|
+
supported: [...VECTOR_METRICS]
|
|
1943
|
+
});
|
|
1840
1944
|
const opsClass = vectorOpsClass(metric);
|
|
1841
1945
|
const indexName = `idx_${table}_${column}_${metric}_${indexType}`;
|
|
1842
1946
|
try {
|
|
@@ -1851,7 +1955,10 @@ function createVectorCapabilities(pool) {
|
|
|
1851
1955
|
}
|
|
1852
1956
|
},
|
|
1853
1957
|
async upsertVector(table, where, column, embedding) {
|
|
1958
|
+
validateTableName$1(table);
|
|
1959
|
+
validateColumnName$1(column);
|
|
1854
1960
|
const keys = Object.keys(where);
|
|
1961
|
+
validateColumnNames(keys);
|
|
1855
1962
|
const values = Object.values(where);
|
|
1856
1963
|
const conditions = keys.map((key, i) => `"${key}" IS NOT DISTINCT FROM $${i + 1}`).join(" AND ");
|
|
1857
1964
|
const vectorParam = `$${values.length + 1}`;
|
|
@@ -1867,6 +1974,8 @@ function createVectorCapabilities(pool) {
|
|
|
1867
1974
|
}
|
|
1868
1975
|
},
|
|
1869
1976
|
async search(table, column, embedding, options) {
|
|
1977
|
+
validateTableName$1(table);
|
|
1978
|
+
validateColumnName$1(column);
|
|
1870
1979
|
const metric = options?.metric || "cosine";
|
|
1871
1980
|
const limit = options?.limit || 10;
|
|
1872
1981
|
const op = vectorOperator(metric);
|
|
@@ -2014,6 +2123,15 @@ async function createDatabase(options, cacheKey) {
|
|
|
2014
2123
|
/**
|
|
2015
2124
|
* Serializes all values in an object for database storage
|
|
2016
2125
|
*/
|
|
2126
|
+
/**
|
|
2127
|
+
* The serializer the transaction-scoped interfaces have always used: none.
|
|
2128
|
+
*
|
|
2129
|
+
* Identity, deliberately — not even the `undefined`-dropping the pool-backed
|
|
2130
|
+
* `serializeRecord` does. Those copies bound `undefined` straight through to
|
|
2131
|
+
* `pg`, which stores NULL, whereas dropping the key omits the column and lets
|
|
2132
|
+
* its default apply. Preserving that is the point of this parameter.
|
|
2133
|
+
*/
|
|
2134
|
+
const passThroughRecord = (record) => record;
|
|
2017
2135
|
const serializeRecord = (record) => {
|
|
2018
2136
|
const serialized = {};
|
|
2019
2137
|
for (const [key, value] of Object.entries(record)) {
|
|
@@ -2190,230 +2308,6 @@ async function createDatabase(options, cacheKey) {
|
|
|
2190
2308
|
return executeNullAwarePostgresUpsertInTransaction(table, conflictColumns, serializedData);
|
|
2191
2309
|
};
|
|
2192
2310
|
/**
|
|
2193
|
-
* Inserts one or more records into a table
|
|
2194
|
-
*
|
|
2195
|
-
* @param table - Table name
|
|
2196
|
-
* @param data - Single record or array of records to insert
|
|
2197
|
-
* @returns Promise resolving to operation result
|
|
2198
|
-
* @throws Error if the insert operation fails
|
|
2199
|
-
*
|
|
2200
|
-
* @example Single record insert:
|
|
2201
|
-
* ```typescript
|
|
2202
|
-
* await db.insert('users', {
|
|
2203
|
-
* name: 'John Doe',
|
|
2204
|
-
* email: 'john@example.com'
|
|
2205
|
-
* });
|
|
2206
|
-
* ```
|
|
2207
|
-
*
|
|
2208
|
-
* @example Multiple record insert:
|
|
2209
|
-
* ```typescript
|
|
2210
|
-
* await db.insert('users', [
|
|
2211
|
-
* { name: 'John', email: 'john@example.com' },
|
|
2212
|
-
* { name: 'Jane', email: 'jane@example.com' }
|
|
2213
|
-
* ]);
|
|
2214
|
-
* ```
|
|
2215
|
-
*/
|
|
2216
|
-
const insert = async (table, data) => {
|
|
2217
|
-
if (Array.isArray(data)) {
|
|
2218
|
-
const serializedRecords = data.map((record) => serializeRecord(record));
|
|
2219
|
-
const keys = Object.keys(serializedRecords[0]);
|
|
2220
|
-
const placeholders = serializedRecords.map((_, i) => `(${keys.map((_, j) => `$${i * keys.length + j + 1}`).join(", ")})`).join(", ");
|
|
2221
|
-
const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
|
|
2222
|
-
const values = serializedRecords.reduce((acc, row) => acc.concat(Object.values(row)), []);
|
|
2223
|
-
return {
|
|
2224
|
-
operation: "insert",
|
|
2225
|
-
affected: (await client.query(query, values)).rowCount ?? 0
|
|
2226
|
-
};
|
|
2227
|
-
}
|
|
2228
|
-
const serializedData = serializeRecord(data);
|
|
2229
|
-
const keys = Object.keys(serializedData);
|
|
2230
|
-
const values = Object.values(serializedData);
|
|
2231
|
-
const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
|
|
2232
|
-
const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
|
|
2233
|
-
return {
|
|
2234
|
-
operation: "insert",
|
|
2235
|
-
affected: (await client.query(query, values)).rowCount ?? 0
|
|
2236
|
-
};
|
|
2237
|
-
};
|
|
2238
|
-
/**
|
|
2239
|
-
* Retrieves a single record matching the where criteria
|
|
2240
|
-
*
|
|
2241
|
-
* @param table - Table name
|
|
2242
|
-
* @param where - Criteria to match records
|
|
2243
|
-
* @returns Promise resolving to query result
|
|
2244
|
-
*/
|
|
2245
|
-
const get = async (table, where) => {
|
|
2246
|
-
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2247
|
-
if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
|
|
2248
|
-
const query = `SELECT * FROM ${table} ${whereClause}`;
|
|
2249
|
-
try {
|
|
2250
|
-
return (await client.query(query, values)).rows[0] || null;
|
|
2251
|
-
} catch (e) {
|
|
2252
|
-
throw new DatabaseError("Failed to retrieve record from table", {
|
|
2253
|
-
table,
|
|
2254
|
-
sql: query,
|
|
2255
|
-
values,
|
|
2256
|
-
originalError: formatDbError(e)
|
|
2257
|
-
});
|
|
2258
|
-
}
|
|
2259
|
-
};
|
|
2260
|
-
/**
|
|
2261
|
-
* Retrieves multiple records matching the where criteria
|
|
2262
|
-
*
|
|
2263
|
-
* @param table - Table name
|
|
2264
|
-
* @param where - Criteria to match records
|
|
2265
|
-
* @returns Promise resolving to array of records
|
|
2266
|
-
*/
|
|
2267
|
-
const list = async (table, where) => {
|
|
2268
|
-
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2269
|
-
const query = `SELECT * FROM ${table} ${whereClause}`;
|
|
2270
|
-
try {
|
|
2271
|
-
return (await client.query(query, values)).rows;
|
|
2272
|
-
} catch (e) {
|
|
2273
|
-
throw new DatabaseError("Failed to list records from table", {
|
|
2274
|
-
table,
|
|
2275
|
-
sql: query,
|
|
2276
|
-
values,
|
|
2277
|
-
originalError: formatDbError(e)
|
|
2278
|
-
});
|
|
2279
|
-
}
|
|
2280
|
-
};
|
|
2281
|
-
/**
|
|
2282
|
-
* Updates records matching the where criteria
|
|
2283
|
-
*
|
|
2284
|
-
* @param table - Table name
|
|
2285
|
-
* @param where - Criteria to match records to update
|
|
2286
|
-
* @param data - New data to set
|
|
2287
|
-
* @returns Promise resolving to operation result
|
|
2288
|
-
*/
|
|
2289
|
-
const update = async (table, where, data) => {
|
|
2290
|
-
const serializedData = serializeRecord(data);
|
|
2291
|
-
const keys = Object.keys(serializedData);
|
|
2292
|
-
const values = Object.values(serializedData);
|
|
2293
|
-
const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
|
|
2294
|
-
const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "postgres");
|
|
2295
|
-
if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
|
|
2296
|
-
const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
|
|
2297
|
-
try {
|
|
2298
|
-
return {
|
|
2299
|
-
operation: "update",
|
|
2300
|
-
affected: (await client.query(sql, [...values, ...whereValues])).rowCount ?? 0
|
|
2301
|
-
};
|
|
2302
|
-
} catch (e) {
|
|
2303
|
-
throw new DatabaseError("Failed to update records in table", {
|
|
2304
|
-
table,
|
|
2305
|
-
sql,
|
|
2306
|
-
values: [...values, ...whereValues],
|
|
2307
|
-
originalError: formatDbError(e)
|
|
2308
|
-
});
|
|
2309
|
-
}
|
|
2310
|
-
};
|
|
2311
|
-
/**
|
|
2312
|
-
* Inserts a record or updates it if it already exists (UPSERT)
|
|
2313
|
-
*
|
|
2314
|
-
* @param table - Table name
|
|
2315
|
-
* @param conflictColumns - Columns that define the uniqueness constraint
|
|
2316
|
-
* @param data - Data to insert or update
|
|
2317
|
-
* @returns Promise resolving to operation result
|
|
2318
|
-
* @throws Error if the upsert operation fails
|
|
2319
|
-
*/
|
|
2320
|
-
const upsert = async (table, conflictColumns, data, options) => {
|
|
2321
|
-
try {
|
|
2322
|
-
return await executePostgresUpsert(client, table, conflictColumns, data, options, true);
|
|
2323
|
-
} catch (e) {
|
|
2324
|
-
if (e instanceof DatabaseError && e.message === "Conflict columns missing from data") throw e;
|
|
2325
|
-
throw new DatabaseError("Failed to upsert record into table", {
|
|
2326
|
-
table,
|
|
2327
|
-
values: data,
|
|
2328
|
-
conflictColumns,
|
|
2329
|
-
originalError: formatDbError(e)
|
|
2330
|
-
});
|
|
2331
|
-
}
|
|
2332
|
-
};
|
|
2333
|
-
/**
|
|
2334
|
-
* Gets a record matching the where criteria or inserts it if not found
|
|
2335
|
-
*
|
|
2336
|
-
* @param table - Table name
|
|
2337
|
-
* @param where - Criteria to match existing record
|
|
2338
|
-
* @returns Promise resolving to the query result or insert result
|
|
2339
|
-
*/
|
|
2340
|
-
const getOrInsert = async (table, where, data) => {
|
|
2341
|
-
const result = await get(table, where);
|
|
2342
|
-
if (result) return result;
|
|
2343
|
-
await insert(table, data);
|
|
2344
|
-
const inserted = await get(table, where);
|
|
2345
|
-
if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
|
|
2346
|
-
table,
|
|
2347
|
-
where,
|
|
2348
|
-
data
|
|
2349
|
-
});
|
|
2350
|
-
return inserted;
|
|
2351
|
-
};
|
|
2352
|
-
/**
|
|
2353
|
-
* Deletes records from a table matching the where criteria
|
|
2354
|
-
*
|
|
2355
|
-
* @param table - Table name
|
|
2356
|
-
* @param where - Criteria to match records for deletion
|
|
2357
|
-
* @returns Promise resolving to operation result with count of deleted rows
|
|
2358
|
-
* @throws Error if the delete operation fails
|
|
2359
|
-
*/
|
|
2360
|
-
const deleteRecords = async (table, where) => {
|
|
2361
|
-
validateTableName$1(table);
|
|
2362
|
-
if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
|
|
2363
|
-
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2364
|
-
try {
|
|
2365
|
-
return {
|
|
2366
|
-
operation: "delete",
|
|
2367
|
-
affected: (await client.query(`DELETE FROM ${table} ${whereClause}`, values)).rowCount ?? 0
|
|
2368
|
-
};
|
|
2369
|
-
} catch (e) {
|
|
2370
|
-
throw new DatabaseError("Failed to delete records from table", {
|
|
2371
|
-
table,
|
|
2372
|
-
where,
|
|
2373
|
-
originalError: formatDbError(e)
|
|
2374
|
-
});
|
|
2375
|
-
}
|
|
2376
|
-
};
|
|
2377
|
-
/**
|
|
2378
|
-
* Counts records in a table matching the where criteria
|
|
2379
|
-
*
|
|
2380
|
-
* @param table - Table name
|
|
2381
|
-
* @param where - Criteria to match records (optional, counts all if omitted)
|
|
2382
|
-
* @returns Promise resolving to count of matching records
|
|
2383
|
-
* @throws Error if the count operation fails
|
|
2384
|
-
*/
|
|
2385
|
-
const count = async (table, where) => {
|
|
2386
|
-
validateTableName$1(table);
|
|
2387
|
-
try {
|
|
2388
|
-
if (!where || Object.keys(where).length === 0) {
|
|
2389
|
-
const result = await client.query(`SELECT COUNT(*) as count FROM ${table}`);
|
|
2390
|
-
return Number(result.rows[0]?.count) || 0;
|
|
2391
|
-
}
|
|
2392
|
-
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2393
|
-
const result = await client.query(`SELECT COUNT(*) as count FROM ${table} ${whereClause}`, values);
|
|
2394
|
-
return Number(result.rows[0]?.count) || 0;
|
|
2395
|
-
} catch (e) {
|
|
2396
|
-
throw new DatabaseError("Failed to count records in table", {
|
|
2397
|
-
table,
|
|
2398
|
-
where,
|
|
2399
|
-
originalError: formatDbError(e)
|
|
2400
|
-
});
|
|
2401
|
-
}
|
|
2402
|
-
};
|
|
2403
|
-
/**
|
|
2404
|
-
* Creates a table-specific interface for simplified table operations
|
|
2405
|
-
*
|
|
2406
|
-
* @param tableName - Table name
|
|
2407
|
-
* @returns TableMethods interface for the specified table
|
|
2408
|
-
*/
|
|
2409
|
-
const table = (tableName) => {
|
|
2410
|
-
return {
|
|
2411
|
-
insert: (data) => insert(tableName, data),
|
|
2412
|
-
get: (data) => get(tableName, data),
|
|
2413
|
-
list: (data) => list(tableName, data)
|
|
2414
|
-
};
|
|
2415
|
-
};
|
|
2416
|
-
/**
|
|
2417
2311
|
* Parses a tagged template literal into a SQL query and values
|
|
2418
2312
|
*
|
|
2419
2313
|
* @param strings - Template strings
|
|
@@ -2433,111 +2327,533 @@ async function createDatabase(options, cacheKey) {
|
|
|
2433
2327
|
};
|
|
2434
2328
|
};
|
|
2435
2329
|
/**
|
|
2436
|
-
*
|
|
2330
|
+
* Builds the complete set of query methods bound to one PostgreSQL client.
|
|
2437
2331
|
*
|
|
2438
|
-
*
|
|
2439
|
-
*
|
|
2440
|
-
*
|
|
2441
|
-
|
|
2442
|
-
const pluck = async (strings, ...vars) => {
|
|
2443
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2444
|
-
try {
|
|
2445
|
-
const firstRow = (await client.query(sql, values)).rows[0];
|
|
2446
|
-
if (!firstRow) return null;
|
|
2447
|
-
return Object.values(firstRow)[0];
|
|
2448
|
-
} catch (e) {
|
|
2449
|
-
throw new DatabaseError("Failed to execute pluck query", {
|
|
2450
|
-
sql,
|
|
2451
|
-
values,
|
|
2452
|
-
originalError: formatDbError(e)
|
|
2453
|
-
});
|
|
2454
|
-
}
|
|
2455
|
-
};
|
|
2456
|
-
/**
|
|
2457
|
-
* Executes a SQL query using template literals and returns a single row
|
|
2332
|
+
* The pool-backed interface and every transaction-scoped interface are
|
|
2333
|
+
* instantiated from this one factory, so there is exactly one implementation
|
|
2334
|
+
* of each method. The transaction interfaces used to be hand-maintained
|
|
2335
|
+
* copies of these bodies, and both had drifted:
|
|
2458
2336
|
*
|
|
2459
|
-
*
|
|
2460
|
-
*
|
|
2461
|
-
*
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
sql,
|
|
2470
|
-
values,
|
|
2471
|
-
originalError: formatDbError(e)
|
|
2472
|
-
});
|
|
2473
|
-
}
|
|
2474
|
-
};
|
|
2475
|
-
/**
|
|
2476
|
-
* Executes a SQL query using template literals and returns multiple rows
|
|
2337
|
+
* - they called the client bare, so a failure inside a transaction threw the
|
|
2338
|
+
* raw `pg` error rather than `DatabaseError`, and `instanceof` answered
|
|
2339
|
+
* differently depending on whether the call was in a transaction. `pg`
|
|
2340
|
+
* exports its own class also named `DatabaseError`, so nothing a developer
|
|
2341
|
+
* would check by eye showed the difference (#1115);
|
|
2342
|
+
* - they re-exposed `tableExists` and `syncSchema` from the enclosing scope,
|
|
2343
|
+
* which close over the *pool*. Both therefore ran on a different connection
|
|
2344
|
+
* than the transaction, so `tx.tableExists()` could not see a table created
|
|
2345
|
+
* in the same transaction and `tx.syncSchema()` committed its DDL
|
|
2346
|
+
* immediately, surviving a rollback (#1111).
|
|
2477
2347
|
*
|
|
2478
|
-
*
|
|
2479
|
-
*
|
|
2480
|
-
* @returns Promise resolving to array of result records
|
|
2481
|
-
*/
|
|
2482
|
-
const many = async (strings, ...vars) => {
|
|
2483
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2484
|
-
try {
|
|
2485
|
-
return (await client.query(sql, values)).rows;
|
|
2486
|
-
} catch (e) {
|
|
2487
|
-
throw new DatabaseError("Failed to execute many query", {
|
|
2488
|
-
sql,
|
|
2489
|
-
values,
|
|
2490
|
-
originalError: formatDbError(e)
|
|
2491
|
-
});
|
|
2492
|
-
}
|
|
2493
|
-
};
|
|
2494
|
-
/**
|
|
2495
|
-
* Executes a SQL query using template literals without returning results
|
|
2348
|
+
* Binding a client is the only difference between the two, so it is the only
|
|
2349
|
+
* thing this factory parameterizes.
|
|
2496
2350
|
*
|
|
2497
|
-
*
|
|
2498
|
-
*
|
|
2499
|
-
*
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
try {
|
|
2504
|
-
await client.query(sql, values);
|
|
2505
|
-
} catch (e) {
|
|
2506
|
-
throw new DatabaseError("Failed to execute query", {
|
|
2507
|
-
sql,
|
|
2508
|
-
values,
|
|
2509
|
-
originalError: formatDbError(e)
|
|
2510
|
-
});
|
|
2511
|
-
}
|
|
2512
|
-
};
|
|
2513
|
-
/**
|
|
2514
|
-
* Executes a raw SQL query with parameterized values
|
|
2351
|
+
* One thing is deliberately NOT unified: how `insert` and `update` serialize
|
|
2352
|
+
* values. The pool-backed methods run every value through `serializeRecord`,
|
|
2353
|
+
* which JSON-stringifies objects and arrays; the transaction-scoped copies
|
|
2354
|
+
* never did, so they handed the raw value to `pg`. That is not a cosmetic
|
|
2355
|
+
* difference — it decides what lands in the column, and no single answer is
|
|
2356
|
+
* right for both shapes:
|
|
2515
2357
|
*
|
|
2516
|
-
*
|
|
2517
|
-
*
|
|
2518
|
-
*
|
|
2358
|
+
* ```
|
|
2359
|
+
* text[] column jsonb column
|
|
2360
|
+
* raw JS array accepted "invalid input syntax for type json"
|
|
2361
|
+
* JSON string "malformed accepted
|
|
2362
|
+
* array literal"
|
|
2363
|
+
* ```
|
|
2519
2364
|
*
|
|
2520
|
-
*
|
|
2521
|
-
*
|
|
2522
|
-
*
|
|
2365
|
+
* So collapsing the two would have silently broken array-column writes inside
|
|
2366
|
+
* a transaction while fixing jsonb-array writes, or the reverse — and would
|
|
2367
|
+
* have corrupted `bytea` writes, where a Buffer would become the JSON of a
|
|
2368
|
+
* Buffer with no error anywhere. Neither belongs in a fix for the error
|
|
2369
|
+
* contract, so the serializer is a parameter: each interface keeps exactly
|
|
2370
|
+
* the behaviour it had, and the difference is stated here instead of being an
|
|
2371
|
+
* accident of two copies drifting apart.
|
|
2372
|
+
*
|
|
2373
|
+
* @param executor - Pool or checked-out client every statement runs on
|
|
2374
|
+
* @param inTransaction - Whether `executor` is already inside a transaction
|
|
2375
|
+
* @param serialize - Applied to each record before `insert`/`update` builds
|
|
2376
|
+
* its SQL. Pass-through for the transaction-scoped interfaces.
|
|
2523
2377
|
*/
|
|
2524
|
-
const
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2378
|
+
const createClientMethods = (executor, inTransaction, serialize) => {
|
|
2379
|
+
/**
|
|
2380
|
+
* Inserts one or more records into a table
|
|
2381
|
+
*
|
|
2382
|
+
* @param table - Table name
|
|
2383
|
+
* @param data - Single record or array of records to insert
|
|
2384
|
+
* @returns Promise resolving to operation result
|
|
2385
|
+
* @throws Error if the insert operation fails
|
|
2386
|
+
*
|
|
2387
|
+
* @example Single record insert:
|
|
2388
|
+
* ```typescript
|
|
2389
|
+
* await db.insert('users', {
|
|
2390
|
+
* name: 'John Doe',
|
|
2391
|
+
* email: 'john@example.com'
|
|
2392
|
+
* });
|
|
2393
|
+
* ```
|
|
2394
|
+
*
|
|
2395
|
+
* @example Multiple record insert:
|
|
2396
|
+
* ```typescript
|
|
2397
|
+
* await db.insert('users', [
|
|
2398
|
+
* { name: 'John', email: 'john@example.com' },
|
|
2399
|
+
* { name: 'Jane', email: 'jane@example.com' }
|
|
2400
|
+
* ]);
|
|
2401
|
+
* ```
|
|
2402
|
+
*/
|
|
2403
|
+
const insert = async (table, data) => {
|
|
2404
|
+
validateTableName$1(table);
|
|
2405
|
+
if (Array.isArray(data)) {
|
|
2406
|
+
if (data.length === 0) return {
|
|
2407
|
+
operation: "insert",
|
|
2408
|
+
affected: 0
|
|
2409
|
+
};
|
|
2410
|
+
const serializedRecords = data.map((record) => serialize(record));
|
|
2411
|
+
const keys = resolveInsertColumns(table, serializedRecords);
|
|
2412
|
+
validateColumnNames(keys);
|
|
2413
|
+
const placeholders = serializedRecords.map((_, i) => `(${keys.map((_, j) => `$${i * keys.length + j + 1}`).join(", ")})`).join(", ");
|
|
2414
|
+
const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
|
|
2415
|
+
const values = serializedRecords.flatMap((row) => keys.map((key) => row[key]));
|
|
2416
|
+
return {
|
|
2417
|
+
operation: "insert",
|
|
2418
|
+
affected: (await executor.query(query, values)).rowCount ?? 0
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
const serializedData = serialize(data);
|
|
2422
|
+
const keys = Object.keys(serializedData);
|
|
2423
|
+
validateColumnNames(keys);
|
|
2424
|
+
const values = Object.values(serializedData);
|
|
2425
|
+
const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
|
|
2426
|
+
const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
|
|
2528
2427
|
return {
|
|
2529
|
-
|
|
2530
|
-
|
|
2428
|
+
operation: "insert",
|
|
2429
|
+
affected: (await executor.query(query, values)).rowCount ?? 0
|
|
2531
2430
|
};
|
|
2532
|
-
}
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2431
|
+
};
|
|
2432
|
+
/**
|
|
2433
|
+
* Retrieves a single record matching the where criteria
|
|
2434
|
+
*
|
|
2435
|
+
* @param table - Table name
|
|
2436
|
+
* @param where - Criteria to match records
|
|
2437
|
+
* @returns Promise resolving to query result
|
|
2438
|
+
*/
|
|
2439
|
+
const get = async (table, where) => {
|
|
2440
|
+
validateTableName$1(table);
|
|
2441
|
+
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2442
|
+
if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
|
|
2443
|
+
const query = `SELECT * FROM ${table} ${whereClause}`;
|
|
2444
|
+
try {
|
|
2445
|
+
return (await executor.query(query, values)).rows[0] || null;
|
|
2446
|
+
} catch (e) {
|
|
2447
|
+
throw new DatabaseError("Failed to retrieve record from table", {
|
|
2448
|
+
table,
|
|
2449
|
+
sql: query,
|
|
2450
|
+
values,
|
|
2451
|
+
originalError: formatDbError(e)
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
};
|
|
2455
|
+
/**
|
|
2456
|
+
* Retrieves multiple records matching the where criteria
|
|
2457
|
+
*
|
|
2458
|
+
* @param table - Table name
|
|
2459
|
+
* @param where - Criteria to match records
|
|
2460
|
+
* @returns Promise resolving to array of records
|
|
2461
|
+
*/
|
|
2462
|
+
const list = async (table, where) => {
|
|
2463
|
+
validateTableName$1(table);
|
|
2464
|
+
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2465
|
+
const query = `SELECT * FROM ${table} ${whereClause}`;
|
|
2466
|
+
try {
|
|
2467
|
+
return (await executor.query(query, values)).rows;
|
|
2468
|
+
} catch (e) {
|
|
2469
|
+
throw new DatabaseError("Failed to list records from table", {
|
|
2470
|
+
table,
|
|
2471
|
+
sql: query,
|
|
2472
|
+
values,
|
|
2473
|
+
originalError: formatDbError(e)
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
};
|
|
2477
|
+
/**
|
|
2478
|
+
* Updates records matching the where criteria
|
|
2479
|
+
*
|
|
2480
|
+
* @param table - Table name
|
|
2481
|
+
* @param where - Criteria to match records to update
|
|
2482
|
+
* @param data - New data to set
|
|
2483
|
+
* @returns Promise resolving to operation result
|
|
2484
|
+
*/
|
|
2485
|
+
const update = async (table, where, data) => {
|
|
2486
|
+
validateTableName$1(table);
|
|
2487
|
+
const serializedData = serialize(data);
|
|
2488
|
+
const keys = Object.keys(serializedData);
|
|
2489
|
+
validateColumnNames(keys);
|
|
2490
|
+
const values = Object.values(serializedData);
|
|
2491
|
+
const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
|
|
2492
|
+
const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "postgres");
|
|
2493
|
+
if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
|
|
2494
|
+
const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
|
|
2495
|
+
try {
|
|
2496
|
+
return {
|
|
2497
|
+
operation: "update",
|
|
2498
|
+
affected: (await executor.query(sql, [...values, ...whereValues])).rowCount ?? 0
|
|
2499
|
+
};
|
|
2500
|
+
} catch (e) {
|
|
2501
|
+
throw new DatabaseError("Failed to update records in table", {
|
|
2502
|
+
table,
|
|
2503
|
+
sql,
|
|
2504
|
+
values: [...values, ...whereValues],
|
|
2505
|
+
originalError: formatDbError(e)
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
};
|
|
2509
|
+
/**
|
|
2510
|
+
* Inserts a record or updates it if it already exists (UPSERT)
|
|
2511
|
+
*
|
|
2512
|
+
* @param table - Table name
|
|
2513
|
+
* @param conflictColumns - Columns that define the uniqueness constraint
|
|
2514
|
+
* @param data - Data to insert or update
|
|
2515
|
+
* @returns Promise resolving to operation result
|
|
2516
|
+
* @throws Error if the upsert operation fails
|
|
2517
|
+
*/
|
|
2518
|
+
const upsert = async (table, conflictColumns, data, options) => {
|
|
2519
|
+
validateTableName$1(table);
|
|
2520
|
+
validateColumnNames([...conflictColumns, ...Object.keys(data)]);
|
|
2521
|
+
try {
|
|
2522
|
+
return await executePostgresUpsert(executor, table, conflictColumns, data, options, !inTransaction);
|
|
2523
|
+
} catch (e) {
|
|
2524
|
+
if (e instanceof DatabaseError && e.message === "Conflict columns missing from data") throw e;
|
|
2525
|
+
throw new DatabaseError("Failed to upsert record into table", {
|
|
2526
|
+
table,
|
|
2527
|
+
values: data,
|
|
2528
|
+
conflictColumns,
|
|
2529
|
+
originalError: formatDbError(e)
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
};
|
|
2533
|
+
/**
|
|
2534
|
+
* Gets a record matching the where criteria or inserts it if not found
|
|
2535
|
+
*
|
|
2536
|
+
* @param table - Table name
|
|
2537
|
+
* @param where - Criteria to match existing record
|
|
2538
|
+
* @returns Promise resolving to the query result or insert result
|
|
2539
|
+
*/
|
|
2540
|
+
const getOrInsert = async (table, where, data) => {
|
|
2541
|
+
validateTableName$1(table);
|
|
2542
|
+
const result = await get(table, where);
|
|
2543
|
+
if (result) return result;
|
|
2544
|
+
await insert(table, data);
|
|
2545
|
+
const inserted = await get(table, where);
|
|
2546
|
+
if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
|
|
2547
|
+
table,
|
|
2548
|
+
where,
|
|
2549
|
+
data
|
|
2550
|
+
});
|
|
2551
|
+
return inserted;
|
|
2552
|
+
};
|
|
2553
|
+
/**
|
|
2554
|
+
* Deletes records from a table matching the where criteria
|
|
2555
|
+
*
|
|
2556
|
+
* @param table - Table name
|
|
2557
|
+
* @param where - Criteria to match records for deletion
|
|
2558
|
+
* @returns Promise resolving to operation result with count of deleted rows
|
|
2559
|
+
* @throws Error if the delete operation fails
|
|
2560
|
+
*/
|
|
2561
|
+
const deleteRecords = async (table, where) => {
|
|
2562
|
+
validateTableName$1(table);
|
|
2563
|
+
if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
|
|
2564
|
+
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2565
|
+
try {
|
|
2566
|
+
return {
|
|
2567
|
+
operation: "delete",
|
|
2568
|
+
affected: (await executor.query(`DELETE FROM ${table} ${whereClause}`, values)).rowCount ?? 0
|
|
2569
|
+
};
|
|
2570
|
+
} catch (e) {
|
|
2571
|
+
throw new DatabaseError("Failed to delete records from table", {
|
|
2572
|
+
table,
|
|
2573
|
+
where,
|
|
2574
|
+
originalError: formatDbError(e)
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
/**
|
|
2579
|
+
* Counts records in a table matching the where criteria
|
|
2580
|
+
*
|
|
2581
|
+
* @param table - Table name
|
|
2582
|
+
* @param where - Criteria to match records (optional, counts all if omitted)
|
|
2583
|
+
* @returns Promise resolving to count of matching records
|
|
2584
|
+
* @throws Error if the count operation fails
|
|
2585
|
+
*/
|
|
2586
|
+
const count = async (table, where) => {
|
|
2587
|
+
validateTableName$1(table);
|
|
2588
|
+
try {
|
|
2589
|
+
if (!where || Object.keys(where).length === 0) {
|
|
2590
|
+
const result = await executor.query(`SELECT COUNT(*) as count FROM ${table}`);
|
|
2591
|
+
return Number(result.rows[0]?.count) || 0;
|
|
2592
|
+
}
|
|
2593
|
+
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2594
|
+
const result = await executor.query(`SELECT COUNT(*) as count FROM ${table} ${whereClause}`, values);
|
|
2595
|
+
return Number(result.rows[0]?.count) || 0;
|
|
2596
|
+
} catch (e) {
|
|
2597
|
+
throw new DatabaseError("Failed to count records in table", {
|
|
2598
|
+
table,
|
|
2599
|
+
where,
|
|
2600
|
+
originalError: formatDbError(e)
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2603
|
+
};
|
|
2604
|
+
/**
|
|
2605
|
+
* Creates a table-specific interface for simplified table operations
|
|
2606
|
+
*
|
|
2607
|
+
* @param tableName - Table name
|
|
2608
|
+
* @returns TableMethods interface for the specified table
|
|
2609
|
+
*/
|
|
2610
|
+
const table = (tableName) => {
|
|
2611
|
+
return {
|
|
2612
|
+
insert: (data) => insert(tableName, data),
|
|
2613
|
+
get: (data) => get(tableName, data),
|
|
2614
|
+
list: (data) => list(tableName, data)
|
|
2615
|
+
};
|
|
2616
|
+
};
|
|
2617
|
+
/**
|
|
2618
|
+
* Executes a SQL query using template literals and returns a single value
|
|
2619
|
+
*
|
|
2620
|
+
* @param strings - Template strings
|
|
2621
|
+
* @param vars - Variables to interpolate into the query
|
|
2622
|
+
* @returns Promise resolving to a single value (first column of first row)
|
|
2623
|
+
*/
|
|
2624
|
+
const pluck = async (strings, ...vars) => {
|
|
2625
|
+
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2626
|
+
try {
|
|
2627
|
+
const firstRow = (await executor.query(sql, values)).rows[0];
|
|
2628
|
+
if (!firstRow) return null;
|
|
2629
|
+
return Object.values(firstRow)[0];
|
|
2630
|
+
} catch (e) {
|
|
2631
|
+
throw new DatabaseError("Failed to execute pluck query", {
|
|
2632
|
+
sql,
|
|
2633
|
+
values,
|
|
2634
|
+
originalError: formatDbError(e)
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
};
|
|
2638
|
+
/**
|
|
2639
|
+
* Executes a SQL query using template literals and returns a single row
|
|
2640
|
+
*
|
|
2641
|
+
* @param strings - Template strings
|
|
2642
|
+
* @param vars - Variables to interpolate into the query
|
|
2643
|
+
* @returns Promise resolving to a single result record or null
|
|
2644
|
+
*/
|
|
2645
|
+
const single = async (strings, ...vars) => {
|
|
2646
|
+
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2647
|
+
try {
|
|
2648
|
+
return (await executor.query(sql, values)).rows[0] || null;
|
|
2649
|
+
} catch (e) {
|
|
2650
|
+
throw new DatabaseError("Failed to execute single query", {
|
|
2651
|
+
sql,
|
|
2652
|
+
values,
|
|
2653
|
+
originalError: formatDbError(e)
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
};
|
|
2657
|
+
/**
|
|
2658
|
+
* Executes a SQL query using template literals and returns multiple rows
|
|
2659
|
+
*
|
|
2660
|
+
* @param strings - Template strings
|
|
2661
|
+
* @param vars - Variables to interpolate into the query
|
|
2662
|
+
* @returns Promise resolving to array of result records
|
|
2663
|
+
*/
|
|
2664
|
+
const many = async (strings, ...vars) => {
|
|
2665
|
+
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2666
|
+
try {
|
|
2667
|
+
return (await executor.query(sql, values)).rows;
|
|
2668
|
+
} catch (e) {
|
|
2669
|
+
throw new DatabaseError("Failed to execute many query", {
|
|
2670
|
+
sql,
|
|
2671
|
+
values,
|
|
2672
|
+
originalError: formatDbError(e)
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2675
|
+
};
|
|
2676
|
+
/**
|
|
2677
|
+
* Executes a SQL query using template literals without returning results
|
|
2678
|
+
*
|
|
2679
|
+
* @param strings - Template strings
|
|
2680
|
+
* @param vars - Variables to interpolate into the query
|
|
2681
|
+
* @returns Promise that resolves when the query completes
|
|
2682
|
+
*/
|
|
2683
|
+
const execute = async (strings, ...vars) => {
|
|
2684
|
+
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2685
|
+
try {
|
|
2686
|
+
await executor.query(sql, values);
|
|
2687
|
+
} catch (e) {
|
|
2688
|
+
throw new DatabaseError("Failed to execute query", {
|
|
2689
|
+
sql,
|
|
2690
|
+
values,
|
|
2691
|
+
originalError: formatDbError(e)
|
|
2692
|
+
});
|
|
2693
|
+
}
|
|
2694
|
+
};
|
|
2695
|
+
/**
|
|
2696
|
+
* Executes a raw SQL query with parameterized values
|
|
2697
|
+
*
|
|
2698
|
+
* Uses PostgreSQL-native placeholders ($1, $2, ...). Legacy ? placeholders
|
|
2699
|
+
* are converted only when the placeholder count matches the supplied values,
|
|
2700
|
+
* so Postgres operators such as JSONB ? remain intact.
|
|
2701
|
+
*
|
|
2702
|
+
* @param sql - SQL query string
|
|
2703
|
+
* @param values - Variables to use as parameters
|
|
2704
|
+
* @returns Promise resolving to query result with rows and count
|
|
2705
|
+
*/
|
|
2706
|
+
const query = async (sql, ...values) => {
|
|
2707
|
+
const query = normalizePostgresRawQuery(sql, values);
|
|
2708
|
+
try {
|
|
2709
|
+
const result = await executor.query(query.sql, query.values);
|
|
2710
|
+
return {
|
|
2711
|
+
rows: result.rows,
|
|
2712
|
+
rowCount: result.rowCount ?? 0
|
|
2713
|
+
};
|
|
2714
|
+
} catch (e) {
|
|
2715
|
+
throw new DatabaseError("Failed to execute raw query", {
|
|
2716
|
+
sql: query.sql,
|
|
2717
|
+
values: query.values,
|
|
2718
|
+
originalError: formatDbError(e)
|
|
2719
|
+
});
|
|
2720
|
+
}
|
|
2721
|
+
};
|
|
2722
|
+
/**
|
|
2723
|
+
* Checks if a table exists in the database
|
|
2724
|
+
*
|
|
2725
|
+
* @param tableName - Name of the table to check
|
|
2726
|
+
* @returns Promise resolving to boolean indicating if the table exists
|
|
2727
|
+
*/
|
|
2728
|
+
const tableExists = async (tableName) => {
|
|
2729
|
+
return (await executor.query(`SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1 AND table_schema = 'public')`, [tableName])).rows[0].exists;
|
|
2730
|
+
};
|
|
2731
|
+
/**
|
|
2732
|
+
* Synchronizes database schema with provided SQL DDL
|
|
2733
|
+
* Creates tables if they don't exist and adds missing columns
|
|
2734
|
+
* Also executes CREATE INDEX statements
|
|
2735
|
+
*
|
|
2736
|
+
* @param schema - SQL schema definition with CREATE TABLE and CREATE INDEX statements
|
|
2737
|
+
* @returns Promise that resolves when schema is synchronized
|
|
2738
|
+
*/
|
|
2739
|
+
/**
|
|
2740
|
+
* Runs a step whose failure `syncSchema` deliberately tolerates.
|
|
2741
|
+
*
|
|
2742
|
+
* Logging and continuing is self-contained on the pool. Inside a
|
|
2743
|
+
* transaction it is not: a failed statement puts PostgreSQL in the aborted
|
|
2744
|
+
* state, and `COMMIT` on an aborted transaction *succeeds* — it reports a
|
|
2745
|
+
* ROLLBACK command tag and no error. The caller is told the transaction
|
|
2746
|
+
* committed while every write in it was discarded, which is the silent
|
|
2747
|
+
* data loss this whole epic is about, re-entering through the schema path.
|
|
2748
|
+
*
|
|
2749
|
+
* A savepoint scopes the failure to the step, which is what "log it and
|
|
2750
|
+
* continue" was always supposed to mean.
|
|
2751
|
+
*/
|
|
2752
|
+
const runTolerated = async (work) => {
|
|
2753
|
+
if (!inTransaction) return work();
|
|
2754
|
+
savepointSequence += 1;
|
|
2755
|
+
const name = `hv_sync_${savepointSequence}`;
|
|
2756
|
+
await executor.query(`SAVEPOINT ${name}`);
|
|
2757
|
+
try {
|
|
2758
|
+
const result = await work();
|
|
2759
|
+
await executor.query(`RELEASE SAVEPOINT ${name}`);
|
|
2760
|
+
return result;
|
|
2761
|
+
} catch (error) {
|
|
2762
|
+
await executor.query(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
2763
|
+
await executor.query(`RELEASE SAVEPOINT ${name}`);
|
|
2764
|
+
throw error;
|
|
2765
|
+
}
|
|
2766
|
+
};
|
|
2767
|
+
const syncSchema = async (schema) => {
|
|
2768
|
+
const commands = schema.trim().split(";").filter((command) => command.trim() !== "");
|
|
2769
|
+
const createIndexRegex = /CREATE (UNIQUE )?INDEX (IF NOT EXISTS )?"?(\w+)"? ON "?(\w+)"?\s*\(([^)]+)\)/i;
|
|
2770
|
+
const indexNames = [];
|
|
2771
|
+
for (const command of commands) {
|
|
2772
|
+
const indexMatch = command.trim().match(createIndexRegex);
|
|
2773
|
+
if (indexMatch) indexNames.push(indexMatch[3]);
|
|
2774
|
+
}
|
|
2775
|
+
const existingIndexes = /* @__PURE__ */ new Set();
|
|
2776
|
+
if (indexNames.length > 0) {
|
|
2777
|
+
const result = await executor.query(`SELECT indexname FROM pg_indexes
|
|
2778
|
+
WHERE schemaname = 'public' AND indexname = ANY($1::text[])`, [indexNames]);
|
|
2779
|
+
for (const row of result.rows) existingIndexes.add(row.indexname);
|
|
2780
|
+
}
|
|
2781
|
+
for (const command of commands) {
|
|
2782
|
+
const trimmedCommand = command.trim();
|
|
2783
|
+
const tableMatch = trimmedCommand.match(/CREATE TABLE (IF NOT EXISTS )?"?(\w+)"? \(([\s\S]+)\)/i);
|
|
2784
|
+
if (tableMatch) {
|
|
2785
|
+
const tableName = tableMatch[2];
|
|
2786
|
+
const columns = tableMatch[3].trim().split(",\n");
|
|
2787
|
+
if (!await tableExists(tableName)) await executor.query(trimmedCommand);
|
|
2788
|
+
else for (const column of columns) {
|
|
2789
|
+
const columnDef = column.trim();
|
|
2790
|
+
const columnMatch = columnDef.match(/"?(\w+)"?\s+(\w+[^,]*)/);
|
|
2791
|
+
if (columnMatch) {
|
|
2792
|
+
const columnName = columnMatch[1];
|
|
2793
|
+
if (columnName.toUpperCase() === "PRIMARY" || columnName.toUpperCase() === "FOREIGN" || columnName.toUpperCase() === "UNIQUE" || columnName.toUpperCase() === "CHECK" || columnName.toUpperCase() === "CONSTRAINT") continue;
|
|
2794
|
+
try {
|
|
2795
|
+
await runTolerated(async () => {
|
|
2796
|
+
if (!(await executor.query(`SELECT EXISTS (
|
|
2797
|
+
SELECT 1 FROM information_schema.columns
|
|
2798
|
+
WHERE table_name = $1
|
|
2799
|
+
AND column_name = $2
|
|
2800
|
+
AND table_schema = 'public'
|
|
2801
|
+
)`, [tableName, columnName])).rows[0].exists) {
|
|
2802
|
+
const alterCommand = `ALTER TABLE "${tableName}" ADD COLUMN ${columnDef}`;
|
|
2803
|
+
await executor.query(alterCommand);
|
|
2804
|
+
}
|
|
2805
|
+
});
|
|
2806
|
+
} catch (error) {
|
|
2807
|
+
console.error(`Error adding column ${columnName} to ${tableName}:`, error);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
continue;
|
|
2812
|
+
}
|
|
2813
|
+
const indexMatch = trimmedCommand.match(createIndexRegex);
|
|
2814
|
+
if (indexMatch) {
|
|
2815
|
+
const indexName = indexMatch[3];
|
|
2816
|
+
const indexTableName = indexMatch[4];
|
|
2817
|
+
if (!existingIndexes.has(indexName)) try {
|
|
2818
|
+
await runTolerated(() => executor.query(trimmedCommand));
|
|
2819
|
+
existingIndexes.add(indexName);
|
|
2820
|
+
} catch (error) {
|
|
2821
|
+
console.warn(`Warning: Failed to create index ${indexName} on ${indexTableName}:`, error instanceof Error ? error.message : String(error));
|
|
2822
|
+
}
|
|
2823
|
+
continue;
|
|
2824
|
+
}
|
|
2825
|
+
if (trimmedCommand.toUpperCase().startsWith("CREATE ") && !trimmedCommand.toUpperCase().includes("CREATE TABLE")) try {
|
|
2826
|
+
await runTolerated(() => executor.query(trimmedCommand));
|
|
2827
|
+
} catch (error) {
|
|
2828
|
+
console.warn(`Warning: DDL statement may have failed:`, error instanceof Error ? error.message : String(error));
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
};
|
|
2832
|
+
return {
|
|
2833
|
+
insert,
|
|
2834
|
+
get,
|
|
2835
|
+
list,
|
|
2836
|
+
update,
|
|
2837
|
+
upsert,
|
|
2838
|
+
getOrInsert,
|
|
2839
|
+
delete: deleteRecords,
|
|
2840
|
+
count,
|
|
2841
|
+
table,
|
|
2842
|
+
many,
|
|
2843
|
+
single,
|
|
2844
|
+
pluck,
|
|
2845
|
+
execute,
|
|
2846
|
+
query,
|
|
2847
|
+
oo: many,
|
|
2848
|
+
oO: single,
|
|
2849
|
+
ox: pluck,
|
|
2850
|
+
xx: execute,
|
|
2851
|
+
tableExists,
|
|
2852
|
+
syncSchema
|
|
2853
|
+
};
|
|
2854
|
+
};
|
|
2855
|
+
const { insert, get, list, update, upsert, getOrInsert, delete: deleteRecords, count, table, many, single, pluck, execute, query, oo, oO, ox, xx, tableExists, syncSchema } = createClientMethods(client, false, serializeRecord);
|
|
2856
|
+
/**
|
|
2541
2857
|
* Acquire a pinned connection from the pool for session-scoped state such as
|
|
2542
2858
|
* `pg_advisory_lock`. Every query on the returned handle runs on the same
|
|
2543
2859
|
* client until {@link SessionHandle.release} drops the connection (which
|
|
@@ -2586,86 +2902,6 @@ async function createDatabase(options, cacheKey) {
|
|
|
2586
2902
|
}
|
|
2587
2903
|
};
|
|
2588
2904
|
};
|
|
2589
|
-
/**
|
|
2590
|
-
* Checks if a table exists in the database
|
|
2591
|
-
*
|
|
2592
|
-
* @param tableName - Name of the table to check
|
|
2593
|
-
* @returns Promise resolving to boolean indicating if the table exists
|
|
2594
|
-
*/
|
|
2595
|
-
const tableExists = async (tableName) => {
|
|
2596
|
-
return (await client.query(`SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1 AND table_schema = 'public')`, [tableName])).rows[0].exists;
|
|
2597
|
-
};
|
|
2598
|
-
/**
|
|
2599
|
-
* Synchronizes database schema with provided SQL DDL
|
|
2600
|
-
* Creates tables if they don't exist and adds missing columns
|
|
2601
|
-
* Also executes CREATE INDEX statements
|
|
2602
|
-
*
|
|
2603
|
-
* @param schema - SQL schema definition with CREATE TABLE and CREATE INDEX statements
|
|
2604
|
-
* @returns Promise that resolves when schema is synchronized
|
|
2605
|
-
*/
|
|
2606
|
-
const syncSchema = async (schema) => {
|
|
2607
|
-
const commands = schema.trim().split(";").filter((command) => command.trim() !== "");
|
|
2608
|
-
const createIndexRegex = /CREATE (UNIQUE )?INDEX (IF NOT EXISTS )?"?(\w+)"? ON "?(\w+)"?\s*\(([^)]+)\)/i;
|
|
2609
|
-
const indexNames = [];
|
|
2610
|
-
for (const command of commands) {
|
|
2611
|
-
const indexMatch = command.trim().match(createIndexRegex);
|
|
2612
|
-
if (indexMatch) indexNames.push(indexMatch[3]);
|
|
2613
|
-
}
|
|
2614
|
-
const existingIndexes = /* @__PURE__ */ new Set();
|
|
2615
|
-
if (indexNames.length > 0) {
|
|
2616
|
-
const result = await client.query(`SELECT indexname FROM pg_indexes
|
|
2617
|
-
WHERE schemaname = 'public' AND indexname = ANY($1::text[])`, [indexNames]);
|
|
2618
|
-
for (const row of result.rows) existingIndexes.add(row.indexname);
|
|
2619
|
-
}
|
|
2620
|
-
for (const command of commands) {
|
|
2621
|
-
const trimmedCommand = command.trim();
|
|
2622
|
-
const tableMatch = trimmedCommand.match(/CREATE TABLE (IF NOT EXISTS )?"?(\w+)"? \(([\s\S]+)\)/i);
|
|
2623
|
-
if (tableMatch) {
|
|
2624
|
-
const tableName = tableMatch[2];
|
|
2625
|
-
const columns = tableMatch[3].trim().split(",\n");
|
|
2626
|
-
if (!await tableExists(tableName)) await client.query(trimmedCommand);
|
|
2627
|
-
else for (const column of columns) {
|
|
2628
|
-
const columnDef = column.trim();
|
|
2629
|
-
const columnMatch = columnDef.match(/"?(\w+)"?\s+(\w+[^,]*)/);
|
|
2630
|
-
if (columnMatch) {
|
|
2631
|
-
const columnName = columnMatch[1];
|
|
2632
|
-
if (columnName.toUpperCase() === "PRIMARY" || columnName.toUpperCase() === "FOREIGN" || columnName.toUpperCase() === "UNIQUE" || columnName.toUpperCase() === "CHECK" || columnName.toUpperCase() === "CONSTRAINT") continue;
|
|
2633
|
-
try {
|
|
2634
|
-
if (!(await client.query(`SELECT EXISTS (
|
|
2635
|
-
SELECT 1 FROM information_schema.columns
|
|
2636
|
-
WHERE table_name = $1
|
|
2637
|
-
AND column_name = $2
|
|
2638
|
-
AND table_schema = 'public'
|
|
2639
|
-
)`, [tableName, columnName])).rows[0].exists) {
|
|
2640
|
-
const alterCommand = `ALTER TABLE "${tableName}" ADD COLUMN ${columnDef}`;
|
|
2641
|
-
await client.query(alterCommand);
|
|
2642
|
-
}
|
|
2643
|
-
} catch (error) {
|
|
2644
|
-
console.error(`Error adding column ${columnName} to ${tableName}:`, error);
|
|
2645
|
-
}
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
continue;
|
|
2649
|
-
}
|
|
2650
|
-
const indexMatch = trimmedCommand.match(createIndexRegex);
|
|
2651
|
-
if (indexMatch) {
|
|
2652
|
-
const indexName = indexMatch[3];
|
|
2653
|
-
const indexTableName = indexMatch[4];
|
|
2654
|
-
if (!existingIndexes.has(indexName)) try {
|
|
2655
|
-
await client.query(trimmedCommand);
|
|
2656
|
-
existingIndexes.add(indexName);
|
|
2657
|
-
} catch (error) {
|
|
2658
|
-
console.warn(`Warning: Failed to create index ${indexName} on ${indexTableName}:`, error instanceof Error ? error.message : String(error));
|
|
2659
|
-
}
|
|
2660
|
-
continue;
|
|
2661
|
-
}
|
|
2662
|
-
if (trimmedCommand.toUpperCase().startsWith("CREATE ") && !trimmedCommand.toUpperCase().includes("CREATE TABLE")) try {
|
|
2663
|
-
await client.query(trimmedCommand);
|
|
2664
|
-
} catch (error) {
|
|
2665
|
-
console.warn(`Warning: DDL statement may have failed:`, error instanceof Error ? error.message : String(error));
|
|
2666
|
-
}
|
|
2667
|
-
}
|
|
2668
|
-
};
|
|
2669
2905
|
let savepointSequence = 0;
|
|
2670
2906
|
/**
|
|
2671
2907
|
* Builds the `transaction` exposed on a transaction-scoped interface.
|
|
@@ -2715,132 +2951,7 @@ async function createDatabase(options, cacheKey) {
|
|
|
2715
2951
|
const txDb = {
|
|
2716
2952
|
url,
|
|
2717
2953
|
client: txClient,
|
|
2718
|
-
|
|
2719
|
-
if (Array.isArray(data)) {
|
|
2720
|
-
const keys = Object.keys(data[0]);
|
|
2721
|
-
const placeholders = data.map((_, i) => `(${keys.map((_, j) => `$${i * keys.length + j + 1}`).join(", ")})`).join(", ");
|
|
2722
|
-
const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
|
|
2723
|
-
const values = data.reduce((acc, row) => acc.concat(Object.values(row)), []);
|
|
2724
|
-
return {
|
|
2725
|
-
operation: "insert",
|
|
2726
|
-
affected: (await txClient.query(query, values)).rowCount ?? 0
|
|
2727
|
-
};
|
|
2728
|
-
}
|
|
2729
|
-
const keys = Object.keys(data);
|
|
2730
|
-
const values = Object.values(data);
|
|
2731
|
-
const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
|
|
2732
|
-
const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
|
|
2733
|
-
return {
|
|
2734
|
-
operation: "insert",
|
|
2735
|
-
affected: (await txClient.query(query, values)).rowCount ?? 0
|
|
2736
|
-
};
|
|
2737
|
-
},
|
|
2738
|
-
get: async (table, where) => {
|
|
2739
|
-
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2740
|
-
if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
|
|
2741
|
-
const query = `SELECT * FROM ${table} ${whereClause}`;
|
|
2742
|
-
return (await txClient.query(query, values)).rows[0] || null;
|
|
2743
|
-
},
|
|
2744
|
-
list: async (table, where) => {
|
|
2745
|
-
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2746
|
-
const query = `SELECT * FROM ${table} ${whereClause}`;
|
|
2747
|
-
return (await txClient.query(query, values)).rows;
|
|
2748
|
-
},
|
|
2749
|
-
update: async (table, where, data) => {
|
|
2750
|
-
const keys = Object.keys(data);
|
|
2751
|
-
const values = Object.values(data);
|
|
2752
|
-
const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
|
|
2753
|
-
const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "postgres");
|
|
2754
|
-
if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
|
|
2755
|
-
const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
|
|
2756
|
-
return {
|
|
2757
|
-
operation: "update",
|
|
2758
|
-
affected: (await txClient.query(sql, [...values, ...whereValues])).rowCount ?? 0
|
|
2759
|
-
};
|
|
2760
|
-
},
|
|
2761
|
-
upsert: async (table, conflictColumns, data, options) => executePostgresUpsert(txClient, table, conflictColumns, data, options, false),
|
|
2762
|
-
getOrInsert: async (table, where, data) => {
|
|
2763
|
-
const result = await txDb.get(table, where);
|
|
2764
|
-
if (result) return result;
|
|
2765
|
-
await txDb.insert(table, data);
|
|
2766
|
-
const inserted = await txDb.get(table, where);
|
|
2767
|
-
if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
|
|
2768
|
-
table,
|
|
2769
|
-
where,
|
|
2770
|
-
data
|
|
2771
|
-
});
|
|
2772
|
-
return inserted;
|
|
2773
|
-
},
|
|
2774
|
-
delete: async (table, where) => {
|
|
2775
|
-
validateTableName$1(table);
|
|
2776
|
-
if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
|
|
2777
|
-
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2778
|
-
return {
|
|
2779
|
-
operation: "delete",
|
|
2780
|
-
affected: (await txClient.query(`DELETE FROM ${table} ${whereClause}`, values)).rowCount ?? 0
|
|
2781
|
-
};
|
|
2782
|
-
},
|
|
2783
|
-
count: async (table, where) => {
|
|
2784
|
-
validateTableName$1(table);
|
|
2785
|
-
if (!where || Object.keys(where).length === 0) {
|
|
2786
|
-
const result = await txClient.query(`SELECT COUNT(*) as count FROM ${table}`);
|
|
2787
|
-
return Number(result.rows[0]?.count) || 0;
|
|
2788
|
-
}
|
|
2789
|
-
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2790
|
-
const result = await txClient.query(`SELECT COUNT(*) as count FROM ${table} ${whereClause}`, values);
|
|
2791
|
-
return Number(result.rows[0]?.count) || 0;
|
|
2792
|
-
},
|
|
2793
|
-
table: (tableName) => ({
|
|
2794
|
-
insert: (data) => txDb.insert(tableName, data),
|
|
2795
|
-
get: (data) => txDb.get(tableName, data),
|
|
2796
|
-
list: (data) => txDb.list(tableName, data)
|
|
2797
|
-
}),
|
|
2798
|
-
many: async (strings, ...vars) => {
|
|
2799
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2800
|
-
return (await txClient.query(sql, values)).rows;
|
|
2801
|
-
},
|
|
2802
|
-
single: async (strings, ...vars) => {
|
|
2803
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2804
|
-
return (await txClient.query(sql, values)).rows[0] || null;
|
|
2805
|
-
},
|
|
2806
|
-
pluck: async (strings, ...vars) => {
|
|
2807
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2808
|
-
const firstRow = (await txClient.query(sql, values)).rows[0];
|
|
2809
|
-
if (!firstRow) return null;
|
|
2810
|
-
return Object.values(firstRow)[0];
|
|
2811
|
-
},
|
|
2812
|
-
execute: async (strings, ...vars) => {
|
|
2813
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2814
|
-
await txClient.query(sql, values);
|
|
2815
|
-
},
|
|
2816
|
-
query: async (sql, ...values) => {
|
|
2817
|
-
const query = normalizePostgresRawQuery(sql, values);
|
|
2818
|
-
const result = await txClient.query(query.sql, query.values);
|
|
2819
|
-
return {
|
|
2820
|
-
rows: result.rows,
|
|
2821
|
-
rowCount: result.rowCount ?? 0
|
|
2822
|
-
};
|
|
2823
|
-
},
|
|
2824
|
-
oo: async (strings, ...vars) => {
|
|
2825
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2826
|
-
return (await txClient.query(sql, values)).rows;
|
|
2827
|
-
},
|
|
2828
|
-
oO: async (strings, ...vars) => {
|
|
2829
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2830
|
-
return (await txClient.query(sql, values)).rows[0] || null;
|
|
2831
|
-
},
|
|
2832
|
-
ox: async (strings, ...vars) => {
|
|
2833
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2834
|
-
const firstRow = (await txClient.query(sql, values)).rows[0];
|
|
2835
|
-
if (!firstRow) return null;
|
|
2836
|
-
return Object.values(firstRow)[0];
|
|
2837
|
-
},
|
|
2838
|
-
xx: async (strings, ...vars) => {
|
|
2839
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2840
|
-
await txClient.query(sql, values);
|
|
2841
|
-
},
|
|
2842
|
-
tableExists,
|
|
2843
|
-
syncSchema,
|
|
2954
|
+
...createClientMethods(txClient, true, passThroughRecord),
|
|
2844
2955
|
transaction: createNestedTransaction(txClient, () => txDb)
|
|
2845
2956
|
};
|
|
2846
2957
|
const result = await callback(txDb);
|
|
@@ -2889,132 +3000,7 @@ async function createDatabase(options, cacheKey) {
|
|
|
2889
3000
|
const txHandle = {
|
|
2890
3001
|
url,
|
|
2891
3002
|
client: txClient,
|
|
2892
|
-
|
|
2893
|
-
if (Array.isArray(data)) {
|
|
2894
|
-
const keys = Object.keys(data[0]);
|
|
2895
|
-
const placeholders = data.map((_, i) => `(${keys.map((_, j) => `$${i * keys.length + j + 1}`).join(", ")})`).join(", ");
|
|
2896
|
-
const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
|
|
2897
|
-
const values = data.reduce((acc, row) => acc.concat(Object.values(row)), []);
|
|
2898
|
-
return {
|
|
2899
|
-
operation: "insert",
|
|
2900
|
-
affected: (await txClient.query(query, values)).rowCount ?? 0
|
|
2901
|
-
};
|
|
2902
|
-
}
|
|
2903
|
-
const keys = Object.keys(data);
|
|
2904
|
-
const values = Object.values(data);
|
|
2905
|
-
const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
|
|
2906
|
-
const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
|
|
2907
|
-
return {
|
|
2908
|
-
operation: "insert",
|
|
2909
|
-
affected: (await txClient.query(query, values)).rowCount ?? 0
|
|
2910
|
-
};
|
|
2911
|
-
},
|
|
2912
|
-
get: async (table, where) => {
|
|
2913
|
-
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2914
|
-
if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
|
|
2915
|
-
const query = `SELECT * FROM ${table} ${whereClause}`;
|
|
2916
|
-
return (await txClient.query(query, values)).rows[0] || null;
|
|
2917
|
-
},
|
|
2918
|
-
list: async (table, where) => {
|
|
2919
|
-
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2920
|
-
const query = `SELECT * FROM ${table} ${whereClause}`;
|
|
2921
|
-
return (await txClient.query(query, values)).rows;
|
|
2922
|
-
},
|
|
2923
|
-
update: async (table, where, data) => {
|
|
2924
|
-
const keys = Object.keys(data);
|
|
2925
|
-
const values = Object.values(data);
|
|
2926
|
-
const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
|
|
2927
|
-
const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "postgres");
|
|
2928
|
-
if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
|
|
2929
|
-
const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
|
|
2930
|
-
return {
|
|
2931
|
-
operation: "update",
|
|
2932
|
-
affected: (await txClient.query(sql, [...values, ...whereValues])).rowCount ?? 0
|
|
2933
|
-
};
|
|
2934
|
-
},
|
|
2935
|
-
upsert: async (table, conflictColumns, data, options) => executePostgresUpsert(txClient, table, conflictColumns, data, options, false),
|
|
2936
|
-
getOrInsert: async (table, where, data) => {
|
|
2937
|
-
const result = await txHandle.get(table, where);
|
|
2938
|
-
if (result) return result;
|
|
2939
|
-
await txHandle.insert(table, data);
|
|
2940
|
-
const inserted = await txHandle.get(table, where);
|
|
2941
|
-
if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
|
|
2942
|
-
table,
|
|
2943
|
-
where,
|
|
2944
|
-
data
|
|
2945
|
-
});
|
|
2946
|
-
return inserted;
|
|
2947
|
-
},
|
|
2948
|
-
delete: async (table, where) => {
|
|
2949
|
-
validateTableName$1(table);
|
|
2950
|
-
if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
|
|
2951
|
-
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2952
|
-
return {
|
|
2953
|
-
operation: "delete",
|
|
2954
|
-
affected: (await txClient.query(`DELETE FROM ${table} ${whereClause}`, values)).rowCount ?? 0
|
|
2955
|
-
};
|
|
2956
|
-
},
|
|
2957
|
-
count: async (table, where) => {
|
|
2958
|
-
validateTableName$1(table);
|
|
2959
|
-
if (!where || Object.keys(where).length === 0) {
|
|
2960
|
-
const result = await txClient.query(`SELECT COUNT(*) as count FROM ${table}`);
|
|
2961
|
-
return Number(result.rows[0]?.count) || 0;
|
|
2962
|
-
}
|
|
2963
|
-
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2964
|
-
const result = await txClient.query(`SELECT COUNT(*) as count FROM ${table} ${whereClause}`, values);
|
|
2965
|
-
return Number(result.rows[0]?.count) || 0;
|
|
2966
|
-
},
|
|
2967
|
-
table: (tableName) => ({
|
|
2968
|
-
insert: (data) => txHandle.insert(tableName, data),
|
|
2969
|
-
get: (data) => txHandle.get(tableName, data),
|
|
2970
|
-
list: (data) => txHandle.list(tableName, data)
|
|
2971
|
-
}),
|
|
2972
|
-
many: async (strings, ...vars) => {
|
|
2973
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2974
|
-
return (await txClient.query(sql, values)).rows;
|
|
2975
|
-
},
|
|
2976
|
-
single: async (strings, ...vars) => {
|
|
2977
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2978
|
-
return (await txClient.query(sql, values)).rows[0] || null;
|
|
2979
|
-
},
|
|
2980
|
-
pluck: async (strings, ...vars) => {
|
|
2981
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2982
|
-
const firstRow = (await txClient.query(sql, values)).rows[0];
|
|
2983
|
-
if (!firstRow) return null;
|
|
2984
|
-
return Object.values(firstRow)[0];
|
|
2985
|
-
},
|
|
2986
|
-
execute: async (strings, ...vars) => {
|
|
2987
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2988
|
-
await txClient.query(sql, values);
|
|
2989
|
-
},
|
|
2990
|
-
query: async (sql, ...values) => {
|
|
2991
|
-
const query = normalizePostgresRawQuery(sql, values);
|
|
2992
|
-
const result = await txClient.query(query.sql, query.values);
|
|
2993
|
-
return {
|
|
2994
|
-
rows: result.rows,
|
|
2995
|
-
rowCount: result.rowCount ?? 0
|
|
2996
|
-
};
|
|
2997
|
-
},
|
|
2998
|
-
oo: async (strings, ...vars) => {
|
|
2999
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
3000
|
-
return (await txClient.query(sql, values)).rows;
|
|
3001
|
-
},
|
|
3002
|
-
oO: async (strings, ...vars) => {
|
|
3003
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
3004
|
-
return (await txClient.query(sql, values)).rows[0] || null;
|
|
3005
|
-
},
|
|
3006
|
-
ox: async (strings, ...vars) => {
|
|
3007
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
3008
|
-
const firstRow = (await txClient.query(sql, values)).rows[0];
|
|
3009
|
-
if (!firstRow) return null;
|
|
3010
|
-
return Object.values(firstRow)[0];
|
|
3011
|
-
},
|
|
3012
|
-
xx: async (strings, ...vars) => {
|
|
3013
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
3014
|
-
await txClient.query(sql, values);
|
|
3015
|
-
},
|
|
3016
|
-
tableExists,
|
|
3017
|
-
syncSchema,
|
|
3003
|
+
...createClientMethods(txClient, true, passThroughRecord),
|
|
3018
3004
|
transaction: createNestedTransaction(txClient, () => txHandle),
|
|
3019
3005
|
commit,
|
|
3020
3006
|
rollback,
|
|
@@ -3022,10 +3008,6 @@ async function createDatabase(options, cacheKey) {
|
|
|
3022
3008
|
};
|
|
3023
3009
|
return txHandle;
|
|
3024
3010
|
};
|
|
3025
|
-
const oo = many;
|
|
3026
|
-
const oO = single;
|
|
3027
|
-
const ox = pluck;
|
|
3028
|
-
const xx = execute;
|
|
3029
3011
|
/**
|
|
3030
3012
|
* Initialize database schemas from JSON manifest
|
|
3031
3013
|
* Supports dependency resolution and schema overrides
|
|
@@ -3311,8 +3293,8 @@ async function getDatabase(options = {}) {
|
|
|
3311
3293
|
}));
|
|
3312
3294
|
if (!options.type && (options.url?.startsWith("file:") || options.url === ":memory:")) options.type = "sqlite";
|
|
3313
3295
|
if (options.type === "postgres") return (await Promise.resolve().then(() => postgres_exports)).getDatabase(options);
|
|
3314
|
-
if (options.type === "sqlite") return (await import("./chunks/sqlite-
|
|
3315
|
-
if (options.type === "duckdb") return (await import("./chunks/duckdb-
|
|
3296
|
+
if (options.type === "sqlite") return (await import("./chunks/sqlite-kBmA-x_C.js")).getDatabase(options);
|
|
3297
|
+
if (options.type === "duckdb") return (await import("./chunks/duckdb-CWy7Nziq.js")).getDatabase(options);
|
|
3316
3298
|
if (options.type === "json") return (await Promise.resolve().then(() => json_exports)).getDatabase(options);
|
|
3317
3299
|
throw new Error("Invalid database type");
|
|
3318
3300
|
}
|