@drzl/validation-core 3.18.0 → 3.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,11 +6,34 @@ import path from "path";
6
6
  import { pathToFileURL } from "url";
7
7
 
8
8
  // src/checks.ts
9
+ function lengthMeasure(column, check) {
10
+ if (column.arrayDimensions) return void 0;
11
+ if (column.shape?.kind === "buffer") return "byteLength";
12
+ if (column.shape) return void 0;
13
+ if (column.tsType !== "string") return void 0;
14
+ return check.unit === "bytes" ? "utf8Bytes" : "codePoints";
15
+ }
16
+ function measureExpression(measure, variable) {
17
+ switch (measure) {
18
+ case "codePoints":
19
+ return `[...${variable}].length`;
20
+ case "utf8Bytes":
21
+ return `new TextEncoder().encode(${variable}).length`;
22
+ case "byteLength":
23
+ return `${variable}.length`;
24
+ }
25
+ }
26
+ function lengthCheckLabel(check) {
27
+ const fn = check.unit === "bytes" ? "octet_length" : "length";
28
+ const rule = `${fn}(${check.column}) ${check.operator} ${check.value}`;
29
+ return check.name ? `${check.name}: ${rule}` : rule;
30
+ }
9
31
  var COMPARISON = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(>=|<=|<>|!=|>|<|=)\s*(.+?)\s*$/;
10
32
  var IN_LIST = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IN\s*\((.+)\)\s*$/i;
11
33
  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;
12
- var LENGTH_OF = /^\s*(?:length|char_length)\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)\s*(>=|<=|<>|!=|>|<|=)\s*(\d+)\s*$/i;
13
- function splitTopLevelAnd(expr) {
34
+ var LENGTH_OF = /^\s*(length|char_length|octet_length)\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)\s*(>=|<=|<>|!=|>|<|=)\s*(\d+)\s*$/i;
35
+ var WORD = /[A-Za-z0-9_]/;
36
+ function splitTopLevel(expr, keyword) {
14
37
  const parts = [];
15
38
  let depth = 0;
16
39
  let inString = false;
@@ -30,18 +53,64 @@ function splitTopLevelAnd(expr) {
30
53
  }
31
54
  if (c === "(") depth++;
32
55
  else if (c === ")") depth--;
33
- else if (depth === 0 && /\s/.test(c)) {
34
- const m = /^\s+AND\s+/i.exec(expr.slice(i));
35
- if (m) {
36
- parts.push(expr.slice(start, i));
37
- i += m[0].length - 1;
38
- start = i + 1;
39
- }
56
+ else if (depth === 0 && WORD.test(c)) {
57
+ if (i > 0 && WORD.test(expr[i - 1])) continue;
58
+ if (expr.slice(i, i + keyword.length).toUpperCase() !== keyword) continue;
59
+ const after = expr[i + keyword.length];
60
+ if (after !== void 0 && WORD.test(after)) continue;
61
+ parts.push(expr.slice(start, i));
62
+ i += keyword.length - 1;
63
+ start = i + 1;
40
64
  }
41
65
  }
42
66
  parts.push(expr.slice(start));
43
67
  return parts;
44
68
  }
69
+ function hasLogicalNot(expr) {
70
+ let inString = false;
71
+ for (let i = 0; i < expr.length; i++) {
72
+ const c = expr[i];
73
+ if (inString) {
74
+ if (c === "'") {
75
+ if (expr[i + 1] === "'") i++;
76
+ else inString = false;
77
+ }
78
+ continue;
79
+ }
80
+ if (c === "'") {
81
+ inString = true;
82
+ continue;
83
+ }
84
+ if (c !== "N" && c !== "n") continue;
85
+ if (i > 0 && WORD.test(expr[i - 1])) continue;
86
+ if (!/^NOT($|[^A-Za-z0-9_])/i.test(expr.slice(i))) continue;
87
+ if (/(^|[^A-Za-z0-9_])IS\s+$/i.test(expr.slice(0, i))) continue;
88
+ return true;
89
+ }
90
+ return false;
91
+ }
92
+ var COMBINING = /(?:^|\s)(\|\||[+\-*/%])(?:\s)/;
93
+ function combiningOperator(expr) {
94
+ let inString = false;
95
+ let bare = "";
96
+ for (let i = 0; i < expr.length; i++) {
97
+ const c = expr[i];
98
+ if (inString) {
99
+ if (c === "'") {
100
+ if (expr[i + 1] === "'") i++;
101
+ else inString = false;
102
+ }
103
+ continue;
104
+ }
105
+ if (c === "'") {
106
+ inString = true;
107
+ bare += " ";
108
+ continue;
109
+ }
110
+ bare += c;
111
+ }
112
+ return COMBINING.exec(bare)?.[1];
113
+ }
45
114
  function unwrap(expr) {
46
115
  let e = expr.trim();
47
116
  while (e.startsWith("(") && e.endsWith(")")) {
@@ -72,16 +141,16 @@ function unwrap(expr) {
72
141
  }
73
142
  return e;
74
143
  }
75
- function splitTopLevelCommas(list) {
144
+ function splitTopLevelCommas(list2) {
76
145
  const parts = [];
77
146
  let depth = 0;
78
147
  let inString = false;
79
148
  let start = 0;
80
- for (let i = 0; i < list.length; i++) {
81
- const c = list[i];
149
+ for (let i = 0; i < list2.length; i++) {
150
+ const c = list2[i];
82
151
  if (inString) {
83
152
  if (c === "'") {
84
- if (list[i + 1] === "'") i++;
153
+ if (list2[i + 1] === "'") i++;
85
154
  else inString = false;
86
155
  }
87
156
  continue;
@@ -90,14 +159,17 @@ function splitTopLevelCommas(list) {
90
159
  else if (c === "(") depth++;
91
160
  else if (c === ")") depth--;
92
161
  else if (c === "," && depth === 0) {
93
- parts.push(list.slice(start, i));
162
+ parts.push(list2.slice(start, i));
94
163
  start = i + 1;
95
164
  }
96
165
  }
97
- parts.push(list.slice(start));
166
+ parts.push(list2.slice(start));
98
167
  return parts;
99
168
  }
100
169
  var BETWEEN = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+BETWEEN\s+(.+?)\s+AND\s+(.+?)\s*$/i;
170
+ var IS_NULL = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IS\s+(NOT\s+)?NULL\s*$/i;
171
+ var IS_DISTINCT = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IS\s+(NOT\s+)?DISTINCT\s+FROM\s+(.+?)\s*$/i;
172
+ var IS_BOOLEAN = /^\s*[A-Za-z_][A-Za-z0-9_]*\s+IS\s+(?:NOT\s+)?(TRUE|FALSE|UNKNOWN)\s*$/i;
101
173
  function literal(raw) {
102
174
  const t = raw.trim();
103
175
  if (/^-?\d+(\.\d+)?$/.test(t)) return { value: t, kind: "number" };
@@ -105,12 +177,111 @@ function literal(raw) {
105
177
  if (m) return { value: m[1].replace(/''/g, "'"), kind: "string" };
106
178
  return void 0;
107
179
  }
180
+ function columnsOf(parsed) {
181
+ const out = /* @__PURE__ */ new Set();
182
+ for (const c of parsed.checks) out.add(c.column);
183
+ for (const s of parsed.sets ?? []) out.add(s.column);
184
+ for (const l of parsed.lengths ?? []) out.add(l.column);
185
+ for (const a of parsed.cardinalities ?? []) out.add(a.column);
186
+ for (const n of parsed.nulls ?? []) out.add(n.column);
187
+ for (const r of parsed.rows ?? []) {
188
+ out.add(r.left);
189
+ out.add(r.right);
190
+ }
191
+ return out;
192
+ }
193
+ function parseDisjunction(branches, name) {
194
+ const guarded = [];
195
+ const rest = [];
196
+ for (const b of branches) {
197
+ const m = unwrap(b).match(IS_NULL);
198
+ if (m && !m[2]) guarded.push(m[1]);
199
+ else rest.push(b);
200
+ }
201
+ const reduced = (() => {
202
+ if (!rest.length)
203
+ return {
204
+ ok: false,
205
+ reason: "every branch of the OR is a null test, which is a rule about the row"
206
+ };
207
+ if (rest.length === 1) return parseCheck(rest[0], name);
208
+ return foldDisjunctionToSet(rest, name);
209
+ })();
210
+ if (!reduced.ok || !guarded.length) return reduced;
211
+ if (reduced.nulls?.length)
212
+ return {
213
+ ok: false,
214
+ reason: "a null test guarded by IS NULL, which is true of every row rather than a narrowing"
215
+ };
216
+ const named = columnsOf(reduced);
217
+ const stray = guarded.filter((g) => !named.has(g));
218
+ if (stray.length)
219
+ return {
220
+ ok: false,
221
+ reason: `${stray.map((s) => `"${s}"`).join(" and ")} IS NULL guards a predicate that does not name it`
222
+ };
223
+ return reduced;
224
+ }
225
+ function foldDisjunctionToSet(branches, name) {
226
+ let column;
227
+ let kind;
228
+ const values = [];
229
+ for (const branch of branches) {
230
+ const parsed = parseCheck(branch, name);
231
+ if (!parsed.ok)
232
+ return { ok: false, reason: `part of an OR was not understood: ${parsed.reason}` };
233
+ if (parsed.rows?.length)
234
+ return {
235
+ ok: false,
236
+ reason: "a branch of the OR compares two columns, which is a rule about the row"
237
+ };
238
+ if (parsed.lengths?.length || parsed.cardinalities?.length)
239
+ return {
240
+ ok: false,
241
+ reason: "a branch of the OR is a count rather than a value, so the OR states no set"
242
+ };
243
+ if (parsed.nulls?.length)
244
+ return { ok: false, reason: "a branch of the OR is a null test rather than a value" };
245
+ const set = parsed.sets?.[0];
246
+ const range = parsed.checks.find((c) => c.operator !== "=");
247
+ if (range)
248
+ return {
249
+ ok: false,
250
+ reason: `a branch of the OR is a range (${range.column} ${range.operator} ${range.value}) rather than a set of values`
251
+ };
252
+ if (parsed.checks.length + (parsed.sets?.length ?? 0) !== 1)
253
+ return { ok: false, reason: "a branch of the OR states more than one thing" };
254
+ const here = set ? { column: set.column, kind: set.kind, values: set.values } : {
255
+ column: parsed.checks[0].column,
256
+ kind: parsed.checks[0].kind,
257
+ values: [parsed.checks[0].value]
258
+ };
259
+ if (column === void 0) {
260
+ column = here.column;
261
+ kind = here.kind;
262
+ }
263
+ if (here.column !== column)
264
+ return {
265
+ ok: false,
266
+ reason: `the OR branches constrain different columns (${column}, ${here.column}), so it states a rule about the row rather than about a field`
267
+ };
268
+ if (here.kind !== kind)
269
+ return { ok: false, reason: "the OR branches mix a string and a number" };
270
+ for (const v of here.values) if (!values.includes(v)) values.push(v);
271
+ }
272
+ return {
273
+ ok: true,
274
+ checks: [],
275
+ sets: [{ column, values, kind, name }]
276
+ };
277
+ }
108
278
  function parseCheck(expression, name) {
109
279
  const expr = unwrap((expression ?? "").trim());
110
280
  if (!expr) return { ok: false, reason: "empty expression" };
111
281
  if (expr.includes("?")) return { ok: false, reason: "expression contains an unresolved value" };
112
- if (/(^|[\s)])OR($|[\s(])/i.test(expr)) return { ok: false, reason: "contains OR" };
113
- if (/(^|[\s(])NOT($|[\s(])/i.test(expr)) return { ok: false, reason: "contains NOT" };
282
+ const branches = splitTopLevel(expr, "OR");
283
+ if (branches.length > 1) return parseDisjunction(branches, name);
284
+ if (hasLogicalNot(expr)) return { ok: false, reason: "contains NOT" };
114
285
  const between = expr.match(BETWEEN);
115
286
  if (between) {
116
287
  const lo = literal(between[2]);
@@ -125,13 +296,14 @@ function parseCheck(expression, name) {
125
296
  ]
126
297
  };
127
298
  }
128
- const parts = splitTopLevelAnd(expr);
299
+ const parts = splitTopLevel(expr, "AND");
129
300
  if (parts.length > 1) {
130
301
  const checks = [];
131
302
  const sets = [];
132
303
  const rows = [];
133
304
  const lengths = [];
134
305
  const cardinalities = [];
306
+ const nulls = [];
135
307
  for (const part of parts) {
136
308
  const parsed = parseCheck(part, name);
137
309
  if (!parsed.ok)
@@ -141,6 +313,7 @@ function parseCheck(expression, name) {
141
313
  if (parsed.rows) rows.push(...parsed.rows);
142
314
  if (parsed.lengths) lengths.push(...parsed.lengths);
143
315
  if (parsed.cardinalities) cardinalities.push(...parsed.cardinalities);
316
+ if (parsed.nulls) nulls.push(...parsed.nulls);
144
317
  }
145
318
  return {
146
319
  ok: true,
@@ -148,16 +321,42 @@ function parseCheck(expression, name) {
148
321
  ...sets.length ? { sets } : {},
149
322
  ...rows.length ? { rows } : {},
150
323
  ...lengths.length ? { lengths } : {},
151
- ...cardinalities.length ? { cardinalities } : {}
324
+ ...cardinalities.length ? { cardinalities } : {},
325
+ ...nulls.length ? { nulls } : {}
152
326
  };
153
327
  }
328
+ const isNull = expr.match(IS_NULL);
329
+ if (isNull) {
330
+ return {
331
+ ok: true,
332
+ checks: [],
333
+ nulls: [{ column: isNull[1], notNull: !!isNull[2], ...name ? { name } : {} }]
334
+ };
335
+ }
336
+ const isDistinct = expr.match(IS_DISTINCT);
337
+ if (isDistinct) {
338
+ const value2 = literal(isDistinct[3]);
339
+ if (!value2) return { ok: false, reason: "the right side of IS DISTINCT FROM is not a literal" };
340
+ const column = isDistinct[1];
341
+ const negated = !!isDistinct[2];
342
+ return {
343
+ ok: true,
344
+ checks: [
345
+ { column, operator: negated ? "=" : "<>", value: value2.value, kind: value2.kind, name }
346
+ ],
347
+ ...negated ? { nulls: [{ column, notNull: true, ...name ? { name } : {} }] } : {}
348
+ };
349
+ }
350
+ if (IS_BOOLEAN.test(expr))
351
+ return { ok: false, reason: "a boolean IS test, whose literal this version does not read" };
154
352
  const lengthOf = expr.match(LENGTH_OF);
155
353
  if (lengthOf) {
156
- const op2 = lengthOf[2] === "!=" ? "<>" : lengthOf[2];
354
+ const op2 = lengthOf[3] === "!=" ? "<>" : lengthOf[3];
355
+ const unit = lengthOf[1].toLowerCase() === "octet_length" ? "bytes" : "characters";
157
356
  return {
158
357
  ok: true,
159
358
  checks: [],
160
- lengths: [{ column: lengthOf[1], operator: op2, value: lengthOf[3], name }]
359
+ lengths: [{ column: lengthOf[2], operator: op2, value: lengthOf[4], unit, name }]
161
360
  };
162
361
  }
163
362
  const cardinalityOf = expr.match(CARDINALITY_OF);
@@ -197,8 +396,16 @@ function parseCheck(expression, name) {
197
396
  ]
198
397
  };
199
398
  }
399
+ const combining = combiningOperator(expr);
400
+ const arithmetic = () => ({
401
+ ok: false,
402
+ reason: `columns combined with "${combining}", which this version does not evaluate`
403
+ });
200
404
  const cmp = expr.match(COMPARISON);
201
- if (!cmp) return { ok: false, reason: "not a single comparison this version understands" };
405
+ if (!cmp) {
406
+ if (combining) return arithmetic();
407
+ return { ok: false, reason: "not a single comparison this version understands" };
408
+ }
202
409
  const value = literal(cmp[3]);
203
410
  if (!value) {
204
411
  const right = cmp[3].trim();
@@ -206,6 +413,7 @@ function parseCheck(expression, name) {
206
413
  const op2 = cmp[2] === "!=" ? "<>" : cmp[2];
207
414
  return { ok: true, checks: [], rows: [{ left: cmp[1], right, operator: op2, name }] };
208
415
  }
416
+ if (combining) return arithmetic();
209
417
  return { ok: false, reason: "right side is not a literal" };
210
418
  }
211
419
  const op = cmp[2] === "!=" ? "<>" : cmp[2];
@@ -214,68 +422,123 @@ function parseCheck(expression, name) {
214
422
  checks: [{ column: cmp[1], operator: op, value: value.value, kind: value.kind, name }]
215
423
  };
216
424
  }
425
+ function wireNumberLiteral(column, value) {
426
+ return column.tsType === "bigint" && /^-?\d+$/.test(value) ? `${value}n` : value;
427
+ }
217
428
  function describeSet(set) {
218
429
  const shown = set.values.map((v) => set.kind === "string" ? `'${v}'` : v).join(", ");
219
430
  return `${set.name ? `${set.name}: ` : ""}${set.column} IN (${shown})`;
220
431
  }
221
-
222
- // src/files.ts
223
- import nodeFs from "fs";
224
- import nodePath from "path";
225
- var IMPORT_EXTENSIONS = ["js", "none", "ts"];
226
- var DEFAULT_IMPORT_EXTENSION = "js";
227
- var TS_EXTENSIONS = [
228
- { ext: ".mts", js: ".mjs", none: ".mjs" },
229
- { ext: ".cts", js: ".cjs", none: ".cjs" },
230
- { ext: ".tsx", js: ".js", none: "" },
231
- { ext: ".ts", js: ".js", none: "" }
232
- ];
233
- function moduleFileName(tsName, fileSuffix) {
234
- return `${tsName}${fileSuffix}`;
432
+ function comparisonWire(c) {
433
+ if (c.shape || c.arrayDimensions) return "opaque";
434
+ if (c.tsType === "number") return "number";
435
+ if (c.tsType === "bigint") return "bigint";
436
+ if (c.tsType !== "string") return "opaque";
437
+ return c.dbType === "NUMERIC" || c.dbType === "BIGINT" ? "numeric-string" : "text";
235
438
  }
236
- function importSpecifier(relativePath, importExtension = DEFAULT_IMPORT_EXTENSION) {
237
- for (const { ext, js, none } of TS_EXTENSIONS) {
238
- if (!relativePath.endsWith(ext)) continue;
239
- const stem = relativePath.slice(0, -ext.length);
240
- if (importExtension === "ts") return relativePath;
241
- return `${stem}${importExtension === "none" ? none : js}`;
242
- }
243
- return relativePath;
244
- }
245
- function moduleSpecifier(tsName, fileSuffix, importExtension = DEFAULT_IMPORT_EXTENSION) {
246
- return importSpecifier(`./${moduleFileName(tsName, fileSuffix)}`, importExtension);
439
+ function canonicalNumericText(text) {
440
+ const m = /^([+-]?)(\d*)(?:\.(\d*))?$/.exec(text.trim());
441
+ if (!m || !m[2] && !m[3]) return void 0;
442
+ const int = (m[2] ?? "").replace(/^0+/, "");
443
+ const frac = (m[3] ?? "").replace(/0+$/, "");
444
+ if (!int && !frac) return "0";
445
+ return (m[1] === "-" ? "-" : "") + (int || "0") + (frac ? "." + frac : "");
247
446
  }
248
- function resolveConfiguredImport(configured, outDirAbs, cwd, importExtension = DEFAULT_IMPORT_EXTENSION) {
249
- if (isPackageSpecifier(configured)) return configured;
250
- const targetAbs = nodePath.isAbsolute(configured) ? configured : nodePath.resolve(configured.startsWith(".") ? outDirAbs : cwd, configured);
251
- const withIndex = pointsAtDirectory(targetAbs) ? `${configured}/index` : configured;
252
- if (configured.startsWith(".")) {
253
- return importSpecifier(withTsExtension(withIndex), importExtension);
447
+ function canonicalMembers(values) {
448
+ const out = [];
449
+ for (const v of values) {
450
+ const c = canonicalNumericText(v);
451
+ if (c !== void 0 && !out.includes(c)) out.push(c);
254
452
  }
255
- const resolved = pointsAtDirectory(targetAbs) ? nodePath.join(targetAbs, "index") : targetAbs;
256
- const rel = nodePath.relative(outDirAbs, resolved).split(nodePath.sep).join("/");
257
- const prefixed = !rel ? "." : rel.startsWith(".") ? rel : `./${rel}`;
258
- return importSpecifier(withTsExtension(prefixed), importExtension);
453
+ return out;
259
454
  }
260
- function isPackageSpecifier(p) {
261
- if (p.startsWith(".") || nodePath.isAbsolute(p)) return false;
262
- if (p.startsWith("@") || p.includes(":")) return true;
263
- return !p.includes("/");
455
+ var NUMERIC_CANON_NAME = "DrzlNumericCanon";
456
+ var NUMERIC_CANON_SOURCE = `/**
457
+ * The canonical spelling of the plain decimal text a numeric wire carries, or null for anything
458
+ * else. The driver spells one value many ways by declared scale ('1', '1.00', '1.0000000000',
459
+ * measured) and the database compares them as numbers, so equality is decided on this form:
460
+ * sign normalised, leading integer zeros and trailing fraction zeros stripped, a bare trailing
461
+ * dot dropped. String arithmetic on purpose: Number() is not usable here, because a numeric
462
+ * column carries more digits than a double holds and rounding would merge values the database
463
+ * keeps distinct.
464
+ */
465
+ const ${NUMERIC_CANON_NAME} = (s: string): string | null => {
466
+ const m = /^([+-]?)(\\d*)(?:\\.(\\d*))?$/.exec(s.trim());
467
+ if (!m || (!m[2] && !m[3])) return null;
468
+ const int = (m[2] ?? '').replace(/^0+/, '');
469
+ const frac = (m[3] ?? '').replace(/0+$/, '');
470
+ if (!int && !frac) return '0';
471
+ return (m[1] === '-' ? '-' : '') + (int || '0') + (frac ? '.' + frac : '');
472
+ };
473
+ `;
474
+ function wireLiteralFit(c, q) {
475
+ const wire = comparisonWire(c);
476
+ if (wire === "opaque") return { fit: "keep" };
477
+ if (wire === "text") {
478
+ if (q.kind === "string") return { fit: "keep" };
479
+ return {
480
+ fit: "unenforced",
481
+ 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`
482
+ };
483
+ }
484
+ const canon = q.values.map((v) => canonicalNumericText(v));
485
+ const bad = q.values[canon.findIndex((x) => x === void 0)];
486
+ if (wire === "numeric-string") {
487
+ if (bad !== void 0)
488
+ return {
489
+ fit: "unenforced",
490
+ reason: `'${bad}' is not plain decimal text, and the driver spells one numeric value many ways, so no exact comparison can be stated`
491
+ };
492
+ if (q.comparison === "range") return { fit: "respell", values: canon };
493
+ return { fit: "canonical", canon: canonicalMembers(q.values) };
494
+ }
495
+ if (q.kind === "number") return { fit: "keep" };
496
+ if (bad !== void 0)
497
+ return {
498
+ fit: "unenforced",
499
+ 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`
500
+ };
501
+ return { fit: "respell", values: canonicalMembers(q.values) };
264
502
  }
265
- function pointsAtDirectory(absPath) {
266
- try {
267
- return nodeFs.statSync(absPath).isDirectory();
268
- } catch {
269
- for (const ext of [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs"]) {
270
- if (nodeFs.existsSync(`${absPath}${ext}`)) return false;
503
+ function applyWirePolicy(columns, checks, sets) {
504
+ const byName = new Map(columns.map((c) => [c.name, c]));
505
+ const outChecks = [];
506
+ const outSets = [];
507
+ const unenforced = [];
508
+ for (const k of checks) {
509
+ const c = byName.get(k.column);
510
+ if (!c) {
511
+ outChecks.push(k);
512
+ continue;
271
513
  }
272
- return !/\.[a-z]+$/i.test(nodePath.basename(absPath));
514
+ const fit = wireLiteralFit(c, {
515
+ kind: k.kind,
516
+ values: [k.value],
517
+ comparison: k.operator === "=" || k.operator === "<>" ? "equality" : "range"
518
+ });
519
+ if (fit.fit === "unenforced") unenforced.push({ column: k.column, reason: fit.reason, check: k });
520
+ else if (fit.fit === "respell") outChecks.push({ ...k, kind: "number", value: fit.values[0] });
521
+ else outChecks.push(k);
522
+ }
523
+ for (const s of sets) {
524
+ const c = byName.get(s.column);
525
+ if (!c) {
526
+ outSets.push(s);
527
+ continue;
528
+ }
529
+ const fit = wireLiteralFit(c, { kind: s.kind, values: s.values, comparison: "equality" });
530
+ if (fit.fit === "unenforced") unenforced.push({ column: s.column, reason: fit.reason, set: s });
531
+ else if (fit.fit === "respell") outSets.push({ ...s, kind: "number", values: fit.values });
532
+ else outSets.push(s);
273
533
  }
534
+ return { checks: outChecks, sets: outSets, unenforced };
274
535
  }
275
- function withTsExtension(p) {
276
- if (/\.(ts|tsx|mts|cts)$/.test(p)) return p;
277
- if (/\.(js|mjs|cjs)$/.test(p)) return p.replace(/\.(js|mjs|cjs)$/, ".ts");
278
- return `${p}.ts`;
536
+ function needsNumericCanon(columns, checks, sets) {
537
+ return columns.some(
538
+ (c) => comparisonWire(c) === "numeric-string" && (sets.some((s) => s.column === c.name) || checks.some(
539
+ (k) => k.column === c.name && (k.operator === "=" || k.operator === "<>")
540
+ ))
541
+ );
279
542
  }
280
543
 
281
544
  // src/naming.ts
@@ -333,8 +596,14 @@ function schemaName(mode, tsName, affix) {
333
596
  function typeName(mode, tsName, affix) {
334
597
  return affix.type.prefix[mode] + applyTableCase(tsName, affix.tableCase) + affix.type.suffix[mode];
335
598
  }
336
- var PREFIX_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
337
- var SUFFIX_RE = /^[A-Za-z0-9_$]+$/;
599
+ var ID_START = "A-Za-z_$";
600
+ var ID_PART = "A-Za-z0-9_$";
601
+ var PREFIX_BODY = `[${ID_START}][${ID_PART}]*`;
602
+ var SUFFIX_BODY = `[${ID_PART}]+`;
603
+ var PREFIX_RE = new RegExp(`^${PREFIX_BODY}$`);
604
+ var SUFFIX_RE = new RegExp(`^${SUFFIX_BODY}$`);
605
+ var AFFIX_PREFIX_PATTERN = `^(?:${PREFIX_BODY})?$`;
606
+ var AFFIX_SUFFIX_PATTERN = `^(?:${SUFFIX_BODY})?$`;
338
607
  function validateAffix(affix, schemaSuffix) {
339
608
  const issues = [];
340
609
  if (!affix) return issues;
@@ -384,7 +653,808 @@ function validateAffix(affix, schemaSuffix) {
384
653
  return issues;
385
654
  }
386
655
 
656
+ // src/branding.ts
657
+ function resolveBranding(opt) {
658
+ if (!opt) return void 0;
659
+ if (opt === true) return { foreignKeys: true, aliases: true };
660
+ if (opt.enabled === false) return void 0;
661
+ return { foreignKeys: opt.foreignKeys !== false, aliases: opt.aliases !== false };
662
+ }
663
+ var at = (tsName, column) => `${tsName}\0${column}`;
664
+ function referenceOf(table, columnName) {
665
+ const col = table.columns.find((c) => c.name === columnName);
666
+ if (col?.references) return { sqlTable: col.references.table, column: col.references.column };
667
+ for (const fk of table.foreignKeys ?? []) {
668
+ const i = fk.columns.indexOf(columnName);
669
+ if (i === -1) continue;
670
+ const target = fk.foreignColumns[i];
671
+ if (target === void 0) continue;
672
+ return { sqlTable: fk.foreignTable, column: target };
673
+ }
674
+ return void 0;
675
+ }
676
+ function isKeyColumn(table, columnName) {
677
+ return (table.primaryKey?.columns ?? []).includes(columnName);
678
+ }
679
+ function buildBrandPlan(tables, opt) {
680
+ const resolved = resolveBranding(opt);
681
+ if (!resolved) return void 0;
682
+ const notes = [];
683
+ const byTs = /* @__PURE__ */ new Map();
684
+ const dupTs = /* @__PURE__ */ new Set();
685
+ for (const t of tables) {
686
+ if (byTs.has(t.tsName)) dupTs.add(t.tsName);
687
+ byTs.set(t.tsName, t);
688
+ }
689
+ for (const name of dupTs) {
690
+ byTs.delete(name);
691
+ notes.push(
692
+ `two tables are exported as "${name}", so nothing on either is branded: a brand token is built from the export name and the two would be indistinguishable.`
693
+ );
694
+ }
695
+ const bySqlName = /* @__PURE__ */ new Map();
696
+ for (const t of byTs.values()) {
697
+ const list2 = bySqlName.get(t.name) ?? [];
698
+ list2.push(t);
699
+ bySqlName.set(t.name, list2);
700
+ }
701
+ const tokens = /* @__PURE__ */ new Map();
702
+ const cache = /* @__PURE__ */ new Map();
703
+ const resolveToken = (tsName, columnName) => {
704
+ const start = at(tsName, columnName);
705
+ if (cache.has(start)) return cache.get(start);
706
+ const origin = byTs.get(tsName)?.columns.find((c) => c.name === columnName);
707
+ if (!origin || origin.arrayDimensions) {
708
+ cache.set(start, void 0);
709
+ return void 0;
710
+ }
711
+ const seen = /* @__PURE__ */ new Set();
712
+ let curTable = tsName;
713
+ let curColumn = columnName;
714
+ let answer;
715
+ let terminal;
716
+ for (; ; ) {
717
+ const key = at(curTable, curColumn);
718
+ if (seen.has(key)) {
719
+ notes.push(
720
+ `${tsName}.${columnName} references a cycle of foreign keys, so it is not branded.`
721
+ );
722
+ break;
723
+ }
724
+ seen.add(key);
725
+ const table = byTs.get(curTable);
726
+ const col = table?.columns.find((c) => c.name === curColumn);
727
+ if (!table || !col) break;
728
+ const ref = resolved.foreignKeys ? referenceOf(table, curColumn) : void 0;
729
+ if (ref) {
730
+ const candidates = bySqlName.get(ref.sqlTable) ?? [];
731
+ if (candidates.length !== 1) {
732
+ notes.push(
733
+ candidates.length === 0 ? `${tsName}.${columnName} references a table "${ref.sqlTable}" that is not in this analysis, so it is not branded.` : `${tsName}.${columnName} references "${ref.sqlTable}", which names more than one table here, so it is not branded.`
734
+ );
735
+ break;
736
+ }
737
+ const next = candidates[0];
738
+ if (!next.columns.some((c) => c.name === ref.column)) {
739
+ notes.push(
740
+ `${tsName}.${columnName} references ${next.tsName}.${ref.column}, which is not a column of that table, so it is not branded.`
741
+ );
742
+ break;
743
+ }
744
+ curTable = next.tsName;
745
+ curColumn = ref.column;
746
+ continue;
747
+ }
748
+ if (isKeyColumn(table, curColumn)) {
749
+ answer = `${curTable}.${curColumn}`;
750
+ terminal = col;
751
+ }
752
+ break;
753
+ }
754
+ if (answer && terminal && terminal.tsType !== origin.tsType) {
755
+ notes.push(
756
+ `${tsName}.${columnName} is a ${origin.tsType} and ${answer} is a ${terminal.tsType}, so it is not branded.`
757
+ );
758
+ answer = void 0;
759
+ }
760
+ cache.set(start, answer);
761
+ return answer;
762
+ };
763
+ for (const t of byTs.values()) {
764
+ for (const c of t.columns) {
765
+ const token = resolveToken(t.tsName, c.name);
766
+ if (token) tokens.set(at(t.tsName, c.name), token);
767
+ }
768
+ }
769
+ const aliases = /* @__PURE__ */ new Map();
770
+ if (resolved.aliases) {
771
+ const claimed = /* @__PURE__ */ new Map();
772
+ const draft = [];
773
+ for (const t of byTs.values()) {
774
+ for (const c of t.columns) {
775
+ const token = tokens.get(at(t.tsName, c.name));
776
+ if (token !== `${t.tsName}.${c.name}`) continue;
777
+ const alias = pascalCase(t.tsName) + pascalCase(c.name);
778
+ draft.push({ tsName: t.tsName, entry: { alias, column: c.name, token } });
779
+ claimed.set(alias, [...claimed.get(alias) ?? [], `${t.tsName}.${c.name}`]);
780
+ }
781
+ }
782
+ for (const { tsName, entry } of draft) {
783
+ const owners = claimed.get(entry.alias) ?? [];
784
+ if (owners.length > 1) continue;
785
+ aliases.set(tsName, [...aliases.get(tsName) ?? [], entry]);
786
+ }
787
+ for (const [alias, owners] of claimed) {
788
+ if (owners.length > 1) {
789
+ notes.push(
790
+ `${owners.join(" and ")} both name their brand alias "${alias}", so neither is exported. The schemas are unaffected; refer to the type as the select type's property instead.`
791
+ );
792
+ }
793
+ }
794
+ }
795
+ return {
796
+ brandOf: (tsName, columnName) => tokens.get(at(tsName, columnName)),
797
+ aliasesFor: (tsName) => aliases.get(tsName) ?? [],
798
+ any: tokens.size > 0,
799
+ notes
800
+ };
801
+ }
802
+
803
+ // src/constraints.ts
804
+ function labelled(name, text) {
805
+ return name ? `${name}: ${text}` : text;
806
+ }
807
+ function literalText(value, kind) {
808
+ return kind === "string" ? `'${value}'` : value;
809
+ }
810
+ function columnCheckText(k) {
811
+ return labelled(k.name, `${k.column} ${k.operator} ${literalText(k.value, k.kind)}`);
812
+ }
813
+ function setText(k) {
814
+ return labelled(
815
+ k.name,
816
+ `${k.column} IN (${k.values.map((v) => literalText(v, k.kind)).join(", ")})`
817
+ );
818
+ }
819
+ var lengthText = lengthCheckLabel;
820
+ function cardinalityText(k) {
821
+ return labelled(k.name, `cardinality(${k.column}) ${k.operator} ${k.value}`);
822
+ }
823
+ function rowText(k) {
824
+ return labelled(k.name, `${k.left} ${k.operator} ${k.right}`);
825
+ }
826
+ function nullText(k) {
827
+ return labelled(k.name, `${k.column} IS ${k.notNull ? "NOT NULL" : "NULL"}`);
828
+ }
829
+ function shapeArticle(c) {
830
+ switch (c.shape?.kind) {
831
+ case "json":
832
+ return "a JSON";
833
+ case "buffer":
834
+ return "a binary";
835
+ case "numberVector":
836
+ return "a vector";
837
+ case "bitstring":
838
+ return "a bit-string";
839
+ case "byteString":
840
+ return "a byte-string";
841
+ case "custom":
842
+ return "a customType";
843
+ default:
844
+ return "a structured";
845
+ }
846
+ }
847
+ function takesScalarChecks(c) {
848
+ return !c.arrayDimensions && !c.shape;
849
+ }
850
+ function foldsIntoBounds(c, k) {
851
+ if (c.arrayDimensions || c.shape) return false;
852
+ if (c.tsType !== "number" && c.tsType !== "bigint") return false;
853
+ return k.kind === "number" && k.operator !== "=" && k.operator !== "<>";
854
+ }
855
+ function statesCap(c, hasSet) {
856
+ if (c.shape || hasSet) return false;
857
+ if (c.enumValues && c.enumValues.length) return false;
858
+ if (c.tsType !== "string") return false;
859
+ return !c.format;
860
+ }
861
+ function classifyTableChecks(table) {
862
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
863
+ const out = [];
864
+ for (const k of table.checks ?? []) {
865
+ const expression = (k.expression ?? "").trim();
866
+ const parsed = parseCheck(k.expression, k.name);
867
+ if (!parsed.ok) {
868
+ out.push({
869
+ ...k.name ? { name: k.name } : {},
870
+ expression,
871
+ parts: [
872
+ {
873
+ text: labelled(k.name, expression),
874
+ columns: [],
875
+ place: "none",
876
+ reason: parsed.reason
877
+ }
878
+ ]
879
+ });
880
+ continue;
881
+ }
882
+ const parts = [];
883
+ const place = (column, text, guard, guardReason, extra = {}) => {
884
+ const c = byName.get(column);
885
+ if (!c) {
886
+ parts.push({
887
+ text,
888
+ columns: [column],
889
+ place: "none",
890
+ reason: `"${column}" is not a column of that table`
891
+ });
892
+ return;
893
+ }
894
+ if (!guard(c)) {
895
+ parts.push({ text, columns: [column], place: "none", reason: guardReason(c) });
896
+ return;
897
+ }
898
+ parts.push({ text, columns: [column], place: "column", ...extra });
899
+ };
900
+ 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`;
901
+ const literalFit = (column, kind, values, comparison) => {
902
+ const col = byName.get(column);
903
+ return col ? wireLiteralFit(col, { kind, values, comparison }) : { fit: "keep" };
904
+ };
905
+ for (const c of parsed.checks) {
906
+ const fit = literalFit(
907
+ c.column,
908
+ c.kind,
909
+ [c.value],
910
+ c.operator === "=" || c.operator === "<>" ? "equality" : "range"
911
+ );
912
+ if (fit.fit === "unenforced") {
913
+ parts.push({
914
+ text: columnCheckText(c),
915
+ columns: [c.column],
916
+ place: "none",
917
+ reason: fit.reason
918
+ });
919
+ continue;
920
+ }
921
+ const shown = fit.fit === "respell" ? { ...c, kind: "number", value: fit.values[0] } : c;
922
+ const col = byName.get(c.column);
923
+ place(c.column, columnCheckText(shown), takesScalarChecks, notScalar, {
924
+ ...col && foldsIntoBounds(col, shown) ? { bound: { column: c.column, operator: shown.operator, value: shown.value } } : {}
925
+ });
926
+ }
927
+ for (const s of parsed.sets ?? []) {
928
+ const fit = literalFit(s.column, s.kind, s.values, "equality");
929
+ if (fit.fit === "unenforced") {
930
+ parts.push({ text: setText(s), columns: [s.column], place: "none", reason: fit.reason });
931
+ continue;
932
+ }
933
+ const shown = fit.fit === "respell" ? { ...s, kind: "number", values: fit.values } : s;
934
+ place(s.column, setText(shown), takesScalarChecks, notScalar, {
935
+ set: { column: s.column, values: shown.values, kind: shown.kind }
936
+ });
937
+ }
938
+ for (const l of parsed.lengths ?? []) {
939
+ place(
940
+ l.column,
941
+ lengthText(l),
942
+ (c) => lengthMeasure(c, l) !== void 0,
943
+ (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`
944
+ );
945
+ }
946
+ for (const a of parsed.cardinalities ?? []) {
947
+ place(
948
+ a.column,
949
+ cardinalityText(a),
950
+ (c) => !!c.arrayDimensions,
951
+ (c) => `"${c.name}" is not an array, so it has no elements to count`
952
+ );
953
+ }
954
+ for (const n of parsed.nulls ?? []) {
955
+ const text = nullText(n);
956
+ if (!byName.has(n.column)) {
957
+ parts.push({
958
+ text,
959
+ columns: [n.column],
960
+ place: "none",
961
+ reason: `"${n.column}" is not a column of that table`
962
+ });
963
+ continue;
964
+ }
965
+ parts.push(
966
+ n.notNull ? { text, columns: [n.column], place: "column", shape: "notNull" } : {
967
+ text,
968
+ columns: [n.column],
969
+ place: "none",
970
+ reason: "the column may hold only NULL, which these schemas do not narrow it to"
971
+ }
972
+ );
973
+ }
974
+ for (const r of parsed.rows ?? []) {
975
+ const text = rowText(r);
976
+ const missing = [r.left, r.right].filter((n) => !byName.has(n));
977
+ parts.push(
978
+ missing.length ? {
979
+ text,
980
+ columns: [r.left, r.right],
981
+ place: "none",
982
+ reason: `${missing.map((n) => `"${n}"`).join(" and ")} ${missing.length > 1 ? "are not columns" : "is not a column"} of that table`
983
+ } : { text, columns: [r.left, r.right], place: "row" }
984
+ );
985
+ }
986
+ out.push({ ...k.name ? { name: k.name } : {}, expression, parts });
987
+ }
988
+ return out;
989
+ }
990
+ var list = (cols) => cols.join(", ");
991
+ function foreignKeyRule(fk) {
992
+ const target = fk.foreignSchema ? `${fk.foreignSchema}.${fk.foreignTable}` : fk.foreignTable;
993
+ return `FOREIGN KEY (${list(fk.columns)}) REFERENCES ${target} (${list(fk.foreignColumns)})` + (fk.onDelete ? ` ON DELETE ${fk.onDelete}` : "") + (fk.onUpdate ? ` ON UPDATE ${fk.onUpdate}` : "");
994
+ }
995
+ function uniquifier() {
996
+ const seen = /* @__PURE__ */ new Map();
997
+ return (id) => {
998
+ const n = seen.get(id) ?? 0;
999
+ seen.set(id, n + 1);
1000
+ return n === 0 ? id : `${id}_${n + 1}`;
1001
+ };
1002
+ }
1003
+ function tableConstraints(table) {
1004
+ const out = [];
1005
+ const id = uniquifier();
1006
+ const named = (key, fallback) => key.name ? { id: id(key.name), name: key.name } : { id: id(fallback) };
1007
+ if (table.primaryKey?.columns.length) {
1008
+ const pk = table.primaryKey;
1009
+ out.push({
1010
+ ...named(pk, `${table.name}_pkey`),
1011
+ kind: "primaryKey",
1012
+ columns: [...pk.columns],
1013
+ rule: `PRIMARY KEY (${list(pk.columns)})`,
1014
+ // No per-row validator can check a key: whether a value is already taken is a fact about
1015
+ // the table. `duplicateFinder` checks the half that needs no database, and even that only
1016
+ // answers whether a batch collides with itself.
1017
+ enforced: false
1018
+ });
1019
+ }
1020
+ for (const u of table.unique ?? []) {
1021
+ if (!u.columns.length) continue;
1022
+ out.push({
1023
+ ...named(u, `${table.name}_${u.columns.join("_")}_key`),
1024
+ kind: "unique",
1025
+ columns: [...u.columns],
1026
+ rule: `UNIQUE (${list(u.columns)})`,
1027
+ enforced: false
1028
+ });
1029
+ }
1030
+ for (const fk of table.foreignKeys ?? []) {
1031
+ if (!fk.columns.length) continue;
1032
+ out.push({
1033
+ ...named(fk, `${table.name}_${fk.columns.join("_")}_fkey`),
1034
+ kind: "foreignKey",
1035
+ columns: [...fk.columns],
1036
+ rule: foreignKeyRule(fk),
1037
+ // The referenced row either exists or it does not, and only the database knows.
1038
+ enforced: false,
1039
+ references: {
1040
+ table: fk.foreignTable,
1041
+ ...fk.foreignSchema ? { schema: fk.foreignSchema } : {},
1042
+ columns: [...fk.foreignColumns],
1043
+ ...fk.onDelete ? { onDelete: fk.onDelete } : {},
1044
+ ...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
1045
+ }
1046
+ });
1047
+ }
1048
+ const classified = classifyTableChecks(table);
1049
+ classified.forEach((k, i) => {
1050
+ const columns = [];
1051
+ for (const p of k.parts) for (const c of p.columns) if (!columns.includes(c)) columns.push(c);
1052
+ const messages = k.parts.filter((p) => p.place !== "none" && !p.bound && !p.set && !p.shape).map((p) => p.text);
1053
+ const bounds = k.parts.filter((p) => p.bound).map((p) => p.bound);
1054
+ const set = k.parts.find((p) => p.set)?.set;
1055
+ const unenforced = k.parts.filter((p) => p.place === "none").map((p) => ({ part: p.text, reason: p.reason ?? "not translated" }));
1056
+ out.push({
1057
+ ...k.name ? { id: id(k.name), name: k.name } : { id: id(`${table.name}_check_${i + 1}`) },
1058
+ kind: "check",
1059
+ columns,
1060
+ rule: `CHECK (${k.expression})`,
1061
+ enforced: k.parts.some((p) => p.place !== "none"),
1062
+ ...unenforced.length ? { unenforced } : {},
1063
+ ...messages.length ? { messages } : {},
1064
+ ...bounds.length ? { bounds } : {},
1065
+ ...set ? { values: set } : {}
1066
+ });
1067
+ });
1068
+ const setColumns = new Set(
1069
+ classified.flatMap((k) => k.parts.filter((p) => p.set).map((p) => p.set.column))
1070
+ );
1071
+ for (const c of table.columns) {
1072
+ if (!statesCap(c, setColumns.has(c.name))) continue;
1073
+ if (c.maxLength !== void 0) {
1074
+ const message = `at most ${c.maxLength} characters`;
1075
+ out.push({
1076
+ id: id(`${table.name}_${c.name}_maxlength`),
1077
+ kind: "maxLength",
1078
+ columns: [c.name],
1079
+ rule: message,
1080
+ enforced: true,
1081
+ messages: [message]
1082
+ });
1083
+ }
1084
+ if (c.maxBytes !== void 0) {
1085
+ const message = `at most ${c.maxBytes} bytes`;
1086
+ out.push({
1087
+ id: id(`${table.name}_${c.name}_maxbytes`),
1088
+ kind: "maxBytes",
1089
+ columns: [c.name],
1090
+ rule: message,
1091
+ enforced: true,
1092
+ messages: [message]
1093
+ });
1094
+ }
1095
+ }
1096
+ return {
1097
+ table: table.name,
1098
+ ...table.schema ? { schema: table.schema } : {},
1099
+ constraints: out
1100
+ };
1101
+ }
1102
+ function resolveConstraints(opt) {
1103
+ if (!opt) return void 0;
1104
+ if (opt === true) return { errorMap: true };
1105
+ if (opt.enabled === false) return void 0;
1106
+ return { errorMap: opt.errorMap !== false };
1107
+ }
1108
+ var CONSTRAINTS_MODULE = "constraints.ts";
1109
+ var TYPES = `/** What a constraint is. */
1110
+ export type DrzlConstraintKind =
1111
+ | 'primaryKey'
1112
+ | 'unique'
1113
+ | 'foreignKey'
1114
+ | 'check'
1115
+ | 'maxLength'
1116
+ | 'maxBytes';
1117
+
1118
+ /** One constraint on one table. */
1119
+ export interface DrzlConstraint {
1120
+ /** Stable within the table. The SQL constraint name where the declaration has one. */
1121
+ id: string;
1122
+ /** The SQL constraint name, absent where the declaration did not give one. */
1123
+ name?: string;
1124
+ kind: DrzlConstraintKind;
1125
+ /** The columns the constraint is about, in declaration order. */
1126
+ columns: string[];
1127
+ /** The rule as a sentence, for a form with nothing better to show. */
1128
+ rule: string;
1129
+ /** Whether a generated schema can reject a row for this constraint. */
1130
+ enforced: boolean;
1131
+ /** The clauses nothing in these schemas checks, and why. */
1132
+ unenforced?: { part: string; reason: string }[];
1133
+ /** The exact messages the generated schemas attach for this constraint. */
1134
+ messages?: string[];
1135
+ /** Bounds folded into a column's range, which is where the constraint name is lost. */
1136
+ bounds?: { column: string; operator: string; value: string }[];
1137
+ /** A set of literals folded into an enum, which is the other place it is lost. */
1138
+ values?: { column: string; values: string[]; kind: 'number' | 'string' };
1139
+ /** Where a foreign key points. */
1140
+ references?: {
1141
+ table: string;
1142
+ schema?: string;
1143
+ columns: string[];
1144
+ onDelete?: string;
1145
+ onUpdate?: string;
1146
+ };
1147
+ }
1148
+
1149
+ /** Every constraint on one table. */
1150
+ export interface DrzlTableConstraints {
1151
+ /** The SQL table name, which is not the Drizzle export name this is exported under. */
1152
+ table: string;
1153
+ /** The SQL schema, present only when the table names one. */
1154
+ schema?: string;
1155
+ constraints: DrzlConstraint[];
1156
+ }`;
1157
+ var MATCHER = `/** A validation issue traced back to the constraint that caused it. */
1158
+ export interface DrzlConstraintMatch {
1159
+ constraint: DrzlConstraint;
1160
+ /**
1161
+ * The column to put the message on.
1162
+ *
1163
+ * Taken from the issue where the library named one, and from the constraint where it did not.
1164
+ * Valibot reports a row-level check with an empty path, so without the fallback a form would
1165
+ * have a message and nowhere to show it.
1166
+ */
1167
+ column?: string;
1168
+ /**
1169
+ * How the constraint was identified.
1170
+ *
1171
+ * \`message\` is an exact match on a string these schemas wrote, and is the only tier that is
1172
+ * certain. \`bound\` matched the numeric bound the library put on the issue against a bound this
1173
+ * constraint folded, which is what a folded CHECK leaves to match on once its name is gone.
1174
+ * \`column\` is the last resort: the column has exactly one constraint stated in the validator's
1175
+ * own vocabulary, so nothing else on the issue can be wrong about which.
1176
+ */
1177
+ matchedBy: 'message' | 'bound' | 'column';
1178
+ }
1179
+
1180
+ /**
1181
+ * The column an issue is about, across the path shapes these libraries use.
1182
+ *
1183
+ * zod and ArkType spell a path item as the key itself; valibot spells it as an object carrying
1184
+ * one. The last key is taken rather than the first, so an issue inside an array or a nested
1185
+ * payload names the field rather than the collection holding it.
1186
+ */
1187
+ function drzlIssueColumn(issue: any): string | undefined {
1188
+ const path = issue?.path;
1189
+ if (!Array.isArray(path)) return undefined;
1190
+ for (let i = path.length - 1; i >= 0; i--) {
1191
+ const item: any = path[i];
1192
+ if (typeof item === 'string') return item;
1193
+ if (item && typeof item === 'object' && typeof item.key === 'string') return item.key;
1194
+ }
1195
+ return undefined;
1196
+ }
1197
+
1198
+ /**
1199
+ * The numeric bound an issue reports, as a decimal string, or nothing.
1200
+ *
1201
+ * Measured rather than guessed: zod 4.4.3 puts \`minimum\`/\`maximum\` on a \`too_small\`/\`too_big\`
1202
+ * issue and valibot 1.4.2 puts \`requirement\` on a \`min_value\`/\`max_value\` one. A bigint bound
1203
+ * is read too, since a 64 bit column's range is not representable as a number.
1204
+ */
1205
+ function drzlIssueBound(issue: any): string | undefined {
1206
+ for (const key of ['minimum', 'maximum', 'requirement']) {
1207
+ const v = issue?.[key];
1208
+ if (typeof v === 'number' || typeof v === 'bigint') return String(v);
1209
+ }
1210
+ return undefined;
1211
+ }
1212
+
1213
+ /**
1214
+ * The constraint a validation issue came from, or nothing.
1215
+ *
1216
+ * Three tiers, because a constraint does not always survive into the issue in the same form. Every
1217
+ * constraint stated as a predicate carries a message these schemas wrote, and that is an exact
1218
+ * lookup. A numeric CHECK is deliberately folded into the column's own range instead, so the
1219
+ * failure is worded by the library and the constraint name is nowhere in it; the bound is, and it
1220
+ * is matched on that. A set constraint becomes an enum and leaves neither, so it is resolved by
1221
+ * the column alone, which is safe only because a column can carry one such constraint at most.
1222
+ *
1223
+ * The two folds are kept apart rather than pooled, and that is what stops the third tier
1224
+ * over-claiming. A folded bound always reports its bound, in every library measured, so an issue
1225
+ * on that column carrying **no** bound is not that constraint: it is the field failing to be a
1226
+ * number at all. Pooling them answered \`invalid_type\` on a column with a numeric CHECK with the
1227
+ * CHECK, which is a rule the row did not break.
1228
+ *
1229
+ * Returns nothing rather than a guess, for the same reason. A \`too_small\` reporting the column's
1230
+ * own type bound has no matching constraint and gets no answer.
1231
+ */
1232
+ export function constraintForIssue(
1233
+ table: string,
1234
+ issue: unknown
1235
+ ): DrzlConstraintMatch | undefined {
1236
+ const ledger = constraintsByTable[table];
1237
+ if (!ledger) return undefined;
1238
+ const raw: any = issue;
1239
+ const column = drzlIssueColumn(raw);
1240
+ const message = typeof raw?.message === 'string' ? raw.message : undefined;
1241
+
1242
+ if (message !== undefined) {
1243
+ for (const constraint of ledger.constraints) {
1244
+ if (!constraint.messages || constraint.messages.indexOf(message) < 0) continue;
1245
+ if (column !== undefined && constraint.columns.indexOf(column) < 0) continue;
1246
+ return {
1247
+ constraint,
1248
+ column: column ?? constraint.columns[0],
1249
+ matchedBy: 'message',
1250
+ };
1251
+ }
1252
+ }
1253
+
1254
+ if (column === undefined) return undefined;
1255
+
1256
+ const bound = drzlIssueBound(raw);
1257
+ if (bound !== undefined) {
1258
+ const hit = ledger.constraints.find(
1259
+ (c) => c.bounds && c.bounds.some((b) => b.column === column && b.value === bound)
1260
+ );
1261
+ return hit ? { constraint: hit, column, matchedBy: 'bound' } : undefined;
1262
+ }
1263
+
1264
+ const sets = ledger.constraints.filter((c) => c.values && c.values.column === column);
1265
+ return sets.length === 1 ? { constraint: sets[0], column, matchedBy: 'column' } : undefined;
1266
+ }`;
1267
+ function constName(tsName) {
1268
+ const safe = tsName.replace(/[^A-Za-z0-9_$]/g, "_");
1269
+ return `${/^[0-9]/.test(safe) ? `_${safe}` : safe}Constraints`;
1270
+ }
1271
+ function renderConstraintsModule(tables, opts = {}) {
1272
+ const entries = tables.map((t) => ({ tsName: t.tsName, facts: tableConstraints(t) }));
1273
+ const consts = entries.map(
1274
+ (e) => `/** Every constraint on \`${e.facts.table}\`. */
1275
+ export const ${constName(e.tsName)}: DrzlTableConstraints = ${JSON.stringify(e.facts, null, 2)};`
1276
+ ).join("\n\n");
1277
+ const record = `/** Every table's constraints, keyed by the Drizzle export name its schemas are named after. */
1278
+ export const constraintsByTable: Record<string, DrzlTableConstraints> = {
1279
+ ` + entries.map((e) => ` ${JSON.stringify(e.tsName)}: ${constName(e.tsName)},`).join("\n") + `
1280
+ };`;
1281
+ return [
1282
+ "/**",
1283
+ " * Every CHECK, unique constraint, primary and foreign key on each table, as data.",
1284
+ " *",
1285
+ " * Generated beside the schemas rather than derived from them: a validator states what a value",
1286
+ " * must look like and says nothing about which constraint said so, and the two constraints a",
1287
+ " * per-row schema cannot check at all, uniqueness and a foreign key, are not in it in any form.",
1288
+ " */",
1289
+ TYPES,
1290
+ consts,
1291
+ record,
1292
+ ...opts.errorMap ? [MATCHER] : []
1293
+ ].join("\n\n");
1294
+ }
1295
+
1296
+ // src/emit.ts
1297
+ var realFs = null;
1298
+ function nodeFs() {
1299
+ return realFs ??= import("fs/promises");
1300
+ }
1301
+ function fileWriter(sink) {
1302
+ if (!sink) {
1303
+ return {
1304
+ async mkdir(dir, options) {
1305
+ return (await nodeFs()).mkdir(dir, options);
1306
+ },
1307
+ async writeFile(file, contents, encoding = "utf8") {
1308
+ return (await nodeFs()).writeFile(file, contents, encoding);
1309
+ }
1310
+ };
1311
+ }
1312
+ return {
1313
+ async mkdir(dir) {
1314
+ await sink.mkdir(dir);
1315
+ return void 0;
1316
+ },
1317
+ async writeFile(file, contents) {
1318
+ await sink.writeFile(file, contents);
1319
+ }
1320
+ };
1321
+ }
1322
+
1323
+ // src/files.ts
1324
+ import nodeFs2 from "fs";
1325
+ import nodePath from "path";
1326
+ var IMPORT_EXTENSIONS = ["js", "none", "ts"];
1327
+ var DEFAULT_IMPORT_EXTENSION = "js";
1328
+ var TS_EXTENSIONS = [
1329
+ { ext: ".mts", js: ".mjs", none: ".mjs" },
1330
+ { ext: ".cts", js: ".cjs", none: ".cjs" },
1331
+ { ext: ".tsx", js: ".js", none: "" },
1332
+ { ext: ".ts", js: ".js", none: "" }
1333
+ ];
1334
+ function moduleFileName(tsName, fileSuffix) {
1335
+ return `${tsName}${fileSuffix}`;
1336
+ }
1337
+ function importSpecifier(relativePath, importExtension = DEFAULT_IMPORT_EXTENSION) {
1338
+ for (const { ext, js, none } of TS_EXTENSIONS) {
1339
+ if (!relativePath.endsWith(ext)) continue;
1340
+ const stem = relativePath.slice(0, -ext.length);
1341
+ if (importExtension === "ts") return relativePath;
1342
+ return `${stem}${importExtension === "none" ? none : js}`;
1343
+ }
1344
+ return relativePath;
1345
+ }
1346
+ function moduleSpecifier(tsName, fileSuffix, importExtension = DEFAULT_IMPORT_EXTENSION) {
1347
+ return importSpecifier(`./${moduleFileName(tsName, fileSuffix)}`, importExtension);
1348
+ }
1349
+ function resolveConfiguredImport(configured, outDirAbs, cwd, importExtension = DEFAULT_IMPORT_EXTENSION) {
1350
+ if (isPackageSpecifier(configured)) return configured;
1351
+ const targetAbs = nodePath.isAbsolute(configured) ? configured : nodePath.resolve(configured.startsWith(".") ? outDirAbs : cwd, configured);
1352
+ const withIndex = pointsAtDirectory(targetAbs) ? `${configured}/index` : configured;
1353
+ if (configured.startsWith(".")) {
1354
+ return importSpecifier(withTsExtension(withIndex), importExtension);
1355
+ }
1356
+ const resolved = pointsAtDirectory(targetAbs) ? nodePath.join(targetAbs, "index") : targetAbs;
1357
+ const rel = nodePath.relative(outDirAbs, resolved).split(nodePath.sep).join("/");
1358
+ const prefixed = !rel ? "." : rel.startsWith(".") ? rel : `./${rel}`;
1359
+ return importSpecifier(withTsExtension(prefixed), importExtension);
1360
+ }
1361
+ function isPackageSpecifier(p) {
1362
+ if (p.startsWith(".") || nodePath.isAbsolute(p)) return false;
1363
+ if (p.startsWith("@") || p.includes(":")) return true;
1364
+ return !p.includes("/");
1365
+ }
1366
+ function pointsAtDirectory(absPath) {
1367
+ try {
1368
+ return nodeFs2.statSync(absPath).isDirectory();
1369
+ } catch {
1370
+ for (const ext of [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs"]) {
1371
+ if (nodeFs2.existsSync(`${absPath}${ext}`)) return false;
1372
+ }
1373
+ return !/\.[a-z]+$/i.test(nodePath.basename(absPath));
1374
+ }
1375
+ }
1376
+ function withTsExtension(p) {
1377
+ if (/\.(ts|tsx|mts|cts)$/.test(p)) return p;
1378
+ if (/\.(js|mjs|cjs)$/.test(p)) return p.replace(/\.(js|mjs|cjs)$/, ".ts");
1379
+ return `${p}.ts`;
1380
+ }
1381
+
1382
+ // src/meta.ts
1383
+ function classifyChecks(table) {
1384
+ const perColumn = /* @__PURE__ */ new Map();
1385
+ const rows = [];
1386
+ const unenforced = [];
1387
+ for (const check of classifyTableChecks(table)) {
1388
+ for (const part of check.parts) {
1389
+ if (part.place === "none") {
1390
+ unenforced.push(part.text);
1391
+ continue;
1392
+ }
1393
+ if (part.place === "row") {
1394
+ rows.push(part.text);
1395
+ continue;
1396
+ }
1397
+ const column = part.columns[0];
1398
+ const list2 = perColumn.get(column) ?? [];
1399
+ list2.push(part.text);
1400
+ perColumn.set(column, list2);
1401
+ }
1402
+ }
1403
+ return { perColumn, rows, unenforced };
1404
+ }
1405
+ function columnDescription(facts) {
1406
+ const parts = [];
1407
+ if (facts.maxLength !== void 0) parts.push(`at most ${facts.maxLength} characters`);
1408
+ if (facts.maxBytes !== void 0) parts.push(`at most ${facts.maxBytes} bytes`);
1409
+ for (const c of facts.checks ?? []) parts.push(`CHECK ${c}`);
1410
+ return parts.length ? parts.join(". ") : void 0;
1411
+ }
1412
+ function tableDescription(facts) {
1413
+ const parts = [];
1414
+ for (const c of facts.checks ?? []) parts.push(`CHECK ${c}`);
1415
+ if (facts.unenforcedChecks?.length) {
1416
+ parts.push(
1417
+ `not enforced by this schema, the database also checks: ${facts.unenforcedChecks.join("; ")}`
1418
+ );
1419
+ }
1420
+ return parts.length ? parts.join(". ") : void 0;
1421
+ }
1422
+ function columnMetaFacts(column, table, opts = {}) {
1423
+ const checks = classifyChecks(table).perColumn.get(column.name);
1424
+ const facts = {
1425
+ ...column.sqlType ? { sqlType: column.sqlType } : {},
1426
+ ...column.maxLength !== void 0 ? { maxLength: column.maxLength } : {},
1427
+ ...column.maxBytes !== void 0 ? { maxBytes: column.maxBytes } : {},
1428
+ ...column.hasDefault ? { hasDefault: true } : {},
1429
+ ...column.isGenerated ? { generated: true } : {},
1430
+ ...checks?.length ? { checks } : {}
1431
+ };
1432
+ if (!opts.description) return facts;
1433
+ const description = columnDescription(facts);
1434
+ return description ? { ...facts, description } : facts;
1435
+ }
1436
+ function tableMetaFacts(table, opts) {
1437
+ const { rows, unenforced } = classifyChecks(table);
1438
+ const pk = table.primaryKey?.columns ?? [];
1439
+ const unique = (table.unique ?? []).map((k) => k.columns).filter((c) => c.length > 0);
1440
+ const facts = {
1441
+ table: table.name,
1442
+ ...table.schema ? { schema: table.schema } : {},
1443
+ ...opts.dialect ? { dialect: opts.dialect } : {},
1444
+ mode: opts.mode,
1445
+ ...pk.length ? { primaryKey: pk } : {},
1446
+ ...unique.length ? { unique } : {},
1447
+ ...table.readOnly ? { readOnly: true } : {},
1448
+ ...rows.length ? { checks: rows } : {},
1449
+ ...unenforced.length ? { unenforcedChecks: unenforced } : {}
1450
+ };
1451
+ if (!opts.description) return facts;
1452
+ const description = tableDescription(facts);
1453
+ return description ? { ...facts, description } : facts;
1454
+ }
1455
+
387
1456
  // src/nested.ts
1457
+ import { qualifiedForeignTable, qualifiedTableName } from "@drzl/analyzer";
388
1458
  var NESTED_PREFIX = "Nested";
389
1459
  function nestedSchemaName(mode, tsName, affix) {
390
1460
  return NESTED_PREFIX + schemaName(mode, tsName, affix);
@@ -412,13 +1482,14 @@ var KINDS_BY_MODE = {
412
1482
  };
413
1483
  var KIND_ORDER = { many: 0, manyToMany: 1, one: 2 };
414
1484
  function omittedColumnsFor(parent, child) {
415
- const back = (child.foreignKeys ?? []).filter((fk) => fk.foreignTable === parent.name);
1485
+ const parentName = qualifiedTableName(parent);
1486
+ const back = (child.foreignKeys ?? []).filter((fk) => qualifiedForeignTable(fk) === parentName);
416
1487
  if (back.length === 1) return { omitted: [...back[0].columns] };
417
1488
  if (back.length === 0) return { omitted: [] };
418
1489
  const named = back.map((fk) => fk.columns.join("+")).join(", ");
419
1490
  return {
420
1491
  omitted: [],
421
- note: `${child.tsName} has ${back.length} foreign keys to ${parent.name} (${named}), so which one this relation uses is not stated. None were omitted: supply them yourself.`
1492
+ note: `${child.tsName} has ${back.length} foreign keys to ${parentName} (${named}), so which one this relation uses is not stated. None were omitted: supply them yourself.`
422
1493
  };
423
1494
  }
424
1495
  function buildNestedPlan(root, tables, relations, mode, depth) {
@@ -427,14 +1498,15 @@ function buildNestedPlan(root, tables, relations, mode, depth) {
427
1498
  }
428
1499
  function buildNode(table, omitted, tables, relations, mode, depth) {
429
1500
  if (depth <= 0) return { table, omitted, arms: [] };
430
- const byDbName = new Map(tables.map((t) => [t.name, t]));
1501
+ const byName = new Map(tables.map((t) => [qualifiedTableName(t), t]));
431
1502
  const allowed = KINDS_BY_MODE[mode];
432
1503
  const columnNames = new Set(table.columns.map((c) => c.name));
433
1504
  const taken = /* @__PURE__ */ new Set();
434
1505
  const arms = [];
435
- const candidates = relations.filter((r) => r.from === table.name && allowed.has(r.kind)).sort((a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind]);
1506
+ const self = qualifiedTableName(table);
1507
+ const candidates = relations.filter((r) => r.from === self && allowed.has(r.kind)).sort((a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind]);
436
1508
  for (const rel of candidates) {
437
- const child = byDbName.get(rel.to);
1509
+ const child = byName.get(rel.to);
438
1510
  if (!child) continue;
439
1511
  const key = child.tsName;
440
1512
  if (columnNames.has(key)) continue;
@@ -470,7 +1542,13 @@ function nestedNodeColumns(columnsForMode, node) {
470
1542
 
471
1543
  // src/duplicates.ts
472
1544
  function usableKeys(table) {
473
- return (table.unique ?? []).filter((k) => k.columns.length > 0);
1545
+ const keys = [];
1546
+ const pk = table.primaryKey;
1547
+ if (pk && pk.columns.length > 0) {
1548
+ keys.push({ name: pk.name ?? `${table.name}_pkey`, columns: pk.columns });
1549
+ }
1550
+ keys.push(...(table.unique ?? []).filter((k) => k.columns.length > 0));
1551
+ return keys;
474
1552
  }
475
1553
  function renderDuplicateFinder(table, fnName, rowType) {
476
1554
  const keys = usableKeys(table);
@@ -480,14 +1558,15 @@ function renderDuplicateFinder(table, fnName, rowType) {
480
1558
  return ` { name: ${JSON.stringify(name)}, columns: ${JSON.stringify(k.columns)} }${i === keys.length - 1 ? "" : ","}`;
481
1559
  }).join("\n");
482
1560
  return `/**
483
- * Rows in \`rows\` that collide with an earlier row on a unique constraint.
1561
+ * Rows in \`rows\` that collide with an earlier row on the primary key or a unique constraint.
484
1562
  *
485
1563
  * Uniqueness is a fact about the table rather than about a row, so no schema can check it. This
486
1564
  * checks the half that needs no database: whether the batch collides with itself. A batch that
487
1565
  * passes here can still collide with rows already stored.
488
1566
  *
489
1567
  * A constraint is skipped for any row where one of its columns is null or absent, matching SQL,
490
- * where NULL is not equal to NULL and a unique index therefore permits repeats.
1568
+ * where NULL is not equal to NULL and a unique index therefore permits repeats. Rows that leave
1569
+ * a generated primary key to the database therefore report nothing on it.
491
1570
  */
492
1571
  export function ${fnName}(
493
1572
  rows: readonly ${rowType}[]
@@ -524,7 +1603,48 @@ var COLUMN_FORMATS = {
524
1603
  // Sign, decimals, exponents, NaN/Infinity, surrounding whitespace, and the underscore digit
525
1604
  // separators and 0x/0o/0b integer literals Postgres 16 added. Agrees with Postgres on all 43
526
1605
  // probes, `1_000` and `0xDEAD_beef` through to `1__0`, `_1`, `0x` and `1e+`.
527
- 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*$"
1606
+ 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*$",
1607
+ // What Postgres itself parses into a `bigint`, for a `bigint({ mode: 'string' })` column whose
1608
+ // value goes to the server as text. `int8in` is a pure integer parser: an optional sign against
1609
+ // the digits, decimal or a `0x`/`0o`/`0b` literal, single `_` separators between digits and one
1610
+ // permitted directly after the base prefix, leading zeros, and surrounding whitespace. Measured
1611
+ // against a real Postgres through PGlite over 16160 probes, boundary sweeps and random shapes:
1612
+ // **zero** values the server takes and this refuses.
1613
+ //
1614
+ // Two things it deliberately does not say.
1615
+ //
1616
+ // **The magnitude.** Every one of the 4474 probes this admits and the server refuses is a value
1617
+ // outside the signed 64 bit range, and nothing else: the syntax half is complete. The exact
1618
+ // bound is expressible, since leading zeros and separators make it a per-digit ladder rather
1619
+ // than a digit count, and it was built and verified at 16160/16160 against the server. It is
1620
+ // not shipped, because at 1237 characters and around twenty alternation branches it exhausts
1621
+ // ArkType's type-level instantiation budget: that generator states a format as a regex literal
1622
+ // inside the type expression, and the emitted module then fails to compile with TS2589.
1623
+ // Measured on arktype 2.2.3, this 101-character pattern compiles and the ladder does not, as
1624
+ // `COLUMN_FORMATS.numeric` at 176 characters already does not. Emitting a module that does not
1625
+ // typecheck is a worse failure than the bound it would buy, and the bound is unreachable by any
1626
+ // value the probe pools carry. The ArkType defect is already reported and carved out of the
1627
+ // parity gate's typecheck stage; when it is fixed, the ladder is what goes here.
1628
+ //
1629
+ // **Whitespace exactly.** Postgres pads with C `isspace`, which is the six ASCII characters, and
1630
+ // JS `\s` also admits NBSP and the Unicode spaces. That admits a handful of strings the server
1631
+ // refuses, which is the safe direction, and it is what `numeric` above already does.
1632
+ pgBigint: "^\\s*[+-]?(\\d(_?\\d)*|0[xX]_?[\\da-fA-F](_?[\\da-fA-F])*|0[oO]_?[0-7](_?[0-7])*|0[bB]_?[01](_?[01])*)\\s*$",
1633
+ // The same column on MySQL, which parses it as a *decimal number* and then rounds. Measured
1634
+ // against MySQL 8.4.11: `'12.5'` stores 13, `'1.5'` stores 2, `'.5'` stores 1, `'1e3'` stores
1635
+ // 1000, and `'92233720368547758070e-1'` stores the int64 maximum. It refuses the two spellings
1636
+ // Postgres takes, `'0x1f'` and `'1_000'`, both as "Data truncated". So no single pattern serves
1637
+ // both servers: their union admits `'12.5'` on Postgres, which is one of the fourteen values
1638
+ // that made this a defect, and their intersection turns away values each server really stores.
1639
+ //
1640
+ // Shared by the signed and the unsigned spelling, and this is why neither magnitude nor sign is
1641
+ // stated here: the value the range applies to is the *rounded* one, so the text does not
1642
+ // determine whether it fits. `'9223372036854775807.4'` is stored and `'9223372036854775807.6'`
1643
+ // is refused; on a `bigint unsigned`, `'-0.4'` and `'-1e-1'` are both stored as 0 while `'-0.5'`
1644
+ // is refused. A pattern cannot do that arithmetic, and guessing at it turns away working rows.
1645
+ // Over 3319 probes against each of a signed and an unsigned column: zero values the server takes
1646
+ // and this refuses.
1647
+ mysqlBigint: "^\\s*[+-]?(\\d+(\\.\\d*)?|\\.\\d+)([eE][+-]?\\d*)?\\s*$"
528
1648
  };
529
1649
  var COERCIBLE_DATE_STRING = "^(?!\\s*[+-])(?!\\s*\\d*\\.?\\d*(?:[eE][+-]?\\d+)?\\s*$)";
530
1650
  function parsesToADate(expr) {
@@ -539,15 +1659,39 @@ function nonFiniteAccepted(c) {
539
1659
  if (c.tsType !== "number" || c.shape) return { nan: false, infinity: false };
540
1660
  return { nan: c.allowsNaN === true, infinity: c.allowsInfinity === true };
541
1661
  }
1662
+ function nonFiniteRefused(c) {
1663
+ if (c.tsType !== "number" || c.shape) return { nan: false, infinity: false };
1664
+ return { nan: c.allowsNaN === false, infinity: c.allowsInfinity === false };
1665
+ }
1666
+ function notNullByCheck(table) {
1667
+ const out = /* @__PURE__ */ new Set();
1668
+ for (const k of table.checks ?? []) {
1669
+ const parsed = parseCheck(k.expression, k.name);
1670
+ if (!parsed.ok) continue;
1671
+ for (const n of parsed.nulls ?? []) if (n.notNull) out.add(n.column);
1672
+ }
1673
+ return out;
1674
+ }
1675
+ function withCheckNullability(table, cols) {
1676
+ const notNull = notNullByCheck(table);
1677
+ if (!notNull.size) return cols;
1678
+ return cols.map((c) => c.nullable && notNull.has(c.name) ? { ...c, nullable: false } : c);
1679
+ }
542
1680
  function insertColumns(table) {
543
- return table.columns.filter((c) => !isGeneratedColumn(c));
1681
+ return withCheckNullability(
1682
+ table,
1683
+ table.columns.filter((c) => !isGeneratedColumn(c))
1684
+ );
544
1685
  }
545
1686
  function updateColumns(table) {
546
1687
  const pkCols = table.primaryKey?.columns ?? [];
547
- return table.columns.filter((c) => !isGeneratedColumn(c) && !pkCols.includes(c.name));
1688
+ return withCheckNullability(
1689
+ table,
1690
+ table.columns.filter((c) => !isGeneratedColumn(c) && !pkCols.includes(c.name))
1691
+ );
548
1692
  }
549
1693
  function selectColumns(table) {
550
- return table.columns;
1694
+ return withCheckNullability(table, table.columns);
551
1695
  }
552
1696
  var reportedEngines = /* @__PURE__ */ new Set();
553
1697
  var ENGINE_PACKAGE = { prettier: "prettier", biome: "@biomejs/biome" };
@@ -573,11 +1717,30 @@ function nearestExistingDir(from) {
573
1717
  dir = parent;
574
1718
  }
575
1719
  }
1720
+ function isProjectInstallPath(manifestPath) {
1721
+ return manifestPath.split(path.sep).includes("node_modules");
1722
+ }
1723
+ function biomeManifest(startDir) {
1724
+ const anchors = [startDir, process.cwd()];
1725
+ let lastError;
1726
+ for (const anchor of anchors) {
1727
+ let resolved;
1728
+ try {
1729
+ const require_ = createRequire(pathToFileURL(path.join(anchor, "noop.js")));
1730
+ resolved = require_.resolve("@biomejs/biome/package.json");
1731
+ } catch (err) {
1732
+ lastError = err;
1733
+ continue;
1734
+ }
1735
+ if (isProjectInstallPath(resolved)) return resolved;
1736
+ lastError = new Error(
1737
+ `@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.`
1738
+ );
1739
+ }
1740
+ throw lastError ?? new Error("@biomejs/biome could not be resolved");
1741
+ }
576
1742
  function biomeBinary(startDir) {
577
- const require_ = createRequire(pathToFileURL(path.join(startDir, "noop.js")));
578
- const manifestPath = require_.resolve("@biomejs/biome/package.json", {
579
- paths: [startDir, process.cwd()]
580
- });
1743
+ const manifestPath = biomeManifest(startDir);
581
1744
  const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
582
1745
  const relative = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.biome;
583
1746
  if (typeof relative !== "string") {
@@ -645,10 +1808,13 @@ async function formatCode(code, filePath, fmt) {
645
1808
  return code;
646
1809
  }
647
1810
  export {
1811
+ AFFIX_PREFIX_PATTERN,
648
1812
  AFFIX_PROBE_TABLE,
1813
+ AFFIX_SUFFIX_PATTERN,
649
1814
  CODEPOINT_LENGTH,
650
1815
  COERCIBLE_DATE_STRING,
651
1816
  COLUMN_FORMATS,
1817
+ CONSTRAINTS_MODULE,
652
1818
  DEFAULT_IMPORT_EXTENSION,
653
1819
  DEFAULT_MODE_PREFIX,
654
1820
  DEFAULT_NESTED_DEPTH,
@@ -658,31 +1824,54 @@ export {
658
1824
  MAX_NESTED_DEPTH,
659
1825
  NAME_MODES,
660
1826
  NESTED_PREFIX,
1827
+ NUMERIC_CANON_NAME,
1828
+ NUMERIC_CANON_SOURCE,
661
1829
  applyTableCase,
1830
+ applyWirePolicy,
1831
+ buildBrandPlan,
662
1832
  buildNestedPlan,
1833
+ canonicalMembers,
1834
+ canonicalNumericText,
1835
+ classifyTableChecks,
1836
+ columnMetaFacts,
1837
+ comparisonWire,
663
1838
  describeSet,
1839
+ fileWriter,
664
1840
  formatCode,
665
1841
  importSpecifier,
666
1842
  insertColumns,
667
1843
  isGeneratedColumn,
668
1844
  isIntegerColumn,
1845
+ isProjectInstallPath,
1846
+ lengthCheckLabel,
1847
+ lengthMeasure,
1848
+ measureExpression,
669
1849
  moduleFileName,
670
1850
  moduleSpecifier,
1851
+ needsNumericCanon,
671
1852
  nestedArmNotes,
672
1853
  nestedNodeColumns,
673
1854
  nestedSchemaName,
674
1855
  nestedTypeName,
675
1856
  nonFiniteAccepted,
1857
+ nonFiniteRefused,
676
1858
  parseCheck,
677
1859
  parsesToADate,
678
1860
  pascalCase,
1861
+ renderConstraintsModule,
679
1862
  renderDuplicateFinder,
680
1863
  resolveAffix,
1864
+ resolveBranding,
681
1865
  resolveConfiguredImport,
1866
+ resolveConstraints,
682
1867
  resolveNestedDepth,
683
1868
  schemaName,
684
1869
  selectColumns,
1870
+ tableConstraints,
1871
+ tableMetaFacts,
685
1872
  typeName,
686
1873
  updateColumns,
687
- validateAffix
1874
+ validateAffix,
1875
+ wireLiteralFit,
1876
+ wireNumberLiteral
688
1877
  };