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