@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 +12 -1
- package/dist/index.cjs +104 -14
- package/dist/index.d.cts +46 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +104 -14
- package/package.json +2 -2
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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] =
|
|
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.
|
|
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
|
|
704
|
-
const
|
|
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
|
|
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${
|
|
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
|
-
...
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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] =
|
|
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.
|
|
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
|
|
664
|
-
const
|
|
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
|
|
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${
|
|
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
|
-
...
|
|
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.
|
|
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
|
}
|