@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/README.md +34 -9
- package/dist/index.cjs +1307 -92
- package/dist/index.d.cts +887 -5
- package/dist/index.d.ts +887 -5
- package/dist/index.js +1280 -91
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,172 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Table as Table$1, Column, Relation, Analysis } from '@drzl/analyzer';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Which columns carry a nominal brand, and what that brand is called.
|
|
5
|
+
*
|
|
6
|
+
* A brand is a type-level marker with no runtime existence at all. Measured on zod 4.4.3,
|
|
7
|
+
* `z.number().brand('X')` returns the *same object* it was called on, and the parsed value of
|
|
8
|
+
* `1` is `1`; the same holds for valibot 1.4.2, arktype 2.2.3 and effect 3.x, each of which
|
|
9
|
+
* hands the value back unchanged. So everything decided here is decided for the benefit of
|
|
10
|
+
* `tsc`, and nothing in it can be checked by parsing a value.
|
|
11
|
+
*
|
|
12
|
+
* That is exactly why the decisions live in one module rather than five. A plan that gives
|
|
13
|
+
* `posts.authorId` the wrong token emits a schema whose types are confidently wrong in every
|
|
14
|
+
* generator at once, and no runtime assertion anywhere in this repository would notice.
|
|
15
|
+
*
|
|
16
|
+
* ## The token
|
|
17
|
+
*
|
|
18
|
+
* `<export name>.<column>`, verbatim: `users.id`, `orgMembers.orgId`. Nothing is transformed,
|
|
19
|
+
* singularised or re-cased, which is the whole reason to spell it this way. Two Drizzle tables
|
|
20
|
+
* cannot share an export name inside one schema module and two columns cannot share a name
|
|
21
|
+
* inside one table, so the token is unique by construction and the question of what to do when
|
|
22
|
+
* two names collide after a transformation does not arise. It also reads back as the thing the
|
|
23
|
+
* user wrote:
|
|
24
|
+
*
|
|
25
|
+
* Type 'number & $brand<"posts.id">' is not assignable to type 'number & $brand<"users.id">'
|
|
26
|
+
*
|
|
27
|
+
* The transformation question does arise for the exported *alias*, which has to be a TypeScript
|
|
28
|
+
* identifier. See `aliasesFor`.
|
|
29
|
+
*
|
|
30
|
+
* ## What a foreign key gets
|
|
31
|
+
*
|
|
32
|
+
* The token of the column it references, not one of its own. `posts.authorId` holds a user's id,
|
|
33
|
+
* so it is a `users.id`; giving it `posts.authorId` would invent a type nothing else in the
|
|
34
|
+
* schema produces and the feature would check nothing. That rule is applied transitively and it
|
|
35
|
+
* beats the column being part of its own table's key, which is what makes a join table keyed on
|
|
36
|
+
* two foreign keys come out right.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
interface BrandingOptions {
|
|
40
|
+
enabled?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Also brand foreign key columns with the brand of the column they reference, so a
|
|
43
|
+
* `Post.authorId` is a `users.id` rather than a `number`.
|
|
44
|
+
*
|
|
45
|
+
* On by default, because it is where nearly all of the value is: a primary key branded alone
|
|
46
|
+
* only stops you passing a `PostId` to something wanting a `UserId`, while branding the
|
|
47
|
+
* foreign keys is what makes the ids flowing between tables carry their origin.
|
|
48
|
+
*/
|
|
49
|
+
foreignKeys?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Also export a named alias per branded key, e.g. `export type UsersId = ...`.
|
|
52
|
+
*
|
|
53
|
+
* On by default. Without one the type is only nameable as an indexed access into the select
|
|
54
|
+
* type, which works and reads badly.
|
|
55
|
+
*/
|
|
56
|
+
aliases?: boolean;
|
|
57
|
+
}
|
|
58
|
+
type BrandingOption = boolean | BrandingOptions;
|
|
59
|
+
interface ResolvedBranding {
|
|
60
|
+
foreignKeys: boolean;
|
|
61
|
+
aliases: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** One `export type <alias> = ...` line a table's module owns. */
|
|
64
|
+
interface BrandAlias {
|
|
65
|
+
/** The identifier, e.g. `UsersId`. */
|
|
66
|
+
alias: string;
|
|
67
|
+
/** The column it is the type of, e.g. `id`. */
|
|
68
|
+
column: string;
|
|
69
|
+
/** The token that column carries, which for an owned alias is `<tsName>.<column>`. */
|
|
70
|
+
token: string;
|
|
71
|
+
}
|
|
72
|
+
interface BrandPlan {
|
|
73
|
+
/** The brand token for one column, or nothing when it carries none. */
|
|
74
|
+
brandOf(tsName: string, columnName: string): string | undefined;
|
|
75
|
+
/** The aliases this table's module exports. Empty when it owns no brand. */
|
|
76
|
+
aliasesFor(tsName: string): BrandAlias[];
|
|
77
|
+
/** Whether anything at all is branded, so a generator can skip its emit path entirely. */
|
|
78
|
+
readonly any: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* What was withheld and why, in the words a user can act on.
|
|
81
|
+
*
|
|
82
|
+
* Withholding is the normal outcome for several shapes here and it is silent in the emitted
|
|
83
|
+
* file, so a run that quietly brands less than the user expects has to say so somewhere.
|
|
84
|
+
*/
|
|
85
|
+
readonly notes: readonly string[];
|
|
86
|
+
}
|
|
87
|
+
/** `{ foreignKeys, aliases }`, or nothing when branding is off. Off is the default. */
|
|
88
|
+
declare function resolveBranding(opt: BrandingOption | undefined): ResolvedBranding | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* Build the plan, or nothing when branding is off.
|
|
91
|
+
*
|
|
92
|
+
* Every path that cannot answer with certainty returns no brand rather than a guess. A wrong
|
|
93
|
+
* brand does not fail loudly: it compiles, and it makes two unrelated ids interchangeable or two
|
|
94
|
+
* related ones incompatible, either of which is worse than the column staying a plain number.
|
|
95
|
+
*/
|
|
96
|
+
declare function buildBrandPlan(tables: Table$1[], opt: BrandingOption | undefined): BrandPlan | undefined;
|
|
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;
|
|
2
170
|
|
|
3
171
|
/**
|
|
4
172
|
* One place that decides what a generated module is called, on disk and in an import.
|
|
@@ -158,6 +326,22 @@ declare function resolveAffix(opts?: {
|
|
|
158
326
|
declare function schemaName(mode: NameMode, tsName: string, affix: ResolvedAffix): string;
|
|
159
327
|
/** Name of the exported type alias, e.g. `InsertusersInput`. */
|
|
160
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_$]+)?$";
|
|
161
345
|
/**
|
|
162
346
|
* Reject affixes that cannot produce a compilable file, before anything is written:
|
|
163
347
|
* - characters that are not legal in a TypeScript identifier
|
|
@@ -193,6 +377,11 @@ declare function validateAffix(affix?: AffixOptions, schemaSuffix?: string): Aff
|
|
|
193
377
|
* base expression rather than on the whole field.
|
|
194
378
|
* 2. **A multi-column check cannot live on a field.** `start_date < end_date` is a statement
|
|
195
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`.
|
|
196
385
|
*/
|
|
197
386
|
/** A comparison of one column against one literal, which is the case worth translating. */
|
|
198
387
|
interface ColumnCheck {
|
|
@@ -231,7 +420,8 @@ interface RowCheck {
|
|
|
231
420
|
name?: string;
|
|
232
421
|
}
|
|
233
422
|
/**
|
|
234
|
-
* A constraint on a column's
|
|
423
|
+
* A constraint on a count derived from a column's value, from `CHECK (length(name) > 3)` and
|
|
424
|
+
* `CHECK (octet_length(blob) <= 5)`.
|
|
235
425
|
*
|
|
236
426
|
* Kept apart from `ColumnCheck` because it is not a comparison of the value: it compares a count
|
|
237
427
|
* derived from it, and the count Postgres takes is code points rather than UTF-16 units. See
|
|
@@ -242,8 +432,88 @@ interface LengthCheck {
|
|
|
242
432
|
operator: ColumnCheck['operator'];
|
|
243
433
|
/** Decimal, as text, matching how the other bounds are carried. */
|
|
244
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';
|
|
245
455
|
name?: string;
|
|
246
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;
|
|
247
517
|
/**
|
|
248
518
|
* A constraint on an array column's element count, from `CHECK (cardinality(tags) > 0)`.
|
|
249
519
|
*
|
|
@@ -256,6 +526,24 @@ interface CardinalityCheck {
|
|
|
256
526
|
value: string;
|
|
257
527
|
name?: string;
|
|
258
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
|
+
}
|
|
259
547
|
/** A check that was understood, or the reason it was not. */
|
|
260
548
|
type ParsedCheck = {
|
|
261
549
|
ok: true;
|
|
@@ -264,6 +552,7 @@ type ParsedCheck = {
|
|
|
264
552
|
rows?: RowCheck[];
|
|
265
553
|
lengths?: LengthCheck[];
|
|
266
554
|
cardinalities?: CardinalityCheck[];
|
|
555
|
+
nulls?: NullCheck[];
|
|
267
556
|
} | {
|
|
268
557
|
ok: false;
|
|
269
558
|
reason: string;
|
|
@@ -274,8 +563,36 @@ type ParsedCheck = {
|
|
|
274
563
|
* Deliberately narrow. `BETWEEN` is included because it is common and means exactly two
|
|
275
564
|
* inclusive bounds; `AND` of arbitrary predicates is not, because getting its scope wrong would
|
|
276
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`.
|
|
277
574
|
*/
|
|
278
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;
|
|
279
596
|
/**
|
|
280
597
|
* A human-readable rendering of a set constraint, for an error message.
|
|
281
598
|
*
|
|
@@ -283,6 +600,473 @@ declare function parseCheck(expression: string | undefined, name?: string): Pars
|
|
|
283
600
|
* schema rather than like its JavaScript translation.
|
|
284
601
|
*/
|
|
285
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;
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* The facts a generated schema can carry beside itself.
|
|
943
|
+
*
|
|
944
|
+
* A validator says what a value must look like. It does not say where the value came from, and a
|
|
945
|
+
* consumer holding only the schema cannot recover that: `z.string()` is a `text`, a `varchar(40)`,
|
|
946
|
+
* a `citext` and a `char(3)` alike, and nothing on it says which, nor whether the database fills
|
|
947
|
+
* it in, nor which columns key the row.
|
|
948
|
+
*
|
|
949
|
+
* Every key here had to pass one test: **it says something the emitted schema does not already
|
|
950
|
+
* say.** Two ways to pass it.
|
|
951
|
+
*
|
|
952
|
+
* the schema never knew it the SQL type, the primary key, the unique constraints, the
|
|
953
|
+
* dialect, whether the database generates or defaults the value.
|
|
954
|
+
*
|
|
955
|
+
* the schema enforces it and a declared width and every CHECK are `.refine()` calls, and
|
|
956
|
+
* cannot show it `z.toJSONSchema` drops a refinement in silence. Measured on zod
|
|
957
|
+
* 4.4.3: `z.object({ s: z.string().refine(...) })` produces
|
|
958
|
+
* `{ s: { type: 'string' } }` with no warning and no trace. So a
|
|
959
|
+
* JSON Schema built from an emitted module is wrong by omission,
|
|
960
|
+
* and nothing in the document says so.
|
|
961
|
+
*
|
|
962
|
+
* Nullability is the counter-example, and its absence is the rule working: `.nullable()` is in the
|
|
963
|
+
* schema and `anyOf: [..., { type: 'null' }]` is in its JSON Schema, so a `nullable` key would be a
|
|
964
|
+
* second copy of an answer the consumer already has. Same for the enum values, the integer-ness of
|
|
965
|
+
* a number and every numeric bound.
|
|
966
|
+
*
|
|
967
|
+
* Nothing here is a user comment, because there are none to carry. Measured against drizzle-orm on
|
|
968
|
+
* both majors: no column, table or builder exposes one, and a `comment` key passed to a column's
|
|
969
|
+
* options object is dropped before the column is built. See the zod generator's documentation.
|
|
970
|
+
*/
|
|
971
|
+
|
|
972
|
+
/** What a column adds beside its schema. Every key is optional; an empty object is a real answer. */
|
|
973
|
+
interface ColumnMetaFacts {
|
|
974
|
+
/** The type as the database declares it: `varchar(255)`, `numeric(10, 2)`, `text[]`. */
|
|
975
|
+
sqlType?: string;
|
|
976
|
+
/**
|
|
977
|
+
* The declared character limit.
|
|
978
|
+
*
|
|
979
|
+
* Also the JSON Schema keyword of the same name, which is why it is spelled this way: the
|
|
980
|
+
* emitted schema enforces the limit inside a `.refine()` closure, `toJSONSchema` drops that, and
|
|
981
|
+
* this key puts the constraint back in the one spelling every JSON Schema validator enforces.
|
|
982
|
+
*
|
|
983
|
+
* On an array column the limit is the *element's*, since that is where the emitted schema
|
|
984
|
+
* applies it.
|
|
985
|
+
*/
|
|
986
|
+
maxLength?: number;
|
|
987
|
+
/** The declared byte limit, which only MySQL's TEXT and BLOB families carry. */
|
|
988
|
+
maxBytes?: number;
|
|
989
|
+
/**
|
|
990
|
+
* The database supplies a value when the write omits one.
|
|
991
|
+
*
|
|
992
|
+
* Not recoverable from the schema: a defaulted column and a nullable one are both `.optional()`
|
|
993
|
+
* on insert, so the wrapper cannot tell them apart, and on select neither leaves a trace.
|
|
994
|
+
*/
|
|
995
|
+
hasDefault?: true;
|
|
996
|
+
/** The database computes the value and refuses to be given one. Absent from the write schemas. */
|
|
997
|
+
generated?: true;
|
|
998
|
+
/**
|
|
999
|
+
* The CHECK constraints this field enforces, named as the failure messages name them.
|
|
1000
|
+
*
|
|
1001
|
+
* Not a restatement of the bound beside it. DRZL deliberately folds a CHECK into the column's
|
|
1002
|
+
* own range, so `minimum: 18` in the JSON Schema is indistinguishable from a type bound, and a
|
|
1003
|
+
* set constraint renders as an enum indistinguishable from a declared one. The provenance is
|
|
1004
|
+
* what this carries, along with the constraint name a database error will quote back.
|
|
1005
|
+
*/
|
|
1006
|
+
checks?: string[];
|
|
1007
|
+
/** Prose for a reader, from the constraints the schema enforces and cannot show. Opt-in. */
|
|
1008
|
+
description?: string;
|
|
1009
|
+
}
|
|
1010
|
+
/** What a table's schema adds beside itself. */
|
|
1011
|
+
interface TableMetaFacts {
|
|
1012
|
+
/** The SQL table name, which is not the Drizzle export name the schema is named after. */
|
|
1013
|
+
table: string;
|
|
1014
|
+
/**
|
|
1015
|
+
* The SQL schema the table lives in, present only when it names one.
|
|
1016
|
+
*
|
|
1017
|
+
* Beside `table` rather than folded into it. `table` is the bare name in every emitted file
|
|
1018
|
+
* that exists, and two tables in two schemas publish the same one, so without this a consumer
|
|
1019
|
+
* reading the metadata of `reporting.users` cannot tell it from `public.users`. Absent for a
|
|
1020
|
+
* table in the default schema, which is what `pgTable` declares and the only thing it can
|
|
1021
|
+
* declare: Drizzle refuses `pgSchema('public')`.
|
|
1022
|
+
*/
|
|
1023
|
+
schema?: string;
|
|
1024
|
+
/** Which database this was analysed from. The same declaration means different things across them. */
|
|
1025
|
+
dialect?: string;
|
|
1026
|
+
/** Which of the three schemas this is. The export name says it; the schema object does not. */
|
|
1027
|
+
mode: string;
|
|
1028
|
+
/** The primary key columns, in order. A per-field flag cannot carry the order or the grouping. */
|
|
1029
|
+
primaryKey?: string[];
|
|
1030
|
+
/**
|
|
1031
|
+
* The unique constraints.
|
|
1032
|
+
*
|
|
1033
|
+
* The one constraint a per-row validator structurally cannot check, which is why
|
|
1034
|
+
* `duplicateFinder` exists at all. Carrying it lets a consumer see what the schema is silent
|
|
1035
|
+
* about rather than assume it is silent because there is nothing to say.
|
|
1036
|
+
*/
|
|
1037
|
+
unique?: string[][];
|
|
1038
|
+
/** The relation refuses writes, which today means a materialized view. */
|
|
1039
|
+
readOnly?: true;
|
|
1040
|
+
/** Row-level CHECKs, enforced as object refinements and so invisible for the same reason. */
|
|
1041
|
+
checks?: string[];
|
|
1042
|
+
/**
|
|
1043
|
+
* CHECK constraints the database enforces and this schema does not.
|
|
1044
|
+
*
|
|
1045
|
+
* Either the parser declined the expression, or it understood it and the column's shape has no
|
|
1046
|
+
* way to state it. Both mean the same thing to a caller: the database can reject a row this
|
|
1047
|
+
* schema accepted. Nothing else in the emitted module mentions these at all.
|
|
1048
|
+
*/
|
|
1049
|
+
unenforcedChecks?: string[];
|
|
1050
|
+
/** Prose for a reader. Opt-in. */
|
|
1051
|
+
description?: string;
|
|
1052
|
+
}
|
|
1053
|
+
interface MetaFactOptions {
|
|
1054
|
+
/** Also write a `description`, which is what an OpenAPI reader renders. Off by default. */
|
|
1055
|
+
description?: boolean;
|
|
1056
|
+
}
|
|
1057
|
+
interface TableMetaOptions extends MetaFactOptions {
|
|
1058
|
+
mode: string;
|
|
1059
|
+
dialect?: string;
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* The metadata for one column.
|
|
1063
|
+
*
|
|
1064
|
+
* `table` is needed because a CHECK is declared on the table and only then attributed to a column,
|
|
1065
|
+
* and because whether a constraint is enforced at all depends on the other columns it names.
|
|
1066
|
+
*/
|
|
1067
|
+
declare function columnMetaFacts(column: Column, table: Table$1, opts?: MetaFactOptions): ColumnMetaFacts;
|
|
1068
|
+
/** The metadata for one table's schema, in one mode. */
|
|
1069
|
+
declare function tableMetaFacts(table: Table$1, opts: TableMetaOptions): TableMetaFacts;
|
|
286
1070
|
|
|
287
1071
|
/**
|
|
288
1072
|
* What a relations-aware nested schema describes, decided once for all five generators.
|
|
@@ -422,7 +1206,7 @@ declare function nestedNodeColumns<T extends {
|
|
|
422
1206
|
*/
|
|
423
1207
|
|
|
424
1208
|
/**
|
|
425
|
-
* `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.
|
|
426
1210
|
*
|
|
427
1211
|
* `rowType` is the name of the insert type, which is what a caller has in hand before an insert.
|
|
428
1212
|
* Passed in rather than derived, because each generator names its types differently.
|
|
@@ -436,6 +1220,16 @@ interface Table {
|
|
|
436
1220
|
primaryKey?: {
|
|
437
1221
|
columns: string[];
|
|
438
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
|
+
}[];
|
|
439
1233
|
}
|
|
440
1234
|
type ValidationLibrary = 'zod' | 'valibot' | 'arktype' | 'typebox' | 'effect';
|
|
441
1235
|
interface FormatOptions {
|
|
@@ -547,11 +1341,39 @@ interface ValidationGenerateOptions {
|
|
|
547
1341
|
* what terminates a cycle, since `users -> posts -> users` simply stops here.
|
|
548
1342
|
*/
|
|
549
1343
|
nestedDepth?: number;
|
|
1344
|
+
/**
|
|
1345
|
+
* Give every primary key, and every foreign key pointing at one, a nominal type, so a
|
|
1346
|
+
* `users.id` cannot be passed where a `posts.id` is wanted.
|
|
1347
|
+
*
|
|
1348
|
+
* Type level only. The brand is a marker in the inferred type and nothing at all at runtime:
|
|
1349
|
+
* measured on zod 4.4.3, `.brand()` returns the same schema object it was called on, and the
|
|
1350
|
+
* parsed value of `1` is `1`, so two branded ids holding `1` are still `===`. Nothing about
|
|
1351
|
+
* what a schema accepts or rejects changes here, and nothing is added to the bundle.
|
|
1352
|
+
*
|
|
1353
|
+
* Off by default, because it changes the inferred type of every consumer of the select
|
|
1354
|
+
* schemas. Turning it on will produce errors in code that was passing the wrong id around,
|
|
1355
|
+
* which is the point, but it is a change to existing call sites rather than an addition.
|
|
1356
|
+
*
|
|
1357
|
+
* `{ foreignKeys: false }` brands only the keys themselves. See `BrandingOptions`.
|
|
1358
|
+
*
|
|
1359
|
+
* TypeBox has no brand helper, so the marker there is an intersection carried by
|
|
1360
|
+
* `Type.Unsafe<T>`, which leaves the runtime schema byte-identical. See the docs page.
|
|
1361
|
+
*/
|
|
1362
|
+
branded?: BrandingOption;
|
|
550
1363
|
emit?: {
|
|
551
1364
|
select?: boolean;
|
|
552
1365
|
insert?: boolean;
|
|
553
1366
|
update?: boolean;
|
|
554
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;
|
|
555
1377
|
}
|
|
556
1378
|
interface ValidationRenderer<TOptions extends ValidationGenerateOptions = ValidationGenerateOptions> {
|
|
557
1379
|
readonly library: ValidationLibrary;
|
|
@@ -597,7 +1419,7 @@ declare function isGeneratedColumn(c: Column, _primaryKeyColumns?: string[]): bo
|
|
|
597
1419
|
* the real hazard is over-rejection. A check that turns away something Postgres accepts breaks
|
|
598
1420
|
* working code, which is worse than the bare `string` it replaces.
|
|
599
1421
|
*
|
|
600
|
-
*
|
|
1422
|
+
* The list is short for that reason. Candidates for `date`, `timestamp`, `time`, `interval`,
|
|
601
1423
|
* `inet`, `cidr` and `macaddr` were all built and all rejected, each by a value Postgres accepts
|
|
602
1424
|
* and the pattern did not:
|
|
603
1425
|
*
|
|
@@ -606,6 +1428,11 @@ declare function isGeneratedColumn(c: Column, _primaryKeyColumns?: string[]): bo
|
|
|
606
1428
|
* macaddr `2020-01-01`, which Postgres pads into `20:20:00:01:00:01`
|
|
607
1429
|
* inet `10.1/16`, `::ffff:1.2.3.4`
|
|
608
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.
|
|
609
1436
|
*/
|
|
610
1437
|
declare const COLUMN_FORMATS: Record<string, string>;
|
|
611
1438
|
/**
|
|
@@ -743,6 +1570,34 @@ declare function nonFiniteAccepted(c: Column): {
|
|
|
743
1570
|
nan: boolean;
|
|
744
1571
|
infinity: boolean;
|
|
745
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
|
+
};
|
|
746
1601
|
declare function insertColumns(table: Table): Column[];
|
|
747
1602
|
/**
|
|
748
1603
|
* Which columns belong in an update schema.
|
|
@@ -759,6 +1614,33 @@ declare function insertColumns(table: Table): Column[];
|
|
|
759
1614
|
*/
|
|
760
1615
|
declare function updateColumns(table: Table): Column[];
|
|
761
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;
|
|
762
1644
|
/**
|
|
763
1645
|
* Pretty-print emitted code with whatever formatter the consumer already has.
|
|
764
1646
|
*
|
|
@@ -786,4 +1668,4 @@ declare function selectColumns(table: Table): Column[];
|
|
|
786
1668
|
*/
|
|
787
1669
|
declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
|
|
788
1670
|
|
|
789
|
-
export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, CODEPOINT_LENGTH, COERCIBLE_DATE_STRING, COLUMN_FORMATS, type CardinalityCheck, type ColumnCheck, 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, NAME_MODES, NESTED_PREFIX, type NameMode, type NestedArm, type NestedMode, type NestedNode, type ParsedCheck, type ResolvedAffix, type RowCheck, type Table, type TableCase, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, applyTableCase, buildNestedPlan, describeSet, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, moduleFileName, moduleSpecifier, nestedArmNotes, nestedNodeColumns, nestedSchemaName, nestedTypeName, nonFiniteAccepted, parseCheck, parsesToADate, pascalCase, renderDuplicateFinder, resolveAffix, resolveConfiguredImport, resolveNestedDepth, schemaName, selectColumns, 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 };
|