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.
@@ -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(fn, column2) {
51
- this.fn = 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({ ...this.node, ...patch }, this.source);
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
- /** Order by a column of `Full`. */
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, col] of Object.entries(columns)) {
285
- out[name] = encodeValue(col, row[name] ?? null);
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, col] of Object.entries(columns)) {
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 = col.flags.notNull && !col.flags.hasDefault;
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(col, data[name]);
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, col] of Object.entries(columnsOf(model))) {
360
- const decoder = decoderFor(col.type);
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 col = columns[key];
392
- if (!col) {
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(col.type.kind)) continue;
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 ${col.type.kind} column \u2014 use sql.raw()/sql.expr\`...\` for a SQL expression`
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) => `${this.columnId(t.column, names)} ${t.direction === "desc" ? "DESC" : "ASC"}`
904
- ).join(", ");
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
- ([col, value]) => `${this.columnId(col, names)} = ${this.renderValue(value, params)}`
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
- compileIn(id, values, params, negate) {
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} ${negate ? "NOT IN" : "IN"} (${list})`;
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 col = this.columnId(onConflict.target[0] ?? "id", names);
1240
- return ` ON DUPLICATE KEY UPDATE ${col} = ${col}`;
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 is not supported on MySQL \u2014 insert, then SELECT by key (e.g. LAST_INSERT_ID())."
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, col] of Object.entries(columnsOf(model))) {
1362
- if (col.flags.primaryKey) return name;
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, col] of Object.entries(columnsOf(model))) {
1451
- if (col.flags.primaryKey) return name;
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(fn) {
2231
+ transaction(fn2) {
1860
2232
  this.exec("BEGIN", []);
1861
2233
  try {
1862
- const out = fn(this);
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(fn) {
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 = fn(this);
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(fn) {
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 fn(scoped);
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 fn(this);
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(fn) {
2047
- return this.session().transaction(fn);
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(fn) {
2070
- return this.session().transaction(fn);
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, col] of Object.entries(columnsOf(model))) {
2559
- const dbName = col.dbName ?? (strategy === "snake_case" ? toSnakeCase(prop) : prop);
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-5QQMVTS5.js.map
2593
- //# sourceMappingURL=chunk-5QQMVTS5.js.map
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