tempest-db-js 0.1.0 → 0.3.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
@@ -45,6 +45,29 @@ var OPERATORS = [
45
45
  "between",
46
46
  "isNull"
47
47
  ];
48
+ var Agg = class {
49
+ constructor(fn, column2) {
50
+ this.fn = fn;
51
+ this.column = column2;
52
+ }
53
+ fn;
54
+ column;
55
+ };
56
+ function count() {
57
+ return new Agg("count", "*");
58
+ }
59
+ function sum(column2) {
60
+ return new Agg("sum", column2);
61
+ }
62
+ function avg(column2) {
63
+ return new Agg("avg", column2);
64
+ }
65
+ function min(column2) {
66
+ return new Agg("min", column2);
67
+ }
68
+ function max(column2) {
69
+ return new Agg("max", column2);
70
+ }
48
71
  var SelectBuilder = class _SelectBuilder {
49
72
  constructor(node, source) {
50
73
  this.node = node;
@@ -59,6 +82,37 @@ var SelectBuilder = class _SelectBuilder {
59
82
  where(input) {
60
83
  return this.with({ where: toCondNode(input) });
61
84
  }
85
+ /** Emit `SELECT DISTINCT` — drop duplicate rows. */
86
+ distinct() {
87
+ return this.with({ distinct: true });
88
+ }
89
+ /**
90
+ * Group by columns and compute aggregates. The result row is the grouped
91
+ * columns (typed from the model) plus one field per aggregate alias.
92
+ *
93
+ * @param groupBy The columns to group by (checked against the model). Pass `[]`
94
+ * for a whole-table aggregate.
95
+ * @param spec A map of result alias → aggregate expression ({@link count},
96
+ * {@link sum}, {@link avg}, {@link min}, {@link max}).
97
+ * @returns A builder whose row is `Pick<Full, K> & { [alias]: aggResult }`.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * select(Order).aggregate(["status"], { n: count(), total: sum("amount") });
102
+ * // rows: { status: string; n: number; total: number | null }[]
103
+ * ```
104
+ */
105
+ aggregate(groupBy, spec) {
106
+ const aggregates = Object.entries(spec).map(([alias, agg]) => ({
107
+ fn: agg.fn,
108
+ column: agg.column,
109
+ alias
110
+ }));
111
+ return new _SelectBuilder(
112
+ { ...this.node, aggregates, groupBy },
113
+ this.source
114
+ );
115
+ }
62
116
  /** Order by a column of `Full`. */
63
117
  orderBy(column2, direction = "asc") {
64
118
  return this.with({
@@ -80,6 +134,9 @@ function select(model, columns) {
80
134
  kind: "select",
81
135
  table: model.tablename,
82
136
  columns: columns ?? "*",
137
+ distinct: false,
138
+ aggregates: [],
139
+ groupBy: [],
83
140
  where: void 0,
84
141
  orderBy: [],
85
142
  limit: void 0,
@@ -105,6 +162,25 @@ var InsertBuilder = class _InsertBuilder {
105
162
  const list = Array.isArray(rows) ? rows : [rows];
106
163
  return this.with({ values: list });
107
164
  }
165
+ /**
166
+ * On a unique/PK conflict on `target`, do nothing (skip the row).
167
+ *
168
+ * @param target The conflicting column(s) — a unique or primary key.
169
+ */
170
+ onConflictDoNothing(target) {
171
+ return this.with({ onConflict: { target, update: "nothing" } });
172
+ }
173
+ /**
174
+ * On a unique/PK conflict on `target`, overwrite the given columns (upsert).
175
+ *
176
+ * @param target The conflicting column(s) — a unique or primary key.
177
+ * @param set The columns to update with new values.
178
+ */
179
+ onConflictDoUpdate(target, set) {
180
+ return this.with({
181
+ onConflict: { target, update: set }
182
+ });
183
+ }
108
184
  returning(columns) {
109
185
  return this.with({ returning: columns ?? "*" });
110
186
  }
@@ -212,7 +288,9 @@ var DIALECT_ALIASES = {
212
288
  sqlite3: "sqlite",
213
289
  postgresql: "postgresql",
214
290
  postgres: "postgresql",
215
- pg: "postgresql"
291
+ pg: "postgresql",
292
+ mysql: "mysql",
293
+ mariadb: "mysql"
216
294
  };
217
295
  function splitScheme(scheme) {
218
296
  const plus = scheme.indexOf("+");
@@ -252,10 +330,10 @@ function parseSqlite(raw, driver, rest) {
252
330
  raw
253
331
  };
254
332
  }
255
- function parsePostgres(raw, driver, rest) {
333
+ function parseNetworkUrl(raw, driver, rest, dialect) {
256
334
  let parsed;
257
335
  try {
258
- parsed = new URL(`postgresql:${rest}`);
336
+ parsed = new URL(`${dialect}:${rest}`);
259
337
  } catch {
260
338
  throw new InvalidDatabaseUrl(raw, "could not parse host/credentials");
261
339
  }
@@ -263,7 +341,7 @@ function parsePostgres(raw, driver, rest) {
263
341
  const options = {};
264
342
  for (const [key, value] of parsed.searchParams) options[key] = value;
265
343
  return {
266
- dialect: "postgresql",
344
+ dialect,
267
345
  driver,
268
346
  host: parsed.hostname || null,
269
347
  port: parsed.port ? Number(parsed.port) : null,
@@ -288,7 +366,8 @@ function parseDatabaseUrl(url) {
288
366
  throw new InvalidDatabaseUrl(url, `unknown dialect ${JSON.stringify(base)}`);
289
367
  }
290
368
  const rest = url.slice(schemeEnd + 1);
291
- return dialect === "sqlite" ? parseSqlite(url, driver, rest) : parsePostgres(url, driver, rest);
369
+ if (dialect === "sqlite") return parseSqlite(url, driver, rest);
370
+ return parseNetworkUrl(url, driver, rest, dialect);
292
371
  }
293
372
  function detectDialect(url) {
294
373
  return parseDatabaseUrl(url).dialect;
@@ -408,12 +487,49 @@ function fromDict(model, data) {
408
487
  function parse(model, json) {
409
488
  return fromDict(model, JSON.parse(json));
410
489
  }
490
+ function decoderForKind(kind) {
491
+ switch (kind) {
492
+ case "bigint":
493
+ return (v) => v == null ? null : typeof v === "bigint" ? v : BigInt(v);
494
+ case "date":
495
+ case "datetime":
496
+ case "timestamp":
497
+ return (v) => v == null ? null : v instanceof Date ? v : new Date(v);
498
+ case "blob":
499
+ return (v) => v == null ? null : v instanceof Uint8Array ? v : fromBase64(v);
500
+ case "json":
501
+ return (v) => v == null ? null : typeof v === "string" ? JSON.parse(v) : v;
502
+ case "numeric":
503
+ return (v) => v == null ? null : typeof v === "string" ? v : String(v);
504
+ case "boolean":
505
+ return (v) => v == null ? null : typeof v === "boolean" ? v : v === 1 || v === "true";
506
+ case "smallint":
507
+ case "integer":
508
+ case "real":
509
+ case "double":
510
+ return (v) => v == null ? null : typeof v === "number" ? v : Number(v);
511
+ default:
512
+ return null;
513
+ }
514
+ }
515
+ var decoderCache = /* @__PURE__ */ new WeakMap();
516
+ function decodersFor(model) {
517
+ const cached = decoderCache.get(model);
518
+ if (cached) return cached;
519
+ const map = /* @__PURE__ */ new Map();
520
+ for (const [name, col] of Object.entries(columnsOf(model))) {
521
+ const decoder = decoderForKind(col.type.kind);
522
+ if (decoder) map.set(name, decoder);
523
+ }
524
+ decoderCache.set(model, map);
525
+ return map;
526
+ }
411
527
  function coerceRow(model, raw) {
412
- const columns = columnsOf(model);
528
+ const decoders = decodersFor(model);
413
529
  const out = {};
414
- for (const [name, value] of Object.entries(raw)) {
415
- const col = columns[name];
416
- out[name] = col ? decodeValue(col, value) : value;
530
+ for (const name of Object.keys(raw)) {
531
+ const decode2 = decoders.get(name);
532
+ out[name] = decode2 ? decode2(raw[name]) : raw[name];
417
533
  }
418
534
  return out;
419
535
  }
@@ -438,10 +554,29 @@ var Params = class {
438
554
  return this.placeholder(this.values.length);
439
555
  }
440
556
  };
441
- var BaseDialect = class {
442
- /** Quote an identifier (column/table) for the active dialect. */
557
+ var BaseDialect = class _BaseDialect {
558
+ /**
559
+ * INSERT SQL templates keyed by structure (dialect|table|columns|rowCount|
560
+ * returning). Shared across dialect instances — the key namespaces by dialect
561
+ * name, and the placeholder text is dialect-specific but structure-determined.
562
+ */
563
+ static insertTemplates = /* @__PURE__ */ new Map();
564
+ /** Quoted-identifier cache (see {@link quoteId}). Shared across dialects. */
565
+ static quotedIds = /* @__PURE__ */ new Map();
566
+ /**
567
+ * Quote an identifier (column/table) for the active dialect.
568
+ *
569
+ * Memoized: identifiers form a small, stable set (column/table names), but this
570
+ * runs for every identifier on every compile. Caching the quoted form removes a
571
+ * regex-replace + string allocation from the hot path. The standard double-quote
572
+ * form is identical across both dialects, so one shared cache is correct.
573
+ */
443
574
  quoteId(name) {
444
- return `"${name.replace(/"/g, '""')}"`;
575
+ const cached = _BaseDialect.quotedIds.get(name);
576
+ if (cached !== void 0) return cached;
577
+ const quoted = `"${name.replace(/"/g, '""')}"`;
578
+ _BaseDialect.quotedIds.set(name, quoted);
579
+ return quoted;
445
580
  }
446
581
  /** Compile any node to `{ sql, params }`. */
447
582
  compile(node) {
@@ -474,10 +609,23 @@ var BaseDialect = class {
474
609
  }
475
610
  // ---- statements -------------------------------------------------------
476
611
  compileSelect(node, params) {
477
- const cols = node.columns === "*" ? "*" : node.columns.map((c) => this.quoteId(c)).join(", ");
478
- let sql2 = `SELECT ${cols} FROM ${this.quoteId(node.table)}`;
612
+ let cols;
613
+ if (node.aggregates.length > 0) {
614
+ const groupSel = node.groupBy.map((c) => this.quoteId(c));
615
+ const aggSel = node.aggregates.map((a) => {
616
+ const inner = a.column === "*" ? "*" : this.quoteId(a.column);
617
+ return `${a.fn.toUpperCase()}(${inner}) AS ${this.quoteId(a.alias)}`;
618
+ });
619
+ cols = [...groupSel, ...aggSel].join(", ");
620
+ } else {
621
+ cols = node.columns === "*" ? "*" : node.columns.map((c) => this.quoteId(c)).join(", ");
622
+ }
623
+ let sql2 = `SELECT ${node.distinct ? "DISTINCT " : ""}${cols} FROM ${this.quoteId(node.table)}`;
479
624
  const where = this.compileCondition(node.where, params, (k) => this.quoteId(k));
480
625
  if (where) sql2 += ` WHERE ${where}`;
626
+ if (node.groupBy.length > 0) {
627
+ sql2 += ` GROUP BY ${node.groupBy.map((c) => this.quoteId(c)).join(", ")}`;
628
+ }
481
629
  if (node.orderBy.length > 0) {
482
630
  const terms = node.orderBy.map(
483
631
  (t) => `${this.quoteId(t.column)} ${t.direction === "desc" ? "DESC" : "ASC"}`
@@ -490,14 +638,58 @@ var BaseDialect = class {
490
638
  }
491
639
  compileInsert(node, params) {
492
640
  const columns = node.values.length > 0 ? Object.keys(node.values[0]) : [];
641
+ for (const row of node.values) {
642
+ for (const c of columns) params.bind(row[c] ?? null);
643
+ }
644
+ const conflictCols = node.onConflict && node.onConflict.update !== "nothing" ? Object.keys(node.onConflict.update) : [];
645
+ for (const c of conflictCols) {
646
+ params.bind((node.onConflict?.update)[c]);
647
+ }
648
+ return this.insertTemplate(node, columns, conflictCols);
649
+ }
650
+ /**
651
+ * The INSERT SQL template for a given structure, cached across calls.
652
+ *
653
+ * The text depends only on (dialect, table, columns, row count, returning,
654
+ * conflict shape) — never on the bound values — and placeholder positions are
655
+ * deterministic from the counts (a fresh statement always starts binding at 1).
656
+ * So a per-row insert loop compiles the string once and reuses it every row.
657
+ */
658
+ insertTemplate(node, columns, conflictCols) {
659
+ const returningKey = node.returning === null ? "" : node.returning === "*" ? "*" : node.returning.join(",");
660
+ const conflictKey = node.onConflict ? `${node.onConflict.target.join(",")}>${node.onConflict.update === "nothing" ? "nothing" : conflictCols.join(",")}` : "";
661
+ const key = `${this.name}|${node.table}|${columns.join(",")}|${node.values.length}|${returningKey}|${conflictKey}`;
662
+ const cached = _BaseDialect.insertTemplates.get(key);
663
+ if (cached !== void 0) return cached;
493
664
  const colSql = columns.map((c) => this.quoteId(c)).join(", ");
494
- const rowsSql = node.values.map(
495
- (row) => `(${columns.map((c) => params.bind(row[c] ?? null)).join(", ")})`
496
- ).join(", ");
665
+ let position = 0;
666
+ const rowsSql = node.values.map(() => `(${columns.map(() => this.placeholder(++position)).join(", ")})`).join(", ");
497
667
  let sql2 = `INSERT INTO ${this.quoteId(node.table)} (${colSql}) VALUES ${rowsSql}`;
668
+ if (node.onConflict) {
669
+ sql2 += this.renderConflict(
670
+ node.onConflict,
671
+ conflictCols,
672
+ () => this.placeholder(++position)
673
+ );
674
+ }
498
675
  sql2 += this.compileReturning(node.returning);
676
+ _BaseDialect.insertTemplates.set(key, sql2);
499
677
  return sql2;
500
678
  }
679
+ /**
680
+ * Render the conflict-handling clause. Standard SQL (SQLite/PostgreSQL) uses
681
+ * `ON CONFLICT (...) DO NOTHING | DO UPDATE SET ...`; MySQL overrides this.
682
+ *
683
+ * @param onConflict The conflict clause from the node.
684
+ * @param conflictCols The columns to overwrite on `DO UPDATE` (empty for nothing).
685
+ * @param nextPlaceholder Yields the next positional placeholder (advances the count).
686
+ */
687
+ renderConflict(onConflict, conflictCols, nextPlaceholder) {
688
+ const target = onConflict.target.map((c) => this.quoteId(c)).join(", ");
689
+ if (onConflict.update === "nothing") return ` ON CONFLICT (${target}) DO NOTHING`;
690
+ const assignments = conflictCols.map((c) => `${this.quoteId(c)} = ${nextPlaceholder()}`).join(", ");
691
+ return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
692
+ }
501
693
  compileUpdate(node, params) {
502
694
  const sets = Object.entries(node.set).map(([col, value]) => `${this.quoteId(col)} = ${params.bind(value)}`).join(", ");
503
695
  let sql2 = `UPDATE ${this.quoteId(node.table)} SET ${sets}`;
@@ -635,8 +827,46 @@ var PostgresDialect = class extends BaseDialect {
635
827
  return `${column2} ILIKE ${param}`;
636
828
  }
637
829
  };
830
+ var mysqlQuotedIds = /* @__PURE__ */ new Map();
831
+ var MysqlDialect = class extends BaseDialect {
832
+ name = "mysql";
833
+ placeholder() {
834
+ return "?";
835
+ }
836
+ ilike(column2, param) {
837
+ return `${column2} LIKE ${param}`;
838
+ }
839
+ quoteId(name) {
840
+ const cached = mysqlQuotedIds.get(name);
841
+ if (cached !== void 0) return cached;
842
+ const quoted = `\`${name.replace(/`/g, "``")}\``;
843
+ mysqlQuotedIds.set(name, quoted);
844
+ return quoted;
845
+ }
846
+ renderConflict(onConflict, conflictCols, nextPlaceholder) {
847
+ if (onConflict.update === "nothing") {
848
+ const col = this.quoteId(onConflict.target[0] ?? "id");
849
+ return ` ON DUPLICATE KEY UPDATE ${col} = ${col}`;
850
+ }
851
+ const assignments = conflictCols.map((c) => `${this.quoteId(c)} = ${nextPlaceholder()}`).join(", ");
852
+ return ` ON DUPLICATE KEY UPDATE ${assignments}`;
853
+ }
854
+ compileReturning(returning) {
855
+ if (returning === null) return "";
856
+ throw new Error(
857
+ "RETURNING is not supported on MySQL \u2014 insert, then SELECT by key (e.g. LAST_INSERT_ID())."
858
+ );
859
+ }
860
+ };
638
861
  function getDialect(name) {
639
- return name === "sqlite" ? new SqliteDialect() : new PostgresDialect();
862
+ switch (name) {
863
+ case "sqlite":
864
+ return new SqliteDialect();
865
+ case "postgresql":
866
+ return new PostgresDialect();
867
+ case "mysql":
868
+ return new MysqlDialect();
869
+ }
640
870
  }
641
871
 
642
872
  // src/join.ts
@@ -817,6 +1047,101 @@ var BaseRepository = class {
817
1047
  }
818
1048
  };
819
1049
 
1050
+ // src/active-record.ts
1051
+ function primaryKeyOf2(model) {
1052
+ for (const [name, col] of Object.entries(columnsOf(model))) {
1053
+ if (col.flags.primaryKey) return name;
1054
+ }
1055
+ throw new Error(`${model.tablename} has no primary key`);
1056
+ }
1057
+ var ActiveRecord = class {
1058
+ constructor(model, session, data) {
1059
+ this.model = model;
1060
+ this.session = session;
1061
+ this.data = data;
1062
+ this.pk = primaryKeyOf2(model);
1063
+ }
1064
+ model;
1065
+ session;
1066
+ data;
1067
+ pk;
1068
+ /** The primary-key value of the wrapped row. */
1069
+ pkValue() {
1070
+ return this.data[this.pk];
1071
+ }
1072
+ pkFilter() {
1073
+ return { [this.pk]: this.pkValue() };
1074
+ }
1075
+ /**
1076
+ * Persist the current `data` — insert if new, otherwise overwrite the existing
1077
+ * row (upsert on the primary key). Refreshes `data` from the returned row.
1078
+ *
1079
+ * @returns This wrapper, for chaining.
1080
+ */
1081
+ async save() {
1082
+ const cols = columnsOf(this.model);
1083
+ const rowData = this.data;
1084
+ const setPatch = {};
1085
+ for (const c of Object.keys(rowData)) {
1086
+ if (c !== this.pk && c in cols) setPatch[c] = rowData[c];
1087
+ }
1088
+ const saved = await this.session.execute(
1089
+ insert(this.model).values(this.data).onConflictDoUpdate(
1090
+ [this.pk],
1091
+ setPatch
1092
+ ).returning()
1093
+ ).one();
1094
+ this.data = saved;
1095
+ return this;
1096
+ }
1097
+ /**
1098
+ * Update the given columns for this row and merge them into `data`.
1099
+ *
1100
+ * @param patch The columns to change.
1101
+ * @returns This wrapper, for chaining.
1102
+ */
1103
+ async update(patch) {
1104
+ await this.session.execute(update(this.model).set(patch).where(this.pkFilter()));
1105
+ this.data = { ...this.data, ...patch };
1106
+ return this;
1107
+ }
1108
+ /**
1109
+ * Delete this row.
1110
+ *
1111
+ * @returns The number of rows affected (0 or 1).
1112
+ */
1113
+ async delete() {
1114
+ return this.session.execute(del(this.model).where(this.pkFilter())).rowsAffected();
1115
+ }
1116
+ /**
1117
+ * Re-fetch this row by primary key and refresh `data`.
1118
+ *
1119
+ * @returns This wrapper, for chaining.
1120
+ * @throws When the row no longer exists.
1121
+ */
1122
+ async reload() {
1123
+ const fresh = await this.session.execute(select(this.model).where(this.pkFilter())).first();
1124
+ if (fresh === null) {
1125
+ throw new Error(
1126
+ `${this.model.tablename} row ${JSON.stringify(this.pkValue())} not found on reload`
1127
+ );
1128
+ }
1129
+ this.data = fresh;
1130
+ return this;
1131
+ }
1132
+ };
1133
+ function activeRecord(model, session) {
1134
+ const pk = primaryKeyOf2(model);
1135
+ return {
1136
+ wrap: (row) => new ActiveRecord(model, session, row),
1137
+ create: (data) => new ActiveRecord(model, session, data),
1138
+ async get(id) {
1139
+ const row = await session.execute(select(model).where({ [pk]: id })).first();
1140
+ return row === null ? null : new ActiveRecord(model, session, row);
1141
+ }
1142
+ };
1143
+ }
1144
+
820
1145
  // src/relations.ts
821
1146
  function hasMany(target, keys) {
822
1147
  return {
@@ -879,6 +1204,14 @@ function encodeSqliteParam(value) {
879
1204
  var NodeSqliteDriver = class _NodeSqliteDriver {
880
1205
  // biome-ignore lint/suspicious/noExplicitAny: node:sqlite DatabaseSync has no shipped types here.
881
1206
  db;
1207
+ /**
1208
+ * Prepared-statement cache keyed by SQL text. tempest-db-js always
1209
+ * parameterizes, so a query shape maps to one stable SQL string — reusing the
1210
+ * compiled statement avoids re-`prepare()` on every call (the dominant cost of
1211
+ * per-row inserts and point lookups).
1212
+ */
1213
+ // biome-ignore lint/suspicious/noExplicitAny: node:sqlite StatementSync has no shipped types here.
1214
+ statements = /* @__PURE__ */ new Map();
882
1215
  // biome-ignore lint/suspicious/noExplicitAny: accept an already-open DatabaseSync handle.
883
1216
  constructor(database) {
884
1217
  this.db = database;
@@ -888,8 +1221,17 @@ var NodeSqliteDriver = class _NodeSqliteDriver {
888
1221
  const { DatabaseSync } = nodeRequire("node:sqlite");
889
1222
  return new _NodeSqliteDriver(new DatabaseSync(path));
890
1223
  }
891
- execute(sql2, params) {
1224
+ /** Return the cached prepared statement for `sql`, preparing it on first use. */
1225
+ // biome-ignore lint/suspicious/noExplicitAny: statement type is unavailable here.
1226
+ prepare(sql2) {
1227
+ const cached = this.statements.get(sql2);
1228
+ if (cached) return cached;
892
1229
  const stmt = this.db.prepare(sql2);
1230
+ this.statements.set(sql2, stmt);
1231
+ return stmt;
1232
+ }
1233
+ execute(sql2, params) {
1234
+ const stmt = this.prepare(sql2);
893
1235
  const bound = params.map(encodeSqliteParam);
894
1236
  if (returnsRows(sql2)) {
895
1237
  return { rows: stmt.all(...bound), changes: 0 };
@@ -898,11 +1240,12 @@ var NodeSqliteDriver = class _NodeSqliteDriver {
898
1240
  return { rows: [], changes: Number(info.changes ?? 0) };
899
1241
  }
900
1242
  *iterate(sql2, params) {
901
- const stmt = this.db.prepare(sql2);
1243
+ const stmt = this.prepare(sql2);
902
1244
  const bound = params.map(encodeSqliteParam);
903
1245
  yield* stmt.iterate(...bound);
904
1246
  }
905
1247
  close() {
1248
+ this.statements.clear();
906
1249
  this.db.close();
907
1250
  }
908
1251
  };
@@ -944,6 +1287,36 @@ var NoResultError = class extends Error {
944
1287
  this.name = "NoResultError";
945
1288
  }
946
1289
  };
1290
+ function previewParam(value) {
1291
+ if (value === null || value === void 0) return "null";
1292
+ if (value instanceof Uint8Array) return `<${value.length} bytes>`;
1293
+ const text = typeof value === "string" ? value : String(value);
1294
+ return text.length > 64 ? `${text.slice(0, 61)}...` : text;
1295
+ }
1296
+ var QueryExecutionError = class extends Error {
1297
+ constructor(cause, sql2, params) {
1298
+ const reason = cause instanceof Error ? cause.message : String(cause);
1299
+ super(
1300
+ `Query failed: ${reason}
1301
+ SQL: ${sql2}
1302
+ params: [${params.map(previewParam).join(", ")}]`
1303
+ );
1304
+ this.cause = cause;
1305
+ this.sql = sql2;
1306
+ this.params = params;
1307
+ this.name = "QueryExecutionError";
1308
+ }
1309
+ cause;
1310
+ sql;
1311
+ params;
1312
+ };
1313
+ function emitLog(logger, sql2, params) {
1314
+ if (!logger) return;
1315
+ try {
1316
+ logger({ sql: sql2, params });
1317
+ } catch {
1318
+ }
1319
+ }
947
1320
  function firstScalar(row) {
948
1321
  if (!row) return null;
949
1322
  const keys = Object.keys(row);
@@ -1013,29 +1386,40 @@ var AsyncResult = class {
1013
1386
  };
1014
1387
  var savepointCounter = 0;
1015
1388
  var SyncSession = class {
1016
- constructor(driver, dialect) {
1389
+ constructor(driver, dialect, logger) {
1017
1390
  this.driver = driver;
1018
1391
  this.dialect = dialect;
1392
+ this.logger = logger;
1019
1393
  }
1020
1394
  driver;
1021
1395
  dialect;
1396
+ logger;
1397
+ /** Log, run, and error-wrap one raw statement. */
1398
+ exec(sql2, params) {
1399
+ emitLog(this.logger, sql2, params);
1400
+ try {
1401
+ return this.driver.execute(sql2, params);
1402
+ } catch (error) {
1403
+ throw new QueryExecutionError(error, sql2, params);
1404
+ }
1405
+ }
1022
1406
  /** Compile, run, and coerce a builder into a result. */
1023
1407
  execute(builder) {
1024
1408
  const node = builder.node;
1025
1409
  const { sql: sql2, params } = this.dialect.compile(node);
1026
- const result = this.driver.execute(sql2, params);
1410
+ const result = this.exec(sql2, params);
1027
1411
  const rows = mapRows(builder, result.rows);
1028
1412
  return new SyncResult(rows, result.changes);
1029
1413
  }
1030
1414
  /** Run `fn` inside a transaction: commit on success, rollback on throw. */
1031
1415
  transaction(fn) {
1032
- this.driver.execute("BEGIN", []);
1416
+ this.exec("BEGIN", []);
1033
1417
  try {
1034
1418
  const out = fn(this);
1035
- this.driver.execute("COMMIT", []);
1419
+ this.exec("COMMIT", []);
1036
1420
  return out;
1037
1421
  } catch (error) {
1038
- this.driver.execute("ROLLBACK", []);
1422
+ this.exec("ROLLBACK", []);
1039
1423
  throw error;
1040
1424
  }
1041
1425
  }
@@ -1043,13 +1427,13 @@ var SyncSession = class {
1043
1427
  beginNested(fn) {
1044
1428
  savepointCounter += 1;
1045
1429
  const name = `qsp_${savepointCounter}`;
1046
- this.driver.execute(`SAVEPOINT ${name}`, []);
1430
+ this.exec(`SAVEPOINT ${name}`, []);
1047
1431
  try {
1048
1432
  const out = fn(this);
1049
- this.driver.execute(`RELEASE ${name}`, []);
1433
+ this.exec(`RELEASE ${name}`, []);
1050
1434
  return out;
1051
1435
  } catch (error) {
1052
- this.driver.execute(`ROLLBACK TO ${name}`, []);
1436
+ this.exec(`ROLLBACK TO ${name}`, []);
1053
1437
  throw error;
1054
1438
  }
1055
1439
  }
@@ -1060,31 +1444,51 @@ var SyncSession = class {
1060
1444
  *stream(builder) {
1061
1445
  const node = builder.node;
1062
1446
  const { sql: sql2, params } = this.dialect.compile(node);
1447
+ emitLog(this.logger, sql2, params);
1063
1448
  if (this.driver.iterate) {
1064
- for (const raw of this.driver.iterate(sql2, params)) {
1065
- yield coerceOne(builder, raw);
1449
+ try {
1450
+ for (const raw of this.driver.iterate(sql2, params)) {
1451
+ yield coerceOne(builder, raw);
1452
+ }
1453
+ } catch (error) {
1454
+ throw new QueryExecutionError(error, sql2, params);
1066
1455
  }
1067
1456
  return;
1068
1457
  }
1069
- for (const raw of this.driver.execute(sql2, params).rows) {
1458
+ for (const raw of this.exec(sql2, params).rows) {
1070
1459
  yield coerceOne(builder, raw);
1071
1460
  }
1072
1461
  }
1073
1462
  close() {
1074
1463
  this.driver.close();
1075
1464
  }
1465
+ /** `using session = ...` closes the driver when the scope exits. */
1466
+ [Symbol.dispose]() {
1467
+ this.close();
1468
+ }
1076
1469
  };
1077
- var AsyncSession = class {
1078
- constructor(driver, dialect) {
1470
+ var AsyncSession = class _AsyncSession {
1471
+ constructor(driver, dialect, logger) {
1079
1472
  this.driver = driver;
1080
1473
  this.dialect = dialect;
1474
+ this.logger = logger;
1081
1475
  }
1082
1476
  driver;
1083
1477
  dialect;
1478
+ logger;
1479
+ /** Log, run, and error-wrap one raw statement. */
1480
+ async exec(sql2, params) {
1481
+ emitLog(this.logger, sql2, params);
1482
+ try {
1483
+ return await this.driver.execute(sql2, params);
1484
+ } catch (error) {
1485
+ throw new QueryExecutionError(error, sql2, params);
1486
+ }
1487
+ }
1084
1488
  execute(builder) {
1085
1489
  const node = builder.node;
1086
1490
  const { sql: sql2, params } = this.dialect.compile(node);
1087
- const inner = this.driver.execute(sql2, params).then((result) => {
1491
+ const inner = this.exec(sql2, params).then((result) => {
1088
1492
  const rows = mapRows(builder, result.rows);
1089
1493
  return new SyncResult(rows, result.changes);
1090
1494
  });
@@ -1094,40 +1498,66 @@ var AsyncSession = class {
1094
1498
  async *stream(builder) {
1095
1499
  const node = builder.node;
1096
1500
  const { sql: sql2, params } = this.dialect.compile(node);
1501
+ emitLog(this.logger, sql2, params);
1097
1502
  if (this.driver.iterate) {
1098
- for await (const raw of this.driver.iterate(sql2, params)) {
1099
- yield coerceOne(builder, raw);
1503
+ try {
1504
+ for await (const raw of this.driver.iterate(sql2, params)) {
1505
+ yield coerceOne(builder, raw);
1506
+ }
1507
+ } catch (error) {
1508
+ throw new QueryExecutionError(error, sql2, params);
1100
1509
  }
1101
1510
  return;
1102
1511
  }
1103
- const result = await this.driver.execute(sql2, params);
1512
+ const result = await this.exec(sql2, params);
1104
1513
  for (const raw of result.rows) {
1105
1514
  yield coerceOne(builder, raw);
1106
1515
  }
1107
1516
  }
1108
1517
  async transaction(fn) {
1109
- await this.driver.execute("BEGIN", []);
1518
+ if (this.driver.reserve) {
1519
+ const reserved = await this.driver.reserve();
1520
+ const scoped = new _AsyncSession(reserved, this.dialect, this.logger);
1521
+ try {
1522
+ await scoped.exec("BEGIN", []);
1523
+ const out = await fn(scoped);
1524
+ await scoped.exec("COMMIT", []);
1525
+ return out;
1526
+ } catch (error) {
1527
+ await scoped.exec("ROLLBACK", []);
1528
+ throw error;
1529
+ } finally {
1530
+ await reserved.release();
1531
+ }
1532
+ }
1533
+ await this.exec("BEGIN", []);
1110
1534
  try {
1111
1535
  const out = await fn(this);
1112
- await this.driver.execute("COMMIT", []);
1536
+ await this.exec("COMMIT", []);
1113
1537
  return out;
1114
1538
  } catch (error) {
1115
- await this.driver.execute("ROLLBACK", []);
1539
+ await this.exec("ROLLBACK", []);
1116
1540
  throw error;
1117
1541
  }
1118
1542
  }
1119
1543
  async close() {
1120
1544
  await this.driver.close();
1121
1545
  }
1546
+ /** `await using session = ...` closes the driver when the scope exits. */
1547
+ async [Symbol.asyncDispose]() {
1548
+ await this.close();
1549
+ }
1122
1550
  };
1123
1551
  var SyncEngine = class {
1124
- constructor(driver) {
1552
+ constructor(driver, logger) {
1125
1553
  this.driver = driver;
1554
+ this.logger = logger;
1126
1555
  }
1127
1556
  driver;
1557
+ logger;
1128
1558
  dialect = "sqlite";
1129
1559
  session() {
1130
- return new SyncSession(this.driver, getDialect("sqlite"));
1560
+ return new SyncSession(this.driver, getDialect("sqlite"), this.logger);
1131
1561
  }
1132
1562
  transaction(fn) {
1133
1563
  return this.session().transaction(fn);
@@ -1135,16 +1565,22 @@ var SyncEngine = class {
1135
1565
  close() {
1136
1566
  this.driver.close();
1137
1567
  }
1568
+ /** `using engine = createSyncEngine(...)` closes the pool when the scope exits. */
1569
+ [Symbol.dispose]() {
1570
+ this.close();
1571
+ }
1138
1572
  };
1139
1573
  var AsyncEngine = class {
1140
- constructor(driver, dialect) {
1574
+ constructor(driver, dialect, logger) {
1141
1575
  this.driver = driver;
1142
1576
  this.dialect = dialect;
1577
+ this.logger = logger;
1143
1578
  }
1144
1579
  driver;
1145
1580
  dialect;
1581
+ logger;
1146
1582
  session() {
1147
- return new AsyncSession(this.driver, getDialect(this.dialect));
1583
+ return new AsyncSession(this.driver, getDialect(this.dialect), this.logger);
1148
1584
  }
1149
1585
  transaction(fn) {
1150
1586
  return this.session().transaction(fn);
@@ -1152,6 +1588,10 @@ var AsyncEngine = class {
1152
1588
  async close() {
1153
1589
  await this.driver.close();
1154
1590
  }
1591
+ /** `await using engine = createEngine(...)` closes the pool when the scope exits. */
1592
+ async [Symbol.asyncDispose]() {
1593
+ await this.close();
1594
+ }
1155
1595
  };
1156
1596
  function asAsync(driver) {
1157
1597
  const syncIterate = driver.iterate?.bind(driver);
@@ -1175,17 +1615,93 @@ function createSyncEngine(url, options) {
1175
1615
  `createSyncEngine supports only SQLite; ${parsed.dialect} is async-only \u2014 use createEngine.`
1176
1616
  );
1177
1617
  }
1178
- return new SyncEngine(openSqliteDriver(parsed.database ?? ":memory:"));
1618
+ return new SyncEngine(
1619
+ openSqliteDriver(parsed.database ?? ":memory:"),
1620
+ options?.onQuery
1621
+ );
1179
1622
  }
1180
1623
  function createEngine(url, options) {
1181
1624
  const parsed = parseDatabaseUrl(url);
1182
1625
  if (parsed.dialect === "sqlite") {
1183
1626
  return new AsyncEngine(
1184
1627
  asAsync(openSqliteDriver(parsed.database ?? ":memory:")),
1185
- "sqlite"
1628
+ "sqlite",
1629
+ options?.onQuery
1630
+ );
1631
+ }
1632
+ if (parsed.dialect === "mysql") {
1633
+ return new AsyncEngine(
1634
+ createMysqlDriver(parsed.raw, options?.pool),
1635
+ "mysql",
1636
+ options?.onQuery
1186
1637
  );
1187
1638
  }
1188
- return new AsyncEngine(createPostgresDriver(parsed.raw, options?.pool), "postgresql");
1639
+ return new AsyncEngine(
1640
+ createPostgresDriver(parsed.raw, options?.pool),
1641
+ "postgresql",
1642
+ options?.onQuery
1643
+ );
1644
+ }
1645
+ function encodeMysqlParam(value) {
1646
+ if (value === void 0 || value === null) return null;
1647
+ if (typeof value === "boolean") return value ? 1 : 0;
1648
+ if (value instanceof Uint8Array) return value;
1649
+ if (value instanceof Date) return value;
1650
+ if (typeof value === "object") return JSON.stringify(value);
1651
+ return value;
1652
+ }
1653
+ function toMysqlResult(rows) {
1654
+ if (Array.isArray(rows)) {
1655
+ return { rows, changes: 0 };
1656
+ }
1657
+ const header = rows;
1658
+ return { rows: [], changes: header.affectedRows ?? 0 };
1659
+ }
1660
+ function createMysqlDriver(url, pool) {
1661
+ let poolHandle;
1662
+ const ensure = async () => {
1663
+ if (poolHandle) return;
1664
+ const moduleName = "mysql2/promise";
1665
+ const mod = await import(
1666
+ /* @vite-ignore */
1667
+ moduleName
1668
+ );
1669
+ const opts = { uri: url };
1670
+ if (pool?.size !== void 0) opts.connectionLimit = pool.size;
1671
+ if (pool?.idleTimeoutMs !== void 0) opts.idleTimeout = pool.idleTimeoutMs;
1672
+ if (pool?.connectTimeoutMs !== void 0) opts.connectTimeout = pool.connectTimeoutMs;
1673
+ poolHandle = mod.createPool(opts);
1674
+ };
1675
+ const runOn = async (queryable, sql2, params) => {
1676
+ const [rows] = await queryable.query(sql2, params.map(encodeMysqlParam));
1677
+ return toMysqlResult(rows);
1678
+ };
1679
+ return {
1680
+ async execute(sql2, params) {
1681
+ await ensure();
1682
+ return runOn(poolHandle, sql2, params);
1683
+ },
1684
+ async reserve() {
1685
+ await ensure();
1686
+ const conn = await poolHandle.getConnection();
1687
+ return {
1688
+ execute: (sql2, params) => runOn(conn, sql2, params),
1689
+ async release() {
1690
+ conn.release();
1691
+ },
1692
+ async close() {
1693
+ conn.release();
1694
+ }
1695
+ };
1696
+ },
1697
+ async close() {
1698
+ if (poolHandle) await poolHandle.end();
1699
+ }
1700
+ };
1701
+ }
1702
+ function toPostgresResult(rows) {
1703
+ const arr = rows;
1704
+ return { rows: Array.from(arr), changes: arr.count ?? arr.length };
1189
1705
  }
1190
1706
  function createPostgresDriver(url, pool) {
1191
1707
  let client;
@@ -1208,10 +1724,21 @@ function createPostgresDriver(url, pool) {
1208
1724
  return {
1209
1725
  async execute(sql2, params) {
1210
1726
  await ensure();
1211
- const rows = await client.unsafe(sql2, params);
1727
+ return toPostgresResult(await client.unsafe(sql2, params));
1728
+ },
1729
+ async reserve() {
1730
+ await ensure();
1731
+ const conn = await client.reserve();
1212
1732
  return {
1213
- rows: Array.from(rows),
1214
- changes: rows.count ?? rows.length
1733
+ async execute(sql2, params) {
1734
+ return toPostgresResult(await conn.unsafe(sql2, params));
1735
+ },
1736
+ async release() {
1737
+ conn.release();
1738
+ },
1739
+ async close() {
1740
+ conn.release();
1741
+ }
1215
1742
  };
1216
1743
  },
1217
1744
  async close() {
@@ -1347,7 +1874,10 @@ var column = {
1347
1874
  var Model = class {
1348
1875
  static tablename;
1349
1876
  };
1877
+ var columnsCache = /* @__PURE__ */ new WeakMap();
1350
1878
  function columnsOf(model) {
1879
+ const cached = columnsCache.get(model);
1880
+ if (cached) return cached;
1351
1881
  const instance = new model();
1352
1882
  const out = {};
1353
1883
  for (const [key, value] of Object.entries(instance)) {
@@ -1355,9 +1885,12 @@ function columnsOf(model) {
1355
1885
  out[key] = value;
1356
1886
  }
1357
1887
  }
1888
+ columnsCache.set(model, out);
1358
1889
  return out;
1359
1890
  }
1360
1891
 
1892
+ exports.ActiveRecord = ActiveRecord;
1893
+ exports.Agg = Agg;
1361
1894
  exports.AsyncEngine = AsyncEngine;
1362
1895
  exports.AsyncResult = AsyncResult;
1363
1896
  exports.AsyncSession = AsyncSession;
@@ -1369,10 +1902,12 @@ exports.InsertBuilder = InsertBuilder;
1369
1902
  exports.InvalidDatabaseUrl = InvalidDatabaseUrl;
1370
1903
  exports.JoinBuilder = JoinBuilder;
1371
1904
  exports.Model = Model;
1905
+ exports.MysqlDialect = MysqlDialect;
1372
1906
  exports.NoResultError = NoResultError;
1373
1907
  exports.NodeSqliteDriver = NodeSqliteDriver;
1374
1908
  exports.OPERATORS = OPERATORS;
1375
1909
  exports.PostgresDialect = PostgresDialect;
1910
+ exports.QueryExecutionError = QueryExecutionError;
1376
1911
  exports.RecordNotFound = RecordNotFound;
1377
1912
  exports.SelectBuilder = SelectBuilder;
1378
1913
  exports.SqliteDialect = SqliteDialect;
@@ -1381,10 +1916,13 @@ exports.SyncResult = SyncResult;
1381
1916
  exports.SyncSession = SyncSession;
1382
1917
  exports.UpdateBuilder = UpdateBuilder;
1383
1918
  exports.ValidationError = ValidationError;
1919
+ exports.activeRecord = activeRecord;
1384
1920
  exports.and = and;
1921
+ exports.avg = avg;
1385
1922
  exports.belongsTo = belongsTo;
1386
1923
  exports.column = column;
1387
1924
  exports.columnsOf = columnsOf;
1925
+ exports.count = count;
1388
1926
  exports.createEngine = createEngine;
1389
1927
  exports.createSyncEngine = createSyncEngine;
1390
1928
  exports.del = del;
@@ -1396,6 +1934,8 @@ exports.insert = insert;
1396
1934
  exports.isCondition = isCondition;
1397
1935
  exports.join = join;
1398
1936
  exports.loadRelations = loadRelations;
1937
+ exports.max = max;
1938
+ exports.min = min;
1399
1939
  exports.not = not;
1400
1940
  exports.or = or;
1401
1941
  exports.parse = parse;
@@ -1403,6 +1943,7 @@ exports.parseDatabaseUrl = parseDatabaseUrl;
1403
1943
  exports.select = select;
1404
1944
  exports.sql = sql;
1405
1945
  exports.stringify = stringify;
1946
+ exports.sum = sum;
1406
1947
  exports.toCondNode = toCondNode;
1407
1948
  exports.toDict = toDict;
1408
1949
  exports.toJSON = toJSON;