@happyvertical/sql 0.80.6 → 0.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/chunks/{schema-manager-a8RlxIE2.js → alter-utils-D7NGXZi0.js} +190 -2
- package/dist/chunks/alter-utils-D7NGXZi0.js.map +1 -0
- package/dist/chunks/{duckdb-D_Wu36JL.js → duckdb-pKOTggPW.js} +44 -20
- package/dist/chunks/duckdb-pKOTggPW.js.map +1 -0
- package/dist/chunks/{sqlite-B4ussRTS.js → sqlite-dbhMszEJ.js} +57 -23
- package/dist/chunks/sqlite-dbhMszEJ.js.map +1 -0
- package/dist/chunks/{sqlite-native-BgiF4301.js → sqlite-native-DrNaIJRO.js} +68 -20
- package/dist/chunks/sqlite-native-DrNaIJRO.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 +638 -747
- 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/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 +3 -3
- 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, r as generateAddColumnStatement, s as validateIndexName, t as escapeQuotedIdentifier, u as buildWhere } from "./chunks/alter-utils-D7NGXZi0.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,
|
|
@@ -503,7 +461,7 @@ async function loadJSONTables(connection, dataDir, providedSchemas = {}, pending
|
|
|
503
461
|
}
|
|
504
462
|
else {
|
|
505
463
|
if (eagerLoadTables) try {
|
|
506
|
-
await connection.run(`CREATE TABLE IF NOT EXISTS "${tableName}" AS SELECT * FROM read_json_auto('${filePath}')`);
|
|
464
|
+
await connection.run(`CREATE TABLE IF NOT EXISTS "${escapeQuotedIdentifier(tableName)}" AS SELECT * FROM read_json_auto('${escapeStringLiteral(filePath)}')`);
|
|
507
465
|
inferredSchemaTables.add(tableName);
|
|
508
466
|
console.log(`[json-adapter] Eagerly created ${tableName} with inferred schema (no UNIQUE constraints)`);
|
|
509
467
|
} catch (error) {
|
|
@@ -708,6 +666,7 @@ async function getDatabase$2(options) {
|
|
|
708
666
|
const { connection, pendingJSONFiles, inferredSchemaTables } = await createJSONConnection(options);
|
|
709
667
|
const writeStrategy = options.writeStrategy || "immediate";
|
|
710
668
|
const { url } = options;
|
|
669
|
+
const connectionLock = createTransactionLock("json", options.transactionQueueTimeout);
|
|
711
670
|
/**
|
|
712
671
|
* Inserts one or more records into a table
|
|
713
672
|
*
|
|
@@ -728,6 +687,7 @@ async function getDatabase$2(options) {
|
|
|
728
687
|
affected: 0
|
|
729
688
|
};
|
|
730
689
|
const keys = Object.keys(records[0]);
|
|
690
|
+
validateColumnNames(keys);
|
|
731
691
|
const values = [];
|
|
732
692
|
let paramIdx = 1;
|
|
733
693
|
const placeholders = records.map((record) => {
|
|
@@ -833,6 +793,7 @@ async function getDatabase$2(options) {
|
|
|
833
793
|
writeStrategy
|
|
834
794
|
});
|
|
835
795
|
const keys = Object.keys(data);
|
|
796
|
+
validateColumnNames(keys);
|
|
836
797
|
const setClause = keys.map((key, idx) => `${key} = $${idx + 1}`).join(", ");
|
|
837
798
|
const { sql: whereClause, values: whereValues } = buildWhere(where, keys.length + 1, "json");
|
|
838
799
|
const dataValues = Object.values(data).map((value) => value instanceof Date ? value.toISOString() : value);
|
|
@@ -882,7 +843,8 @@ async function getDatabase$2(options) {
|
|
|
882
843
|
const values = [];
|
|
883
844
|
const paramIdx = { value: 1 };
|
|
884
845
|
const assignments = keys.map((key) => {
|
|
885
|
-
|
|
846
|
+
const valueExpr = buildJSONValueExpression(data[key], values, paramIdx);
|
|
847
|
+
return `"${escapeQuotedIdentifier(key)}" = ${valueExpr}`;
|
|
886
848
|
});
|
|
887
849
|
const { sql: whereClause, values: whereValues } = buildWhere(conflictWhere, paramIdx.value, "json");
|
|
888
850
|
const sql = `UPDATE "${table}" SET ${assignments.join(", ")} ${whereClause}`;
|
|
@@ -981,11 +943,12 @@ async function getDatabase$2(options) {
|
|
|
981
943
|
paramIdx++;
|
|
982
944
|
}
|
|
983
945
|
}
|
|
984
|
-
const conflict = conflictColumns.map((col) => `"${col}"`).join(", ");
|
|
985
|
-
const quotedKeys = keys.map((key) => `"${key}"`).join(", ");
|
|
946
|
+
const conflict = conflictColumns.map((col) => `"${escapeQuotedIdentifier(col)}"`).join(", ");
|
|
947
|
+
const quotedKeys = keys.map((key) => `"${escapeQuotedIdentifier(key)}"`).join(", ");
|
|
986
948
|
const quotedUpdateSetParts = keys.map((key, i) => {
|
|
987
949
|
const part = updateSetParts[i];
|
|
988
|
-
|
|
950
|
+
const valueExpr = part.substring(part.indexOf("=") + 1).trim();
|
|
951
|
+
return `"${escapeQuotedIdentifier(key)}" = ${valueExpr}`;
|
|
989
952
|
});
|
|
990
953
|
const sql = `INSERT INTO "${table}" (${quotedKeys}) VALUES (${placeholders.join(", ")}) ON CONFLICT(${conflict}) DO UPDATE SET ${quotedUpdateSetParts.join(", ")}`;
|
|
991
954
|
try {
|
|
@@ -1101,6 +1064,7 @@ async function getDatabase$2(options) {
|
|
|
1101
1064
|
* @returns Promise resolving to boolean indicating if the table exists
|
|
1102
1065
|
*/
|
|
1103
1066
|
const tableExists = async (tableName) => {
|
|
1067
|
+
validateTableName(tableName);
|
|
1104
1068
|
try {
|
|
1105
1069
|
await connection.runAndReadAll(`SELECT * FROM ${tableName} LIMIT 1`);
|
|
1106
1070
|
return true;
|
|
@@ -1124,6 +1088,7 @@ async function getDatabase$2(options) {
|
|
|
1124
1088
|
* @param dataDir - Directory to write files
|
|
1125
1089
|
*/
|
|
1126
1090
|
const exportTableToJSON = async (connection, table, dataDir) => {
|
|
1091
|
+
validateTableName(table);
|
|
1127
1092
|
const filePath = join(dataDir, `${table}.json`);
|
|
1128
1093
|
const schemaPath = join(dataDir, `${table}.schema.sql`);
|
|
1129
1094
|
try {
|
|
@@ -1199,7 +1164,7 @@ async function getDatabase$2(options) {
|
|
|
1199
1164
|
const { constants } = await import("node:fs");
|
|
1200
1165
|
await access(filePath, constants.F_OK);
|
|
1201
1166
|
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}')`);
|
|
1167
|
+
await connection.run(`CREATE TABLE "${tableName}" AS SELECT * FROM read_json_auto('${escapeStringLiteral(filePath)}')`);
|
|
1203
1168
|
pendingJSONFiles.delete(tableName);
|
|
1204
1169
|
console.log(`[json-adapter] Created ${tableName} with inferred schema (no UNIQUE constraints)`);
|
|
1205
1170
|
} catch (error) {
|
|
@@ -1497,7 +1462,7 @@ async function getDatabase$2(options) {
|
|
|
1497
1462
|
* @param callback - Function to execute within transaction
|
|
1498
1463
|
* @returns Promise resolving to callback result
|
|
1499
1464
|
*/
|
|
1500
|
-
const transaction = async (callback) => {
|
|
1465
|
+
const transaction = async (callback) => connectionLock.run(async () => {
|
|
1501
1466
|
try {
|
|
1502
1467
|
await connection.run("BEGIN TRANSACTION");
|
|
1503
1468
|
const result = await callback(createTransactionScope());
|
|
@@ -1509,7 +1474,7 @@ async function getDatabase$2(options) {
|
|
|
1509
1474
|
} catch {}
|
|
1510
1475
|
throw error;
|
|
1511
1476
|
}
|
|
1512
|
-
};
|
|
1477
|
+
});
|
|
1513
1478
|
/**
|
|
1514
1479
|
* Builds the transaction-scoped interface handed to a transaction callback.
|
|
1515
1480
|
*
|
|
@@ -1558,18 +1523,30 @@ async function getDatabase$2(options) {
|
|
|
1558
1523
|
* @returns Promise resolving to a TransactionHandle
|
|
1559
1524
|
*/
|
|
1560
1525
|
const beginTransaction = async () => {
|
|
1561
|
-
await
|
|
1526
|
+
const releaseConnection = await connectionLock.acquire();
|
|
1527
|
+
try {
|
|
1528
|
+
await connection.run("BEGIN TRANSACTION");
|
|
1529
|
+
} catch (error) {
|
|
1530
|
+
releaseConnection();
|
|
1531
|
+
throw error;
|
|
1532
|
+
}
|
|
1562
1533
|
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 () => {
|
|
1534
|
+
const end = async (command) => {
|
|
1569
1535
|
if (!active) throw new DatabaseError("Transaction already ended", {});
|
|
1570
|
-
|
|
1571
|
-
|
|
1536
|
+
try {
|
|
1537
|
+
await connection.run(command);
|
|
1538
|
+
} catch (error) {
|
|
1539
|
+
try {
|
|
1540
|
+
await connection.run("ROLLBACK");
|
|
1541
|
+
} catch {}
|
|
1542
|
+
throw error;
|
|
1543
|
+
} finally {
|
|
1544
|
+
active = false;
|
|
1545
|
+
releaseConnection();
|
|
1546
|
+
}
|
|
1572
1547
|
};
|
|
1548
|
+
const commit = () => end("COMMIT");
|
|
1549
|
+
const rollback = () => end("ROLLBACK");
|
|
1573
1550
|
const isActive = () => active;
|
|
1574
1551
|
return {
|
|
1575
1552
|
...createTransactionScope(),
|
|
@@ -1818,10 +1795,36 @@ function toVectorLiteral(embedding) {
|
|
|
1818
1795
|
* @param pool - PostgreSQL connection pool
|
|
1819
1796
|
* @returns VectorCapabilities implementation
|
|
1820
1797
|
*/
|
|
1798
|
+
/**
|
|
1799
|
+
* `type` is interpolated into `USING <type>`, which cannot be parameterized and
|
|
1800
|
+
* is not an identifier the quoting helpers cover. `VectorIndexOptions.type`
|
|
1801
|
+
* constrains it at compile time only, so anything reaching this at runtime —
|
|
1802
|
+
* a JavaScript caller, or a `filter as VectorIndexOptions` on parsed JSON — has
|
|
1803
|
+
* to be checked against the values pgvector actually offers.
|
|
1804
|
+
*/
|
|
1805
|
+
var VECTOR_INDEX_TYPES = /* @__PURE__ */ new Set(["hnsw", "ivfflat"]);
|
|
1806
|
+
/**
|
|
1807
|
+
* `metric` needs the same treatment as `type`, for a less obvious reason.
|
|
1808
|
+
*
|
|
1809
|
+
* It never reaches SQL directly — `vectorOpsClass`/`vectorOperator` map it
|
|
1810
|
+
* through a closed switch with a safe default, so an unknown metric produces a
|
|
1811
|
+
* valid operator class. But it is also concatenated into the *generated index
|
|
1812
|
+
* name*, which is then interpolated into a quoted identifier, so a metric
|
|
1813
|
+
* carrying a `"` still ends that identifier. The safe-default mapping is what
|
|
1814
|
+
* makes this easy to miss: the part of the metric that looks dangerous is
|
|
1815
|
+
* handled, and the part that is actually dangerous is somewhere else.
|
|
1816
|
+
*/
|
|
1817
|
+
var VECTOR_METRICS = /* @__PURE__ */ new Set([
|
|
1818
|
+
"cosine",
|
|
1819
|
+
"l2",
|
|
1820
|
+
"ip"
|
|
1821
|
+
]);
|
|
1821
1822
|
function createVectorCapabilities(pool) {
|
|
1822
1823
|
return {
|
|
1823
1824
|
async ensureColumn(table, column, dimensions) {
|
|
1824
1825
|
if (!Number.isInteger(dimensions) || dimensions <= 0) throw new DatabaseError("Vector dimensions must be a positive integer", { dimensions });
|
|
1826
|
+
validateTableName$1(table);
|
|
1827
|
+
validateColumnName$1(column);
|
|
1825
1828
|
try {
|
|
1826
1829
|
await pool.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
1827
1830
|
await pool.query(`ALTER TABLE "${table}" ADD COLUMN IF NOT EXISTS "${column}" vector(${dimensions})`);
|
|
@@ -1835,8 +1838,22 @@ function createVectorCapabilities(pool) {
|
|
|
1835
1838
|
}
|
|
1836
1839
|
},
|
|
1837
1840
|
async ensureIndex(table, column, options) {
|
|
1841
|
+
validateTableName$1(table);
|
|
1842
|
+
validateColumnName$1(column);
|
|
1838
1843
|
const metric = options?.metric || "cosine";
|
|
1839
1844
|
const indexType = options?.type || "hnsw";
|
|
1845
|
+
if (!VECTOR_INDEX_TYPES.has(indexType)) throw new DatabaseError("Unsupported vector index type", {
|
|
1846
|
+
table,
|
|
1847
|
+
column,
|
|
1848
|
+
type: indexType,
|
|
1849
|
+
supported: [...VECTOR_INDEX_TYPES]
|
|
1850
|
+
});
|
|
1851
|
+
if (!VECTOR_METRICS.has(metric)) throw new DatabaseError("Unsupported vector metric", {
|
|
1852
|
+
table,
|
|
1853
|
+
column,
|
|
1854
|
+
metric,
|
|
1855
|
+
supported: [...VECTOR_METRICS]
|
|
1856
|
+
});
|
|
1840
1857
|
const opsClass = vectorOpsClass(metric);
|
|
1841
1858
|
const indexName = `idx_${table}_${column}_${metric}_${indexType}`;
|
|
1842
1859
|
try {
|
|
@@ -1851,7 +1868,10 @@ function createVectorCapabilities(pool) {
|
|
|
1851
1868
|
}
|
|
1852
1869
|
},
|
|
1853
1870
|
async upsertVector(table, where, column, embedding) {
|
|
1871
|
+
validateTableName$1(table);
|
|
1872
|
+
validateColumnName$1(column);
|
|
1854
1873
|
const keys = Object.keys(where);
|
|
1874
|
+
validateColumnNames(keys);
|
|
1855
1875
|
const values = Object.values(where);
|
|
1856
1876
|
const conditions = keys.map((key, i) => `"${key}" IS NOT DISTINCT FROM $${i + 1}`).join(" AND ");
|
|
1857
1877
|
const vectorParam = `$${values.length + 1}`;
|
|
@@ -1867,6 +1887,8 @@ function createVectorCapabilities(pool) {
|
|
|
1867
1887
|
}
|
|
1868
1888
|
},
|
|
1869
1889
|
async search(table, column, embedding, options) {
|
|
1890
|
+
validateTableName$1(table);
|
|
1891
|
+
validateColumnName$1(column);
|
|
1870
1892
|
const metric = options?.metric || "cosine";
|
|
1871
1893
|
const limit = options?.limit || 10;
|
|
1872
1894
|
const op = vectorOperator(metric);
|
|
@@ -2014,6 +2036,15 @@ async function createDatabase(options, cacheKey) {
|
|
|
2014
2036
|
/**
|
|
2015
2037
|
* Serializes all values in an object for database storage
|
|
2016
2038
|
*/
|
|
2039
|
+
/**
|
|
2040
|
+
* The serializer the transaction-scoped interfaces have always used: none.
|
|
2041
|
+
*
|
|
2042
|
+
* Identity, deliberately — not even the `undefined`-dropping the pool-backed
|
|
2043
|
+
* `serializeRecord` does. Those copies bound `undefined` straight through to
|
|
2044
|
+
* `pg`, which stores NULL, whereas dropping the key omits the column and lets
|
|
2045
|
+
* its default apply. Preserving that is the point of this parameter.
|
|
2046
|
+
*/
|
|
2047
|
+
const passThroughRecord = (record) => record;
|
|
2017
2048
|
const serializeRecord = (record) => {
|
|
2018
2049
|
const serialized = {};
|
|
2019
2050
|
for (const [key, value] of Object.entries(record)) {
|
|
@@ -2190,230 +2221,6 @@ async function createDatabase(options, cacheKey) {
|
|
|
2190
2221
|
return executeNullAwarePostgresUpsertInTransaction(table, conflictColumns, serializedData);
|
|
2191
2222
|
};
|
|
2192
2223
|
/**
|
|
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
2224
|
* Parses a tagged template literal into a SQL query and values
|
|
2418
2225
|
*
|
|
2419
2226
|
* @param strings - Template strings
|
|
@@ -2433,139 +2240,557 @@ async function createDatabase(options, cacheKey) {
|
|
|
2433
2240
|
};
|
|
2434
2241
|
};
|
|
2435
2242
|
/**
|
|
2436
|
-
*
|
|
2243
|
+
* Builds the complete set of query methods bound to one PostgreSQL client.
|
|
2437
2244
|
*
|
|
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
|
|
2245
|
+
* The pool-backed interface and every transaction-scoped interface are
|
|
2246
|
+
* instantiated from this one factory, so there is exactly one implementation
|
|
2247
|
+
* of each method. The transaction interfaces used to be hand-maintained
|
|
2248
|
+
* copies of these bodies, and both had drifted:
|
|
2458
2249
|
*
|
|
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
|
|
2250
|
+
* - they called the client bare, so a failure inside a transaction threw the
|
|
2251
|
+
* raw `pg` error rather than `DatabaseError`, and `instanceof` answered
|
|
2252
|
+
* differently depending on whether the call was in a transaction. `pg`
|
|
2253
|
+
* exports its own class also named `DatabaseError`, so nothing a developer
|
|
2254
|
+
* would check by eye showed the difference (#1115);
|
|
2255
|
+
* - they re-exposed `tableExists` and `syncSchema` from the enclosing scope,
|
|
2256
|
+
* which close over the *pool*. Both therefore ran on a different connection
|
|
2257
|
+
* than the transaction, so `tx.tableExists()` could not see a table created
|
|
2258
|
+
* in the same transaction and `tx.syncSchema()` committed its DDL
|
|
2259
|
+
* immediately, surviving a rollback (#1111).
|
|
2477
2260
|
*
|
|
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
|
|
2261
|
+
* Binding a client is the only difference between the two, so it is the only
|
|
2262
|
+
* thing this factory parameterizes.
|
|
2496
2263
|
*
|
|
2497
|
-
*
|
|
2498
|
-
*
|
|
2499
|
-
*
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
});
|
|
2511
|
-
}
|
|
2512
|
-
};
|
|
2513
|
-
/**
|
|
2514
|
-
* Executes a raw SQL query with parameterized values
|
|
2264
|
+
* One thing is deliberately NOT unified: how `insert` and `update` serialize
|
|
2265
|
+
* values. The pool-backed methods run every value through `serializeRecord`,
|
|
2266
|
+
* which JSON-stringifies objects and arrays; the transaction-scoped copies
|
|
2267
|
+
* never did, so they handed the raw value to `pg`. That is not a cosmetic
|
|
2268
|
+
* difference — it decides what lands in the column, and no single answer is
|
|
2269
|
+
* right for both shapes:
|
|
2270
|
+
*
|
|
2271
|
+
* ```
|
|
2272
|
+
* text[] column jsonb column
|
|
2273
|
+
* raw JS array accepted "invalid input syntax for type json"
|
|
2274
|
+
* JSON string "malformed accepted
|
|
2275
|
+
* array literal"
|
|
2276
|
+
* ```
|
|
2515
2277
|
*
|
|
2516
|
-
*
|
|
2517
|
-
*
|
|
2518
|
-
*
|
|
2278
|
+
* So collapsing the two would have silently broken array-column writes inside
|
|
2279
|
+
* a transaction while fixing jsonb-array writes, or the reverse — and would
|
|
2280
|
+
* have corrupted `bytea` writes, where a Buffer would become the JSON of a
|
|
2281
|
+
* Buffer with no error anywhere. Neither belongs in a fix for the error
|
|
2282
|
+
* contract, so the serializer is a parameter: each interface keeps exactly
|
|
2283
|
+
* the behaviour it had, and the difference is stated here instead of being an
|
|
2284
|
+
* accident of two copies drifting apart.
|
|
2519
2285
|
*
|
|
2520
|
-
* @param
|
|
2521
|
-
* @param
|
|
2522
|
-
* @
|
|
2286
|
+
* @param executor - Pool or checked-out client every statement runs on
|
|
2287
|
+
* @param inTransaction - Whether `executor` is already inside a transaction
|
|
2288
|
+
* @param serialize - Applied to each record before `insert`/`update` builds
|
|
2289
|
+
* its SQL. Pass-through for the transaction-scoped interfaces.
|
|
2523
2290
|
*/
|
|
2524
|
-
const
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2291
|
+
const createClientMethods = (executor, inTransaction, serialize) => {
|
|
2292
|
+
/**
|
|
2293
|
+
* Inserts one or more records into a table
|
|
2294
|
+
*
|
|
2295
|
+
* @param table - Table name
|
|
2296
|
+
* @param data - Single record or array of records to insert
|
|
2297
|
+
* @returns Promise resolving to operation result
|
|
2298
|
+
* @throws Error if the insert operation fails
|
|
2299
|
+
*
|
|
2300
|
+
* @example Single record insert:
|
|
2301
|
+
* ```typescript
|
|
2302
|
+
* await db.insert('users', {
|
|
2303
|
+
* name: 'John Doe',
|
|
2304
|
+
* email: 'john@example.com'
|
|
2305
|
+
* });
|
|
2306
|
+
* ```
|
|
2307
|
+
*
|
|
2308
|
+
* @example Multiple record insert:
|
|
2309
|
+
* ```typescript
|
|
2310
|
+
* await db.insert('users', [
|
|
2311
|
+
* { name: 'John', email: 'john@example.com' },
|
|
2312
|
+
* { name: 'Jane', email: 'jane@example.com' }
|
|
2313
|
+
* ]);
|
|
2314
|
+
* ```
|
|
2315
|
+
*/
|
|
2316
|
+
const insert = async (table, data) => {
|
|
2317
|
+
validateTableName$1(table);
|
|
2318
|
+
if (Array.isArray(data)) {
|
|
2319
|
+
const serializedRecords = data.map((record) => serialize(record));
|
|
2320
|
+
const keys = Object.keys(serializedRecords[0]);
|
|
2321
|
+
validateColumnNames(keys);
|
|
2322
|
+
const placeholders = serializedRecords.map((_, i) => `(${keys.map((_, j) => `$${i * keys.length + j + 1}`).join(", ")})`).join(", ");
|
|
2323
|
+
const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
|
|
2324
|
+
const values = serializedRecords.reduce((acc, row) => acc.concat(Object.values(row)), []);
|
|
2325
|
+
return {
|
|
2326
|
+
operation: "insert",
|
|
2327
|
+
affected: (await executor.query(query, values)).rowCount ?? 0
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
const serializedData = serialize(data);
|
|
2331
|
+
const keys = Object.keys(serializedData);
|
|
2332
|
+
validateColumnNames(keys);
|
|
2333
|
+
const values = Object.values(serializedData);
|
|
2334
|
+
const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
|
|
2335
|
+
const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
|
|
2528
2336
|
return {
|
|
2529
|
-
|
|
2530
|
-
|
|
2337
|
+
operation: "insert",
|
|
2338
|
+
affected: (await executor.query(query, values)).rowCount ?? 0
|
|
2531
2339
|
};
|
|
2532
|
-
}
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2340
|
+
};
|
|
2341
|
+
/**
|
|
2342
|
+
* Retrieves a single record matching the where criteria
|
|
2343
|
+
*
|
|
2344
|
+
* @param table - Table name
|
|
2345
|
+
* @param where - Criteria to match records
|
|
2346
|
+
* @returns Promise resolving to query result
|
|
2347
|
+
*/
|
|
2348
|
+
const get = async (table, where) => {
|
|
2349
|
+
validateTableName$1(table);
|
|
2350
|
+
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2351
|
+
if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
|
|
2352
|
+
const query = `SELECT * FROM ${table} ${whereClause}`;
|
|
2353
|
+
try {
|
|
2354
|
+
return (await executor.query(query, values)).rows[0] || null;
|
|
2355
|
+
} catch (e) {
|
|
2356
|
+
throw new DatabaseError("Failed to retrieve record from table", {
|
|
2357
|
+
table,
|
|
2358
|
+
sql: query,
|
|
2359
|
+
values,
|
|
2360
|
+
originalError: formatDbError(e)
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
/**
|
|
2365
|
+
* Retrieves multiple records matching the where criteria
|
|
2366
|
+
*
|
|
2367
|
+
* @param table - Table name
|
|
2368
|
+
* @param where - Criteria to match records
|
|
2369
|
+
* @returns Promise resolving to array of records
|
|
2370
|
+
*/
|
|
2371
|
+
const list = async (table, where) => {
|
|
2372
|
+
validateTableName$1(table);
|
|
2373
|
+
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2374
|
+
const query = `SELECT * FROM ${table} ${whereClause}`;
|
|
2375
|
+
try {
|
|
2376
|
+
return (await executor.query(query, values)).rows;
|
|
2377
|
+
} catch (e) {
|
|
2378
|
+
throw new DatabaseError("Failed to list records from table", {
|
|
2379
|
+
table,
|
|
2380
|
+
sql: query,
|
|
2381
|
+
values,
|
|
2382
|
+
originalError: formatDbError(e)
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
};
|
|
2386
|
+
/**
|
|
2387
|
+
* Updates records matching the where criteria
|
|
2388
|
+
*
|
|
2389
|
+
* @param table - Table name
|
|
2390
|
+
* @param where - Criteria to match records to update
|
|
2391
|
+
* @param data - New data to set
|
|
2392
|
+
* @returns Promise resolving to operation result
|
|
2393
|
+
*/
|
|
2394
|
+
const update = async (table, where, data) => {
|
|
2395
|
+
validateTableName$1(table);
|
|
2396
|
+
const serializedData = serialize(data);
|
|
2397
|
+
const keys = Object.keys(serializedData);
|
|
2398
|
+
validateColumnNames(keys);
|
|
2399
|
+
const values = Object.values(serializedData);
|
|
2400
|
+
const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
|
|
2401
|
+
const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "postgres");
|
|
2402
|
+
if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
|
|
2403
|
+
const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
|
|
2404
|
+
try {
|
|
2405
|
+
return {
|
|
2406
|
+
operation: "update",
|
|
2407
|
+
affected: (await executor.query(sql, [...values, ...whereValues])).rowCount ?? 0
|
|
2408
|
+
};
|
|
2409
|
+
} catch (e) {
|
|
2410
|
+
throw new DatabaseError("Failed to update records in table", {
|
|
2411
|
+
table,
|
|
2412
|
+
sql,
|
|
2413
|
+
values: [...values, ...whereValues],
|
|
2414
|
+
originalError: formatDbError(e)
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2417
|
+
};
|
|
2418
|
+
/**
|
|
2419
|
+
* Inserts a record or updates it if it already exists (UPSERT)
|
|
2420
|
+
*
|
|
2421
|
+
* @param table - Table name
|
|
2422
|
+
* @param conflictColumns - Columns that define the uniqueness constraint
|
|
2423
|
+
* @param data - Data to insert or update
|
|
2424
|
+
* @returns Promise resolving to operation result
|
|
2425
|
+
* @throws Error if the upsert operation fails
|
|
2426
|
+
*/
|
|
2427
|
+
const upsert = async (table, conflictColumns, data, options) => {
|
|
2428
|
+
validateTableName$1(table);
|
|
2429
|
+
validateColumnNames([...conflictColumns, ...Object.keys(data)]);
|
|
2430
|
+
try {
|
|
2431
|
+
return await executePostgresUpsert(executor, table, conflictColumns, data, options, !inTransaction);
|
|
2432
|
+
} catch (e) {
|
|
2433
|
+
if (e instanceof DatabaseError && e.message === "Conflict columns missing from data") throw e;
|
|
2434
|
+
throw new DatabaseError("Failed to upsert record into table", {
|
|
2435
|
+
table,
|
|
2436
|
+
values: data,
|
|
2437
|
+
conflictColumns,
|
|
2438
|
+
originalError: formatDbError(e)
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
};
|
|
2442
|
+
/**
|
|
2443
|
+
* Gets a record matching the where criteria or inserts it if not found
|
|
2444
|
+
*
|
|
2445
|
+
* @param table - Table name
|
|
2446
|
+
* @param where - Criteria to match existing record
|
|
2447
|
+
* @returns Promise resolving to the query result or insert result
|
|
2448
|
+
*/
|
|
2449
|
+
const getOrInsert = async (table, where, data) => {
|
|
2450
|
+
validateTableName$1(table);
|
|
2451
|
+
const result = await get(table, where);
|
|
2452
|
+
if (result) return result;
|
|
2453
|
+
await insert(table, data);
|
|
2454
|
+
const inserted = await get(table, where);
|
|
2455
|
+
if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
|
|
2456
|
+
table,
|
|
2457
|
+
where,
|
|
2458
|
+
data
|
|
2459
|
+
});
|
|
2460
|
+
return inserted;
|
|
2461
|
+
};
|
|
2462
|
+
/**
|
|
2463
|
+
* Deletes records from a table matching the where criteria
|
|
2464
|
+
*
|
|
2465
|
+
* @param table - Table name
|
|
2466
|
+
* @param where - Criteria to match records for deletion
|
|
2467
|
+
* @returns Promise resolving to operation result with count of deleted rows
|
|
2468
|
+
* @throws Error if the delete operation fails
|
|
2469
|
+
*/
|
|
2470
|
+
const deleteRecords = async (table, where) => {
|
|
2471
|
+
validateTableName$1(table);
|
|
2472
|
+
if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
|
|
2473
|
+
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2474
|
+
try {
|
|
2475
|
+
return {
|
|
2476
|
+
operation: "delete",
|
|
2477
|
+
affected: (await executor.query(`DELETE FROM ${table} ${whereClause}`, values)).rowCount ?? 0
|
|
2478
|
+
};
|
|
2479
|
+
} catch (e) {
|
|
2480
|
+
throw new DatabaseError("Failed to delete records from table", {
|
|
2481
|
+
table,
|
|
2482
|
+
where,
|
|
2483
|
+
originalError: formatDbError(e)
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
};
|
|
2487
|
+
/**
|
|
2488
|
+
* Counts records in a table matching the where criteria
|
|
2489
|
+
*
|
|
2490
|
+
* @param table - Table name
|
|
2491
|
+
* @param where - Criteria to match records (optional, counts all if omitted)
|
|
2492
|
+
* @returns Promise resolving to count of matching records
|
|
2493
|
+
* @throws Error if the count operation fails
|
|
2494
|
+
*/
|
|
2495
|
+
const count = async (table, where) => {
|
|
2496
|
+
validateTableName$1(table);
|
|
2497
|
+
try {
|
|
2498
|
+
if (!where || Object.keys(where).length === 0) {
|
|
2499
|
+
const result = await executor.query(`SELECT COUNT(*) as count FROM ${table}`);
|
|
2500
|
+
return Number(result.rows[0]?.count) || 0;
|
|
2501
|
+
}
|
|
2502
|
+
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2503
|
+
const result = await executor.query(`SELECT COUNT(*) as count FROM ${table} ${whereClause}`, values);
|
|
2504
|
+
return Number(result.rows[0]?.count) || 0;
|
|
2505
|
+
} catch (e) {
|
|
2506
|
+
throw new DatabaseError("Failed to count records in table", {
|
|
2507
|
+
table,
|
|
2508
|
+
where,
|
|
2509
|
+
originalError: formatDbError(e)
|
|
2510
|
+
});
|
|
2511
|
+
}
|
|
2512
|
+
};
|
|
2513
|
+
/**
|
|
2514
|
+
* Creates a table-specific interface for simplified table operations
|
|
2515
|
+
*
|
|
2516
|
+
* @param tableName - Table name
|
|
2517
|
+
* @returns TableMethods interface for the specified table
|
|
2518
|
+
*/
|
|
2519
|
+
const table = (tableName) => {
|
|
2520
|
+
return {
|
|
2521
|
+
insert: (data) => insert(tableName, data),
|
|
2522
|
+
get: (data) => get(tableName, data),
|
|
2523
|
+
list: (data) => list(tableName, data)
|
|
2524
|
+
};
|
|
2525
|
+
};
|
|
2526
|
+
/**
|
|
2527
|
+
* Executes a SQL query using template literals and returns a single value
|
|
2528
|
+
*
|
|
2529
|
+
* @param strings - Template strings
|
|
2530
|
+
* @param vars - Variables to interpolate into the query
|
|
2531
|
+
* @returns Promise resolving to a single value (first column of first row)
|
|
2532
|
+
*/
|
|
2533
|
+
const pluck = async (strings, ...vars) => {
|
|
2534
|
+
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2535
|
+
try {
|
|
2536
|
+
const firstRow = (await executor.query(sql, values)).rows[0];
|
|
2537
|
+
if (!firstRow) return null;
|
|
2538
|
+
return Object.values(firstRow)[0];
|
|
2539
|
+
} catch (e) {
|
|
2540
|
+
throw new DatabaseError("Failed to execute pluck query", {
|
|
2541
|
+
sql,
|
|
2542
|
+
values,
|
|
2543
|
+
originalError: formatDbError(e)
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
};
|
|
2547
|
+
/**
|
|
2548
|
+
* Executes a SQL query using template literals and returns a single row
|
|
2549
|
+
*
|
|
2550
|
+
* @param strings - Template strings
|
|
2551
|
+
* @param vars - Variables to interpolate into the query
|
|
2552
|
+
* @returns Promise resolving to a single result record or null
|
|
2553
|
+
*/
|
|
2554
|
+
const single = async (strings, ...vars) => {
|
|
2555
|
+
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2556
|
+
try {
|
|
2557
|
+
return (await executor.query(sql, values)).rows[0] || null;
|
|
2558
|
+
} catch (e) {
|
|
2559
|
+
throw new DatabaseError("Failed to execute single query", {
|
|
2560
|
+
sql,
|
|
2561
|
+
values,
|
|
2562
|
+
originalError: formatDbError(e)
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
};
|
|
2566
|
+
/**
|
|
2567
|
+
* Executes a SQL query using template literals and returns multiple rows
|
|
2568
|
+
*
|
|
2569
|
+
* @param strings - Template strings
|
|
2570
|
+
* @param vars - Variables to interpolate into the query
|
|
2571
|
+
* @returns Promise resolving to array of result records
|
|
2572
|
+
*/
|
|
2573
|
+
const many = async (strings, ...vars) => {
|
|
2574
|
+
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2575
|
+
try {
|
|
2576
|
+
return (await executor.query(sql, values)).rows;
|
|
2577
|
+
} catch (e) {
|
|
2578
|
+
throw new DatabaseError("Failed to execute many query", {
|
|
2579
|
+
sql,
|
|
2580
|
+
values,
|
|
2581
|
+
originalError: formatDbError(e)
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
};
|
|
2585
|
+
/**
|
|
2586
|
+
* Executes a SQL query using template literals without returning results
|
|
2587
|
+
*
|
|
2588
|
+
* @param strings - Template strings
|
|
2589
|
+
* @param vars - Variables to interpolate into the query
|
|
2590
|
+
* @returns Promise that resolves when the query completes
|
|
2591
|
+
*/
|
|
2592
|
+
const execute = async (strings, ...vars) => {
|
|
2593
|
+
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2594
|
+
try {
|
|
2595
|
+
await executor.query(sql, values);
|
|
2596
|
+
} catch (e) {
|
|
2597
|
+
throw new DatabaseError("Failed to execute query", {
|
|
2598
|
+
sql,
|
|
2599
|
+
values,
|
|
2600
|
+
originalError: formatDbError(e)
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2603
|
+
};
|
|
2604
|
+
/**
|
|
2605
|
+
* Executes a raw SQL query with parameterized values
|
|
2606
|
+
*
|
|
2607
|
+
* Uses PostgreSQL-native placeholders ($1, $2, ...). Legacy ? placeholders
|
|
2608
|
+
* are converted only when the placeholder count matches the supplied values,
|
|
2609
|
+
* so Postgres operators such as JSONB ? remain intact.
|
|
2610
|
+
*
|
|
2611
|
+
* @param sql - SQL query string
|
|
2612
|
+
* @param values - Variables to use as parameters
|
|
2613
|
+
* @returns Promise resolving to query result with rows and count
|
|
2614
|
+
*/
|
|
2615
|
+
const query = async (sql, ...values) => {
|
|
2616
|
+
const query = normalizePostgresRawQuery(sql, values);
|
|
2617
|
+
try {
|
|
2618
|
+
const result = await executor.query(query.sql, query.values);
|
|
2619
|
+
return {
|
|
2620
|
+
rows: result.rows,
|
|
2621
|
+
rowCount: result.rowCount ?? 0
|
|
2622
|
+
};
|
|
2623
|
+
} catch (e) {
|
|
2624
|
+
throw new DatabaseError("Failed to execute raw query", {
|
|
2625
|
+
sql: query.sql,
|
|
2626
|
+
values: query.values,
|
|
2627
|
+
originalError: formatDbError(e)
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
};
|
|
2631
|
+
/**
|
|
2632
|
+
* Checks if a table exists in the database
|
|
2633
|
+
*
|
|
2634
|
+
* @param tableName - Name of the table to check
|
|
2635
|
+
* @returns Promise resolving to boolean indicating if the table exists
|
|
2636
|
+
*/
|
|
2637
|
+
const tableExists = async (tableName) => {
|
|
2638
|
+
return (await executor.query(`SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1 AND table_schema = 'public')`, [tableName])).rows[0].exists;
|
|
2639
|
+
};
|
|
2640
|
+
/**
|
|
2641
|
+
* Synchronizes database schema with provided SQL DDL
|
|
2642
|
+
* Creates tables if they don't exist and adds missing columns
|
|
2643
|
+
* Also executes CREATE INDEX statements
|
|
2644
|
+
*
|
|
2645
|
+
* @param schema - SQL schema definition with CREATE TABLE and CREATE INDEX statements
|
|
2646
|
+
* @returns Promise that resolves when schema is synchronized
|
|
2647
|
+
*/
|
|
2648
|
+
/**
|
|
2649
|
+
* Runs a step whose failure `syncSchema` deliberately tolerates.
|
|
2650
|
+
*
|
|
2651
|
+
* Logging and continuing is self-contained on the pool. Inside a
|
|
2652
|
+
* transaction it is not: a failed statement puts PostgreSQL in the aborted
|
|
2653
|
+
* state, and `COMMIT` on an aborted transaction *succeeds* — it reports a
|
|
2654
|
+
* ROLLBACK command tag and no error. The caller is told the transaction
|
|
2655
|
+
* committed while every write in it was discarded, which is the silent
|
|
2656
|
+
* data loss this whole epic is about, re-entering through the schema path.
|
|
2657
|
+
*
|
|
2658
|
+
* A savepoint scopes the failure to the step, which is what "log it and
|
|
2659
|
+
* continue" was always supposed to mean.
|
|
2660
|
+
*/
|
|
2661
|
+
const runTolerated = async (work) => {
|
|
2662
|
+
if (!inTransaction) return work();
|
|
2663
|
+
savepointSequence += 1;
|
|
2664
|
+
const name = `hv_sync_${savepointSequence}`;
|
|
2665
|
+
await executor.query(`SAVEPOINT ${name}`);
|
|
2666
|
+
try {
|
|
2667
|
+
const result = await work();
|
|
2668
|
+
await executor.query(`RELEASE SAVEPOINT ${name}`);
|
|
2669
|
+
return result;
|
|
2670
|
+
} catch (error) {
|
|
2671
|
+
await executor.query(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
2672
|
+
await executor.query(`RELEASE SAVEPOINT ${name}`);
|
|
2673
|
+
throw error;
|
|
2674
|
+
}
|
|
2675
|
+
};
|
|
2676
|
+
const syncSchema = async (schema) => {
|
|
2677
|
+
const commands = schema.trim().split(";").filter((command) => command.trim() !== "");
|
|
2678
|
+
const createIndexRegex = /CREATE (UNIQUE )?INDEX (IF NOT EXISTS )?"?(\w+)"? ON "?(\w+)"?\s*\(([^)]+)\)/i;
|
|
2679
|
+
const indexNames = [];
|
|
2680
|
+
for (const command of commands) {
|
|
2681
|
+
const indexMatch = command.trim().match(createIndexRegex);
|
|
2682
|
+
if (indexMatch) indexNames.push(indexMatch[3]);
|
|
2683
|
+
}
|
|
2684
|
+
const existingIndexes = /* @__PURE__ */ new Set();
|
|
2685
|
+
if (indexNames.length > 0) {
|
|
2686
|
+
const result = await executor.query(`SELECT indexname FROM pg_indexes
|
|
2687
|
+
WHERE schemaname = 'public' AND indexname = ANY($1::text[])`, [indexNames]);
|
|
2688
|
+
for (const row of result.rows) existingIndexes.add(row.indexname);
|
|
2689
|
+
}
|
|
2690
|
+
for (const command of commands) {
|
|
2691
|
+
const trimmedCommand = command.trim();
|
|
2692
|
+
const tableMatch = trimmedCommand.match(/CREATE TABLE (IF NOT EXISTS )?"?(\w+)"? \(([\s\S]+)\)/i);
|
|
2693
|
+
if (tableMatch) {
|
|
2694
|
+
const tableName = tableMatch[2];
|
|
2695
|
+
const columns = tableMatch[3].trim().split(",\n");
|
|
2696
|
+
if (!await tableExists(tableName)) await executor.query(trimmedCommand);
|
|
2697
|
+
else for (const column of columns) {
|
|
2698
|
+
const columnDef = column.trim();
|
|
2699
|
+
const columnMatch = columnDef.match(/"?(\w+)"?\s+(\w+[^,]*)/);
|
|
2700
|
+
if (columnMatch) {
|
|
2701
|
+
const columnName = columnMatch[1];
|
|
2702
|
+
if (columnName.toUpperCase() === "PRIMARY" || columnName.toUpperCase() === "FOREIGN" || columnName.toUpperCase() === "UNIQUE" || columnName.toUpperCase() === "CHECK" || columnName.toUpperCase() === "CONSTRAINT") continue;
|
|
2703
|
+
try {
|
|
2704
|
+
await runTolerated(async () => {
|
|
2705
|
+
if (!(await executor.query(`SELECT EXISTS (
|
|
2706
|
+
SELECT 1 FROM information_schema.columns
|
|
2707
|
+
WHERE table_name = $1
|
|
2708
|
+
AND column_name = $2
|
|
2709
|
+
AND table_schema = 'public'
|
|
2710
|
+
)`, [tableName, columnName])).rows[0].exists) {
|
|
2711
|
+
const alterCommand = `ALTER TABLE "${tableName}" ADD COLUMN ${columnDef}`;
|
|
2712
|
+
await executor.query(alterCommand);
|
|
2713
|
+
}
|
|
2714
|
+
});
|
|
2715
|
+
} catch (error) {
|
|
2716
|
+
console.error(`Error adding column ${columnName} to ${tableName}:`, error);
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
continue;
|
|
2721
|
+
}
|
|
2722
|
+
const indexMatch = trimmedCommand.match(createIndexRegex);
|
|
2723
|
+
if (indexMatch) {
|
|
2724
|
+
const indexName = indexMatch[3];
|
|
2725
|
+
const indexTableName = indexMatch[4];
|
|
2726
|
+
if (!existingIndexes.has(indexName)) try {
|
|
2727
|
+
await runTolerated(() => executor.query(trimmedCommand));
|
|
2728
|
+
existingIndexes.add(indexName);
|
|
2729
|
+
} catch (error) {
|
|
2730
|
+
console.warn(`Warning: Failed to create index ${indexName} on ${indexTableName}:`, error instanceof Error ? error.message : String(error));
|
|
2731
|
+
}
|
|
2732
|
+
continue;
|
|
2733
|
+
}
|
|
2734
|
+
if (trimmedCommand.toUpperCase().startsWith("CREATE ") && !trimmedCommand.toUpperCase().includes("CREATE TABLE")) try {
|
|
2735
|
+
await runTolerated(() => executor.query(trimmedCommand));
|
|
2736
|
+
} catch (error) {
|
|
2737
|
+
console.warn(`Warning: DDL statement may have failed:`, error instanceof Error ? error.message : String(error));
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
};
|
|
2741
|
+
return {
|
|
2742
|
+
insert,
|
|
2743
|
+
get,
|
|
2744
|
+
list,
|
|
2745
|
+
update,
|
|
2746
|
+
upsert,
|
|
2747
|
+
getOrInsert,
|
|
2748
|
+
delete: deleteRecords,
|
|
2749
|
+
count,
|
|
2750
|
+
table,
|
|
2751
|
+
many,
|
|
2752
|
+
single,
|
|
2753
|
+
pluck,
|
|
2754
|
+
execute,
|
|
2755
|
+
query,
|
|
2756
|
+
oo: many,
|
|
2757
|
+
oO: single,
|
|
2758
|
+
ox: pluck,
|
|
2759
|
+
xx: execute,
|
|
2760
|
+
tableExists,
|
|
2761
|
+
syncSchema
|
|
2762
|
+
};
|
|
2763
|
+
};
|
|
2764
|
+
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);
|
|
2765
|
+
/**
|
|
2766
|
+
* Acquire a pinned connection from the pool for session-scoped state such as
|
|
2767
|
+
* `pg_advisory_lock`. Every query on the returned handle runs on the same
|
|
2768
|
+
* client until {@link SessionHandle.release} drops the connection (which
|
|
2769
|
+
* frees any session locks it held). The connection is held for the handle's
|
|
2770
|
+
* lifetime, so hold one session per process, not per operation.
|
|
2771
|
+
*/
|
|
2772
|
+
const acquireSession = async () => {
|
|
2773
|
+
const sessionClient = await pool.connect();
|
|
2774
|
+
let released = false;
|
|
2775
|
+
let lost = false;
|
|
2776
|
+
sessionClient.on("error", () => {
|
|
2777
|
+
lost = true;
|
|
2778
|
+
});
|
|
2779
|
+
return {
|
|
2780
|
+
query: async (sql, ...values) => {
|
|
2781
|
+
if (released) throw new DatabaseError("Session has been released", {});
|
|
2782
|
+
if (lost) throw new DatabaseError("Session connection was lost", {});
|
|
2783
|
+
const normalized = normalizePostgresRawQuery(sql, values);
|
|
2784
|
+
try {
|
|
2785
|
+
const result = await sessionClient.query(normalized.sql, normalized.values);
|
|
2786
|
+
return {
|
|
2787
|
+
rows: result.rows,
|
|
2788
|
+
rowCount: result.rowCount ?? 0
|
|
2789
|
+
};
|
|
2790
|
+
} catch (e) {
|
|
2791
|
+
throw new DatabaseError("Failed to execute session query", {
|
|
2792
|
+
sql: normalized.sql,
|
|
2793
|
+
values: normalized.values,
|
|
2569
2794
|
originalError: formatDbError(e)
|
|
2570
2795
|
});
|
|
2571
2796
|
}
|
|
@@ -2586,86 +2811,6 @@ async function createDatabase(options, cacheKey) {
|
|
|
2586
2811
|
}
|
|
2587
2812
|
};
|
|
2588
2813
|
};
|
|
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
2814
|
let savepointSequence = 0;
|
|
2670
2815
|
/**
|
|
2671
2816
|
* Builds the `transaction` exposed on a transaction-scoped interface.
|
|
@@ -2715,132 +2860,7 @@ async function createDatabase(options, cacheKey) {
|
|
|
2715
2860
|
const txDb = {
|
|
2716
2861
|
url,
|
|
2717
2862
|
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,
|
|
2863
|
+
...createClientMethods(txClient, true, passThroughRecord),
|
|
2844
2864
|
transaction: createNestedTransaction(txClient, () => txDb)
|
|
2845
2865
|
};
|
|
2846
2866
|
const result = await callback(txDb);
|
|
@@ -2889,132 +2909,7 @@ async function createDatabase(options, cacheKey) {
|
|
|
2889
2909
|
const txHandle = {
|
|
2890
2910
|
url,
|
|
2891
2911
|
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,
|
|
2912
|
+
...createClientMethods(txClient, true, passThroughRecord),
|
|
3018
2913
|
transaction: createNestedTransaction(txClient, () => txHandle),
|
|
3019
2914
|
commit,
|
|
3020
2915
|
rollback,
|
|
@@ -3022,10 +2917,6 @@ async function createDatabase(options, cacheKey) {
|
|
|
3022
2917
|
};
|
|
3023
2918
|
return txHandle;
|
|
3024
2919
|
};
|
|
3025
|
-
const oo = many;
|
|
3026
|
-
const oO = single;
|
|
3027
|
-
const ox = pluck;
|
|
3028
|
-
const xx = execute;
|
|
3029
2920
|
/**
|
|
3030
2921
|
* Initialize database schemas from JSON manifest
|
|
3031
2922
|
* Supports dependency resolution and schema overrides
|
|
@@ -3311,8 +3202,8 @@ async function getDatabase(options = {}) {
|
|
|
3311
3202
|
}));
|
|
3312
3203
|
if (!options.type && (options.url?.startsWith("file:") || options.url === ":memory:")) options.type = "sqlite";
|
|
3313
3204
|
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-
|
|
3205
|
+
if (options.type === "sqlite") return (await import("./chunks/sqlite-dbhMszEJ.js")).getDatabase(options);
|
|
3206
|
+
if (options.type === "duckdb") return (await import("./chunks/duckdb-pKOTggPW.js")).getDatabase(options);
|
|
3316
3207
|
if (options.type === "json") return (await Promise.resolve().then(() => json_exports)).getDatabase(options);
|
|
3317
3208
|
throw new Error("Invalid database type");
|
|
3318
3209
|
}
|