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
|
@@ -11,6 +11,142 @@ function toCondNode(input) {
|
|
|
11
11
|
function wrap(node) {
|
|
12
12
|
return { [CONDITION]: true, node };
|
|
13
13
|
}
|
|
14
|
+
function toExprNode(operand) {
|
|
15
|
+
return isExpression(operand) ? operand.node : { kind: "value", value: operand };
|
|
16
|
+
}
|
|
17
|
+
function assertValueOperands(op, operands) {
|
|
18
|
+
if (operands.some(isExpression)) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`The "${op}" operator binds its operands, so it takes values, not expressions.`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function isExpression(value) {
|
|
25
|
+
return value instanceof Expression;
|
|
26
|
+
}
|
|
27
|
+
var Expression = class {
|
|
28
|
+
constructor(node) {
|
|
29
|
+
this.node = node;
|
|
30
|
+
}
|
|
31
|
+
node;
|
|
32
|
+
/** Compare this expression against another expression or a bound value. */
|
|
33
|
+
compare(op, operand) {
|
|
34
|
+
return wrap({ kind: "compare", left: this.node, op, right: toExprNode(operand) });
|
|
35
|
+
}
|
|
36
|
+
/** `=` (or `IS NULL` for a null value). */
|
|
37
|
+
eq(operand) {
|
|
38
|
+
return this.compare("eq", operand);
|
|
39
|
+
}
|
|
40
|
+
/** `<>` (or `IS NOT NULL` for a null value). */
|
|
41
|
+
ne(operand) {
|
|
42
|
+
return this.compare("ne", operand);
|
|
43
|
+
}
|
|
44
|
+
/** `>`. */
|
|
45
|
+
gt(operand) {
|
|
46
|
+
return this.compare("gt", operand);
|
|
47
|
+
}
|
|
48
|
+
/** `>=`. */
|
|
49
|
+
gte(operand) {
|
|
50
|
+
return this.compare("gte", operand);
|
|
51
|
+
}
|
|
52
|
+
/** `<`. */
|
|
53
|
+
lt(operand) {
|
|
54
|
+
return this.compare("lt", operand);
|
|
55
|
+
}
|
|
56
|
+
/** `<=`. */
|
|
57
|
+
lte(operand) {
|
|
58
|
+
return this.compare("lte", operand);
|
|
59
|
+
}
|
|
60
|
+
/** `LIKE` — `%` and `_` in the operand are wildcards. */
|
|
61
|
+
like(pattern) {
|
|
62
|
+
return this.compare("like", pattern);
|
|
63
|
+
}
|
|
64
|
+
/** `ILIKE` — case-insensitive **pattern** matching, wildcards included. */
|
|
65
|
+
ilike(pattern) {
|
|
66
|
+
return this.compare("ilike", pattern);
|
|
67
|
+
}
|
|
68
|
+
/** Case-insensitive equality (`lower(a) = lower(b)`), with no wildcards. */
|
|
69
|
+
ieq(operand) {
|
|
70
|
+
return this.compare("ieq", operand);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* `IN (...)` over a list of values.
|
|
74
|
+
*
|
|
75
|
+
* @param values The values to test against.
|
|
76
|
+
* @returns The condition.
|
|
77
|
+
* @throws Error When an entry is an {@link Expression} — a list operand is
|
|
78
|
+
* bound, so an expression there would be serialized as a parameter instead of
|
|
79
|
+
* rendered as SQL.
|
|
80
|
+
*/
|
|
81
|
+
in(values) {
|
|
82
|
+
assertValueOperands("in", values);
|
|
83
|
+
return this.compare("in", values);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* `NOT IN (...)` over a list of values.
|
|
87
|
+
*
|
|
88
|
+
* @param values The values to exclude.
|
|
89
|
+
* @returns The condition.
|
|
90
|
+
* @throws Error When an entry is an {@link Expression} (see {@link Expression.in}).
|
|
91
|
+
*/
|
|
92
|
+
notIn(values) {
|
|
93
|
+
assertValueOperands("notIn", values);
|
|
94
|
+
return this.compare("notIn", values);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* `BETWEEN lo AND hi` (inclusive).
|
|
98
|
+
*
|
|
99
|
+
* @param lo The lower bound.
|
|
100
|
+
* @param hi The upper bound.
|
|
101
|
+
* @returns The condition.
|
|
102
|
+
* @throws Error When a bound is an {@link Expression} (see {@link Expression.in}).
|
|
103
|
+
*/
|
|
104
|
+
between(lo, hi) {
|
|
105
|
+
assertValueOperands("between", [lo, hi]);
|
|
106
|
+
return this.compare("between", [lo, hi]);
|
|
107
|
+
}
|
|
108
|
+
/** `IS NULL` (true) / `IS NOT NULL` (false). */
|
|
109
|
+
isNull(value = true) {
|
|
110
|
+
return this.compare("isNull", value);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
function col(name) {
|
|
114
|
+
return new Expression({ kind: "column", name });
|
|
115
|
+
}
|
|
116
|
+
function val(value) {
|
|
117
|
+
return new Expression({ kind: "value", value });
|
|
118
|
+
}
|
|
119
|
+
var FUNCTION_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
120
|
+
function toArg(arg) {
|
|
121
|
+
return typeof arg === "string" ? { kind: "column", name: arg } : arg.node;
|
|
122
|
+
}
|
|
123
|
+
function call(name, ...args) {
|
|
124
|
+
if (!FUNCTION_NAME.test(name)) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`fn.call() takes a plain SQL function name; got ${JSON.stringify(name)}.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return new Expression({ kind: "fn", name, args: args.map(toArg) });
|
|
130
|
+
}
|
|
131
|
+
var fn = {
|
|
132
|
+
/** `lower(x)`. */
|
|
133
|
+
lower: (arg) => call("lower", arg),
|
|
134
|
+
/** `upper(x)`. */
|
|
135
|
+
upper: (arg) => call("upper", arg),
|
|
136
|
+
/** `trim(x)`. */
|
|
137
|
+
trim: (arg) => call("trim", arg),
|
|
138
|
+
/** `length(x)`. */
|
|
139
|
+
length: (arg) => call("length", arg),
|
|
140
|
+
/** `abs(x)`. */
|
|
141
|
+
abs: (arg) => call("abs", arg),
|
|
142
|
+
/** `coalesce(a, b, ...)`. */
|
|
143
|
+
coalesce: (...args) => call("coalesce", ...args),
|
|
144
|
+
/**
|
|
145
|
+
* Any other SQL function, by name. Portability is the caller's problem —
|
|
146
|
+
* `date_trunc` is PostgreSQL, `strftime` is SQLite.
|
|
147
|
+
*/
|
|
148
|
+
call
|
|
149
|
+
};
|
|
14
150
|
function and(...inputs) {
|
|
15
151
|
return wrap({
|
|
16
152
|
kind: "and",
|
|
@@ -28,6 +164,9 @@ function not(input) {
|
|
|
28
164
|
}
|
|
29
165
|
|
|
30
166
|
// src/query.ts
|
|
167
|
+
function isSubquery(value) {
|
|
168
|
+
return typeof value === "object" && value !== null && value.node?.kind === "select";
|
|
169
|
+
}
|
|
31
170
|
var OPERATORS = [
|
|
32
171
|
"eq",
|
|
33
172
|
"ne",
|
|
@@ -47,8 +186,8 @@ var OPERATORS = [
|
|
|
47
186
|
"overlaps"
|
|
48
187
|
];
|
|
49
188
|
var Agg = class {
|
|
50
|
-
constructor(
|
|
51
|
-
this.fn =
|
|
189
|
+
constructor(fn2, column2) {
|
|
190
|
+
this.fn = fn2;
|
|
52
191
|
this.column = column2;
|
|
53
192
|
}
|
|
54
193
|
fn;
|
|
@@ -77,12 +216,41 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
77
216
|
node;
|
|
78
217
|
source;
|
|
79
218
|
with(patch) {
|
|
80
|
-
return new _SelectBuilder(
|
|
219
|
+
return new _SelectBuilder(
|
|
220
|
+
{ ...this.node, ...patch },
|
|
221
|
+
this.source
|
|
222
|
+
);
|
|
81
223
|
}
|
|
82
224
|
/** Add a WHERE filter: the object form (keys typed) or an `and`/`or`/`not`. */
|
|
83
225
|
where(input) {
|
|
84
226
|
return this.with({ where: toCondNode(input) });
|
|
85
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Filter by the result of the aggregation (`HAVING`).
|
|
230
|
+
*
|
|
231
|
+
* Only available on a grouped builder — `.having()` before `.aggregate()` is a
|
|
232
|
+
* compile error, not invalid SQL at runtime. Keys are the aggregate aliases you
|
|
233
|
+
* named plus the grouped columns; `WHERE` still filters rows *before* grouping,
|
|
234
|
+
* which is a different question.
|
|
235
|
+
*
|
|
236
|
+
* @param input The condition, keyed by alias or grouped column.
|
|
237
|
+
* @returns A builder carrying the `HAVING` clause.
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```ts
|
|
241
|
+
* select(Outbound)
|
|
242
|
+
* .where({ status: "queued" })
|
|
243
|
+
* .aggregate(["consumer"], { n: count() })
|
|
244
|
+
* .having({ n: { gt: 100 } });
|
|
245
|
+
* // ... GROUP BY "consumer" HAVING COUNT(*) > $2
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
having(input) {
|
|
249
|
+
return new _SelectBuilder(
|
|
250
|
+
{ ...this.node, having: toCondNode(input) },
|
|
251
|
+
this.source
|
|
252
|
+
);
|
|
253
|
+
}
|
|
86
254
|
/** Emit `SELECT DISTINCT` — drop duplicate rows. */
|
|
87
255
|
distinct() {
|
|
88
256
|
return this.with({ distinct: true });
|
|
@@ -114,7 +282,15 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
114
282
|
this.source
|
|
115
283
|
);
|
|
116
284
|
}
|
|
117
|
-
/**
|
|
285
|
+
/**
|
|
286
|
+
* Order by a column of the model, or — on a grouped query — by an aggregate
|
|
287
|
+
* alias. Unlike `HAVING`, every dialect accepts the output alias in `ORDER BY`,
|
|
288
|
+
* so the alias is emitted as written.
|
|
289
|
+
*
|
|
290
|
+
* @param column A model column, or a projected alias.
|
|
291
|
+
* @param direction `"asc"` (default) or `"desc"`.
|
|
292
|
+
* @returns A builder carrying the ordering term.
|
|
293
|
+
*/
|
|
118
294
|
orderBy(column2, direction = "asc") {
|
|
119
295
|
return this.with({
|
|
120
296
|
orderBy: [...this.node.orderBy, { column: column2, direction }]
|
|
@@ -128,6 +304,37 @@ var SelectBuilder = class _SelectBuilder {
|
|
|
128
304
|
offset(n) {
|
|
129
305
|
return this.with({ offset: n });
|
|
130
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Narrow this SELECT to a single column and mark it as a subquery, so it can be
|
|
309
|
+
* the operand of `in` / `notIn`.
|
|
310
|
+
*
|
|
311
|
+
* The whole query — `where`, `orderBy`, `limit`, and a locking clause — is
|
|
312
|
+
* embedded in the outer statement, which is what collapses the claim-a-batch
|
|
313
|
+
* pattern into one round trip instead of selecting ids and sending them back.
|
|
314
|
+
*
|
|
315
|
+
* @param column The single column to project (checked against the model).
|
|
316
|
+
* @returns A subquery carrying that column's type.
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* ```ts
|
|
320
|
+
* update(Outbound)
|
|
321
|
+
* .set({ status: "sending", attempts: sql.raw("attempts + 1") })
|
|
322
|
+
* .where({
|
|
323
|
+
* id: {
|
|
324
|
+
* in: select(Outbound)
|
|
325
|
+
* .where({ status: "queued" })
|
|
326
|
+
* .orderBy("nextAttemptAt")
|
|
327
|
+
* .limit(10)
|
|
328
|
+
* .forUpdate({ skipLocked: true })
|
|
329
|
+
* .asSubquery("id"),
|
|
330
|
+
* },
|
|
331
|
+
* })
|
|
332
|
+
* .returning();
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
asSubquery(column2) {
|
|
336
|
+
return { node: { ...this.node, columns: [column2] } };
|
|
337
|
+
}
|
|
131
338
|
/**
|
|
132
339
|
* Lock the selected rows for update (`SELECT ... FOR UPDATE`), à la
|
|
133
340
|
* SQLAlchemy's `with_for_update()`.
|
|
@@ -281,8 +488,8 @@ function toDict(model, row) {
|
|
|
281
488
|
function toJSON(model, row) {
|
|
282
489
|
const columns = columnsOf(model);
|
|
283
490
|
const out = {};
|
|
284
|
-
for (const [name,
|
|
285
|
-
out[name] = encodeValue(
|
|
491
|
+
for (const [name, col2] of Object.entries(columns)) {
|
|
492
|
+
out[name] = encodeValue(col2, row[name] ?? null);
|
|
286
493
|
}
|
|
287
494
|
return out;
|
|
288
495
|
}
|
|
@@ -293,10 +500,10 @@ function fromDict(model, data) {
|
|
|
293
500
|
const columns = columnsOf(model);
|
|
294
501
|
const out = {};
|
|
295
502
|
const issues = [];
|
|
296
|
-
for (const [name,
|
|
503
|
+
for (const [name, col2] of Object.entries(columns)) {
|
|
297
504
|
const present = name in data && data[name] !== void 0 && data[name] !== null;
|
|
298
505
|
if (!present) {
|
|
299
|
-
const required =
|
|
506
|
+
const required = col2.flags.notNull && !col2.flags.hasDefault;
|
|
300
507
|
if (required) {
|
|
301
508
|
issues.push(`missing required column "${name}"`);
|
|
302
509
|
continue;
|
|
@@ -305,7 +512,7 @@ function fromDict(model, data) {
|
|
|
305
512
|
continue;
|
|
306
513
|
}
|
|
307
514
|
try {
|
|
308
|
-
out[name] = decodeValue(
|
|
515
|
+
out[name] = decodeValue(col2, data[name]);
|
|
309
516
|
} catch (error) {
|
|
310
517
|
issues.push(`column "${name}": ${error.message}`);
|
|
311
518
|
}
|
|
@@ -356,8 +563,8 @@ function mapperFor(model) {
|
|
|
356
563
|
const props = columnPropsOf(model);
|
|
357
564
|
const names = props ? Object.fromEntries(Object.entries(props).map(([db, prop]) => [prop, db])) : null;
|
|
358
565
|
const decoders = /* @__PURE__ */ new Map();
|
|
359
|
-
for (const [prop,
|
|
360
|
-
const decoder = decoderFor(
|
|
566
|
+
for (const [prop, col2] of Object.entries(columnsOf(model))) {
|
|
567
|
+
const decoder = decoderFor(col2.type);
|
|
361
568
|
if (decoder) decoders.set(names?.[prop] ?? prop, decoder);
|
|
362
569
|
}
|
|
363
570
|
const mapper = { props, decoders };
|
|
@@ -388,15 +595,15 @@ function assertWritableValues(model, values, clause) {
|
|
|
388
595
|
const columns = columnsOf(model);
|
|
389
596
|
const issues = [];
|
|
390
597
|
for (const [key, value] of Object.entries(values)) {
|
|
391
|
-
const
|
|
392
|
-
if (!
|
|
598
|
+
const col2 = columns[key];
|
|
599
|
+
if (!col2) {
|
|
393
600
|
issues.push(`${clause}: "${key}" is not a column of ${model.tablename}`);
|
|
394
601
|
continue;
|
|
395
602
|
}
|
|
396
603
|
if (isSqlExpression(value) || isBindableScalar(value)) continue;
|
|
397
|
-
if (typeof value === "object" && STRUCTURED_KINDS.has(
|
|
604
|
+
if (typeof value === "object" && STRUCTURED_KINDS.has(col2.type.kind)) continue;
|
|
398
605
|
issues.push(
|
|
399
|
-
`${clause}: "${key}" got ${describeValue(value)}, which cannot be bound to a ${
|
|
606
|
+
`${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`
|
|
400
607
|
);
|
|
401
608
|
}
|
|
402
609
|
if (issues.length > 0) throw new ValidationError(model.tablename, issues);
|
|
@@ -752,6 +959,15 @@ var BaseDialect = class _BaseDialect {
|
|
|
752
959
|
static insertTemplates = /* @__PURE__ */ new Map();
|
|
753
960
|
/** Quoted-identifier cache (see {@link quoteId}). Shared across dialects. */
|
|
754
961
|
static quotedIds = /* @__PURE__ */ new Map();
|
|
962
|
+
/**
|
|
963
|
+
* Validate a subquery operand before it is rendered, for dialects that restrict
|
|
964
|
+
* what an `IN (SELECT ...)` may contain. The default accepts everything.
|
|
965
|
+
*
|
|
966
|
+
* @param _node The subquery's AST.
|
|
967
|
+
* @throws Error When the dialect cannot execute this subquery.
|
|
968
|
+
*/
|
|
969
|
+
checkSubquery(_node) {
|
|
970
|
+
}
|
|
755
971
|
/**
|
|
756
972
|
* The SQL operator for an array containment/overlap test.
|
|
757
973
|
*
|
|
@@ -875,6 +1091,19 @@ var BaseDialect = class _BaseDialect {
|
|
|
875
1091
|
return ` ${strength}${of}${wait}`;
|
|
876
1092
|
}
|
|
877
1093
|
// ---- statements -------------------------------------------------------
|
|
1094
|
+
/**
|
|
1095
|
+
* Compile a SELECT.
|
|
1096
|
+
*
|
|
1097
|
+
* Two alias rules differ between clauses and are handled here: PostgreSQL does
|
|
1098
|
+
* NOT accept a `SELECT` alias in `HAVING`, so an aggregate key is re-emitted as
|
|
1099
|
+
* its expression (`COUNT(*) > $1`), a form every dialect accepts; `ORDER BY`,
|
|
1100
|
+
* by contrast, accepts the output alias everywhere, so it is emitted as
|
|
1101
|
+
* written.
|
|
1102
|
+
*
|
|
1103
|
+
* @param node The select AST.
|
|
1104
|
+
* @param params The parameter collector.
|
|
1105
|
+
* @returns The SQL text.
|
|
1106
|
+
*/
|
|
878
1107
|
compileSelect(node, params) {
|
|
879
1108
|
const names = node.names;
|
|
880
1109
|
let cols;
|
|
@@ -898,10 +1127,21 @@ var BaseDialect = class _BaseDialect {
|
|
|
898
1127
|
if (node.groupBy.length > 0) {
|
|
899
1128
|
sql2 += ` GROUP BY ${node.groupBy.map((c) => this.columnId(c, names)).join(", ")}`;
|
|
900
1129
|
}
|
|
1130
|
+
const aggByAlias = new Map(node.aggregates.map((a) => [a.alias, a]));
|
|
1131
|
+
if (node.having) {
|
|
1132
|
+
const having = this.compileCondition(node.having, params, (key) => {
|
|
1133
|
+
const agg = aggByAlias.get(key);
|
|
1134
|
+
if (!agg) return this.columnId(key, names);
|
|
1135
|
+
const inner = agg.column === "*" ? "*" : this.columnId(agg.column, names);
|
|
1136
|
+
return `${agg.fn.toUpperCase()}(${inner})`;
|
|
1137
|
+
});
|
|
1138
|
+
if (having) sql2 += ` HAVING ${having}`;
|
|
1139
|
+
}
|
|
901
1140
|
if (node.orderBy.length > 0) {
|
|
902
|
-
const terms = node.orderBy.map(
|
|
903
|
-
(t)
|
|
904
|
-
|
|
1141
|
+
const terms = node.orderBy.map((t) => {
|
|
1142
|
+
const id = aggByAlias.has(t.column) ? this.quoteId(t.column) : this.columnId(t.column, names);
|
|
1143
|
+
return `${id} ${t.direction === "desc" ? "DESC" : "ASC"}`;
|
|
1144
|
+
}).join(", ");
|
|
905
1145
|
sql2 += ` ORDER BY ${terms}`;
|
|
906
1146
|
}
|
|
907
1147
|
if (node.limit !== void 0) sql2 += ` LIMIT ${params.bind(node.limit)}`;
|
|
@@ -1037,7 +1277,7 @@ var BaseDialect = class _BaseDialect {
|
|
|
1037
1277
|
compileUpdate(node, params) {
|
|
1038
1278
|
const names = node.names;
|
|
1039
1279
|
const sets = Object.entries(node.set).map(
|
|
1040
|
-
([
|
|
1280
|
+
([col2, value]) => `${this.columnId(col2, names)} = ${this.renderValue(value, params)}`
|
|
1041
1281
|
).join(", ");
|
|
1042
1282
|
let sql2 = `UPDATE ${this.quoteId(node.table)} SET ${sets}`;
|
|
1043
1283
|
const where = this.compileCondition(
|
|
@@ -1130,6 +1370,83 @@ var BaseDialect = class _BaseDialect {
|
|
|
1130
1370
|
const inner = this.compileCondition(node.part, params, idFor);
|
|
1131
1371
|
return inner ? `NOT (${inner})` : "";
|
|
1132
1372
|
}
|
|
1373
|
+
case "compare": {
|
|
1374
|
+
const left = this.renderExpr(node.left, params, idFor);
|
|
1375
|
+
if (node.right.kind === "value") {
|
|
1376
|
+
return this.compileOperator(left, node.op, node.right.value, params);
|
|
1377
|
+
}
|
|
1378
|
+
return this.compileExprOperator(
|
|
1379
|
+
left,
|
|
1380
|
+
node.op,
|
|
1381
|
+
this.renderExpr(node.right, params, idFor)
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Render one side of a comparison.
|
|
1388
|
+
*
|
|
1389
|
+
* A column reference goes through `idFor`, so an explicit `.name()` mapping and
|
|
1390
|
+
* join qualification apply here exactly as they do in the object form of
|
|
1391
|
+
* `where` — `col()` is not a way around them. Only a `value` node binds.
|
|
1392
|
+
*
|
|
1393
|
+
* @param node The expression AST.
|
|
1394
|
+
* @param params The parameter collector.
|
|
1395
|
+
* @param idFor The identifier resolver for the enclosing statement.
|
|
1396
|
+
* @returns The SQL text of the expression.
|
|
1397
|
+
*/
|
|
1398
|
+
renderExpr(node, params, idFor) {
|
|
1399
|
+
switch (node.kind) {
|
|
1400
|
+
case "column":
|
|
1401
|
+
return idFor(node.name);
|
|
1402
|
+
case "value":
|
|
1403
|
+
return params.bind(node.value);
|
|
1404
|
+
case "fn": {
|
|
1405
|
+
const args = node.args.map((a) => this.renderExpr(a, params, idFor)).join(", ");
|
|
1406
|
+
return `${node.name}(${args})`;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Compile a comparison whose right-hand side is another expression rather than
|
|
1412
|
+
* a bound value (`total > paid`, `lower(a) = lower(b)`).
|
|
1413
|
+
*
|
|
1414
|
+
* The list and null operators are excluded: `IN`, `BETWEEN` and `IS NULL` take
|
|
1415
|
+
* a value operand, and accepting an expression there would silently compile to
|
|
1416
|
+
* something else.
|
|
1417
|
+
*
|
|
1418
|
+
* @param left The rendered left-hand side.
|
|
1419
|
+
* @param op The operator name.
|
|
1420
|
+
* @param right The rendered right-hand side.
|
|
1421
|
+
* @returns The SQL text of the predicate.
|
|
1422
|
+
* @throws Error When the operator needs a value operand.
|
|
1423
|
+
*/
|
|
1424
|
+
compileExprOperator(left, op, right) {
|
|
1425
|
+
switch (op) {
|
|
1426
|
+
case "eq":
|
|
1427
|
+
return `${left} = ${right}`;
|
|
1428
|
+
case "ne":
|
|
1429
|
+
return `${left} <> ${right}`;
|
|
1430
|
+
case "gt":
|
|
1431
|
+
return `${left} > ${right}`;
|
|
1432
|
+
case "gte":
|
|
1433
|
+
return `${left} >= ${right}`;
|
|
1434
|
+
case "lt":
|
|
1435
|
+
return `${left} < ${right}`;
|
|
1436
|
+
case "lte":
|
|
1437
|
+
return `${left} <= ${right}`;
|
|
1438
|
+
case "like":
|
|
1439
|
+
return `${left} LIKE ${right}`;
|
|
1440
|
+
case "ilike":
|
|
1441
|
+
return this.ilike(left, right);
|
|
1442
|
+
case "ieq":
|
|
1443
|
+
return `lower(${left}) = lower(${right})`;
|
|
1444
|
+
case "contains":
|
|
1445
|
+
case "containedBy":
|
|
1446
|
+
case "overlaps":
|
|
1447
|
+
return `${left} ${this.arrayOperator(op)} ${right}`;
|
|
1448
|
+
default:
|
|
1449
|
+
throw new Error(`The "${op}" operator takes a value operand, not an expression.`);
|
|
1133
1450
|
}
|
|
1134
1451
|
}
|
|
1135
1452
|
compileOperator(id, op, operand, params) {
|
|
@@ -1172,12 +1489,33 @@ var BaseDialect = class _BaseDialect {
|
|
|
1172
1489
|
throw new Error(`Unknown operator ${JSON.stringify(op)}`);
|
|
1173
1490
|
}
|
|
1174
1491
|
}
|
|
1175
|
-
|
|
1492
|
+
/**
|
|
1493
|
+
* Compile `IN` / `NOT IN`, whose operand is either a value list or a
|
|
1494
|
+
* single-column subquery.
|
|
1495
|
+
*
|
|
1496
|
+
* The subquery is rendered at the position it appears in the outer statement
|
|
1497
|
+
* and shares the same parameter collector, so its own placeholders land in the
|
|
1498
|
+
* right order — and it keeps its own `names` map, since the inner model may use
|
|
1499
|
+
* a different naming convention than the outer one.
|
|
1500
|
+
*
|
|
1501
|
+
* @param id The quoted column identifier being tested.
|
|
1502
|
+
* @param operand A list of values, or a {@link Subquery}.
|
|
1503
|
+
* @param params The parameter collector for the statement being compiled.
|
|
1504
|
+
* @param negate True for `NOT IN`.
|
|
1505
|
+
* @returns The SQL text of the predicate.
|
|
1506
|
+
*/
|
|
1507
|
+
compileIn(id, operand, params, negate) {
|
|
1508
|
+
const keyword = negate ? "NOT IN" : "IN";
|
|
1509
|
+
if (isSubquery(operand)) {
|
|
1510
|
+
this.checkSubquery(operand.node);
|
|
1511
|
+
return `${id} ${keyword} (${this.compileSelect(operand.node, params)})`;
|
|
1512
|
+
}
|
|
1513
|
+
const values = operand;
|
|
1176
1514
|
if (values.length === 0) {
|
|
1177
1515
|
return negate ? "1 = 1" : "1 = 0";
|
|
1178
1516
|
}
|
|
1179
1517
|
const list = values.map((v) => params.bind(v)).join(", ");
|
|
1180
|
-
return `${id} ${
|
|
1518
|
+
return `${id} ${keyword} (${list})`;
|
|
1181
1519
|
}
|
|
1182
1520
|
};
|
|
1183
1521
|
var SqliteDialect = class extends BaseDialect {
|
|
@@ -1229,6 +1567,19 @@ var MysqlDialect = class extends BaseDialect {
|
|
|
1229
1567
|
mysqlQuotedIds.set(name, quoted);
|
|
1230
1568
|
return quoted;
|
|
1231
1569
|
}
|
|
1570
|
+
/**
|
|
1571
|
+
* MySQL rejects `LIMIT` inside an `IN` subquery with
|
|
1572
|
+
* `ER_NOT_SUPPORTED_YET: This version of MySQL doesn't yet support
|
|
1573
|
+
* 'LIMIT & IN/ALL/ANY/SOME subquery'`. Failing at compile time names the fix
|
|
1574
|
+
* instead of surfacing that error from the driver at runtime.
|
|
1575
|
+
*/
|
|
1576
|
+
checkSubquery(node) {
|
|
1577
|
+
if (node.limit !== void 0 || node.offset !== void 0) {
|
|
1578
|
+
throw new Error(
|
|
1579
|
+
"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."
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1232
1583
|
renderConflict(onConflict, conflictCols, nextValue, names) {
|
|
1233
1584
|
if (onConflict.targetWhere || onConflict.updateWhere) {
|
|
1234
1585
|
throw new Error(
|
|
@@ -1236,16 +1587,25 @@ var MysqlDialect = class extends BaseDialect {
|
|
|
1236
1587
|
);
|
|
1237
1588
|
}
|
|
1238
1589
|
if (onConflict.update === "nothing") {
|
|
1239
|
-
const
|
|
1240
|
-
return ` ON DUPLICATE KEY UPDATE ${
|
|
1590
|
+
const col2 = this.columnId(onConflict.target[0] ?? "id", names);
|
|
1591
|
+
return ` ON DUPLICATE KEY UPDATE ${col2} = ${col2}`;
|
|
1241
1592
|
}
|
|
1242
1593
|
const assignments = conflictCols.map((c) => `${this.columnId(c, names)} = ${nextValue()}`).join(", ");
|
|
1243
1594
|
return ` ON DUPLICATE KEY UPDATE ${assignments}`;
|
|
1244
1595
|
}
|
|
1596
|
+
/**
|
|
1597
|
+
* MySQL has no `RETURNING`, so it cannot be compiled into a statement.
|
|
1598
|
+
*
|
|
1599
|
+
* `session.execute()` still honors `.returning()` on a **single-row INSERT** by
|
|
1600
|
+
* running the insert and reading the row back by key on the same connection —
|
|
1601
|
+
* that is execution, not compilation, so it never reaches here. Compiling a
|
|
1602
|
+
* node with `returning` directly is an error, rather than SQL that silently
|
|
1603
|
+
* returns nothing.
|
|
1604
|
+
*/
|
|
1245
1605
|
compileReturning(returning) {
|
|
1246
1606
|
if (returning === null) return "";
|
|
1247
1607
|
throw new Error(
|
|
1248
|
-
"RETURNING
|
|
1608
|
+
"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."
|
|
1249
1609
|
);
|
|
1250
1610
|
}
|
|
1251
1611
|
};
|
|
@@ -1358,8 +1718,8 @@ var RecordNotFound = class extends Error {
|
|
|
1358
1718
|
}
|
|
1359
1719
|
};
|
|
1360
1720
|
function primaryKeyOf(model) {
|
|
1361
|
-
for (const [name,
|
|
1362
|
-
if (
|
|
1721
|
+
for (const [name, col2] of Object.entries(columnsOf(model))) {
|
|
1722
|
+
if (col2.flags.primaryKey) return name;
|
|
1363
1723
|
}
|
|
1364
1724
|
throw new Error(`${model.tablename} has no primary key`);
|
|
1365
1725
|
}
|
|
@@ -1447,8 +1807,8 @@ var BaseRepository = class {
|
|
|
1447
1807
|
|
|
1448
1808
|
// src/active-record.ts
|
|
1449
1809
|
function primaryKeyOf2(model) {
|
|
1450
|
-
for (const [name,
|
|
1451
|
-
if (
|
|
1810
|
+
for (const [name, col2] of Object.entries(columnsOf(model))) {
|
|
1811
|
+
if (col2.flags.primaryKey) return name;
|
|
1452
1812
|
}
|
|
1453
1813
|
throw new Error(`${model.tablename} has no primary key`);
|
|
1454
1814
|
}
|
|
@@ -1784,6 +2144,18 @@ var AsyncResult = class {
|
|
|
1784
2144
|
}
|
|
1785
2145
|
};
|
|
1786
2146
|
var savepointCounter = 0;
|
|
2147
|
+
function needsInsertReadBack(dialect, node) {
|
|
2148
|
+
return dialect.name === "mysql" && node.kind === "insert" && node.returning !== null;
|
|
2149
|
+
}
|
|
2150
|
+
function singlePrimaryKey(model) {
|
|
2151
|
+
const keys = Object.entries(columnsOf(model)).filter(([, column2]) => column2.flags.primaryKey).map(([name]) => name);
|
|
2152
|
+
if (keys.length !== 1) {
|
|
2153
|
+
throw new Error(
|
|
2154
|
+
`${model.tablename} needs exactly one primary key to read an insert back on a dialect without RETURNING; found ${keys.length}.`
|
|
2155
|
+
);
|
|
2156
|
+
}
|
|
2157
|
+
return keys[0];
|
|
2158
|
+
}
|
|
1787
2159
|
function assertRawParams(params) {
|
|
1788
2160
|
if (!Array.isArray(params)) {
|
|
1789
2161
|
throw new TypeError(
|
|
@@ -1856,10 +2228,10 @@ var SyncSession = class {
|
|
|
1856
2228
|
return new SyncResult(rows, result.changes);
|
|
1857
2229
|
}
|
|
1858
2230
|
/** Run `fn` inside a transaction: commit on success, rollback on throw. */
|
|
1859
|
-
transaction(
|
|
2231
|
+
transaction(fn2) {
|
|
1860
2232
|
this.exec("BEGIN", []);
|
|
1861
2233
|
try {
|
|
1862
|
-
const out =
|
|
2234
|
+
const out = fn2(this);
|
|
1863
2235
|
this.exec("COMMIT", []);
|
|
1864
2236
|
return out;
|
|
1865
2237
|
} catch (error) {
|
|
@@ -1868,12 +2240,12 @@ var SyncSession = class {
|
|
|
1868
2240
|
}
|
|
1869
2241
|
}
|
|
1870
2242
|
/** Run `fn` inside a SAVEPOINT (nested transaction). */
|
|
1871
|
-
beginNested(
|
|
2243
|
+
beginNested(fn2) {
|
|
1872
2244
|
savepointCounter += 1;
|
|
1873
2245
|
const name = `qsp_${savepointCounter}`;
|
|
1874
2246
|
this.exec(`SAVEPOINT ${name}`, []);
|
|
1875
2247
|
try {
|
|
1876
|
-
const out =
|
|
2248
|
+
const out = fn2(this);
|
|
1877
2249
|
this.exec(`RELEASE ${name}`, []);
|
|
1878
2250
|
return out;
|
|
1879
2251
|
} catch (error) {
|
|
@@ -1971,6 +2343,11 @@ var AsyncSession = class _AsyncSession {
|
|
|
1971
2343
|
}
|
|
1972
2344
|
execute(builder) {
|
|
1973
2345
|
const node = builder.node;
|
|
2346
|
+
if (needsInsertReadBack(this.dialect, node)) {
|
|
2347
|
+
return new AsyncResult(
|
|
2348
|
+
this.insertAndReadBack(builder, node)
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
1974
2351
|
const { sql: sql2, params } = this.dialect.compile(node);
|
|
1975
2352
|
const inner = this.exec(sql2, params).then((result) => {
|
|
1976
2353
|
const rows = mapRows(builder, result.rows);
|
|
@@ -1978,6 +2355,53 @@ var AsyncSession = class _AsyncSession {
|
|
|
1978
2355
|
});
|
|
1979
2356
|
return new AsyncResult(inner);
|
|
1980
2357
|
}
|
|
2358
|
+
/**
|
|
2359
|
+
* Honor `.returning()` on a dialect without `RETURNING`, by inserting and then
|
|
2360
|
+
* reading the row back by key.
|
|
2361
|
+
*
|
|
2362
|
+
* Both statements must run on **one** connection, because `LAST_INSERT_ID()` is
|
|
2363
|
+
* per-connection: outside a transaction the pooled driver is reserved for the
|
|
2364
|
+
* pair; inside one, the session already holds a pinned connection (a reserved
|
|
2365
|
+
* driver exposes no `reserve`), so it runs there directly.
|
|
2366
|
+
*
|
|
2367
|
+
* @param builder The insert builder, for its source model.
|
|
2368
|
+
* @param node The insert AST, whose `returning` drives the read-back.
|
|
2369
|
+
* @returns The result view over the read-back row.
|
|
2370
|
+
* @throws Error When the insert writes more than one row — `LAST_INSERT_ID()`
|
|
2371
|
+
* identifies only the first, and the rest are consecutive only under some
|
|
2372
|
+
* auto-increment lock modes.
|
|
2373
|
+
*/
|
|
2374
|
+
async insertAndReadBack(builder, node) {
|
|
2375
|
+
if (node.values.length !== 1) {
|
|
2376
|
+
throw new Error(
|
|
2377
|
+
`${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().`
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
const model = builder.source;
|
|
2381
|
+
const pk = singlePrimaryKey(model);
|
|
2382
|
+
const supplied = node.values[0][pk];
|
|
2383
|
+
const readBack = select(model).where(
|
|
2384
|
+
supplied === void 0 || supplied === null ? col(pk).eq(fn.call("LAST_INSERT_ID")) : { [pk]: supplied }
|
|
2385
|
+
);
|
|
2386
|
+
const insertSql = this.dialect.compile({ ...node, returning: null });
|
|
2387
|
+
const selectSql = this.dialect.compile(
|
|
2388
|
+
node.returning === "*" || node.returning === null ? readBack.node : { ...readBack.node, columns: node.returning }
|
|
2389
|
+
);
|
|
2390
|
+
const run = async (driver) => {
|
|
2391
|
+
const scoped = new _AsyncSession(driver, this.dialect, this.logger);
|
|
2392
|
+
const written = await scoped.exec(insertSql.sql, insertSql.params);
|
|
2393
|
+
const read = await scoped.exec(selectSql.sql, selectSql.params);
|
|
2394
|
+
const rows = read.rows.map((row) => coerceRow(model, row));
|
|
2395
|
+
return new SyncResult(rows, written.changes);
|
|
2396
|
+
};
|
|
2397
|
+
if (!this.driver.reserve) return run(this.driver);
|
|
2398
|
+
const reserved = await this.driver.reserve();
|
|
2399
|
+
try {
|
|
2400
|
+
return await run(reserved);
|
|
2401
|
+
} finally {
|
|
2402
|
+
await reserved.release();
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
1981
2405
|
/** Lazily iterate result rows. Uses driver streaming when available. */
|
|
1982
2406
|
async *stream(builder) {
|
|
1983
2407
|
const node = builder.node;
|
|
@@ -1998,13 +2422,13 @@ var AsyncSession = class _AsyncSession {
|
|
|
1998
2422
|
yield coerceOne(builder, raw);
|
|
1999
2423
|
}
|
|
2000
2424
|
}
|
|
2001
|
-
async transaction(
|
|
2425
|
+
async transaction(fn2) {
|
|
2002
2426
|
if (this.driver.reserve) {
|
|
2003
2427
|
const reserved = await this.driver.reserve();
|
|
2004
2428
|
const scoped = new _AsyncSession(reserved, this.dialect, this.logger);
|
|
2005
2429
|
try {
|
|
2006
2430
|
await scoped.exec("BEGIN", []);
|
|
2007
|
-
const out = await
|
|
2431
|
+
const out = await fn2(scoped);
|
|
2008
2432
|
await scoped.exec("COMMIT", []);
|
|
2009
2433
|
return out;
|
|
2010
2434
|
} catch (error) {
|
|
@@ -2016,7 +2440,7 @@ var AsyncSession = class _AsyncSession {
|
|
|
2016
2440
|
}
|
|
2017
2441
|
await this.exec("BEGIN", []);
|
|
2018
2442
|
try {
|
|
2019
|
-
const out = await
|
|
2443
|
+
const out = await fn2(this);
|
|
2020
2444
|
await this.exec("COMMIT", []);
|
|
2021
2445
|
return out;
|
|
2022
2446
|
} catch (error) {
|
|
@@ -2043,8 +2467,8 @@ var SyncEngine = class {
|
|
|
2043
2467
|
session() {
|
|
2044
2468
|
return new SyncSession(this.driver, getDialect("sqlite"), this.logger);
|
|
2045
2469
|
}
|
|
2046
|
-
transaction(
|
|
2047
|
-
return this.session().transaction(
|
|
2470
|
+
transaction(fn2) {
|
|
2471
|
+
return this.session().transaction(fn2);
|
|
2048
2472
|
}
|
|
2049
2473
|
close() {
|
|
2050
2474
|
this.driver.close();
|
|
@@ -2066,8 +2490,8 @@ var AsyncEngine = class {
|
|
|
2066
2490
|
session() {
|
|
2067
2491
|
return new AsyncSession(this.driver, getDialect(this.dialect), this.logger);
|
|
2068
2492
|
}
|
|
2069
|
-
transaction(
|
|
2070
|
-
return this.session().transaction(
|
|
2493
|
+
transaction(fn2) {
|
|
2494
|
+
return this.session().transaction(fn2);
|
|
2071
2495
|
}
|
|
2072
2496
|
async close() {
|
|
2073
2497
|
await this.driver.close();
|
|
@@ -2077,6 +2501,16 @@ var AsyncEngine = class {
|
|
|
2077
2501
|
await this.close();
|
|
2078
2502
|
}
|
|
2079
2503
|
};
|
|
2504
|
+
function toAsyncDriver(driver) {
|
|
2505
|
+
return {
|
|
2506
|
+
async execute(sql2, params) {
|
|
2507
|
+
return await driver.execute(sql2, params);
|
|
2508
|
+
},
|
|
2509
|
+
async close() {
|
|
2510
|
+
await driver.close();
|
|
2511
|
+
}
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2080
2514
|
function asAsync(driver) {
|
|
2081
2515
|
const syncIterate = driver.iterate?.bind(driver);
|
|
2082
2516
|
return {
|
|
@@ -2555,8 +2989,8 @@ function columnNamesOf(model) {
|
|
|
2555
2989
|
const map = {};
|
|
2556
2990
|
const seen = /* @__PURE__ */ new Map();
|
|
2557
2991
|
let renamed = false;
|
|
2558
|
-
for (const [prop,
|
|
2559
|
-
const dbName =
|
|
2992
|
+
for (const [prop, col2] of Object.entries(columnsOf(model))) {
|
|
2993
|
+
const dbName = col2.dbName ?? (strategy === "snake_case" ? toSnakeCase(prop) : prop);
|
|
2560
2994
|
const collision = seen.get(dbName);
|
|
2561
2995
|
if (collision !== void 0) {
|
|
2562
2996
|
throw new Error(
|
|
@@ -2588,6 +3022,6 @@ function dbColumn(names, prop) {
|
|
|
2588
3022
|
return names?.[prop] ?? prop;
|
|
2589
3023
|
}
|
|
2590
3024
|
|
|
2591
|
-
export { ActiveRecord, Agg, AsyncEngine, AsyncResult, AsyncSession, BaseDialect, BaseRepository, Column, DeleteBuilder, InsertBuilder, InvalidDatabaseUrl, JoinBuilder, Model, MysqlDialect, NoResultError, NodeSqliteDriver, OPERATORS, Params, PostgresDialect, QueryExecutionError, RecordNotFound, SelectBuilder, SqliteDialect, SyncEngine, SyncResult, SyncSession, UpdateBuilder, ValidationError, activeRecord, and, avg, belongsTo, column, columnNamesOf, columnPropsOf, columnsOf, count, createEngine, createSyncEngine, dbColumn, del, detectDialect, foreignKey, fromDict, getDialect, hasMany, insert, isCondition, isSqlExpression, join, loadRelations, max, min, not, or, parse, parseDatabaseUrl, renderPortableToken, select, sql, stringify, sum, toCondNode, toDict, toJSON, toSnakeCase, unique, update };
|
|
2592
|
-
//# sourceMappingURL=chunk-
|
|
2593
|
-
//# sourceMappingURL=chunk-
|
|
3025
|
+
export { ActiveRecord, Agg, AsyncEngine, AsyncResult, AsyncSession, BaseDialect, BaseRepository, Column, DeleteBuilder, Expression, InsertBuilder, InvalidDatabaseUrl, JoinBuilder, Model, MysqlDialect, NoResultError, NodeSqliteDriver, OPERATORS, Params, PostgresDialect, QueryExecutionError, RecordNotFound, SelectBuilder, SqliteDialect, SyncEngine, SyncResult, SyncSession, UpdateBuilder, ValidationError, activeRecord, and, avg, belongsTo, col, column, columnNamesOf, columnPropsOf, columnsOf, count, createEngine, createSyncEngine, dbColumn, del, detectDialect, fn, foreignKey, fromDict, getDialect, hasMany, insert, isCondition, isExpression, isSqlExpression, isSubquery, join, loadRelations, max, min, not, or, parse, parseDatabaseUrl, renderPortableToken, select, sql, stringify, sum, toAsyncDriver, toCondNode, toDict, toJSON, toSnakeCase, unique, update, val };
|
|
3026
|
+
//# sourceMappingURL=chunk-G7O5DCCC.js.map
|
|
3027
|
+
//# sourceMappingURL=chunk-G7O5DCCC.js.map
|