@drzl/analyzer 1.18.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -8,28 +8,93 @@ interface Issue {
8
8
  level: 'info' | 'warn' | 'error';
9
9
  message: string;
10
10
  hint?: string;
11
+ /**
12
+ * Where the issue is, as `table` or `table.column`.
13
+ *
14
+ * Declared since this interface existed and set by nothing, so every consumer wanting to group
15
+ * warnings by table had to read the names back out of the English in `message`. `drzl doctor` is
16
+ * the first such consumer and a report built by regex over prose breaks the first time a message
17
+ * is reworded, so the names are stated here instead.
18
+ *
19
+ * Still optional: an issue about the schema as a whole, such as an unidentifiable dialect, is
20
+ * about no table and says so by omitting this.
21
+ */
11
22
  path?: string;
12
23
  }
24
+ /**
25
+ * Why a column has no type, where "nobody has modelled it" is not the answer.
26
+ *
27
+ * The distinction exists because it changes the advice. A column class this file has no arm for is
28
+ * a gap someone can close, and the warning tells its author to say so. A Gel temporal column is
29
+ * not: the value is an instance of a class from the `gel` package, DRZL cannot import that package,
30
+ * and no generator could emit a check for it even knowing the name. Leaving it `unknown` is the
31
+ * measured answer rather than an omission, and its warning should say that instead of asking for a
32
+ * bug report that is already closed.
33
+ */
34
+ type UnnameableReason = 'gel-temporal';
13
35
  interface ColumnRef {
14
36
  table: string;
15
37
  column: string;
16
38
  }
39
+ /**
40
+ * A link between two tables, each end named by `qualifiedTableName`.
41
+ *
42
+ * Qualified rather than bare, because a bare database name identifies a table only while no two
43
+ * SQL schemas hold it: `to: 'users'` cannot say whether it means `public.users` or
44
+ * `reporting.users`, and every consumer resolves these strings back to a table object. A table in
45
+ * the default schema has no prefix, so nothing about a single-schema analysis changes.
46
+ */
17
47
  interface Relation {
18
48
  kind: 'one' | 'many' | 'manyToMany';
49
+ /** Qualified table name: `users`, or `reporting.users`. */
19
50
  from: string;
51
+ /** Qualified table name: `users`, or `reporting.users`. */
20
52
  to: string;
53
+ /** Qualified name of the join table, for m2m. */
21
54
  via?: string;
22
55
  }
23
56
  interface Column {
24
57
  name: string;
25
58
  tsType: string;
59
+ /**
60
+ * A coarse label for the column's kind, not its type.
61
+ *
62
+ * `varchar`, `char` and `text` are all `TEXT` here, deliberately: two consumers read this and
63
+ * both ask coarse questions. `isIntegerColumn` asks whether a number is whole, and
64
+ * `comparisonWire` in `@drzl/validation-core` asks whether a string wire carries decimal text
65
+ * the database compares numerically, which is `NUMERIC` (every decimal family) and `BIGINT`
66
+ * (the v1 string mode). Use `sqlType` for the question this name suggests it answers.
67
+ */
26
68
  dbType: string;
69
+ /**
70
+ * The column's type as the database declares it, from Drizzle's own `getSQLType()`:
71
+ * `varchar(255)`, `numeric(10, 2)`, `timestamp with time zone`, `text[]`, or an enum's type
72
+ * name.
73
+ *
74
+ * The one fact about a column that no validator schema can carry, and the one every other fact
75
+ * here is a consequence of. A generator that emits metadata beside its schemas has nothing else
76
+ * to put in it: `dbType` is a label rather than a type, and the declared width lives in
77
+ * `maxLength`, the precision in `min`/`max`, and neither of those says which type produced it.
78
+ *
79
+ * The two Drizzle majors disagree about an array and are reconciled here, measured rather than
80
+ * assumed: 0.4x wraps the column in a `PgArray` whose own answer is already `text[]`, while v1
81
+ * leaves the class alone and raises `dimensions`, so its answer is the bare `text`. The suffix
82
+ * is added from `arrayDimensions` when the type does not already carry one, so a consumer
83
+ * cannot tell which major produced its metadata.
84
+ *
85
+ * Absent where the builder has no `getSQLType` or it throws. Nothing is guessed from the class
86
+ * name: an invented type string reads exactly like a real one, and there is no way for a
87
+ * consumer to tell them apart.
88
+ */
89
+ sqlType?: string;
27
90
  nullable: boolean;
28
91
  hasDefault: boolean;
29
92
  isGenerated: boolean;
30
93
  defaultExpression?: string;
31
94
  references?: {
32
95
  table: string;
96
+ /** SQL schema of the referenced table, absent for the default one. See `ForeignKey`. */
97
+ schema?: string;
33
98
  column: string;
34
99
  onDelete?: string;
35
100
  onUpdate?: string;
@@ -48,8 +113,16 @@ interface Column {
48
113
  *
49
114
  * Strings rather than numbers because a 64 bit bound is not representable as a JS number:
50
115
  * `9223372036854775807` rounds to `9223372036854775808` the moment it becomes one, so a
51
- * numeric field here would silently emit a wrong bound. Absent for floats and for `numeric`,
52
- * which have no integer range.
116
+ * numeric field here would silently emit a wrong bound. A 20 digit `numeric(20,0)` bound is
117
+ * further past that again.
118
+ *
119
+ * Not an integer range, despite the name this field used to be described by. An inexact column
120
+ * carries one too: a `real` is bounded by the magnitude the database refuses past, and a
121
+ * `numeric(10,2)` by the width its own declaration states. `integer` says which kind it is, and
122
+ * saying it is what stops a bounded float schema refusing 1.5.
123
+ *
124
+ * Absent where nothing declares a bound: an 8 byte float holds every finite JS number, and a
125
+ * `numeric` with no precision holds arbitrary precision.
53
126
  */
54
127
  min?: string;
55
128
  max?: string;
@@ -62,16 +135,57 @@ interface Column {
62
135
  * generators fall back to the old inference there.
63
136
  */
64
137
  integer?: boolean;
138
+ /**
139
+ * Whether the column stores and returns `NaN`, and whether it does the same for an infinity.
140
+ *
141
+ * A range cannot say this. `>=`/`<=` refuses `Infinity` whatever the two numbers are, and `NaN`
142
+ * compares false against both ends, so a bounded float column described by its range alone
143
+ * refused three values Postgres stores in it and hands back on SELECT. That is a read-path
144
+ * defect: every read of such a row fails validation on a column behaving exactly as documented.
145
+ * The generators render these as a union beside the range rather than as a wider range.
146
+ *
147
+ * Measured against PostgreSQL 18.3 through PGlite, on the bound-parameter path a validator
148
+ * guards. `real` and `double precision` return all three unchanged. An unconstrained `numeric`
149
+ * does too, but a `numeric(10,2)` refuses either infinity with `22003 numeric field overflow`
150
+ * while still taking `NaN`, and `integer`/`bigint` refuse all three.
151
+ *
152
+ * So `numeric` in `{ mode: 'number' }` answers each of the two separately: `allowsNaN` at any
153
+ * width, and `allowsInfinity` only where the declaration carries no precision. That used to be a
154
+ * flat `false`, and the recorded reason was that nothing here read a column's precision or scale,
155
+ * so the two declarations were indistinguishable and the narrower answer was the safer one.
156
+ * `declaredDecimalRange` reads both numbers now, the two are distinguishable, and each says what
157
+ * its own server does.
158
+ *
159
+ * Postgres and Gel. MySQL refuses all three on a `float`/`double`, and on a `decimal` refuses
160
+ * them outright rather than storing `0.00`: measured on MySQL 8.4.11 in `STRICT_TRANS_TABLES`,
161
+ * all three answer `Incorrect decimal value`. SQLite returns both infinities and silently turns
162
+ * `NaN` into NULL, which is real and is filed on its own: a column needs both halves of that
163
+ * answer or none.
164
+ *
165
+ * Gel joined on a measurement of its own rather than on being Postgres-backed: a live Gel 7.1
166
+ * stored `nan`, `inf` and `-inf` in both `std::float32` and `std::float64` and handed all three
167
+ * back unchanged, through a cast and through a stored property.
168
+ *
169
+ * Absent on every other column, including the string mode of `numeric`, which already carries the
170
+ * same fact as a pattern; see `COLUMN_FORMATS.numeric` in `@drzl/validation-core`.
171
+ */
172
+ allowsNaN?: boolean;
173
+ allowsInfinity?: boolean;
65
174
  /**
66
175
  * A string column whose contents have a shape the database enforces.
67
176
  *
68
- * Only formats checked against Postgres itself appear here, and the list is short because most
177
+ * Only formats checked against a real server appear here, and the list is short because most
69
178
  * candidates failed: Postgres reads `'today'` and `'January 8, 1999'` as dates, pads
70
179
  * `'2020-01-01'` into a macaddr, and accepts `'10.1/16'` as an inet. A check for any of those
71
180
  * would reject input the database accepts, and turning away valid data is worse than not
72
181
  * checking at all. See `COLUMN_FORMATS` in `@drzl/validation-core`.
182
+ *
183
+ * Two of the keys name a dialect, because the same column has two different answers: a
184
+ * `bigint({ mode: 'string' })` is parsed by Postgres as an integer literal, `'0x1f'` and
185
+ * `'1_000'` included, and by MySQL as a decimal number it then rounds, so `'12.5'` is a row on
186
+ * one server and an error on the other.
73
187
  */
74
- format?: 'uuid' | 'numeric';
188
+ format?: 'uuid' | 'numeric' | 'pgBigint' | 'mysqlBigint';
75
189
  /**
76
190
  * The column's default, when it is a literal a schema can reproduce.
77
191
  *
@@ -228,6 +342,17 @@ interface ForeignKey {
228
342
  name?: string;
229
343
  columns: string[];
230
344
  foreignTable: string;
345
+ /**
346
+ * The SQL schema the referenced table lives in, absent for the default one, exactly as
347
+ * `Table.schema` is.
348
+ *
349
+ * `foreignTable` is a bare database name and Postgres lets two schemas hold the same one, so a
350
+ * key pointing at `reporting.users` recorded the identical string a key pointing at
351
+ * `public.users` records. Every consumer that resolves a key back to a table object did so by
352
+ * that string, and therefore resolved to whichever of the two it saw first. Use
353
+ * `qualifiedForeignTable` rather than reading the two fields apart.
354
+ */
355
+ foreignSchema?: string;
231
356
  foreignColumns: string[];
232
357
  onDelete?: string;
233
358
  onUpdate?: string;
@@ -235,6 +360,15 @@ interface ForeignKey {
235
360
  interface Table {
236
361
  name: string;
237
362
  tsName: string;
363
+ /**
364
+ * The SQL schema the table was declared in, from `pgSchema('reporting').table(...)` and the
365
+ * MySQL and SingleStore equivalents. Absent for a table declared with plain `pgTable`, which is
366
+ * the only spelling of the default schema there is: Drizzle refuses `pgSchema('public')`
367
+ * outright, with "Postgres is using public schema by default".
368
+ *
369
+ * `name` stays bare, so two tables in two schemas share one. `qualifiedTableName` is what tells
370
+ * them apart, and is what every name-addressed surface in DRZL matches against.
371
+ */
238
372
  schema?: string;
239
373
  columns: Column[];
240
374
  primaryKey?: Key;
@@ -254,6 +388,28 @@ interface Enum {
254
388
  name: string;
255
389
  values: string[];
256
390
  }
391
+ /**
392
+ * The one name that identifies a table across every SQL schema in an analysis.
393
+ *
394
+ * `reporting.users` where the table names a schema, and the bare `users` where it does not. The
395
+ * bare form for the default schema is deliberate and is what makes this safe to reach for
396
+ * everywhere: on a schema module that never calls `pgSchema`, and that is nearly all of them, this
397
+ * returns exactly `table.name`, so every file name, every export, every config pattern and every
398
+ * emitted path is byte for byte what it was.
399
+ *
400
+ * `public.users` is not produced here. Drizzle refuses `pgSchema('public')`, so no analysis can
401
+ * ever carry `schema: 'public'`, and a table with no schema *is* the public one. `public.` exists
402
+ * only as a spelling a config may use, resolved by `@drzl/cli`.
403
+ */
404
+ declare function qualifiedTableName(table: {
405
+ name: string;
406
+ schema?: string;
407
+ }): string;
408
+ /** The same name, for the far end of a foreign key. */
409
+ declare function qualifiedForeignTable(fk: {
410
+ foreignTable: string;
411
+ foreignSchema?: string;
412
+ }): string;
257
413
  interface Analysis {
258
414
  drizzleVersion?: string;
259
415
  dialect: Dialect;
@@ -325,7 +481,14 @@ declare function isRelationsV2(val: any): boolean;
325
481
  declare function readRelationsV2(val: any, issues?: Issue[]): Relation[];
326
482
  declare class SchemaAnalyzer {
327
483
  private readonly schemaPath;
328
- constructor(schemaPath: string);
484
+ /**
485
+ * One path or several. The plural exists for drizzle-kit interop: kit's `schema` key names
486
+ * files in the plural (arrays, globs), and the commonest multi-file layout is a directory of
487
+ * one file per table with no barrel, so there is no single module to point at. Entries are
488
+ * concrete files, never globs; expansion is the caller's job, so this class's contract stays
489
+ * "load exactly these modules and read their exports as one schema".
490
+ */
491
+ constructor(schemaPath: string | readonly string[]);
329
492
  private getSymbol;
330
493
  /**
331
494
  * Drizzle keys the Columns object by TypeScript property name, but every other piece of
@@ -415,6 +578,31 @@ declare class SchemaAnalyzer {
415
578
  * promise a precision that cannot survive the round trip.
416
579
  */
417
580
  private static readonly INT_RANGES;
581
+ /**
582
+ * The same widths with `{ unsigned: true }` set, which is the half the table above cannot see.
583
+ *
584
+ * On 0.4x the flag moves no class name: `int('x', { unsigned: true })` still builds a
585
+ * `MySqlInt`, and only `config.unsigned` and the ` unsigned` suffix on `getSQLType()` record
586
+ * the difference, measured off real 0.45.2 columns. So the table above answered every unsigned
587
+ * width with its signed range, and the emitted select schema refused every stored value in the
588
+ * upper half of the column: an `int unsigned` holding 4294967295 failed validation on a row the
589
+ * database returned, and the same one width up meant `bigint unsigned` refused
590
+ * 18446744073709551615n.
591
+ *
592
+ * The ceilings are the type's, verified against a live MySQL 8.4.11: 255, 65535, 16777215 and
593
+ * 4294967295 store and return, -1 and each ceiling plus one are refused with
594
+ * ER_WARN_DATA_OUT_OF_RANGE. The bigint pair keeps the two modes apart for the reason the
595
+ * signed pair above does: number mode tops out at the safe-integer bound the wire imposes,
596
+ * bigint mode at the column's own 2^64-1, which a bigint can spell. SingleStore is MySQL wire
597
+ * compatible, ships the same builders with the same `config.unsigned`, and v1 states the same
598
+ * `uintN` semantics for it, measured off real rc.4 columns; the entries keep the majors in
599
+ * agreement, which is what the cross-major diff in `scripts/verify-packed.sh` holds together.
600
+ *
601
+ * Keyed by class exactly like `INT_RANGES`, and consulted only when `config.unsigned` is
602
+ * `true`, so no Postgres or SQLite column can ever reach it: neither dialect has an unsigned
603
+ * spelling, neither builder accepts the flag, and no class of theirs is named here.
604
+ */
605
+ private static readonly UNSIGNED_INT_RANGES;
418
606
  /**
419
607
  * The numeric column classes that are not exact, and the magnitude each one can really hold.
420
608
  *
@@ -451,6 +639,52 @@ declare class SchemaAnalyzer {
451
639
  * `number double` on drizzle v1, which is where these pairings come from.
452
640
  */
453
641
  private static readonly INEXACT_RANGES;
642
+ /**
643
+ * The number columns whose server has an answer about a non-finite double, and what it is.
644
+ *
645
+ * Three states rather than two, and the third is the reason this table has a `false` half at all.
646
+ * A column present here with `true` stores the value and hands it back, so a schema refusing it
647
+ * refuses rows the column returns. A column present with `false` is one the server was asked
648
+ * about and refused, so a schema accepting it promises what the server will not take. A column
649
+ * *absent* is one nobody has measured, and the generators leave whatever their library does alone
650
+ * rather than guessing; `nonFiniteAccepted` and `nonFiniteRefused` in `@drzl/validation-core` are
651
+ * the two readings of that.
652
+ *
653
+ * The class-name half of what `describeV1Column` reads off the codec, and the two must agree: a
654
+ * fact stated on one path and not the other is a schema that changes when the user upgrades
655
+ * drizzle, which the cross-major diff in `verify-packed.sh` fails on. Every class name here is the
656
+ * same on both majors, read off real columns on 0.45.2 and on 1.0.0-rc.4, so this table also
657
+ * answers for a v1 column and the two answers are identical rather than merely compatible.
658
+ * non-finite-numbers.spec.ts asserts that agreement through the real analyzer.
659
+ *
660
+ * Postgres and Gel store all three. Gel joined on a measurement of its own rather than on being
661
+ * Postgres-backed: a live Gel 7.1 stored `nan`, `inf` and `-inf` in both `std::float32` and
662
+ * `std::float64` and handed all three back, through a cast and again through a stored property.
663
+ * Without them every row of such a column failed validation.
664
+ *
665
+ * MySQL and SingleStore refuse all three, and that used to be left unstated on the reasoning that
666
+ * a column stating nothing costs nothing. It cost two libraries: `v.number()` and ArkType's
667
+ * `number` take both infinities where `z.number()` and `Type.Number()` refuse them, so an
668
+ * unbounded `double` or `real` accepted a value the server answers `ER_WARN_DATA_OUT_OF_RANGE`
669
+ * for. Measured on MySQL 8.4.11 in `STRICT_TRANS_TABLES`, on the binary prepared path, which is
670
+ * the one that puts the real IEEE double on the wire: `float`, `double` and `real` refuse
671
+ * `Infinity`, `-Infinity` and `NaN` alike, while `double` and `real` store 1e300 and
672
+ * 3.4028235e38 unchanged. SingleStore is MySQL wire-compatible and unmeasured, and takes MySQL's
673
+ * answer here exactly as it already takes MySQL's float32 bound in `INEXACT_RANGES`.
674
+ *
675
+ * No SQLite class belongs here in either direction. A real SQLite 3.53.4 stores both infinities in
676
+ * a `real` and hands them back, and silently turns `NaN` into NULL, so it is neither the Postgres
677
+ * answer nor the MySQL one; it is filed on its own and a column needs both halves of it or none.
678
+ *
679
+ * The decimal families are absent too. `PgNumeric` is a string whose pattern already accepts `NaN`
680
+ * and `Infinity`. `PgNumericNumber` is a per-column question this table cannot ask: it takes `NaN`
681
+ * at any width and an infinity only where no precision is declared, and `columnConstraints`
682
+ * answers it beside the bound that decides it. MySQL's `decimal` is absent because the two client
683
+ * paths disagree: on the binary prepared path MySQL 8.4.11 silently stored `0.00` for all three,
684
+ * where the text path answers `Incorrect decimal value`, and "refuses" is only half true of a
685
+ * column that accepted the row.
686
+ */
687
+ private static readonly NON_FINITE_BY_CLASS;
454
688
  /**
455
689
  * Constraints the column definition already carries, which the analysis used to throw away.
456
690
  *
@@ -463,4 +697,4 @@ declare class SchemaAnalyzer {
463
697
  analyze(opts?: AnalyzeOptions): Promise<Analysis>;
464
698
  }
465
699
 
466
- export { type Analysis, type AnalyzeOptions, type Check, type Column, type ColumnRef, type ColumnShape, type Dialect, type Enum, type ForeignKey, type Index, type Issue, type Key, type Relation, SchemaAnalyzer, type Table, SchemaAnalyzer as default, describeV1Column, isDrizzleView, isReadOnlyRelation, isRelationsV2, readRelationsV2 };
700
+ export { type Analysis, type AnalyzeOptions, type Check, type Column, type ColumnRef, type ColumnShape, type Dialect, type Enum, type ForeignKey, type Index, type Issue, type Key, type Relation, SchemaAnalyzer, type Table, type UnnameableReason, SchemaAnalyzer as default, describeV1Column, isDrizzleView, isReadOnlyRelation, isRelationsV2, qualifiedForeignTable, qualifiedTableName, readRelationsV2 };