tempest-db-js 0.8.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 +8 -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-NH6K5LTX.js → chunk-HXK6WIBP.js} +2996 -271
- package/dist/chunk-HXK6WIBP.js.map +1 -0
- package/dist/{chunk-MF6O56RO.js → chunk-ZBIHRVUS.js} +255 -11
- package/dist/chunk-ZBIHRVUS.js.map +1 -0
- package/dist/index.cjs +3051 -272
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3017 -556
- package/dist/index.d.ts +3017 -556
- 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-MF6O56RO.js.map +0 -1
- package/dist/chunk-NH6K5LTX.js.map +0 -1
package/dist/bin.cjs
CHANGED
|
@@ -5,14 +5,127 @@ var fs = require('fs');
|
|
|
5
5
|
var path = require('path');
|
|
6
6
|
var promises = require('readline/promises');
|
|
7
7
|
var url = require('url');
|
|
8
|
+
var child_process = require('child_process');
|
|
9
|
+
var promises$1 = require('fs/promises');
|
|
10
|
+
var util = require('util');
|
|
8
11
|
var module$1 = require('module');
|
|
9
12
|
|
|
10
13
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
14
|
+
// src/conditions.ts
|
|
15
|
+
var CONDITION = /* @__PURE__ */ Symbol.for("tempest-db-js.condition");
|
|
16
|
+
function wrap(node) {
|
|
17
|
+
return { [CONDITION]: true, node };
|
|
18
|
+
}
|
|
19
|
+
function toExprNode(operand) {
|
|
20
|
+
return isExpression(operand) ? operand.node : { kind: "value", value: operand };
|
|
21
|
+
}
|
|
22
|
+
function assertValueOperands(op, operands) {
|
|
23
|
+
if (operands.some(isExpression)) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`The "${op}" operator binds its operands, so it takes values, not expressions.`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function isExpression(value) {
|
|
30
|
+
return value instanceof Expression;
|
|
31
|
+
}
|
|
32
|
+
var Expression = class {
|
|
33
|
+
constructor(node) {
|
|
34
|
+
this.node = node;
|
|
35
|
+
}
|
|
36
|
+
node;
|
|
37
|
+
/** Compare this expression against another expression or a bound value. */
|
|
38
|
+
compare(op, operand) {
|
|
39
|
+
return wrap({ kind: "compare", left: this.node, op, right: toExprNode(operand) });
|
|
40
|
+
}
|
|
41
|
+
/** `=` (or `IS NULL` for a null value). */
|
|
42
|
+
eq(operand) {
|
|
43
|
+
return this.compare("eq", operand);
|
|
44
|
+
}
|
|
45
|
+
/** `<>` (or `IS NOT NULL` for a null value). */
|
|
46
|
+
ne(operand) {
|
|
47
|
+
return this.compare("ne", operand);
|
|
48
|
+
}
|
|
49
|
+
/** `>`. */
|
|
50
|
+
gt(operand) {
|
|
51
|
+
return this.compare("gt", operand);
|
|
52
|
+
}
|
|
53
|
+
/** `>=`. */
|
|
54
|
+
gte(operand) {
|
|
55
|
+
return this.compare("gte", operand);
|
|
56
|
+
}
|
|
57
|
+
/** `<`. */
|
|
58
|
+
lt(operand) {
|
|
59
|
+
return this.compare("lt", operand);
|
|
60
|
+
}
|
|
61
|
+
/** `<=`. */
|
|
62
|
+
lte(operand) {
|
|
63
|
+
return this.compare("lte", operand);
|
|
64
|
+
}
|
|
65
|
+
/** `LIKE` — `%` and `_` in the operand are wildcards. */
|
|
66
|
+
like(pattern) {
|
|
67
|
+
return this.compare("like", pattern);
|
|
68
|
+
}
|
|
69
|
+
/** `ILIKE` — case-insensitive **pattern** matching, wildcards included. */
|
|
70
|
+
ilike(pattern) {
|
|
71
|
+
return this.compare("ilike", pattern);
|
|
72
|
+
}
|
|
73
|
+
/** Case-insensitive equality (`lower(a) = lower(b)`), with no wildcards. */
|
|
74
|
+
ieq(operand) {
|
|
75
|
+
return this.compare("ieq", operand);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `IN (...)` over a list of values.
|
|
79
|
+
*
|
|
80
|
+
* @param values The values to test against.
|
|
81
|
+
* @returns The condition.
|
|
82
|
+
* @throws Error When an entry is an {@link Expression} — a list operand is
|
|
83
|
+
* bound, so an expression there would be serialized as a parameter instead of
|
|
84
|
+
* rendered as SQL.
|
|
85
|
+
*/
|
|
86
|
+
in(values) {
|
|
87
|
+
assertValueOperands("in", values);
|
|
88
|
+
return this.compare("in", values);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* `NOT IN (...)` over a list of values.
|
|
92
|
+
*
|
|
93
|
+
* @param values The values to exclude.
|
|
94
|
+
* @returns The condition.
|
|
95
|
+
* @throws Error When an entry is an {@link Expression} (see {@link Expression.in}).
|
|
96
|
+
*/
|
|
97
|
+
notIn(values) {
|
|
98
|
+
assertValueOperands("notIn", values);
|
|
99
|
+
return this.compare("notIn", values);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* `BETWEEN lo AND hi` (inclusive).
|
|
103
|
+
*
|
|
104
|
+
* @param lo The lower bound.
|
|
105
|
+
* @param hi The upper bound.
|
|
106
|
+
* @returns The condition.
|
|
107
|
+
* @throws Error When a bound is an {@link Expression} (see {@link Expression.in}).
|
|
108
|
+
*/
|
|
109
|
+
between(lo, hi) {
|
|
110
|
+
assertValueOperands("between", [lo, hi]);
|
|
111
|
+
return this.compare("between", [lo, hi]);
|
|
112
|
+
}
|
|
113
|
+
/** `IS NULL` (true) / `IS NOT NULL` (false). */
|
|
114
|
+
isNull(value = true) {
|
|
115
|
+
return this.compare("isNull", value);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
11
119
|
// src/expressions.ts
|
|
120
|
+
function renderExcluded(column, bare, dialect) {
|
|
121
|
+
return dialect === "mysql" ? `VALUES(${bare})` : `excluded.${column}`;
|
|
122
|
+
}
|
|
12
123
|
function renderPortableToken(token, dialect) {
|
|
13
124
|
switch (token) {
|
|
14
125
|
case "now":
|
|
15
|
-
|
|
126
|
+
if (dialect === "postgresql") return "now()";
|
|
127
|
+
if (dialect === "sqlite") return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
128
|
+
return "CURRENT_TIMESTAMP";
|
|
16
129
|
case "current_date":
|
|
17
130
|
return "CURRENT_DATE";
|
|
18
131
|
case "current_time":
|
|
@@ -24,7 +137,134 @@ function renderPortableToken(token, dialect) {
|
|
|
24
137
|
}
|
|
25
138
|
}
|
|
26
139
|
|
|
140
|
+
// src/query.ts
|
|
141
|
+
function isSubquery(value) {
|
|
142
|
+
return typeof value === "object" && value !== null && value.node?.kind === "select";
|
|
143
|
+
}
|
|
144
|
+
var OPERATORS = [
|
|
145
|
+
"eq",
|
|
146
|
+
"ne",
|
|
147
|
+
"gt",
|
|
148
|
+
"gte",
|
|
149
|
+
"lt",
|
|
150
|
+
"lte",
|
|
151
|
+
"like",
|
|
152
|
+
"ilike",
|
|
153
|
+
"ieq",
|
|
154
|
+
"iContains",
|
|
155
|
+
"in",
|
|
156
|
+
"notIn",
|
|
157
|
+
"between",
|
|
158
|
+
"isNull",
|
|
159
|
+
"contains",
|
|
160
|
+
"containedBy",
|
|
161
|
+
"overlaps"
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
// src/url.ts
|
|
165
|
+
var InvalidDatabaseUrl = class extends Error {
|
|
166
|
+
constructor(url, reason) {
|
|
167
|
+
super(`Invalid database URL ${JSON.stringify(url)}: ${reason}`);
|
|
168
|
+
this.name = "InvalidDatabaseUrl";
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
var DIALECT_ALIASES = {
|
|
172
|
+
sqlite: "sqlite",
|
|
173
|
+
sqlite3: "sqlite",
|
|
174
|
+
postgresql: "postgresql",
|
|
175
|
+
postgres: "postgresql",
|
|
176
|
+
pg: "postgresql",
|
|
177
|
+
mysql: "mysql",
|
|
178
|
+
mariadb: "mysql"
|
|
179
|
+
};
|
|
180
|
+
function splitScheme(scheme) {
|
|
181
|
+
const plus = scheme.indexOf("+");
|
|
182
|
+
if (plus === -1) return { base: scheme.toLowerCase(), driver: null };
|
|
183
|
+
return {
|
|
184
|
+
base: scheme.slice(0, plus).toLowerCase(),
|
|
185
|
+
driver: scheme.slice(plus + 1) || null
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function decode(value) {
|
|
189
|
+
try {
|
|
190
|
+
return decodeURIComponent(value);
|
|
191
|
+
} catch {
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function parseSqlite(raw, driver, rest) {
|
|
196
|
+
let database;
|
|
197
|
+
if (rest.endsWith(":memory:")) {
|
|
198
|
+
database = ":memory:";
|
|
199
|
+
} else if (rest.startsWith("///")) {
|
|
200
|
+
database = rest.slice(3) || ":memory:";
|
|
201
|
+
} else if (rest.startsWith("//")) {
|
|
202
|
+
database = rest.slice(2) || ":memory:";
|
|
203
|
+
} else {
|
|
204
|
+
database = rest || ":memory:";
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
dialect: "sqlite",
|
|
208
|
+
driver,
|
|
209
|
+
host: null,
|
|
210
|
+
port: null,
|
|
211
|
+
user: null,
|
|
212
|
+
password: null,
|
|
213
|
+
database: decode(database),
|
|
214
|
+
options: {},
|
|
215
|
+
raw
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function parseNetworkUrl(raw, driver, rest, dialect) {
|
|
219
|
+
let parsed;
|
|
220
|
+
try {
|
|
221
|
+
parsed = new URL(`${dialect}:${rest}`);
|
|
222
|
+
} catch {
|
|
223
|
+
throw new InvalidDatabaseUrl(raw, "could not parse host/credentials");
|
|
224
|
+
}
|
|
225
|
+
const database = decode(parsed.pathname.replace(/^\//, "")) || null;
|
|
226
|
+
const options = {};
|
|
227
|
+
for (const [key, value] of parsed.searchParams) options[key] = value;
|
|
228
|
+
return {
|
|
229
|
+
dialect,
|
|
230
|
+
driver,
|
|
231
|
+
host: parsed.hostname || null,
|
|
232
|
+
port: parsed.port ? Number(parsed.port) : null,
|
|
233
|
+
user: parsed.username ? decode(parsed.username) : null,
|
|
234
|
+
password: parsed.password ? decode(parsed.password) : null,
|
|
235
|
+
database,
|
|
236
|
+
options,
|
|
237
|
+
raw
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function parseDatabaseUrl(url) {
|
|
241
|
+
const schemeEnd = url.indexOf(":");
|
|
242
|
+
if (schemeEnd === -1) {
|
|
243
|
+
throw new InvalidDatabaseUrl(
|
|
244
|
+
url,
|
|
245
|
+
"missing scheme (expected e.g. sqlite:// or postgresql://)"
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
const { base, driver } = splitScheme(url.slice(0, schemeEnd));
|
|
249
|
+
const dialect = DIALECT_ALIASES[base];
|
|
250
|
+
if (!dialect) {
|
|
251
|
+
throw new InvalidDatabaseUrl(url, `unknown dialect ${JSON.stringify(base)}`);
|
|
252
|
+
}
|
|
253
|
+
const rest = url.slice(schemeEnd + 1);
|
|
254
|
+
if (dialect === "sqlite") return parseSqlite(url, driver, rest);
|
|
255
|
+
return parseNetworkUrl(url, driver, rest, dialect);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/search.ts
|
|
259
|
+
function escapeLike(value) {
|
|
260
|
+
return value.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
27
263
|
// src/index.ts
|
|
264
|
+
var EXPRESSION = /* @__PURE__ */ Symbol.for("tempest-db-js.expression");
|
|
265
|
+
function isSqlExpression(value) {
|
|
266
|
+
return typeof value === "object" && value !== null && value[EXPRESSION] === true;
|
|
267
|
+
}
|
|
28
268
|
function isDefaultValue(value) {
|
|
29
269
|
return typeof value === "object" && value !== null && "kind" in value && (value.kind === "literal" || value.kind === "expression");
|
|
30
270
|
}
|
|
@@ -44,13 +284,14 @@ function parseReference(ref, options) {
|
|
|
44
284
|
};
|
|
45
285
|
}
|
|
46
286
|
var Column = class _Column {
|
|
47
|
-
constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null, dbName = null) {
|
|
287
|
+
constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null, dbName = null, codec = null) {
|
|
48
288
|
this.type = type;
|
|
49
289
|
this.flags = flags;
|
|
50
290
|
this.defaultValue = defaultValue;
|
|
51
291
|
this.onUpdateValue = onUpdateValue;
|
|
52
292
|
this.reference = reference;
|
|
53
293
|
this.dbName = dbName;
|
|
294
|
+
this.codec = codec;
|
|
54
295
|
}
|
|
55
296
|
type;
|
|
56
297
|
flags;
|
|
@@ -58,6 +299,7 @@ var Column = class _Column {
|
|
|
58
299
|
onUpdateValue;
|
|
59
300
|
reference;
|
|
60
301
|
dbName;
|
|
302
|
+
codec;
|
|
61
303
|
/** Clone this column with one facet replaced, carrying every other over. */
|
|
62
304
|
derive(patch) {
|
|
63
305
|
return new _Column(
|
|
@@ -66,7 +308,8 @@ var Column = class _Column {
|
|
|
66
308
|
patch.defaultValue !== void 0 ? patch.defaultValue : this.defaultValue,
|
|
67
309
|
patch.onUpdateValue !== void 0 ? patch.onUpdateValue : this.onUpdateValue,
|
|
68
310
|
patch.reference !== void 0 ? patch.reference : this.reference,
|
|
69
|
-
patch.dbName !== void 0 ? patch.dbName : this.dbName
|
|
311
|
+
patch.dbName !== void 0 ? patch.dbName : this.dbName,
|
|
312
|
+
patch.codec !== void 0 ? patch.codec : this.codec
|
|
70
313
|
);
|
|
71
314
|
}
|
|
72
315
|
primaryKey() {
|
|
@@ -204,8 +447,1328 @@ function columnNamesOf(model) {
|
|
|
204
447
|
return result;
|
|
205
448
|
}
|
|
206
449
|
|
|
450
|
+
// src/dialect.ts
|
|
451
|
+
var OPERATOR_SET = new Set(OPERATORS);
|
|
452
|
+
var MULTI_VALUE_OPERATORS = /* @__PURE__ */ new Set(["in", "notIn", "between"]);
|
|
453
|
+
function encodeOperand(op, operand, encode) {
|
|
454
|
+
if (op === "isNull") return operand;
|
|
455
|
+
if (MULTI_VALUE_OPERATORS.has(op)) {
|
|
456
|
+
return Array.isArray(operand) ? operand.map(encode) : operand;
|
|
457
|
+
}
|
|
458
|
+
return encode(operand);
|
|
459
|
+
}
|
|
460
|
+
function codecEncoder(codecs) {
|
|
461
|
+
if (!codecs) return (_key, value) => value;
|
|
462
|
+
return (key, value) => {
|
|
463
|
+
const codec = codecs[key];
|
|
464
|
+
return codec ? codec.toDb(value) : value;
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function isOperatorObject(value) {
|
|
468
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date || value instanceof Uint8Array) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
const keys = Object.keys(value);
|
|
472
|
+
return keys.length > 0 && keys.every((k) => OPERATOR_SET.has(k));
|
|
473
|
+
}
|
|
474
|
+
var Params = class {
|
|
475
|
+
constructor(placeholder2) {
|
|
476
|
+
this.placeholder = placeholder2;
|
|
477
|
+
}
|
|
478
|
+
placeholder;
|
|
479
|
+
values = [];
|
|
480
|
+
bind(value) {
|
|
481
|
+
this.values.push(value);
|
|
482
|
+
return this.placeholder(this.values.length);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
var LiteralParams = class extends Params {
|
|
486
|
+
constructor() {
|
|
487
|
+
super(() => "");
|
|
488
|
+
}
|
|
489
|
+
bind(value) {
|
|
490
|
+
if (value === null || value === void 0) return "NULL";
|
|
491
|
+
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
|
|
492
|
+
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
493
|
+
if (value instanceof Date) return `'${value.toISOString()}'`;
|
|
494
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
function insertColumns(rows) {
|
|
498
|
+
const columns = [];
|
|
499
|
+
const seen = /* @__PURE__ */ new Set();
|
|
500
|
+
for (const row of rows) {
|
|
501
|
+
for (const key of Object.keys(row)) {
|
|
502
|
+
if (seen.has(key)) continue;
|
|
503
|
+
seen.add(key);
|
|
504
|
+
columns.push(key);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return columns;
|
|
508
|
+
}
|
|
509
|
+
function insertHasExpression(node) {
|
|
510
|
+
for (const row of node.values) {
|
|
511
|
+
for (const value of Object.values(row)) {
|
|
512
|
+
if (isSqlExpression(value)) return true;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const update = node.onConflict?.update;
|
|
516
|
+
if (update && update !== "nothing") {
|
|
517
|
+
for (const value of Object.values(update)) {
|
|
518
|
+
if (isSqlExpression(value)) return true;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
var BaseDialect = class _BaseDialect {
|
|
524
|
+
/**
|
|
525
|
+
* INSERT SQL templates keyed by structure (dialect|table|columns|rowCount|
|
|
526
|
+
* returning). Shared across dialect instances — the key namespaces by dialect
|
|
527
|
+
* name, and the placeholder text is dialect-specific but structure-determined.
|
|
528
|
+
*/
|
|
529
|
+
static insertTemplates = /* @__PURE__ */ new Map();
|
|
530
|
+
/** Quoted-identifier cache (see {@link quoteId}). Shared across dialects. */
|
|
531
|
+
static quotedIds = /* @__PURE__ */ new Map();
|
|
532
|
+
/**
|
|
533
|
+
* Render a case-insensitive LIKE whose pattern carries escaped wildcards.
|
|
534
|
+
*
|
|
535
|
+
* The `ESCAPE` clause is not decoration: PostgreSQL treats `\` as the escape
|
|
536
|
+
* character by default, **SQLite has none at all** until one is declared, so
|
|
537
|
+
* without this the escaping done on our side would be meaningless there.
|
|
538
|
+
*
|
|
539
|
+
* @param column The rendered column.
|
|
540
|
+
* @param param The bound pattern.
|
|
541
|
+
* @returns The rendered comparison.
|
|
542
|
+
*/
|
|
543
|
+
ilikeEscaped(column, param) {
|
|
544
|
+
return `${this.ilike(column, param)} ESCAPE '\\'`;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Validate a subquery operand before it is rendered, for dialects that restrict
|
|
548
|
+
* what an `IN (SELECT ...)` may contain. The default accepts everything.
|
|
549
|
+
*
|
|
550
|
+
* @param _node The subquery's AST.
|
|
551
|
+
* @throws Error When the dialect cannot execute this subquery.
|
|
552
|
+
*/
|
|
553
|
+
checkSubquery(_node) {
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* The SQL operator for an array containment/overlap test.
|
|
557
|
+
*
|
|
558
|
+
* Only PostgreSQL has native arrays; the other dialects throw rather than
|
|
559
|
+
* emitting an operator that means something else there.
|
|
560
|
+
*
|
|
561
|
+
* @param op The array operator name.
|
|
562
|
+
* @returns The SQL operator text.
|
|
563
|
+
* @throws Error On a dialect without native array support.
|
|
564
|
+
*/
|
|
565
|
+
arrayOperator(op) {
|
|
566
|
+
throw new Error(
|
|
567
|
+
`The "${op}" operator needs native array support, which ${this.name} does not have.`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Quote an identifier (column/table) for the active dialect.
|
|
572
|
+
*
|
|
573
|
+
* Memoized: identifiers form a small, stable set (column/table names), but this
|
|
574
|
+
* runs for every identifier on every compile. Caching the quoted form removes a
|
|
575
|
+
* regex-replace + string allocation from the hot path. The standard double-quote
|
|
576
|
+
* form is identical across both dialects, so one shared cache is correct.
|
|
577
|
+
*/
|
|
578
|
+
quoteId(name) {
|
|
579
|
+
const cached = _BaseDialect.quotedIds.get(name);
|
|
580
|
+
if (cached !== void 0) return cached;
|
|
581
|
+
const quoted = `"${name.replace(/"/g, '""')}"`;
|
|
582
|
+
_BaseDialect.quotedIds.set(name, quoted);
|
|
583
|
+
return quoted;
|
|
584
|
+
}
|
|
585
|
+
/** Compile any node to `{ sql, params }`. */
|
|
586
|
+
compile(node) {
|
|
587
|
+
const params = new Params((i) => this.placeholder(i));
|
|
588
|
+
let sql;
|
|
589
|
+
switch (node.kind) {
|
|
590
|
+
case "select":
|
|
591
|
+
sql = this.compileSelect(node, params);
|
|
592
|
+
break;
|
|
593
|
+
case "insert":
|
|
594
|
+
sql = this.compileInsert(node, params);
|
|
595
|
+
break;
|
|
596
|
+
case "update":
|
|
597
|
+
sql = this.compileUpdate(node, params);
|
|
598
|
+
break;
|
|
599
|
+
case "delete":
|
|
600
|
+
sql = this.compileDelete(node, params);
|
|
601
|
+
break;
|
|
602
|
+
case "join_select":
|
|
603
|
+
sql = this.compileJoin(node, params);
|
|
604
|
+
break;
|
|
605
|
+
case "set_op":
|
|
606
|
+
sql = this.compileSetOp(node, params);
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
return { sql, params: params.values };
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Render a condition as schema SQL, with values inlined.
|
|
613
|
+
*
|
|
614
|
+
* Same compiler as a `WHERE`, different parameter strategy — so a `CHECK` and
|
|
615
|
+
* the query language cannot drift apart in what they mean.
|
|
616
|
+
*
|
|
617
|
+
* @param node The condition.
|
|
618
|
+
* @param params A {@link LiteralParams}.
|
|
619
|
+
* @returns The rendered predicate.
|
|
620
|
+
*/
|
|
621
|
+
renderConditionLiteral(node, params) {
|
|
622
|
+
return this.compileCondition(node, params, (key) => this.columnId(key, void 0));
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Render the `WITH` clause of a statement.
|
|
626
|
+
*
|
|
627
|
+
* `RECURSIVE` is a property of the **clause**, not of an entry: one recursive
|
|
628
|
+
* entry makes the whole `WITH` recursive, which is what the SQL standard says
|
|
629
|
+
* and what PostgreSQL and SQLite both implement.
|
|
630
|
+
*
|
|
631
|
+
* @param entries The `WITH` entries, if any.
|
|
632
|
+
* @param params The parameter collector.
|
|
633
|
+
* @returns The clause with a trailing space, or an empty string.
|
|
634
|
+
*/
|
|
635
|
+
compileWith(entries, params) {
|
|
636
|
+
if (!entries || entries.length === 0) return "";
|
|
637
|
+
const recursive = entries.some((entry) => entry.recursive) ? "RECURSIVE " : "";
|
|
638
|
+
const rendered = entries.map((entry) => {
|
|
639
|
+
const body = entry.body.kind === "set_op" ? this.compileSetOp(entry.body, params) : this.compileSelect(entry.body, params);
|
|
640
|
+
const hint = entry.materialized === null ? "" : entry.materialized ? " MATERIALIZED" : " NOT MATERIALIZED";
|
|
641
|
+
return `${this.quoteId(entry.name)} AS${hint} (${body})`;
|
|
642
|
+
});
|
|
643
|
+
return `WITH ${recursive}${rendered.join(", ")} `;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* The `EXPLAIN` prefix for this dialect.
|
|
647
|
+
*
|
|
648
|
+
* @param analyze Whether to measure by actually running the statement.
|
|
649
|
+
* @returns The prefix to put in front of the statement.
|
|
650
|
+
* @throws Error When the dialect cannot do what was asked.
|
|
651
|
+
*/
|
|
652
|
+
explainPrefix(analyze) {
|
|
653
|
+
return analyze ? "EXPLAIN (FORMAT JSON, ANALYZE)" : "EXPLAIN (FORMAT JSON)";
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* The SQL keyword for a set operation.
|
|
657
|
+
*
|
|
658
|
+
* @param op The operator.
|
|
659
|
+
* @returns The keyword.
|
|
660
|
+
* @throws Error On a dialect that does not implement it.
|
|
661
|
+
*/
|
|
662
|
+
setOperator(op) {
|
|
663
|
+
switch (op) {
|
|
664
|
+
case "union":
|
|
665
|
+
return "UNION";
|
|
666
|
+
case "unionAll":
|
|
667
|
+
return "UNION ALL";
|
|
668
|
+
case "intersect":
|
|
669
|
+
return "INTERSECT";
|
|
670
|
+
case "except":
|
|
671
|
+
return "EXCEPT";
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Compile a set operation.
|
|
676
|
+
*
|
|
677
|
+
* A branch carrying its own `ORDER BY`/`LIMIT` is parenthesized: without the
|
|
678
|
+
* parentheses those clauses bind to the **combined** result, which is a
|
|
679
|
+
* different query and a classic source of silently wrong output.
|
|
680
|
+
*
|
|
681
|
+
* @param node The set-operation node.
|
|
682
|
+
* @param params The parameter collector.
|
|
683
|
+
* @returns The rendered statement.
|
|
684
|
+
*/
|
|
685
|
+
compileSetOp(node, params) {
|
|
686
|
+
const keyword = ` ${this.setOperator(node.op)} `;
|
|
687
|
+
const branches = node.branches.map((branch) => {
|
|
688
|
+
const sql2 = branch.kind === "join_select" ? this.compileJoin(branch, params) : this.compileSelect(branch, params);
|
|
689
|
+
const scoped = branch.orderBy.length > 0 || branch.limit !== void 0;
|
|
690
|
+
return scoped ? `(${sql2})` : sql2;
|
|
691
|
+
});
|
|
692
|
+
let sql = branches.join(keyword);
|
|
693
|
+
if (node.orderBy.length > 0) {
|
|
694
|
+
const terms = node.orderBy.map((t) => {
|
|
695
|
+
const id = typeof t.column === "string" ? this.quoteId(t.column) : this.renderExpr(t.column, params, (k) => this.quoteId(k));
|
|
696
|
+
return `${id} ${t.direction === "desc" ? "DESC" : "ASC"}`;
|
|
697
|
+
}).join(", ");
|
|
698
|
+
sql += ` ORDER BY ${terms}`;
|
|
699
|
+
}
|
|
700
|
+
if (node.limit !== void 0) sql += ` LIMIT ${params.bind(node.limit)}`;
|
|
701
|
+
if (node.offset !== void 0) sql += ` OFFSET ${params.bind(node.offset)}`;
|
|
702
|
+
return sql;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Render a qualified `alias.column` ref as `"alias"."column"`, translating the
|
|
706
|
+
* property name to the real column name for that alias's model.
|
|
707
|
+
*
|
|
708
|
+
* @param ref The `alias.property` reference (a bare name is left unqualified).
|
|
709
|
+
* @param names The node's per-alias name maps, if any source renames columns.
|
|
710
|
+
* @returns The quoted, qualified identifier.
|
|
711
|
+
*/
|
|
712
|
+
qualify(ref, names) {
|
|
713
|
+
const dot = ref.indexOf(".");
|
|
714
|
+
if (dot === -1) return this.quoteId(ref);
|
|
715
|
+
const alias = ref.slice(0, dot);
|
|
716
|
+
const prop = ref.slice(dot + 1);
|
|
717
|
+
return `${this.quoteId(alias)}.${this.columnId(prop, names?.[alias])}`;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Quote a column identifier, translating the model property name to the real
|
|
721
|
+
* database column name first.
|
|
722
|
+
*
|
|
723
|
+
* `names` is `undefined` for a model that renames nothing — the overwhelmingly
|
|
724
|
+
* common case — so this stays a single lookup plus the memoized quote.
|
|
725
|
+
*
|
|
726
|
+
* @param prop The model property name as written in the builder.
|
|
727
|
+
* @param names The node's property → column map, if any.
|
|
728
|
+
* @returns The quoted database identifier.
|
|
729
|
+
*/
|
|
730
|
+
columnId(prop, names) {
|
|
731
|
+
const mapped = names?.[prop];
|
|
732
|
+
if (mapped !== void 0) return this.quoteId(mapped);
|
|
733
|
+
const dot = prop.indexOf(".");
|
|
734
|
+
if (dot > 0 && dot < prop.length - 1) {
|
|
735
|
+
return `${this.quoteId(prop.slice(0, dot))}.${this.quoteId(prop.slice(dot + 1))}`;
|
|
736
|
+
}
|
|
737
|
+
return this.quoteId(prop);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Render a {@link SqlExpression} inline, binding the parameters it carries.
|
|
741
|
+
*
|
|
742
|
+
* This is what keeps `set({ attempts: sql.raw("attempts + 1") })` an expression
|
|
743
|
+
* instead of a bound object: the fragment goes into the statement text, and
|
|
744
|
+
* only a `sql.expr` template's interpolations become parameters.
|
|
745
|
+
*
|
|
746
|
+
* @param expr The branded expression.
|
|
747
|
+
* @param params The parameter collector for the statement being compiled.
|
|
748
|
+
* @returns The SQL text of the expression.
|
|
749
|
+
*/
|
|
750
|
+
renderExpression(expr, params) {
|
|
751
|
+
const token = expr.expression;
|
|
752
|
+
if (typeof token === "string") return renderPortableToken(token, this.name);
|
|
753
|
+
if ("raw" in token) return token.raw;
|
|
754
|
+
if ("excluded" in token) {
|
|
755
|
+
return renderExcluded(this.quoteId(token.excluded), token.excluded, this.name);
|
|
756
|
+
}
|
|
757
|
+
const parts = token.parts;
|
|
758
|
+
let sql = parts[0] ?? "";
|
|
759
|
+
for (let i = 1; i < parts.length; i++) {
|
|
760
|
+
sql += `${params.bind(expr.params[i - 1])}${parts[i]}`;
|
|
761
|
+
}
|
|
762
|
+
return sql;
|
|
763
|
+
}
|
|
764
|
+
/** Render one write value: a SQL expression inline, anything else as a parameter. */
|
|
765
|
+
renderValue(value, params) {
|
|
766
|
+
if (isSqlExpression(value)) return this.renderExpression(value, params);
|
|
767
|
+
if (isExpression(value)) {
|
|
768
|
+
return this.renderExpr(value.node, params, (k) => this.columnId(k, void 0));
|
|
769
|
+
}
|
|
770
|
+
return params.bind(value);
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* The statements that open a transaction with the requested characteristics.
|
|
774
|
+
*
|
|
775
|
+
* Returned as a list because the dialects disagree on shape: PostgreSQL takes
|
|
776
|
+
* everything on the `BEGIN` itself, MySQL needs a separate `SET TRANSACTION`
|
|
777
|
+
* before it, and SQLite has no syntax at all.
|
|
778
|
+
*
|
|
779
|
+
* @param options The requested isolation level and read-only flag.
|
|
780
|
+
* @returns The statements to run, in order.
|
|
781
|
+
* @throws Error When the dialect cannot honor what was asked.
|
|
782
|
+
*/
|
|
783
|
+
beginStatements(options) {
|
|
784
|
+
const parts = ["BEGIN"];
|
|
785
|
+
if (options?.isolation)
|
|
786
|
+
parts.push(`ISOLATION LEVEL ${options.isolation.toUpperCase()}`);
|
|
787
|
+
if (options?.readOnly) parts.push("READ ONLY");
|
|
788
|
+
return [parts.join(" ")];
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Render a row-level locking clause (`FOR UPDATE ...`).
|
|
792
|
+
*
|
|
793
|
+
* Standard on PostgreSQL and MySQL 8.0+; SQLite overrides it to throw.
|
|
794
|
+
*
|
|
795
|
+
* @param lock The locking clause from the node.
|
|
796
|
+
* @returns The SQL text, leading space included.
|
|
797
|
+
*/
|
|
798
|
+
renderLock(lock) {
|
|
799
|
+
const strength = lock.strength === "update" ? "FOR UPDATE" : "FOR SHARE";
|
|
800
|
+
const of = lock.of.length > 0 ? ` OF ${lock.of.map((t) => this.quoteId(t)).join(", ")}` : "";
|
|
801
|
+
const wait = lock.wait === "skipLocked" ? " SKIP LOCKED" : lock.wait === "noWait" ? " NOWAIT" : "";
|
|
802
|
+
return ` ${strength}${of}${wait}`;
|
|
803
|
+
}
|
|
804
|
+
// ---- statements -------------------------------------------------------
|
|
805
|
+
/**
|
|
806
|
+
* Compile a SELECT.
|
|
807
|
+
*
|
|
808
|
+
* Two alias rules differ between clauses and are handled here: PostgreSQL does
|
|
809
|
+
* NOT accept a `SELECT` alias in `HAVING`, so an aggregate key is re-emitted as
|
|
810
|
+
* its expression (`COUNT(*) > $1`), a form every dialect accepts; `ORDER BY`,
|
|
811
|
+
* by contrast, accepts the output alias everywhere, so it is emitted as
|
|
812
|
+
* written.
|
|
813
|
+
*
|
|
814
|
+
* @param node The select AST.
|
|
815
|
+
* @param params The parameter collector.
|
|
816
|
+
* @returns The SQL text.
|
|
817
|
+
*/
|
|
818
|
+
compileSelect(node, params) {
|
|
819
|
+
const names = node.names;
|
|
820
|
+
let cols;
|
|
821
|
+
if (node.aggregates.length > 0) {
|
|
822
|
+
const groupSel = node.groupBy.map((c) => this.columnId(c, names));
|
|
823
|
+
const aggSel = node.aggregates.map((a) => {
|
|
824
|
+
const inner = this.aggregateOperand(a, params, names);
|
|
825
|
+
return `${a.fn.toUpperCase()}(${inner}) AS ${this.quoteId(a.alias)}`;
|
|
826
|
+
});
|
|
827
|
+
cols = [...groupSel, ...aggSel].join(", ");
|
|
828
|
+
} else {
|
|
829
|
+
cols = node.columns === "*" ? "*" : node.columns.map((c) => this.columnId(c, names)).join(", ");
|
|
830
|
+
}
|
|
831
|
+
const computed = Object.entries(node.computed ?? {}).map(
|
|
832
|
+
([alias, expr]) => `${this.renderExpr(expr, params, (k) => this.columnId(k, names))} AS ${this.quoteId(alias)}`
|
|
833
|
+
);
|
|
834
|
+
if (computed.length > 0) cols = [cols, ...computed].join(", ");
|
|
835
|
+
const from = node.alias ? `${this.quoteId(node.table)} AS ${this.quoteId(node.alias)}` : this.quoteId(node.table);
|
|
836
|
+
let sql = `${this.compileWith(node.with, params)}SELECT ${node.distinct ? "DISTINCT " : ""}${cols} FROM ${from}`;
|
|
837
|
+
const where = this.compileCondition(
|
|
838
|
+
node.where,
|
|
839
|
+
params,
|
|
840
|
+
(k) => this.columnId(k, names),
|
|
841
|
+
codecEncoder(node.codecs)
|
|
842
|
+
);
|
|
843
|
+
if (where) sql += ` WHERE ${where}`;
|
|
844
|
+
if (node.groupBy.length > 0) {
|
|
845
|
+
sql += ` GROUP BY ${node.groupBy.map((c) => this.columnId(c, names)).join(", ")}`;
|
|
846
|
+
}
|
|
847
|
+
const aggByAlias = new Map(node.aggregates.map((a) => [a.alias, a]));
|
|
848
|
+
if (node.having) {
|
|
849
|
+
const having = this.compileCondition(node.having, params, (key) => {
|
|
850
|
+
const agg = aggByAlias.get(key);
|
|
851
|
+
if (!agg) return this.columnId(key, names);
|
|
852
|
+
const inner = this.aggregateOperand(agg, params, names);
|
|
853
|
+
return `${agg.fn.toUpperCase()}(${inner})`;
|
|
854
|
+
});
|
|
855
|
+
if (having) sql += ` HAVING ${having}`;
|
|
856
|
+
}
|
|
857
|
+
if (node.orderBy.length > 0) {
|
|
858
|
+
const terms = node.orderBy.map((t) => {
|
|
859
|
+
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);
|
|
860
|
+
return `${id} ${t.direction === "desc" ? "DESC" : "ASC"}`;
|
|
861
|
+
}).join(", ");
|
|
862
|
+
sql += ` ORDER BY ${terms}`;
|
|
863
|
+
}
|
|
864
|
+
if (node.limit !== void 0) sql += ` LIMIT ${params.bind(node.limit)}`;
|
|
865
|
+
if (node.offset !== void 0) sql += ` OFFSET ${params.bind(node.offset)}`;
|
|
866
|
+
if (node.lock) {
|
|
867
|
+
if (node.distinct || node.groupBy.length > 0 || node.aggregates.length > 0) {
|
|
868
|
+
throw new Error(
|
|
869
|
+
"FOR UPDATE / FOR SHARE cannot be combined with DISTINCT or an aggregate query \u2014 lock the underlying rows in a separate SELECT."
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
sql += this.renderLock(node.lock);
|
|
873
|
+
}
|
|
874
|
+
return sql;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Compile an INSERT.
|
|
878
|
+
*
|
|
879
|
+
* Takes the cached fast path only when the statement text is a pure function of
|
|
880
|
+
* its structure. A SQL expression among the values, or a conflict predicate,
|
|
881
|
+
* makes the text depend on the values themselves — those compile uncached, in
|
|
882
|
+
* SQL order, so placeholder positions stay correct.
|
|
883
|
+
*/
|
|
884
|
+
compileInsert(node, params) {
|
|
885
|
+
if (node.fromSelect) {
|
|
886
|
+
const target = node.fromSelect.columns.map((c) => this.columnId(c, node.names)).join(", ");
|
|
887
|
+
const source = node.fromSelect.select;
|
|
888
|
+
const query = source.kind === "join_select" ? this.compileJoin(source, params) : source.kind === "set_op" ? this.compileSetOp(source, params) : this.compileSelect(source, params);
|
|
889
|
+
const returning = this.compileReturning(node.returning, node.names);
|
|
890
|
+
return `INSERT INTO ${this.quoteId(node.table)} (${target}) ${query}${returning}`;
|
|
891
|
+
}
|
|
892
|
+
const columns = insertColumns(node.values);
|
|
893
|
+
const conflict = node.onConflict;
|
|
894
|
+
const cacheable = conflict?.targetWhere === void 0 && conflict?.updateWhere === void 0 && !insertHasExpression(node);
|
|
895
|
+
if (!cacheable) return this.compileInsertDirect(node, columns, params);
|
|
896
|
+
for (const row of node.values) {
|
|
897
|
+
for (const c of columns) params.bind(row[c] ?? null);
|
|
898
|
+
}
|
|
899
|
+
const conflictCols = conflict && conflict.update !== "nothing" ? Object.keys(conflict.update) : [];
|
|
900
|
+
for (const c of conflictCols) {
|
|
901
|
+
params.bind((conflict?.update)[c]);
|
|
902
|
+
}
|
|
903
|
+
return this.insertTemplate(node, columns, conflictCols, params);
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Compile an INSERT without the template cache, rendering clauses in statement
|
|
907
|
+
* order so every parameter is bound at the position it appears.
|
|
908
|
+
*
|
|
909
|
+
* @param node The insert node.
|
|
910
|
+
* @param columns The column keys shared by every row.
|
|
911
|
+
* @param params The parameter collector.
|
|
912
|
+
* @returns The SQL text.
|
|
913
|
+
*/
|
|
914
|
+
compileInsertDirect(node, columns, params) {
|
|
915
|
+
const names = node.names;
|
|
916
|
+
const colSql = columns.map((c) => this.columnId(c, names)).join(", ");
|
|
917
|
+
const rowsSql = node.values.map((row) => {
|
|
918
|
+
const cells = columns.map(
|
|
919
|
+
(c) => this.renderValue(row[c] ?? null, params)
|
|
920
|
+
);
|
|
921
|
+
return `(${cells.join(", ")})`;
|
|
922
|
+
}).join(", ");
|
|
923
|
+
let sql = `INSERT INTO ${this.quoteId(node.table)} (${colSql}) VALUES ${rowsSql}`;
|
|
924
|
+
if (node.onConflict) {
|
|
925
|
+
const update = node.onConflict.update;
|
|
926
|
+
const conflictCols = update === "nothing" ? [] : Object.keys(update);
|
|
927
|
+
let cursor = 0;
|
|
928
|
+
sql += this.renderConflict(
|
|
929
|
+
node.onConflict,
|
|
930
|
+
conflictCols,
|
|
931
|
+
() => {
|
|
932
|
+
const key = conflictCols[cursor++];
|
|
933
|
+
return this.renderValue(update[key], params);
|
|
934
|
+
},
|
|
935
|
+
names,
|
|
936
|
+
params
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
sql += this.compileReturning(node.returning, names);
|
|
940
|
+
return sql;
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* The INSERT SQL template for a given structure, cached across calls.
|
|
944
|
+
*
|
|
945
|
+
* The text depends only on (dialect, table, columns, row count, returning,
|
|
946
|
+
* conflict shape) — never on the bound values — and placeholder positions are
|
|
947
|
+
* deterministic from the counts (a fresh statement always starts binding at 1).
|
|
948
|
+
* So a per-row insert loop compiles the string once and reuses it every row.
|
|
949
|
+
*/
|
|
950
|
+
insertTemplate(node, columns, conflictCols, params) {
|
|
951
|
+
const returningKey = node.returning === null ? "" : node.returning === "*" ? "*" : node.returning.join(",");
|
|
952
|
+
const conflictKey = node.onConflict ? `${node.onConflict.target.join(",")}>${node.onConflict.update === "nothing" ? "nothing" : conflictCols.join(",")}` : "";
|
|
953
|
+
const key = `${this.name}|${node.table}|${columns.join(",")}|${node.values.length}|${returningKey}|${conflictKey}`;
|
|
954
|
+
const cached = _BaseDialect.insertTemplates.get(key);
|
|
955
|
+
if (cached !== void 0) return cached;
|
|
956
|
+
const names = node.names;
|
|
957
|
+
const colSql = columns.map((c) => this.columnId(c, names)).join(", ");
|
|
958
|
+
let position = 0;
|
|
959
|
+
const rowsSql = node.values.map(() => `(${columns.map(() => this.placeholder(++position)).join(", ")})`).join(", ");
|
|
960
|
+
let sql = `INSERT INTO ${this.quoteId(node.table)} (${colSql}) VALUES ${rowsSql}`;
|
|
961
|
+
if (node.onConflict) {
|
|
962
|
+
sql += this.renderConflict(
|
|
963
|
+
node.onConflict,
|
|
964
|
+
conflictCols,
|
|
965
|
+
() => this.placeholder(++position),
|
|
966
|
+
names,
|
|
967
|
+
params
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
sql += this.compileReturning(node.returning, names);
|
|
971
|
+
_BaseDialect.insertTemplates.set(key, sql);
|
|
972
|
+
return sql;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Render the conflict-handling clause. Standard SQL (SQLite/PostgreSQL) uses
|
|
976
|
+
* `ON CONFLICT (...) [WHERE predicate] DO NOTHING | DO UPDATE SET ... [WHERE ...]`;
|
|
977
|
+
* MySQL overrides this.
|
|
978
|
+
*
|
|
979
|
+
* The index predicate is rendered before the `DO UPDATE` assignments because
|
|
980
|
+
* that is where it sits in the statement, so its parameters bind first.
|
|
981
|
+
*
|
|
982
|
+
* @param onConflict The conflict clause from the node.
|
|
983
|
+
* @param conflictCols The columns to overwrite on `DO UPDATE` (empty for nothing).
|
|
984
|
+
* @param nextValue Yields the SQL for the next `DO UPDATE` assignment value.
|
|
985
|
+
* @param names The node's property → column map, if any.
|
|
986
|
+
* @param params The parameter collector, for the predicates.
|
|
987
|
+
* @returns The SQL text, leading space included.
|
|
988
|
+
*/
|
|
989
|
+
renderConflict(onConflict, conflictCols, nextValue, names, params) {
|
|
990
|
+
const idFor = (key) => this.columnId(key, names);
|
|
991
|
+
const target = onConflict.target.map(idFor).join(", ");
|
|
992
|
+
const indexWhere = this.compileCondition(onConflict.targetWhere, params, idFor);
|
|
993
|
+
const targetSql = indexWhere ? `(${target}) WHERE ${indexWhere}` : `(${target})`;
|
|
994
|
+
if (onConflict.update === "nothing") return ` ON CONFLICT ${targetSql} DO NOTHING`;
|
|
995
|
+
const assignments = conflictCols.map((c) => `${idFor(c)} = ${nextValue()}`).join(", ");
|
|
996
|
+
let sql = ` ON CONFLICT ${targetSql} DO UPDATE SET ${assignments}`;
|
|
997
|
+
const updateWhere = this.compileCondition(onConflict.updateWhere, params, idFor);
|
|
998
|
+
if (updateWhere) sql += ` WHERE ${updateWhere}`;
|
|
999
|
+
return sql;
|
|
1000
|
+
}
|
|
1001
|
+
compileUpdate(node, params) {
|
|
1002
|
+
const names = node.names;
|
|
1003
|
+
const sets = Object.entries(node.set).map(
|
|
1004
|
+
([col, value]) => `${this.columnId(col, names)} = ${this.renderValue(value, params)}`
|
|
1005
|
+
).join(", ");
|
|
1006
|
+
let sql = `UPDATE ${this.quoteId(node.table)} SET ${sets}`;
|
|
1007
|
+
sql += this.compileExtraSources("FROM", node.from);
|
|
1008
|
+
const where = this.compileCondition(
|
|
1009
|
+
node.where,
|
|
1010
|
+
params,
|
|
1011
|
+
(k) => this.columnId(k, names),
|
|
1012
|
+
codecEncoder(node.codecs)
|
|
1013
|
+
);
|
|
1014
|
+
if (where) sql += ` WHERE ${where}`;
|
|
1015
|
+
sql += this.compileReturning(node.returning, names);
|
|
1016
|
+
return sql;
|
|
1017
|
+
}
|
|
1018
|
+
compileDelete(node, params) {
|
|
1019
|
+
const names = node.names;
|
|
1020
|
+
let sql = `DELETE FROM ${this.quoteId(node.table)}`;
|
|
1021
|
+
sql += this.compileExtraSources("USING", node.using);
|
|
1022
|
+
const where = this.compileCondition(
|
|
1023
|
+
node.where,
|
|
1024
|
+
params,
|
|
1025
|
+
(k) => this.columnId(k, names),
|
|
1026
|
+
codecEncoder(node.codecs)
|
|
1027
|
+
);
|
|
1028
|
+
if (where) sql += ` WHERE ${where}`;
|
|
1029
|
+
sql += this.compileReturning(node.returning, names);
|
|
1030
|
+
return sql;
|
|
1031
|
+
}
|
|
1032
|
+
compileJoin(node, params) {
|
|
1033
|
+
const names = node.names;
|
|
1034
|
+
const cols = node.selections.map((s) => {
|
|
1035
|
+
const ref = `${s.alias}.${s.column}`;
|
|
1036
|
+
const label = node.pick ? s.column : ref;
|
|
1037
|
+
return `${this.qualify(ref, names)} AS ${this.quoteId(label)}`;
|
|
1038
|
+
}).join(", ");
|
|
1039
|
+
let sql = `${this.compileWith(node.with, params)}SELECT ${cols} FROM ${this.quoteId(node.base.table)} AS ${this.quoteId(node.base.alias)}`;
|
|
1040
|
+
for (const j of node.joins) {
|
|
1041
|
+
const kw = j.kind === "left" ? "LEFT JOIN" : "INNER JOIN";
|
|
1042
|
+
const on = j.on.map(([l, r]) => `${this.qualify(l, names)} = ${this.qualify(r, names)}`).join(" AND ");
|
|
1043
|
+
sql += ` ${kw} ${this.quoteId(j.table)} AS ${this.quoteId(j.alias)} ON ${on}`;
|
|
1044
|
+
}
|
|
1045
|
+
const where = this.compileCondition(
|
|
1046
|
+
node.where,
|
|
1047
|
+
params,
|
|
1048
|
+
(k) => this.qualify(k, names)
|
|
1049
|
+
);
|
|
1050
|
+
if (where) sql += ` WHERE ${where}`;
|
|
1051
|
+
if (node.orderBy.length > 0) {
|
|
1052
|
+
const terms = node.orderBy.map(
|
|
1053
|
+
(t) => `${this.qualify(t.ref, names)} ${t.direction === "desc" ? "DESC" : "ASC"}`
|
|
1054
|
+
).join(", ");
|
|
1055
|
+
sql += ` ORDER BY ${terms}`;
|
|
1056
|
+
}
|
|
1057
|
+
if (node.limit !== void 0) sql += ` LIMIT ${params.bind(node.limit)}`;
|
|
1058
|
+
if (node.offset !== void 0) sql += ` OFFSET ${params.bind(node.offset)}`;
|
|
1059
|
+
return sql;
|
|
1060
|
+
}
|
|
1061
|
+
// ---- clauses ----------------------------------------------------------
|
|
1062
|
+
/**
|
|
1063
|
+
* Render the extra sources of an `UPDATE ... FROM` / `DELETE ... USING`.
|
|
1064
|
+
*
|
|
1065
|
+
* The dialects that do not have the clause override this and throw: emitting it
|
|
1066
|
+
* anyway would produce a statement the server rejects, and quietly dropping it
|
|
1067
|
+
* would change which rows are written.
|
|
1068
|
+
*
|
|
1069
|
+
* @param keyword `FROM` or `USING`.
|
|
1070
|
+
* @param sources The extra tables, if any.
|
|
1071
|
+
* @returns The clause with a leading space, or an empty string.
|
|
1072
|
+
*/
|
|
1073
|
+
compileExtraSources(keyword, sources) {
|
|
1074
|
+
if (!sources || sources.length === 0) return "";
|
|
1075
|
+
const list = sources.map((s) => `${this.quoteId(s.table)} AS ${this.quoteId(s.alias)}`).join(", ");
|
|
1076
|
+
return ` ${keyword} ${list}`;
|
|
1077
|
+
}
|
|
1078
|
+
compileReturning(returning, names) {
|
|
1079
|
+
if (returning === null) return "";
|
|
1080
|
+
if (returning === "*") return " RETURNING *";
|
|
1081
|
+
return ` RETURNING ${returning.map((c) => this.columnId(c, names)).join(", ")}`;
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Compile a condition tree (fields / and / or / not) to SQL. `idFor` renders a
|
|
1085
|
+
* key to a quoted identifier — `quoteId` for single-table, `qualify` for joins —
|
|
1086
|
+
* so select/update/delete/join all share this one compiler.
|
|
1087
|
+
*/
|
|
1088
|
+
compileCondition(node, params, idFor, encode = (_key, value) => value) {
|
|
1089
|
+
if (!node) return "";
|
|
1090
|
+
switch (node.kind) {
|
|
1091
|
+
case "fields": {
|
|
1092
|
+
const conditions = [];
|
|
1093
|
+
for (const [key, value] of Object.entries(node.fields)) {
|
|
1094
|
+
const id = idFor(key);
|
|
1095
|
+
if (isExpression(value)) {
|
|
1096
|
+
conditions.push(
|
|
1097
|
+
this.compileExprOperator(
|
|
1098
|
+
id,
|
|
1099
|
+
"eq",
|
|
1100
|
+
this.renderExpr(value.node, params, idFor)
|
|
1101
|
+
)
|
|
1102
|
+
);
|
|
1103
|
+
} else if (isOperatorObject(value)) {
|
|
1104
|
+
for (const [op, operand] of Object.entries(value)) {
|
|
1105
|
+
conditions.push(
|
|
1106
|
+
isExpression(operand) ? this.compileExprOperator(
|
|
1107
|
+
id,
|
|
1108
|
+
op,
|
|
1109
|
+
this.renderExpr(operand.node, params, idFor)
|
|
1110
|
+
) : this.compileOperator(
|
|
1111
|
+
id,
|
|
1112
|
+
op,
|
|
1113
|
+
encodeOperand(op, operand, (v) => encode(key, v)),
|
|
1114
|
+
params
|
|
1115
|
+
)
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
} else {
|
|
1119
|
+
const operand = encode(key, value);
|
|
1120
|
+
conditions.push(
|
|
1121
|
+
operand === null ? `${id} IS NULL` : `${id} = ${params.bind(operand)}`
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return conditions.join(" AND ");
|
|
1126
|
+
}
|
|
1127
|
+
case "and":
|
|
1128
|
+
case "or": {
|
|
1129
|
+
const parts = node.parts.map((p) => this.compileCondition(p, params, idFor, encode)).filter((s) => s.length > 0);
|
|
1130
|
+
if (parts.length === 0) return "";
|
|
1131
|
+
const sep = node.kind === "and" ? " AND " : " OR ";
|
|
1132
|
+
return parts.map((p) => `(${p})`).join(sep);
|
|
1133
|
+
}
|
|
1134
|
+
case "not": {
|
|
1135
|
+
const inner = this.compileCondition(node.part, params, idFor, encode);
|
|
1136
|
+
return inner ? `NOT (${inner})` : "";
|
|
1137
|
+
}
|
|
1138
|
+
case "exists": {
|
|
1139
|
+
const select = node.select;
|
|
1140
|
+
this.checkSubquery(select);
|
|
1141
|
+
const keyword = node.negate ? "NOT EXISTS" : "EXISTS";
|
|
1142
|
+
return `${keyword} (${this.compileSelect(select, params)})`;
|
|
1143
|
+
}
|
|
1144
|
+
case "fullText":
|
|
1145
|
+
return this.compileFullText(node, params, idFor);
|
|
1146
|
+
case "compare": {
|
|
1147
|
+
const left = this.renderExpr(node.left, params, idFor);
|
|
1148
|
+
if (node.right.kind === "value") {
|
|
1149
|
+
return this.compileOperator(left, node.op, node.right.value, params);
|
|
1150
|
+
}
|
|
1151
|
+
return this.compileExprOperator(
|
|
1152
|
+
left,
|
|
1153
|
+
node.op,
|
|
1154
|
+
this.renderExpr(node.right, params, idFor)
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Compile a full-text condition.
|
|
1161
|
+
*
|
|
1162
|
+
* PostgreSQL gets the real thing (`@@ websearch_to_tsquery`); the dialects with
|
|
1163
|
+
* no text-search engine override this and compile the node's prebuilt substring
|
|
1164
|
+
* fallback instead, so the query still returns the right rows.
|
|
1165
|
+
*
|
|
1166
|
+
* @param node The full-text condition node.
|
|
1167
|
+
* @param params The parameter collector.
|
|
1168
|
+
* @param idFor Column-name resolver.
|
|
1169
|
+
* @returns The rendered condition.
|
|
1170
|
+
*/
|
|
1171
|
+
compileFullText(node, params, idFor) {
|
|
1172
|
+
const config = params.bind(node.language);
|
|
1173
|
+
return `${this.tsVector(node.columns, config, idFor)} @@ websearch_to_tsquery(${config}::regconfig, ${params.bind(node.term)})`;
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Render one side of a comparison.
|
|
1177
|
+
*
|
|
1178
|
+
* A column reference goes through `idFor`, so an explicit `.name()` mapping and
|
|
1179
|
+
* join qualification apply here exactly as they do in the object form of
|
|
1180
|
+
* `where` — `col()` is not a way around them. Only a `value` node binds.
|
|
1181
|
+
*
|
|
1182
|
+
* @param node The expression AST.
|
|
1183
|
+
* @param params The parameter collector.
|
|
1184
|
+
* @param idFor The identifier resolver for the enclosing statement.
|
|
1185
|
+
* @returns The SQL text of the expression.
|
|
1186
|
+
*/
|
|
1187
|
+
/**
|
|
1188
|
+
* Render what an aggregate is applied to: `*`, a column, or an expression.
|
|
1189
|
+
*
|
|
1190
|
+
* An expression operand is what makes a conditional aggregate
|
|
1191
|
+
* (`SUM(CASE WHEN ... END)`) expressible — one pass over the table instead of a
|
|
1192
|
+
* query per bucket.
|
|
1193
|
+
*
|
|
1194
|
+
* @param agg The aggregate term.
|
|
1195
|
+
* @param params The parameter collector.
|
|
1196
|
+
* @param names The node's column-name map.
|
|
1197
|
+
* @returns The rendered operand.
|
|
1198
|
+
*/
|
|
1199
|
+
aggregateOperand(agg, params, names) {
|
|
1200
|
+
if (agg.column === "*") return "*";
|
|
1201
|
+
if (typeof agg.column === "string") return this.columnId(agg.column, names);
|
|
1202
|
+
return this.renderExpr(agg.column, params, (k) => this.columnId(k, names));
|
|
1203
|
+
}
|
|
1204
|
+
renderExpr(node, params, idFor) {
|
|
1205
|
+
switch (node.kind) {
|
|
1206
|
+
case "column":
|
|
1207
|
+
return idFor(node.name);
|
|
1208
|
+
case "value":
|
|
1209
|
+
return params.bind(node.value);
|
|
1210
|
+
case "fn": {
|
|
1211
|
+
const args = node.args.map((a) => this.renderExpr(a, params, idFor)).join(", ");
|
|
1212
|
+
return `${node.name}(${args})`;
|
|
1213
|
+
}
|
|
1214
|
+
case "case": {
|
|
1215
|
+
const branches = node.branches.map(
|
|
1216
|
+
(b) => `WHEN ${this.compileCondition(b.when, params, idFor)} THEN ${this.renderExpr(b.result, params, idFor)}`
|
|
1217
|
+
).join(" ");
|
|
1218
|
+
const fallback = node.fallback === null ? "" : ` ELSE ${this.renderExpr(node.fallback, params, idFor)}`;
|
|
1219
|
+
return `CASE ${branches}${fallback} END`;
|
|
1220
|
+
}
|
|
1221
|
+
case "cast":
|
|
1222
|
+
return `CAST(${this.renderExpr(node.operand, params, idFor)} AS ${this.castTypeName(node.to)})`;
|
|
1223
|
+
case "scalar": {
|
|
1224
|
+
const select = node.select;
|
|
1225
|
+
this.checkSubquery(select);
|
|
1226
|
+
return `(${this.compileSelect(select, params)})`;
|
|
1227
|
+
}
|
|
1228
|
+
case "rank":
|
|
1229
|
+
return this.renderRank(node.columns, node.term, node.language, params, idFor);
|
|
1230
|
+
case "star":
|
|
1231
|
+
return "*";
|
|
1232
|
+
case "window": {
|
|
1233
|
+
const call = this.renderExpr(node.fn, params, idFor);
|
|
1234
|
+
const parts = [];
|
|
1235
|
+
if (node.partitionBy.length > 0) {
|
|
1236
|
+
parts.push(`PARTITION BY ${node.partitionBy.map(idFor).join(", ")}`);
|
|
1237
|
+
}
|
|
1238
|
+
if (node.orderBy.length > 0) {
|
|
1239
|
+
const terms = node.orderBy.map((t) => `${idFor(t.column)} ${t.direction === "desc" ? "DESC" : "ASC"}`).join(", ");
|
|
1240
|
+
parts.push(`ORDER BY ${terms}`);
|
|
1241
|
+
}
|
|
1242
|
+
if (node.frame) parts.push(node.frame);
|
|
1243
|
+
return `${call} OVER (${parts.join(" ")})`;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Render a full-text relevance score.
|
|
1249
|
+
*
|
|
1250
|
+
* PostgreSQL has `ts_rank`; the others have nothing equivalent, and they
|
|
1251
|
+
* override this to a constant so that ordering by it is a no-op rather than a
|
|
1252
|
+
* compile error — the fallback keeps returning the right rows, only unranked.
|
|
1253
|
+
*
|
|
1254
|
+
* @param columns The columns making up the document.
|
|
1255
|
+
* @param term The search term.
|
|
1256
|
+
* @param language The text-search configuration.
|
|
1257
|
+
* @param params The parameter collector.
|
|
1258
|
+
* @param idFor Column-name resolver.
|
|
1259
|
+
* @returns The rendered score expression.
|
|
1260
|
+
*/
|
|
1261
|
+
renderRank(columns, term, language, params, idFor) {
|
|
1262
|
+
const config = params.bind(language);
|
|
1263
|
+
return `ts_rank(${this.tsVector(columns, config, idFor)}, websearch_to_tsquery(${config}::regconfig, ${params.bind(term)}))`;
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Build the `to_tsvector(...)` document out of the searched columns.
|
|
1267
|
+
*
|
|
1268
|
+
* `coalesce(col, '')` matters: in SQL a `NULL` anywhere in a concatenation makes
|
|
1269
|
+
* the whole document `NULL`, so one empty column would silently exclude the row.
|
|
1270
|
+
*
|
|
1271
|
+
* @param columns The columns making up the document.
|
|
1272
|
+
* @param config The already-bound placeholder for the text-search config.
|
|
1273
|
+
* @param idFor Column-name resolver.
|
|
1274
|
+
* @returns The rendered `to_tsvector(...)` call.
|
|
1275
|
+
*/
|
|
1276
|
+
tsVector(columns, config, idFor) {
|
|
1277
|
+
const document = columns.map((c) => `coalesce(${idFor(c)}, '')`).join(" || ' ' || ");
|
|
1278
|
+
return `to_tsvector(${config}::regconfig, ${document})`;
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* The SQL type name this dialect accepts in a `CAST`.
|
|
1282
|
+
*
|
|
1283
|
+
* The base mapping is the standard one PostgreSQL takes; SQLite and MySQL
|
|
1284
|
+
* override it, because the names genuinely differ (MySQL's `CAST(x AS SIGNED)`
|
|
1285
|
+
* is not `INTEGER`, and SQLite only has five storage classes to aim at).
|
|
1286
|
+
*
|
|
1287
|
+
* @param to The portable cast target.
|
|
1288
|
+
* @returns The dialect's own type name.
|
|
1289
|
+
*/
|
|
1290
|
+
castTypeName(to) {
|
|
1291
|
+
switch (to) {
|
|
1292
|
+
case "integer":
|
|
1293
|
+
return "INTEGER";
|
|
1294
|
+
case "bigint":
|
|
1295
|
+
return "BIGINT";
|
|
1296
|
+
case "real":
|
|
1297
|
+
return "DOUBLE PRECISION";
|
|
1298
|
+
case "numeric":
|
|
1299
|
+
return "NUMERIC";
|
|
1300
|
+
case "text":
|
|
1301
|
+
return "TEXT";
|
|
1302
|
+
case "boolean":
|
|
1303
|
+
return "BOOLEAN";
|
|
1304
|
+
case "date":
|
|
1305
|
+
return "DATE";
|
|
1306
|
+
case "datetime":
|
|
1307
|
+
case "timestamp":
|
|
1308
|
+
return "TIMESTAMP";
|
|
1309
|
+
case "uuid":
|
|
1310
|
+
return "UUID";
|
|
1311
|
+
case "json":
|
|
1312
|
+
return "JSON";
|
|
1313
|
+
case "jsonb":
|
|
1314
|
+
return "JSONB";
|
|
1315
|
+
case "blob":
|
|
1316
|
+
return "BYTEA";
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Compile a comparison whose right-hand side is another expression rather than
|
|
1321
|
+
* a bound value (`total > paid`, `lower(a) = lower(b)`).
|
|
1322
|
+
*
|
|
1323
|
+
* The list and null operators are excluded: `IN`, `BETWEEN` and `IS NULL` take
|
|
1324
|
+
* a value operand, and accepting an expression there would silently compile to
|
|
1325
|
+
* something else.
|
|
1326
|
+
*
|
|
1327
|
+
* @param left The rendered left-hand side.
|
|
1328
|
+
* @param op The operator name.
|
|
1329
|
+
* @param right The rendered right-hand side.
|
|
1330
|
+
* @returns The SQL text of the predicate.
|
|
1331
|
+
* @throws Error When the operator needs a value operand.
|
|
1332
|
+
*/
|
|
1333
|
+
compileExprOperator(left, op, right) {
|
|
1334
|
+
switch (op) {
|
|
1335
|
+
case "eq":
|
|
1336
|
+
return `${left} = ${right}`;
|
|
1337
|
+
case "ne":
|
|
1338
|
+
return `${left} <> ${right}`;
|
|
1339
|
+
case "gt":
|
|
1340
|
+
return `${left} > ${right}`;
|
|
1341
|
+
case "gte":
|
|
1342
|
+
return `${left} >= ${right}`;
|
|
1343
|
+
case "lt":
|
|
1344
|
+
return `${left} < ${right}`;
|
|
1345
|
+
case "lte":
|
|
1346
|
+
return `${left} <= ${right}`;
|
|
1347
|
+
case "like":
|
|
1348
|
+
return `${left} LIKE ${right}`;
|
|
1349
|
+
case "ilike":
|
|
1350
|
+
return this.ilike(left, right);
|
|
1351
|
+
case "ieq":
|
|
1352
|
+
return `lower(${left}) = lower(${right})`;
|
|
1353
|
+
case "iContains":
|
|
1354
|
+
throw new Error(
|
|
1355
|
+
'The "iContains" operator matches a literal, so it takes a value, not an expression.'
|
|
1356
|
+
);
|
|
1357
|
+
case "contains":
|
|
1358
|
+
case "containedBy":
|
|
1359
|
+
case "overlaps":
|
|
1360
|
+
return `${left} ${this.arrayOperator(op)} ${right}`;
|
|
1361
|
+
default:
|
|
1362
|
+
throw new Error(`The "${op}" operator takes a value operand, not an expression.`);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
compileOperator(id, op, operand, params) {
|
|
1366
|
+
switch (op) {
|
|
1367
|
+
case "eq":
|
|
1368
|
+
return operand === null ? `${id} IS NULL` : `${id} = ${params.bind(operand)}`;
|
|
1369
|
+
case "ne":
|
|
1370
|
+
return operand === null ? `${id} IS NOT NULL` : `${id} <> ${params.bind(operand)}`;
|
|
1371
|
+
case "gt":
|
|
1372
|
+
return `${id} > ${params.bind(operand)}`;
|
|
1373
|
+
case "gte":
|
|
1374
|
+
return `${id} >= ${params.bind(operand)}`;
|
|
1375
|
+
case "lt":
|
|
1376
|
+
return `${id} < ${params.bind(operand)}`;
|
|
1377
|
+
case "lte":
|
|
1378
|
+
return `${id} <= ${params.bind(operand)}`;
|
|
1379
|
+
case "like":
|
|
1380
|
+
return `${id} LIKE ${params.bind(operand)}`;
|
|
1381
|
+
case "ilike":
|
|
1382
|
+
return this.ilike(id, params.bind(operand));
|
|
1383
|
+
case "ieq":
|
|
1384
|
+
return operand === null ? `${id} IS NULL` : `lower(${id}) = lower(${params.bind(operand)})`;
|
|
1385
|
+
case "iContains":
|
|
1386
|
+
return this.ilikeEscaped(id, params.bind(`%${escapeLike(String(operand))}%`));
|
|
1387
|
+
case "contains":
|
|
1388
|
+
return `${id} ${this.arrayOperator("contains")} ${params.bind(operand)}`;
|
|
1389
|
+
case "containedBy":
|
|
1390
|
+
return `${id} ${this.arrayOperator("containedBy")} ${params.bind(operand)}`;
|
|
1391
|
+
case "overlaps":
|
|
1392
|
+
return `${id} ${this.arrayOperator("overlaps")} ${params.bind(operand)}`;
|
|
1393
|
+
case "in":
|
|
1394
|
+
return this.compileIn(id, operand, params, false);
|
|
1395
|
+
case "notIn":
|
|
1396
|
+
return this.compileIn(id, operand, params, true);
|
|
1397
|
+
case "between": {
|
|
1398
|
+
const [lo, hi] = operand;
|
|
1399
|
+
return `${id} BETWEEN ${params.bind(lo)} AND ${params.bind(hi)}`;
|
|
1400
|
+
}
|
|
1401
|
+
case "isNull":
|
|
1402
|
+
return operand ? `${id} IS NULL` : `${id} IS NOT NULL`;
|
|
1403
|
+
default:
|
|
1404
|
+
throw new Error(`Unknown operator ${JSON.stringify(op)}`);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Compile `IN` / `NOT IN`, whose operand is either a value list or a
|
|
1409
|
+
* single-column subquery.
|
|
1410
|
+
*
|
|
1411
|
+
* The subquery is rendered at the position it appears in the outer statement
|
|
1412
|
+
* and shares the same parameter collector, so its own placeholders land in the
|
|
1413
|
+
* right order — and it keeps its own `names` map, since the inner model may use
|
|
1414
|
+
* a different naming convention than the outer one.
|
|
1415
|
+
*
|
|
1416
|
+
* @param id The quoted column identifier being tested.
|
|
1417
|
+
* @param operand A list of values, or a {@link Subquery}.
|
|
1418
|
+
* @param params The parameter collector for the statement being compiled.
|
|
1419
|
+
* @param negate True for `NOT IN`.
|
|
1420
|
+
* @returns The SQL text of the predicate.
|
|
1421
|
+
*/
|
|
1422
|
+
compileIn(id, operand, params, negate) {
|
|
1423
|
+
const keyword = negate ? "NOT IN" : "IN";
|
|
1424
|
+
if (isSubquery(operand)) {
|
|
1425
|
+
this.checkSubquery(operand.node);
|
|
1426
|
+
return `${id} ${keyword} (${this.compileSelect(operand.node, params)})`;
|
|
1427
|
+
}
|
|
1428
|
+
const values = operand;
|
|
1429
|
+
if (values.length === 0) {
|
|
1430
|
+
return negate ? "1 = 1" : "1 = 0";
|
|
1431
|
+
}
|
|
1432
|
+
const list = values.map((v) => params.bind(v)).join(", ");
|
|
1433
|
+
return `${id} ${keyword} (${list})`;
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
var SqliteDialect = class extends BaseDialect {
|
|
1437
|
+
name = "sqlite";
|
|
1438
|
+
/**
|
|
1439
|
+
* SQLite explains with `EXPLAIN QUERY PLAN`, and has no `ANALYZE` — the plain
|
|
1440
|
+
* `EXPLAIN` there dumps bytecode, which answers a different question.
|
|
1441
|
+
*/
|
|
1442
|
+
explainPrefix(analyze) {
|
|
1443
|
+
if (analyze) {
|
|
1444
|
+
throw new Error(
|
|
1445
|
+
"SQLite has no EXPLAIN ANALYZE; use EXPLAIN QUERY PLAN (analyze: false)."
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
return "EXPLAIN QUERY PLAN";
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* SQLite has `UPDATE ... FROM` (3.33+) but no `DELETE ... USING`.
|
|
1452
|
+
*
|
|
1453
|
+
* The portable shape there is a subquery — `where({ id: { in: … } })` — so this
|
|
1454
|
+
* throws and says so, rather than emitting a clause SQLite does not parse.
|
|
1455
|
+
*/
|
|
1456
|
+
compileExtraSources(keyword, sources) {
|
|
1457
|
+
if (keyword === "USING" && sources && sources.length > 0) {
|
|
1458
|
+
throw new Error(
|
|
1459
|
+
'SQLite has no DELETE ... USING; filter with a subquery instead: where({ id: { in: select(Other, ["id"]).asSubquery("id") } }).'
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
return super.compileExtraSources(keyword, sources);
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* SQLite has no text-search engine, so the prebuilt substring fallback is
|
|
1466
|
+
* compiled instead. The rows are right; the ranking is what is missing.
|
|
1467
|
+
*/
|
|
1468
|
+
compileFullText(node, params, idFor) {
|
|
1469
|
+
return this.compileCondition(node.fallback, params, idFor);
|
|
1470
|
+
}
|
|
1471
|
+
/** No text-search engine means no score: a constant, so ordering by it is inert. */
|
|
1472
|
+
renderRank() {
|
|
1473
|
+
return "0";
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* SQLite has five storage classes, so most targets collapse onto `TEXT` or
|
|
1477
|
+
* `INTEGER`. Naming a type it does not know would not fail — SQLite applies the
|
|
1478
|
+
* closest affinity — but it would make the cast mean something different here
|
|
1479
|
+
* than on the other databases, which is what this mapping avoids.
|
|
1480
|
+
*/
|
|
1481
|
+
castTypeName(to) {
|
|
1482
|
+
switch (to) {
|
|
1483
|
+
case "integer":
|
|
1484
|
+
case "bigint":
|
|
1485
|
+
case "boolean":
|
|
1486
|
+
return "INTEGER";
|
|
1487
|
+
case "real":
|
|
1488
|
+
return "REAL";
|
|
1489
|
+
case "numeric":
|
|
1490
|
+
return "NUMERIC";
|
|
1491
|
+
case "blob":
|
|
1492
|
+
return "BLOB";
|
|
1493
|
+
default:
|
|
1494
|
+
return "TEXT";
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
placeholder() {
|
|
1498
|
+
return "?";
|
|
1499
|
+
}
|
|
1500
|
+
ilike(column, param) {
|
|
1501
|
+
return `${column} LIKE ${param}`;
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* SQLite runs one writer at a time, so its only isolation level **is**
|
|
1505
|
+
* serializable — there is no syntax to ask for another, and no weaker level to
|
|
1506
|
+
* fall back to. Asking for one is an error rather than a silent no-op, since a
|
|
1507
|
+
* caller who wrote `repeatable read` was reasoning about a guarantee.
|
|
1508
|
+
*
|
|
1509
|
+
* `readOnly` likewise has no per-transaction form here (`PRAGMA query_only` is
|
|
1510
|
+
* per connection), so it is refused instead of quietly ignored.
|
|
1511
|
+
*/
|
|
1512
|
+
beginStatements(options) {
|
|
1513
|
+
if (options?.isolation && options.isolation !== "serializable") {
|
|
1514
|
+
throw new Error(
|
|
1515
|
+
`SQLite only implements the "serializable" isolation level; ${JSON.stringify(options.isolation)} has no equivalent here.`
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
if (options?.readOnly) {
|
|
1519
|
+
throw new Error(
|
|
1520
|
+
"SQLite has no read-only transaction; open the engine with { sqlite: { ... } } on a read-only connection instead."
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
return ["BEGIN"];
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* SQLite has no row-level locking, so a lock request is an error rather than a
|
|
1527
|
+
* silently unlocked `SELECT` — a lock that does not exist only shows up as
|
|
1528
|
+
* duplicated work under production concurrency.
|
|
1529
|
+
*/
|
|
1530
|
+
renderLock() {
|
|
1531
|
+
throw new Error(
|
|
1532
|
+
"SQLite has no row-level locking \u2014 FOR UPDATE / FOR SHARE is unsupported. Serialize the claim inside a transaction instead."
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1536
|
+
var PostgresDialect = class extends BaseDialect {
|
|
1537
|
+
name = "postgresql";
|
|
1538
|
+
placeholder(index) {
|
|
1539
|
+
return `$${index}`;
|
|
1540
|
+
}
|
|
1541
|
+
ilike(column, param) {
|
|
1542
|
+
return `${column} ILIKE ${param}`;
|
|
1543
|
+
}
|
|
1544
|
+
arrayOperator(op) {
|
|
1545
|
+
if (op === "contains") return "@>";
|
|
1546
|
+
if (op === "containedBy") return "<@";
|
|
1547
|
+
return "&&";
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
var mysqlQuotedIds = /* @__PURE__ */ new Map();
|
|
1551
|
+
var MysqlDialect = class extends BaseDialect {
|
|
1552
|
+
name = "mysql";
|
|
1553
|
+
placeholder() {
|
|
1554
|
+
return "?";
|
|
1555
|
+
}
|
|
1556
|
+
ilike(column, param) {
|
|
1557
|
+
return `${column} LIKE ${param}`;
|
|
1558
|
+
}
|
|
1559
|
+
/** MySQL's `EXPLAIN FORMAT=JSON` spells the option differently. */
|
|
1560
|
+
explainPrefix(analyze) {
|
|
1561
|
+
return analyze ? "EXPLAIN ANALYZE" : "EXPLAIN FORMAT=JSON";
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* MySQL only gained `INTERSECT`/`EXCEPT` in 8.0.31, and this project does not
|
|
1565
|
+
* invest in MySQL beyond what already works — so they are refused here rather
|
|
1566
|
+
* than emitted against a server that may reject them.
|
|
1567
|
+
*/
|
|
1568
|
+
setOperator(op) {
|
|
1569
|
+
if (op === "intersect" || op === "except") {
|
|
1570
|
+
throw new Error(
|
|
1571
|
+
`MySQL support for ${op.toUpperCase()} is out of scope for tempest-db-js; express it with a join or NOT EXISTS.`
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
return super.setOperator(op);
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* MySQL writes multi-table updates as `UPDATE a JOIN b`, and has no
|
|
1578
|
+
* `DELETE ... USING` in this shape. Both are out of the project's active scope,
|
|
1579
|
+
* so they are refused rather than emitted against a server that rejects them.
|
|
1580
|
+
*/
|
|
1581
|
+
compileExtraSources(keyword, sources) {
|
|
1582
|
+
if (sources && sources.length > 0) {
|
|
1583
|
+
throw new Error(
|
|
1584
|
+
`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.`
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
return "";
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* MySQL's full-text search needs a `FULLTEXT` index and different syntax, and it
|
|
1591
|
+
* is outside this project's active scope — the substring fallback is compiled,
|
|
1592
|
+
* like on SQLite.
|
|
1593
|
+
*/
|
|
1594
|
+
compileFullText(node, params, idFor) {
|
|
1595
|
+
return this.compileCondition(node.fallback, params, idFor);
|
|
1596
|
+
}
|
|
1597
|
+
/** No `ts_rank` equivalent in scope: a constant, so ordering by it is inert. */
|
|
1598
|
+
renderRank() {
|
|
1599
|
+
return "0";
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* MySQL sets the level with a statement **before** the transaction opens, and
|
|
1603
|
+
* spells the read-only flag on `START TRANSACTION` rather than on `BEGIN`.
|
|
1604
|
+
*/
|
|
1605
|
+
beginStatements(options) {
|
|
1606
|
+
const statements = [];
|
|
1607
|
+
if (options?.isolation) {
|
|
1608
|
+
statements.push(
|
|
1609
|
+
`SET TRANSACTION ISOLATION LEVEL ${options.isolation.toUpperCase()}`
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
statements.push(options?.readOnly ? "START TRANSACTION READ ONLY" : "BEGIN");
|
|
1613
|
+
return statements;
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* MySQL's `CAST` takes its own vocabulary — `SIGNED`, not `INTEGER`; `CHAR`,
|
|
1617
|
+
* not `TEXT` — and rejects the standard names outright.
|
|
1618
|
+
*/
|
|
1619
|
+
castTypeName(to) {
|
|
1620
|
+
switch (to) {
|
|
1621
|
+
case "integer":
|
|
1622
|
+
case "bigint":
|
|
1623
|
+
case "boolean":
|
|
1624
|
+
return "SIGNED";
|
|
1625
|
+
case "real":
|
|
1626
|
+
case "numeric":
|
|
1627
|
+
return "DECIMAL";
|
|
1628
|
+
case "text":
|
|
1629
|
+
case "uuid":
|
|
1630
|
+
return "CHAR";
|
|
1631
|
+
case "date":
|
|
1632
|
+
return "DATE";
|
|
1633
|
+
case "datetime":
|
|
1634
|
+
case "timestamp":
|
|
1635
|
+
return "DATETIME";
|
|
1636
|
+
case "json":
|
|
1637
|
+
case "jsonb":
|
|
1638
|
+
return "JSON";
|
|
1639
|
+
case "blob":
|
|
1640
|
+
return "BINARY";
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
quoteId(name) {
|
|
1644
|
+
const cached = mysqlQuotedIds.get(name);
|
|
1645
|
+
if (cached !== void 0) return cached;
|
|
1646
|
+
const quoted = `\`${name.replace(/`/g, "``")}\``;
|
|
1647
|
+
mysqlQuotedIds.set(name, quoted);
|
|
1648
|
+
return quoted;
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* MySQL rejects `LIMIT` inside an `IN` subquery with
|
|
1652
|
+
* `ER_NOT_SUPPORTED_YET: This version of MySQL doesn't yet support
|
|
1653
|
+
* 'LIMIT & IN/ALL/ANY/SOME subquery'`. Failing at compile time names the fix
|
|
1654
|
+
* instead of surfacing that error from the driver at runtime.
|
|
1655
|
+
*/
|
|
1656
|
+
checkSubquery(node) {
|
|
1657
|
+
if (node.limit !== void 0 || node.offset !== void 0) {
|
|
1658
|
+
throw new Error(
|
|
1659
|
+
"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."
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
renderConflict(onConflict, conflictCols, nextValue, names) {
|
|
1664
|
+
if (onConflict.targetWhere || onConflict.updateWhere) {
|
|
1665
|
+
throw new Error(
|
|
1666
|
+
"MySQL's ON DUPLICATE KEY UPDATE has no conflict-target predicate \u2014 a partial unique index is PostgreSQL/SQLite only."
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
if (onConflict.update === "nothing") {
|
|
1670
|
+
const col = this.columnId(onConflict.target[0] ?? "id", names);
|
|
1671
|
+
return ` ON DUPLICATE KEY UPDATE ${col} = ${col}`;
|
|
1672
|
+
}
|
|
1673
|
+
const assignments = conflictCols.map((c) => `${this.columnId(c, names)} = ${nextValue()}`).join(", ");
|
|
1674
|
+
return ` ON DUPLICATE KEY UPDATE ${assignments}`;
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* MySQL has no `RETURNING`, so it cannot be compiled into a statement.
|
|
1678
|
+
*
|
|
1679
|
+
* `session.execute()` still honors `.returning()` on a **single-row INSERT** by
|
|
1680
|
+
* running the insert and reading the row back by key on the same connection —
|
|
1681
|
+
* that is execution, not compilation, so it never reaches here. Compiling a
|
|
1682
|
+
* node with `returning` directly is an error, rather than SQL that silently
|
|
1683
|
+
* returns nothing.
|
|
1684
|
+
*/
|
|
1685
|
+
compileReturning(returning) {
|
|
1686
|
+
if (returning === null) return "";
|
|
1687
|
+
throw new Error(
|
|
1688
|
+
"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."
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
function getDialect(name) {
|
|
1693
|
+
switch (name) {
|
|
1694
|
+
case "sqlite":
|
|
1695
|
+
return new SqliteDialect();
|
|
1696
|
+
case "postgresql":
|
|
1697
|
+
return new PostgresDialect();
|
|
1698
|
+
case "mysql":
|
|
1699
|
+
return new MysqlDialect();
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
|
|
207
1703
|
// src/engine.ts
|
|
208
|
-
module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bin.cjs', document.baseURI).href)));
|
|
1704
|
+
var nodeRequire = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bin.cjs', document.baseURI).href)));
|
|
1705
|
+
function encodeSqliteParam(value) {
|
|
1706
|
+
if (value === void 0 || value === null) return null;
|
|
1707
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
1708
|
+
if (value instanceof Date) return value.toISOString();
|
|
1709
|
+
if (value instanceof Uint8Array) return value;
|
|
1710
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
1711
|
+
return value;
|
|
1712
|
+
}
|
|
1713
|
+
var NodeSqliteDriver = class _NodeSqliteDriver {
|
|
1714
|
+
// biome-ignore lint/suspicious/noExplicitAny: node:sqlite DatabaseSync has no shipped types here.
|
|
1715
|
+
db;
|
|
1716
|
+
/**
|
|
1717
|
+
* Prepared-statement cache keyed by SQL text. tempest-db-js always
|
|
1718
|
+
* parameterizes, so a query shape maps to one stable SQL string — reusing the
|
|
1719
|
+
* compiled statement avoids re-`prepare()` on every call (the dominant cost of
|
|
1720
|
+
* per-row inserts and point lookups).
|
|
1721
|
+
*/
|
|
1722
|
+
// biome-ignore lint/suspicious/noExplicitAny: node:sqlite StatementSync has no shipped types here.
|
|
1723
|
+
statements = /* @__PURE__ */ new Map();
|
|
1724
|
+
// biome-ignore lint/suspicious/noExplicitAny: accept an already-open DatabaseSync handle.
|
|
1725
|
+
constructor(database) {
|
|
1726
|
+
this.db = database;
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Open a `node:sqlite` database at the given path (or `:memory:`).
|
|
1730
|
+
*
|
|
1731
|
+
* @param path The database file, or `":memory:"`.
|
|
1732
|
+
* @param options Passed straight to `DatabaseSync` (`readOnly`, `timeout`, …).
|
|
1733
|
+
* @returns A driver over the open handle.
|
|
1734
|
+
*/
|
|
1735
|
+
static open(path, options) {
|
|
1736
|
+
const { DatabaseSync } = nodeRequire("node:sqlite");
|
|
1737
|
+
return new _NodeSqliteDriver(
|
|
1738
|
+
options ? new DatabaseSync(path, { ...options }) : new DatabaseSync(path)
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
/** Return the cached prepared statement for `sql`, preparing it on first use. */
|
|
1742
|
+
// biome-ignore lint/suspicious/noExplicitAny: statement type is unavailable here.
|
|
1743
|
+
prepare(sql) {
|
|
1744
|
+
const cached = this.statements.get(sql);
|
|
1745
|
+
if (cached) return cached;
|
|
1746
|
+
const stmt = this.db.prepare(sql);
|
|
1747
|
+
this.statements.set(sql, stmt);
|
|
1748
|
+
return stmt;
|
|
1749
|
+
}
|
|
1750
|
+
execute(sql, params) {
|
|
1751
|
+
const stmt = this.prepare(sql);
|
|
1752
|
+
const bound = params.map(encodeSqliteParam);
|
|
1753
|
+
if (returnsRows(sql)) {
|
|
1754
|
+
return { rows: stmt.all(...bound), changes: 0 };
|
|
1755
|
+
}
|
|
1756
|
+
const info = stmt.run(...bound);
|
|
1757
|
+
return { rows: [], changes: Number(info.changes ?? 0) };
|
|
1758
|
+
}
|
|
1759
|
+
*iterate(sql, params) {
|
|
1760
|
+
const stmt = this.prepare(sql);
|
|
1761
|
+
const bound = params.map(encodeSqliteParam);
|
|
1762
|
+
yield* stmt.iterate(...bound);
|
|
1763
|
+
}
|
|
1764
|
+
close() {
|
|
1765
|
+
this.statements.clear();
|
|
1766
|
+
this.db.close();
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
function returnsRows(sql) {
|
|
1770
|
+
return /^\s*(select|with|values|table|pragma|explain)\b/i.test(sql) || /\breturning\b/i.test(sql);
|
|
1771
|
+
}
|
|
209
1772
|
function toAsyncDriver(driver) {
|
|
210
1773
|
return {
|
|
211
1774
|
async execute(sql, params) {
|
|
@@ -217,6 +1780,104 @@ function toAsyncDriver(driver) {
|
|
|
217
1780
|
};
|
|
218
1781
|
}
|
|
219
1782
|
|
|
1783
|
+
// src/backup.ts
|
|
1784
|
+
var run = util.promisify(child_process.execFile);
|
|
1785
|
+
var BackupToolMissing = class extends Error {
|
|
1786
|
+
constructor(tool) {
|
|
1787
|
+
super(`${tool} is not on the PATH; install the PostgreSQL client tools to back up.`);
|
|
1788
|
+
this.name = "BackupToolMissing";
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
var UnsupportedBackupBackend = class extends Error {
|
|
1792
|
+
constructor(dialect) {
|
|
1793
|
+
super(
|
|
1794
|
+
`Backups are implemented for PostgreSQL and SQLite; ${dialect} is not covered.`
|
|
1795
|
+
);
|
|
1796
|
+
this.name = "UnsupportedBackupBackend";
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
function backupFormat(file) {
|
|
1800
|
+
return file.endsWith(".sql") ? "plain" : "custom";
|
|
1801
|
+
}
|
|
1802
|
+
function postgresEnv(password) {
|
|
1803
|
+
return password ? { ...process.env, PGPASSWORD: password } : { ...process.env };
|
|
1804
|
+
}
|
|
1805
|
+
function toolUrl(url) {
|
|
1806
|
+
return url.replace(/^([a-z0-9]+)\+[a-z0-9_-]+:/i, "$1:");
|
|
1807
|
+
}
|
|
1808
|
+
async function spawn(tool, args, env) {
|
|
1809
|
+
try {
|
|
1810
|
+
await run(tool, [...args], { env, maxBuffer: 1024 * 1024 * 64 });
|
|
1811
|
+
} catch (error) {
|
|
1812
|
+
if (error.code === "ENOENT") {
|
|
1813
|
+
throw new BackupToolMissing(tool);
|
|
1814
|
+
}
|
|
1815
|
+
throw error;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
async function backupDatabase(url, file, options) {
|
|
1819
|
+
const parsed = parseDatabaseUrl(url);
|
|
1820
|
+
if (parsed.dialect === "sqlite") {
|
|
1821
|
+
const source = parsed.database ?? ":memory:";
|
|
1822
|
+
const driver = NodeSqliteDriver.open(source);
|
|
1823
|
+
try {
|
|
1824
|
+
driver.execute("VACUUM INTO ?", [file]);
|
|
1825
|
+
} finally {
|
|
1826
|
+
driver.close();
|
|
1827
|
+
}
|
|
1828
|
+
return { file, dialect: "sqlite", via: "VACUUM INTO" };
|
|
1829
|
+
}
|
|
1830
|
+
if (parsed.dialect !== "postgresql") {
|
|
1831
|
+
throw new UnsupportedBackupBackend(parsed.dialect);
|
|
1832
|
+
}
|
|
1833
|
+
const format = backupFormat(file);
|
|
1834
|
+
await spawn(
|
|
1835
|
+
"pg_dump",
|
|
1836
|
+
[
|
|
1837
|
+
"--dbname",
|
|
1838
|
+
toolUrl(url),
|
|
1839
|
+
...format === "custom" ? ["--format=custom"] : [],
|
|
1840
|
+
"--file",
|
|
1841
|
+
file,
|
|
1842
|
+
...options?.extraArgs ?? []
|
|
1843
|
+
],
|
|
1844
|
+
postgresEnv(parsed.password)
|
|
1845
|
+
);
|
|
1846
|
+
return { file, dialect: "postgresql", via: "pg_dump" };
|
|
1847
|
+
}
|
|
1848
|
+
async function restoreDatabase(url, file, options) {
|
|
1849
|
+
const parsed = parseDatabaseUrl(url);
|
|
1850
|
+
if (parsed.dialect === "sqlite") {
|
|
1851
|
+
const target = parsed.database ?? ":memory:";
|
|
1852
|
+
await promises$1.copyFile(file, target);
|
|
1853
|
+
return { file, dialect: "sqlite", via: "copy" };
|
|
1854
|
+
}
|
|
1855
|
+
if (parsed.dialect !== "postgresql") {
|
|
1856
|
+
throw new UnsupportedBackupBackend(parsed.dialect);
|
|
1857
|
+
}
|
|
1858
|
+
const env = postgresEnv(parsed.password);
|
|
1859
|
+
if (backupFormat(file) === "plain") {
|
|
1860
|
+
await spawn(
|
|
1861
|
+
"psql",
|
|
1862
|
+
["--dbname", toolUrl(url), "--file", file, ...options?.extraArgs ?? []],
|
|
1863
|
+
env
|
|
1864
|
+
);
|
|
1865
|
+
return { file, dialect: "postgresql", via: "psql" };
|
|
1866
|
+
}
|
|
1867
|
+
await spawn(
|
|
1868
|
+
"pg_restore",
|
|
1869
|
+
[
|
|
1870
|
+
"--dbname",
|
|
1871
|
+
toolUrl(url),
|
|
1872
|
+
...options?.force ? ["--clean", "--if-exists"] : [],
|
|
1873
|
+
file,
|
|
1874
|
+
...options?.extraArgs ?? []
|
|
1875
|
+
],
|
|
1876
|
+
env
|
|
1877
|
+
);
|
|
1878
|
+
return { file, dialect: "postgresql", via: "pg_restore" };
|
|
1879
|
+
}
|
|
1880
|
+
|
|
220
1881
|
// src/migrations/operations.ts
|
|
221
1882
|
var IrreversibleMigration = class extends Error {
|
|
222
1883
|
constructor(message) {
|
|
@@ -250,6 +1911,10 @@ function invert(op) {
|
|
|
250
1911
|
return { kind: "recreate_table", from: op.to, to: op.from };
|
|
251
1912
|
case "add_constraint":
|
|
252
1913
|
return { kind: "drop_constraint", table: op.table, constraint: op.constraint };
|
|
1914
|
+
case "create_index":
|
|
1915
|
+
return { kind: "drop_index", table: op.table, index: op.index };
|
|
1916
|
+
case "drop_index":
|
|
1917
|
+
return { kind: "create_index", table: op.table, index: op.index };
|
|
253
1918
|
case "drop_constraint":
|
|
254
1919
|
return { kind: "add_constraint", table: op.table, constraint: op.constraint };
|
|
255
1920
|
case "execute":
|
|
@@ -440,7 +2105,8 @@ function renderDefault(def, dialect, type) {
|
|
|
440
2105
|
"sql.expr`...` binds parameters and cannot be rendered as a DEFAULT \u2014 use sql.raw()."
|
|
441
2106
|
);
|
|
442
2107
|
}
|
|
443
|
-
|
|
2108
|
+
const rendered = renderPortableToken(expr, dialect);
|
|
2109
|
+
return dialect === "sqlite" && rendered.includes("(") ? `(${rendered})` : rendered;
|
|
444
2110
|
}
|
|
445
2111
|
const value = def.value;
|
|
446
2112
|
if (value === null) return "NULL";
|
|
@@ -488,9 +2154,31 @@ function renderForeignKeyConstraint(fk, dialect) {
|
|
|
488
2154
|
function tableConstraintClauses(table, dialect) {
|
|
489
2155
|
return [
|
|
490
2156
|
...table.uniqueConstraints.map((uc) => renderUniqueConstraint(uc, dialect)),
|
|
491
|
-
...table.foreignKeys.map((fk) => renderForeignKeyConstraint(fk, dialect))
|
|
2157
|
+
...table.foreignKeys.map((fk) => renderForeignKeyConstraint(fk, dialect)),
|
|
2158
|
+
...table.checks.map((ck) => renderCheckConstraint(ck, dialect))
|
|
492
2159
|
];
|
|
493
2160
|
}
|
|
2161
|
+
function renderPredicate(node, dialect) {
|
|
2162
|
+
const literals = new LiteralParams();
|
|
2163
|
+
return getDialect(dialect).renderConditionLiteral(node, literals);
|
|
2164
|
+
}
|
|
2165
|
+
function renderCheckConstraint(ck, dialect) {
|
|
2166
|
+
return `CONSTRAINT ${quoteId(ck.name, dialect)} CHECK (${renderPredicate(ck.expression, dialect)})`;
|
|
2167
|
+
}
|
|
2168
|
+
function renderCreateIndex(table, ix, dialect) {
|
|
2169
|
+
if (ix.where && dialect === "mysql") {
|
|
2170
|
+
throw new Error(
|
|
2171
|
+
`MySQL has no partial index; ${ix.name} on ${table} declares a WHERE predicate.`
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2174
|
+
const unique = ix.unique ? "UNIQUE " : "";
|
|
2175
|
+
const cols = ix.columns.map((c) => quoteId(c, dialect)).join(", ");
|
|
2176
|
+
const where = ix.where ? ` WHERE ${renderPredicate(ix.where, dialect)}` : "";
|
|
2177
|
+
return `CREATE ${unique}INDEX ${quoteId(ix.name, dialect)} ON ${quoteId(table, dialect)} (${cols})${where}`;
|
|
2178
|
+
}
|
|
2179
|
+
function renderDropIndex(table, ix, dialect) {
|
|
2180
|
+
return dialect === "mysql" ? `DROP INDEX ${quoteId(ix.name, dialect)} ON ${quoteId(table, dialect)}` : `DROP INDEX ${quoteId(ix.name, dialect)}`;
|
|
2181
|
+
}
|
|
494
2182
|
function renderColumnDef(col, dialect) {
|
|
495
2183
|
let sql = `${quoteId(col.name, dialect)} ${renderColumnType(col.type, dialect)}`;
|
|
496
2184
|
if (col.notNull) sql += " NOT NULL";
|
|
@@ -543,7 +2231,10 @@ function renderCreateTable(table, dialect) {
|
|
|
543
2231
|
...typeStmts,
|
|
544
2232
|
`CREATE TABLE ${quoteId(table.name, dialect)} (
|
|
545
2233
|
${cols.join(",\n ")}
|
|
546
|
-
)
|
|
2234
|
+
)`,
|
|
2235
|
+
// Indexes are separate statements, not table clauses — which is also why a
|
|
2236
|
+
// SQLite table rebuild has to recreate them.
|
|
2237
|
+
...table.indexes.map((ix) => renderCreateIndex(table.name, ix, dialect))
|
|
547
2238
|
];
|
|
548
2239
|
}
|
|
549
2240
|
function renderOperation(op, dialect) {
|
|
@@ -576,6 +2267,10 @@ function renderOperation(op, dialect) {
|
|
|
576
2267
|
return renderAddConstraint(op.table, op.constraint, dialect);
|
|
577
2268
|
case "drop_constraint":
|
|
578
2269
|
return renderDropConstraint(op.table, op.constraint, dialect);
|
|
2270
|
+
case "create_index":
|
|
2271
|
+
return [renderCreateIndex(op.table, op.index, dialect)];
|
|
2272
|
+
case "drop_index":
|
|
2273
|
+
return [renderDropIndex(op.table, op.index, dialect)];
|
|
579
2274
|
case "execute":
|
|
580
2275
|
return [op.up];
|
|
581
2276
|
}
|
|
@@ -586,7 +2281,7 @@ function renderAddConstraint(table, constraint, dialect) {
|
|
|
586
2281
|
`add_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
|
|
587
2282
|
);
|
|
588
2283
|
}
|
|
589
|
-
const clause = constraint.type === "unique" ? renderUniqueConstraint(constraint.constraint, dialect) : renderForeignKeyConstraint(constraint.constraint, dialect);
|
|
2284
|
+
const clause = constraint.type === "unique" ? renderUniqueConstraint(constraint.constraint, dialect) : constraint.type === "check" ? renderCheckConstraint(constraint.constraint, dialect) : renderForeignKeyConstraint(constraint.constraint, dialect);
|
|
590
2285
|
return [`ALTER TABLE ${quoteId(table, dialect)} ADD ${clause}`];
|
|
591
2286
|
}
|
|
592
2287
|
function renderDropConstraint(table, constraint, dialect) {
|
|
@@ -623,6 +2318,10 @@ function renderSqliteRebuild(from, to) {
|
|
|
623
2318
|
common.length > 0 ? `INSERT INTO ${quoteId(tmp, "sqlite")} (${commonSql}) SELECT ${commonSql} FROM ${quoteId(from.name, "sqlite")}` : `-- no common columns to copy from ${from.name}`,
|
|
624
2319
|
`DROP TABLE ${quoteId(from.name, "sqlite")}`,
|
|
625
2320
|
`ALTER TABLE ${quoteId(tmp, "sqlite")} RENAME TO ${quoteId(to.name, "sqlite")}`,
|
|
2321
|
+
// The rebuild dropped the old table, and its indexes with it: SQLite ties an
|
|
2322
|
+
// index to the table it was created on. Recreating them is part of the
|
|
2323
|
+
// rebuild, not a follow-up somebody has to remember.
|
|
2324
|
+
...to.indexes.map((ix) => renderCreateIndex(to.name, ix, "sqlite")),
|
|
626
2325
|
"PRAGMA foreign_keys=on"
|
|
627
2326
|
];
|
|
628
2327
|
}
|
|
@@ -725,8 +2424,49 @@ function diffConstraints(current, target) {
|
|
|
725
2424
|
ops.push(addForeignKey(table, tgt));
|
|
726
2425
|
}
|
|
727
2426
|
}
|
|
2427
|
+
const currentCk = new Map(current.checks.map((c) => [c.name, c]));
|
|
2428
|
+
const targetCk = new Map(target.checks.map((c) => [c.name, c]));
|
|
2429
|
+
for (const [name, cur] of currentCk) {
|
|
2430
|
+
const tgt = targetCk.get(name);
|
|
2431
|
+
if (!tgt || checkSignature(cur) !== checkSignature(tgt)) {
|
|
2432
|
+
ops.push({ kind: "drop_constraint", table, constraint: checkNamed(cur) });
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
for (const [name, tgt] of targetCk) {
|
|
2436
|
+
const cur = currentCk.get(name);
|
|
2437
|
+
if (!cur || checkSignature(cur) !== checkSignature(tgt)) {
|
|
2438
|
+
ops.push({ kind: "add_constraint", table, constraint: checkNamed(tgt) });
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
const currentIx = new Map(current.indexes.map((i) => [i.name, i]));
|
|
2442
|
+
const targetIx = new Map(target.indexes.map((i) => [i.name, i]));
|
|
2443
|
+
for (const [name, cur] of currentIx) {
|
|
2444
|
+
const tgt = targetIx.get(name);
|
|
2445
|
+
if (!tgt || indexSignature(cur) !== indexSignature(tgt)) {
|
|
2446
|
+
ops.push({ kind: "drop_index", table, index: cur });
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
for (const [name, tgt] of targetIx) {
|
|
2450
|
+
const cur = currentIx.get(name);
|
|
2451
|
+
if (!cur || indexSignature(cur) !== indexSignature(tgt)) {
|
|
2452
|
+
ops.push({ kind: "create_index", table, index: tgt });
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
728
2455
|
return ops;
|
|
729
2456
|
}
|
|
2457
|
+
function checkSignature(ck) {
|
|
2458
|
+
return JSON.stringify(ck.expression);
|
|
2459
|
+
}
|
|
2460
|
+
function indexSignature(ix) {
|
|
2461
|
+
return JSON.stringify({
|
|
2462
|
+
columns: ix.columns,
|
|
2463
|
+
unique: ix.unique,
|
|
2464
|
+
where: ix.where
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
function checkNamed(ck) {
|
|
2468
|
+
return { type: "check", constraint: ck };
|
|
2469
|
+
}
|
|
730
2470
|
function uniqueNamed(uc) {
|
|
731
2471
|
return { type: "unique", constraint: uc };
|
|
732
2472
|
}
|
|
@@ -841,6 +2581,41 @@ function heads(migrations) {
|
|
|
841
2581
|
function constraintName(prefix, table, columns) {
|
|
842
2582
|
return `${prefix}_${table}_${columns.join("_")}`;
|
|
843
2583
|
}
|
|
2584
|
+
function renameConditionColumns(node, toColumn) {
|
|
2585
|
+
const expr = (current) => {
|
|
2586
|
+
switch (current.kind) {
|
|
2587
|
+
case "column":
|
|
2588
|
+
return { kind: "column", name: toColumn(current.name) };
|
|
2589
|
+
case "fn":
|
|
2590
|
+
return { ...current, args: current.args.map(expr) };
|
|
2591
|
+
case "cast":
|
|
2592
|
+
return { ...current, operand: expr(current.operand) };
|
|
2593
|
+
default:
|
|
2594
|
+
return current;
|
|
2595
|
+
}
|
|
2596
|
+
};
|
|
2597
|
+
switch (node.kind) {
|
|
2598
|
+
case "fields": {
|
|
2599
|
+
const fields = {};
|
|
2600
|
+
for (const [key, value] of Object.entries(node.fields)) {
|
|
2601
|
+
fields[toColumn(key)] = value;
|
|
2602
|
+
}
|
|
2603
|
+
return { kind: "fields", fields };
|
|
2604
|
+
}
|
|
2605
|
+
case "and":
|
|
2606
|
+
case "or":
|
|
2607
|
+
return {
|
|
2608
|
+
...node,
|
|
2609
|
+
parts: node.parts.map((part) => renameConditionColumns(part, toColumn))
|
|
2610
|
+
};
|
|
2611
|
+
case "not":
|
|
2612
|
+
return { kind: "not", part: renameConditionColumns(node.part, toColumn) };
|
|
2613
|
+
case "compare":
|
|
2614
|
+
return { ...node, left: expr(node.left), right: expr(node.right) };
|
|
2615
|
+
default:
|
|
2616
|
+
return node;
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
844
2619
|
function reflectTable(model) {
|
|
845
2620
|
const names = columnNamesOf(model);
|
|
846
2621
|
const toColumn = (prop) => names?.[prop] ?? prop;
|
|
@@ -862,6 +2637,8 @@ function reflectTable(model) {
|
|
|
862
2637
|
}
|
|
863
2638
|
const uniqueConstraints = [];
|
|
864
2639
|
const foreignKeys = [];
|
|
2640
|
+
const checks = [];
|
|
2641
|
+
const indexes = [];
|
|
865
2642
|
for (const c of model.tableArgs?.() ?? []) {
|
|
866
2643
|
const cols = c.columns.map(toColumn);
|
|
867
2644
|
if (c.kind === "unique") {
|
|
@@ -869,6 +2646,18 @@ function reflectTable(model) {
|
|
|
869
2646
|
name: c.name ?? constraintName("uq", model.tablename, cols),
|
|
870
2647
|
columns: cols
|
|
871
2648
|
});
|
|
2649
|
+
} else if (c.kind === "check") {
|
|
2650
|
+
checks.push({
|
|
2651
|
+
name: c.name ?? constraintName("ck", model.tablename, cols),
|
|
2652
|
+
expression: renameConditionColumns(c.expression, toColumn)
|
|
2653
|
+
});
|
|
2654
|
+
} else if (c.kind === "index") {
|
|
2655
|
+
indexes.push({
|
|
2656
|
+
name: c.name ?? constraintName("ix", model.tablename, cols),
|
|
2657
|
+
columns: cols,
|
|
2658
|
+
unique: c.unique === true,
|
|
2659
|
+
where: c.where ? renameConditionColumns(c.where, toColumn) : null
|
|
2660
|
+
});
|
|
872
2661
|
} else {
|
|
873
2662
|
foreignKeys.push({
|
|
874
2663
|
name: c.name ?? constraintName("fk", model.tablename, cols),
|
|
@@ -880,7 +2669,15 @@ function reflectTable(model) {
|
|
|
880
2669
|
});
|
|
881
2670
|
}
|
|
882
2671
|
}
|
|
883
|
-
return {
|
|
2672
|
+
return {
|
|
2673
|
+
name: model.tablename,
|
|
2674
|
+
columns,
|
|
2675
|
+
primaryKey,
|
|
2676
|
+
uniqueConstraints,
|
|
2677
|
+
foreignKeys,
|
|
2678
|
+
checks,
|
|
2679
|
+
indexes
|
|
2680
|
+
};
|
|
884
2681
|
}
|
|
885
2682
|
function reflectSchema(models) {
|
|
886
2683
|
const tables = {};
|
|
@@ -997,6 +2794,7 @@ function compareSqliteSchemas(actual, expected) {
|
|
|
997
2794
|
);
|
|
998
2795
|
}
|
|
999
2796
|
}
|
|
2797
|
+
issues.push(...indexDrift(tableName, actualTable, expectedTable));
|
|
1000
2798
|
}
|
|
1001
2799
|
for (const tableName of Object.keys(actual.tables)) {
|
|
1002
2800
|
if (!expected.tables[tableName]) {
|
|
@@ -1050,6 +2848,32 @@ function sqliteUniqueFromPragma(table, rows) {
|
|
|
1050
2848
|
function isUniqueIndex(idx) {
|
|
1051
2849
|
return Number(idx.unique) === 1 && idx.origin !== "pk";
|
|
1052
2850
|
}
|
|
2851
|
+
function indexDrift(tableName, actual, expected) {
|
|
2852
|
+
const issues = [];
|
|
2853
|
+
const signature = (ix) => `${ix.columns.join(",")}${ix.unique ? " unique" : ""}`;
|
|
2854
|
+
const actualByName = new Map(actual.indexes.map((ix) => [ix.name, ix]));
|
|
2855
|
+
const expectedByName = new Map(expected.indexes.map((ix) => [ix.name, ix]));
|
|
2856
|
+
for (const [name, exp] of expectedByName) {
|
|
2857
|
+
const act = actualByName.get(name);
|
|
2858
|
+
if (!act) {
|
|
2859
|
+
issues.push(`index "${tableName}.${name}" is missing from the database`);
|
|
2860
|
+
continue;
|
|
2861
|
+
}
|
|
2862
|
+
if (signature(act) !== signature(exp)) {
|
|
2863
|
+
issues.push(
|
|
2864
|
+
`index "${tableName}.${name}" differs: model (${signature(exp)}), db (${signature(act)})`
|
|
2865
|
+
);
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
for (const name of actualByName.keys()) {
|
|
2869
|
+
if (!expectedByName.has(name)) {
|
|
2870
|
+
issues.push(
|
|
2871
|
+
`index "${tableName}.${name}" exists in the database but not in the model`
|
|
2872
|
+
);
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
return issues;
|
|
2876
|
+
}
|
|
1053
2877
|
var SQLITE_TABLES_SQL = "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'";
|
|
1054
2878
|
async function introspectSqliteAsync(driver) {
|
|
1055
2879
|
const tablesRows = (await driver.execute(SQLITE_TABLES_SQL, [])).rows;
|
|
@@ -1067,12 +2891,27 @@ async function introspectSqliteAsync(driver) {
|
|
|
1067
2891
|
const unique = sqliteUniqueFromPragma(tableName, cols);
|
|
1068
2892
|
if (unique) uniqueConstraints.push(unique);
|
|
1069
2893
|
}
|
|
2894
|
+
const explicitIndexes = [];
|
|
2895
|
+
for (const idx of indexes) {
|
|
2896
|
+
if (idx.origin !== "c" || idx.partial === 1) continue;
|
|
2897
|
+
const cols = (await driver.execute(`PRAGMA index_info(${JSON.stringify(idx.name)})`, [])).rows;
|
|
2898
|
+
const sorted = [...cols].sort((a, b) => a.seqno - b.seqno).map((c) => c.name);
|
|
2899
|
+
if (sorted.length === 0) continue;
|
|
2900
|
+
explicitIndexes.push({
|
|
2901
|
+
name: idx.name,
|
|
2902
|
+
columns: sorted,
|
|
2903
|
+
unique: Number(idx.unique) === 1,
|
|
2904
|
+
where: null
|
|
2905
|
+
});
|
|
2906
|
+
}
|
|
1070
2907
|
tables[tableName] = {
|
|
1071
2908
|
name: tableName,
|
|
1072
2909
|
columns,
|
|
1073
2910
|
primaryKey,
|
|
1074
2911
|
uniqueConstraints,
|
|
1075
|
-
foreignKeys: sqliteForeignKeysFromPragma(tableName, fkRows)
|
|
2912
|
+
foreignKeys: sqliteForeignKeysFromPragma(tableName, fkRows),
|
|
2913
|
+
checks: [],
|
|
2914
|
+
indexes: explicitIndexes
|
|
1076
2915
|
};
|
|
1077
2916
|
}
|
|
1078
2917
|
return { tables };
|
|
@@ -1198,6 +3037,8 @@ async function introspectPostgres(driver) {
|
|
|
1198
3037
|
columns,
|
|
1199
3038
|
primaryKey,
|
|
1200
3039
|
uniqueConstraints: await postgresUniques(driver, tableName),
|
|
3040
|
+
checks: [],
|
|
3041
|
+
indexes: await postgresIndexes(driver, tableName),
|
|
1201
3042
|
foreignKeys: await postgresForeignKeys(driver, tableName)
|
|
1202
3043
|
};
|
|
1203
3044
|
}
|
|
@@ -1240,6 +3081,29 @@ async function postgresUniques(driver, table) {
|
|
|
1240
3081
|
columns: r.cols ?? []
|
|
1241
3082
|
}));
|
|
1242
3083
|
}
|
|
3084
|
+
async function postgresIndexes(driver, table) {
|
|
3085
|
+
const { rows } = await driver.execute(
|
|
3086
|
+
`SELECT i.relname AS name,
|
|
3087
|
+
ix.indisunique AS is_unique,
|
|
3088
|
+
ix.indpred IS NOT NULL AS is_partial,
|
|
3089
|
+
array_to_string(array_agg(a.attname ORDER BY k.ord), ',') AS columns
|
|
3090
|
+
FROM pg_index ix
|
|
3091
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
3092
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
3093
|
+
JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, ord) ON true
|
|
3094
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
3095
|
+
WHERE t.relname = $1
|
|
3096
|
+
AND NOT EXISTS (SELECT 1 FROM pg_constraint c WHERE c.conindid = ix.indexrelid)
|
|
3097
|
+
GROUP BY i.relname, ix.indisunique, ix.indpred`,
|
|
3098
|
+
[table]
|
|
3099
|
+
);
|
|
3100
|
+
return rows.filter((row) => !row.is_partial).map((row) => ({
|
|
3101
|
+
name: row.name,
|
|
3102
|
+
columns: row.columns.split(","),
|
|
3103
|
+
unique: row.is_unique === true,
|
|
3104
|
+
where: null
|
|
3105
|
+
}));
|
|
3106
|
+
}
|
|
1243
3107
|
function describeKind(type) {
|
|
1244
3108
|
if (type.kind !== "array") return type.kind;
|
|
1245
3109
|
return `${type.meta.element ? describeKind(type.meta.element) : "unknown"}[]`;
|
|
@@ -1290,6 +3154,7 @@ async function checkDriftPostgres(driver, models) {
|
|
|
1290
3154
|
);
|
|
1291
3155
|
}
|
|
1292
3156
|
}
|
|
3157
|
+
issues.push(...indexDrift(tableName, actualTable, expectedTable));
|
|
1293
3158
|
}
|
|
1294
3159
|
for (const tableName of Object.keys(actual.tables)) {
|
|
1295
3160
|
if (!expected.tables[tableName]) {
|
|
@@ -1615,7 +3480,22 @@ function applyOperation(schema, op) {
|
|
|
1615
3480
|
tables[op.table] = op.constraint.type === "unique" ? {
|
|
1616
3481
|
...t,
|
|
1617
3482
|
uniqueConstraints: [...t.uniqueConstraints, op.constraint.constraint]
|
|
1618
|
-
} : { ...t, foreignKeys: [...t.foreignKeys, op.constraint.constraint] };
|
|
3483
|
+
} : op.constraint.type === "check" ? { ...t, checks: [...t.checks, op.constraint.constraint] } : { ...t, foreignKeys: [...t.foreignKeys, op.constraint.constraint] };
|
|
3484
|
+
}
|
|
3485
|
+
break;
|
|
3486
|
+
}
|
|
3487
|
+
case "create_index": {
|
|
3488
|
+
const t = tables[op.table];
|
|
3489
|
+
if (t) tables[op.table] = { ...t, indexes: [...t.indexes, op.index] };
|
|
3490
|
+
break;
|
|
3491
|
+
}
|
|
3492
|
+
case "drop_index": {
|
|
3493
|
+
const t = tables[op.table];
|
|
3494
|
+
if (t) {
|
|
3495
|
+
tables[op.table] = {
|
|
3496
|
+
...t,
|
|
3497
|
+
indexes: t.indexes.filter((i) => i.name !== op.index.name)
|
|
3498
|
+
};
|
|
1619
3499
|
}
|
|
1620
3500
|
break;
|
|
1621
3501
|
}
|
|
@@ -1626,7 +3506,7 @@ function applyOperation(schema, op) {
|
|
|
1626
3506
|
tables[op.table] = op.constraint.type === "unique" ? {
|
|
1627
3507
|
...t,
|
|
1628
3508
|
uniqueConstraints: t.uniqueConstraints.filter((u) => u.name !== dropName)
|
|
1629
|
-
} : {
|
|
3509
|
+
} : op.constraint.type === "check" ? { ...t, checks: t.checks.filter((c) => c.name !== dropName) } : {
|
|
1630
3510
|
...t,
|
|
1631
3511
|
foreignKeys: t.foreignKeys.filter((f) => f.name !== dropName)
|
|
1632
3512
|
};
|
|
@@ -1831,8 +3711,37 @@ async function promptRenames(config, rest) {
|
|
|
1831
3711
|
}
|
|
1832
3712
|
return flags;
|
|
1833
3713
|
}
|
|
3714
|
+
async function runBackupCommand(argv) {
|
|
3715
|
+
const [command, file, ...rest] = argv;
|
|
3716
|
+
if (command !== "backup" && command !== "restore") return false;
|
|
3717
|
+
const urlFlag = rest.indexOf("--url");
|
|
3718
|
+
const url = (urlFlag >= 0 ? rest[urlFlag + 1] : void 0) ?? rest.find((arg) => arg.startsWith("--url="))?.slice("--url=".length) ?? process.env.DATABASE_URL;
|
|
3719
|
+
if (!file || !url) {
|
|
3720
|
+
process.stderr.write(
|
|
3721
|
+
`tempest-db: usage: tempest-db ${command} <file> --url <database-url>
|
|
3722
|
+
(or set DATABASE_URL)
|
|
3723
|
+
`
|
|
3724
|
+
);
|
|
3725
|
+
process.exitCode = 1;
|
|
3726
|
+
return true;
|
|
3727
|
+
}
|
|
3728
|
+
const force = rest.includes("--force");
|
|
3729
|
+
try {
|
|
3730
|
+
const result = command === "backup" ? await backupDatabase(url, file, { force }) : await restoreDatabase(url, file, { force });
|
|
3731
|
+
process.stdout.write(
|
|
3732
|
+
`${command === "backup" ? "backed up" : "restored"} ${result.dialect} via ${result.via}: ${result.file}
|
|
3733
|
+
`
|
|
3734
|
+
);
|
|
3735
|
+
} catch (error) {
|
|
3736
|
+
process.stderr.write(`tempest-db: ${error.message}
|
|
3737
|
+
`);
|
|
3738
|
+
process.exitCode = 1;
|
|
3739
|
+
}
|
|
3740
|
+
return true;
|
|
3741
|
+
}
|
|
1834
3742
|
async function main(argv) {
|
|
1835
3743
|
const { configPath, rest } = extractConfigFlag(argv);
|
|
3744
|
+
if (await runBackupCommand(rest)) return;
|
|
1836
3745
|
const resolved = resolveConfigPath(configPath);
|
|
1837
3746
|
if (!resolved) {
|
|
1838
3747
|
process.stderr.write(
|