@drzl/validation-core 3.20.0 → 3.22.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/dist/index.cjs CHANGED
@@ -30,10 +30,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ AFFIX_PREFIX_PATTERN: () => AFFIX_PREFIX_PATTERN,
33
34
  AFFIX_PROBE_TABLE: () => AFFIX_PROBE_TABLE,
35
+ AFFIX_SUFFIX_PATTERN: () => AFFIX_SUFFIX_PATTERN,
34
36
  CODEPOINT_LENGTH: () => CODEPOINT_LENGTH,
35
37
  COERCIBLE_DATE_STRING: () => COERCIBLE_DATE_STRING,
36
38
  COLUMN_FORMATS: () => COLUMN_FORMATS,
39
+ CONSTRAINTS_MODULE: () => CONSTRAINTS_MODULE,
37
40
  DEFAULT_IMPORT_EXTENSION: () => DEFAULT_IMPORT_EXTENSION,
38
41
  DEFAULT_MODE_PREFIX: () => DEFAULT_MODE_PREFIX,
39
42
  DEFAULT_NESTED_DEPTH: () => DEFAULT_NESTED_DEPTH,
@@ -43,37 +46,56 @@ __export(index_exports, {
43
46
  MAX_NESTED_DEPTH: () => MAX_NESTED_DEPTH,
44
47
  NAME_MODES: () => NAME_MODES,
45
48
  NESTED_PREFIX: () => NESTED_PREFIX,
49
+ NUMERIC_CANON_NAME: () => NUMERIC_CANON_NAME,
50
+ NUMERIC_CANON_SOURCE: () => NUMERIC_CANON_SOURCE,
46
51
  applyTableCase: () => applyTableCase,
52
+ applyWirePolicy: () => applyWirePolicy,
47
53
  buildBrandPlan: () => buildBrandPlan,
48
54
  buildNestedPlan: () => buildNestedPlan,
55
+ canonicalMembers: () => canonicalMembers,
56
+ canonicalNumericText: () => canonicalNumericText,
57
+ classifyTableChecks: () => classifyTableChecks,
49
58
  columnMetaFacts: () => columnMetaFacts,
59
+ comparisonWire: () => comparisonWire,
50
60
  describeSet: () => describeSet,
61
+ fileWriter: () => fileWriter,
51
62
  formatCode: () => formatCode,
52
63
  importSpecifier: () => importSpecifier,
53
64
  insertColumns: () => insertColumns,
54
65
  isGeneratedColumn: () => isGeneratedColumn,
55
66
  isIntegerColumn: () => isIntegerColumn,
67
+ isProjectInstallPath: () => isProjectInstallPath,
68
+ lengthCheckLabel: () => lengthCheckLabel,
69
+ lengthMeasure: () => lengthMeasure,
70
+ measureExpression: () => measureExpression,
56
71
  moduleFileName: () => moduleFileName,
57
72
  moduleSpecifier: () => moduleSpecifier,
73
+ needsNumericCanon: () => needsNumericCanon,
58
74
  nestedArmNotes: () => nestedArmNotes,
59
75
  nestedNodeColumns: () => nestedNodeColumns,
60
76
  nestedSchemaName: () => nestedSchemaName,
61
77
  nestedTypeName: () => nestedTypeName,
62
78
  nonFiniteAccepted: () => nonFiniteAccepted,
79
+ nonFiniteRefused: () => nonFiniteRefused,
63
80
  parseCheck: () => parseCheck,
64
81
  parsesToADate: () => parsesToADate,
65
82
  pascalCase: () => pascalCase,
83
+ renderConstraintsModule: () => renderConstraintsModule,
66
84
  renderDuplicateFinder: () => renderDuplicateFinder,
67
85
  resolveAffix: () => resolveAffix,
68
86
  resolveBranding: () => resolveBranding,
69
87
  resolveConfiguredImport: () => resolveConfiguredImport,
88
+ resolveConstraints: () => resolveConstraints,
70
89
  resolveNestedDepth: () => resolveNestedDepth,
71
90
  schemaName: () => schemaName,
72
91
  selectColumns: () => selectColumns,
92
+ tableConstraints: () => tableConstraints,
73
93
  tableMetaFacts: () => tableMetaFacts,
74
94
  typeName: () => typeName,
75
95
  updateColumns: () => updateColumns,
76
- validateAffix: () => validateAffix
96
+ validateAffix: () => validateAffix,
97
+ wireLiteralFit: () => wireLiteralFit,
98
+ wireNumberLiteral: () => wireNumberLiteral
77
99
  });
78
100
  module.exports = __toCommonJS(index_exports);
79
101
  var import_node_child_process = require("child_process");
@@ -82,6 +104,542 @@ var import_node_module = require("module");
82
104
  var import_node_path2 = __toESM(require("path"), 1);
83
105
  var import_node_url = require("url");
84
106
 
107
+ // src/checks.ts
108
+ function lengthMeasure(column, check) {
109
+ if (column.arrayDimensions) return void 0;
110
+ if (column.shape?.kind === "buffer") return "byteLength";
111
+ if (column.shape) return void 0;
112
+ if (column.tsType !== "string") return void 0;
113
+ return check.unit === "bytes" ? "utf8Bytes" : "codePoints";
114
+ }
115
+ function measureExpression(measure, variable) {
116
+ switch (measure) {
117
+ case "codePoints":
118
+ return `[...${variable}].length`;
119
+ case "utf8Bytes":
120
+ return `new TextEncoder().encode(${variable}).length`;
121
+ case "byteLength":
122
+ return `${variable}.length`;
123
+ }
124
+ }
125
+ function lengthCheckLabel(check) {
126
+ const fn = check.unit === "bytes" ? "octet_length" : "length";
127
+ const rule = `${fn}(${check.column}) ${check.operator} ${check.value}`;
128
+ return check.name ? `${check.name}: ${rule}` : rule;
129
+ }
130
+ var COMPARISON = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(>=|<=|<>|!=|>|<|=)\s*(.+?)\s*$/;
131
+ var IN_LIST = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IN\s*\((.+)\)\s*$/i;
132
+ var CARDINALITY_OF = /^\s*(?:cardinality\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)|array_length\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,\s*1\s*\))\s*(>=|<=|<>|!=|>|<|=)\s*(\d+)\s*$/i;
133
+ var LENGTH_OF = /^\s*(length|char_length|octet_length)\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)\s*(>=|<=|<>|!=|>|<|=)\s*(\d+)\s*$/i;
134
+ var WORD = /[A-Za-z0-9_]/;
135
+ function splitTopLevel(expr, keyword) {
136
+ const parts = [];
137
+ let depth = 0;
138
+ let inString = false;
139
+ let start = 0;
140
+ for (let i = 0; i < expr.length; i++) {
141
+ const c = expr[i];
142
+ if (inString) {
143
+ if (c === "'") {
144
+ if (expr[i + 1] === "'") i++;
145
+ else inString = false;
146
+ }
147
+ continue;
148
+ }
149
+ if (c === "'") {
150
+ inString = true;
151
+ continue;
152
+ }
153
+ if (c === "(") depth++;
154
+ else if (c === ")") depth--;
155
+ else if (depth === 0 && WORD.test(c)) {
156
+ if (i > 0 && WORD.test(expr[i - 1])) continue;
157
+ if (expr.slice(i, i + keyword.length).toUpperCase() !== keyword) continue;
158
+ const after = expr[i + keyword.length];
159
+ if (after !== void 0 && WORD.test(after)) continue;
160
+ parts.push(expr.slice(start, i));
161
+ i += keyword.length - 1;
162
+ start = i + 1;
163
+ }
164
+ }
165
+ parts.push(expr.slice(start));
166
+ return parts;
167
+ }
168
+ function hasLogicalNot(expr) {
169
+ let inString = false;
170
+ for (let i = 0; i < expr.length; i++) {
171
+ const c = expr[i];
172
+ if (inString) {
173
+ if (c === "'") {
174
+ if (expr[i + 1] === "'") i++;
175
+ else inString = false;
176
+ }
177
+ continue;
178
+ }
179
+ if (c === "'") {
180
+ inString = true;
181
+ continue;
182
+ }
183
+ if (c !== "N" && c !== "n") continue;
184
+ if (i > 0 && WORD.test(expr[i - 1])) continue;
185
+ if (!/^NOT($|[^A-Za-z0-9_])/i.test(expr.slice(i))) continue;
186
+ if (/(^|[^A-Za-z0-9_])IS\s+$/i.test(expr.slice(0, i))) continue;
187
+ return true;
188
+ }
189
+ return false;
190
+ }
191
+ var COMBINING = /(?:^|\s)(\|\||[+\-*/%])(?:\s)/;
192
+ function combiningOperator(expr) {
193
+ let inString = false;
194
+ let bare = "";
195
+ for (let i = 0; i < expr.length; i++) {
196
+ const c = expr[i];
197
+ if (inString) {
198
+ if (c === "'") {
199
+ if (expr[i + 1] === "'") i++;
200
+ else inString = false;
201
+ }
202
+ continue;
203
+ }
204
+ if (c === "'") {
205
+ inString = true;
206
+ bare += " ";
207
+ continue;
208
+ }
209
+ bare += c;
210
+ }
211
+ return COMBINING.exec(bare)?.[1];
212
+ }
213
+ function unwrap(expr) {
214
+ let e = expr.trim();
215
+ while (e.startsWith("(") && e.endsWith(")")) {
216
+ let depth = 0;
217
+ let inString = false;
218
+ let wrapsWhole = true;
219
+ for (let i = 0; i < e.length; i++) {
220
+ const c = e[i];
221
+ if (inString) {
222
+ if (c === "'") {
223
+ if (e[i + 1] === "'") i++;
224
+ else inString = false;
225
+ }
226
+ continue;
227
+ }
228
+ if (c === "'") inString = true;
229
+ else if (c === "(") depth++;
230
+ else if (c === ")") {
231
+ depth--;
232
+ if (depth === 0 && i < e.length - 1) {
233
+ wrapsWhole = false;
234
+ break;
235
+ }
236
+ }
237
+ }
238
+ if (!wrapsWhole) break;
239
+ e = e.slice(1, -1).trim();
240
+ }
241
+ return e;
242
+ }
243
+ function splitTopLevelCommas(list2) {
244
+ const parts = [];
245
+ let depth = 0;
246
+ let inString = false;
247
+ let start = 0;
248
+ for (let i = 0; i < list2.length; i++) {
249
+ const c = list2[i];
250
+ if (inString) {
251
+ if (c === "'") {
252
+ if (list2[i + 1] === "'") i++;
253
+ else inString = false;
254
+ }
255
+ continue;
256
+ }
257
+ if (c === "'") inString = true;
258
+ else if (c === "(") depth++;
259
+ else if (c === ")") depth--;
260
+ else if (c === "," && depth === 0) {
261
+ parts.push(list2.slice(start, i));
262
+ start = i + 1;
263
+ }
264
+ }
265
+ parts.push(list2.slice(start));
266
+ return parts;
267
+ }
268
+ var BETWEEN = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+BETWEEN\s+(.+?)\s+AND\s+(.+?)\s*$/i;
269
+ var IS_NULL = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IS\s+(NOT\s+)?NULL\s*$/i;
270
+ var IS_DISTINCT = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IS\s+(NOT\s+)?DISTINCT\s+FROM\s+(.+?)\s*$/i;
271
+ var IS_BOOLEAN = /^\s*[A-Za-z_][A-Za-z0-9_]*\s+IS\s+(?:NOT\s+)?(TRUE|FALSE|UNKNOWN)\s*$/i;
272
+ function literal(raw) {
273
+ const t = raw.trim();
274
+ if (/^-?\d+(\.\d+)?$/.test(t)) return { value: t, kind: "number" };
275
+ const m = t.match(/^'((?:[^']|'')*)'$/);
276
+ if (m) return { value: m[1].replace(/''/g, "'"), kind: "string" };
277
+ return void 0;
278
+ }
279
+ function columnsOf(parsed) {
280
+ const out = /* @__PURE__ */ new Set();
281
+ for (const c of parsed.checks) out.add(c.column);
282
+ for (const s of parsed.sets ?? []) out.add(s.column);
283
+ for (const l of parsed.lengths ?? []) out.add(l.column);
284
+ for (const a of parsed.cardinalities ?? []) out.add(a.column);
285
+ for (const n of parsed.nulls ?? []) out.add(n.column);
286
+ for (const r of parsed.rows ?? []) {
287
+ out.add(r.left);
288
+ out.add(r.right);
289
+ }
290
+ return out;
291
+ }
292
+ function parseDisjunction(branches, name) {
293
+ const guarded = [];
294
+ const rest = [];
295
+ for (const b of branches) {
296
+ const m = unwrap(b).match(IS_NULL);
297
+ if (m && !m[2]) guarded.push(m[1]);
298
+ else rest.push(b);
299
+ }
300
+ const reduced = (() => {
301
+ if (!rest.length)
302
+ return {
303
+ ok: false,
304
+ reason: "every branch of the OR is a null test, which is a rule about the row"
305
+ };
306
+ if (rest.length === 1) return parseCheck(rest[0], name);
307
+ return foldDisjunctionToSet(rest, name);
308
+ })();
309
+ if (!reduced.ok || !guarded.length) return reduced;
310
+ if (reduced.nulls?.length)
311
+ return {
312
+ ok: false,
313
+ reason: "a null test guarded by IS NULL, which is true of every row rather than a narrowing"
314
+ };
315
+ const named = columnsOf(reduced);
316
+ const stray = guarded.filter((g) => !named.has(g));
317
+ if (stray.length)
318
+ return {
319
+ ok: false,
320
+ reason: `${stray.map((s) => `"${s}"`).join(" and ")} IS NULL guards a predicate that does not name it`
321
+ };
322
+ return reduced;
323
+ }
324
+ function foldDisjunctionToSet(branches, name) {
325
+ let column;
326
+ let kind;
327
+ const values = [];
328
+ for (const branch of branches) {
329
+ const parsed = parseCheck(branch, name);
330
+ if (!parsed.ok)
331
+ return { ok: false, reason: `part of an OR was not understood: ${parsed.reason}` };
332
+ if (parsed.rows?.length)
333
+ return {
334
+ ok: false,
335
+ reason: "a branch of the OR compares two columns, which is a rule about the row"
336
+ };
337
+ if (parsed.lengths?.length || parsed.cardinalities?.length)
338
+ return {
339
+ ok: false,
340
+ reason: "a branch of the OR is a count rather than a value, so the OR states no set"
341
+ };
342
+ if (parsed.nulls?.length)
343
+ return { ok: false, reason: "a branch of the OR is a null test rather than a value" };
344
+ const set = parsed.sets?.[0];
345
+ const range = parsed.checks.find((c) => c.operator !== "=");
346
+ if (range)
347
+ return {
348
+ ok: false,
349
+ reason: `a branch of the OR is a range (${range.column} ${range.operator} ${range.value}) rather than a set of values`
350
+ };
351
+ if (parsed.checks.length + (parsed.sets?.length ?? 0) !== 1)
352
+ return { ok: false, reason: "a branch of the OR states more than one thing" };
353
+ const here = set ? { column: set.column, kind: set.kind, values: set.values } : {
354
+ column: parsed.checks[0].column,
355
+ kind: parsed.checks[0].kind,
356
+ values: [parsed.checks[0].value]
357
+ };
358
+ if (column === void 0) {
359
+ column = here.column;
360
+ kind = here.kind;
361
+ }
362
+ if (here.column !== column)
363
+ return {
364
+ ok: false,
365
+ reason: `the OR branches constrain different columns (${column}, ${here.column}), so it states a rule about the row rather than about a field`
366
+ };
367
+ if (here.kind !== kind)
368
+ return { ok: false, reason: "the OR branches mix a string and a number" };
369
+ for (const v of here.values) if (!values.includes(v)) values.push(v);
370
+ }
371
+ return {
372
+ ok: true,
373
+ checks: [],
374
+ sets: [{ column, values, kind, name }]
375
+ };
376
+ }
377
+ function parseCheck(expression, name) {
378
+ const expr = unwrap((expression ?? "").trim());
379
+ if (!expr) return { ok: false, reason: "empty expression" };
380
+ if (expr.includes("?")) return { ok: false, reason: "expression contains an unresolved value" };
381
+ const branches = splitTopLevel(expr, "OR");
382
+ if (branches.length > 1) return parseDisjunction(branches, name);
383
+ if (hasLogicalNot(expr)) return { ok: false, reason: "contains NOT" };
384
+ const between = expr.match(BETWEEN);
385
+ if (between) {
386
+ const lo = literal(between[2]);
387
+ const hi = literal(between[3]);
388
+ if (!lo || !hi) return { ok: false, reason: "BETWEEN bounds are not literals" };
389
+ if (lo.kind !== hi.kind) return { ok: false, reason: "BETWEEN bounds are of mixed types" };
390
+ return {
391
+ ok: true,
392
+ checks: [
393
+ { column: between[1], operator: ">=", value: lo.value, kind: lo.kind, name },
394
+ { column: between[1], operator: "<=", value: hi.value, kind: hi.kind, name }
395
+ ]
396
+ };
397
+ }
398
+ const parts = splitTopLevel(expr, "AND");
399
+ if (parts.length > 1) {
400
+ const checks = [];
401
+ const sets = [];
402
+ const rows = [];
403
+ const lengths = [];
404
+ const cardinalities = [];
405
+ const nulls = [];
406
+ for (const part of parts) {
407
+ const parsed = parseCheck(part, name);
408
+ if (!parsed.ok)
409
+ return { ok: false, reason: `part of an AND was not understood: ${parsed.reason}` };
410
+ checks.push(...parsed.checks);
411
+ if (parsed.sets) sets.push(...parsed.sets);
412
+ if (parsed.rows) rows.push(...parsed.rows);
413
+ if (parsed.lengths) lengths.push(...parsed.lengths);
414
+ if (parsed.cardinalities) cardinalities.push(...parsed.cardinalities);
415
+ if (parsed.nulls) nulls.push(...parsed.nulls);
416
+ }
417
+ return {
418
+ ok: true,
419
+ checks,
420
+ ...sets.length ? { sets } : {},
421
+ ...rows.length ? { rows } : {},
422
+ ...lengths.length ? { lengths } : {},
423
+ ...cardinalities.length ? { cardinalities } : {},
424
+ ...nulls.length ? { nulls } : {}
425
+ };
426
+ }
427
+ const isNull = expr.match(IS_NULL);
428
+ if (isNull) {
429
+ return {
430
+ ok: true,
431
+ checks: [],
432
+ nulls: [{ column: isNull[1], notNull: !!isNull[2], ...name ? { name } : {} }]
433
+ };
434
+ }
435
+ const isDistinct = expr.match(IS_DISTINCT);
436
+ if (isDistinct) {
437
+ const value2 = literal(isDistinct[3]);
438
+ if (!value2) return { ok: false, reason: "the right side of IS DISTINCT FROM is not a literal" };
439
+ const column = isDistinct[1];
440
+ const negated = !!isDistinct[2];
441
+ return {
442
+ ok: true,
443
+ checks: [
444
+ { column, operator: negated ? "=" : "<>", value: value2.value, kind: value2.kind, name }
445
+ ],
446
+ ...negated ? { nulls: [{ column, notNull: true, ...name ? { name } : {} }] } : {}
447
+ };
448
+ }
449
+ if (IS_BOOLEAN.test(expr))
450
+ return { ok: false, reason: "a boolean IS test, whose literal this version does not read" };
451
+ const lengthOf = expr.match(LENGTH_OF);
452
+ if (lengthOf) {
453
+ const op2 = lengthOf[3] === "!=" ? "<>" : lengthOf[3];
454
+ const unit = lengthOf[1].toLowerCase() === "octet_length" ? "bytes" : "characters";
455
+ return {
456
+ ok: true,
457
+ checks: [],
458
+ lengths: [{ column: lengthOf[2], operator: op2, value: lengthOf[4], unit, name }]
459
+ };
460
+ }
461
+ const cardinalityOf = expr.match(CARDINALITY_OF);
462
+ if (cardinalityOf) {
463
+ const op2 = cardinalityOf[3] === "!=" ? "<>" : cardinalityOf[3];
464
+ return {
465
+ ok: true,
466
+ checks: [],
467
+ cardinalities: [
468
+ {
469
+ column: cardinalityOf[1] ?? cardinalityOf[2],
470
+ operator: op2,
471
+ value: cardinalityOf[4],
472
+ ...name ? { name } : {}
473
+ }
474
+ ]
475
+ };
476
+ }
477
+ const inList = expr.match(IN_LIST);
478
+ if (inList) {
479
+ const raw = splitTopLevelCommas(inList[2]);
480
+ const parsedValues = raw.map((r) => literal(r));
481
+ if (parsedValues.some((v) => !v)) return { ok: false, reason: "IN list holds a non-literal" };
482
+ const kinds = new Set(parsedValues.map((v) => v.kind));
483
+ if (kinds.size > 1) return { ok: false, reason: "IN list mixes types" };
484
+ if (!parsedValues.length) return { ok: false, reason: "IN list is empty" };
485
+ return {
486
+ ok: true,
487
+ checks: [],
488
+ sets: [
489
+ {
490
+ column: inList[1],
491
+ values: parsedValues.map((v) => v.value),
492
+ kind: parsedValues[0].kind,
493
+ name
494
+ }
495
+ ]
496
+ };
497
+ }
498
+ const combining = combiningOperator(expr);
499
+ const arithmetic = () => ({
500
+ ok: false,
501
+ reason: `columns combined with "${combining}", which this version does not evaluate`
502
+ });
503
+ const cmp = expr.match(COMPARISON);
504
+ if (!cmp) {
505
+ if (combining) return arithmetic();
506
+ return { ok: false, reason: "not a single comparison this version understands" };
507
+ }
508
+ const value = literal(cmp[3]);
509
+ if (!value) {
510
+ const right = cmp[3].trim();
511
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(right)) {
512
+ const op2 = cmp[2] === "!=" ? "<>" : cmp[2];
513
+ return { ok: true, checks: [], rows: [{ left: cmp[1], right, operator: op2, name }] };
514
+ }
515
+ if (combining) return arithmetic();
516
+ return { ok: false, reason: "right side is not a literal" };
517
+ }
518
+ const op = cmp[2] === "!=" ? "<>" : cmp[2];
519
+ return {
520
+ ok: true,
521
+ checks: [{ column: cmp[1], operator: op, value: value.value, kind: value.kind, name }]
522
+ };
523
+ }
524
+ function wireNumberLiteral(column, value) {
525
+ return column.tsType === "bigint" && /^-?\d+$/.test(value) ? `${value}n` : value;
526
+ }
527
+ function describeSet(set) {
528
+ const shown = set.values.map((v) => set.kind === "string" ? `'${v}'` : v).join(", ");
529
+ return `${set.name ? `${set.name}: ` : ""}${set.column} IN (${shown})`;
530
+ }
531
+ function comparisonWire(c) {
532
+ if (c.shape || c.arrayDimensions) return "opaque";
533
+ if (c.tsType === "number") return "number";
534
+ if (c.tsType === "bigint") return "bigint";
535
+ if (c.tsType !== "string") return "opaque";
536
+ return c.dbType === "NUMERIC" || c.dbType === "BIGINT" ? "numeric-string" : "text";
537
+ }
538
+ function canonicalNumericText(text) {
539
+ const m = /^([+-]?)(\d*)(?:\.(\d*))?$/.exec(text.trim());
540
+ if (!m || !m[2] && !m[3]) return void 0;
541
+ const int = (m[2] ?? "").replace(/^0+/, "");
542
+ const frac = (m[3] ?? "").replace(/0+$/, "");
543
+ if (!int && !frac) return "0";
544
+ return (m[1] === "-" ? "-" : "") + (int || "0") + (frac ? "." + frac : "");
545
+ }
546
+ function canonicalMembers(values) {
547
+ const out = [];
548
+ for (const v of values) {
549
+ const c = canonicalNumericText(v);
550
+ if (c !== void 0 && !out.includes(c)) out.push(c);
551
+ }
552
+ return out;
553
+ }
554
+ var NUMERIC_CANON_NAME = "DrzlNumericCanon";
555
+ var NUMERIC_CANON_SOURCE = `/**
556
+ * The canonical spelling of the plain decimal text a numeric wire carries, or null for anything
557
+ * else. The driver spells one value many ways by declared scale ('1', '1.00', '1.0000000000',
558
+ * measured) and the database compares them as numbers, so equality is decided on this form:
559
+ * sign normalised, leading integer zeros and trailing fraction zeros stripped, a bare trailing
560
+ * dot dropped. String arithmetic on purpose: Number() is not usable here, because a numeric
561
+ * column carries more digits than a double holds and rounding would merge values the database
562
+ * keeps distinct.
563
+ */
564
+ const ${NUMERIC_CANON_NAME} = (s: string): string | null => {
565
+ const m = /^([+-]?)(\\d*)(?:\\.(\\d*))?$/.exec(s.trim());
566
+ if (!m || (!m[2] && !m[3])) return null;
567
+ const int = (m[2] ?? '').replace(/^0+/, '');
568
+ const frac = (m[3] ?? '').replace(/0+$/, '');
569
+ if (!int && !frac) return '0';
570
+ return (m[1] === '-' ? '-' : '') + (int || '0') + (frac ? '.' + frac : '');
571
+ };
572
+ `;
573
+ function wireLiteralFit(c, q) {
574
+ const wire = comparisonWire(c);
575
+ if (wire === "opaque") return { fit: "keep" };
576
+ if (wire === "text") {
577
+ if (q.kind === "string") return { fit: "keep" };
578
+ return {
579
+ fit: "unenforced",
580
+ reason: `"${c.name ?? "?"}" is a text column, and the database compares a number literal against it by coercion rules that differ per dialect, which no exact predicate restates`
581
+ };
582
+ }
583
+ const canon = q.values.map((v) => canonicalNumericText(v));
584
+ const bad = q.values[canon.findIndex((x) => x === void 0)];
585
+ if (wire === "numeric-string") {
586
+ if (bad !== void 0)
587
+ return {
588
+ fit: "unenforced",
589
+ reason: `'${bad}' is not plain decimal text, and the driver spells one numeric value many ways, so no exact comparison can be stated`
590
+ };
591
+ if (q.comparison === "range") return { fit: "respell", values: canon };
592
+ return { fit: "canonical", canon: canonicalMembers(q.values) };
593
+ }
594
+ if (q.kind === "number") return { fit: "keep" };
595
+ if (bad !== void 0)
596
+ return {
597
+ fit: "unenforced",
598
+ reason: `'${bad}' is quoted text on a ${wire} wire, and it is not plain decimal, so the numeric comparison the database performs cannot be restated exactly`
599
+ };
600
+ return { fit: "respell", values: canonicalMembers(q.values) };
601
+ }
602
+ function applyWirePolicy(columns, checks, sets) {
603
+ const byName = new Map(columns.map((c) => [c.name, c]));
604
+ const outChecks = [];
605
+ const outSets = [];
606
+ const unenforced = [];
607
+ for (const k of checks) {
608
+ const c = byName.get(k.column);
609
+ if (!c) {
610
+ outChecks.push(k);
611
+ continue;
612
+ }
613
+ const fit = wireLiteralFit(c, {
614
+ kind: k.kind,
615
+ values: [k.value],
616
+ comparison: k.operator === "=" || k.operator === "<>" ? "equality" : "range"
617
+ });
618
+ if (fit.fit === "unenforced") unenforced.push({ column: k.column, reason: fit.reason, check: k });
619
+ else if (fit.fit === "respell") outChecks.push({ ...k, kind: "number", value: fit.values[0] });
620
+ else outChecks.push(k);
621
+ }
622
+ for (const s of sets) {
623
+ const c = byName.get(s.column);
624
+ if (!c) {
625
+ outSets.push(s);
626
+ continue;
627
+ }
628
+ const fit = wireLiteralFit(c, { kind: s.kind, values: s.values, comparison: "equality" });
629
+ if (fit.fit === "unenforced") unenforced.push({ column: s.column, reason: fit.reason, set: s });
630
+ else if (fit.fit === "respell") outSets.push({ ...s, kind: "number", values: fit.values });
631
+ else outSets.push(s);
632
+ }
633
+ return { checks: outChecks, sets: outSets, unenforced };
634
+ }
635
+ function needsNumericCanon(columns, checks, sets) {
636
+ return columns.some(
637
+ (c) => comparisonWire(c) === "numeric-string" && (sets.some((s) => s.column === c.name) || checks.some(
638
+ (k) => k.column === c.name && (k.operator === "=" || k.operator === "<>")
639
+ ))
640
+ );
641
+ }
642
+
85
643
  // src/naming.ts
86
644
  var NAME_MODES = ["insert", "update", "select"];
87
645
  var DEFAULT_MODE_PREFIX = {
@@ -137,8 +695,14 @@ function schemaName(mode, tsName, affix) {
137
695
  function typeName(mode, tsName, affix) {
138
696
  return affix.type.prefix[mode] + applyTableCase(tsName, affix.tableCase) + affix.type.suffix[mode];
139
697
  }
140
- var PREFIX_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
141
- var SUFFIX_RE = /^[A-Za-z0-9_$]+$/;
698
+ var ID_START = "A-Za-z_$";
699
+ var ID_PART = "A-Za-z0-9_$";
700
+ var PREFIX_BODY = `[${ID_START}][${ID_PART}]*`;
701
+ var SUFFIX_BODY = `[${ID_PART}]+`;
702
+ var PREFIX_RE = new RegExp(`^${PREFIX_BODY}$`);
703
+ var SUFFIX_RE = new RegExp(`^${SUFFIX_BODY}$`);
704
+ var AFFIX_PREFIX_PATTERN = `^(?:${PREFIX_BODY})?$`;
705
+ var AFFIX_SUFFIX_PATTERN = `^(?:${SUFFIX_BODY})?$`;
142
706
  function validateAffix(affix, schemaSuffix) {
143
707
  const issues = [];
144
708
  if (!affix) return issues;
@@ -229,9 +793,9 @@ function buildBrandPlan(tables, opt) {
229
793
  }
230
794
  const bySqlName = /* @__PURE__ */ new Map();
231
795
  for (const t of byTs.values()) {
232
- const list = bySqlName.get(t.name) ?? [];
233
- list.push(t);
234
- bySqlName.set(t.name, list);
796
+ const list2 = bySqlName.get(t.name) ?? [];
797
+ list2.push(t);
798
+ bySqlName.set(t.name, list2);
235
799
  }
236
800
  const tokens = /* @__PURE__ */ new Map();
237
801
  const cache = /* @__PURE__ */ new Map();
@@ -335,219 +899,525 @@ function buildBrandPlan(tables, opt) {
335
899
  };
336
900
  }
337
901
 
338
- // src/checks.ts
339
- var COMPARISON = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(>=|<=|<>|!=|>|<|=)\s*(.+?)\s*$/;
340
- var IN_LIST = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IN\s*\((.+)\)\s*$/i;
341
- var CARDINALITY_OF = /^\s*(?:cardinality\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)|array_length\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,\s*1\s*\))\s*(>=|<=|<>|!=|>|<|=)\s*(\d+)\s*$/i;
342
- var LENGTH_OF = /^\s*(?:length|char_length)\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)\s*(>=|<=|<>|!=|>|<|=)\s*(\d+)\s*$/i;
343
- function splitTopLevelAnd(expr) {
344
- const parts = [];
345
- let depth = 0;
346
- let inString = false;
347
- let start = 0;
348
- for (let i = 0; i < expr.length; i++) {
349
- const c = expr[i];
350
- if (inString) {
351
- if (c === "'") {
352
- if (expr[i + 1] === "'") i++;
353
- else inString = false;
354
- }
355
- continue;
356
- }
357
- if (c === "'") {
358
- inString = true;
902
+ // src/constraints.ts
903
+ function labelled(name, text) {
904
+ return name ? `${name}: ${text}` : text;
905
+ }
906
+ function literalText(value, kind) {
907
+ return kind === "string" ? `'${value}'` : value;
908
+ }
909
+ function columnCheckText(k) {
910
+ return labelled(k.name, `${k.column} ${k.operator} ${literalText(k.value, k.kind)}`);
911
+ }
912
+ function setText(k) {
913
+ return labelled(
914
+ k.name,
915
+ `${k.column} IN (${k.values.map((v) => literalText(v, k.kind)).join(", ")})`
916
+ );
917
+ }
918
+ var lengthText = lengthCheckLabel;
919
+ function cardinalityText(k) {
920
+ return labelled(k.name, `cardinality(${k.column}) ${k.operator} ${k.value}`);
921
+ }
922
+ function rowText(k) {
923
+ return labelled(k.name, `${k.left} ${k.operator} ${k.right}`);
924
+ }
925
+ function nullText(k) {
926
+ return labelled(k.name, `${k.column} IS ${k.notNull ? "NOT NULL" : "NULL"}`);
927
+ }
928
+ function shapeArticle(c) {
929
+ switch (c.shape?.kind) {
930
+ case "json":
931
+ return "a JSON";
932
+ case "buffer":
933
+ return "a binary";
934
+ case "numberVector":
935
+ return "a vector";
936
+ case "bitstring":
937
+ return "a bit-string";
938
+ case "byteString":
939
+ return "a byte-string";
940
+ case "custom":
941
+ return "a customType";
942
+ default:
943
+ return "a structured";
944
+ }
945
+ }
946
+ function takesScalarChecks(c) {
947
+ return !c.arrayDimensions && !c.shape;
948
+ }
949
+ function foldsIntoBounds(c, k) {
950
+ if (c.arrayDimensions || c.shape) return false;
951
+ if (c.tsType !== "number" && c.tsType !== "bigint") return false;
952
+ return k.kind === "number" && k.operator !== "=" && k.operator !== "<>";
953
+ }
954
+ function statesCap(c, hasSet) {
955
+ if (c.shape || hasSet) return false;
956
+ if (c.enumValues && c.enumValues.length) return false;
957
+ if (c.tsType !== "string") return false;
958
+ return !c.format;
959
+ }
960
+ function classifyTableChecks(table) {
961
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
962
+ const out = [];
963
+ for (const k of table.checks ?? []) {
964
+ const expression = (k.expression ?? "").trim();
965
+ const parsed = parseCheck(k.expression, k.name);
966
+ if (!parsed.ok) {
967
+ out.push({
968
+ ...k.name ? { name: k.name } : {},
969
+ expression,
970
+ parts: [
971
+ {
972
+ text: labelled(k.name, expression),
973
+ columns: [],
974
+ place: "none",
975
+ reason: parsed.reason
976
+ }
977
+ ]
978
+ });
359
979
  continue;
360
980
  }
361
- if (c === "(") depth++;
362
- else if (c === ")") depth--;
363
- else if (depth === 0 && /\s/.test(c)) {
364
- const m = /^\s+AND\s+/i.exec(expr.slice(i));
365
- if (m) {
366
- parts.push(expr.slice(start, i));
367
- i += m[0].length - 1;
368
- start = i + 1;
981
+ const parts = [];
982
+ const place = (column, text, guard, guardReason, extra = {}) => {
983
+ const c = byName.get(column);
984
+ if (!c) {
985
+ parts.push({
986
+ text,
987
+ columns: [column],
988
+ place: "none",
989
+ reason: `"${column}" is not a column of that table`
990
+ });
991
+ return;
369
992
  }
370
- }
371
- }
372
- parts.push(expr.slice(start));
373
- return parts;
374
- }
375
- function unwrap(expr) {
376
- let e = expr.trim();
377
- while (e.startsWith("(") && e.endsWith(")")) {
378
- let depth = 0;
379
- let inString = false;
380
- let wrapsWhole = true;
381
- for (let i = 0; i < e.length; i++) {
382
- const c = e[i];
383
- if (inString) {
384
- if (c === "'") {
385
- if (e[i + 1] === "'") i++;
386
- else inString = false;
387
- }
993
+ if (!guard(c)) {
994
+ parts.push({ text, columns: [column], place: "none", reason: guardReason(c) });
995
+ return;
996
+ }
997
+ parts.push({ text, columns: [column], place: "column", ...extra });
998
+ };
999
+ const notScalar = (c) => c.arrayDimensions ? `"${c.name}" is an array, and the clause describes a scalar` : `"${c.name}" is a structured column, and the clause describes a scalar`;
1000
+ const literalFit = (column, kind, values, comparison) => {
1001
+ const col = byName.get(column);
1002
+ return col ? wireLiteralFit(col, { kind, values, comparison }) : { fit: "keep" };
1003
+ };
1004
+ for (const c of parsed.checks) {
1005
+ const fit = literalFit(
1006
+ c.column,
1007
+ c.kind,
1008
+ [c.value],
1009
+ c.operator === "=" || c.operator === "<>" ? "equality" : "range"
1010
+ );
1011
+ if (fit.fit === "unenforced") {
1012
+ parts.push({
1013
+ text: columnCheckText(c),
1014
+ columns: [c.column],
1015
+ place: "none",
1016
+ reason: fit.reason
1017
+ });
1018
+ continue;
1019
+ }
1020
+ const shown = fit.fit === "respell" ? { ...c, kind: "number", value: fit.values[0] } : c;
1021
+ const col = byName.get(c.column);
1022
+ place(c.column, columnCheckText(shown), takesScalarChecks, notScalar, {
1023
+ ...col && foldsIntoBounds(col, shown) ? { bound: { column: c.column, operator: shown.operator, value: shown.value } } : {}
1024
+ });
1025
+ }
1026
+ for (const s of parsed.sets ?? []) {
1027
+ const fit = literalFit(s.column, s.kind, s.values, "equality");
1028
+ if (fit.fit === "unenforced") {
1029
+ parts.push({ text: setText(s), columns: [s.column], place: "none", reason: fit.reason });
388
1030
  continue;
389
1031
  }
390
- if (c === "'") inString = true;
391
- else if (c === "(") depth++;
392
- else if (c === ")") {
393
- depth--;
394
- if (depth === 0 && i < e.length - 1) {
395
- wrapsWhole = false;
396
- break;
397
- }
1032
+ const shown = fit.fit === "respell" ? { ...s, kind: "number", values: fit.values } : s;
1033
+ place(s.column, setText(shown), takesScalarChecks, notScalar, {
1034
+ set: { column: s.column, values: shown.values, kind: shown.kind }
1035
+ });
1036
+ }
1037
+ for (const l of parsed.lengths ?? []) {
1038
+ place(
1039
+ l.column,
1040
+ lengthText(l),
1041
+ (c) => lengthMeasure(c, l) !== void 0,
1042
+ (c) => c.arrayDimensions ? `"${c.name}" is an array, so it has no ${l.unit === "bytes" ? "bytes" : "characters"} to count` : c.shape ? `"${c.name}" is ${shapeArticle(c)} column, whose ${l.unit === "bytes" ? "byte" : "character"} count in JavaScript is not the one the database took` : `"${c.name}" is not a string, so it has no ${l.unit === "bytes" ? "bytes" : "characters"} to count`
1043
+ );
1044
+ }
1045
+ for (const a of parsed.cardinalities ?? []) {
1046
+ place(
1047
+ a.column,
1048
+ cardinalityText(a),
1049
+ (c) => !!c.arrayDimensions,
1050
+ (c) => `"${c.name}" is not an array, so it has no elements to count`
1051
+ );
1052
+ }
1053
+ for (const n of parsed.nulls ?? []) {
1054
+ const text = nullText(n);
1055
+ if (!byName.has(n.column)) {
1056
+ parts.push({
1057
+ text,
1058
+ columns: [n.column],
1059
+ place: "none",
1060
+ reason: `"${n.column}" is not a column of that table`
1061
+ });
1062
+ continue;
398
1063
  }
1064
+ parts.push(
1065
+ n.notNull ? { text, columns: [n.column], place: "column", shape: "notNull" } : {
1066
+ text,
1067
+ columns: [n.column],
1068
+ place: "none",
1069
+ reason: "the column may hold only NULL, which these schemas do not narrow it to"
1070
+ }
1071
+ );
399
1072
  }
400
- if (!wrapsWhole) break;
401
- e = e.slice(1, -1).trim();
1073
+ for (const r of parsed.rows ?? []) {
1074
+ const text = rowText(r);
1075
+ const missing = [r.left, r.right].filter((n) => !byName.has(n));
1076
+ parts.push(
1077
+ missing.length ? {
1078
+ text,
1079
+ columns: [r.left, r.right],
1080
+ place: "none",
1081
+ reason: `${missing.map((n) => `"${n}"`).join(" and ")} ${missing.length > 1 ? "are not columns" : "is not a column"} of that table`
1082
+ } : { text, columns: [r.left, r.right], place: "row" }
1083
+ );
1084
+ }
1085
+ out.push({ ...k.name ? { name: k.name } : {}, expression, parts });
402
1086
  }
403
- return e;
1087
+ return out;
404
1088
  }
405
- function splitTopLevelCommas(list) {
406
- const parts = [];
407
- let depth = 0;
408
- let inString = false;
409
- let start = 0;
410
- for (let i = 0; i < list.length; i++) {
411
- const c = list[i];
412
- if (inString) {
413
- if (c === "'") {
414
- if (list[i + 1] === "'") i++;
415
- else inString = false;
1089
+ var list = (cols) => cols.join(", ");
1090
+ function foreignKeyRule(fk) {
1091
+ const target = fk.foreignSchema ? `${fk.foreignSchema}.${fk.foreignTable}` : fk.foreignTable;
1092
+ return `FOREIGN KEY (${list(fk.columns)}) REFERENCES ${target} (${list(fk.foreignColumns)})` + (fk.onDelete ? ` ON DELETE ${fk.onDelete}` : "") + (fk.onUpdate ? ` ON UPDATE ${fk.onUpdate}` : "");
1093
+ }
1094
+ function uniquifier() {
1095
+ const seen = /* @__PURE__ */ new Map();
1096
+ return (id) => {
1097
+ const n = seen.get(id) ?? 0;
1098
+ seen.set(id, n + 1);
1099
+ return n === 0 ? id : `${id}_${n + 1}`;
1100
+ };
1101
+ }
1102
+ function tableConstraints(table) {
1103
+ const out = [];
1104
+ const id = uniquifier();
1105
+ const named = (key, fallback) => key.name ? { id: id(key.name), name: key.name } : { id: id(fallback) };
1106
+ if (table.primaryKey?.columns.length) {
1107
+ const pk = table.primaryKey;
1108
+ out.push({
1109
+ ...named(pk, `${table.name}_pkey`),
1110
+ kind: "primaryKey",
1111
+ columns: [...pk.columns],
1112
+ rule: `PRIMARY KEY (${list(pk.columns)})`,
1113
+ // No per-row validator can check a key: whether a value is already taken is a fact about
1114
+ // the table. `duplicateFinder` checks the half that needs no database, and even that only
1115
+ // answers whether a batch collides with itself.
1116
+ enforced: false
1117
+ });
1118
+ }
1119
+ for (const u of table.unique ?? []) {
1120
+ if (!u.columns.length) continue;
1121
+ out.push({
1122
+ ...named(u, `${table.name}_${u.columns.join("_")}_key`),
1123
+ kind: "unique",
1124
+ columns: [...u.columns],
1125
+ rule: `UNIQUE (${list(u.columns)})`,
1126
+ enforced: false
1127
+ });
1128
+ }
1129
+ for (const fk of table.foreignKeys ?? []) {
1130
+ if (!fk.columns.length) continue;
1131
+ out.push({
1132
+ ...named(fk, `${table.name}_${fk.columns.join("_")}_fkey`),
1133
+ kind: "foreignKey",
1134
+ columns: [...fk.columns],
1135
+ rule: foreignKeyRule(fk),
1136
+ // The referenced row either exists or it does not, and only the database knows.
1137
+ enforced: false,
1138
+ references: {
1139
+ table: fk.foreignTable,
1140
+ ...fk.foreignSchema ? { schema: fk.foreignSchema } : {},
1141
+ columns: [...fk.foreignColumns],
1142
+ ...fk.onDelete ? { onDelete: fk.onDelete } : {},
1143
+ ...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
416
1144
  }
417
- continue;
1145
+ });
1146
+ }
1147
+ const classified = classifyTableChecks(table);
1148
+ classified.forEach((k, i) => {
1149
+ const columns = [];
1150
+ for (const p of k.parts) for (const c of p.columns) if (!columns.includes(c)) columns.push(c);
1151
+ const messages = k.parts.filter((p) => p.place !== "none" && !p.bound && !p.set && !p.shape).map((p) => p.text);
1152
+ const bounds = k.parts.filter((p) => p.bound).map((p) => p.bound);
1153
+ const set = k.parts.find((p) => p.set)?.set;
1154
+ const unenforced = k.parts.filter((p) => p.place === "none").map((p) => ({ part: p.text, reason: p.reason ?? "not translated" }));
1155
+ out.push({
1156
+ ...k.name ? { id: id(k.name), name: k.name } : { id: id(`${table.name}_check_${i + 1}`) },
1157
+ kind: "check",
1158
+ columns,
1159
+ rule: `CHECK (${k.expression})`,
1160
+ enforced: k.parts.some((p) => p.place !== "none"),
1161
+ ...unenforced.length ? { unenforced } : {},
1162
+ ...messages.length ? { messages } : {},
1163
+ ...bounds.length ? { bounds } : {},
1164
+ ...set ? { values: set } : {}
1165
+ });
1166
+ });
1167
+ const setColumns = new Set(
1168
+ classified.flatMap((k) => k.parts.filter((p) => p.set).map((p) => p.set.column))
1169
+ );
1170
+ for (const c of table.columns) {
1171
+ if (!statesCap(c, setColumns.has(c.name))) continue;
1172
+ if (c.maxLength !== void 0) {
1173
+ const message = `at most ${c.maxLength} characters`;
1174
+ out.push({
1175
+ id: id(`${table.name}_${c.name}_maxlength`),
1176
+ kind: "maxLength",
1177
+ columns: [c.name],
1178
+ rule: message,
1179
+ enforced: true,
1180
+ messages: [message]
1181
+ });
418
1182
  }
419
- if (c === "'") inString = true;
420
- else if (c === "(") depth++;
421
- else if (c === ")") depth--;
422
- else if (c === "," && depth === 0) {
423
- parts.push(list.slice(start, i));
424
- start = i + 1;
1183
+ if (c.maxBytes !== void 0) {
1184
+ const message = `at most ${c.maxBytes} bytes`;
1185
+ out.push({
1186
+ id: id(`${table.name}_${c.name}_maxbytes`),
1187
+ kind: "maxBytes",
1188
+ columns: [c.name],
1189
+ rule: message,
1190
+ enforced: true,
1191
+ messages: [message]
1192
+ });
425
1193
  }
426
1194
  }
427
- parts.push(list.slice(start));
428
- return parts;
1195
+ return {
1196
+ table: table.name,
1197
+ ...table.schema ? { schema: table.schema } : {},
1198
+ constraints: out
1199
+ };
429
1200
  }
430
- var BETWEEN = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+BETWEEN\s+(.+?)\s+AND\s+(.+?)\s*$/i;
431
- function literal(raw) {
432
- const t = raw.trim();
433
- if (/^-?\d+(\.\d+)?$/.test(t)) return { value: t, kind: "number" };
434
- const m = t.match(/^'((?:[^']|'')*)'$/);
435
- if (m) return { value: m[1].replace(/''/g, "'"), kind: "string" };
436
- return void 0;
1201
+ function resolveConstraints(opt) {
1202
+ if (!opt) return void 0;
1203
+ if (opt === true) return { errorMap: true };
1204
+ if (opt.enabled === false) return void 0;
1205
+ return { errorMap: opt.errorMap !== false };
437
1206
  }
438
- function parseCheck(expression, name) {
439
- const expr = unwrap((expression ?? "").trim());
440
- if (!expr) return { ok: false, reason: "empty expression" };
441
- if (expr.includes("?")) return { ok: false, reason: "expression contains an unresolved value" };
442
- if (/(^|[\s)])OR($|[\s(])/i.test(expr)) return { ok: false, reason: "contains OR" };
443
- if (/(^|[\s(])NOT($|[\s(])/i.test(expr)) return { ok: false, reason: "contains NOT" };
444
- const between = expr.match(BETWEEN);
445
- if (between) {
446
- const lo = literal(between[2]);
447
- const hi = literal(between[3]);
448
- if (!lo || !hi) return { ok: false, reason: "BETWEEN bounds are not literals" };
449
- if (lo.kind !== hi.kind) return { ok: false, reason: "BETWEEN bounds are of mixed types" };
450
- return {
451
- ok: true,
452
- checks: [
453
- { column: between[1], operator: ">=", value: lo.value, kind: lo.kind, name },
454
- { column: between[1], operator: "<=", value: hi.value, kind: hi.kind, name }
455
- ]
456
- };
1207
+ var CONSTRAINTS_MODULE = "constraints.ts";
1208
+ var TYPES = `/** What a constraint is. */
1209
+ export type DrzlConstraintKind =
1210
+ | 'primaryKey'
1211
+ | 'unique'
1212
+ | 'foreignKey'
1213
+ | 'check'
1214
+ | 'maxLength'
1215
+ | 'maxBytes';
1216
+
1217
+ /** One constraint on one table. */
1218
+ export interface DrzlConstraint {
1219
+ /** Stable within the table. The SQL constraint name where the declaration has one. */
1220
+ id: string;
1221
+ /** The SQL constraint name, absent where the declaration did not give one. */
1222
+ name?: string;
1223
+ kind: DrzlConstraintKind;
1224
+ /** The columns the constraint is about, in declaration order. */
1225
+ columns: string[];
1226
+ /** The rule as a sentence, for a form with nothing better to show. */
1227
+ rule: string;
1228
+ /** Whether a generated schema can reject a row for this constraint. */
1229
+ enforced: boolean;
1230
+ /** The clauses nothing in these schemas checks, and why. */
1231
+ unenforced?: { part: string; reason: string }[];
1232
+ /** The exact messages the generated schemas attach for this constraint. */
1233
+ messages?: string[];
1234
+ /** Bounds folded into a column's range, which is where the constraint name is lost. */
1235
+ bounds?: { column: string; operator: string; value: string }[];
1236
+ /** A set of literals folded into an enum, which is the other place it is lost. */
1237
+ values?: { column: string; values: string[]; kind: 'number' | 'string' };
1238
+ /** Where a foreign key points. */
1239
+ references?: {
1240
+ table: string;
1241
+ schema?: string;
1242
+ columns: string[];
1243
+ onDelete?: string;
1244
+ onUpdate?: string;
1245
+ };
1246
+ }
1247
+
1248
+ /** Every constraint on one table. */
1249
+ export interface DrzlTableConstraints {
1250
+ /** The SQL table name, which is not the Drizzle export name this is exported under. */
1251
+ table: string;
1252
+ /** The SQL schema, present only when the table names one. */
1253
+ schema?: string;
1254
+ constraints: DrzlConstraint[];
1255
+ }`;
1256
+ var MATCHER = `/** A validation issue traced back to the constraint that caused it. */
1257
+ export interface DrzlConstraintMatch {
1258
+ constraint: DrzlConstraint;
1259
+ /**
1260
+ * The column to put the message on.
1261
+ *
1262
+ * Taken from the issue where the library named one, and from the constraint where it did not.
1263
+ * Valibot reports a row-level check with an empty path, so without the fallback a form would
1264
+ * have a message and nowhere to show it.
1265
+ */
1266
+ column?: string;
1267
+ /**
1268
+ * How the constraint was identified.
1269
+ *
1270
+ * \`message\` is an exact match on a string these schemas wrote, and is the only tier that is
1271
+ * certain. \`bound\` matched the numeric bound the library put on the issue against a bound this
1272
+ * constraint folded, which is what a folded CHECK leaves to match on once its name is gone.
1273
+ * \`column\` is the last resort: the column has exactly one constraint stated in the validator's
1274
+ * own vocabulary, so nothing else on the issue can be wrong about which.
1275
+ */
1276
+ matchedBy: 'message' | 'bound' | 'column';
1277
+ }
1278
+
1279
+ /**
1280
+ * The column an issue is about, across the path shapes these libraries use.
1281
+ *
1282
+ * zod and ArkType spell a path item as the key itself; valibot spells it as an object carrying
1283
+ * one. The last key is taken rather than the first, so an issue inside an array or a nested
1284
+ * payload names the field rather than the collection holding it.
1285
+ */
1286
+ function drzlIssueColumn(issue: any): string | undefined {
1287
+ const path = issue?.path;
1288
+ if (!Array.isArray(path)) return undefined;
1289
+ for (let i = path.length - 1; i >= 0; i--) {
1290
+ const item: any = path[i];
1291
+ if (typeof item === 'string') return item;
1292
+ if (item && typeof item === 'object' && typeof item.key === 'string') return item.key;
457
1293
  }
458
- const parts = splitTopLevelAnd(expr);
459
- if (parts.length > 1) {
460
- const checks = [];
461
- const sets = [];
462
- const rows = [];
463
- const lengths = [];
464
- const cardinalities = [];
465
- for (const part of parts) {
466
- const parsed = parseCheck(part, name);
467
- if (!parsed.ok)
468
- return { ok: false, reason: `part of an AND was not understood: ${parsed.reason}` };
469
- checks.push(...parsed.checks);
470
- if (parsed.sets) sets.push(...parsed.sets);
471
- if (parsed.rows) rows.push(...parsed.rows);
472
- if (parsed.lengths) lengths.push(...parsed.lengths);
473
- if (parsed.cardinalities) cardinalities.push(...parsed.cardinalities);
474
- }
475
- return {
476
- ok: true,
477
- checks,
478
- ...sets.length ? { sets } : {},
479
- ...rows.length ? { rows } : {},
480
- ...lengths.length ? { lengths } : {},
481
- ...cardinalities.length ? { cardinalities } : {}
482
- };
1294
+ return undefined;
1295
+ }
1296
+
1297
+ /**
1298
+ * The numeric bound an issue reports, as a decimal string, or nothing.
1299
+ *
1300
+ * Measured rather than guessed: zod 4.4.3 puts \`minimum\`/\`maximum\` on a \`too_small\`/\`too_big\`
1301
+ * issue and valibot 1.4.2 puts \`requirement\` on a \`min_value\`/\`max_value\` one. A bigint bound
1302
+ * is read too, since a 64 bit column's range is not representable as a number.
1303
+ */
1304
+ function drzlIssueBound(issue: any): string | undefined {
1305
+ for (const key of ['minimum', 'maximum', 'requirement']) {
1306
+ const v = issue?.[key];
1307
+ if (typeof v === 'number' || typeof v === 'bigint') return String(v);
483
1308
  }
484
- const lengthOf = expr.match(LENGTH_OF);
485
- if (lengthOf) {
486
- const op2 = lengthOf[2] === "!=" ? "<>" : lengthOf[2];
487
- return {
488
- ok: true,
489
- checks: [],
490
- lengths: [{ column: lengthOf[1], operator: op2, value: lengthOf[3], name }]
491
- };
1309
+ return undefined;
1310
+ }
1311
+
1312
+ /**
1313
+ * The constraint a validation issue came from, or nothing.
1314
+ *
1315
+ * Three tiers, because a constraint does not always survive into the issue in the same form. Every
1316
+ * constraint stated as a predicate carries a message these schemas wrote, and that is an exact
1317
+ * lookup. A numeric CHECK is deliberately folded into the column's own range instead, so the
1318
+ * failure is worded by the library and the constraint name is nowhere in it; the bound is, and it
1319
+ * is matched on that. A set constraint becomes an enum and leaves neither, so it is resolved by
1320
+ * the column alone, which is safe only because a column can carry one such constraint at most.
1321
+ *
1322
+ * The two folds are kept apart rather than pooled, and that is what stops the third tier
1323
+ * over-claiming. A folded bound always reports its bound, in every library measured, so an issue
1324
+ * on that column carrying **no** bound is not that constraint: it is the field failing to be a
1325
+ * number at all. Pooling them answered \`invalid_type\` on a column with a numeric CHECK with the
1326
+ * CHECK, which is a rule the row did not break.
1327
+ *
1328
+ * Returns nothing rather than a guess, for the same reason. A \`too_small\` reporting the column's
1329
+ * own type bound has no matching constraint and gets no answer.
1330
+ */
1331
+ export function constraintForIssue(
1332
+ table: string,
1333
+ issue: unknown
1334
+ ): DrzlConstraintMatch | undefined {
1335
+ const ledger = constraintsByTable[table];
1336
+ if (!ledger) return undefined;
1337
+ const raw: any = issue;
1338
+ const column = drzlIssueColumn(raw);
1339
+ const message = typeof raw?.message === 'string' ? raw.message : undefined;
1340
+
1341
+ if (message !== undefined) {
1342
+ for (const constraint of ledger.constraints) {
1343
+ if (!constraint.messages || constraint.messages.indexOf(message) < 0) continue;
1344
+ if (column !== undefined && constraint.columns.indexOf(column) < 0) continue;
1345
+ return {
1346
+ constraint,
1347
+ column: column ?? constraint.columns[0],
1348
+ matchedBy: 'message',
1349
+ };
1350
+ }
492
1351
  }
493
- const cardinalityOf = expr.match(CARDINALITY_OF);
494
- if (cardinalityOf) {
495
- const op2 = cardinalityOf[3] === "!=" ? "<>" : cardinalityOf[3];
496
- return {
497
- ok: true,
498
- checks: [],
499
- cardinalities: [
500
- {
501
- column: cardinalityOf[1] ?? cardinalityOf[2],
502
- operator: op2,
503
- value: cardinalityOf[4],
504
- ...name ? { name } : {}
505
- }
506
- ]
507
- };
1352
+
1353
+ if (column === undefined) return undefined;
1354
+
1355
+ const bound = drzlIssueBound(raw);
1356
+ if (bound !== undefined) {
1357
+ const hit = ledger.constraints.find(
1358
+ (c) => c.bounds && c.bounds.some((b) => b.column === column && b.value === bound)
1359
+ );
1360
+ return hit ? { constraint: hit, column, matchedBy: 'bound' } : undefined;
508
1361
  }
509
- const inList = expr.match(IN_LIST);
510
- if (inList) {
511
- const raw = splitTopLevelCommas(inList[2]);
512
- const parsedValues = raw.map((r) => literal(r));
513
- if (parsedValues.some((v) => !v)) return { ok: false, reason: "IN list holds a non-literal" };
514
- const kinds = new Set(parsedValues.map((v) => v.kind));
515
- if (kinds.size > 1) return { ok: false, reason: "IN list mixes types" };
516
- if (!parsedValues.length) return { ok: false, reason: "IN list is empty" };
1362
+
1363
+ const sets = ledger.constraints.filter((c) => c.values && c.values.column === column);
1364
+ return sets.length === 1 ? { constraint: sets[0], column, matchedBy: 'column' } : undefined;
1365
+ }`;
1366
+ function constName(tsName) {
1367
+ const safe = tsName.replace(/[^A-Za-z0-9_$]/g, "_");
1368
+ return `${/^[0-9]/.test(safe) ? `_${safe}` : safe}Constraints`;
1369
+ }
1370
+ function renderConstraintsModule(tables, opts = {}) {
1371
+ const entries = tables.map((t) => ({ tsName: t.tsName, facts: tableConstraints(t) }));
1372
+ const consts = entries.map(
1373
+ (e) => `/** Every constraint on \`${e.facts.table}\`. */
1374
+ export const ${constName(e.tsName)}: DrzlTableConstraints = ${JSON.stringify(e.facts, null, 2)};`
1375
+ ).join("\n\n");
1376
+ const record = `/** Every table's constraints, keyed by the Drizzle export name its schemas are named after. */
1377
+ export const constraintsByTable: Record<string, DrzlTableConstraints> = {
1378
+ ` + entries.map((e) => ` ${JSON.stringify(e.tsName)}: ${constName(e.tsName)},`).join("\n") + `
1379
+ };`;
1380
+ return [
1381
+ "/**",
1382
+ " * Every CHECK, unique constraint, primary and foreign key on each table, as data.",
1383
+ " *",
1384
+ " * Generated beside the schemas rather than derived from them: a validator states what a value",
1385
+ " * must look like and says nothing about which constraint said so, and the two constraints a",
1386
+ " * per-row schema cannot check at all, uniqueness and a foreign key, are not in it in any form.",
1387
+ " */",
1388
+ TYPES,
1389
+ consts,
1390
+ record,
1391
+ ...opts.errorMap ? [MATCHER] : []
1392
+ ].join("\n\n");
1393
+ }
1394
+
1395
+ // src/emit.ts
1396
+ var realFs = null;
1397
+ function nodeFs() {
1398
+ return realFs ??= import("fs/promises");
1399
+ }
1400
+ function fileWriter(sink) {
1401
+ if (!sink) {
517
1402
  return {
518
- ok: true,
519
- checks: [],
520
- sets: [
521
- {
522
- column: inList[1],
523
- values: parsedValues.map((v) => v.value),
524
- kind: parsedValues[0].kind,
525
- name
526
- }
527
- ]
1403
+ async mkdir(dir, options) {
1404
+ return (await nodeFs()).mkdir(dir, options);
1405
+ },
1406
+ async writeFile(file, contents, encoding = "utf8") {
1407
+ return (await nodeFs()).writeFile(file, contents, encoding);
1408
+ }
528
1409
  };
529
1410
  }
530
- const cmp = expr.match(COMPARISON);
531
- if (!cmp) return { ok: false, reason: "not a single comparison this version understands" };
532
- const value = literal(cmp[3]);
533
- if (!value) {
534
- const right = cmp[3].trim();
535
- if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(right)) {
536
- const op2 = cmp[2] === "!=" ? "<>" : cmp[2];
537
- return { ok: true, checks: [], rows: [{ left: cmp[1], right, operator: op2, name }] };
538
- }
539
- return { ok: false, reason: "right side is not a literal" };
540
- }
541
- const op = cmp[2] === "!=" ? "<>" : cmp[2];
542
1411
  return {
543
- ok: true,
544
- checks: [{ column: cmp[1], operator: op, value: value.value, kind: value.kind, name }]
1412
+ async mkdir(dir) {
1413
+ await sink.mkdir(dir);
1414
+ return void 0;
1415
+ },
1416
+ async writeFile(file, contents) {
1417
+ await sink.writeFile(file, contents);
1418
+ }
545
1419
  };
546
1420
  }
547
- function describeSet(set) {
548
- const shown = set.values.map((v) => set.kind === "string" ? `'${v}'` : v).join(", ");
549
- return `${set.name ? `${set.name}: ` : ""}${set.column} IN (${shown})`;
550
- }
551
1421
 
552
1422
  // src/files.ts
553
1423
  var import_node_fs = __toESM(require("fs"), 1);
@@ -609,62 +1479,24 @@ function withTsExtension(p) {
609
1479
  }
610
1480
 
611
1481
  // src/meta.ts
612
- function labelled(name, text) {
613
- return name ? `${name}: ${text}` : text;
614
- }
615
- function literalText(value, kind) {
616
- return kind === "string" ? `'${value}'` : value;
617
- }
618
- function columnCheckText(k) {
619
- return labelled(k.name, `${k.column} ${k.operator} ${literalText(k.value, k.kind)}`);
620
- }
621
- function setText(k) {
622
- return labelled(
623
- k.name,
624
- `${k.column} IN (${k.values.map((v) => literalText(v, k.kind)).join(", ")})`
625
- );
626
- }
627
- function lengthText(k) {
628
- return labelled(k.name, `length(${k.column}) ${k.operator} ${k.value}`);
629
- }
630
- function cardinalityText(k) {
631
- return labelled(k.name, `cardinality(${k.column}) ${k.operator} ${k.value}`);
632
- }
633
- function rowText(k) {
634
- return labelled(k.name, `${k.left} ${k.operator} ${k.right}`);
635
- }
636
- function takesScalarChecks(c) {
637
- return !c.arrayDimensions && !c.shape;
638
- }
639
1482
  function classifyChecks(table) {
640
1483
  const perColumn = /* @__PURE__ */ new Map();
641
1484
  const rows = [];
642
1485
  const unenforced = [];
643
- const byName = new Map(table.columns.map((c) => [c.name, c]));
644
- const add = (column, text, guard) => {
645
- const c = byName.get(column);
646
- if (!c || !guard(c)) {
647
- unenforced.push(text);
648
- return;
649
- }
650
- const list = perColumn.get(column) ?? [];
651
- list.push(text);
652
- perColumn.set(column, list);
653
- };
654
- for (const k of table.checks ?? []) {
655
- const parsed = parseCheck(k.expression, k.name);
656
- if (!parsed.ok) {
657
- unenforced.push(labelled(k.name, (k.expression ?? "").trim()));
658
- continue;
659
- }
660
- for (const c of parsed.checks) add(c.column, columnCheckText(c), takesScalarChecks);
661
- for (const s of parsed.sets ?? []) add(s.column, setText(s), takesScalarChecks);
662
- for (const l of parsed.lengths ?? []) add(l.column, lengthText(l), takesScalarChecks);
663
- for (const a of parsed.cardinalities ?? [])
664
- add(a.column, cardinalityText(a), (c) => !!c.arrayDimensions);
665
- for (const r of parsed.rows ?? []) {
666
- if (byName.has(r.left) && byName.has(r.right)) rows.push(rowText(r));
667
- else unenforced.push(rowText(r));
1486
+ for (const check of classifyTableChecks(table)) {
1487
+ for (const part of check.parts) {
1488
+ if (part.place === "none") {
1489
+ unenforced.push(part.text);
1490
+ continue;
1491
+ }
1492
+ if (part.place === "row") {
1493
+ rows.push(part.text);
1494
+ continue;
1495
+ }
1496
+ const column = part.columns[0];
1497
+ const list2 = perColumn.get(column) ?? [];
1498
+ list2.push(part.text);
1499
+ perColumn.set(column, list2);
668
1500
  }
669
1501
  }
670
1502
  return { perColumn, rows, unenforced };
@@ -809,7 +1641,13 @@ function nestedNodeColumns(columnsForMode, node) {
809
1641
 
810
1642
  // src/duplicates.ts
811
1643
  function usableKeys(table) {
812
- return (table.unique ?? []).filter((k) => k.columns.length > 0);
1644
+ const keys = [];
1645
+ const pk = table.primaryKey;
1646
+ if (pk && pk.columns.length > 0) {
1647
+ keys.push({ name: pk.name ?? `${table.name}_pkey`, columns: pk.columns });
1648
+ }
1649
+ keys.push(...(table.unique ?? []).filter((k) => k.columns.length > 0));
1650
+ return keys;
813
1651
  }
814
1652
  function renderDuplicateFinder(table, fnName, rowType) {
815
1653
  const keys = usableKeys(table);
@@ -819,14 +1657,15 @@ function renderDuplicateFinder(table, fnName, rowType) {
819
1657
  return ` { name: ${JSON.stringify(name)}, columns: ${JSON.stringify(k.columns)} }${i === keys.length - 1 ? "" : ","}`;
820
1658
  }).join("\n");
821
1659
  return `/**
822
- * Rows in \`rows\` that collide with an earlier row on a unique constraint.
1660
+ * Rows in \`rows\` that collide with an earlier row on the primary key or a unique constraint.
823
1661
  *
824
1662
  * Uniqueness is a fact about the table rather than about a row, so no schema can check it. This
825
1663
  * checks the half that needs no database: whether the batch collides with itself. A batch that
826
1664
  * passes here can still collide with rows already stored.
827
1665
  *
828
1666
  * A constraint is skipped for any row where one of its columns is null or absent, matching SQL,
829
- * where NULL is not equal to NULL and a unique index therefore permits repeats.
1667
+ * where NULL is not equal to NULL and a unique index therefore permits repeats. Rows that leave
1668
+ * a generated primary key to the database therefore report nothing on it.
830
1669
  */
831
1670
  export function ${fnName}(
832
1671
  rows: readonly ${rowType}[]
@@ -863,7 +1702,48 @@ var COLUMN_FORMATS = {
863
1702
  // Sign, decimals, exponents, NaN/Infinity, surrounding whitespace, and the underscore digit
864
1703
  // separators and 0x/0o/0b integer literals Postgres 16 added. Agrees with Postgres on all 43
865
1704
  // probes, `1_000` and `0xDEAD_beef` through to `1__0`, `_1`, `0x` and `1e+`.
866
- numeric: "^\\s*([+-]?(0[xX][0-9a-fA-F](_?[0-9a-fA-F])*|0[oO][0-7](_?[0-7])*|0[bB][01](_?[01])*)|[+-]?(\\d(_?\\d)*(\\.(\\d(_?\\d)*)?)?|\\.\\d(_?\\d)*)([eE][+-]?\\d(_?\\d)*)?|[+-]?(NaN|Infinity))\\s*$"
1705
+ numeric: "^\\s*([+-]?(0[xX][0-9a-fA-F](_?[0-9a-fA-F])*|0[oO][0-7](_?[0-7])*|0[bB][01](_?[01])*)|[+-]?(\\d(_?\\d)*(\\.(\\d(_?\\d)*)?)?|\\.\\d(_?\\d)*)([eE][+-]?\\d(_?\\d)*)?|[+-]?(NaN|Infinity))\\s*$",
1706
+ // What Postgres itself parses into a `bigint`, for a `bigint({ mode: 'string' })` column whose
1707
+ // value goes to the server as text. `int8in` is a pure integer parser: an optional sign against
1708
+ // the digits, decimal or a `0x`/`0o`/`0b` literal, single `_` separators between digits and one
1709
+ // permitted directly after the base prefix, leading zeros, and surrounding whitespace. Measured
1710
+ // against a real Postgres through PGlite over 16160 probes, boundary sweeps and random shapes:
1711
+ // **zero** values the server takes and this refuses.
1712
+ //
1713
+ // Two things it deliberately does not say.
1714
+ //
1715
+ // **The magnitude.** Every one of the 4474 probes this admits and the server refuses is a value
1716
+ // outside the signed 64 bit range, and nothing else: the syntax half is complete. The exact
1717
+ // bound is expressible, since leading zeros and separators make it a per-digit ladder rather
1718
+ // than a digit count, and it was built and verified at 16160/16160 against the server. It is
1719
+ // not shipped, because at 1237 characters and around twenty alternation branches it exhausts
1720
+ // ArkType's type-level instantiation budget: that generator states a format as a regex literal
1721
+ // inside the type expression, and the emitted module then fails to compile with TS2589.
1722
+ // Measured on arktype 2.2.3, this 101-character pattern compiles and the ladder does not, as
1723
+ // `COLUMN_FORMATS.numeric` at 176 characters already does not. Emitting a module that does not
1724
+ // typecheck is a worse failure than the bound it would buy, and the bound is unreachable by any
1725
+ // value the probe pools carry. The ArkType defect is already reported and carved out of the
1726
+ // parity gate's typecheck stage; when it is fixed, the ladder is what goes here.
1727
+ //
1728
+ // **Whitespace exactly.** Postgres pads with C `isspace`, which is the six ASCII characters, and
1729
+ // JS `\s` also admits NBSP and the Unicode spaces. That admits a handful of strings the server
1730
+ // refuses, which is the safe direction, and it is what `numeric` above already does.
1731
+ pgBigint: "^\\s*[+-]?(\\d(_?\\d)*|0[xX]_?[\\da-fA-F](_?[\\da-fA-F])*|0[oO]_?[0-7](_?[0-7])*|0[bB]_?[01](_?[01])*)\\s*$",
1732
+ // The same column on MySQL, which parses it as a *decimal number* and then rounds. Measured
1733
+ // against MySQL 8.4.11: `'12.5'` stores 13, `'1.5'` stores 2, `'.5'` stores 1, `'1e3'` stores
1734
+ // 1000, and `'92233720368547758070e-1'` stores the int64 maximum. It refuses the two spellings
1735
+ // Postgres takes, `'0x1f'` and `'1_000'`, both as "Data truncated". So no single pattern serves
1736
+ // both servers: their union admits `'12.5'` on Postgres, which is one of the fourteen values
1737
+ // that made this a defect, and their intersection turns away values each server really stores.
1738
+ //
1739
+ // Shared by the signed and the unsigned spelling, and this is why neither magnitude nor sign is
1740
+ // stated here: the value the range applies to is the *rounded* one, so the text does not
1741
+ // determine whether it fits. `'9223372036854775807.4'` is stored and `'9223372036854775807.6'`
1742
+ // is refused; on a `bigint unsigned`, `'-0.4'` and `'-1e-1'` are both stored as 0 while `'-0.5'`
1743
+ // is refused. A pattern cannot do that arithmetic, and guessing at it turns away working rows.
1744
+ // Over 3319 probes against each of a signed and an unsigned column: zero values the server takes
1745
+ // and this refuses.
1746
+ mysqlBigint: "^\\s*[+-]?(\\d+(\\.\\d*)?|\\.\\d+)([eE][+-]?\\d*)?\\s*$"
867
1747
  };
868
1748
  var COERCIBLE_DATE_STRING = "^(?!\\s*[+-])(?!\\s*\\d*\\.?\\d*(?:[eE][+-]?\\d+)?\\s*$)";
869
1749
  function parsesToADate(expr) {
@@ -878,15 +1758,39 @@ function nonFiniteAccepted(c) {
878
1758
  if (c.tsType !== "number" || c.shape) return { nan: false, infinity: false };
879
1759
  return { nan: c.allowsNaN === true, infinity: c.allowsInfinity === true };
880
1760
  }
1761
+ function nonFiniteRefused(c) {
1762
+ if (c.tsType !== "number" || c.shape) return { nan: false, infinity: false };
1763
+ return { nan: c.allowsNaN === false, infinity: c.allowsInfinity === false };
1764
+ }
1765
+ function notNullByCheck(table) {
1766
+ const out = /* @__PURE__ */ new Set();
1767
+ for (const k of table.checks ?? []) {
1768
+ const parsed = parseCheck(k.expression, k.name);
1769
+ if (!parsed.ok) continue;
1770
+ for (const n of parsed.nulls ?? []) if (n.notNull) out.add(n.column);
1771
+ }
1772
+ return out;
1773
+ }
1774
+ function withCheckNullability(table, cols) {
1775
+ const notNull = notNullByCheck(table);
1776
+ if (!notNull.size) return cols;
1777
+ return cols.map((c) => c.nullable && notNull.has(c.name) ? { ...c, nullable: false } : c);
1778
+ }
881
1779
  function insertColumns(table) {
882
- return table.columns.filter((c) => !isGeneratedColumn(c));
1780
+ return withCheckNullability(
1781
+ table,
1782
+ table.columns.filter((c) => !isGeneratedColumn(c))
1783
+ );
883
1784
  }
884
1785
  function updateColumns(table) {
885
1786
  const pkCols = table.primaryKey?.columns ?? [];
886
- return table.columns.filter((c) => !isGeneratedColumn(c) && !pkCols.includes(c.name));
1787
+ return withCheckNullability(
1788
+ table,
1789
+ table.columns.filter((c) => !isGeneratedColumn(c) && !pkCols.includes(c.name))
1790
+ );
887
1791
  }
888
1792
  function selectColumns(table) {
889
- return table.columns;
1793
+ return withCheckNullability(table, table.columns);
890
1794
  }
891
1795
  var reportedEngines = /* @__PURE__ */ new Set();
892
1796
  var ENGINE_PACKAGE = { prettier: "prettier", biome: "@biomejs/biome" };
@@ -912,11 +1816,30 @@ function nearestExistingDir(from) {
912
1816
  dir = parent;
913
1817
  }
914
1818
  }
1819
+ function isProjectInstallPath(manifestPath) {
1820
+ return manifestPath.split(import_node_path2.default.sep).includes("node_modules");
1821
+ }
1822
+ function biomeManifest(startDir) {
1823
+ const anchors = [startDir, process.cwd()];
1824
+ let lastError;
1825
+ for (const anchor of anchors) {
1826
+ let resolved;
1827
+ try {
1828
+ const require_ = (0, import_node_module.createRequire)((0, import_node_url.pathToFileURL)(import_node_path2.default.join(anchor, "noop.js")));
1829
+ resolved = require_.resolve("@biomejs/biome/package.json");
1830
+ } catch (err) {
1831
+ lastError = err;
1832
+ continue;
1833
+ }
1834
+ if (isProjectInstallPath(resolved)) return resolved;
1835
+ lastError = new Error(
1836
+ `@biomejs/biome resolved to ${resolved}, which is not part of this project's installed dependencies. Add @biomejs/biome to the project to format with it.`
1837
+ );
1838
+ }
1839
+ throw lastError ?? new Error("@biomejs/biome could not be resolved");
1840
+ }
915
1841
  function biomeBinary(startDir) {
916
- const require_ = (0, import_node_module.createRequire)((0, import_node_url.pathToFileURL)(import_node_path2.default.join(startDir, "noop.js")));
917
- const manifestPath = require_.resolve("@biomejs/biome/package.json", {
918
- paths: [startDir, process.cwd()]
919
- });
1842
+ const manifestPath = biomeManifest(startDir);
920
1843
  const manifest = JSON.parse((0, import_node_fs2.readFileSync)(manifestPath, "utf8"));
921
1844
  const relative = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.biome;
922
1845
  if (typeof relative !== "string") {
@@ -985,10 +1908,13 @@ async function formatCode(code, filePath, fmt) {
985
1908
  }
986
1909
  // Annotate the CommonJS export names for ESM import in node:
987
1910
  0 && (module.exports = {
1911
+ AFFIX_PREFIX_PATTERN,
988
1912
  AFFIX_PROBE_TABLE,
1913
+ AFFIX_SUFFIX_PATTERN,
989
1914
  CODEPOINT_LENGTH,
990
1915
  COERCIBLE_DATE_STRING,
991
1916
  COLUMN_FORMATS,
1917
+ CONSTRAINTS_MODULE,
992
1918
  DEFAULT_IMPORT_EXTENSION,
993
1919
  DEFAULT_MODE_PREFIX,
994
1920
  DEFAULT_NESTED_DEPTH,
@@ -998,35 +1924,54 @@ async function formatCode(code, filePath, fmt) {
998
1924
  MAX_NESTED_DEPTH,
999
1925
  NAME_MODES,
1000
1926
  NESTED_PREFIX,
1927
+ NUMERIC_CANON_NAME,
1928
+ NUMERIC_CANON_SOURCE,
1001
1929
  applyTableCase,
1930
+ applyWirePolicy,
1002
1931
  buildBrandPlan,
1003
1932
  buildNestedPlan,
1933
+ canonicalMembers,
1934
+ canonicalNumericText,
1935
+ classifyTableChecks,
1004
1936
  columnMetaFacts,
1937
+ comparisonWire,
1005
1938
  describeSet,
1939
+ fileWriter,
1006
1940
  formatCode,
1007
1941
  importSpecifier,
1008
1942
  insertColumns,
1009
1943
  isGeneratedColumn,
1010
1944
  isIntegerColumn,
1945
+ isProjectInstallPath,
1946
+ lengthCheckLabel,
1947
+ lengthMeasure,
1948
+ measureExpression,
1011
1949
  moduleFileName,
1012
1950
  moduleSpecifier,
1951
+ needsNumericCanon,
1013
1952
  nestedArmNotes,
1014
1953
  nestedNodeColumns,
1015
1954
  nestedSchemaName,
1016
1955
  nestedTypeName,
1017
1956
  nonFiniteAccepted,
1957
+ nonFiniteRefused,
1018
1958
  parseCheck,
1019
1959
  parsesToADate,
1020
1960
  pascalCase,
1961
+ renderConstraintsModule,
1021
1962
  renderDuplicateFinder,
1022
1963
  resolveAffix,
1023
1964
  resolveBranding,
1024
1965
  resolveConfiguredImport,
1966
+ resolveConstraints,
1025
1967
  resolveNestedDepth,
1026
1968
  schemaName,
1027
1969
  selectColumns,
1970
+ tableConstraints,
1028
1971
  tableMetaFacts,
1029
1972
  typeName,
1030
1973
  updateColumns,
1031
- validateAffix
1974
+ validateAffix,
1975
+ wireLiteralFit,
1976
+ wireNumberLiteral
1032
1977
  });