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