@drzl/analyzer 1.14.0 → 1.16.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
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  SchemaAnalyzer: () => SchemaAnalyzer,
34
34
  default: () => index_default,
35
35
  describeV1Column: () => describeV1Column,
36
+ isDrizzleView: () => isDrizzleView,
36
37
  isReadOnlyRelation: () => isReadOnlyRelation,
37
38
  isRelationsV2: () => isRelationsV2,
38
39
  readRelationsV2: () => readRelationsV2
@@ -57,12 +58,18 @@ var MYSQL_TEXT_CAPS = {
57
58
  mediumblob: 16777215,
58
59
  longblob: 4294967295
59
60
  };
60
- var V1_FLOAT_BOUNDS = {
61
- float: ["-8388608", "8388607"],
62
- // real / float4, 2^23
63
- double: ["-140737488355328", "140737488355327"]
64
- // double precision / float8, 2^47
61
+ var FLOAT32_MAX = "340282346638528859811704183484516925440";
62
+ var PG_FLOAT4_INPUT_MAX = "340282356779733661637539395458142568448";
63
+ var PG_FLOAT4_RANGE = [`-${PG_FLOAT4_INPUT_MAX}`, PG_FLOAT4_INPUT_MAX];
64
+ var MYSQL_FLOAT_RANGE = [`-${FLOAT32_MAX}`, FLOAT32_MAX];
65
+ var JS_SAFE_INTEGER_BOUNDS = ["-9007199254740991", "9007199254740991"];
66
+ var TUPLE_CLASS_SHAPES = {
67
+ // `line()` is the trap here: its `drizzle:entityKind` is `PgLine` while its constructor is
68
+ // `PgLineTuple`, and this path matches on the constructor.
69
+ PgPointTuple: { kind: "tuple", length: 2 },
70
+ PgLineTuple: { kind: "tuple", length: 3 }
65
71
  };
72
+ var V1_ONLY_ENTITY_KINDS = /^(?:MsSql|Cockroach)/;
66
73
  function describeV1Column(column) {
67
74
  const codec = column?.codec;
68
75
  const dataType = column?.dataType;
@@ -75,8 +82,9 @@ function describeV1Column(column) {
75
82
  shape: { kind: "custom", sqlType: typeof sqlType === "string" ? sqlType : void 0 }
76
83
  };
77
84
  }
85
+ const entityKind = String(column?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
78
86
  const [js, semantic = ""] = dataType.split(" ");
79
- if (typeof codec !== "string" && !semantic) return null;
87
+ if (typeof codec !== "string" && !semantic && !V1_ONLY_ENTITY_KINDS.test(entityKind)) return null;
80
88
  const out = {};
81
89
  switch (semantic) {
82
90
  case "int8":
@@ -111,7 +119,8 @@ function describeV1Column(column) {
111
119
  break;
112
120
  case "float":
113
121
  case "double": {
114
- [out.min, out.max] = V1_FLOAT_BOUNDS[semantic];
122
+ if (semantic === "float")
123
+ [out.min, out.max] = codec === "float4" || entityKind.startsWith("Cockroach") ? PG_FLOAT4_RANGE : MYSQL_FLOAT_RANGE;
115
124
  out.integer = false;
116
125
  out.tsType = "number";
117
126
  out.dbType = semantic === "float" ? "REAL" : "DOUBLE";
@@ -200,7 +209,7 @@ function describeV1Column(column) {
200
209
  out.tsType = "number";
201
210
  out.dbType = "NUMERIC";
202
211
  out.integer = false;
203
- [out.min, out.max] = ["-9007199254740991", "9007199254740991"];
212
+ [out.min, out.max] = JS_SAFE_INTEGER_BOUNDS;
204
213
  } else if (js === "string") {
205
214
  out.tsType = "string";
206
215
  out.dbType = codec === "varchar" ? "VARCHAR" : codec === "char" ? "CHAR" : "TEXT";
@@ -228,15 +237,33 @@ function declaredLength(column) {
228
237
  const n = column?.length ?? column?.config?.length ?? column?.config?.dimensions;
229
238
  return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : void 0;
230
239
  }
231
- function getSymbolOf(target, key) {
232
- if (!target) return void 0;
240
+ var VIEW_CONFIG_FIELDS = {
241
+ "drizzle:Columns": "selectedFields",
242
+ "drizzle:Name": "name",
243
+ "drizzle:Schema": "schema"
244
+ };
245
+ function ownSymbolOf(target, key) {
233
246
  try {
234
247
  for (const sym of Object.getOwnPropertySymbols(target)) {
235
248
  if (sym.description === key) return target[sym];
236
249
  }
237
250
  } catch {
238
251
  }
239
- return target[Symbol.for(key)];
252
+ return void 0;
253
+ }
254
+ function getSymbolOf(target, key) {
255
+ if (!target) return void 0;
256
+ const own = ownSymbolOf(target, key);
257
+ if (own !== void 0) return own;
258
+ const direct = target[Symbol.for(key)];
259
+ if (direct !== void 0) return direct;
260
+ const field = VIEW_CONFIG_FIELDS[key];
261
+ if (!field) return void 0;
262
+ const cfg = ownSymbolOf(target, "drizzle:ViewBaseConfig");
263
+ return cfg ? cfg[field] : void 0;
264
+ }
265
+ function isDrizzleView(val) {
266
+ return !!val && typeof val === "object" && !!getSymbolOf(val, "drizzle:ViewBaseConfig");
240
267
  }
241
268
  function isReadOnlyRelation(val) {
242
269
  if (!val || typeof val !== "object") return false;
@@ -280,17 +307,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
280
307
  this.schemaPath = schemaPath;
281
308
  }
282
309
  getSymbol(table, key) {
283
- if (!table) return void 0;
284
- try {
285
- const syms = Object.getOwnPropertySymbols(table);
286
- for (const s of syms) {
287
- if (s.description === key) {
288
- return table[s];
289
- }
290
- }
291
- } catch {
292
- }
293
- return table[Symbol.for(key)];
310
+ return getSymbolOf(table, key);
294
311
  }
295
312
  /**
296
313
  * Drizzle keys the Columns object by TypeScript property name, but every other piece of
@@ -507,6 +524,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
507
524
  [out.min, out.max] = range;
508
525
  out.integer = true;
509
526
  }
527
+ if (Object.prototype.hasOwnProperty.call(_SchemaAnalyzer.INEXACT_RANGES, ctor)) {
528
+ const inexact = _SchemaAnalyzer.INEXACT_RANGES[ctor];
529
+ if (inexact) [out.min, out.max] = inexact;
530
+ out.integer = false;
531
+ }
510
532
  if (/^(Pg)?UUID$/i.test(ctor) || /Uuid$/i.test(ctor)) out.format = "uuid";
511
533
  return out;
512
534
  }
@@ -578,9 +600,25 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
578
600
  return { tsType: "Date", dbType: "TIMESTAMP" };
579
601
  case "PgNumeric":
580
602
  return { tsType: "string", dbType: "NUMERIC" };
581
- case "PgFloat":
582
603
  case "PgDoublePrecision":
583
604
  return { tsType: "number", dbType: "DOUBLE" };
605
+ // `real()` builds a `PgReal`, which matched no arm and fell through to the coarse
606
+ // `/Numeric|Float|Double|Real/i` below, so a real column was labelled NUMERIC while v1
607
+ // called it REAL. The arm above used to name `PgFloat` alongside `PgDoublePrecision`, and
608
+ // no such class exists in pg-core on either major: `float` is MySQL's spelling and Gel's
609
+ // is `GelReal`, both of which are matched elsewhere. Enumerated from the module's own
610
+ // exports on 0.45.2 and on 1.0.0-rc.4, which name only PgReal and PgDoublePrecision.
611
+ case "PgReal":
612
+ return { tsType: "number", dbType: "REAL" };
613
+ // 0.4x names a point and a line by their mode. `point()` is a `PgPointTuple` and `line()` a
614
+ // `PgLineTuple`, whose entity kind is `PgLine` while its constructor is not, and both used
615
+ // to fall through to `/Point|Line/i` and come back `string`. The driver hands back [x, y]
616
+ // and [a, b, c], so a select schema built on 0.4x refused every row, and an insert schema
617
+ // took the one string form `mapToDriverValue` turns into something Postgres rejects.
618
+ case "PgPointTuple":
619
+ return { tsType: "[number, number]", dbType: "POINT" };
620
+ case "PgLineTuple":
621
+ return { tsType: "[number, number, number]", dbType: "LINE" };
584
622
  case "PgJson":
585
623
  case "PgJsonb":
586
624
  return { tsType: "any", dbType: ctor === "PgJsonb" ? "JSONB" : "JSON" };
@@ -654,11 +692,10 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
654
692
  if (/Json/i.test(ctor)) return { tsType: "any", dbType: "JSON" };
655
693
  if (/Text/i.test(ctor)) return { tsType: "string", dbType: "TEXT" };
656
694
  if (/Bytes/i.test(ctor)) return { tsType: "Uint8Array", dbType: "BLOB" };
695
+ if (/Bool/i.test(ctor)) return { tsType: "boolean", dbType: "BOOLEAN" };
657
696
  if (/TimestampTz/i.test(ctor)) return { tsType: "Date", dbType: "TIMESTAMPTZ" };
658
- if (/Timestamp/i.test(ctor)) return { tsType: "string", dbType: "TIMESTAMP" };
659
- if (/LocalDateString|LocalTime/i.test(ctor)) return { tsType: "string", dbType: "TEXT" };
660
- if (/DateDuration|RelDuration|Duration/i.test(ctor))
661
- return { tsType: "string", dbType: "TEXT" };
697
+ if (/Timestamp|LocalDateString|LocalTime|DateDuration|RelDuration|Duration/i.test(ctor))
698
+ return { tsType: "unknown", dbType: "UNKNOWN" };
662
699
  }
663
700
  return { tsType: "unknown", dbType: "UNKNOWN" };
664
701
  }
@@ -703,8 +740,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
703
740
  const sqlKind = String(outerCol?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
704
741
  const sqlType = sqlKind.startsWith("MySql") && typeof col?.getSQLType === "function" ? String(col.getSQLType()).toLowerCase() : void 0;
705
742
  const byteCap = sqlType ? MYSQL_TEXT_CAPS[sqlType] : void 0;
706
- const jsonShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : void 0;
707
- const shape = (v1?.shape ?? jsonShape)?.kind;
743
+ const fallbackShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : TUPLE_CLASS_SHAPES[String(col?.constructor?.name ?? "")];
744
+ const shape = (v1?.shape ?? fallbackShape)?.kind;
708
745
  const finalTs = v1?.tsType ?? tsType;
709
746
  const wide = (finalTs === "unknown" || finalTs === "any") && (!shape || shape === "custom");
710
747
  if (wide) {
@@ -733,7 +770,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
733
770
  // that spread has nothing to say and this is the only source.
734
771
  ...arrayDims ? { arrayDimensions: arrayDims } : {},
735
772
  // Only where v1 did not already describe the value, so a shaped column keeps its shape.
736
- ...jsonShape && !v1?.shape ? { shape: jsonShape } : {},
773
+ ...fallbackShape && !v1?.shape ? { shape: fallbackShape } : {},
737
774
  ...byteCap && v1?.maxBytes === void 0 ? { maxBytes: byteCap } : {}
738
775
  });
739
776
  }
@@ -853,12 +890,14 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
853
890
  const relations = [];
854
891
  const enums = [];
855
892
  const columnEnums = [];
893
+ const viewTables = [];
856
894
  for (const [name, val] of Object.entries(exportsObj)) {
857
895
  try {
858
896
  const cols = this.getSymbol(val, "drizzle:Columns");
859
897
  if (cols && typeof cols === "object") {
860
898
  const table = this.analyzeTable(name, val, issues);
861
899
  tables.push(table);
900
+ if (isDrizzleView(val)) viewTables.push(table);
862
901
  for (const col of table.columns) {
863
902
  const enumVals = cols[col.name]?.enumValues;
864
903
  if (enumVals && enumVals.length) {
@@ -909,7 +948,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
909
948
  let dialect = "unknown";
910
949
  const marks = /* @__PURE__ */ new Set();
911
950
  for (const [_, val] of Object.entries(exportsObj)) {
912
- const cols = val?.[/* @__PURE__ */ Symbol.for("drizzle:Columns")];
951
+ const cols = this.getSymbol(val, "drizzle:Columns");
913
952
  if (!cols) continue;
914
953
  for (const c of Object.values(cols)) {
915
954
  const kind = c?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")];
@@ -926,6 +965,9 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
926
965
  else if (/MySql|Mysql/i.test(names)) dialect = "mysql";
927
966
  else if (/Pg|Postgres/i.test(names)) dialect = "postgres";
928
967
  else if (/Gel/i.test(names)) dialect = "gel";
968
+ if (dialect === "sqlite") {
969
+ for (const view of viewTables) view.readOnly = true;
970
+ }
929
971
  if (dialect === "unknown" && tables.length) {
930
972
  issues.push({
931
973
  code: "DRZL_ANL_DIALECT",
@@ -1011,12 +1053,67 @@ _SchemaAnalyzer.INT_RANGES = {
1011
1053
  MySqlBigInt64: ["-9223372036854775808", "9223372036854775807"],
1012
1054
  SingleStoreBigInt64: ["-9223372036854775808", "9223372036854775807"]
1013
1055
  };
1056
+ /**
1057
+ * The numeric column classes that are not exact, and the magnitude each one can really hold.
1058
+ *
1059
+ * Only drizzle v1 states this outright, as a `float` or `double` semantic on `dataType`. On
1060
+ * 0.4x the same columns reach the analyzer by class name, `INT_RANGES` was the only range table
1061
+ * on that path, and so nothing said anything about them at all: not the range, and not that
1062
+ * they are inexact. The parity gate measured seven of the ten classes below and reported DRZL
1063
+ * differing from the first-party validator for the same major on all seven. The three
1064
+ * SingleStore classes are in no fixture either pass carries and are covered by unit tests alone.
1065
+ *
1066
+ * Differing is what the gate reports, not what it forbids. Most of its waivers have DRZL
1067
+ * accepting something official refuses and the run counts them; an earlier version of this
1068
+ * sentence said the gate exists to forbid being looser, which the same commit's own success
1069
+ * banner denies.
1070
+ *
1071
+ * `null` is a value in this table and is not the same as a class it does not name. It says the
1072
+ * column is inexact and that no finite magnitude bound is truthful for it, which is the case for
1073
+ * every 8 byte float: float8 is the JavaScript number's own format, so Postgres accepts every
1074
+ * finite JS number into one, measured to `Number.MAX_VALUE`. Read with an own-property test for
1075
+ * that reason, and because a plain object answers to `constructor` and `toString`.
1076
+ *
1077
+ * `integer: false` travels with every entry, bound or not, and what it decides depends on which.
1078
+ * `isIntegerColumn` falls back to "declares both bounds" when the flag is absent, so on a bounded
1079
+ * entry the flag is the only thing stopping the emitted schema calling `.int()` and refusing 1.5.
1080
+ * On an unbounded one it decides nothing, measured both ways against the real function in
1081
+ * `@drzl/validation-core`'s integer-column.spec.ts. It is stated there because it is true of the
1082
+ * column, not because it guards anything. That spec is where the measurement moved when the
1083
+ * analyzer's copy of it turned out to be a closed loop; this sentence went on naming
1084
+ * floats-and-tuples-0.4x.spec.ts, which asserts the flag is present and nothing about what it
1085
+ * decides.
1086
+ *
1087
+ * The widths are the type's, not the name's: MySQL and SingleStore `real` is a synonym for
1088
+ * `double` unless REAL_AS_FLOAT is set, and SQLite `real` is an 8 byte IEEE float. Both are
1089
+ * `number double` on drizzle v1, which is where these pairings come from.
1090
+ */
1091
+ _SchemaAnalyzer.INEXACT_RANGES = {
1092
+ // 4 byte floats, the one width a database refuses a magnitude for, and the two that have one
1093
+ // refuse at different values. SingleStore is MySQL wire-compatible and unmeasured here, so it
1094
+ // takes MySQL's rather than the wider of the two.
1095
+ PgReal: PG_FLOAT4_RANGE,
1096
+ MySqlFloat: MYSQL_FLOAT_RANGE,
1097
+ SingleStoreFloat: MYSQL_FLOAT_RANGE,
1098
+ // 8 byte floats, which hold every finite JS number
1099
+ PgDoublePrecision: null,
1100
+ MySqlDouble: null,
1101
+ MySqlReal: null,
1102
+ SQLiteReal: null,
1103
+ SingleStoreDouble: null,
1104
+ SingleStoreReal: null,
1105
+ // `numeric({ mode: 'number' })`, which v1 reaches through the bare-number arm of
1106
+ // `describeV1Column`. This one is about what a JS number can carry rather than about the
1107
+ // column, which Postgres caps far lower: it refuses 2147483648 into a `numeric(10,2)`.
1108
+ PgNumericNumber: JS_SAFE_INTEGER_BOUNDS
1109
+ };
1014
1110
  var SchemaAnalyzer = _SchemaAnalyzer;
1015
1111
  var index_default = SchemaAnalyzer;
1016
1112
  // Annotate the CommonJS export names for ESM import in node:
1017
1113
  0 && (module.exports = {
1018
1114
  SchemaAnalyzer,
1019
1115
  describeV1Column,
1116
+ isDrizzleView,
1020
1117
  isReadOnlyRelation,
1021
1118
  isRelationsV2,
1022
1119
  readRelationsV2
package/dist/index.d.cts CHANGED
@@ -231,6 +231,17 @@ interface AnalyzeOptions {
231
231
  * path below untouched.
232
232
  */
233
233
  declare function describeV1Column(column: any): Partial<Column> | null;
234
+ /**
235
+ * Whether an export is a Drizzle view, of any dialect and on either major.
236
+ *
237
+ * Asked of `drizzle:ViewBaseConfig` rather than of `drizzle:IsDrizzleView`, which reads like the
238
+ * obvious question and is not there to be asked: the marker was introduced in 0.39.0, and a view
239
+ * built on 0.29.5, 0.33.0 or 0.36.4 answers undefined to it. The config is an own symbol on all
240
+ * eleven releases probed and on every view form measured, on both majors: the query-builder and
241
+ * explicit-column-list forms of pg view, pg materialized view, mysql view and sqlite view,
242
+ * `.existing()`, and the schema-qualified pg view and materialized view.
243
+ */
244
+ declare function isDrizzleView(val: any): boolean;
234
245
  /**
235
246
  * Whether an export is a Relations v2 definition, from `defineRelations(schema, (r) => ...)`.
236
247
  *
@@ -354,6 +365,42 @@ declare class SchemaAnalyzer {
354
365
  * promise a precision that cannot survive the round trip.
355
366
  */
356
367
  private static readonly INT_RANGES;
368
+ /**
369
+ * The numeric column classes that are not exact, and the magnitude each one can really hold.
370
+ *
371
+ * Only drizzle v1 states this outright, as a `float` or `double` semantic on `dataType`. On
372
+ * 0.4x the same columns reach the analyzer by class name, `INT_RANGES` was the only range table
373
+ * on that path, and so nothing said anything about them at all: not the range, and not that
374
+ * they are inexact. The parity gate measured seven of the ten classes below and reported DRZL
375
+ * differing from the first-party validator for the same major on all seven. The three
376
+ * SingleStore classes are in no fixture either pass carries and are covered by unit tests alone.
377
+ *
378
+ * Differing is what the gate reports, not what it forbids. Most of its waivers have DRZL
379
+ * accepting something official refuses and the run counts them; an earlier version of this
380
+ * sentence said the gate exists to forbid being looser, which the same commit's own success
381
+ * banner denies.
382
+ *
383
+ * `null` is a value in this table and is not the same as a class it does not name. It says the
384
+ * column is inexact and that no finite magnitude bound is truthful for it, which is the case for
385
+ * every 8 byte float: float8 is the JavaScript number's own format, so Postgres accepts every
386
+ * finite JS number into one, measured to `Number.MAX_VALUE`. Read with an own-property test for
387
+ * that reason, and because a plain object answers to `constructor` and `toString`.
388
+ *
389
+ * `integer: false` travels with every entry, bound or not, and what it decides depends on which.
390
+ * `isIntegerColumn` falls back to "declares both bounds" when the flag is absent, so on a bounded
391
+ * entry the flag is the only thing stopping the emitted schema calling `.int()` and refusing 1.5.
392
+ * On an unbounded one it decides nothing, measured both ways against the real function in
393
+ * `@drzl/validation-core`'s integer-column.spec.ts. It is stated there because it is true of the
394
+ * column, not because it guards anything. That spec is where the measurement moved when the
395
+ * analyzer's copy of it turned out to be a closed loop; this sentence went on naming
396
+ * floats-and-tuples-0.4x.spec.ts, which asserts the flag is present and nothing about what it
397
+ * decides.
398
+ *
399
+ * The widths are the type's, not the name's: MySQL and SingleStore `real` is a synonym for
400
+ * `double` unless REAL_AS_FLOAT is set, and SQLite `real` is an 8 byte IEEE float. Both are
401
+ * `number double` on drizzle v1, which is where these pairings come from.
402
+ */
403
+ private static readonly INEXACT_RANGES;
357
404
  /**
358
405
  * Constraints the column definition already carries, which the analysis used to throw away.
359
406
  *
@@ -366,4 +413,4 @@ declare class SchemaAnalyzer {
366
413
  analyze(opts?: AnalyzeOptions): Promise<Analysis>;
367
414
  }
368
415
 
369
- 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, isReadOnlyRelation, isRelationsV2, readRelationsV2 };
416
+ 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 };
package/dist/index.d.ts CHANGED
@@ -231,6 +231,17 @@ interface AnalyzeOptions {
231
231
  * path below untouched.
232
232
  */
233
233
  declare function describeV1Column(column: any): Partial<Column> | null;
234
+ /**
235
+ * Whether an export is a Drizzle view, of any dialect and on either major.
236
+ *
237
+ * Asked of `drizzle:ViewBaseConfig` rather than of `drizzle:IsDrizzleView`, which reads like the
238
+ * obvious question and is not there to be asked: the marker was introduced in 0.39.0, and a view
239
+ * built on 0.29.5, 0.33.0 or 0.36.4 answers undefined to it. The config is an own symbol on all
240
+ * eleven releases probed and on every view form measured, on both majors: the query-builder and
241
+ * explicit-column-list forms of pg view, pg materialized view, mysql view and sqlite view,
242
+ * `.existing()`, and the schema-qualified pg view and materialized view.
243
+ */
244
+ declare function isDrizzleView(val: any): boolean;
234
245
  /**
235
246
  * Whether an export is a Relations v2 definition, from `defineRelations(schema, (r) => ...)`.
236
247
  *
@@ -354,6 +365,42 @@ declare class SchemaAnalyzer {
354
365
  * promise a precision that cannot survive the round trip.
355
366
  */
356
367
  private static readonly INT_RANGES;
368
+ /**
369
+ * The numeric column classes that are not exact, and the magnitude each one can really hold.
370
+ *
371
+ * Only drizzle v1 states this outright, as a `float` or `double` semantic on `dataType`. On
372
+ * 0.4x the same columns reach the analyzer by class name, `INT_RANGES` was the only range table
373
+ * on that path, and so nothing said anything about them at all: not the range, and not that
374
+ * they are inexact. The parity gate measured seven of the ten classes below and reported DRZL
375
+ * differing from the first-party validator for the same major on all seven. The three
376
+ * SingleStore classes are in no fixture either pass carries and are covered by unit tests alone.
377
+ *
378
+ * Differing is what the gate reports, not what it forbids. Most of its waivers have DRZL
379
+ * accepting something official refuses and the run counts them; an earlier version of this
380
+ * sentence said the gate exists to forbid being looser, which the same commit's own success
381
+ * banner denies.
382
+ *
383
+ * `null` is a value in this table and is not the same as a class it does not name. It says the
384
+ * column is inexact and that no finite magnitude bound is truthful for it, which is the case for
385
+ * every 8 byte float: float8 is the JavaScript number's own format, so Postgres accepts every
386
+ * finite JS number into one, measured to `Number.MAX_VALUE`. Read with an own-property test for
387
+ * that reason, and because a plain object answers to `constructor` and `toString`.
388
+ *
389
+ * `integer: false` travels with every entry, bound or not, and what it decides depends on which.
390
+ * `isIntegerColumn` falls back to "declares both bounds" when the flag is absent, so on a bounded
391
+ * entry the flag is the only thing stopping the emitted schema calling `.int()` and refusing 1.5.
392
+ * On an unbounded one it decides nothing, measured both ways against the real function in
393
+ * `@drzl/validation-core`'s integer-column.spec.ts. It is stated there because it is true of the
394
+ * column, not because it guards anything. That spec is where the measurement moved when the
395
+ * analyzer's copy of it turned out to be a closed loop; this sentence went on naming
396
+ * floats-and-tuples-0.4x.spec.ts, which asserts the flag is present and nothing about what it
397
+ * decides.
398
+ *
399
+ * The widths are the type's, not the name's: MySQL and SingleStore `real` is a synonym for
400
+ * `double` unless REAL_AS_FLOAT is set, and SQLite `real` is an 8 byte IEEE float. Both are
401
+ * `number double` on drizzle v1, which is where these pairings come from.
402
+ */
403
+ private static readonly INEXACT_RANGES;
357
404
  /**
358
405
  * Constraints the column definition already carries, which the analysis used to throw away.
359
406
  *
@@ -366,4 +413,4 @@ declare class SchemaAnalyzer {
366
413
  analyze(opts?: AnalyzeOptions): Promise<Analysis>;
367
414
  }
368
415
 
369
- 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, isReadOnlyRelation, isRelationsV2, readRelationsV2 };
416
+ 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 };
package/dist/index.js CHANGED
@@ -17,12 +17,18 @@ 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
  };
31
+ var V1_ONLY_ENTITY_KINDS = /^(?:MsSql|Cockroach)/;
26
32
  function describeV1Column(column) {
27
33
  const codec = column?.codec;
28
34
  const dataType = column?.dataType;
@@ -35,8 +41,9 @@ function describeV1Column(column) {
35
41
  shape: { kind: "custom", sqlType: typeof sqlType === "string" ? sqlType : void 0 }
36
42
  };
37
43
  }
44
+ const entityKind = String(column?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
38
45
  const [js, semantic = ""] = dataType.split(" ");
39
- if (typeof codec !== "string" && !semantic) return null;
46
+ if (typeof codec !== "string" && !semantic && !V1_ONLY_ENTITY_KINDS.test(entityKind)) return null;
40
47
  const out = {};
41
48
  switch (semantic) {
42
49
  case "int8":
@@ -71,7 +78,8 @@ function describeV1Column(column) {
71
78
  break;
72
79
  case "float":
73
80
  case "double": {
74
- [out.min, out.max] = V1_FLOAT_BOUNDS[semantic];
81
+ if (semantic === "float")
82
+ [out.min, out.max] = codec === "float4" || entityKind.startsWith("Cockroach") ? PG_FLOAT4_RANGE : MYSQL_FLOAT_RANGE;
75
83
  out.integer = false;
76
84
  out.tsType = "number";
77
85
  out.dbType = semantic === "float" ? "REAL" : "DOUBLE";
@@ -160,7 +168,7 @@ function describeV1Column(column) {
160
168
  out.tsType = "number";
161
169
  out.dbType = "NUMERIC";
162
170
  out.integer = false;
163
- [out.min, out.max] = ["-9007199254740991", "9007199254740991"];
171
+ [out.min, out.max] = JS_SAFE_INTEGER_BOUNDS;
164
172
  } else if (js === "string") {
165
173
  out.tsType = "string";
166
174
  out.dbType = codec === "varchar" ? "VARCHAR" : codec === "char" ? "CHAR" : "TEXT";
@@ -188,15 +196,33 @@ function declaredLength(column) {
188
196
  const n = column?.length ?? column?.config?.length ?? column?.config?.dimensions;
189
197
  return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : void 0;
190
198
  }
191
- function getSymbolOf(target, key) {
192
- if (!target) return void 0;
199
+ var VIEW_CONFIG_FIELDS = {
200
+ "drizzle:Columns": "selectedFields",
201
+ "drizzle:Name": "name",
202
+ "drizzle:Schema": "schema"
203
+ };
204
+ function ownSymbolOf(target, key) {
193
205
  try {
194
206
  for (const sym of Object.getOwnPropertySymbols(target)) {
195
207
  if (sym.description === key) return target[sym];
196
208
  }
197
209
  } catch {
198
210
  }
199
- return target[Symbol.for(key)];
211
+ return void 0;
212
+ }
213
+ function getSymbolOf(target, key) {
214
+ if (!target) return void 0;
215
+ const own = ownSymbolOf(target, key);
216
+ if (own !== void 0) return own;
217
+ const direct = target[Symbol.for(key)];
218
+ if (direct !== void 0) return direct;
219
+ const field = VIEW_CONFIG_FIELDS[key];
220
+ if (!field) return void 0;
221
+ const cfg = ownSymbolOf(target, "drizzle:ViewBaseConfig");
222
+ return cfg ? cfg[field] : void 0;
223
+ }
224
+ function isDrizzleView(val) {
225
+ return !!val && typeof val === "object" && !!getSymbolOf(val, "drizzle:ViewBaseConfig");
200
226
  }
201
227
  function isReadOnlyRelation(val) {
202
228
  if (!val || typeof val !== "object") return false;
@@ -240,17 +266,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
240
266
  this.schemaPath = schemaPath;
241
267
  }
242
268
  getSymbol(table, key) {
243
- if (!table) return void 0;
244
- try {
245
- const syms = Object.getOwnPropertySymbols(table);
246
- for (const s of syms) {
247
- if (s.description === key) {
248
- return table[s];
249
- }
250
- }
251
- } catch {
252
- }
253
- return table[Symbol.for(key)];
269
+ return getSymbolOf(table, key);
254
270
  }
255
271
  /**
256
272
  * Drizzle keys the Columns object by TypeScript property name, but every other piece of
@@ -467,6 +483,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
467
483
  [out.min, out.max] = range;
468
484
  out.integer = true;
469
485
  }
486
+ if (Object.prototype.hasOwnProperty.call(_SchemaAnalyzer.INEXACT_RANGES, ctor)) {
487
+ const inexact = _SchemaAnalyzer.INEXACT_RANGES[ctor];
488
+ if (inexact) [out.min, out.max] = inexact;
489
+ out.integer = false;
490
+ }
470
491
  if (/^(Pg)?UUID$/i.test(ctor) || /Uuid$/i.test(ctor)) out.format = "uuid";
471
492
  return out;
472
493
  }
@@ -538,9 +559,25 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
538
559
  return { tsType: "Date", dbType: "TIMESTAMP" };
539
560
  case "PgNumeric":
540
561
  return { tsType: "string", dbType: "NUMERIC" };
541
- case "PgFloat":
542
562
  case "PgDoublePrecision":
543
563
  return { tsType: "number", dbType: "DOUBLE" };
564
+ // `real()` builds a `PgReal`, which matched no arm and fell through to the coarse
565
+ // `/Numeric|Float|Double|Real/i` below, so a real column was labelled NUMERIC while v1
566
+ // called it REAL. The arm above used to name `PgFloat` alongside `PgDoublePrecision`, and
567
+ // no such class exists in pg-core on either major: `float` is MySQL's spelling and Gel's
568
+ // is `GelReal`, both of which are matched elsewhere. Enumerated from the module's own
569
+ // exports on 0.45.2 and on 1.0.0-rc.4, which name only PgReal and PgDoublePrecision.
570
+ case "PgReal":
571
+ return { tsType: "number", dbType: "REAL" };
572
+ // 0.4x names a point and a line by their mode. `point()` is a `PgPointTuple` and `line()` a
573
+ // `PgLineTuple`, whose entity kind is `PgLine` while its constructor is not, and both used
574
+ // to fall through to `/Point|Line/i` and come back `string`. The driver hands back [x, y]
575
+ // and [a, b, c], so a select schema built on 0.4x refused every row, and an insert schema
576
+ // took the one string form `mapToDriverValue` turns into something Postgres rejects.
577
+ case "PgPointTuple":
578
+ return { tsType: "[number, number]", dbType: "POINT" };
579
+ case "PgLineTuple":
580
+ return { tsType: "[number, number, number]", dbType: "LINE" };
544
581
  case "PgJson":
545
582
  case "PgJsonb":
546
583
  return { tsType: "any", dbType: ctor === "PgJsonb" ? "JSONB" : "JSON" };
@@ -614,11 +651,10 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
614
651
  if (/Json/i.test(ctor)) return { tsType: "any", dbType: "JSON" };
615
652
  if (/Text/i.test(ctor)) return { tsType: "string", dbType: "TEXT" };
616
653
  if (/Bytes/i.test(ctor)) return { tsType: "Uint8Array", dbType: "BLOB" };
654
+ if (/Bool/i.test(ctor)) return { tsType: "boolean", dbType: "BOOLEAN" };
617
655
  if (/TimestampTz/i.test(ctor)) return { tsType: "Date", dbType: "TIMESTAMPTZ" };
618
- if (/Timestamp/i.test(ctor)) return { tsType: "string", dbType: "TIMESTAMP" };
619
- if (/LocalDateString|LocalTime/i.test(ctor)) return { tsType: "string", dbType: "TEXT" };
620
- if (/DateDuration|RelDuration|Duration/i.test(ctor))
621
- return { tsType: "string", dbType: "TEXT" };
656
+ if (/Timestamp|LocalDateString|LocalTime|DateDuration|RelDuration|Duration/i.test(ctor))
657
+ return { tsType: "unknown", dbType: "UNKNOWN" };
622
658
  }
623
659
  return { tsType: "unknown", dbType: "UNKNOWN" };
624
660
  }
@@ -663,8 +699,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
663
699
  const sqlKind = String(outerCol?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
664
700
  const sqlType = sqlKind.startsWith("MySql") && typeof col?.getSQLType === "function" ? String(col.getSQLType()).toLowerCase() : void 0;
665
701
  const byteCap = sqlType ? MYSQL_TEXT_CAPS[sqlType] : void 0;
666
- const jsonShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : void 0;
667
- const shape = (v1?.shape ?? jsonShape)?.kind;
702
+ const fallbackShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : TUPLE_CLASS_SHAPES[String(col?.constructor?.name ?? "")];
703
+ const shape = (v1?.shape ?? fallbackShape)?.kind;
668
704
  const finalTs = v1?.tsType ?? tsType;
669
705
  const wide = (finalTs === "unknown" || finalTs === "any") && (!shape || shape === "custom");
670
706
  if (wide) {
@@ -693,7 +729,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
693
729
  // that spread has nothing to say and this is the only source.
694
730
  ...arrayDims ? { arrayDimensions: arrayDims } : {},
695
731
  // Only where v1 did not already describe the value, so a shaped column keeps its shape.
696
- ...jsonShape && !v1?.shape ? { shape: jsonShape } : {},
732
+ ...fallbackShape && !v1?.shape ? { shape: fallbackShape } : {},
697
733
  ...byteCap && v1?.maxBytes === void 0 ? { maxBytes: byteCap } : {}
698
734
  });
699
735
  }
@@ -813,12 +849,14 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
813
849
  const relations = [];
814
850
  const enums = [];
815
851
  const columnEnums = [];
852
+ const viewTables = [];
816
853
  for (const [name, val] of Object.entries(exportsObj)) {
817
854
  try {
818
855
  const cols = this.getSymbol(val, "drizzle:Columns");
819
856
  if (cols && typeof cols === "object") {
820
857
  const table = this.analyzeTable(name, val, issues);
821
858
  tables.push(table);
859
+ if (isDrizzleView(val)) viewTables.push(table);
822
860
  for (const col of table.columns) {
823
861
  const enumVals = cols[col.name]?.enumValues;
824
862
  if (enumVals && enumVals.length) {
@@ -869,7 +907,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
869
907
  let dialect = "unknown";
870
908
  const marks = /* @__PURE__ */ new Set();
871
909
  for (const [_, val] of Object.entries(exportsObj)) {
872
- const cols = val?.[/* @__PURE__ */ Symbol.for("drizzle:Columns")];
910
+ const cols = this.getSymbol(val, "drizzle:Columns");
873
911
  if (!cols) continue;
874
912
  for (const c of Object.values(cols)) {
875
913
  const kind = c?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")];
@@ -886,6 +924,9 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
886
924
  else if (/MySql|Mysql/i.test(names)) dialect = "mysql";
887
925
  else if (/Pg|Postgres/i.test(names)) dialect = "postgres";
888
926
  else if (/Gel/i.test(names)) dialect = "gel";
927
+ if (dialect === "sqlite") {
928
+ for (const view of viewTables) view.readOnly = true;
929
+ }
889
930
  if (dialect === "unknown" && tables.length) {
890
931
  issues.push({
891
932
  code: "DRZL_ANL_DIALECT",
@@ -971,12 +1012,67 @@ _SchemaAnalyzer.INT_RANGES = {
971
1012
  MySqlBigInt64: ["-9223372036854775808", "9223372036854775807"],
972
1013
  SingleStoreBigInt64: ["-9223372036854775808", "9223372036854775807"]
973
1014
  };
1015
+ /**
1016
+ * The numeric column classes that are not exact, and the magnitude each one can really hold.
1017
+ *
1018
+ * Only drizzle v1 states this outright, as a `float` or `double` semantic on `dataType`. On
1019
+ * 0.4x the same columns reach the analyzer by class name, `INT_RANGES` was the only range table
1020
+ * on that path, and so nothing said anything about them at all: not the range, and not that
1021
+ * they are inexact. The parity gate measured seven of the ten classes below and reported DRZL
1022
+ * differing from the first-party validator for the same major on all seven. The three
1023
+ * SingleStore classes are in no fixture either pass carries and are covered by unit tests alone.
1024
+ *
1025
+ * Differing is what the gate reports, not what it forbids. Most of its waivers have DRZL
1026
+ * accepting something official refuses and the run counts them; an earlier version of this
1027
+ * sentence said the gate exists to forbid being looser, which the same commit's own success
1028
+ * banner denies.
1029
+ *
1030
+ * `null` is a value in this table and is not the same as a class it does not name. It says the
1031
+ * column is inexact and that no finite magnitude bound is truthful for it, which is the case for
1032
+ * every 8 byte float: float8 is the JavaScript number's own format, so Postgres accepts every
1033
+ * finite JS number into one, measured to `Number.MAX_VALUE`. Read with an own-property test for
1034
+ * that reason, and because a plain object answers to `constructor` and `toString`.
1035
+ *
1036
+ * `integer: false` travels with every entry, bound or not, and what it decides depends on which.
1037
+ * `isIntegerColumn` falls back to "declares both bounds" when the flag is absent, so on a bounded
1038
+ * entry the flag is the only thing stopping the emitted schema calling `.int()` and refusing 1.5.
1039
+ * On an unbounded one it decides nothing, measured both ways against the real function in
1040
+ * `@drzl/validation-core`'s integer-column.spec.ts. It is stated there because it is true of the
1041
+ * column, not because it guards anything. That spec is where the measurement moved when the
1042
+ * analyzer's copy of it turned out to be a closed loop; this sentence went on naming
1043
+ * floats-and-tuples-0.4x.spec.ts, which asserts the flag is present and nothing about what it
1044
+ * decides.
1045
+ *
1046
+ * The widths are the type's, not the name's: MySQL and SingleStore `real` is a synonym for
1047
+ * `double` unless REAL_AS_FLOAT is set, and SQLite `real` is an 8 byte IEEE float. Both are
1048
+ * `number double` on drizzle v1, which is where these pairings come from.
1049
+ */
1050
+ _SchemaAnalyzer.INEXACT_RANGES = {
1051
+ // 4 byte floats, the one width a database refuses a magnitude for, and the two that have one
1052
+ // refuse at different values. SingleStore is MySQL wire-compatible and unmeasured here, so it
1053
+ // takes MySQL's rather than the wider of the two.
1054
+ PgReal: PG_FLOAT4_RANGE,
1055
+ MySqlFloat: MYSQL_FLOAT_RANGE,
1056
+ SingleStoreFloat: MYSQL_FLOAT_RANGE,
1057
+ // 8 byte floats, which hold every finite JS number
1058
+ PgDoublePrecision: null,
1059
+ MySqlDouble: null,
1060
+ MySqlReal: null,
1061
+ SQLiteReal: null,
1062
+ SingleStoreDouble: null,
1063
+ SingleStoreReal: null,
1064
+ // `numeric({ mode: 'number' })`, which v1 reaches through the bare-number arm of
1065
+ // `describeV1Column`. This one is about what a JS number can carry rather than about the
1066
+ // column, which Postgres caps far lower: it refuses 2147483648 into a `numeric(10,2)`.
1067
+ PgNumericNumber: JS_SAFE_INTEGER_BOUNDS
1068
+ };
974
1069
  var SchemaAnalyzer = _SchemaAnalyzer;
975
1070
  var index_default = SchemaAnalyzer;
976
1071
  export {
977
1072
  SchemaAnalyzer,
978
1073
  index_default as default,
979
1074
  describeV1Column,
1075
+ isDrizzleView,
980
1076
  isReadOnlyRelation,
981
1077
  isRelationsV2,
982
1078
  readRelationsV2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drzl/analyzer",
3
- "version": "1.14.0",
3
+ "version": "1.16.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
  }