tempest-db-js 0.1.0 → 0.2.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
  }
@@ -408,12 +484,49 @@ function fromDict(model, data) {
408
484
  function parse(model, json) {
409
485
  return fromDict(model, JSON.parse(json));
410
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
+ }
411
524
  function coerceRow(model, raw) {
412
- const columns = columnsOf(model);
525
+ const decoders = decodersFor(model);
413
526
  const out = {};
414
- for (const [name, value] of Object.entries(raw)) {
415
- const col = columns[name];
416
- 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];
417
530
  }
418
531
  return out;
419
532
  }
@@ -438,10 +551,29 @@ var Params = class {
438
551
  return this.placeholder(this.values.length);
439
552
  }
440
553
  };
441
- var BaseDialect = class {
442
- /** 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
+ */
443
571
  quoteId(name) {
444
- 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;
445
577
  }
446
578
  /** Compile any node to `{ sql, params }`. */
447
579
  compile(node) {
@@ -474,10 +606,23 @@ var BaseDialect = class {
474
606
  }
475
607
  // ---- statements -------------------------------------------------------
476
608
  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)}`;
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)}`;
479
621
  const where = this.compileCondition(node.where, params, (k) => this.quoteId(k));
480
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
+ }
481
626
  if (node.orderBy.length > 0) {
482
627
  const terms = node.orderBy.map(
483
628
  (t) => `${this.quoteId(t.column)} ${t.direction === "desc" ? "DESC" : "ASC"}`
@@ -490,12 +635,44 @@ var BaseDialect = class {
490
635
  }
491
636
  compileInsert(node, params) {
492
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;
493
661
  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(", ");
662
+ let position = 0;
663
+ const rowsSql = node.values.map(() => `(${columns.map(() => this.placeholder(++position)).join(", ")})`).join(", ");
497
664
  let sql2 = `INSERT INTO ${this.quoteId(node.table)} (${colSql}) VALUES ${rowsSql}`;
665
+ if (node.onConflict) {
666
+ const target = node.onConflict.target.map((c) => this.quoteId(c)).join(", ");
667
+ if (node.onConflict.update === "nothing") {
668
+ sql2 += ` ON CONFLICT (${target}) DO NOTHING`;
669
+ } else {
670
+ const assignments = conflictCols.map((c) => `${this.quoteId(c)} = ${this.placeholder(++position)}`).join(", ");
671
+ sql2 += ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
672
+ }
673
+ }
498
674
  sql2 += this.compileReturning(node.returning);
675
+ _BaseDialect.insertTemplates.set(key, sql2);
499
676
  return sql2;
500
677
  }
501
678
  compileUpdate(node, params) {
@@ -817,6 +994,101 @@ var BaseRepository = class {
817
994
  }
818
995
  };
819
996
 
997
+ // src/active-record.ts
998
+ function primaryKeyOf2(model) {
999
+ for (const [name, col] of Object.entries(columnsOf(model))) {
1000
+ if (col.flags.primaryKey) return name;
1001
+ }
1002
+ throw new Error(`${model.tablename} has no primary key`);
1003
+ }
1004
+ var ActiveRecord = class {
1005
+ constructor(model, session, data) {
1006
+ this.model = model;
1007
+ this.session = session;
1008
+ this.data = data;
1009
+ this.pk = primaryKeyOf2(model);
1010
+ }
1011
+ model;
1012
+ session;
1013
+ data;
1014
+ pk;
1015
+ /** The primary-key value of the wrapped row. */
1016
+ pkValue() {
1017
+ return this.data[this.pk];
1018
+ }
1019
+ pkFilter() {
1020
+ return { [this.pk]: this.pkValue() };
1021
+ }
1022
+ /**
1023
+ * Persist the current `data` — insert if new, otherwise overwrite the existing
1024
+ * row (upsert on the primary key). Refreshes `data` from the returned row.
1025
+ *
1026
+ * @returns This wrapper, for chaining.
1027
+ */
1028
+ async save() {
1029
+ const cols = columnsOf(this.model);
1030
+ const rowData = this.data;
1031
+ const setPatch = {};
1032
+ for (const c of Object.keys(rowData)) {
1033
+ if (c !== this.pk && c in cols) setPatch[c] = rowData[c];
1034
+ }
1035
+ const saved = await this.session.execute(
1036
+ insert(this.model).values(this.data).onConflictDoUpdate(
1037
+ [this.pk],
1038
+ setPatch
1039
+ ).returning()
1040
+ ).one();
1041
+ this.data = saved;
1042
+ return this;
1043
+ }
1044
+ /**
1045
+ * Update the given columns for this row and merge them into `data`.
1046
+ *
1047
+ * @param patch The columns to change.
1048
+ * @returns This wrapper, for chaining.
1049
+ */
1050
+ async update(patch) {
1051
+ await this.session.execute(update(this.model).set(patch).where(this.pkFilter()));
1052
+ this.data = { ...this.data, ...patch };
1053
+ return this;
1054
+ }
1055
+ /**
1056
+ * Delete this row.
1057
+ *
1058
+ * @returns The number of rows affected (0 or 1).
1059
+ */
1060
+ async delete() {
1061
+ return this.session.execute(del(this.model).where(this.pkFilter())).rowsAffected();
1062
+ }
1063
+ /**
1064
+ * Re-fetch this row by primary key and refresh `data`.
1065
+ *
1066
+ * @returns This wrapper, for chaining.
1067
+ * @throws When the row no longer exists.
1068
+ */
1069
+ async reload() {
1070
+ const fresh = await this.session.execute(select(this.model).where(this.pkFilter())).first();
1071
+ if (fresh === null) {
1072
+ throw new Error(
1073
+ `${this.model.tablename} row ${JSON.stringify(this.pkValue())} not found on reload`
1074
+ );
1075
+ }
1076
+ this.data = fresh;
1077
+ return this;
1078
+ }
1079
+ };
1080
+ function activeRecord(model, session) {
1081
+ const pk = primaryKeyOf2(model);
1082
+ return {
1083
+ wrap: (row) => new ActiveRecord(model, session, row),
1084
+ create: (data) => new ActiveRecord(model, session, data),
1085
+ async get(id) {
1086
+ const row = await session.execute(select(model).where({ [pk]: id })).first();
1087
+ return row === null ? null : new ActiveRecord(model, session, row);
1088
+ }
1089
+ };
1090
+ }
1091
+
820
1092
  // src/relations.ts
821
1093
  function hasMany(target, keys) {
822
1094
  return {
@@ -879,6 +1151,14 @@ function encodeSqliteParam(value) {
879
1151
  var NodeSqliteDriver = class _NodeSqliteDriver {
880
1152
  // biome-ignore lint/suspicious/noExplicitAny: node:sqlite DatabaseSync has no shipped types here.
881
1153
  db;
1154
+ /**
1155
+ * Prepared-statement cache keyed by SQL text. tempest-db-js always
1156
+ * parameterizes, so a query shape maps to one stable SQL string — reusing the
1157
+ * compiled statement avoids re-`prepare()` on every call (the dominant cost of
1158
+ * per-row inserts and point lookups).
1159
+ */
1160
+ // biome-ignore lint/suspicious/noExplicitAny: node:sqlite StatementSync has no shipped types here.
1161
+ statements = /* @__PURE__ */ new Map();
882
1162
  // biome-ignore lint/suspicious/noExplicitAny: accept an already-open DatabaseSync handle.
883
1163
  constructor(database) {
884
1164
  this.db = database;
@@ -888,8 +1168,17 @@ var NodeSqliteDriver = class _NodeSqliteDriver {
888
1168
  const { DatabaseSync } = nodeRequire("node:sqlite");
889
1169
  return new _NodeSqliteDriver(new DatabaseSync(path));
890
1170
  }
891
- execute(sql2, params) {
1171
+ /** Return the cached prepared statement for `sql`, preparing it on first use. */
1172
+ // biome-ignore lint/suspicious/noExplicitAny: statement type is unavailable here.
1173
+ prepare(sql2) {
1174
+ const cached = this.statements.get(sql2);
1175
+ if (cached) return cached;
892
1176
  const stmt = this.db.prepare(sql2);
1177
+ this.statements.set(sql2, stmt);
1178
+ return stmt;
1179
+ }
1180
+ execute(sql2, params) {
1181
+ const stmt = this.prepare(sql2);
893
1182
  const bound = params.map(encodeSqliteParam);
894
1183
  if (returnsRows(sql2)) {
895
1184
  return { rows: stmt.all(...bound), changes: 0 };
@@ -898,11 +1187,12 @@ var NodeSqliteDriver = class _NodeSqliteDriver {
898
1187
  return { rows: [], changes: Number(info.changes ?? 0) };
899
1188
  }
900
1189
  *iterate(sql2, params) {
901
- const stmt = this.db.prepare(sql2);
1190
+ const stmt = this.prepare(sql2);
902
1191
  const bound = params.map(encodeSqliteParam);
903
1192
  yield* stmt.iterate(...bound);
904
1193
  }
905
1194
  close() {
1195
+ this.statements.clear();
906
1196
  this.db.close();
907
1197
  }
908
1198
  };
@@ -944,6 +1234,36 @@ var NoResultError = class extends Error {
944
1234
  this.name = "NoResultError";
945
1235
  }
946
1236
  };
1237
+ function previewParam(value) {
1238
+ if (value === null || value === void 0) return "null";
1239
+ if (value instanceof Uint8Array) return `<${value.length} bytes>`;
1240
+ const text = typeof value === "string" ? value : String(value);
1241
+ return text.length > 64 ? `${text.slice(0, 61)}...` : text;
1242
+ }
1243
+ var QueryExecutionError = class extends Error {
1244
+ constructor(cause, sql2, params) {
1245
+ const reason = cause instanceof Error ? cause.message : String(cause);
1246
+ super(
1247
+ `Query failed: ${reason}
1248
+ SQL: ${sql2}
1249
+ params: [${params.map(previewParam).join(", ")}]`
1250
+ );
1251
+ this.cause = cause;
1252
+ this.sql = sql2;
1253
+ this.params = params;
1254
+ this.name = "QueryExecutionError";
1255
+ }
1256
+ cause;
1257
+ sql;
1258
+ params;
1259
+ };
1260
+ function emitLog(logger, sql2, params) {
1261
+ if (!logger) return;
1262
+ try {
1263
+ logger({ sql: sql2, params });
1264
+ } catch {
1265
+ }
1266
+ }
947
1267
  function firstScalar(row) {
948
1268
  if (!row) return null;
949
1269
  const keys = Object.keys(row);
@@ -1013,29 +1333,40 @@ var AsyncResult = class {
1013
1333
  };
1014
1334
  var savepointCounter = 0;
1015
1335
  var SyncSession = class {
1016
- constructor(driver, dialect) {
1336
+ constructor(driver, dialect, logger) {
1017
1337
  this.driver = driver;
1018
1338
  this.dialect = dialect;
1339
+ this.logger = logger;
1019
1340
  }
1020
1341
  driver;
1021
1342
  dialect;
1343
+ logger;
1344
+ /** Log, run, and error-wrap one raw statement. */
1345
+ exec(sql2, params) {
1346
+ emitLog(this.logger, sql2, params);
1347
+ try {
1348
+ return this.driver.execute(sql2, params);
1349
+ } catch (error) {
1350
+ throw new QueryExecutionError(error, sql2, params);
1351
+ }
1352
+ }
1022
1353
  /** Compile, run, and coerce a builder into a result. */
1023
1354
  execute(builder) {
1024
1355
  const node = builder.node;
1025
1356
  const { sql: sql2, params } = this.dialect.compile(node);
1026
- const result = this.driver.execute(sql2, params);
1357
+ const result = this.exec(sql2, params);
1027
1358
  const rows = mapRows(builder, result.rows);
1028
1359
  return new SyncResult(rows, result.changes);
1029
1360
  }
1030
1361
  /** Run `fn` inside a transaction: commit on success, rollback on throw. */
1031
1362
  transaction(fn) {
1032
- this.driver.execute("BEGIN", []);
1363
+ this.exec("BEGIN", []);
1033
1364
  try {
1034
1365
  const out = fn(this);
1035
- this.driver.execute("COMMIT", []);
1366
+ this.exec("COMMIT", []);
1036
1367
  return out;
1037
1368
  } catch (error) {
1038
- this.driver.execute("ROLLBACK", []);
1369
+ this.exec("ROLLBACK", []);
1039
1370
  throw error;
1040
1371
  }
1041
1372
  }
@@ -1043,13 +1374,13 @@ var SyncSession = class {
1043
1374
  beginNested(fn) {
1044
1375
  savepointCounter += 1;
1045
1376
  const name = `qsp_${savepointCounter}`;
1046
- this.driver.execute(`SAVEPOINT ${name}`, []);
1377
+ this.exec(`SAVEPOINT ${name}`, []);
1047
1378
  try {
1048
1379
  const out = fn(this);
1049
- this.driver.execute(`RELEASE ${name}`, []);
1380
+ this.exec(`RELEASE ${name}`, []);
1050
1381
  return out;
1051
1382
  } catch (error) {
1052
- this.driver.execute(`ROLLBACK TO ${name}`, []);
1383
+ this.exec(`ROLLBACK TO ${name}`, []);
1053
1384
  throw error;
1054
1385
  }
1055
1386
  }
@@ -1060,31 +1391,51 @@ var SyncSession = class {
1060
1391
  *stream(builder) {
1061
1392
  const node = builder.node;
1062
1393
  const { sql: sql2, params } = this.dialect.compile(node);
1394
+ emitLog(this.logger, sql2, params);
1063
1395
  if (this.driver.iterate) {
1064
- for (const raw of this.driver.iterate(sql2, params)) {
1065
- yield coerceOne(builder, raw);
1396
+ try {
1397
+ for (const raw of this.driver.iterate(sql2, params)) {
1398
+ yield coerceOne(builder, raw);
1399
+ }
1400
+ } catch (error) {
1401
+ throw new QueryExecutionError(error, sql2, params);
1066
1402
  }
1067
1403
  return;
1068
1404
  }
1069
- for (const raw of this.driver.execute(sql2, params).rows) {
1405
+ for (const raw of this.exec(sql2, params).rows) {
1070
1406
  yield coerceOne(builder, raw);
1071
1407
  }
1072
1408
  }
1073
1409
  close() {
1074
1410
  this.driver.close();
1075
1411
  }
1412
+ /** `using session = ...` closes the driver when the scope exits. */
1413
+ [Symbol.dispose]() {
1414
+ this.close();
1415
+ }
1076
1416
  };
1077
- var AsyncSession = class {
1078
- constructor(driver, dialect) {
1417
+ var AsyncSession = class _AsyncSession {
1418
+ constructor(driver, dialect, logger) {
1079
1419
  this.driver = driver;
1080
1420
  this.dialect = dialect;
1421
+ this.logger = logger;
1081
1422
  }
1082
1423
  driver;
1083
1424
  dialect;
1425
+ logger;
1426
+ /** Log, run, and error-wrap one raw statement. */
1427
+ async exec(sql2, params) {
1428
+ emitLog(this.logger, sql2, params);
1429
+ try {
1430
+ return await this.driver.execute(sql2, params);
1431
+ } catch (error) {
1432
+ throw new QueryExecutionError(error, sql2, params);
1433
+ }
1434
+ }
1084
1435
  execute(builder) {
1085
1436
  const node = builder.node;
1086
1437
  const { sql: sql2, params } = this.dialect.compile(node);
1087
- const inner = this.driver.execute(sql2, params).then((result) => {
1438
+ const inner = this.exec(sql2, params).then((result) => {
1088
1439
  const rows = mapRows(builder, result.rows);
1089
1440
  return new SyncResult(rows, result.changes);
1090
1441
  });
@@ -1094,40 +1445,66 @@ var AsyncSession = class {
1094
1445
  async *stream(builder) {
1095
1446
  const node = builder.node;
1096
1447
  const { sql: sql2, params } = this.dialect.compile(node);
1448
+ emitLog(this.logger, sql2, params);
1097
1449
  if (this.driver.iterate) {
1098
- for await (const raw of this.driver.iterate(sql2, params)) {
1099
- yield coerceOne(builder, raw);
1450
+ try {
1451
+ for await (const raw of this.driver.iterate(sql2, params)) {
1452
+ yield coerceOne(builder, raw);
1453
+ }
1454
+ } catch (error) {
1455
+ throw new QueryExecutionError(error, sql2, params);
1100
1456
  }
1101
1457
  return;
1102
1458
  }
1103
- const result = await this.driver.execute(sql2, params);
1459
+ const result = await this.exec(sql2, params);
1104
1460
  for (const raw of result.rows) {
1105
1461
  yield coerceOne(builder, raw);
1106
1462
  }
1107
1463
  }
1108
1464
  async transaction(fn) {
1109
- await this.driver.execute("BEGIN", []);
1465
+ if (this.driver.reserve) {
1466
+ const reserved = await this.driver.reserve();
1467
+ const scoped = new _AsyncSession(reserved, this.dialect, this.logger);
1468
+ try {
1469
+ await scoped.exec("BEGIN", []);
1470
+ const out = await fn(scoped);
1471
+ await scoped.exec("COMMIT", []);
1472
+ return out;
1473
+ } catch (error) {
1474
+ await scoped.exec("ROLLBACK", []);
1475
+ throw error;
1476
+ } finally {
1477
+ await reserved.release();
1478
+ }
1479
+ }
1480
+ await this.exec("BEGIN", []);
1110
1481
  try {
1111
1482
  const out = await fn(this);
1112
- await this.driver.execute("COMMIT", []);
1483
+ await this.exec("COMMIT", []);
1113
1484
  return out;
1114
1485
  } catch (error) {
1115
- await this.driver.execute("ROLLBACK", []);
1486
+ await this.exec("ROLLBACK", []);
1116
1487
  throw error;
1117
1488
  }
1118
1489
  }
1119
1490
  async close() {
1120
1491
  await this.driver.close();
1121
1492
  }
1493
+ /** `await using session = ...` closes the driver when the scope exits. */
1494
+ async [Symbol.asyncDispose]() {
1495
+ await this.close();
1496
+ }
1122
1497
  };
1123
1498
  var SyncEngine = class {
1124
- constructor(driver) {
1499
+ constructor(driver, logger) {
1125
1500
  this.driver = driver;
1501
+ this.logger = logger;
1126
1502
  }
1127
1503
  driver;
1504
+ logger;
1128
1505
  dialect = "sqlite";
1129
1506
  session() {
1130
- return new SyncSession(this.driver, getDialect("sqlite"));
1507
+ return new SyncSession(this.driver, getDialect("sqlite"), this.logger);
1131
1508
  }
1132
1509
  transaction(fn) {
1133
1510
  return this.session().transaction(fn);
@@ -1135,16 +1512,22 @@ var SyncEngine = class {
1135
1512
  close() {
1136
1513
  this.driver.close();
1137
1514
  }
1515
+ /** `using engine = createSyncEngine(...)` closes the pool when the scope exits. */
1516
+ [Symbol.dispose]() {
1517
+ this.close();
1518
+ }
1138
1519
  };
1139
1520
  var AsyncEngine = class {
1140
- constructor(driver, dialect) {
1521
+ constructor(driver, dialect, logger) {
1141
1522
  this.driver = driver;
1142
1523
  this.dialect = dialect;
1524
+ this.logger = logger;
1143
1525
  }
1144
1526
  driver;
1145
1527
  dialect;
1528
+ logger;
1146
1529
  session() {
1147
- return new AsyncSession(this.driver, getDialect(this.dialect));
1530
+ return new AsyncSession(this.driver, getDialect(this.dialect), this.logger);
1148
1531
  }
1149
1532
  transaction(fn) {
1150
1533
  return this.session().transaction(fn);
@@ -1152,6 +1535,10 @@ var AsyncEngine = class {
1152
1535
  async close() {
1153
1536
  await this.driver.close();
1154
1537
  }
1538
+ /** `await using engine = createEngine(...)` closes the pool when the scope exits. */
1539
+ async [Symbol.asyncDispose]() {
1540
+ await this.close();
1541
+ }
1155
1542
  };
1156
1543
  function asAsync(driver) {
1157
1544
  const syncIterate = driver.iterate?.bind(driver);
@@ -1175,17 +1562,29 @@ function createSyncEngine(url, options) {
1175
1562
  `createSyncEngine supports only SQLite; ${parsed.dialect} is async-only \u2014 use createEngine.`
1176
1563
  );
1177
1564
  }
1178
- return new SyncEngine(openSqliteDriver(parsed.database ?? ":memory:"));
1565
+ return new SyncEngine(
1566
+ openSqliteDriver(parsed.database ?? ":memory:"),
1567
+ options?.onQuery
1568
+ );
1179
1569
  }
1180
1570
  function createEngine(url, options) {
1181
1571
  const parsed = parseDatabaseUrl(url);
1182
1572
  if (parsed.dialect === "sqlite") {
1183
1573
  return new AsyncEngine(
1184
1574
  asAsync(openSqliteDriver(parsed.database ?? ":memory:")),
1185
- "sqlite"
1575
+ "sqlite",
1576
+ options?.onQuery
1186
1577
  );
1187
1578
  }
1188
- return new AsyncEngine(createPostgresDriver(parsed.raw, options?.pool), "postgresql");
1579
+ return new AsyncEngine(
1580
+ createPostgresDriver(parsed.raw, options?.pool),
1581
+ "postgresql",
1582
+ options?.onQuery
1583
+ );
1584
+ }
1585
+ function toPostgresResult(rows) {
1586
+ const arr = rows;
1587
+ return { rows: Array.from(arr), changes: arr.count ?? arr.length };
1189
1588
  }
1190
1589
  function createPostgresDriver(url, pool) {
1191
1590
  let client;
@@ -1208,10 +1607,21 @@ function createPostgresDriver(url, pool) {
1208
1607
  return {
1209
1608
  async execute(sql2, params) {
1210
1609
  await ensure();
1211
- const rows = await client.unsafe(sql2, params);
1610
+ return toPostgresResult(await client.unsafe(sql2, params));
1611
+ },
1612
+ async reserve() {
1613
+ await ensure();
1614
+ const conn = await client.reserve();
1212
1615
  return {
1213
- rows: Array.from(rows),
1214
- changes: rows.count ?? rows.length
1616
+ async execute(sql2, params) {
1617
+ return toPostgresResult(await conn.unsafe(sql2, params));
1618
+ },
1619
+ async release() {
1620
+ conn.release();
1621
+ },
1622
+ async close() {
1623
+ conn.release();
1624
+ }
1215
1625
  };
1216
1626
  },
1217
1627
  async close() {
@@ -1347,7 +1757,10 @@ var column = {
1347
1757
  var Model = class {
1348
1758
  static tablename;
1349
1759
  };
1760
+ var columnsCache = /* @__PURE__ */ new WeakMap();
1350
1761
  function columnsOf(model) {
1762
+ const cached = columnsCache.get(model);
1763
+ if (cached) return cached;
1351
1764
  const instance = new model();
1352
1765
  const out = {};
1353
1766
  for (const [key, value] of Object.entries(instance)) {
@@ -1355,9 +1768,12 @@ function columnsOf(model) {
1355
1768
  out[key] = value;
1356
1769
  }
1357
1770
  }
1771
+ columnsCache.set(model, out);
1358
1772
  return out;
1359
1773
  }
1360
1774
 
1775
+ exports.ActiveRecord = ActiveRecord;
1776
+ exports.Agg = Agg;
1361
1777
  exports.AsyncEngine = AsyncEngine;
1362
1778
  exports.AsyncResult = AsyncResult;
1363
1779
  exports.AsyncSession = AsyncSession;
@@ -1373,6 +1789,7 @@ exports.NoResultError = NoResultError;
1373
1789
  exports.NodeSqliteDriver = NodeSqliteDriver;
1374
1790
  exports.OPERATORS = OPERATORS;
1375
1791
  exports.PostgresDialect = PostgresDialect;
1792
+ exports.QueryExecutionError = QueryExecutionError;
1376
1793
  exports.RecordNotFound = RecordNotFound;
1377
1794
  exports.SelectBuilder = SelectBuilder;
1378
1795
  exports.SqliteDialect = SqliteDialect;
@@ -1381,10 +1798,13 @@ exports.SyncResult = SyncResult;
1381
1798
  exports.SyncSession = SyncSession;
1382
1799
  exports.UpdateBuilder = UpdateBuilder;
1383
1800
  exports.ValidationError = ValidationError;
1801
+ exports.activeRecord = activeRecord;
1384
1802
  exports.and = and;
1803
+ exports.avg = avg;
1385
1804
  exports.belongsTo = belongsTo;
1386
1805
  exports.column = column;
1387
1806
  exports.columnsOf = columnsOf;
1807
+ exports.count = count;
1388
1808
  exports.createEngine = createEngine;
1389
1809
  exports.createSyncEngine = createSyncEngine;
1390
1810
  exports.del = del;
@@ -1396,6 +1816,8 @@ exports.insert = insert;
1396
1816
  exports.isCondition = isCondition;
1397
1817
  exports.join = join;
1398
1818
  exports.loadRelations = loadRelations;
1819
+ exports.max = max;
1820
+ exports.min = min;
1399
1821
  exports.not = not;
1400
1822
  exports.or = or;
1401
1823
  exports.parse = parse;
@@ -1403,6 +1825,7 @@ exports.parseDatabaseUrl = parseDatabaseUrl;
1403
1825
  exports.select = select;
1404
1826
  exports.sql = sql;
1405
1827
  exports.stringify = stringify;
1828
+ exports.sum = sum;
1406
1829
  exports.toCondNode = toCondNode;
1407
1830
  exports.toDict = toDict;
1408
1831
  exports.toJSON = toJSON;