tempest-db-js 0.5.0 → 0.7.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,19 +595,34 @@ 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);
403
610
  }
611
+ function assertConsistentRows(model, rows) {
612
+ if (rows.length < 2) return;
613
+ const union = /* @__PURE__ */ new Set();
614
+ for (const row of rows) for (const key of Object.keys(row)) union.add(key);
615
+ const inconsistent = [...union].filter((key) => rows.some((row) => !(key in row)));
616
+ if (inconsistent.length === 0) return;
617
+ const columns = columnsOf(model);
618
+ const defaulted = inconsistent.filter((key) => columns[key]?.flags.hasDefault);
619
+ if (defaulted.length === 0) return;
620
+ const named = defaulted.map((c) => `"${c}"`).join(", ");
621
+ const verb = defaulted.length === 1 ? "has" : "have";
622
+ throw new ValidationError(model.tablename, [
623
+ `values: ${named} ${verb} a default but is missing from some rows of this multi-row insert \u2014 every row shares one column list, so the omitting rows would be written as NULL instead of taking the default. Give the column in every row, or insert the rows separately.`
624
+ ]);
625
+ }
404
626
  function describeValue(value) {
405
627
  if (typeof value === "function") return "a function";
406
628
  if (Array.isArray(value)) return "an array";
@@ -423,11 +645,13 @@ var InsertBuilder = class _InsertBuilder {
423
645
  * @param rows One row, or an array of rows.
424
646
  * @returns A builder carrying the rows.
425
647
  * @throws ValidationError When a value is not a column value the dialect can
426
- * bind (see the `sql` helpers for writing an expression instead).
648
+ * bind (see the `sql` helpers for writing an expression instead), or when the
649
+ * rows of a multi-row insert disagree about a column that has a default.
427
650
  */
428
651
  values(rows) {
429
652
  const list = Array.isArray(rows) ? rows : [rows];
430
653
  for (const row of list) assertWritableValues(this.source, row, "values");
654
+ assertConsistentRows(this.source, list);
431
655
  return this.with({ values: list });
432
656
  }
433
657
  /**
@@ -729,6 +953,18 @@ var Params = class {
729
953
  return this.placeholder(this.values.length);
730
954
  }
731
955
  };
956
+ function insertColumns(rows) {
957
+ const columns = [];
958
+ const seen = /* @__PURE__ */ new Set();
959
+ for (const row of rows) {
960
+ for (const key of Object.keys(row)) {
961
+ if (seen.has(key)) continue;
962
+ seen.add(key);
963
+ columns.push(key);
964
+ }
965
+ }
966
+ return columns;
967
+ }
732
968
  function insertHasExpression(node) {
733
969
  for (const row of node.values) {
734
970
  for (const value of Object.values(row)) {
@@ -752,6 +988,15 @@ var BaseDialect = class _BaseDialect {
752
988
  static insertTemplates = /* @__PURE__ */ new Map();
753
989
  /** Quoted-identifier cache (see {@link quoteId}). Shared across dialects. */
754
990
  static quotedIds = /* @__PURE__ */ new Map();
991
+ /**
992
+ * Validate a subquery operand before it is rendered, for dialects that restrict
993
+ * what an `IN (SELECT ...)` may contain. The default accepts everything.
994
+ *
995
+ * @param _node The subquery's AST.
996
+ * @throws Error When the dialect cannot execute this subquery.
997
+ */
998
+ checkSubquery(_node) {
999
+ }
755
1000
  /**
756
1001
  * The SQL operator for an array containment/overlap test.
757
1002
  *
@@ -875,6 +1120,19 @@ var BaseDialect = class _BaseDialect {
875
1120
  return ` ${strength}${of}${wait}`;
876
1121
  }
877
1122
  // ---- statements -------------------------------------------------------
1123
+ /**
1124
+ * Compile a SELECT.
1125
+ *
1126
+ * Two alias rules differ between clauses and are handled here: PostgreSQL does
1127
+ * NOT accept a `SELECT` alias in `HAVING`, so an aggregate key is re-emitted as
1128
+ * its expression (`COUNT(*) > $1`), a form every dialect accepts; `ORDER BY`,
1129
+ * by contrast, accepts the output alias everywhere, so it is emitted as
1130
+ * written.
1131
+ *
1132
+ * @param node The select AST.
1133
+ * @param params The parameter collector.
1134
+ * @returns The SQL text.
1135
+ */
878
1136
  compileSelect(node, params) {
879
1137
  const names = node.names;
880
1138
  let cols;
@@ -898,10 +1156,21 @@ var BaseDialect = class _BaseDialect {
898
1156
  if (node.groupBy.length > 0) {
899
1157
  sql2 += ` GROUP BY ${node.groupBy.map((c) => this.columnId(c, names)).join(", ")}`;
900
1158
  }
1159
+ const aggByAlias = new Map(node.aggregates.map((a) => [a.alias, a]));
1160
+ if (node.having) {
1161
+ const having = this.compileCondition(node.having, params, (key) => {
1162
+ const agg = aggByAlias.get(key);
1163
+ if (!agg) return this.columnId(key, names);
1164
+ const inner = agg.column === "*" ? "*" : this.columnId(agg.column, names);
1165
+ return `${agg.fn.toUpperCase()}(${inner})`;
1166
+ });
1167
+ if (having) sql2 += ` HAVING ${having}`;
1168
+ }
901
1169
  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(", ");
1170
+ const terms = node.orderBy.map((t) => {
1171
+ const id = aggByAlias.has(t.column) ? this.quoteId(t.column) : this.columnId(t.column, names);
1172
+ return `${id} ${t.direction === "desc" ? "DESC" : "ASC"}`;
1173
+ }).join(", ");
905
1174
  sql2 += ` ORDER BY ${terms}`;
906
1175
  }
907
1176
  if (node.limit !== void 0) sql2 += ` LIMIT ${params.bind(node.limit)}`;
@@ -925,7 +1194,7 @@ var BaseDialect = class _BaseDialect {
925
1194
  * SQL order, so placeholder positions stay correct.
926
1195
  */
927
1196
  compileInsert(node, params) {
928
- const columns = node.values.length > 0 ? Object.keys(node.values[0]) : [];
1197
+ const columns = insertColumns(node.values);
929
1198
  const conflict = node.onConflict;
930
1199
  const cacheable = conflict?.targetWhere === void 0 && conflict?.updateWhere === void 0 && !insertHasExpression(node);
931
1200
  if (!cacheable) return this.compileInsertDirect(node, columns, params);
@@ -1037,7 +1306,7 @@ var BaseDialect = class _BaseDialect {
1037
1306
  compileUpdate(node, params) {
1038
1307
  const names = node.names;
1039
1308
  const sets = Object.entries(node.set).map(
1040
- ([col, value]) => `${this.columnId(col, names)} = ${this.renderValue(value, params)}`
1309
+ ([col2, value]) => `${this.columnId(col2, names)} = ${this.renderValue(value, params)}`
1041
1310
  ).join(", ");
1042
1311
  let sql2 = `UPDATE ${this.quoteId(node.table)} SET ${sets}`;
1043
1312
  const where = this.compileCondition(
@@ -1130,6 +1399,83 @@ var BaseDialect = class _BaseDialect {
1130
1399
  const inner = this.compileCondition(node.part, params, idFor);
1131
1400
  return inner ? `NOT (${inner})` : "";
1132
1401
  }
1402
+ case "compare": {
1403
+ const left = this.renderExpr(node.left, params, idFor);
1404
+ if (node.right.kind === "value") {
1405
+ return this.compileOperator(left, node.op, node.right.value, params);
1406
+ }
1407
+ return this.compileExprOperator(
1408
+ left,
1409
+ node.op,
1410
+ this.renderExpr(node.right, params, idFor)
1411
+ );
1412
+ }
1413
+ }
1414
+ }
1415
+ /**
1416
+ * Render one side of a comparison.
1417
+ *
1418
+ * A column reference goes through `idFor`, so an explicit `.name()` mapping and
1419
+ * join qualification apply here exactly as they do in the object form of
1420
+ * `where` — `col()` is not a way around them. Only a `value` node binds.
1421
+ *
1422
+ * @param node The expression AST.
1423
+ * @param params The parameter collector.
1424
+ * @param idFor The identifier resolver for the enclosing statement.
1425
+ * @returns The SQL text of the expression.
1426
+ */
1427
+ renderExpr(node, params, idFor) {
1428
+ switch (node.kind) {
1429
+ case "column":
1430
+ return idFor(node.name);
1431
+ case "value":
1432
+ return params.bind(node.value);
1433
+ case "fn": {
1434
+ const args = node.args.map((a) => this.renderExpr(a, params, idFor)).join(", ");
1435
+ return `${node.name}(${args})`;
1436
+ }
1437
+ }
1438
+ }
1439
+ /**
1440
+ * Compile a comparison whose right-hand side is another expression rather than
1441
+ * a bound value (`total > paid`, `lower(a) = lower(b)`).
1442
+ *
1443
+ * The list and null operators are excluded: `IN`, `BETWEEN` and `IS NULL` take
1444
+ * a value operand, and accepting an expression there would silently compile to
1445
+ * something else.
1446
+ *
1447
+ * @param left The rendered left-hand side.
1448
+ * @param op The operator name.
1449
+ * @param right The rendered right-hand side.
1450
+ * @returns The SQL text of the predicate.
1451
+ * @throws Error When the operator needs a value operand.
1452
+ */
1453
+ compileExprOperator(left, op, right) {
1454
+ switch (op) {
1455
+ case "eq":
1456
+ return `${left} = ${right}`;
1457
+ case "ne":
1458
+ return `${left} <> ${right}`;
1459
+ case "gt":
1460
+ return `${left} > ${right}`;
1461
+ case "gte":
1462
+ return `${left} >= ${right}`;
1463
+ case "lt":
1464
+ return `${left} < ${right}`;
1465
+ case "lte":
1466
+ return `${left} <= ${right}`;
1467
+ case "like":
1468
+ return `${left} LIKE ${right}`;
1469
+ case "ilike":
1470
+ return this.ilike(left, right);
1471
+ case "ieq":
1472
+ return `lower(${left}) = lower(${right})`;
1473
+ case "contains":
1474
+ case "containedBy":
1475
+ case "overlaps":
1476
+ return `${left} ${this.arrayOperator(op)} ${right}`;
1477
+ default:
1478
+ throw new Error(`The "${op}" operator takes a value operand, not an expression.`);
1133
1479
  }
1134
1480
  }
1135
1481
  compileOperator(id, op, operand, params) {
@@ -1172,12 +1518,33 @@ var BaseDialect = class _BaseDialect {
1172
1518
  throw new Error(`Unknown operator ${JSON.stringify(op)}`);
1173
1519
  }
1174
1520
  }
1175
- compileIn(id, values, params, negate) {
1521
+ /**
1522
+ * Compile `IN` / `NOT IN`, whose operand is either a value list or a
1523
+ * single-column subquery.
1524
+ *
1525
+ * The subquery is rendered at the position it appears in the outer statement
1526
+ * and shares the same parameter collector, so its own placeholders land in the
1527
+ * right order — and it keeps its own `names` map, since the inner model may use
1528
+ * a different naming convention than the outer one.
1529
+ *
1530
+ * @param id The quoted column identifier being tested.
1531
+ * @param operand A list of values, or a {@link Subquery}.
1532
+ * @param params The parameter collector for the statement being compiled.
1533
+ * @param negate True for `NOT IN`.
1534
+ * @returns The SQL text of the predicate.
1535
+ */
1536
+ compileIn(id, operand, params, negate) {
1537
+ const keyword = negate ? "NOT IN" : "IN";
1538
+ if (isSubquery(operand)) {
1539
+ this.checkSubquery(operand.node);
1540
+ return `${id} ${keyword} (${this.compileSelect(operand.node, params)})`;
1541
+ }
1542
+ const values = operand;
1176
1543
  if (values.length === 0) {
1177
1544
  return negate ? "1 = 1" : "1 = 0";
1178
1545
  }
1179
1546
  const list = values.map((v) => params.bind(v)).join(", ");
1180
- return `${id} ${negate ? "NOT IN" : "IN"} (${list})`;
1547
+ return `${id} ${keyword} (${list})`;
1181
1548
  }
1182
1549
  };
1183
1550
  var SqliteDialect = class extends BaseDialect {
@@ -1229,6 +1596,19 @@ var MysqlDialect = class extends BaseDialect {
1229
1596
  mysqlQuotedIds.set(name, quoted);
1230
1597
  return quoted;
1231
1598
  }
1599
+ /**
1600
+ * MySQL rejects `LIMIT` inside an `IN` subquery with
1601
+ * `ER_NOT_SUPPORTED_YET: This version of MySQL doesn't yet support
1602
+ * 'LIMIT & IN/ALL/ANY/SOME subquery'`. Failing at compile time names the fix
1603
+ * instead of surfacing that error from the driver at runtime.
1604
+ */
1605
+ checkSubquery(node) {
1606
+ if (node.limit !== void 0 || node.offset !== void 0) {
1607
+ throw new Error(
1608
+ "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."
1609
+ );
1610
+ }
1611
+ }
1232
1612
  renderConflict(onConflict, conflictCols, nextValue, names) {
1233
1613
  if (onConflict.targetWhere || onConflict.updateWhere) {
1234
1614
  throw new Error(
@@ -1236,16 +1616,25 @@ var MysqlDialect = class extends BaseDialect {
1236
1616
  );
1237
1617
  }
1238
1618
  if (onConflict.update === "nothing") {
1239
- const col = this.columnId(onConflict.target[0] ?? "id", names);
1240
- return ` ON DUPLICATE KEY UPDATE ${col} = ${col}`;
1619
+ const col2 = this.columnId(onConflict.target[0] ?? "id", names);
1620
+ return ` ON DUPLICATE KEY UPDATE ${col2} = ${col2}`;
1241
1621
  }
1242
1622
  const assignments = conflictCols.map((c) => `${this.columnId(c, names)} = ${nextValue()}`).join(", ");
1243
1623
  return ` ON DUPLICATE KEY UPDATE ${assignments}`;
1244
1624
  }
1625
+ /**
1626
+ * MySQL has no `RETURNING`, so it cannot be compiled into a statement.
1627
+ *
1628
+ * `session.execute()` still honors `.returning()` on a **single-row INSERT** by
1629
+ * running the insert and reading the row back by key on the same connection —
1630
+ * that is execution, not compilation, so it never reaches here. Compiling a
1631
+ * node with `returning` directly is an error, rather than SQL that silently
1632
+ * returns nothing.
1633
+ */
1245
1634
  compileReturning(returning) {
1246
1635
  if (returning === null) return "";
1247
1636
  throw new Error(
1248
- "RETURNING is not supported on MySQL \u2014 insert, then SELECT by key (e.g. LAST_INSERT_ID())."
1637
+ "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
1638
  );
1250
1639
  }
1251
1640
  };
@@ -1358,8 +1747,8 @@ var RecordNotFound = class extends Error {
1358
1747
  }
1359
1748
  };
1360
1749
  function primaryKeyOf(model) {
1361
- for (const [name, col] of Object.entries(columnsOf(model))) {
1362
- if (col.flags.primaryKey) return name;
1750
+ for (const [name, col2] of Object.entries(columnsOf(model))) {
1751
+ if (col2.flags.primaryKey) return name;
1363
1752
  }
1364
1753
  throw new Error(`${model.tablename} has no primary key`);
1365
1754
  }
@@ -1447,8 +1836,8 @@ var BaseRepository = class {
1447
1836
 
1448
1837
  // src/active-record.ts
1449
1838
  function primaryKeyOf2(model) {
1450
- for (const [name, col] of Object.entries(columnsOf(model))) {
1451
- if (col.flags.primaryKey) return name;
1839
+ for (const [name, col2] of Object.entries(columnsOf(model))) {
1840
+ if (col2.flags.primaryKey) return name;
1452
1841
  }
1453
1842
  throw new Error(`${model.tablename} has no primary key`);
1454
1843
  }
@@ -1614,10 +2003,18 @@ var NodeSqliteDriver = class _NodeSqliteDriver {
1614
2003
  constructor(database) {
1615
2004
  this.db = database;
1616
2005
  }
1617
- /** Open a `node:sqlite` database at the given path (or `:memory:`). */
1618
- static open(path) {
2006
+ /**
2007
+ * Open a `node:sqlite` database at the given path (or `:memory:`).
2008
+ *
2009
+ * @param path The database file, or `":memory:"`.
2010
+ * @param options Passed straight to `DatabaseSync` (`readOnly`, `timeout`, …).
2011
+ * @returns A driver over the open handle.
2012
+ */
2013
+ static open(path, options) {
1619
2014
  const { DatabaseSync } = nodeRequire("node:sqlite");
1620
- return new _NodeSqliteDriver(new DatabaseSync(path));
2015
+ return new _NodeSqliteDriver(
2016
+ options ? new DatabaseSync(path, { ...options }) : new DatabaseSync(path)
2017
+ );
1621
2018
  }
1622
2019
  /** Return the cached prepared statement for `sql`, preparing it on first use. */
1623
2020
  // biome-ignore lint/suspicious/noExplicitAny: statement type is unavailable here.
@@ -1784,6 +2181,18 @@ var AsyncResult = class {
1784
2181
  }
1785
2182
  };
1786
2183
  var savepointCounter = 0;
2184
+ function needsInsertReadBack(dialect, node) {
2185
+ return dialect.name === "mysql" && node.kind === "insert" && node.returning !== null;
2186
+ }
2187
+ function singlePrimaryKey(model) {
2188
+ const keys = Object.entries(columnsOf(model)).filter(([, column2]) => column2.flags.primaryKey).map(([name]) => name);
2189
+ if (keys.length !== 1) {
2190
+ throw new Error(
2191
+ `${model.tablename} needs exactly one primary key to read an insert back on a dialect without RETURNING; found ${keys.length}.`
2192
+ );
2193
+ }
2194
+ return keys[0];
2195
+ }
1787
2196
  function assertRawParams(params) {
1788
2197
  if (!Array.isArray(params)) {
1789
2198
  throw new TypeError(
@@ -1856,10 +2265,10 @@ var SyncSession = class {
1856
2265
  return new SyncResult(rows, result.changes);
1857
2266
  }
1858
2267
  /** Run `fn` inside a transaction: commit on success, rollback on throw. */
1859
- transaction(fn) {
2268
+ transaction(fn2) {
1860
2269
  this.exec("BEGIN", []);
1861
2270
  try {
1862
- const out = fn(this);
2271
+ const out = fn2(this);
1863
2272
  this.exec("COMMIT", []);
1864
2273
  return out;
1865
2274
  } catch (error) {
@@ -1868,12 +2277,12 @@ var SyncSession = class {
1868
2277
  }
1869
2278
  }
1870
2279
  /** Run `fn` inside a SAVEPOINT (nested transaction). */
1871
- beginNested(fn) {
2280
+ beginNested(fn2) {
1872
2281
  savepointCounter += 1;
1873
2282
  const name = `qsp_${savepointCounter}`;
1874
2283
  this.exec(`SAVEPOINT ${name}`, []);
1875
2284
  try {
1876
- const out = fn(this);
2285
+ const out = fn2(this);
1877
2286
  this.exec(`RELEASE ${name}`, []);
1878
2287
  return out;
1879
2288
  } catch (error) {
@@ -1971,6 +2380,11 @@ var AsyncSession = class _AsyncSession {
1971
2380
  }
1972
2381
  execute(builder) {
1973
2382
  const node = builder.node;
2383
+ if (needsInsertReadBack(this.dialect, node)) {
2384
+ return new AsyncResult(
2385
+ this.insertAndReadBack(builder, node)
2386
+ );
2387
+ }
1974
2388
  const { sql: sql2, params } = this.dialect.compile(node);
1975
2389
  const inner = this.exec(sql2, params).then((result) => {
1976
2390
  const rows = mapRows(builder, result.rows);
@@ -1978,6 +2392,53 @@ var AsyncSession = class _AsyncSession {
1978
2392
  });
1979
2393
  return new AsyncResult(inner);
1980
2394
  }
2395
+ /**
2396
+ * Honor `.returning()` on a dialect without `RETURNING`, by inserting and then
2397
+ * reading the row back by key.
2398
+ *
2399
+ * Both statements must run on **one** connection, because `LAST_INSERT_ID()` is
2400
+ * per-connection: outside a transaction the pooled driver is reserved for the
2401
+ * pair; inside one, the session already holds a pinned connection (a reserved
2402
+ * driver exposes no `reserve`), so it runs there directly.
2403
+ *
2404
+ * @param builder The insert builder, for its source model.
2405
+ * @param node The insert AST, whose `returning` drives the read-back.
2406
+ * @returns The result view over the read-back row.
2407
+ * @throws Error When the insert writes more than one row — `LAST_INSERT_ID()`
2408
+ * identifies only the first, and the rest are consecutive only under some
2409
+ * auto-increment lock modes.
2410
+ */
2411
+ async insertAndReadBack(builder, node) {
2412
+ if (node.values.length !== 1) {
2413
+ throw new Error(
2414
+ `${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().`
2415
+ );
2416
+ }
2417
+ const model = builder.source;
2418
+ const pk = singlePrimaryKey(model);
2419
+ const supplied = node.values[0][pk];
2420
+ const readBack = select(model).where(
2421
+ supplied === void 0 || supplied === null ? col(pk).eq(fn.call("LAST_INSERT_ID")) : { [pk]: supplied }
2422
+ );
2423
+ const insertSql = this.dialect.compile({ ...node, returning: null });
2424
+ const selectSql = this.dialect.compile(
2425
+ node.returning === "*" || node.returning === null ? readBack.node : { ...readBack.node, columns: node.returning }
2426
+ );
2427
+ const run = async (driver) => {
2428
+ const scoped = new _AsyncSession(driver, this.dialect, this.logger);
2429
+ const written = await scoped.exec(insertSql.sql, insertSql.params);
2430
+ const read = await scoped.exec(selectSql.sql, selectSql.params);
2431
+ const rows = read.rows.map((row) => coerceRow(model, row));
2432
+ return new SyncResult(rows, written.changes);
2433
+ };
2434
+ if (!this.driver.reserve) return run(this.driver);
2435
+ const reserved = await this.driver.reserve();
2436
+ try {
2437
+ return await run(reserved);
2438
+ } finally {
2439
+ await reserved.release();
2440
+ }
2441
+ }
1981
2442
  /** Lazily iterate result rows. Uses driver streaming when available. */
1982
2443
  async *stream(builder) {
1983
2444
  const node = builder.node;
@@ -1998,13 +2459,13 @@ var AsyncSession = class _AsyncSession {
1998
2459
  yield coerceOne(builder, raw);
1999
2460
  }
2000
2461
  }
2001
- async transaction(fn) {
2462
+ async transaction(fn2) {
2002
2463
  if (this.driver.reserve) {
2003
2464
  const reserved = await this.driver.reserve();
2004
2465
  const scoped = new _AsyncSession(reserved, this.dialect, this.logger);
2005
2466
  try {
2006
2467
  await scoped.exec("BEGIN", []);
2007
- const out = await fn(scoped);
2468
+ const out = await fn2(scoped);
2008
2469
  await scoped.exec("COMMIT", []);
2009
2470
  return out;
2010
2471
  } catch (error) {
@@ -2016,7 +2477,7 @@ var AsyncSession = class _AsyncSession {
2016
2477
  }
2017
2478
  await this.exec("BEGIN", []);
2018
2479
  try {
2019
- const out = await fn(this);
2480
+ const out = await fn2(this);
2020
2481
  await this.exec("COMMIT", []);
2021
2482
  return out;
2022
2483
  } catch (error) {
@@ -2032,6 +2493,13 @@ var AsyncSession = class _AsyncSession {
2032
2493
  await this.close();
2033
2494
  }
2034
2495
  };
2496
+ function emitNotice(logger, notice) {
2497
+ if (!logger) return;
2498
+ try {
2499
+ logger(notice);
2500
+ } catch {
2501
+ }
2502
+ }
2035
2503
  var SyncEngine = class {
2036
2504
  constructor(driver, logger) {
2037
2505
  this.driver = driver;
@@ -2043,8 +2511,8 @@ var SyncEngine = class {
2043
2511
  session() {
2044
2512
  return new SyncSession(this.driver, getDialect("sqlite"), this.logger);
2045
2513
  }
2046
- transaction(fn) {
2047
- return this.session().transaction(fn);
2514
+ transaction(fn2) {
2515
+ return this.session().transaction(fn2);
2048
2516
  }
2049
2517
  close() {
2050
2518
  this.driver.close();
@@ -2066,8 +2534,8 @@ var AsyncEngine = class {
2066
2534
  session() {
2067
2535
  return new AsyncSession(this.driver, getDialect(this.dialect), this.logger);
2068
2536
  }
2069
- transaction(fn) {
2070
- return this.session().transaction(fn);
2537
+ transaction(fn2) {
2538
+ return this.session().transaction(fn2);
2071
2539
  }
2072
2540
  async close() {
2073
2541
  await this.driver.close();
@@ -2077,6 +2545,16 @@ var AsyncEngine = class {
2077
2545
  await this.close();
2078
2546
  }
2079
2547
  };
2548
+ function toAsyncDriver(driver) {
2549
+ return {
2550
+ async execute(sql2, params) {
2551
+ return await driver.execute(sql2, params);
2552
+ },
2553
+ async close() {
2554
+ await driver.close();
2555
+ }
2556
+ };
2557
+ }
2080
2558
  function asAsync(driver) {
2081
2559
  const syncIterate = driver.iterate?.bind(driver);
2082
2560
  return {
@@ -2089,8 +2567,8 @@ function asAsync(driver) {
2089
2567
  } : {}
2090
2568
  };
2091
2569
  }
2092
- function openSqliteDriver(path, _options) {
2093
- return NodeSqliteDriver.open(path);
2570
+ function openSqliteDriver(path, options) {
2571
+ return NodeSqliteDriver.open(path, options?.driverOptions);
2094
2572
  }
2095
2573
  function createSyncEngine(url, options) {
2096
2574
  const parsed = parseDatabaseUrl(url);
@@ -2100,7 +2578,7 @@ function createSyncEngine(url, options) {
2100
2578
  );
2101
2579
  }
2102
2580
  return new SyncEngine(
2103
- openSqliteDriver(parsed.database ?? ":memory:"),
2581
+ openSqliteDriver(parsed.database ?? ":memory:", options),
2104
2582
  options?.onQuery
2105
2583
  );
2106
2584
  }
@@ -2108,20 +2586,20 @@ function createEngine(url, options) {
2108
2586
  const parsed = parseDatabaseUrl(url);
2109
2587
  if (parsed.dialect === "sqlite") {
2110
2588
  return new AsyncEngine(
2111
- asAsync(openSqliteDriver(parsed.database ?? ":memory:")),
2589
+ asAsync(openSqliteDriver(parsed.database ?? ":memory:", options)),
2112
2590
  "sqlite",
2113
2591
  options?.onQuery
2114
2592
  );
2115
2593
  }
2116
2594
  if (parsed.dialect === "mysql") {
2117
2595
  return new AsyncEngine(
2118
- createMysqlDriver(parsed.raw, options?.pool),
2596
+ createMysqlDriver(parsed.raw, options),
2119
2597
  "mysql",
2120
2598
  options?.onQuery
2121
2599
  );
2122
2600
  }
2123
2601
  return new AsyncEngine(
2124
- createPostgresDriver(parsed.raw, options?.pool),
2602
+ createPostgresDriver(parsed.raw, options),
2125
2603
  "postgresql",
2126
2604
  options?.onQuery
2127
2605
  );
@@ -2141,7 +2619,8 @@ function toMysqlResult(rows) {
2141
2619
  const header = rows;
2142
2620
  return { rows: [], changes: header.affectedRows ?? 0 };
2143
2621
  }
2144
- function createMysqlDriver(url, pool) {
2622
+ function createMysqlDriver(url, options) {
2623
+ const pool = options?.pool;
2145
2624
  let poolHandle;
2146
2625
  const ensure = async () => {
2147
2626
  if (poolHandle) return;
@@ -2154,6 +2633,7 @@ function createMysqlDriver(url, pool) {
2154
2633
  if (pool?.size !== void 0) opts.connectionLimit = pool.size;
2155
2634
  if (pool?.idleTimeoutMs !== void 0) opts.idleTimeout = pool.idleTimeoutMs;
2156
2635
  if (pool?.connectTimeoutMs !== void 0) opts.connectTimeout = pool.connectTimeoutMs;
2636
+ Object.assign(opts, options?.driverOptions ?? {});
2157
2637
  poolHandle = mod.createPool(opts);
2158
2638
  };
2159
2639
  const runOn = async (queryable, sql2, params) => {
@@ -2187,7 +2667,8 @@ function toPostgresResult(rows) {
2187
2667
  const arr = rows;
2188
2668
  return { rows: Array.from(arr), changes: arr.count ?? arr.length };
2189
2669
  }
2190
- function createPostgresDriver(url, pool) {
2670
+ function createPostgresDriver(url, options) {
2671
+ const pool = options?.pool;
2191
2672
  let client;
2192
2673
  const ensure = async () => {
2193
2674
  if (client) return;
@@ -2203,6 +2684,8 @@ function createPostgresDriver(url, pool) {
2203
2684
  if (pool?.connectTimeoutMs !== void 0) {
2204
2685
  opts.connect_timeout = Math.ceil(pool.connectTimeoutMs / 1e3);
2205
2686
  }
2687
+ opts.onnotice = (notice) => emitNotice(options?.onNotice, notice);
2688
+ Object.assign(opts, options?.driverOptions ?? {});
2206
2689
  client = (mod.default ?? mod)(url, opts);
2207
2690
  };
2208
2691
  return {
@@ -2555,8 +3038,8 @@ function columnNamesOf(model) {
2555
3038
  const map = {};
2556
3039
  const seen = /* @__PURE__ */ new Map();
2557
3040
  let renamed = false;
2558
- for (const [prop, col] of Object.entries(columnsOf(model))) {
2559
- const dbName = col.dbName ?? (strategy === "snake_case" ? toSnakeCase(prop) : prop);
3041
+ for (const [prop, col2] of Object.entries(columnsOf(model))) {
3042
+ const dbName = col2.dbName ?? (strategy === "snake_case" ? toSnakeCase(prop) : prop);
2560
3043
  const collision = seen.get(dbName);
2561
3044
  if (collision !== void 0) {
2562
3045
  throw new Error(
@@ -2588,6 +3071,6 @@ function dbColumn(names, prop) {
2588
3071
  return names?.[prop] ?? prop;
2589
3072
  }
2590
3073
 
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
3074
+ 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 };
3075
+ //# sourceMappingURL=chunk-4AWUP7BM.js.map
3076
+ //# sourceMappingURL=chunk-4AWUP7BM.js.map