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.
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,19 +598,34 @@ 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);
406
613
  }
614
+ function assertConsistentRows(model, rows) {
615
+ if (rows.length < 2) return;
616
+ const union = /* @__PURE__ */ new Set();
617
+ for (const row of rows) for (const key of Object.keys(row)) union.add(key);
618
+ const inconsistent = [...union].filter((key) => rows.some((row) => !(key in row)));
619
+ if (inconsistent.length === 0) return;
620
+ const columns = columnsOf(model);
621
+ const defaulted = inconsistent.filter((key) => columns[key]?.flags.hasDefault);
622
+ if (defaulted.length === 0) return;
623
+ const named = defaulted.map((c) => `"${c}"`).join(", ");
624
+ const verb = defaulted.length === 1 ? "has" : "have";
625
+ throw new ValidationError(model.tablename, [
626
+ `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.`
627
+ ]);
628
+ }
407
629
  function describeValue(value) {
408
630
  if (typeof value === "function") return "a function";
409
631
  if (Array.isArray(value)) return "an array";
@@ -426,11 +648,13 @@ var InsertBuilder = class _InsertBuilder {
426
648
  * @param rows One row, or an array of rows.
427
649
  * @returns A builder carrying the rows.
428
650
  * @throws ValidationError When a value is not a column value the dialect can
429
- * bind (see the `sql` helpers for writing an expression instead).
651
+ * bind (see the `sql` helpers for writing an expression instead), or when the
652
+ * rows of a multi-row insert disagree about a column that has a default.
430
653
  */
431
654
  values(rows) {
432
655
  const list = Array.isArray(rows) ? rows : [rows];
433
656
  for (const row of list) assertWritableValues(this.source, row, "values");
657
+ assertConsistentRows(this.source, list);
434
658
  return this.with({ values: list });
435
659
  }
436
660
  /**
@@ -732,6 +956,18 @@ var Params = class {
732
956
  return this.placeholder(this.values.length);
733
957
  }
734
958
  };
959
+ function insertColumns(rows) {
960
+ const columns = [];
961
+ const seen = /* @__PURE__ */ new Set();
962
+ for (const row of rows) {
963
+ for (const key of Object.keys(row)) {
964
+ if (seen.has(key)) continue;
965
+ seen.add(key);
966
+ columns.push(key);
967
+ }
968
+ }
969
+ return columns;
970
+ }
735
971
  function insertHasExpression(node) {
736
972
  for (const row of node.values) {
737
973
  for (const value of Object.values(row)) {
@@ -755,6 +991,15 @@ var BaseDialect = class _BaseDialect {
755
991
  static insertTemplates = /* @__PURE__ */ new Map();
756
992
  /** Quoted-identifier cache (see {@link quoteId}). Shared across dialects. */
757
993
  static quotedIds = /* @__PURE__ */ new Map();
994
+ /**
995
+ * Validate a subquery operand before it is rendered, for dialects that restrict
996
+ * what an `IN (SELECT ...)` may contain. The default accepts everything.
997
+ *
998
+ * @param _node The subquery's AST.
999
+ * @throws Error When the dialect cannot execute this subquery.
1000
+ */
1001
+ checkSubquery(_node) {
1002
+ }
758
1003
  /**
759
1004
  * The SQL operator for an array containment/overlap test.
760
1005
  *
@@ -878,6 +1123,19 @@ var BaseDialect = class _BaseDialect {
878
1123
  return ` ${strength}${of}${wait}`;
879
1124
  }
880
1125
  // ---- statements -------------------------------------------------------
1126
+ /**
1127
+ * Compile a SELECT.
1128
+ *
1129
+ * Two alias rules differ between clauses and are handled here: PostgreSQL does
1130
+ * NOT accept a `SELECT` alias in `HAVING`, so an aggregate key is re-emitted as
1131
+ * its expression (`COUNT(*) > $1`), a form every dialect accepts; `ORDER BY`,
1132
+ * by contrast, accepts the output alias everywhere, so it is emitted as
1133
+ * written.
1134
+ *
1135
+ * @param node The select AST.
1136
+ * @param params The parameter collector.
1137
+ * @returns The SQL text.
1138
+ */
881
1139
  compileSelect(node, params) {
882
1140
  const names = node.names;
883
1141
  let cols;
@@ -901,10 +1159,21 @@ var BaseDialect = class _BaseDialect {
901
1159
  if (node.groupBy.length > 0) {
902
1160
  sql2 += ` GROUP BY ${node.groupBy.map((c) => this.columnId(c, names)).join(", ")}`;
903
1161
  }
1162
+ const aggByAlias = new Map(node.aggregates.map((a) => [a.alias, a]));
1163
+ if (node.having) {
1164
+ const having = this.compileCondition(node.having, params, (key) => {
1165
+ const agg = aggByAlias.get(key);
1166
+ if (!agg) return this.columnId(key, names);
1167
+ const inner = agg.column === "*" ? "*" : this.columnId(agg.column, names);
1168
+ return `${agg.fn.toUpperCase()}(${inner})`;
1169
+ });
1170
+ if (having) sql2 += ` HAVING ${having}`;
1171
+ }
904
1172
  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(", ");
1173
+ const terms = node.orderBy.map((t) => {
1174
+ const id = aggByAlias.has(t.column) ? this.quoteId(t.column) : this.columnId(t.column, names);
1175
+ return `${id} ${t.direction === "desc" ? "DESC" : "ASC"}`;
1176
+ }).join(", ");
908
1177
  sql2 += ` ORDER BY ${terms}`;
909
1178
  }
910
1179
  if (node.limit !== void 0) sql2 += ` LIMIT ${params.bind(node.limit)}`;
@@ -928,7 +1197,7 @@ var BaseDialect = class _BaseDialect {
928
1197
  * SQL order, so placeholder positions stay correct.
929
1198
  */
930
1199
  compileInsert(node, params) {
931
- const columns = node.values.length > 0 ? Object.keys(node.values[0]) : [];
1200
+ const columns = insertColumns(node.values);
932
1201
  const conflict = node.onConflict;
933
1202
  const cacheable = conflict?.targetWhere === void 0 && conflict?.updateWhere === void 0 && !insertHasExpression(node);
934
1203
  if (!cacheable) return this.compileInsertDirect(node, columns, params);
@@ -1040,7 +1309,7 @@ var BaseDialect = class _BaseDialect {
1040
1309
  compileUpdate(node, params) {
1041
1310
  const names = node.names;
1042
1311
  const sets = Object.entries(node.set).map(
1043
- ([col, value]) => `${this.columnId(col, names)} = ${this.renderValue(value, params)}`
1312
+ ([col2, value]) => `${this.columnId(col2, names)} = ${this.renderValue(value, params)}`
1044
1313
  ).join(", ");
1045
1314
  let sql2 = `UPDATE ${this.quoteId(node.table)} SET ${sets}`;
1046
1315
  const where = this.compileCondition(
@@ -1133,6 +1402,83 @@ var BaseDialect = class _BaseDialect {
1133
1402
  const inner = this.compileCondition(node.part, params, idFor);
1134
1403
  return inner ? `NOT (${inner})` : "";
1135
1404
  }
1405
+ case "compare": {
1406
+ const left = this.renderExpr(node.left, params, idFor);
1407
+ if (node.right.kind === "value") {
1408
+ return this.compileOperator(left, node.op, node.right.value, params);
1409
+ }
1410
+ return this.compileExprOperator(
1411
+ left,
1412
+ node.op,
1413
+ this.renderExpr(node.right, params, idFor)
1414
+ );
1415
+ }
1416
+ }
1417
+ }
1418
+ /**
1419
+ * Render one side of a comparison.
1420
+ *
1421
+ * A column reference goes through `idFor`, so an explicit `.name()` mapping and
1422
+ * join qualification apply here exactly as they do in the object form of
1423
+ * `where` — `col()` is not a way around them. Only a `value` node binds.
1424
+ *
1425
+ * @param node The expression AST.
1426
+ * @param params The parameter collector.
1427
+ * @param idFor The identifier resolver for the enclosing statement.
1428
+ * @returns The SQL text of the expression.
1429
+ */
1430
+ renderExpr(node, params, idFor) {
1431
+ switch (node.kind) {
1432
+ case "column":
1433
+ return idFor(node.name);
1434
+ case "value":
1435
+ return params.bind(node.value);
1436
+ case "fn": {
1437
+ const args = node.args.map((a) => this.renderExpr(a, params, idFor)).join(", ");
1438
+ return `${node.name}(${args})`;
1439
+ }
1440
+ }
1441
+ }
1442
+ /**
1443
+ * Compile a comparison whose right-hand side is another expression rather than
1444
+ * a bound value (`total > paid`, `lower(a) = lower(b)`).
1445
+ *
1446
+ * The list and null operators are excluded: `IN`, `BETWEEN` and `IS NULL` take
1447
+ * a value operand, and accepting an expression there would silently compile to
1448
+ * something else.
1449
+ *
1450
+ * @param left The rendered left-hand side.
1451
+ * @param op The operator name.
1452
+ * @param right The rendered right-hand side.
1453
+ * @returns The SQL text of the predicate.
1454
+ * @throws Error When the operator needs a value operand.
1455
+ */
1456
+ compileExprOperator(left, op, right) {
1457
+ switch (op) {
1458
+ case "eq":
1459
+ return `${left} = ${right}`;
1460
+ case "ne":
1461
+ return `${left} <> ${right}`;
1462
+ case "gt":
1463
+ return `${left} > ${right}`;
1464
+ case "gte":
1465
+ return `${left} >= ${right}`;
1466
+ case "lt":
1467
+ return `${left} < ${right}`;
1468
+ case "lte":
1469
+ return `${left} <= ${right}`;
1470
+ case "like":
1471
+ return `${left} LIKE ${right}`;
1472
+ case "ilike":
1473
+ return this.ilike(left, right);
1474
+ case "ieq":
1475
+ return `lower(${left}) = lower(${right})`;
1476
+ case "contains":
1477
+ case "containedBy":
1478
+ case "overlaps":
1479
+ return `${left} ${this.arrayOperator(op)} ${right}`;
1480
+ default:
1481
+ throw new Error(`The "${op}" operator takes a value operand, not an expression.`);
1136
1482
  }
1137
1483
  }
1138
1484
  compileOperator(id, op, operand, params) {
@@ -1175,12 +1521,33 @@ var BaseDialect = class _BaseDialect {
1175
1521
  throw new Error(`Unknown operator ${JSON.stringify(op)}`);
1176
1522
  }
1177
1523
  }
1178
- compileIn(id, values, params, negate) {
1524
+ /**
1525
+ * Compile `IN` / `NOT IN`, whose operand is either a value list or a
1526
+ * single-column subquery.
1527
+ *
1528
+ * The subquery is rendered at the position it appears in the outer statement
1529
+ * and shares the same parameter collector, so its own placeholders land in the
1530
+ * right order — and it keeps its own `names` map, since the inner model may use
1531
+ * a different naming convention than the outer one.
1532
+ *
1533
+ * @param id The quoted column identifier being tested.
1534
+ * @param operand A list of values, or a {@link Subquery}.
1535
+ * @param params The parameter collector for the statement being compiled.
1536
+ * @param negate True for `NOT IN`.
1537
+ * @returns The SQL text of the predicate.
1538
+ */
1539
+ compileIn(id, operand, params, negate) {
1540
+ const keyword = negate ? "NOT IN" : "IN";
1541
+ if (isSubquery(operand)) {
1542
+ this.checkSubquery(operand.node);
1543
+ return `${id} ${keyword} (${this.compileSelect(operand.node, params)})`;
1544
+ }
1545
+ const values = operand;
1179
1546
  if (values.length === 0) {
1180
1547
  return negate ? "1 = 1" : "1 = 0";
1181
1548
  }
1182
1549
  const list = values.map((v) => params.bind(v)).join(", ");
1183
- return `${id} ${negate ? "NOT IN" : "IN"} (${list})`;
1550
+ return `${id} ${keyword} (${list})`;
1184
1551
  }
1185
1552
  };
1186
1553
  var SqliteDialect = class extends BaseDialect {
@@ -1232,6 +1599,19 @@ var MysqlDialect = class extends BaseDialect {
1232
1599
  mysqlQuotedIds.set(name, quoted);
1233
1600
  return quoted;
1234
1601
  }
1602
+ /**
1603
+ * MySQL rejects `LIMIT` inside an `IN` subquery with
1604
+ * `ER_NOT_SUPPORTED_YET: This version of MySQL doesn't yet support
1605
+ * 'LIMIT & IN/ALL/ANY/SOME subquery'`. Failing at compile time names the fix
1606
+ * instead of surfacing that error from the driver at runtime.
1607
+ */
1608
+ checkSubquery(node) {
1609
+ if (node.limit !== void 0 || node.offset !== void 0) {
1610
+ throw new Error(
1611
+ "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."
1612
+ );
1613
+ }
1614
+ }
1235
1615
  renderConflict(onConflict, conflictCols, nextValue, names) {
1236
1616
  if (onConflict.targetWhere || onConflict.updateWhere) {
1237
1617
  throw new Error(
@@ -1239,16 +1619,25 @@ var MysqlDialect = class extends BaseDialect {
1239
1619
  );
1240
1620
  }
1241
1621
  if (onConflict.update === "nothing") {
1242
- const col = this.columnId(onConflict.target[0] ?? "id", names);
1243
- return ` ON DUPLICATE KEY UPDATE ${col} = ${col}`;
1622
+ const col2 = this.columnId(onConflict.target[0] ?? "id", names);
1623
+ return ` ON DUPLICATE KEY UPDATE ${col2} = ${col2}`;
1244
1624
  }
1245
1625
  const assignments = conflictCols.map((c) => `${this.columnId(c, names)} = ${nextValue()}`).join(", ");
1246
1626
  return ` ON DUPLICATE KEY UPDATE ${assignments}`;
1247
1627
  }
1628
+ /**
1629
+ * MySQL has no `RETURNING`, so it cannot be compiled into a statement.
1630
+ *
1631
+ * `session.execute()` still honors `.returning()` on a **single-row INSERT** by
1632
+ * running the insert and reading the row back by key on the same connection —
1633
+ * that is execution, not compilation, so it never reaches here. Compiling a
1634
+ * node with `returning` directly is an error, rather than SQL that silently
1635
+ * returns nothing.
1636
+ */
1248
1637
  compileReturning(returning) {
1249
1638
  if (returning === null) return "";
1250
1639
  throw new Error(
1251
- "RETURNING is not supported on MySQL \u2014 insert, then SELECT by key (e.g. LAST_INSERT_ID())."
1640
+ "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
1641
  );
1253
1642
  }
1254
1643
  };
@@ -1361,8 +1750,8 @@ var RecordNotFound = class extends Error {
1361
1750
  }
1362
1751
  };
1363
1752
  function primaryKeyOf(model) {
1364
- for (const [name, col] of Object.entries(columnsOf(model))) {
1365
- if (col.flags.primaryKey) return name;
1753
+ for (const [name, col2] of Object.entries(columnsOf(model))) {
1754
+ if (col2.flags.primaryKey) return name;
1366
1755
  }
1367
1756
  throw new Error(`${model.tablename} has no primary key`);
1368
1757
  }
@@ -1450,8 +1839,8 @@ var BaseRepository = class {
1450
1839
 
1451
1840
  // src/active-record.ts
1452
1841
  function primaryKeyOf2(model) {
1453
- for (const [name, col] of Object.entries(columnsOf(model))) {
1454
- if (col.flags.primaryKey) return name;
1842
+ for (const [name, col2] of Object.entries(columnsOf(model))) {
1843
+ if (col2.flags.primaryKey) return name;
1455
1844
  }
1456
1845
  throw new Error(`${model.tablename} has no primary key`);
1457
1846
  }
@@ -1617,10 +2006,18 @@ var NodeSqliteDriver = class _NodeSqliteDriver {
1617
2006
  constructor(database) {
1618
2007
  this.db = database;
1619
2008
  }
1620
- /** Open a `node:sqlite` database at the given path (or `:memory:`). */
1621
- static open(path) {
2009
+ /**
2010
+ * Open a `node:sqlite` database at the given path (or `:memory:`).
2011
+ *
2012
+ * @param path The database file, or `":memory:"`.
2013
+ * @param options Passed straight to `DatabaseSync` (`readOnly`, `timeout`, …).
2014
+ * @returns A driver over the open handle.
2015
+ */
2016
+ static open(path, options) {
1622
2017
  const { DatabaseSync } = nodeRequire("node:sqlite");
1623
- return new _NodeSqliteDriver(new DatabaseSync(path));
2018
+ return new _NodeSqliteDriver(
2019
+ options ? new DatabaseSync(path, { ...options }) : new DatabaseSync(path)
2020
+ );
1624
2021
  }
1625
2022
  /** Return the cached prepared statement for `sql`, preparing it on first use. */
1626
2023
  // biome-ignore lint/suspicious/noExplicitAny: statement type is unavailable here.
@@ -1787,6 +2184,18 @@ var AsyncResult = class {
1787
2184
  }
1788
2185
  };
1789
2186
  var savepointCounter = 0;
2187
+ function needsInsertReadBack(dialect, node) {
2188
+ return dialect.name === "mysql" && node.kind === "insert" && node.returning !== null;
2189
+ }
2190
+ function singlePrimaryKey(model) {
2191
+ const keys = Object.entries(columnsOf(model)).filter(([, column2]) => column2.flags.primaryKey).map(([name]) => name);
2192
+ if (keys.length !== 1) {
2193
+ throw new Error(
2194
+ `${model.tablename} needs exactly one primary key to read an insert back on a dialect without RETURNING; found ${keys.length}.`
2195
+ );
2196
+ }
2197
+ return keys[0];
2198
+ }
1790
2199
  function assertRawParams(params) {
1791
2200
  if (!Array.isArray(params)) {
1792
2201
  throw new TypeError(
@@ -1859,10 +2268,10 @@ var SyncSession = class {
1859
2268
  return new SyncResult(rows, result.changes);
1860
2269
  }
1861
2270
  /** Run `fn` inside a transaction: commit on success, rollback on throw. */
1862
- transaction(fn) {
2271
+ transaction(fn2) {
1863
2272
  this.exec("BEGIN", []);
1864
2273
  try {
1865
- const out = fn(this);
2274
+ const out = fn2(this);
1866
2275
  this.exec("COMMIT", []);
1867
2276
  return out;
1868
2277
  } catch (error) {
@@ -1871,12 +2280,12 @@ var SyncSession = class {
1871
2280
  }
1872
2281
  }
1873
2282
  /** Run `fn` inside a SAVEPOINT (nested transaction). */
1874
- beginNested(fn) {
2283
+ beginNested(fn2) {
1875
2284
  savepointCounter += 1;
1876
2285
  const name = `qsp_${savepointCounter}`;
1877
2286
  this.exec(`SAVEPOINT ${name}`, []);
1878
2287
  try {
1879
- const out = fn(this);
2288
+ const out = fn2(this);
1880
2289
  this.exec(`RELEASE ${name}`, []);
1881
2290
  return out;
1882
2291
  } catch (error) {
@@ -1974,6 +2383,11 @@ var AsyncSession = class _AsyncSession {
1974
2383
  }
1975
2384
  execute(builder) {
1976
2385
  const node = builder.node;
2386
+ if (needsInsertReadBack(this.dialect, node)) {
2387
+ return new AsyncResult(
2388
+ this.insertAndReadBack(builder, node)
2389
+ );
2390
+ }
1977
2391
  const { sql: sql2, params } = this.dialect.compile(node);
1978
2392
  const inner = this.exec(sql2, params).then((result) => {
1979
2393
  const rows = mapRows(builder, result.rows);
@@ -1981,6 +2395,53 @@ var AsyncSession = class _AsyncSession {
1981
2395
  });
1982
2396
  return new AsyncResult(inner);
1983
2397
  }
2398
+ /**
2399
+ * Honor `.returning()` on a dialect without `RETURNING`, by inserting and then
2400
+ * reading the row back by key.
2401
+ *
2402
+ * Both statements must run on **one** connection, because `LAST_INSERT_ID()` is
2403
+ * per-connection: outside a transaction the pooled driver is reserved for the
2404
+ * pair; inside one, the session already holds a pinned connection (a reserved
2405
+ * driver exposes no `reserve`), so it runs there directly.
2406
+ *
2407
+ * @param builder The insert builder, for its source model.
2408
+ * @param node The insert AST, whose `returning` drives the read-back.
2409
+ * @returns The result view over the read-back row.
2410
+ * @throws Error When the insert writes more than one row — `LAST_INSERT_ID()`
2411
+ * identifies only the first, and the rest are consecutive only under some
2412
+ * auto-increment lock modes.
2413
+ */
2414
+ async insertAndReadBack(builder, node) {
2415
+ if (node.values.length !== 1) {
2416
+ throw new Error(
2417
+ `${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().`
2418
+ );
2419
+ }
2420
+ const model = builder.source;
2421
+ const pk = singlePrimaryKey(model);
2422
+ const supplied = node.values[0][pk];
2423
+ const readBack = select(model).where(
2424
+ supplied === void 0 || supplied === null ? col(pk).eq(fn.call("LAST_INSERT_ID")) : { [pk]: supplied }
2425
+ );
2426
+ const insertSql = this.dialect.compile({ ...node, returning: null });
2427
+ const selectSql = this.dialect.compile(
2428
+ node.returning === "*" || node.returning === null ? readBack.node : { ...readBack.node, columns: node.returning }
2429
+ );
2430
+ const run = async (driver) => {
2431
+ const scoped = new _AsyncSession(driver, this.dialect, this.logger);
2432
+ const written = await scoped.exec(insertSql.sql, insertSql.params);
2433
+ const read = await scoped.exec(selectSql.sql, selectSql.params);
2434
+ const rows = read.rows.map((row) => coerceRow(model, row));
2435
+ return new SyncResult(rows, written.changes);
2436
+ };
2437
+ if (!this.driver.reserve) return run(this.driver);
2438
+ const reserved = await this.driver.reserve();
2439
+ try {
2440
+ return await run(reserved);
2441
+ } finally {
2442
+ await reserved.release();
2443
+ }
2444
+ }
1984
2445
  /** Lazily iterate result rows. Uses driver streaming when available. */
1985
2446
  async *stream(builder) {
1986
2447
  const node = builder.node;
@@ -2001,13 +2462,13 @@ var AsyncSession = class _AsyncSession {
2001
2462
  yield coerceOne(builder, raw);
2002
2463
  }
2003
2464
  }
2004
- async transaction(fn) {
2465
+ async transaction(fn2) {
2005
2466
  if (this.driver.reserve) {
2006
2467
  const reserved = await this.driver.reserve();
2007
2468
  const scoped = new _AsyncSession(reserved, this.dialect, this.logger);
2008
2469
  try {
2009
2470
  await scoped.exec("BEGIN", []);
2010
- const out = await fn(scoped);
2471
+ const out = await fn2(scoped);
2011
2472
  await scoped.exec("COMMIT", []);
2012
2473
  return out;
2013
2474
  } catch (error) {
@@ -2019,7 +2480,7 @@ var AsyncSession = class _AsyncSession {
2019
2480
  }
2020
2481
  await this.exec("BEGIN", []);
2021
2482
  try {
2022
- const out = await fn(this);
2483
+ const out = await fn2(this);
2023
2484
  await this.exec("COMMIT", []);
2024
2485
  return out;
2025
2486
  } catch (error) {
@@ -2035,6 +2496,13 @@ var AsyncSession = class _AsyncSession {
2035
2496
  await this.close();
2036
2497
  }
2037
2498
  };
2499
+ function emitNotice(logger, notice) {
2500
+ if (!logger) return;
2501
+ try {
2502
+ logger(notice);
2503
+ } catch {
2504
+ }
2505
+ }
2038
2506
  var SyncEngine = class {
2039
2507
  constructor(driver, logger) {
2040
2508
  this.driver = driver;
@@ -2046,8 +2514,8 @@ var SyncEngine = class {
2046
2514
  session() {
2047
2515
  return new SyncSession(this.driver, getDialect("sqlite"), this.logger);
2048
2516
  }
2049
- transaction(fn) {
2050
- return this.session().transaction(fn);
2517
+ transaction(fn2) {
2518
+ return this.session().transaction(fn2);
2051
2519
  }
2052
2520
  close() {
2053
2521
  this.driver.close();
@@ -2069,8 +2537,8 @@ var AsyncEngine = class {
2069
2537
  session() {
2070
2538
  return new AsyncSession(this.driver, getDialect(this.dialect), this.logger);
2071
2539
  }
2072
- transaction(fn) {
2073
- return this.session().transaction(fn);
2540
+ transaction(fn2) {
2541
+ return this.session().transaction(fn2);
2074
2542
  }
2075
2543
  async close() {
2076
2544
  await this.driver.close();
@@ -2080,6 +2548,16 @@ var AsyncEngine = class {
2080
2548
  await this.close();
2081
2549
  }
2082
2550
  };
2551
+ function toAsyncDriver(driver) {
2552
+ return {
2553
+ async execute(sql2, params) {
2554
+ return await driver.execute(sql2, params);
2555
+ },
2556
+ async close() {
2557
+ await driver.close();
2558
+ }
2559
+ };
2560
+ }
2083
2561
  function asAsync(driver) {
2084
2562
  const syncIterate = driver.iterate?.bind(driver);
2085
2563
  return {
@@ -2092,8 +2570,8 @@ function asAsync(driver) {
2092
2570
  } : {}
2093
2571
  };
2094
2572
  }
2095
- function openSqliteDriver(path, _options) {
2096
- return NodeSqliteDriver.open(path);
2573
+ function openSqliteDriver(path, options) {
2574
+ return NodeSqliteDriver.open(path, options?.driverOptions);
2097
2575
  }
2098
2576
  function createSyncEngine(url, options) {
2099
2577
  const parsed = parseDatabaseUrl(url);
@@ -2103,7 +2581,7 @@ function createSyncEngine(url, options) {
2103
2581
  );
2104
2582
  }
2105
2583
  return new SyncEngine(
2106
- openSqliteDriver(parsed.database ?? ":memory:"),
2584
+ openSqliteDriver(parsed.database ?? ":memory:", options),
2107
2585
  options?.onQuery
2108
2586
  );
2109
2587
  }
@@ -2111,20 +2589,20 @@ function createEngine(url, options) {
2111
2589
  const parsed = parseDatabaseUrl(url);
2112
2590
  if (parsed.dialect === "sqlite") {
2113
2591
  return new AsyncEngine(
2114
- asAsync(openSqliteDriver(parsed.database ?? ":memory:")),
2592
+ asAsync(openSqliteDriver(parsed.database ?? ":memory:", options)),
2115
2593
  "sqlite",
2116
2594
  options?.onQuery
2117
2595
  );
2118
2596
  }
2119
2597
  if (parsed.dialect === "mysql") {
2120
2598
  return new AsyncEngine(
2121
- createMysqlDriver(parsed.raw, options?.pool),
2599
+ createMysqlDriver(parsed.raw, options),
2122
2600
  "mysql",
2123
2601
  options?.onQuery
2124
2602
  );
2125
2603
  }
2126
2604
  return new AsyncEngine(
2127
- createPostgresDriver(parsed.raw, options?.pool),
2605
+ createPostgresDriver(parsed.raw, options),
2128
2606
  "postgresql",
2129
2607
  options?.onQuery
2130
2608
  );
@@ -2144,7 +2622,8 @@ function toMysqlResult(rows) {
2144
2622
  const header = rows;
2145
2623
  return { rows: [], changes: header.affectedRows ?? 0 };
2146
2624
  }
2147
- function createMysqlDriver(url, pool) {
2625
+ function createMysqlDriver(url, options) {
2626
+ const pool = options?.pool;
2148
2627
  let poolHandle;
2149
2628
  const ensure = async () => {
2150
2629
  if (poolHandle) return;
@@ -2157,6 +2636,7 @@ function createMysqlDriver(url, pool) {
2157
2636
  if (pool?.size !== void 0) opts.connectionLimit = pool.size;
2158
2637
  if (pool?.idleTimeoutMs !== void 0) opts.idleTimeout = pool.idleTimeoutMs;
2159
2638
  if (pool?.connectTimeoutMs !== void 0) opts.connectTimeout = pool.connectTimeoutMs;
2639
+ Object.assign(opts, options?.driverOptions ?? {});
2160
2640
  poolHandle = mod.createPool(opts);
2161
2641
  };
2162
2642
  const runOn = async (queryable, sql2, params) => {
@@ -2190,7 +2670,8 @@ function toPostgresResult(rows) {
2190
2670
  const arr = rows;
2191
2671
  return { rows: Array.from(arr), changes: arr.count ?? arr.length };
2192
2672
  }
2193
- function createPostgresDriver(url, pool) {
2673
+ function createPostgresDriver(url, options) {
2674
+ const pool = options?.pool;
2194
2675
  let client;
2195
2676
  const ensure = async () => {
2196
2677
  if (client) return;
@@ -2206,6 +2687,8 @@ function createPostgresDriver(url, pool) {
2206
2687
  if (pool?.connectTimeoutMs !== void 0) {
2207
2688
  opts.connect_timeout = Math.ceil(pool.connectTimeoutMs / 1e3);
2208
2689
  }
2690
+ opts.onnotice = (notice) => emitNotice(options?.onNotice, notice);
2691
+ Object.assign(opts, options?.driverOptions ?? {});
2209
2692
  client = (mod.default ?? mod)(url, opts);
2210
2693
  };
2211
2694
  return {
@@ -2558,8 +3041,8 @@ function columnNamesOf(model) {
2558
3041
  const map = {};
2559
3042
  const seen = /* @__PURE__ */ new Map();
2560
3043
  let renamed = false;
2561
- for (const [prop, col] of Object.entries(columnsOf(model))) {
2562
- const dbName = col.dbName ?? (strategy === "snake_case" ? toSnakeCase(prop) : prop);
3044
+ for (const [prop, col2] of Object.entries(columnsOf(model))) {
3045
+ const dbName = col2.dbName ?? (strategy === "snake_case" ? toSnakeCase(prop) : prop);
2563
3046
  const collision = seen.get(dbName);
2564
3047
  if (collision !== void 0) {
2565
3048
  throw new Error(
@@ -2600,6 +3083,7 @@ exports.BaseDialect = BaseDialect;
2600
3083
  exports.BaseRepository = BaseRepository;
2601
3084
  exports.Column = Column;
2602
3085
  exports.DeleteBuilder = DeleteBuilder;
3086
+ exports.Expression = Expression;
2603
3087
  exports.InsertBuilder = InsertBuilder;
2604
3088
  exports.InvalidDatabaseUrl = InvalidDatabaseUrl;
2605
3089
  exports.JoinBuilder = JoinBuilder;
@@ -2623,6 +3107,7 @@ exports.activeRecord = activeRecord;
2623
3107
  exports.and = and;
2624
3108
  exports.avg = avg;
2625
3109
  exports.belongsTo = belongsTo;
3110
+ exports.col = col;
2626
3111
  exports.column = column;
2627
3112
  exports.columnNamesOf = columnNamesOf;
2628
3113
  exports.columnPropsOf = columnPropsOf;
@@ -2633,13 +3118,16 @@ exports.createSyncEngine = createSyncEngine;
2633
3118
  exports.dbColumn = dbColumn;
2634
3119
  exports.del = del;
2635
3120
  exports.detectDialect = detectDialect;
3121
+ exports.fn = fn;
2636
3122
  exports.foreignKey = foreignKey;
2637
3123
  exports.fromDict = fromDict;
2638
3124
  exports.getDialect = getDialect;
2639
3125
  exports.hasMany = hasMany;
2640
3126
  exports.insert = insert;
2641
3127
  exports.isCondition = isCondition;
3128
+ exports.isExpression = isExpression;
2642
3129
  exports.isSqlExpression = isSqlExpression;
3130
+ exports.isSubquery = isSubquery;
2643
3131
  exports.join = join;
2644
3132
  exports.loadRelations = loadRelations;
2645
3133
  exports.max = max;
@@ -2652,11 +3140,13 @@ exports.select = select;
2652
3140
  exports.sql = sql;
2653
3141
  exports.stringify = stringify;
2654
3142
  exports.sum = sum;
3143
+ exports.toAsyncDriver = toAsyncDriver;
2655
3144
  exports.toCondNode = toCondNode;
2656
3145
  exports.toDict = toDict;
2657
3146
  exports.toJSON = toJSON;
2658
3147
  exports.toSnakeCase = toSnakeCase;
2659
3148
  exports.unique = unique;
2660
3149
  exports.update = update;
3150
+ exports.val = val;
2661
3151
  //# sourceMappingURL=index.cjs.map
2662
3152
  //# sourceMappingURL=index.cjs.map