sqlstack 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/cjs/adapters/pg.d.ts.map +1 -1
  2. package/dist/cjs/adapters/pg.js +13 -9
  3. package/dist/cjs/adapters/pg.js.map +1 -1
  4. package/dist/cjs/adapters/sqlite.d.ts.map +1 -1
  5. package/dist/cjs/adapters/sqlite.js +15 -8
  6. package/dist/cjs/adapters/sqlite.js.map +1 -1
  7. package/dist/cjs/core/errors.d.ts +5 -0
  8. package/dist/cjs/core/errors.d.ts.map +1 -1
  9. package/dist/cjs/core/errors.js +15 -1
  10. package/dist/cjs/core/errors.js.map +1 -1
  11. package/dist/cjs/decorators/query.d.ts.map +1 -1
  12. package/dist/cjs/decorators/query.js +255 -241
  13. package/dist/cjs/decorators/query.js.map +1 -1
  14. package/dist/cjs/index.d.ts +1 -0
  15. package/dist/cjs/index.d.ts.map +1 -1
  16. package/dist/cjs/index.js +4 -1
  17. package/dist/cjs/index.js.map +1 -1
  18. package/dist/cjs/registry.d.ts +3 -4
  19. package/dist/cjs/registry.d.ts.map +1 -1
  20. package/dist/cjs/registry.js.map +1 -1
  21. package/dist/cjs/run.d.ts +29 -0
  22. package/dist/cjs/run.d.ts.map +1 -0
  23. package/dist/cjs/run.js +38 -0
  24. package/dist/cjs/run.js.map +1 -0
  25. package/dist/cjs/testing/index.d.ts +4 -0
  26. package/dist/cjs/testing/index.d.ts.map +1 -1
  27. package/dist/cjs/testing/index.js +5 -1
  28. package/dist/cjs/testing/index.js.map +1 -1
  29. package/dist/cjs/testing/schema/ddl.d.ts +1 -10
  30. package/dist/cjs/testing/schema/ddl.d.ts.map +1 -1
  31. package/dist/cjs/testing/schema/ddl.js +29 -13
  32. package/dist/cjs/testing/schema/ddl.js.map +1 -1
  33. package/dist/cjs/testing/schema/normalize.js +3 -3
  34. package/dist/cjs/testing/schema/normalize.js.map +1 -1
  35. package/dist/cjs/testing/schema/types.d.ts +1 -20
  36. package/dist/cjs/testing/schema/types.d.ts.map +1 -1
  37. package/dist/cjs/testing/schema/types.js +0 -4
  38. package/dist/cjs/testing/schema/types.js.map +1 -1
  39. package/dist/cjs/testing/sqlite/builder.d.ts +24 -0
  40. package/dist/cjs/testing/sqlite/builder.d.ts.map +1 -0
  41. package/dist/cjs/testing/sqlite/builder.js +134 -0
  42. package/dist/cjs/testing/sqlite/builder.js.map +1 -0
  43. package/dist/cjs/testing/sqlite/database.d.ts +18 -0
  44. package/dist/cjs/testing/sqlite/database.d.ts.map +1 -0
  45. package/dist/cjs/testing/sqlite/database.js +123 -0
  46. package/dist/cjs/testing/sqlite/database.js.map +1 -0
  47. package/dist/cjs/testing/sqlite/loadData.d.ts +7 -0
  48. package/dist/cjs/testing/sqlite/loadData.d.ts.map +1 -0
  49. package/dist/cjs/testing/sqlite/loadData.js +55 -0
  50. package/dist/cjs/testing/sqlite/loadData.js.map +1 -0
  51. package/dist/esm/adapters/pg.js +13 -9
  52. package/dist/esm/adapters/pg.js.map +1 -1
  53. package/dist/esm/adapters/sqlite.js +15 -8
  54. package/dist/esm/adapters/sqlite.js.map +1 -1
  55. package/dist/esm/core/errors.js +13 -0
  56. package/dist/esm/core/errors.js.map +1 -1
  57. package/dist/esm/decorators/query.js +256 -242
  58. package/dist/esm/decorators/query.js.map +1 -1
  59. package/dist/esm/index.js +2 -0
  60. package/dist/esm/index.js.map +1 -1
  61. package/dist/esm/registry.js.map +1 -1
  62. package/dist/esm/run.js +34 -0
  63. package/dist/esm/run.js.map +1 -0
  64. package/dist/esm/testing/index.js +2 -0
  65. package/dist/esm/testing/index.js.map +1 -1
  66. package/dist/esm/testing/schema/ddl.js +29 -13
  67. package/dist/esm/testing/schema/ddl.js.map +1 -1
  68. package/dist/esm/testing/schema/normalize.js +3 -3
  69. package/dist/esm/testing/schema/normalize.js.map +1 -1
  70. package/dist/esm/testing/schema/types.js +0 -4
  71. package/dist/esm/testing/schema/types.js.map +1 -1
  72. package/dist/esm/testing/sqlite/builder.js +98 -0
  73. package/dist/esm/testing/sqlite/builder.js.map +1 -0
  74. package/dist/esm/testing/sqlite/database.js +85 -0
  75. package/dist/esm/testing/sqlite/database.js.map +1 -0
  76. package/dist/esm/testing/sqlite/loadData.js +48 -0
  77. package/dist/esm/testing/sqlite/loadData.js.map +1 -0
  78. package/package.json +4 -3
  79. package/readme.md +6 -8
@@ -1,5 +1,5 @@
1
1
  import { getClassLabels, getMethodLabels, setMethodLabels } from '../core/metadata.js';
2
- import { SqlStackError, ExactlyOneRowError, NoRowsError, RowsExistError, DatabaseError } from '../core/errors.js';
2
+ import { SqlStackError, ExactlyOneRowError, NoRowsError, RowsExistError, DatabaseError, ExpectedRowsError } from '../core/errors.js';
3
3
  import { isValidationDisabled, validateOutput } from '../core/validator.js';
4
4
  import { resolveExecution } from '../runtime.js';
5
5
  import { createSqliteBinder, createMysqlBinder, createPostgresBinder } from '../binders/sqlBinder.js';
@@ -8,6 +8,7 @@ import { registerDecoratedMethod, recordQueryMethodOnClass } from '../core/resol
8
8
  import { defaultSourceScanResolver } from '../core/sourceScanResolver.js';
9
9
  import { getActiveTransaction } from '../transactions.js';
10
10
  import { SqlFragment } from '../sql.js';
11
+ import { runStorage } from '../run.js';
11
12
  export function Query(arg) {
12
13
  return function (_target, prop, desc) {
13
14
  const options = typeof arg === 'string' ? undefined : arg;
@@ -44,281 +45,269 @@ export function Query(arg) {
44
45
  desc.value = async function (...callArgs) {
45
46
  const labels = getMethodLabels(this, prop);
46
47
  const classLabels = getClassLabels(this);
47
- // 0) Check if the method body returns a SqlFragment (template literal path)
48
- let fragmentResult;
49
- try {
50
- fragmentResult = await original.apply(this, callArgs);
51
- }
52
- catch {
53
- // Method body threw (e.g. placeholder `throw new Error('replaced')`) — fall through to file/inline SQL
54
- fragmentResult = undefined;
55
- }
56
- if (fragmentResult instanceof SqlFragment) {
57
- const { sql: boundSql, params } = fragmentResult.toSQL();
48
+ // The full file/inline pipeline (resolve source, bind, execute,
49
+ // shape/transform/validate). Invoked either by run() from inside the
50
+ // method body, or directly as the legacy fallback when the body throws.
51
+ const pipeline = async () => {
52
+ // 1) prepare args with defaults
53
+ const rawInput = callArgs.length === 1 ? callArgs[0] : callArgs; // object or positional array
54
+ // 2) choose database and dialect via the active environment
58
55
  const dbName = labels.db ?? options?.db ?? classLabels?.db;
59
- const { entry } = await resolveExecution(dbName);
56
+ const { entry, resolver: activeResolver } = await resolveExecution(dbName);
60
57
  const db = entry.db;
61
- let out;
62
- try {
63
- const tx = getActiveTransaction(entry);
64
- out = tx ? await tx.query(boundSql, params) : await db.query(boundSql, params);
65
- }
66
- catch (dbError) {
67
- throw new DatabaseError(dbError?.message || String(dbError), boundSql, dbError instanceof Error ? dbError : undefined);
68
- }
69
- if (Array.isArray(out)) {
70
- const shaped = applyShape(out, labels.shape);
71
- const transformed = labels.transform ? labels.transform(shaped) : shaped;
72
- if (labels.validateSchema && !isValidationDisabled()) {
73
- if (Array.isArray(transformed)) {
74
- for (const row of transformed)
75
- validateOutput(labels.validateSchema, row);
76
- }
77
- else if (transformed !== null && transformed !== undefined) {
78
- validateOutput(labels.validateSchema, transformed);
79
- }
80
- }
81
- return transformed;
82
- }
83
- return out;
84
- }
85
- // 1) prepare args with defaults
86
- const rawInput = callArgs.length === 1 ? callArgs[0] : callArgs; // object or positional array
87
- // 2) choose database and dialect via the active environment
88
- const dbName = labels.db ?? options?.db ?? classLabels?.db;
89
- const { entry, resolver: activeResolver } = await resolveExecution(dbName);
90
- const db = entry.db;
91
- const dialect = options?.dialect ?? db.dialect ?? 'sqlite';
92
- // 3) load SQL source (inline, environment resolver, v1 fs fallback,
93
- // or the zero-config source scan — bare @Query() with nothing installed)
94
- const sql = labels?.source?.kind === 'inline'
95
- ? labels.source.sql
96
- : activeResolver
97
- ? activeResolver.resolve({
98
- className: this?.constructor?.name ?? '<anonymous>',
99
- methodName: String(prop),
100
- dialect,
101
- rootDir: labels?.source?.kind === 'fs' ? labels.source.rootDir : undefined,
102
- })
103
- : labels?.source?.kind === 'fs'
104
- ? resolveSqlFromFs(labels.source.rootDir, String(prop), dialect).sql
105
- : defaultSourceScanResolver()?.resolve({
58
+ const dialect = options?.dialect ?? db.dialect ?? 'sqlite';
59
+ // 3) load SQL source (inline, environment resolver, v1 fs fallback,
60
+ // or the zero-config source scan — bare @Query() with nothing installed)
61
+ const sql = labels?.source?.kind === 'inline'
62
+ ? labels.source.sql
63
+ : activeResolver
64
+ ? activeResolver.resolve({
106
65
  className: this?.constructor?.name ?? '<anonymous>',
107
66
  methodName: String(prop),
108
67
  dialect,
109
- });
110
- if (!sql) {
111
- throw new SqlStackError(`No SQL source provided for method ${String(prop)}. Use @Query() with a file/folder path or inline sql.`);
112
- }
113
- // 4) bind placeholders
114
- // Decide argument mode: positional if SQL contains :argN
115
- let workingSql = sql;
116
- const hasPositional = /:arg[1-9][0-9]*/.test(workingSql);
117
- // Prepare args for binder
118
- let args;
119
- if (hasPositional) {
120
- args = callArgs;
121
- }
122
- else {
123
- const namedBase = callArgs.length > 0 ? callArgs[0] : undefined;
124
- args = applyDefaults(labels.defaults, namedBase ?? {});
125
- }
126
- // Compute paging and inject :offset/:limit (named mode) or rewrite to positional
127
- const pageCfg = labels.page;
128
- if (pageCfg) {
129
- const { size, zeroBase } = pageCfg;
130
- const base = zeroBase ? 0 : 1;
131
- // Extract page number from input (object) or a trailing options object in positional mode
132
- const pageFromObject = (val) => {
133
- if (val && typeof val === 'object' && !Array.isArray(val) && typeof val.page !== 'undefined') {
134
- const p = val.page;
135
- if (typeof p === 'number' && isFinite(p))
136
- return p;
137
- }
138
- return undefined;
139
- };
140
- let pageNum;
68
+ rootDir: labels?.source?.kind === 'fs' ? labels.source.rootDir : undefined,
69
+ })
70
+ : labels?.source?.kind === 'fs'
71
+ ? resolveSqlFromFs(labels.source.rootDir, String(prop), dialect).sql
72
+ : defaultSourceScanResolver()?.resolve({
73
+ className: this?.constructor?.name ?? '<anonymous>',
74
+ methodName: String(prop),
75
+ dialect,
76
+ });
77
+ if (!sql) {
78
+ throw new SqlStackError(`No SQL source provided for method ${String(prop)}. Use @Query() with a file/folder path or inline sql.`);
79
+ }
80
+ // 4) bind placeholders
81
+ // Decide argument mode: positional if SQL contains :argN
82
+ let workingSql = sql;
83
+ const hasPositional = /:arg[1-9][0-9]*/.test(workingSql);
84
+ // Prepare args for binder
85
+ let args;
141
86
  if (hasPositional) {
142
- // Variadic-style: only the LAST argument may carry `{ page }`
143
- if (callArgs.length > 0) {
144
- const last = callArgs[callArgs.length - 1];
145
- pageNum = pageFromObject(last);
146
- }
87
+ args = callArgs;
147
88
  }
148
89
  else {
149
- // Prefer last-arg options if provided; otherwise read from named object
150
- if (callArgs.length > 1) {
151
- const last = callArgs[callArgs.length - 1];
152
- pageNum = pageFromObject(last);
90
+ const namedBase = callArgs.length > 0 ? callArgs[0] : undefined;
91
+ args = applyDefaults(labels.defaults, namedBase ?? {});
92
+ }
93
+ // Compute paging and inject :offset/:limit (named mode) or rewrite to positional
94
+ const pageCfg = labels.page;
95
+ if (pageCfg) {
96
+ const { size, zeroBase } = pageCfg;
97
+ const base = zeroBase ? 0 : 1;
98
+ // Extract page number from input (object) or a trailing options object in positional mode
99
+ const pageFromObject = (val) => {
100
+ if (val && typeof val === 'object' && !Array.isArray(val) && typeof val.page !== 'undefined') {
101
+ const p = val.page;
102
+ if (typeof p === 'number' && isFinite(p))
103
+ return p;
104
+ }
105
+ return undefined;
106
+ };
107
+ let pageNum;
108
+ if (hasPositional) {
109
+ // Variadic-style: only the LAST argument may carry `{ page }`
110
+ if (callArgs.length > 0) {
111
+ const last = callArgs[callArgs.length - 1];
112
+ pageNum = pageFromObject(last);
113
+ }
153
114
  }
154
- if (pageNum === undefined) {
155
- pageNum = pageFromObject(args);
115
+ else {
116
+ // Prefer last-arg options if provided; otherwise read from named object
117
+ if (callArgs.length > 1) {
118
+ const last = callArgs[callArgs.length - 1];
119
+ pageNum = pageFromObject(last);
120
+ }
121
+ if (pageNum === undefined) {
122
+ pageNum = pageFromObject(args);
123
+ }
156
124
  }
157
- }
158
- const effectivePage = typeof pageNum === 'number' ? pageNum : base;
159
- const offset = (effectivePage - base) * size;
160
- const limit = size;
161
- if (hasPositional) {
162
- // Replace occurrences of :offset and :limit with new :argN tokens appended to args
163
- const baseIndex = callArgs.length; // start numbering after user args
164
- let nextIndex = baseIndex + 1;
165
- const additions = [];
166
- const rewrite = (sqlText) => {
167
- const out = [];
168
- let i = 0;
169
- let inSQ = false, inDQ = false, inLC = false, inBC = false;
170
- const len = sqlText.length;
171
- while (i < len) {
172
- const ch = sqlText[i];
173
- const next = i + 1 < len ? sqlText[i + 1] : '';
174
- if (inLC) {
175
- out.push(ch);
176
- if (ch === '\n')
177
- inLC = false;
178
- i++;
179
- continue;
180
- }
181
- if (inBC) {
182
- out.push(ch);
183
- if (ch === '*' && next === '/') {
184
- out.push(next);
125
+ const effectivePage = typeof pageNum === 'number' ? pageNum : base;
126
+ const offset = (effectivePage - base) * size;
127
+ const limit = size;
128
+ if (hasPositional) {
129
+ // Replace occurrences of :offset and :limit with new :argN tokens appended to args
130
+ const baseIndex = callArgs.length; // start numbering after user args
131
+ let nextIndex = baseIndex + 1;
132
+ const additions = [];
133
+ const rewrite = (sqlText) => {
134
+ const out = [];
135
+ let i = 0;
136
+ let inSQ = false, inDQ = false, inLC = false, inBC = false;
137
+ const len = sqlText.length;
138
+ while (i < len) {
139
+ const ch = sqlText[i];
140
+ const next = i + 1 < len ? sqlText[i + 1] : '';
141
+ if (inLC) {
142
+ out.push(ch);
143
+ if (ch === '\n')
144
+ inLC = false;
185
145
  i++;
186
- inBC = false;
146
+ continue;
187
147
  }
188
- i++;
189
- continue;
190
- }
191
- if (inSQ) {
192
- out.push(ch);
193
- if (ch === "'" && next === "'") {
194
- out.push(next);
148
+ if (inBC) {
149
+ out.push(ch);
150
+ if (ch === '*' && next === '/') {
151
+ out.push(next);
152
+ i++;
153
+ inBC = false;
154
+ }
155
+ i++;
156
+ continue;
157
+ }
158
+ if (inSQ) {
159
+ out.push(ch);
160
+ if (ch === "'" && next === "'") {
161
+ out.push(next);
162
+ i += 2;
163
+ continue;
164
+ }
165
+ if (ch === "'")
166
+ inSQ = false;
167
+ i++;
168
+ continue;
169
+ }
170
+ if (inDQ) {
171
+ out.push(ch);
172
+ if (ch === '"' && next === '"') {
173
+ out.push(next);
174
+ i += 2;
175
+ continue;
176
+ }
177
+ if (ch === '"')
178
+ inDQ = false;
179
+ i++;
180
+ continue;
181
+ }
182
+ if (ch === '-' && next === '-') {
183
+ out.push(ch, next);
195
184
  i += 2;
185
+ inLC = true;
196
186
  continue;
197
187
  }
198
- if (ch === "'")
199
- inSQ = false;
200
- i++;
201
- continue;
202
- }
203
- if (inDQ) {
204
- out.push(ch);
205
- if (ch === '"' && next === '"') {
206
- out.push(next);
188
+ if (ch === '/' && next === '*') {
189
+ out.push(ch, next);
207
190
  i += 2;
191
+ inBC = true;
208
192
  continue;
209
193
  }
210
- if (ch === '"')
211
- inDQ = false;
212
- i++;
213
- continue;
214
- }
215
- if (ch === '-' && next === '-') {
216
- out.push(ch, next);
217
- i += 2;
218
- inLC = true;
219
- continue;
220
- }
221
- if (ch === '/' && next === '*') {
222
- out.push(ch, next);
223
- i += 2;
224
- inBC = true;
225
- continue;
226
- }
227
- if (ch === '"') {
228
- out.push(ch);
229
- inDQ = true;
230
- i++;
231
- continue;
232
- }
233
- if (ch === "'") {
234
- out.push(ch);
235
- inSQ = true;
236
- i++;
237
- continue;
238
- }
239
- if (ch === ':' && next === ':') {
240
- out.push('::');
241
- i += 2;
242
- continue;
243
- }
244
- if (ch === ':') {
245
- // parse token
246
- let j = i + 1;
247
- const start = j;
248
- while (j < len) {
249
- const c = sqlText[j];
250
- if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '_')
251
- j++;
252
- else
253
- break;
194
+ if (ch === '"') {
195
+ out.push(ch);
196
+ inDQ = true;
197
+ i++;
198
+ continue;
254
199
  }
255
- const name = sqlText.slice(start, j);
256
- if (name === 'offset') {
257
- const token = `:arg${nextIndex++}`;
258
- out.push(token);
259
- additions.push(offset);
260
- i = j;
200
+ if (ch === "'") {
201
+ out.push(ch);
202
+ inSQ = true;
203
+ i++;
261
204
  continue;
262
205
  }
263
- if (name === 'limit') {
264
- const token = `:arg${nextIndex++}`;
265
- out.push(token);
266
- additions.push(limit);
267
- i = j;
206
+ if (ch === ':' && next === ':') {
207
+ out.push('::');
208
+ i += 2;
268
209
  continue;
269
210
  }
270
- // otherwise leave untouched
271
- out.push(':');
211
+ if (ch === ':') {
212
+ // parse token
213
+ let j = i + 1;
214
+ const start = j;
215
+ while (j < len) {
216
+ const c = sqlText[j];
217
+ if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '_')
218
+ j++;
219
+ else
220
+ break;
221
+ }
222
+ const name = sqlText.slice(start, j);
223
+ if (name === 'offset') {
224
+ const token = `:arg${nextIndex++}`;
225
+ out.push(token);
226
+ additions.push(offset);
227
+ i = j;
228
+ continue;
229
+ }
230
+ if (name === 'limit') {
231
+ const token = `:arg${nextIndex++}`;
232
+ out.push(token);
233
+ additions.push(limit);
234
+ i = j;
235
+ continue;
236
+ }
237
+ // otherwise leave untouched
238
+ out.push(':');
239
+ i++;
240
+ continue;
241
+ }
242
+ out.push(ch);
272
243
  i++;
273
- continue;
274
244
  }
275
- out.push(ch);
276
- i++;
245
+ return out.join('');
246
+ };
247
+ const rewritten = rewrite(workingSql);
248
+ if (rewritten !== workingSql) {
249
+ workingSql = rewritten;
250
+ // append values so :argK map correctly
251
+ args.push(...additions);
277
252
  }
278
- return out.join('');
279
- };
280
- const rewritten = rewrite(workingSql);
281
- if (rewritten !== workingSql) {
282
- workingSql = rewritten;
283
- // append values so :argK map correctly
284
- args.push(...additions);
253
+ }
254
+ else {
255
+ // Named-mode: attach computed values (override any provided)
256
+ args = { ...args, offset, limit };
285
257
  }
286
258
  }
287
- else {
288
- // Named-mode: attach computed values (override any provided)
289
- args = { ...args, offset, limit };
259
+ const binder = dialect === 'postgres' ? createPostgresBinder() : dialect === 'mysql' ? createMysqlBinder() : createSqliteBinder();
260
+ const { sql: boundSql, params } = binder.bind(workingSql, args);
261
+ // 5) execute query via registry DB
262
+ let out;
263
+ try {
264
+ const tx = getActiveTransaction(entry);
265
+ out = tx ? await tx.query(boundSql, params) : await db.query(boundSql, params);
290
266
  }
291
- }
292
- const binder = dialect === 'postgres' ? createPostgresBinder() : dialect === 'mysql' ? createMysqlBinder() : createSqliteBinder();
293
- const { sql: boundSql, params } = binder.bind(workingSql, args);
294
- // 5) execute query via registry DB
295
- let out;
267
+ catch (dbError) {
268
+ // Attach SQL query (with placeholders) to error for debugging
269
+ throw new DatabaseError(dbError?.message || String(dbError), boundSql, dbError instanceof Error ? dbError : undefined);
270
+ }
271
+ // 6) If rows were returned (SELECT/CTE/RETURNING), shape + transform + validate;
272
+ // otherwise return the write result as-is (or fail if a shape was requested)
273
+ return finishResult(out, labels, this, prop);
274
+ };
275
+ // 0) Invoke the method body inside a run() context. Three outcomes:
276
+ // - body calls run(): the pipeline executed inside the body; return
277
+ // the body's result verbatim (it may have caught/transformed it).
278
+ // - body returns a SqlFragment (template literal path): execute it.
279
+ // - body throws without calling run() (legacy placeholder throw):
280
+ // fall through to the pipeline.
281
+ const runCtx = { ran: false, execute: pipeline };
282
+ let fragmentResult;
296
283
  try {
297
- const tx = getActiveTransaction(entry);
298
- out = tx ? await tx.query(boundSql, params) : await db.query(boundSql, params);
284
+ fragmentResult = await runStorage.run(runCtx, () => original.apply(this, callArgs));
299
285
  }
300
- catch (dbError) {
301
- // Attach SQL query (with placeholders) to error for debugging
302
- throw new DatabaseError(dbError?.message || String(dbError), boundSql, dbError instanceof Error ? dbError : undefined);
286
+ catch (bodyError) {
287
+ // A body that invoked run() is real logic — its errors propagate.
288
+ if (runCtx.ran)
289
+ throw bodyError;
290
+ // Method body threw (e.g. placeholder `throw new Error('replaced')`) — fall through to file/inline SQL
291
+ fragmentResult = undefined;
303
292
  }
304
- // 6) If rows were returned (SELECT/CTE), shape + transform + validate
305
- if (Array.isArray(out)) {
306
- const shaped = applyShape(out, labels.shape);
307
- const transformed = labels.transform ? labels.transform(shaped) : shaped;
308
- // 7) validate result via JSON Schema, if provided
309
- if (labels.validateSchema && !isValidationDisabled()) {
310
- if (Array.isArray(transformed)) {
311
- for (const row of transformed)
312
- validateOutput(labels.validateSchema, row);
313
- }
314
- else if (transformed !== null && transformed !== undefined) {
315
- validateOutput(labels.validateSchema, transformed);
316
- }
293
+ if (runCtx.ran)
294
+ return fragmentResult;
295
+ if (fragmentResult instanceof SqlFragment) {
296
+ const { sql: boundSql, params } = fragmentResult.toSQL();
297
+ const dbName = labels.db ?? options?.db ?? classLabels?.db;
298
+ const { entry } = await resolveExecution(dbName);
299
+ const db = entry.db;
300
+ let out;
301
+ try {
302
+ const tx = getActiveTransaction(entry);
303
+ out = tx ? await tx.query(boundSql, params) : await db.query(boundSql, params);
304
+ }
305
+ catch (dbError) {
306
+ throw new DatabaseError(dbError?.message || String(dbError), boundSql, dbError instanceof Error ? dbError : undefined);
317
307
  }
318
- return transformed;
308
+ return finishResult(out, labels, this, prop);
319
309
  }
320
- // Non-SELECT: return the write result as-is
321
- return out;
310
+ return pipeline();
322
311
  };
323
312
  return desc;
324
313
  };
@@ -330,6 +319,31 @@ function applyDefaults(defaults, input) {
330
319
  return input; // defaults only apply to named-object args
331
320
  return { ...defaults, ...(input || {}) };
332
321
  }
322
+ // Shared tail of both execution paths. Rows (any statement that produced a
323
+ // result set) are shaped, transformed, and validated. A write result is
324
+ // returned verbatim — unless a result-shape decorator was requested, in which
325
+ // case the statement cannot satisfy it and we fail at the call site instead of
326
+ // silently handing back a truthy `{ rowsAffected }` object.
327
+ function finishResult(out, labels, self, prop) {
328
+ if (!Array.isArray(out)) {
329
+ if (labels.shape) {
330
+ throw new ExpectedRowsError(`${self?.constructor?.name ?? '<anonymous>'}.${String(prop)}`, labels.shape);
331
+ }
332
+ return out;
333
+ }
334
+ const shaped = applyShape(out, labels.shape);
335
+ const transformed = labels.transform ? labels.transform(shaped) : shaped;
336
+ if (labels.validateSchema && !isValidationDisabled()) {
337
+ if (Array.isArray(transformed)) {
338
+ for (const row of transformed)
339
+ validateOutput(labels.validateSchema, row);
340
+ }
341
+ else if (transformed !== null && transformed !== undefined) {
342
+ validateOutput(labels.validateSchema, transformed);
343
+ }
344
+ }
345
+ return transformed;
346
+ }
333
347
  function applyShape(rows, shape) {
334
348
  switch (shape) {
335
349
  case 'single':