@drzl/analyzer 1.13.0 → 1.15.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 CHANGED
@@ -47,6 +47,17 @@ The CLI consumes this analysis to generate validation, services, and routers.
47
47
  - relations (incl. inferred), enums
48
48
  - issues (warnings/errors) for constraints and shape
49
49
 
50
+ Per column, beyond the type: `nullable`, `hasDefault`, `defaultValue` (literal defaults only),
51
+ `isGenerated`, `enumValues`, `maxLength` (characters), `maxBytes` (MySQL's TEXT family is a byte
52
+ budget, which is a different measurement on the same kind of column), `min`/`max`,
53
+ `arrayDimensions`, `format`, and `shape` for values that are not scalars (json, buffer, tuple,
54
+ vector, bitstring, customType).
55
+
56
+ `DRZL_ANL_UNKNOWN_COLUMN` is reported for any column whose validator would accept anything, which
57
+ is the shape a missing type mapping takes: nothing throws, and every row passes.
58
+
50
59
  ## Notes
51
60
 
52
- - Best‑effort introspection aligned with Drizzle symbols across versions.
61
+ - Best‑effort introspection aligned with Drizzle symbols across versions. Both live majors are
62
+ covered and diffed against each other in CI: 0.4x and v1 model arrays and enums differently, and
63
+ reading only one silently typed every `.array()` column as `unknown` on the other.
package/dist/index.cjs CHANGED
@@ -57,11 +57,16 @@ var MYSQL_TEXT_CAPS = {
57
57
  mediumblob: 16777215,
58
58
  longblob: 4294967295
59
59
  };
60
- var V1_FLOAT_BOUNDS = {
61
- float: ["-8388608", "8388607"],
62
- // real / float4, 2^23
63
- double: ["-140737488355328", "140737488355327"]
64
- // double precision / float8, 2^47
60
+ var FLOAT32_MAX = "340282346638528859811704183484516925440";
61
+ var PG_FLOAT4_INPUT_MAX = "340282356779733661637539395458142568448";
62
+ var PG_FLOAT4_RANGE = [`-${PG_FLOAT4_INPUT_MAX}`, PG_FLOAT4_INPUT_MAX];
63
+ var MYSQL_FLOAT_RANGE = [`-${FLOAT32_MAX}`, FLOAT32_MAX];
64
+ var JS_SAFE_INTEGER_BOUNDS = ["-9007199254740991", "9007199254740991"];
65
+ var TUPLE_CLASS_SHAPES = {
66
+ // `line()` is the trap here: its `drizzle:entityKind` is `PgLine` while its constructor is
67
+ // `PgLineTuple`, and this path matches on the constructor.
68
+ PgPointTuple: { kind: "tuple", length: 2 },
69
+ PgLineTuple: { kind: "tuple", length: 3 }
65
70
  };
66
71
  function describeV1Column(column) {
67
72
  const codec = column?.codec;
@@ -111,7 +116,8 @@ function describeV1Column(column) {
111
116
  break;
112
117
  case "float":
113
118
  case "double": {
114
- [out.min, out.max] = V1_FLOAT_BOUNDS[semantic];
119
+ if (semantic === "float")
120
+ [out.min, out.max] = codec === "float4" ? PG_FLOAT4_RANGE : MYSQL_FLOAT_RANGE;
115
121
  out.integer = false;
116
122
  out.tsType = "number";
117
123
  out.dbType = semantic === "float" ? "REAL" : "DOUBLE";
@@ -200,13 +206,13 @@ function describeV1Column(column) {
200
206
  out.tsType = "number";
201
207
  out.dbType = "NUMERIC";
202
208
  out.integer = false;
203
- [out.min, out.max] = ["-9007199254740991", "9007199254740991"];
209
+ [out.min, out.max] = JS_SAFE_INTEGER_BOUNDS;
204
210
  } else if (js === "string") {
205
211
  out.tsType = "string";
206
212
  out.dbType = codec === "varchar" ? "VARCHAR" : codec === "char" ? "CHAR" : "TEXT";
207
213
  const kind = String(column?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
208
214
  const cap = codec && kind.startsWith("MySql") ? MYSQL_TEXT_CAPS[codec] : void 0;
209
- if (cap) out.maxLength = cap;
215
+ if (cap) out.maxBytes = cap;
210
216
  } else {
211
217
  return null;
212
218
  }
@@ -507,6 +513,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
507
513
  [out.min, out.max] = range;
508
514
  out.integer = true;
509
515
  }
516
+ if (Object.prototype.hasOwnProperty.call(_SchemaAnalyzer.INEXACT_RANGES, ctor)) {
517
+ const inexact = _SchemaAnalyzer.INEXACT_RANGES[ctor];
518
+ if (inexact) [out.min, out.max] = inexact;
519
+ out.integer = false;
520
+ }
510
521
  if (/^(Pg)?UUID$/i.test(ctor) || /Uuid$/i.test(ctor)) out.format = "uuid";
511
522
  return out;
512
523
  }
@@ -578,9 +589,25 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
578
589
  return { tsType: "Date", dbType: "TIMESTAMP" };
579
590
  case "PgNumeric":
580
591
  return { tsType: "string", dbType: "NUMERIC" };
581
- case "PgFloat":
582
592
  case "PgDoublePrecision":
583
593
  return { tsType: "number", dbType: "DOUBLE" };
594
+ // `real()` builds a `PgReal`, which matched no arm and fell through to the coarse
595
+ // `/Numeric|Float|Double|Real/i` below, so a real column was labelled NUMERIC while v1
596
+ // called it REAL. The arm above used to name `PgFloat` alongside `PgDoublePrecision`, and
597
+ // no such class exists in pg-core on either major: `float` is MySQL's spelling and Gel's
598
+ // is `GelReal`, both of which are matched elsewhere. Enumerated from the module's own
599
+ // exports on 0.45.2 and on 1.0.0-rc.4, which name only PgReal and PgDoublePrecision.
600
+ case "PgReal":
601
+ return { tsType: "number", dbType: "REAL" };
602
+ // 0.4x names a point and a line by their mode. `point()` is a `PgPointTuple` and `line()` a
603
+ // `PgLineTuple`, whose entity kind is `PgLine` while its constructor is not, and both used
604
+ // to fall through to `/Point|Line/i` and come back `string`. The driver hands back [x, y]
605
+ // and [a, b, c], so a select schema built on 0.4x refused every row, and an insert schema
606
+ // took the one string form `mapToDriverValue` turns into something Postgres rejects.
607
+ case "PgPointTuple":
608
+ return { tsType: "[number, number]", dbType: "POINT" };
609
+ case "PgLineTuple":
610
+ return { tsType: "[number, number, number]", dbType: "LINE" };
584
611
  case "PgJson":
585
612
  case "PgJsonb":
586
613
  return { tsType: "any", dbType: ctor === "PgJsonb" ? "JSONB" : "JSON" };
@@ -700,16 +727,19 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
700
727
  const v1 = describeV1Column(col);
701
728
  const constraints = this.columnConstraints(col);
702
729
  if (v1?.shape) delete constraints.maxLength;
703
- const jsonShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : void 0;
704
- const shape = (v1?.shape ?? jsonShape)?.kind;
730
+ const sqlKind = String(outerCol?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
731
+ const sqlType = sqlKind.startsWith("MySql") && typeof col?.getSQLType === "function" ? String(col.getSQLType()).toLowerCase() : void 0;
732
+ const byteCap = sqlType ? MYSQL_TEXT_CAPS[sqlType] : void 0;
733
+ const fallbackShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : TUPLE_CLASS_SHAPES[String(col?.constructor?.name ?? "")];
734
+ const shape = (v1?.shape ?? fallbackShape)?.kind;
705
735
  const finalTs = v1?.tsType ?? tsType;
706
736
  const wide = (finalTs === "unknown" || finalTs === "any") && (!shape || shape === "custom");
707
737
  if (wide) {
708
- const sqlType = typeof col?.getSQLType === "function" ? col.getSQLType() : void 0;
738
+ const sqlType2 = typeof col?.getSQLType === "function" ? col.getSQLType() : void 0;
709
739
  issues.push({
710
740
  code: "DRZL_ANL_UNKNOWN_COLUMN",
711
741
  level: "warn",
712
- message: `Column "${colName}" on table "${tsName}" has no known type${sqlType ? ` (SQL type ${sqlType})` : ""}, so its validator will accept any value.`,
742
+ message: `Column "${colName}" on table "${tsName}" has no known type${sqlType2 ? ` (SQL type ${sqlType2})` : ""}, so its validator will accept any value.`,
713
743
  hint: shape === "custom" ? "A customType has no runtime shape to read. Declare it with .$type<T>() and turn on typedColumns to give the validator the type." : "Open an issue naming the column type so it can be modelled, or declare it with .$type<T>() and turn on typedColumns."
714
744
  });
715
745
  }
@@ -730,7 +760,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
730
760
  // that spread has nothing to say and this is the only source.
731
761
  ...arrayDims ? { arrayDimensions: arrayDims } : {},
732
762
  // Only where v1 did not already describe the value, so a shaped column keeps its shape.
733
- ...jsonShape && !v1?.shape ? { shape: jsonShape } : {}
763
+ ...fallbackShape && !v1?.shape ? { shape: fallbackShape } : {},
764
+ ...byteCap && v1?.maxBytes === void 0 ? { maxBytes: byteCap } : {}
734
765
  });
735
766
  }
736
767
  const name = this.getSymbol(tbl, "drizzle:Name") || tsName;
@@ -770,6 +801,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
770
801
  const cfg = entry?.config ?? entry ?? {};
771
802
  const cols = (cfg.columns ?? []).map((c) => toTs(c?.name)).filter(Boolean);
772
803
  if (!cols.length) continue;
804
+ const entityKind = String(entry?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
805
+ if (entityKind.endsWith("UniqueConstraintBuilder")) {
806
+ unique.push({ columns: cols, name: entry?.name });
807
+ continue;
808
+ }
773
809
  if (cfg.unique === void 0) {
774
810
  pkCols.splice(0, pkCols.length, ...cols);
775
811
  continue;
@@ -1002,6 +1038,60 @@ _SchemaAnalyzer.INT_RANGES = {
1002
1038
  MySqlBigInt64: ["-9223372036854775808", "9223372036854775807"],
1003
1039
  SingleStoreBigInt64: ["-9223372036854775808", "9223372036854775807"]
1004
1040
  };
1041
+ /**
1042
+ * The numeric column classes that are not exact, and the magnitude each one can really hold.
1043
+ *
1044
+ * Only drizzle v1 states this outright, as a `float` or `double` semantic on `dataType`. On
1045
+ * 0.4x the same columns reach the analyzer by class name, `INT_RANGES` was the only range table
1046
+ * on that path, and so nothing said anything about them at all: not the range, and not that
1047
+ * they are inexact. The parity gate measured seven of the ten classes below and reported DRZL
1048
+ * differing from the first-party validator for the same major on all seven. The three
1049
+ * SingleStore classes are in no fixture either pass carries and are covered by unit tests alone.
1050
+ *
1051
+ * Differing is what the gate reports, not what it forbids. Most of its waivers have DRZL
1052
+ * accepting something official refuses and the run counts them; an earlier version of this
1053
+ * sentence said the gate exists to forbid being looser, which the same commit's own success
1054
+ * banner denies.
1055
+ *
1056
+ * `null` is a value in this table and is not the same as a class it does not name. It says the
1057
+ * column is inexact and that no finite magnitude bound is truthful for it, which is the case for
1058
+ * every 8 byte float: float8 is the JavaScript number's own format, so Postgres accepts every
1059
+ * finite JS number into one, measured to `Number.MAX_VALUE`. Read with an own-property test for
1060
+ * that reason, and because a plain object answers to `constructor` and `toString`.
1061
+ *
1062
+ * `integer: false` travels with every entry, bound or not, and what it decides depends on which.
1063
+ * `isIntegerColumn` falls back to "declares both bounds" when the flag is absent, so on a bounded
1064
+ * entry the flag is the only thing stopping the emitted schema calling `.int()` and refusing 1.5.
1065
+ * On an unbounded one it decides nothing, measured both ways against the real function in
1066
+ * `@drzl/validation-core`'s integer-column.spec.ts. It is stated there because it is true of the
1067
+ * column, not because it guards anything. That spec is where the measurement moved when the
1068
+ * analyzer's copy of it turned out to be a closed loop; this sentence went on naming
1069
+ * floats-and-tuples-0.4x.spec.ts, which asserts the flag is present and nothing about what it
1070
+ * decides.
1071
+ *
1072
+ * The widths are the type's, not the name's: MySQL and SingleStore `real` is a synonym for
1073
+ * `double` unless REAL_AS_FLOAT is set, and SQLite `real` is an 8 byte IEEE float. Both are
1074
+ * `number double` on drizzle v1, which is where these pairings come from.
1075
+ */
1076
+ _SchemaAnalyzer.INEXACT_RANGES = {
1077
+ // 4 byte floats, the one width a database refuses a magnitude for, and the two that have one
1078
+ // refuse at different values. SingleStore is MySQL wire-compatible and unmeasured here, so it
1079
+ // takes MySQL's rather than the wider of the two.
1080
+ PgReal: PG_FLOAT4_RANGE,
1081
+ MySqlFloat: MYSQL_FLOAT_RANGE,
1082
+ SingleStoreFloat: MYSQL_FLOAT_RANGE,
1083
+ // 8 byte floats, which hold every finite JS number
1084
+ PgDoublePrecision: null,
1085
+ MySqlDouble: null,
1086
+ MySqlReal: null,
1087
+ SQLiteReal: null,
1088
+ SingleStoreDouble: null,
1089
+ SingleStoreReal: null,
1090
+ // `numeric({ mode: 'number' })`, which v1 reaches through the bare-number arm of
1091
+ // `describeV1Column`. This one is about what a JS number can carry rather than about the
1092
+ // column, which Postgres caps far lower: it refuses 2147483648 into a `numeric(10,2)`.
1093
+ PgNumericNumber: JS_SAFE_INTEGER_BOUNDS
1094
+ };
1005
1095
  var SchemaAnalyzer = _SchemaAnalyzer;
1006
1096
  var index_default = SchemaAnalyzer;
1007
1097
  // Annotate the CommonJS export names for ESM import in node:
package/dist/index.d.cts CHANGED
@@ -81,6 +81,16 @@ interface Column {
81
81
  * value than the one actually stored, so only the literal case is carried.
82
82
  */
83
83
  defaultValue?: unknown;
84
+ /**
85
+ * A cap measured in bytes rather than characters.
86
+ *
87
+ * MySQL's TEXT and BLOB families carry their limit in the type itself, and that limit is a byte
88
+ * budget: `tinytext` is 255 bytes, which on utf8mb4 is 255 ascii characters or 63 emoji.
89
+ * `maxLength` cannot express that, and using it applied the number as a character count.
90
+ *
91
+ * Only MySQL sets this. Postgres `text` has no limit and its `varchar(n)` counts characters.
92
+ */
93
+ maxBytes?: number;
84
94
  /**
85
95
  * Array depth for a column declared with `.array()`, absent when the column is a scalar.
86
96
  *
@@ -344,6 +354,42 @@ declare class SchemaAnalyzer {
344
354
  * promise a precision that cannot survive the round trip.
345
355
  */
346
356
  private static readonly INT_RANGES;
357
+ /**
358
+ * The numeric column classes that are not exact, and the magnitude each one can really hold.
359
+ *
360
+ * Only drizzle v1 states this outright, as a `float` or `double` semantic on `dataType`. On
361
+ * 0.4x the same columns reach the analyzer by class name, `INT_RANGES` was the only range table
362
+ * on that path, and so nothing said anything about them at all: not the range, and not that
363
+ * they are inexact. The parity gate measured seven of the ten classes below and reported DRZL
364
+ * differing from the first-party validator for the same major on all seven. The three
365
+ * SingleStore classes are in no fixture either pass carries and are covered by unit tests alone.
366
+ *
367
+ * Differing is what the gate reports, not what it forbids. Most of its waivers have DRZL
368
+ * accepting something official refuses and the run counts them; an earlier version of this
369
+ * sentence said the gate exists to forbid being looser, which the same commit's own success
370
+ * banner denies.
371
+ *
372
+ * `null` is a value in this table and is not the same as a class it does not name. It says the
373
+ * column is inexact and that no finite magnitude bound is truthful for it, which is the case for
374
+ * every 8 byte float: float8 is the JavaScript number's own format, so Postgres accepts every
375
+ * finite JS number into one, measured to `Number.MAX_VALUE`. Read with an own-property test for
376
+ * that reason, and because a plain object answers to `constructor` and `toString`.
377
+ *
378
+ * `integer: false` travels with every entry, bound or not, and what it decides depends on which.
379
+ * `isIntegerColumn` falls back to "declares both bounds" when the flag is absent, so on a bounded
380
+ * entry the flag is the only thing stopping the emitted schema calling `.int()` and refusing 1.5.
381
+ * On an unbounded one it decides nothing, measured both ways against the real function in
382
+ * `@drzl/validation-core`'s integer-column.spec.ts. It is stated there because it is true of the
383
+ * column, not because it guards anything. That spec is where the measurement moved when the
384
+ * analyzer's copy of it turned out to be a closed loop; this sentence went on naming
385
+ * floats-and-tuples-0.4x.spec.ts, which asserts the flag is present and nothing about what it
386
+ * decides.
387
+ *
388
+ * The widths are the type's, not the name's: MySQL and SingleStore `real` is a synonym for
389
+ * `double` unless REAL_AS_FLOAT is set, and SQLite `real` is an 8 byte IEEE float. Both are
390
+ * `number double` on drizzle v1, which is where these pairings come from.
391
+ */
392
+ private static readonly INEXACT_RANGES;
347
393
  /**
348
394
  * Constraints the column definition already carries, which the analysis used to throw away.
349
395
  *
package/dist/index.d.ts CHANGED
@@ -81,6 +81,16 @@ interface Column {
81
81
  * value than the one actually stored, so only the literal case is carried.
82
82
  */
83
83
  defaultValue?: unknown;
84
+ /**
85
+ * A cap measured in bytes rather than characters.
86
+ *
87
+ * MySQL's TEXT and BLOB families carry their limit in the type itself, and that limit is a byte
88
+ * budget: `tinytext` is 255 bytes, which on utf8mb4 is 255 ascii characters or 63 emoji.
89
+ * `maxLength` cannot express that, and using it applied the number as a character count.
90
+ *
91
+ * Only MySQL sets this. Postgres `text` has no limit and its `varchar(n)` counts characters.
92
+ */
93
+ maxBytes?: number;
84
94
  /**
85
95
  * Array depth for a column declared with `.array()`, absent when the column is a scalar.
86
96
  *
@@ -344,6 +354,42 @@ declare class SchemaAnalyzer {
344
354
  * promise a precision that cannot survive the round trip.
345
355
  */
346
356
  private static readonly INT_RANGES;
357
+ /**
358
+ * The numeric column classes that are not exact, and the magnitude each one can really hold.
359
+ *
360
+ * Only drizzle v1 states this outright, as a `float` or `double` semantic on `dataType`. On
361
+ * 0.4x the same columns reach the analyzer by class name, `INT_RANGES` was the only range table
362
+ * on that path, and so nothing said anything about them at all: not the range, and not that
363
+ * they are inexact. The parity gate measured seven of the ten classes below and reported DRZL
364
+ * differing from the first-party validator for the same major on all seven. The three
365
+ * SingleStore classes are in no fixture either pass carries and are covered by unit tests alone.
366
+ *
367
+ * Differing is what the gate reports, not what it forbids. Most of its waivers have DRZL
368
+ * accepting something official refuses and the run counts them; an earlier version of this
369
+ * sentence said the gate exists to forbid being looser, which the same commit's own success
370
+ * banner denies.
371
+ *
372
+ * `null` is a value in this table and is not the same as a class it does not name. It says the
373
+ * column is inexact and that no finite magnitude bound is truthful for it, which is the case for
374
+ * every 8 byte float: float8 is the JavaScript number's own format, so Postgres accepts every
375
+ * finite JS number into one, measured to `Number.MAX_VALUE`. Read with an own-property test for
376
+ * that reason, and because a plain object answers to `constructor` and `toString`.
377
+ *
378
+ * `integer: false` travels with every entry, bound or not, and what it decides depends on which.
379
+ * `isIntegerColumn` falls back to "declares both bounds" when the flag is absent, so on a bounded
380
+ * entry the flag is the only thing stopping the emitted schema calling `.int()` and refusing 1.5.
381
+ * On an unbounded one it decides nothing, measured both ways against the real function in
382
+ * `@drzl/validation-core`'s integer-column.spec.ts. It is stated there because it is true of the
383
+ * column, not because it guards anything. That spec is where the measurement moved when the
384
+ * analyzer's copy of it turned out to be a closed loop; this sentence went on naming
385
+ * floats-and-tuples-0.4x.spec.ts, which asserts the flag is present and nothing about what it
386
+ * decides.
387
+ *
388
+ * The widths are the type's, not the name's: MySQL and SingleStore `real` is a synonym for
389
+ * `double` unless REAL_AS_FLOAT is set, and SQLite `real` is an 8 byte IEEE float. Both are
390
+ * `number double` on drizzle v1, which is where these pairings come from.
391
+ */
392
+ private static readonly INEXACT_RANGES;
347
393
  /**
348
394
  * Constraints the column definition already carries, which the analysis used to throw away.
349
395
  *
package/dist/index.js CHANGED
@@ -17,11 +17,16 @@ var MYSQL_TEXT_CAPS = {
17
17
  mediumblob: 16777215,
18
18
  longblob: 4294967295
19
19
  };
20
- var V1_FLOAT_BOUNDS = {
21
- float: ["-8388608", "8388607"],
22
- // real / float4, 2^23
23
- double: ["-140737488355328", "140737488355327"]
24
- // double precision / float8, 2^47
20
+ var FLOAT32_MAX = "340282346638528859811704183484516925440";
21
+ var PG_FLOAT4_INPUT_MAX = "340282356779733661637539395458142568448";
22
+ var PG_FLOAT4_RANGE = [`-${PG_FLOAT4_INPUT_MAX}`, PG_FLOAT4_INPUT_MAX];
23
+ var MYSQL_FLOAT_RANGE = [`-${FLOAT32_MAX}`, FLOAT32_MAX];
24
+ var JS_SAFE_INTEGER_BOUNDS = ["-9007199254740991", "9007199254740991"];
25
+ var TUPLE_CLASS_SHAPES = {
26
+ // `line()` is the trap here: its `drizzle:entityKind` is `PgLine` while its constructor is
27
+ // `PgLineTuple`, and this path matches on the constructor.
28
+ PgPointTuple: { kind: "tuple", length: 2 },
29
+ PgLineTuple: { kind: "tuple", length: 3 }
25
30
  };
26
31
  function describeV1Column(column) {
27
32
  const codec = column?.codec;
@@ -71,7 +76,8 @@ function describeV1Column(column) {
71
76
  break;
72
77
  case "float":
73
78
  case "double": {
74
- [out.min, out.max] = V1_FLOAT_BOUNDS[semantic];
79
+ if (semantic === "float")
80
+ [out.min, out.max] = codec === "float4" ? PG_FLOAT4_RANGE : MYSQL_FLOAT_RANGE;
75
81
  out.integer = false;
76
82
  out.tsType = "number";
77
83
  out.dbType = semantic === "float" ? "REAL" : "DOUBLE";
@@ -160,13 +166,13 @@ function describeV1Column(column) {
160
166
  out.tsType = "number";
161
167
  out.dbType = "NUMERIC";
162
168
  out.integer = false;
163
- [out.min, out.max] = ["-9007199254740991", "9007199254740991"];
169
+ [out.min, out.max] = JS_SAFE_INTEGER_BOUNDS;
164
170
  } else if (js === "string") {
165
171
  out.tsType = "string";
166
172
  out.dbType = codec === "varchar" ? "VARCHAR" : codec === "char" ? "CHAR" : "TEXT";
167
173
  const kind = String(column?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
168
174
  const cap = codec && kind.startsWith("MySql") ? MYSQL_TEXT_CAPS[codec] : void 0;
169
- if (cap) out.maxLength = cap;
175
+ if (cap) out.maxBytes = cap;
170
176
  } else {
171
177
  return null;
172
178
  }
@@ -467,6 +473,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
467
473
  [out.min, out.max] = range;
468
474
  out.integer = true;
469
475
  }
476
+ if (Object.prototype.hasOwnProperty.call(_SchemaAnalyzer.INEXACT_RANGES, ctor)) {
477
+ const inexact = _SchemaAnalyzer.INEXACT_RANGES[ctor];
478
+ if (inexact) [out.min, out.max] = inexact;
479
+ out.integer = false;
480
+ }
470
481
  if (/^(Pg)?UUID$/i.test(ctor) || /Uuid$/i.test(ctor)) out.format = "uuid";
471
482
  return out;
472
483
  }
@@ -538,9 +549,25 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
538
549
  return { tsType: "Date", dbType: "TIMESTAMP" };
539
550
  case "PgNumeric":
540
551
  return { tsType: "string", dbType: "NUMERIC" };
541
- case "PgFloat":
542
552
  case "PgDoublePrecision":
543
553
  return { tsType: "number", dbType: "DOUBLE" };
554
+ // `real()` builds a `PgReal`, which matched no arm and fell through to the coarse
555
+ // `/Numeric|Float|Double|Real/i` below, so a real column was labelled NUMERIC while v1
556
+ // called it REAL. The arm above used to name `PgFloat` alongside `PgDoublePrecision`, and
557
+ // no such class exists in pg-core on either major: `float` is MySQL's spelling and Gel's
558
+ // is `GelReal`, both of which are matched elsewhere. Enumerated from the module's own
559
+ // exports on 0.45.2 and on 1.0.0-rc.4, which name only PgReal and PgDoublePrecision.
560
+ case "PgReal":
561
+ return { tsType: "number", dbType: "REAL" };
562
+ // 0.4x names a point and a line by their mode. `point()` is a `PgPointTuple` and `line()` a
563
+ // `PgLineTuple`, whose entity kind is `PgLine` while its constructor is not, and both used
564
+ // to fall through to `/Point|Line/i` and come back `string`. The driver hands back [x, y]
565
+ // and [a, b, c], so a select schema built on 0.4x refused every row, and an insert schema
566
+ // took the one string form `mapToDriverValue` turns into something Postgres rejects.
567
+ case "PgPointTuple":
568
+ return { tsType: "[number, number]", dbType: "POINT" };
569
+ case "PgLineTuple":
570
+ return { tsType: "[number, number, number]", dbType: "LINE" };
544
571
  case "PgJson":
545
572
  case "PgJsonb":
546
573
  return { tsType: "any", dbType: ctor === "PgJsonb" ? "JSONB" : "JSON" };
@@ -660,16 +687,19 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
660
687
  const v1 = describeV1Column(col);
661
688
  const constraints = this.columnConstraints(col);
662
689
  if (v1?.shape) delete constraints.maxLength;
663
- const jsonShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : void 0;
664
- const shape = (v1?.shape ?? jsonShape)?.kind;
690
+ const sqlKind = String(outerCol?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
691
+ const sqlType = sqlKind.startsWith("MySql") && typeof col?.getSQLType === "function" ? String(col.getSQLType()).toLowerCase() : void 0;
692
+ const byteCap = sqlType ? MYSQL_TEXT_CAPS[sqlType] : void 0;
693
+ const fallbackShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : TUPLE_CLASS_SHAPES[String(col?.constructor?.name ?? "")];
694
+ const shape = (v1?.shape ?? fallbackShape)?.kind;
665
695
  const finalTs = v1?.tsType ?? tsType;
666
696
  const wide = (finalTs === "unknown" || finalTs === "any") && (!shape || shape === "custom");
667
697
  if (wide) {
668
- const sqlType = typeof col?.getSQLType === "function" ? col.getSQLType() : void 0;
698
+ const sqlType2 = typeof col?.getSQLType === "function" ? col.getSQLType() : void 0;
669
699
  issues.push({
670
700
  code: "DRZL_ANL_UNKNOWN_COLUMN",
671
701
  level: "warn",
672
- message: `Column "${colName}" on table "${tsName}" has no known type${sqlType ? ` (SQL type ${sqlType})` : ""}, so its validator will accept any value.`,
702
+ message: `Column "${colName}" on table "${tsName}" has no known type${sqlType2 ? ` (SQL type ${sqlType2})` : ""}, so its validator will accept any value.`,
673
703
  hint: shape === "custom" ? "A customType has no runtime shape to read. Declare it with .$type<T>() and turn on typedColumns to give the validator the type." : "Open an issue naming the column type so it can be modelled, or declare it with .$type<T>() and turn on typedColumns."
674
704
  });
675
705
  }
@@ -690,7 +720,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
690
720
  // that spread has nothing to say and this is the only source.
691
721
  ...arrayDims ? { arrayDimensions: arrayDims } : {},
692
722
  // Only where v1 did not already describe the value, so a shaped column keeps its shape.
693
- ...jsonShape && !v1?.shape ? { shape: jsonShape } : {}
723
+ ...fallbackShape && !v1?.shape ? { shape: fallbackShape } : {},
724
+ ...byteCap && v1?.maxBytes === void 0 ? { maxBytes: byteCap } : {}
694
725
  });
695
726
  }
696
727
  const name = this.getSymbol(tbl, "drizzle:Name") || tsName;
@@ -730,6 +761,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
730
761
  const cfg = entry?.config ?? entry ?? {};
731
762
  const cols = (cfg.columns ?? []).map((c) => toTs(c?.name)).filter(Boolean);
732
763
  if (!cols.length) continue;
764
+ const entityKind = String(entry?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
765
+ if (entityKind.endsWith("UniqueConstraintBuilder")) {
766
+ unique.push({ columns: cols, name: entry?.name });
767
+ continue;
768
+ }
733
769
  if (cfg.unique === void 0) {
734
770
  pkCols.splice(0, pkCols.length, ...cols);
735
771
  continue;
@@ -962,6 +998,60 @@ _SchemaAnalyzer.INT_RANGES = {
962
998
  MySqlBigInt64: ["-9223372036854775808", "9223372036854775807"],
963
999
  SingleStoreBigInt64: ["-9223372036854775808", "9223372036854775807"]
964
1000
  };
1001
+ /**
1002
+ * The numeric column classes that are not exact, and the magnitude each one can really hold.
1003
+ *
1004
+ * Only drizzle v1 states this outright, as a `float` or `double` semantic on `dataType`. On
1005
+ * 0.4x the same columns reach the analyzer by class name, `INT_RANGES` was the only range table
1006
+ * on that path, and so nothing said anything about them at all: not the range, and not that
1007
+ * they are inexact. The parity gate measured seven of the ten classes below and reported DRZL
1008
+ * differing from the first-party validator for the same major on all seven. The three
1009
+ * SingleStore classes are in no fixture either pass carries and are covered by unit tests alone.
1010
+ *
1011
+ * Differing is what the gate reports, not what it forbids. Most of its waivers have DRZL
1012
+ * accepting something official refuses and the run counts them; an earlier version of this
1013
+ * sentence said the gate exists to forbid being looser, which the same commit's own success
1014
+ * banner denies.
1015
+ *
1016
+ * `null` is a value in this table and is not the same as a class it does not name. It says the
1017
+ * column is inexact and that no finite magnitude bound is truthful for it, which is the case for
1018
+ * every 8 byte float: float8 is the JavaScript number's own format, so Postgres accepts every
1019
+ * finite JS number into one, measured to `Number.MAX_VALUE`. Read with an own-property test for
1020
+ * that reason, and because a plain object answers to `constructor` and `toString`.
1021
+ *
1022
+ * `integer: false` travels with every entry, bound or not, and what it decides depends on which.
1023
+ * `isIntegerColumn` falls back to "declares both bounds" when the flag is absent, so on a bounded
1024
+ * entry the flag is the only thing stopping the emitted schema calling `.int()` and refusing 1.5.
1025
+ * On an unbounded one it decides nothing, measured both ways against the real function in
1026
+ * `@drzl/validation-core`'s integer-column.spec.ts. It is stated there because it is true of the
1027
+ * column, not because it guards anything. That spec is where the measurement moved when the
1028
+ * analyzer's copy of it turned out to be a closed loop; this sentence went on naming
1029
+ * floats-and-tuples-0.4x.spec.ts, which asserts the flag is present and nothing about what it
1030
+ * decides.
1031
+ *
1032
+ * The widths are the type's, not the name's: MySQL and SingleStore `real` is a synonym for
1033
+ * `double` unless REAL_AS_FLOAT is set, and SQLite `real` is an 8 byte IEEE float. Both are
1034
+ * `number double` on drizzle v1, which is where these pairings come from.
1035
+ */
1036
+ _SchemaAnalyzer.INEXACT_RANGES = {
1037
+ // 4 byte floats, the one width a database refuses a magnitude for, and the two that have one
1038
+ // refuse at different values. SingleStore is MySQL wire-compatible and unmeasured here, so it
1039
+ // takes MySQL's rather than the wider of the two.
1040
+ PgReal: PG_FLOAT4_RANGE,
1041
+ MySqlFloat: MYSQL_FLOAT_RANGE,
1042
+ SingleStoreFloat: MYSQL_FLOAT_RANGE,
1043
+ // 8 byte floats, which hold every finite JS number
1044
+ PgDoublePrecision: null,
1045
+ MySqlDouble: null,
1046
+ MySqlReal: null,
1047
+ SQLiteReal: null,
1048
+ SingleStoreDouble: null,
1049
+ SingleStoreReal: null,
1050
+ // `numeric({ mode: 'number' })`, which v1 reaches through the bare-number arm of
1051
+ // `describeV1Column`. This one is about what a JS number can carry rather than about the
1052
+ // column, which Postgres caps far lower: it refuses 2147483648 into a `numeric(10,2)`.
1053
+ PgNumericNumber: JS_SAFE_INTEGER_BOUNDS
1054
+ };
965
1055
  var SchemaAnalyzer = _SchemaAnalyzer;
966
1056
  var index_default = SchemaAnalyzer;
967
1057
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drzl/analyzer",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -43,7 +43,7 @@
43
43
  "url": "https://github.com/sponsors/omar-dulaimi"
44
44
  },
45
45
  "scripts": {
46
- "build": "tsup src/index.ts --dts --format esm,cjs",
46
+ "build": "tsup src/index.ts --dts --format esm,cjs --clean",
47
47
  "lint": "eslint . --ext .ts",
48
48
  "test": "vitest run --testTimeout=20000"
49
49
  }