@nextlyhq/adapter-drizzle 0.0.2-alpha.6 → 0.0.2-alpha.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -3,8 +3,19 @@
3
3
  var drizzleOrm = require('drizzle-orm');
4
4
 
5
5
  // src/adapter.ts
6
+ function buildDrizzleOrderBy(columns, orderBy) {
7
+ if (!orderBy?.length) return [];
8
+ return orderBy.flatMap((spec) => {
9
+ const col = columns[spec.column];
10
+ if (!col) return [];
11
+ const direction = spec.direction === "desc" ? drizzleOrm.desc(col) : drizzleOrm.asc(col);
12
+ if (spec.nulls === void 0) return [direction];
13
+ const isNull2 = drizzleOrm.sql`${col} is null`;
14
+ return [spec.nulls === "last" ? drizzleOrm.asc(isNull2) : drizzleOrm.desc(isNull2), direction];
15
+ });
16
+ }
6
17
  function buildDrizzleWhere(table, where) {
7
- const columns = drizzleOrm.getTableColumns(table);
18
+ const columns = drizzleOrm.getColumns(table);
8
19
  return processWhereClause(columns, where);
9
20
  }
10
21
  function processWhereClause(columns, where) {
@@ -38,13 +49,26 @@ function processWhereClause(columns, where) {
38
49
  parts.push(drizzleOrm.not(notCondition));
39
50
  }
40
51
  }
41
- if (parts.length === 0) return void 0;
52
+ if (parts.length === 0) {
53
+ if (namesABranch(where)) {
54
+ throw new Error(
55
+ `Where clause produced no condition: ${JSON.stringify(where)}. Every branch resolved to nothing, which would match every row. Pass {} to mean "no filter".`
56
+ );
57
+ }
58
+ return void 0;
59
+ }
42
60
  if (parts.length === 1) return parts[0];
43
61
  return drizzleOrm.and(...parts);
44
62
  }
45
63
  function isWhereCondition(item) {
46
64
  return "column" in item && "op" in item;
47
65
  }
66
+ function namesABranch(where) {
67
+ return Boolean(where.and?.length || where.or?.length || where.not);
68
+ }
69
+ function containsPattern(value) {
70
+ return `%${value.replace(/[!%_]/g, (character) => `!${character}`)}%`;
71
+ }
48
72
  function buildCondition(columns, cond) {
49
73
  const column = columns[cond.column];
50
74
  if (!column) {
@@ -83,12 +107,37 @@ function buildCondition(columns, cond) {
83
107
  case "NOT BETWEEN":
84
108
  return drizzleOrm.notBetween(col, cond.value, cond.valueTo);
85
109
  case "CONTAINS":
86
- return drizzleOrm.like(col, `%${String(cond.value)}%`);
110
+ return drizzleOrm.sql`${col} like ${containsPattern(String(cond.value))} escape '!'`;
87
111
  default:
88
112
  throw new Error(`Unsupported operator: ${cond.op}`);
89
113
  }
90
114
  }
91
115
 
116
+ // src/transaction-forwarders.ts
117
+ function createTransactionForwarders(delegator, txDb) {
118
+ return {
119
+ select: async (table, options) => {
120
+ return delegator.select(table, options, txDb());
121
+ },
122
+ selectOne: async (table, options) => {
123
+ return delegator.selectOne(table, options, txDb());
124
+ },
125
+ update: async (table, data, where, options) => {
126
+ return delegator.update(table, data, where, options, txDb());
127
+ },
128
+ updateCount: async (table, data, where) => {
129
+ return delegator.updateCount(table, data, where, txDb());
130
+ },
131
+ delete: async (table, where, options) => {
132
+ return delegator.delete(table, where, options, txDb());
133
+ },
134
+ upsert: async (table, data, options) => {
135
+ return delegator.upsert(table, data, options, txDb());
136
+ },
137
+ getDrizzle: () => txDb()
138
+ };
139
+ }
140
+
92
141
  // src/types/error.ts
93
142
  function isDatabaseError(error) {
94
143
  return typeof error === "object" && error !== null && "kind" in error && typeof error.kind === "string";
@@ -108,6 +157,51 @@ function createDatabaseError(options) {
108
157
  }
109
158
 
110
159
  // src/adapter.ts
160
+ var DATE_DATA_TYPE = /(?:^|\s)date$/;
161
+ var WALL_CLOCK_ALIAS_STEM = "__nx_wc";
162
+ var WALL_CLOCK_PATTERN = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?$/;
163
+ function parseWallClockAsUtc(text) {
164
+ const match = WALL_CLOCK_PATTERN.exec(text.trim());
165
+ if (!match) return void 0;
166
+ const [, year, month, day, hour, minute, second, fraction = ""] = match;
167
+ const millis = Number(`${fraction}000`.slice(0, 3));
168
+ const parsed = new Date(
169
+ Date.UTC(
170
+ Number(year),
171
+ Number(month) - 1,
172
+ Number(day),
173
+ Number(hour),
174
+ Number(minute),
175
+ Number(second),
176
+ millis
177
+ )
178
+ );
179
+ if (Number(year) < 100) parsed.setUTCFullYear(Number(year));
180
+ return parsed;
181
+ }
182
+ function isDateColumn(colDef) {
183
+ if (!colDef || typeof colDef !== "object") return false;
184
+ const candidate = colDef;
185
+ return typeof candidate.mapFromDriverValue === "function" && typeof candidate.mapToDriverValue === "function" && DATE_DATA_TYPE.test(String(candidate.dataType));
186
+ }
187
+ function carriesTimeZone(colDef) {
188
+ return colDef.withTimezone === true;
189
+ }
190
+ function affectedRowCount(result) {
191
+ if (Array.isArray(result)) {
192
+ const header = result[0];
193
+ if (header && typeof header.affectedRows === "number") {
194
+ return header.affectedRows;
195
+ }
196
+ return result.length;
197
+ }
198
+ const record = result;
199
+ const rowCount = record?.rowCount;
200
+ if (typeof rowCount === "number") return rowCount;
201
+ const changes = record?.changes;
202
+ if (typeof changes === "number") return changes;
203
+ return 0;
204
+ }
111
205
  var DrizzleAdapter = class {
112
206
  // ============================================================
113
207
  // Drizzle Query API Support
@@ -136,6 +230,45 @@ var DrizzleAdapter = class {
136
230
  getTableObject(tableName) {
137
231
  return this.tableResolver?.getTable(tableName) ?? null;
138
232
  }
233
+ /**
234
+ * Run a Drizzle-built statement and return its rows.
235
+ *
236
+ * @remarks
237
+ * The CRUD methods resolve their table through the schema registry and reject
238
+ * any name it does not declare, which leaves no way to read from a table the
239
+ * ORM does not know — one mid-rename, above all — except by assembling SQL and
240
+ * quoting identifiers by hand. This takes Drizzle's `sql` template instead, so
241
+ * the dialect in use decides the quoting and the parameter binding.
242
+ *
243
+ * Concrete rather than abstract so existing adapters keep working unchanged.
244
+ * The three drivers disagree about both the call and the result: node-postgres
245
+ * returns `{ rows }`, mysql2 a `[rows, fields]` tuple, and better-sqlite3 has
246
+ * no `execute` at all and answers `all`. Keeping that here rather than at each
247
+ * call site is the point — a caller reasoning about it would be reasoning
248
+ * about a driver it cannot see.
249
+ *
250
+ * @param statement - Drizzle `sql` template to run
251
+ * @returns Rows the statement produced
252
+ */
253
+ async queryStatement(statement) {
254
+ if (this.getCapabilities().dialect === "sqlite") {
255
+ const db2 = this.getDrizzle();
256
+ return await db2.all(statement);
257
+ }
258
+ const db = this.getDrizzle();
259
+ const result = await db.execute(statement);
260
+ if (Array.isArray(result)) {
261
+ return Array.isArray(result[0]) ? result[0] : result;
262
+ }
263
+ if (typeof result === "object" && result !== null && Array.isArray(result.rows)) {
264
+ return result.rows;
265
+ }
266
+ throw this.createDatabaseError(
267
+ "query",
268
+ "Drizzle statement returned a result shape this adapter does not recognise; refusing to report it as an empty result.",
269
+ void 0
270
+ );
271
+ }
139
272
  /**
140
273
  * Map data keys from SQL column names (snake_case) to Drizzle JS property names (camelCase).
141
274
  * Drizzle schemas define columns as e.g. `createdAt: timestamp("created_at")` — the JS
@@ -175,6 +308,281 @@ var DrizzleAdapter = class {
175
308
  }
176
309
  return mapped;
177
310
  }
311
+ /**
312
+ * Map data keys from Drizzle JS property names to SQL column names for the
313
+ * raw-SQL transaction insert path. The transaction context builds INSERT
314
+ * statements from Object.keys(data) used directly as column identifiers, so a
315
+ * table whose Drizzle property names differ from its SQL column names
316
+ * (camelCase core tables like nextly_versions) needs its keys translated
317
+ * first. For tables whose property names already equal their column names
318
+ * (the dynamic dc_/single_/comp_ tables) every lookup is identity, so
319
+ * existing callers are unaffected.
320
+ */
321
+ mapKeysToSqlColumns(tableObj, data) {
322
+ if (!tableObj || typeof tableObj !== "object") return data;
323
+ const jsToSql = /* @__PURE__ */ new Map();
324
+ for (const [jsName, colDef] of Object.entries(
325
+ tableObj
326
+ )) {
327
+ if (colDef && typeof colDef === "object" && "name" in colDef && typeof colDef.name === "string") {
328
+ jsToSql.set(jsName, colDef.name);
329
+ }
330
+ }
331
+ if (jsToSql.size === 0) return data;
332
+ const out = {};
333
+ for (const [key, value] of Object.entries(data)) {
334
+ out[jsToSql.get(key) ?? key] = value;
335
+ }
336
+ return out;
337
+ }
338
+ /**
339
+ * Map a list of column identifiers (Drizzle property names) to their SQL
340
+ * column names, for the raw-SQL transaction insert paths that build a
341
+ * RETURNING clause from `options.returning`. Same identity behavior as
342
+ * `mapKeysToSqlColumns`: names that are already SQL columns (the dynamic
343
+ * dc_/single_/comp_ tables) pass through unchanged.
344
+ */
345
+ mapColumnNamesToSql(tableObj, names) {
346
+ if (!tableObj || typeof tableObj !== "object") return names;
347
+ const jsToSql = /* @__PURE__ */ new Map();
348
+ for (const [jsName, colDef] of Object.entries(
349
+ tableObj
350
+ )) {
351
+ if (colDef && typeof colDef === "object" && "name" in colDef && typeof colDef.name === "string") {
352
+ jsToSql.set(jsName, colDef.name);
353
+ }
354
+ }
355
+ if (jsToSql.size === 0) return names;
356
+ return names.map((n) => jsToSql.get(n) ?? n);
357
+ }
358
+ /**
359
+ * Remap a raw-SQL result row's KEYS from SQL column names to Drizzle property
360
+ * names, so the raw-SQL transaction insert paths return the same key casing
361
+ * as the non-transactional (Drizzle) insert. Keys only - values are left
362
+ * untouched, so this does not change how JSON/date columns are decoded. For
363
+ * tables whose property names already equal their SQL columns (the dynamic
364
+ * dc_/single_/comp_ tables) every lookup is identity, so existing callers see
365
+ * no change.
366
+ */
367
+ mapRowKeysToJs(tableObj, row) {
368
+ if (!tableObj || typeof tableObj !== "object" || !row || typeof row !== "object") {
369
+ return row;
370
+ }
371
+ const sqlToJs = /* @__PURE__ */ new Map();
372
+ for (const [jsName, colDef] of Object.entries(
373
+ tableObj
374
+ )) {
375
+ if (colDef && typeof colDef === "object" && "name" in colDef && typeof colDef.name === "string") {
376
+ sqlToJs.set(colDef.name, jsName);
377
+ }
378
+ }
379
+ if (sqlToJs.size === 0) return row;
380
+ const out = {};
381
+ for (const [key, value] of Object.entries(row)) {
382
+ out[sqlToJs.get(key) ?? key] = value;
383
+ }
384
+ return out;
385
+ }
386
+ /**
387
+ * Decode a raw-SQL result row's DATE column values the way a Drizzle query
388
+ * would, so a write answers with the same representation a read of the same
389
+ * column gives.
390
+ *
391
+ * A Drizzle query path runs every value through its column definition; a
392
+ * raw-SQL path does not, so a dialect that stores a timestamp as a number
393
+ * answers a write with that number while every read answers a `Date`. Only
394
+ * date columns are touched, and a value that is already a `Date` is left
395
+ * alone, so a driver that decodes on its own is unaffected.
396
+ *
397
+ * Runs after {@link mapRowKeysToJs}: keys are Drizzle property names by then,
398
+ * which is what the table definition is keyed by.
399
+ */
400
+ mapDateValuesFromDriver(tableObj, row) {
401
+ if (!tableObj || typeof tableObj !== "object" || !row || typeof row !== "object") {
402
+ return row;
403
+ }
404
+ let decoded;
405
+ for (const [jsName, colDef] of Object.entries(
406
+ tableObj
407
+ )) {
408
+ if (!isDateColumn(colDef)) continue;
409
+ const value = row[jsName];
410
+ if (value == null || value instanceof Date) continue;
411
+ decoded ??= { ...row };
412
+ decoded[jsName] = colDef.mapFromDriverValue(value);
413
+ }
414
+ return decoded ?? row;
415
+ }
416
+ /**
417
+ * Encode a row's DATE values the way a Drizzle query would before binding
418
+ * them, so what lands in the column does not depend on the server's timezone.
419
+ *
420
+ * A column declared without a time zone stores a wall clock and records
421
+ * nothing about which zone it belongs to, so both ends have to agree. Drizzle
422
+ * writes UTC and reads UTC. A driver handed a `Date` writes the LOCAL wall
423
+ * clock instead, and the same row then reads back shifted by the offset --
424
+ * on a UTC server the two agree and nothing is visibly wrong, which is why
425
+ * this survives CI.
426
+ *
427
+ * Only date columns are touched. A value that is not a `Date` is left as it
428
+ * is, so a caller that already encoded one is not encoded twice.
429
+ *
430
+ * Keys may be spelled either way at this point, so both the Drizzle property
431
+ * name and the SQL column name resolve.
432
+ */
433
+ mapDateValuesToDriver(tableObj, data) {
434
+ if (!tableObj || typeof tableObj !== "object" || !data) return data;
435
+ const dateColumns = /* @__PURE__ */ new Map();
436
+ for (const [jsName, colDef] of Object.entries(
437
+ tableObj
438
+ )) {
439
+ if (!isDateColumn(colDef)) continue;
440
+ dateColumns.set(jsName, colDef);
441
+ const sqlName = colDef.name;
442
+ if (typeof sqlName === "string") dateColumns.set(sqlName, colDef);
443
+ }
444
+ if (dateColumns.size === 0) return data;
445
+ let encoded;
446
+ for (const [key, value] of Object.entries(data)) {
447
+ if (!(value instanceof Date)) continue;
448
+ const column = dateColumns.get(key);
449
+ if (!column) continue;
450
+ encoded ??= { ...data };
451
+ encoded[key] = column.mapToDriverValue(value);
452
+ }
453
+ return encoded ?? data;
454
+ }
455
+ /**
456
+ * Bring a row about to be written through a raw-SQL statement into the shape
457
+ * a Drizzle query would have bound: SQL column names for keys, encoded values
458
+ * for date columns.
459
+ *
460
+ * The mirror of {@link mapRowFromRawSql}. The raw-SQL write paths exist
461
+ * because a dialect needs SQL a Drizzle query cannot express, not because
462
+ * their callers want different values in the table.
463
+ */
464
+ mapRowToRawSql(tableObj, data) {
465
+ return this.mapDateValuesToDriver(
466
+ tableObj,
467
+ this.mapKeysToSqlColumns(tableObj, data)
468
+ );
469
+ }
470
+ /**
471
+ * Bring a raw-SQL result row into the shape a Drizzle query would have
472
+ * produced: Drizzle property names for keys, decoded values for date columns.
473
+ *
474
+ * The raw-SQL write paths exist because a dialect needs SQL a Drizzle query
475
+ * cannot express, not because their callers want a different row shape, so
476
+ * every one of them ends here.
477
+ */
478
+ mapRowFromRawSql(tableObj, row, aliases = []) {
479
+ const mapped = this.mapDateValuesFromDriver(
480
+ tableObj,
481
+ this.mapRowKeysToJs(tableObj, row)
482
+ );
483
+ return this.applyWallClockAliases(mapped, aliases);
484
+ }
485
+ /**
486
+ * The date columns a statement is about to return, and the alias each one's
487
+ * wall clock is spelled out under.
488
+ *
489
+ * A driver turns a timestamp into a `Date` using ITS zone before any of this
490
+ * code runs, and that conversion is lossy: a wall clock inside a
491
+ * daylight-saving gap is a local time that does not exist, so the driver
492
+ * normalizes it and the original is gone. Asking the database for the wall
493
+ * clock as text alongside the row is the only way to read it exactly.
494
+ *
495
+ * `"*"` covers every date column; a projection covers only the ones it asked
496
+ * for, so a caller still gets back what it requested and nothing more.
497
+ */
498
+ dateWallClockAliases(tableObj, returning) {
499
+ if (!tableObj || typeof tableObj !== "object" || returning === void 0) {
500
+ return [];
501
+ }
502
+ const requested = returning === "*" ? void 0 : new Set(returning.map(String));
503
+ const taken = /* @__PURE__ */ new Set();
504
+ for (const [jsName, colDef] of Object.entries(
505
+ tableObj
506
+ )) {
507
+ taken.add(jsName);
508
+ const columnName = colDef.name;
509
+ if (typeof columnName === "string") taken.add(columnName);
510
+ }
511
+ let next = 0;
512
+ const freeAlias = () => {
513
+ let candidate = `${WALL_CLOCK_ALIAS_STEM}${next++}`;
514
+ while (taken.has(candidate)) {
515
+ candidate = `${WALL_CLOCK_ALIAS_STEM}${next++}`;
516
+ }
517
+ taken.add(candidate);
518
+ return candidate;
519
+ };
520
+ const aliases = [];
521
+ for (const [jsName, colDef] of Object.entries(
522
+ tableObj
523
+ )) {
524
+ if (!isDateColumn(colDef)) continue;
525
+ if (carriesTimeZone(colDef)) continue;
526
+ const sqlName = colDef.name;
527
+ if (typeof sqlName !== "string") continue;
528
+ if (requested && !requested.has(sqlName) && !requested.has(jsName)) {
529
+ continue;
530
+ }
531
+ aliases.push({ sqlName, alias: freeAlias(), jsName });
532
+ }
533
+ return aliases;
534
+ }
535
+ /**
536
+ * Replace each date in a row with the wall clock the database spelled out,
537
+ * read as UTC, and drop the aliases that carried it.
538
+ *
539
+ * The statement writes a UTC wall clock, so reading one back as UTC is what
540
+ * makes a write and a later read agree. Whatever the column actually stored
541
+ * is what arrives -- a column keeping only whole seconds reports whole
542
+ * seconds -- because this is the database's own text, not a value
543
+ * reconstructed from the one that was bound.
544
+ */
545
+ applyWallClockAliases(row, aliases) {
546
+ if (!row || typeof row !== "object" || aliases.length === 0) return row;
547
+ const record = row;
548
+ let resolved;
549
+ for (const { alias, jsName } of aliases) {
550
+ if (!(alias in record)) continue;
551
+ resolved ??= { ...record };
552
+ const text = resolved[alias];
553
+ delete resolved[alias];
554
+ if (typeof text !== "string") continue;
555
+ const parsed = parseWallClockAsUtc(text);
556
+ if (parsed !== void 0) resolved[jsName] = parsed;
557
+ }
558
+ return resolved ?? row;
559
+ }
560
+ /**
561
+ * Build a Drizzle column projection object (`{ propertyName: column }`) from a
562
+ * requested column list, used by `select` (columns) and `insert` (returning).
563
+ * A requested name resolves against either the Drizzle property name
564
+ * (camelCase) or the SQL column name (snake_case); the projection is keyed by
565
+ * the property name so the row shape matches a full select. Returns undefined
566
+ * for `"*"` or when nothing resolves, so callers fall back to all columns.
567
+ */
568
+ buildColumnProjection(tableObj, names) {
569
+ if (names == null || names === "*" || !tableObj || typeof tableObj !== "object") {
570
+ return void 0;
571
+ }
572
+ const cols = drizzleOrm.getColumns(tableObj);
573
+ const byAnyName = {};
574
+ for (const [jsName, col] of Object.entries(cols)) {
575
+ byAnyName[jsName] = { jsName, col };
576
+ const sqlName = col?.name;
577
+ if (typeof sqlName === "string") byAnyName[sqlName] = { jsName, col };
578
+ }
579
+ const projection = {};
580
+ for (const name of names) {
581
+ const hit = byAnyName[name];
582
+ if (hit) projection[hit.jsName] = hit.col;
583
+ }
584
+ return Object.keys(projection).length ? projection : void 0;
585
+ }
178
586
  // ============================================================
179
587
  // Connection Status (Default implementations, can override)
180
588
  // ============================================================
@@ -328,12 +736,13 @@ var DrizzleAdapter = class {
328
736
  * });
329
737
  * ```
330
738
  */
331
- async select(table, options) {
739
+ async select(table, options, executor) {
332
740
  const tableObj = this.getTableObject(table);
333
741
  if (tableObj) {
334
742
  try {
335
- const db = this.getDrizzle();
336
- let query = db.select().from(tableObj);
743
+ const db = executor ?? this.getDrizzle();
744
+ const projection = options?.columns?.length ? this.buildColumnProjection(tableObj, options.columns) : void 0;
745
+ let query = projection ? db.select(projection).from(tableObj) : db.select().from(tableObj);
337
746
  if (options?.where) {
338
747
  const whereCondition = buildDrizzleWhere(
339
748
  tableObj,
@@ -344,12 +753,10 @@ var DrizzleAdapter = class {
344
753
  }
345
754
  }
346
755
  if (options?.orderBy?.length) {
347
- const columns = drizzleOrm.getTableColumns(tableObj);
348
- const orderClauses = options.orderBy.map((o) => {
349
- const col = columns[o.column];
350
- if (!col) return void 0;
351
- return o.direction === "desc" ? drizzleOrm.desc(col) : drizzleOrm.asc(col);
352
- }).filter(Boolean);
756
+ const orderClauses = buildDrizzleOrderBy(
757
+ drizzleOrm.getColumns(tableObj),
758
+ options.orderBy
759
+ );
353
760
  if (orderClauses.length) {
354
761
  query = query.orderBy(...orderClauses);
355
762
  }
@@ -360,6 +767,16 @@ var DrizzleAdapter = class {
360
767
  if (options?.offset !== void 0) {
361
768
  query = query.offset(options.offset);
362
769
  }
770
+ if (options?.forUpdate && !executor) {
771
+ throw this.createDatabaseError(
772
+ "query",
773
+ "forUpdate requires a transaction executor: a lock request on the pooled connection takes no durable lock and cannot prevent a concurrent write.",
774
+ void 0
775
+ );
776
+ }
777
+ if (options?.forUpdate && this.dialect !== "sqlite") {
778
+ query = query.for("update");
779
+ }
363
780
  return await query;
364
781
  } catch (error) {
365
782
  throw this.handleQueryError(error, "select", table);
@@ -391,8 +808,12 @@ var DrizzleAdapter = class {
391
808
  * });
392
809
  * ```
393
810
  */
394
- async selectOne(table, options) {
395
- const results = await this.select(table, { ...options, limit: 1 });
811
+ async selectOne(table, options, executor) {
812
+ const results = await this.select(
813
+ table,
814
+ { ...options, limit: 1 },
815
+ executor
816
+ );
396
817
  return results.length > 0 ? results[0] : null;
397
818
  }
398
819
  /**
@@ -424,19 +845,22 @@ var DrizzleAdapter = class {
424
845
  const mappedData = this.mapDataToColumnNames(tableObj, data);
425
846
  const db = this.getDrizzle();
426
847
  const caps = this.getCapabilities();
427
- if (caps.supportsReturning && options?.returning) {
428
- const result2 = await db.insert(tableObj).values(mappedData).returning();
848
+ const returning = options?.returning;
849
+ const wantsReturning = returning != null && !(Array.isArray(returning) && returning.length === 0);
850
+ if (caps.supportsReturning && wantsReturning) {
851
+ const projection = this.buildColumnProjection(tableObj, returning);
852
+ const insertQuery = db.insert(tableObj).values(mappedData);
853
+ const result2 = await (projection ? insertQuery.returning(projection) : insertQuery.returning());
429
854
  return Array.isArray(result2) ? result2[0] : result2;
430
855
  }
431
856
  const result = await db.insert(tableObj).values(mappedData);
432
- if (!caps.supportsReturning && options?.returning) {
433
- if (data.id !== void 0) {
434
- return await this.selectOne(table, {
435
- where: {
436
- and: [{ column: "id", op: "=", value: data.id }]
437
- }
438
- });
439
- }
857
+ if (!caps.supportsReturning && wantsReturning && data.id !== void 0) {
858
+ return await this.selectOne(table, {
859
+ columns: returning === "*" ? void 0 : returning,
860
+ where: {
861
+ and: [{ column: "id", op: "=", value: data.id }]
862
+ }
863
+ });
440
864
  }
441
865
  return Array.isArray(result) ? result[0] : result;
442
866
  } catch (error) {
@@ -506,11 +930,11 @@ var DrizzleAdapter = class {
506
930
  * );
507
931
  * ```
508
932
  */
509
- async update(table, data, where, options) {
933
+ async update(table, data, where, options, executor) {
510
934
  const tableObj = this.getTableObject(table);
511
935
  if (tableObj) {
512
936
  try {
513
- const db = this.getDrizzle();
937
+ const db = executor ?? this.getDrizzle();
514
938
  const caps = this.getCapabilities();
515
939
  const mappedData = this.mapDataToColumnNames(tableObj, data);
516
940
  let query = db.update(tableObj).set(mappedData);
@@ -523,7 +947,7 @@ var DrizzleAdapter = class {
523
947
  }
524
948
  await query;
525
949
  if (!caps.supportsReturning && options?.returning) {
526
- return await this.select(table, { where });
950
+ return await this.select(table, { where }, executor);
527
951
  }
528
952
  return [];
529
953
  } catch (error) {
@@ -536,6 +960,42 @@ var DrizzleAdapter = class {
536
960
  void 0
537
961
  );
538
962
  }
963
+ /**
964
+ * Update records in a table and report how many rows the statement affected.
965
+ *
966
+ * The count is the whole return value, mirroring `delete`. `update` cannot answer this: without
967
+ * `returning` it discards the driver's count, and WITH `returning` on a dialect that lacks
968
+ * RETURNING it re-SELECTs using the same WHERE — so a conditional update that just changed a
969
+ * column named in that WHERE reads back zero rows and a write that landed reports as unmatched.
970
+ * Reading the driver's own count has no second query to disagree with the first.
971
+ *
972
+ * 🔴 MySQL reports CHANGED rows, not matched rows: an UPDATE that matches a row but writes values
973
+ * identical to what it holds counts zero. A caller using this as a compare-and-set must therefore
974
+ * include a column the write always moves — a version bump, a timestamp with enough resolution —
975
+ * so that matched implies changed. Postgres (`rowCount`) and SQLite (`changes`) count matched
976
+ * rows and do not need the precaution, which is exactly why it cannot be dropped: the dialect
977
+ * where the distinction exists is the one with no RETURNING to fall back on.
978
+ */
979
+ async updateCount(table, data, where, executor) {
980
+ const tableObj = this.getTableObject(table);
981
+ if (tableObj) {
982
+ try {
983
+ const db = executor ?? this.getDrizzle();
984
+ const mappedData = this.mapDataToColumnNames(tableObj, data);
985
+ const statement = db.update(tableObj).set(mappedData);
986
+ const whereCondition = buildDrizzleWhere(tableObj, where);
987
+ const result = await (whereCondition ? statement.where(whereCondition) : statement);
988
+ return affectedRowCount(result);
989
+ } catch (error) {
990
+ throw this.handleQueryError(error, "update", table);
991
+ }
992
+ }
993
+ throw this.createDatabaseError(
994
+ "query",
995
+ `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`,
996
+ void 0
997
+ );
998
+ }
539
999
  /**
540
1000
  * Delete records from a table.
541
1001
  *
@@ -558,18 +1018,18 @@ var DrizzleAdapter = class {
558
1018
  * console.log(`Deleted ${count} users`);
559
1019
  * ```
560
1020
  */
561
- async delete(table, where, _options) {
1021
+ async delete(table, where, _options, executor) {
562
1022
  const tableObj = this.getTableObject(table);
563
1023
  if (tableObj) {
564
1024
  try {
565
- const db = this.getDrizzle();
1025
+ const db = executor ?? this.getDrizzle();
566
1026
  let query = db.delete(tableObj);
567
1027
  const whereCondition = buildDrizzleWhere(tableObj, where);
568
1028
  if (whereCondition) {
569
1029
  query = query.where(whereCondition);
570
1030
  }
571
1031
  const result = await query;
572
- return Array.isArray(result) ? result.length : result?.rowCount ?? result?.changes ?? 0;
1032
+ return affectedRowCount(result);
573
1033
  } catch (error) {
574
1034
  throw this.handleQueryError(error, "delete", table);
575
1035
  }
@@ -607,13 +1067,13 @@ var DrizzleAdapter = class {
607
1067
  * });
608
1068
  * ```
609
1069
  */
610
- async upsert(table, data, options) {
1070
+ async upsert(table, data, options, executor) {
611
1071
  const tableObj = this.getTableObject(table);
612
1072
  if (tableObj) {
613
1073
  try {
614
- const db = this.getDrizzle();
1074
+ const db = executor ?? this.getDrizzle();
615
1075
  const caps = this.getCapabilities();
616
- const columns = drizzleOrm.getTableColumns(tableObj);
1076
+ const columns = drizzleOrm.getColumns(tableObj);
617
1077
  const conflictTarget = options.conflictColumns.map((col) => columns[col]).filter(Boolean);
618
1078
  const conflictSet = new Set(options.conflictColumns);
619
1079
  const updateData = {};
@@ -638,17 +1098,21 @@ var DrizzleAdapter = class {
638
1098
  }
639
1099
  await query;
640
1100
  if (options.conflictColumns.length && data[options.conflictColumns[0]] !== void 0) {
641
- return await this.selectOne(table, {
642
- where: {
643
- and: [
644
- {
645
- column: options.conflictColumns[0],
646
- op: "=",
647
- value: data[options.conflictColumns[0]]
648
- }
649
- ]
650
- }
651
- });
1101
+ return await this.selectOne(
1102
+ table,
1103
+ {
1104
+ where: {
1105
+ and: [
1106
+ {
1107
+ column: options.conflictColumns[0],
1108
+ op: "=",
1109
+ value: data[options.conflictColumns[0]]
1110
+ }
1111
+ ]
1112
+ }
1113
+ },
1114
+ executor
1115
+ );
652
1116
  }
653
1117
  return data;
654
1118
  } catch (error) {
@@ -888,21 +1352,25 @@ var DrizzleAdapter = class {
888
1352
  */
889
1353
  async tableExists(tableName, schema) {
890
1354
  try {
891
- let sql;
1355
+ let sql3;
892
1356
  const params = [];
893
1357
  switch (this.dialect) {
894
1358
  case "postgresql":
895
- sql = `
1359
+ sql3 = `
896
1360
  SELECT EXISTS (
897
1361
  SELECT FROM information_schema.tables
898
- WHERE table_schema = $1
1362
+ WHERE table_schema = COALESCE($1, (
1363
+ SELECT n.nspname FROM pg_class c
1364
+ JOIN pg_namespace n ON n.oid = c.relnamespace
1365
+ WHERE c.oid = to_regclass($2)
1366
+ ))
899
1367
  AND table_name = $2
900
1368
  ) as exists
901
1369
  `;
902
- params.push(schema ?? "public", tableName);
1370
+ params.push(schema ?? null, tableName);
903
1371
  break;
904
1372
  case "mysql":
905
- sql = `
1373
+ sql3 = `
906
1374
  SELECT COUNT(*) as count
907
1375
  FROM information_schema.tables
908
1376
  WHERE table_schema = DATABASE()
@@ -911,7 +1379,7 @@ var DrizzleAdapter = class {
911
1379
  params.push(tableName);
912
1380
  break;
913
1381
  case "sqlite":
914
- sql = `
1382
+ sql3 = `
915
1383
  SELECT COUNT(*) as count
916
1384
  FROM sqlite_master
917
1385
  WHERE type = 'table'
@@ -927,7 +1395,7 @@ var DrizzleAdapter = class {
927
1395
  );
928
1396
  }
929
1397
  const results = await this.executeQuery(
930
- sql,
1398
+ sql3,
931
1399
  params
932
1400
  );
933
1401
  if (results.length === 0) {
@@ -959,11 +1427,11 @@ var DrizzleAdapter = class {
959
1427
  */
960
1428
  async listTables(schema) {
961
1429
  try {
962
- let sql;
1430
+ let sql3;
963
1431
  const params = [];
964
1432
  switch (this.dialect) {
965
1433
  case "postgresql":
966
- sql = `
1434
+ sql3 = `
967
1435
  SELECT table_name
968
1436
  FROM information_schema.tables
969
1437
  WHERE table_schema = $1
@@ -973,8 +1441,8 @@ var DrizzleAdapter = class {
973
1441
  params.push(schema ?? "public");
974
1442
  break;
975
1443
  case "mysql":
976
- sql = `
977
- SELECT table_name
1444
+ sql3 = `
1445
+ SELECT table_name AS table_name
978
1446
  FROM information_schema.tables
979
1447
  WHERE table_schema = DATABASE()
980
1448
  AND table_type = 'BASE TABLE'
@@ -982,7 +1450,7 @@ var DrizzleAdapter = class {
982
1450
  `;
983
1451
  break;
984
1452
  case "sqlite":
985
- sql = `
1453
+ sql3 = `
986
1454
  SELECT name as table_name
987
1455
  FROM sqlite_master
988
1456
  WHERE type = 'table'
@@ -998,7 +1466,7 @@ var DrizzleAdapter = class {
998
1466
  );
999
1467
  }
1000
1468
  const results = await this.executeQuery(
1001
- sql,
1469
+ sql3,
1002
1470
  params
1003
1471
  );
1004
1472
  return results.map((row) => row.table_name);
@@ -1054,30 +1522,64 @@ var DrizzleAdapter = class {
1054
1522
  });
1055
1523
  }
1056
1524
  /**
1057
- * Handle query errors and convert to DatabaseError.
1525
+ * Helper to create transaction context CRUD forwarding methods.
1526
+ *
1527
+ * @param txDb - Thunk returning the transaction-bound Drizzle executor
1528
+ * @returns Object providing forwarded CRUD and getDrizzle methods
1529
+ *
1530
+ * @protected
1531
+ */
1532
+ createTransactionForwarders(txDb) {
1533
+ return createTransactionForwarders(this, txDb);
1534
+ }
1535
+ /**
1536
+ * Classify an error into a DatabaseError.
1058
1537
  *
1059
1538
  * @remarks
1060
- * Protected helper for consistent error handling across CRUD operations.
1061
- * Subclasses can override to add dialect-specific error classification.
1539
+ * Subclasses can override this method to add dialect-specific error code classification.
1062
1540
  *
1063
1541
  * @param error - Original error
1064
- * @param operation - Operation that failed
1065
- * @param table - Table name
1542
+ * @param sql - SQL statement that caused the error (optional)
1066
1543
  * @returns DatabaseError instance
1067
1544
  *
1068
1545
  * @protected
1069
1546
  */
1070
- handleQueryError(error, operation, table) {
1547
+ classifyError(error, sql3) {
1071
1548
  if (isDatabaseError(error)) {
1072
1549
  return error;
1073
1550
  }
1074
1551
  const errorMessage = error instanceof Error ? error.message : String(error);
1075
1552
  return this.createDatabaseError(
1076
1553
  "query",
1077
- `${operation} operation failed on table '${table}': ${errorMessage}`,
1554
+ sql3 ? `Query failed: ${errorMessage}` : errorMessage,
1078
1555
  error instanceof Error ? error : void 0
1079
1556
  );
1080
1557
  }
1558
+ /**
1559
+ * Handle query errors and convert to DatabaseError.
1560
+ *
1561
+ * @remarks
1562
+ * Protected helper for consistent error handling across CRUD operations.
1563
+ * Delegates to `classifyError` and attaches operation and table context if not present.
1564
+ * Subclasses override `classifyError` to provide dialect-specific error code classification.
1565
+ *
1566
+ * @param error - Original error
1567
+ * @param operation - Operation that failed
1568
+ * @param table - Table name
1569
+ * @returns DatabaseError instance
1570
+ *
1571
+ * @protected
1572
+ */
1573
+ handleQueryError(error, operation, table) {
1574
+ const dbError = this.classifyError(error);
1575
+ if (!dbError.message.includes(operation)) {
1576
+ dbError.message = `${operation} operation failed on table '${table}': ${dbError.message}`;
1577
+ }
1578
+ if (!dbError.table) {
1579
+ dbError.table = table;
1580
+ }
1581
+ return dbError;
1582
+ }
1081
1583
  };
1082
1584
  var ALLOWED_DEFAULT_SQL_EXPRESSIONS = /* @__PURE__ */ new Set([
1083
1585
  "current_timestamp",