@rawsql-ts/sql-contract 0.3.1 → 0.3.2

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.
@@ -0,0 +1,778 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preprocessMutationSpec = preprocessMutationSpec;
4
+ exports.isMutationSafeRewriter = isMutationSafeRewriter;
5
+ exports.assertMutationSafeRewriters = assertMutationSafeRewriters;
6
+ exports.assertDeleteGuard = assertDeleteGuard;
7
+ const IDENTIFIER_START = /^[A-Za-z_]$/;
8
+ const IDENTIFIER_PART = /^[A-Za-z0-9_$]$/;
9
+ function isIdentifierStart(char) {
10
+ return IDENTIFIER_START.test(char);
11
+ }
12
+ function isIdentifierPart(char) {
13
+ return IDENTIFIER_PART.test(char);
14
+ }
15
+ function parseDollarTag(sql, index) {
16
+ var _a;
17
+ if (sql[index] !== '$') {
18
+ return null;
19
+ }
20
+ if (sql[index + 1] === '$') {
21
+ return '$$';
22
+ }
23
+ if (!isIdentifierStart((_a = sql[index + 1]) !== null && _a !== void 0 ? _a : '')) {
24
+ return null;
25
+ }
26
+ let cursor = index + 1;
27
+ while (cursor < sql.length && isIdentifierPart(sql[cursor])) {
28
+ cursor += 1;
29
+ }
30
+ if (sql[cursor] !== '$') {
31
+ return null;
32
+ }
33
+ return sql.slice(index, cursor + 1);
34
+ }
35
+ function tokenizeSql(sql) {
36
+ const tokens = [];
37
+ let index = 0;
38
+ while (index < sql.length) {
39
+ const char = sql[index];
40
+ const next = sql[index + 1];
41
+ if (/\s/.test(char)) {
42
+ index += 1;
43
+ continue;
44
+ }
45
+ if (char === '-' && next === '-') {
46
+ index += 2;
47
+ while (index < sql.length && sql[index] !== '\n') {
48
+ index += 1;
49
+ }
50
+ continue;
51
+ }
52
+ if (char === '/' && next === '*') {
53
+ index += 2;
54
+ while (index < sql.length && !(sql[index] === '*' && sql[index + 1] === '/')) {
55
+ index += 1;
56
+ }
57
+ index = Math.min(index + 2, sql.length);
58
+ continue;
59
+ }
60
+ if (char === "'") {
61
+ const start = index;
62
+ index += 1;
63
+ while (index < sql.length) {
64
+ if (sql[index] === "'" && sql[index + 1] === "'") {
65
+ index += 2;
66
+ continue;
67
+ }
68
+ if (sql[index] === "'") {
69
+ index += 1;
70
+ break;
71
+ }
72
+ index += 1;
73
+ }
74
+ tokens.push({ kind: 'literal', value: sql.slice(start, index), start, end: index });
75
+ continue;
76
+ }
77
+ if (char === '"') {
78
+ const start = index;
79
+ index += 1;
80
+ while (index < sql.length) {
81
+ if (sql[index] === '"' && sql[index + 1] === '"') {
82
+ index += 2;
83
+ continue;
84
+ }
85
+ if (sql[index] === '"') {
86
+ index += 1;
87
+ break;
88
+ }
89
+ index += 1;
90
+ }
91
+ tokens.push({ kind: 'word', value: sql.slice(start, index), start, end: index });
92
+ continue;
93
+ }
94
+ if (char === '[') {
95
+ const start = index;
96
+ index += 1;
97
+ while (index < sql.length) {
98
+ if (sql[index] === ']' && sql[index + 1] === ']') {
99
+ index += 2;
100
+ continue;
101
+ }
102
+ if (sql[index] === ']') {
103
+ index += 1;
104
+ break;
105
+ }
106
+ index += 1;
107
+ }
108
+ tokens.push({ kind: 'word', value: sql.slice(start, index), start, end: index });
109
+ continue;
110
+ }
111
+ if (char === '`') {
112
+ const start = index;
113
+ index += 1;
114
+ while (index < sql.length) {
115
+ if (sql[index] === '`' && sql[index + 1] === '`') {
116
+ index += 2;
117
+ continue;
118
+ }
119
+ if (sql[index] === '`') {
120
+ index += 1;
121
+ break;
122
+ }
123
+ index += 1;
124
+ }
125
+ tokens.push({ kind: 'word', value: sql.slice(start, index), start, end: index });
126
+ continue;
127
+ }
128
+ if (char === '$') {
129
+ const tag = parseDollarTag(sql, index);
130
+ if (tag) {
131
+ const start = index;
132
+ index += tag.length;
133
+ while (index < sql.length) {
134
+ if (sql.startsWith(tag, index)) {
135
+ index += tag.length;
136
+ break;
137
+ }
138
+ index += 1;
139
+ }
140
+ tokens.push({ kind: 'literal', value: sql.slice(start, index), start, end: index });
141
+ continue;
142
+ }
143
+ }
144
+ if (char === ':' && next === ':') {
145
+ tokens.push({ kind: 'operator', value: '::', start: index, end: index + 2 });
146
+ index += 2;
147
+ continue;
148
+ }
149
+ if ((char === ':' || char === '@' || char === '$') && isIdentifierStart(next !== null && next !== void 0 ? next : '')) {
150
+ const start = index;
151
+ index += 2;
152
+ while (index < sql.length && isIdentifierPart(sql[index])) {
153
+ index += 1;
154
+ }
155
+ tokens.push({ kind: 'parameter', value: sql.slice(start, index), start, end: index });
156
+ continue;
157
+ }
158
+ if (char === '$' && next === '{') {
159
+ const start = index;
160
+ index += 2;
161
+ while (index < sql.length && sql[index] !== '}') {
162
+ index += 1;
163
+ }
164
+ index = Math.min(index + 1, sql.length);
165
+ tokens.push({ kind: 'parameter', value: sql.slice(start, index), start, end: index });
166
+ continue;
167
+ }
168
+ if (char === '?') {
169
+ tokens.push({ kind: 'parameter', value: '?', start: index, end: index + 1 });
170
+ index += 1;
171
+ continue;
172
+ }
173
+ if (isIdentifierStart(char)) {
174
+ const start = index;
175
+ index += 1;
176
+ while (index < sql.length && isIdentifierPart(sql[index])) {
177
+ index += 1;
178
+ }
179
+ tokens.push({ kind: 'word', value: sql.slice(start, index), start, end: index });
180
+ continue;
181
+ }
182
+ if (char === ',') {
183
+ tokens.push({ kind: 'comma', value: char, start: index, end: index + 1 });
184
+ index += 1;
185
+ continue;
186
+ }
187
+ if (char === '(') {
188
+ tokens.push({ kind: 'openParen', value: char, start: index, end: index + 1 });
189
+ index += 1;
190
+ continue;
191
+ }
192
+ if (char === ')') {
193
+ tokens.push({ kind: 'closeParen', value: char, start: index, end: index + 1 });
194
+ index += 1;
195
+ continue;
196
+ }
197
+ if (char === '.') {
198
+ tokens.push({ kind: 'dot', value: char, start: index, end: index + 1 });
199
+ index += 1;
200
+ continue;
201
+ }
202
+ if (char === '=') {
203
+ tokens.push({ kind: 'operator', value: char, start: index, end: index + 1 });
204
+ index += 1;
205
+ continue;
206
+ }
207
+ tokens.push({ kind: 'other', value: char, start: index, end: index + 1 });
208
+ index += 1;
209
+ }
210
+ return tokens;
211
+ }
212
+ function lowerWord(token) {
213
+ return token.value.toLowerCase();
214
+ }
215
+ function createContractViolation(ContractViolationError, specId, message) {
216
+ return new ContractViolationError(message, specId);
217
+ }
218
+ function detectStatement(tokens) {
219
+ let depth = 0;
220
+ let withSeen = false;
221
+ for (let index = 0; index < tokens.length; index += 1) {
222
+ const token = tokens[index];
223
+ if (token.kind === 'openParen') {
224
+ depth += 1;
225
+ continue;
226
+ }
227
+ if (token.kind === 'closeParen') {
228
+ depth = Math.max(depth - 1, 0);
229
+ continue;
230
+ }
231
+ if (token.kind !== 'word' || depth !== 0) {
232
+ continue;
233
+ }
234
+ const word = lowerWord(token);
235
+ if (!withSeen && word === 'with') {
236
+ withSeen = true;
237
+ continue;
238
+ }
239
+ if (word === 'update' || word === 'delete' || word === 'insert') {
240
+ return {
241
+ kind: word,
242
+ tokens,
243
+ statementTokenIndex: index,
244
+ };
245
+ }
246
+ }
247
+ return {
248
+ kind: 'unknown',
249
+ tokens,
250
+ statementTokenIndex: -1,
251
+ };
252
+ }
253
+ function findClauseBounds(statement, clauseName, terminators) {
254
+ let depth = 0;
255
+ let startTokenIndex = -1;
256
+ for (let index = statement.statementTokenIndex + 1; index < statement.tokens.length; index += 1) {
257
+ const token = statement.tokens[index];
258
+ if (token.kind === 'openParen') {
259
+ depth += 1;
260
+ continue;
261
+ }
262
+ if (token.kind === 'closeParen') {
263
+ depth = Math.max(depth - 1, 0);
264
+ continue;
265
+ }
266
+ if (depth === 0 && token.kind === 'other' && token.value === ';') {
267
+ if (startTokenIndex < 0) {
268
+ return null;
269
+ }
270
+ return {
271
+ startTokenIndex,
272
+ endTokenIndex: index,
273
+ };
274
+ }
275
+ if (depth !== 0 || token.kind !== 'word') {
276
+ continue;
277
+ }
278
+ const word = lowerWord(token);
279
+ if (startTokenIndex < 0) {
280
+ if (word === clauseName) {
281
+ startTokenIndex = index + 1;
282
+ }
283
+ continue;
284
+ }
285
+ if (terminators.includes(word)) {
286
+ return {
287
+ startTokenIndex,
288
+ endTokenIndex: index,
289
+ };
290
+ }
291
+ }
292
+ if (startTokenIndex < 0) {
293
+ return null;
294
+ }
295
+ return {
296
+ startTokenIndex,
297
+ endTokenIndex: statement.tokens.length,
298
+ };
299
+ }
300
+ function normalizeNamedParameter(raw) {
301
+ if (raw === '?') {
302
+ return null;
303
+ }
304
+ if (raw.startsWith('${') && raw.endsWith('}')) {
305
+ return raw.slice(2, -1);
306
+ }
307
+ if (raw.startsWith(':') || raw.startsWith('@') || raw.startsWith('$')) {
308
+ return raw.slice(1);
309
+ }
310
+ return null;
311
+ }
312
+ function sanitizeNamedParams(params) {
313
+ const sanitized = {};
314
+ for (const [key, value] of Object.entries(params)) {
315
+ if (value !== undefined) {
316
+ sanitized[key] = value;
317
+ }
318
+ }
319
+ return sanitized;
320
+ }
321
+ function removeNamedParams(params, names) {
322
+ const removed = new Set(names);
323
+ const next = {};
324
+ for (const [key, value] of Object.entries(params)) {
325
+ if (!removed.has(key)) {
326
+ next[key] = value;
327
+ }
328
+ }
329
+ return next;
330
+ }
331
+ function normalizeInsertConfig(spec) {
332
+ var _a, _b, _c;
333
+ const insertConfig = ((_a = spec.mutation) === null || _a === void 0 ? void 0 : _a.kind) === 'insert' ? spec.mutation.insert : undefined;
334
+ return {
335
+ subtractUndefinedColumns: (_b = insertConfig === null || insertConfig === void 0 ? void 0 : insertConfig.subtractUndefinedColumns) !== null && _b !== void 0 ? _b : true,
336
+ failOnEmptyColumns: (_c = insertConfig === null || insertConfig === void 0 ? void 0 : insertConfig.failOnEmptyColumns) !== null && _c !== void 0 ? _c : true,
337
+ };
338
+ }
339
+ function normalizeUpdateConfig(spec) {
340
+ var _a, _b, _c, _d, _e, _f;
341
+ const updateConfig = ((_a = spec.mutation) === null || _a === void 0 ? void 0 : _a.kind) === 'update' ? spec.mutation.update : undefined;
342
+ const whereConfig = ((_b = spec.mutation) === null || _b === void 0 ? void 0 : _b.kind) === 'update' ? spec.mutation.where : undefined;
343
+ return {
344
+ subtractUndefinedAssignments: (_c = updateConfig === null || updateConfig === void 0 ? void 0 : updateConfig.subtractUndefinedAssignments) !== null && _c !== void 0 ? _c : true,
345
+ failOnEmptySet: (_d = updateConfig === null || updateConfig === void 0 ? void 0 : updateConfig.failOnEmptySet) !== null && _d !== void 0 ? _d : true,
346
+ where: {
347
+ requireWhereClause: (_e = whereConfig === null || whereConfig === void 0 ? void 0 : whereConfig.requireWhereClause) !== null && _e !== void 0 ? _e : true,
348
+ requireAllNamedParams: (_f = whereConfig === null || whereConfig === void 0 ? void 0 : whereConfig.requireAllNamedParams) !== null && _f !== void 0 ? _f : true,
349
+ },
350
+ };
351
+ }
352
+ function normalizeDeleteConfig(spec) {
353
+ var _a, _b, _c, _d, _e;
354
+ const deleteConfig = ((_a = spec.mutation) === null || _a === void 0 ? void 0 : _a.kind) === 'delete' ? spec.mutation.delete : undefined;
355
+ const whereConfig = ((_b = spec.mutation) === null || _b === void 0 ? void 0 : _b.kind) === 'delete' ? spec.mutation.where : undefined;
356
+ return {
357
+ where: {
358
+ requireWhereClause: (_c = whereConfig === null || whereConfig === void 0 ? void 0 : whereConfig.requireWhereClause) !== null && _c !== void 0 ? _c : true,
359
+ requireAllNamedParams: (_d = whereConfig === null || whereConfig === void 0 ? void 0 : whereConfig.requireAllNamedParams) !== null && _d !== void 0 ? _d : true,
360
+ },
361
+ affectedRowsGuard: (_e = deleteConfig === null || deleteConfig === void 0 ? void 0 : deleteConfig.affectedRowsGuard) !== null && _e !== void 0 ? _e : { mode: 'exactly', count: 1 },
362
+ };
363
+ }
364
+ function assertNamedMutationParams(ContractViolationError, spec, params) {
365
+ if (!params || typeof params !== 'object' || Array.isArray(params)) {
366
+ throw createContractViolation(ContractViolationError, spec.id, `Spec "${spec.id}" expects named parameters.`);
367
+ }
368
+ return params;
369
+ }
370
+ function collectWhereParameterNames(ContractViolationError, spec, tokens, bounds) {
371
+ if (!bounds) {
372
+ return [];
373
+ }
374
+ const names = new Set();
375
+ for (let index = bounds.startTokenIndex; index < bounds.endTokenIndex; index += 1) {
376
+ const token = tokens[index];
377
+ if (token.kind !== 'parameter') {
378
+ continue;
379
+ }
380
+ const name = normalizeNamedParameter(token.value);
381
+ if (!name) {
382
+ throw createContractViolation(ContractViolationError, spec.id, `Spec "${spec.id}" requires named parameters in WHERE clauses.`);
383
+ }
384
+ names.add(name);
385
+ }
386
+ return Array.from(names);
387
+ }
388
+ function assertWherePolicy(ContractViolationError, spec, statementKind, whereBounds, params, requireWhereClause, requireAllNamedParams, tokens) {
389
+ if (requireWhereClause && !whereBounds) {
390
+ throw createContractViolation(ContractViolationError, spec.id, `${statementKind === 'update' ? 'Update' : 'Delete'} spec "${spec.id}" requires a WHERE clause.`);
391
+ }
392
+ if (!whereBounds || !requireAllNamedParams) {
393
+ return;
394
+ }
395
+ for (const name of collectWhereParameterNames(ContractViolationError, spec, tokens, whereBounds)) {
396
+ if (!Object.prototype.hasOwnProperty.call(params, name) || params[name] === undefined) {
397
+ throw createContractViolation(ContractViolationError, spec.id, `Spec "${spec.id}" is missing required WHERE parameter ":${name}".`);
398
+ }
399
+ }
400
+ }
401
+ function splitAssignmentSegments(tokens, bounds) {
402
+ return splitTopLevelSegments(tokens, bounds);
403
+ }
404
+ function splitTopLevelSegments(tokens, bounds) {
405
+ const segments = [];
406
+ let depth = 0;
407
+ let segmentStart = bounds.startTokenIndex;
408
+ for (let index = bounds.startTokenIndex; index < bounds.endTokenIndex; index += 1) {
409
+ const token = tokens[index];
410
+ if (token.kind === 'openParen') {
411
+ depth += 1;
412
+ continue;
413
+ }
414
+ if (token.kind === 'closeParen') {
415
+ depth = Math.max(depth - 1, 0);
416
+ continue;
417
+ }
418
+ if (depth === 0 && token.kind === 'comma') {
419
+ segments.push({
420
+ startTokenIndex: segmentStart,
421
+ endTokenIndex: index,
422
+ });
423
+ segmentStart = index + 1;
424
+ }
425
+ }
426
+ if (segmentStart < bounds.endTokenIndex) {
427
+ segments.push({
428
+ startTokenIndex: segmentStart,
429
+ endTokenIndex: bounds.endTokenIndex,
430
+ });
431
+ }
432
+ return segments;
433
+ }
434
+ function findMatchingParen(tokens, openTokenIndex) {
435
+ let depth = 0;
436
+ for (let index = openTokenIndex; index < tokens.length; index += 1) {
437
+ const token = tokens[index];
438
+ if (token.kind === 'openParen') {
439
+ depth += 1;
440
+ continue;
441
+ }
442
+ if (token.kind === 'closeParen') {
443
+ depth -= 1;
444
+ if (depth === 0) {
445
+ return index;
446
+ }
447
+ }
448
+ }
449
+ return -1;
450
+ }
451
+ function findInsertClauseBounds(statement) {
452
+ let columnsOpenIndex = -1;
453
+ for (let index = statement.statementTokenIndex + 1; index < statement.tokens.length; index += 1) {
454
+ const token = statement.tokens[index];
455
+ if (token.kind === 'other' && token.value === ';') {
456
+ return null;
457
+ }
458
+ if (token.kind === 'openParen') {
459
+ columnsOpenIndex = index;
460
+ break;
461
+ }
462
+ }
463
+ if (columnsOpenIndex < 0) {
464
+ return null;
465
+ }
466
+ const columnsCloseIndex = findMatchingParen(statement.tokens, columnsOpenIndex);
467
+ if (columnsCloseIndex < 0) {
468
+ return null;
469
+ }
470
+ let valuesWordIndex = -1;
471
+ for (let index = columnsCloseIndex + 1; index < statement.tokens.length; index += 1) {
472
+ const token = statement.tokens[index];
473
+ if (token.kind === 'other' && token.value === ';') {
474
+ return null;
475
+ }
476
+ if (token.kind !== 'word') {
477
+ continue;
478
+ }
479
+ const word = lowerWord(token);
480
+ if (word === 'values') {
481
+ valuesWordIndex = index;
482
+ break;
483
+ }
484
+ if (word === 'select') {
485
+ return null;
486
+ }
487
+ }
488
+ if (valuesWordIndex < 0) {
489
+ return null;
490
+ }
491
+ let valuesOpenIndex = -1;
492
+ for (let index = valuesWordIndex + 1; index < statement.tokens.length; index += 1) {
493
+ const token = statement.tokens[index];
494
+ if (token.kind === 'other' && token.value === ';') {
495
+ return null;
496
+ }
497
+ if (token.kind === 'openParen') {
498
+ valuesOpenIndex = index;
499
+ break;
500
+ }
501
+ }
502
+ if (valuesOpenIndex < 0) {
503
+ return null;
504
+ }
505
+ const valuesCloseIndex = findMatchingParen(statement.tokens, valuesOpenIndex);
506
+ if (valuesCloseIndex < 0) {
507
+ return null;
508
+ }
509
+ let parenDepth = 0;
510
+ for (let index = valuesCloseIndex + 1; index < statement.tokens.length; index += 1) {
511
+ const token = statement.tokens[index];
512
+ if (token.kind === 'other' && token.value === ';' && parenDepth === 0) {
513
+ break;
514
+ }
515
+ if (token.kind === 'openParen') {
516
+ parenDepth += 1;
517
+ continue;
518
+ }
519
+ if (token.kind === 'closeParen') {
520
+ parenDepth = Math.max(parenDepth - 1, 0);
521
+ continue;
522
+ }
523
+ if (token.kind === 'word' && lowerWord(token) === 'returning' && parenDepth === 0) {
524
+ break;
525
+ }
526
+ if (token.kind === 'comma' && parenDepth === 0) {
527
+ return null;
528
+ }
529
+ }
530
+ return {
531
+ columns: {
532
+ startTokenIndex: columnsOpenIndex + 1,
533
+ endTokenIndex: columnsCloseIndex,
534
+ },
535
+ values: {
536
+ startTokenIndex: valuesOpenIndex + 1,
537
+ endTokenIndex: valuesCloseIndex,
538
+ },
539
+ };
540
+ }
541
+ function isSimpleIdentifierChain(tokens) {
542
+ if (tokens.length === 0 || tokens.length % 2 === 0) {
543
+ return false;
544
+ }
545
+ for (let index = 0; index < tokens.length; index += 1) {
546
+ const token = tokens[index];
547
+ if (index % 2 === 0) {
548
+ if (token.kind !== 'word') {
549
+ return false;
550
+ }
551
+ continue;
552
+ }
553
+ if (token.kind !== 'dot') {
554
+ return false;
555
+ }
556
+ }
557
+ return true;
558
+ }
559
+ function isSubtractableAssignment(segmentSql, segmentTokens) {
560
+ if (segmentSql.includes('/*') || segmentSql.includes('--')) {
561
+ return null;
562
+ }
563
+ const equalsIndex = segmentTokens.findIndex((token) => token.kind === 'operator' && token.value === '=');
564
+ if (equalsIndex <= 0 || equalsIndex !== segmentTokens.length - 2) {
565
+ return null;
566
+ }
567
+ if (!isSimpleIdentifierChain(segmentTokens.slice(0, equalsIndex))) {
568
+ return null;
569
+ }
570
+ const rhs = segmentTokens[segmentTokens.length - 1];
571
+ if (rhs.kind !== 'parameter') {
572
+ return null;
573
+ }
574
+ const name = normalizeNamedParameter(rhs.value);
575
+ return name ? { name } : null;
576
+ }
577
+ function isSubtractableInsertValue(segmentSql, segmentTokens) {
578
+ if (segmentSql.includes('/*') || segmentSql.includes('--') || segmentTokens.length !== 1) {
579
+ return null;
580
+ }
581
+ const token = segmentTokens[0];
582
+ if (token.kind !== 'parameter') {
583
+ return null;
584
+ }
585
+ const name = normalizeNamedParameter(token.value);
586
+ return name ? { name } : null;
587
+ }
588
+ function rewriteInsertValuesClause(ContractViolationError, spec, sql, tokens, statement, params, config) {
589
+ if (!config.subtractUndefinedColumns) {
590
+ return { sql, params };
591
+ }
592
+ const bounds = findInsertClauseBounds(statement);
593
+ if (!bounds) {
594
+ return { sql, params };
595
+ }
596
+ const columnSegments = splitTopLevelSegments(tokens, bounds.columns);
597
+ const valueSegments = splitTopLevelSegments(tokens, bounds.values);
598
+ if (columnSegments.length === 0 || columnSegments.length !== valueSegments.length) {
599
+ return { sql, params };
600
+ }
601
+ const keptColumns = [];
602
+ const keptValues = [];
603
+ const droppedNames = new Set();
604
+ for (let index = 0; index < columnSegments.length; index += 1) {
605
+ const columnTokens = tokens.slice(columnSegments[index].startTokenIndex, columnSegments[index].endTokenIndex);
606
+ const valueTokens = tokens.slice(valueSegments[index].startTokenIndex, valueSegments[index].endTokenIndex);
607
+ if (columnTokens.length === 0 || valueTokens.length === 0) {
608
+ return { sql, params };
609
+ }
610
+ const columnSql = sql.slice(columnTokens[0].start, columnTokens[columnTokens.length - 1].end).trim();
611
+ const valueSql = sql.slice(valueTokens[0].start, valueTokens[valueTokens.length - 1].end).trim();
612
+ const subtractable = isSubtractableInsertValue(valueSql, valueTokens);
613
+ // Phase 1 only subtracts direct named placeholders from single-row VALUES inserts.
614
+ if (subtractable &&
615
+ (!Object.prototype.hasOwnProperty.call(params, subtractable.name) ||
616
+ params[subtractable.name] === undefined)) {
617
+ droppedNames.add(subtractable.name);
618
+ continue;
619
+ }
620
+ keptColumns.push(columnSql);
621
+ keptValues.push(valueSql);
622
+ }
623
+ if (config.failOnEmptyColumns && keptColumns.length === 0) {
624
+ throw createContractViolation(ContractViolationError, spec.id, `Insert spec "${spec.id}" removed every insert column because all values were undefined/missing.`);
625
+ }
626
+ if (keptColumns.length === 0 || droppedNames.size === 0) {
627
+ return { sql, params };
628
+ }
629
+ const columnsStart = tokens[bounds.columns.startTokenIndex].start;
630
+ const columnsEnd = tokens[bounds.columns.endTokenIndex - 1].end;
631
+ const valuesStart = tokens[bounds.values.startTokenIndex].start;
632
+ const valuesEnd = tokens[bounds.values.endTokenIndex - 1].end;
633
+ const trailingTokens = tokenizeSql(sql.slice(valuesEnd));
634
+ const removableNames = new Set();
635
+ // Keep params when they are still referenced after VALUES (e.g. ON CONFLICT ... DO UPDATE).
636
+ for (const name of droppedNames) {
637
+ const isReferencedLater = trailingTokens.some((token) => {
638
+ if (token.kind !== 'parameter') {
639
+ return false;
640
+ }
641
+ return normalizeNamedParameter(token.value) === name;
642
+ });
643
+ if (!isReferencedLater) {
644
+ removableNames.add(name);
645
+ }
646
+ }
647
+ return {
648
+ sql: `${sql.slice(0, columnsStart)}${keptColumns.join(', ')}` +
649
+ `${sql.slice(columnsEnd, valuesStart)}${keptValues.join(', ')}` +
650
+ sql.slice(valuesEnd),
651
+ params: removableNames.size > 0
652
+ ? removeNamedParams(params, removableNames)
653
+ : params,
654
+ };
655
+ }
656
+ function rewriteUpdateSetClause(ContractViolationError, spec, sql, tokens, setBounds, params, config) {
657
+ if (!setBounds || !config.subtractUndefinedAssignments) {
658
+ return sql;
659
+ }
660
+ const keptSegments = [];
661
+ for (const segment of splitAssignmentSegments(tokens, setBounds)) {
662
+ const segmentTokens = tokens.slice(segment.startTokenIndex, segment.endTokenIndex);
663
+ if (segmentTokens.length === 0) {
664
+ continue;
665
+ }
666
+ const segmentStart = segmentTokens[0].start;
667
+ const segmentEnd = segmentTokens[segmentTokens.length - 1].end;
668
+ const segmentSql = sql.slice(segmentStart, segmentEnd);
669
+ const subtractable = isSubtractableAssignment(segmentSql, segmentTokens);
670
+ if (subtractable &&
671
+ (!Object.prototype.hasOwnProperty.call(params, subtractable.name) ||
672
+ params[subtractable.name] === undefined)) {
673
+ continue;
674
+ }
675
+ keptSegments.push(segmentSql.trim());
676
+ }
677
+ if (config.failOnEmptySet && keptSegments.length === 0) {
678
+ throw createContractViolation(ContractViolationError, spec.id, `Update spec "${spec.id}" removed every SET assignment.`);
679
+ }
680
+ if (keptSegments.length === 0) {
681
+ return sql;
682
+ }
683
+ const setStart = tokens[setBounds.startTokenIndex].start;
684
+ const setEnd = tokens[setBounds.endTokenIndex - 1].end;
685
+ return `${sql.slice(0, setStart)}${keptSegments.join(', ')}${sql.slice(setEnd)}`;
686
+ }
687
+ function preprocessMutationSpec(ContractViolationError, spec, sql, params) {
688
+ if (!spec.mutation) {
689
+ return {
690
+ sql,
691
+ params,
692
+ mutation: undefined,
693
+ };
694
+ }
695
+ if (spec.params.shape !== 'named') {
696
+ throw createContractViolation(ContractViolationError, spec.id, `Spec "${spec.id}" declares mutation processing but does not use named parameters.`);
697
+ }
698
+ const rawNamedParams = assertNamedMutationParams(ContractViolationError, spec, params);
699
+ const tokens = tokenizeSql(sql);
700
+ const statement = detectStatement(tokens);
701
+ if (spec.mutation.kind === 'insert') {
702
+ if (statement.kind !== 'insert') {
703
+ throw createContractViolation(ContractViolationError, spec.id, `Spec "${spec.id}" declares an insert mutation but the SQL is not an INSERT statement.`);
704
+ }
705
+ const config = normalizeInsertConfig(spec);
706
+ const rewritten = rewriteInsertValuesClause(ContractViolationError, spec, sql, tokens, statement, rawNamedParams, config);
707
+ return {
708
+ sql: rewritten.sql,
709
+ params: rewritten.params,
710
+ mutation: { kind: 'insert', insert: config },
711
+ };
712
+ }
713
+ const namedParams = sanitizeNamedParams(rawNamedParams);
714
+ if (spec.mutation.kind === 'update') {
715
+ if (statement.kind !== 'update') {
716
+ throw createContractViolation(ContractViolationError, spec.id, `Spec "${spec.id}" declares an update mutation but the SQL is not an UPDATE statement.`);
717
+ }
718
+ const config = normalizeUpdateConfig(spec);
719
+ const whereBounds = findClauseBounds(statement, 'where', ['returning']);
720
+ assertWherePolicy(ContractViolationError, spec, 'update', whereBounds, namedParams, config.where.requireWhereClause, config.where.requireAllNamedParams, tokens);
721
+ const setBounds = findClauseBounds(statement, 'set', ['from', 'where', 'returning']);
722
+ return {
723
+ sql: rewriteUpdateSetClause(ContractViolationError, spec, sql, tokens, setBounds, namedParams, config),
724
+ params: namedParams,
725
+ mutation: {
726
+ kind: 'update',
727
+ update: config,
728
+ },
729
+ };
730
+ }
731
+ if (statement.kind !== 'delete') {
732
+ throw createContractViolation(ContractViolationError, spec.id, `Spec "${spec.id}" declares a delete mutation but the SQL is not a DELETE statement.`);
733
+ }
734
+ const config = normalizeDeleteConfig(spec);
735
+ const whereBounds = findClauseBounds(statement, 'where', ['returning']);
736
+ assertWherePolicy(ContractViolationError, spec, 'delete', whereBounds, namedParams, config.where.requireWhereClause, config.where.requireAllNamedParams, tokens);
737
+ return {
738
+ sql,
739
+ params: namedParams,
740
+ mutation: {
741
+ kind: 'delete',
742
+ delete: config,
743
+ },
744
+ };
745
+ }
746
+ function isMutationSafeRewriter(rewriter) {
747
+ return (!!rewriter &&
748
+ typeof rewriter === 'object' &&
749
+ 'mutationSafety' in rewriter &&
750
+ rewriter.mutationSafety === 'safe');
751
+ }
752
+ function assertMutationSafeRewriters(ContractViolationError, spec, rewriters) {
753
+ var _a;
754
+ for (const rewriter of rewriters) {
755
+ if (!isMutationSafeRewriter(rewriter)) {
756
+ const name = rewriter && typeof rewriter === 'object' && 'name' in rewriter
757
+ ? String((_a = rewriter.name) !== null && _a !== void 0 ? _a : 'unknown')
758
+ : 'unknown';
759
+ throw createContractViolation(ContractViolationError, spec.id, `Spec "${spec.id}" uses rewriter "${name}", which is not allowed for mutation preprocessing in Phase 1.`);
760
+ }
761
+ }
762
+ }
763
+ function assertDeleteGuard(ContractViolationError, spec, mutation, rowCount) {
764
+ if (!mutation || mutation.kind !== 'delete') {
765
+ return;
766
+ }
767
+ const guard = mutation.delete.affectedRowsGuard;
768
+ if (guard.mode === 'none') {
769
+ return;
770
+ }
771
+ if (rowCount === undefined) {
772
+ throw createContractViolation(ContractViolationError, spec.id, `Delete spec "${spec.id}" requires affected row count, but the configured executor did not expose rowCount. Disable the guard explicitly or use an executor that returns rowCount.`);
773
+ }
774
+ if (rowCount !== guard.count) {
775
+ throw createContractViolation(ContractViolationError, spec.id, `Delete spec "${spec.id}" expected exactly ${guard.count} affected row${guard.count === 1 ? '' : 's'} but received ${rowCount}.`);
776
+ }
777
+ }
778
+ //# sourceMappingURL=mutation.js.map