@vibeorm/runtime 1.0.2 → 1.1.1

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/README.md CHANGED
@@ -36,6 +36,25 @@ const db = VibeClient({
36
36
 
37
37
  Creates a VibeORM client instance. Called by generated code — takes model metadata, optional Zod schemas, and client options.
38
38
 
39
+ `countStrategy` can be set globally in client options and overridden per count call:
40
+
41
+ ```ts
42
+ await db.user.count({
43
+ where: { isActive: true },
44
+ countStrategy: "subquery",
45
+ });
46
+ ```
47
+
48
+ Per-operation `countStrategy` takes precedence over the client default.
49
+
50
+ ### Mutation RETURNING behavior
51
+
52
+ `create`, `update`, `delete`, and `upsert` now narrow SQL `RETURNING` columns when `select` is provided, reducing row payload size.
53
+
54
+ - Default behavior: full scalar row is returned (same as before)
55
+ - With `select`: only selected scalar columns are returned (plus internal keys when needed)
56
+ - With `returning: true`: force full scalar `RETURNING` payload even when `select` is present
57
+
39
58
  ### `toSqlExecutor({ adapter })`
40
59
 
41
60
  Bridges a `DatabaseAdapter` to a `SqlExecutor` for use with the migrate package.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeorm/runtime",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
4
4
  "description": "Driver-agnostic query engine and client runtime for VibeORM",
5
5
  "license": "MIT",
6
6
  "keywords": [
package/src/adapter.ts CHANGED
@@ -6,6 +6,35 @@
6
6
  * and interacts with the database exclusively through this contract.
7
7
  */
8
8
 
9
+ // ─── Transaction Options ──────────────────────────────────────────
10
+
11
+ /**
12
+ * Options for controlling transaction behavior.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * await db.$transaction(async (tx) => {
17
+ * await tx.account.update({ where: { id: 1 }, data: { balance: { decrement: 100 } } });
18
+ * await tx.account.update({ where: { id: 2 }, data: { balance: { increment: 100 } } });
19
+ * }, { isolationLevel: "Serializable", timeout: 5000 });
20
+ * ```
21
+ */
22
+ export type TransactionOptions = {
23
+ /**
24
+ * PostgreSQL isolation level for the transaction.
25
+ * - "ReadCommitted" (default): Standard PostgreSQL default.
26
+ * - "RepeatableRead": Prevents non-repeatable reads.
27
+ * - "Serializable": Full serializability — conflicts throw serialization_failure.
28
+ */
29
+ isolationLevel?: "ReadCommitted" | "RepeatableRead" | "Serializable";
30
+ /**
31
+ * Maximum time in milliseconds the transaction may run before being cancelled.
32
+ * Implemented via `SET LOCAL statement_timeout = N` inside the transaction,
33
+ * so it is scoped to the transaction and does not affect other connections.
34
+ */
35
+ timeout?: number;
36
+ };
37
+
9
38
  // ─── Core Adapter Interface ───────────────────────────────────────
10
39
 
11
40
  /**
@@ -38,8 +67,11 @@ export type DatabaseAdapter = {
38
67
  * Run a function inside a database transaction.
39
68
  * The callback receives a transactional adapter with the same interface.
40
69
  * If the callback throws, the transaction is automatically rolled back.
70
+ *
71
+ * @param fn - The transactional callback.
72
+ * @param options - Optional isolation level and timeout.
41
73
  */
42
- transaction<T>(fn: (txAdapter: DatabaseAdapter) => Promise<T>): Promise<T>;
74
+ transaction<T>(fn: (txAdapter: DatabaseAdapter) => Promise<T>, options?: TransactionOptions): Promise<T>;
43
75
 
44
76
  /**
45
77
  * Eagerly warm up the connection pool.
package/src/client.ts CHANGED
@@ -16,8 +16,8 @@ import type {
16
16
  ProfilingContext,
17
17
  } from "./types.ts";
18
18
  import { getScalarFieldMap, getModelByNameMap, PgArray } from "./types.ts";
19
- import type { DatabaseAdapter } from "./adapter.ts";
20
- import { VibeValidationError } from "./errors.ts";
19
+ import type { DatabaseAdapter, TransactionOptions } from "./adapter.ts";
20
+ import { VibeValidationError, VibeRequestError, normalizeError } from "./errors.ts";
21
21
  import {
22
22
  buildSelectQuery,
23
23
  buildInsertQuery,
@@ -30,11 +30,10 @@ import {
30
30
  buildGroupByQuery,
31
31
  buildUpsertQuery,
32
32
  } from "./query-builder.ts";
33
- import { loadRelations } from "./relation-loader.ts";
33
+ import { loadRelations, resolveRelationsToLoad } from "./relation-loader.ts";
34
34
  import {
35
35
  loadRelationsWithLateralJoin,
36
36
  executeLateralJoinQuery,
37
- resolveRelationsToLoad,
38
37
  } from "./lateral-join-builder.ts";
39
38
  import { generateDefault } from "./id-generators.ts";
40
39
 
@@ -51,6 +50,8 @@ export function createClient(params: {
51
50
  const adapter = options.adapter;
52
51
  const shouldLog = options?.log === true || options?.log === "query";
53
52
  const shouldDebug = !!options?.debug;
53
+ const defaultOrderByPk = options?.defaultOrderByPk ?? false;
54
+ const distinctOrderByPk = options?.distinctOrderByPk ?? true;
54
55
 
55
56
  /** Emit a query profile to the user's debug handler or console. */
56
57
  function debugEmit(profile: QueryProfile): void {
@@ -158,6 +159,7 @@ export function createClient(params: {
158
159
  // SQL executor function — delegates to the adapter.
159
160
  // Array values are converted via adapter.formatArrayParam so that
160
161
  // = ANY($1) works correctly.
162
+ // Database errors are normalized into VibeRequestError / VibeTransientError.
161
163
  async function executeSql(params: { text: string; values: unknown[] }): Promise<Record<string, unknown>[]> {
162
164
  const { text, values: rawValues } = params;
163
165
  // Convert PgArray instances - let the adapter handle the format
@@ -170,7 +172,11 @@ export function createClient(params: {
170
172
  }
171
173
  }
172
174
 
173
- return adapter.execute({ text, values });
175
+ try {
176
+ return await adapter.execute({ text, values });
177
+ } catch (err) {
178
+ throw normalizeError({ error: err });
179
+ }
174
180
  }
175
181
 
176
182
  // Create delegate for a model
@@ -252,6 +258,7 @@ export function createClient(params: {
252
258
  args,
253
259
  executor: executeSql,
254
260
  profilingCtx,
261
+ defaultOrderByPk,
255
262
  });
256
263
  const t1 = performance.now();
257
264
  const sizeBytes = estimateResultSize({ records: result });
@@ -278,6 +285,7 @@ export function createClient(params: {
278
285
  allModelsMeta,
279
286
  args,
280
287
  executor,
288
+ defaultOrderByPk,
281
289
  });
282
290
  validateOutput({ records: result, operation: "findMany" });
283
291
  return result;
@@ -291,6 +299,8 @@ export function createClient(params: {
291
299
  modelMeta,
292
300
  allModelsMeta,
293
301
  args,
302
+ defaultOrderByPk,
303
+ distinctOrderByPk,
294
304
  });
295
305
  const t1 = performance.now();
296
306
  let records = await profExec(query);
@@ -327,6 +337,8 @@ export function createClient(params: {
327
337
  modelMeta,
328
338
  allModelsMeta,
329
339
  args,
340
+ defaultOrderByPk,
341
+ distinctOrderByPk,
330
342
  });
331
343
  let records = await executor(query);
332
344
 
@@ -352,6 +364,7 @@ export function createClient(params: {
352
364
  allModelsMeta,
353
365
  args: argsWithLimit,
354
366
  executor,
367
+ defaultOrderByPk,
355
368
  });
356
369
  validateOutput({ records, operation: "findFirst" });
357
370
  return records[0] ?? null;
@@ -361,7 +374,7 @@ export function createClient(params: {
361
374
  const profilingCtx: ProfilingContext = { relationProfiles: [] };
362
375
  const { executor: profExec } = createProfiledExecutor(profilingCtx);
363
376
  const t0 = performance.now();
364
- const query = buildSelectQuery({ modelMeta, allModelsMeta, args: argsWithLimit });
377
+ const query = buildSelectQuery({ modelMeta, allModelsMeta, args: argsWithLimit, defaultOrderByPk, distinctOrderByPk });
365
378
  const t1 = performance.now();
366
379
  let records = await profExec(query);
367
380
  const t2 = performance.now();
@@ -385,6 +398,8 @@ export function createClient(params: {
385
398
  modelMeta,
386
399
  allModelsMeta,
387
400
  args: argsWithLimit,
401
+ defaultOrderByPk,
402
+ distinctOrderByPk,
388
403
  });
389
404
  let records = await executor(query);
390
405
 
@@ -414,6 +429,7 @@ export function createClient(params: {
414
429
  allModelsMeta,
415
430
  args: argsWithLimit,
416
431
  executor,
432
+ defaultOrderByPk,
417
433
  });
418
434
  validateOutput({ records, operation: "findUnique" });
419
435
  return records[0] ?? null;
@@ -423,7 +439,7 @@ export function createClient(params: {
423
439
  const profilingCtx: ProfilingContext = { relationProfiles: [] };
424
440
  const { executor: profExec } = createProfiledExecutor(profilingCtx);
425
441
  const t0 = performance.now();
426
- const query = buildSelectQuery({ modelMeta, allModelsMeta, args: argsWithLimit });
442
+ const query = buildSelectQuery({ modelMeta, allModelsMeta, args: argsWithLimit, defaultOrderByPk, distinctOrderByPk });
427
443
  const t1 = performance.now();
428
444
  let records = await profExec(query);
429
445
  const t2 = performance.now();
@@ -447,6 +463,8 @@ export function createClient(params: {
447
463
  modelMeta,
448
464
  allModelsMeta,
449
465
  args: argsWithLimit,
466
+ defaultOrderByPk,
467
+ distinctOrderByPk,
450
468
  });
451
469
  let records = await executor(query);
452
470
 
@@ -466,9 +484,11 @@ export function createClient(params: {
466
484
  async function findUniqueOrThrow(args: Record<string, unknown>) {
467
485
  const result = await findUnique(args);
468
486
  if (!result) {
469
- throw new Error(
470
- `No ${modelMeta.name} found for the given where clause`
471
- );
487
+ throw new VibeRequestError({
488
+ code: "NOT_FOUND",
489
+ message: `No ${modelMeta.name} found for the given where clause`,
490
+ meta: { model: modelMeta.name, operation: "findUniqueOrThrow" },
491
+ });
472
492
  }
473
493
  return result;
474
494
  }
@@ -476,9 +496,11 @@ export function createClient(params: {
476
496
  async function findFirstOrThrow(args: Record<string, unknown> = {}) {
477
497
  const result = await findFirst(args);
478
498
  if (!result) {
479
- throw new Error(
480
- `No ${modelMeta.name} found for the given where clause`
481
- );
499
+ throw new VibeRequestError({
500
+ code: "NOT_FOUND",
501
+ message: `No ${modelMeta.name} found for the given where clause`,
502
+ meta: { model: modelMeta.name, operation: "findFirstOrThrow" },
503
+ });
482
504
  }
483
505
  return result;
484
506
  }
@@ -503,6 +525,7 @@ export function createClient(params: {
503
525
  const query = buildInsertQuery({
504
526
  modelMeta,
505
527
  data: processedData,
528
+ args: deferredCreates.length === 0 ? args : undefined,
506
529
  });
507
530
  let records = await executor(query);
508
531
 
@@ -537,7 +560,7 @@ export function createClient(params: {
537
560
  executor,
538
561
  });
539
562
 
540
- records = applySelectFiltering({ records, args, modelMeta });
563
+ records = applySelectFiltering({ records, args, modelMeta, allowReturningOverride: true });
541
564
 
542
565
  validateOutput({ records, operation: "create" });
543
566
  return records[0]!;
@@ -569,13 +592,16 @@ export function createClient(params: {
569
592
  allModelsMeta,
570
593
  where: whereInput,
571
594
  data: scalarData,
595
+ args: nestedOps.length === 0 ? args : undefined,
572
596
  });
573
597
  let records = await executor(query);
574
598
 
575
599
  if (records.length === 0) {
576
- throw new Error(
577
- `An operation failed because it depends on one or more records that were required but not found. No ${modelMeta.name} found for the given where clause.`
578
- );
600
+ throw new VibeRequestError({
601
+ code: "NOT_FOUND",
602
+ message: `An operation failed because it depends on one or more records that were required but not found. No ${modelMeta.name} found for the given where clause.`,
603
+ meta: { model: modelMeta.name, operation: "update" },
604
+ });
579
605
  }
580
606
 
581
607
  const updatedRecord = records[0]!;
@@ -599,7 +625,7 @@ export function createClient(params: {
599
625
  executor,
600
626
  });
601
627
 
602
- records = applySelectFiltering({ records, args, modelMeta });
628
+ records = applySelectFiltering({ records, args, modelMeta, allowReturningOverride: true });
603
629
 
604
630
  validateOutput({ records, operation: "update" });
605
631
  return records[0]!;
@@ -629,6 +655,7 @@ export function createClient(params: {
629
655
  where: whereInput,
630
656
  create: createData,
631
657
  update: updateData,
658
+ args,
632
659
  });
633
660
 
634
661
  let records = await executor(query);
@@ -638,7 +665,11 @@ export function createClient(params: {
638
665
  if (records.length === 0) {
639
666
  const existing = await findUnique({ where, select: args.select, include: args.include });
640
667
  if (existing) return existing;
641
- throw new Error(`Upsert failed: could not insert or find ${modelMeta.name}`);
668
+ throw new VibeRequestError({
669
+ code: "NOT_FOUND",
670
+ message: `Upsert failed: could not insert or find ${modelMeta.name}`,
671
+ meta: { model: modelMeta.name, operation: "upsert" },
672
+ });
642
673
  }
643
674
 
644
675
  records = await loadRelationsForStrategy({
@@ -649,7 +680,7 @@ export function createClient(params: {
649
680
  executor,
650
681
  });
651
682
 
652
- records = applySelectFiltering({ records, args, modelMeta });
683
+ records = applySelectFiltering({ records, args, modelMeta, allowReturningOverride: true });
653
684
 
654
685
  validateOutput({ records, operation: "upsert" });
655
686
  return records[0]!;
@@ -666,17 +697,20 @@ export function createClient(params: {
666
697
  modelMeta,
667
698
  allModelsMeta,
668
699
  where: whereInput,
700
+ args,
669
701
  });
670
702
  let records = await executor(query);
671
703
 
672
704
  if (records.length === 0) {
673
- throw new Error(
674
- `An operation failed because it depends on one or more records that were required but not found. No ${modelMeta.name} found for the given where clause.`
675
- );
705
+ throw new VibeRequestError({
706
+ code: "NOT_FOUND",
707
+ message: `An operation failed because it depends on one or more records that were required but not found. No ${modelMeta.name} found for the given where clause.`,
708
+ meta: { model: modelMeta.name, operation: "delete" },
709
+ });
676
710
  }
677
711
 
678
712
  coerceFieldTypes({ records, modelMeta });
679
- records = applySelectFiltering({ records, args, modelMeta });
713
+ records = applySelectFiltering({ records, args, modelMeta, allowReturningOverride: true });
680
714
 
681
715
  validateOutput({ records, operation: "delete" });
682
716
  return records[0]!;
@@ -704,7 +738,11 @@ export function createClient(params: {
704
738
  }
705
739
 
706
740
  async function count(args: Record<string, unknown> = {}) {
707
- const countStrategy = options?.countStrategy ?? "direct";
741
+ const operationCountStrategy = args.countStrategy;
742
+ const countStrategy =
743
+ operationCountStrategy === "direct" || operationCountStrategy === "subquery"
744
+ ? operationCountStrategy
745
+ : options?.countStrategy ?? "direct";
708
746
 
709
747
  if (shouldDebug) {
710
748
  const t0 = performance.now();
@@ -817,7 +855,7 @@ export function createClient(params: {
817
855
  executor,
818
856
  });
819
857
 
820
- records = applySelectFiltering({ records, args, modelMeta });
858
+ records = applySelectFiltering({ records, args, modelMeta, allowReturningOverride: true });
821
859
 
822
860
  validateOutput({ records, operation: "createManyAndReturn" });
823
861
  return records;
@@ -940,6 +978,7 @@ export function createClient(params: {
940
978
  args: params.args,
941
979
  executor: params.executor,
942
980
  profilingCtx: params.profilingCtx,
981
+ defaultOrderByPk,
943
982
  });
944
983
  } else {
945
984
  records = await loadRelations({
@@ -949,6 +988,7 @@ export function createClient(params: {
949
988
  args: params.args,
950
989
  executor: params.executor,
951
990
  profilingCtx: params.profilingCtx,
991
+ defaultOrderByPk,
952
992
  });
953
993
  }
954
994
 
@@ -979,72 +1019,99 @@ export function createClient(params: {
979
1019
  });
980
1020
  }
981
1021
 
982
- // $transaction — supports both callback style and array-of-promises style
1022
+ // $transaction — supports both callback style and array-of-promises style.
1023
+ //
1024
+ // Array-of-promises style: wraps pre-created promises in a transaction.
1025
+ // Note: The promises must be created from THIS client's delegates.
1026
+ // They execute within the transaction's commit/rollback boundary, but
1027
+ // their SQL was dispatched on the non-transactional adapter. For full
1028
+ // transactional isolation, use the callback style instead.
983
1029
  client.$transaction = async function <T>(
984
- fnOrPromises: ((tx: Record<string, unknown>) => Promise<T>) | Promise<unknown>[]
1030
+ fnOrPromises: ((tx: Record<string, unknown>) => Promise<T>) | Promise<unknown>[],
1031
+ txOptions?: TransactionOptions
985
1032
  ): Promise<T | unknown[]> {
986
1033
  // Array-of-promises style
987
1034
  if (Array.isArray(fnOrPromises)) {
988
- return adapter.transaction(async () => {
989
- return Promise.all(fnOrPromises);
990
- });
1035
+ try {
1036
+ return await adapter.transaction(async () => {
1037
+ return Promise.all(fnOrPromises);
1038
+ }, txOptions);
1039
+ } catch (err) {
1040
+ throw normalizeError({ error: err });
1041
+ }
991
1042
  }
992
1043
 
993
1044
  // Callback style
994
1045
  const fn = fnOrPromises as (tx: Record<string, unknown>) => Promise<T>;
995
- return adapter.transaction(async (txAdapter) => {
996
- // Create a transactional executor
997
- async function txExecutor(txParams: { text: string; values: unknown[] }): Promise<Record<string, unknown>[]> {
998
- const values = txParams.values.map((v) => (v instanceof PgArray ? txAdapter.formatArrayParam(v.values) : v));
999
- if (shouldLog) {
1000
- console.log(`[vibeorm:tx] ${txParams.text}`);
1001
- if (values.length > 0) {
1002
- console.log(`[vibeorm:tx] params:`, values);
1046
+ try {
1047
+ return await adapter.transaction(async (txAdapter) => {
1048
+ // Create a transactional executor normalizes DB errors
1049
+ async function txExecutor(txParams: { text: string; values: unknown[] }): Promise<Record<string, unknown>[]> {
1050
+ const values = txParams.values.map((v) => (v instanceof PgArray ? txAdapter.formatArrayParam(v.values) : v));
1051
+ if (shouldLog) {
1052
+ console.log(`[vibeorm:tx] ${txParams.text}`);
1053
+ if (values.length > 0) {
1054
+ console.log(`[vibeorm:tx] params:`, values);
1055
+ }
1056
+ }
1057
+ try {
1058
+ return await txAdapter.execute({ text: txParams.text, values });
1059
+ } catch (err) {
1060
+ throw normalizeError({ error: err });
1003
1061
  }
1004
1062
  }
1005
- return txAdapter.execute({ text: txParams.text, values });
1006
- }
1007
-
1008
- // Build transactional delegates
1009
- const txClient: Record<string, unknown> = {};
1010
- for (const [key, meta] of Object.entries(allModelsMeta)) {
1011
- txClient[key] = createDelegate({
1012
- modelKey: key,
1013
- modelMeta: meta,
1014
- executor: txExecutor,
1015
- schemas: params.schemas?.[key],
1016
- });
1017
- }
1018
1063
 
1019
- // Add $queryRaw and $executeRaw to transactional client
1020
- txClient.$queryRaw = async function <T = unknown>(
1021
- strings: TemplateStringsArray,
1022
- ...values: unknown[]
1023
- ): Promise<T[]> {
1024
- const { text, params: sqlParams } = buildTaggedTemplateSql({ strings, values });
1025
- if (shouldLog) {
1026
- console.log(`[vibeorm:tx] ${text}`);
1027
- if (sqlParams.length > 0) console.log(`[vibeorm:tx] params:`, sqlParams);
1064
+ // Build transactional delegates
1065
+ const txClient: Record<string, unknown> = {};
1066
+ for (const [key, meta] of Object.entries(allModelsMeta)) {
1067
+ txClient[key] = createDelegate({
1068
+ modelKey: key,
1069
+ modelMeta: meta,
1070
+ executor: txExecutor,
1071
+ schemas: params.schemas?.[key],
1072
+ });
1028
1073
  }
1029
- const result = await txAdapter.executeUnsafe({ text, values: sqlParams });
1030
- return result.rows as T[];
1031
- };
1032
1074
 
1033
- txClient.$executeRaw = async function (
1034
- strings: TemplateStringsArray,
1035
- ...values: unknown[]
1036
- ): Promise<number> {
1037
- const { text, params: sqlParams } = buildTaggedTemplateSql({ strings, values });
1038
- if (shouldLog) {
1039
- console.log(`[vibeorm:tx] ${text}`);
1040
- if (sqlParams.length > 0) console.log(`[vibeorm:tx] params:`, sqlParams);
1041
- }
1042
- const result = await txAdapter.executeUnsafe({ text, values: sqlParams });
1043
- return result.affectedRows;
1044
- };
1075
+ // Add $queryRaw and $executeRaw to transactional client
1076
+ txClient.$queryRaw = async function <T = unknown>(
1077
+ strings: TemplateStringsArray,
1078
+ ...values: unknown[]
1079
+ ): Promise<T[]> {
1080
+ const { text, params: sqlParams } = buildTaggedTemplateSql({ strings, values });
1081
+ if (shouldLog) {
1082
+ console.log(`[vibeorm:tx] ${text}`);
1083
+ if (sqlParams.length > 0) console.log(`[vibeorm:tx] params:`, sqlParams);
1084
+ }
1085
+ try {
1086
+ const result = await txAdapter.executeUnsafe({ text, values: sqlParams });
1087
+ return result.rows as T[];
1088
+ } catch (err) {
1089
+ throw normalizeError({ error: err });
1090
+ }
1091
+ };
1045
1092
 
1046
- return fn(txClient);
1047
- });
1093
+ txClient.$executeRaw = async function (
1094
+ strings: TemplateStringsArray,
1095
+ ...values: unknown[]
1096
+ ): Promise<number> {
1097
+ const { text, params: sqlParams } = buildTaggedTemplateSql({ strings, values });
1098
+ if (shouldLog) {
1099
+ console.log(`[vibeorm:tx] ${text}`);
1100
+ if (sqlParams.length > 0) console.log(`[vibeorm:tx] params:`, sqlParams);
1101
+ }
1102
+ try {
1103
+ const result = await txAdapter.executeUnsafe({ text, values: sqlParams });
1104
+ return result.affectedRows;
1105
+ } catch (err) {
1106
+ throw normalizeError({ error: err });
1107
+ }
1108
+ };
1109
+
1110
+ return fn(txClient);
1111
+ }, txOptions);
1112
+ } catch (err) {
1113
+ throw normalizeError({ error: err });
1114
+ }
1048
1115
  };
1049
1116
 
1050
1117
  // $queryRaw — tagged template literal for safe parameterized queries
@@ -1059,8 +1126,12 @@ export function createClient(params: {
1059
1126
  console.log(`[vibeorm:raw] params:`, sqlParams);
1060
1127
  }
1061
1128
  }
1062
- const result = await adapter.executeUnsafe({ text, values: sqlParams });
1063
- return result.rows as T[];
1129
+ try {
1130
+ const result = await adapter.executeUnsafe({ text, values: sqlParams });
1131
+ return result.rows as T[];
1132
+ } catch (err) {
1133
+ throw normalizeError({ error: err });
1134
+ }
1064
1135
  };
1065
1136
 
1066
1137
  // $executeRaw — tagged template literal for INSERT/UPDATE/DELETE returning affected count
@@ -1075,8 +1146,12 @@ export function createClient(params: {
1075
1146
  console.log(`[vibeorm:raw] params:`, sqlParams);
1076
1147
  }
1077
1148
  }
1078
- const result = await adapter.executeUnsafe({ text, values: sqlParams });
1079
- return result.affectedRows;
1149
+ try {
1150
+ const result = await adapter.executeUnsafe({ text, values: sqlParams });
1151
+ return result.affectedRows;
1152
+ } catch (err) {
1153
+ throw normalizeError({ error: err });
1154
+ }
1080
1155
  };
1081
1156
 
1082
1157
  // $queryRawUnsafe — accepts a plain SQL string + params array
@@ -1090,12 +1165,16 @@ export function createClient(params: {
1090
1165
  console.log(`[vibeorm:raw] params:`, values);
1091
1166
  }
1092
1167
  }
1093
- if (values.length === 0) {
1094
- const result = await adapter.executeUnsafe({ text: query });
1095
- return result.rows as T[];
1168
+ try {
1169
+ if (values.length === 0) {
1170
+ const result = await adapter.executeUnsafe({ text: query });
1171
+ return result.rows as T[];
1172
+ }
1173
+ const rows = await adapter.execute({ text: query, values });
1174
+ return rows as T[];
1175
+ } catch (err) {
1176
+ throw normalizeError({ error: err });
1096
1177
  }
1097
- const rows = await adapter.execute({ text: query, values });
1098
- return rows as T[];
1099
1178
  };
1100
1179
 
1101
1180
  // $executeRawUnsafe — accepts a plain SQL string + params array, returns affected count
@@ -1109,8 +1188,12 @@ export function createClient(params: {
1109
1188
  console.log(`[vibeorm:raw] params:`, values);
1110
1189
  }
1111
1190
  }
1112
- const result = await adapter.executeUnsafe({ text: query, values: values.length > 0 ? values : undefined });
1113
- return result.affectedRows;
1191
+ try {
1192
+ const result = await adapter.executeUnsafe({ text: query, values: values.length > 0 ? values : undefined });
1193
+ return result.affectedRows;
1194
+ } catch (err) {
1195
+ throw normalizeError({ error: err });
1196
+ }
1114
1197
  };
1115
1198
 
1116
1199
  // $connect
@@ -1179,8 +1262,12 @@ function applySelectFiltering(params: {
1179
1262
  records: Record<string, unknown>[];
1180
1263
  args: Record<string, unknown>;
1181
1264
  modelMeta: ModelMeta;
1265
+ allowReturningOverride?: boolean;
1182
1266
  }): Record<string, unknown>[] {
1183
- const { records, args, modelMeta } = params;
1267
+ const { records, args, modelMeta, allowReturningOverride = false } = params;
1268
+ if (allowReturningOverride && args.returning === true) {
1269
+ return records;
1270
+ }
1184
1271
  const select = args.select as Record<string, boolean | object> | undefined;
1185
1272
  if (!select) return records;
1186
1273