@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.d.cts CHANGED
@@ -95,6 +95,79 @@ declare function resolveBranding(opt: BrandingOption | undefined): ResolvedBrand
95
95
  */
96
96
  declare function buildBrandPlan(tables: Table$1[], opt: BrandingOption | undefined): BrandPlan | undefined;
97
97
 
98
+ /**
99
+ * Where a generator's files go, so that "write it" and "tell me what you would write" are the same
100
+ * code path (plan items 68, 80, 81).
101
+ *
102
+ * Three requests turn out to be one mechanism. `--dry-run` needs to know what would be written
103
+ * without writing it, `generate` needs to say which files it created and which it changed rather
104
+ * than only how many it wrote, and `--check` needs to show a diff of the difference. All three are
105
+ * the same question asked per file: what content is about to land here, and what is here now. The
106
+ * only thing a generator has to give up for that is deciding, itself, that the answer goes to disk.
107
+ *
108
+ * ## Why an option and not an interception
109
+ *
110
+ * The tempting version is to leave every generator alone and patch `node:fs/promises` for the
111
+ * duration of the run, since each one reaches it through `await import('node:fs/promises')`. That
112
+ * was measured and rejected. Patching the CommonJS exports object is visible through a later
113
+ * dynamic import, but a module namespace that already exists is a snapshot and never changes:
114
+ *
115
+ * const ns = await import('node:fs/promises'); // namespace built here
116
+ * require('node:fs/promises').writeFile = spy; // patch applied after
117
+ * ns.writeFile === spy // false, measured on Node 22.22
118
+ *
119
+ * The CLI links `chokidar`, which imports `node:fs/promises` at module scope, so by the time any
120
+ * command body runs the namespace usually exists and the patch is invisible. A dry run built on
121
+ * that would write real files whenever an unrelated dependency happened to import first, which is
122
+ * the worst possible failure for this feature: silent, environmental, and destructive. An explicit
123
+ * option cannot fail that way, because a generator that never received a sink is a generator whose
124
+ * call site can be read.
125
+ *
126
+ * ## Shape
127
+ *
128
+ * `fileWriter` returns an object with the two methods every generator already calls on the
129
+ * `node:fs/promises` namespace, with the same signatures, so adopting it is one line per generator
130
+ * and no write site changes at all:
131
+ *
132
+ * const fs = await import('node:fs/promises'); -> const fs = fileWriter(opts.fileSink);
133
+ *
134
+ * That matters more than it looks. Fourteen generators write files, between one and six times
135
+ * each, and a change that touched every one of those sites would be fourteen chances to miss one
136
+ * and ship a dry run that writes.
137
+ */
138
+ /**
139
+ * Somewhere for a generator's output to go that is not the filesystem.
140
+ *
141
+ * `mkdir` is part of it because a dry run that creates directories is not a dry run: run in an
142
+ * empty project, the honest answer leaves the directory empty, and `fs.mkdir(out, { recursive:
143
+ * true })` is the first thing every generator does.
144
+ */
145
+ interface FileSink {
146
+ mkdir(dir: string): void | Promise<void>;
147
+ writeFile(file: string, contents: string): void | Promise<void>;
148
+ }
149
+ /**
150
+ * The `node:fs/promises` subset generators use, narrowed to the two calls they actually make.
151
+ *
152
+ * Declared structurally rather than imported from `node:fs` so that the real namespace satisfies
153
+ * it without a cast and a sink can too. The `options` and `encoding` parameters are accepted and
154
+ * ignored by a sink: every call site in this repository passes `{ recursive: true }` and `'utf8'`,
155
+ * and a sink that took different arguments would make the swap a rewrite rather than a rename.
156
+ */
157
+ interface GeneratorFs {
158
+ mkdir(dir: string, options?: {
159
+ recursive?: boolean;
160
+ }): Promise<unknown>;
161
+ writeFile(file: string, contents: string, encoding?: BufferEncoding): Promise<void>;
162
+ }
163
+ /**
164
+ * The filesystem, or whatever was passed instead of it.
165
+ *
166
+ * Without a sink this is `node:fs/promises` itself, so a run with no sink performs the same calls
167
+ * in the same order as before this existed.
168
+ */
169
+ declare function fileWriter(sink?: FileSink): GeneratorFs;
170
+
98
171
  /**
99
172
  * One place that decides what a generated module is called, on disk and in an import.
100
173
  *
@@ -253,6 +326,22 @@ declare function resolveAffix(opts?: {
253
326
  declare function schemaName(mode: NameMode, tsName: string, affix: ResolvedAffix): string;
254
327
  /** Name of the exported type alias, e.g. `InsertusersInput`. */
255
328
  declare function typeName(mode: NameMode, tsName: string, affix: ResolvedAffix): string;
329
+ /**
330
+ * The same two rules, as JSON Schema `pattern` strings, for `drzl.config.schema.json`.
331
+ *
332
+ * `z.toJSONSchema` drops refinements without saying so, and the affix rules live in
333
+ * `ConfigSchema`'s only `.superRefine`. A generated schema would therefore have told an editor
334
+ * that `suffix: 'my-schema'` was fine, and the CLI would then have refused to generate from it.
335
+ * Re-encoding the character half here keeps one definition of the rule rather than a regex in
336
+ * this file and a hand-copied pattern in the schema builder.
337
+ *
338
+ * The body is optional because `checkOne` returns early for the empty string: an empty affix
339
+ * means "none" and is always legal. Equivalence with `validateAffix` is not argued from that
340
+ * sentence, it is fuzzed over every printable ASCII position in
341
+ * packages/cli/test/config-json-schema.spec.ts.
342
+ */
343
+ declare const AFFIX_PREFIX_PATTERN = "^(?:[A-Za-z_$][A-Za-z0-9_$]*)?$";
344
+ declare const AFFIX_SUFFIX_PATTERN = "^(?:[A-Za-z0-9_$]+)?$";
256
345
  /**
257
346
  * Reject affixes that cannot produce a compilable file, before anything is written:
258
347
  * - characters that are not legal in a TypeScript identifier
@@ -288,6 +377,11 @@ declare function validateAffix(affix?: AffixOptions, schemaSuffix?: string): Aff
288
377
  * base expression rather than on the whole field.
289
378
  * 2. **A multi-column check cannot live on a field.** `start_date < end_date` is a statement
290
379
  * about the row, so it is not returned here at all.
380
+ * 3. **A conjunction splits and a disjunction does not.** Every part of an `AND` has to hold on
381
+ * its own, so a list of checks says exactly what it says. An `OR` is *weaker* than either
382
+ * branch: a schema enforcing one branch turns away every row that satisfied the other. So a
383
+ * disjunction is read only where the whole of it collapses to one statement, and refused
384
+ * whole otherwise. See `parseDisjunction`.
291
385
  */
292
386
  /** A comparison of one column against one literal, which is the case worth translating. */
293
387
  interface ColumnCheck {
@@ -326,7 +420,8 @@ interface RowCheck {
326
420
  name?: string;
327
421
  }
328
422
  /**
329
- * A constraint on a column's *character* count, from `CHECK (length(name) > 3)`.
423
+ * A constraint on a count derived from a column's value, from `CHECK (length(name) > 3)` and
424
+ * `CHECK (octet_length(blob) <= 5)`.
330
425
  *
331
426
  * Kept apart from `ColumnCheck` because it is not a comparison of the value: it compares a count
332
427
  * derived from it, and the count Postgres takes is code points rather than UTF-16 units. See
@@ -337,8 +432,88 @@ interface LengthCheck {
337
432
  operator: ColumnCheck['operator'];
338
433
  /** Decimal, as text, matching how the other bounds are carried. */
339
434
  value: string;
435
+ /**
436
+ * Which count the expression asked for.
437
+ *
438
+ * Absent on a pre-1.x parse, where every count was a character count, so a consumer reading this
439
+ * treats `undefined` as `'characters'`. `lengthMeasure` is the one place that decision is made.
440
+ *
441
+ * This is *not* the whole answer, because the same function means different things on different
442
+ * column types. Measured on PostgreSQL 17.5 through PGlite, on a `text` holding three emoji and
443
+ * a `bytea` holding six bytes:
444
+ *
445
+ * | expression | text | bytea |
446
+ * | ----------------- | ---- | -------------- |
447
+ * | `octet_length(x)` | 12 | 6 |
448
+ * | `length(x)` | 3 | 6 |
449
+ * | `char_length(x)` | 3 | does not exist |
450
+ *
451
+ * So the unit says what the *expression* asked for and `lengthMeasure` says how to answer it for
452
+ * a given column.
453
+ */
454
+ unit?: 'characters' | 'bytes';
340
455
  name?: string;
341
456
  }
457
+ /**
458
+ * How a `LengthCheck` is answered in JavaScript, once the column it names is known.
459
+ *
460
+ * Three answers, and no two of them agree on the same value:
461
+ *
462
+ * `codePoints` `[...v].length` a character count
463
+ * `utf8Bytes` `new TextEncoder().encode(v).length` a byte count of a string
464
+ * `byteLength` `v.length`, on a Uint8Array a byte count of a binary payload
465
+ *
466
+ * `v.length` on a *string* is none of them: it counts UTF-16 units, which is 6 for the three emoji
467
+ * whose character count is 3 and whose byte count is 12.
468
+ */
469
+ type LengthMeasure = 'codePoints' | 'utf8Bytes' | 'byteLength';
470
+ /**
471
+ * How to answer a count on this column, or nothing where no expression answers it.
472
+ *
473
+ * The one place the column type and the expression meet, held here rather than in each generator so
474
+ * the emitted predicate and the constraint ledger cannot disagree about what is enforced.
475
+ *
476
+ * Three refusals, each because the count would be a different measurement than the database took:
477
+ *
478
+ * - **An array.** Postgres has no `length(anyarray)`; `cardinality` is the array's own count and is
479
+ * carried separately.
480
+ * - **A byte-string column**, MySQL's `binary(n)`/`varbinary(n)`. It arrives as a string produced by
481
+ * a lossy decode, so neither its code points nor their UTF-8 re-encoding is the server's byte
482
+ * count: `<ff ff ff>` from a `varbinary(3)` comes back as 3 code points that re-encode to 9
483
+ * bytes. See `ColumnShape`.
484
+ * - **Anything that is not a string or a binary payload.** `length` is defined for text, bytea and
485
+ * bit strings, so a count on a number column cannot come from a schema the database accepted, and
486
+ * spreading a number with `[...v]` would throw rather than fail.
487
+ *
488
+ * A `bytea` answers both functions with the same number, which is why the unit is not consulted
489
+ * there. `char_length(bytea)` is the one spelling that would break that, and Postgres does not have
490
+ * it: `function char_length(bytea) does not exist`, measured.
491
+ */
492
+ declare function lengthMeasure(column: {
493
+ tsType?: string;
494
+ arrayDimensions?: number;
495
+ shape?: {
496
+ kind: string;
497
+ };
498
+ }, check: LengthCheck): LengthMeasure | undefined;
499
+ /**
500
+ * The JavaScript expression that takes one of the three measurements, over a named variable.
501
+ *
502
+ * A function of the variable name rather than a constant, because the five validation generators
503
+ * put the count in three different places: zod and valibot pipe the value itself, while ArkType and
504
+ * TypeBox reach it off the row object as `o["blob"]`. `CODEPOINT_LENGTH` is the fixed-variable
505
+ * spelling of the first arm and stays for the call sites that already use it.
506
+ */
507
+ declare function measureExpression(measure: LengthMeasure, variable: string): string;
508
+ /**
509
+ * A count constraint as the sentence every emitted schema attaches and the ledger keys on.
510
+ *
511
+ * Held here rather than spelled out in each generator, because the constraint error map matches an
512
+ * issue's message against this string exactly: the two drifting by one character is a map that
513
+ * silently answers nothing. `char_length` is normalised to `length`, as it always has been, since
514
+ * the two are the same function in Postgres.
515
+ */
516
+ declare function lengthCheckLabel(check: LengthCheck): string;
342
517
  /**
343
518
  * A constraint on an array column's element count, from `CHECK (cardinality(tags) > 0)`.
344
519
  *
@@ -351,6 +526,24 @@ interface CardinalityCheck {
351
526
  value: string;
352
527
  name?: string;
353
528
  }
529
+ /**
530
+ * A test of whether a column holds NULL, from `CHECK (col IS NOT NULL)`.
531
+ *
532
+ * Not a comparison: SQL's comparison operators all yield NULL against a NULL operand, and these
533
+ * two are the operators that answer TRUE or FALSE instead. That is why they are a kind of their
534
+ * own rather than another `operator` on `ColumnCheck`, whose whole placement rule is that a check
535
+ * sits *inside* the nullable wrapper because NULL never reaches it.
536
+ *
537
+ * `notNull` is the only direction any emitted schema states, and it states it by not being
538
+ * nullable rather than by carrying a predicate. `IS NULL` is carried so the constraint can be
539
+ * reported precisely rather than as "not understood", and is enforced nowhere.
540
+ */
541
+ interface NullCheck {
542
+ column: string;
543
+ /** `true` for `IS NOT NULL`, `false` for `IS NULL`. */
544
+ notNull: boolean;
545
+ name?: string;
546
+ }
354
547
  /** A check that was understood, or the reason it was not. */
355
548
  type ParsedCheck = {
356
549
  ok: true;
@@ -359,6 +552,7 @@ type ParsedCheck = {
359
552
  rows?: RowCheck[];
360
553
  lengths?: LengthCheck[];
361
554
  cardinalities?: CardinalityCheck[];
555
+ nulls?: NullCheck[];
362
556
  } | {
363
557
  ok: false;
364
558
  reason: string;
@@ -369,8 +563,36 @@ type ParsedCheck = {
369
563
  * Deliberately narrow. `BETWEEN` is included because it is common and means exactly two
370
564
  * inclusive bounds; `AND` of arbitrary predicates is not, because getting its scope wrong would
371
565
  * silently change what is enforced.
566
+ *
567
+ * **The order below is load bearing, in both directions.** `BETWEEN` is matched before the `AND`
568
+ * split because it *holds* an `AND`; splitting first turned every `BETWEEN` into an unparseable
569
+ * pair and dropped a constraint that had been enforced. The unary `IS` predicates are matched
570
+ * *after* the splits for the mirror-image reason: none of them holds an `AND` or an `OR`, and
571
+ * their trailing operand is greedy, so matching first would let `a IS DISTINCT FROM 5 AND b > 0`
572
+ * swallow the second predicate. `OR` is split before `AND` because SQL binds `AND` tighter, and
573
+ * no SQL operator spells an `OR` inside itself the way `BETWEEN` spells an `AND`.
372
574
  */
373
575
  declare function parseCheck(expression: string | undefined, name?: string): ParsedCheck;
576
+ /**
577
+ * A number-kind literal, spelled for the wire type of the column it constrains.
578
+ *
579
+ * `CHECK (big IN (1, 2))` on a `bigint({ mode: 'bigint' })` column pins a set of *bigints*: the
580
+ * driver returns `1n` there, and `1n === 1` is false in JavaScript, so a number literal in the
581
+ * emitted schema rejects every row the database returns. The literal has to be spelled `1n` on
582
+ * that wire, and stay `1` where a number really arrives, which is what `bigint({ mode: 'number' })`
583
+ * does. The wire type is `tsType`, which the analyzer sets per mode from what the driver hands
584
+ * back (see the `PgBigInt53`/`PgBigInt64` arms and `test/decimal-modes.spec.ts` in the analyzer),
585
+ * so the decision keys on the value's real type rather than on the SQL type name.
586
+ *
587
+ * The integer test is load bearing rather than defensive: `1.5n` is a syntax error, so an emitted
588
+ * module carrying it throws at import. A non-integer literal also has nothing to gain from the
589
+ * suffix, because no stored bigint ever equals 1.5; it keeps its number spelling, which no bigint
590
+ * satisfies either, so the schema and the database agree about every value on the wire while the
591
+ * module keeps parsing.
592
+ */
593
+ declare function wireNumberLiteral(column: {
594
+ tsType?: string;
595
+ }, value: string): string;
374
596
  /**
375
597
  * A human-readable rendering of a set constraint, for an error message.
376
598
  *
@@ -378,6 +600,343 @@ declare function parseCheck(expression: string | undefined, name?: string): Pars
378
600
  * schema rather than like its JavaScript translation.
379
601
  */
380
602
  declare function describeSet(set: ColumnSet): string;
603
+ /**
604
+ * How the database compares a CHECK literal against this column, and so how an emitted schema
605
+ * must state the comparison. `wireNumberLiteral` above decides a literal's *spelling*; this
606
+ * decides which comparison that spelling takes part in, which is the other half of the same
607
+ * rule: the literal's kind and the column's wire are reconciled by the database's semantics,
608
+ * never by whether the schema happened to quote the literal.
609
+ *
610
+ * `number` the driver hands back a JS number; literals compare with `===`/ranges.
611
+ * `bigint` a JS bigint; same, spelled with the `n` suffix.
612
+ * `numeric-string` the driver hands back *decimal text* and the database compares it as a
613
+ * number: `numeric`/`decimal` in string mode on every dialect, and v1's
614
+ * `bigint({ mode: 'string' })`. Measured on PostgreSQL 17.5 and MySQL
615
+ * 8.4.11: `1 = 1.00` is true and a `numeric(10,2)` returns '1.00' for a
616
+ * stored 1, so neither a number literal nor the literal's own text can be
617
+ * compared to what arrives; only a canonical decimal spelling can.
618
+ * `text` ordinary strings, compared as strings.
619
+ * `opaque` everything else: dates, booleans, arrays, shaped columns. No literal
620
+ * policy applies and the existing arms keep their behaviour.
621
+ *
622
+ * Keyed on `tsType` plus `dbType` rather than on the SQL type name, for the reason
623
+ * `wireNumberLiteral` records: `tsType` is the analyzer's measured wire, and `dbType` is the
624
+ * coarse kind label both majors agree on (`NUMERIC` for every decimal family, `BIGINT` for the
625
+ * one string-mode integer wire).
626
+ */
627
+ type ComparisonWire = 'number' | 'bigint' | 'numeric-string' | 'text' | 'opaque';
628
+ declare function comparisonWire(c: {
629
+ tsType?: string;
630
+ dbType?: string;
631
+ shape?: {
632
+ kind: string;
633
+ };
634
+ arrayDimensions?: number;
635
+ }): ComparisonWire;
636
+ /**
637
+ * The canonical spelling of plain decimal text, or nothing for anything else.
638
+ *
639
+ * The one name under which '1', '1.00', '01', '+1', '1.' and ' 1 ' are the same value, which is
640
+ * exactly the equality the database applies on a numeric wire: measured through PGlite, a bare
641
+ * `numeric` returns '1.000000' for an inserted 1.000000 and admits it into `CHECK (n IN (1, 2))`,
642
+ * and a `numeric(10,2)` returns '1.00' for a stored 1. Sign is normalised (`numeric` has no
643
+ * negative zero: '-0.00' comes back '0.00'), leading integer zeros and trailing fraction zeros
644
+ * are stripped, and a bare trailing dot is dropped.
645
+ *
646
+ * String arithmetic deliberately: `Number()` is not usable here. A numeric column carries more
647
+ * digits than a double holds, and Number('99999999999999999999') and
648
+ * Number('99999999999999999998') are the same double, so rounding through one would merge
649
+ * members the database keeps distinct.
650
+ *
651
+ * Everything outside plain decimal answers nothing, exponent forms included. Postgres does
652
+ * accept `'1e3'` as numeric *input*, and `CHECK (n IN ('1e3', 2))` is valid DDL that admits
653
+ * 1000, but its output spelling is always plain ('1000' came back, measured), and a member this
654
+ * function cannot name makes the whole constraint unenforceable rather than approximated: see
655
+ * `wireLiteralFit`.
656
+ */
657
+ declare function canonicalNumericText(text: string): string | undefined;
658
+ /** The canonical forms of a member list, deduplicated, members outside the domain dropped. */
659
+ declare function canonicalMembers(values: string[]): string[];
660
+ /** Name of the canonical helper emitted into a module that compares on a numeric string wire. */
661
+ declare const NUMERIC_CANON_NAME = "DrzlNumericCanon";
662
+ /**
663
+ * The helper itself, emitted once per file that needs it, in every generator from this one
664
+ * string so six copies cannot drift. `test/numeric-wire-literals.spec.ts` evaluates it beside
665
+ * `canonicalNumericText` over the probe corpus to hold the two to one answer.
666
+ */
667
+ declare const NUMERIC_CANON_SOURCE = "/**\n * The canonical spelling of the plain decimal text a numeric wire carries, or null for anything\n * else. The driver spells one value many ways by declared scale ('1', '1.00', '1.0000000000',\n * measured) and the database compares them as numbers, so equality is decided on this form:\n * sign normalised, leading integer zeros and trailing fraction zeros stripped, a bare trailing\n * dot dropped. String arithmetic on purpose: Number() is not usable here, because a numeric\n * column carries more digits than a double holds and rounding would merge values the database\n * keeps distinct.\n */\nconst DrzlNumericCanon = (s: string): string | null => {\n const m = /^([+-]?)(\\d*)(?:\\.(\\d*))?$/.exec(s.trim());\n if (!m || (!m[2] && !m[3])) return null;\n const int = (m[2] ?? '').replace(/^0+/, '');\n const frac = (m[3] ?? '').replace(/0+$/, '');\n if (!int && !frac) return '0';\n return (m[1] === '-' ? '-' : '') + (int || '0') + (frac ? '.' + frac : '');\n};\n";
668
+ /** What `wireLiteralFit` is asked about: one literal list, one comparison class. */
669
+ interface WireLiteralQuestion {
670
+ kind: 'number' | 'string';
671
+ values: string[];
672
+ /** `=`, `<>` and `IN` are `equality`; the four ordering operators are `range`. */
673
+ comparison: 'equality' | 'range';
674
+ }
675
+ /**
676
+ * Whether these literals can be enforced against this column, and in what form.
677
+ *
678
+ * `keep` the spelling already follows the wire: nothing changes.
679
+ * `respell` quoted plain decimal on a number or bigint wire. The database coerces the
680
+ * text and compares numerically (`bigint CHECK (big IN ('1','2'))` admitted 1
681
+ * and refused 3, measured), so the literal becomes its canonical number-kind
682
+ * self and every existing number arm applies. Canonical rather than verbatim,
683
+ * because `018` and `018n` are syntax errors in an emitted module.
684
+ * `canonical` any equality on a numeric string wire: the emitted schema compares canonical
685
+ * decimal spellings through `NUMERIC_CANON_NAME`. A range there is `respell`:
686
+ * it is emitted as a coerced numeric compare instead, since ordering needs no
687
+ * exact spelling, only a monotonic one.
688
+ * `unenforced` no exact statement exists. Three shapes land here, each measured: a number
689
+ * literal against a text column (Postgres refuses the DDL outright, MySQL
690
+ * compares through double coercion that admits '1.00' and '2.0'), quoted text
691
+ * that is not plain decimal on a number or bigint wire, and a member outside
692
+ * the canonical domain on a numeric string wire (`'1e3'` is valid DDL whose
693
+ * rows come back '1000'). Enforcing a guess would reject rows the database
694
+ * admits, which is the defect class this policy exists to remove, so these are
695
+ * left to the base schema and reported.
696
+ */
697
+ type WireLiteralFit = {
698
+ fit: 'keep';
699
+ } | {
700
+ fit: 'respell';
701
+ values: string[];
702
+ } | {
703
+ fit: 'canonical';
704
+ canon: string[];
705
+ } | {
706
+ fit: 'unenforced';
707
+ reason: string;
708
+ };
709
+ declare function wireLiteralFit(c: {
710
+ name?: string;
711
+ tsType?: string;
712
+ dbType?: string;
713
+ shape?: {
714
+ kind: string;
715
+ };
716
+ arrayDimensions?: number;
717
+ }, q: WireLiteralQuestion): WireLiteralFit;
718
+ /** A clause the wire policy left unenforced, with the reason the ledger reports. */
719
+ interface UnenforcedLiteral {
720
+ column: string;
721
+ reason: string;
722
+ check?: ColumnCheck;
723
+ set?: ColumnSet;
724
+ }
725
+ /**
726
+ * The wire policy over a whole table's parsed checks, for the generators.
727
+ *
728
+ * Respelled literals come back as the number-kind twins every existing arm already handles,
729
+ * unenforceable clauses are dropped from the lists and returned beside them, and the
730
+ * numeric-string clauses pass through untouched for the canonical vehicles to state. The
731
+ * constraint ledger applies `wireLiteralFit` itself so its texts and reasons cannot drift from
732
+ * what is emitted here.
733
+ */
734
+ declare function applyWirePolicy(columns: Array<{
735
+ name: string;
736
+ tsType?: string;
737
+ dbType?: string;
738
+ shape?: {
739
+ kind: string;
740
+ };
741
+ arrayDimensions?: number;
742
+ }>, checks: ColumnCheck[], sets: ColumnSet[]): {
743
+ checks: ColumnCheck[];
744
+ sets: ColumnSet[];
745
+ unenforced: UnenforcedLiteral[];
746
+ };
747
+ /**
748
+ * Whether any column of this table needs `NUMERIC_CANON_SOURCE` emitted beside it.
749
+ *
750
+ * One condition shared by every generator's preamble and its field emitter, for the reason the
751
+ * TypeBox generator records on `tbNeedsCapKind`: two copies of one condition drift, and what a
752
+ * drifted copy emits is a reference to a helper the file never defined.
753
+ */
754
+ declare function needsNumericCanon(columns: Array<{
755
+ name: string;
756
+ tsType?: string;
757
+ dbType?: string;
758
+ shape?: {
759
+ kind: string;
760
+ };
761
+ arrayDimensions?: number;
762
+ }>, checks: ColumnCheck[], sets: ColumnSet[]): boolean;
763
+
764
+ /**
765
+ * Every constraint on a table, as data a consumer can read without holding a validator.
766
+ *
767
+ * `meta` already carries most of these facts, and this is deliberately not a second copy of it.
768
+ * Three differences, each of which is why both exist.
769
+ *
770
+ * **shape** `meta` renders a CHECK as prose: `"age_adult: age >= 18"`. That is the right
771
+ * form for its destination, which is `z.toJSONSchema` and then an OpenAPI
772
+ * viewer, and the wrong form for a form builder, which wants the bound as a
773
+ * number and would otherwise have to parse SQL back out of a sentence. Here the
774
+ * operand is data and the sentence is beside it.
775
+ *
776
+ * **content** `meta` has no foreign keys at all, and its `unique` is a list of column groups
777
+ * with the constraint names dropped. A form cannot render a picker without the
778
+ * first, and, more sharply, nothing can be mapped back to a constraint that has
779
+ * no name, which is what makes the second load bearing rather than cosmetic.
780
+ *
781
+ * **addressing** `meta` is reachable only by holding the emitted schema object and asking each
782
+ * field, per mode. This is a plain record keyed by table, so the answer to "what
783
+ * constrains this table" costs one property read and no validator import.
784
+ *
785
+ * What is *not* here is anything `meta` states about a column rather than a constraint: the SQL
786
+ * type, whether the database defaults the value, the numeric range of the column's own type. Those
787
+ * are facts about a field, `meta` carries them, and restating them here would be exactly the
788
+ * duplication the paragraph above is arguing against.
789
+ *
790
+ * Both halves are built from `classifyTableChecks` below, which `meta` also uses, so the two can
791
+ * disagree about what is enforced only by both being wrong at once.
792
+ */
793
+
794
+ /** Where one part of a parsed CHECK lands. */
795
+ type CheckPartPlace = 'column' | 'row' | 'none';
796
+ /**
797
+ * One clause of one CHECK, classified by what the generated schemas do with it.
798
+ *
799
+ * A single `CHECK` declaration can split into several of these: `BETWEEN` is two bounds, and a
800
+ * conjunction is one part per operand. They are kept apart here because enforcement is decided
801
+ * per clause, and rejoined into one constraint by `tableConstraints`, because a database has one
802
+ * constraint there and quotes one name back.
803
+ */
804
+ interface CheckPart {
805
+ /** The clause as text. Also the message the emitted schema attaches, when it attaches one. */
806
+ text: string;
807
+ /** The columns the clause is about, in the order the expression names them. */
808
+ columns: string[];
809
+ place: CheckPartPlace;
810
+ /** Why nothing enforces it, when nothing does. */
811
+ reason?: string;
812
+ /** Present when the clause was folded into the column's range instead of a predicate. */
813
+ bound?: {
814
+ column: string;
815
+ operator: ColumnCheck['operator'];
816
+ value: string;
817
+ };
818
+ /** Present when the clause was folded into a set of literals instead of a predicate. */
819
+ set?: {
820
+ column: string;
821
+ values: string[];
822
+ kind: 'number' | 'string';
823
+ };
824
+ /**
825
+ * Present when the clause is enforced by the field's shape rather than by a predicate.
826
+ *
827
+ * The third fold, after a bound and a set, and the one that leaves *nothing* to match on:
828
+ * `CHECK (col IS NOT NULL)` is enforced by the field not being nullable, and the failure it
829
+ * produces is the library's own "expected string, received null". Marked so `tableConstraints`
830
+ * does not offer the clause text as a message, which would have the error map keying on a
831
+ * string no emitted module writes.
832
+ */
833
+ shape?: 'notNull';
834
+ }
835
+ /** One declared CHECK, with each of its clauses placed. */
836
+ interface ClassifiedCheck {
837
+ name?: string;
838
+ /** The expression as declared, trimmed. */
839
+ expression: string;
840
+ parts: CheckPart[];
841
+ }
842
+ /**
843
+ * Every CHECK on a table, split into clauses and placed.
844
+ *
845
+ * The single reading of `parseCheck` plus the shape guards, shared by `meta` and by the constraint
846
+ * ledger so that "the database also checks this and we do not" is one answer rather than two.
847
+ */
848
+ declare function classifyTableChecks(table: Table$1): ClassifiedCheck[];
849
+ /** What a constraint is. */
850
+ type ConstraintKind = 'primaryKey' | 'unique' | 'foreignKey' | 'check' | 'maxLength' | 'maxBytes';
851
+ /** One clause of a constraint that nothing in the generated schemas checks. */
852
+ interface UnenforcedPart {
853
+ part: string;
854
+ reason: string;
855
+ }
856
+ /** One constraint on one table. */
857
+ interface ConstraintFacts {
858
+ /**
859
+ * Stable identifier, unique within the table.
860
+ *
861
+ * The SQL constraint name where the declaration has one, and a derived name where it does not.
862
+ * This is what an error map keys on and what a caller stores against its own copy for a
863
+ * message, so it has to exist even for the constraints SQL leaves anonymous.
864
+ */
865
+ id: string;
866
+ /** The SQL constraint name, absent where the declaration did not give one. */
867
+ name?: string;
868
+ kind: ConstraintKind;
869
+ /** The columns the constraint is about, in declaration order. */
870
+ columns: string[];
871
+ /** The rule as a sentence, for a form with nothing better to show. */
872
+ rule: string;
873
+ /** Whether a generated schema can reject a row for this constraint. */
874
+ enforced: boolean;
875
+ /** The clauses nothing enforces, and why. Absent when there are none. */
876
+ unenforced?: UnenforcedPart[];
877
+ /**
878
+ * The exact messages a generated schema attaches for this constraint.
879
+ *
880
+ * Absent where the schema states the constraint in the validator's own vocabulary instead, which
881
+ * is where the constraint name is lost and the error map has to key on something else.
882
+ */
883
+ messages?: string[];
884
+ /** Bounds folded into a column's range. What a bound-carrying issue is matched against. */
885
+ bounds?: {
886
+ column: string;
887
+ operator: string;
888
+ value: string;
889
+ }[];
890
+ /** A set of literals folded into an enum. */
891
+ values?: {
892
+ column: string;
893
+ values: string[];
894
+ kind: 'number' | 'string';
895
+ };
896
+ /** Where a foreign key points. */
897
+ references?: {
898
+ table: string;
899
+ schema?: string;
900
+ columns: string[];
901
+ onDelete?: string;
902
+ onUpdate?: string;
903
+ };
904
+ }
905
+ /** Every constraint on one table. */
906
+ interface TableConstraints {
907
+ /** The SQL table name, which is not the Drizzle export name. */
908
+ table: string;
909
+ /** The SQL schema, present only when the table names one. */
910
+ schema?: string;
911
+ constraints: ConstraintFacts[];
912
+ }
913
+ /** Every constraint on a table, as data. */
914
+ declare function tableConstraints(table: Table$1): TableConstraints;
915
+ interface ConstraintsModuleOptions {
916
+ /** Also emit `constraintForIssue`, which is the half that maps a failure back. */
917
+ errorMap?: boolean;
918
+ }
919
+ /** What `constraints` is asking for, once the boolean shorthand is expanded. */
920
+ type ConstraintsOption = boolean | {
921
+ enabled?: boolean;
922
+ errorMap?: boolean;
923
+ };
924
+ declare function resolveConstraints(opt: ConstraintsOption | undefined): ConstraintsModuleOptions | undefined;
925
+ /** The file every generator writes the ledger to. Fixed, like the barrel's own name. */
926
+ declare const CONSTRAINTS_MODULE = "constraints.ts";
927
+ /**
928
+ * The `constraints.ts` module, as source.
929
+ *
930
+ * Plain objects and, optionally, one function. Nothing here imports a validator or any part of
931
+ * DRZL, so a consumer can read the ledger from a script, a form builder or a server route without
932
+ * pulling a schema in. The matcher is a separate export for the same reason a separate option
933
+ * turns it off: a consumer who only renders forms should not carry the code that maps failures.
934
+ *
935
+ * `JSON.stringify` rather than a hand-rolled literal, because every string here comes from a
936
+ * schema the user wrote: a table named `it's` or a CHECK holding a quote has to survive into valid
937
+ * TypeScript. The formatter unquotes the keys that do not need quoting.
938
+ */
939
+ declare function renderConstraintsModule(tables: Table$1[], opts?: ConstraintsModuleOptions): string;
381
940
 
382
941
  /**
383
942
  * The facts a generated schema can carry beside itself.
@@ -647,7 +1206,7 @@ declare function nestedNodeColumns<T extends {
647
1206
  */
648
1207
 
649
1208
  /**
650
- * `findDuplicate<Table>s` for a table with unique constraints, or nothing.
1209
+ * `findDuplicate<Table>s` for a table with a primary key or unique constraints, or nothing.
651
1210
  *
652
1211
  * `rowType` is the name of the insert type, which is what a caller has in hand before an insert.
653
1212
  * Passed in rather than derived, because each generator names its types differently.
@@ -661,6 +1220,16 @@ interface Table {
661
1220
  primaryKey?: {
662
1221
  columns: string[];
663
1222
  };
1223
+ /**
1224
+ * The declared CHECK constraints, because one of them decides whether a column is nullable.
1225
+ *
1226
+ * Structurally the analyzer's `Check[]`, and here rather than only there because the three
1227
+ * column selectors below have to read it and this is the shape they are declared against.
1228
+ */
1229
+ checks?: {
1230
+ name?: string;
1231
+ expression?: string;
1232
+ }[];
664
1233
  }
665
1234
  type ValidationLibrary = 'zod' | 'valibot' | 'arktype' | 'typebox' | 'effect';
666
1235
  interface FormatOptions {
@@ -796,6 +1365,15 @@ interface ValidationGenerateOptions {
796
1365
  insert?: boolean;
797
1366
  update?: boolean;
798
1367
  };
1368
+ /**
1369
+ * Where the generated files go, when that is not the filesystem.
1370
+ *
1371
+ * Omitted, they go to disk exactly as before. Passed, every write and every `mkdir` is handed to
1372
+ * the sink instead, which is what `drzl generate --dry-run` and `drzl generate --check` are
1373
+ * built on: both need the content a run would produce without the run producing it. See
1374
+ * `emit.ts` for why this is an option rather than an interception of `node:fs/promises`.
1375
+ */
1376
+ fileSink?: FileSink;
799
1377
  }
800
1378
  interface ValidationRenderer<TOptions extends ValidationGenerateOptions = ValidationGenerateOptions> {
801
1379
  readonly library: ValidationLibrary;
@@ -841,7 +1419,7 @@ declare function isGeneratedColumn(c: Column, _primaryKeyColumns?: string[]): bo
841
1419
  * the real hazard is over-rejection. A check that turns away something Postgres accepts breaks
842
1420
  * working code, which is worse than the bare `string` it replaces.
843
1421
  *
844
- * That is why there is exactly one entry. Candidates for `date`, `timestamp`, `time`, `interval`,
1422
+ * The list is short for that reason. Candidates for `date`, `timestamp`, `time`, `interval`,
845
1423
  * `inet`, `cidr` and `macaddr` were all built and all rejected, each by a value Postgres accepts
846
1424
  * and the pattern did not:
847
1425
  *
@@ -850,6 +1428,11 @@ declare function isGeneratedColumn(c: Column, _primaryKeyColumns?: string[]): bo
850
1428
  * macaddr `2020-01-01`, which Postgres pads into `20:20:00:01:00:01`
851
1429
  * inet `10.1/16`, `::ffff:1.2.3.4`
852
1430
  * cidr parses as `inet` and then demands zero host bits, which no regex can state
1431
+ *
1432
+ * **One key per dialect where the servers disagree.** `numeric` is Postgres's alone, and the
1433
+ * analyzer withholds it from SQLite for that reason. The two `bigint` entries are the case where
1434
+ * withholding is not enough, because both dialects have a real answer and the answers contradict
1435
+ * each other in both directions. See them below.
853
1436
  */
854
1437
  declare const COLUMN_FORMATS: Record<string, string>;
855
1438
  /**
@@ -987,6 +1570,34 @@ declare function nonFiniteAccepted(c: Column): {
987
1570
  nan: boolean;
988
1571
  infinity: boolean;
989
1572
  };
1573
+ /**
1574
+ * The non-finite doubles the emitted schema must *refuse*, as the analyzer stated them.
1575
+ *
1576
+ * The other reading of the same two flags, and not the negation of `nonFiniteAccepted`. Three
1577
+ * states, because a column that was measured and refused the value is a different thing from a
1578
+ * column nobody measured: `true` is stored and returned, `false` is offered and refused, absent is
1579
+ * unstated. `nonFiniteAccepted` reads `=== true` and this reads `=== false`, so an unstated column
1580
+ * answers no to both and every generator leaves it exactly as its library renders it.
1581
+ *
1582
+ * The distinction is load-bearing rather than tidy. MySQL and SQLite both leave a `real` unbounded,
1583
+ * and MySQL answers `ER_WARN_DATA_OUT_OF_RANGE` for an infinity where SQLite stores it and hands it
1584
+ * back. Reading "not accepted" as "refuse it" would have made one schema right and the other wrong
1585
+ * from the same reading, and the SQLite half is filed separately because that engine also turns
1586
+ * `NaN` into NULL and a column needs both halves of that answer or none.
1587
+ *
1588
+ * Guarded on `tsType` exactly as its sibling is, so a stale analysis carrying the flags on a string
1589
+ * or a shaped column cannot put a numeric predicate beside a `z.string()`.
1590
+ *
1591
+ * What each generator does with a yes is its own: `z.number()` and `Type.Number()` already refuse
1592
+ * every non-finite number with no bound at all, measured, so they emit nothing and read this only
1593
+ * through the tests that pin that. `v.number()` and ArkType's `number` take both infinities, so
1594
+ * those two add a finite predicate wherever no bound already holds one back. Effect builds on
1595
+ * `Schema.Finite` unconditionally and needs nothing either.
1596
+ */
1597
+ declare function nonFiniteRefused(c: Column): {
1598
+ nan: boolean;
1599
+ infinity: boolean;
1600
+ };
990
1601
  declare function insertColumns(table: Table): Column[];
991
1602
  /**
992
1603
  * Which columns belong in an update schema.
@@ -1003,6 +1614,33 @@ declare function insertColumns(table: Table): Column[];
1003
1614
  */
1004
1615
  declare function updateColumns(table: Table): Column[];
1005
1616
  declare function selectColumns(table: Table): Column[];
1617
+ /**
1618
+ * Whether a resolved manifest path belongs to a project's installed dependencies.
1619
+ *
1620
+ * Resolving a package is normally the same question as having it installed, and on Node and Deno
1621
+ * it is: their resolvers walk `node_modules` and answer a missing package with MODULE_NOT_FOUND.
1622
+ * Bun's does not. When nothing is found it auto-installs the package from npm and resolves into
1623
+ * its own global cache, so `require.resolve('@biomejs/biome/package.json')` succeeds under Bun
1624
+ * against a project whose package.json has never mentioned Biome. Measured under Bun 1.3.14:
1625
+ *
1626
+ * node -> MODULE_NOT_FOUND
1627
+ * deno -> MODULE_NOT_FOUND
1628
+ * bun -> /home/<user>/.bun/install/cache/@biomejs/biome@2.5.7@@@1/package.json
1629
+ *
1630
+ * That made `drzl generate` emit Biome-formatted files under Bun and unformatted files under Node
1631
+ * from the same schema and the same config, so `generate --check` under Node called every file
1632
+ * out of date; and it fetched a package, plus a multi-megabyte native binary, from the network in
1633
+ * the middle of codegen. It was not stable within Bun either, since whether the auto-install fired
1634
+ * depended on the state of a cache outside the project.
1635
+ *
1636
+ * A `node_modules` path segment is the discriminator, and it is exact rather than a heuristic:
1637
+ * npm, pnpm's `.pnpm` store and Yarn PnP's zip and unplugged paths all reach a package through
1638
+ * one, and Bun's auto-install cache is the one shape that does not, because it is not a project
1639
+ * install. Under Node and Deno this can never fire, since their resolvers have no other kind of
1640
+ * path to return, so it costs those runtimes nothing. A Bun project that really does install
1641
+ * Biome resolves through its `node_modules` like everyone else and still formats.
1642
+ */
1643
+ declare function isProjectInstallPath(manifestPath: string): boolean;
1006
1644
  /**
1007
1645
  * Pretty-print emitted code with whatever formatter the consumer already has.
1008
1646
  *
@@ -1030,4 +1668,4 @@ declare function selectColumns(table: Table): Column[];
1030
1668
  */
1031
1669
  declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
1032
1670
 
1033
- export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, type BrandAlias, type BrandPlan, type BrandingOption, type BrandingOptions, CODEPOINT_LENGTH, COERCIBLE_DATE_STRING, COLUMN_FORMATS, type CardinalityCheck, type ColumnCheck, type ColumnMetaFacts, type ColumnSet, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_NESTED_DEPTH, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FormatOptions, IMPORT_EXTENSIONS, type ImportExtension, type LengthCheck, MAX_NESTED_DEPTH, type MetaFactOptions, NAME_MODES, NESTED_PREFIX, type NameMode, type NestedArm, type NestedMode, type NestedNode, type ParsedCheck, type ResolvedAffix, type ResolvedBranding, type RowCheck, type Table, type TableCase, type TableMetaFacts, type TableMetaOptions, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, applyTableCase, buildBrandPlan, buildNestedPlan, columnMetaFacts, describeSet, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, moduleFileName, moduleSpecifier, nestedArmNotes, nestedNodeColumns, nestedSchemaName, nestedTypeName, nonFiniteAccepted, parseCheck, parsesToADate, pascalCase, renderDuplicateFinder, resolveAffix, resolveBranding, resolveConfiguredImport, resolveNestedDepth, schemaName, selectColumns, tableMetaFacts, typeName, updateColumns, validateAffix };
1671
+ export { AFFIX_PREFIX_PATTERN, AFFIX_PROBE_TABLE, AFFIX_SUFFIX_PATTERN, type AffixIssue, type AffixOptions, type AffixValue, type BrandAlias, type BrandPlan, type BrandingOption, type BrandingOptions, CODEPOINT_LENGTH, COERCIBLE_DATE_STRING, COLUMN_FORMATS, CONSTRAINTS_MODULE, type CardinalityCheck, type CheckPart, type CheckPartPlace, type ClassifiedCheck, type ColumnCheck, type ColumnMetaFacts, type ColumnSet, type ComparisonWire, type ConstraintFacts, type ConstraintKind, type ConstraintsModuleOptions, type ConstraintsOption, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_NESTED_DEPTH, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FileSink, type FormatOptions, type GeneratorFs, IMPORT_EXTENSIONS, type ImportExtension, type LengthCheck, type LengthMeasure, MAX_NESTED_DEPTH, type MetaFactOptions, NAME_MODES, NESTED_PREFIX, NUMERIC_CANON_NAME, NUMERIC_CANON_SOURCE, type NameMode, type NestedArm, type NestedMode, type NestedNode, type NullCheck, type ParsedCheck, type ResolvedAffix, type ResolvedBranding, type RowCheck, type Table, type TableCase, type TableConstraints, type TableMetaFacts, type TableMetaOptions, type UnenforcedLiteral, type UnenforcedPart, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, type WireLiteralFit, type WireLiteralQuestion, applyTableCase, applyWirePolicy, buildBrandPlan, buildNestedPlan, canonicalMembers, canonicalNumericText, classifyTableChecks, columnMetaFacts, comparisonWire, describeSet, fileWriter, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, isProjectInstallPath, lengthCheckLabel, lengthMeasure, measureExpression, moduleFileName, moduleSpecifier, needsNumericCanon, nestedArmNotes, nestedNodeColumns, nestedSchemaName, nestedTypeName, nonFiniteAccepted, nonFiniteRefused, parseCheck, parsesToADate, pascalCase, renderConstraintsModule, renderDuplicateFinder, resolveAffix, resolveBranding, resolveConfiguredImport, resolveConstraints, resolveNestedDepth, schemaName, selectColumns, tableConstraints, tableMetaFacts, typeName, updateColumns, validateAffix, wireLiteralFit, wireNumberLiteral };