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.
@@ -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
  }
@@ -405,12 +481,49 @@ function fromDict(model, data) {
405
481
  function parse(model, json) {
406
482
  return fromDict(model, JSON.parse(json));
407
483
  }
484
+ function decoderForKind(kind) {
485
+ switch (kind) {
486
+ case "bigint":
487
+ return (v) => v == null ? null : typeof v === "bigint" ? v : BigInt(v);
488
+ case "date":
489
+ case "datetime":
490
+ case "timestamp":
491
+ return (v) => v == null ? null : v instanceof Date ? v : new Date(v);
492
+ case "blob":
493
+ return (v) => v == null ? null : v instanceof Uint8Array ? v : fromBase64(v);
494
+ case "json":
495
+ return (v) => v == null ? null : typeof v === "string" ? JSON.parse(v) : v;
496
+ case "numeric":
497
+ return (v) => v == null ? null : typeof v === "string" ? v : String(v);
498
+ case "boolean":
499
+ return (v) => v == null ? null : typeof v === "boolean" ? v : v === 1 || v === "true";
500
+ case "smallint":
501
+ case "integer":
502
+ case "real":
503
+ case "double":
504
+ return (v) => v == null ? null : typeof v === "number" ? v : Number(v);
505
+ default:
506
+ return null;
507
+ }
508
+ }
509
+ var decoderCache = /* @__PURE__ */ new WeakMap();
510
+ function decodersFor(model) {
511
+ const cached = decoderCache.get(model);
512
+ if (cached) return cached;
513
+ const map = /* @__PURE__ */ new Map();
514
+ for (const [name, col] of Object.entries(columnsOf(model))) {
515
+ const decoder = decoderForKind(col.type.kind);
516
+ if (decoder) map.set(name, decoder);
517
+ }
518
+ decoderCache.set(model, map);
519
+ return map;
520
+ }
408
521
  function coerceRow(model, raw) {
409
- const columns = columnsOf(model);
522
+ const decoders = decodersFor(model);
410
523
  const out = {};
411
- for (const [name, value] of Object.entries(raw)) {
412
- const col = columns[name];
413
- out[name] = col ? decodeValue(col, value) : value;
524
+ for (const name of Object.keys(raw)) {
525
+ const decode2 = decoders.get(name);
526
+ out[name] = decode2 ? decode2(raw[name]) : raw[name];
414
527
  }
415
528
  return out;
416
529
  }
@@ -435,10 +548,29 @@ var Params = class {
435
548
  return this.placeholder(this.values.length);
436
549
  }
437
550
  };
438
- var BaseDialect = class {
439
- /** Quote an identifier (column/table) for the active dialect. */
551
+ var BaseDialect = class _BaseDialect {
552
+ /**
553
+ * INSERT SQL templates keyed by structure (dialect|table|columns|rowCount|
554
+ * returning). Shared across dialect instances — the key namespaces by dialect
555
+ * name, and the placeholder text is dialect-specific but structure-determined.
556
+ */
557
+ static insertTemplates = /* @__PURE__ */ new Map();
558
+ /** Quoted-identifier cache (see {@link quoteId}). Shared across dialects. */
559
+ static quotedIds = /* @__PURE__ */ new Map();
560
+ /**
561
+ * Quote an identifier (column/table) for the active dialect.
562
+ *
563
+ * Memoized: identifiers form a small, stable set (column/table names), but this
564
+ * runs for every identifier on every compile. Caching the quoted form removes a
565
+ * regex-replace + string allocation from the hot path. The standard double-quote
566
+ * form is identical across both dialects, so one shared cache is correct.
567
+ */
440
568
  quoteId(name) {
441
- return `"${name.replace(/"/g, '""')}"`;
569
+ const cached = _BaseDialect.quotedIds.get(name);
570
+ if (cached !== void 0) return cached;
571
+ const quoted = `"${name.replace(/"/g, '""')}"`;
572
+ _BaseDialect.quotedIds.set(name, quoted);
573
+ return quoted;
442
574
  }
443
575
  /** Compile any node to `{ sql, params }`. */
444
576
  compile(node) {
@@ -471,10 +603,23 @@ var BaseDialect = class {
471
603
  }
472
604
  // ---- statements -------------------------------------------------------
473
605
  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)}`;
606
+ let cols;
607
+ if (node.aggregates.length > 0) {
608
+ const groupSel = node.groupBy.map((c) => this.quoteId(c));
609
+ const aggSel = node.aggregates.map((a) => {
610
+ const inner = a.column === "*" ? "*" : this.quoteId(a.column);
611
+ return `${a.fn.toUpperCase()}(${inner}) AS ${this.quoteId(a.alias)}`;
612
+ });
613
+ cols = [...groupSel, ...aggSel].join(", ");
614
+ } else {
615
+ cols = node.columns === "*" ? "*" : node.columns.map((c) => this.quoteId(c)).join(", ");
616
+ }
617
+ let sql2 = `SELECT ${node.distinct ? "DISTINCT " : ""}${cols} FROM ${this.quoteId(node.table)}`;
476
618
  const where = this.compileCondition(node.where, params, (k) => this.quoteId(k));
477
619
  if (where) sql2 += ` WHERE ${where}`;
620
+ if (node.groupBy.length > 0) {
621
+ sql2 += ` GROUP BY ${node.groupBy.map((c) => this.quoteId(c)).join(", ")}`;
622
+ }
478
623
  if (node.orderBy.length > 0) {
479
624
  const terms = node.orderBy.map(
480
625
  (t) => `${this.quoteId(t.column)} ${t.direction === "desc" ? "DESC" : "ASC"}`
@@ -487,12 +632,44 @@ var BaseDialect = class {
487
632
  }
488
633
  compileInsert(node, params) {
489
634
  const columns = node.values.length > 0 ? Object.keys(node.values[0]) : [];
635
+ for (const row of node.values) {
636
+ for (const c of columns) params.bind(row[c] ?? null);
637
+ }
638
+ const conflictCols = node.onConflict && node.onConflict.update !== "nothing" ? Object.keys(node.onConflict.update) : [];
639
+ for (const c of conflictCols) {
640
+ params.bind((node.onConflict?.update)[c]);
641
+ }
642
+ return this.insertTemplate(node, columns, conflictCols);
643
+ }
644
+ /**
645
+ * The INSERT SQL template for a given structure, cached across calls.
646
+ *
647
+ * The text depends only on (dialect, table, columns, row count, returning,
648
+ * conflict shape) — never on the bound values — and placeholder positions are
649
+ * deterministic from the counts (a fresh statement always starts binding at 1).
650
+ * So a per-row insert loop compiles the string once and reuses it every row.
651
+ */
652
+ insertTemplate(node, columns, conflictCols) {
653
+ const returningKey = node.returning === null ? "" : node.returning === "*" ? "*" : node.returning.join(",");
654
+ const conflictKey = node.onConflict ? `${node.onConflict.target.join(",")}>${node.onConflict.update === "nothing" ? "nothing" : conflictCols.join(",")}` : "";
655
+ const key = `${this.name}|${node.table}|${columns.join(",")}|${node.values.length}|${returningKey}|${conflictKey}`;
656
+ const cached = _BaseDialect.insertTemplates.get(key);
657
+ if (cached !== void 0) return cached;
490
658
  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(", ");
659
+ let position = 0;
660
+ const rowsSql = node.values.map(() => `(${columns.map(() => this.placeholder(++position)).join(", ")})`).join(", ");
494
661
  let sql2 = `INSERT INTO ${this.quoteId(node.table)} (${colSql}) VALUES ${rowsSql}`;
662
+ if (node.onConflict) {
663
+ const target = node.onConflict.target.map((c) => this.quoteId(c)).join(", ");
664
+ if (node.onConflict.update === "nothing") {
665
+ sql2 += ` ON CONFLICT (${target}) DO NOTHING`;
666
+ } else {
667
+ const assignments = conflictCols.map((c) => `${this.quoteId(c)} = ${this.placeholder(++position)}`).join(", ");
668
+ sql2 += ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
669
+ }
670
+ }
495
671
  sql2 += this.compileReturning(node.returning);
672
+ _BaseDialect.insertTemplates.set(key, sql2);
496
673
  return sql2;
497
674
  }
498
675
  compileUpdate(node, params) {
@@ -814,6 +991,101 @@ var BaseRepository = class {
814
991
  }
815
992
  };
816
993
 
994
+ // src/active-record.ts
995
+ function primaryKeyOf2(model) {
996
+ for (const [name, col] of Object.entries(columnsOf(model))) {
997
+ if (col.flags.primaryKey) return name;
998
+ }
999
+ throw new Error(`${model.tablename} has no primary key`);
1000
+ }
1001
+ var ActiveRecord = class {
1002
+ constructor(model, session, data) {
1003
+ this.model = model;
1004
+ this.session = session;
1005
+ this.data = data;
1006
+ this.pk = primaryKeyOf2(model);
1007
+ }
1008
+ model;
1009
+ session;
1010
+ data;
1011
+ pk;
1012
+ /** The primary-key value of the wrapped row. */
1013
+ pkValue() {
1014
+ return this.data[this.pk];
1015
+ }
1016
+ pkFilter() {
1017
+ return { [this.pk]: this.pkValue() };
1018
+ }
1019
+ /**
1020
+ * Persist the current `data` — insert if new, otherwise overwrite the existing
1021
+ * row (upsert on the primary key). Refreshes `data` from the returned row.
1022
+ *
1023
+ * @returns This wrapper, for chaining.
1024
+ */
1025
+ async save() {
1026
+ const cols = columnsOf(this.model);
1027
+ const rowData = this.data;
1028
+ const setPatch = {};
1029
+ for (const c of Object.keys(rowData)) {
1030
+ if (c !== this.pk && c in cols) setPatch[c] = rowData[c];
1031
+ }
1032
+ const saved = await this.session.execute(
1033
+ insert(this.model).values(this.data).onConflictDoUpdate(
1034
+ [this.pk],
1035
+ setPatch
1036
+ ).returning()
1037
+ ).one();
1038
+ this.data = saved;
1039
+ return this;
1040
+ }
1041
+ /**
1042
+ * Update the given columns for this row and merge them into `data`.
1043
+ *
1044
+ * @param patch The columns to change.
1045
+ * @returns This wrapper, for chaining.
1046
+ */
1047
+ async update(patch) {
1048
+ await this.session.execute(update(this.model).set(patch).where(this.pkFilter()));
1049
+ this.data = { ...this.data, ...patch };
1050
+ return this;
1051
+ }
1052
+ /**
1053
+ * Delete this row.
1054
+ *
1055
+ * @returns The number of rows affected (0 or 1).
1056
+ */
1057
+ async delete() {
1058
+ return this.session.execute(del(this.model).where(this.pkFilter())).rowsAffected();
1059
+ }
1060
+ /**
1061
+ * Re-fetch this row by primary key and refresh `data`.
1062
+ *
1063
+ * @returns This wrapper, for chaining.
1064
+ * @throws When the row no longer exists.
1065
+ */
1066
+ async reload() {
1067
+ const fresh = await this.session.execute(select(this.model).where(this.pkFilter())).first();
1068
+ if (fresh === null) {
1069
+ throw new Error(
1070
+ `${this.model.tablename} row ${JSON.stringify(this.pkValue())} not found on reload`
1071
+ );
1072
+ }
1073
+ this.data = fresh;
1074
+ return this;
1075
+ }
1076
+ };
1077
+ function activeRecord(model, session) {
1078
+ const pk = primaryKeyOf2(model);
1079
+ return {
1080
+ wrap: (row) => new ActiveRecord(model, session, row),
1081
+ create: (data) => new ActiveRecord(model, session, data),
1082
+ async get(id) {
1083
+ const row = await session.execute(select(model).where({ [pk]: id })).first();
1084
+ return row === null ? null : new ActiveRecord(model, session, row);
1085
+ }
1086
+ };
1087
+ }
1088
+
817
1089
  // src/relations.ts
818
1090
  function hasMany(target, keys) {
819
1091
  return {
@@ -876,6 +1148,14 @@ function encodeSqliteParam(value) {
876
1148
  var NodeSqliteDriver = class _NodeSqliteDriver {
877
1149
  // biome-ignore lint/suspicious/noExplicitAny: node:sqlite DatabaseSync has no shipped types here.
878
1150
  db;
1151
+ /**
1152
+ * Prepared-statement cache keyed by SQL text. tempest-db-js always
1153
+ * parameterizes, so a query shape maps to one stable SQL string — reusing the
1154
+ * compiled statement avoids re-`prepare()` on every call (the dominant cost of
1155
+ * per-row inserts and point lookups).
1156
+ */
1157
+ // biome-ignore lint/suspicious/noExplicitAny: node:sqlite StatementSync has no shipped types here.
1158
+ statements = /* @__PURE__ */ new Map();
879
1159
  // biome-ignore lint/suspicious/noExplicitAny: accept an already-open DatabaseSync handle.
880
1160
  constructor(database) {
881
1161
  this.db = database;
@@ -885,8 +1165,17 @@ var NodeSqliteDriver = class _NodeSqliteDriver {
885
1165
  const { DatabaseSync } = nodeRequire("node:sqlite");
886
1166
  return new _NodeSqliteDriver(new DatabaseSync(path));
887
1167
  }
888
- execute(sql2, params) {
1168
+ /** Return the cached prepared statement for `sql`, preparing it on first use. */
1169
+ // biome-ignore lint/suspicious/noExplicitAny: statement type is unavailable here.
1170
+ prepare(sql2) {
1171
+ const cached = this.statements.get(sql2);
1172
+ if (cached) return cached;
889
1173
  const stmt = this.db.prepare(sql2);
1174
+ this.statements.set(sql2, stmt);
1175
+ return stmt;
1176
+ }
1177
+ execute(sql2, params) {
1178
+ const stmt = this.prepare(sql2);
890
1179
  const bound = params.map(encodeSqliteParam);
891
1180
  if (returnsRows(sql2)) {
892
1181
  return { rows: stmt.all(...bound), changes: 0 };
@@ -895,11 +1184,12 @@ var NodeSqliteDriver = class _NodeSqliteDriver {
895
1184
  return { rows: [], changes: Number(info.changes ?? 0) };
896
1185
  }
897
1186
  *iterate(sql2, params) {
898
- const stmt = this.db.prepare(sql2);
1187
+ const stmt = this.prepare(sql2);
899
1188
  const bound = params.map(encodeSqliteParam);
900
1189
  yield* stmt.iterate(...bound);
901
1190
  }
902
1191
  close() {
1192
+ this.statements.clear();
903
1193
  this.db.close();
904
1194
  }
905
1195
  };
@@ -941,6 +1231,36 @@ var NoResultError = class extends Error {
941
1231
  this.name = "NoResultError";
942
1232
  }
943
1233
  };
1234
+ function previewParam(value) {
1235
+ if (value === null || value === void 0) return "null";
1236
+ if (value instanceof Uint8Array) return `<${value.length} bytes>`;
1237
+ const text = typeof value === "string" ? value : String(value);
1238
+ return text.length > 64 ? `${text.slice(0, 61)}...` : text;
1239
+ }
1240
+ var QueryExecutionError = class extends Error {
1241
+ constructor(cause, sql2, params) {
1242
+ const reason = cause instanceof Error ? cause.message : String(cause);
1243
+ super(
1244
+ `Query failed: ${reason}
1245
+ SQL: ${sql2}
1246
+ params: [${params.map(previewParam).join(", ")}]`
1247
+ );
1248
+ this.cause = cause;
1249
+ this.sql = sql2;
1250
+ this.params = params;
1251
+ this.name = "QueryExecutionError";
1252
+ }
1253
+ cause;
1254
+ sql;
1255
+ params;
1256
+ };
1257
+ function emitLog(logger, sql2, params) {
1258
+ if (!logger) return;
1259
+ try {
1260
+ logger({ sql: sql2, params });
1261
+ } catch {
1262
+ }
1263
+ }
944
1264
  function firstScalar(row) {
945
1265
  if (!row) return null;
946
1266
  const keys = Object.keys(row);
@@ -1010,29 +1330,40 @@ var AsyncResult = class {
1010
1330
  };
1011
1331
  var savepointCounter = 0;
1012
1332
  var SyncSession = class {
1013
- constructor(driver, dialect) {
1333
+ constructor(driver, dialect, logger) {
1014
1334
  this.driver = driver;
1015
1335
  this.dialect = dialect;
1336
+ this.logger = logger;
1016
1337
  }
1017
1338
  driver;
1018
1339
  dialect;
1340
+ logger;
1341
+ /** Log, run, and error-wrap one raw statement. */
1342
+ exec(sql2, params) {
1343
+ emitLog(this.logger, sql2, params);
1344
+ try {
1345
+ return this.driver.execute(sql2, params);
1346
+ } catch (error) {
1347
+ throw new QueryExecutionError(error, sql2, params);
1348
+ }
1349
+ }
1019
1350
  /** Compile, run, and coerce a builder into a result. */
1020
1351
  execute(builder) {
1021
1352
  const node = builder.node;
1022
1353
  const { sql: sql2, params } = this.dialect.compile(node);
1023
- const result = this.driver.execute(sql2, params);
1354
+ const result = this.exec(sql2, params);
1024
1355
  const rows = mapRows(builder, result.rows);
1025
1356
  return new SyncResult(rows, result.changes);
1026
1357
  }
1027
1358
  /** Run `fn` inside a transaction: commit on success, rollback on throw. */
1028
1359
  transaction(fn) {
1029
- this.driver.execute("BEGIN", []);
1360
+ this.exec("BEGIN", []);
1030
1361
  try {
1031
1362
  const out = fn(this);
1032
- this.driver.execute("COMMIT", []);
1363
+ this.exec("COMMIT", []);
1033
1364
  return out;
1034
1365
  } catch (error) {
1035
- this.driver.execute("ROLLBACK", []);
1366
+ this.exec("ROLLBACK", []);
1036
1367
  throw error;
1037
1368
  }
1038
1369
  }
@@ -1040,13 +1371,13 @@ var SyncSession = class {
1040
1371
  beginNested(fn) {
1041
1372
  savepointCounter += 1;
1042
1373
  const name = `qsp_${savepointCounter}`;
1043
- this.driver.execute(`SAVEPOINT ${name}`, []);
1374
+ this.exec(`SAVEPOINT ${name}`, []);
1044
1375
  try {
1045
1376
  const out = fn(this);
1046
- this.driver.execute(`RELEASE ${name}`, []);
1377
+ this.exec(`RELEASE ${name}`, []);
1047
1378
  return out;
1048
1379
  } catch (error) {
1049
- this.driver.execute(`ROLLBACK TO ${name}`, []);
1380
+ this.exec(`ROLLBACK TO ${name}`, []);
1050
1381
  throw error;
1051
1382
  }
1052
1383
  }
@@ -1057,31 +1388,51 @@ var SyncSession = class {
1057
1388
  *stream(builder) {
1058
1389
  const node = builder.node;
1059
1390
  const { sql: sql2, params } = this.dialect.compile(node);
1391
+ emitLog(this.logger, sql2, params);
1060
1392
  if (this.driver.iterate) {
1061
- for (const raw of this.driver.iterate(sql2, params)) {
1062
- yield coerceOne(builder, raw);
1393
+ try {
1394
+ for (const raw of this.driver.iterate(sql2, params)) {
1395
+ yield coerceOne(builder, raw);
1396
+ }
1397
+ } catch (error) {
1398
+ throw new QueryExecutionError(error, sql2, params);
1063
1399
  }
1064
1400
  return;
1065
1401
  }
1066
- for (const raw of this.driver.execute(sql2, params).rows) {
1402
+ for (const raw of this.exec(sql2, params).rows) {
1067
1403
  yield coerceOne(builder, raw);
1068
1404
  }
1069
1405
  }
1070
1406
  close() {
1071
1407
  this.driver.close();
1072
1408
  }
1409
+ /** `using session = ...` closes the driver when the scope exits. */
1410
+ [Symbol.dispose]() {
1411
+ this.close();
1412
+ }
1073
1413
  };
1074
- var AsyncSession = class {
1075
- constructor(driver, dialect) {
1414
+ var AsyncSession = class _AsyncSession {
1415
+ constructor(driver, dialect, logger) {
1076
1416
  this.driver = driver;
1077
1417
  this.dialect = dialect;
1418
+ this.logger = logger;
1078
1419
  }
1079
1420
  driver;
1080
1421
  dialect;
1422
+ logger;
1423
+ /** Log, run, and error-wrap one raw statement. */
1424
+ async exec(sql2, params) {
1425
+ emitLog(this.logger, sql2, params);
1426
+ try {
1427
+ return await this.driver.execute(sql2, params);
1428
+ } catch (error) {
1429
+ throw new QueryExecutionError(error, sql2, params);
1430
+ }
1431
+ }
1081
1432
  execute(builder) {
1082
1433
  const node = builder.node;
1083
1434
  const { sql: sql2, params } = this.dialect.compile(node);
1084
- const inner = this.driver.execute(sql2, params).then((result) => {
1435
+ const inner = this.exec(sql2, params).then((result) => {
1085
1436
  const rows = mapRows(builder, result.rows);
1086
1437
  return new SyncResult(rows, result.changes);
1087
1438
  });
@@ -1091,40 +1442,66 @@ var AsyncSession = class {
1091
1442
  async *stream(builder) {
1092
1443
  const node = builder.node;
1093
1444
  const { sql: sql2, params } = this.dialect.compile(node);
1445
+ emitLog(this.logger, sql2, params);
1094
1446
  if (this.driver.iterate) {
1095
- for await (const raw of this.driver.iterate(sql2, params)) {
1096
- yield coerceOne(builder, raw);
1447
+ try {
1448
+ for await (const raw of this.driver.iterate(sql2, params)) {
1449
+ yield coerceOne(builder, raw);
1450
+ }
1451
+ } catch (error) {
1452
+ throw new QueryExecutionError(error, sql2, params);
1097
1453
  }
1098
1454
  return;
1099
1455
  }
1100
- const result = await this.driver.execute(sql2, params);
1456
+ const result = await this.exec(sql2, params);
1101
1457
  for (const raw of result.rows) {
1102
1458
  yield coerceOne(builder, raw);
1103
1459
  }
1104
1460
  }
1105
1461
  async transaction(fn) {
1106
- await this.driver.execute("BEGIN", []);
1462
+ if (this.driver.reserve) {
1463
+ const reserved = await this.driver.reserve();
1464
+ const scoped = new _AsyncSession(reserved, this.dialect, this.logger);
1465
+ try {
1466
+ await scoped.exec("BEGIN", []);
1467
+ const out = await fn(scoped);
1468
+ await scoped.exec("COMMIT", []);
1469
+ return out;
1470
+ } catch (error) {
1471
+ await scoped.exec("ROLLBACK", []);
1472
+ throw error;
1473
+ } finally {
1474
+ await reserved.release();
1475
+ }
1476
+ }
1477
+ await this.exec("BEGIN", []);
1107
1478
  try {
1108
1479
  const out = await fn(this);
1109
- await this.driver.execute("COMMIT", []);
1480
+ await this.exec("COMMIT", []);
1110
1481
  return out;
1111
1482
  } catch (error) {
1112
- await this.driver.execute("ROLLBACK", []);
1483
+ await this.exec("ROLLBACK", []);
1113
1484
  throw error;
1114
1485
  }
1115
1486
  }
1116
1487
  async close() {
1117
1488
  await this.driver.close();
1118
1489
  }
1490
+ /** `await using session = ...` closes the driver when the scope exits. */
1491
+ async [Symbol.asyncDispose]() {
1492
+ await this.close();
1493
+ }
1119
1494
  };
1120
1495
  var SyncEngine = class {
1121
- constructor(driver) {
1496
+ constructor(driver, logger) {
1122
1497
  this.driver = driver;
1498
+ this.logger = logger;
1123
1499
  }
1124
1500
  driver;
1501
+ logger;
1125
1502
  dialect = "sqlite";
1126
1503
  session() {
1127
- return new SyncSession(this.driver, getDialect("sqlite"));
1504
+ return new SyncSession(this.driver, getDialect("sqlite"), this.logger);
1128
1505
  }
1129
1506
  transaction(fn) {
1130
1507
  return this.session().transaction(fn);
@@ -1132,16 +1509,22 @@ var SyncEngine = class {
1132
1509
  close() {
1133
1510
  this.driver.close();
1134
1511
  }
1512
+ /** `using engine = createSyncEngine(...)` closes the pool when the scope exits. */
1513
+ [Symbol.dispose]() {
1514
+ this.close();
1515
+ }
1135
1516
  };
1136
1517
  var AsyncEngine = class {
1137
- constructor(driver, dialect) {
1518
+ constructor(driver, dialect, logger) {
1138
1519
  this.driver = driver;
1139
1520
  this.dialect = dialect;
1521
+ this.logger = logger;
1140
1522
  }
1141
1523
  driver;
1142
1524
  dialect;
1525
+ logger;
1143
1526
  session() {
1144
- return new AsyncSession(this.driver, getDialect(this.dialect));
1527
+ return new AsyncSession(this.driver, getDialect(this.dialect), this.logger);
1145
1528
  }
1146
1529
  transaction(fn) {
1147
1530
  return this.session().transaction(fn);
@@ -1149,6 +1532,10 @@ var AsyncEngine = class {
1149
1532
  async close() {
1150
1533
  await this.driver.close();
1151
1534
  }
1535
+ /** `await using engine = createEngine(...)` closes the pool when the scope exits. */
1536
+ async [Symbol.asyncDispose]() {
1537
+ await this.close();
1538
+ }
1152
1539
  };
1153
1540
  function asAsync(driver) {
1154
1541
  const syncIterate = driver.iterate?.bind(driver);
@@ -1172,17 +1559,29 @@ function createSyncEngine(url, options) {
1172
1559
  `createSyncEngine supports only SQLite; ${parsed.dialect} is async-only \u2014 use createEngine.`
1173
1560
  );
1174
1561
  }
1175
- return new SyncEngine(openSqliteDriver(parsed.database ?? ":memory:"));
1562
+ return new SyncEngine(
1563
+ openSqliteDriver(parsed.database ?? ":memory:"),
1564
+ options?.onQuery
1565
+ );
1176
1566
  }
1177
1567
  function createEngine(url, options) {
1178
1568
  const parsed = parseDatabaseUrl(url);
1179
1569
  if (parsed.dialect === "sqlite") {
1180
1570
  return new AsyncEngine(
1181
1571
  asAsync(openSqliteDriver(parsed.database ?? ":memory:")),
1182
- "sqlite"
1572
+ "sqlite",
1573
+ options?.onQuery
1183
1574
  );
1184
1575
  }
1185
- return new AsyncEngine(createPostgresDriver(parsed.raw, options?.pool), "postgresql");
1576
+ return new AsyncEngine(
1577
+ createPostgresDriver(parsed.raw, options?.pool),
1578
+ "postgresql",
1579
+ options?.onQuery
1580
+ );
1581
+ }
1582
+ function toPostgresResult(rows) {
1583
+ const arr = rows;
1584
+ return { rows: Array.from(arr), changes: arr.count ?? arr.length };
1186
1585
  }
1187
1586
  function createPostgresDriver(url, pool) {
1188
1587
  let client;
@@ -1205,10 +1604,21 @@ function createPostgresDriver(url, pool) {
1205
1604
  return {
1206
1605
  async execute(sql2, params) {
1207
1606
  await ensure();
1208
- const rows = await client.unsafe(sql2, params);
1607
+ return toPostgresResult(await client.unsafe(sql2, params));
1608
+ },
1609
+ async reserve() {
1610
+ await ensure();
1611
+ const conn = await client.reserve();
1209
1612
  return {
1210
- rows: Array.from(rows),
1211
- changes: rows.count ?? rows.length
1613
+ async execute(sql2, params) {
1614
+ return toPostgresResult(await conn.unsafe(sql2, params));
1615
+ },
1616
+ async release() {
1617
+ conn.release();
1618
+ },
1619
+ async close() {
1620
+ conn.release();
1621
+ }
1212
1622
  };
1213
1623
  },
1214
1624
  async close() {
@@ -1344,7 +1754,10 @@ var column = {
1344
1754
  var Model = class {
1345
1755
  static tablename;
1346
1756
  };
1757
+ var columnsCache = /* @__PURE__ */ new WeakMap();
1347
1758
  function columnsOf(model) {
1759
+ const cached = columnsCache.get(model);
1760
+ if (cached) return cached;
1348
1761
  const instance = new model();
1349
1762
  const out = {};
1350
1763
  for (const [key, value] of Object.entries(instance)) {
@@ -1352,9 +1765,10 @@ function columnsOf(model) {
1352
1765
  out[key] = value;
1353
1766
  }
1354
1767
  }
1768
+ columnsCache.set(model, out);
1355
1769
  return out;
1356
1770
  }
1357
1771
 
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
1772
+ export { ActiveRecord, Agg, AsyncEngine, AsyncResult, AsyncSession, BaseDialect, BaseRepository, Column, DeleteBuilder, InsertBuilder, InvalidDatabaseUrl, JoinBuilder, Model, 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 };
1773
+ //# sourceMappingURL=chunk-AGDD7K3F.js.map
1774
+ //# sourceMappingURL=chunk-AGDD7K3F.js.map