@happyvertical/sql 0.102.3 → 0.102.5
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 +26 -0
- package/dist/chunks/{duckdb-LMyfKZGi.js → duckdb-c4vdDzPL.js} +5 -4
- package/dist/chunks/{duckdb-LMyfKZGi.js.map → duckdb-c4vdDzPL.js.map} +1 -1
- package/dist/chunks/duckdb-resources-DBlvosDo.js +44 -0
- package/dist/chunks/duckdb-resources-DBlvosDo.js.map +1 -0
- package/dist/chunks/{postgres-dialect-Bfswr4Vs.js → postgres-dialect-CGv6DxSP.js} +3 -775
- package/dist/chunks/postgres-dialect-CGv6DxSP.js.map +1 -0
- package/dist/chunks/query-RzXJ4B9s.js +353 -0
- package/dist/chunks/query-RzXJ4B9s.js.map +1 -0
- package/dist/chunks/schema-manager-cxsySSDA.js +777 -0
- package/dist/chunks/schema-manager-cxsySSDA.js.map +1 -0
- package/dist/chunks/{sqlite-connection-cache-ChWav9iY.js → sqlite-connection-cache-CYThh9l5.js} +2 -2
- package/dist/chunks/{sqlite-connection-cache-ChWav9iY.js.map → sqlite-connection-cache-CYThh9l5.js.map} +1 -1
- package/dist/chunks/{sqlite-native--z4BnolL.js → sqlite-native-CsJ_BLqN.js} +5 -4
- package/dist/chunks/{sqlite-native--z4BnolL.js.map → sqlite-native-CsJ_BLqN.js.map} +1 -1
- package/dist/chunks/{sqlite-0PMv1va2.js → sqlite-rILMdW7Z.js} +6 -5
- package/dist/chunks/{sqlite-0PMv1va2.js.map → sqlite-rILMdW7Z.js.map} +1 -1
- package/dist/index.d.ts +3 -45
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -238
- package/dist/index.js.map +1 -1
- package/dist/pglite.js +2 -1
- package/dist/pglite.js.map +1 -1
- package/dist/query.d.ts +61 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +3 -0
- package/package.json +6 -2
- package/dist/chunks/duckdb-resources-Cci6Lo6H.js +0 -156
- package/dist/chunks/duckdb-resources-Cci6Lo6H.js.map +0 -1
- package/dist/chunks/postgres-dialect-Bfswr4Vs.js.map +0 -1
|
@@ -1,777 +1,5 @@
|
|
|
1
|
+
import { c as wrapDatabaseError, n as buildWhere, o as resolveInsertColumns, r as formatDbError } from "./schema-manager-cxsySSDA.js";
|
|
1
2
|
import { DatabaseError } from "@happyvertical/utils";
|
|
2
|
-
//#region src/shared/utils.ts
|
|
3
|
-
/**
|
|
4
|
-
* Shared SQL utilities that work in both browser and Node.js environments
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* Formats a database error to include its useful non-secret details.
|
|
8
|
-
*
|
|
9
|
-
* Database drivers often include additional properties beyond just `message`:
|
|
10
|
-
* - PostgreSQL: code, detail, hint, severity
|
|
11
|
-
* - DuckDB: may include additional context
|
|
12
|
-
* - SQLite/LibSQL: may include errno, code
|
|
13
|
-
*
|
|
14
|
-
* This function extracts an allowlist of driver properties and redacts
|
|
15
|
-
* credential-shaped text. When parameter values are available, prefer
|
|
16
|
-
* {@link wrapDatabaseError}; it also redacts values echoed by the driver.
|
|
17
|
-
*
|
|
18
|
-
* @param error - The caught error object
|
|
19
|
-
* @returns Formatted error string with all available error details
|
|
20
|
-
*/
|
|
21
|
-
function formatDbError(error) {
|
|
22
|
-
return formatDbErrorWithSecrets(error, []);
|
|
23
|
-
}
|
|
24
|
-
var DRIVER_ERROR_FIELDS = [
|
|
25
|
-
"code",
|
|
26
|
-
"detail",
|
|
27
|
-
"hint",
|
|
28
|
-
"severity",
|
|
29
|
-
"errno"
|
|
30
|
-
];
|
|
31
|
-
var SENSITIVE_CONTEXT_KEY = /^(?:args?|parameters?|params?|queries|query|sql|values?)$/i;
|
|
32
|
-
var CREDENTIAL_KEY_PART = /^(?:credential|credentials|passphrase|password|pwd|secret|token)$/i;
|
|
33
|
-
var CREDENTIAL_KEY_QUALIFIER = /^(?:access|api|auth|client|private|refresh|service|signing)$/i;
|
|
34
|
-
var DATABASE_USER_KEY = /^(?:user(?:name)?|(?:database|db|pg|postgres|postgresql)[._-]?user(?:name)?)$/i;
|
|
35
|
-
var DATABASE_URL_USERINFO = /\b((?:https?|libsql|postgres|postgresql):\/\/)[^\s/@]+@/giu;
|
|
36
|
-
var DATABASE_URL = /\b(?:https?|libsql|postgres|postgresql):\/\/[^\s"'`<>()]+/giu;
|
|
37
|
-
var CREDENTIAL_ASSIGNMENT = /(?<![\p{L}\p{N}_-])(["']?)([\p{L}\p{N}_.-]+(?: key)?)\1(\s*[:=]\s*)(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^\s,;}\]]+)/giu;
|
|
38
|
-
var BEARER_CREDENTIAL = /\bbearer\s+[^\s,;]+/giu;
|
|
39
|
-
var SQL_NUMERIC_LITERAL = /(?<![\p{L}\p{N}_$])[-+]?(?:\d(?:_?\d)*(?:\.(?:\d(?:_?\d)*)?)?|\.\d(?:_?\d)*)(?:e[-+]?\d(?:_?\d)*)?(?![\p{L}\p{N}_$])/giu;
|
|
40
|
-
var POSTGRES_KEY_VALUE = /(\bKey\s*\([^\r\n)]*\)\s*=\s*\()([\s\S]*?)(\)\s*(?:already exists|is duplicated)\b)/giu;
|
|
41
|
-
var POSTGRES_FAILING_ROW = /(\bFailing row contains\s*\()([\s\S]*)(\)(?:\.|$))/giu;
|
|
42
|
-
var CAST_INPUT_VALUE = /(\bwith value\s+)([\s\S]*?)(\s+(?:can't|cannot)\s+be cast\b)/giu;
|
|
43
|
-
function isCredentialKey(key) {
|
|
44
|
-
if (DATABASE_USER_KEY.test(key)) return true;
|
|
45
|
-
const parts = key.replace(/([\p{Ll}\p{N}])(\p{Lu})/gu, "$1 $2").split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
46
|
-
if (parts.length === 1 && /^(?:auth|credential|key|pass)$/i.test(parts[0])) return true;
|
|
47
|
-
if (parts.some((part) => CREDENTIAL_KEY_PART.test(part))) return true;
|
|
48
|
-
return parts.some((part, index) => /^(?:key|auth)$/i.test(part) && index > 0 && CREDENTIAL_KEY_QUALIFIER.test(parts[index - 1]));
|
|
49
|
-
}
|
|
50
|
-
function toDriverError(error) {
|
|
51
|
-
return error && (typeof error === "object" || typeof error === "function") ? error : {};
|
|
52
|
-
}
|
|
53
|
-
function redactUrl(value) {
|
|
54
|
-
try {
|
|
55
|
-
const url = new URL(value);
|
|
56
|
-
if (url.username) url.username = "[redacted]";
|
|
57
|
-
if (url.password) url.password = "[redacted]";
|
|
58
|
-
for (const key of url.searchParams.keys()) if (isCredentialKey(key)) url.searchParams.set(key, "[redacted]");
|
|
59
|
-
if (url.hash) url.hash = "[redacted]";
|
|
60
|
-
return url.toString();
|
|
61
|
-
} catch {
|
|
62
|
-
return value.replace(/\/\/[^/@\s]+@/u, "//[redacted]@");
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
function redactKnownValue(input, value) {
|
|
66
|
-
if (!value || !input.includes(value)) return input;
|
|
67
|
-
if (!/^[\p{L}\p{N}_-]+$/u.test(value)) return input.split(value).join("[redacted]");
|
|
68
|
-
const identifierCharacter = /^[\p{L}\p{N}_-]$/u;
|
|
69
|
-
let cursor = 0;
|
|
70
|
-
let redacted = "";
|
|
71
|
-
let match = input.indexOf(value);
|
|
72
|
-
while (match !== -1) {
|
|
73
|
-
const end = match + value.length;
|
|
74
|
-
const before = [...input.slice(Math.max(0, match - 2), match)].at(-1);
|
|
75
|
-
const afterCodePoint = input.codePointAt(end);
|
|
76
|
-
const after = afterCodePoint === void 0 ? void 0 : String.fromCodePoint(afterCodePoint);
|
|
77
|
-
const bounded = (!before || !identifierCharacter.test(before)) && (!after || !identifierCharacter.test(after));
|
|
78
|
-
redacted += input.slice(cursor, match);
|
|
79
|
-
redacted += bounded ? "[redacted]" : value;
|
|
80
|
-
cursor = end;
|
|
81
|
-
match = input.indexOf(value, cursor);
|
|
82
|
-
}
|
|
83
|
-
return redacted + input.slice(cursor);
|
|
84
|
-
}
|
|
85
|
-
function redactDatabaseErrorText(value, knownSecrets) {
|
|
86
|
-
let redacted = value.replace(DATABASE_URL_USERINFO, "$1[redacted]@").replace(DATABASE_URL, redactUrl).replace(CREDENTIAL_ASSIGNMENT, (assignment, quote, key, delimiter) => isCredentialKey(key) ? `${quote}${key}${quote}${delimiter}[redacted]` : assignment).replace(BEARER_CREDENTIAL, "Bearer [redacted]").replace(POSTGRES_KEY_VALUE, (_match, prefix, _value, suffix) => `${prefix}[redacted]${suffix}`).replace(POSTGRES_FAILING_ROW, (_match, prefix, _value, suffix) => `${prefix}[redacted]${suffix}`).replace(CAST_INPUT_VALUE, (_match, prefix, _value, suffix) => `${prefix}[redacted]${suffix}`);
|
|
87
|
-
for (const secret of [...new Set(knownSecrets)].sort((left, right) => right.length - left.length)) redacted = redactKnownValue(redacted, secret);
|
|
88
|
-
return redacted;
|
|
89
|
-
}
|
|
90
|
-
function collectSensitiveValues(value, output, seen) {
|
|
91
|
-
if (typeof value === "string") {
|
|
92
|
-
if (value) output.add(value);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
|
|
96
|
-
output.add(String(value));
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (!value || typeof value !== "object" || seen.has(value)) return;
|
|
100
|
-
seen.add(value);
|
|
101
|
-
try {
|
|
102
|
-
if (value instanceof Date) {
|
|
103
|
-
output.add(value.toISOString());
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (ArrayBuffer.isView(value)) {
|
|
107
|
-
const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
108
|
-
if (bytes.length > 0) {
|
|
109
|
-
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
110
|
-
output.add([...bytes].join(","));
|
|
111
|
-
output.add(hex);
|
|
112
|
-
output.add(`\\x${hex}`);
|
|
113
|
-
output.add(`\\x${hex.toUpperCase()}`);
|
|
114
|
-
output.add(`<Buffer ${[...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join(" ")}>`);
|
|
115
|
-
if (typeof Buffer !== "undefined") output.add(Buffer.from(bytes).toString("base64"));
|
|
116
|
-
}
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
} catch {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
try {
|
|
123
|
-
const serialized = JSON.stringify(value);
|
|
124
|
-
if (serialized && serialized !== "{}" && serialized !== "[]") output.add(serialized);
|
|
125
|
-
} catch {}
|
|
126
|
-
let descriptors;
|
|
127
|
-
try {
|
|
128
|
-
descriptors = Object.getOwnPropertyDescriptors(value);
|
|
129
|
-
} catch {
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
for (const [key, descriptor] of Object.entries(descriptors)) if (key !== "length" && "value" in descriptor) collectSensitiveValues(descriptor.value, output, seen);
|
|
133
|
-
}
|
|
134
|
-
function collectSqlLiterals(sql, output) {
|
|
135
|
-
if (sql) output.add(sql);
|
|
136
|
-
for (const match of sql.matchAll(/'((?:''|[^'])*)'/gu)) {
|
|
137
|
-
const literal = match[1].replaceAll("''", "'");
|
|
138
|
-
if (literal) output.add(literal);
|
|
139
|
-
}
|
|
140
|
-
for (const match of sql.matchAll(/\$\$([\s\S]*?)\$\$/gu)) if (match[1]) output.add(match[1]);
|
|
141
|
-
for (const match of sql.matchAll(/\$([A-Za-z_][A-Za-z0-9_]*)\$([\s\S]*?)\$\1\$/gu)) if (match[2]) output.add(match[2]);
|
|
142
|
-
for (const match of sql.matchAll(SQL_NUMERIC_LITERAL)) {
|
|
143
|
-
output.add(match[0]);
|
|
144
|
-
output.add(match[0].replaceAll("_", ""));
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
function contextSecrets(context) {
|
|
148
|
-
const output = /* @__PURE__ */ new Set();
|
|
149
|
-
for (const [key, value] of Object.entries(context)) if (/^(?:args?|parameters?|params?|values?)$/i.test(key)) collectSensitiveValues(value, output, /* @__PURE__ */ new Set());
|
|
150
|
-
else if (/^(?:queries|query|sql)$/i.test(key) && typeof value === "string") collectSqlLiterals(value, output);
|
|
151
|
-
return [...output];
|
|
152
|
-
}
|
|
153
|
-
function sanitizeContextValue(value, knownSecrets, seen) {
|
|
154
|
-
if (typeof value === "string") return redactDatabaseErrorText(value, knownSecrets);
|
|
155
|
-
if (!value || typeof value !== "object") return value;
|
|
156
|
-
if (seen.has(value)) return "[circular]";
|
|
157
|
-
seen.add(value);
|
|
158
|
-
if (Array.isArray(value)) return value.map((entry) => sanitizeContextValue(entry, knownSecrets, seen));
|
|
159
|
-
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, isCredentialKey(key) ? "[redacted]" : sanitizeContextValue(entry, knownSecrets, seen)]));
|
|
160
|
-
}
|
|
161
|
-
function sanitizeDatabaseErrorContext(context, knownSecrets) {
|
|
162
|
-
return Object.fromEntries(Object.entries(context).map(([key, value]) => [key, SENSITIVE_CONTEXT_KEY.test(key) || isCredentialKey(key) ? "[redacted]" : sanitizeContextValue(value, knownSecrets, /* @__PURE__ */ new WeakSet())]));
|
|
163
|
-
}
|
|
164
|
-
function formatDbErrorWithSecrets(error, knownSecrets) {
|
|
165
|
-
const dbError = toDriverError(error);
|
|
166
|
-
const parts = [];
|
|
167
|
-
if (dbError.message) parts.push(redactDatabaseErrorText(dbError.message, knownSecrets));
|
|
168
|
-
if (dbError.code) parts.push(`code=${redactDatabaseErrorText(dbError.code, knownSecrets)}`);
|
|
169
|
-
if (dbError.detail) parts.push(`detail=${redactDatabaseErrorText(dbError.detail, knownSecrets)}`);
|
|
170
|
-
if (dbError.hint) parts.push(`hint=${redactDatabaseErrorText(dbError.hint, knownSecrets)}`);
|
|
171
|
-
if (dbError.severity) parts.push(`severity=${redactDatabaseErrorText(dbError.severity, knownSecrets)}`);
|
|
172
|
-
if (dbError.errno !== void 0) parts.push(`errno=${dbError.errno}`);
|
|
173
|
-
return parts.length > 0 ? parts.join(", ") : redactDatabaseErrorText(String(error), knownSecrets);
|
|
174
|
-
}
|
|
175
|
-
function safeDatabaseCause(error, knownSecrets) {
|
|
176
|
-
const dbError = toDriverError(error);
|
|
177
|
-
const message = dbError.message ? redactDatabaseErrorText(dbError.message, knownSecrets) : formatDbErrorWithSecrets(error, knownSecrets);
|
|
178
|
-
const cause = new Error(message);
|
|
179
|
-
cause.name = dbError.name ? redactDatabaseErrorText(dbError.name, knownSecrets) : "DatabaseDriverError";
|
|
180
|
-
for (const field of DRIVER_ERROR_FIELDS) {
|
|
181
|
-
const value = dbError[field];
|
|
182
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") Object.defineProperty(cause, field, {
|
|
183
|
-
configurable: true,
|
|
184
|
-
enumerable: true,
|
|
185
|
-
value: typeof value === "string" ? redactDatabaseErrorText(value, knownSecrets) : value,
|
|
186
|
-
writable: true
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
return cause;
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Wraps an engine failure in a serializable, secret-safe DatabaseError.
|
|
193
|
-
*
|
|
194
|
-
* Raw SQL and parameter values are replaced in context. The native `cause` is
|
|
195
|
-
* a sanitized driver-error snapshot rather than the original object so normal
|
|
196
|
-
* Error inspection cannot bypass the same redaction applied to messages and
|
|
197
|
-
* JSON serialization.
|
|
198
|
-
*/
|
|
199
|
-
function wrapDatabaseError(message, error, context = {}) {
|
|
200
|
-
const knownSecrets = contextSecrets(context);
|
|
201
|
-
const originalError = formatDbErrorWithSecrets(error, knownSecrets);
|
|
202
|
-
return new DatabaseError(`${message}: ${originalError}`, {
|
|
203
|
-
...sanitizeDatabaseErrorContext(context, knownSecrets),
|
|
204
|
-
originalError
|
|
205
|
-
}, safeDatabaseCause(error, knownSecrets));
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Resolves the column list for a multi-row INSERT and rejects batches whose
|
|
209
|
-
* records disagree on it.
|
|
210
|
-
*
|
|
211
|
-
* A batch INSERT emits one column list for the whole statement, so it can only
|
|
212
|
-
* come from the first record. That leaves two ways for a later record to be
|
|
213
|
-
* written wrongly, both of them silent:
|
|
214
|
-
*
|
|
215
|
-
* - Its keys are the same but in a different insertion order. Callers that
|
|
216
|
-
* bound values with `Object.values(record)` bound them by position, so the
|
|
217
|
-
* values cross-assigned into each other's columns. Projecting each record
|
|
218
|
-
* through the returned column list — `keys.map((key) => record[key])` — is
|
|
219
|
-
* what makes key order irrelevant, and every caller must do it.
|
|
220
|
-
* - Its key set genuinely differs. An extra key has no column to go to and a
|
|
221
|
-
* missing one has no value, so the record cannot be expressed by this
|
|
222
|
-
* statement at all. Guessing (dropping the extra, writing NULL for the
|
|
223
|
-
* missing) is what makes the failure invisible, so this throws instead.
|
|
224
|
-
*
|
|
225
|
-
* A key whose value is `undefined` counts as absent. Adapters that serialize
|
|
226
|
-
* records first drop such keys before this point while the rest pass them
|
|
227
|
-
* through, so comparing raw key lists would make the same batch throw on some
|
|
228
|
-
* adapters and silently write NULL on others. Only the comparison ignores them;
|
|
229
|
-
* the returned column list is untouched, so each adapter keeps its own
|
|
230
|
-
* single-record handling of `undefined`.
|
|
231
|
-
*
|
|
232
|
-
* @param table - Table being inserted into, for error context
|
|
233
|
-
* @param records - Non-empty batch, as the adapter will bind it
|
|
234
|
-
* @returns Column list taken from the first record
|
|
235
|
-
* @throws DatabaseError if any later record's key set differs from the first's
|
|
236
|
-
*/
|
|
237
|
-
function resolveInsertColumns(table, records) {
|
|
238
|
-
const definedKeys = (record) => new Set(Object.keys(record).filter((key) => record[key] !== void 0));
|
|
239
|
-
const keys = Object.keys(records[0]);
|
|
240
|
-
const expected = definedKeys(records[0]);
|
|
241
|
-
for (let index = 1; index < records.length; index++) {
|
|
242
|
-
const recordKeys = definedKeys(records[index]);
|
|
243
|
-
const missing = [...expected].filter((key) => !recordKeys.has(key));
|
|
244
|
-
const extra = [...recordKeys].filter((key) => !expected.has(key));
|
|
245
|
-
if (missing.length === 0 && extra.length === 0) continue;
|
|
246
|
-
const problems = [missing.length > 0 ? `missing ${missing.join(", ")}` : "", extra.length > 0 ? `unexpected ${extra.join(", ")}` : ""].filter(Boolean);
|
|
247
|
-
throw new DatabaseError(`Batch insert records must all have the same keys; record ${index} has ${problems.join(" and ")} relative to record 0 (${[...expected].join(", ")})`, {
|
|
248
|
-
table,
|
|
249
|
-
recordIndex: index,
|
|
250
|
-
expectedKeys: keys,
|
|
251
|
-
recordKeys: [...recordKeys],
|
|
252
|
-
missing,
|
|
253
|
-
extra,
|
|
254
|
-
hint: "A batch INSERT writes one column list for every row, taken from the first record, so record 0 defines the columns even when it is the one that looks wrong. A key whose value is undefined counts as absent. Split records with differing keys into separate insert() calls, or fill the gaps explicitly with null."
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
return keys;
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Map of valid SQL operators for use in WHERE clauses
|
|
261
|
-
*/
|
|
262
|
-
var VALID_OPERATORS = {
|
|
263
|
-
"=": "=",
|
|
264
|
-
">": ">",
|
|
265
|
-
">=": ">=",
|
|
266
|
-
"<": "<",
|
|
267
|
-
"<=": "<=",
|
|
268
|
-
"!=": "!=",
|
|
269
|
-
like: "LIKE",
|
|
270
|
-
contains: "CONTAINS",
|
|
271
|
-
in: "IN",
|
|
272
|
-
"not in": "NOT IN"
|
|
273
|
-
};
|
|
274
|
-
function isSimpleSqlIdentifier(field) {
|
|
275
|
-
return /^[a-zA-Z0-9_.]+$/.test(field);
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Marks a condition key as caller-authored SQL text.
|
|
279
|
-
*
|
|
280
|
-
* A fixed, non-secret sentinel rather than a random per-process token. A random
|
|
281
|
-
* token has to travel inside the key, which means it reaches every place a key
|
|
282
|
-
* is echoed — error messages, logs, each adapter's `DatabaseError` context — and
|
|
283
|
-
* one disclosure would let a caller mint raw keys for the life of the process.
|
|
284
|
-
* A registry of minted expressions avoids that but is not referentially
|
|
285
|
-
* transparent: it would make `raw('revenue >')` in one query change what the
|
|
286
|
-
* plain key `'revenue >'` means in every other one.
|
|
287
|
-
*
|
|
288
|
-
* The NUL delimiters cannot appear in an identifier or in SQL a developer would
|
|
289
|
-
* write, so the marker never collides with a legitimate key.
|
|
290
|
-
*/
|
|
291
|
-
var RAW_KEY_PREFIX = "\0hv-sql-raw\0";
|
|
292
|
-
/**
|
|
293
|
-
* Marks a WHERE-clause key as SQL expression text rather than an identifier.
|
|
294
|
-
*
|
|
295
|
-
* {@link buildWhere} validates every condition key as a plain identifier. Wrap a
|
|
296
|
-
* key in `raw()` to opt one condition out of that check:
|
|
297
|
-
*
|
|
298
|
-
* ```typescript
|
|
299
|
-
* buildWhere({
|
|
300
|
-
* status: 'paid', // validated as an identifier
|
|
301
|
-
* [raw('SUM(total) >')]: 100, // caller-authored SQL, not validated
|
|
302
|
-
* });
|
|
303
|
-
* ```
|
|
304
|
-
*
|
|
305
|
-
* Never build the argument from end-user input. `raw()` is an assertion that
|
|
306
|
-
* the caller, not the request, authored this SQL.
|
|
307
|
-
*
|
|
308
|
-
* The returned key carries a marker, so it is a distinct string from the bare
|
|
309
|
-
* expression: minting `raw('revenue >')` never changes what the plain key
|
|
310
|
-
* `'revenue >'` means anywhere else.
|
|
311
|
-
*
|
|
312
|
-
* The marker is a fixed sentinel, not a secret. It stops an expression key being
|
|
313
|
-
* used by accident — the shape of the key no longer grants raw access, a call to
|
|
314
|
-
* `raw()` does — but a caller that controls a whole key string verbatim,
|
|
315
|
-
* including the NUL-delimited marker, can still reproduce it. Validate at your
|
|
316
|
-
* own trust boundary; do not rely on this as the only barrier against hostile
|
|
317
|
-
* input.
|
|
318
|
-
*
|
|
319
|
-
* Enforcement is at runtime: `WhereClause` keys are plain `string`, so a
|
|
320
|
-
* computed key widens and TypeScript will not reject an unmarked expression key
|
|
321
|
-
* at the call site.
|
|
322
|
-
*
|
|
323
|
-
* A trailing operator is recognised only if it is one of `=`, `!=`, `>`, `>=`,
|
|
324
|
-
* `<`, `<=`, `like`, `contains`, `in`, `not in`. Any other trailing token is
|
|
325
|
-
* treated as part of the expression and `=` is appended, so
|
|
326
|
-
* `raw('name ILIKE')` silently emits `name ILIKE = $1`, which the database then
|
|
327
|
-
* rejects. Fold an unsupported operator into the expression instead:
|
|
328
|
-
* `raw('LOWER(name) like')`.
|
|
329
|
-
*
|
|
330
|
-
* @param expression - SQL field or expression text, optionally ending in a
|
|
331
|
-
* supported operator (for example `SUM(total) >`)
|
|
332
|
-
* @returns A branded key for use in a {@link WhereClause}
|
|
333
|
-
* @throws If the expression is empty
|
|
334
|
-
*/
|
|
335
|
-
function raw(expression) {
|
|
336
|
-
const trimmed = unwrapRawKey(expression).key.trim();
|
|
337
|
-
if (!trimmed) throw new Error("raw() requires a non-empty SQL expression");
|
|
338
|
-
return `${RAW_KEY_PREFIX}${trimmed}`;
|
|
339
|
-
}
|
|
340
|
-
/**
|
|
341
|
-
* Splits the raw marker off a condition key.
|
|
342
|
-
* @internal
|
|
343
|
-
*/
|
|
344
|
-
function unwrapRawKey(fullKey) {
|
|
345
|
-
return fullKey.startsWith(RAW_KEY_PREFIX) ? {
|
|
346
|
-
key: fullKey.slice(12),
|
|
347
|
-
isRaw: true
|
|
348
|
-
} : {
|
|
349
|
-
key: fullKey,
|
|
350
|
-
isRaw: false
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
function parseConditionKey(fullKey) {
|
|
354
|
-
const trimmed = fullKey.trim();
|
|
355
|
-
const lower = trimmed.toLowerCase();
|
|
356
|
-
for (const operator of [
|
|
357
|
-
"not in",
|
|
358
|
-
"contains",
|
|
359
|
-
"like",
|
|
360
|
-
"in",
|
|
361
|
-
"!=",
|
|
362
|
-
">=",
|
|
363
|
-
"<=",
|
|
364
|
-
">",
|
|
365
|
-
"<",
|
|
366
|
-
"="
|
|
367
|
-
]) if (lower.endsWith(` ${operator}`)) return {
|
|
368
|
-
field: trimmed.slice(0, -(operator.length + 1)).trim(),
|
|
369
|
-
operator
|
|
370
|
-
};
|
|
371
|
-
return {
|
|
372
|
-
field: trimmed,
|
|
373
|
-
operator: "="
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* Builds a single condition for a WHERE clause
|
|
378
|
-
* @internal
|
|
379
|
-
*/
|
|
380
|
-
var buildCondition = (fullKey, value, currIndex, adapterType) => {
|
|
381
|
-
const { key, isRaw } = unwrapRawKey(fullKey);
|
|
382
|
-
const { field, operator } = parseConditionKey(key);
|
|
383
|
-
if (!isRaw && !isSimpleSqlIdentifier(field)) throw new Error(`Invalid SQL identifier: ${field}. Condition keys must be plain identifiers; wrap developer-authored SQL expressions in raw().`);
|
|
384
|
-
const sqlOperator = VALID_OPERATORS[operator] || "=";
|
|
385
|
-
const values = [];
|
|
386
|
-
let sql;
|
|
387
|
-
if (sqlOperator === "CONTAINS") {
|
|
388
|
-
if (!adapterType) throw new Error("CONTAINS requires an adapter type so buildWhere can preserve case-sensitive semantics");
|
|
389
|
-
if (typeof value !== "string") throw new Error("CONTAINS requires a string value");
|
|
390
|
-
const placeholder = `$${currIndex.value++}`;
|
|
391
|
-
sql = `${adapterType === "sqlite" ? "instr" : "strpos"}(${field}, ${placeholder}) > 0`;
|
|
392
|
-
values.push(value);
|
|
393
|
-
} else if (value === null) sql = `${field} IS ${sqlOperator === "=" ? "NULL" : "NOT NULL"}`;
|
|
394
|
-
else if ((sqlOperator === "IN" || sqlOperator === "NOT IN") && Array.isArray(value)) {
|
|
395
|
-
if (value.length === 0) throw new Error(`${sqlOperator} requires at least one value`);
|
|
396
|
-
sql = `${field} ${sqlOperator} (${value.map(() => `$${currIndex.value++}`).join(", ")})`;
|
|
397
|
-
values.push(...value);
|
|
398
|
-
} else if (value instanceof Date) {
|
|
399
|
-
if (adapterType === "duckdb" || adapterType === "json") sql = `${field} ${sqlOperator} CAST($${currIndex.value++} AS TIMESTAMP)${sqlOperator === "LIKE" ? " ESCAPE '\\'" : ""}`;
|
|
400
|
-
else sql = `${field} ${sqlOperator} $${currIndex.value++}${sqlOperator === "LIKE" ? " ESCAPE '\\'" : ""}`;
|
|
401
|
-
values.push(value.toISOString());
|
|
402
|
-
} else {
|
|
403
|
-
sql = `${field} ${sqlOperator} $${currIndex.value++}${sqlOperator === "LIKE" ? " ESCAPE '\\'" : ""}`;
|
|
404
|
-
values.push(value);
|
|
405
|
-
}
|
|
406
|
-
return {
|
|
407
|
-
sql,
|
|
408
|
-
values
|
|
409
|
-
};
|
|
410
|
-
};
|
|
411
|
-
/**
|
|
412
|
-
* Builds a SQL WHERE clause with parameterized values and flexible operators
|
|
413
|
-
*
|
|
414
|
-
* @param where - Conditions as object (AND-only) or 2D array (OR/AND compound)
|
|
415
|
-
* @param startIndex - Starting index for parameter numbering (default: 1)
|
|
416
|
-
* @param adapterType - Database adapter type. Controls Date and `contains`
|
|
417
|
-
* handling:
|
|
418
|
-
* - `'duckdb'` / `'json'`: Wraps Date values in `CAST($N AS TIMESTAMP)` to
|
|
419
|
-
* prevent DuckDB ANY-type inference issues.
|
|
420
|
-
* - `'sqlite'` / `'postgres'` / `undefined`: Passes ISO strings directly
|
|
421
|
-
* (these adapters handle ISO timestamp strings natively).
|
|
422
|
-
* - Required for `contains`, whose case-sensitive literal substring SQL is
|
|
423
|
-
* emitted with adapter-specific functions.
|
|
424
|
-
*
|
|
425
|
-
* Every condition key is validated as a plain SQL identifier, with or without
|
|
426
|
-
* an operator suffix. To use expression text as a key, wrap it in {@link raw}:
|
|
427
|
-
*
|
|
428
|
-
* ```typescript
|
|
429
|
-
* buildWhere({ [raw('LOWER(status) =')]: 'paid' });
|
|
430
|
-
* ```
|
|
431
|
-
*
|
|
432
|
-
* `raw()` is the only way to reach expression text, so mapping untrusted input
|
|
433
|
-
* into a condition key throws rather than emitting attacker-controlled SQL.
|
|
434
|
-
*
|
|
435
|
-
* @returns Object containing the SQL clause and array of values
|
|
436
|
-
* @throws If a key that is not wrapped in {@link raw} is not a plain identifier
|
|
437
|
-
*
|
|
438
|
-
* @example Basic Usage (Object format - AND-only):
|
|
439
|
-
* ```typescript
|
|
440
|
-
* buildWhere({
|
|
441
|
-
* 'status': 'active', // equals operator is default
|
|
442
|
-
* 'price >': 100, // greater than
|
|
443
|
-
* 'stock <=': 5, // less than or equal
|
|
444
|
-
* 'category in': ['A', 'B'], // IN clause for arrays
|
|
445
|
-
* 'name like': '%shirt%', // LIKE pattern; backslash escapes `%` / `_`
|
|
446
|
-
* 'notes contains': '100% off' // literal, case-sensitive substring
|
|
447
|
-
* });
|
|
448
|
-
* ```
|
|
449
|
-
*
|
|
450
|
-
* @example 2D Array Format (OR/AND compound logic):
|
|
451
|
-
* ```typescript
|
|
452
|
-
* // WHERE (status = 'active' AND price > 100) OR (status = 'pending' AND priority = 'high')
|
|
453
|
-
* buildWhere([
|
|
454
|
-
* [{ status: 'active' }, { 'price >': 100 }],
|
|
455
|
-
* [{ status: 'pending' }, { priority: 'high' }]
|
|
456
|
-
* ]);
|
|
457
|
-
* ```
|
|
458
|
-
*
|
|
459
|
-
* @example NULL Handling:
|
|
460
|
-
* ```typescript
|
|
461
|
-
* buildWhere({
|
|
462
|
-
* 'deleted_at': null, // becomes "deleted_at IS NULL"
|
|
463
|
-
* 'updated_at !=': null, // becomes "updated_at IS NOT NULL"
|
|
464
|
-
* 'status': 'active' // regular comparison
|
|
465
|
-
* });
|
|
466
|
-
* ```
|
|
467
|
-
*
|
|
468
|
-
* @example Common Patterns:
|
|
469
|
-
* ```typescript
|
|
470
|
-
* // Price range
|
|
471
|
-
* buildWhere({
|
|
472
|
-
* 'price >=': 10,
|
|
473
|
-
* 'price <': 100
|
|
474
|
-
* });
|
|
475
|
-
*
|
|
476
|
-
* // Date filtering
|
|
477
|
-
* buildWhere({
|
|
478
|
-
* 'created_at >': startDate,
|
|
479
|
-
* 'created_at <=': endDate,
|
|
480
|
-
* 'deleted_at': null
|
|
481
|
-
* });
|
|
482
|
-
*
|
|
483
|
-
* // Search with LIKE
|
|
484
|
-
* buildWhere({
|
|
485
|
-
* 'title like': '%search%',
|
|
486
|
-
* 'description like': '%search%',
|
|
487
|
-
* 'status': 'published'
|
|
488
|
-
* });
|
|
489
|
-
*
|
|
490
|
-
* // Multiple values with IN
|
|
491
|
-
* buildWhere({
|
|
492
|
-
* 'role in': ['admin', 'editor'],
|
|
493
|
-
* 'active': true,
|
|
494
|
-
* 'last_login !=': null
|
|
495
|
-
* });
|
|
496
|
-
* ```
|
|
497
|
-
*
|
|
498
|
-
* The function handles:
|
|
499
|
-
* - Standard comparisons (=, >, >=, <, <=, !=)
|
|
500
|
-
* - NULL checks (IS NULL, IS NOT NULL)
|
|
501
|
-
* - IN clauses for arrays
|
|
502
|
-
* - LIKE for pattern matching with an explicit backslash escape character
|
|
503
|
-
* - Case-sensitive literal substring matching with `contains`
|
|
504
|
-
* - Object format: Multiple conditions combined with AND
|
|
505
|
-
* - 2D array format: Inner arrays ANDed, outer array ORed
|
|
506
|
-
*/
|
|
507
|
-
var buildWhere = (where, startIndex = 1, adapterType) => {
|
|
508
|
-
let sql = "";
|
|
509
|
-
const values = [];
|
|
510
|
-
const currIndex = { value: startIndex };
|
|
511
|
-
if (Array.isArray(where)) {
|
|
512
|
-
if (where.length === 0) return {
|
|
513
|
-
sql: "",
|
|
514
|
-
values: []
|
|
515
|
-
};
|
|
516
|
-
const orGroups = [];
|
|
517
|
-
for (const andGroup of where) {
|
|
518
|
-
if (!Array.isArray(andGroup) || andGroup.length === 0) continue;
|
|
519
|
-
const andConditions = [];
|
|
520
|
-
for (const conditionObj of andGroup) for (const [fullKey, value] of Object.entries(conditionObj)) {
|
|
521
|
-
const result = buildCondition(fullKey, value, currIndex, adapterType);
|
|
522
|
-
andConditions.push(result.sql);
|
|
523
|
-
values.push(...result.values);
|
|
524
|
-
}
|
|
525
|
-
if (andConditions.length > 0) orGroups.push(`(${andConditions.join(" AND ")})`);
|
|
526
|
-
}
|
|
527
|
-
if (orGroups.length > 0) sql = `WHERE ${orGroups.join(" OR ")}`;
|
|
528
|
-
return {
|
|
529
|
-
sql,
|
|
530
|
-
values
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
if (where && Object.keys(where).length > 0) {
|
|
534
|
-
sql = "WHERE ";
|
|
535
|
-
for (const [fullKey, value] of Object.entries(where)) {
|
|
536
|
-
const result = buildCondition(fullKey, value, currIndex, adapterType);
|
|
537
|
-
if (sql !== "WHERE ") sql += " AND ";
|
|
538
|
-
sql += result.sql;
|
|
539
|
-
values.push(...result.values);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
return {
|
|
543
|
-
sql,
|
|
544
|
-
values
|
|
545
|
-
};
|
|
546
|
-
};
|
|
547
|
-
//#endregion
|
|
548
|
-
//#region src/schema-manager.ts
|
|
549
|
-
/**
|
|
550
|
-
* Manages schema initialization from JSON manifests with dependency resolution
|
|
551
|
-
*
|
|
552
|
-
* Handles topological sorting of schema dependencies, table creation,
|
|
553
|
-
* index and trigger setup, and version tracking to avoid redundant re-initialization.
|
|
554
|
-
*/
|
|
555
|
-
var DatabaseSchemaManager = class DatabaseSchemaManager {
|
|
556
|
-
static initializationLock = /* @__PURE__ */ new Map();
|
|
557
|
-
initializedSchemas = /* @__PURE__ */ new Set();
|
|
558
|
-
schemaVersions = /* @__PURE__ */ new Map();
|
|
559
|
-
/**
|
|
560
|
-
* Initializes database schemas from a JSON manifest with dependency resolution
|
|
561
|
-
*
|
|
562
|
-
* Schemas are topologically sorted by their declared dependencies and
|
|
563
|
-
* initialized in order. Supports legacy raw-SQL schemas via the `schema` option,
|
|
564
|
-
* JSON manifests via the `manifest` option, and schema overrides for extending
|
|
565
|
-
* base schemas.
|
|
566
|
-
*
|
|
567
|
-
* @param db - Database interface to execute DDL against
|
|
568
|
-
* @param options - Schema initialization options (manifest, overrides, force, debug)
|
|
569
|
-
* @returns Result with lists of initialized/skipped schemas, errors, and execution time
|
|
570
|
-
*/
|
|
571
|
-
async initializeSchemas(db, options) {
|
|
572
|
-
const startTime = Date.now();
|
|
573
|
-
const result = {
|
|
574
|
-
initialized: [],
|
|
575
|
-
skipped: [],
|
|
576
|
-
errors: [],
|
|
577
|
-
executionTime: 0
|
|
578
|
-
};
|
|
579
|
-
try {
|
|
580
|
-
let schemas = {};
|
|
581
|
-
if (options.schema) {
|
|
582
|
-
console.log("[schema] Legacy SQL schema provided, converting to manifest format");
|
|
583
|
-
if (db.syncSchema) {
|
|
584
|
-
await db.syncSchema(options.schema);
|
|
585
|
-
result.initialized.push("legacy-sql");
|
|
586
|
-
}
|
|
587
|
-
result.executionTime = Date.now() - startTime;
|
|
588
|
-
return result;
|
|
589
|
-
}
|
|
590
|
-
if (options.manifest) schemas = { ...options.manifest.schemas };
|
|
591
|
-
if (options.overrides) schemas = {
|
|
592
|
-
...schemas,
|
|
593
|
-
...options.overrides
|
|
594
|
-
};
|
|
595
|
-
if (Object.keys(schemas).length === 0) {
|
|
596
|
-
console.warn("[schema] No schemas provided for initialization");
|
|
597
|
-
result.executionTime = Date.now() - startTime;
|
|
598
|
-
return result;
|
|
599
|
-
}
|
|
600
|
-
const dependencyGraph = this.buildDependencyGraph(schemas);
|
|
601
|
-
const initializationOrder = this.resolveDependencies(dependencyGraph);
|
|
602
|
-
if (options.debug) console.log("[schema] Initialization order:", initializationOrder);
|
|
603
|
-
for (const schemaName of initializationOrder) {
|
|
604
|
-
const schema = schemas[schemaName];
|
|
605
|
-
if (!schema) continue;
|
|
606
|
-
try {
|
|
607
|
-
await this.initializeSchema(db, schemaName, schema, {
|
|
608
|
-
force: options.force,
|
|
609
|
-
debug: options.debug
|
|
610
|
-
});
|
|
611
|
-
result.initialized.push(schemaName);
|
|
612
|
-
} catch (error) {
|
|
613
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
614
|
-
result.errors.push({
|
|
615
|
-
schema: schemaName,
|
|
616
|
-
error: errorMessage
|
|
617
|
-
});
|
|
618
|
-
if (options.debug) console.error(`[schema] Failed to initialize ${schemaName}:`, error);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
result.executionTime = Date.now() - startTime;
|
|
622
|
-
if (options.debug) console.log("[schema] Initialization complete:", result);
|
|
623
|
-
return result;
|
|
624
|
-
} catch (error) {
|
|
625
|
-
result.executionTime = Date.now() - startTime;
|
|
626
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
627
|
-
result.errors.push({
|
|
628
|
-
schema: "dependency-resolution",
|
|
629
|
-
error: errorMessage
|
|
630
|
-
});
|
|
631
|
-
return result;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
/**
|
|
635
|
-
* Initialize a single schema
|
|
636
|
-
*/
|
|
637
|
-
async initializeSchema(db, schemaName, schema, options) {
|
|
638
|
-
const { tableName, version } = schema;
|
|
639
|
-
const lockKey = `${schemaName}:${tableName}`;
|
|
640
|
-
if (DatabaseSchemaManager.initializationLock.has(lockKey)) {
|
|
641
|
-
await DatabaseSchemaManager.initializationLock.get(lockKey);
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
if (!options.force && this.isSchemaUpToDate(schemaName, version)) {
|
|
645
|
-
if (options.debug) console.log(`[schema] Skipping ${schemaName} - already up to date`);
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
const initPromise = this.performSchemaInitialization(db, schemaName, schema, options);
|
|
649
|
-
DatabaseSchemaManager.initializationLock.set(lockKey, initPromise);
|
|
650
|
-
try {
|
|
651
|
-
await initPromise;
|
|
652
|
-
this.markSchemaInitialized(schemaName, version);
|
|
653
|
-
} finally {
|
|
654
|
-
DatabaseSchemaManager.initializationLock.delete(lockKey);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
/**
|
|
658
|
-
* Perform the actual schema initialization
|
|
659
|
-
*/
|
|
660
|
-
async performSchemaInitialization(db, schemaName, schema, options) {
|
|
661
|
-
const { tableName, columns, indexes, triggers } = schema;
|
|
662
|
-
if (options.debug) console.log(`[schema] Initializing ${schemaName} (${tableName})`);
|
|
663
|
-
if (!await db.tableExists(tableName)) {
|
|
664
|
-
await this.createTable(db, schema);
|
|
665
|
-
for (const index of indexes) await this.createIndex(db, tableName, index);
|
|
666
|
-
for (const trigger of triggers) await this.createTrigger(db, trigger);
|
|
667
|
-
} else if (options.force) {
|
|
668
|
-
await db.query(`DROP TABLE IF EXISTS ${tableName}`);
|
|
669
|
-
await this.createTable(db, schema);
|
|
670
|
-
for (const index of indexes) await this.createIndex(db, tableName, index);
|
|
671
|
-
for (const trigger of triggers) await this.createTrigger(db, trigger);
|
|
672
|
-
} else await this.updateSchemaIfNeeded(db, schema);
|
|
673
|
-
}
|
|
674
|
-
/**
|
|
675
|
-
* Create table from schema definition
|
|
676
|
-
*/
|
|
677
|
-
async createTable(db, schema) {
|
|
678
|
-
const { tableName, columns, foreignKeys } = schema;
|
|
679
|
-
const columnDefinitions = [];
|
|
680
|
-
for (const [columnName, columnDef] of Object.entries(columns)) {
|
|
681
|
-
let def = `${columnName} ${columnDef.type}`;
|
|
682
|
-
if (columnDef.primaryKey) def += " PRIMARY KEY";
|
|
683
|
-
if (columnDef.unique && !columnDef.primaryKey) def += " UNIQUE";
|
|
684
|
-
if (columnDef.notNull) def += " NOT NULL";
|
|
685
|
-
if (columnDef.defaultValue !== void 0) def += ` DEFAULT ${columnDef.defaultValue}`;
|
|
686
|
-
if (columnDef.check) def += ` CHECK (${columnDef.check})`;
|
|
687
|
-
columnDefinitions.push(def);
|
|
688
|
-
}
|
|
689
|
-
for (const fk of foreignKeys) {
|
|
690
|
-
let fkDef = `FOREIGN KEY (${fk.column}) REFERENCES ${fk.referencesTable}(${fk.referencesColumn})`;
|
|
691
|
-
if (fk.onDelete) fkDef += ` ON DELETE ${fk.onDelete}`;
|
|
692
|
-
if (fk.onUpdate) fkDef += ` ON UPDATE ${fk.onUpdate}`;
|
|
693
|
-
columnDefinitions.push(fkDef);
|
|
694
|
-
}
|
|
695
|
-
const createTableSQL = `CREATE TABLE ${tableName} (\n ${columnDefinitions.join(",\n ")}\n)`;
|
|
696
|
-
await db.query(createTableSQL);
|
|
697
|
-
}
|
|
698
|
-
/**
|
|
699
|
-
* Create index from definition
|
|
700
|
-
*/
|
|
701
|
-
async createIndex(db, tableName, index) {
|
|
702
|
-
const uniqueClause = index.unique ? "UNIQUE " : "";
|
|
703
|
-
const whereClause = index.where ? ` WHERE ${index.where}` : "";
|
|
704
|
-
const createIndexSQL = `CREATE ${uniqueClause}INDEX ${index.name} ON ${tableName} (${index.columns.join(", ")})${whereClause}`;
|
|
705
|
-
await db.query(createIndexSQL);
|
|
706
|
-
}
|
|
707
|
-
/**
|
|
708
|
-
* Create trigger from definition
|
|
709
|
-
*/
|
|
710
|
-
async createTrigger(db, trigger) {
|
|
711
|
-
const conditionClause = trigger.condition ? ` WHEN ${trigger.condition}` : "";
|
|
712
|
-
const createTriggerSQL = `CREATE TRIGGER ${trigger.name} ${trigger.when} ${trigger.event} ON ${trigger.table}${conditionClause} BEGIN ${trigger.body} END`;
|
|
713
|
-
await db.query(createTriggerSQL);
|
|
714
|
-
}
|
|
715
|
-
/**
|
|
716
|
-
* Update schema if changes are detected
|
|
717
|
-
*/
|
|
718
|
-
async updateSchemaIfNeeded(_db, schema) {
|
|
719
|
-
console.log(`[schema] Schema update logic not yet implemented for ${schema.tableName}`);
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
|
-
* Build dependency graph from schemas
|
|
723
|
-
*/
|
|
724
|
-
buildDependencyGraph(schemas) {
|
|
725
|
-
const graph = /* @__PURE__ */ new Map();
|
|
726
|
-
for (const [schemaName, schema] of Object.entries(schemas)) {
|
|
727
|
-
const dependencies = schema.dependencies.filter((dep) => Object.values(schemas).some((s) => s.tableName === dep));
|
|
728
|
-
graph.set(schemaName, dependencies.map((dep) => Object.entries(schemas).find(([_, s]) => s.tableName === dep)?.[0] || dep));
|
|
729
|
-
}
|
|
730
|
-
return graph;
|
|
731
|
-
}
|
|
732
|
-
/**
|
|
733
|
-
* Resolve dependencies using topological sort
|
|
734
|
-
*/
|
|
735
|
-
resolveDependencies(graph) {
|
|
736
|
-
const resolved = [];
|
|
737
|
-
const visited = /* @__PURE__ */ new Set();
|
|
738
|
-
const visiting = /* @__PURE__ */ new Set();
|
|
739
|
-
const visit = (node) => {
|
|
740
|
-
if (visiting.has(node)) throw new Error(`Circular dependency detected involving ${node}`);
|
|
741
|
-
if (visited.has(node)) return;
|
|
742
|
-
visiting.add(node);
|
|
743
|
-
const dependencies = graph.get(node) || [];
|
|
744
|
-
for (const dep of dependencies) if (graph.has(dep)) visit(dep);
|
|
745
|
-
visiting.delete(node);
|
|
746
|
-
visited.add(node);
|
|
747
|
-
resolved.push(node);
|
|
748
|
-
};
|
|
749
|
-
for (const node of graph.keys()) if (!visited.has(node)) visit(node);
|
|
750
|
-
return resolved;
|
|
751
|
-
}
|
|
752
|
-
/**
|
|
753
|
-
* Check if schema is up to date
|
|
754
|
-
*/
|
|
755
|
-
isSchemaUpToDate(schemaName, version) {
|
|
756
|
-
return this.initializedSchemas.has(schemaName) && this.schemaVersions.get(schemaName) === version;
|
|
757
|
-
}
|
|
758
|
-
/**
|
|
759
|
-
* Mark schema as initialized
|
|
760
|
-
*/
|
|
761
|
-
markSchemaInitialized(schemaName, version) {
|
|
762
|
-
this.initializedSchemas.add(schemaName);
|
|
763
|
-
this.schemaVersions.set(schemaName, version);
|
|
764
|
-
}
|
|
765
|
-
/**
|
|
766
|
-
* Reset initialization state (for testing)
|
|
767
|
-
*/
|
|
768
|
-
reset() {
|
|
769
|
-
this.initializedSchemas.clear();
|
|
770
|
-
this.schemaVersions.clear();
|
|
771
|
-
DatabaseSchemaManager.initializationLock.clear();
|
|
772
|
-
}
|
|
773
|
-
};
|
|
774
|
-
//#endregion
|
|
775
3
|
//#region src/shared/alter-utils.ts
|
|
776
4
|
/**
|
|
777
5
|
* Generates an ALTER TABLE ADD COLUMN statement
|
|
@@ -2723,6 +1951,6 @@ function createAlterTable(client) {
|
|
|
2723
1951
|
};
|
|
2724
1952
|
}
|
|
2725
1953
|
//#endregion
|
|
2726
|
-
export {
|
|
1954
|
+
export { validateColumnNames as _, createVectorCapabilities as a, createTransactionLock as c, validateDatabaseCacheOptions as d, escapeQuotedIdentifier as f, validateColumnName as g, generateCreateIndexStatement as h, createTransactionErrorTracker as i, ConnectionCache as l, generateAddColumnStatement as m, createGetTableSchema as n, normalizePostgresRawQuery as o, escapeStringLiteral as p, createPostgresDialect as r, DEFAULT_TRANSACTION_QUEUE_TIMEOUT_MS as s, createAlterTable as t, enableCachedValidation as u, validateIndexName as v, validateTableName as y };
|
|
2727
1955
|
|
|
2728
|
-
//# sourceMappingURL=postgres-dialect-
|
|
1956
|
+
//# sourceMappingURL=postgres-dialect-CGv6DxSP.js.map
|