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/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(fn, column2) {
54
- this.fn = 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({ ...this.node, ...patch }, this.source);
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
- /** Order by a column of `Full`. */
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, col] of Object.entries(columns)) {
288
- out[name] = encodeValue(col, row[name] ?? null);
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, col] of Object.entries(columns)) {
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 = col.flags.notNull && !col.flags.hasDefault;
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(col, data[name]);
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, col] of Object.entries(columnsOf(model))) {
363
- const decoder = decoderFor(col.type);
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 col = columns[key];
395
- if (!col) {
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(col.type.kind)) continue;
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 ${col.type.kind} column \u2014 use sql.raw()/sql.expr\`...\` for a SQL expression`
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) => `${this.columnId(t.column, names)} ${t.direction === "desc" ? "DESC" : "ASC"}`
907
- ).join(", ");
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
- ([col, value]) => `${this.columnId(col, names)} = ${this.renderValue(value, params)}`
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
- compileIn(id, values, params, negate) {
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} ${negate ? "NOT IN" : "IN"} (${list})`;
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 col = this.columnId(onConflict.target[0] ?? "id", names);
1243
- return ` ON DUPLICATE KEY UPDATE ${col} = ${col}`;
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 is not supported on MySQL \u2014 insert, then SELECT by key (e.g. LAST_INSERT_ID())."
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, col] of Object.entries(columnsOf(model))) {
1365
- if (col.flags.primaryKey) return name;
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, col] of Object.entries(columnsOf(model))) {
1454
- if (col.flags.primaryKey) return name;
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(fn) {
2234
+ transaction(fn2) {
1863
2235
  this.exec("BEGIN", []);
1864
2236
  try {
1865
- const out = fn(this);
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(fn) {
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 = fn(this);
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(fn) {
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 fn(scoped);
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 fn(this);
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(fn) {
2050
- return this.session().transaction(fn);
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(fn) {
2073
- return this.session().transaction(fn);
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, col] of Object.entries(columnsOf(model))) {
2562
- const dbName = col.dbName ?? (strategy === "snake_case" ? toSnakeCase(prop) : prop);
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