@happyvertical/sql 0.102.1 → 0.102.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT.md +1 -1
- package/README.md +99 -4
- package/dist/chunks/{duckdb-DfikVgov.js → duckdb-LMyfKZGi.js} +3 -3
- package/dist/chunks/{duckdb-DfikVgov.js.map → duckdb-LMyfKZGi.js.map} +1 -1
- package/dist/chunks/postgres-dialect-Bfswr4Vs.js +2728 -0
- package/dist/chunks/postgres-dialect-Bfswr4Vs.js.map +1 -0
- package/dist/chunks/{sqlite-Cz2NMiDs.js → sqlite-0PMv1va2.js} +5 -5
- package/dist/chunks/{sqlite-Cz2NMiDs.js.map → sqlite-0PMv1va2.js.map} +1 -1
- package/dist/chunks/{sqlite-connection-cache-BAakj2HO.js → sqlite-connection-cache-ChWav9iY.js} +2 -2
- package/dist/chunks/{sqlite-connection-cache-BAakj2HO.js.map → sqlite-connection-cache-ChWav9iY.js.map} +1 -1
- package/dist/chunks/{sqlite-native-ByGwGEbS.js → sqlite-native--z4BnolL.js} +4 -4
- package/dist/chunks/{sqlite-native-ByGwGEbS.js.map → sqlite-native--z4BnolL.js.map} +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +163 -1365
- package/dist/index.js.map +1 -1
- package/dist/pglite.d.ts +150 -0
- package/dist/pglite.d.ts.map +1 -0
- package/dist/pglite.js +339 -0
- package/dist/pglite.js.map +1 -0
- package/dist/postgres.d.ts.map +1 -1
- package/dist/shared/postgres-dialect.d.ts +153 -0
- package/dist/shared/postgres-dialect.d.ts.map +1 -0
- package/dist/shared/types.d.ts +4 -4
- package/metadata.json +1 -0
- package/package.json +13 -2
- package/dist/chunks/connection-cache-Da5UcY5u.js +0 -1305
- package/dist/chunks/connection-cache-Da5UcY5u.js.map +0 -1
- package/dist/chunks/types-CwWJ-yju.js +0 -139
- package/dist/chunks/types-CwWJ-yju.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as parseConditionKey, D as wrapDatabaseError, E as unwrapRawKey, S as formatDbError, T as resolveInsertColumns, _ as validateColumnNames, a as createVectorCapabilities, b as DatabaseSchemaManager, c as createTransactionLock, d as validateDatabaseCacheOptions, f as escapeQuotedIdentifier, i as createTransactionErrorTracker, l as ConnectionCache, n as createGetTableSchema, o as normalizePostgresRawQuery, p as escapeStringLiteral, r as createPostgresDialect, t as createAlterTable, u as enableCachedValidation, w as raw, x as buildWhere } from "./chunks/postgres-dialect-Bfswr4Vs.js";
|
|
2
2
|
import { n as throwWithDuckDBCleanup, r as convertUniqueIndexesToInlineConstraints, t as createDuckDBResourceCloser } from "./chunks/duckdb-resources-Cci6Lo6H.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
4
|
import { t as redactDatabaseUrl$1 } from "./chunks/redact-database-url-CfMDvSdy.js";
|
|
5
|
-
import { i as createTransactionLock, n as resolveSchemas, t as NestedTransactionError } from "./chunks/types-CwWJ-yju.js";
|
|
6
5
|
import { DatabaseError, loadEnvConfig } from "@happyvertical/utils";
|
|
7
6
|
import { mkdir, readdir, stat } from "node:fs/promises";
|
|
8
7
|
import { basename, extname, join } from "node:path";
|
|
@@ -299,6 +298,48 @@ function countValueFromRow(value) {
|
|
|
299
298
|
return "0";
|
|
300
299
|
}
|
|
301
300
|
//#endregion
|
|
301
|
+
//#region src/shared/types.ts
|
|
302
|
+
/**
|
|
303
|
+
* Error thrown when `transaction()` is called on a transaction-scoped interface
|
|
304
|
+
* belonging to an adapter whose engine has no savepoints.
|
|
305
|
+
*
|
|
306
|
+
* DuckDB — and therefore the JSON adapter built on it — does not implement
|
|
307
|
+
* `SAVEPOINT`, so there is no way to re-enter the current transaction. Adapters
|
|
308
|
+
* that can re-enter (PostgreSQL, SQLite) do so under a savepoint and never
|
|
309
|
+
* throw this.
|
|
310
|
+
*
|
|
311
|
+
* The alternative would be to open a second, independent transaction, which is
|
|
312
|
+
* what these adapters used to do; on a shared connection that silently
|
|
313
|
+
* destroyed the enclosing transaction's uncommitted work.
|
|
314
|
+
*
|
|
315
|
+
* @example Ensuring a helper works on every adapter
|
|
316
|
+
* ```typescript
|
|
317
|
+
* // Take the transaction as a parameter rather than opening a nested one.
|
|
318
|
+
* async function recordClick(db: DatabaseInterface, id: string) {
|
|
319
|
+
* await db.update('links', { id }, { clicks: 1 });
|
|
320
|
+
* }
|
|
321
|
+
* await db.transaction(async (tx) => recordClick(tx, id));
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
var NestedTransactionError = class extends DatabaseError {
|
|
325
|
+
constructor(message, context) {
|
|
326
|
+
super(message, context);
|
|
327
|
+
this.name = "NestedTransactionError";
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
/**
|
|
331
|
+
* Resolves a SchemasOption to its concrete value.
|
|
332
|
+
*
|
|
333
|
+
* Adapters that need schemas (JSON, DuckDB) call this to unwrap the
|
|
334
|
+
* lazy function. Adapters that don't (Postgres, SQLite) simply ignore
|
|
335
|
+
* the `schemas` option without calling this.
|
|
336
|
+
*/
|
|
337
|
+
function resolveSchemas(schemas) {
|
|
338
|
+
if (!schemas) return void 0;
|
|
339
|
+
if (typeof schemas === "function") return schemas();
|
|
340
|
+
return schemas;
|
|
341
|
+
}
|
|
342
|
+
//#endregion
|
|
302
343
|
//#region src/json.ts
|
|
303
344
|
var json_exports = /* @__PURE__ */ __exportAll({
|
|
304
345
|
clearConnectionCache: () => clearConnectionCache,
|
|
@@ -1960,309 +2001,6 @@ async function derivePostgresConnectionCacheKey(options, effective) {
|
|
|
1960
2001
|
return `pg:hmac-sha256:v1:${Buffer.from(signature).toString("hex")}`;
|
|
1961
2002
|
}
|
|
1962
2003
|
/**
|
|
1963
|
-
* Operator for the given distance metric in pgvector
|
|
1964
|
-
*/
|
|
1965
|
-
function vectorOperator(metric) {
|
|
1966
|
-
switch (metric) {
|
|
1967
|
-
case "cosine": return "<=>";
|
|
1968
|
-
case "l2": return "<->";
|
|
1969
|
-
case "ip": return "<#>";
|
|
1970
|
-
default: return "<=>";
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
function getMaxPostgresParameterIndex(sql) {
|
|
1974
|
-
let maxIndex = 0;
|
|
1975
|
-
for (const match of sql.matchAll(/\$(\d+)/g)) {
|
|
1976
|
-
const index = Number(match[1]);
|
|
1977
|
-
if (Number.isSafeInteger(index) && index > maxIndex) maxIndex = index;
|
|
1978
|
-
}
|
|
1979
|
-
return maxIndex;
|
|
1980
|
-
}
|
|
1981
|
-
function usesSingleArrayParameter(sql) {
|
|
1982
|
-
const identifier = String.raw`(?:"[^"]+"|[a-z_][\w$]*)`;
|
|
1983
|
-
const qualifiedIdentifier = String.raw`${identifier}(?:\s*\.\s*${identifier})?`;
|
|
1984
|
-
const arrayType = String.raw`${qualifiedIdentifier}(?:\s*\([^)]*\))?\s*\[\]`;
|
|
1985
|
-
return new RegExp(String.raw`\$1\s*::\s*${arrayType}`, "i").test(sql) || new RegExp(String.raw`\bCAST\s*\(\s*\$1\s+AS\s+${arrayType}\s*\)`, "i").test(sql) || /\b(?:ANY|ALL|SOME)\s*\(\s*\$1\s*\)/i.test(sql);
|
|
1986
|
-
}
|
|
1987
|
-
function normalizeRawQueryValues(sql, values) {
|
|
1988
|
-
if (values.length !== 1 || !Array.isArray(values[0])) return values;
|
|
1989
|
-
const valuesArray = values[0];
|
|
1990
|
-
const maxParameterIndex = getMaxPostgresParameterIndex(sql);
|
|
1991
|
-
if (maxParameterIndex === 1 && usesSingleArrayParameter(sql)) return values;
|
|
1992
|
-
if (maxParameterIndex === 0 || maxParameterIndex === valuesArray.length) return valuesArray;
|
|
1993
|
-
return values;
|
|
1994
|
-
}
|
|
1995
|
-
function rewriteQuestionMarkPlaceholders(sql) {
|
|
1996
|
-
let output = "";
|
|
1997
|
-
let count = 0;
|
|
1998
|
-
let index = 0;
|
|
1999
|
-
const copyQuoted = (quote, escaped = false) => {
|
|
2000
|
-
output += quote;
|
|
2001
|
-
index += 1;
|
|
2002
|
-
while (index < sql.length) {
|
|
2003
|
-
const char = sql[index];
|
|
2004
|
-
output += char;
|
|
2005
|
-
index += 1;
|
|
2006
|
-
if (escaped && char === "\\" && index < sql.length) {
|
|
2007
|
-
output += sql[index];
|
|
2008
|
-
index += 1;
|
|
2009
|
-
continue;
|
|
2010
|
-
}
|
|
2011
|
-
if (char === quote) {
|
|
2012
|
-
if (sql[index] === quote) {
|
|
2013
|
-
output += sql[index];
|
|
2014
|
-
index += 1;
|
|
2015
|
-
continue;
|
|
2016
|
-
}
|
|
2017
|
-
break;
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
};
|
|
2021
|
-
const copyUntil = (end) => {
|
|
2022
|
-
const endIndex = sql.indexOf(end, index + end.length);
|
|
2023
|
-
if (endIndex === -1) {
|
|
2024
|
-
output += sql.slice(index);
|
|
2025
|
-
index = sql.length;
|
|
2026
|
-
return;
|
|
2027
|
-
}
|
|
2028
|
-
output += sql.slice(index, endIndex + end.length);
|
|
2029
|
-
index = endIndex + end.length;
|
|
2030
|
-
};
|
|
2031
|
-
while (index < sql.length) {
|
|
2032
|
-
const char = sql[index];
|
|
2033
|
-
const next = sql[index + 1];
|
|
2034
|
-
if ((char === "E" || char === "e") && next === "'") {
|
|
2035
|
-
output += char;
|
|
2036
|
-
index += 1;
|
|
2037
|
-
copyQuoted("'", true);
|
|
2038
|
-
continue;
|
|
2039
|
-
}
|
|
2040
|
-
if (char === "'") {
|
|
2041
|
-
copyQuoted("'");
|
|
2042
|
-
continue;
|
|
2043
|
-
}
|
|
2044
|
-
if (char === "\"") {
|
|
2045
|
-
copyQuoted("\"");
|
|
2046
|
-
continue;
|
|
2047
|
-
}
|
|
2048
|
-
if (char === "-" && next === "-") {
|
|
2049
|
-
const endIndex = sql.indexOf("\n", index + 2);
|
|
2050
|
-
if (endIndex === -1) {
|
|
2051
|
-
output += sql.slice(index);
|
|
2052
|
-
index = sql.length;
|
|
2053
|
-
} else {
|
|
2054
|
-
output += sql.slice(index, endIndex + 1);
|
|
2055
|
-
index = endIndex + 1;
|
|
2056
|
-
}
|
|
2057
|
-
continue;
|
|
2058
|
-
}
|
|
2059
|
-
if (char === "/" && next === "*") {
|
|
2060
|
-
copyUntil("*/");
|
|
2061
|
-
continue;
|
|
2062
|
-
}
|
|
2063
|
-
if (char === "$") {
|
|
2064
|
-
const tag = sql.slice(index).match(/^\$[A-Za-z_][\w$]*\$|^\$\$/);
|
|
2065
|
-
if (tag) {
|
|
2066
|
-
copyUntil(tag[0]);
|
|
2067
|
-
continue;
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
if (char === "?" && next !== "|" && next !== "&") {
|
|
2071
|
-
count += 1;
|
|
2072
|
-
output += `$${count}`;
|
|
2073
|
-
index += 1;
|
|
2074
|
-
continue;
|
|
2075
|
-
}
|
|
2076
|
-
output += char;
|
|
2077
|
-
index += 1;
|
|
2078
|
-
}
|
|
2079
|
-
return {
|
|
2080
|
-
sql: output,
|
|
2081
|
-
count
|
|
2082
|
-
};
|
|
2083
|
-
}
|
|
2084
|
-
function usesSingleQuestionArrayParameter(sql) {
|
|
2085
|
-
const identifier = String.raw`(?:"[^"]+"|[a-z_][\w$]*)`;
|
|
2086
|
-
const qualifiedIdentifier = String.raw`${identifier}(?:\s*\.\s*${identifier})?`;
|
|
2087
|
-
const arrayType = String.raw`${qualifiedIdentifier}(?:\s*\([^)]*\))?\s*\[\]`;
|
|
2088
|
-
return new RegExp(String.raw`\?\s*::\s*${arrayType}`, "i").test(sql) || new RegExp(String.raw`\bCAST\s*\(\s*\?\s+AS\s+${arrayType}\s*\)`, "i").test(sql) || /\b(?:ANY|ALL|SOME)\s*\(\s*\?\s*\)/i.test(sql);
|
|
2089
|
-
}
|
|
2090
|
-
function normalizeQuestionMarkQueryValues(sql, values, placeholderCount) {
|
|
2091
|
-
if (values.length !== 1 || !Array.isArray(values[0])) return values;
|
|
2092
|
-
if (placeholderCount === 1 && usesSingleQuestionArrayParameter(sql)) return values;
|
|
2093
|
-
if (placeholderCount === values[0].length) return values[0];
|
|
2094
|
-
return values;
|
|
2095
|
-
}
|
|
2096
|
-
function normalizePostgresRawQuery(sql, values) {
|
|
2097
|
-
if (getMaxPostgresParameterIndex(sql) > 0) return {
|
|
2098
|
-
sql,
|
|
2099
|
-
values: normalizeRawQueryValues(sql, values)
|
|
2100
|
-
};
|
|
2101
|
-
const rewritten = rewriteQuestionMarkPlaceholders(sql);
|
|
2102
|
-
const placeholderCount = rewritten.count;
|
|
2103
|
-
const queryValues = normalizeQuestionMarkQueryValues(sql, values, placeholderCount);
|
|
2104
|
-
if (placeholderCount > 0 && placeholderCount === queryValues.length) return {
|
|
2105
|
-
sql: rewritten.sql,
|
|
2106
|
-
values: queryValues
|
|
2107
|
-
};
|
|
2108
|
-
return {
|
|
2109
|
-
sql,
|
|
2110
|
-
values: normalizeRawQueryValues(sql, values)
|
|
2111
|
-
};
|
|
2112
|
-
}
|
|
2113
|
-
/**
|
|
2114
|
-
* pgvector index operator class for CREATE INDEX
|
|
2115
|
-
*/
|
|
2116
|
-
function vectorOpsClass(metric) {
|
|
2117
|
-
switch (metric) {
|
|
2118
|
-
case "cosine": return "vector_cosine_ops";
|
|
2119
|
-
case "l2": return "vector_l2_ops";
|
|
2120
|
-
case "ip": return "vector_ip_ops";
|
|
2121
|
-
default: return "vector_cosine_ops";
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
|
-
/**
|
|
2125
|
-
* Convert a number[] to a pgvector literal string: '[0.1,0.2,0.3]'
|
|
2126
|
-
*/
|
|
2127
|
-
function toVectorLiteral(embedding) {
|
|
2128
|
-
if (embedding.length === 0) throw new DatabaseError("Vector embedding must not be empty", {});
|
|
2129
|
-
for (let i = 0; i < embedding.length; i++) if (!Number.isFinite(embedding[i])) throw new DatabaseError("Vector embedding contains invalid value (NaN or Infinity)", {
|
|
2130
|
-
index: i,
|
|
2131
|
-
value: embedding[i]
|
|
2132
|
-
});
|
|
2133
|
-
return `[${embedding.join(",")}]`;
|
|
2134
|
-
}
|
|
2135
|
-
/**
|
|
2136
|
-
* Creates vector capabilities backed by pgvector for a PostgreSQL pool.
|
|
2137
|
-
*
|
|
2138
|
-
* @param pool - PostgreSQL connection pool
|
|
2139
|
-
* @returns VectorCapabilities implementation
|
|
2140
|
-
*/
|
|
2141
|
-
/**
|
|
2142
|
-
* `type` is interpolated into `USING <type>`, which cannot be parameterized and
|
|
2143
|
-
* is not an identifier the quoting helpers cover. `VectorIndexOptions.type`
|
|
2144
|
-
* constrains it at compile time only, so anything reaching this at runtime —
|
|
2145
|
-
* a JavaScript caller, or a `filter as VectorIndexOptions` on parsed JSON — has
|
|
2146
|
-
* to be checked against the values pgvector actually offers.
|
|
2147
|
-
*/
|
|
2148
|
-
var VECTOR_INDEX_TYPES = /* @__PURE__ */ new Set(["hnsw", "ivfflat"]);
|
|
2149
|
-
/**
|
|
2150
|
-
* `metric` needs the same treatment as `type`, for a less obvious reason.
|
|
2151
|
-
*
|
|
2152
|
-
* It never reaches SQL directly — `vectorOpsClass`/`vectorOperator` map it
|
|
2153
|
-
* through a closed switch with a safe default, so an unknown metric produces a
|
|
2154
|
-
* valid operator class. But it is also concatenated into the *generated index
|
|
2155
|
-
* name*, which is then interpolated into a quoted identifier, so a metric
|
|
2156
|
-
* carrying a `"` still ends that identifier. The safe-default mapping is what
|
|
2157
|
-
* makes this easy to miss: the part of the metric that looks dangerous is
|
|
2158
|
-
* handled, and the part that is actually dangerous is somewhere else.
|
|
2159
|
-
*/
|
|
2160
|
-
var VECTOR_METRICS = /* @__PURE__ */ new Set([
|
|
2161
|
-
"cosine",
|
|
2162
|
-
"l2",
|
|
2163
|
-
"ip"
|
|
2164
|
-
]);
|
|
2165
|
-
function createVectorCapabilities(pool) {
|
|
2166
|
-
return {
|
|
2167
|
-
async ensureColumn(table, column, dimensions) {
|
|
2168
|
-
if (!Number.isInteger(dimensions) || dimensions <= 0) throw new DatabaseError("Vector dimensions must be a positive integer", { dimensions });
|
|
2169
|
-
validateTableName$1(table);
|
|
2170
|
-
validateColumnName$1(column);
|
|
2171
|
-
try {
|
|
2172
|
-
await pool.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
2173
|
-
await pool.query(`ALTER TABLE "${table}" ADD COLUMN IF NOT EXISTS "${column}" vector(${dimensions})`);
|
|
2174
|
-
} catch (e) {
|
|
2175
|
-
throw new DatabaseError("Failed to ensure vector column", {
|
|
2176
|
-
table,
|
|
2177
|
-
column,
|
|
2178
|
-
dimensions,
|
|
2179
|
-
originalError: formatDbError(e)
|
|
2180
|
-
});
|
|
2181
|
-
}
|
|
2182
|
-
},
|
|
2183
|
-
async ensureIndex(table, column, options) {
|
|
2184
|
-
validateTableName$1(table);
|
|
2185
|
-
validateColumnName$1(column);
|
|
2186
|
-
const metric = options?.metric || "cosine";
|
|
2187
|
-
const indexType = options?.type || "hnsw";
|
|
2188
|
-
if (!VECTOR_INDEX_TYPES.has(indexType)) throw new DatabaseError("Unsupported vector index type", {
|
|
2189
|
-
table,
|
|
2190
|
-
column,
|
|
2191
|
-
type: indexType,
|
|
2192
|
-
supported: [...VECTOR_INDEX_TYPES]
|
|
2193
|
-
});
|
|
2194
|
-
if (!VECTOR_METRICS.has(metric)) throw new DatabaseError("Unsupported vector metric", {
|
|
2195
|
-
table,
|
|
2196
|
-
column,
|
|
2197
|
-
metric,
|
|
2198
|
-
supported: [...VECTOR_METRICS]
|
|
2199
|
-
});
|
|
2200
|
-
const opsClass = vectorOpsClass(metric);
|
|
2201
|
-
const indexName = `idx_${table}_${column}_${metric}_${indexType}`;
|
|
2202
|
-
try {
|
|
2203
|
-
await pool.query(`CREATE INDEX IF NOT EXISTS "${indexName}" ON "${table}" USING ${indexType} ("${column}" ${opsClass})`);
|
|
2204
|
-
} catch (e) {
|
|
2205
|
-
throw new DatabaseError("Failed to ensure vector index", {
|
|
2206
|
-
table,
|
|
2207
|
-
column,
|
|
2208
|
-
indexName,
|
|
2209
|
-
originalError: formatDbError(e)
|
|
2210
|
-
});
|
|
2211
|
-
}
|
|
2212
|
-
},
|
|
2213
|
-
async upsertVector(table, where, column, embedding) {
|
|
2214
|
-
validateTableName$1(table);
|
|
2215
|
-
validateColumnName$1(column);
|
|
2216
|
-
const keys = Object.keys(where);
|
|
2217
|
-
validateColumnNames(keys);
|
|
2218
|
-
const values = Object.values(where);
|
|
2219
|
-
const conditions = keys.map((key, i) => `"${key}" IS NOT DISTINCT FROM $${i + 1}`).join(" AND ");
|
|
2220
|
-
const vectorParam = `$${values.length + 1}`;
|
|
2221
|
-
try {
|
|
2222
|
-
await pool.query(`UPDATE "${table}" SET "${column}" = ${vectorParam}::vector WHERE ${conditions}`, [...values, toVectorLiteral(embedding)]);
|
|
2223
|
-
} catch (e) {
|
|
2224
|
-
throw new DatabaseError("Failed to upsert vector", {
|
|
2225
|
-
table,
|
|
2226
|
-
column,
|
|
2227
|
-
where,
|
|
2228
|
-
originalError: formatDbError(e)
|
|
2229
|
-
});
|
|
2230
|
-
}
|
|
2231
|
-
},
|
|
2232
|
-
async search(table, column, embedding, options) {
|
|
2233
|
-
validateTableName$1(table);
|
|
2234
|
-
validateColumnName$1(column);
|
|
2235
|
-
const metric = options?.metric || "cosine";
|
|
2236
|
-
const limit = options?.limit || 10;
|
|
2237
|
-
const op = vectorOperator(metric);
|
|
2238
|
-
const queryParams = [toVectorLiteral(embedding)];
|
|
2239
|
-
let whereClause = `"${column}" IS NOT NULL`;
|
|
2240
|
-
if (options?.where) {
|
|
2241
|
-
whereClause += ` AND (${options.where})`;
|
|
2242
|
-
if (options.params) queryParams.push(...options.params);
|
|
2243
|
-
}
|
|
2244
|
-
const limitParam = `$${queryParams.length + 1}`;
|
|
2245
|
-
queryParams.push(limit);
|
|
2246
|
-
const sql = `SELECT *, ("${column}" ${op} $1::vector) AS distance FROM "${table}" WHERE ${whereClause} ORDER BY "${column}" ${op} $1::vector LIMIT ${limitParam}`;
|
|
2247
|
-
try {
|
|
2248
|
-
return (await pool.query(sql, queryParams)).rows.map((row) => ({
|
|
2249
|
-
...row,
|
|
2250
|
-
id: row.id,
|
|
2251
|
-
distance: Number.parseFloat(row.distance)
|
|
2252
|
-
}));
|
|
2253
|
-
} catch (e) {
|
|
2254
|
-
throw new DatabaseError("Failed to execute vector search", {
|
|
2255
|
-
table,
|
|
2256
|
-
column,
|
|
2257
|
-
metric,
|
|
2258
|
-
limit,
|
|
2259
|
-
originalError: formatDbError(e)
|
|
2260
|
-
});
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
};
|
|
2264
|
-
}
|
|
2265
|
-
/**
|
|
2266
2004
|
* Creates a PostgreSQL database adapter
|
|
2267
2005
|
*
|
|
2268
2006
|
* Loads configuration from environment variables with backward compatibility:
|
|
@@ -2329,110 +2067,6 @@ async function createDatabase(config) {
|
|
|
2329
2067
|
};
|
|
2330
2068
|
const client = pool;
|
|
2331
2069
|
/**
|
|
2332
|
-
* Serializes a value for database storage
|
|
2333
|
-
* Converts objects and arrays to JSON strings
|
|
2334
|
-
* Converts Dates to ISO strings
|
|
2335
|
-
* Passes through primitives unchanged
|
|
2336
|
-
*/
|
|
2337
|
-
const serializeValue = (value) => {
|
|
2338
|
-
if (value === null || value === void 0) return value;
|
|
2339
|
-
if (value instanceof Date) return value.toISOString();
|
|
2340
|
-
if (Buffer.isBuffer(value) || ArrayBuffer.isView(value)) return value;
|
|
2341
|
-
if (typeof value === "object") return JSON.stringify(value);
|
|
2342
|
-
return value;
|
|
2343
|
-
};
|
|
2344
|
-
/**
|
|
2345
|
-
* Serializes all values in an object for database storage
|
|
2346
|
-
*/
|
|
2347
|
-
const serializeRecord = (record) => {
|
|
2348
|
-
const serialized = {};
|
|
2349
|
-
for (const [key, value] of Object.entries(record)) {
|
|
2350
|
-
if (value === void 0) continue;
|
|
2351
|
-
serialized[key] = serializeValue(value);
|
|
2352
|
-
}
|
|
2353
|
-
return serialized;
|
|
2354
|
-
};
|
|
2355
|
-
let serverVersionNumPromise;
|
|
2356
|
-
const nullsNotDistinctIndexCache = /* @__PURE__ */ new Map();
|
|
2357
|
-
const hasNullConflictValue = (conflictColumns, data) => conflictColumns.some((col) => data[col] === null);
|
|
2358
|
-
const validateUpsertConflictColumns = (table, conflictColumns, serializedData) => {
|
|
2359
|
-
const missingColumns = conflictColumns.filter((col) => !(col in serializedData));
|
|
2360
|
-
if (missingColumns.length > 0) throw new DatabaseError("Conflict columns missing from data", {
|
|
2361
|
-
table,
|
|
2362
|
-
conflictColumns,
|
|
2363
|
-
missingColumns,
|
|
2364
|
-
availableColumns: Object.keys(serializedData),
|
|
2365
|
-
hint: "All columns specified in ON CONFLICT must be present in the data being inserted. Undefined values should be replaced with null or an appropriate default."
|
|
2366
|
-
});
|
|
2367
|
-
};
|
|
2368
|
-
const getServerVersionNum = async () => {
|
|
2369
|
-
serverVersionNumPromise ??= client.query("SHOW server_version_num").then((result) => Number(result.rows[0]?.server_version_num) || 0);
|
|
2370
|
-
return serverVersionNumPromise;
|
|
2371
|
-
};
|
|
2372
|
-
const hasNativeNullsNotDistinctIndex = async (table, conflictColumns) => {
|
|
2373
|
-
if (await getServerVersionNum() < 15e4) return false;
|
|
2374
|
-
const cacheKey = `${table}:${conflictColumns.join(",")}`;
|
|
2375
|
-
let cached = nullsNotDistinctIndexCache.get(cacheKey);
|
|
2376
|
-
if (!cached) {
|
|
2377
|
-
cached = client.query(`
|
|
2378
|
-
SELECT 1
|
|
2379
|
-
FROM pg_index i
|
|
2380
|
-
WHERE i.indrelid = to_regclass($1)
|
|
2381
|
-
AND i.indisunique
|
|
2382
|
-
AND i.indnullsnotdistinct
|
|
2383
|
-
AND ARRAY(
|
|
2384
|
-
SELECT a.attname::text
|
|
2385
|
-
FROM unnest(i.indkey) WITH ORDINALITY AS cols(attnum, ord)
|
|
2386
|
-
JOIN pg_attribute a
|
|
2387
|
-
ON a.attrelid = i.indrelid
|
|
2388
|
-
AND a.attnum = cols.attnum
|
|
2389
|
-
ORDER BY cols.ord
|
|
2390
|
-
) = $2::text[]
|
|
2391
|
-
LIMIT 1
|
|
2392
|
-
`, [table, conflictColumns]).then((result) => result.rows.length > 0).catch(() => false);
|
|
2393
|
-
nullsNotDistinctIndexCache.set(cacheKey, cached);
|
|
2394
|
-
}
|
|
2395
|
-
return cached;
|
|
2396
|
-
};
|
|
2397
|
-
const buildPostgresUpsertParts = (conflictColumns, serializedData) => {
|
|
2398
|
-
const keys = Object.keys(serializedData);
|
|
2399
|
-
const columns = keys.map(quotePostgresColumnName);
|
|
2400
|
-
return {
|
|
2401
|
-
columns,
|
|
2402
|
-
values: Object.values(serializedData),
|
|
2403
|
-
placeholders: keys.map((_, i) => `$${i + 1}`).join(", "),
|
|
2404
|
-
updateSet: columns.map((column, i) => `${column} = $${i + 1}`).join(", "),
|
|
2405
|
-
conflict: conflictColumns.map(quotePostgresColumnName).join(", ")
|
|
2406
|
-
};
|
|
2407
|
-
};
|
|
2408
|
-
const executeStandardPostgresUpsert = async (executor, table, conflictColumns, serializedData) => {
|
|
2409
|
-
const { columns, values, placeholders, updateSet, conflict } = buildPostgresUpsertParts(conflictColumns, serializedData);
|
|
2410
|
-
const sql = `INSERT INTO ${table} (${columns.join(", ")}) VALUES (${placeholders}) ON CONFLICT(${conflict}) DO UPDATE SET ${updateSet}`;
|
|
2411
|
-
return {
|
|
2412
|
-
operation: "upsert",
|
|
2413
|
-
affected: (await executor.query(sql, values)).rowCount ?? 0
|
|
2414
|
-
};
|
|
2415
|
-
};
|
|
2416
|
-
const executeNullAwarePostgresUpsert = async (executor, table, conflictColumns, serializedData) => {
|
|
2417
|
-
const { columns, values, placeholders, updateSet } = buildPostgresUpsertParts(conflictColumns, serializedData);
|
|
2418
|
-
const lockKey = conflictColumns.map((col) => `${col}:${JSON.stringify(serializedData[col])}`).join("|");
|
|
2419
|
-
await executor.query("SELECT pg_advisory_xact_lock(hashtext($1), hashtext($2))", [`@happyvertical/sql.upsert:${table}`, lockKey]);
|
|
2420
|
-
const whereStart = values.length + 1;
|
|
2421
|
-
const whereClause = conflictColumns.map((col, i) => `${quotePostgresColumnName(col)} IS NOT DISTINCT FROM $${whereStart + i}`).join(" AND ");
|
|
2422
|
-
const whereValues = conflictColumns.map((col) => serializedData[col]);
|
|
2423
|
-
const updateSql = `UPDATE ${table} SET ${updateSet} WHERE ${whereClause}`;
|
|
2424
|
-
const updateResult = await executor.query(updateSql, [...values, ...whereValues]);
|
|
2425
|
-
if ((updateResult.rowCount ?? 0) > 0) return {
|
|
2426
|
-
operation: "upsert",
|
|
2427
|
-
affected: updateResult.rowCount ?? 0
|
|
2428
|
-
};
|
|
2429
|
-
const insertSql = `INSERT INTO ${table} (${columns.join(", ")}) VALUES (${placeholders})`;
|
|
2430
|
-
return {
|
|
2431
|
-
operation: "upsert",
|
|
2432
|
-
affected: (await executor.query(insertSql, values)).rowCount ?? 0
|
|
2433
|
-
};
|
|
2434
|
-
};
|
|
2435
|
-
/**
|
|
2436
2070
|
* Absorbs 'error' events for the clients currently checked out for a
|
|
2437
2071
|
* transaction, keyed by client so the listener can be removed on release.
|
|
2438
2072
|
*/
|
|
@@ -2508,48 +2142,8 @@ async function createDatabase(config) {
|
|
|
2508
2142
|
* implicit rollback look successful.
|
|
2509
2143
|
*/
|
|
2510
2144
|
const createTransactionExecutor = (txClient) => {
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
const pending = /* @__PURE__ */ new Set();
|
|
2514
|
-
const isRollbackToSavepoint = (query) => {
|
|
2515
|
-
const sql = typeof query === "string" ? query : typeof query === "object" && query !== null && "text" in query && typeof query.text === "string" ? query.text : void 0;
|
|
2516
|
-
if (sql === void 0) return false;
|
|
2517
|
-
let statement = sql.trimStart();
|
|
2518
|
-
while (statement.startsWith("--") || statement.startsWith("/*")) {
|
|
2519
|
-
if (statement.startsWith("--")) {
|
|
2520
|
-
const newline = statement.search(/[\r\n]/);
|
|
2521
|
-
if (newline === -1) return false;
|
|
2522
|
-
statement = statement.slice(newline + 1).trimStart();
|
|
2523
|
-
continue;
|
|
2524
|
-
}
|
|
2525
|
-
const commentEnd = statement.indexOf("*/", 2);
|
|
2526
|
-
if (commentEnd === -1) return false;
|
|
2527
|
-
statement = statement.slice(commentEnd + 2).trimStart();
|
|
2528
|
-
}
|
|
2529
|
-
return /^ROLLBACK\s+TO(?:\s+SAVEPOINT)?\s+/i.test(statement);
|
|
2530
|
-
};
|
|
2531
|
-
const errorCode = (error) => {
|
|
2532
|
-
if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") return error.code;
|
|
2533
|
-
};
|
|
2534
|
-
const preserveFirstError = (error) => {
|
|
2535
|
-
if (errorCode(error) === "25P02" && firstError !== void 0) return firstError;
|
|
2536
|
-
if (errorCode(error) === "25P02" && firstUnconfirmedError !== void 0) return firstUnconfirmedError;
|
|
2537
|
-
if (error instanceof DatabaseError$1) firstError ??= error;
|
|
2538
|
-
else firstUnconfirmedError ??= error;
|
|
2539
|
-
return error;
|
|
2540
|
-
};
|
|
2541
|
-
const track = (operation, query) => {
|
|
2542
|
-
const tracked = operation.then((result) => {
|
|
2543
|
-
if (isRollbackToSavepoint(query)) firstError = void 0;
|
|
2544
|
-
firstUnconfirmedError = void 0;
|
|
2545
|
-
return result;
|
|
2546
|
-
}).catch((error) => {
|
|
2547
|
-
throw preserveFirstError(error);
|
|
2548
|
-
});
|
|
2549
|
-
pending.add(tracked);
|
|
2550
|
-
tracked.finally(() => pending.delete(tracked)).catch(() => {});
|
|
2551
|
-
return tracked;
|
|
2552
|
-
};
|
|
2145
|
+
const tracker = createTransactionErrorTracker((error) => error instanceof DatabaseError$1);
|
|
2146
|
+
const { pending, preserve: preserveFirstError } = tracker;
|
|
2553
2147
|
return {
|
|
2554
2148
|
client: new Proxy(txClient, { get(target, property) {
|
|
2555
2149
|
if (property === "query") return (...args) => {
|
|
@@ -2584,8 +2178,7 @@ async function createDatabase(config) {
|
|
|
2584
2178
|
query.handleReadyForQuery = function(...handlerArgs) {
|
|
2585
2179
|
try {
|
|
2586
2180
|
const result = Reflect.apply(handleReadyForQuery, this, handlerArgs);
|
|
2587
|
-
|
|
2588
|
-
firstUnconfirmedError = void 0;
|
|
2181
|
+
tracker.confirm(query);
|
|
2589
2182
|
return result;
|
|
2590
2183
|
} finally {
|
|
2591
2184
|
finish();
|
|
@@ -2607,10 +2200,7 @@ async function createDatabase(config) {
|
|
|
2607
2200
|
pending.add(completion);
|
|
2608
2201
|
args[callbackIndex] = (error, result) => {
|
|
2609
2202
|
const callbackError = error === null || error === void 0 ? error : preserveFirstError(error);
|
|
2610
|
-
if (callbackError === null || callbackError === void 0)
|
|
2611
|
-
if (isRollbackToSavepoint(query)) firstError = void 0;
|
|
2612
|
-
firstUnconfirmedError = void 0;
|
|
2613
|
-
}
|
|
2203
|
+
if (callbackError === null || callbackError === void 0) tracker.confirm(query);
|
|
2614
2204
|
pending.delete(completion);
|
|
2615
2205
|
complete();
|
|
2616
2206
|
callback(callbackError, result);
|
|
@@ -2624,7 +2214,7 @@ async function createDatabase(config) {
|
|
|
2624
2214
|
}
|
|
2625
2215
|
}
|
|
2626
2216
|
const result = Reflect.apply(target.query, target, args);
|
|
2627
|
-
if (typeof result === "object" && result !== null && "then" in result && typeof result.then === "function") return track(Promise.resolve(result), query);
|
|
2217
|
+
if (typeof result === "object" && result !== null && "then" in result && typeof result.then === "function") return tracker.track(Promise.resolve(result), query);
|
|
2628
2218
|
if (typeof result === "object" && result !== null && "once" in result && typeof result.once === "function") {
|
|
2629
2219
|
let complete;
|
|
2630
2220
|
const completion = new Promise((resolve) => {
|
|
@@ -2640,8 +2230,7 @@ async function createDatabase(config) {
|
|
|
2640
2230
|
finish();
|
|
2641
2231
|
});
|
|
2642
2232
|
result.once("end", () => {
|
|
2643
|
-
|
|
2644
|
-
firstUnconfirmedError = void 0;
|
|
2233
|
+
tracker.confirm(query);
|
|
2645
2234
|
finish();
|
|
2646
2235
|
});
|
|
2647
2236
|
result.once("close", finish);
|
|
@@ -2650,15 +2239,11 @@ async function createDatabase(config) {
|
|
|
2650
2239
|
};
|
|
2651
2240
|
return Reflect.get(target, property, target);
|
|
2652
2241
|
} }),
|
|
2653
|
-
executor: { query: (sql, values) => track(txClient.query(sql, values), sql) },
|
|
2654
|
-
clearError:
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
while (pending.size > 0) await Promise.allSettled([...pending]);
|
|
2659
|
-
},
|
|
2660
|
-
getError: () => firstError,
|
|
2661
|
-
getUnconfirmedError: () => firstUnconfirmedError
|
|
2242
|
+
executor: { query: (sql, values) => tracker.track(txClient.query(sql, values), sql) },
|
|
2243
|
+
clearError: tracker.clearError,
|
|
2244
|
+
drain: tracker.drain,
|
|
2245
|
+
getError: tracker.getError,
|
|
2246
|
+
getUnconfirmedError: tracker.getUnconfirmedError
|
|
2662
2247
|
};
|
|
2663
2248
|
};
|
|
2664
2249
|
const assertCommitted = (result, transactionState) => {
|
|
@@ -2667,733 +2252,124 @@ async function createDatabase(config) {
|
|
|
2667
2252
|
if (queryError !== void 0) throw queryError;
|
|
2668
2253
|
throw new DatabaseError("PostgreSQL rolled back the transaction at commit", { command: result.command });
|
|
2669
2254
|
};
|
|
2670
|
-
const
|
|
2255
|
+
const { serializeRecord, createClientMethods, createNestedTransaction } = createPostgresDialect({
|
|
2256
|
+
client,
|
|
2257
|
+
probeExecutor: () => client,
|
|
2258
|
+
runInTransaction: async (work) => {
|
|
2259
|
+
const txClient = await acquireTxClient();
|
|
2260
|
+
try {
|
|
2261
|
+
await txClient.query("BEGIN");
|
|
2262
|
+
const result = await work(txClient);
|
|
2263
|
+
const commitResult = await txClient.query("COMMIT");
|
|
2264
|
+
assertCommitted(commitResult);
|
|
2265
|
+
releaseTxClient(txClient);
|
|
2266
|
+
return result;
|
|
2267
|
+
} catch (error) {
|
|
2268
|
+
const rollbackError = await discardTxClient(txClient);
|
|
2269
|
+
if (rollbackError !== void 0 && error instanceof Error) error.cause ??= rollbackError;
|
|
2270
|
+
throw error;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
});
|
|
2274
|
+
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);
|
|
2275
|
+
const getTableSchema = createGetTableSchema({
|
|
2276
|
+
many,
|
|
2277
|
+
tableExists
|
|
2278
|
+
});
|
|
2279
|
+
const alterTable = createAlterTable(client);
|
|
2280
|
+
/**
|
|
2281
|
+
* Acquire a pinned connection from the pool for session-scoped state such as
|
|
2282
|
+
* `pg_advisory_lock`. Every query on the returned handle runs on the same
|
|
2283
|
+
* client until {@link SessionHandle.release} drops the connection (which
|
|
2284
|
+
* frees any session locks it held). The connection is held for the handle's
|
|
2285
|
+
* lifetime, so hold one session per process, not per operation.
|
|
2286
|
+
*/
|
|
2287
|
+
const acquireSession = async () => {
|
|
2288
|
+
const sessionClient = await pool.connect();
|
|
2289
|
+
let released = false;
|
|
2290
|
+
let lost = false;
|
|
2291
|
+
sessionClient.on("error", () => {
|
|
2292
|
+
lost = true;
|
|
2293
|
+
});
|
|
2294
|
+
return {
|
|
2295
|
+
query: async (sql, ...values) => {
|
|
2296
|
+
if (released) throw new DatabaseError("Session has been released", {});
|
|
2297
|
+
if (lost) throw new DatabaseError("Session connection was lost", {});
|
|
2298
|
+
const normalized = normalizePostgresRawQuery(sql, values);
|
|
2299
|
+
try {
|
|
2300
|
+
const result = await sessionClient.query(normalized.sql, normalized.values);
|
|
2301
|
+
return {
|
|
2302
|
+
rows: result.rows,
|
|
2303
|
+
rowCount: result.rowCount ?? 0
|
|
2304
|
+
};
|
|
2305
|
+
} catch (e) {
|
|
2306
|
+
throw wrapDatabaseError("Failed to execute session query", e, {
|
|
2307
|
+
sql: normalized.sql,
|
|
2308
|
+
values: normalized.values
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
},
|
|
2312
|
+
isActive: () => !released && !lost,
|
|
2313
|
+
release: async () => {
|
|
2314
|
+
if (released) return;
|
|
2315
|
+
released = true;
|
|
2316
|
+
if (!lost) {
|
|
2317
|
+
try {
|
|
2318
|
+
await sessionClient.query("ROLLBACK");
|
|
2319
|
+
} catch {}
|
|
2320
|
+
try {
|
|
2321
|
+
await sessionClient.query("SELECT pg_advisory_unlock_all()");
|
|
2322
|
+
} catch {}
|
|
2323
|
+
}
|
|
2324
|
+
sessionClient.release(true);
|
|
2325
|
+
}
|
|
2326
|
+
};
|
|
2327
|
+
};
|
|
2328
|
+
/**
|
|
2329
|
+
* Executes a callback within a database transaction
|
|
2330
|
+
* Automatically commits on success or rolls back on error
|
|
2331
|
+
*
|
|
2332
|
+
* @param callback - Function to execute within transaction
|
|
2333
|
+
* @returns Promise resolving to callback result
|
|
2334
|
+
*/
|
|
2335
|
+
const transaction = async (callback) => {
|
|
2671
2336
|
const txClient = await acquireTxClient();
|
|
2337
|
+
let nestedTransaction;
|
|
2338
|
+
const transactionState = createTransactionExecutor(txClient);
|
|
2672
2339
|
try {
|
|
2673
2340
|
await txClient.query("BEGIN");
|
|
2674
|
-
|
|
2341
|
+
let txDb;
|
|
2342
|
+
nestedTransaction = createNestedTransaction(transactionState.executor, () => txDb, transactionState.clearError, new AsyncLocalStorage());
|
|
2343
|
+
txDb = {
|
|
2344
|
+
url,
|
|
2345
|
+
client: transactionState.client,
|
|
2346
|
+
...createClientMethods(transactionState.executor, true, serializeRecord, transactionState.clearError),
|
|
2347
|
+
transaction: nestedTransaction
|
|
2348
|
+
};
|
|
2349
|
+
const result = await callback(txDb);
|
|
2350
|
+
await nestedTransaction.drain();
|
|
2351
|
+
await transactionState.drain();
|
|
2352
|
+
const statementError = transactionState.getError();
|
|
2353
|
+
if (statementError !== void 0) throw statementError;
|
|
2675
2354
|
const commitResult = await txClient.query("COMMIT");
|
|
2676
|
-
assertCommitted(commitResult);
|
|
2355
|
+
assertCommitted(commitResult, transactionState);
|
|
2677
2356
|
releaseTxClient(txClient);
|
|
2678
2357
|
return result;
|
|
2679
2358
|
} catch (error) {
|
|
2359
|
+
await nestedTransaction?.drain();
|
|
2360
|
+
await transactionState.drain();
|
|
2680
2361
|
const rollbackError = await discardTxClient(txClient);
|
|
2681
2362
|
if (rollbackError !== void 0 && error instanceof Error) error.cause ??= rollbackError;
|
|
2682
2363
|
throw error;
|
|
2683
2364
|
}
|
|
2684
2365
|
};
|
|
2685
|
-
const executePostgresUpsert = async (executor, table, conflictColumns, serializedData, options, acquireTransaction) => {
|
|
2686
|
-
validateUpsertConflictColumns(table, conflictColumns, serializedData);
|
|
2687
|
-
if (options?.nullsDistinct || !hasNullConflictValue(conflictColumns, serializedData)) return executeStandardPostgresUpsert(executor, table, conflictColumns, serializedData);
|
|
2688
|
-
if (await hasNativeNullsNotDistinctIndex(table, conflictColumns)) return executeStandardPostgresUpsert(executor, table, conflictColumns, serializedData);
|
|
2689
|
-
if (!acquireTransaction) return executeNullAwarePostgresUpsert(executor, table, conflictColumns, serializedData);
|
|
2690
|
-
return executeNullAwarePostgresUpsertInTransaction(table, conflictColumns, serializedData);
|
|
2691
|
-
};
|
|
2692
|
-
/**
|
|
2693
|
-
* Parses a tagged template literal into a SQL query and values
|
|
2694
|
-
*
|
|
2695
|
-
* @param strings - Template strings
|
|
2696
|
-
* @param vars - Variables to interpolate into the query
|
|
2697
|
-
* @returns Object with SQL query and values array
|
|
2698
|
-
*/
|
|
2699
|
-
const parseTemplate = (strings, ...vars) => {
|
|
2700
|
-
let sql = strings[0];
|
|
2701
|
-
const values = [];
|
|
2702
|
-
for (let i = 0; i < vars.length; i++) {
|
|
2703
|
-
values.push(vars[i]);
|
|
2704
|
-
sql += `$${i + 1}${strings[i + 1]}`;
|
|
2705
|
-
}
|
|
2706
|
-
return {
|
|
2707
|
-
sql,
|
|
2708
|
-
values
|
|
2709
|
-
};
|
|
2710
|
-
};
|
|
2711
2366
|
/**
|
|
2712
|
-
*
|
|
2713
|
-
*
|
|
2714
|
-
* The pool-backed interface and every transaction-scoped interface are
|
|
2715
|
-
* instantiated from this one factory, so there is exactly one implementation
|
|
2716
|
-
* of each method. The transaction interfaces used to be hand-maintained
|
|
2717
|
-
* copies of these bodies, and both had drifted:
|
|
2718
|
-
*
|
|
2719
|
-
* - they called the client bare, so a failure inside a transaction threw the
|
|
2720
|
-
* raw `pg` error rather than `DatabaseError`, and `instanceof` answered
|
|
2721
|
-
* differently depending on whether the call was in a transaction. `pg`
|
|
2722
|
-
* exports its own class also named `DatabaseError`, so nothing a developer
|
|
2723
|
-
* would check by eye showed the difference (#1115);
|
|
2724
|
-
* - they re-exposed `tableExists` and `syncSchema` from the enclosing scope,
|
|
2725
|
-
* which close over the *pool*. Both therefore ran on a different connection
|
|
2726
|
-
* than the transaction, so `tx.tableExists()` could not see a table created
|
|
2727
|
-
* in the same transaction and `tx.syncSchema()` committed its DDL
|
|
2728
|
-
* immediately, surviving a rollback (#1111).
|
|
2367
|
+
* Begins a new transaction and returns a handle for manual control
|
|
2729
2368
|
*
|
|
2730
|
-
*
|
|
2731
|
-
*
|
|
2369
|
+
* Unlike transaction(), this gives you explicit control over commit/rollback.
|
|
2370
|
+
* Ideal for test isolation where you want to rollback after each test.
|
|
2732
2371
|
*
|
|
2733
|
-
* @
|
|
2734
|
-
* @param inTransaction - Whether `executor` is already inside a transaction
|
|
2735
|
-
* @param serialize - Applied to each record before `insert`/`update` builds
|
|
2736
|
-
* its SQL
|
|
2737
|
-
* @param clearTransactionError - Clears a statement error after a savepoint
|
|
2738
|
-
* has successfully restored the transaction to a usable state
|
|
2739
|
-
*/
|
|
2740
|
-
const createClientMethods = (executor, inTransaction, serialize, clearTransactionError = () => {}) => {
|
|
2741
|
-
/**
|
|
2742
|
-
* Inserts one or more records into a table
|
|
2743
|
-
*
|
|
2744
|
-
* @param table - Table name
|
|
2745
|
-
* @param data - Single record or array of records to insert
|
|
2746
|
-
* @returns Promise resolving to operation result
|
|
2747
|
-
* @throws Error if the insert operation fails
|
|
2748
|
-
*
|
|
2749
|
-
* @example Single record insert:
|
|
2750
|
-
* ```typescript
|
|
2751
|
-
* await db.insert('users', {
|
|
2752
|
-
* name: 'John Doe',
|
|
2753
|
-
* email: 'john@example.com'
|
|
2754
|
-
* });
|
|
2755
|
-
* ```
|
|
2756
|
-
*
|
|
2757
|
-
* @example Multiple record insert:
|
|
2758
|
-
* ```typescript
|
|
2759
|
-
* await db.insert('users', [
|
|
2760
|
-
* { name: 'John', email: 'john@example.com' },
|
|
2761
|
-
* { name: 'Jane', email: 'jane@example.com' }
|
|
2762
|
-
* ]);
|
|
2763
|
-
* ```
|
|
2764
|
-
*/
|
|
2765
|
-
const insert = async (table, data) => {
|
|
2766
|
-
validateTableName$1(table);
|
|
2767
|
-
if (Array.isArray(data)) {
|
|
2768
|
-
if (data.length === 0) return {
|
|
2769
|
-
operation: "insert",
|
|
2770
|
-
affected: 0
|
|
2771
|
-
};
|
|
2772
|
-
const serializedRecords = data.map((record) => serialize(record));
|
|
2773
|
-
const keys = resolveInsertColumns(table, serializedRecords);
|
|
2774
|
-
validateColumnNames(keys);
|
|
2775
|
-
const placeholders = serializedRecords.map((_, i) => `(${keys.map((_, j) => `$${i * keys.length + j + 1}`).join(", ")})`).join(", ");
|
|
2776
|
-
const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
|
|
2777
|
-
const values = serializedRecords.flatMap((row) => keys.map((key) => row[key]));
|
|
2778
|
-
return {
|
|
2779
|
-
operation: "insert",
|
|
2780
|
-
affected: (await executor.query(query, values)).rowCount ?? 0
|
|
2781
|
-
};
|
|
2782
|
-
}
|
|
2783
|
-
const serializedData = serialize(data);
|
|
2784
|
-
const keys = Object.keys(serializedData);
|
|
2785
|
-
validateColumnNames(keys);
|
|
2786
|
-
const values = Object.values(serializedData);
|
|
2787
|
-
const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
|
|
2788
|
-
const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
|
|
2789
|
-
return {
|
|
2790
|
-
operation: "insert",
|
|
2791
|
-
affected: (await executor.query(query, values)).rowCount ?? 0
|
|
2792
|
-
};
|
|
2793
|
-
};
|
|
2794
|
-
/**
|
|
2795
|
-
* Retrieves a single record matching the where criteria
|
|
2796
|
-
*
|
|
2797
|
-
* @param table - Table name
|
|
2798
|
-
* @param where - Criteria to match records
|
|
2799
|
-
* @returns Promise resolving to query result
|
|
2800
|
-
*/
|
|
2801
|
-
const get = async (table, where) => {
|
|
2802
|
-
validateTableName$1(table);
|
|
2803
|
-
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2804
|
-
if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
|
|
2805
|
-
const query = `SELECT * FROM ${table} ${whereClause}`;
|
|
2806
|
-
try {
|
|
2807
|
-
return (await executor.query(query, values)).rows[0] || null;
|
|
2808
|
-
} catch (e) {
|
|
2809
|
-
throw new DatabaseError("Failed to retrieve record from table", {
|
|
2810
|
-
table,
|
|
2811
|
-
sql: query,
|
|
2812
|
-
values,
|
|
2813
|
-
originalError: formatDbError(e)
|
|
2814
|
-
});
|
|
2815
|
-
}
|
|
2816
|
-
};
|
|
2817
|
-
/**
|
|
2818
|
-
* Retrieves multiple records matching the where criteria
|
|
2819
|
-
*
|
|
2820
|
-
* @param table - Table name
|
|
2821
|
-
* @param where - Criteria to match records
|
|
2822
|
-
* @returns Promise resolving to array of records
|
|
2823
|
-
*/
|
|
2824
|
-
const list = async (table, where) => {
|
|
2825
|
-
validateTableName$1(table);
|
|
2826
|
-
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2827
|
-
const query = `SELECT * FROM ${table} ${whereClause}`;
|
|
2828
|
-
try {
|
|
2829
|
-
return (await executor.query(query, values)).rows;
|
|
2830
|
-
} catch (e) {
|
|
2831
|
-
throw new DatabaseError("Failed to list records from table", {
|
|
2832
|
-
table,
|
|
2833
|
-
sql: query,
|
|
2834
|
-
values,
|
|
2835
|
-
originalError: formatDbError(e)
|
|
2836
|
-
});
|
|
2837
|
-
}
|
|
2838
|
-
};
|
|
2839
|
-
/**
|
|
2840
|
-
* Updates records matching the where criteria
|
|
2841
|
-
*
|
|
2842
|
-
* @param table - Table name
|
|
2843
|
-
* @param where - Criteria to match records to update
|
|
2844
|
-
* @param data - New data to set
|
|
2845
|
-
* @returns Promise resolving to operation result
|
|
2846
|
-
*/
|
|
2847
|
-
const update = async (table, where, data) => {
|
|
2848
|
-
validateTableName$1(table);
|
|
2849
|
-
const serializedData = serialize(data);
|
|
2850
|
-
const keys = Object.keys(serializedData);
|
|
2851
|
-
validateColumnNames(keys);
|
|
2852
|
-
const values = Object.values(serializedData);
|
|
2853
|
-
const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
|
|
2854
|
-
const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "postgres");
|
|
2855
|
-
if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
|
|
2856
|
-
const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
|
|
2857
|
-
try {
|
|
2858
|
-
return {
|
|
2859
|
-
operation: "update",
|
|
2860
|
-
affected: (await executor.query(sql, [...values, ...whereValues])).rowCount ?? 0
|
|
2861
|
-
};
|
|
2862
|
-
} catch (e) {
|
|
2863
|
-
throw new DatabaseError("Failed to update records in table", {
|
|
2864
|
-
table,
|
|
2865
|
-
sql,
|
|
2866
|
-
values: [...values, ...whereValues],
|
|
2867
|
-
originalError: formatDbError(e)
|
|
2868
|
-
});
|
|
2869
|
-
}
|
|
2870
|
-
};
|
|
2871
|
-
/**
|
|
2872
|
-
* Inserts a record or updates it if it already exists (UPSERT)
|
|
2873
|
-
*
|
|
2874
|
-
* @param table - Table name
|
|
2875
|
-
* @param conflictColumns - Columns that define the uniqueness constraint
|
|
2876
|
-
* @param data - Data to insert or update
|
|
2877
|
-
* @returns Promise resolving to operation result
|
|
2878
|
-
* @throws Error if the upsert operation fails
|
|
2879
|
-
*/
|
|
2880
|
-
const upsert = async (table, conflictColumns, data, options) => {
|
|
2881
|
-
validateTableName$1(table);
|
|
2882
|
-
const serializedData = serializeRecord(data);
|
|
2883
|
-
const conflictCols = [...conflictColumns];
|
|
2884
|
-
validateColumnNames([...conflictCols, ...Object.keys(serializedData)]);
|
|
2885
|
-
try {
|
|
2886
|
-
return await executePostgresUpsert(executor, table, conflictCols, serializedData, options, !inTransaction);
|
|
2887
|
-
} catch (e) {
|
|
2888
|
-
if (e instanceof DatabaseError && e.message === "Conflict columns missing from data") throw e;
|
|
2889
|
-
throw new DatabaseError("Failed to upsert record into table", {
|
|
2890
|
-
table,
|
|
2891
|
-
values: data,
|
|
2892
|
-
conflictColumns,
|
|
2893
|
-
originalError: formatDbError(e)
|
|
2894
|
-
});
|
|
2895
|
-
}
|
|
2896
|
-
};
|
|
2897
|
-
/**
|
|
2898
|
-
* Gets a record matching the where criteria or inserts it if not found
|
|
2899
|
-
*
|
|
2900
|
-
* @param table - Table name
|
|
2901
|
-
* @param where - Criteria to match existing record
|
|
2902
|
-
* @returns Promise resolving to the query result or insert result
|
|
2903
|
-
*/
|
|
2904
|
-
const getOrInsert = async (table, where, data) => {
|
|
2905
|
-
validateTableName$1(table);
|
|
2906
|
-
const result = await get(table, where);
|
|
2907
|
-
if (result) return result;
|
|
2908
|
-
await insert(table, data);
|
|
2909
|
-
const inserted = await get(table, where);
|
|
2910
|
-
if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
|
|
2911
|
-
table,
|
|
2912
|
-
where,
|
|
2913
|
-
data
|
|
2914
|
-
});
|
|
2915
|
-
return inserted;
|
|
2916
|
-
};
|
|
2917
|
-
/**
|
|
2918
|
-
* Deletes records from a table matching the where criteria
|
|
2919
|
-
*
|
|
2920
|
-
* @param table - Table name
|
|
2921
|
-
* @param where - Criteria to match records for deletion
|
|
2922
|
-
* @returns Promise resolving to operation result with count of deleted rows
|
|
2923
|
-
* @throws Error if the delete operation fails
|
|
2924
|
-
*/
|
|
2925
|
-
const deleteRecords = async (table, where) => {
|
|
2926
|
-
validateTableName$1(table);
|
|
2927
|
-
if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
|
|
2928
|
-
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2929
|
-
try {
|
|
2930
|
-
return {
|
|
2931
|
-
operation: "delete",
|
|
2932
|
-
affected: (await executor.query(`DELETE FROM ${table} ${whereClause}`, values)).rowCount ?? 0
|
|
2933
|
-
};
|
|
2934
|
-
} catch (e) {
|
|
2935
|
-
throw new DatabaseError("Failed to delete records from table", {
|
|
2936
|
-
table,
|
|
2937
|
-
where,
|
|
2938
|
-
originalError: formatDbError(e)
|
|
2939
|
-
});
|
|
2940
|
-
}
|
|
2941
|
-
};
|
|
2942
|
-
/**
|
|
2943
|
-
* Counts records in a table matching the where criteria
|
|
2944
|
-
*
|
|
2945
|
-
* @param table - Table name
|
|
2946
|
-
* @param where - Criteria to match records (optional, counts all if omitted)
|
|
2947
|
-
* @returns Promise resolving to count of matching records
|
|
2948
|
-
* @throws Error if the count operation fails
|
|
2949
|
-
*/
|
|
2950
|
-
const count = async (table, where) => {
|
|
2951
|
-
validateTableName$1(table);
|
|
2952
|
-
try {
|
|
2953
|
-
if (!where || Object.keys(where).length === 0) {
|
|
2954
|
-
const result = await executor.query(`SELECT COUNT(*) as count FROM ${table}`);
|
|
2955
|
-
return Number(result.rows[0]?.count) || 0;
|
|
2956
|
-
}
|
|
2957
|
-
const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
|
|
2958
|
-
const result = await executor.query(`SELECT COUNT(*) as count FROM ${table} ${whereClause}`, values);
|
|
2959
|
-
return Number(result.rows[0]?.count) || 0;
|
|
2960
|
-
} catch (e) {
|
|
2961
|
-
throw new DatabaseError("Failed to count records in table", {
|
|
2962
|
-
table,
|
|
2963
|
-
where,
|
|
2964
|
-
originalError: formatDbError(e)
|
|
2965
|
-
});
|
|
2966
|
-
}
|
|
2967
|
-
};
|
|
2968
|
-
/**
|
|
2969
|
-
* Creates a table-specific interface for simplified table operations
|
|
2970
|
-
*
|
|
2971
|
-
* @param tableName - Table name
|
|
2972
|
-
* @returns TableMethods interface for the specified table
|
|
2973
|
-
*/
|
|
2974
|
-
const table = (tableName) => {
|
|
2975
|
-
return {
|
|
2976
|
-
insert: (data) => insert(tableName, data),
|
|
2977
|
-
get: (data) => get(tableName, data),
|
|
2978
|
-
list: (data) => list(tableName, data)
|
|
2979
|
-
};
|
|
2980
|
-
};
|
|
2981
|
-
/**
|
|
2982
|
-
* Executes a SQL query using template literals and returns a single value
|
|
2983
|
-
*
|
|
2984
|
-
* @param strings - Template strings
|
|
2985
|
-
* @param vars - Variables to interpolate into the query
|
|
2986
|
-
* @returns Promise resolving to a single value (first column of first row)
|
|
2987
|
-
*/
|
|
2988
|
-
const pluck = async (strings, ...vars) => {
|
|
2989
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2990
|
-
try {
|
|
2991
|
-
const firstRow = (await executor.query(sql, values)).rows[0];
|
|
2992
|
-
if (!firstRow) return null;
|
|
2993
|
-
return Object.values(firstRow)[0];
|
|
2994
|
-
} catch (e) {
|
|
2995
|
-
throw new DatabaseError("Failed to execute pluck query", {
|
|
2996
|
-
sql,
|
|
2997
|
-
values,
|
|
2998
|
-
originalError: formatDbError(e)
|
|
2999
|
-
});
|
|
3000
|
-
}
|
|
3001
|
-
};
|
|
3002
|
-
/**
|
|
3003
|
-
* Executes a SQL query using template literals and returns a single row
|
|
3004
|
-
*
|
|
3005
|
-
* @param strings - Template strings
|
|
3006
|
-
* @param vars - Variables to interpolate into the query
|
|
3007
|
-
* @returns Promise resolving to a single result record or null
|
|
3008
|
-
*/
|
|
3009
|
-
const single = async (strings, ...vars) => {
|
|
3010
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
3011
|
-
try {
|
|
3012
|
-
return (await executor.query(sql, values)).rows[0] || null;
|
|
3013
|
-
} catch (e) {
|
|
3014
|
-
throw new DatabaseError("Failed to execute single query", {
|
|
3015
|
-
sql,
|
|
3016
|
-
values,
|
|
3017
|
-
originalError: formatDbError(e)
|
|
3018
|
-
});
|
|
3019
|
-
}
|
|
3020
|
-
};
|
|
3021
|
-
/**
|
|
3022
|
-
* Executes a SQL query using template literals and returns multiple rows
|
|
3023
|
-
*
|
|
3024
|
-
* @param strings - Template strings
|
|
3025
|
-
* @param vars - Variables to interpolate into the query
|
|
3026
|
-
* @returns Promise resolving to array of result records
|
|
3027
|
-
*/
|
|
3028
|
-
const many = async (strings, ...vars) => {
|
|
3029
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
3030
|
-
try {
|
|
3031
|
-
return (await executor.query(sql, values)).rows;
|
|
3032
|
-
} catch (e) {
|
|
3033
|
-
throw new DatabaseError("Failed to execute many query", {
|
|
3034
|
-
sql,
|
|
3035
|
-
values,
|
|
3036
|
-
originalError: formatDbError(e)
|
|
3037
|
-
});
|
|
3038
|
-
}
|
|
3039
|
-
};
|
|
3040
|
-
/**
|
|
3041
|
-
* Executes a SQL query using template literals without returning results
|
|
3042
|
-
*
|
|
3043
|
-
* @param strings - Template strings
|
|
3044
|
-
* @param vars - Variables to interpolate into the query
|
|
3045
|
-
* @returns Promise that resolves when the query completes
|
|
3046
|
-
*/
|
|
3047
|
-
const execute = async (strings, ...vars) => {
|
|
3048
|
-
const { sql, values } = parseTemplate(strings, ...vars);
|
|
3049
|
-
try {
|
|
3050
|
-
await executor.query(sql, values);
|
|
3051
|
-
} catch (e) {
|
|
3052
|
-
throw new DatabaseError("Failed to execute query", {
|
|
3053
|
-
sql,
|
|
3054
|
-
values,
|
|
3055
|
-
originalError: formatDbError(e)
|
|
3056
|
-
});
|
|
3057
|
-
}
|
|
3058
|
-
};
|
|
3059
|
-
/**
|
|
3060
|
-
* Executes a raw SQL query with parameterized values
|
|
3061
|
-
*
|
|
3062
|
-
* Uses PostgreSQL-native placeholders ($1, $2, ...). Legacy ? placeholders
|
|
3063
|
-
* are converted only when the placeholder count matches the supplied values,
|
|
3064
|
-
* so Postgres operators such as JSONB ? remain intact.
|
|
3065
|
-
*
|
|
3066
|
-
* @param sql - SQL query string
|
|
3067
|
-
* @param values - Variables to use as parameters
|
|
3068
|
-
* @returns Promise resolving to query result with rows and count
|
|
3069
|
-
*/
|
|
3070
|
-
const query = async (sql, ...values) => {
|
|
3071
|
-
const query = normalizePostgresRawQuery(sql, values);
|
|
3072
|
-
try {
|
|
3073
|
-
const result = await executor.query(query.sql, query.values);
|
|
3074
|
-
return {
|
|
3075
|
-
rows: result.rows,
|
|
3076
|
-
rowCount: result.rowCount ?? 0
|
|
3077
|
-
};
|
|
3078
|
-
} catch (e) {
|
|
3079
|
-
throw wrapDatabaseError("Failed to execute raw query", e, {
|
|
3080
|
-
sql: query.sql,
|
|
3081
|
-
values: query.values
|
|
3082
|
-
});
|
|
3083
|
-
}
|
|
3084
|
-
};
|
|
3085
|
-
/**
|
|
3086
|
-
* Checks if a table exists in the database
|
|
3087
|
-
*
|
|
3088
|
-
* @param tableName - Name of the table to check
|
|
3089
|
-
* @returns Promise resolving to boolean indicating if the table exists
|
|
3090
|
-
*/
|
|
3091
|
-
const tableExists = async (tableName) => {
|
|
3092
|
-
return (await executor.query(`SELECT EXISTS (
|
|
3093
|
-
SELECT 1
|
|
3094
|
-
FROM information_schema.tables
|
|
3095
|
-
WHERE table_name = $1
|
|
3096
|
-
AND table_schema = (
|
|
3097
|
-
SELECT namespace.nspname
|
|
3098
|
-
FROM pg_catalog.pg_class AS relation
|
|
3099
|
-
JOIN pg_catalog.pg_namespace AS namespace
|
|
3100
|
-
ON namespace.oid = relation.relnamespace
|
|
3101
|
-
WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident($1))
|
|
3102
|
-
)
|
|
3103
|
-
)`, [tableName])).rows[0].exists;
|
|
3104
|
-
};
|
|
3105
|
-
/**
|
|
3106
|
-
* Synchronizes database schema with provided SQL DDL
|
|
3107
|
-
* Creates tables if they don't exist and adds missing columns
|
|
3108
|
-
* Also executes CREATE INDEX statements
|
|
3109
|
-
*
|
|
3110
|
-
* @param schema - SQL schema definition with CREATE TABLE and CREATE INDEX statements
|
|
3111
|
-
* @returns Promise that resolves when schema is synchronized
|
|
3112
|
-
*/
|
|
3113
|
-
/**
|
|
3114
|
-
* Runs a step whose failure `syncSchema` deliberately tolerates.
|
|
3115
|
-
*
|
|
3116
|
-
* Logging and continuing is self-contained on the pool. Inside a
|
|
3117
|
-
* transaction it is not: a failed statement puts PostgreSQL in the aborted
|
|
3118
|
-
* state, and `COMMIT` on an aborted transaction *succeeds* — it reports a
|
|
3119
|
-
* ROLLBACK command tag and no error. The caller is told the transaction
|
|
3120
|
-
* committed while every write in it was discarded, which is the silent
|
|
3121
|
-
* data loss this whole epic is about, re-entering through the schema path.
|
|
3122
|
-
*
|
|
3123
|
-
* A savepoint scopes the failure to the step, which is what "log it and
|
|
3124
|
-
* continue" was always supposed to mean.
|
|
3125
|
-
*/
|
|
3126
|
-
const runTolerated = async (work) => {
|
|
3127
|
-
if (!inTransaction) return work();
|
|
3128
|
-
savepointSequence += 1;
|
|
3129
|
-
const name = `hv_sync_${savepointSequence}`;
|
|
3130
|
-
await executor.query(`SAVEPOINT ${name}`);
|
|
3131
|
-
try {
|
|
3132
|
-
const result = await work();
|
|
3133
|
-
await executor.query(`RELEASE SAVEPOINT ${name}`);
|
|
3134
|
-
return result;
|
|
3135
|
-
} catch (error) {
|
|
3136
|
-
await executor.query(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
3137
|
-
clearTransactionError();
|
|
3138
|
-
await executor.query(`RELEASE SAVEPOINT ${name}`);
|
|
3139
|
-
throw error;
|
|
3140
|
-
}
|
|
3141
|
-
};
|
|
3142
|
-
const syncSchema = async (schema) => {
|
|
3143
|
-
const commands = schema.trim().split(";").filter((command) => command.trim() !== "");
|
|
3144
|
-
const createIndexRegex = /CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?"?(?<indexName>\w+)"?\s+ON\s+"?(?<tableName>\w+)"?\s*(?:USING\s+"?\w+"?\s*)?\([^)]+\)/i;
|
|
3145
|
-
const parseCreateIndex = (command) => {
|
|
3146
|
-
const { indexName, tableName } = command.match(createIndexRegex)?.groups ?? {};
|
|
3147
|
-
return indexName && tableName ? {
|
|
3148
|
-
indexName,
|
|
3149
|
-
tableName
|
|
3150
|
-
} : null;
|
|
3151
|
-
};
|
|
3152
|
-
const indexTargets = [];
|
|
3153
|
-
for (const command of commands) {
|
|
3154
|
-
const indexMatch = parseCreateIndex(command.trim());
|
|
3155
|
-
if (indexMatch) indexTargets.push(indexMatch);
|
|
3156
|
-
}
|
|
3157
|
-
const existingIndexes = /* @__PURE__ */ new Set();
|
|
3158
|
-
if (indexTargets.length > 0) {
|
|
3159
|
-
const result = await executor.query(`SELECT target.table_name, target.index_name
|
|
3160
|
-
FROM unnest($1::text[], $2::text[])
|
|
3161
|
-
AS target(table_name, index_name)
|
|
3162
|
-
JOIN pg_catalog.pg_class AS relation
|
|
3163
|
-
ON relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident(target.table_name))
|
|
3164
|
-
JOIN pg_catalog.pg_namespace AS namespace
|
|
3165
|
-
ON namespace.oid = relation.relnamespace
|
|
3166
|
-
JOIN pg_catalog.pg_indexes AS existing
|
|
3167
|
-
ON existing.schemaname = namespace.nspname
|
|
3168
|
-
AND existing.tablename = target.table_name
|
|
3169
|
-
AND existing.indexname = target.index_name`, [indexTargets.map((target) => target.tableName), indexTargets.map((target) => target.indexName)]);
|
|
3170
|
-
for (const row of result.rows) existingIndexes.add(`${row.table_name}\u0000${row.index_name}`);
|
|
3171
|
-
}
|
|
3172
|
-
for (const command of commands) {
|
|
3173
|
-
const trimmedCommand = command.trim();
|
|
3174
|
-
const tableMatch = trimmedCommand.match(/CREATE TABLE (IF NOT EXISTS )?"?(\w+)"? \(([\s\S]+)\)/i);
|
|
3175
|
-
if (tableMatch) {
|
|
3176
|
-
const tableName = tableMatch[2];
|
|
3177
|
-
const columns = tableMatch[3].trim().split(",\n");
|
|
3178
|
-
if (!await tableExists(tableName)) await executor.query(trimmedCommand);
|
|
3179
|
-
else for (const column of columns) {
|
|
3180
|
-
const columnDef = column.trim();
|
|
3181
|
-
if (/^(?:CONSTRAINT\s+(?:"(?:[^"]|"")*"|\w+)\s+)?(?:(?:PRIMARY\s+KEY|FOREIGN\s+KEY|UNIQUE|CHECK)\b|EXCLUDE(?=\s+(?:USING\b|\()))/i.test(columnDef)) continue;
|
|
3182
|
-
const columnMatch = columnDef.match(/^("(?:[^"]|"")*"|\w+)\s+\w+[^,]*/);
|
|
3183
|
-
if (columnMatch) {
|
|
3184
|
-
const identifier = columnMatch[1];
|
|
3185
|
-
const columnName = identifier.startsWith("\"") ? identifier.slice(1, -1).replace(/""/g, "\"") : identifier;
|
|
3186
|
-
if (columnName.toUpperCase() === "PRIMARY" || columnName.toUpperCase() === "FOREIGN" || columnName.toUpperCase() === "UNIQUE" || columnName.toUpperCase() === "CHECK" || columnName.toUpperCase() === "CONSTRAINT") continue;
|
|
3187
|
-
try {
|
|
3188
|
-
await runTolerated(async () => {
|
|
3189
|
-
if (!(await executor.query(`SELECT EXISTS (
|
|
3190
|
-
SELECT 1 FROM information_schema.columns
|
|
3191
|
-
WHERE table_name = $1
|
|
3192
|
-
AND column_name = $2
|
|
3193
|
-
AND table_schema = (
|
|
3194
|
-
SELECT namespace.nspname
|
|
3195
|
-
FROM pg_catalog.pg_class AS relation
|
|
3196
|
-
JOIN pg_catalog.pg_namespace AS namespace
|
|
3197
|
-
ON namespace.oid = relation.relnamespace
|
|
3198
|
-
WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident($1))
|
|
3199
|
-
)
|
|
3200
|
-
)`, [tableName, columnName])).rows[0].exists) {
|
|
3201
|
-
const alterCommand = `ALTER TABLE "${tableName}" ADD COLUMN ${columnDef}`;
|
|
3202
|
-
await executor.query(alterCommand);
|
|
3203
|
-
}
|
|
3204
|
-
});
|
|
3205
|
-
} catch (error) {
|
|
3206
|
-
console.error(`Error adding column ${columnName} to ${tableName}:`, error);
|
|
3207
|
-
}
|
|
3208
|
-
}
|
|
3209
|
-
}
|
|
3210
|
-
continue;
|
|
3211
|
-
}
|
|
3212
|
-
const indexMatch = parseCreateIndex(trimmedCommand);
|
|
3213
|
-
if (indexMatch) {
|
|
3214
|
-
const { indexName, tableName: indexTableName } = indexMatch;
|
|
3215
|
-
const indexKey = `${indexTableName}\u0000${indexName}`;
|
|
3216
|
-
if (!existingIndexes.has(indexKey)) try {
|
|
3217
|
-
await runTolerated(() => executor.query(trimmedCommand));
|
|
3218
|
-
existingIndexes.add(indexKey);
|
|
3219
|
-
} catch (error) {
|
|
3220
|
-
console.warn(`Warning: Failed to create index ${indexName} on ${indexTableName}:`, error instanceof Error ? error.message : String(error));
|
|
3221
|
-
}
|
|
3222
|
-
continue;
|
|
3223
|
-
}
|
|
3224
|
-
if (trimmedCommand.toUpperCase().startsWith("CREATE ") && !trimmedCommand.toUpperCase().includes("CREATE TABLE")) try {
|
|
3225
|
-
await runTolerated(() => executor.query(trimmedCommand));
|
|
3226
|
-
} catch (error) {
|
|
3227
|
-
console.warn(`Warning: DDL statement may have failed:`, error instanceof Error ? error.message : String(error));
|
|
3228
|
-
}
|
|
3229
|
-
}
|
|
3230
|
-
};
|
|
3231
|
-
return {
|
|
3232
|
-
insert,
|
|
3233
|
-
get,
|
|
3234
|
-
list,
|
|
3235
|
-
update,
|
|
3236
|
-
upsert,
|
|
3237
|
-
getOrInsert,
|
|
3238
|
-
delete: deleteRecords,
|
|
3239
|
-
count,
|
|
3240
|
-
table,
|
|
3241
|
-
many,
|
|
3242
|
-
single,
|
|
3243
|
-
pluck,
|
|
3244
|
-
execute,
|
|
3245
|
-
query,
|
|
3246
|
-
oo: many,
|
|
3247
|
-
oO: single,
|
|
3248
|
-
ox: pluck,
|
|
3249
|
-
xx: execute,
|
|
3250
|
-
tableExists,
|
|
3251
|
-
syncSchema
|
|
3252
|
-
};
|
|
3253
|
-
};
|
|
3254
|
-
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);
|
|
3255
|
-
/**
|
|
3256
|
-
* Acquire a pinned connection from the pool for session-scoped state such as
|
|
3257
|
-
* `pg_advisory_lock`. Every query on the returned handle runs on the same
|
|
3258
|
-
* client until {@link SessionHandle.release} drops the connection (which
|
|
3259
|
-
* frees any session locks it held). The connection is held for the handle's
|
|
3260
|
-
* lifetime, so hold one session per process, not per operation.
|
|
3261
|
-
*/
|
|
3262
|
-
const acquireSession = async () => {
|
|
3263
|
-
const sessionClient = await pool.connect();
|
|
3264
|
-
let released = false;
|
|
3265
|
-
let lost = false;
|
|
3266
|
-
sessionClient.on("error", () => {
|
|
3267
|
-
lost = true;
|
|
3268
|
-
});
|
|
3269
|
-
return {
|
|
3270
|
-
query: async (sql, ...values) => {
|
|
3271
|
-
if (released) throw new DatabaseError("Session has been released", {});
|
|
3272
|
-
if (lost) throw new DatabaseError("Session connection was lost", {});
|
|
3273
|
-
const normalized = normalizePostgresRawQuery(sql, values);
|
|
3274
|
-
try {
|
|
3275
|
-
const result = await sessionClient.query(normalized.sql, normalized.values);
|
|
3276
|
-
return {
|
|
3277
|
-
rows: result.rows,
|
|
3278
|
-
rowCount: result.rowCount ?? 0
|
|
3279
|
-
};
|
|
3280
|
-
} catch (e) {
|
|
3281
|
-
throw wrapDatabaseError("Failed to execute session query", e, {
|
|
3282
|
-
sql: normalized.sql,
|
|
3283
|
-
values: normalized.values
|
|
3284
|
-
});
|
|
3285
|
-
}
|
|
3286
|
-
},
|
|
3287
|
-
isActive: () => !released && !lost,
|
|
3288
|
-
release: async () => {
|
|
3289
|
-
if (released) return;
|
|
3290
|
-
released = true;
|
|
3291
|
-
if (!lost) {
|
|
3292
|
-
try {
|
|
3293
|
-
await sessionClient.query("ROLLBACK");
|
|
3294
|
-
} catch {}
|
|
3295
|
-
try {
|
|
3296
|
-
await sessionClient.query("SELECT pg_advisory_unlock_all()");
|
|
3297
|
-
} catch {}
|
|
3298
|
-
}
|
|
3299
|
-
sessionClient.release(true);
|
|
3300
|
-
}
|
|
3301
|
-
};
|
|
3302
|
-
};
|
|
3303
|
-
let savepointSequence = 0;
|
|
3304
|
-
/**
|
|
3305
|
-
* Builds the `transaction` exposed on a transaction-scoped interface.
|
|
3306
|
-
*
|
|
3307
|
-
* Re-entering runs the callback under a SAVEPOINT on the *same* connection,
|
|
3308
|
-
* which is what callers assume nesting already did. Re-exposing the top-level
|
|
3309
|
-
* `transaction` here (the previous behaviour) checked out a second pooled
|
|
3310
|
-
* connection and began an independent transaction, so the nested work could
|
|
3311
|
-
* not see the enclosing transaction's uncommitted rows, and if the enclosing
|
|
3312
|
-
* transaction held a lock the nested one needed, the two deadlocked in a way
|
|
3313
|
-
* PostgreSQL cannot detect: the outer connection is blocked on a promise
|
|
3314
|
-
* rather than on a lock, so `deadlock_timeout` never fires and the process
|
|
3315
|
-
* hangs until some unrelated timeout.
|
|
3316
|
-
*
|
|
3317
|
-
* @param executor - Error-preserving executor bound to the enclosing client
|
|
3318
|
-
* @param scopeFor - Builds the interface handed to the nested callback
|
|
3319
|
-
*/
|
|
3320
|
-
const createNestedTransaction = (executor, scopeFor, clearTransactionError) => {
|
|
3321
|
-
const nestedScope = new AsyncLocalStorage();
|
|
3322
|
-
const rootScope = { tail: Promise.resolve() };
|
|
3323
|
-
const runScope = async (scope, callback) => {
|
|
3324
|
-
savepointSequence += 1;
|
|
3325
|
-
const name = `hv_sp_${savepointSequence}`;
|
|
3326
|
-
await executor.query(`SAVEPOINT ${name}`);
|
|
3327
|
-
try {
|
|
3328
|
-
const result = await callback(scopeFor());
|
|
3329
|
-
await scope.tail;
|
|
3330
|
-
await executor.query(`RELEASE SAVEPOINT ${name}`);
|
|
3331
|
-
return result;
|
|
3332
|
-
} catch (error) {
|
|
3333
|
-
await scope.tail;
|
|
3334
|
-
try {
|
|
3335
|
-
await executor.query(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
3336
|
-
clearTransactionError();
|
|
3337
|
-
await executor.query(`RELEASE SAVEPOINT ${name}`);
|
|
3338
|
-
} catch {}
|
|
3339
|
-
throw error;
|
|
3340
|
-
}
|
|
3341
|
-
};
|
|
3342
|
-
const transaction = async (callback) => {
|
|
3343
|
-
const parentScope = nestedScope.getStore() ?? rootScope;
|
|
3344
|
-
const scope = { tail: Promise.resolve() };
|
|
3345
|
-
const current = parentScope.tail.then(() => nestedScope.run(scope, () => runScope(scope, callback)));
|
|
3346
|
-
parentScope.tail = current.then(() => void 0, () => void 0);
|
|
3347
|
-
return current;
|
|
3348
|
-
};
|
|
3349
|
-
transaction.drain = () => rootScope.tail;
|
|
3350
|
-
return transaction;
|
|
3351
|
-
};
|
|
3352
|
-
/**
|
|
3353
|
-
* Executes a callback within a database transaction
|
|
3354
|
-
* Automatically commits on success or rolls back on error
|
|
3355
|
-
*
|
|
3356
|
-
* @param callback - Function to execute within transaction
|
|
3357
|
-
* @returns Promise resolving to callback result
|
|
3358
|
-
*/
|
|
3359
|
-
const transaction = async (callback) => {
|
|
3360
|
-
const txClient = await acquireTxClient();
|
|
3361
|
-
let nestedTransaction;
|
|
3362
|
-
const transactionState = createTransactionExecutor(txClient);
|
|
3363
|
-
try {
|
|
3364
|
-
await txClient.query("BEGIN");
|
|
3365
|
-
let txDb;
|
|
3366
|
-
nestedTransaction = createNestedTransaction(transactionState.executor, () => txDb, transactionState.clearError);
|
|
3367
|
-
txDb = {
|
|
3368
|
-
url,
|
|
3369
|
-
client: transactionState.client,
|
|
3370
|
-
...createClientMethods(transactionState.executor, true, serializeRecord, transactionState.clearError),
|
|
3371
|
-
transaction: nestedTransaction
|
|
3372
|
-
};
|
|
3373
|
-
const result = await callback(txDb);
|
|
3374
|
-
await nestedTransaction.drain();
|
|
3375
|
-
await transactionState.drain();
|
|
3376
|
-
const statementError = transactionState.getError();
|
|
3377
|
-
if (statementError !== void 0) throw statementError;
|
|
3378
|
-
const commitResult = await txClient.query("COMMIT");
|
|
3379
|
-
assertCommitted(commitResult, transactionState);
|
|
3380
|
-
releaseTxClient(txClient);
|
|
3381
|
-
return result;
|
|
3382
|
-
} catch (error) {
|
|
3383
|
-
await nestedTransaction?.drain();
|
|
3384
|
-
await transactionState.drain();
|
|
3385
|
-
const rollbackError = await discardTxClient(txClient);
|
|
3386
|
-
if (rollbackError !== void 0 && error instanceof Error) error.cause ??= rollbackError;
|
|
3387
|
-
throw error;
|
|
3388
|
-
}
|
|
3389
|
-
};
|
|
3390
|
-
/**
|
|
3391
|
-
* Begins a new transaction and returns a handle for manual control
|
|
3392
|
-
*
|
|
3393
|
-
* Unlike transaction(), this gives you explicit control over commit/rollback.
|
|
3394
|
-
* Ideal for test isolation where you want to rollback after each test.
|
|
3395
|
-
*
|
|
3396
|
-
* @returns Promise resolving to a TransactionHandle
|
|
2372
|
+
* @returns Promise resolving to a TransactionHandle
|
|
3397
2373
|
*/
|
|
3398
2374
|
const beginTransaction = async () => {
|
|
3399
2375
|
const txClient = await acquireTxClient();
|
|
@@ -3428,7 +2404,7 @@ async function createDatabase(config) {
|
|
|
3428
2404
|
const rollback = () => end("ROLLBACK");
|
|
3429
2405
|
const isActive = () => active;
|
|
3430
2406
|
let txHandle;
|
|
3431
|
-
nestedTransaction = createNestedTransaction(transactionState.executor, () => txHandle, transactionState.clearError);
|
|
2407
|
+
nestedTransaction = createNestedTransaction(transactionState.executor, () => txHandle, transactionState.clearError, new AsyncLocalStorage());
|
|
3432
2408
|
txHandle = {
|
|
3433
2409
|
url,
|
|
3434
2410
|
client: transactionState.client,
|
|
@@ -3476,137 +2452,6 @@ async function createDatabase(config) {
|
|
|
3476
2452
|
};
|
|
3477
2453
|
await schemaManager.initializeSchemas(currentDb, options);
|
|
3478
2454
|
};
|
|
3479
|
-
/**
|
|
3480
|
-
* Retrieves the schema information for a table
|
|
3481
|
-
*
|
|
3482
|
-
* @param table - Table name
|
|
3483
|
-
* @returns Promise resolving to table schema info or null if table doesn't exist
|
|
3484
|
-
* @throws Error if the query fails
|
|
3485
|
-
*/
|
|
3486
|
-
const getTableSchema = async (table) => {
|
|
3487
|
-
validateTableName$1(table);
|
|
3488
|
-
try {
|
|
3489
|
-
if (!await tableExists(table)) return null;
|
|
3490
|
-
const columnRows = await many`
|
|
3491
|
-
SELECT
|
|
3492
|
-
column_name,
|
|
3493
|
-
data_type,
|
|
3494
|
-
is_nullable,
|
|
3495
|
-
column_default
|
|
3496
|
-
FROM information_schema.columns
|
|
3497
|
-
WHERE table_name = ${table}
|
|
3498
|
-
AND table_schema = (
|
|
3499
|
-
SELECT namespace.nspname
|
|
3500
|
-
FROM pg_catalog.pg_class AS relation
|
|
3501
|
-
JOIN pg_catalog.pg_namespace AS namespace
|
|
3502
|
-
ON namespace.oid = relation.relnamespace
|
|
3503
|
-
WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident(${table}))
|
|
3504
|
-
)
|
|
3505
|
-
ORDER BY ordinal_position
|
|
3506
|
-
`;
|
|
3507
|
-
const pkRows = await many`
|
|
3508
|
-
SELECT kcu.column_name
|
|
3509
|
-
FROM information_schema.table_constraints tc
|
|
3510
|
-
JOIN information_schema.key_column_usage kcu
|
|
3511
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
3512
|
-
AND tc.table_schema = kcu.table_schema
|
|
3513
|
-
WHERE tc.table_name = ${table}
|
|
3514
|
-
AND tc.table_schema = (
|
|
3515
|
-
SELECT namespace.nspname
|
|
3516
|
-
FROM pg_catalog.pg_class AS relation
|
|
3517
|
-
JOIN pg_catalog.pg_namespace AS namespace
|
|
3518
|
-
ON namespace.oid = relation.relnamespace
|
|
3519
|
-
WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident(${table}))
|
|
3520
|
-
)
|
|
3521
|
-
AND tc.constraint_type = 'PRIMARY KEY'
|
|
3522
|
-
`;
|
|
3523
|
-
const pkColumns = new Set(pkRows.map((row) => row.column_name));
|
|
3524
|
-
const columns = {};
|
|
3525
|
-
for (const row of columnRows) {
|
|
3526
|
-
const colName = row.column_name;
|
|
3527
|
-
columns[colName] = {
|
|
3528
|
-
type: row.data_type,
|
|
3529
|
-
primaryKey: pkColumns.has(colName),
|
|
3530
|
-
notNull: row.is_nullable === "NO",
|
|
3531
|
-
defaultValue: row.column_default
|
|
3532
|
-
};
|
|
3533
|
-
}
|
|
3534
|
-
const indexRows = await many`
|
|
3535
|
-
SELECT
|
|
3536
|
-
indexname,
|
|
3537
|
-
indexdef
|
|
3538
|
-
FROM pg_indexes
|
|
3539
|
-
WHERE tablename = ${table}
|
|
3540
|
-
AND schemaname = (
|
|
3541
|
-
SELECT namespace.nspname
|
|
3542
|
-
FROM pg_catalog.pg_class AS relation
|
|
3543
|
-
JOIN pg_catalog.pg_namespace AS namespace
|
|
3544
|
-
ON namespace.oid = relation.relnamespace
|
|
3545
|
-
WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident(${table}))
|
|
3546
|
-
)
|
|
3547
|
-
AND indexname NOT LIKE '%_pkey'
|
|
3548
|
-
`;
|
|
3549
|
-
const indexes = [];
|
|
3550
|
-
for (const row of indexRows) {
|
|
3551
|
-
const indexName = row.indexname;
|
|
3552
|
-
const indexDef = row.indexdef;
|
|
3553
|
-
const colMatch = indexDef.match(/\(([^)]+)\)/);
|
|
3554
|
-
const indexColumns = colMatch ? colMatch[1].split(",").map((col) => col.trim()) : [];
|
|
3555
|
-
const isUnique = indexDef.toUpperCase().includes("UNIQUE");
|
|
3556
|
-
indexes.push({
|
|
3557
|
-
name: indexName,
|
|
3558
|
-
columns: indexColumns,
|
|
3559
|
-
unique: isUnique
|
|
3560
|
-
});
|
|
3561
|
-
}
|
|
3562
|
-
const fkRows = await many`
|
|
3563
|
-
SELECT
|
|
3564
|
-
kcu.column_name,
|
|
3565
|
-
ccu.table_name AS foreign_table_name,
|
|
3566
|
-
ccu.column_name AS foreign_column_name,
|
|
3567
|
-
rc.delete_rule,
|
|
3568
|
-
rc.update_rule
|
|
3569
|
-
FROM information_schema.table_constraints AS tc
|
|
3570
|
-
JOIN information_schema.key_column_usage AS kcu
|
|
3571
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
3572
|
-
AND tc.table_schema = kcu.table_schema
|
|
3573
|
-
JOIN information_schema.constraint_column_usage AS ccu
|
|
3574
|
-
ON ccu.constraint_name = tc.constraint_name
|
|
3575
|
-
AND ccu.table_schema = tc.table_schema
|
|
3576
|
-
JOIN information_schema.referential_constraints AS rc
|
|
3577
|
-
ON rc.constraint_name = tc.constraint_name
|
|
3578
|
-
AND rc.constraint_schema = tc.table_schema
|
|
3579
|
-
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
3580
|
-
AND tc.table_name = ${table}
|
|
3581
|
-
AND tc.table_schema = (
|
|
3582
|
-
SELECT namespace.nspname
|
|
3583
|
-
FROM pg_catalog.pg_class AS relation
|
|
3584
|
-
JOIN pg_catalog.pg_namespace AS namespace
|
|
3585
|
-
ON namespace.oid = relation.relnamespace
|
|
3586
|
-
WHERE relation.oid = pg_catalog.to_regclass(pg_catalog.quote_ident(${table}))
|
|
3587
|
-
)
|
|
3588
|
-
`;
|
|
3589
|
-
const foreignKeys = [];
|
|
3590
|
-
for (const fkRow of fkRows) foreignKeys.push({
|
|
3591
|
-
column: fkRow.column_name,
|
|
3592
|
-
referencesTable: fkRow.foreign_table_name,
|
|
3593
|
-
referencesColumn: fkRow.foreign_column_name,
|
|
3594
|
-
onDelete: fkRow.delete_rule,
|
|
3595
|
-
onUpdate: fkRow.update_rule
|
|
3596
|
-
});
|
|
3597
|
-
return {
|
|
3598
|
-
tableName: table,
|
|
3599
|
-
columns,
|
|
3600
|
-
indexes,
|
|
3601
|
-
foreignKeys
|
|
3602
|
-
};
|
|
3603
|
-
} catch (e) {
|
|
3604
|
-
throw new DatabaseError("Failed to retrieve table schema", {
|
|
3605
|
-
table,
|
|
3606
|
-
originalError: formatDbError(e)
|
|
3607
|
-
});
|
|
3608
|
-
}
|
|
3609
|
-
};
|
|
3610
2455
|
db = {
|
|
3611
2456
|
url,
|
|
3612
2457
|
client,
|
|
@@ -3635,55 +2480,7 @@ async function createDatabase(config) {
|
|
|
3635
2480
|
beginTransaction,
|
|
3636
2481
|
acquireSession,
|
|
3637
2482
|
getTableSchema,
|
|
3638
|
-
alterTable
|
|
3639
|
-
/**
|
|
3640
|
-
* Adds a new column to an existing table
|
|
3641
|
-
*
|
|
3642
|
-
* @param table - Table name
|
|
3643
|
-
* @param column - Column definition with name
|
|
3644
|
-
* @returns Promise that resolves when column is added
|
|
3645
|
-
* @throws Error if the alter operation fails
|
|
3646
|
-
*/
|
|
3647
|
-
addColumn: async (table, column) => {
|
|
3648
|
-
validateTableName$1(table);
|
|
3649
|
-
validateColumnName$1(column.name);
|
|
3650
|
-
let sql;
|
|
3651
|
-
try {
|
|
3652
|
-
sql = generateAddColumnStatement(table, column, "postgres");
|
|
3653
|
-
await client.query(sql);
|
|
3654
|
-
} catch (e) {
|
|
3655
|
-
throw wrapDatabaseError("Failed to add column to table", e, {
|
|
3656
|
-
table,
|
|
3657
|
-
column: column.name,
|
|
3658
|
-
...sql ? { sql } : {}
|
|
3659
|
-
});
|
|
3660
|
-
}
|
|
3661
|
-
},
|
|
3662
|
-
/**
|
|
3663
|
-
* Adds a new index to an existing table
|
|
3664
|
-
*
|
|
3665
|
-
* @param table - Table name
|
|
3666
|
-
* @param index - Index definition
|
|
3667
|
-
* @returns Promise that resolves when index is created
|
|
3668
|
-
* @throws Error if the create index operation fails
|
|
3669
|
-
*/
|
|
3670
|
-
addIndex: async (table, index) => {
|
|
3671
|
-
validateTableName$1(table);
|
|
3672
|
-
validateIndexName(index.name);
|
|
3673
|
-
for (const col of index.columns) validateColumnName$1(col);
|
|
3674
|
-
let sql;
|
|
3675
|
-
try {
|
|
3676
|
-
sql = generateCreateIndexStatement(table, index);
|
|
3677
|
-
await client.query(sql);
|
|
3678
|
-
} catch (e) {
|
|
3679
|
-
throw wrapDatabaseError("Failed to create index on table", e, {
|
|
3680
|
-
table,
|
|
3681
|
-
index: index.name,
|
|
3682
|
-
...sql ? { sql } : {}
|
|
3683
|
-
});
|
|
3684
|
-
}
|
|
3685
|
-
}
|
|
3686
|
-
},
|
|
2483
|
+
alterTable,
|
|
3687
2484
|
vector: createVectorCapabilities(pool),
|
|
3688
2485
|
close: async () => {
|
|
3689
2486
|
await pool.end();
|
|
@@ -3706,7 +2503,7 @@ function isDatabaseInstance(value) {
|
|
|
3706
2503
|
* Creates a database connection based on the provided options, or returns an existing database instance
|
|
3707
2504
|
*
|
|
3708
2505
|
* Loads configuration from environment variables using the HAVE_SQL_* pattern:
|
|
3709
|
-
* - HAVE_SQL_TYPE → type ('sqlite' | 'postgres' | 'duckdb' | 'json')
|
|
2506
|
+
* - HAVE_SQL_TYPE → type ('sqlite' | 'postgres' | 'pglite' | 'duckdb' | 'json')
|
|
3710
2507
|
* - HAVE_SQL_URL → url (connection string)
|
|
3711
2508
|
* - HAVE_SQL_HOST → host (database server hostname)
|
|
3712
2509
|
* - HAVE_SQL_PORT → port (database server port number)
|
|
@@ -3752,8 +2549,9 @@ async function getDatabase(options = {}) {
|
|
|
3752
2549
|
}));
|
|
3753
2550
|
if (!options.type && (options.url?.startsWith("file:") || options.url === ":memory:")) options.type = "sqlite";
|
|
3754
2551
|
if (options.type === "postgres") return (await Promise.resolve().then(() => postgres_exports)).getDatabase(options);
|
|
3755
|
-
if (options.type === "
|
|
3756
|
-
if (options.type === "
|
|
2552
|
+
if (options.type === "pglite") return (await import("./pglite.js")).getDatabase(options);
|
|
2553
|
+
if (options.type === "sqlite") return (await import("./chunks/sqlite-0PMv1va2.js")).getDatabase(options);
|
|
2554
|
+
if (options.type === "duckdb") return (await import("./chunks/duckdb-LMyfKZGi.js")).getDatabase(options);
|
|
3757
2555
|
if (options.type === "json") return (await Promise.resolve().then(() => json_exports)).getDatabase(options);
|
|
3758
2556
|
throw new Error("Invalid database type");
|
|
3759
2557
|
}
|