@nextlyhq/adapter-drizzle 0.0.2-alpha.68 → 0.0.2-alpha.69

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.
Files changed (63) hide show
  1. package/dist/adapter-BypoOEEN.d.cts +1166 -0
  2. package/dist/adapter-CSP-Ek-L.d.ts +1166 -0
  3. package/dist/index-BYb6cDli.d.ts +1168 -0
  4. package/dist/index-C4HJsfwA.d.cts +1168 -0
  5. package/dist/index.cjs +2258 -2183
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +5 -10
  8. package/dist/index.d.ts +5 -10
  9. package/dist/index.mjs +2257 -2181
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/migrations.cjs +345 -146
  12. package/dist/migrations.cjs.map +1 -1
  13. package/dist/migrations.d.cts +93 -110
  14. package/dist/migrations.d.ts +93 -110
  15. package/dist/migrations.mjs +344 -144
  16. package/dist/migrations.mjs.map +1 -1
  17. package/dist/schema/index.cjs +21 -6
  18. package/dist/schema/index.cjs.map +1 -1
  19. package/dist/schema/index.d.cts +5 -116
  20. package/dist/schema/index.d.ts +5 -116
  21. package/dist/schema/index.mjs +20 -4
  22. package/dist/schema/index.mjs.map +1 -1
  23. package/dist/schema-Bkkr3JMC.d.cts +264 -0
  24. package/dist/schema-Bkkr3JMC.d.ts +264 -0
  25. package/dist/schema-name.cjs +34 -12
  26. package/dist/schema-name.cjs.map +1 -1
  27. package/dist/schema-name.d.cts +3 -15
  28. package/dist/schema-name.d.ts +3 -15
  29. package/dist/schema-name.mjs +33 -10
  30. package/dist/schema-name.mjs.map +1 -1
  31. package/dist/text-match.cjs +42 -8
  32. package/dist/text-match.cjs.map +1 -1
  33. package/dist/text-match.d.cts +4 -16
  34. package/dist/text-match.d.ts +4 -16
  35. package/dist/text-match.mjs +41 -6
  36. package/dist/text-match.mjs.map +1 -1
  37. package/dist/types/index.cjs +8 -69
  38. package/dist/types/index.d.cts +3 -225
  39. package/dist/types/index.d.ts +3 -225
  40. package/dist/types/index.mjs +1 -64
  41. package/dist/types-CNyA1oIm.cjs +185 -0
  42. package/dist/types-CNyA1oIm.cjs.map +1 -0
  43. package/dist/types-CaF_5jNk.mjs +150 -0
  44. package/dist/types-CaF_5jNk.mjs.map +1 -0
  45. package/dist/version-check.cjs +116 -133
  46. package/dist/version-check.cjs.map +1 -1
  47. package/dist/version-check.d.cts +46 -47
  48. package/dist/version-check.d.ts +46 -47
  49. package/dist/version-check.mjs +115 -131
  50. package/dist/version-check.mjs.map +1 -1
  51. package/package.json +12 -16
  52. package/dist/adapter-bUAaPzjV.d.cts +0 -1143
  53. package/dist/adapter-iJ5hbUvL.d.ts +0 -1143
  54. package/dist/core-CVO7WYDj.d.cts +0 -74
  55. package/dist/core-CVO7WYDj.d.ts +0 -74
  56. package/dist/error-BrdknH2s.d.cts +0 -122
  57. package/dist/error-BrdknH2s.d.ts +0 -122
  58. package/dist/migration-BqHebSwM.d.ts +0 -831
  59. package/dist/migration-Bu0WLdX4.d.cts +0 -831
  60. package/dist/schema-BDn8WfSL.d.cts +0 -200
  61. package/dist/schema-BIQ0YQZ_.d.ts +0 -200
  62. package/dist/types/index.cjs.map +0 -1
  63. package/dist/types/index.mjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,1755 +1,1863 @@
1
- 'use strict';
2
-
3
- var async_hooks = require('async_hooks');
4
- var drizzleOrm = require('drizzle-orm');
5
-
6
- // src/adapter.ts
7
-
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_text_match = require("./text-match.cjs");
3
+ const require_types = require("./types-CNyA1oIm.cjs");
4
+ let node_async_hooks = require("node:async_hooks");
5
+ let drizzle_orm = require("drizzle-orm");
8
6
  // src/column-kinds.ts
7
+ /**
8
+ * Whether a column stores JSON, in the spelling each dialect's column class
9
+ * reports. Drizzle serializes objects for these itself, so a value that
10
+ * arrives already serialized has to be parsed first or it is serialized
11
+ * twice.
12
+ *
13
+ * SQLite matches only text columns declared with `{ mode: "json" }`
14
+ * (`SQLiteTextJson`, dataType `json`). A plain `SQLiteText` column stores a
15
+ * serialized string as it is and must not be parsed, or better-sqlite3 is
16
+ * handed an object it cannot bind.
17
+ */
9
18
  function isJsonColumn(column) {
10
- return column.dataType === "json" || column.columnType === "PgJsonb" || column.columnType === "PgJson" || column.columnType === "MySqlJson" || column.columnType === "SQLiteTextJson";
19
+ return column.dataType === "json" || column.columnType === "PgJsonb" || column.columnType === "PgJson" || column.columnType === "MySqlJson" || column.columnType === "SQLiteTextJson";
11
20
  }
21
+ /**
22
+ * Whether a value is a structured document — a plain object or an array —
23
+ * rather than a scalar, a date, or binary. What a driver does with one of
24
+ * these when the column is not modelled differs (node-postgres spells an
25
+ * array as a PostgreSQL array literal, mysql2 as a value list), and none of
26
+ * it is JSON; serializing here is what makes the same bytes land everywhere.
27
+ */
12
28
  function isStructuredValue(value) {
13
- if (value === null || typeof value !== "object") return false;
14
- if (value instanceof Date) return false;
15
- if (Buffer.isBuffer(value) || ArrayBuffer.isView(value)) return false;
16
- return true;
29
+ if (value === null || typeof value !== "object") return false;
30
+ if (value instanceof Date) return false;
31
+ if (Buffer.isBuffer(value) || ArrayBuffer.isView(value)) return false;
32
+ return true;
17
33
  }
34
+ /**
35
+ * How a value binds to a column the runtime model does not declare, on a
36
+ * driver that binds scalars, dates and binary natively: structured values are
37
+ * serialized as JSON text, everything else passes through.
38
+ */
18
39
  function bindStructuredAsJson(value) {
19
- return isStructuredValue(value) ? JSON.stringify(value) : value;
40
+ return isStructuredValue(value) ? JSON.stringify(value) : value;
20
41
  }
42
+ // src/drizzle-order.ts
43
+ /**
44
+ * Turn order specifications into drizzle order clauses.
45
+ *
46
+ * Extracted from `DrizzleAdapter.select` so the rendered SQL can be asserted
47
+ * directly. That matters more than it looks: `OrderBySpec.nulls` was declared,
48
+ * documented as "can be explicitly controlled", and read NOWHERE — every caller
49
+ * that set it silently got the dialect default instead, and no test could have
50
+ * caught that while the mapping lived inline in a method that needs a database.
51
+ *
52
+ * ## Why null placement is a leading sort key, not `NULLS FIRST/LAST`
53
+ *
54
+ * That clause is not portable: PostgreSQL and SQLite accept it, MySQL does not.
55
+ * `col IS NULL` is a boolean expression all three evaluate, and sorting on it
56
+ * ascending puts non-null rows first — which is `NULLS LAST` by another name,
57
+ * spelled in something every supported engine understands.
58
+ *
59
+ * The defaults genuinely disagree, which is why the control exists at all:
60
+ * PostgreSQL treats NULL as largest and puts it FIRST on a descending order,
61
+ * while MySQL and SQLite treat it as smallest and put it last. A "newest
62
+ * scheduled first" list containing undated drafts therefore returns different
63
+ * rows per engine, and under a limit it can return only drafts.
64
+ */
21
65
  function buildDrizzleOrderBy(columns, orderBy) {
22
- if (!orderBy?.length) return [];
23
- return orderBy.flatMap((spec) => {
24
- const col = columns[spec.column];
25
- if (!col) return [];
26
- const direction = spec.direction === "desc" ? drizzleOrm.desc(col) : drizzleOrm.asc(col);
27
- if (spec.nulls === void 0) return [direction];
28
- const isNull2 = drizzleOrm.sql`${col} is null`;
29
- return [spec.nulls === "last" ? drizzleOrm.asc(isNull2) : drizzleOrm.desc(isNull2), direction];
30
- });
31
- }
32
- function containsPattern(value) {
33
- return `%${value.replace(/[!%_]/g, (character) => `!${character}`)}%`;
66
+ if (!orderBy?.length) return [];
67
+ return orderBy.flatMap((spec) => {
68
+ const col = columns[spec.column];
69
+ if (!col) return [];
70
+ const direction = spec.direction === "desc" ? (0, drizzle_orm.desc)(col) : (0, drizzle_orm.asc)(col);
71
+ if (spec.nulls === void 0) return [direction];
72
+ const isNull = drizzle_orm.sql`${col} is null`;
73
+ return [spec.nulls === "last" ? (0, drizzle_orm.asc)(isNull) : (0, drizzle_orm.desc)(isNull), direction];
74
+ });
34
75
  }
35
- function caseInsensitiveContains(column, value) {
36
- return drizzleOrm.sql`lower(${column}) like lower(${containsPattern(value)}) escape '!'`;
37
- }
38
-
39
76
  // src/drizzle-where.ts
40
77
  function buildDrizzleWhere(table, where) {
41
- const columns = drizzleOrm.getColumns(table);
42
- return processWhereClause(columns, where);
78
+ return processWhereClause((0, drizzle_orm.getColumns)(table), where);
43
79
  }
44
80
  function processWhereClause(columns, where) {
45
- const parts = [];
46
- if (where.and?.length) {
47
- const andParts = where.and.map((item) => {
48
- if (isWhereCondition(item)) {
49
- return buildCondition(columns, item);
50
- }
51
- return processWhereClause(columns, item);
52
- }).filter((p) => p !== void 0);
53
- if (andParts.length) {
54
- parts.push(drizzleOrm.and(...andParts));
55
- }
56
- }
57
- if (where.or?.length) {
58
- const orParts = where.or.map((item) => {
59
- if (isWhereCondition(item)) {
60
- return buildCondition(columns, item);
61
- }
62
- return processWhereClause(columns, item);
63
- }).filter((p) => p !== void 0);
64
- if (orParts.length) {
65
- parts.push(drizzleOrm.or(...orParts));
66
- }
67
- }
68
- if (where.not) {
69
- const notItem = where.not;
70
- const notCondition = isWhereCondition(notItem) ? buildCondition(columns, notItem) : processWhereClause(columns, notItem);
71
- if (notCondition) {
72
- parts.push(drizzleOrm.not(notCondition));
73
- }
74
- }
75
- if (parts.length === 0) {
76
- if (namesABranch(where)) {
77
- throw new Error(
78
- `Where clause produced no condition: ${JSON.stringify(where)}. Every branch resolved to nothing, which would match every row. Pass {} to mean "no filter".`
79
- );
80
- }
81
- return void 0;
82
- }
83
- if (parts.length === 1) return parts[0];
84
- return drizzleOrm.and(...parts);
81
+ const parts = [];
82
+ if (where.and?.length) {
83
+ const andParts = where.and.map((item) => {
84
+ if (isWhereCondition(item)) return buildCondition(columns, item);
85
+ return processWhereClause(columns, item);
86
+ }).filter((p) => p !== void 0);
87
+ if (andParts.length) parts.push((0, drizzle_orm.and)(...andParts));
88
+ }
89
+ if (where.or?.length) {
90
+ const orParts = where.or.map((item) => {
91
+ if (isWhereCondition(item)) return buildCondition(columns, item);
92
+ return processWhereClause(columns, item);
93
+ }).filter((p) => p !== void 0);
94
+ if (orParts.length) parts.push((0, drizzle_orm.or)(...orParts));
95
+ }
96
+ if (where.not) {
97
+ const notItem = where.not;
98
+ const notCondition = isWhereCondition(notItem) ? buildCondition(columns, notItem) : processWhereClause(columns, notItem);
99
+ if (notCondition) parts.push((0, drizzle_orm.not)(notCondition));
100
+ }
101
+ if (parts.length === 0) {
102
+ if (namesABranch(where)) throw new Error(`Where clause produced no condition: ${JSON.stringify(where)}. Every branch resolved to nothing, which would match every row. Pass {} to mean "no filter".`);
103
+ return;
104
+ }
105
+ if (parts.length === 1) return parts[0];
106
+ return (0, drizzle_orm.and)(...parts);
85
107
  }
86
108
  function isWhereCondition(item) {
87
- return "column" in item && "op" in item;
109
+ return "column" in item && "op" in item;
88
110
  }
111
+ /**
112
+ * Whether the clause asked for a constraint at all — as opposed to being the empty clause a
113
+ * caller passes to mean "no filter".
114
+ *
115
+ * An empty `and`/`or` array does NOT count: nothing was named, so nothing could be dropped.
116
+ * What counts is a branch with members in it, or a `not`, because those are the shapes that can
117
+ * resolve to nothing and leave the caller believing a filter was applied.
118
+ */
89
119
  function namesABranch(where) {
90
- return Boolean(where.and?.length || where.or?.length || where.not);
120
+ return Boolean(where.and?.length || where.or?.length || where.not);
91
121
  }
92
122
  function buildCondition(columns, cond) {
93
- const column = columns[cond.column];
94
- if (!column) {
95
- throw new Error(
96
- `Column "${cond.column}" not found in table. Available: ${Object.keys(columns).join(", ")}`
97
- );
98
- }
99
- const col = column;
100
- switch (cond.op) {
101
- case "=":
102
- return drizzleOrm.eq(col, cond.value);
103
- case "!=":
104
- return drizzleOrm.ne(col, cond.value);
105
- case ">":
106
- return drizzleOrm.gt(col, cond.value);
107
- case "<":
108
- return drizzleOrm.lt(col, cond.value);
109
- case ">=":
110
- return drizzleOrm.gte(col, cond.value);
111
- case "<=":
112
- return drizzleOrm.lte(col, cond.value);
113
- case "LIKE":
114
- return drizzleOrm.like(col, cond.value);
115
- case "ILIKE":
116
- return drizzleOrm.sql`lower(${col}) like lower(${cond.value})`;
117
- case "IN":
118
- return drizzleOrm.inArray(col, cond.value);
119
- case "NOT IN":
120
- return drizzleOrm.notInArray(col, cond.value);
121
- case "IS NULL":
122
- return drizzleOrm.isNull(col);
123
- case "IS NOT NULL":
124
- return drizzleOrm.isNotNull(col);
125
- case "BETWEEN":
126
- return drizzleOrm.between(col, cond.value, cond.valueTo);
127
- case "NOT BETWEEN":
128
- return drizzleOrm.notBetween(col, cond.value, cond.valueTo);
129
- case "CONTAINS":
130
- return drizzleOrm.sql`${col} like ${containsPattern(String(cond.value))} escape '!'`;
131
- case "ICONTAINS":
132
- return caseInsensitiveContains(col, String(cond.value));
133
- default:
134
- throw new Error(`Unsupported operator: ${cond.op}`);
135
- }
123
+ const column = columns[cond.column];
124
+ if (!column) throw new Error(`Column "${cond.column}" not found in table. Available: ${Object.keys(columns).join(", ")}`);
125
+ const col = column;
126
+ switch (cond.op) {
127
+ case "=": return (0, drizzle_orm.eq)(col, cond.value);
128
+ case "!=": return (0, drizzle_orm.ne)(col, cond.value);
129
+ case ">": return (0, drizzle_orm.gt)(col, cond.value);
130
+ case "<": return (0, drizzle_orm.lt)(col, cond.value);
131
+ case ">=": return (0, drizzle_orm.gte)(col, cond.value);
132
+ case "<=": return (0, drizzle_orm.lte)(col, cond.value);
133
+ case "LIKE": return (0, drizzle_orm.like)(col, cond.value);
134
+ case "ILIKE": return drizzle_orm.sql`lower(${col}) like lower(${cond.value})`;
135
+ case "IN": return (0, drizzle_orm.inArray)(col, cond.value);
136
+ case "NOT IN": return (0, drizzle_orm.notInArray)(col, cond.value);
137
+ case "IS NULL": return (0, drizzle_orm.isNull)(col);
138
+ case "IS NOT NULL": return (0, drizzle_orm.isNotNull)(col);
139
+ case "BETWEEN": return (0, drizzle_orm.between)(col, cond.value, cond.valueTo);
140
+ case "NOT BETWEEN": return (0, drizzle_orm.notBetween)(col, cond.value, cond.valueTo);
141
+ case "CONTAINS": return drizzle_orm.sql`${col} like ${require_text_match.containsPattern(String(cond.value))} escape '!'`;
142
+ case "ICONTAINS": return require_text_match.caseInsensitiveContains(col, String(cond.value));
143
+ default: throw new Error(`Unsupported operator: ${cond.op}`);
144
+ }
136
145
  }
137
-
138
146
  // src/pinned-transaction.ts
147
+ /**
148
+ * Replaces `db`'s `transaction` with one that runs each transaction on a
149
+ * connection from `checkout`.
150
+ *
151
+ * Drizzle, bound to that single connection, runs BEGIN, the work and COMMIT
152
+ * or ROLLBACK as it always does. When the transaction fails, a second
153
+ * ROLLBACK goes to the connection: a no-op when Drizzle's own reached it,
154
+ * and the confirmation that the connection is out of the transaction when
155
+ * Drizzle's did not. If that ROLLBACK fails too, nothing can confirm it, and
156
+ * the connection is closed rather than pooled. The work's own error is what
157
+ * the caller receives, never a ROLLBACK's.
158
+ *
159
+ * @param db - A Drizzle handle on the adapter's pool
160
+ * @param checkout - Checks out a connection for one transaction
161
+ * @returns The same handle, its `transaction` replaced
162
+ */
139
163
  function pinTransactions(db, checkout) {
140
- const pinned = async (work, config) => {
141
- const connection = await checkout();
142
- let dirty = false;
143
- let workFailure;
144
- try {
145
- return await connection.drizzle.transaction(async (tx) => {
146
- try {
147
- return await work(tx);
148
- } catch (error) {
149
- workFailure = { error };
150
- throw error;
151
- }
152
- }, config);
153
- } catch (error) {
154
- await connection.rollback().catch(() => {
155
- dirty = true;
156
- });
157
- throw workFailure ? workFailure.error : error;
158
- } finally {
159
- connection.release(dirty);
160
- }
161
- };
162
- Object.defineProperty(db, "transaction", {
163
- value: pinned,
164
- configurable: true,
165
- writable: true
166
- });
167
- return db;
164
+ const pinned = async (work, config) => {
165
+ const connection = await checkout();
166
+ let dirty = false;
167
+ let workFailure;
168
+ try {
169
+ return await connection.drizzle.transaction(async (tx) => {
170
+ try {
171
+ return await work(tx);
172
+ } catch (error) {
173
+ workFailure = { error };
174
+ throw error;
175
+ }
176
+ }, config);
177
+ } catch (error) {
178
+ await connection.rollback().catch(() => {
179
+ dirty = true;
180
+ });
181
+ throw workFailure ? workFailure.error : error;
182
+ } finally {
183
+ connection.release(dirty);
184
+ }
185
+ };
186
+ Object.defineProperty(db, "transaction", {
187
+ value: pinned,
188
+ configurable: true,
189
+ writable: true
190
+ });
191
+ return db;
168
192
  }
169
-
170
193
  // src/transaction-forwarders.ts
194
+ /**
195
+ * Create CRUD and Drizzle instance forwarding methods for a TransactionContext.
196
+ *
197
+ * @remarks
198
+ * TransactionContext delegates its CRUD methods to the adapter's Drizzle CRUD
199
+ * implementation while binding the transaction executor so queries run inside
200
+ * the transaction rather than on the connection pool.
201
+ *
202
+ * @param delegator - Object providing the underlying adapter CRUD methods
203
+ * @param txDb - Thunk returning the transaction-bound Drizzle instance
204
+ * @returns Object with forwarded TransactionContext methods
205
+ */
171
206
  function createTransactionForwarders(delegator, txDb) {
172
- return {
173
- select: async (table, options) => {
174
- return delegator.select(table, options, txDb());
175
- },
176
- selectOne: async (table, options) => {
177
- return delegator.selectOne(table, options, txDb());
178
- },
179
- updateCount: async (table, data, where) => {
180
- return delegator.updateCount(table, data, where, txDb());
181
- },
182
- delete: async (table, where, options) => {
183
- return delegator.delete(table, where, options, txDb());
184
- },
185
- upsert: async (table, data, options) => {
186
- return delegator.upsert(table, data, options, txDb());
187
- },
188
- getDrizzle: () => txDb()
189
- };
207
+ return {
208
+ select: async (table, options) => {
209
+ return delegator.select(table, options, txDb());
210
+ },
211
+ selectOne: async (table, options) => {
212
+ return delegator.selectOne(table, options, txDb());
213
+ },
214
+ updateCount: async (table, data, where) => {
215
+ return delegator.updateCount(table, data, where, txDb());
216
+ },
217
+ delete: async (table, where, options) => {
218
+ return delegator.delete(table, where, options, txDb());
219
+ },
220
+ upsert: async (table, data, options) => {
221
+ return delegator.upsert(table, data, options, txDb());
222
+ },
223
+ getDrizzle: () => txDb()
224
+ };
190
225
  }
226
+ /**
227
+ * The Drizzle handles one transaction hands out, built once each.
228
+ *
229
+ * `bare` is the instance the delegated CRUD and `getDrizzle()` run on. `withRelations(relations)` is `drizzleWithRelations`: an instance
230
+ * on the SAME connection whose `query` namespace is populated, so relational
231
+ * reads stay inside the transaction. Both are built lazily — a transaction
232
+ * using only raw execute never constructs one — and the relational ones are
233
+ * memoized per relations object, exactly as `getDrizzle()` memoizes its
234
+ * pooled ones.
235
+ *
236
+ * Shared because the three adapters differ only in how they build an
237
+ * instance on their connection, which is what `build` supplies.
238
+ */
191
239
  function transactionDrizzleHandles(build) {
192
- let bareInstance;
193
- const bare = () => bareInstance ??= build();
194
- const relational = /* @__PURE__ */ new WeakMap();
195
- const withRelations = (relations) => {
196
- let cached = relational.get(relations);
197
- if (cached === void 0) {
198
- cached = build(relations);
199
- relational.set(relations, cached);
200
- }
201
- return cached;
202
- };
203
- return { bare, withRelations };
204
- }
205
-
206
- // src/types/error.ts
207
- function isDatabaseError(error) {
208
- return typeof error === "object" && error !== null && "kind" in error && typeof error.kind === "string";
209
- }
210
- function createDatabaseError(options) {
211
- const error = new Error(options.message);
212
- error.name = "DatabaseError";
213
- error.kind = options.kind;
214
- if (options.code !== void 0) error.code = options.code;
215
- if (options.constraint !== void 0) error.constraint = options.constraint;
216
- if (options.table !== void 0) error.table = options.table;
217
- if (options.column !== void 0) error.column = options.column;
218
- if (options.detail !== void 0) error.detail = options.detail;
219
- if (options.hint !== void 0) error.hint = options.hint;
220
- if (options.cause !== void 0) error.cause = options.cause;
221
- return error;
240
+ let bareInstance;
241
+ const bare = () => bareInstance ??= build();
242
+ const relational = /* @__PURE__ */ new WeakMap();
243
+ const withRelations = (relations) => {
244
+ let cached = relational.get(relations);
245
+ if (cached === void 0) {
246
+ cached = build(relations);
247
+ relational.set(relations, cached);
248
+ }
249
+ return cached;
250
+ };
251
+ return {
252
+ bare,
253
+ withRelations
254
+ };
222
255
  }
256
+ // src/update-statement.ts
257
+ /**
258
+ * The transactional UPDATE, assembled by the adapter rather than by the
259
+ * Drizzle query builder.
260
+ *
261
+ * `db.update(table).set(row)` builds its SET clause by walking the table's
262
+ * DECLARED columns and probing `row` for each — never the other way round —
263
+ * so a key naming a column the runtime model does not declare is dropped
264
+ * without a word. One place relies on writing exactly such a column: the
265
+ * localization transition window. `localized` has been flipped on a
266
+ * collection, the runtime model has moved its translatable columns to a
267
+ * companion table that does not exist yet, and the physical main table still
268
+ * holds them. The default locale keeps writing there until the companion
269
+ * appears, and the transactional INSERT already reaches it because every
270
+ * adapter builds that statement itself. This is the UPDATE half of the same
271
+ * rule, shared by the three adapters so the statement is spelled once.
272
+ *
273
+ * Built on Drizzle's `sql` template rather than a string, so each dialect
274
+ * quotes identifiers and numbers placeholders its own way, and the WHERE is
275
+ * the `buildDrizzleWhere` every other read and write goes through — the raw
276
+ * where builders were removed on purpose and this does not bring one back.
277
+ * It returns at most the rows' identity: a caller who asked for the rows back
278
+ * gets them from a read on the same transaction, decoded the way every read
279
+ * is decoded, rather than from a RETURNING list this module would have to
280
+ * decode a second way — and on a dialect with RETURNING that read is by the
281
+ * identities this statement reports, so it is exactly the rows it changed.
282
+ *
283
+ * A value binds in one of three ways, decided per column:
284
+ * - a column the model DECLARES binds through that column's own encoder.
285
+ * `sql.param(value, column)` is the `Param` the query builder would have
286
+ * made for it, so a date, a JSON document or a boolean reaches the driver
287
+ * as the bytes the builder sent before this existed — for every caller
288
+ * that was already using the builder;
289
+ * - a column it does NOT declare binds through the adapter's own sanitizer,
290
+ * which is how that adapter's transactional INSERT binds every value;
291
+ * - a value that is itself a `Column` or an `SQL` fragment is an expression
292
+ * (`{ destination: table.source }` copies a column) and is written as one,
293
+ * which is what the query builder's `mapUpdateSet` does with it.
294
+ *
295
+ * A declared column with an `$onUpdate` callback that the caller did not name
296
+ * is assigned the callback's value, as the query builder assigns it on every
297
+ * update it builds.
298
+ *
299
+ * A key whose value is `undefined` is not written at all — JSON's meaning of
300
+ * an absent key, and what the builder's `mapUpdateSet` did with it; under
301
+ * both spellings of one column the later key decides, undefined included.
302
+ * `null` is written as NULL. A key naming no column on the physical table is
303
+ * a SQL error from the database: the query builder was the one path that
304
+ * could not say so, which is the defect the transition window was hiding
305
+ * behind. A patch that names nothing is refused before any `$onUpdate`
306
+ * column is considered, as the query builder refused it.
307
+ *
308
+ * @module update-statement
309
+ */
223
310
  function isBindableColumn(value) {
224
- return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.mapToDriverValue === "function";
311
+ return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.mapToDriverValue === "function";
225
312
  }
313
+ /**
314
+ * A JSON column's encoder serializes whatever it is given, so a value that
315
+ * arrives already serialized would be serialized twice. The query-builder path
316
+ * parses such a string first; this keeps the same bytes on the wire.
317
+ */
226
318
  function valueForJsonColumn(value) {
227
- if (typeof value !== "string") return value;
228
- try {
229
- return JSON.parse(value);
230
- } catch {
231
- return value;
232
- }
319
+ if (typeof value !== "string") return value;
320
+ try {
321
+ return JSON.parse(value);
322
+ } catch {
323
+ return value;
324
+ }
233
325
  }
326
+ /**
327
+ * A table's columns, indexed by both spellings a caller may use, and the
328
+ * declared columns in order for the `$onUpdate` sweep.
329
+ */
234
330
  function columnsOf(tableObj) {
235
- const byName = /* @__PURE__ */ new Map();
236
- const declared = [];
237
- for (const [jsName, column] of Object.entries(
238
- drizzleOrm.getColumns(tableObj)
239
- )) {
240
- if (!isBindableColumn(column)) continue;
241
- byName.set(column.name, column);
242
- byName.set(jsName, column);
243
- declared.push(column);
244
- }
245
- return { byName, declared };
331
+ const byName = /* @__PURE__ */ new Map();
332
+ const declared = [];
333
+ for (const [jsName, column] of Object.entries((0, drizzle_orm.getColumns)(tableObj))) {
334
+ if (!isBindableColumn(column)) continue;
335
+ byName.set(column.name, column);
336
+ byName.set(jsName, column);
337
+ declared.push(column);
338
+ }
339
+ return {
340
+ byName,
341
+ declared
342
+ };
246
343
  }
344
+ /** How one value is written: as the expression it already is, or bound. */
247
345
  function boundValue(value, column, bindUnmodeled) {
248
- if (drizzleOrm.is(value, drizzleOrm.SQL) || drizzleOrm.is(value, drizzleOrm.Column)) return drizzleOrm.sql`${value}`;
249
- if (column) {
250
- return drizzleOrm.sql`${drizzleOrm.sql.param(
251
- isJsonColumn(column) ? valueForJsonColumn(value) : value,
252
- column
253
- )}`;
254
- }
255
- return drizzleOrm.sql`${drizzleOrm.sql.param(bindUnmodeled(value))}`;
346
+ if ((0, drizzle_orm.is)(value, drizzle_orm.SQL) || (0, drizzle_orm.is)(value, drizzle_orm.Column)) return drizzle_orm.sql`${value}`;
347
+ if (column) return drizzle_orm.sql`${drizzle_orm.sql.param(isJsonColumn(column) ? valueForJsonColumn(value) : value, column)}`;
348
+ return drizzle_orm.sql`${drizzle_orm.sql.param(bindUnmodeled(value))}`;
256
349
  }
350
+ /**
351
+ * Build the UPDATE for one table.
352
+ *
353
+ * @returns the statement, or `null` when there is nothing to write — every
354
+ * key was `undefined`, or there were none. An UPDATE with an empty SET is a
355
+ * syntax error, so the adapter refuses it by name instead of running it.
356
+ */
257
357
  function buildUpdateStatement(input) {
258
- const { byName, declared } = columnsOf(input.tableObj);
259
- const assignments = /* @__PURE__ */ new Map();
260
- for (const [key, value] of Object.entries(input.data)) {
261
- const column = byName.get(key);
262
- const name = column?.name ?? key;
263
- if (value === void 0) {
264
- assignments.delete(name);
265
- continue;
266
- }
267
- assignments.set(
268
- name,
269
- drizzleOrm.sql`${drizzleOrm.sql.identifier(name)} = ${boundValue(value, column, input.bindUnmodeled)}`
270
- );
271
- }
272
- if (assignments.size === 0) return null;
273
- for (const column of declared) {
274
- if (assignments.has(column.name) || column.onUpdateFn === void 0) {
275
- continue;
276
- }
277
- const value = column.onUpdateFn();
278
- assignments.set(
279
- column.name,
280
- drizzleOrm.sql`${drizzleOrm.sql.identifier(column.name)} = ${boundValue(value, column, input.bindUnmodeled)}`
281
- );
282
- }
283
- const statement = drizzleOrm.sql`UPDATE ${input.tableObj} SET ${drizzleOrm.sql.join([...assignments.values()], drizzleOrm.sql`, `)}`;
284
- const condition = buildDrizzleWhere(input.tableObj, input.where);
285
- if (condition) statement.append(drizzleOrm.sql` WHERE ${condition}`);
286
- if (input.returning && input.returning.length > 0) {
287
- statement.append(drizzleOrm.sql` RETURNING ${drizzleOrm.sql.join(input.returning, drizzleOrm.sql`, `)}`);
288
- }
289
- return statement;
358
+ const { byName, declared } = columnsOf(input.tableObj);
359
+ const assignments = /* @__PURE__ */ new Map();
360
+ for (const [key, value] of Object.entries(input.data)) {
361
+ const column = byName.get(key);
362
+ const name = column?.name ?? key;
363
+ if (value === void 0) {
364
+ assignments.delete(name);
365
+ continue;
366
+ }
367
+ assignments.set(name, drizzle_orm.sql`${drizzle_orm.sql.identifier(name)} = ${boundValue(value, column, input.bindUnmodeled)}`);
368
+ }
369
+ if (assignments.size === 0) return null;
370
+ for (const column of declared) {
371
+ if (assignments.has(column.name) || column.onUpdateFn === void 0) continue;
372
+ const value = column.onUpdateFn();
373
+ assignments.set(column.name, drizzle_orm.sql`${drizzle_orm.sql.identifier(column.name)} = ${boundValue(value, column, input.bindUnmodeled)}`);
374
+ }
375
+ const statement = drizzle_orm.sql`UPDATE ${input.tableObj} SET ${drizzle_orm.sql.join([...assignments.values()], drizzle_orm.sql`, `)}`;
376
+ const condition = buildDrizzleWhere(input.tableObj, input.where);
377
+ if (condition) statement.append(drizzle_orm.sql` WHERE ${condition}`);
378
+ if (input.returning && input.returning.length > 0) statement.append(drizzle_orm.sql` RETURNING ${drizzle_orm.sql.join(input.returning, drizzle_orm.sql`, `)}`);
379
+ return statement;
290
380
  }
291
-
292
381
  // src/adapter.ts
293
- var DATE_DATA_TYPE = /(?:^|\s)date$/;
294
- var WALL_CLOCK_ALIAS_STEM = "__nx_wc";
295
- var WALL_CLOCK_PATTERN = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?$/;
382
+ /**
383
+ * Base database adapter abstract class.
384
+ *
385
+ * @remarks
386
+ * This abstract class provides the foundation for all dialect-specific database adapters
387
+ * (PostgreSQL, MySQL, SQLite). It defines required abstract methods that must be implemented
388
+ * by subclasses, while providing default implementations for CRUD operations and query building.
389
+ *
390
+ * Dialect adapters can override any default method to provide optimized implementations.
391
+ *
392
+ * @packageDocumentation
393
+ */
394
+ /**
395
+ * Where a test harness installs the observer {@link DrizzleAdapter.notePoolAccess}
396
+ * reports a transaction escape to: a global registry key rather than an
397
+ * import, so the harness and every copy of this package agree on it.
398
+ */
399
+ const TRANSACTION_ESCAPE_OBSERVER = Symbol.for("nextly.transactionEscapeObserver");
400
+ /**
401
+ * The SQL text a pool call was handed: a string, or the `text` (pg) or `sql`
402
+ * (mysql2) of a query object. Undefined for a connection checkout.
403
+ */
404
+ function statementOf(argument) {
405
+ if (typeof argument === "string") return argument;
406
+ if (typeof argument !== "object" || argument === null) return void 0;
407
+ const { text, sql } = argument;
408
+ if (typeof text === "string") return text;
409
+ return typeof sql === "string" ? sql : void 0;
410
+ }
411
+ /** The escape observer a test harness installed; undefined in production. */
412
+ function escapeObserver() {
413
+ const observe = globalThis[TRANSACTION_ESCAPE_OBSERVER];
414
+ return typeof observe === "function" ? observe : void 0;
415
+ }
416
+ /**
417
+ * The current stack, deep enough to reach the code that made the query. The
418
+ * driver's and Drizzle's own frames fill V8's default of ten, which cut every
419
+ * report off before its caller. Raised only for this capture and restored
420
+ * after, so nothing else sees the deeper limit; it runs only for an observer a
421
+ * test harness installed.
422
+ */
423
+ function deepStack() {
424
+ const limit = Error.stackTraceLimit;
425
+ try {
426
+ Error.stackTraceLimit = 64;
427
+ return (/* @__PURE__ */ new Error()).stack ?? "";
428
+ } finally {
429
+ Error.stackTraceLimit = limit;
430
+ }
431
+ }
432
+ /**
433
+ * Matches the `dataType` a Drizzle timestamp/datetime column reports, in both
434
+ * the bare form and the object-qualified form some column classes carry.
435
+ */
436
+ const DATE_DATA_TYPE = /(?:^|\s)date$/;
437
+ /**
438
+ * How far an echoed timestamp may sit from the one that was bound and still
439
+ * count as the same value.
440
+ *
441
+ * A column with no fractional precision keeps whole seconds, so a stored value
442
+ * can differ from the bound one by up to a second without anything being wrong.
443
+ * Every real UTC offset is at least fifteen minutes, so a shift is never
444
+ * mistaken for rounding.
445
+ */
446
+ /**
447
+ * Stem for the alias a statement spells a timestamp's wall clock out under.
448
+ *
449
+ * Short on purpose. PostgreSQL truncates an identifier at 63 bytes, and a
450
+ * truncated alias is worse than a rejected one: the statement still succeeds,
451
+ * the lookup misses the name it asked for, and the row keeps both the driver's
452
+ * value and a stray property. A stem plus an index stays far inside the limit
453
+ * whatever the column is called.
454
+ */
455
+ const WALL_CLOCK_ALIAS_STEM = "__nx_wc";
456
+ /**
457
+ * A timestamp as the database itself spells it, in a fixed field order.
458
+ *
459
+ * Matched rather than handed to `new Date`, so nothing depends on the host's
460
+ * parsing of a partial date. The fraction is optional and of any length, which
461
+ * covers PostgreSQL's milliseconds and MySQL's microseconds alike.
462
+ */
463
+ const WALL_CLOCK_PATTERN = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?$/;
464
+ /**
465
+ * Read a database's own rendering of a timestamp as UTC.
466
+ *
467
+ * The statement wrote a UTC wall clock, so reading one back as UTC is what
468
+ * makes a write agree with a later read. The rendering is one the statement
469
+ * asked for by name, so its shape does not depend on a session setting.
470
+ *
471
+ * Anything that does not match answers nothing, leaving the driver's value in
472
+ * place rather than replacing it with an `Invalid Date`.
473
+ */
296
474
  function parseWallClockAsUtc(text) {
297
- const match = WALL_CLOCK_PATTERN.exec(text.trim());
298
- if (!match) return void 0;
299
- const [, year, month, day, hour, minute, second, fraction = ""] = match;
300
- const millis = Number(`${fraction}000`.slice(0, 3));
301
- const parsed = new Date(
302
- Date.UTC(
303
- Number(year),
304
- Number(month) - 1,
305
- Number(day),
306
- Number(hour),
307
- Number(minute),
308
- Number(second),
309
- millis
310
- )
311
- );
312
- if (Number(year) < 100) parsed.setUTCFullYear(Number(year));
313
- return parsed;
475
+ const match = WALL_CLOCK_PATTERN.exec(text.trim());
476
+ if (!match) return void 0;
477
+ const [, year, month, day, hour, minute, second, fraction = ""] = match;
478
+ const millis = Number(`${fraction}000`.slice(0, 3));
479
+ const parsed = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second), millis));
480
+ if (Number(year) < 100) parsed.setUTCFullYear(Number(year));
481
+ return parsed;
314
482
  }
483
+ /** Whether a table property is a date column that can convert its own values. */
315
484
  function isDateColumn(colDef) {
316
- if (!colDef || typeof colDef !== "object") return false;
317
- const candidate = colDef;
318
- return typeof candidate.mapFromDriverValue === "function" && typeof candidate.mapToDriverValue === "function" && DATE_DATA_TYPE.test(String(candidate.dataType));
485
+ if (!colDef || typeof colDef !== "object") return false;
486
+ const candidate = colDef;
487
+ return typeof candidate.mapFromDriverValue === "function" && typeof candidate.mapToDriverValue === "function" && DATE_DATA_TYPE.test(String(candidate.dataType));
319
488
  }
489
+ /**
490
+ * Whether a date column records the zone its value belongs to.
491
+ *
492
+ * A column declared WITH a time zone stores an instant, so a driver reading it
493
+ * back gets the same moment whatever zone either end is in -- there is nothing
494
+ * ambiguous to resolve and nothing to correct. Rendering one as text would
495
+ * spell it in the session's zone, and reading that as UTC would move it.
496
+ */
320
497
  function carriesTimeZone(colDef) {
321
- return colDef.withTimezone === true;
498
+ return colDef.withTimezone === true;
322
499
  }
500
+ /**
501
+ * Abstract base class for database adapters.
502
+ *
503
+ * @remarks
504
+ * All dialect-specific adapters must extend this class and implement the abstract methods.
505
+ * Default implementations are provided for CRUD operations, which can be overridden for
506
+ * optimization or dialect-specific behavior.
507
+ *
508
+ * ## Required Implementations
509
+ *
510
+ * Subclasses must implement:
511
+ * - `dialect` - Database dialect identifier
512
+ * - `connect()` - Establish database connection
513
+ * - `disconnect()` - Close database connection
514
+ * - `executeQuery()` - Execute raw SQL query
515
+ * - `transaction()` - Execute operations within a transaction
516
+ * - `getCapabilities()` - Report database feature support
517
+ *
518
+ * ## Optional Overrides
519
+ *
520
+ * Subclasses can override default CRUD methods for optimization:
521
+ * - `select()`, `selectOne()` - Custom query optimization
522
+ * - `insert()`, `insertMany()` - Bulk insert optimization
523
+ * - `update()`, `delete()` - Custom update/delete logic
524
+ * - `upsert()` - Dialect-specific upsert syntax
525
+ *
526
+ * @example
527
+ * ```typescript
528
+ * export class PostgresAdapter extends DrizzleAdapter {
529
+ * readonly dialect = 'postgresql' as const;
530
+ *
531
+ * async connect() {
532
+ * this.pool = new Pool({ connectionString: this.config.url });
533
+ * // ... connection logic
534
+ * }
535
+ *
536
+ * async executeQuery<T>(sql: string, params?: SqlParam[]) {
537
+ * const result = await this.pool.query(sql, params);
538
+ * return result.rows as T[];
539
+ * }
540
+ *
541
+ * // ... other required methods
542
+ * }
543
+ * ```
544
+ *
545
+ * @public
546
+ */
547
+ /**
548
+ * Rows affected by a write, normalised across the driver shapes Drizzle returns.
549
+ *
550
+ * mysql2 resolves to `[ResultSetHeader, FieldPacket[]]`, so treating any array
551
+ * as a row list reports the tuple length — a constant 2 whether the statement
552
+ * matched nothing, one row, or many. Callers that branch on the count (a
553
+ * conditional claim, a retention loop) silently get the wrong answer on MySQL,
554
+ * so the header is read first. node-postgres reports `rowCount` and
555
+ * better-sqlite3 reports `changes`; a plain array is a RETURNING row list, whose
556
+ * length is the count.
557
+ */
323
558
  function affectedRowCount(result) {
324
- if (Array.isArray(result)) {
325
- const header = result[0];
326
- if (header && typeof header.affectedRows === "number") {
327
- return header.affectedRows;
328
- }
329
- return result.length;
330
- }
331
- const record = result;
332
- const rowCount = record?.rowCount;
333
- if (typeof rowCount === "number") return rowCount;
334
- const changes = record?.changes;
335
- if (typeof changes === "number") return changes;
336
- return 0;
559
+ if (Array.isArray(result)) {
560
+ const header = result[0];
561
+ if (header && typeof header.affectedRows === "number") return header.affectedRows;
562
+ return result.length;
563
+ }
564
+ const record = result;
565
+ const rowCount = record?.rowCount;
566
+ if (typeof rowCount === "number") return rowCount;
567
+ const changes = record?.changes;
568
+ if (typeof changes === "number") return changes;
569
+ return 0;
337
570
  }
338
571
  var DrizzleAdapter = class {
339
- /**
340
- * Whether a `transaction()` call made from the current async context would
341
- * join a transaction already open on this adapter, as a savepoint, rather
342
- * than open one of its own.
343
- *
344
- * A caller about to do something no rollback can undo — removing a stored
345
- * file, say — asks this first: work that joins an enclosing transaction is
346
- * undone with it, while the effect it performs after its own savepoint
347
- * releases is not.
348
- *
349
- * False here. Concrete rather than abstract so existing adapters keep
350
- * working unchanged, and false is right for a pooled adapter: a call made
351
- * inside another transaction's work opens its own transaction on its own
352
- * connection, which commits independently.
353
- *
354
- * @returns True when the call would nest in an open transaction
355
- */
356
- inTransaction() {
357
- return false;
358
- }
359
- /**
360
- * The transaction each async flow is inside, kept per flow rather than per
361
- * connection: on SQLite every request shares the one connection, and
362
- * another request's open transaction says nothing about this one.
363
- */
364
- flowTransactions = new async_hooks.AsyncLocalStorage();
365
- /**
366
- * Whether the calling async flow is inside a transaction on this adapter
367
- * that has not finished: its own, or one it was started from inside.
368
- *
369
- * A write that must outlive any rollback — a count of a failed attempt, the
370
- * spend of a single-use secret — asks this before running in a transaction
371
- * of its own. Made inside an open transaction on SQLite, it joins that
372
- * transaction as a savepoint and is undone with it. On PostgreSQL and MySQL
373
- * it would commit on a connection of its own, so refusing it there too
374
- * keeps code that passes on one dialect from failing on another. Unlike
375
- * {@link inTransaction}, which asks whether a call would nest and is always
376
- * false on a pooled adapter, this answers alike on every dialect, and
377
- * another flow's open transaction never answers true.
378
- *
379
- * Covers transactions opened through {@link transaction} and through the
380
- * Drizzle handle's own `transaction` on the dialects whose handle takes
381
- * async work. A dialect that keeps scopes of its own judges from those
382
- * instead; an adapter that does neither answers false.
383
- *
384
- * @experimental
385
- * @returns True while a transaction the calling flow is inside is open
386
- */
387
- transactionOpenInFlow() {
388
- for (let at = this.flowTransactions.getStore(); at !== void 0; at = at.enclosing) {
389
- if (at.open) return true;
390
- }
391
- return false;
392
- }
393
- /**
394
- * Run a whole transaction, from its BEGIN until its COMMIT or ROLLBACK has
395
- * run, as the calling flow's open transaction, for
396
- * {@link transactionOpenInFlow}. A dialect's `transaction` runs through
397
- * this unless it judges the flow from scopes of its own.
398
- *
399
- * The transaction counts as open for its work and for anything the work
400
- * starts, awaited or not, until it has finished. Something the work starts
401
- * that outlives it then sees it closed, or the one enclosing it still open.
402
- *
403
- * @param run - The transaction, BEGIN to COMMIT or ROLLBACK
404
- * @returns What `run` resolves to
405
- */
406
- runInFlowTransaction(run) {
407
- const scope = {
408
- open: true,
409
- enclosing: this.flowTransactions.getStore()
410
- };
411
- return this.flowTransactions.run(scope, async () => {
412
- try {
413
- return await run();
414
- } finally {
415
- scope.open = false;
416
- }
417
- });
418
- }
419
- /**
420
- * Run each of a pool-bound Drizzle handle's transactions on a connection
421
- * `checkout` hands it, and return that connection as the adapter's own
422
- * `transaction()` does: closed rather than pooled when no ROLLBACK can be
423
- * confirmed on it, and never leaked by a failed BEGIN (`pinTransactions`).
424
- *
425
- * For PostgreSQL's and MySQL's handles. Apply it before
426
- * {@link trackFlowTransactions}, which wraps whatever `transaction` the
427
- * handle has at that point.
428
- *
429
- * @param db - A Drizzle handle on this adapter's pool
430
- * @param checkout - Checks out a connection for one transaction
431
- * @returns The same handle, its `transaction` replaced
432
- */
433
- pinDrizzleTransactions(db, checkout) {
434
- return pinTransactions(db, checkout);
435
- }
436
- /**
437
- * Make a Drizzle handle's own `transaction` count for
438
- * {@link transactionOpenInFlow}, as the adapter's does.
439
- *
440
- * Core's services run their transactions through the handle on PostgreSQL
441
- * and MySQL; without this, a call made inside one would be refused on
442
- * SQLite, where they go through the adapter, and let through on the
443
- * others. Not for SQLite's handle: its `transaction` takes only
444
- * synchronous work, which no async flow can be inside.
445
- *
446
- * @param db - A Drizzle handle this adapter created
447
- * @returns The same handle, its `transaction` tracked
448
- */
449
- trackFlowTransactions(db) {
450
- const transactor = db;
451
- const untracked = transactor.transaction.bind(transactor);
452
- const tracked = (work, config) => this.runInFlowTransaction(() => untracked(work, config));
453
- Object.defineProperty(db, "transaction", {
454
- value: tracked,
455
- configurable: true,
456
- writable: true
457
- });
458
- return db;
459
- }
460
- /**
461
- * Run `effect` once the write made from the current async context has
462
- * committed: an event, a hook, a cache flush, anything outside the database
463
- * that must not happen for a change that is rolled back.
464
- *
465
- * A caller invokes this after its own transaction has resolved. Where that
466
- * transaction was nested in an enclosing one, as a savepoint, the change is
467
- * not durable yet, and an adapter that nests (see {@link inTransaction})
468
- * holds `effect` until the outermost transaction commits, and drops it when
469
- * that transaction, or the savepoint it was registered in, rolls back. A
470
- * held effect runs after the commit, before the outermost `transaction()`
471
- * call resolves, in registration order; one that then throws or rejects is
472
- * handed to `onDeferredFailure` rather than failing a transaction that has
473
- * committed.
474
- *
475
- * Here `effect` runs now and the returned promise settles with it, its
476
- * failure included. Concrete rather than abstract, as `inTransaction` is,
477
- * and right for a pooled adapter: each transaction has its own connection
478
- * and has committed by the time its caller continues.
479
- *
480
- * @experimental
481
- * @param effect - What to run once the change is durable
482
- * @param onDeferredFailure - Receives the error of a held effect that fails
483
- * @returns Settles once `effect` has run, or once it has been held
484
- */
485
- async afterCommit(effect, _onDeferredFailure) {
486
- await effect();
487
- }
488
- // ============================================================
489
- // Drizzle Query API Support
490
- // ============================================================
491
- /**
492
- * Table resolver for looking up Drizzle table objects by name.
493
- * When set, CRUD methods use Drizzle's query API instead of raw SQL.
494
- * Set via setTableResolver() after boot-time schema loading.
495
- */
496
- tableResolver = null;
497
- /**
498
- * Set the table resolver for Drizzle query API support.
499
- * When a resolver is set, CRUD methods (select, insert, update, delete, upsert)
500
- * will use Drizzle's query API (db.select().from(), etc.) instead of raw SQL
501
- * string building. Falls back to raw SQL if the resolver doesn't have the table.
502
- *
503
- * @param resolver - TableResolver implementation (e.g. SchemaRegistry)
504
- */
505
- setTableResolver(resolver) {
506
- this.tableResolver = resolver;
507
- }
508
- /**
509
- * Get a Drizzle table object by name from the resolver.
510
- * Returns null if no resolver is set or table is not found.
511
- */
512
- getTableObject(tableName) {
513
- return this.tableResolver?.getTable(tableName) ?? null;
514
- }
515
- /**
516
- * Run a Drizzle-built statement and return its rows.
517
- *
518
- * @remarks
519
- * The CRUD methods resolve their table through the schema registry and reject
520
- * any name it does not declare, which leaves no way to read from a table the
521
- * ORM does not know — one mid-rename, above all — except by assembling SQL and
522
- * quoting identifiers by hand. This takes Drizzle's `sql` template instead, so
523
- * the dialect in use decides the quoting and the parameter binding.
524
- *
525
- * Concrete rather than abstract so existing adapters keep working unchanged.
526
- * The three drivers disagree about both the call and the result: node-postgres
527
- * returns `{ rows }`, mysql2 a `[rows, fields]` tuple, and better-sqlite3 has
528
- * no `execute` at all and answers `all`. Keeping that here rather than at each
529
- * call site is the point — a caller reasoning about it would be reasoning
530
- * about a driver it cannot see.
531
- *
532
- * @param statement - Drizzle `sql` template to run
533
- * @returns Rows the statement produced
534
- */
535
- async queryStatement(statement) {
536
- if (this.getCapabilities().dialect === "sqlite") {
537
- const db2 = this.getDrizzle();
538
- return await db2.all(statement);
539
- }
540
- const db = this.getDrizzle();
541
- const result = await db.execute(statement);
542
- if (Array.isArray(result)) {
543
- return Array.isArray(result[0]) ? result[0] : result;
544
- }
545
- if (typeof result === "object" && result !== null && Array.isArray(result.rows)) {
546
- return result.rows;
547
- }
548
- throw this.createDatabaseError(
549
- "query",
550
- "Drizzle statement returned a result shape this adapter does not recognise; refusing to report it as an empty result.",
551
- void 0
552
- );
553
- }
554
- /**
555
- * Map data keys from SQL column names (snake_case) to Drizzle JS property names (camelCase).
556
- * Drizzle schemas define columns as e.g. `createdAt: timestamp("created_at")` — the JS
557
- * property is `createdAt` but the SQL column is `created_at`. Services pass snake_case keys
558
- * because they match the DB column names. This method maps them to the JS names Drizzle expects.
559
- */
560
- mapDataToColumnNames(tableObj, data) {
561
- if (!tableObj || typeof tableObj !== "object") return data;
562
- const sqlToJs = /* @__PURE__ */ new Map();
563
- const jsonColumns = /* @__PURE__ */ new Set();
564
- for (const [jsName, colDef] of Object.entries(
565
- tableObj
566
- )) {
567
- if (!colDef || typeof colDef !== "object" || !("name" in colDef) || typeof colDef.name !== "string")
568
- continue;
569
- const sqlName = colDef.name;
570
- sqlToJs.set(sqlName, jsName);
571
- if (isJsonColumn(colDef)) {
572
- jsonColumns.add(jsName);
573
- }
574
- }
575
- if (sqlToJs.size === 0) return data;
576
- const mapped = {};
577
- for (const [key, value] of Object.entries(data)) {
578
- const jsKey = sqlToJs.get(key) ?? key;
579
- if (jsonColumns.has(jsKey) && typeof value === "string") {
580
- try {
581
- mapped[jsKey] = JSON.parse(value);
582
- } catch {
583
- mapped[jsKey] = value;
584
- }
585
- } else {
586
- mapped[jsKey] = value;
587
- }
588
- }
589
- return mapped;
590
- }
591
- /**
592
- * Map data keys from Drizzle JS property names to SQL column names for the
593
- * raw-SQL transaction insert path. The transaction context builds INSERT
594
- * statements from Object.keys(data) used directly as column identifiers, so a
595
- * table whose Drizzle property names differ from its SQL column names
596
- * (camelCase core tables like nextly_versions) needs its keys translated
597
- * first. For tables whose property names already equal their column names
598
- * (the dynamic dc_/single_/comp_ tables) every lookup is identity, so
599
- * existing callers are unaffected.
600
- */
601
- mapKeysToSqlColumns(tableObj, data) {
602
- if (!tableObj || typeof tableObj !== "object") return data;
603
- const jsToSql = /* @__PURE__ */ new Map();
604
- for (const [jsName, colDef] of Object.entries(
605
- tableObj
606
- )) {
607
- if (colDef && typeof colDef === "object" && "name" in colDef && typeof colDef.name === "string") {
608
- jsToSql.set(jsName, colDef.name);
609
- }
610
- }
611
- if (jsToSql.size === 0) return data;
612
- const out = {};
613
- for (const [key, value] of Object.entries(data)) {
614
- out[jsToSql.get(key) ?? key] = value;
615
- }
616
- return out;
617
- }
618
- /**
619
- * Map a list of column identifiers (Drizzle property names) to their SQL
620
- * column names, for the raw-SQL transaction insert paths that build a
621
- * RETURNING clause from `options.returning`. Same identity behavior as
622
- * `mapKeysToSqlColumns`: names that are already SQL columns (the dynamic
623
- * dc_/single_/comp_ tables) pass through unchanged.
624
- */
625
- mapColumnNamesToSql(tableObj, names) {
626
- if (!tableObj || typeof tableObj !== "object") return names;
627
- const jsToSql = /* @__PURE__ */ new Map();
628
- for (const [jsName, colDef] of Object.entries(
629
- tableObj
630
- )) {
631
- if (colDef && typeof colDef === "object" && "name" in colDef && typeof colDef.name === "string") {
632
- jsToSql.set(jsName, colDef.name);
633
- }
634
- }
635
- if (jsToSql.size === 0) return names;
636
- return names.map((n) => jsToSql.get(n) ?? n);
637
- }
638
- /**
639
- * Remap a raw-SQL result row's KEYS from SQL column names to Drizzle property
640
- * names, so the raw-SQL transaction insert paths return the same key casing
641
- * as the non-transactional (Drizzle) insert. Keys only - values are left
642
- * untouched, so this does not change how JSON/date columns are decoded. For
643
- * tables whose property names already equal their SQL columns (the dynamic
644
- * dc_/single_/comp_ tables) every lookup is identity, so existing callers see
645
- * no change.
646
- */
647
- mapRowKeysToJs(tableObj, row) {
648
- if (!tableObj || typeof tableObj !== "object" || !row || typeof row !== "object") {
649
- return row;
650
- }
651
- const sqlToJs = /* @__PURE__ */ new Map();
652
- for (const [jsName, colDef] of Object.entries(
653
- tableObj
654
- )) {
655
- if (colDef && typeof colDef === "object" && "name" in colDef && typeof colDef.name === "string") {
656
- sqlToJs.set(colDef.name, jsName);
657
- }
658
- }
659
- if (sqlToJs.size === 0) return row;
660
- const out = {};
661
- for (const [key, value] of Object.entries(row)) {
662
- out[sqlToJs.get(key) ?? key] = value;
663
- }
664
- return out;
665
- }
666
- /**
667
- * Decode a raw-SQL result row's DATE column values the way a Drizzle query
668
- * would, so a write answers with the same representation a read of the same
669
- * column gives.
670
- *
671
- * A Drizzle query path runs every value through its column definition; a
672
- * raw-SQL path does not, so a dialect that stores a timestamp as a number
673
- * answers a write with that number while every read answers a `Date`. Only
674
- * date columns are touched, and a value that is already a `Date` is left
675
- * alone, so a driver that decodes on its own is unaffected.
676
- *
677
- * Runs after {@link mapRowKeysToJs}: keys are Drizzle property names by then,
678
- * which is what the table definition is keyed by.
679
- */
680
- mapDateValuesFromDriver(tableObj, row) {
681
- if (!tableObj || typeof tableObj !== "object" || !row || typeof row !== "object") {
682
- return row;
683
- }
684
- let decoded;
685
- for (const [jsName, colDef] of Object.entries(
686
- tableObj
687
- )) {
688
- if (!isDateColumn(colDef)) continue;
689
- const value = row[jsName];
690
- if (value == null || value instanceof Date) continue;
691
- decoded ??= { ...row };
692
- decoded[jsName] = colDef.mapFromDriverValue(value);
693
- }
694
- return decoded ?? row;
695
- }
696
- /**
697
- * Encode a row's DATE values the way a Drizzle query would before binding
698
- * them, so what lands in the column does not depend on the server's timezone.
699
- *
700
- * A column declared without a time zone stores a wall clock and records
701
- * nothing about which zone it belongs to, so both ends have to agree. Drizzle
702
- * writes UTC and reads UTC. A driver handed a `Date` writes the LOCAL wall
703
- * clock instead, and the same row then reads back shifted by the offset --
704
- * on a UTC server the two agree and nothing is visibly wrong, which is why
705
- * this survives CI.
706
- *
707
- * Only date columns are touched. A value that is not a `Date` is left as it
708
- * is, so a caller that already encoded one is not encoded twice.
709
- *
710
- * Keys may be spelled either way at this point, so both the Drizzle property
711
- * name and the SQL column name resolve.
712
- */
713
- mapDateValuesToDriver(tableObj, data) {
714
- if (!tableObj || typeof tableObj !== "object" || !data) return data;
715
- const dateColumns = /* @__PURE__ */ new Map();
716
- for (const [jsName, colDef] of Object.entries(
717
- tableObj
718
- )) {
719
- if (!isDateColumn(colDef)) continue;
720
- dateColumns.set(jsName, colDef);
721
- const sqlName = colDef.name;
722
- if (typeof sqlName === "string") dateColumns.set(sqlName, colDef);
723
- }
724
- if (dateColumns.size === 0) return data;
725
- let encoded;
726
- for (const [key, value] of Object.entries(data)) {
727
- if (!(value instanceof Date)) continue;
728
- const column = dateColumns.get(key);
729
- if (!column) continue;
730
- encoded ??= { ...data };
731
- encoded[key] = column.mapToDriverValue(value);
732
- }
733
- return encoded ?? data;
734
- }
735
- /**
736
- * Bring a row about to be written through a raw-SQL statement into the shape
737
- * a Drizzle query would have bound: SQL column names for keys, encoded values
738
- * for date columns.
739
- *
740
- * The mirror of {@link mapRowFromRawSql}. The raw-SQL write paths exist
741
- * because a dialect needs SQL a Drizzle query cannot express, not because
742
- * their callers want different values in the table.
743
- */
744
- mapRowToRawSql(tableObj, data) {
745
- return this.mapDateValuesToDriver(
746
- tableObj,
747
- this.mapKeysToSqlColumns(tableObj, data)
748
- );
749
- }
750
- /**
751
- * Bring a raw-SQL result row into the shape a Drizzle query would have
752
- * produced: Drizzle property names for keys, decoded values for date columns.
753
- *
754
- * The raw-SQL write paths exist because a dialect needs SQL a Drizzle query
755
- * cannot express, not because their callers want a different row shape, so
756
- * every one of them ends here.
757
- */
758
- mapRowFromRawSql(tableObj, row, aliases = []) {
759
- const mapped = this.mapDateValuesFromDriver(
760
- tableObj,
761
- this.mapRowKeysToJs(tableObj, row)
762
- );
763
- return this.applyWallClockAliases(mapped, aliases);
764
- }
765
- /**
766
- * The date columns a statement is about to return, and the alias each one's
767
- * wall clock is spelled out under.
768
- *
769
- * A driver turns a timestamp into a `Date` using ITS zone before any of this
770
- * code runs, and that conversion is lossy: a wall clock inside a
771
- * daylight-saving gap is a local time that does not exist, so the driver
772
- * normalizes it and the original is gone. Asking the database for the wall
773
- * clock as text alongside the row is the only way to read it exactly.
774
- *
775
- * `"*"` covers every date column; a projection covers only the ones it asked
776
- * for, so a caller still gets back what it requested and nothing more.
777
- */
778
- dateWallClockAliases(tableObj, returning) {
779
- if (!tableObj || typeof tableObj !== "object" || returning === void 0) {
780
- return [];
781
- }
782
- const requested = returning === "*" ? void 0 : new Set(returning.map(String));
783
- const taken = /* @__PURE__ */ new Set();
784
- for (const [jsName, colDef] of Object.entries(
785
- tableObj
786
- )) {
787
- taken.add(jsName);
788
- const columnName = colDef.name;
789
- if (typeof columnName === "string") taken.add(columnName);
790
- }
791
- let next = 0;
792
- const freeAlias = () => {
793
- let candidate = `${WALL_CLOCK_ALIAS_STEM}${next++}`;
794
- while (taken.has(candidate)) {
795
- candidate = `${WALL_CLOCK_ALIAS_STEM}${next++}`;
796
- }
797
- taken.add(candidate);
798
- return candidate;
799
- };
800
- const aliases = [];
801
- for (const [jsName, colDef] of Object.entries(
802
- tableObj
803
- )) {
804
- if (!isDateColumn(colDef)) continue;
805
- if (carriesTimeZone(colDef)) continue;
806
- const sqlName = colDef.name;
807
- if (typeof sqlName !== "string") continue;
808
- if (requested && !requested.has(sqlName) && !requested.has(jsName)) {
809
- continue;
810
- }
811
- aliases.push({ sqlName, alias: freeAlias(), jsName });
812
- }
813
- return aliases;
814
- }
815
- /**
816
- * Replace each date in a row with the wall clock the database spelled out,
817
- * read as UTC, and drop the aliases that carried it.
818
- *
819
- * The statement writes a UTC wall clock, so reading one back as UTC is what
820
- * makes a write and a later read agree. Whatever the column actually stored
821
- * is what arrives -- a column keeping only whole seconds reports whole
822
- * seconds -- because this is the database's own text, not a value
823
- * reconstructed from the one that was bound.
824
- */
825
- applyWallClockAliases(row, aliases) {
826
- if (!row || typeof row !== "object" || aliases.length === 0) return row;
827
- const record = row;
828
- let resolved;
829
- for (const { alias, jsName } of aliases) {
830
- if (!(alias in record)) continue;
831
- resolved ??= { ...record };
832
- const text = resolved[alias];
833
- delete resolved[alias];
834
- if (typeof text !== "string") continue;
835
- const parsed = parseWallClockAsUtc(text);
836
- if (parsed !== void 0) resolved[jsName] = parsed;
837
- }
838
- return resolved ?? row;
839
- }
840
- /**
841
- * Build a Drizzle column projection object (`{ propertyName: column }`) from a
842
- * requested column list, used by `select` (columns) and `insert` (returning).
843
- * A requested name resolves against either the Drizzle property name
844
- * (camelCase) or the SQL column name (snake_case); the projection is keyed by
845
- * the property name so the row shape matches a full select. Returns undefined
846
- * for `"*"` or when nothing resolves, so callers fall back to all columns.
847
- */
848
- buildColumnProjection(tableObj, names) {
849
- if (names == null || names === "*" || !tableObj || typeof tableObj !== "object") {
850
- return void 0;
851
- }
852
- const { projection } = this.resolveColumns(tableObj, names);
853
- return Object.keys(projection).length ? projection : void 0;
854
- }
855
- /**
856
- * The requested column names split into what this table has and what it does not.
857
- *
858
- * One resolution, reported both ways, because two callers need different halves
859
- * of the same answer: a projection drops what it cannot find, while a distinct
860
- * count has to REFUSE it. Resolving twice would let the two disagree about what
861
- * "found" means — the SQL-name alias below is exactly the kind of detail a
862
- * second implementation gets wrong — and the count would then reject a column
863
- * the projection happily uses.
864
- *
865
- * Both spellings resolve: a caller may name the Drizzle property or the SQL
866
- * column, and either maps to the same projection key.
867
- */
868
- resolveColumns(tableObj, names) {
869
- const cols = drizzleOrm.getColumns(tableObj);
870
- const byAnyName = {};
871
- for (const [jsName, col] of Object.entries(cols)) {
872
- byAnyName[jsName] = { jsName, col };
873
- const sqlName = col?.name;
874
- if (typeof sqlName === "string") byAnyName[sqlName] = { jsName, col };
875
- }
876
- const projection = {};
877
- const unresolved = [];
878
- for (const name of names) {
879
- const hit = byAnyName[name];
880
- if (hit) projection[hit.jsName] = hit.col;
881
- else unresolved.push(name);
882
- }
883
- return { projection, unresolved };
884
- }
885
- // ============================================================
886
- // Connection Status (Default implementations, can override)
887
- // ============================================================
888
- /**
889
- * Check if the adapter is currently connected.
890
- *
891
- * @remarks
892
- * Default implementation returns false. Subclasses should override
893
- * to provide accurate connection status.
894
- *
895
- * @returns True if connected, false otherwise
896
- */
897
- isConnected() {
898
- return false;
899
- }
900
- /**
901
- * Get connection pool statistics.
902
- *
903
- * @remarks
904
- * Returns null by default. Subclasses with connection pooling
905
- * should override to provide pool statistics.
906
- *
907
- * @returns Pool statistics or null if not applicable
908
- */
909
- getPoolStats() {
910
- return null;
911
- }
912
- // ============================================================
913
- // Timeout Utilities
914
- // ============================================================
915
- /**
916
- * Default query timeout in milliseconds.
917
- *
918
- * @remarks
919
- * This value is used by executeWithTimeout() when no explicit timeout
920
- * is provided. Subclasses should set this from their config.
921
- *
922
- * @default 15000 (15 seconds)
923
- *
924
- * @protected
925
- */
926
- defaultQueryTimeoutMs = 15e3;
927
- /**
928
- * Execute an async operation with a timeout.
929
- *
930
- * @remarks
931
- * Wraps an async operation with a timeout that aborts if the operation
932
- * exceeds the specified duration. Uses Promise.race for clean timeout
933
- * handling without memory leaks.
934
- *
935
- * When the timeout is reached, a DatabaseError with kind 'timeout' is thrown.
936
- * Note that this does NOT cancel the underlying database query - it only
937
- * prevents the calling code from waiting indefinitely. For true query
938
- * cancellation, use database-level statement timeouts (PostgreSQL) or
939
- * similar mechanisms.
940
- *
941
- * @param operation - Async operation to execute
942
- * @param timeoutMs - Timeout in milliseconds (defaults to defaultQueryTimeoutMs)
943
- * @returns Result of the operation
944
- *
945
- * @throws {DatabaseError} With kind 'timeout' if operation exceeds timeout
946
- *
947
- * @example
948
- * ```typescript
949
- * // Use default timeout
950
- * const result = await adapter.executeWithTimeout(
951
- * () => adapter.select('users', { limit: 1000 })
952
- * );
953
- *
954
- * // Use custom timeout for specific operation
955
- * const result = await adapter.executeWithTimeout(
956
- * () => adapter.select('large_table'),
957
- * 60000 // 60 seconds for large queries
958
- * );
959
- * ```
960
- *
961
- * @public
962
- */
963
- async executeWithTimeout(operation, timeoutMs) {
964
- const timeout = timeoutMs ?? this.defaultQueryTimeoutMs;
965
- if (timeout <= 0) {
966
- return operation();
967
- }
968
- let timeoutId;
969
- const timeoutPromise = new Promise((_, reject) => {
970
- timeoutId = setTimeout(() => {
971
- reject(
972
- this.createDatabaseError(
973
- "timeout",
974
- `Query execution timed out after ${timeout}ms`
975
- )
976
- );
977
- }, timeout);
978
- });
979
- try {
980
- const result = await Promise.race([operation(), timeoutPromise]);
981
- return result;
982
- } finally {
983
- if (timeoutId !== void 0) {
984
- clearTimeout(timeoutId);
985
- }
986
- }
987
- }
988
- /**
989
- * Set the default query timeout.
990
- *
991
- * @remarks
992
- * This method allows runtime configuration of the default timeout.
993
- * Subclasses should call this in their constructor or connect() method
994
- * based on their configuration.
995
- *
996
- * @param timeoutMs - Timeout in milliseconds (0 to disable)
997
- *
998
- * @protected
999
- */
1000
- setDefaultQueryTimeout(timeoutMs) {
1001
- this.defaultQueryTimeoutMs = timeoutMs;
1002
- }
1003
- /**
1004
- * Get the current default query timeout.
1005
- *
1006
- * @returns Current default timeout in milliseconds
1007
- *
1008
- * @public
1009
- */
1010
- getDefaultQueryTimeout() {
1011
- return this.defaultQueryTimeoutMs;
1012
- }
1013
- // ============================================================
1014
- // CRUD Operations (Default implementations, can override)
1015
- // ============================================================
1016
- /**
1017
- * Select multiple records from a table.
1018
- *
1019
- * @remarks
1020
- * Default implementation builds a SELECT query and executes it.
1021
- * Subclasses can override for optimization or dialect-specific features.
1022
- *
1023
- * @param table - Table name
1024
- * @param options - Select options (filtering, sorting, pagination)
1025
- * @returns Array of matching records
1026
- *
1027
- * @throws {DatabaseError} If query fails
1028
- *
1029
- * @example
1030
- * ```typescript
1031
- * const users = await adapter.select('users', {
1032
- * where: { and: [{ column: 'role', op: '=', value: 'admin' }] },
1033
- * orderBy: [{ column: 'created_at', direction: 'desc' }],
1034
- * limit: 10
1035
- * });
1036
- * ```
1037
- */
1038
- async select(table, options, executor) {
1039
- const tableObj = this.getTableObject(table);
1040
- if (tableObj) {
1041
- try {
1042
- const db = executor ?? this.getDrizzle();
1043
- const projection = options?.columns?.length ? this.buildColumnProjection(tableObj, options.columns) : void 0;
1044
- let query = projection ? db.select(projection).from(tableObj) : db.select().from(tableObj);
1045
- if (options?.where) {
1046
- const whereCondition = buildDrizzleWhere(
1047
- tableObj,
1048
- options.where
1049
- );
1050
- if (whereCondition) {
1051
- query = query.where(whereCondition);
1052
- }
1053
- }
1054
- if (options?.orderBy?.length) {
1055
- const orderClauses = buildDrizzleOrderBy(
1056
- drizzleOrm.getColumns(tableObj),
1057
- options.orderBy
1058
- );
1059
- if (orderClauses.length) {
1060
- query = query.orderBy(...orderClauses);
1061
- }
1062
- }
1063
- if (options?.limit !== void 0) {
1064
- query = query.limit(options.limit);
1065
- }
1066
- if (options?.offset !== void 0) {
1067
- query = query.offset(options.offset);
1068
- }
1069
- if (options?.forUpdate && !executor) {
1070
- throw this.createDatabaseError(
1071
- "query",
1072
- "forUpdate requires a transaction executor: a lock request on the pooled connection takes no durable lock and cannot prevent a concurrent write.",
1073
- void 0
1074
- );
1075
- }
1076
- if (options?.forUpdate && this.dialect !== "sqlite") {
1077
- query = query.for("update");
1078
- }
1079
- return await query;
1080
- } catch (error) {
1081
- throw this.handleQueryError(error, "select", table);
1082
- }
1083
- }
1084
- throw this.createDatabaseError(
1085
- "query",
1086
- `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`,
1087
- void 0
1088
- );
1089
- }
1090
- /**
1091
- * Select a single record from a table.
1092
- *
1093
- * @remarks
1094
- * Default implementation uses `select()` with limit 1 and returns first result.
1095
- * Returns null if no matching record is found.
1096
- *
1097
- * @param table - Table name
1098
- * @param options - Select options
1099
- * @returns First matching record or null
1100
- *
1101
- * @throws {DatabaseError} If query fails
1102
- *
1103
- * @example
1104
- * ```typescript
1105
- * const user = await adapter.selectOne('users', {
1106
- * where: { and: [{ column: 'email', op: '=', value: 'user@example.com' }] }
1107
- * });
1108
- * ```
1109
- */
1110
- /** The registered table object, or a refusal naming what is missing. */
1111
- requireTableObject(table) {
1112
- const tableObj = this.getTableObject(table);
1113
- if (tableObj) return tableObj;
1114
- throw this.createDatabaseError(
1115
- "query",
1116
- `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`,
1117
- void 0
1118
- );
1119
- }
1120
- /**
1121
- * The columns a distinct count groups by, or `undefined` for a row count.
1122
- *
1123
- * 🔴 EVERY named column must resolve, not merely one of them. A `distinctOn`
1124
- * naming no column this table has would fall through to a row count, and one
1125
- * naming a valid column beside a misspelled or since-removed column would
1126
- * count distinct over a NARROWER key than the caller asked for. Both answer a
1127
- * different question than the one asked, silently; the second is the worse of
1128
- * the two, because a subset key groups more rows together and so UNDERCOUNTS,
1129
- * which reads as a plausible number rather than as a fault. Refused here,
1130
- * where the mistake was made, rather than surfacing as a wrong figure.
1131
- */
1132
- countProjection(tableObj, table, options) {
1133
- const distinctOn = options?.distinctOn;
1134
- if (!distinctOn?.length) return void 0;
1135
- const { projection, unresolved } = this.resolveColumns(
1136
- tableObj,
1137
- distinctOn
1138
- );
1139
- if (unresolved.length > 0) {
1140
- throw this.createDatabaseError(
1141
- "query",
1142
- `distinctOn names ${unresolved.length === 1 ? "a column" : "columns"} that do not exist on "${table}": ${unresolved.join(", ")}`,
1143
- void 0
1144
- );
1145
- }
1146
- return projection;
1147
- }
1148
- /**
1149
- * `COUNT(*)` over a `SELECT DISTINCT` subquery.
1150
- *
1151
- * Its own function because it is a genuinely different query from the plain
1152
- * count rather than a variation on one, and because building both inline made
1153
- * the public method a chain of decisions the complexity gate objected to
1154
- * before a reader would have.
1155
- */
1156
- async countDistinctRows(db, tableObj, projection, where) {
1157
- const inner = db.selectDistinct(projection).from(tableObj).$dynamic();
1158
- const distinctRows = where ? inner.where(where) : inner;
1159
- const [row] = await db.select({ value: drizzleOrm.count() }).from(distinctRows.as("nextly_distinct"));
1160
- return Number(row?.value ?? 0);
1161
- }
1162
- /** `COUNT(*)` over the table itself. */
1163
- async countAllRows(db, tableObj, where) {
1164
- const base = db.select({ value: drizzleOrm.count() }).from(tableObj).$dynamic();
1165
- const [row] = await (where ? base.where(where) : base);
1166
- return Number(row?.value ?? 0);
1167
- }
1168
- /**
1169
- * How many rows match, or how many DISTINCT combinations of some columns do.
1170
- *
1171
- * The data layer had no count at all: collection totals go through
1172
- * `countEntries`, an access-controlled path built for collection tables, and
1173
- * nothing could count a system table. A caller wanting one selected the rows
1174
- * and measured the array, which transfers every row to learn a number and
1175
- * cannot be bounded without making the number wrong.
1176
- *
1177
- * 🔴 `distinctOn` compiles to `COUNT(*)` over a `SELECT DISTINCT` SUBQUERY,
1178
- * never to `COUNT(DISTINCT a, b)`. The inline form is not portable and fails
1179
- * in the direction that is hardest to notice -- MySQL accepts it, PostgreSQL
1180
- * needs a row constructor, and SQLite rejects it outright with "wrong number
1181
- * of arguments to function count()" -- so a query written against one engine
1182
- * is a syntax error on another. The subquery is the single form all three
1183
- * accept, and building it here rather than at each call site is what keeps
1184
- * the answer identical per dialect.
1185
- */
1186
- async count(table, options, executor) {
1187
- const tableObj = this.requireTableObject(table);
1188
- try {
1189
- const db = executor ?? this.getDrizzle();
1190
- const where = options?.where ? buildDrizzleWhere(tableObj, options.where) : void 0;
1191
- const projection = this.countProjection(tableObj, table, options);
1192
- return projection ? await this.countDistinctRows(db, tableObj, projection, where) : await this.countAllRows(db, tableObj, where);
1193
- } catch (error) {
1194
- throw this.handleQueryError(error, "count", table);
1195
- }
1196
- }
1197
- async selectOne(table, options, executor) {
1198
- const results = await this.select(
1199
- table,
1200
- { ...options, limit: 1 },
1201
- executor
1202
- );
1203
- return results.length > 0 ? results[0] : null;
1204
- }
1205
- /**
1206
- * Insert a single record into a table.
1207
- *
1208
- * @remarks
1209
- * Default implementation handles databases with and without RETURNING support.
1210
- * For databases without RETURNING (MySQL), performs INSERT followed by SELECT.
1211
- *
1212
- * @param table - Table name
1213
- * @param data - Record data to insert
1214
- * @param options - Insert options
1215
- * @returns Inserted record (with RETURNING columns if specified)
1216
- *
1217
- * @throws {DatabaseError} If insert fails
1218
- *
1219
- * @example
1220
- * ```typescript
1221
- * const user = await adapter.insert('users', {
1222
- * email: 'user@example.com',
1223
- * name: 'John Doe'
1224
- * }, { returning: ['id', 'email', 'created_at'] });
1225
- * ```
1226
- */
1227
- async insert(table, data, options) {
1228
- const tableObj = this.getTableObject(table);
1229
- if (tableObj) {
1230
- try {
1231
- const mappedData = this.mapDataToColumnNames(tableObj, data);
1232
- const db = this.getDrizzle();
1233
- const caps = this.getCapabilities();
1234
- const returning = options?.returning;
1235
- const wantsReturning = returning != null && !(Array.isArray(returning) && returning.length === 0);
1236
- if (caps.supportsReturning && wantsReturning) {
1237
- const projection = this.buildColumnProjection(tableObj, returning);
1238
- const insertQuery = db.insert(tableObj).values(mappedData);
1239
- const result2 = await (projection ? insertQuery.returning(projection) : insertQuery.returning());
1240
- return Array.isArray(result2) ? result2[0] : result2;
1241
- }
1242
- const result = await db.insert(tableObj).values(mappedData);
1243
- if (!caps.supportsReturning && wantsReturning && data.id !== void 0) {
1244
- return await this.selectOne(table, {
1245
- columns: returning === "*" ? void 0 : returning,
1246
- where: {
1247
- and: [{ column: "id", op: "=", value: data.id }]
1248
- }
1249
- });
1250
- }
1251
- return Array.isArray(result) ? result[0] : result;
1252
- } catch (error) {
1253
- throw this.handleQueryError(error, "insert", table);
1254
- }
1255
- }
1256
- throw this.createDatabaseError(
1257
- "query",
1258
- `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`,
1259
- void 0
1260
- );
1261
- }
1262
- /**
1263
- * Insert multiple records into a table.
1264
- *
1265
- * @remarks
1266
- * Default implementation performs individual inserts in sequence.
1267
- * Subclasses can override for bulk insert optimization (e.g., COPY in PostgreSQL).
1268
- *
1269
- * @param table - Table name
1270
- * @param data - Array of records to insert
1271
- * @param options - Insert options
1272
- * @returns Inserted records (with RETURNING columns if specified)
1273
- *
1274
- * @throws {DatabaseError} If insert fails
1275
- *
1276
- * @example
1277
- * ```typescript
1278
- * const users = await adapter.insertMany('users', [
1279
- * { email: 'user1@example.com', name: 'User 1' },
1280
- * { email: 'user2@example.com', name: 'User 2' }
1281
- * ], { returning: ['id'] });
1282
- * ```
1283
- */
1284
- async insertMany(table, data, options) {
1285
- if (data.length === 0) {
1286
- return [];
1287
- }
1288
- const results = [];
1289
- for (const record of data) {
1290
- const result = await this.insert(table, record, options);
1291
- results.push(result);
1292
- }
1293
- return results;
1294
- }
1295
- /**
1296
- * Update records in a table.
1297
- *
1298
- * @remarks
1299
- * Default implementation builds an UPDATE query with WHERE clause.
1300
- * Returns updated records if RETURNING is supported and requested.
1301
- *
1302
- * @param table - Table name
1303
- * @param data - Data to update
1304
- * @param where - Conditions for records to update
1305
- * @param options - Update options
1306
- * @returns Updated records (with RETURNING columns if specified)
1307
- *
1308
- * @throws {DatabaseError} If update fails
1309
- *
1310
- * @example
1311
- * ```typescript
1312
- * const updated = await adapter.update('users',
1313
- * { status: 'active' },
1314
- * { and: [{ column: 'id', op: '=', value: userId }] },
1315
- * { returning: ['id', 'status', 'updated_at'] }
1316
- * );
1317
- * ```
1318
- */
1319
- async update(table, data, where, options, executor) {
1320
- const tableObj = this.getTableObject(table);
1321
- if (tableObj) {
1322
- try {
1323
- const db = executor ?? this.getDrizzle();
1324
- const caps = this.getCapabilities();
1325
- const mappedData = this.mapDataToColumnNames(tableObj, data);
1326
- let query = db.update(tableObj).set(mappedData);
1327
- const whereCondition = buildDrizzleWhere(tableObj, where);
1328
- if (whereCondition) {
1329
- query = query.where(whereCondition);
1330
- }
1331
- if (caps.supportsReturning && options?.returning) {
1332
- return await query.returning();
1333
- }
1334
- await query;
1335
- if (!caps.supportsReturning && options?.returning) {
1336
- return await this.select(table, { where }, executor);
1337
- }
1338
- return [];
1339
- } catch (error) {
1340
- throw this.handleQueryError(error, "update", table);
1341
- }
1342
- }
1343
- throw this.createDatabaseError(
1344
- "query",
1345
- `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`,
1346
- void 0
1347
- );
1348
- }
1349
- /**
1350
- * Update records in a table and report how many rows the statement affected.
1351
- *
1352
- * The count is the whole return value, mirroring `delete`. `update` cannot answer this: without
1353
- * `returning` it discards the driver's count, and WITH `returning` on a dialect that lacks
1354
- * RETURNING it re-SELECTs using the same WHERE — so a conditional update that just changed a
1355
- * column named in that WHERE reads back zero rows and a write that landed reports as unmatched.
1356
- * Reading the driver's own count has no second query to disagree with the first.
1357
- *
1358
- * 🔴 MySQL reports CHANGED rows, not matched rows: an UPDATE that matches a row but writes values
1359
- * identical to what it holds counts zero. A caller using this as a compare-and-set must therefore
1360
- * include a column the write always moves — a version bump, a timestamp with enough resolution —
1361
- * so that matched implies changed. Postgres (`rowCount`) and SQLite (`changes`) count matched
1362
- * rows and do not need the precaution, which is exactly why it cannot be dropped: the dialect
1363
- * where the distinction exists is the one with no RETURNING to fall back on.
1364
- */
1365
- async updateCount(table, data, where, executor) {
1366
- const tableObj = this.getTableObject(table);
1367
- if (tableObj) {
1368
- try {
1369
- const db = executor ?? this.getDrizzle();
1370
- const mappedData = this.mapDataToColumnNames(tableObj, data);
1371
- const statement = db.update(tableObj).set(mappedData);
1372
- const whereCondition = buildDrizzleWhere(tableObj, where);
1373
- const result = await (whereCondition ? statement.where(whereCondition) : statement);
1374
- return affectedRowCount(result);
1375
- } catch (error) {
1376
- throw this.handleQueryError(error, "update", table);
1377
- }
1378
- }
1379
- throw this.createDatabaseError(
1380
- "query",
1381
- `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`,
1382
- void 0
1383
- );
1384
- }
1385
- /**
1386
- * Delete records from a table.
1387
- *
1388
- * @remarks
1389
- * Default implementation builds a DELETE query with WHERE clause.
1390
- * Returns the number of deleted records.
1391
- *
1392
- * @param table - Table name
1393
- * @param where - Conditions for records to delete
1394
- * @param options - Delete options
1395
- * @returns Number of deleted records
1396
- *
1397
- * @throws {DatabaseError} If delete fails
1398
- *
1399
- * @example
1400
- * ```typescript
1401
- * const count = await adapter.delete('users', {
1402
- * and: [{ column: 'status', op: '=', value: 'inactive' }]
1403
- * });
1404
- * console.log(`Deleted ${count} users`);
1405
- * ```
1406
- */
1407
- async delete(table, where, _options, executor) {
1408
- const tableObj = this.getTableObject(table);
1409
- if (tableObj) {
1410
- try {
1411
- const db = executor ?? this.getDrizzle();
1412
- let query = db.delete(tableObj);
1413
- const whereCondition = buildDrizzleWhere(tableObj, where);
1414
- if (whereCondition) {
1415
- query = query.where(whereCondition);
1416
- }
1417
- const result = await query;
1418
- return affectedRowCount(result);
1419
- } catch (error) {
1420
- throw this.handleQueryError(error, "delete", table);
1421
- }
1422
- }
1423
- throw this.createDatabaseError(
1424
- "query",
1425
- `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`,
1426
- void 0
1427
- );
1428
- }
1429
- /**
1430
- * Upsert (INSERT or UPDATE) a record.
1431
- *
1432
- * @remarks
1433
- * Default implementation uses dialect-specific ON CONFLICT syntax.
1434
- * PostgreSQL/SQLite: ON CONFLICT ... DO UPDATE
1435
- * MySQL: the MySQL adapter overrides this, resolving the conflict by the
1436
- * conflict columns (see its `upsert`)
1437
- *
1438
- * @param table - Table name
1439
- * @param data - Record data
1440
- * @param options - Upsert options (must specify conflict columns)
1441
- * @returns Upserted record (with RETURNING columns if specified)
1442
- *
1443
- * @throws {DatabaseError} If upsert fails
1444
- *
1445
- * @example
1446
- * ```typescript
1447
- * const user = await adapter.upsert('users', {
1448
- * email: 'user@example.com',
1449
- * name: 'Updated Name'
1450
- * }, {
1451
- * conflictColumns: ['email'],
1452
- * updateColumns: ['name'],
1453
- * returning: ['id', 'email', 'name']
1454
- * });
1455
- * ```
1456
- */
1457
- async upsert(table, data, options, executor) {
1458
- const tableObj = this.getTableObject(table);
1459
- if (tableObj) {
1460
- try {
1461
- const db = executor ?? this.getDrizzle();
1462
- const caps = this.getCapabilities();
1463
- const columns = drizzleOrm.getColumns(tableObj);
1464
- const conflictTarget = options.conflictColumns.map((col) => columns[col]).filter(Boolean);
1465
- const conflictSet = new Set(options.conflictColumns);
1466
- const updateData = {};
1467
- const updateColumns = options.updateColumns ?? Object.keys(data);
1468
- for (const key of updateColumns) {
1469
- if (!conflictSet.has(key) && key in data) {
1470
- updateData[key] = data[key];
1471
- }
1472
- }
1473
- const keepExisting = Object.keys(updateData).length === 0;
1474
- this.assertUpsertTarget(table, data, options, keepExisting);
1475
- let query;
1476
- if (caps.supportsOnConflict) {
1477
- const inserted = db.insert(tableObj).values(data);
1478
- query = keepExisting ? inserted.onConflictDoNothing({ target: conflictTarget }) : inserted.onConflictDoUpdate({
1479
- target: conflictTarget,
1480
- set: updateData
1481
- });
1482
- } else {
1483
- query = db.insert(tableObj).values(data);
1484
- }
1485
- if (caps.supportsReturning) {
1486
- const result = await query.returning();
1487
- const row = Array.isArray(result) ? result[0] : result;
1488
- if (row !== void 0) return row;
1489
- } else {
1490
- await query;
1491
- }
1492
- const conflictValues = options.conflictColumns.map(
1493
- (column) => [column, data[column]]
1494
- );
1495
- if (conflictValues.length > 0 && conflictValues.every(([, value]) => value !== void 0)) {
1496
- return await this.selectOne(
1497
- table,
1498
- {
1499
- where: {
1500
- and: conflictValues.map(([column, value]) => ({
1501
- column,
1502
- op: "=",
1503
- value
1504
- }))
1505
- }
1506
- },
1507
- executor
1508
- );
1509
- }
1510
- return data;
1511
- } catch (error) {
1512
- throw this.handleQueryError(error, "upsert", table);
1513
- }
1514
- }
1515
- throw this.createDatabaseError(
1516
- "query",
1517
- `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`,
1518
- void 0
1519
- );
1520
- }
1521
- // ============================================================
1522
- // Migration Support (Default implementations, can override)
1523
- // ============================================================
1524
- /**
1525
- * Run pending migrations.
1526
- *
1527
- * @remarks
1528
- * Default implementation is a placeholder. Subclasses should implement
1529
- * migration tracking and execution logic.
1530
- *
1531
- * @param migrations - Array of migrations to run
1532
- * @returns Migration result with applied and pending migrations
1533
- *
1534
- * @throws {DatabaseError} If migration fails
1535
- */
1536
- // Base implementation throws synchronously; dialect adapters override with async logic.
1537
- // eslint-disable-next-line @typescript-eslint/require-await
1538
- async migrate(_migrations) {
1539
- throw this.createDatabaseError(
1540
- "query",
1541
- "migrate() must be implemented by dialect-specific adapter. Use PostgresAdapter, MySqlAdapter, or SqliteAdapter.",
1542
- void 0
1543
- );
1544
- }
1545
- /**
1546
- * Rollback the last migration.
1547
- *
1548
- * @remarks
1549
- * Default implementation is a placeholder. Subclasses should implement
1550
- * migration rollback logic.
1551
- *
1552
- * @returns Migration result after rollback
1553
- *
1554
- * @throws {DatabaseError} If rollback fails
1555
- */
1556
- // Base implementation throws synchronously; dialect adapters override with async logic.
1557
- // eslint-disable-next-line @typescript-eslint/require-await
1558
- async rollback() {
1559
- throw this.createDatabaseError(
1560
- "query",
1561
- "rollback() must be implemented by dialect-specific adapter. Use PostgresAdapter, MySqlAdapter, or SqliteAdapter.",
1562
- void 0
1563
- );
1564
- }
1565
- /**
1566
- * Get migration status.
1567
- *
1568
- * @remarks
1569
- * Default implementation is a placeholder. Subclasses should implement
1570
- * migration status checking.
1571
- *
1572
- * @returns Current migration status
1573
- *
1574
- * @throws {DatabaseError} If status check fails
1575
- */
1576
- async getMigrationStatus() {
1577
- try {
1578
- const rows = await this.executeQuery(`SELECT * FROM "__drizzle_migrations" ORDER BY created_at ASC`);
1579
- const applied = rows.map((r) => ({
1580
- id: String(r.id),
1581
- name: r.hash,
1582
- appliedAt: new Date(r.created_at),
1583
- checksum: r.hash
1584
- }));
1585
- return {
1586
- applied,
1587
- pending: [],
1588
- current: applied.length > 0 ? applied[applied.length - 1].id : null
1589
- };
1590
- } catch {
1591
- return { applied: [], pending: [], current: null };
1592
- }
1593
- }
1594
- // ============================================================
1595
- // Schema Operations (Default implementations, can override)
1596
- // ============================================================
1597
- /**
1598
- * Create a new table.
1599
- *
1600
- * @remarks
1601
- * Default implementation is a placeholder. Subclasses should implement
1602
- * table creation logic.
1603
- *
1604
- * @param definition - Table definition
1605
- * @param options - Creation options
1606
- *
1607
- * @throws {DatabaseError} If table creation fails
1608
- */
1609
- async createTable(definition, options) {
1610
- const columnDefs = definition.columns.map((col) => {
1611
- let colSql = `${this.escapeIdentifier(col.name)} ${col.type.toUpperCase()}`;
1612
- if (col.primaryKey) colSql += " PRIMARY KEY";
1613
- if (col.nullable === false) colSql += " NOT NULL";
1614
- if (col.unique) colSql += " UNIQUE";
1615
- if (col.default !== void 0) {
1616
- colSql += ` DEFAULT ${renderDefaultValue(col.default, col.name)}`;
1617
- }
1618
- return colSql;
1619
- });
1620
- const ifNotExists = options?.ifNotExists !== false ? "IF NOT EXISTS " : "";
1621
- const query = `CREATE TABLE ${ifNotExists}${this.escapeIdentifier(definition.name)} (
1622
- ${columnDefs.join(",\n ")}
1623
- )`;
1624
- try {
1625
- await this.executeQuery(query);
1626
- } catch (error) {
1627
- throw this.handleQueryError(error, "createTable", definition.name);
1628
- }
1629
- }
1630
- /**
1631
- * Drop a table.
1632
- *
1633
- * @remarks
1634
- * Default implementation is a placeholder. Subclasses should implement
1635
- * table dropping logic.
1636
- *
1637
- * @param tableName - Name of table to drop
1638
- * @param options - Drop options
1639
- *
1640
- * @throws {DatabaseError} If table drop fails
1641
- */
1642
- async dropTable(tableName, options) {
1643
- const ifExists = options?.ifExists !== false ? "IF EXISTS " : "";
1644
- const cascade = options?.cascade ? " CASCADE" : "";
1645
- const query = `DROP TABLE ${ifExists}${this.escapeIdentifier(tableName)}${cascade}`;
1646
- try {
1647
- await this.executeQuery(query);
1648
- } catch (error) {
1649
- throw this.handleQueryError(error, "dropTable", tableName);
1650
- }
1651
- }
1652
- /**
1653
- * Alter an existing table.
1654
- *
1655
- * @remarks
1656
- * Default implementation is a placeholder. Subclasses should implement
1657
- * table alteration logic.
1658
- *
1659
- * @param tableName - Name of table to alter
1660
- * @param operations - Alteration operations
1661
- * @param options - Alter options
1662
- *
1663
- * @throws {DatabaseError} If table alteration fails
1664
- */
1665
- async alterTable(tableName, operations, _options) {
1666
- const quotedTable = this.escapeIdentifier(tableName);
1667
- for (const op of operations) {
1668
- let query;
1669
- switch (op.kind) {
1670
- case "add_column": {
1671
- let colDef = `${this.escapeIdentifier(op.column.name)} ${op.column.type.toUpperCase()}`;
1672
- if (op.column.nullable === false) colDef += " NOT NULL";
1673
- if (op.column.unique) colDef += " UNIQUE";
1674
- if (op.column.default !== void 0) {
1675
- const defaultVal = typeof op.column.default === "object" && op.column.default !== null && "sql" in op.column.default ? op.column.default.sql : typeof op.column.default === "string" ? `'${op.column.default}'` : String(op.column.default);
1676
- colDef += ` DEFAULT ${defaultVal}`;
1677
- }
1678
- query = `ALTER TABLE ${quotedTable} ADD COLUMN ${colDef}`;
1679
- break;
1680
- }
1681
- case "drop_column": {
1682
- const cascade = op.cascade ? " CASCADE" : "";
1683
- query = `ALTER TABLE ${quotedTable} DROP COLUMN ${this.escapeIdentifier(op.columnName)}${cascade}`;
1684
- break;
1685
- }
1686
- case "rename_column":
1687
- query = `ALTER TABLE ${quotedTable} RENAME COLUMN ${this.escapeIdentifier(op.from)} TO ${this.escapeIdentifier(op.to)}`;
1688
- break;
1689
- case "modify_column": {
1690
- query = `ALTER TABLE ${quotedTable} ALTER COLUMN ${this.escapeIdentifier(op.column.name)} TYPE ${op.column.type.toUpperCase()}`;
1691
- break;
1692
- }
1693
- case "add_constraint":
1694
- {
1695
- const constraintCols = op.constraint.columns ?? [];
1696
- const colList = constraintCols.map((c) => this.escapeIdentifier(c)).join(", ");
1697
- if (op.constraint.type === "check" && op.constraint.expression) {
1698
- query = `ALTER TABLE ${quotedTable} ADD CONSTRAINT ${this.escapeIdentifier(op.constraint.name)} CHECK (${op.constraint.expression})`;
1699
- } else {
1700
- query = `ALTER TABLE ${quotedTable} ADD CONSTRAINT ${this.escapeIdentifier(op.constraint.name)} ${op.constraint.type.toUpperCase()} (${colList})`;
1701
- }
1702
- break;
1703
- }
1704
- case "drop_constraint": {
1705
- const cascadeConstraint = op.cascade ? " CASCADE" : "";
1706
- query = `ALTER TABLE ${quotedTable} DROP CONSTRAINT ${this.escapeIdentifier(op.constraintName)}${cascadeConstraint}`;
1707
- break;
1708
- }
1709
- default:
1710
- throw this.createDatabaseError(
1711
- "query",
1712
- `Unsupported alter table operation: ${op.kind}`,
1713
- void 0
1714
- );
1715
- }
1716
- try {
1717
- await this.executeQuery(query);
1718
- } catch (error) {
1719
- throw this.handleQueryError(error, "alterTable", tableName);
1720
- }
1721
- }
1722
- }
1723
- /**
1724
- * Check if a table exists in the database.
1725
- *
1726
- * @remarks
1727
- * Uses dialect-specific information schema queries to check table existence.
1728
- * This is useful for development mode auto-sync to determine whether to
1729
- * CREATE or DROP/CREATE tables.
1730
- *
1731
- * @param tableName - Name of table to check
1732
- * @param schema - Optional schema name (defaults to 'public' for PostgreSQL)
1733
- * @returns True if table exists, false otherwise
1734
- *
1735
- * @throws {DatabaseError} If query fails
1736
- *
1737
- * @example
1738
- * ```typescript
1739
- * const exists = await adapter.tableExists('users');
1740
- * if (exists) {
1741
- * await adapter.dropTable('users');
1742
- * }
1743
- * await adapter.createTable(userTableDef);
1744
- * ```
1745
- */
1746
- async tableExists(tableName, schema) {
1747
- try {
1748
- let sql6;
1749
- const params = [];
1750
- switch (this.dialect) {
1751
- case "postgresql":
1752
- sql6 = `
572
+ /**
573
+ * Whether a `transaction()` call made from the current async context would
574
+ * join a transaction already open on this adapter, as a savepoint, rather
575
+ * than open one of its own.
576
+ *
577
+ * A caller about to do something no rollback can undo — removing a stored
578
+ * file, say — asks this first: work that joins an enclosing transaction is
579
+ * undone with it, while the effect it performs after its own savepoint
580
+ * releases is not.
581
+ *
582
+ * False here. Concrete rather than abstract so existing adapters keep
583
+ * working unchanged, and false is right for a pooled adapter: a call made
584
+ * inside another transaction's work opens its own transaction on its own
585
+ * connection, which commits independently.
586
+ *
587
+ * @returns True when the call would nest in an open transaction
588
+ */
589
+ inTransaction() {
590
+ return false;
591
+ }
592
+ /**
593
+ * The transaction each async flow is inside, kept per flow rather than per
594
+ * connection: on SQLite every request shares the one connection, and
595
+ * another request's open transaction says nothing about this one.
596
+ */
597
+ flowTransactions = new node_async_hooks.AsyncLocalStorage();
598
+ /** Each pool's reporting views, by method list (`observedPool`). Gone with the pool. */
599
+ observedPools = /* @__PURE__ */ new WeakMap();
600
+ /**
601
+ * Whether the calling async flow is inside a transaction on this adapter
602
+ * that has not finished: its own, or one it was started from inside.
603
+ *
604
+ * A write that must outlive any rollback — a count of a failed attempt, the
605
+ * spend of a single-use secret — asks this before running in a transaction
606
+ * of its own. Made inside an open transaction on SQLite, it joins that
607
+ * transaction as a savepoint and is undone with it. On PostgreSQL and MySQL
608
+ * it would commit on a connection of its own, so refusing it there too
609
+ * keeps code that passes on one dialect from failing on another. Unlike
610
+ * {@link inTransaction}, which asks whether a call would nest and is always
611
+ * false on a pooled adapter, this answers alike on every dialect, and
612
+ * another flow's open transaction never answers true.
613
+ *
614
+ * Covers transactions opened through {@link transaction} and through the
615
+ * Drizzle handle's own `transaction` on the dialects whose handle takes
616
+ * async work. A dialect that keeps scopes of its own judges from those
617
+ * instead; an adapter that does neither answers false.
618
+ *
619
+ * @experimental
620
+ * @returns True while a transaction the calling flow is inside is open
621
+ */
622
+ transactionOpenInFlow() {
623
+ for (let at = this.flowTransactions.getStore(); at !== void 0; at = at.enclosing) if (at.open) return true;
624
+ return false;
625
+ }
626
+ /**
627
+ * Run a whole transaction, from its BEGIN until its COMMIT or ROLLBACK has
628
+ * run, as the calling flow's open transaction, for
629
+ * {@link transactionOpenInFlow}. A dialect's `transaction` runs through
630
+ * this unless it judges the flow from scopes of its own.
631
+ *
632
+ * The transaction counts as open for its work and for anything the work
633
+ * starts, awaited or not, until it has finished. Something the work starts
634
+ * that outlives it then sees it closed, or the one enclosing it still open.
635
+ *
636
+ * @param run - The transaction, BEGIN to COMMIT or ROLLBACK
637
+ * @returns What `run` resolves to
638
+ */
639
+ runInFlowTransaction(run) {
640
+ const scope = {
641
+ open: true,
642
+ connected: false,
643
+ enclosing: this.flowTransactions.getStore(),
644
+ openedAt: escapeObserver() === void 0 ? void 0 : deepStack()
645
+ };
646
+ return this.flowTransactions.run(scope, async () => {
647
+ try {
648
+ return await run();
649
+ } finally {
650
+ scope.open = false;
651
+ }
652
+ });
653
+ }
654
+ /**
655
+ * Record that the calling flow's transaction now holds its own connection.
656
+ *
657
+ * A pooled dialect calls this right after checking out the connection a
658
+ * transaction runs on, so {@link notePoolAccess} can tell that checkout,
659
+ * which belongs to the transaction, from any later one, which escapes it.
660
+ */
661
+ markFlowTransactionConnected() {
662
+ const scope = this.flowTransactions.getStore();
663
+ if (scope !== void 0) scope.connected = true;
664
+ }
665
+ /**
666
+ * Check out, through `checkout`, the connection the calling flow's
667
+ * transaction runs on. A transaction opened inside another's work takes a
668
+ * second connection, so the checkout is reported first
669
+ * ({@link notePoolAccess}); once it holds its own, the transaction is
670
+ * marked connected, so its later checkouts are told apart from it.
671
+ *
672
+ * @param checkout - Takes a connection from the dialect's pool
673
+ * @returns The connection
674
+ */
675
+ async connectFlowTransaction(checkout) {
676
+ this.notePoolAccess();
677
+ const connection = await checkout();
678
+ this.markFlowTransactionConnected();
679
+ return connection;
680
+ }
681
+ /**
682
+ * Called by a pooled dialect before it takes anything from its pool: a
683
+ * query, or a connection checkout.
684
+ *
685
+ * While the calling flow is inside a transaction that already holds its own
686
+ * connection, a pool access runs on another connection: outside the
687
+ * transaction, blind to its uncommitted writes, not undone by its rollback,
688
+ * and able to stall a small pool. Such an access is reported to the
689
+ * observer a test harness installs under
690
+ * `Symbol.for("nextly.transactionEscapeObserver")`, with the stack that
691
+ * names its call site. With no observer installed, which is every
692
+ * production process, this is one property read.
693
+ *
694
+ * @param statement - What the pool call was handed, its SQL passed on so
695
+ * the harness can say which statement escaped; nothing for a checkout
696
+ */
697
+ notePoolAccess(statement) {
698
+ const observe = escapeObserver();
699
+ if (observe === void 0) return;
700
+ for (let at = this.flowTransactions.getStore(); at !== void 0; at = at.enclosing) if (at.open && at.connected) {
701
+ observe({
702
+ dialect: this.dialect,
703
+ stack: deepStack(),
704
+ openedAt: at.openedAt,
705
+ statement: statementOf(statement)
706
+ });
707
+ return;
708
+ }
709
+ }
710
+ /**
711
+ * `pool`, with {@link notePoolAccess} called before each of `methods`, for
712
+ * handing to code that queries the pool directly, such as Drizzle's handle.
713
+ * Everything else, `instanceof` included, reaches the pool unchanged.
714
+ *
715
+ * One view per pool and method list, made once: every Drizzle handle on the pool is given the
716
+ * same one, so `getDrizzle()` and `getDrizzle(relations)` share a `$client`, as they did when
717
+ * each was handed the pool itself.
718
+ *
719
+ * @param pool - The driver's pool
720
+ * @param methods - The pool methods that take a connection
721
+ * @returns A view of `pool` that reports those calls
722
+ */
723
+ observedPool(pool, methods) {
724
+ const key = methods.join(",");
725
+ const views = this.observedPools.get(pool) ?? /* @__PURE__ */ new Map();
726
+ this.observedPools.set(pool, views);
727
+ const cached = views.get(key);
728
+ if (cached) return cached;
729
+ const view = new Proxy(pool, { get: (target, property, receiver) => {
730
+ const value = Reflect.get(target, property, receiver);
731
+ if (typeof property !== "string" || !methods.includes(property) || typeof value !== "function") return value;
732
+ return (...args) => {
733
+ this.notePoolAccess(args[0]);
734
+ return Reflect.apply(value, target, args);
735
+ };
736
+ } });
737
+ views.set(key, view);
738
+ return view;
739
+ }
740
+ /**
741
+ * Run each of a pool-bound Drizzle handle's transactions on a connection
742
+ * `checkout` hands it, and return that connection as the adapter's own
743
+ * `transaction()` does: closed rather than pooled when no ROLLBACK can be
744
+ * confirmed on it, and never leaked by a failed BEGIN (`pinTransactions`).
745
+ *
746
+ * For PostgreSQL's and MySQL's handles. Apply it before
747
+ * {@link trackFlowTransactions}, which wraps whatever `transaction` the
748
+ * handle has at that point.
749
+ *
750
+ * @param db - A Drizzle handle on this adapter's pool
751
+ * @param checkout - Checks out a connection for one transaction
752
+ * @returns The same handle, its `transaction` replaced
753
+ */
754
+ pinDrizzleTransactions(db, checkout) {
755
+ return pinTransactions(db, checkout);
756
+ }
757
+ /**
758
+ * Make a Drizzle handle's own `transaction` count for
759
+ * {@link transactionOpenInFlow}, as the adapter's does.
760
+ *
761
+ * Core's services run their transactions through the handle on PostgreSQL
762
+ * and MySQL; without this, a call made inside one would be refused on
763
+ * SQLite, where they go through the adapter, and let through on the
764
+ * others. Not for SQLite's handle: its `transaction` takes only
765
+ * synchronous work, which no async flow can be inside.
766
+ *
767
+ * @param db - A Drizzle handle this adapter created
768
+ * @returns The same handle, its `transaction` tracked
769
+ */
770
+ trackFlowTransactions(db) {
771
+ const transactor = db;
772
+ const untracked = transactor.transaction.bind(transactor);
773
+ const tracked = (work, config) => this.runInFlowTransaction(() => untracked(work, config));
774
+ Object.defineProperty(db, "transaction", {
775
+ value: tracked,
776
+ configurable: true,
777
+ writable: true
778
+ });
779
+ return db;
780
+ }
781
+ /**
782
+ * Run `effect` once the write made from the current async context has
783
+ * committed: an event, a hook, a cache flush, anything outside the database
784
+ * that must not happen for a change that is rolled back.
785
+ *
786
+ * A caller invokes this after its own transaction has resolved. Where that
787
+ * transaction was nested in an enclosing one, as a savepoint, the change is
788
+ * not durable yet, and an adapter that nests (see {@link inTransaction})
789
+ * holds `effect` until the outermost transaction commits, and drops it when
790
+ * that transaction, or the savepoint it was registered in, rolls back. A
791
+ * held effect runs after the commit, before the outermost `transaction()`
792
+ * call resolves, in registration order; one that then throws or rejects is
793
+ * handed to `onDeferredFailure` rather than failing a transaction that has
794
+ * committed.
795
+ *
796
+ * Here `effect` runs now and the returned promise settles with it, its
797
+ * failure included. Concrete rather than abstract, as `inTransaction` is,
798
+ * and right for a pooled adapter: each transaction has its own connection
799
+ * and has committed by the time its caller continues.
800
+ *
801
+ * @experimental
802
+ * @param effect - What to run once the change is durable
803
+ * @param onDeferredFailure - Receives the error of a held effect that fails
804
+ * @returns Settles once `effect` has run, or once it has been held
805
+ */
806
+ async afterCommit(effect, _onDeferredFailure) {
807
+ await effect();
808
+ }
809
+ /**
810
+ * Table resolver for looking up Drizzle table objects by name.
811
+ * When set, CRUD methods use Drizzle's query API instead of raw SQL.
812
+ * Set via setTableResolver() after boot-time schema loading.
813
+ */
814
+ tableResolver = null;
815
+ /**
816
+ * Set the table resolver for Drizzle query API support.
817
+ * When a resolver is set, CRUD methods (select, insert, update, delete, upsert)
818
+ * will use Drizzle's query API (db.select().from(), etc.) instead of raw SQL
819
+ * string building. Falls back to raw SQL if the resolver doesn't have the table.
820
+ *
821
+ * @param resolver - TableResolver implementation (e.g. SchemaRegistry)
822
+ */
823
+ setTableResolver(resolver) {
824
+ this.tableResolver = resolver;
825
+ }
826
+ /**
827
+ * Get a Drizzle table object by name from the resolver.
828
+ * Returns null if no resolver is set or table is not found.
829
+ */
830
+ getTableObject(tableName) {
831
+ return this.tableResolver?.getTable(tableName) ?? null;
832
+ }
833
+ /**
834
+ * Run a Drizzle-built statement and return its rows.
835
+ *
836
+ * @remarks
837
+ * The CRUD methods resolve their table through the schema registry and reject
838
+ * any name it does not declare, which leaves no way to read from a table the
839
+ * ORM does not know — one mid-rename, above all — except by assembling SQL and
840
+ * quoting identifiers by hand. This takes Drizzle's `sql` template instead, so
841
+ * the dialect in use decides the quoting and the parameter binding.
842
+ *
843
+ * Concrete rather than abstract so existing adapters keep working unchanged.
844
+ * The three drivers disagree about both the call and the result: node-postgres
845
+ * returns `{ rows }`, mysql2 a `[rows, fields]` tuple, and better-sqlite3 has
846
+ * no `execute` at all and answers `all`. Keeping that here rather than at each
847
+ * call site is the point — a caller reasoning about it would be reasoning
848
+ * about a driver it cannot see.
849
+ *
850
+ * @param statement - Drizzle `sql` template to run
851
+ * @returns Rows the statement produced
852
+ */
853
+ async queryStatement(statement) {
854
+ if (this.getCapabilities().dialect === "sqlite") return await this.getDrizzle().all(statement);
855
+ const result = await this.getDrizzle().execute(statement);
856
+ if (Array.isArray(result)) return Array.isArray(result[0]) ? result[0] : result;
857
+ if (typeof result === "object" && result !== null && Array.isArray(result.rows)) return result.rows;
858
+ throw this.createDatabaseError("query", "Drizzle statement returned a result shape this adapter does not recognise; refusing to report it as an empty result.", void 0);
859
+ }
860
+ /**
861
+ * Map data keys from SQL column names (snake_case) to Drizzle JS property names (camelCase).
862
+ * Drizzle schemas define columns as e.g. `createdAt: timestamp("created_at")` — the JS
863
+ * property is `createdAt` but the SQL column is `created_at`. Services pass snake_case keys
864
+ * because they match the DB column names. This method maps them to the JS names Drizzle expects.
865
+ */
866
+ mapDataToColumnNames(tableObj, data) {
867
+ if (!tableObj || typeof tableObj !== "object") return data;
868
+ const sqlToJs = /* @__PURE__ */ new Map();
869
+ const jsonColumns = /* @__PURE__ */ new Set();
870
+ for (const [jsName, colDef] of Object.entries(tableObj)) {
871
+ if (!colDef || typeof colDef !== "object" || !("name" in colDef) || typeof colDef.name !== "string") continue;
872
+ const sqlName = colDef.name;
873
+ sqlToJs.set(sqlName, jsName);
874
+ if (isJsonColumn(colDef)) jsonColumns.add(jsName);
875
+ }
876
+ if (sqlToJs.size === 0) return data;
877
+ const mapped = {};
878
+ for (const [key, value] of Object.entries(data)) {
879
+ const jsKey = sqlToJs.get(key) ?? key;
880
+ if (jsonColumns.has(jsKey) && typeof value === "string") try {
881
+ mapped[jsKey] = JSON.parse(value);
882
+ } catch {
883
+ mapped[jsKey] = value;
884
+ }
885
+ else mapped[jsKey] = value;
886
+ }
887
+ return mapped;
888
+ }
889
+ /**
890
+ * Map data keys from Drizzle JS property names to SQL column names for the
891
+ * raw-SQL transaction insert path. The transaction context builds INSERT
892
+ * statements from Object.keys(data) used directly as column identifiers, so a
893
+ * table whose Drizzle property names differ from its SQL column names
894
+ * (camelCase core tables like nextly_versions) needs its keys translated
895
+ * first. For tables whose property names already equal their column names
896
+ * (the dynamic dc_/single_/comp_ tables) every lookup is identity, so
897
+ * existing callers are unaffected.
898
+ */
899
+ mapKeysToSqlColumns(tableObj, data) {
900
+ if (!tableObj || typeof tableObj !== "object") return data;
901
+ const jsToSql = /* @__PURE__ */ new Map();
902
+ for (const [jsName, colDef] of Object.entries(tableObj)) if (colDef && typeof colDef === "object" && "name" in colDef && typeof colDef.name === "string") jsToSql.set(jsName, colDef.name);
903
+ if (jsToSql.size === 0) return data;
904
+ const out = {};
905
+ for (const [key, value] of Object.entries(data)) out[jsToSql.get(key) ?? key] = value;
906
+ return out;
907
+ }
908
+ /**
909
+ * Map a list of column identifiers (Drizzle property names) to their SQL
910
+ * column names, for the raw-SQL transaction insert paths that build a
911
+ * RETURNING clause from `options.returning`. Same identity behavior as
912
+ * `mapKeysToSqlColumns`: names that are already SQL columns (the dynamic
913
+ * dc_/single_/comp_ tables) pass through unchanged.
914
+ */
915
+ mapColumnNamesToSql(tableObj, names) {
916
+ if (!tableObj || typeof tableObj !== "object") return names;
917
+ const jsToSql = /* @__PURE__ */ new Map();
918
+ for (const [jsName, colDef] of Object.entries(tableObj)) if (colDef && typeof colDef === "object" && "name" in colDef && typeof colDef.name === "string") jsToSql.set(jsName, colDef.name);
919
+ if (jsToSql.size === 0) return names;
920
+ return names.map((n) => jsToSql.get(n) ?? n);
921
+ }
922
+ /**
923
+ * Remap a raw-SQL result row's KEYS from SQL column names to Drizzle property
924
+ * names, so the raw-SQL transaction insert paths return the same key casing
925
+ * as the non-transactional (Drizzle) insert. Keys only - values are left
926
+ * untouched, so this does not change how JSON/date columns are decoded. For
927
+ * tables whose property names already equal their SQL columns (the dynamic
928
+ * dc_/single_/comp_ tables) every lookup is identity, so existing callers see
929
+ * no change.
930
+ */
931
+ mapRowKeysToJs(tableObj, row) {
932
+ if (!tableObj || typeof tableObj !== "object" || !row || typeof row !== "object") return row;
933
+ const sqlToJs = /* @__PURE__ */ new Map();
934
+ for (const [jsName, colDef] of Object.entries(tableObj)) if (colDef && typeof colDef === "object" && "name" in colDef && typeof colDef.name === "string") sqlToJs.set(colDef.name, jsName);
935
+ if (sqlToJs.size === 0) return row;
936
+ const out = {};
937
+ for (const [key, value] of Object.entries(row)) out[sqlToJs.get(key) ?? key] = value;
938
+ return out;
939
+ }
940
+ /**
941
+ * Decode a raw-SQL result row's DATE column values the way a Drizzle query
942
+ * would, so a write answers with the same representation a read of the same
943
+ * column gives.
944
+ *
945
+ * A Drizzle query path runs every value through its column definition; a
946
+ * raw-SQL path does not, so a dialect that stores a timestamp as a number
947
+ * answers a write with that number while every read answers a `Date`. Only
948
+ * date columns are touched, and a value that is already a `Date` is left
949
+ * alone, so a driver that decodes on its own is unaffected.
950
+ *
951
+ * Runs after {@link mapRowKeysToJs}: keys are Drizzle property names by then,
952
+ * which is what the table definition is keyed by.
953
+ */
954
+ mapDateValuesFromDriver(tableObj, row) {
955
+ if (!tableObj || typeof tableObj !== "object" || !row || typeof row !== "object") return row;
956
+ let decoded;
957
+ for (const [jsName, colDef] of Object.entries(tableObj)) {
958
+ if (!isDateColumn(colDef)) continue;
959
+ const value = row[jsName];
960
+ if (value == null || value instanceof Date) continue;
961
+ decoded ??= { ...row };
962
+ decoded[jsName] = colDef.mapFromDriverValue(value);
963
+ }
964
+ return decoded ?? row;
965
+ }
966
+ /**
967
+ * Encode a row's DATE values the way a Drizzle query would before binding
968
+ * them, so what lands in the column does not depend on the server's timezone.
969
+ *
970
+ * A column declared without a time zone stores a wall clock and records
971
+ * nothing about which zone it belongs to, so both ends have to agree. Drizzle
972
+ * writes UTC and reads UTC. A driver handed a `Date` writes the LOCAL wall
973
+ * clock instead, and the same row then reads back shifted by the offset --
974
+ * on a UTC server the two agree and nothing is visibly wrong, which is why
975
+ * this survives CI.
976
+ *
977
+ * Only date columns are touched. A value that is not a `Date` is left as it
978
+ * is, so a caller that already encoded one is not encoded twice.
979
+ *
980
+ * Keys may be spelled either way at this point, so both the Drizzle property
981
+ * name and the SQL column name resolve.
982
+ */
983
+ mapDateValuesToDriver(tableObj, data) {
984
+ if (!tableObj || typeof tableObj !== "object" || !data) return data;
985
+ const dateColumns = /* @__PURE__ */ new Map();
986
+ for (const [jsName, colDef] of Object.entries(tableObj)) {
987
+ if (!isDateColumn(colDef)) continue;
988
+ dateColumns.set(jsName, colDef);
989
+ const sqlName = colDef.name;
990
+ if (typeof sqlName === "string") dateColumns.set(sqlName, colDef);
991
+ }
992
+ if (dateColumns.size === 0) return data;
993
+ let encoded;
994
+ for (const [key, value] of Object.entries(data)) {
995
+ if (!(value instanceof Date)) continue;
996
+ const column = dateColumns.get(key);
997
+ if (!column) continue;
998
+ encoded ??= { ...data };
999
+ encoded[key] = column.mapToDriverValue(value);
1000
+ }
1001
+ return encoded ?? data;
1002
+ }
1003
+ /**
1004
+ * Bring a row about to be written through a raw-SQL statement into the shape
1005
+ * a Drizzle query would have bound: SQL column names for keys, encoded values
1006
+ * for date columns.
1007
+ *
1008
+ * The mirror of {@link mapRowFromRawSql}. The raw-SQL write paths exist
1009
+ * because a dialect needs SQL a Drizzle query cannot express, not because
1010
+ * their callers want different values in the table.
1011
+ */
1012
+ mapRowToRawSql(tableObj, data) {
1013
+ return this.mapDateValuesToDriver(tableObj, this.mapKeysToSqlColumns(tableObj, data));
1014
+ }
1015
+ /**
1016
+ * Bring a raw-SQL result row into the shape a Drizzle query would have
1017
+ * produced: Drizzle property names for keys, decoded values for date columns.
1018
+ *
1019
+ * The raw-SQL write paths exist because a dialect needs SQL a Drizzle query
1020
+ * cannot express, not because their callers want a different row shape, so
1021
+ * every one of them ends here.
1022
+ */
1023
+ mapRowFromRawSql(tableObj, row, aliases = []) {
1024
+ const mapped = this.mapDateValuesFromDriver(tableObj, this.mapRowKeysToJs(tableObj, row));
1025
+ return this.applyWallClockAliases(mapped, aliases);
1026
+ }
1027
+ /**
1028
+ * The date columns a statement is about to return, and the alias each one's
1029
+ * wall clock is spelled out under.
1030
+ *
1031
+ * A driver turns a timestamp into a `Date` using ITS zone before any of this
1032
+ * code runs, and that conversion is lossy: a wall clock inside a
1033
+ * daylight-saving gap is a local time that does not exist, so the driver
1034
+ * normalizes it and the original is gone. Asking the database for the wall
1035
+ * clock as text alongside the row is the only way to read it exactly.
1036
+ *
1037
+ * `"*"` covers every date column; a projection covers only the ones it asked
1038
+ * for, so a caller still gets back what it requested and nothing more.
1039
+ */
1040
+ dateWallClockAliases(tableObj, returning) {
1041
+ if (!tableObj || typeof tableObj !== "object" || returning === void 0) return [];
1042
+ const requested = returning === "*" ? void 0 : new Set(returning.map(String));
1043
+ const taken = /* @__PURE__ */ new Set();
1044
+ for (const [jsName, colDef] of Object.entries(tableObj)) {
1045
+ taken.add(jsName);
1046
+ const columnName = colDef.name;
1047
+ if (typeof columnName === "string") taken.add(columnName);
1048
+ }
1049
+ let next = 0;
1050
+ const freeAlias = () => {
1051
+ let candidate = `${WALL_CLOCK_ALIAS_STEM}${next++}`;
1052
+ while (taken.has(candidate)) candidate = `${WALL_CLOCK_ALIAS_STEM}${next++}`;
1053
+ taken.add(candidate);
1054
+ return candidate;
1055
+ };
1056
+ const aliases = [];
1057
+ for (const [jsName, colDef] of Object.entries(tableObj)) {
1058
+ if (!isDateColumn(colDef)) continue;
1059
+ if (carriesTimeZone(colDef)) continue;
1060
+ const sqlName = colDef.name;
1061
+ if (typeof sqlName !== "string") continue;
1062
+ if (requested && !requested.has(sqlName) && !requested.has(jsName)) continue;
1063
+ aliases.push({
1064
+ sqlName,
1065
+ alias: freeAlias(),
1066
+ jsName
1067
+ });
1068
+ }
1069
+ return aliases;
1070
+ }
1071
+ /**
1072
+ * Replace each date in a row with the wall clock the database spelled out,
1073
+ * read as UTC, and drop the aliases that carried it.
1074
+ *
1075
+ * The statement writes a UTC wall clock, so reading one back as UTC is what
1076
+ * makes a write and a later read agree. Whatever the column actually stored
1077
+ * is what arrives -- a column keeping only whole seconds reports whole
1078
+ * seconds -- because this is the database's own text, not a value
1079
+ * reconstructed from the one that was bound.
1080
+ */
1081
+ applyWallClockAliases(row, aliases) {
1082
+ if (!row || typeof row !== "object" || aliases.length === 0) return row;
1083
+ const record = row;
1084
+ let resolved;
1085
+ for (const { alias, jsName } of aliases) {
1086
+ if (!(alias in record)) continue;
1087
+ resolved ??= { ...record };
1088
+ const text = resolved[alias];
1089
+ delete resolved[alias];
1090
+ if (typeof text !== "string") continue;
1091
+ const parsed = parseWallClockAsUtc(text);
1092
+ if (parsed !== void 0) resolved[jsName] = parsed;
1093
+ }
1094
+ return resolved ?? row;
1095
+ }
1096
+ /**
1097
+ * Build a Drizzle column projection object (`{ propertyName: column }`) from a
1098
+ * requested column list, used by `select` (columns) and `insert` (returning).
1099
+ * A requested name resolves against either the Drizzle property name
1100
+ * (camelCase) or the SQL column name (snake_case); the projection is keyed by
1101
+ * the property name so the row shape matches a full select. Returns undefined
1102
+ * for `"*"` or when nothing resolves, so callers fall back to all columns.
1103
+ */
1104
+ buildColumnProjection(tableObj, names) {
1105
+ if (names == null || names === "*" || !tableObj || typeof tableObj !== "object") return;
1106
+ const { projection } = this.resolveColumns(tableObj, names);
1107
+ return Object.keys(projection).length ? projection : void 0;
1108
+ }
1109
+ /**
1110
+ * The requested column names split into what this table has and what it does not.
1111
+ *
1112
+ * One resolution, reported both ways, because two callers need different halves
1113
+ * of the same answer: a projection drops what it cannot find, while a distinct
1114
+ * count has to REFUSE it. Resolving twice would let the two disagree about what
1115
+ * "found" means — the SQL-name alias below is exactly the kind of detail a
1116
+ * second implementation gets wrong — and the count would then reject a column
1117
+ * the projection happily uses.
1118
+ *
1119
+ * Both spellings resolve: a caller may name the Drizzle property or the SQL
1120
+ * column, and either maps to the same projection key.
1121
+ */
1122
+ resolveColumns(tableObj, names) {
1123
+ const cols = (0, drizzle_orm.getColumns)(tableObj);
1124
+ const byAnyName = {};
1125
+ for (const [jsName, col] of Object.entries(cols)) {
1126
+ byAnyName[jsName] = {
1127
+ jsName,
1128
+ col
1129
+ };
1130
+ const sqlName = col?.name;
1131
+ if (typeof sqlName === "string") byAnyName[sqlName] = {
1132
+ jsName,
1133
+ col
1134
+ };
1135
+ }
1136
+ const projection = {};
1137
+ const unresolved = [];
1138
+ for (const name of names) {
1139
+ const hit = byAnyName[name];
1140
+ if (hit) projection[hit.jsName] = hit.col;
1141
+ else unresolved.push(name);
1142
+ }
1143
+ return {
1144
+ projection,
1145
+ unresolved
1146
+ };
1147
+ }
1148
+ /**
1149
+ * Check if the adapter is currently connected.
1150
+ *
1151
+ * @remarks
1152
+ * Default implementation returns false. Subclasses should override
1153
+ * to provide accurate connection status.
1154
+ *
1155
+ * @returns True if connected, false otherwise
1156
+ */
1157
+ isConnected() {
1158
+ return false;
1159
+ }
1160
+ /**
1161
+ * Get connection pool statistics.
1162
+ *
1163
+ * @remarks
1164
+ * Returns null by default. Subclasses with connection pooling
1165
+ * should override to provide pool statistics.
1166
+ *
1167
+ * @returns Pool statistics or null if not applicable
1168
+ */
1169
+ getPoolStats() {
1170
+ return null;
1171
+ }
1172
+ /**
1173
+ * Default query timeout in milliseconds.
1174
+ *
1175
+ * @remarks
1176
+ * This value is used by executeWithTimeout() when no explicit timeout
1177
+ * is provided. Subclasses should set this from their config.
1178
+ *
1179
+ * @default 15000 (15 seconds)
1180
+ *
1181
+ * @protected
1182
+ */
1183
+ defaultQueryTimeoutMs = 15e3;
1184
+ /**
1185
+ * Execute an async operation with a timeout.
1186
+ *
1187
+ * @remarks
1188
+ * Wraps an async operation with a timeout that aborts if the operation
1189
+ * exceeds the specified duration. Uses Promise.race for clean timeout
1190
+ * handling without memory leaks.
1191
+ *
1192
+ * When the timeout is reached, a DatabaseError with kind 'timeout' is thrown.
1193
+ * Note that this does NOT cancel the underlying database query - it only
1194
+ * prevents the calling code from waiting indefinitely. For true query
1195
+ * cancellation, use database-level statement timeouts (PostgreSQL) or
1196
+ * similar mechanisms.
1197
+ *
1198
+ * @param operation - Async operation to execute
1199
+ * @param timeoutMs - Timeout in milliseconds (defaults to defaultQueryTimeoutMs)
1200
+ * @returns Result of the operation
1201
+ *
1202
+ * @throws {DatabaseError} With kind 'timeout' if operation exceeds timeout
1203
+ *
1204
+ * @example
1205
+ * ```typescript
1206
+ * // Use default timeout
1207
+ * const result = await adapter.executeWithTimeout(
1208
+ * () => adapter.select('users', { limit: 1000 })
1209
+ * );
1210
+ *
1211
+ * // Use custom timeout for specific operation
1212
+ * const result = await adapter.executeWithTimeout(
1213
+ * () => adapter.select('large_table'),
1214
+ * 60000 // 60 seconds for large queries
1215
+ * );
1216
+ * ```
1217
+ *
1218
+ * @public
1219
+ */
1220
+ async executeWithTimeout(operation, timeoutMs) {
1221
+ const timeout = timeoutMs ?? this.defaultQueryTimeoutMs;
1222
+ if (timeout <= 0) return operation();
1223
+ let timeoutId;
1224
+ const timeoutPromise = new Promise((_, reject) => {
1225
+ timeoutId = setTimeout(() => {
1226
+ reject(this.createDatabaseError("timeout", `Query execution timed out after ${timeout}ms`));
1227
+ }, timeout);
1228
+ });
1229
+ try {
1230
+ return await Promise.race([operation(), timeoutPromise]);
1231
+ } finally {
1232
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
1233
+ }
1234
+ }
1235
+ /**
1236
+ * Set the default query timeout.
1237
+ *
1238
+ * @remarks
1239
+ * This method allows runtime configuration of the default timeout.
1240
+ * Subclasses should call this in their constructor or connect() method
1241
+ * based on their configuration.
1242
+ *
1243
+ * @param timeoutMs - Timeout in milliseconds (0 to disable)
1244
+ *
1245
+ * @protected
1246
+ */
1247
+ setDefaultQueryTimeout(timeoutMs) {
1248
+ this.defaultQueryTimeoutMs = timeoutMs;
1249
+ }
1250
+ /**
1251
+ * Get the current default query timeout.
1252
+ *
1253
+ * @returns Current default timeout in milliseconds
1254
+ *
1255
+ * @public
1256
+ */
1257
+ getDefaultQueryTimeout() {
1258
+ return this.defaultQueryTimeoutMs;
1259
+ }
1260
+ /**
1261
+ * Select multiple records from a table.
1262
+ *
1263
+ * @remarks
1264
+ * Default implementation builds a SELECT query and executes it.
1265
+ * Subclasses can override for optimization or dialect-specific features.
1266
+ *
1267
+ * @param table - Table name
1268
+ * @param options - Select options (filtering, sorting, pagination)
1269
+ * @returns Array of matching records
1270
+ *
1271
+ * @throws {DatabaseError} If query fails
1272
+ *
1273
+ * @example
1274
+ * ```typescript
1275
+ * const users = await adapter.select('users', {
1276
+ * where: { and: [{ column: 'role', op: '=', value: 'admin' }] },
1277
+ * orderBy: [{ column: 'created_at', direction: 'desc' }],
1278
+ * limit: 10
1279
+ * });
1280
+ * ```
1281
+ */
1282
+ async select(table, options, executor) {
1283
+ const tableObj = this.getTableObject(table);
1284
+ if (tableObj) try {
1285
+ const db = executor ?? this.getDrizzle();
1286
+ const projection = options?.columns?.length ? this.buildColumnProjection(tableObj, options.columns) : void 0;
1287
+ let query = projection ? db.select(projection).from(tableObj) : db.select().from(tableObj);
1288
+ if (options?.where) {
1289
+ const whereCondition = buildDrizzleWhere(tableObj, options.where);
1290
+ if (whereCondition) query = query.where(whereCondition);
1291
+ }
1292
+ if (options?.orderBy?.length) {
1293
+ const orderClauses = buildDrizzleOrderBy((0, drizzle_orm.getColumns)(tableObj), options.orderBy);
1294
+ if (orderClauses.length) query = query.orderBy(...orderClauses);
1295
+ }
1296
+ if (options?.limit !== void 0) query = query.limit(options.limit);
1297
+ if (options?.offset !== void 0) query = query.offset(options.offset);
1298
+ if (options?.forUpdate && !executor) throw this.createDatabaseError("query", "forUpdate requires a transaction executor: a lock request on the pooled connection takes no durable lock and cannot prevent a concurrent write.", void 0);
1299
+ if (options?.forUpdate && this.dialect !== "sqlite") query = query.for("update");
1300
+ return await query;
1301
+ } catch (error) {
1302
+ throw this.handleQueryError(error, "select", table);
1303
+ }
1304
+ throw this.createDatabaseError("query", `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`, void 0);
1305
+ }
1306
+ /**
1307
+ * Select a single record from a table.
1308
+ *
1309
+ * @remarks
1310
+ * Default implementation uses `select()` with limit 1 and returns first result.
1311
+ * Returns null if no matching record is found.
1312
+ *
1313
+ * @param table - Table name
1314
+ * @param options - Select options
1315
+ * @returns First matching record or null
1316
+ *
1317
+ * @throws {DatabaseError} If query fails
1318
+ *
1319
+ * @example
1320
+ * ```typescript
1321
+ * const user = await adapter.selectOne('users', {
1322
+ * where: { and: [{ column: 'email', op: '=', value: 'user@example.com' }] }
1323
+ * });
1324
+ * ```
1325
+ */
1326
+ /** The registered table object, or a refusal naming what is missing. */
1327
+ requireTableObject(table) {
1328
+ const tableObj = this.getTableObject(table);
1329
+ if (tableObj) return tableObj;
1330
+ throw this.createDatabaseError("query", `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`, void 0);
1331
+ }
1332
+ /**
1333
+ * The columns a distinct count groups by, or `undefined` for a row count.
1334
+ *
1335
+ * 🔴 EVERY named column must resolve, not merely one of them. A `distinctOn`
1336
+ * naming no column this table has would fall through to a row count, and one
1337
+ * naming a valid column beside a misspelled or since-removed column would
1338
+ * count distinct over a NARROWER key than the caller asked for. Both answer a
1339
+ * different question than the one asked, silently; the second is the worse of
1340
+ * the two, because a subset key groups more rows together and so UNDERCOUNTS,
1341
+ * which reads as a plausible number rather than as a fault. Refused here,
1342
+ * where the mistake was made, rather than surfacing as a wrong figure.
1343
+ */
1344
+ countProjection(tableObj, table, options) {
1345
+ const distinctOn = options?.distinctOn;
1346
+ if (!distinctOn?.length) return void 0;
1347
+ const { projection, unresolved } = this.resolveColumns(tableObj, distinctOn);
1348
+ if (unresolved.length > 0) throw this.createDatabaseError("query", `distinctOn names ${unresolved.length === 1 ? "a column" : "columns"} that do not exist on "${table}": ${unresolved.join(", ")}`, void 0);
1349
+ return projection;
1350
+ }
1351
+ /**
1352
+ * `COUNT(*)` over a `SELECT DISTINCT` subquery.
1353
+ *
1354
+ * Its own function because it is a genuinely different query from the plain
1355
+ * count rather than a variation on one, and because building both inline made
1356
+ * the public method a chain of decisions the complexity gate objected to
1357
+ * before a reader would have.
1358
+ */
1359
+ async countDistinctRows(db, tableObj, projection, where) {
1360
+ const inner = db.selectDistinct(projection).from(tableObj).$dynamic();
1361
+ const distinctRows = where ? inner.where(where) : inner;
1362
+ const [row] = await db.select({ value: (0, drizzle_orm.count)() }).from(distinctRows.as("nextly_distinct"));
1363
+ return Number(row?.value ?? 0);
1364
+ }
1365
+ /** `COUNT(*)` over the table itself. */
1366
+ async countAllRows(db, tableObj, where) {
1367
+ const base = db.select({ value: (0, drizzle_orm.count)() }).from(tableObj).$dynamic();
1368
+ const [row] = await (where ? base.where(where) : base);
1369
+ return Number(row?.value ?? 0);
1370
+ }
1371
+ /**
1372
+ * How many rows match, or how many DISTINCT combinations of some columns do.
1373
+ *
1374
+ * The data layer had no count at all: collection totals go through
1375
+ * `countEntries`, an access-controlled path built for collection tables, and
1376
+ * nothing could count a system table. A caller wanting one selected the rows
1377
+ * and measured the array, which transfers every row to learn a number and
1378
+ * cannot be bounded without making the number wrong.
1379
+ *
1380
+ * 🔴 `distinctOn` compiles to `COUNT(*)` over a `SELECT DISTINCT` SUBQUERY,
1381
+ * never to `COUNT(DISTINCT a, b)`. The inline form is not portable and fails
1382
+ * in the direction that is hardest to notice -- MySQL accepts it, PostgreSQL
1383
+ * needs a row constructor, and SQLite rejects it outright with "wrong number
1384
+ * of arguments to function count()" -- so a query written against one engine
1385
+ * is a syntax error on another. The subquery is the single form all three
1386
+ * accept, and building it here rather than at each call site is what keeps
1387
+ * the answer identical per dialect.
1388
+ */
1389
+ async count(table, options, executor) {
1390
+ const tableObj = this.requireTableObject(table);
1391
+ try {
1392
+ const db = executor ?? this.getDrizzle();
1393
+ const where = options?.where ? buildDrizzleWhere(tableObj, options.where) : void 0;
1394
+ const projection = this.countProjection(tableObj, table, options);
1395
+ return projection ? await this.countDistinctRows(db, tableObj, projection, where) : await this.countAllRows(db, tableObj, where);
1396
+ } catch (error) {
1397
+ throw this.handleQueryError(error, "count", table);
1398
+ }
1399
+ }
1400
+ async selectOne(table, options, executor) {
1401
+ const results = await this.select(table, {
1402
+ ...options,
1403
+ limit: 1
1404
+ }, executor);
1405
+ return results.length > 0 ? results[0] : null;
1406
+ }
1407
+ /**
1408
+ * Insert a single record into a table.
1409
+ *
1410
+ * @remarks
1411
+ * Default implementation handles databases with and without RETURNING support.
1412
+ * For databases without RETURNING (MySQL), performs INSERT followed by SELECT.
1413
+ *
1414
+ * @param table - Table name
1415
+ * @param data - Record data to insert
1416
+ * @param options - Insert options
1417
+ * @returns Inserted record (with RETURNING columns if specified)
1418
+ *
1419
+ * @throws {DatabaseError} If insert fails
1420
+ *
1421
+ * @example
1422
+ * ```typescript
1423
+ * const user = await adapter.insert('users', {
1424
+ * email: 'user@example.com',
1425
+ * name: 'John Doe'
1426
+ * }, { returning: ['id', 'email', 'created_at'] });
1427
+ * ```
1428
+ */
1429
+ async insert(table, data, options) {
1430
+ const tableObj = this.getTableObject(table);
1431
+ if (tableObj) try {
1432
+ const mappedData = this.mapDataToColumnNames(tableObj, data);
1433
+ const db = this.getDrizzle();
1434
+ const caps = this.getCapabilities();
1435
+ const returning = options?.returning;
1436
+ const wantsReturning = returning != null && !(Array.isArray(returning) && returning.length === 0);
1437
+ if (caps.supportsReturning && wantsReturning) {
1438
+ const projection = this.buildColumnProjection(tableObj, returning);
1439
+ const insertQuery = db.insert(tableObj).values(mappedData);
1440
+ const result = await (projection ? insertQuery.returning(projection) : insertQuery.returning());
1441
+ return Array.isArray(result) ? result[0] : result;
1442
+ }
1443
+ const result = await db.insert(tableObj).values(mappedData);
1444
+ if (!caps.supportsReturning && wantsReturning && data.id !== void 0) return await this.selectOne(table, {
1445
+ columns: returning === "*" ? void 0 : returning,
1446
+ where: { and: [{
1447
+ column: "id",
1448
+ op: "=",
1449
+ value: data.id
1450
+ }] }
1451
+ });
1452
+ return Array.isArray(result) ? result[0] : result;
1453
+ } catch (error) {
1454
+ throw this.handleQueryError(error, "insert", table);
1455
+ }
1456
+ throw this.createDatabaseError("query", `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`, void 0);
1457
+ }
1458
+ /**
1459
+ * Insert multiple records into a table.
1460
+ *
1461
+ * @remarks
1462
+ * Default implementation performs individual inserts in sequence.
1463
+ * Subclasses can override for bulk insert optimization (e.g., COPY in PostgreSQL).
1464
+ *
1465
+ * @param table - Table name
1466
+ * @param data - Array of records to insert
1467
+ * @param options - Insert options
1468
+ * @returns Inserted records (with RETURNING columns if specified)
1469
+ *
1470
+ * @throws {DatabaseError} If insert fails
1471
+ *
1472
+ * @example
1473
+ * ```typescript
1474
+ * const users = await adapter.insertMany('users', [
1475
+ * { email: 'user1@example.com', name: 'User 1' },
1476
+ * { email: 'user2@example.com', name: 'User 2' }
1477
+ * ], { returning: ['id'] });
1478
+ * ```
1479
+ */
1480
+ async insertMany(table, data, options) {
1481
+ if (data.length === 0) return [];
1482
+ const results = [];
1483
+ for (const record of data) {
1484
+ const result = await this.insert(table, record, options);
1485
+ results.push(result);
1486
+ }
1487
+ return results;
1488
+ }
1489
+ /**
1490
+ * Update records in a table.
1491
+ *
1492
+ * @remarks
1493
+ * Default implementation builds an UPDATE query with WHERE clause.
1494
+ * Returns updated records if RETURNING is supported and requested.
1495
+ *
1496
+ * @param table - Table name
1497
+ * @param data - Data to update
1498
+ * @param where - Conditions for records to update
1499
+ * @param options - Update options
1500
+ * @returns Updated records (with RETURNING columns if specified)
1501
+ *
1502
+ * @throws {DatabaseError} If update fails
1503
+ *
1504
+ * @example
1505
+ * ```typescript
1506
+ * const updated = await adapter.update('users',
1507
+ * { status: 'active' },
1508
+ * { and: [{ column: 'id', op: '=', value: userId }] },
1509
+ * { returning: ['id', 'status', 'updated_at'] }
1510
+ * );
1511
+ * ```
1512
+ */
1513
+ async update(table, data, where, options, executor) {
1514
+ const tableObj = this.getTableObject(table);
1515
+ if (tableObj) try {
1516
+ const db = executor ?? this.getDrizzle();
1517
+ const caps = this.getCapabilities();
1518
+ const mappedData = this.mapDataToColumnNames(tableObj, data);
1519
+ let query = db.update(tableObj).set(mappedData);
1520
+ const whereCondition = buildDrizzleWhere(tableObj, where);
1521
+ if (whereCondition) query = query.where(whereCondition);
1522
+ if (caps.supportsReturning && options?.returning) return await query.returning();
1523
+ await query;
1524
+ if (!caps.supportsReturning && options?.returning) return await this.select(table, { where }, executor);
1525
+ return [];
1526
+ } catch (error) {
1527
+ throw this.handleQueryError(error, "update", table);
1528
+ }
1529
+ throw this.createDatabaseError("query", `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`, void 0);
1530
+ }
1531
+ /**
1532
+ * Update records in a table and report how many rows the statement affected.
1533
+ *
1534
+ * The count is the whole return value, mirroring `delete`. `update` cannot answer this: without
1535
+ * `returning` it discards the driver's count, and WITH `returning` on a dialect that lacks
1536
+ * RETURNING it re-SELECTs using the same WHERE — so a conditional update that just changed a
1537
+ * column named in that WHERE reads back zero rows and a write that landed reports as unmatched.
1538
+ * Reading the driver's own count has no second query to disagree with the first.
1539
+ *
1540
+ * 🔴 MySQL reports CHANGED rows, not matched rows: an UPDATE that matches a row but writes values
1541
+ * identical to what it holds counts zero. A caller using this as a compare-and-set must therefore
1542
+ * include a column the write always moves — a version bump, a timestamp with enough resolution —
1543
+ * so that matched implies changed. Postgres (`rowCount`) and SQLite (`changes`) count matched
1544
+ * rows and do not need the precaution, which is exactly why it cannot be dropped: the dialect
1545
+ * where the distinction exists is the one with no RETURNING to fall back on.
1546
+ */
1547
+ async updateCount(table, data, where, executor) {
1548
+ const tableObj = this.getTableObject(table);
1549
+ if (tableObj) try {
1550
+ const db = executor ?? this.getDrizzle();
1551
+ const mappedData = this.mapDataToColumnNames(tableObj, data);
1552
+ const statement = db.update(tableObj).set(mappedData);
1553
+ const whereCondition = buildDrizzleWhere(tableObj, where);
1554
+ return affectedRowCount(await (whereCondition ? statement.where(whereCondition) : statement));
1555
+ } catch (error) {
1556
+ throw this.handleQueryError(error, "update", table);
1557
+ }
1558
+ throw this.createDatabaseError("query", `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`, void 0);
1559
+ }
1560
+ /**
1561
+ * Delete records from a table.
1562
+ *
1563
+ * @remarks
1564
+ * Default implementation builds a DELETE query with WHERE clause.
1565
+ * Returns the number of deleted records.
1566
+ *
1567
+ * @param table - Table name
1568
+ * @param where - Conditions for records to delete
1569
+ * @param options - Delete options
1570
+ * @returns Number of deleted records
1571
+ *
1572
+ * @throws {DatabaseError} If delete fails
1573
+ *
1574
+ * @example
1575
+ * ```typescript
1576
+ * const count = await adapter.delete('users', {
1577
+ * and: [{ column: 'status', op: '=', value: 'inactive' }]
1578
+ * });
1579
+ * console.log(`Deleted ${count} users`);
1580
+ * ```
1581
+ */
1582
+ async delete(table, where, _options, executor) {
1583
+ const tableObj = this.getTableObject(table);
1584
+ if (tableObj) try {
1585
+ let query = (executor ?? this.getDrizzle()).delete(tableObj);
1586
+ const whereCondition = buildDrizzleWhere(tableObj, where);
1587
+ if (whereCondition) query = query.where(whereCondition);
1588
+ return affectedRowCount(await query);
1589
+ } catch (error) {
1590
+ throw this.handleQueryError(error, "delete", table);
1591
+ }
1592
+ throw this.createDatabaseError("query", `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`, void 0);
1593
+ }
1594
+ /**
1595
+ * Upsert (INSERT or UPDATE) a record.
1596
+ *
1597
+ * @remarks
1598
+ * Default implementation uses dialect-specific ON CONFLICT syntax.
1599
+ * PostgreSQL/SQLite: ON CONFLICT ... DO UPDATE
1600
+ * MySQL: the MySQL adapter overrides this, resolving the conflict by the
1601
+ * conflict columns (see its `upsert`)
1602
+ *
1603
+ * @param table - Table name
1604
+ * @param data - Record data
1605
+ * @param options - Upsert options (must specify conflict columns)
1606
+ * @returns Upserted record (with RETURNING columns if specified)
1607
+ *
1608
+ * @throws {DatabaseError} If upsert fails
1609
+ *
1610
+ * @example
1611
+ * ```typescript
1612
+ * const user = await adapter.upsert('users', {
1613
+ * email: 'user@example.com',
1614
+ * name: 'Updated Name'
1615
+ * }, {
1616
+ * conflictColumns: ['email'],
1617
+ * updateColumns: ['name'],
1618
+ * returning: ['id', 'email', 'name']
1619
+ * });
1620
+ * ```
1621
+ */
1622
+ async upsert(table, data, options, executor) {
1623
+ const tableObj = this.getTableObject(table);
1624
+ if (tableObj) try {
1625
+ const db = executor ?? this.getDrizzle();
1626
+ const caps = this.getCapabilities();
1627
+ const columns = (0, drizzle_orm.getColumns)(tableObj);
1628
+ const conflictTarget = options.conflictColumns.map((col) => columns[col]).filter(Boolean);
1629
+ const conflictSet = new Set(options.conflictColumns);
1630
+ const updateData = {};
1631
+ const updateColumns = options.updateColumns ?? Object.keys(data);
1632
+ for (const key of updateColumns) if (!conflictSet.has(key) && key in data) updateData[key] = data[key];
1633
+ const keepExisting = Object.keys(updateData).length === 0;
1634
+ this.assertUpsertTarget(table, data, options, keepExisting);
1635
+ let query;
1636
+ if (caps.supportsOnConflict) {
1637
+ const inserted = db.insert(tableObj).values(data);
1638
+ query = keepExisting ? inserted.onConflictDoNothing({ target: conflictTarget }) : inserted.onConflictDoUpdate({
1639
+ target: conflictTarget,
1640
+ set: updateData
1641
+ });
1642
+ } else query = db.insert(tableObj).values(data);
1643
+ if (caps.supportsReturning) {
1644
+ const result = await query.returning();
1645
+ const row = Array.isArray(result) ? result[0] : result;
1646
+ if (row !== void 0) return row;
1647
+ } else await query;
1648
+ const conflictValues = options.conflictColumns.map((column) => [column, data[column]]);
1649
+ if (conflictValues.length > 0 && conflictValues.every(([, value]) => value !== void 0)) return await this.selectOne(table, { where: { and: conflictValues.map(([column, value]) => ({
1650
+ column,
1651
+ op: "=",
1652
+ value
1653
+ })) } }, executor);
1654
+ return data;
1655
+ } catch (error) {
1656
+ throw this.handleQueryError(error, "upsert", table);
1657
+ }
1658
+ throw this.createDatabaseError("query", `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`, void 0);
1659
+ }
1660
+ /**
1661
+ * Run pending migrations.
1662
+ *
1663
+ * @remarks
1664
+ * Default implementation is a placeholder. Subclasses should implement
1665
+ * migration tracking and execution logic.
1666
+ *
1667
+ * @param migrations - Array of migrations to run
1668
+ * @returns Migration result with applied and pending migrations
1669
+ *
1670
+ * @throws {DatabaseError} If migration fails
1671
+ */
1672
+ async migrate(_migrations) {
1673
+ throw this.createDatabaseError("query", "migrate() must be implemented by dialect-specific adapter. Use PostgresAdapter, MySqlAdapter, or SqliteAdapter.", void 0);
1674
+ }
1675
+ /**
1676
+ * Rollback the last migration.
1677
+ *
1678
+ * @remarks
1679
+ * Default implementation is a placeholder. Subclasses should implement
1680
+ * migration rollback logic.
1681
+ *
1682
+ * @returns Migration result after rollback
1683
+ *
1684
+ * @throws {DatabaseError} If rollback fails
1685
+ */
1686
+ async rollback() {
1687
+ throw this.createDatabaseError("query", "rollback() must be implemented by dialect-specific adapter. Use PostgresAdapter, MySqlAdapter, or SqliteAdapter.", void 0);
1688
+ }
1689
+ /**
1690
+ * Get migration status.
1691
+ *
1692
+ * @remarks
1693
+ * Default implementation is a placeholder. Subclasses should implement
1694
+ * migration status checking.
1695
+ *
1696
+ * @returns Current migration status
1697
+ *
1698
+ * @throws {DatabaseError} If status check fails
1699
+ */
1700
+ async getMigrationStatus() {
1701
+ try {
1702
+ const applied = (await this.executeQuery(`SELECT * FROM "__drizzle_migrations" ORDER BY created_at ASC`)).map((r) => ({
1703
+ id: String(r.id),
1704
+ name: r.hash,
1705
+ appliedAt: new Date(r.created_at),
1706
+ checksum: r.hash
1707
+ }));
1708
+ return {
1709
+ applied,
1710
+ pending: [],
1711
+ current: applied.length > 0 ? applied[applied.length - 1].id : null
1712
+ };
1713
+ } catch {
1714
+ return {
1715
+ applied: [],
1716
+ pending: [],
1717
+ current: null
1718
+ };
1719
+ }
1720
+ }
1721
+ /**
1722
+ * Create a new table.
1723
+ *
1724
+ * @remarks
1725
+ * Default implementation is a placeholder. Subclasses should implement
1726
+ * table creation logic.
1727
+ *
1728
+ * @param definition - Table definition
1729
+ * @param options - Creation options
1730
+ *
1731
+ * @throws {DatabaseError} If table creation fails
1732
+ */
1733
+ async createTable(definition, options) {
1734
+ const columnDefs = definition.columns.map((col) => {
1735
+ let colSql = `${this.escapeIdentifier(col.name)} ${col.type.toUpperCase()}`;
1736
+ if (col.primaryKey) colSql += " PRIMARY KEY";
1737
+ if (col.nullable === false) colSql += " NOT NULL";
1738
+ if (col.unique) colSql += " UNIQUE";
1739
+ if (col.default !== void 0) colSql += ` DEFAULT ${renderDefaultValue(col.default, col.name)}`;
1740
+ return colSql;
1741
+ });
1742
+ const query = `CREATE TABLE ${options?.ifNotExists !== false ? "IF NOT EXISTS " : ""}${this.escapeIdentifier(definition.name)} (\n ${columnDefs.join(",\n ")}\n)`;
1743
+ try {
1744
+ await this.executeQuery(query);
1745
+ } catch (error) {
1746
+ throw this.handleQueryError(error, "createTable", definition.name);
1747
+ }
1748
+ }
1749
+ /**
1750
+ * Drop a table.
1751
+ *
1752
+ * @remarks
1753
+ * Default implementation is a placeholder. Subclasses should implement
1754
+ * table dropping logic.
1755
+ *
1756
+ * @param tableName - Name of table to drop
1757
+ * @param options - Drop options
1758
+ *
1759
+ * @throws {DatabaseError} If table drop fails
1760
+ */
1761
+ async dropTable(tableName, options) {
1762
+ const ifExists = options?.ifExists !== false ? "IF EXISTS " : "";
1763
+ const cascade = options?.cascade ? " CASCADE" : "";
1764
+ const query = `DROP TABLE ${ifExists}${this.escapeIdentifier(tableName)}${cascade}`;
1765
+ try {
1766
+ await this.executeQuery(query);
1767
+ } catch (error) {
1768
+ throw this.handleQueryError(error, "dropTable", tableName);
1769
+ }
1770
+ }
1771
+ /**
1772
+ * Alter an existing table.
1773
+ *
1774
+ * @remarks
1775
+ * Default implementation is a placeholder. Subclasses should implement
1776
+ * table alteration logic.
1777
+ *
1778
+ * @param tableName - Name of table to alter
1779
+ * @param operations - Alteration operations
1780
+ * @param options - Alter options
1781
+ *
1782
+ * @throws {DatabaseError} If table alteration fails
1783
+ */
1784
+ async alterTable(tableName, operations, _options) {
1785
+ const quotedTable = this.escapeIdentifier(tableName);
1786
+ for (const op of operations) {
1787
+ let query;
1788
+ switch (op.kind) {
1789
+ case "add_column": {
1790
+ let colDef = `${this.escapeIdentifier(op.column.name)} ${op.column.type.toUpperCase()}`;
1791
+ if (op.column.nullable === false) colDef += " NOT NULL";
1792
+ if (op.column.unique) colDef += " UNIQUE";
1793
+ if (op.column.default !== void 0) {
1794
+ const defaultVal = typeof op.column.default === "object" && op.column.default !== null && "sql" in op.column.default ? op.column.default.sql : typeof op.column.default === "string" ? `'${op.column.default}'` : String(op.column.default);
1795
+ colDef += ` DEFAULT ${defaultVal}`;
1796
+ }
1797
+ query = `ALTER TABLE ${quotedTable} ADD COLUMN ${colDef}`;
1798
+ break;
1799
+ }
1800
+ case "drop_column": {
1801
+ const cascade = op.cascade ? " CASCADE" : "";
1802
+ query = `ALTER TABLE ${quotedTable} DROP COLUMN ${this.escapeIdentifier(op.columnName)}${cascade}`;
1803
+ break;
1804
+ }
1805
+ case "rename_column":
1806
+ query = `ALTER TABLE ${quotedTable} RENAME COLUMN ${this.escapeIdentifier(op.from)} TO ${this.escapeIdentifier(op.to)}`;
1807
+ break;
1808
+ case "modify_column":
1809
+ query = `ALTER TABLE ${quotedTable} ALTER COLUMN ${this.escapeIdentifier(op.column.name)} TYPE ${op.column.type.toUpperCase()}`;
1810
+ break;
1811
+ case "add_constraint": {
1812
+ const colList = (op.constraint.columns ?? []).map((c) => this.escapeIdentifier(c)).join(", ");
1813
+ if (op.constraint.type === "check" && op.constraint.expression) query = `ALTER TABLE ${quotedTable} ADD CONSTRAINT ${this.escapeIdentifier(op.constraint.name)} CHECK (${op.constraint.expression})`;
1814
+ else query = `ALTER TABLE ${quotedTable} ADD CONSTRAINT ${this.escapeIdentifier(op.constraint.name)} ${op.constraint.type.toUpperCase()} (${colList})`;
1815
+ break;
1816
+ }
1817
+ case "drop_constraint": {
1818
+ const cascadeConstraint = op.cascade ? " CASCADE" : "";
1819
+ query = `ALTER TABLE ${quotedTable} DROP CONSTRAINT ${this.escapeIdentifier(op.constraintName)}${cascadeConstraint}`;
1820
+ break;
1821
+ }
1822
+ default: throw this.createDatabaseError("query", `Unsupported alter table operation: ${op.kind}`, void 0);
1823
+ }
1824
+ try {
1825
+ await this.executeQuery(query);
1826
+ } catch (error) {
1827
+ throw this.handleQueryError(error, "alterTable", tableName);
1828
+ }
1829
+ }
1830
+ }
1831
+ /**
1832
+ * Check if a table exists in the database.
1833
+ *
1834
+ * @remarks
1835
+ * Uses dialect-specific information schema queries to check table existence.
1836
+ * This is useful for development mode auto-sync to determine whether to
1837
+ * CREATE or DROP/CREATE tables.
1838
+ *
1839
+ * @param tableName - Name of table to check
1840
+ * @param schema - Optional schema name (defaults to 'public' for PostgreSQL)
1841
+ * @returns True if table exists, false otherwise
1842
+ *
1843
+ * @throws {DatabaseError} If query fails
1844
+ *
1845
+ * @example
1846
+ * ```typescript
1847
+ * const exists = await adapter.tableExists('users');
1848
+ * if (exists) {
1849
+ * await adapter.dropTable('users');
1850
+ * }
1851
+ * await adapter.createTable(userTableDef);
1852
+ * ```
1853
+ */
1854
+ async tableExists(tableName, schema) {
1855
+ try {
1856
+ let sql;
1857
+ const params = [];
1858
+ switch (this.dialect) {
1859
+ case "postgresql":
1860
+ sql = `
1753
1861
  SELECT EXISTS (
1754
1862
  SELECT FROM information_schema.tables
1755
1863
  WHERE table_schema = COALESCE($1, (
@@ -1760,520 +1868,487 @@ var DrizzleAdapter = class {
1760
1868
  AND table_name = $2
1761
1869
  ) as exists
1762
1870
  `;
1763
- params.push(schema ?? null, tableName);
1764
- break;
1765
- case "mysql":
1766
- sql6 = `
1871
+ params.push(schema ?? null, tableName);
1872
+ break;
1873
+ case "mysql":
1874
+ sql = `
1767
1875
  SELECT COUNT(*) as count
1768
1876
  FROM information_schema.tables
1769
1877
  WHERE table_schema = DATABASE()
1770
1878
  AND table_name = ?
1771
1879
  `;
1772
- params.push(tableName);
1773
- break;
1774
- case "sqlite":
1775
- sql6 = `
1880
+ params.push(tableName);
1881
+ break;
1882
+ case "sqlite":
1883
+ sql = `
1776
1884
  SELECT COUNT(*) as count
1777
1885
  FROM sqlite_master
1778
1886
  WHERE type = 'table'
1779
1887
  AND name = ?
1780
1888
  `;
1781
- params.push(tableName);
1782
- break;
1783
- default:
1784
- throw this.createDatabaseError(
1785
- "query",
1786
- `tableExists not implemented for dialect: ${String(this.dialect)}`,
1787
- void 0
1788
- );
1789
- }
1790
- const results = await this.executeQuery(
1791
- sql6,
1792
- params
1793
- );
1794
- if (results.length === 0) {
1795
- return false;
1796
- }
1797
- const row = results[0];
1798
- if ("exists" in row) {
1799
- return row.exists === true || row.exists === "t" || row.exists === 1;
1800
- }
1801
- if ("count" in row) {
1802
- return Number(row.count) > 0;
1803
- }
1804
- return false;
1805
- } catch (error) {
1806
- throw this.handleQueryError(error, "tableExists", tableName);
1807
- }
1808
- }
1809
- /**
1810
- * Get list of all tables in the database.
1811
- *
1812
- * @remarks
1813
- * Uses dialect-specific information schema queries to list tables.
1814
- * Useful for detecting orphaned tables or validating schema state.
1815
- *
1816
- * @param schema - Optional schema name (defaults to 'public' for PostgreSQL)
1817
- * @returns Array of table names
1818
- *
1819
- * @throws {DatabaseError} If query fails
1820
- */
1821
- async listTables(schema) {
1822
- try {
1823
- let sql6;
1824
- const params = [];
1825
- switch (this.dialect) {
1826
- case "postgresql":
1827
- sql6 = `
1889
+ params.push(tableName);
1890
+ break;
1891
+ default: throw this.createDatabaseError("query", `tableExists not implemented for dialect: ${String(this.dialect)}`, void 0);
1892
+ }
1893
+ const results = await this.executeQuery(sql, params);
1894
+ if (results.length === 0) return false;
1895
+ const row = results[0];
1896
+ if ("exists" in row) return row.exists === true || row.exists === "t" || row.exists === 1;
1897
+ if ("count" in row) return Number(row.count) > 0;
1898
+ return false;
1899
+ } catch (error) {
1900
+ throw this.handleQueryError(error, "tableExists", tableName);
1901
+ }
1902
+ }
1903
+ /**
1904
+ * Get list of all tables in the database.
1905
+ *
1906
+ * @remarks
1907
+ * Uses dialect-specific information schema queries to list tables.
1908
+ * Useful for detecting orphaned tables or validating schema state.
1909
+ *
1910
+ * @param schema - Optional schema name (defaults to 'public' for PostgreSQL)
1911
+ * @returns Array of table names
1912
+ *
1913
+ * @throws {DatabaseError} If query fails
1914
+ */
1915
+ async listTables(schema) {
1916
+ try {
1917
+ let sql;
1918
+ const params = [];
1919
+ switch (this.dialect) {
1920
+ case "postgresql":
1921
+ sql = `
1828
1922
  SELECT table_name
1829
1923
  FROM information_schema.tables
1830
1924
  WHERE table_schema = $1
1831
1925
  AND table_type = 'BASE TABLE'
1832
1926
  ORDER BY table_name
1833
1927
  `;
1834
- params.push(schema ?? "public");
1835
- break;
1836
- case "mysql":
1837
- sql6 = `
1928
+ params.push(schema ?? "public");
1929
+ break;
1930
+ case "mysql":
1931
+ sql = `
1838
1932
  SELECT table_name AS table_name
1839
1933
  FROM information_schema.tables
1840
1934
  WHERE table_schema = DATABASE()
1841
1935
  AND table_type = 'BASE TABLE'
1842
1936
  ORDER BY table_name
1843
1937
  `;
1844
- break;
1845
- case "sqlite":
1846
- sql6 = `
1938
+ break;
1939
+ case "sqlite":
1940
+ sql = `
1847
1941
  SELECT name as table_name
1848
1942
  FROM sqlite_master
1849
1943
  WHERE type = 'table'
1850
1944
  AND name NOT LIKE 'sqlite_%'
1851
1945
  ORDER BY name
1852
1946
  `;
1853
- break;
1854
- default:
1855
- throw this.createDatabaseError(
1856
- "query",
1857
- `listTables not implemented for dialect: ${String(this.dialect)}`,
1858
- void 0
1859
- );
1860
- }
1861
- const results = await this.executeQuery(
1862
- sql6,
1863
- params
1864
- );
1865
- return results.map((row) => row.table_name);
1866
- } catch (error) {
1867
- throw this.handleQueryError(error, "listTables", "");
1868
- }
1869
- }
1870
- // ============================================================
1871
- // Protected Utilities
1872
- // ============================================================
1873
- // Old raw SQL query builders (buildSelectQuery, buildInsertQuery, buildUpdateQuery,
1874
- // buildDeleteQuery, buildUpsertQuery, buildWhereClause, buildPlaceholder,
1875
- // buildPlaceholders) have been removed. CRUD methods now use Drizzle query API
1876
- // via the TableResolver set during boot. See drizzle-where.ts for where clause
1877
- // translation.
1878
- // NOTE: The following content up to escapeIdentifier has been removed.
1879
- // If you're looking for the old query builder methods, see git history
1880
- // (commit before "refactor: remove dead SQL builder code").
1881
- /**
1882
- * Escape a table or column identifier.
1883
- *
1884
- * @remarks
1885
- * Default uses double quotes (SQL standard).
1886
- * MySQL adapter should override to use backticks.
1887
- *
1888
- * @param identifier - Identifier to escape
1889
- * @returns Escaped identifier
1890
- *
1891
- * @protected
1892
- */
1893
- escapeIdentifier(identifier) {
1894
- return `"${identifier.replace(/"/g, '""')}"`;
1895
- }
1896
- /**
1897
- * Create a DatabaseError with proper error kind classification.
1898
- *
1899
- * @remarks
1900
- * Protected helper for subclasses to create consistent errors.
1901
- * Subclasses can override to add dialect-specific error classification.
1902
- *
1903
- * @param kind - Error kind
1904
- * @param message - Error message
1905
- * @param cause - Original error
1906
- * @returns DatabaseError instance
1907
- *
1908
- * @protected
1909
- */
1910
- createDatabaseError(kind, message, cause) {
1911
- return createDatabaseError({
1912
- kind,
1913
- message,
1914
- cause
1915
- });
1916
- }
1917
- /**
1918
- * Helper to create transaction context CRUD forwarding methods.
1919
- *
1920
- * @param txDb - Thunk returning the transaction-bound Drizzle executor
1921
- * @returns Object providing forwarded CRUD and getDrizzle methods
1922
- *
1923
- * @protected
1924
- */
1925
- createTransactionForwarders(txDb) {
1926
- return createTransactionForwarders(this, txDb);
1927
- }
1928
- /**
1929
- * The Drizzle handles one transaction hands out; see
1930
- * `transactionDrizzleHandles`. `build` makes an instance on the
1931
- * transaction's own connection, with the relations config when given one.
1932
- *
1933
- * @protected
1934
- */
1935
- transactionDrizzleHandles(build) {
1936
- return transactionDrizzleHandles(build);
1937
- }
1938
- /**
1939
- * A transaction context's `update`, for each adapter to bind.
1940
- *
1941
- * Not forwarded to the pooled `update` the way `select` and `delete` are:
1942
- * that one goes through the Drizzle query builder, which writes the columns
1943
- * the runtime MODEL declares, and a transaction's update must reach the
1944
- * columns the physical TABLE has — the localization transition window
1945
- * writes a column the model has already moved to a companion table that
1946
- * does not exist yet. The INSERT half of every transaction context already
1947
- * builds its own statement for that reason; `update-statement.ts` is the
1948
- * UPDATE half, spelled once for the three adapters, and this runs it: on
1949
- * the transaction executor, classified with the operation and table named
1950
- * as the pooled `update` classifies its failures, and read back through
1951
- * `select` on the same executor when the caller asked for rows.
1952
- *
1953
- * That read is by the rows' IDENTITY where the dialect can report it: on
1954
- * PostgreSQL and SQLite the statement carries `RETURNING <primary key>`, and
1955
- * the read-back asks for exactly those rows — so a row another transaction
1956
- * adds under the same predicate meanwhile is not among them, and an update
1957
- * whose own write falsifies its predicate still reads its rows back. MySQL
1958
- * has no RETURNING, so there the read re-runs the predicate, as the pooled
1959
- * `update` always has on that dialect; a table with no primary key in its
1960
- * model reads back the same way.
1961
- *
1962
- * @param txDb - thunk returning the transaction-bound Drizzle instance
1963
- * @param run - how this dialect runs a statement on that instance, and the
1964
- * rows it returns when the statement carries RETURNING: better-sqlite3
1965
- * answers synchronously, the pooled drivers do not
1966
- * @param bindUnmodeled - how a value binds when the model declares no
1967
- * column for it; a declared column binds through its own encoder
1968
- * @returns the context's `update` method
1969
- */
1970
- transactionUpdate(txDb, run, bindUnmodeled) {
1971
- return async (table, data, where, options) => {
1972
- const wantsRows = this.updateReturnsRows(options?.returning);
1973
- const tableObj = this.resolvedTableObject(table);
1974
- const identity = wantsRows && this.getCapabilities().supportsReturning ? this.identityColumns(tableObj) : [];
1975
- let reported;
1976
- try {
1977
- reported = await run(
1978
- this.buildTransactionUpdate(tableObj, table, data, where, {
1979
- bindUnmodeled,
1980
- returning: identity.map((key) => drizzleOrm.sql`${drizzleOrm.sql.identifier(key.sqlName)}`)
1981
- }),
1982
- identity.length > 0
1983
- );
1984
- } catch (error) {
1985
- throw this.handleQueryError(error, "update", table);
1986
- }
1987
- if (!wantsRows) return [];
1988
- if (identity.length === 0 || !Array.isArray(reported)) {
1989
- return this.select(table, { where }, txDb());
1990
- }
1991
- const rows = [];
1992
- for (let at = 0; at < reported.length; at += IDENTITY_READ_BACK_CHUNK) {
1993
- rows.push(
1994
- ...await this.select(
1995
- table,
1996
- {
1997
- where: rowsByIdentity(
1998
- identity,
1999
- reported.slice(at, at + IDENTITY_READ_BACK_CHUNK)
2000
- )
2001
- },
2002
- txDb()
2003
- )
2004
- );
2005
- }
2006
- return rows;
2007
- };
2008
- }
2009
- /**
2010
- * The columns that identify a row of this table, under both spellings: the
2011
- * SQL name RETURNING reports and the Drizzle property name the query
2012
- * builder's WHERE is addressed by. Columns flagged primary first; failing
2013
- * that, the table-level primary key the dialect's table config carries
2014
- * (`primaryKey({ columns })` leaves each column's flag false), which each
2015
- * adapter reads through its own dialect. Empty when the model declares
2016
- * neither, and the read-back falls back to the caller's predicate.
2017
- */
2018
- identityColumns(tableObj) {
2019
- const columns = Object.entries(drizzleOrm.getColumns(tableObj));
2020
- const flagged = columns.filter(([, c]) => c.primary === true);
2021
- const members = flagged.length > 0 ? flagged : columns.filter(
2022
- ([, c]) => this.compositePrimaryKey(tableObj).includes(c)
2023
- );
2024
- return members.flatMap(
2025
- ([jsName, c]) => typeof c.name === "string" ? [{ jsName, sqlName: c.name }] : []
2026
- );
2027
- }
2028
- /**
2029
- * How a value binds to a column the model does not declare, on a driver
2030
- * that binds scalars, dates and binary natively: a structured value goes
2031
- * as JSON text, since what node-postgres and mysql2 make of a bare object
2032
- * or array is not JSON. For the adapters to hand `transactionUpdate`.
2033
- */
2034
- bindUnmodeledStructuredAsJson(value) {
2035
- return bindStructuredAsJson(value);
2036
- }
2037
- /**
2038
- * Refuse an upsert target every dialect would not answer alike: a conflict
2039
- * column named twice, which SQLite rejects as no constraint's target and
2040
- * MySQL would accept; and, when nothing is to be updated, a conflict column
2041
- * left for the database to fill, since the row that stands is then read
2042
- * back by a value the caller never gave. Shared with the dialects that
2043
- * override `upsert`, so the refusals are one rule.
2044
- */
2045
- assertUpsertTarget(table, data, options, keepExisting) {
2046
- const columns = options.conflictColumns;
2047
- if (new Set(columns).size !== columns.length) {
2048
- throw createDatabaseError({
2049
- kind: "query",
2050
- message: `upsert on ${table} names a conflict column more than once (${columns.join(", ")}).`
2051
- });
2052
- }
2053
- if (keepExisting && columns.some((column) => data[column] === void 0)) {
2054
- throw createDatabaseError({
2055
- kind: "query",
2056
- message: `upsert on ${table} needs a value for every conflict column (${columns.join(", ")}).`
2057
- });
2058
- }
2059
- }
2060
- /**
2061
- * The columns of a table-level `primaryKey({ columns })`, as the dialect's
2062
- * table config reports them. The base class cannot read that config without
2063
- * naming a dialect, so each adapter answers for its own; a dialect that does
2064
- * not answer reports none.
2065
- */
2066
- compositePrimaryKey(_tableObj) {
2067
- return [];
2068
- }
2069
- /** The registered table object, or the error every CRUD method throws without one. */
2070
- resolvedTableObject(table) {
2071
- const tableObj = this.getTableObject(table);
2072
- if (!tableObj || typeof tableObj !== "object") {
2073
- throw this.createDatabaseError(
2074
- "query",
2075
- `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`,
2076
- void 0
2077
- );
2078
- }
2079
- return tableObj;
2080
- }
2081
- /**
2082
- * The statement `transactionUpdate` runs.
2083
- *
2084
- * @throws when nothing would be written — every key `undefined`, or none at
2085
- * all. The query builder refused that too ("No values to set"), and a
2086
- * patch that names nothing is a caller's mistake rather than a write of
2087
- * nothing.
2088
- */
2089
- buildTransactionUpdate(tableObj, table, data, where, binding) {
2090
- const statement = buildUpdateStatement({
2091
- tableObj,
2092
- data,
2093
- where,
2094
- bindUnmodeled: binding.bindUnmodeled,
2095
- returning: binding.returning
2096
- });
2097
- if (!statement) {
2098
- throw this.createDatabaseError(
2099
- "query",
2100
- `No values to set: the update of "${table}" names no column with a defined value.`,
2101
- void 0
2102
- );
2103
- }
2104
- return statement;
2105
- }
2106
- /**
2107
- * Whether a transaction's `update` reads the rows it changed back: the
2108
- * caller named columns, or `*`. An empty list, like no option at all, asks
2109
- * for nothing and `update` answers `[]`, as it always has. The rows come
2110
- * from a read of the update's own WHERE on its own transaction, so they are
2111
- * the model's view of the row, decoded as every read is decoded — which is
2112
- * what the query-builder update returned for them, and why the named list
2113
- * selects nothing narrower.
2114
- */
2115
- updateReturnsRows(returning) {
2116
- return returning !== void 0 && !(Array.isArray(returning) && returning.length === 0);
2117
- }
2118
- /**
2119
- * The driver's own error inside `error`: the first in its `cause` chain that
2120
- * `isDriverError` recognises, or `error` itself when none does.
2121
- *
2122
- * A query that runs through Drizzle fails with Drizzle's `DrizzleQueryError`,
2123
- * whose message is the SQL statement and which carries no driver code; the
2124
- * driver's error, the only object that knows what actually went wrong, is
2125
- * its `cause`. A dialect's {@link classifyError} classifies from what this
2126
- * returns, so a failure reads the same whether or not Drizzle wrapped it.
2127
- *
2128
- * The walk is bounded, because an error chain can be made cyclic and a
2129
- * classifier that can hang is worse than one that gives up. Falling back to
2130
- * `error` keeps an error that carries no code anywhere classified as before.
2131
- *
2132
- * @param error - The error as it was caught
2133
- * @param isDriverError - Whether a link in the chain is the driver's error
2134
- * @returns The driver's error, or `error` when no link is
2135
- */
2136
- driverErrorIn(error, isDriverError) {
2137
- let cursor = error;
2138
- for (let depth = 0; depth < 10; depth++) {
2139
- if (cursor === null || typeof cursor !== "object") break;
2140
- const candidate = cursor;
2141
- if (isDriverError(candidate)) return candidate;
2142
- cursor = candidate.cause;
2143
- }
2144
- return error ?? {};
2145
- }
2146
- /**
2147
- * Classify an error into a DatabaseError.
2148
- *
2149
- * @remarks
2150
- * Subclasses can override this method to add dialect-specific error code classification.
2151
- *
2152
- * @param error - Original error
2153
- * @param sql - SQL statement that caused the error (optional)
2154
- * @returns DatabaseError instance
2155
- *
2156
- * @protected
2157
- */
2158
- classifyError(error, sql6) {
2159
- if (isDatabaseError(error)) {
2160
- return error;
2161
- }
2162
- const errorMessage = error instanceof Error ? error.message : String(error);
2163
- return this.createDatabaseError(
2164
- "query",
2165
- sql6 ? `Query failed: ${errorMessage}` : errorMessage,
2166
- error instanceof Error ? error : void 0
2167
- );
2168
- }
2169
- /**
2170
- * Handle query errors and convert to DatabaseError.
2171
- *
2172
- * @remarks
2173
- * Protected helper for consistent error handling across CRUD operations.
2174
- * Delegates to `classifyError` and attaches operation and table context if not present.
2175
- * Subclasses override `classifyError` to provide dialect-specific error code classification.
2176
- *
2177
- * @param error - Original error
2178
- * @param operation - Operation that failed
2179
- * @param table - Table name
2180
- * @returns DatabaseError instance
2181
- *
2182
- * @protected
2183
- */
2184
- handleQueryError(error, operation, table) {
2185
- const dbError = this.classifyError(error);
2186
- const context = `${operation} operation failed on table '${table}'`;
2187
- if (!dbError.message.includes(context)) {
2188
- dbError.message = `${context}: ${dbError.message}`;
2189
- }
2190
- if (!dbError.table) {
2191
- dbError.table = table;
2192
- }
2193
- return dbError;
2194
- }
1947
+ break;
1948
+ default: throw this.createDatabaseError("query", `listTables not implemented for dialect: ${String(this.dialect)}`, void 0);
1949
+ }
1950
+ return (await this.executeQuery(sql, params)).map((row) => row.table_name);
1951
+ } catch (error) {
1952
+ throw this.handleQueryError(error, "listTables", "");
1953
+ }
1954
+ }
1955
+ /**
1956
+ * Escape a table or column identifier.
1957
+ *
1958
+ * @remarks
1959
+ * Default uses double quotes (SQL standard).
1960
+ * MySQL adapter should override to use backticks.
1961
+ *
1962
+ * @param identifier - Identifier to escape
1963
+ * @returns Escaped identifier
1964
+ *
1965
+ * @protected
1966
+ */
1967
+ escapeIdentifier(identifier) {
1968
+ return `"${identifier.replace(/"/g, "\"\"")}"`;
1969
+ }
1970
+ /**
1971
+ * Create a DatabaseError with proper error kind classification.
1972
+ *
1973
+ * @remarks
1974
+ * Protected helper for subclasses to create consistent errors.
1975
+ * Subclasses can override to add dialect-specific error classification.
1976
+ *
1977
+ * @param kind - Error kind
1978
+ * @param message - Error message
1979
+ * @param cause - Original error
1980
+ * @returns DatabaseError instance
1981
+ *
1982
+ * @protected
1983
+ */
1984
+ createDatabaseError(kind, message, cause) {
1985
+ return require_types.createDatabaseError({
1986
+ kind,
1987
+ message,
1988
+ cause
1989
+ });
1990
+ }
1991
+ /**
1992
+ * Helper to create transaction context CRUD forwarding methods.
1993
+ *
1994
+ * @param txDb - Thunk returning the transaction-bound Drizzle executor
1995
+ * @returns Object providing forwarded CRUD and getDrizzle methods
1996
+ *
1997
+ * @protected
1998
+ */
1999
+ createTransactionForwarders(txDb) {
2000
+ return createTransactionForwarders(this, txDb);
2001
+ }
2002
+ /**
2003
+ * The Drizzle handles one transaction hands out; see
2004
+ * `transactionDrizzleHandles`. `build` makes an instance on the
2005
+ * transaction's own connection, with the relations config when given one.
2006
+ *
2007
+ * @protected
2008
+ */
2009
+ transactionDrizzleHandles(build) {
2010
+ return transactionDrizzleHandles(build);
2011
+ }
2012
+ /**
2013
+ * A transaction context's `update`, for each adapter to bind.
2014
+ *
2015
+ * Not forwarded to the pooled `update` the way `select` and `delete` are:
2016
+ * that one goes through the Drizzle query builder, which writes the columns
2017
+ * the runtime MODEL declares, and a transaction's update must reach the
2018
+ * columns the physical TABLE has — the localization transition window
2019
+ * writes a column the model has already moved to a companion table that
2020
+ * does not exist yet. The INSERT half of every transaction context already
2021
+ * builds its own statement for that reason; `update-statement.ts` is the
2022
+ * UPDATE half, spelled once for the three adapters, and this runs it: on
2023
+ * the transaction executor, classified with the operation and table named
2024
+ * as the pooled `update` classifies its failures, and read back through
2025
+ * `select` on the same executor when the caller asked for rows.
2026
+ *
2027
+ * That read is by the rows' IDENTITY where the dialect can report it: on
2028
+ * PostgreSQL and SQLite the statement carries `RETURNING <primary key>`, and
2029
+ * the read-back asks for exactly those rows — so a row another transaction
2030
+ * adds under the same predicate meanwhile is not among them, and an update
2031
+ * whose own write falsifies its predicate still reads its rows back. MySQL
2032
+ * has no RETURNING, so there the read re-runs the predicate, as the pooled
2033
+ * `update` always has on that dialect; a table with no primary key in its
2034
+ * model reads back the same way.
2035
+ *
2036
+ * @param txDb - thunk returning the transaction-bound Drizzle instance
2037
+ * @param run - how this dialect runs a statement on that instance, and the
2038
+ * rows it returns when the statement carries RETURNING: better-sqlite3
2039
+ * answers synchronously, the pooled drivers do not
2040
+ * @param bindUnmodeled - how a value binds when the model declares no
2041
+ * column for it; a declared column binds through its own encoder
2042
+ * @returns the context's `update` method
2043
+ */
2044
+ transactionUpdate(txDb, run, bindUnmodeled) {
2045
+ return async (table, data, where, options) => {
2046
+ const wantsRows = this.updateReturnsRows(options?.returning);
2047
+ const tableObj = this.resolvedTableObject(table);
2048
+ const identity = wantsRows && this.getCapabilities().supportsReturning ? this.identityColumns(tableObj) : [];
2049
+ let reported;
2050
+ try {
2051
+ reported = await run(this.buildTransactionUpdate(tableObj, table, data, where, {
2052
+ bindUnmodeled,
2053
+ returning: identity.map((key) => drizzle_orm.sql`${drizzle_orm.sql.identifier(key.sqlName)}`)
2054
+ }), identity.length > 0);
2055
+ } catch (error) {
2056
+ throw this.handleQueryError(error, "update", table);
2057
+ }
2058
+ if (!wantsRows) return [];
2059
+ if (identity.length === 0 || !Array.isArray(reported)) return this.select(table, { where }, txDb());
2060
+ const rows = [];
2061
+ for (let at = 0; at < reported.length; at += IDENTITY_READ_BACK_CHUNK) rows.push(...await this.select(table, { where: rowsByIdentity(identity, reported.slice(at, at + IDENTITY_READ_BACK_CHUNK)) }, txDb()));
2062
+ return rows;
2063
+ };
2064
+ }
2065
+ /**
2066
+ * The columns that identify a row of this table, under both spellings: the
2067
+ * SQL name RETURNING reports and the Drizzle property name the query
2068
+ * builder's WHERE is addressed by. Columns flagged primary first; failing
2069
+ * that, the table-level primary key the dialect's table config carries
2070
+ * (`primaryKey({ columns })` leaves each column's flag false), which each
2071
+ * adapter reads through its own dialect. Empty when the model declares
2072
+ * neither, and the read-back falls back to the caller's predicate.
2073
+ */
2074
+ identityColumns(tableObj) {
2075
+ const columns = Object.entries((0, drizzle_orm.getColumns)(tableObj));
2076
+ const flagged = columns.filter(([, c]) => c.primary === true);
2077
+ return (flagged.length > 0 ? flagged : columns.filter(([, c]) => this.compositePrimaryKey(tableObj).includes(c))).flatMap(([jsName, c]) => typeof c.name === "string" ? [{
2078
+ jsName,
2079
+ sqlName: c.name
2080
+ }] : []);
2081
+ }
2082
+ /**
2083
+ * How a value binds to a column the model does not declare, on a driver
2084
+ * that binds scalars, dates and binary natively: a structured value goes
2085
+ * as JSON text, since what node-postgres and mysql2 make of a bare object
2086
+ * or array is not JSON. For the adapters to hand `transactionUpdate`.
2087
+ */
2088
+ bindUnmodeledStructuredAsJson(value) {
2089
+ return bindStructuredAsJson(value);
2090
+ }
2091
+ /**
2092
+ * Refuse an upsert target every dialect would not answer alike: a conflict
2093
+ * column named twice, which SQLite rejects as no constraint's target and
2094
+ * MySQL would accept; and, when nothing is to be updated, a conflict column
2095
+ * left for the database to fill, since the row that stands is then read
2096
+ * back by a value the caller never gave. Shared with the dialects that
2097
+ * override `upsert`, so the refusals are one rule.
2098
+ */
2099
+ assertUpsertTarget(table, data, options, keepExisting) {
2100
+ const columns = options.conflictColumns;
2101
+ if (new Set(columns).size !== columns.length) throw require_types.createDatabaseError({
2102
+ kind: "query",
2103
+ message: `upsert on ${table} names a conflict column more than once (${columns.join(", ")}).`
2104
+ });
2105
+ if (keepExisting && columns.some((column) => data[column] === void 0)) throw require_types.createDatabaseError({
2106
+ kind: "query",
2107
+ message: `upsert on ${table} needs a value for every conflict column (${columns.join(", ")}).`
2108
+ });
2109
+ }
2110
+ /**
2111
+ * The columns of a table-level `primaryKey({ columns })`, as the dialect's
2112
+ * table config reports them. The base class cannot read that config without
2113
+ * naming a dialect, so each adapter answers for its own; a dialect that does
2114
+ * not answer reports none.
2115
+ */
2116
+ compositePrimaryKey(_tableObj) {
2117
+ return [];
2118
+ }
2119
+ /** The registered table object, or the error every CRUD method throws without one. */
2120
+ resolvedTableObject(table) {
2121
+ const tableObj = this.getTableObject(table);
2122
+ if (!tableObj || typeof tableObj !== "object") throw this.createDatabaseError("query", `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`, void 0);
2123
+ return tableObj;
2124
+ }
2125
+ /**
2126
+ * The statement `transactionUpdate` runs.
2127
+ *
2128
+ * @throws when nothing would be written — every key `undefined`, or none at
2129
+ * all. The query builder refused that too ("No values to set"), and a
2130
+ * patch that names nothing is a caller's mistake rather than a write of
2131
+ * nothing.
2132
+ */
2133
+ buildTransactionUpdate(tableObj, table, data, where, binding) {
2134
+ const statement = buildUpdateStatement({
2135
+ table,
2136
+ tableObj,
2137
+ data,
2138
+ where,
2139
+ bindUnmodeled: binding.bindUnmodeled,
2140
+ returning: binding.returning
2141
+ });
2142
+ if (!statement) throw this.createDatabaseError("query", `No values to set: the update of "${table}" names no column with a defined value.`, void 0);
2143
+ return statement;
2144
+ }
2145
+ /**
2146
+ * Whether a transaction's `update` reads the rows it changed back: the
2147
+ * caller named columns, or `*`. An empty list, like no option at all, asks
2148
+ * for nothing and `update` answers `[]`, as it always has. The rows come
2149
+ * from a read of the update's own WHERE on its own transaction, so they are
2150
+ * the model's view of the row, decoded as every read is decoded — which is
2151
+ * what the query-builder update returned for them, and why the named list
2152
+ * selects nothing narrower.
2153
+ */
2154
+ updateReturnsRows(returning) {
2155
+ return returning !== void 0 && !(Array.isArray(returning) && returning.length === 0);
2156
+ }
2157
+ /**
2158
+ * The driver's own error inside `error`: the first in its `cause` chain that
2159
+ * `isDriverError` recognises, or `error` itself when none does.
2160
+ *
2161
+ * A query that runs through Drizzle fails with Drizzle's `DrizzleQueryError`,
2162
+ * whose message is the SQL statement and which carries no driver code; the
2163
+ * driver's error, the only object that knows what actually went wrong, is
2164
+ * its `cause`. A dialect's {@link classifyError} classifies from what this
2165
+ * returns, so a failure reads the same whether or not Drizzle wrapped it.
2166
+ *
2167
+ * The walk is bounded, because an error chain can be made cyclic and a
2168
+ * classifier that can hang is worse than one that gives up. Falling back to
2169
+ * `error` keeps an error that carries no code anywhere classified as before.
2170
+ *
2171
+ * @param error - The error as it was caught
2172
+ * @param isDriverError - Whether a link in the chain is the driver's error
2173
+ * @returns The driver's error, or `error` when no link is
2174
+ */
2175
+ driverErrorIn(error, isDriverError) {
2176
+ let cursor = error;
2177
+ for (let depth = 0; depth < 10; depth++) {
2178
+ if (cursor === null || typeof cursor !== "object") break;
2179
+ const candidate = cursor;
2180
+ if (isDriverError(candidate)) return candidate;
2181
+ cursor = candidate.cause;
2182
+ }
2183
+ return error ?? {};
2184
+ }
2185
+ /**
2186
+ * Classify an error into a DatabaseError.
2187
+ *
2188
+ * @remarks
2189
+ * Subclasses can override this method to add dialect-specific error code classification.
2190
+ *
2191
+ * @param error - Original error
2192
+ * @param sql - SQL statement that caused the error (optional)
2193
+ * @returns DatabaseError instance
2194
+ *
2195
+ * @protected
2196
+ */
2197
+ classifyError(error, sql) {
2198
+ if (require_types.isDatabaseError(error)) return error;
2199
+ const errorMessage = error instanceof Error ? error.message : String(error);
2200
+ return this.createDatabaseError("query", sql ? `Query failed: ${errorMessage}` : errorMessage, error instanceof Error ? error : void 0);
2201
+ }
2202
+ /**
2203
+ * Handle query errors and convert to DatabaseError.
2204
+ *
2205
+ * @remarks
2206
+ * Protected helper for consistent error handling across CRUD operations.
2207
+ * Delegates to `classifyError` and attaches operation and table context if not present.
2208
+ * Subclasses override `classifyError` to provide dialect-specific error code classification.
2209
+ *
2210
+ * @param error - Original error
2211
+ * @param operation - Operation that failed
2212
+ * @param table - Table name
2213
+ * @returns DatabaseError instance
2214
+ *
2215
+ * @protected
2216
+ */
2217
+ handleQueryError(error, operation, table) {
2218
+ const dbError = this.classifyError(error);
2219
+ const context = `${operation} operation failed on table '${table}'`;
2220
+ if (!dbError.message.includes(context)) dbError.message = `${context}: ${dbError.message}`;
2221
+ if (!dbError.table) dbError.table = table;
2222
+ return dbError;
2223
+ }
2195
2224
  };
2196
- var IDENTITY_READ_BACK_CHUNK = 200;
2225
+ /**
2226
+ * How many reported rows one read-back statement names. Well under SQLite's
2227
+ * expression-depth limit of a thousand and its default variable limit, and
2228
+ * under PostgreSQL's parameter limit, with a composite key of several columns.
2229
+ */
2230
+ const IDENTITY_READ_BACK_CHUNK = 200;
2231
+ /**
2232
+ * The predicate that names exactly the rows a statement reported, addressed
2233
+ * by the Drizzle property names the where builder resolves: for a single-
2234
+ * column key one `IN` over the reported values; for a composite key one `and`
2235
+ * of every key column per row, `or`-ed together, so the key is matched as a
2236
+ * whole rather than column by column.
2237
+ */
2197
2238
  function rowsByIdentity(identity, rows) {
2198
- if (identity.length === 1) {
2199
- const [key] = identity;
2200
- return {
2201
- and: [
2202
- {
2203
- column: key.jsName,
2204
- op: "IN",
2205
- value: rows.map((row) => row[key.sqlName])
2206
- }
2207
- ]
2208
- };
2209
- }
2210
- return {
2211
- or: rows.map(
2212
- (row) => ({
2213
- and: identity.map(
2214
- (key) => ({
2215
- column: key.jsName,
2216
- op: "=",
2217
- value: row[key.sqlName]
2218
- })
2219
- )
2220
- })
2221
- )
2222
- };
2239
+ if (identity.length === 1) {
2240
+ const [key] = identity;
2241
+ return { and: [{
2242
+ column: key.jsName,
2243
+ op: "IN",
2244
+ value: rows.map((row) => row[key.sqlName])
2245
+ }] };
2246
+ }
2247
+ return { or: rows.map((row) => ({ and: identity.map((key) => ({
2248
+ column: key.jsName,
2249
+ op: "=",
2250
+ value: row[key.sqlName]
2251
+ })) })) };
2223
2252
  }
2224
- var ALLOWED_DEFAULT_SQL_EXPRESSIONS = /* @__PURE__ */ new Set([
2225
- "current_timestamp",
2226
- "now()",
2227
- "gen_random_uuid()",
2228
- "uuid_generate_v4()",
2229
- "current_date",
2230
- "current_time"
2253
+ /**
2254
+ * Render a `ColumnDefinition.default` as a SQL fragment
2255
+ * for embedding in a `DEFAULT` clause. DDL is not protected by Drizzle's
2256
+ * prepared statements — these expressions are concatenated into raw SQL —
2257
+ * so we treat the default as untrusted input even though it currently
2258
+ * flows from server-side schema definitions: an admin-defined dynamic
2259
+ * collection would otherwise turn into RCE-on-DB instead of just CMS
2260
+ * admin.
2261
+ *
2262
+ * Rules:
2263
+ *
2264
+ * - **null / number / boolean** → rendered as a SQL literal.
2265
+ * - **string** → quoted; embedded `'` doubled; `;`, `\`, control
2266
+ * characters, and null bytes rejected so they can't break out of
2267
+ * the surrounding quote pair into adjacent DDL.
2268
+ * - **`{ sql: "…" }`** → only the explicit allowlist of named
2269
+ * functions below is honored (case-insensitive). Anything else is
2270
+ * a hard error: the previous code embedded the raw `sql` string
2271
+ * verbatim, which is the SQL injection primitive the audit flagged.
2272
+ */
2273
+ const ALLOWED_DEFAULT_SQL_EXPRESSIONS = /* @__PURE__ */ new Set([
2274
+ "current_timestamp",
2275
+ "now()",
2276
+ "gen_random_uuid()",
2277
+ "uuid_generate_v4()",
2278
+ "current_date",
2279
+ "current_time"
2231
2280
  ]);
2232
2281
  function renderDefaultValue(value, columnName) {
2233
- if (value === null) return "NULL";
2234
- if (typeof value === "number") {
2235
- if (!Number.isFinite(value)) {
2236
- throw new Error(
2237
- `DEFAULT for column "${columnName}" must be a finite number (got ${value}).`
2238
- );
2239
- }
2240
- return String(value);
2241
- }
2242
- if (typeof value === "boolean") {
2243
- return value ? "TRUE" : "FALSE";
2244
- }
2245
- if (typeof value === "string") {
2246
- if (/[;\\\n\r\0]/.test(value)) {
2247
- throw new Error(
2248
- `DEFAULT string for column "${columnName}" contains characters that are not allowed in DDL (no semicolons, backslashes, control chars, or null bytes).`
2249
- );
2250
- }
2251
- return `'${value.replace(/'/g, "''")}'`;
2252
- }
2253
- if (typeof value === "object" && value !== null && "sql" in value) {
2254
- const raw = value.sql;
2255
- if (typeof raw !== "string") {
2256
- throw new Error(
2257
- `DEFAULT for column "${columnName}" has a { sql } expression that is not a string.`
2258
- );
2259
- }
2260
- const normalized = raw.trim().toLowerCase();
2261
- if (!ALLOWED_DEFAULT_SQL_EXPRESSIONS.has(normalized)) {
2262
- throw new Error(
2263
- `DEFAULT for column "${columnName}" uses a SQL expression "${raw}" that is not on the allowlist. Allowed: ${[...ALLOWED_DEFAULT_SQL_EXPRESSIONS].join(", ")}.`
2264
- );
2265
- }
2266
- return raw.trim();
2267
- }
2268
- throw new Error(
2269
- `DEFAULT for column "${columnName}" has an unsupported type (${typeof value}).`
2270
- );
2282
+ if (value === null) return "NULL";
2283
+ if (typeof value === "number") {
2284
+ if (!Number.isFinite(value)) throw new Error(`DEFAULT for column "${columnName}" must be a finite number (got ${value}).`);
2285
+ return String(value);
2286
+ }
2287
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
2288
+ if (typeof value === "string") {
2289
+ if (/[;\\\n\r\0]/.test(value)) throw new Error(`DEFAULT string for column "${columnName}" contains characters that are not allowed in DDL (no semicolons, backslashes, control chars, or null bytes).`);
2290
+ return `'${value.replace(/'/g, "''")}'`;
2291
+ }
2292
+ if (typeof value === "object" && value !== null && "sql" in value) {
2293
+ const raw = value.sql;
2294
+ if (typeof raw !== "string") throw new Error(`DEFAULT for column "${columnName}" has a { sql } expression that is not a string.`);
2295
+ const normalized = raw.trim().toLowerCase();
2296
+ if (!ALLOWED_DEFAULT_SQL_EXPRESSIONS.has(normalized)) throw new Error(`DEFAULT for column "${columnName}" uses a SQL expression "${raw}" that is not on the allowlist. Allowed: ${[...ALLOWED_DEFAULT_SQL_EXPRESSIONS].join(", ")}.`);
2297
+ return raw.trim();
2298
+ }
2299
+ throw new Error(`DEFAULT for column "${columnName}" has an unsupported type (${typeof value}).`);
2271
2300
  }
2272
-
2273
2301
  // src/index.ts
2274
- var version = "0.1.0";
2275
-
2302
+ /**
2303
+ * @nextly/adapter-drizzle
2304
+ *
2305
+ * Shared Drizzle ORM adapter logic for Nextly database adapters.
2306
+ * This package provides the base adapter class and utilities used by
2307
+ * dialect-specific adapters (PostgreSQL, MySQL, SQLite).
2308
+ *
2309
+ * @remarks
2310
+ * This is the main entry point that exports only the core adapter class.
2311
+ * Specialized utilities are available via subpath exports for optimal tree-shaking.
2312
+ *
2313
+ * ## Subpath Exports
2314
+ *
2315
+ * **Types:**
2316
+ * ```typescript
2317
+ * import type {
2318
+ * DatabaseCapabilities,
2319
+ * TransactionContext,
2320
+ * WhereClause,
2321
+ * SelectOptions,
2322
+ * } from '@nextly/adapter-drizzle/types';
2323
+ * ```
2324
+ *
2325
+ * **Query Builder:**
2326
+ * ```typescript
2327
+ * import { QueryBuilder } from '@nextly/adapter-drizzle/query-builder';
2328
+ * ```
2329
+ *
2330
+ * **Migrations:**
2331
+ * ```typescript
2332
+ * import {
2333
+ * calculateChecksum,
2334
+ * sortMigrations,
2335
+ * migrationHelpers,
2336
+ * } from '@nextly/adapter-drizzle/migrations';
2337
+ * ```
2338
+ *
2339
+ * **Schema Utilities:**
2340
+ * ```typescript
2341
+ * import { schemaUtils } from '@nextly/adapter-drizzle/schema';
2342
+ * ```
2343
+ *
2344
+ * @packageDocumentation
2345
+ */
2346
+ /**
2347
+ * Package version
2348
+ * @public
2349
+ */
2350
+ const version = "0.1.0";
2276
2351
  exports.DrizzleAdapter = DrizzleAdapter;
2277
2352
  exports.version = version;
2278
- //# sourceMappingURL=index.cjs.map
2353
+
2279
2354
  //# sourceMappingURL=index.cjs.map