tempest-db-js 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/bin.cjs +1921 -12
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +31 -2
- package/dist/bin.js.map +1 -1
- package/dist/chunk-HXK6WIBP.js +5897 -0
- package/dist/chunk-HXK6WIBP.js.map +1 -0
- package/dist/{chunk-SI4CLSF7.js → chunk-ZBIHRVUS.js} +255 -11
- package/dist/chunk-ZBIHRVUS.js.map +1 -0
- package/dist/index.cjs +3156 -280
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3058 -549
- package/dist/index.d.ts +3058 -549
- package/dist/index.js +1 -1
- package/dist/migrations/index.cjs +1654 -11
- package/dist/migrations/index.cjs.map +1 -1
- package/dist/migrations/index.d.cts +30 -1
- package/dist/migrations/index.d.ts +30 -1
- package/dist/migrations/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-4AWUP7BM.js +0 -3076
- package/dist/chunk-4AWUP7BM.js.map +0 -1
- package/dist/chunk-SI4CLSF7.js.map +0 -1
|
@@ -3,11 +3,145 @@
|
|
|
3
3
|
var module$1 = require('module');
|
|
4
4
|
|
|
5
5
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
6
|
+
// src/conditions.ts
|
|
7
|
+
var CONDITION = /* @__PURE__ */ Symbol.for("tempest-db-js.condition");
|
|
8
|
+
function wrap(node) {
|
|
9
|
+
return { [CONDITION]: true, node };
|
|
10
|
+
}
|
|
11
|
+
function toExprNode(operand) {
|
|
12
|
+
return isExpression(operand) ? operand.node : { kind: "value", value: operand };
|
|
13
|
+
}
|
|
14
|
+
function assertValueOperands(op, operands) {
|
|
15
|
+
if (operands.some(isExpression)) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`The "${op}" operator binds its operands, so it takes values, not expressions.`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function isExpression(value) {
|
|
22
|
+
return value instanceof Expression;
|
|
23
|
+
}
|
|
24
|
+
var Expression = class {
|
|
25
|
+
constructor(node) {
|
|
26
|
+
this.node = node;
|
|
27
|
+
}
|
|
28
|
+
node;
|
|
29
|
+
/** Compare this expression against another expression or a bound value. */
|
|
30
|
+
compare(op, operand) {
|
|
31
|
+
return wrap({ kind: "compare", left: this.node, op, right: toExprNode(operand) });
|
|
32
|
+
}
|
|
33
|
+
/** `=` (or `IS NULL` for a null value). */
|
|
34
|
+
eq(operand) {
|
|
35
|
+
return this.compare("eq", operand);
|
|
36
|
+
}
|
|
37
|
+
/** `<>` (or `IS NOT NULL` for a null value). */
|
|
38
|
+
ne(operand) {
|
|
39
|
+
return this.compare("ne", operand);
|
|
40
|
+
}
|
|
41
|
+
/** `>`. */
|
|
42
|
+
gt(operand) {
|
|
43
|
+
return this.compare("gt", operand);
|
|
44
|
+
}
|
|
45
|
+
/** `>=`. */
|
|
46
|
+
gte(operand) {
|
|
47
|
+
return this.compare("gte", operand);
|
|
48
|
+
}
|
|
49
|
+
/** `<`. */
|
|
50
|
+
lt(operand) {
|
|
51
|
+
return this.compare("lt", operand);
|
|
52
|
+
}
|
|
53
|
+
/** `<=`. */
|
|
54
|
+
lte(operand) {
|
|
55
|
+
return this.compare("lte", operand);
|
|
56
|
+
}
|
|
57
|
+
/** `LIKE` — `%` and `_` in the operand are wildcards. */
|
|
58
|
+
like(pattern) {
|
|
59
|
+
return this.compare("like", pattern);
|
|
60
|
+
}
|
|
61
|
+
/** `ILIKE` — case-insensitive **pattern** matching, wildcards included. */
|
|
62
|
+
ilike(pattern) {
|
|
63
|
+
return this.compare("ilike", pattern);
|
|
64
|
+
}
|
|
65
|
+
/** Case-insensitive equality (`lower(a) = lower(b)`), with no wildcards. */
|
|
66
|
+
ieq(operand) {
|
|
67
|
+
return this.compare("ieq", operand);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* `IN (...)` over a list of values.
|
|
71
|
+
*
|
|
72
|
+
* @param values The values to test against.
|
|
73
|
+
* @returns The condition.
|
|
74
|
+
* @throws Error When an entry is an {@link Expression} — a list operand is
|
|
75
|
+
* bound, so an expression there would be serialized as a parameter instead of
|
|
76
|
+
* rendered as SQL.
|
|
77
|
+
*/
|
|
78
|
+
in(values) {
|
|
79
|
+
assertValueOperands("in", values);
|
|
80
|
+
return this.compare("in", values);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* `NOT IN (...)` over a list of values.
|
|
84
|
+
*
|
|
85
|
+
* @param values The values to exclude.
|
|
86
|
+
* @returns The condition.
|
|
87
|
+
* @throws Error When an entry is an {@link Expression} (see {@link Expression.in}).
|
|
88
|
+
*/
|
|
89
|
+
notIn(values) {
|
|
90
|
+
assertValueOperands("notIn", values);
|
|
91
|
+
return this.compare("notIn", values);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* `BETWEEN lo AND hi` (inclusive).
|
|
95
|
+
*
|
|
96
|
+
* @param lo The lower bound.
|
|
97
|
+
* @param hi The upper bound.
|
|
98
|
+
* @returns The condition.
|
|
99
|
+
* @throws Error When a bound is an {@link Expression} (see {@link Expression.in}).
|
|
100
|
+
*/
|
|
101
|
+
between(lo, hi) {
|
|
102
|
+
assertValueOperands("between", [lo, hi]);
|
|
103
|
+
return this.compare("between", [lo, hi]);
|
|
104
|
+
}
|
|
105
|
+
/** `IS NULL` (true) / `IS NOT NULL` (false). */
|
|
106
|
+
isNull(value = true) {
|
|
107
|
+
return this.compare("isNull", value);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// src/query.ts
|
|
112
|
+
function isSubquery(value) {
|
|
113
|
+
return typeof value === "object" && value !== null && value.node?.kind === "select";
|
|
114
|
+
}
|
|
115
|
+
var OPERATORS = [
|
|
116
|
+
"eq",
|
|
117
|
+
"ne",
|
|
118
|
+
"gt",
|
|
119
|
+
"gte",
|
|
120
|
+
"lt",
|
|
121
|
+
"lte",
|
|
122
|
+
"like",
|
|
123
|
+
"ilike",
|
|
124
|
+
"ieq",
|
|
125
|
+
"iContains",
|
|
126
|
+
"in",
|
|
127
|
+
"notIn",
|
|
128
|
+
"between",
|
|
129
|
+
"isNull",
|
|
130
|
+
"contains",
|
|
131
|
+
"containedBy",
|
|
132
|
+
"overlaps"
|
|
133
|
+
];
|
|
134
|
+
|
|
6
135
|
// src/expressions.ts
|
|
136
|
+
function renderExcluded(column, bare, dialect) {
|
|
137
|
+
return dialect === "mysql" ? `VALUES(${bare})` : `excluded.${column}`;
|
|
138
|
+
}
|
|
7
139
|
function renderPortableToken(token, dialect) {
|
|
8
140
|
switch (token) {
|
|
9
141
|
case "now":
|
|
10
|
-
|
|
142
|
+
if (dialect === "postgresql") return "now()";
|
|
143
|
+
if (dialect === "sqlite") return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
144
|
+
return "CURRENT_TIMESTAMP";
|
|
11
145
|
case "current_date":
|
|
12
146
|
return "CURRENT_DATE";
|
|
13
147
|
case "current_time":
|
|
@@ -18,6 +152,1264 @@ function renderPortableToken(token, dialect) {
|
|
|
18
152
|
return "(lower(hex(randomblob(16))))";
|
|
19
153
|
}
|
|
20
154
|
}
|
|
155
|
+
|
|
156
|
+
// src/search.ts
|
|
157
|
+
function escapeLike(value) {
|
|
158
|
+
return value.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/dialect.ts
|
|
162
|
+
var OPERATOR_SET = new Set(OPERATORS);
|
|
163
|
+
var MULTI_VALUE_OPERATORS = /* @__PURE__ */ new Set(["in", "notIn", "between"]);
|
|
164
|
+
function encodeOperand(op, operand, encode) {
|
|
165
|
+
if (op === "isNull") return operand;
|
|
166
|
+
if (MULTI_VALUE_OPERATORS.has(op)) {
|
|
167
|
+
return Array.isArray(operand) ? operand.map(encode) : operand;
|
|
168
|
+
}
|
|
169
|
+
return encode(operand);
|
|
170
|
+
}
|
|
171
|
+
function codecEncoder(codecs) {
|
|
172
|
+
if (!codecs) return (_key, value) => value;
|
|
173
|
+
return (key, value) => {
|
|
174
|
+
const codec = codecs[key];
|
|
175
|
+
return codec ? codec.toDb(value) : value;
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function isOperatorObject(value) {
|
|
179
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date || value instanceof Uint8Array) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
const keys = Object.keys(value);
|
|
183
|
+
return keys.length > 0 && keys.every((k) => OPERATOR_SET.has(k));
|
|
184
|
+
}
|
|
185
|
+
var Params = class {
|
|
186
|
+
constructor(placeholder2) {
|
|
187
|
+
this.placeholder = placeholder2;
|
|
188
|
+
}
|
|
189
|
+
placeholder;
|
|
190
|
+
values = [];
|
|
191
|
+
bind(value) {
|
|
192
|
+
this.values.push(value);
|
|
193
|
+
return this.placeholder(this.values.length);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
var LiteralParams = class extends Params {
|
|
197
|
+
constructor() {
|
|
198
|
+
super(() => "");
|
|
199
|
+
}
|
|
200
|
+
bind(value) {
|
|
201
|
+
if (value === null || value === void 0) return "NULL";
|
|
202
|
+
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
|
|
203
|
+
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
204
|
+
if (value instanceof Date) return `'${value.toISOString()}'`;
|
|
205
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
function insertColumns(rows) {
|
|
209
|
+
const columns = [];
|
|
210
|
+
const seen = /* @__PURE__ */ new Set();
|
|
211
|
+
for (const row of rows) {
|
|
212
|
+
for (const key of Object.keys(row)) {
|
|
213
|
+
if (seen.has(key)) continue;
|
|
214
|
+
seen.add(key);
|
|
215
|
+
columns.push(key);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return columns;
|
|
219
|
+
}
|
|
220
|
+
function insertHasExpression(node) {
|
|
221
|
+
for (const row of node.values) {
|
|
222
|
+
for (const value of Object.values(row)) {
|
|
223
|
+
if (isSqlExpression(value)) return true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const update = node.onConflict?.update;
|
|
227
|
+
if (update && update !== "nothing") {
|
|
228
|
+
for (const value of Object.values(update)) {
|
|
229
|
+
if (isSqlExpression(value)) return true;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
var BaseDialect = class _BaseDialect {
|
|
235
|
+
/**
|
|
236
|
+
* INSERT SQL templates keyed by structure (dialect|table|columns|rowCount|
|
|
237
|
+
* returning). Shared across dialect instances — the key namespaces by dialect
|
|
238
|
+
* name, and the placeholder text is dialect-specific but structure-determined.
|
|
239
|
+
*/
|
|
240
|
+
static insertTemplates = /* @__PURE__ */ new Map();
|
|
241
|
+
/** Quoted-identifier cache (see {@link quoteId}). Shared across dialects. */
|
|
242
|
+
static quotedIds = /* @__PURE__ */ new Map();
|
|
243
|
+
/**
|
|
244
|
+
* Render a case-insensitive LIKE whose pattern carries escaped wildcards.
|
|
245
|
+
*
|
|
246
|
+
* The `ESCAPE` clause is not decoration: PostgreSQL treats `\` as the escape
|
|
247
|
+
* character by default, **SQLite has none at all** until one is declared, so
|
|
248
|
+
* without this the escaping done on our side would be meaningless there.
|
|
249
|
+
*
|
|
250
|
+
* @param column The rendered column.
|
|
251
|
+
* @param param The bound pattern.
|
|
252
|
+
* @returns The rendered comparison.
|
|
253
|
+
*/
|
|
254
|
+
ilikeEscaped(column, param) {
|
|
255
|
+
return `${this.ilike(column, param)} ESCAPE '\\'`;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Validate a subquery operand before it is rendered, for dialects that restrict
|
|
259
|
+
* what an `IN (SELECT ...)` may contain. The default accepts everything.
|
|
260
|
+
*
|
|
261
|
+
* @param _node The subquery's AST.
|
|
262
|
+
* @throws Error When the dialect cannot execute this subquery.
|
|
263
|
+
*/
|
|
264
|
+
checkSubquery(_node) {
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The SQL operator for an array containment/overlap test.
|
|
268
|
+
*
|
|
269
|
+
* Only PostgreSQL has native arrays; the other dialects throw rather than
|
|
270
|
+
* emitting an operator that means something else there.
|
|
271
|
+
*
|
|
272
|
+
* @param op The array operator name.
|
|
273
|
+
* @returns The SQL operator text.
|
|
274
|
+
* @throws Error On a dialect without native array support.
|
|
275
|
+
*/
|
|
276
|
+
arrayOperator(op) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`The "${op}" operator needs native array support, which ${this.name} does not have.`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Quote an identifier (column/table) for the active dialect.
|
|
283
|
+
*
|
|
284
|
+
* Memoized: identifiers form a small, stable set (column/table names), but this
|
|
285
|
+
* runs for every identifier on every compile. Caching the quoted form removes a
|
|
286
|
+
* regex-replace + string allocation from the hot path. The standard double-quote
|
|
287
|
+
* form is identical across both dialects, so one shared cache is correct.
|
|
288
|
+
*/
|
|
289
|
+
quoteId(name) {
|
|
290
|
+
const cached = _BaseDialect.quotedIds.get(name);
|
|
291
|
+
if (cached !== void 0) return cached;
|
|
292
|
+
const quoted = `"${name.replace(/"/g, '""')}"`;
|
|
293
|
+
_BaseDialect.quotedIds.set(name, quoted);
|
|
294
|
+
return quoted;
|
|
295
|
+
}
|
|
296
|
+
/** Compile any node to `{ sql, params }`. */
|
|
297
|
+
compile(node) {
|
|
298
|
+
const params = new Params((i) => this.placeholder(i));
|
|
299
|
+
let sql;
|
|
300
|
+
switch (node.kind) {
|
|
301
|
+
case "select":
|
|
302
|
+
sql = this.compileSelect(node, params);
|
|
303
|
+
break;
|
|
304
|
+
case "insert":
|
|
305
|
+
sql = this.compileInsert(node, params);
|
|
306
|
+
break;
|
|
307
|
+
case "update":
|
|
308
|
+
sql = this.compileUpdate(node, params);
|
|
309
|
+
break;
|
|
310
|
+
case "delete":
|
|
311
|
+
sql = this.compileDelete(node, params);
|
|
312
|
+
break;
|
|
313
|
+
case "join_select":
|
|
314
|
+
sql = this.compileJoin(node, params);
|
|
315
|
+
break;
|
|
316
|
+
case "set_op":
|
|
317
|
+
sql = this.compileSetOp(node, params);
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
return { sql, params: params.values };
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Render a condition as schema SQL, with values inlined.
|
|
324
|
+
*
|
|
325
|
+
* Same compiler as a `WHERE`, different parameter strategy — so a `CHECK` and
|
|
326
|
+
* the query language cannot drift apart in what they mean.
|
|
327
|
+
*
|
|
328
|
+
* @param node The condition.
|
|
329
|
+
* @param params A {@link LiteralParams}.
|
|
330
|
+
* @returns The rendered predicate.
|
|
331
|
+
*/
|
|
332
|
+
renderConditionLiteral(node, params) {
|
|
333
|
+
return this.compileCondition(node, params, (key) => this.columnId(key, void 0));
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Render the `WITH` clause of a statement.
|
|
337
|
+
*
|
|
338
|
+
* `RECURSIVE` is a property of the **clause**, not of an entry: one recursive
|
|
339
|
+
* entry makes the whole `WITH` recursive, which is what the SQL standard says
|
|
340
|
+
* and what PostgreSQL and SQLite both implement.
|
|
341
|
+
*
|
|
342
|
+
* @param entries The `WITH` entries, if any.
|
|
343
|
+
* @param params The parameter collector.
|
|
344
|
+
* @returns The clause with a trailing space, or an empty string.
|
|
345
|
+
*/
|
|
346
|
+
compileWith(entries, params) {
|
|
347
|
+
if (!entries || entries.length === 0) return "";
|
|
348
|
+
const recursive = entries.some((entry) => entry.recursive) ? "RECURSIVE " : "";
|
|
349
|
+
const rendered = entries.map((entry) => {
|
|
350
|
+
const body = entry.body.kind === "set_op" ? this.compileSetOp(entry.body, params) : this.compileSelect(entry.body, params);
|
|
351
|
+
const hint = entry.materialized === null ? "" : entry.materialized ? " MATERIALIZED" : " NOT MATERIALIZED";
|
|
352
|
+
return `${this.quoteId(entry.name)} AS${hint} (${body})`;
|
|
353
|
+
});
|
|
354
|
+
return `WITH ${recursive}${rendered.join(", ")} `;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* The `EXPLAIN` prefix for this dialect.
|
|
358
|
+
*
|
|
359
|
+
* @param analyze Whether to measure by actually running the statement.
|
|
360
|
+
* @returns The prefix to put in front of the statement.
|
|
361
|
+
* @throws Error When the dialect cannot do what was asked.
|
|
362
|
+
*/
|
|
363
|
+
explainPrefix(analyze) {
|
|
364
|
+
return analyze ? "EXPLAIN (FORMAT JSON, ANALYZE)" : "EXPLAIN (FORMAT JSON)";
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* The SQL keyword for a set operation.
|
|
368
|
+
*
|
|
369
|
+
* @param op The operator.
|
|
370
|
+
* @returns The keyword.
|
|
371
|
+
* @throws Error On a dialect that does not implement it.
|
|
372
|
+
*/
|
|
373
|
+
setOperator(op) {
|
|
374
|
+
switch (op) {
|
|
375
|
+
case "union":
|
|
376
|
+
return "UNION";
|
|
377
|
+
case "unionAll":
|
|
378
|
+
return "UNION ALL";
|
|
379
|
+
case "intersect":
|
|
380
|
+
return "INTERSECT";
|
|
381
|
+
case "except":
|
|
382
|
+
return "EXCEPT";
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Compile a set operation.
|
|
387
|
+
*
|
|
388
|
+
* A branch carrying its own `ORDER BY`/`LIMIT` is parenthesized: without the
|
|
389
|
+
* parentheses those clauses bind to the **combined** result, which is a
|
|
390
|
+
* different query and a classic source of silently wrong output.
|
|
391
|
+
*
|
|
392
|
+
* @param node The set-operation node.
|
|
393
|
+
* @param params The parameter collector.
|
|
394
|
+
* @returns The rendered statement.
|
|
395
|
+
*/
|
|
396
|
+
compileSetOp(node, params) {
|
|
397
|
+
const keyword = ` ${this.setOperator(node.op)} `;
|
|
398
|
+
const branches = node.branches.map((branch) => {
|
|
399
|
+
const sql2 = branch.kind === "join_select" ? this.compileJoin(branch, params) : this.compileSelect(branch, params);
|
|
400
|
+
const scoped = branch.orderBy.length > 0 || branch.limit !== void 0;
|
|
401
|
+
return scoped ? `(${sql2})` : sql2;
|
|
402
|
+
});
|
|
403
|
+
let sql = branches.join(keyword);
|
|
404
|
+
if (node.orderBy.length > 0) {
|
|
405
|
+
const terms = node.orderBy.map((t) => {
|
|
406
|
+
const id = typeof t.column === "string" ? this.quoteId(t.column) : this.renderExpr(t.column, params, (k) => this.quoteId(k));
|
|
407
|
+
return `${id} ${t.direction === "desc" ? "DESC" : "ASC"}`;
|
|
408
|
+
}).join(", ");
|
|
409
|
+
sql += ` ORDER BY ${terms}`;
|
|
410
|
+
}
|
|
411
|
+
if (node.limit !== void 0) sql += ` LIMIT ${params.bind(node.limit)}`;
|
|
412
|
+
if (node.offset !== void 0) sql += ` OFFSET ${params.bind(node.offset)}`;
|
|
413
|
+
return sql;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Render a qualified `alias.column` ref as `"alias"."column"`, translating the
|
|
417
|
+
* property name to the real column name for that alias's model.
|
|
418
|
+
*
|
|
419
|
+
* @param ref The `alias.property` reference (a bare name is left unqualified).
|
|
420
|
+
* @param names The node's per-alias name maps, if any source renames columns.
|
|
421
|
+
* @returns The quoted, qualified identifier.
|
|
422
|
+
*/
|
|
423
|
+
qualify(ref, names) {
|
|
424
|
+
const dot = ref.indexOf(".");
|
|
425
|
+
if (dot === -1) return this.quoteId(ref);
|
|
426
|
+
const alias = ref.slice(0, dot);
|
|
427
|
+
const prop = ref.slice(dot + 1);
|
|
428
|
+
return `${this.quoteId(alias)}.${this.columnId(prop, names?.[alias])}`;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Quote a column identifier, translating the model property name to the real
|
|
432
|
+
* database column name first.
|
|
433
|
+
*
|
|
434
|
+
* `names` is `undefined` for a model that renames nothing — the overwhelmingly
|
|
435
|
+
* common case — so this stays a single lookup plus the memoized quote.
|
|
436
|
+
*
|
|
437
|
+
* @param prop The model property name as written in the builder.
|
|
438
|
+
* @param names The node's property → column map, if any.
|
|
439
|
+
* @returns The quoted database identifier.
|
|
440
|
+
*/
|
|
441
|
+
columnId(prop, names) {
|
|
442
|
+
const mapped = names?.[prop];
|
|
443
|
+
if (mapped !== void 0) return this.quoteId(mapped);
|
|
444
|
+
const dot = prop.indexOf(".");
|
|
445
|
+
if (dot > 0 && dot < prop.length - 1) {
|
|
446
|
+
return `${this.quoteId(prop.slice(0, dot))}.${this.quoteId(prop.slice(dot + 1))}`;
|
|
447
|
+
}
|
|
448
|
+
return this.quoteId(prop);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Render a {@link SqlExpression} inline, binding the parameters it carries.
|
|
452
|
+
*
|
|
453
|
+
* This is what keeps `set({ attempts: sql.raw("attempts + 1") })` an expression
|
|
454
|
+
* instead of a bound object: the fragment goes into the statement text, and
|
|
455
|
+
* only a `sql.expr` template's interpolations become parameters.
|
|
456
|
+
*
|
|
457
|
+
* @param expr The branded expression.
|
|
458
|
+
* @param params The parameter collector for the statement being compiled.
|
|
459
|
+
* @returns The SQL text of the expression.
|
|
460
|
+
*/
|
|
461
|
+
renderExpression(expr, params) {
|
|
462
|
+
const token = expr.expression;
|
|
463
|
+
if (typeof token === "string") return renderPortableToken(token, this.name);
|
|
464
|
+
if ("raw" in token) return token.raw;
|
|
465
|
+
if ("excluded" in token) {
|
|
466
|
+
return renderExcluded(this.quoteId(token.excluded), token.excluded, this.name);
|
|
467
|
+
}
|
|
468
|
+
const parts = token.parts;
|
|
469
|
+
let sql = parts[0] ?? "";
|
|
470
|
+
for (let i = 1; i < parts.length; i++) {
|
|
471
|
+
sql += `${params.bind(expr.params[i - 1])}${parts[i]}`;
|
|
472
|
+
}
|
|
473
|
+
return sql;
|
|
474
|
+
}
|
|
475
|
+
/** Render one write value: a SQL expression inline, anything else as a parameter. */
|
|
476
|
+
renderValue(value, params) {
|
|
477
|
+
if (isSqlExpression(value)) return this.renderExpression(value, params);
|
|
478
|
+
if (isExpression(value)) {
|
|
479
|
+
return this.renderExpr(value.node, params, (k) => this.columnId(k, void 0));
|
|
480
|
+
}
|
|
481
|
+
return params.bind(value);
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* The statements that open a transaction with the requested characteristics.
|
|
485
|
+
*
|
|
486
|
+
* Returned as a list because the dialects disagree on shape: PostgreSQL takes
|
|
487
|
+
* everything on the `BEGIN` itself, MySQL needs a separate `SET TRANSACTION`
|
|
488
|
+
* before it, and SQLite has no syntax at all.
|
|
489
|
+
*
|
|
490
|
+
* @param options The requested isolation level and read-only flag.
|
|
491
|
+
* @returns The statements to run, in order.
|
|
492
|
+
* @throws Error When the dialect cannot honor what was asked.
|
|
493
|
+
*/
|
|
494
|
+
beginStatements(options) {
|
|
495
|
+
const parts = ["BEGIN"];
|
|
496
|
+
if (options?.isolation)
|
|
497
|
+
parts.push(`ISOLATION LEVEL ${options.isolation.toUpperCase()}`);
|
|
498
|
+
if (options?.readOnly) parts.push("READ ONLY");
|
|
499
|
+
return [parts.join(" ")];
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Render a row-level locking clause (`FOR UPDATE ...`).
|
|
503
|
+
*
|
|
504
|
+
* Standard on PostgreSQL and MySQL 8.0+; SQLite overrides it to throw.
|
|
505
|
+
*
|
|
506
|
+
* @param lock The locking clause from the node.
|
|
507
|
+
* @returns The SQL text, leading space included.
|
|
508
|
+
*/
|
|
509
|
+
renderLock(lock) {
|
|
510
|
+
const strength = lock.strength === "update" ? "FOR UPDATE" : "FOR SHARE";
|
|
511
|
+
const of = lock.of.length > 0 ? ` OF ${lock.of.map((t) => this.quoteId(t)).join(", ")}` : "";
|
|
512
|
+
const wait = lock.wait === "skipLocked" ? " SKIP LOCKED" : lock.wait === "noWait" ? " NOWAIT" : "";
|
|
513
|
+
return ` ${strength}${of}${wait}`;
|
|
514
|
+
}
|
|
515
|
+
// ---- statements -------------------------------------------------------
|
|
516
|
+
/**
|
|
517
|
+
* Compile a SELECT.
|
|
518
|
+
*
|
|
519
|
+
* Two alias rules differ between clauses and are handled here: PostgreSQL does
|
|
520
|
+
* NOT accept a `SELECT` alias in `HAVING`, so an aggregate key is re-emitted as
|
|
521
|
+
* its expression (`COUNT(*) > $1`), a form every dialect accepts; `ORDER BY`,
|
|
522
|
+
* by contrast, accepts the output alias everywhere, so it is emitted as
|
|
523
|
+
* written.
|
|
524
|
+
*
|
|
525
|
+
* @param node The select AST.
|
|
526
|
+
* @param params The parameter collector.
|
|
527
|
+
* @returns The SQL text.
|
|
528
|
+
*/
|
|
529
|
+
compileSelect(node, params) {
|
|
530
|
+
const names = node.names;
|
|
531
|
+
let cols;
|
|
532
|
+
if (node.aggregates.length > 0) {
|
|
533
|
+
const groupSel = node.groupBy.map((c) => this.columnId(c, names));
|
|
534
|
+
const aggSel = node.aggregates.map((a) => {
|
|
535
|
+
const inner = this.aggregateOperand(a, params, names);
|
|
536
|
+
return `${a.fn.toUpperCase()}(${inner}) AS ${this.quoteId(a.alias)}`;
|
|
537
|
+
});
|
|
538
|
+
cols = [...groupSel, ...aggSel].join(", ");
|
|
539
|
+
} else {
|
|
540
|
+
cols = node.columns === "*" ? "*" : node.columns.map((c) => this.columnId(c, names)).join(", ");
|
|
541
|
+
}
|
|
542
|
+
const computed = Object.entries(node.computed ?? {}).map(
|
|
543
|
+
([alias, expr]) => `${this.renderExpr(expr, params, (k) => this.columnId(k, names))} AS ${this.quoteId(alias)}`
|
|
544
|
+
);
|
|
545
|
+
if (computed.length > 0) cols = [cols, ...computed].join(", ");
|
|
546
|
+
const from = node.alias ? `${this.quoteId(node.table)} AS ${this.quoteId(node.alias)}` : this.quoteId(node.table);
|
|
547
|
+
let sql = `${this.compileWith(node.with, params)}SELECT ${node.distinct ? "DISTINCT " : ""}${cols} FROM ${from}`;
|
|
548
|
+
const where = this.compileCondition(
|
|
549
|
+
node.where,
|
|
550
|
+
params,
|
|
551
|
+
(k) => this.columnId(k, names),
|
|
552
|
+
codecEncoder(node.codecs)
|
|
553
|
+
);
|
|
554
|
+
if (where) sql += ` WHERE ${where}`;
|
|
555
|
+
if (node.groupBy.length > 0) {
|
|
556
|
+
sql += ` GROUP BY ${node.groupBy.map((c) => this.columnId(c, names)).join(", ")}`;
|
|
557
|
+
}
|
|
558
|
+
const aggByAlias = new Map(node.aggregates.map((a) => [a.alias, a]));
|
|
559
|
+
if (node.having) {
|
|
560
|
+
const having = this.compileCondition(node.having, params, (key) => {
|
|
561
|
+
const agg = aggByAlias.get(key);
|
|
562
|
+
if (!agg) return this.columnId(key, names);
|
|
563
|
+
const inner = this.aggregateOperand(agg, params, names);
|
|
564
|
+
return `${agg.fn.toUpperCase()}(${inner})`;
|
|
565
|
+
});
|
|
566
|
+
if (having) sql += ` HAVING ${having}`;
|
|
567
|
+
}
|
|
568
|
+
if (node.orderBy.length > 0) {
|
|
569
|
+
const terms = node.orderBy.map((t) => {
|
|
570
|
+
const id = typeof t.column !== "string" ? this.renderExpr(t.column, params, (k) => this.columnId(k, names)) : aggByAlias.has(t.column) ? this.quoteId(t.column) : this.columnId(t.column, names);
|
|
571
|
+
return `${id} ${t.direction === "desc" ? "DESC" : "ASC"}`;
|
|
572
|
+
}).join(", ");
|
|
573
|
+
sql += ` ORDER BY ${terms}`;
|
|
574
|
+
}
|
|
575
|
+
if (node.limit !== void 0) sql += ` LIMIT ${params.bind(node.limit)}`;
|
|
576
|
+
if (node.offset !== void 0) sql += ` OFFSET ${params.bind(node.offset)}`;
|
|
577
|
+
if (node.lock) {
|
|
578
|
+
if (node.distinct || node.groupBy.length > 0 || node.aggregates.length > 0) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
"FOR UPDATE / FOR SHARE cannot be combined with DISTINCT or an aggregate query \u2014 lock the underlying rows in a separate SELECT."
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
sql += this.renderLock(node.lock);
|
|
584
|
+
}
|
|
585
|
+
return sql;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Compile an INSERT.
|
|
589
|
+
*
|
|
590
|
+
* Takes the cached fast path only when the statement text is a pure function of
|
|
591
|
+
* its structure. A SQL expression among the values, or a conflict predicate,
|
|
592
|
+
* makes the text depend on the values themselves — those compile uncached, in
|
|
593
|
+
* SQL order, so placeholder positions stay correct.
|
|
594
|
+
*/
|
|
595
|
+
compileInsert(node, params) {
|
|
596
|
+
if (node.fromSelect) {
|
|
597
|
+
const target = node.fromSelect.columns.map((c) => this.columnId(c, node.names)).join(", ");
|
|
598
|
+
const source = node.fromSelect.select;
|
|
599
|
+
const query = source.kind === "join_select" ? this.compileJoin(source, params) : source.kind === "set_op" ? this.compileSetOp(source, params) : this.compileSelect(source, params);
|
|
600
|
+
const returning = this.compileReturning(node.returning, node.names);
|
|
601
|
+
return `INSERT INTO ${this.quoteId(node.table)} (${target}) ${query}${returning}`;
|
|
602
|
+
}
|
|
603
|
+
const columns = insertColumns(node.values);
|
|
604
|
+
const conflict = node.onConflict;
|
|
605
|
+
const cacheable = conflict?.targetWhere === void 0 && conflict?.updateWhere === void 0 && !insertHasExpression(node);
|
|
606
|
+
if (!cacheable) return this.compileInsertDirect(node, columns, params);
|
|
607
|
+
for (const row of node.values) {
|
|
608
|
+
for (const c of columns) params.bind(row[c] ?? null);
|
|
609
|
+
}
|
|
610
|
+
const conflictCols = conflict && conflict.update !== "nothing" ? Object.keys(conflict.update) : [];
|
|
611
|
+
for (const c of conflictCols) {
|
|
612
|
+
params.bind((conflict?.update)[c]);
|
|
613
|
+
}
|
|
614
|
+
return this.insertTemplate(node, columns, conflictCols, params);
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Compile an INSERT without the template cache, rendering clauses in statement
|
|
618
|
+
* order so every parameter is bound at the position it appears.
|
|
619
|
+
*
|
|
620
|
+
* @param node The insert node.
|
|
621
|
+
* @param columns The column keys shared by every row.
|
|
622
|
+
* @param params The parameter collector.
|
|
623
|
+
* @returns The SQL text.
|
|
624
|
+
*/
|
|
625
|
+
compileInsertDirect(node, columns, params) {
|
|
626
|
+
const names = node.names;
|
|
627
|
+
const colSql = columns.map((c) => this.columnId(c, names)).join(", ");
|
|
628
|
+
const rowsSql = node.values.map((row) => {
|
|
629
|
+
const cells = columns.map(
|
|
630
|
+
(c) => this.renderValue(row[c] ?? null, params)
|
|
631
|
+
);
|
|
632
|
+
return `(${cells.join(", ")})`;
|
|
633
|
+
}).join(", ");
|
|
634
|
+
let sql = `INSERT INTO ${this.quoteId(node.table)} (${colSql}) VALUES ${rowsSql}`;
|
|
635
|
+
if (node.onConflict) {
|
|
636
|
+
const update = node.onConflict.update;
|
|
637
|
+
const conflictCols = update === "nothing" ? [] : Object.keys(update);
|
|
638
|
+
let cursor = 0;
|
|
639
|
+
sql += this.renderConflict(
|
|
640
|
+
node.onConflict,
|
|
641
|
+
conflictCols,
|
|
642
|
+
() => {
|
|
643
|
+
const key = conflictCols[cursor++];
|
|
644
|
+
return this.renderValue(update[key], params);
|
|
645
|
+
},
|
|
646
|
+
names,
|
|
647
|
+
params
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
sql += this.compileReturning(node.returning, names);
|
|
651
|
+
return sql;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* The INSERT SQL template for a given structure, cached across calls.
|
|
655
|
+
*
|
|
656
|
+
* The text depends only on (dialect, table, columns, row count, returning,
|
|
657
|
+
* conflict shape) — never on the bound values — and placeholder positions are
|
|
658
|
+
* deterministic from the counts (a fresh statement always starts binding at 1).
|
|
659
|
+
* So a per-row insert loop compiles the string once and reuses it every row.
|
|
660
|
+
*/
|
|
661
|
+
insertTemplate(node, columns, conflictCols, params) {
|
|
662
|
+
const returningKey = node.returning === null ? "" : node.returning === "*" ? "*" : node.returning.join(",");
|
|
663
|
+
const conflictKey = node.onConflict ? `${node.onConflict.target.join(",")}>${node.onConflict.update === "nothing" ? "nothing" : conflictCols.join(",")}` : "";
|
|
664
|
+
const key = `${this.name}|${node.table}|${columns.join(",")}|${node.values.length}|${returningKey}|${conflictKey}`;
|
|
665
|
+
const cached = _BaseDialect.insertTemplates.get(key);
|
|
666
|
+
if (cached !== void 0) return cached;
|
|
667
|
+
const names = node.names;
|
|
668
|
+
const colSql = columns.map((c) => this.columnId(c, names)).join(", ");
|
|
669
|
+
let position = 0;
|
|
670
|
+
const rowsSql = node.values.map(() => `(${columns.map(() => this.placeholder(++position)).join(", ")})`).join(", ");
|
|
671
|
+
let sql = `INSERT INTO ${this.quoteId(node.table)} (${colSql}) VALUES ${rowsSql}`;
|
|
672
|
+
if (node.onConflict) {
|
|
673
|
+
sql += this.renderConflict(
|
|
674
|
+
node.onConflict,
|
|
675
|
+
conflictCols,
|
|
676
|
+
() => this.placeholder(++position),
|
|
677
|
+
names,
|
|
678
|
+
params
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
sql += this.compileReturning(node.returning, names);
|
|
682
|
+
_BaseDialect.insertTemplates.set(key, sql);
|
|
683
|
+
return sql;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Render the conflict-handling clause. Standard SQL (SQLite/PostgreSQL) uses
|
|
687
|
+
* `ON CONFLICT (...) [WHERE predicate] DO NOTHING | DO UPDATE SET ... [WHERE ...]`;
|
|
688
|
+
* MySQL overrides this.
|
|
689
|
+
*
|
|
690
|
+
* The index predicate is rendered before the `DO UPDATE` assignments because
|
|
691
|
+
* that is where it sits in the statement, so its parameters bind first.
|
|
692
|
+
*
|
|
693
|
+
* @param onConflict The conflict clause from the node.
|
|
694
|
+
* @param conflictCols The columns to overwrite on `DO UPDATE` (empty for nothing).
|
|
695
|
+
* @param nextValue Yields the SQL for the next `DO UPDATE` assignment value.
|
|
696
|
+
* @param names The node's property → column map, if any.
|
|
697
|
+
* @param params The parameter collector, for the predicates.
|
|
698
|
+
* @returns The SQL text, leading space included.
|
|
699
|
+
*/
|
|
700
|
+
renderConflict(onConflict, conflictCols, nextValue, names, params) {
|
|
701
|
+
const idFor = (key) => this.columnId(key, names);
|
|
702
|
+
const target = onConflict.target.map(idFor).join(", ");
|
|
703
|
+
const indexWhere = this.compileCondition(onConflict.targetWhere, params, idFor);
|
|
704
|
+
const targetSql = indexWhere ? `(${target}) WHERE ${indexWhere}` : `(${target})`;
|
|
705
|
+
if (onConflict.update === "nothing") return ` ON CONFLICT ${targetSql} DO NOTHING`;
|
|
706
|
+
const assignments = conflictCols.map((c) => `${idFor(c)} = ${nextValue()}`).join(", ");
|
|
707
|
+
let sql = ` ON CONFLICT ${targetSql} DO UPDATE SET ${assignments}`;
|
|
708
|
+
const updateWhere = this.compileCondition(onConflict.updateWhere, params, idFor);
|
|
709
|
+
if (updateWhere) sql += ` WHERE ${updateWhere}`;
|
|
710
|
+
return sql;
|
|
711
|
+
}
|
|
712
|
+
compileUpdate(node, params) {
|
|
713
|
+
const names = node.names;
|
|
714
|
+
const sets = Object.entries(node.set).map(
|
|
715
|
+
([col, value]) => `${this.columnId(col, names)} = ${this.renderValue(value, params)}`
|
|
716
|
+
).join(", ");
|
|
717
|
+
let sql = `UPDATE ${this.quoteId(node.table)} SET ${sets}`;
|
|
718
|
+
sql += this.compileExtraSources("FROM", node.from);
|
|
719
|
+
const where = this.compileCondition(
|
|
720
|
+
node.where,
|
|
721
|
+
params,
|
|
722
|
+
(k) => this.columnId(k, names),
|
|
723
|
+
codecEncoder(node.codecs)
|
|
724
|
+
);
|
|
725
|
+
if (where) sql += ` WHERE ${where}`;
|
|
726
|
+
sql += this.compileReturning(node.returning, names);
|
|
727
|
+
return sql;
|
|
728
|
+
}
|
|
729
|
+
compileDelete(node, params) {
|
|
730
|
+
const names = node.names;
|
|
731
|
+
let sql = `DELETE FROM ${this.quoteId(node.table)}`;
|
|
732
|
+
sql += this.compileExtraSources("USING", node.using);
|
|
733
|
+
const where = this.compileCondition(
|
|
734
|
+
node.where,
|
|
735
|
+
params,
|
|
736
|
+
(k) => this.columnId(k, names),
|
|
737
|
+
codecEncoder(node.codecs)
|
|
738
|
+
);
|
|
739
|
+
if (where) sql += ` WHERE ${where}`;
|
|
740
|
+
sql += this.compileReturning(node.returning, names);
|
|
741
|
+
return sql;
|
|
742
|
+
}
|
|
743
|
+
compileJoin(node, params) {
|
|
744
|
+
const names = node.names;
|
|
745
|
+
const cols = node.selections.map((s) => {
|
|
746
|
+
const ref = `${s.alias}.${s.column}`;
|
|
747
|
+
const label = node.pick ? s.column : ref;
|
|
748
|
+
return `${this.qualify(ref, names)} AS ${this.quoteId(label)}`;
|
|
749
|
+
}).join(", ");
|
|
750
|
+
let sql = `${this.compileWith(node.with, params)}SELECT ${cols} FROM ${this.quoteId(node.base.table)} AS ${this.quoteId(node.base.alias)}`;
|
|
751
|
+
for (const j of node.joins) {
|
|
752
|
+
const kw = j.kind === "left" ? "LEFT JOIN" : "INNER JOIN";
|
|
753
|
+
const on = j.on.map(([l, r]) => `${this.qualify(l, names)} = ${this.qualify(r, names)}`).join(" AND ");
|
|
754
|
+
sql += ` ${kw} ${this.quoteId(j.table)} AS ${this.quoteId(j.alias)} ON ${on}`;
|
|
755
|
+
}
|
|
756
|
+
const where = this.compileCondition(
|
|
757
|
+
node.where,
|
|
758
|
+
params,
|
|
759
|
+
(k) => this.qualify(k, names)
|
|
760
|
+
);
|
|
761
|
+
if (where) sql += ` WHERE ${where}`;
|
|
762
|
+
if (node.orderBy.length > 0) {
|
|
763
|
+
const terms = node.orderBy.map(
|
|
764
|
+
(t) => `${this.qualify(t.ref, names)} ${t.direction === "desc" ? "DESC" : "ASC"}`
|
|
765
|
+
).join(", ");
|
|
766
|
+
sql += ` ORDER BY ${terms}`;
|
|
767
|
+
}
|
|
768
|
+
if (node.limit !== void 0) sql += ` LIMIT ${params.bind(node.limit)}`;
|
|
769
|
+
if (node.offset !== void 0) sql += ` OFFSET ${params.bind(node.offset)}`;
|
|
770
|
+
return sql;
|
|
771
|
+
}
|
|
772
|
+
// ---- clauses ----------------------------------------------------------
|
|
773
|
+
/**
|
|
774
|
+
* Render the extra sources of an `UPDATE ... FROM` / `DELETE ... USING`.
|
|
775
|
+
*
|
|
776
|
+
* The dialects that do not have the clause override this and throw: emitting it
|
|
777
|
+
* anyway would produce a statement the server rejects, and quietly dropping it
|
|
778
|
+
* would change which rows are written.
|
|
779
|
+
*
|
|
780
|
+
* @param keyword `FROM` or `USING`.
|
|
781
|
+
* @param sources The extra tables, if any.
|
|
782
|
+
* @returns The clause with a leading space, or an empty string.
|
|
783
|
+
*/
|
|
784
|
+
compileExtraSources(keyword, sources) {
|
|
785
|
+
if (!sources || sources.length === 0) return "";
|
|
786
|
+
const list = sources.map((s) => `${this.quoteId(s.table)} AS ${this.quoteId(s.alias)}`).join(", ");
|
|
787
|
+
return ` ${keyword} ${list}`;
|
|
788
|
+
}
|
|
789
|
+
compileReturning(returning, names) {
|
|
790
|
+
if (returning === null) return "";
|
|
791
|
+
if (returning === "*") return " RETURNING *";
|
|
792
|
+
return ` RETURNING ${returning.map((c) => this.columnId(c, names)).join(", ")}`;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Compile a condition tree (fields / and / or / not) to SQL. `idFor` renders a
|
|
796
|
+
* key to a quoted identifier — `quoteId` for single-table, `qualify` for joins —
|
|
797
|
+
* so select/update/delete/join all share this one compiler.
|
|
798
|
+
*/
|
|
799
|
+
compileCondition(node, params, idFor, encode = (_key, value) => value) {
|
|
800
|
+
if (!node) return "";
|
|
801
|
+
switch (node.kind) {
|
|
802
|
+
case "fields": {
|
|
803
|
+
const conditions = [];
|
|
804
|
+
for (const [key, value] of Object.entries(node.fields)) {
|
|
805
|
+
const id = idFor(key);
|
|
806
|
+
if (isExpression(value)) {
|
|
807
|
+
conditions.push(
|
|
808
|
+
this.compileExprOperator(
|
|
809
|
+
id,
|
|
810
|
+
"eq",
|
|
811
|
+
this.renderExpr(value.node, params, idFor)
|
|
812
|
+
)
|
|
813
|
+
);
|
|
814
|
+
} else if (isOperatorObject(value)) {
|
|
815
|
+
for (const [op, operand] of Object.entries(value)) {
|
|
816
|
+
conditions.push(
|
|
817
|
+
isExpression(operand) ? this.compileExprOperator(
|
|
818
|
+
id,
|
|
819
|
+
op,
|
|
820
|
+
this.renderExpr(operand.node, params, idFor)
|
|
821
|
+
) : this.compileOperator(
|
|
822
|
+
id,
|
|
823
|
+
op,
|
|
824
|
+
encodeOperand(op, operand, (v) => encode(key, v)),
|
|
825
|
+
params
|
|
826
|
+
)
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
} else {
|
|
830
|
+
const operand = encode(key, value);
|
|
831
|
+
conditions.push(
|
|
832
|
+
operand === null ? `${id} IS NULL` : `${id} = ${params.bind(operand)}`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return conditions.join(" AND ");
|
|
837
|
+
}
|
|
838
|
+
case "and":
|
|
839
|
+
case "or": {
|
|
840
|
+
const parts = node.parts.map((p) => this.compileCondition(p, params, idFor, encode)).filter((s) => s.length > 0);
|
|
841
|
+
if (parts.length === 0) return "";
|
|
842
|
+
const sep = node.kind === "and" ? " AND " : " OR ";
|
|
843
|
+
return parts.map((p) => `(${p})`).join(sep);
|
|
844
|
+
}
|
|
845
|
+
case "not": {
|
|
846
|
+
const inner = this.compileCondition(node.part, params, idFor, encode);
|
|
847
|
+
return inner ? `NOT (${inner})` : "";
|
|
848
|
+
}
|
|
849
|
+
case "exists": {
|
|
850
|
+
const select = node.select;
|
|
851
|
+
this.checkSubquery(select);
|
|
852
|
+
const keyword = node.negate ? "NOT EXISTS" : "EXISTS";
|
|
853
|
+
return `${keyword} (${this.compileSelect(select, params)})`;
|
|
854
|
+
}
|
|
855
|
+
case "fullText":
|
|
856
|
+
return this.compileFullText(node, params, idFor);
|
|
857
|
+
case "compare": {
|
|
858
|
+
const left = this.renderExpr(node.left, params, idFor);
|
|
859
|
+
if (node.right.kind === "value") {
|
|
860
|
+
return this.compileOperator(left, node.op, node.right.value, params);
|
|
861
|
+
}
|
|
862
|
+
return this.compileExprOperator(
|
|
863
|
+
left,
|
|
864
|
+
node.op,
|
|
865
|
+
this.renderExpr(node.right, params, idFor)
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Compile a full-text condition.
|
|
872
|
+
*
|
|
873
|
+
* PostgreSQL gets the real thing (`@@ websearch_to_tsquery`); the dialects with
|
|
874
|
+
* no text-search engine override this and compile the node's prebuilt substring
|
|
875
|
+
* fallback instead, so the query still returns the right rows.
|
|
876
|
+
*
|
|
877
|
+
* @param node The full-text condition node.
|
|
878
|
+
* @param params The parameter collector.
|
|
879
|
+
* @param idFor Column-name resolver.
|
|
880
|
+
* @returns The rendered condition.
|
|
881
|
+
*/
|
|
882
|
+
compileFullText(node, params, idFor) {
|
|
883
|
+
const config = params.bind(node.language);
|
|
884
|
+
return `${this.tsVector(node.columns, config, idFor)} @@ websearch_to_tsquery(${config}::regconfig, ${params.bind(node.term)})`;
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Render one side of a comparison.
|
|
888
|
+
*
|
|
889
|
+
* A column reference goes through `idFor`, so an explicit `.name()` mapping and
|
|
890
|
+
* join qualification apply here exactly as they do in the object form of
|
|
891
|
+
* `where` — `col()` is not a way around them. Only a `value` node binds.
|
|
892
|
+
*
|
|
893
|
+
* @param node The expression AST.
|
|
894
|
+
* @param params The parameter collector.
|
|
895
|
+
* @param idFor The identifier resolver for the enclosing statement.
|
|
896
|
+
* @returns The SQL text of the expression.
|
|
897
|
+
*/
|
|
898
|
+
/**
|
|
899
|
+
* Render what an aggregate is applied to: `*`, a column, or an expression.
|
|
900
|
+
*
|
|
901
|
+
* An expression operand is what makes a conditional aggregate
|
|
902
|
+
* (`SUM(CASE WHEN ... END)`) expressible — one pass over the table instead of a
|
|
903
|
+
* query per bucket.
|
|
904
|
+
*
|
|
905
|
+
* @param agg The aggregate term.
|
|
906
|
+
* @param params The parameter collector.
|
|
907
|
+
* @param names The node's column-name map.
|
|
908
|
+
* @returns The rendered operand.
|
|
909
|
+
*/
|
|
910
|
+
aggregateOperand(agg, params, names) {
|
|
911
|
+
if (agg.column === "*") return "*";
|
|
912
|
+
if (typeof agg.column === "string") return this.columnId(agg.column, names);
|
|
913
|
+
return this.renderExpr(agg.column, params, (k) => this.columnId(k, names));
|
|
914
|
+
}
|
|
915
|
+
renderExpr(node, params, idFor) {
|
|
916
|
+
switch (node.kind) {
|
|
917
|
+
case "column":
|
|
918
|
+
return idFor(node.name);
|
|
919
|
+
case "value":
|
|
920
|
+
return params.bind(node.value);
|
|
921
|
+
case "fn": {
|
|
922
|
+
const args = node.args.map((a) => this.renderExpr(a, params, idFor)).join(", ");
|
|
923
|
+
return `${node.name}(${args})`;
|
|
924
|
+
}
|
|
925
|
+
case "case": {
|
|
926
|
+
const branches = node.branches.map(
|
|
927
|
+
(b) => `WHEN ${this.compileCondition(b.when, params, idFor)} THEN ${this.renderExpr(b.result, params, idFor)}`
|
|
928
|
+
).join(" ");
|
|
929
|
+
const fallback = node.fallback === null ? "" : ` ELSE ${this.renderExpr(node.fallback, params, idFor)}`;
|
|
930
|
+
return `CASE ${branches}${fallback} END`;
|
|
931
|
+
}
|
|
932
|
+
case "cast":
|
|
933
|
+
return `CAST(${this.renderExpr(node.operand, params, idFor)} AS ${this.castTypeName(node.to)})`;
|
|
934
|
+
case "scalar": {
|
|
935
|
+
const select = node.select;
|
|
936
|
+
this.checkSubquery(select);
|
|
937
|
+
return `(${this.compileSelect(select, params)})`;
|
|
938
|
+
}
|
|
939
|
+
case "rank":
|
|
940
|
+
return this.renderRank(node.columns, node.term, node.language, params, idFor);
|
|
941
|
+
case "star":
|
|
942
|
+
return "*";
|
|
943
|
+
case "window": {
|
|
944
|
+
const call = this.renderExpr(node.fn, params, idFor);
|
|
945
|
+
const parts = [];
|
|
946
|
+
if (node.partitionBy.length > 0) {
|
|
947
|
+
parts.push(`PARTITION BY ${node.partitionBy.map(idFor).join(", ")}`);
|
|
948
|
+
}
|
|
949
|
+
if (node.orderBy.length > 0) {
|
|
950
|
+
const terms = node.orderBy.map((t) => `${idFor(t.column)} ${t.direction === "desc" ? "DESC" : "ASC"}`).join(", ");
|
|
951
|
+
parts.push(`ORDER BY ${terms}`);
|
|
952
|
+
}
|
|
953
|
+
if (node.frame) parts.push(node.frame);
|
|
954
|
+
return `${call} OVER (${parts.join(" ")})`;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Render a full-text relevance score.
|
|
960
|
+
*
|
|
961
|
+
* PostgreSQL has `ts_rank`; the others have nothing equivalent, and they
|
|
962
|
+
* override this to a constant so that ordering by it is a no-op rather than a
|
|
963
|
+
* compile error — the fallback keeps returning the right rows, only unranked.
|
|
964
|
+
*
|
|
965
|
+
* @param columns The columns making up the document.
|
|
966
|
+
* @param term The search term.
|
|
967
|
+
* @param language The text-search configuration.
|
|
968
|
+
* @param params The parameter collector.
|
|
969
|
+
* @param idFor Column-name resolver.
|
|
970
|
+
* @returns The rendered score expression.
|
|
971
|
+
*/
|
|
972
|
+
renderRank(columns, term, language, params, idFor) {
|
|
973
|
+
const config = params.bind(language);
|
|
974
|
+
return `ts_rank(${this.tsVector(columns, config, idFor)}, websearch_to_tsquery(${config}::regconfig, ${params.bind(term)}))`;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Build the `to_tsvector(...)` document out of the searched columns.
|
|
978
|
+
*
|
|
979
|
+
* `coalesce(col, '')` matters: in SQL a `NULL` anywhere in a concatenation makes
|
|
980
|
+
* the whole document `NULL`, so one empty column would silently exclude the row.
|
|
981
|
+
*
|
|
982
|
+
* @param columns The columns making up the document.
|
|
983
|
+
* @param config The already-bound placeholder for the text-search config.
|
|
984
|
+
* @param idFor Column-name resolver.
|
|
985
|
+
* @returns The rendered `to_tsvector(...)` call.
|
|
986
|
+
*/
|
|
987
|
+
tsVector(columns, config, idFor) {
|
|
988
|
+
const document = columns.map((c) => `coalesce(${idFor(c)}, '')`).join(" || ' ' || ");
|
|
989
|
+
return `to_tsvector(${config}::regconfig, ${document})`;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* The SQL type name this dialect accepts in a `CAST`.
|
|
993
|
+
*
|
|
994
|
+
* The base mapping is the standard one PostgreSQL takes; SQLite and MySQL
|
|
995
|
+
* override it, because the names genuinely differ (MySQL's `CAST(x AS SIGNED)`
|
|
996
|
+
* is not `INTEGER`, and SQLite only has five storage classes to aim at).
|
|
997
|
+
*
|
|
998
|
+
* @param to The portable cast target.
|
|
999
|
+
* @returns The dialect's own type name.
|
|
1000
|
+
*/
|
|
1001
|
+
castTypeName(to) {
|
|
1002
|
+
switch (to) {
|
|
1003
|
+
case "integer":
|
|
1004
|
+
return "INTEGER";
|
|
1005
|
+
case "bigint":
|
|
1006
|
+
return "BIGINT";
|
|
1007
|
+
case "real":
|
|
1008
|
+
return "DOUBLE PRECISION";
|
|
1009
|
+
case "numeric":
|
|
1010
|
+
return "NUMERIC";
|
|
1011
|
+
case "text":
|
|
1012
|
+
return "TEXT";
|
|
1013
|
+
case "boolean":
|
|
1014
|
+
return "BOOLEAN";
|
|
1015
|
+
case "date":
|
|
1016
|
+
return "DATE";
|
|
1017
|
+
case "datetime":
|
|
1018
|
+
case "timestamp":
|
|
1019
|
+
return "TIMESTAMP";
|
|
1020
|
+
case "uuid":
|
|
1021
|
+
return "UUID";
|
|
1022
|
+
case "json":
|
|
1023
|
+
return "JSON";
|
|
1024
|
+
case "jsonb":
|
|
1025
|
+
return "JSONB";
|
|
1026
|
+
case "blob":
|
|
1027
|
+
return "BYTEA";
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Compile a comparison whose right-hand side is another expression rather than
|
|
1032
|
+
* a bound value (`total > paid`, `lower(a) = lower(b)`).
|
|
1033
|
+
*
|
|
1034
|
+
* The list and null operators are excluded: `IN`, `BETWEEN` and `IS NULL` take
|
|
1035
|
+
* a value operand, and accepting an expression there would silently compile to
|
|
1036
|
+
* something else.
|
|
1037
|
+
*
|
|
1038
|
+
* @param left The rendered left-hand side.
|
|
1039
|
+
* @param op The operator name.
|
|
1040
|
+
* @param right The rendered right-hand side.
|
|
1041
|
+
* @returns The SQL text of the predicate.
|
|
1042
|
+
* @throws Error When the operator needs a value operand.
|
|
1043
|
+
*/
|
|
1044
|
+
compileExprOperator(left, op, right) {
|
|
1045
|
+
switch (op) {
|
|
1046
|
+
case "eq":
|
|
1047
|
+
return `${left} = ${right}`;
|
|
1048
|
+
case "ne":
|
|
1049
|
+
return `${left} <> ${right}`;
|
|
1050
|
+
case "gt":
|
|
1051
|
+
return `${left} > ${right}`;
|
|
1052
|
+
case "gte":
|
|
1053
|
+
return `${left} >= ${right}`;
|
|
1054
|
+
case "lt":
|
|
1055
|
+
return `${left} < ${right}`;
|
|
1056
|
+
case "lte":
|
|
1057
|
+
return `${left} <= ${right}`;
|
|
1058
|
+
case "like":
|
|
1059
|
+
return `${left} LIKE ${right}`;
|
|
1060
|
+
case "ilike":
|
|
1061
|
+
return this.ilike(left, right);
|
|
1062
|
+
case "ieq":
|
|
1063
|
+
return `lower(${left}) = lower(${right})`;
|
|
1064
|
+
case "iContains":
|
|
1065
|
+
throw new Error(
|
|
1066
|
+
'The "iContains" operator matches a literal, so it takes a value, not an expression.'
|
|
1067
|
+
);
|
|
1068
|
+
case "contains":
|
|
1069
|
+
case "containedBy":
|
|
1070
|
+
case "overlaps":
|
|
1071
|
+
return `${left} ${this.arrayOperator(op)} ${right}`;
|
|
1072
|
+
default:
|
|
1073
|
+
throw new Error(`The "${op}" operator takes a value operand, not an expression.`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
compileOperator(id, op, operand, params) {
|
|
1077
|
+
switch (op) {
|
|
1078
|
+
case "eq":
|
|
1079
|
+
return operand === null ? `${id} IS NULL` : `${id} = ${params.bind(operand)}`;
|
|
1080
|
+
case "ne":
|
|
1081
|
+
return operand === null ? `${id} IS NOT NULL` : `${id} <> ${params.bind(operand)}`;
|
|
1082
|
+
case "gt":
|
|
1083
|
+
return `${id} > ${params.bind(operand)}`;
|
|
1084
|
+
case "gte":
|
|
1085
|
+
return `${id} >= ${params.bind(operand)}`;
|
|
1086
|
+
case "lt":
|
|
1087
|
+
return `${id} < ${params.bind(operand)}`;
|
|
1088
|
+
case "lte":
|
|
1089
|
+
return `${id} <= ${params.bind(operand)}`;
|
|
1090
|
+
case "like":
|
|
1091
|
+
return `${id} LIKE ${params.bind(operand)}`;
|
|
1092
|
+
case "ilike":
|
|
1093
|
+
return this.ilike(id, params.bind(operand));
|
|
1094
|
+
case "ieq":
|
|
1095
|
+
return operand === null ? `${id} IS NULL` : `lower(${id}) = lower(${params.bind(operand)})`;
|
|
1096
|
+
case "iContains":
|
|
1097
|
+
return this.ilikeEscaped(id, params.bind(`%${escapeLike(String(operand))}%`));
|
|
1098
|
+
case "contains":
|
|
1099
|
+
return `${id} ${this.arrayOperator("contains")} ${params.bind(operand)}`;
|
|
1100
|
+
case "containedBy":
|
|
1101
|
+
return `${id} ${this.arrayOperator("containedBy")} ${params.bind(operand)}`;
|
|
1102
|
+
case "overlaps":
|
|
1103
|
+
return `${id} ${this.arrayOperator("overlaps")} ${params.bind(operand)}`;
|
|
1104
|
+
case "in":
|
|
1105
|
+
return this.compileIn(id, operand, params, false);
|
|
1106
|
+
case "notIn":
|
|
1107
|
+
return this.compileIn(id, operand, params, true);
|
|
1108
|
+
case "between": {
|
|
1109
|
+
const [lo, hi] = operand;
|
|
1110
|
+
return `${id} BETWEEN ${params.bind(lo)} AND ${params.bind(hi)}`;
|
|
1111
|
+
}
|
|
1112
|
+
case "isNull":
|
|
1113
|
+
return operand ? `${id} IS NULL` : `${id} IS NOT NULL`;
|
|
1114
|
+
default:
|
|
1115
|
+
throw new Error(`Unknown operator ${JSON.stringify(op)}`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Compile `IN` / `NOT IN`, whose operand is either a value list or a
|
|
1120
|
+
* single-column subquery.
|
|
1121
|
+
*
|
|
1122
|
+
* The subquery is rendered at the position it appears in the outer statement
|
|
1123
|
+
* and shares the same parameter collector, so its own placeholders land in the
|
|
1124
|
+
* right order — and it keeps its own `names` map, since the inner model may use
|
|
1125
|
+
* a different naming convention than the outer one.
|
|
1126
|
+
*
|
|
1127
|
+
* @param id The quoted column identifier being tested.
|
|
1128
|
+
* @param operand A list of values, or a {@link Subquery}.
|
|
1129
|
+
* @param params The parameter collector for the statement being compiled.
|
|
1130
|
+
* @param negate True for `NOT IN`.
|
|
1131
|
+
* @returns The SQL text of the predicate.
|
|
1132
|
+
*/
|
|
1133
|
+
compileIn(id, operand, params, negate) {
|
|
1134
|
+
const keyword = negate ? "NOT IN" : "IN";
|
|
1135
|
+
if (isSubquery(operand)) {
|
|
1136
|
+
this.checkSubquery(operand.node);
|
|
1137
|
+
return `${id} ${keyword} (${this.compileSelect(operand.node, params)})`;
|
|
1138
|
+
}
|
|
1139
|
+
const values = operand;
|
|
1140
|
+
if (values.length === 0) {
|
|
1141
|
+
return negate ? "1 = 1" : "1 = 0";
|
|
1142
|
+
}
|
|
1143
|
+
const list = values.map((v) => params.bind(v)).join(", ");
|
|
1144
|
+
return `${id} ${keyword} (${list})`;
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
var SqliteDialect = class extends BaseDialect {
|
|
1148
|
+
name = "sqlite";
|
|
1149
|
+
/**
|
|
1150
|
+
* SQLite explains with `EXPLAIN QUERY PLAN`, and has no `ANALYZE` — the plain
|
|
1151
|
+
* `EXPLAIN` there dumps bytecode, which answers a different question.
|
|
1152
|
+
*/
|
|
1153
|
+
explainPrefix(analyze) {
|
|
1154
|
+
if (analyze) {
|
|
1155
|
+
throw new Error(
|
|
1156
|
+
"SQLite has no EXPLAIN ANALYZE; use EXPLAIN QUERY PLAN (analyze: false)."
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
return "EXPLAIN QUERY PLAN";
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* SQLite has `UPDATE ... FROM` (3.33+) but no `DELETE ... USING`.
|
|
1163
|
+
*
|
|
1164
|
+
* The portable shape there is a subquery — `where({ id: { in: … } })` — so this
|
|
1165
|
+
* throws and says so, rather than emitting a clause SQLite does not parse.
|
|
1166
|
+
*/
|
|
1167
|
+
compileExtraSources(keyword, sources) {
|
|
1168
|
+
if (keyword === "USING" && sources && sources.length > 0) {
|
|
1169
|
+
throw new Error(
|
|
1170
|
+
'SQLite has no DELETE ... USING; filter with a subquery instead: where({ id: { in: select(Other, ["id"]).asSubquery("id") } }).'
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
return super.compileExtraSources(keyword, sources);
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* SQLite has no text-search engine, so the prebuilt substring fallback is
|
|
1177
|
+
* compiled instead. The rows are right; the ranking is what is missing.
|
|
1178
|
+
*/
|
|
1179
|
+
compileFullText(node, params, idFor) {
|
|
1180
|
+
return this.compileCondition(node.fallback, params, idFor);
|
|
1181
|
+
}
|
|
1182
|
+
/** No text-search engine means no score: a constant, so ordering by it is inert. */
|
|
1183
|
+
renderRank() {
|
|
1184
|
+
return "0";
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* SQLite has five storage classes, so most targets collapse onto `TEXT` or
|
|
1188
|
+
* `INTEGER`. Naming a type it does not know would not fail — SQLite applies the
|
|
1189
|
+
* closest affinity — but it would make the cast mean something different here
|
|
1190
|
+
* than on the other databases, which is what this mapping avoids.
|
|
1191
|
+
*/
|
|
1192
|
+
castTypeName(to) {
|
|
1193
|
+
switch (to) {
|
|
1194
|
+
case "integer":
|
|
1195
|
+
case "bigint":
|
|
1196
|
+
case "boolean":
|
|
1197
|
+
return "INTEGER";
|
|
1198
|
+
case "real":
|
|
1199
|
+
return "REAL";
|
|
1200
|
+
case "numeric":
|
|
1201
|
+
return "NUMERIC";
|
|
1202
|
+
case "blob":
|
|
1203
|
+
return "BLOB";
|
|
1204
|
+
default:
|
|
1205
|
+
return "TEXT";
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
placeholder() {
|
|
1209
|
+
return "?";
|
|
1210
|
+
}
|
|
1211
|
+
ilike(column, param) {
|
|
1212
|
+
return `${column} LIKE ${param}`;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* SQLite runs one writer at a time, so its only isolation level **is**
|
|
1216
|
+
* serializable — there is no syntax to ask for another, and no weaker level to
|
|
1217
|
+
* fall back to. Asking for one is an error rather than a silent no-op, since a
|
|
1218
|
+
* caller who wrote `repeatable read` was reasoning about a guarantee.
|
|
1219
|
+
*
|
|
1220
|
+
* `readOnly` likewise has no per-transaction form here (`PRAGMA query_only` is
|
|
1221
|
+
* per connection), so it is refused instead of quietly ignored.
|
|
1222
|
+
*/
|
|
1223
|
+
beginStatements(options) {
|
|
1224
|
+
if (options?.isolation && options.isolation !== "serializable") {
|
|
1225
|
+
throw new Error(
|
|
1226
|
+
`SQLite only implements the "serializable" isolation level; ${JSON.stringify(options.isolation)} has no equivalent here.`
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
if (options?.readOnly) {
|
|
1230
|
+
throw new Error(
|
|
1231
|
+
"SQLite has no read-only transaction; open the engine with { sqlite: { ... } } on a read-only connection instead."
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
return ["BEGIN"];
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* SQLite has no row-level locking, so a lock request is an error rather than a
|
|
1238
|
+
* silently unlocked `SELECT` — a lock that does not exist only shows up as
|
|
1239
|
+
* duplicated work under production concurrency.
|
|
1240
|
+
*/
|
|
1241
|
+
renderLock() {
|
|
1242
|
+
throw new Error(
|
|
1243
|
+
"SQLite has no row-level locking \u2014 FOR UPDATE / FOR SHARE is unsupported. Serialize the claim inside a transaction instead."
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
var PostgresDialect = class extends BaseDialect {
|
|
1248
|
+
name = "postgresql";
|
|
1249
|
+
placeholder(index) {
|
|
1250
|
+
return `$${index}`;
|
|
1251
|
+
}
|
|
1252
|
+
ilike(column, param) {
|
|
1253
|
+
return `${column} ILIKE ${param}`;
|
|
1254
|
+
}
|
|
1255
|
+
arrayOperator(op) {
|
|
1256
|
+
if (op === "contains") return "@>";
|
|
1257
|
+
if (op === "containedBy") return "<@";
|
|
1258
|
+
return "&&";
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
var mysqlQuotedIds = /* @__PURE__ */ new Map();
|
|
1262
|
+
var MysqlDialect = class extends BaseDialect {
|
|
1263
|
+
name = "mysql";
|
|
1264
|
+
placeholder() {
|
|
1265
|
+
return "?";
|
|
1266
|
+
}
|
|
1267
|
+
ilike(column, param) {
|
|
1268
|
+
return `${column} LIKE ${param}`;
|
|
1269
|
+
}
|
|
1270
|
+
/** MySQL's `EXPLAIN FORMAT=JSON` spells the option differently. */
|
|
1271
|
+
explainPrefix(analyze) {
|
|
1272
|
+
return analyze ? "EXPLAIN ANALYZE" : "EXPLAIN FORMAT=JSON";
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* MySQL only gained `INTERSECT`/`EXCEPT` in 8.0.31, and this project does not
|
|
1276
|
+
* invest in MySQL beyond what already works — so they are refused here rather
|
|
1277
|
+
* than emitted against a server that may reject them.
|
|
1278
|
+
*/
|
|
1279
|
+
setOperator(op) {
|
|
1280
|
+
if (op === "intersect" || op === "except") {
|
|
1281
|
+
throw new Error(
|
|
1282
|
+
`MySQL support for ${op.toUpperCase()} is out of scope for tempest-db-js; express it with a join or NOT EXISTS.`
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
return super.setOperator(op);
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* MySQL writes multi-table updates as `UPDATE a JOIN b`, and has no
|
|
1289
|
+
* `DELETE ... USING` in this shape. Both are out of the project's active scope,
|
|
1290
|
+
* so they are refused rather than emitted against a server that rejects them.
|
|
1291
|
+
*/
|
|
1292
|
+
compileExtraSources(keyword, sources) {
|
|
1293
|
+
if (sources && sources.length > 0) {
|
|
1294
|
+
throw new Error(
|
|
1295
|
+
`MySQL does not take ${keyword} on a write in this form; it spells multi-table writes as UPDATE a JOIN b, which is out of scope for tempest-db-js.`
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
return "";
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* MySQL's full-text search needs a `FULLTEXT` index and different syntax, and it
|
|
1302
|
+
* is outside this project's active scope — the substring fallback is compiled,
|
|
1303
|
+
* like on SQLite.
|
|
1304
|
+
*/
|
|
1305
|
+
compileFullText(node, params, idFor) {
|
|
1306
|
+
return this.compileCondition(node.fallback, params, idFor);
|
|
1307
|
+
}
|
|
1308
|
+
/** No `ts_rank` equivalent in scope: a constant, so ordering by it is inert. */
|
|
1309
|
+
renderRank() {
|
|
1310
|
+
return "0";
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* MySQL sets the level with a statement **before** the transaction opens, and
|
|
1314
|
+
* spells the read-only flag on `START TRANSACTION` rather than on `BEGIN`.
|
|
1315
|
+
*/
|
|
1316
|
+
beginStatements(options) {
|
|
1317
|
+
const statements = [];
|
|
1318
|
+
if (options?.isolation) {
|
|
1319
|
+
statements.push(
|
|
1320
|
+
`SET TRANSACTION ISOLATION LEVEL ${options.isolation.toUpperCase()}`
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
statements.push(options?.readOnly ? "START TRANSACTION READ ONLY" : "BEGIN");
|
|
1324
|
+
return statements;
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* MySQL's `CAST` takes its own vocabulary — `SIGNED`, not `INTEGER`; `CHAR`,
|
|
1328
|
+
* not `TEXT` — and rejects the standard names outright.
|
|
1329
|
+
*/
|
|
1330
|
+
castTypeName(to) {
|
|
1331
|
+
switch (to) {
|
|
1332
|
+
case "integer":
|
|
1333
|
+
case "bigint":
|
|
1334
|
+
case "boolean":
|
|
1335
|
+
return "SIGNED";
|
|
1336
|
+
case "real":
|
|
1337
|
+
case "numeric":
|
|
1338
|
+
return "DECIMAL";
|
|
1339
|
+
case "text":
|
|
1340
|
+
case "uuid":
|
|
1341
|
+
return "CHAR";
|
|
1342
|
+
case "date":
|
|
1343
|
+
return "DATE";
|
|
1344
|
+
case "datetime":
|
|
1345
|
+
case "timestamp":
|
|
1346
|
+
return "DATETIME";
|
|
1347
|
+
case "json":
|
|
1348
|
+
case "jsonb":
|
|
1349
|
+
return "JSON";
|
|
1350
|
+
case "blob":
|
|
1351
|
+
return "BINARY";
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
quoteId(name) {
|
|
1355
|
+
const cached = mysqlQuotedIds.get(name);
|
|
1356
|
+
if (cached !== void 0) return cached;
|
|
1357
|
+
const quoted = `\`${name.replace(/`/g, "``")}\``;
|
|
1358
|
+
mysqlQuotedIds.set(name, quoted);
|
|
1359
|
+
return quoted;
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* MySQL rejects `LIMIT` inside an `IN` subquery with
|
|
1363
|
+
* `ER_NOT_SUPPORTED_YET: This version of MySQL doesn't yet support
|
|
1364
|
+
* 'LIMIT & IN/ALL/ANY/SOME subquery'`. Failing at compile time names the fix
|
|
1365
|
+
* instead of surfacing that error from the driver at runtime.
|
|
1366
|
+
*/
|
|
1367
|
+
checkSubquery(node) {
|
|
1368
|
+
if (node.limit !== void 0 || node.offset !== void 0) {
|
|
1369
|
+
throw new Error(
|
|
1370
|
+
"MySQL does not support LIMIT/OFFSET inside an IN subquery. Select the ids first and pass them as a list, or wrap the subquery in a derived table."
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
renderConflict(onConflict, conflictCols, nextValue, names) {
|
|
1375
|
+
if (onConflict.targetWhere || onConflict.updateWhere) {
|
|
1376
|
+
throw new Error(
|
|
1377
|
+
"MySQL's ON DUPLICATE KEY UPDATE has no conflict-target predicate \u2014 a partial unique index is PostgreSQL/SQLite only."
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
if (onConflict.update === "nothing") {
|
|
1381
|
+
const col = this.columnId(onConflict.target[0] ?? "id", names);
|
|
1382
|
+
return ` ON DUPLICATE KEY UPDATE ${col} = ${col}`;
|
|
1383
|
+
}
|
|
1384
|
+
const assignments = conflictCols.map((c) => `${this.columnId(c, names)} = ${nextValue()}`).join(", ");
|
|
1385
|
+
return ` ON DUPLICATE KEY UPDATE ${assignments}`;
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* MySQL has no `RETURNING`, so it cannot be compiled into a statement.
|
|
1389
|
+
*
|
|
1390
|
+
* `session.execute()` still honors `.returning()` on a **single-row INSERT** by
|
|
1391
|
+
* running the insert and reading the row back by key on the same connection —
|
|
1392
|
+
* that is execution, not compilation, so it never reaches here. Compiling a
|
|
1393
|
+
* node with `returning` directly is an error, rather than SQL that silently
|
|
1394
|
+
* returns nothing.
|
|
1395
|
+
*/
|
|
1396
|
+
compileReturning(returning) {
|
|
1397
|
+
if (returning === null) return "";
|
|
1398
|
+
throw new Error(
|
|
1399
|
+
"RETURNING cannot be compiled for MySQL. session.execute() reads a single-row INSERT back by key (LAST_INSERT_ID()); UPDATE/DELETE have no equivalent \u2014 run a SELECT yourself."
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
function getDialect(name) {
|
|
1404
|
+
switch (name) {
|
|
1405
|
+
case "sqlite":
|
|
1406
|
+
return new SqliteDialect();
|
|
1407
|
+
case "postgresql":
|
|
1408
|
+
return new PostgresDialect();
|
|
1409
|
+
case "mysql":
|
|
1410
|
+
return new MysqlDialect();
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
21
1413
|
module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
22
1414
|
function toAsyncDriver(driver) {
|
|
23
1415
|
return {
|
|
@@ -31,6 +1423,10 @@ function toAsyncDriver(driver) {
|
|
|
31
1423
|
}
|
|
32
1424
|
|
|
33
1425
|
// src/index.ts
|
|
1426
|
+
var EXPRESSION = /* @__PURE__ */ Symbol.for("tempest-db-js.expression");
|
|
1427
|
+
function isSqlExpression(value) {
|
|
1428
|
+
return typeof value === "object" && value !== null && value[EXPRESSION] === true;
|
|
1429
|
+
}
|
|
34
1430
|
function isDefaultValue(value) {
|
|
35
1431
|
return typeof value === "object" && value !== null && "kind" in value && (value.kind === "literal" || value.kind === "expression");
|
|
36
1432
|
}
|
|
@@ -50,13 +1446,14 @@ function parseReference(ref, options) {
|
|
|
50
1446
|
};
|
|
51
1447
|
}
|
|
52
1448
|
var Column = class _Column {
|
|
53
|
-
constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null, dbName = null) {
|
|
1449
|
+
constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null, dbName = null, codec = null) {
|
|
54
1450
|
this.type = type;
|
|
55
1451
|
this.flags = flags;
|
|
56
1452
|
this.defaultValue = defaultValue;
|
|
57
1453
|
this.onUpdateValue = onUpdateValue;
|
|
58
1454
|
this.reference = reference;
|
|
59
1455
|
this.dbName = dbName;
|
|
1456
|
+
this.codec = codec;
|
|
60
1457
|
}
|
|
61
1458
|
type;
|
|
62
1459
|
flags;
|
|
@@ -64,6 +1461,7 @@ var Column = class _Column {
|
|
|
64
1461
|
onUpdateValue;
|
|
65
1462
|
reference;
|
|
66
1463
|
dbName;
|
|
1464
|
+
codec;
|
|
67
1465
|
/** Clone this column with one facet replaced, carrying every other over. */
|
|
68
1466
|
derive(patch) {
|
|
69
1467
|
return new _Column(
|
|
@@ -72,7 +1470,8 @@ var Column = class _Column {
|
|
|
72
1470
|
patch.defaultValue !== void 0 ? patch.defaultValue : this.defaultValue,
|
|
73
1471
|
patch.onUpdateValue !== void 0 ? patch.onUpdateValue : this.onUpdateValue,
|
|
74
1472
|
patch.reference !== void 0 ? patch.reference : this.reference,
|
|
75
|
-
patch.dbName !== void 0 ? patch.dbName : this.dbName
|
|
1473
|
+
patch.dbName !== void 0 ? patch.dbName : this.dbName,
|
|
1474
|
+
patch.codec !== void 0 ? patch.codec : this.codec
|
|
76
1475
|
);
|
|
77
1476
|
}
|
|
78
1477
|
primaryKey() {
|
|
@@ -214,6 +1613,41 @@ function columnNamesOf(model) {
|
|
|
214
1613
|
function constraintName(prefix, table, columns) {
|
|
215
1614
|
return `${prefix}_${table}_${columns.join("_")}`;
|
|
216
1615
|
}
|
|
1616
|
+
function renameConditionColumns(node, toColumn) {
|
|
1617
|
+
const expr = (current) => {
|
|
1618
|
+
switch (current.kind) {
|
|
1619
|
+
case "column":
|
|
1620
|
+
return { kind: "column", name: toColumn(current.name) };
|
|
1621
|
+
case "fn":
|
|
1622
|
+
return { ...current, args: current.args.map(expr) };
|
|
1623
|
+
case "cast":
|
|
1624
|
+
return { ...current, operand: expr(current.operand) };
|
|
1625
|
+
default:
|
|
1626
|
+
return current;
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
switch (node.kind) {
|
|
1630
|
+
case "fields": {
|
|
1631
|
+
const fields = {};
|
|
1632
|
+
for (const [key, value] of Object.entries(node.fields)) {
|
|
1633
|
+
fields[toColumn(key)] = value;
|
|
1634
|
+
}
|
|
1635
|
+
return { kind: "fields", fields };
|
|
1636
|
+
}
|
|
1637
|
+
case "and":
|
|
1638
|
+
case "or":
|
|
1639
|
+
return {
|
|
1640
|
+
...node,
|
|
1641
|
+
parts: node.parts.map((part) => renameConditionColumns(part, toColumn))
|
|
1642
|
+
};
|
|
1643
|
+
case "not":
|
|
1644
|
+
return { kind: "not", part: renameConditionColumns(node.part, toColumn) };
|
|
1645
|
+
case "compare":
|
|
1646
|
+
return { ...node, left: expr(node.left), right: expr(node.right) };
|
|
1647
|
+
default:
|
|
1648
|
+
return node;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
217
1651
|
function reflectTable(model) {
|
|
218
1652
|
const names = columnNamesOf(model);
|
|
219
1653
|
const toColumn = (prop) => names?.[prop] ?? prop;
|
|
@@ -235,6 +1669,8 @@ function reflectTable(model) {
|
|
|
235
1669
|
}
|
|
236
1670
|
const uniqueConstraints = [];
|
|
237
1671
|
const foreignKeys = [];
|
|
1672
|
+
const checks = [];
|
|
1673
|
+
const indexes = [];
|
|
238
1674
|
for (const c of model.tableArgs?.() ?? []) {
|
|
239
1675
|
const cols = c.columns.map(toColumn);
|
|
240
1676
|
if (c.kind === "unique") {
|
|
@@ -242,6 +1678,18 @@ function reflectTable(model) {
|
|
|
242
1678
|
name: c.name ?? constraintName("uq", model.tablename, cols),
|
|
243
1679
|
columns: cols
|
|
244
1680
|
});
|
|
1681
|
+
} else if (c.kind === "check") {
|
|
1682
|
+
checks.push({
|
|
1683
|
+
name: c.name ?? constraintName("ck", model.tablename, cols),
|
|
1684
|
+
expression: renameConditionColumns(c.expression, toColumn)
|
|
1685
|
+
});
|
|
1686
|
+
} else if (c.kind === "index") {
|
|
1687
|
+
indexes.push({
|
|
1688
|
+
name: c.name ?? constraintName("ix", model.tablename, cols),
|
|
1689
|
+
columns: cols,
|
|
1690
|
+
unique: c.unique === true,
|
|
1691
|
+
where: c.where ? renameConditionColumns(c.where, toColumn) : null
|
|
1692
|
+
});
|
|
245
1693
|
} else {
|
|
246
1694
|
foreignKeys.push({
|
|
247
1695
|
name: c.name ?? constraintName("fk", model.tablename, cols),
|
|
@@ -253,7 +1701,15 @@ function reflectTable(model) {
|
|
|
253
1701
|
});
|
|
254
1702
|
}
|
|
255
1703
|
}
|
|
256
|
-
return {
|
|
1704
|
+
return {
|
|
1705
|
+
name: model.tablename,
|
|
1706
|
+
columns,
|
|
1707
|
+
primaryKey,
|
|
1708
|
+
uniqueConstraints,
|
|
1709
|
+
foreignKeys,
|
|
1710
|
+
checks,
|
|
1711
|
+
indexes
|
|
1712
|
+
};
|
|
257
1713
|
}
|
|
258
1714
|
function reflectSchema(models) {
|
|
259
1715
|
const tables = {};
|
|
@@ -300,6 +1756,10 @@ function invert(op) {
|
|
|
300
1756
|
return { kind: "recreate_table", from: op.to, to: op.from };
|
|
301
1757
|
case "add_constraint":
|
|
302
1758
|
return { kind: "drop_constraint", table: op.table, constraint: op.constraint };
|
|
1759
|
+
case "create_index":
|
|
1760
|
+
return { kind: "drop_index", table: op.table, index: op.index };
|
|
1761
|
+
case "drop_index":
|
|
1762
|
+
return { kind: "create_index", table: op.table, index: op.index };
|
|
303
1763
|
case "drop_constraint":
|
|
304
1764
|
return { kind: "add_constraint", table: op.table, constraint: op.constraint };
|
|
305
1765
|
case "execute":
|
|
@@ -443,7 +1903,8 @@ function renderDefault(def, dialect, type) {
|
|
|
443
1903
|
"sql.expr`...` binds parameters and cannot be rendered as a DEFAULT \u2014 use sql.raw()."
|
|
444
1904
|
);
|
|
445
1905
|
}
|
|
446
|
-
|
|
1906
|
+
const rendered = renderPortableToken(expr, dialect);
|
|
1907
|
+
return dialect === "sqlite" && rendered.includes("(") ? `(${rendered})` : rendered;
|
|
447
1908
|
}
|
|
448
1909
|
const value = def.value;
|
|
449
1910
|
if (value === null) return "NULL";
|
|
@@ -491,9 +1952,31 @@ function renderForeignKeyConstraint(fk, dialect) {
|
|
|
491
1952
|
function tableConstraintClauses(table, dialect) {
|
|
492
1953
|
return [
|
|
493
1954
|
...table.uniqueConstraints.map((uc) => renderUniqueConstraint(uc, dialect)),
|
|
494
|
-
...table.foreignKeys.map((fk) => renderForeignKeyConstraint(fk, dialect))
|
|
1955
|
+
...table.foreignKeys.map((fk) => renderForeignKeyConstraint(fk, dialect)),
|
|
1956
|
+
...table.checks.map((ck) => renderCheckConstraint(ck, dialect))
|
|
495
1957
|
];
|
|
496
1958
|
}
|
|
1959
|
+
function renderPredicate(node, dialect) {
|
|
1960
|
+
const literals = new LiteralParams();
|
|
1961
|
+
return getDialect(dialect).renderConditionLiteral(node, literals);
|
|
1962
|
+
}
|
|
1963
|
+
function renderCheckConstraint(ck, dialect) {
|
|
1964
|
+
return `CONSTRAINT ${quoteId(ck.name, dialect)} CHECK (${renderPredicate(ck.expression, dialect)})`;
|
|
1965
|
+
}
|
|
1966
|
+
function renderCreateIndex(table, ix, dialect) {
|
|
1967
|
+
if (ix.where && dialect === "mysql") {
|
|
1968
|
+
throw new Error(
|
|
1969
|
+
`MySQL has no partial index; ${ix.name} on ${table} declares a WHERE predicate.`
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
const unique = ix.unique ? "UNIQUE " : "";
|
|
1973
|
+
const cols = ix.columns.map((c) => quoteId(c, dialect)).join(", ");
|
|
1974
|
+
const where = ix.where ? ` WHERE ${renderPredicate(ix.where, dialect)}` : "";
|
|
1975
|
+
return `CREATE ${unique}INDEX ${quoteId(ix.name, dialect)} ON ${quoteId(table, dialect)} (${cols})${where}`;
|
|
1976
|
+
}
|
|
1977
|
+
function renderDropIndex(table, ix, dialect) {
|
|
1978
|
+
return dialect === "mysql" ? `DROP INDEX ${quoteId(ix.name, dialect)} ON ${quoteId(table, dialect)}` : `DROP INDEX ${quoteId(ix.name, dialect)}`;
|
|
1979
|
+
}
|
|
497
1980
|
function renderColumnDef(col, dialect) {
|
|
498
1981
|
let sql = `${quoteId(col.name, dialect)} ${renderColumnType(col.type, dialect)}`;
|
|
499
1982
|
if (col.notNull) sql += " NOT NULL";
|
|
@@ -546,7 +2029,10 @@ function renderCreateTable(table, dialect) {
|
|
|
546
2029
|
...typeStmts,
|
|
547
2030
|
`CREATE TABLE ${quoteId(table.name, dialect)} (
|
|
548
2031
|
${cols.join(",\n ")}
|
|
549
|
-
)
|
|
2032
|
+
)`,
|
|
2033
|
+
// Indexes are separate statements, not table clauses — which is also why a
|
|
2034
|
+
// SQLite table rebuild has to recreate them.
|
|
2035
|
+
...table.indexes.map((ix) => renderCreateIndex(table.name, ix, dialect))
|
|
550
2036
|
];
|
|
551
2037
|
}
|
|
552
2038
|
function renderOperation(op, dialect) {
|
|
@@ -579,6 +2065,10 @@ function renderOperation(op, dialect) {
|
|
|
579
2065
|
return renderAddConstraint(op.table, op.constraint, dialect);
|
|
580
2066
|
case "drop_constraint":
|
|
581
2067
|
return renderDropConstraint(op.table, op.constraint, dialect);
|
|
2068
|
+
case "create_index":
|
|
2069
|
+
return [renderCreateIndex(op.table, op.index, dialect)];
|
|
2070
|
+
case "drop_index":
|
|
2071
|
+
return [renderDropIndex(op.table, op.index, dialect)];
|
|
582
2072
|
case "execute":
|
|
583
2073
|
return [op.up];
|
|
584
2074
|
}
|
|
@@ -589,7 +2079,7 @@ function renderAddConstraint(table, constraint, dialect) {
|
|
|
589
2079
|
`add_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
|
|
590
2080
|
);
|
|
591
2081
|
}
|
|
592
|
-
const clause = constraint.type === "unique" ? renderUniqueConstraint(constraint.constraint, dialect) : renderForeignKeyConstraint(constraint.constraint, dialect);
|
|
2082
|
+
const clause = constraint.type === "unique" ? renderUniqueConstraint(constraint.constraint, dialect) : constraint.type === "check" ? renderCheckConstraint(constraint.constraint, dialect) : renderForeignKeyConstraint(constraint.constraint, dialect);
|
|
593
2083
|
return [`ALTER TABLE ${quoteId(table, dialect)} ADD ${clause}`];
|
|
594
2084
|
}
|
|
595
2085
|
function renderDropConstraint(table, constraint, dialect) {
|
|
@@ -626,6 +2116,10 @@ function renderSqliteRebuild(from, to) {
|
|
|
626
2116
|
common.length > 0 ? `INSERT INTO ${quoteId(tmp, "sqlite")} (${commonSql}) SELECT ${commonSql} FROM ${quoteId(from.name, "sqlite")}` : `-- no common columns to copy from ${from.name}`,
|
|
627
2117
|
`DROP TABLE ${quoteId(from.name, "sqlite")}`,
|
|
628
2118
|
`ALTER TABLE ${quoteId(tmp, "sqlite")} RENAME TO ${quoteId(to.name, "sqlite")}`,
|
|
2119
|
+
// The rebuild dropped the old table, and its indexes with it: SQLite ties an
|
|
2120
|
+
// index to the table it was created on. Recreating them is part of the
|
|
2121
|
+
// rebuild, not a follow-up somebody has to remember.
|
|
2122
|
+
...to.indexes.map((ix) => renderCreateIndex(to.name, ix, "sqlite")),
|
|
629
2123
|
"PRAGMA foreign_keys=on"
|
|
630
2124
|
];
|
|
631
2125
|
}
|
|
@@ -728,8 +2222,49 @@ function diffConstraints(current, target) {
|
|
|
728
2222
|
ops.push(addForeignKey(table, tgt));
|
|
729
2223
|
}
|
|
730
2224
|
}
|
|
2225
|
+
const currentCk = new Map(current.checks.map((c) => [c.name, c]));
|
|
2226
|
+
const targetCk = new Map(target.checks.map((c) => [c.name, c]));
|
|
2227
|
+
for (const [name, cur] of currentCk) {
|
|
2228
|
+
const tgt = targetCk.get(name);
|
|
2229
|
+
if (!tgt || checkSignature(cur) !== checkSignature(tgt)) {
|
|
2230
|
+
ops.push({ kind: "drop_constraint", table, constraint: checkNamed(cur) });
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
for (const [name, tgt] of targetCk) {
|
|
2234
|
+
const cur = currentCk.get(name);
|
|
2235
|
+
if (!cur || checkSignature(cur) !== checkSignature(tgt)) {
|
|
2236
|
+
ops.push({ kind: "add_constraint", table, constraint: checkNamed(tgt) });
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
const currentIx = new Map(current.indexes.map((i) => [i.name, i]));
|
|
2240
|
+
const targetIx = new Map(target.indexes.map((i) => [i.name, i]));
|
|
2241
|
+
for (const [name, cur] of currentIx) {
|
|
2242
|
+
const tgt = targetIx.get(name);
|
|
2243
|
+
if (!tgt || indexSignature(cur) !== indexSignature(tgt)) {
|
|
2244
|
+
ops.push({ kind: "drop_index", table, index: cur });
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
for (const [name, tgt] of targetIx) {
|
|
2248
|
+
const cur = currentIx.get(name);
|
|
2249
|
+
if (!cur || indexSignature(cur) !== indexSignature(tgt)) {
|
|
2250
|
+
ops.push({ kind: "create_index", table, index: tgt });
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
731
2253
|
return ops;
|
|
732
2254
|
}
|
|
2255
|
+
function checkSignature(ck) {
|
|
2256
|
+
return JSON.stringify(ck.expression);
|
|
2257
|
+
}
|
|
2258
|
+
function indexSignature(ix) {
|
|
2259
|
+
return JSON.stringify({
|
|
2260
|
+
columns: ix.columns,
|
|
2261
|
+
unique: ix.unique,
|
|
2262
|
+
where: ix.where
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
function checkNamed(ck) {
|
|
2266
|
+
return { type: "check", constraint: ck };
|
|
2267
|
+
}
|
|
733
2268
|
function uniqueNamed(uc) {
|
|
734
2269
|
return { type: "unique", constraint: uc };
|
|
735
2270
|
}
|
|
@@ -1183,6 +2718,8 @@ function introspectSqlite(driver) {
|
|
|
1183
2718
|
columns,
|
|
1184
2719
|
primaryKey,
|
|
1185
2720
|
uniqueConstraints: sqliteUniques(driver, tableName),
|
|
2721
|
+
checks: [],
|
|
2722
|
+
indexes: sqliteIndexes(driver, tableName),
|
|
1186
2723
|
foreignKeys: sqliteForeignKeys(driver, tableName)
|
|
1187
2724
|
};
|
|
1188
2725
|
}
|
|
@@ -1271,6 +2808,7 @@ function compareSqliteSchemas(actual, expected) {
|
|
|
1271
2808
|
);
|
|
1272
2809
|
}
|
|
1273
2810
|
}
|
|
2811
|
+
issues.push(...indexDrift(tableName, actualTable, expectedTable));
|
|
1274
2812
|
}
|
|
1275
2813
|
for (const tableName of Object.keys(actual.tables)) {
|
|
1276
2814
|
if (!expected.tables[tableName]) {
|
|
@@ -1321,9 +2859,58 @@ function sqliteUniqueFromPragma(table, rows) {
|
|
|
1321
2859
|
if (cols.length === 0) return null;
|
|
1322
2860
|
return { name: `uq_${table}_${cols.join("_")}`, columns: cols };
|
|
1323
2861
|
}
|
|
2862
|
+
function indexesFromPragma(rows, columnsOfIndex) {
|
|
2863
|
+
const indexes = [];
|
|
2864
|
+
for (const idx of rows) {
|
|
2865
|
+
if (idx.origin !== "c") continue;
|
|
2866
|
+
if (idx.partial === 1) continue;
|
|
2867
|
+
const cols = [...columnsOfIndex(idx.name)].sort((a, b) => a.seqno - b.seqno).map((c) => c.name);
|
|
2868
|
+
if (cols.length === 0) continue;
|
|
2869
|
+
indexes.push({
|
|
2870
|
+
name: idx.name,
|
|
2871
|
+
columns: cols,
|
|
2872
|
+
unique: Number(idx.unique) === 1,
|
|
2873
|
+
where: null
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
return indexes;
|
|
2877
|
+
}
|
|
2878
|
+
function sqliteIndexes(driver, table) {
|
|
2879
|
+
const rows = driver.execute(`PRAGMA index_list(${JSON.stringify(table)})`, []).rows;
|
|
2880
|
+
return indexesFromPragma(
|
|
2881
|
+
rows,
|
|
2882
|
+
(name) => driver.execute(`PRAGMA index_info(${JSON.stringify(name)})`, []).rows
|
|
2883
|
+
);
|
|
2884
|
+
}
|
|
1324
2885
|
function isUniqueIndex(idx) {
|
|
1325
2886
|
return Number(idx.unique) === 1 && idx.origin !== "pk";
|
|
1326
2887
|
}
|
|
2888
|
+
function indexDrift(tableName, actual, expected) {
|
|
2889
|
+
const issues = [];
|
|
2890
|
+
const signature = (ix) => `${ix.columns.join(",")}${ix.unique ? " unique" : ""}`;
|
|
2891
|
+
const actualByName = new Map(actual.indexes.map((ix) => [ix.name, ix]));
|
|
2892
|
+
const expectedByName = new Map(expected.indexes.map((ix) => [ix.name, ix]));
|
|
2893
|
+
for (const [name, exp] of expectedByName) {
|
|
2894
|
+
const act = actualByName.get(name);
|
|
2895
|
+
if (!act) {
|
|
2896
|
+
issues.push(`index "${tableName}.${name}" is missing from the database`);
|
|
2897
|
+
continue;
|
|
2898
|
+
}
|
|
2899
|
+
if (signature(act) !== signature(exp)) {
|
|
2900
|
+
issues.push(
|
|
2901
|
+
`index "${tableName}.${name}" differs: model (${signature(exp)}), db (${signature(act)})`
|
|
2902
|
+
);
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
for (const name of actualByName.keys()) {
|
|
2906
|
+
if (!expectedByName.has(name)) {
|
|
2907
|
+
issues.push(
|
|
2908
|
+
`index "${tableName}.${name}" exists in the database but not in the model`
|
|
2909
|
+
);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
return issues;
|
|
2913
|
+
}
|
|
1327
2914
|
var SQLITE_TABLES_SQL = "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'";
|
|
1328
2915
|
async function introspectSqliteAsync(driver) {
|
|
1329
2916
|
const tablesRows = (await driver.execute(SQLITE_TABLES_SQL, [])).rows;
|
|
@@ -1341,12 +2928,27 @@ async function introspectSqliteAsync(driver) {
|
|
|
1341
2928
|
const unique = sqliteUniqueFromPragma(tableName, cols);
|
|
1342
2929
|
if (unique) uniqueConstraints.push(unique);
|
|
1343
2930
|
}
|
|
2931
|
+
const explicitIndexes = [];
|
|
2932
|
+
for (const idx of indexes) {
|
|
2933
|
+
if (idx.origin !== "c" || idx.partial === 1) continue;
|
|
2934
|
+
const cols = (await driver.execute(`PRAGMA index_info(${JSON.stringify(idx.name)})`, [])).rows;
|
|
2935
|
+
const sorted = [...cols].sort((a, b) => a.seqno - b.seqno).map((c) => c.name);
|
|
2936
|
+
if (sorted.length === 0) continue;
|
|
2937
|
+
explicitIndexes.push({
|
|
2938
|
+
name: idx.name,
|
|
2939
|
+
columns: sorted,
|
|
2940
|
+
unique: Number(idx.unique) === 1,
|
|
2941
|
+
where: null
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
1344
2944
|
tables[tableName] = {
|
|
1345
2945
|
name: tableName,
|
|
1346
2946
|
columns,
|
|
1347
2947
|
primaryKey,
|
|
1348
2948
|
uniqueConstraints,
|
|
1349
|
-
foreignKeys: sqliteForeignKeysFromPragma(tableName, fkRows)
|
|
2949
|
+
foreignKeys: sqliteForeignKeysFromPragma(tableName, fkRows),
|
|
2950
|
+
checks: [],
|
|
2951
|
+
indexes: explicitIndexes
|
|
1350
2952
|
};
|
|
1351
2953
|
}
|
|
1352
2954
|
return { tables };
|
|
@@ -1472,6 +3074,8 @@ async function introspectPostgres(driver) {
|
|
|
1472
3074
|
columns,
|
|
1473
3075
|
primaryKey,
|
|
1474
3076
|
uniqueConstraints: await postgresUniques(driver, tableName),
|
|
3077
|
+
checks: [],
|
|
3078
|
+
indexes: await postgresIndexes(driver, tableName),
|
|
1475
3079
|
foreignKeys: await postgresForeignKeys(driver, tableName)
|
|
1476
3080
|
};
|
|
1477
3081
|
}
|
|
@@ -1514,6 +3118,29 @@ async function postgresUniques(driver, table) {
|
|
|
1514
3118
|
columns: r.cols ?? []
|
|
1515
3119
|
}));
|
|
1516
3120
|
}
|
|
3121
|
+
async function postgresIndexes(driver, table) {
|
|
3122
|
+
const { rows } = await driver.execute(
|
|
3123
|
+
`SELECT i.relname AS name,
|
|
3124
|
+
ix.indisunique AS is_unique,
|
|
3125
|
+
ix.indpred IS NOT NULL AS is_partial,
|
|
3126
|
+
array_to_string(array_agg(a.attname ORDER BY k.ord), ',') AS columns
|
|
3127
|
+
FROM pg_index ix
|
|
3128
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
3129
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
3130
|
+
JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, ord) ON true
|
|
3131
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
3132
|
+
WHERE t.relname = $1
|
|
3133
|
+
AND NOT EXISTS (SELECT 1 FROM pg_constraint c WHERE c.conindid = ix.indexrelid)
|
|
3134
|
+
GROUP BY i.relname, ix.indisunique, ix.indpred`,
|
|
3135
|
+
[table]
|
|
3136
|
+
);
|
|
3137
|
+
return rows.filter((row) => !row.is_partial).map((row) => ({
|
|
3138
|
+
name: row.name,
|
|
3139
|
+
columns: row.columns.split(","),
|
|
3140
|
+
unique: row.is_unique === true,
|
|
3141
|
+
where: null
|
|
3142
|
+
}));
|
|
3143
|
+
}
|
|
1517
3144
|
function describeKind(type) {
|
|
1518
3145
|
if (type.kind !== "array") return type.kind;
|
|
1519
3146
|
return `${type.meta.element ? describeKind(type.meta.element) : "unknown"}[]`;
|
|
@@ -1564,6 +3191,7 @@ async function checkDriftPostgres(driver, models) {
|
|
|
1564
3191
|
);
|
|
1565
3192
|
}
|
|
1566
3193
|
}
|
|
3194
|
+
issues.push(...indexDrift(tableName, actualTable, expectedTable));
|
|
1567
3195
|
}
|
|
1568
3196
|
for (const tableName of Object.keys(actual.tables)) {
|
|
1569
3197
|
if (!expected.tables[tableName]) {
|
|
@@ -1641,7 +3269,22 @@ function applyOperation(schema, op) {
|
|
|
1641
3269
|
tables[op.table] = op.constraint.type === "unique" ? {
|
|
1642
3270
|
...t,
|
|
1643
3271
|
uniqueConstraints: [...t.uniqueConstraints, op.constraint.constraint]
|
|
1644
|
-
} : { ...t, foreignKeys: [...t.foreignKeys, op.constraint.constraint] };
|
|
3272
|
+
} : op.constraint.type === "check" ? { ...t, checks: [...t.checks, op.constraint.constraint] } : { ...t, foreignKeys: [...t.foreignKeys, op.constraint.constraint] };
|
|
3273
|
+
}
|
|
3274
|
+
break;
|
|
3275
|
+
}
|
|
3276
|
+
case "create_index": {
|
|
3277
|
+
const t = tables[op.table];
|
|
3278
|
+
if (t) tables[op.table] = { ...t, indexes: [...t.indexes, op.index] };
|
|
3279
|
+
break;
|
|
3280
|
+
}
|
|
3281
|
+
case "drop_index": {
|
|
3282
|
+
const t = tables[op.table];
|
|
3283
|
+
if (t) {
|
|
3284
|
+
tables[op.table] = {
|
|
3285
|
+
...t,
|
|
3286
|
+
indexes: t.indexes.filter((i) => i.name !== op.index.name)
|
|
3287
|
+
};
|
|
1645
3288
|
}
|
|
1646
3289
|
break;
|
|
1647
3290
|
}
|
|
@@ -1652,7 +3295,7 @@ function applyOperation(schema, op) {
|
|
|
1652
3295
|
tables[op.table] = op.constraint.type === "unique" ? {
|
|
1653
3296
|
...t,
|
|
1654
3297
|
uniqueConstraints: t.uniqueConstraints.filter((u) => u.name !== dropName)
|
|
1655
|
-
} : {
|
|
3298
|
+
} : op.constraint.type === "check" ? { ...t, checks: t.checks.filter((c) => c.name !== dropName) } : {
|
|
1656
3299
|
...t,
|
|
1657
3300
|
foreignKeys: t.foreignKeys.filter((f) => f.name !== dropName)
|
|
1658
3301
|
};
|