tempest-db-js 0.5.0 → 0.6.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 +7 -3
- package/dist/bin.cjs +580 -329
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +3 -3
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-5QQMVTS5.js → chunk-G7O5DCCC.js} +478 -44
- package/dist/chunk-G7O5DCCC.js.map +1 -0
- package/dist/{chunk-EPMLFNFK.js → chunk-KOW3LSWP.js} +109 -59
- package/dist/chunk-KOW3LSWP.js.map +1 -0
- package/dist/index.cjs +482 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +368 -16
- package/dist/index.d.ts +368 -16
- package/dist/index.js +1 -1
- package/dist/migrations/index.cjs +122 -55
- package/dist/migrations/index.cjs.map +1 -1
- package/dist/migrations/index.d.cts +46 -3
- package/dist/migrations/index.d.ts +46 -3
- package/dist/migrations/index.js +2 -2
- package/package.json +6 -1
- package/dist/chunk-5QQMVTS5.js.map +0 -1
- package/dist/chunk-EPMLFNFK.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -14,6 +14,142 @@ function toCondNode(input) {
|
|
|
14
14
|
function wrap(node) {
|
|
15
15
|
return { [CONDITION]: true, node };
|
|
16
16
|
}
|
|
17
|
+
function toExprNode(operand) {
|
|
18
|
+
return isExpression(operand) ? operand.node : { kind: "value", value: operand };
|
|
19
|
+
}
|
|
20
|
+
function assertValueOperands(op, operands) {
|
|
21
|
+
if (operands.some(isExpression)) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`The "${op}" operator binds its operands, so it takes values, not expressions.`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function isExpression(value) {
|
|
28
|
+
return value instanceof Expression;
|
|
29
|
+
}
|
|
30
|
+
var Expression = class {
|
|
31
|
+
constructor(node) {
|
|
32
|
+
this.node = node;
|
|
33
|
+
}
|
|
34
|
+
node;
|
|
35
|
+
/** Compare this expression against another expression or a bound value. */
|
|
36
|
+
compare(op, operand) {
|
|
37
|
+
return wrap({ kind: "compare", left: this.node, op, right: toExprNode(operand) });
|
|
38
|
+
}
|
|
39
|
+
/** `=` (or `IS NULL` for a null value). */
|
|
40
|
+
eq(operand) {
|
|
41
|
+
return this.compare("eq", operand);
|
|
42
|
+
}
|
|
43
|
+
/** `<>` (or `IS NOT NULL` for a null value). */
|
|
44
|
+
ne(operand) {
|
|
45
|
+
return this.compare("ne", operand);
|
|
46
|
+
}
|
|
47
|
+
/** `>`. */
|
|
48
|
+
gt(operand) {
|
|
49
|
+
return this.compare("gt", operand);
|
|
50
|
+
}
|
|
51
|
+
/** `>=`. */
|
|
52
|
+
gte(operand) {
|
|
53
|
+
return this.compare("gte", operand);
|
|
54
|
+
}
|
|
55
|
+
/** `<`. */
|
|
56
|
+
lt(operand) {
|
|
57
|
+
return this.compare("lt", operand);
|
|
58
|
+
}
|
|
59
|
+
/** `<=`. */
|
|
60
|
+
lte(operand) {
|
|
61
|
+
return this.compare("lte", operand);
|
|
62
|
+
}
|
|
63
|
+
/** `LIKE` — `%` and `_` in the operand are wildcards. */
|
|
64
|
+
like(pattern) {
|
|
65
|
+
return this.compare("like", pattern);
|
|
66
|
+
}
|
|
67
|
+
/** `ILIKE` — case-insensitive **pattern** matching, wildcards included. */
|
|
68
|
+
ilike(pattern) {
|
|
69
|
+
return this.compare("ilike", pattern);
|
|
70
|
+
}
|
|
71
|
+
/** Case-insensitive equality (`lower(a) = lower(b)`), with no wildcards. */
|
|
72
|
+
ieq(operand) {
|
|
73
|
+
return this.compare("ieq", operand);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* `IN (...)` over a list of values.
|
|
77
|
+
*
|
|
78
|
+
* @param values The values to test against.
|
|
79
|
+
* @returns The condition.
|
|
80
|
+
* @throws Error When an entry is an {@link Expression} — a list operand is
|
|
81
|
+
* bound, so an expression there would be serialized as a parameter instead of
|
|
82
|
+
* rendered as SQL.
|
|
83
|
+
*/
|
|
84
|
+
in(values) {
|
|
85
|
+
assertValueOperands("in", values);
|
|
86
|
+
return this.compare("in", values);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* `NOT IN (...)` over a list of values.
|
|
90
|
+
*
|
|
91
|
+
* @param values The values to exclude.
|
|
92
|
+
* @returns The condition.
|
|
93
|
+
* @throws Error When an entry is an {@link Expression} (see {@link Expression.in}).
|
|
94
|
+
*/
|
|
95
|
+
notIn(values) {
|
|
96
|
+
assertValueOperands("notIn", values);
|
|
97
|
+
return this.compare("notIn", values);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* `BETWEEN lo AND hi` (inclusive).
|
|
101
|
+
*
|
|
102
|
+
* @param lo The lower bound.
|
|
103
|
+
* @param hi The upper bound.
|
|
104
|
+
* @returns The condition.
|
|
105
|
+
* @throws Error When a bound is an {@link Expression} (see {@link Expression.in}).
|
|
106
|
+
*/
|
|
107
|
+
between(lo, hi) {
|
|
108
|
+
assertValueOperands("between", [lo, hi]);
|
|
109
|
+
return this.compare("between", [lo, hi]);
|
|
110
|
+
}
|
|
111
|
+
/** `IS NULL` (true) / `IS NOT NULL` (false). */
|
|
112
|
+
isNull(value = true) {
|
|
113
|
+
return this.compare("isNull", value);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
function col(name) {
|
|
117
|
+
return new Expression({ kind: "column", name });
|
|
118
|
+
}
|
|
119
|
+
function val(value) {
|
|
120
|
+
return new Expression({ kind: "value", value });
|
|
121
|
+
}
|
|
122
|
+
var FUNCTION_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
123
|
+
function toArg(arg) {
|
|
124
|
+
return typeof arg === "string" ? { kind: "column", name: arg } : arg.node;
|
|
125
|
+
}
|
|
126
|
+
function call(name, ...args) {
|
|
127
|
+
if (!FUNCTION_NAME.test(name)) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`fn.call() takes a plain SQL function name; got ${JSON.stringify(name)}.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return new Expression({ kind: "fn", name, args: args.map(toArg) });
|
|
133
|
+
}
|
|
134
|
+
var fn = {
|
|
135
|
+
/** `lower(x)`. */
|
|
136
|
+
lower: (arg) => call("lower", arg),
|
|
137
|
+
/** `upper(x)`. */
|
|
138
|
+
upper: (arg) => call("upper", arg),
|
|
139
|
+
/** `trim(x)`. */
|
|
140
|
+
trim: (arg) => call("trim", arg),
|
|
141
|
+
/** `length(x)`. */
|
|
142
|
+
length: (arg) => call("length", arg),
|
|
143
|
+
/** `abs(x)`. */
|
|
144
|
+
abs: (arg) => call("abs", arg),
|
|
145
|
+
/** `coalesce(a, b, ...)`. */
|
|
146
|
+
coalesce: (...args) => call("coalesce", ...args),
|
|
147
|
+
/**
|
|
148
|
+
* Any other SQL function, by name. Portability is the caller's problem —
|
|
149
|
+
* `date_trunc` is PostgreSQL, `strftime` is SQLite.
|
|
150
|
+
*/
|
|
151
|
+
call
|
|
152
|
+
};
|
|
17
153
|
function and(...inputs) {
|
|
18
154
|
return wrap({
|
|
19
155
|
kind: "and",
|
|
@@ -31,6 +167,9 @@ function not(input) {
|
|
|
31
167
|
}
|
|
32
168
|
|
|
33
169
|
// src/query.ts
|
|
170
|
+
function isSubquery(value) {
|
|
171
|
+
return typeof value === "object" && value !== null && value.node?.kind === "select";
|
|
172
|
+
}
|
|
34
173
|
var OPERATORS = [
|
|
35
174
|
"eq",
|
|
36
175
|
"ne",
|
|
@@ -50,8 +189,8 @@ var OPERATORS = [
|
|
|
50
189
|
"overlaps"
|
|
51
190
|
];
|
|
52
191
|
var Agg = class {
|
|
53
|
-
constructor(
|
|
54
|
-
this.fn =
|
|
192
|
+
constructor(fn2, column2) {
|
|
193
|
+
this.fn = fn2;
|
|
55
194
|
this.column = column2;
|
|
56
195
|
}
|
|
57
196
|
fn;
|
|
@@ -80,12 +219,41 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
80
219
|
node;
|
|
81
220
|
source;
|
|
82
221
|
with(patch) {
|
|
83
|
-
return new _SelectBuilder(
|
|
222
|
+
return new _SelectBuilder(
|
|
223
|
+
{ ...this.node, ...patch },
|
|
224
|
+
this.source
|
|
225
|
+
);
|
|
84
226
|
}
|
|
85
227
|
/** Add a WHERE filter: the object form (keys typed) or an `and`/`or`/`not`. */
|
|
86
228
|
where(input) {
|
|
87
229
|
return this.with({ where: toCondNode(input) });
|
|
88
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Filter by the result of the aggregation (`HAVING`).
|
|
233
|
+
*
|
|
234
|
+
* Only available on a grouped builder — `.having()` before `.aggregate()` is a
|
|
235
|
+
* compile error, not invalid SQL at runtime. Keys are the aggregate aliases you
|
|
236
|
+
* named plus the grouped columns; `WHERE` still filters rows *before* grouping,
|
|
237
|
+
* which is a different question.
|
|
238
|
+
*
|
|
239
|
+
* @param input The condition, keyed by alias or grouped column.
|
|
240
|
+
* @returns A builder carrying the `HAVING` clause.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```ts
|
|
244
|
+
* select(Outbound)
|
|
245
|
+
* .where({ status: "queued" })
|
|
246
|
+
* .aggregate(["consumer"], { n: count() })
|
|
247
|
+
* .having({ n: { gt: 100 } });
|
|
248
|
+
* // ... GROUP BY "consumer" HAVING COUNT(*) > $2
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
having(input) {
|
|
252
|
+
return new _SelectBuilder(
|
|
253
|
+
{ ...this.node, having: toCondNode(input) },
|
|
254
|
+
this.source
|
|
255
|
+
);
|
|
256
|
+
}
|
|
89
257
|
/** Emit `SELECT DISTINCT` — drop duplicate rows. */
|
|
90
258
|
distinct() {
|
|
91
259
|
return this.with({ distinct: true });
|
|
@@ -117,7 +285,15 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
117
285
|
this.source
|
|
118
286
|
);
|
|
119
287
|
}
|
|
120
|
-
/**
|
|
288
|
+
/**
|
|
289
|
+
* Order by a column of the model, or — on a grouped query — by an aggregate
|
|
290
|
+
* alias. Unlike `HAVING`, every dialect accepts the output alias in `ORDER BY`,
|
|
291
|
+
* so the alias is emitted as written.
|
|
292
|
+
*
|
|
293
|
+
* @param column A model column, or a projected alias.
|
|
294
|
+
* @param direction `"asc"` (default) or `"desc"`.
|
|
295
|
+
* @returns A builder carrying the ordering term.
|
|
296
|
+
*/
|
|
121
297
|
orderBy(column2, direction = "asc") {
|
|
122
298
|
return this.with({
|
|
123
299
|
orderBy: [...this.node.orderBy, { column: column2, direction }]
|
|
@@ -131,6 +307,37 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
131
307
|
offset(n) {
|
|
132
308
|
return this.with({ offset: n });
|
|
133
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Narrow this SELECT to a single column and mark it as a subquery, so it can be
|
|
312
|
+
* the operand of `in` / `notIn`.
|
|
313
|
+
*
|
|
314
|
+
* The whole query — `where`, `orderBy`, `limit`, and a locking clause — is
|
|
315
|
+
* embedded in the outer statement, which is what collapses the claim-a-batch
|
|
316
|
+
* pattern into one round trip instead of selecting ids and sending them back.
|
|
317
|
+
*
|
|
318
|
+
* @param column The single column to project (checked against the model).
|
|
319
|
+
* @returns A subquery carrying that column's type.
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```ts
|
|
323
|
+
* update(Outbound)
|
|
324
|
+
* .set({ status: "sending", attempts: sql.raw("attempts + 1") })
|
|
325
|
+
* .where({
|
|
326
|
+
* id: {
|
|
327
|
+
* in: select(Outbound)
|
|
328
|
+
* .where({ status: "queued" })
|
|
329
|
+
* .orderBy("nextAttemptAt")
|
|
330
|
+
* .limit(10)
|
|
331
|
+
* .forUpdate({ skipLocked: true })
|
|
332
|
+
* .asSubquery("id"),
|
|
333
|
+
* },
|
|
334
|
+
* })
|
|
335
|
+
* .returning();
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
asSubquery(column2) {
|
|
339
|
+
return { node: { ...this.node, columns: [column2] } };
|
|
340
|
+
}
|
|
134
341
|
/**
|
|
135
342
|
* Lock the selected rows for update (`SELECT ... FOR UPDATE`), à la
|
|
136
343
|
* SQLAlchemy's `with_for_update()`.
|
|
@@ -284,8 +491,8 @@ function toDict(model, row) {
|
|
|
284
491
|
function toJSON(model, row) {
|
|
285
492
|
const columns = columnsOf(model);
|
|
286
493
|
const out = {};
|
|
287
|
-
for (const [name,
|
|
288
|
-
out[name] = encodeValue(
|
|
494
|
+
for (const [name, col2] of Object.entries(columns)) {
|
|
495
|
+
out[name] = encodeValue(col2, row[name] ?? null);
|
|
289
496
|
}
|
|
290
497
|
return out;
|
|
291
498
|
}
|
|
@@ -296,10 +503,10 @@ function fromDict(model, data) {
|
|
|
296
503
|
const columns = columnsOf(model);
|
|
297
504
|
const out = {};
|
|
298
505
|
const issues = [];
|
|
299
|
-
for (const [name,
|
|
506
|
+
for (const [name, col2] of Object.entries(columns)) {
|
|
300
507
|
const present = name in data && data[name] !== void 0 && data[name] !== null;
|
|
301
508
|
if (!present) {
|
|
302
|
-
const required =
|
|
509
|
+
const required = col2.flags.notNull && !col2.flags.hasDefault;
|
|
303
510
|
if (required) {
|
|
304
511
|
issues.push(`missing required column "${name}"`);
|
|
305
512
|
continue;
|
|
@@ -308,7 +515,7 @@ function fromDict(model, data) {
|
|
|
308
515
|
continue;
|
|
309
516
|
}
|
|
310
517
|
try {
|
|
311
|
-
out[name] = decodeValue(
|
|
518
|
+
out[name] = decodeValue(col2, data[name]);
|
|
312
519
|
} catch (error) {
|
|
313
520
|
issues.push(`column "${name}": ${error.message}`);
|
|
314
521
|
}
|
|
@@ -359,8 +566,8 @@ function mapperFor(model) {
|
|
|
359
566
|
const props = columnPropsOf(model);
|
|
360
567
|
const names = props ? Object.fromEntries(Object.entries(props).map(([db, prop]) => [prop, db])) : null;
|
|
361
568
|
const decoders = /* @__PURE__ */ new Map();
|
|
362
|
-
for (const [prop,
|
|
363
|
-
const decoder = decoderFor(
|
|
569
|
+
for (const [prop, col2] of Object.entries(columnsOf(model))) {
|
|
570
|
+
const decoder = decoderFor(col2.type);
|
|
364
571
|
if (decoder) decoders.set(names?.[prop] ?? prop, decoder);
|
|
365
572
|
}
|
|
366
573
|
const mapper = { props, decoders };
|
|
@@ -391,15 +598,15 @@ function assertWritableValues(model, values, clause) {
|
|
|
391
598
|
const columns = columnsOf(model);
|
|
392
599
|
const issues = [];
|
|
393
600
|
for (const [key, value] of Object.entries(values)) {
|
|
394
|
-
const
|
|
395
|
-
if (!
|
|
601
|
+
const col2 = columns[key];
|
|
602
|
+
if (!col2) {
|
|
396
603
|
issues.push(`${clause}: "${key}" is not a column of ${model.tablename}`);
|
|
397
604
|
continue;
|
|
398
605
|
}
|
|
399
606
|
if (isSqlExpression(value) || isBindableScalar(value)) continue;
|
|
400
|
-
if (typeof value === "object" && STRUCTURED_KINDS.has(
|
|
607
|
+
if (typeof value === "object" && STRUCTURED_KINDS.has(col2.type.kind)) continue;
|
|
401
608
|
issues.push(
|
|
402
|
-
`${clause}: "${key}" got ${describeValue(value)}, which cannot be bound to a ${
|
|
609
|
+
`${clause}: "${key}" got ${describeValue(value)}, which cannot be bound to a ${col2.type.kind} column \u2014 use sql.raw()/sql.expr\`...\` for a SQL expression`
|
|
403
610
|
);
|
|
404
611
|
}
|
|
405
612
|
if (issues.length > 0) throw new ValidationError(model.tablename, issues);
|
|
@@ -755,6 +962,15 @@ var BaseDialect = class _BaseDialect {
|
|
|
755
962
|
static insertTemplates = /* @__PURE__ */ new Map();
|
|
756
963
|
/** Quoted-identifier cache (see {@link quoteId}). Shared across dialects. */
|
|
757
964
|
static quotedIds = /* @__PURE__ */ new Map();
|
|
965
|
+
/**
|
|
966
|
+
* Validate a subquery operand before it is rendered, for dialects that restrict
|
|
967
|
+
* what an `IN (SELECT ...)` may contain. The default accepts everything.
|
|
968
|
+
*
|
|
969
|
+
* @param _node The subquery's AST.
|
|
970
|
+
* @throws Error When the dialect cannot execute this subquery.
|
|
971
|
+
*/
|
|
972
|
+
checkSubquery(_node) {
|
|
973
|
+
}
|
|
758
974
|
/**
|
|
759
975
|
* The SQL operator for an array containment/overlap test.
|
|
760
976
|
*
|
|
@@ -878,6 +1094,19 @@ var BaseDialect = class _BaseDialect {
|
|
|
878
1094
|
return ` ${strength}${of}${wait}`;
|
|
879
1095
|
}
|
|
880
1096
|
// ---- statements -------------------------------------------------------
|
|
1097
|
+
/**
|
|
1098
|
+
* Compile a SELECT.
|
|
1099
|
+
*
|
|
1100
|
+
* Two alias rules differ between clauses and are handled here: PostgreSQL does
|
|
1101
|
+
* NOT accept a `SELECT` alias in `HAVING`, so an aggregate key is re-emitted as
|
|
1102
|
+
* its expression (`COUNT(*) > $1`), a form every dialect accepts; `ORDER BY`,
|
|
1103
|
+
* by contrast, accepts the output alias everywhere, so it is emitted as
|
|
1104
|
+
* written.
|
|
1105
|
+
*
|
|
1106
|
+
* @param node The select AST.
|
|
1107
|
+
* @param params The parameter collector.
|
|
1108
|
+
* @returns The SQL text.
|
|
1109
|
+
*/
|
|
881
1110
|
compileSelect(node, params) {
|
|
882
1111
|
const names = node.names;
|
|
883
1112
|
let cols;
|
|
@@ -901,10 +1130,21 @@ var BaseDialect = class _BaseDialect {
|
|
|
901
1130
|
if (node.groupBy.length > 0) {
|
|
902
1131
|
sql2 += ` GROUP BY ${node.groupBy.map((c) => this.columnId(c, names)).join(", ")}`;
|
|
903
1132
|
}
|
|
1133
|
+
const aggByAlias = new Map(node.aggregates.map((a) => [a.alias, a]));
|
|
1134
|
+
if (node.having) {
|
|
1135
|
+
const having = this.compileCondition(node.having, params, (key) => {
|
|
1136
|
+
const agg = aggByAlias.get(key);
|
|
1137
|
+
if (!agg) return this.columnId(key, names);
|
|
1138
|
+
const inner = agg.column === "*" ? "*" : this.columnId(agg.column, names);
|
|
1139
|
+
return `${agg.fn.toUpperCase()}(${inner})`;
|
|
1140
|
+
});
|
|
1141
|
+
if (having) sql2 += ` HAVING ${having}`;
|
|
1142
|
+
}
|
|
904
1143
|
if (node.orderBy.length > 0) {
|
|
905
|
-
const terms = node.orderBy.map(
|
|
906
|
-
(t)
|
|
907
|
-
|
|
1144
|
+
const terms = node.orderBy.map((t) => {
|
|
1145
|
+
const id = aggByAlias.has(t.column) ? this.quoteId(t.column) : this.columnId(t.column, names);
|
|
1146
|
+
return `${id} ${t.direction === "desc" ? "DESC" : "ASC"}`;
|
|
1147
|
+
}).join(", ");
|
|
908
1148
|
sql2 += ` ORDER BY ${terms}`;
|
|
909
1149
|
}
|
|
910
1150
|
if (node.limit !== void 0) sql2 += ` LIMIT ${params.bind(node.limit)}`;
|
|
@@ -1040,7 +1280,7 @@ var BaseDialect = class _BaseDialect {
|
|
|
1040
1280
|
compileUpdate(node, params) {
|
|
1041
1281
|
const names = node.names;
|
|
1042
1282
|
const sets = Object.entries(node.set).map(
|
|
1043
|
-
([
|
|
1283
|
+
([col2, value]) => `${this.columnId(col2, names)} = ${this.renderValue(value, params)}`
|
|
1044
1284
|
).join(", ");
|
|
1045
1285
|
let sql2 = `UPDATE ${this.quoteId(node.table)} SET ${sets}`;
|
|
1046
1286
|
const where = this.compileCondition(
|
|
@@ -1133,6 +1373,83 @@ var BaseDialect = class _BaseDialect {
|
|
|
1133
1373
|
const inner = this.compileCondition(node.part, params, idFor);
|
|
1134
1374
|
return inner ? `NOT (${inner})` : "";
|
|
1135
1375
|
}
|
|
1376
|
+
case "compare": {
|
|
1377
|
+
const left = this.renderExpr(node.left, params, idFor);
|
|
1378
|
+
if (node.right.kind === "value") {
|
|
1379
|
+
return this.compileOperator(left, node.op, node.right.value, params);
|
|
1380
|
+
}
|
|
1381
|
+
return this.compileExprOperator(
|
|
1382
|
+
left,
|
|
1383
|
+
node.op,
|
|
1384
|
+
this.renderExpr(node.right, params, idFor)
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Render one side of a comparison.
|
|
1391
|
+
*
|
|
1392
|
+
* A column reference goes through `idFor`, so an explicit `.name()` mapping and
|
|
1393
|
+
* join qualification apply here exactly as they do in the object form of
|
|
1394
|
+
* `where` — `col()` is not a way around them. Only a `value` node binds.
|
|
1395
|
+
*
|
|
1396
|
+
* @param node The expression AST.
|
|
1397
|
+
* @param params The parameter collector.
|
|
1398
|
+
* @param idFor The identifier resolver for the enclosing statement.
|
|
1399
|
+
* @returns The SQL text of the expression.
|
|
1400
|
+
*/
|
|
1401
|
+
renderExpr(node, params, idFor) {
|
|
1402
|
+
switch (node.kind) {
|
|
1403
|
+
case "column":
|
|
1404
|
+
return idFor(node.name);
|
|
1405
|
+
case "value":
|
|
1406
|
+
return params.bind(node.value);
|
|
1407
|
+
case "fn": {
|
|
1408
|
+
const args = node.args.map((a) => this.renderExpr(a, params, idFor)).join(", ");
|
|
1409
|
+
return `${node.name}(${args})`;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Compile a comparison whose right-hand side is another expression rather than
|
|
1415
|
+
* a bound value (`total > paid`, `lower(a) = lower(b)`).
|
|
1416
|
+
*
|
|
1417
|
+
* The list and null operators are excluded: `IN`, `BETWEEN` and `IS NULL` take
|
|
1418
|
+
* a value operand, and accepting an expression there would silently compile to
|
|
1419
|
+
* something else.
|
|
1420
|
+
*
|
|
1421
|
+
* @param left The rendered left-hand side.
|
|
1422
|
+
* @param op The operator name.
|
|
1423
|
+
* @param right The rendered right-hand side.
|
|
1424
|
+
* @returns The SQL text of the predicate.
|
|
1425
|
+
* @throws Error When the operator needs a value operand.
|
|
1426
|
+
*/
|
|
1427
|
+
compileExprOperator(left, op, right) {
|
|
1428
|
+
switch (op) {
|
|
1429
|
+
case "eq":
|
|
1430
|
+
return `${left} = ${right}`;
|
|
1431
|
+
case "ne":
|
|
1432
|
+
return `${left} <> ${right}`;
|
|
1433
|
+
case "gt":
|
|
1434
|
+
return `${left} > ${right}`;
|
|
1435
|
+
case "gte":
|
|
1436
|
+
return `${left} >= ${right}`;
|
|
1437
|
+
case "lt":
|
|
1438
|
+
return `${left} < ${right}`;
|
|
1439
|
+
case "lte":
|
|
1440
|
+
return `${left} <= ${right}`;
|
|
1441
|
+
case "like":
|
|
1442
|
+
return `${left} LIKE ${right}`;
|
|
1443
|
+
case "ilike":
|
|
1444
|
+
return this.ilike(left, right);
|
|
1445
|
+
case "ieq":
|
|
1446
|
+
return `lower(${left}) = lower(${right})`;
|
|
1447
|
+
case "contains":
|
|
1448
|
+
case "containedBy":
|
|
1449
|
+
case "overlaps":
|
|
1450
|
+
return `${left} ${this.arrayOperator(op)} ${right}`;
|
|
1451
|
+
default:
|
|
1452
|
+
throw new Error(`The "${op}" operator takes a value operand, not an expression.`);
|
|
1136
1453
|
}
|
|
1137
1454
|
}
|
|
1138
1455
|
compileOperator(id, op, operand, params) {
|
|
@@ -1175,12 +1492,33 @@ var BaseDialect = class _BaseDialect {
|
|
|
1175
1492
|
throw new Error(`Unknown operator ${JSON.stringify(op)}`);
|
|
1176
1493
|
}
|
|
1177
1494
|
}
|
|
1178
|
-
|
|
1495
|
+
/**
|
|
1496
|
+
* Compile `IN` / `NOT IN`, whose operand is either a value list or a
|
|
1497
|
+
* single-column subquery.
|
|
1498
|
+
*
|
|
1499
|
+
* The subquery is rendered at the position it appears in the outer statement
|
|
1500
|
+
* and shares the same parameter collector, so its own placeholders land in the
|
|
1501
|
+
* right order — and it keeps its own `names` map, since the inner model may use
|
|
1502
|
+
* a different naming convention than the outer one.
|
|
1503
|
+
*
|
|
1504
|
+
* @param id The quoted column identifier being tested.
|
|
1505
|
+
* @param operand A list of values, or a {@link Subquery}.
|
|
1506
|
+
* @param params The parameter collector for the statement being compiled.
|
|
1507
|
+
* @param negate True for `NOT IN`.
|
|
1508
|
+
* @returns The SQL text of the predicate.
|
|
1509
|
+
*/
|
|
1510
|
+
compileIn(id, operand, params, negate) {
|
|
1511
|
+
const keyword = negate ? "NOT IN" : "IN";
|
|
1512
|
+
if (isSubquery(operand)) {
|
|
1513
|
+
this.checkSubquery(operand.node);
|
|
1514
|
+
return `${id} ${keyword} (${this.compileSelect(operand.node, params)})`;
|
|
1515
|
+
}
|
|
1516
|
+
const values = operand;
|
|
1179
1517
|
if (values.length === 0) {
|
|
1180
1518
|
return negate ? "1 = 1" : "1 = 0";
|
|
1181
1519
|
}
|
|
1182
1520
|
const list = values.map((v) => params.bind(v)).join(", ");
|
|
1183
|
-
return `${id} ${
|
|
1521
|
+
return `${id} ${keyword} (${list})`;
|
|
1184
1522
|
}
|
|
1185
1523
|
};
|
|
1186
1524
|
var SqliteDialect = class extends BaseDialect {
|
|
@@ -1232,6 +1570,19 @@ var MysqlDialect = class extends BaseDialect {
|
|
|
1232
1570
|
mysqlQuotedIds.set(name, quoted);
|
|
1233
1571
|
return quoted;
|
|
1234
1572
|
}
|
|
1573
|
+
/**
|
|
1574
|
+
* MySQL rejects `LIMIT` inside an `IN` subquery with
|
|
1575
|
+
* `ER_NOT_SUPPORTED_YET: This version of MySQL doesn't yet support
|
|
1576
|
+
* 'LIMIT & IN/ALL/ANY/SOME subquery'`. Failing at compile time names the fix
|
|
1577
|
+
* instead of surfacing that error from the driver at runtime.
|
|
1578
|
+
*/
|
|
1579
|
+
checkSubquery(node) {
|
|
1580
|
+
if (node.limit !== void 0 || node.offset !== void 0) {
|
|
1581
|
+
throw new Error(
|
|
1582
|
+
"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."
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1235
1586
|
renderConflict(onConflict, conflictCols, nextValue, names) {
|
|
1236
1587
|
if (onConflict.targetWhere || onConflict.updateWhere) {
|
|
1237
1588
|
throw new Error(
|
|
@@ -1239,16 +1590,25 @@ var MysqlDialect = class extends BaseDialect {
|
|
|
1239
1590
|
);
|
|
1240
1591
|
}
|
|
1241
1592
|
if (onConflict.update === "nothing") {
|
|
1242
|
-
const
|
|
1243
|
-
return ` ON DUPLICATE KEY UPDATE ${
|
|
1593
|
+
const col2 = this.columnId(onConflict.target[0] ?? "id", names);
|
|
1594
|
+
return ` ON DUPLICATE KEY UPDATE ${col2} = ${col2}`;
|
|
1244
1595
|
}
|
|
1245
1596
|
const assignments = conflictCols.map((c) => `${this.columnId(c, names)} = ${nextValue()}`).join(", ");
|
|
1246
1597
|
return ` ON DUPLICATE KEY UPDATE ${assignments}`;
|
|
1247
1598
|
}
|
|
1599
|
+
/**
|
|
1600
|
+
* MySQL has no `RETURNING`, so it cannot be compiled into a statement.
|
|
1601
|
+
*
|
|
1602
|
+
* `session.execute()` still honors `.returning()` on a **single-row INSERT** by
|
|
1603
|
+
* running the insert and reading the row back by key on the same connection —
|
|
1604
|
+
* that is execution, not compilation, so it never reaches here. Compiling a
|
|
1605
|
+
* node with `returning` directly is an error, rather than SQL that silently
|
|
1606
|
+
* returns nothing.
|
|
1607
|
+
*/
|
|
1248
1608
|
compileReturning(returning) {
|
|
1249
1609
|
if (returning === null) return "";
|
|
1250
1610
|
throw new Error(
|
|
1251
|
-
"RETURNING
|
|
1611
|
+
"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."
|
|
1252
1612
|
);
|
|
1253
1613
|
}
|
|
1254
1614
|
};
|
|
@@ -1361,8 +1721,8 @@ var RecordNotFound = class extends Error {
|
|
|
1361
1721
|
}
|
|
1362
1722
|
};
|
|
1363
1723
|
function primaryKeyOf(model) {
|
|
1364
|
-
for (const [name,
|
|
1365
|
-
if (
|
|
1724
|
+
for (const [name, col2] of Object.entries(columnsOf(model))) {
|
|
1725
|
+
if (col2.flags.primaryKey) return name;
|
|
1366
1726
|
}
|
|
1367
1727
|
throw new Error(`${model.tablename} has no primary key`);
|
|
1368
1728
|
}
|
|
@@ -1450,8 +1810,8 @@ var BaseRepository = class {
|
|
|
1450
1810
|
|
|
1451
1811
|
// src/active-record.ts
|
|
1452
1812
|
function primaryKeyOf2(model) {
|
|
1453
|
-
for (const [name,
|
|
1454
|
-
if (
|
|
1813
|
+
for (const [name, col2] of Object.entries(columnsOf(model))) {
|
|
1814
|
+
if (col2.flags.primaryKey) return name;
|
|
1455
1815
|
}
|
|
1456
1816
|
throw new Error(`${model.tablename} has no primary key`);
|
|
1457
1817
|
}
|
|
@@ -1787,6 +2147,18 @@ var AsyncResult = class {
|
|
|
1787
2147
|
}
|
|
1788
2148
|
};
|
|
1789
2149
|
var savepointCounter = 0;
|
|
2150
|
+
function needsInsertReadBack(dialect, node) {
|
|
2151
|
+
return dialect.name === "mysql" && node.kind === "insert" && node.returning !== null;
|
|
2152
|
+
}
|
|
2153
|
+
function singlePrimaryKey(model) {
|
|
2154
|
+
const keys = Object.entries(columnsOf(model)).filter(([, column2]) => column2.flags.primaryKey).map(([name]) => name);
|
|
2155
|
+
if (keys.length !== 1) {
|
|
2156
|
+
throw new Error(
|
|
2157
|
+
`${model.tablename} needs exactly one primary key to read an insert back on a dialect without RETURNING; found ${keys.length}.`
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
return keys[0];
|
|
2161
|
+
}
|
|
1790
2162
|
function assertRawParams(params) {
|
|
1791
2163
|
if (!Array.isArray(params)) {
|
|
1792
2164
|
throw new TypeError(
|
|
@@ -1859,10 +2231,10 @@ var SyncSession = class {
|
|
|
1859
2231
|
return new SyncResult(rows, result.changes);
|
|
1860
2232
|
}
|
|
1861
2233
|
/** Run `fn` inside a transaction: commit on success, rollback on throw. */
|
|
1862
|
-
transaction(
|
|
2234
|
+
transaction(fn2) {
|
|
1863
2235
|
this.exec("BEGIN", []);
|
|
1864
2236
|
try {
|
|
1865
|
-
const out =
|
|
2237
|
+
const out = fn2(this);
|
|
1866
2238
|
this.exec("COMMIT", []);
|
|
1867
2239
|
return out;
|
|
1868
2240
|
} catch (error) {
|
|
@@ -1871,12 +2243,12 @@ var SyncSession = class {
|
|
|
1871
2243
|
}
|
|
1872
2244
|
}
|
|
1873
2245
|
/** Run `fn` inside a SAVEPOINT (nested transaction). */
|
|
1874
|
-
beginNested(
|
|
2246
|
+
beginNested(fn2) {
|
|
1875
2247
|
savepointCounter += 1;
|
|
1876
2248
|
const name = `qsp_${savepointCounter}`;
|
|
1877
2249
|
this.exec(`SAVEPOINT ${name}`, []);
|
|
1878
2250
|
try {
|
|
1879
|
-
const out =
|
|
2251
|
+
const out = fn2(this);
|
|
1880
2252
|
this.exec(`RELEASE ${name}`, []);
|
|
1881
2253
|
return out;
|
|
1882
2254
|
} catch (error) {
|
|
@@ -1974,6 +2346,11 @@ var AsyncSession = class _AsyncSession {
|
|
|
1974
2346
|
}
|
|
1975
2347
|
execute(builder) {
|
|
1976
2348
|
const node = builder.node;
|
|
2349
|
+
if (needsInsertReadBack(this.dialect, node)) {
|
|
2350
|
+
return new AsyncResult(
|
|
2351
|
+
this.insertAndReadBack(builder, node)
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
1977
2354
|
const { sql: sql2, params } = this.dialect.compile(node);
|
|
1978
2355
|
const inner = this.exec(sql2, params).then((result) => {
|
|
1979
2356
|
const rows = mapRows(builder, result.rows);
|
|
@@ -1981,6 +2358,53 @@ var AsyncSession = class _AsyncSession {
|
|
|
1981
2358
|
});
|
|
1982
2359
|
return new AsyncResult(inner);
|
|
1983
2360
|
}
|
|
2361
|
+
/**
|
|
2362
|
+
* Honor `.returning()` on a dialect without `RETURNING`, by inserting and then
|
|
2363
|
+
* reading the row back by key.
|
|
2364
|
+
*
|
|
2365
|
+
* Both statements must run on **one** connection, because `LAST_INSERT_ID()` is
|
|
2366
|
+
* per-connection: outside a transaction the pooled driver is reserved for the
|
|
2367
|
+
* pair; inside one, the session already holds a pinned connection (a reserved
|
|
2368
|
+
* driver exposes no `reserve`), so it runs there directly.
|
|
2369
|
+
*
|
|
2370
|
+
* @param builder The insert builder, for its source model.
|
|
2371
|
+
* @param node The insert AST, whose `returning` drives the read-back.
|
|
2372
|
+
* @returns The result view over the read-back row.
|
|
2373
|
+
* @throws Error When the insert writes more than one row — `LAST_INSERT_ID()`
|
|
2374
|
+
* identifies only the first, and the rest are consecutive only under some
|
|
2375
|
+
* auto-increment lock modes.
|
|
2376
|
+
*/
|
|
2377
|
+
async insertAndReadBack(builder, node) {
|
|
2378
|
+
if (node.values.length !== 1) {
|
|
2379
|
+
throw new Error(
|
|
2380
|
+
`${this.dialect.name} has no RETURNING, and reading back a multi-row insert is not reliable \u2014 insert one row at a time, or drop .returning().`
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
const model = builder.source;
|
|
2384
|
+
const pk = singlePrimaryKey(model);
|
|
2385
|
+
const supplied = node.values[0][pk];
|
|
2386
|
+
const readBack = select(model).where(
|
|
2387
|
+
supplied === void 0 || supplied === null ? col(pk).eq(fn.call("LAST_INSERT_ID")) : { [pk]: supplied }
|
|
2388
|
+
);
|
|
2389
|
+
const insertSql = this.dialect.compile({ ...node, returning: null });
|
|
2390
|
+
const selectSql = this.dialect.compile(
|
|
2391
|
+
node.returning === "*" || node.returning === null ? readBack.node : { ...readBack.node, columns: node.returning }
|
|
2392
|
+
);
|
|
2393
|
+
const run = async (driver) => {
|
|
2394
|
+
const scoped = new _AsyncSession(driver, this.dialect, this.logger);
|
|
2395
|
+
const written = await scoped.exec(insertSql.sql, insertSql.params);
|
|
2396
|
+
const read = await scoped.exec(selectSql.sql, selectSql.params);
|
|
2397
|
+
const rows = read.rows.map((row) => coerceRow(model, row));
|
|
2398
|
+
return new SyncResult(rows, written.changes);
|
|
2399
|
+
};
|
|
2400
|
+
if (!this.driver.reserve) return run(this.driver);
|
|
2401
|
+
const reserved = await this.driver.reserve();
|
|
2402
|
+
try {
|
|
2403
|
+
return await run(reserved);
|
|
2404
|
+
} finally {
|
|
2405
|
+
await reserved.release();
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
1984
2408
|
/** Lazily iterate result rows. Uses driver streaming when available. */
|
|
1985
2409
|
async *stream(builder) {
|
|
1986
2410
|
const node = builder.node;
|
|
@@ -2001,13 +2425,13 @@ var AsyncSession = class _AsyncSession {
|
|
|
2001
2425
|
yield coerceOne(builder, raw);
|
|
2002
2426
|
}
|
|
2003
2427
|
}
|
|
2004
|
-
async transaction(
|
|
2428
|
+
async transaction(fn2) {
|
|
2005
2429
|
if (this.driver.reserve) {
|
|
2006
2430
|
const reserved = await this.driver.reserve();
|
|
2007
2431
|
const scoped = new _AsyncSession(reserved, this.dialect, this.logger);
|
|
2008
2432
|
try {
|
|
2009
2433
|
await scoped.exec("BEGIN", []);
|
|
2010
|
-
const out = await
|
|
2434
|
+
const out = await fn2(scoped);
|
|
2011
2435
|
await scoped.exec("COMMIT", []);
|
|
2012
2436
|
return out;
|
|
2013
2437
|
} catch (error) {
|
|
@@ -2019,7 +2443,7 @@ var AsyncSession = class _AsyncSession {
|
|
|
2019
2443
|
}
|
|
2020
2444
|
await this.exec("BEGIN", []);
|
|
2021
2445
|
try {
|
|
2022
|
-
const out = await
|
|
2446
|
+
const out = await fn2(this);
|
|
2023
2447
|
await this.exec("COMMIT", []);
|
|
2024
2448
|
return out;
|
|
2025
2449
|
} catch (error) {
|
|
@@ -2046,8 +2470,8 @@ var SyncEngine = class {
|
|
|
2046
2470
|
session() {
|
|
2047
2471
|
return new SyncSession(this.driver, getDialect("sqlite"), this.logger);
|
|
2048
2472
|
}
|
|
2049
|
-
transaction(
|
|
2050
|
-
return this.session().transaction(
|
|
2473
|
+
transaction(fn2) {
|
|
2474
|
+
return this.session().transaction(fn2);
|
|
2051
2475
|
}
|
|
2052
2476
|
close() {
|
|
2053
2477
|
this.driver.close();
|
|
@@ -2069,8 +2493,8 @@ var AsyncEngine = class {
|
|
|
2069
2493
|
session() {
|
|
2070
2494
|
return new AsyncSession(this.driver, getDialect(this.dialect), this.logger);
|
|
2071
2495
|
}
|
|
2072
|
-
transaction(
|
|
2073
|
-
return this.session().transaction(
|
|
2496
|
+
transaction(fn2) {
|
|
2497
|
+
return this.session().transaction(fn2);
|
|
2074
2498
|
}
|
|
2075
2499
|
async close() {
|
|
2076
2500
|
await this.driver.close();
|
|
@@ -2080,6 +2504,16 @@ var AsyncEngine = class {
|
|
|
2080
2504
|
await this.close();
|
|
2081
2505
|
}
|
|
2082
2506
|
};
|
|
2507
|
+
function toAsyncDriver(driver) {
|
|
2508
|
+
return {
|
|
2509
|
+
async execute(sql2, params) {
|
|
2510
|
+
return await driver.execute(sql2, params);
|
|
2511
|
+
},
|
|
2512
|
+
async close() {
|
|
2513
|
+
await driver.close();
|
|
2514
|
+
}
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2083
2517
|
function asAsync(driver) {
|
|
2084
2518
|
const syncIterate = driver.iterate?.bind(driver);
|
|
2085
2519
|
return {
|
|
@@ -2558,8 +2992,8 @@ function columnNamesOf(model) {
|
|
|
2558
2992
|
const map = {};
|
|
2559
2993
|
const seen = /* @__PURE__ */ new Map();
|
|
2560
2994
|
let renamed = false;
|
|
2561
|
-
for (const [prop,
|
|
2562
|
-
const dbName =
|
|
2995
|
+
for (const [prop, col2] of Object.entries(columnsOf(model))) {
|
|
2996
|
+
const dbName = col2.dbName ?? (strategy === "snake_case" ? toSnakeCase(prop) : prop);
|
|
2563
2997
|
const collision = seen.get(dbName);
|
|
2564
2998
|
if (collision !== void 0) {
|
|
2565
2999
|
throw new Error(
|
|
@@ -2600,6 +3034,7 @@ exports.BaseDialect = BaseDialect;
|
|
|
2600
3034
|
exports.BaseRepository = BaseRepository;
|
|
2601
3035
|
exports.Column = Column;
|
|
2602
3036
|
exports.DeleteBuilder = DeleteBuilder;
|
|
3037
|
+
exports.Expression = Expression;
|
|
2603
3038
|
exports.InsertBuilder = InsertBuilder;
|
|
2604
3039
|
exports.InvalidDatabaseUrl = InvalidDatabaseUrl;
|
|
2605
3040
|
exports.JoinBuilder = JoinBuilder;
|
|
@@ -2623,6 +3058,7 @@ exports.activeRecord = activeRecord;
|
|
|
2623
3058
|
exports.and = and;
|
|
2624
3059
|
exports.avg = avg;
|
|
2625
3060
|
exports.belongsTo = belongsTo;
|
|
3061
|
+
exports.col = col;
|
|
2626
3062
|
exports.column = column;
|
|
2627
3063
|
exports.columnNamesOf = columnNamesOf;
|
|
2628
3064
|
exports.columnPropsOf = columnPropsOf;
|
|
@@ -2633,13 +3069,16 @@ exports.createSyncEngine = createSyncEngine;
|
|
|
2633
3069
|
exports.dbColumn = dbColumn;
|
|
2634
3070
|
exports.del = del;
|
|
2635
3071
|
exports.detectDialect = detectDialect;
|
|
3072
|
+
exports.fn = fn;
|
|
2636
3073
|
exports.foreignKey = foreignKey;
|
|
2637
3074
|
exports.fromDict = fromDict;
|
|
2638
3075
|
exports.getDialect = getDialect;
|
|
2639
3076
|
exports.hasMany = hasMany;
|
|
2640
3077
|
exports.insert = insert;
|
|
2641
3078
|
exports.isCondition = isCondition;
|
|
3079
|
+
exports.isExpression = isExpression;
|
|
2642
3080
|
exports.isSqlExpression = isSqlExpression;
|
|
3081
|
+
exports.isSubquery = isSubquery;
|
|
2643
3082
|
exports.join = join;
|
|
2644
3083
|
exports.loadRelations = loadRelations;
|
|
2645
3084
|
exports.max = max;
|
|
@@ -2652,11 +3091,13 @@ exports.select = select;
|
|
|
2652
3091
|
exports.sql = sql;
|
|
2653
3092
|
exports.stringify = stringify;
|
|
2654
3093
|
exports.sum = sum;
|
|
3094
|
+
exports.toAsyncDriver = toAsyncDriver;
|
|
2655
3095
|
exports.toCondNode = toCondNode;
|
|
2656
3096
|
exports.toDict = toDict;
|
|
2657
3097
|
exports.toJSON = toJSON;
|
|
2658
3098
|
exports.toSnakeCase = toSnakeCase;
|
|
2659
3099
|
exports.unique = unique;
|
|
2660
3100
|
exports.update = update;
|
|
3101
|
+
exports.val = val;
|
|
2661
3102
|
//# sourceMappingURL=index.cjs.map
|
|
2662
3103
|
//# sourceMappingURL=index.cjs.map
|