@drzl/analyzer 1.18.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -36,10 +36,18 @@ __export(index_exports, {
36
36
  isDrizzleView: () => isDrizzleView,
37
37
  isReadOnlyRelation: () => isReadOnlyRelation,
38
38
  isRelationsV2: () => isRelationsV2,
39
+ qualifiedForeignTable: () => qualifiedForeignTable,
40
+ qualifiedTableName: () => qualifiedTableName,
39
41
  readRelationsV2: () => readRelationsV2
40
42
  });
41
43
  module.exports = __toCommonJS(index_exports);
42
44
  var import_meta = {};
45
+ function qualifiedTableName(table) {
46
+ return table.schema ? `${table.schema}.${table.name}` : table.name;
47
+ }
48
+ function qualifiedForeignTable(fk) {
49
+ return fk.foreignSchema ? `${fk.foreignSchema}.${fk.foreignTable}` : fk.foreignTable;
50
+ }
43
51
  function renderSqlLiteral(v) {
44
52
  if (v === null || v === void 0) return "NULL";
45
53
  if (typeof v === "number" || typeof v === "bigint") return String(v);
@@ -71,15 +79,23 @@ var GEOMETRIC_CLASS_SHAPES = {
71
79
  // The object modes. `line({ mode: 'abc' })` is a `PgLineABC` and not a `PgLineObject`, and any
72
80
  // mode but `'tuple'` builds the object class: `point({ mode: 'abc' })` is a `PgPointObject` too.
73
81
  PgPointObject: { kind: "numberObject", fields: ["x", "y"] },
74
- PgLineABC: { kind: "numberObject", fields: ["a", "b", "c"] }
82
+ PgLineABC: { kind: "numberObject", fields: ["a", "b", "c"] },
83
+ // `geometry()` and `geometry({ mode: 'xy' })` are two classes, not one class with a flag, and
84
+ // the fuzzer found both unnamed on this path. Their driver mappers disagree the same way the
85
+ // point ones do: the default hands back `[1, 2]` and the xy mode hands back `{ x: 1, y: 2 }`.
86
+ PgGeometry: { kind: "tuple", length: 2 },
87
+ PgGeometryObject: { kind: "numberObject", fields: ["x", "y"] }
75
88
  };
76
89
  var V1_ONLY_ENTITY_KINDS = /^(?:MsSql|Cockroach)/;
90
+ var NUMBER_VECTOR_CLASSES = /* @__PURE__ */ new Set(["PgVector", "PgHalfVector", "SingleStoreVector"]);
91
+ var BIT_STRING_CLASSES = /* @__PURE__ */ new Set(["PgBinaryVector"]);
77
92
  var BYTE_STRING_CLASSES = /* @__PURE__ */ new Set([
78
93
  "MySqlBinary",
79
94
  "MySqlVarBinary",
80
95
  "SingleStoreBinary",
81
96
  "SingleStoreVarBinary"
82
97
  ]);
98
+ var BUFFER_CLASSES = /* @__PURE__ */ new Set(["SQLiteBlobBuffer"]);
83
99
  function describeV1Column(column) {
84
100
  const codec = column?.codec;
85
101
  const dataType = column?.dataType;
@@ -98,14 +114,24 @@ function describeV1Column(column) {
98
114
  const out = {};
99
115
  switch (semantic) {
100
116
  case "int8":
117
+ case "uint8":
101
118
  case "int16":
102
119
  case "int24":
103
120
  case "int32":
104
121
  case "int53":
105
122
  case "uint53":
106
123
  case "int64": {
124
+ if (DECIMAL_BIGINT_MODE.test(entityKind)) {
125
+ out.tsType = "bigint";
126
+ out.dbType = "NUMERIC";
127
+ out.integer = true;
128
+ const range2 = decimalModeRange(column, entityKind, "bigint");
129
+ if (range2) [out.min, out.max] = range2;
130
+ break;
131
+ }
107
132
  const range = {
108
133
  int8: ["-128", "127"],
134
+ uint8: ["0", "255"],
109
135
  int16: ["-32768", "32767"],
110
136
  int24: ["-8388608", "8388607"],
111
137
  int32: ["-2147483648", "2147483647"],
@@ -118,7 +144,7 @@ function describeV1Column(column) {
118
144
  [out.min, out.max] = range;
119
145
  out.integer = true;
120
146
  out.tsType = js === "bigint" ? "bigint" : "number";
121
- out.dbType = semantic === "int8" ? "TINYINT" : semantic === "int16" ? "SMALLINT" : semantic === "int24" ? "MEDIUMINT" : semantic === "int32" ? "INTEGER" : "BIGINT";
147
+ out.dbType = semantic === "int8" || semantic === "uint8" ? "TINYINT" : semantic === "int16" ? "SMALLINT" : semantic === "int24" ? "MEDIUMINT" : semantic === "int32" ? "INTEGER" : "BIGINT";
122
148
  break;
123
149
  }
124
150
  case "year":
@@ -134,6 +160,10 @@ function describeV1Column(column) {
134
160
  out.integer = false;
135
161
  out.tsType = "number";
136
162
  out.dbType = semantic === "float" ? "REAL" : "DOUBLE";
163
+ if (codec === "float4" || codec === "float8") {
164
+ out.allowsNaN = true;
165
+ out.allowsInfinity = true;
166
+ }
137
167
  break;
138
168
  }
139
169
  case "uuid":
@@ -163,6 +193,16 @@ function describeV1Column(column) {
163
193
  out.dbType = codec?.startsWith("timestamp") ? "TIMESTAMP" : "DATE";
164
194
  break;
165
195
  case "timestamp":
196
+ // `datetime` is the same fact under MySQL's name for it, and it had no arm, so every column
197
+ // stating it fell to the bare-string arm and was labelled TEXT. The columns that reach this
198
+ // are the string modes of `datetime` on mssql, mysql and singlestore, plus mssql's
199
+ // `datetime2` and `datetimeoffset`, swept over every builder the six v1 cores export; the
200
+ // `{ mode: 'date' }` half of the same builders states `object date` and takes the arm above.
201
+ // A label only, since `dbType` is read outside this file in exactly one place,
202
+ // `isIntegerColumn`, which the generators consult for a `tsType` of `number`. It matters
203
+ // because the class-name path already answers TIMESTAMP for the same 0.4x column, and the
204
+ // two majors disagreeing about a column is what the cross-major diff exists to catch.
205
+ case "datetime":
166
206
  out.tsType = js === "string" ? "string" : "Date";
167
207
  out.dbType = "TIMESTAMP";
168
208
  break;
@@ -184,13 +224,25 @@ function describeV1Column(column) {
184
224
  const entity = String(column?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
185
225
  const bytes = entity.startsWith("MySql") || entity.startsWith("SingleStore");
186
226
  out.tsType = "string";
187
- out.dbType = codec === "bit" ? "BIT" : "BINARY";
227
+ out.dbType = bytes ? "BINARY" : "BIT";
188
228
  out.shape = bytes ? { kind: "byteString", length: declaredLength(column) } : {
189
229
  kind: "bitstring",
190
230
  length: declaredLength(column),
191
231
  // A Postgres `bit(3)` holds exactly three digits; a Cockroach `varbit(16)` holds at
192
232
  // most that many, which is why `''` is valid there and not here.
193
- exact: codec === "bit"
233
+ //
234
+ // `codec === 'bit'` alone was Postgres's answer applied to everything, and Cockroach
235
+ // states no codec, so both of its builders came back `exact: false` and a `bit(3)`
236
+ // was indistinguishable from a `varbit(3)`. Measured on CockroachDB v24.3.5: a
237
+ // `bit(3)` refuses '', '1', '10' and '1011' with "bit string length n does not match
238
+ // type BIT(3)" and takes '101'; a `varbit(8)` takes '', '1' and '10101010' and
239
+ // refuses nine digits with "too large for type VARBIT(8)". `drizzle-orm/zod` at
240
+ // 1.0.0-rc.4 answers the same for both columns.
241
+ //
242
+ // The class rather than a prefix, because `CockroachVarbit` starts with neither
243
+ // `CockroachBit` nor anything else this could key on without catching the varying
244
+ // half too.
245
+ exact: codec === "bit" || entity === "CockroachBit"
194
246
  };
195
247
  break;
196
248
  }
@@ -216,7 +268,16 @@ function describeV1Column(column) {
216
268
  out.dbType = "LINE";
217
269
  out.shape = js === "object" ? { kind: "numberObject", fields: ["a", "b", "c"] } : { kind: "tuple", length: 3 };
218
270
  break;
271
+ // `halfvec` beside `vector`, because they differ in storage width and in nothing a validator
272
+ // can see: `mapFromDriverValue` on both hands back `[1, 2, 3]`. It had no arm and came back
273
+ // `unknown` on this path too, which the fuzzer found.
274
+ //
275
+ // `sparsevec` is deliberately not here. Its name says vector and its value is the string
276
+ // `{1:1.5,3:2}/3`, so typing it `number[]` for symmetry would reject every row the database
277
+ // returns. Its codec already answers `string` on its own, which is the same conclusion reached
278
+ // without this arm.
219
279
  case "vector":
280
+ case "halfvec":
220
281
  out.tsType = "number[]";
221
282
  out.dbType = "VECTOR";
222
283
  out.shape = { kind: "numberVector", length: declaredLength(column) };
@@ -233,7 +294,12 @@ function describeV1Column(column) {
233
294
  out.tsType = "number";
234
295
  out.dbType = "NUMERIC";
235
296
  out.integer = false;
236
- [out.min, out.max] = JS_SAFE_INTEGER_BOUNDS;
297
+ const range = decimalModeRange(column, entityKind, "number");
298
+ if (range) [out.min, out.max] = range;
299
+ if (codec === "numeric:number") {
300
+ out.allowsNaN = true;
301
+ out.allowsInfinity = !declaredDecimalRange(column);
302
+ }
237
303
  } else if (js === "string") {
238
304
  out.tsType = "string";
239
305
  out.dbType = codec === "varchar" ? "VARCHAR" : codec === "char" ? "CHAR" : "TEXT";
@@ -261,6 +327,28 @@ function declaredLength(column) {
261
327
  const n = column?.length ?? column?.config?.length ?? column?.config?.dimensions;
262
328
  return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : void 0;
263
329
  }
330
+ function declaredDecimalRange(column) {
331
+ const cfg = column?.config ?? {};
332
+ const precision = column?.precision ?? cfg.precision;
333
+ const scale = column?.scale ?? cfg.scale ?? 0;
334
+ if (typeof precision !== "number" || !Number.isInteger(precision) || precision < 1)
335
+ return void 0;
336
+ if (typeof scale !== "number" || !Number.isInteger(scale) || scale < 0) return void 0;
337
+ const nines = "9".repeat(precision);
338
+ const max = scale === 0 ? nines : scale < precision ? `${nines.slice(0, precision - scale)}.${nines.slice(precision - scale)}` : `0.${"0".repeat(scale - precision)}${nines}`;
339
+ return [`-${max}`, max];
340
+ }
341
+ var DECIMAL_NUMBER_MODE = /(?:Numeric|Decimal)Number$/;
342
+ var DECIMAL_BIGINT_MODE = /(?:Numeric|Decimal)BigInt$/;
343
+ var MYSQL_IMPLICIT_DECIMAL_RANGE = ["-9999999999", "9999999999"];
344
+ function decimalModeRange(column, kind, mode) {
345
+ const declared = declaredDecimalRange(column);
346
+ if (declared) return declared;
347
+ if (kind.startsWith("MySql") || kind.startsWith("SingleStore"))
348
+ return MYSQL_IMPLICIT_DECIMAL_RANGE;
349
+ if (kind.startsWith("SQLite")) return void 0;
350
+ return mode === "number" ? JS_SAFE_INTEGER_BOUNDS : void 0;
351
+ }
264
352
  var VIEW_CONFIG_FIELDS = {
265
353
  "drizzle:Columns": "selectedFields",
266
354
  "drizzle:Name": "name",
@@ -305,27 +393,43 @@ function isRelationsV2(val) {
305
393
  )
306
394
  );
307
395
  }
396
+ function qualifiedNameOfDrizzleTable(tbl) {
397
+ const name = getSymbolOf(tbl, "drizzle:Name");
398
+ if (typeof name !== "string" || !name) return void 0;
399
+ const schema = getSymbolOf(tbl, "drizzle:Schema");
400
+ return typeof schema === "string" && schema ? `${schema}.${name}` : name;
401
+ }
308
402
  function readRelationsV2(val, issues = []) {
309
403
  const out = [];
310
404
  for (const [tableKey, entry] of Object.entries(val)) {
311
- const from = getSymbolOf(entry.table, "drizzle:Name") ?? entry.name ?? tableKey;
405
+ const from = qualifiedNameOfDrizzleTable(entry.table) ?? entry.name ?? tableKey;
312
406
  for (const [fieldName, r] of Object.entries(entry.relations ?? {})) {
313
- const to = r?.targetTableName;
407
+ const to = qualifiedNameOfDrizzleTable(r?.targetTable) ?? r?.targetTableName;
314
408
  if (typeof to !== "string" || !to) {
315
409
  issues.push({
316
410
  code: "DRZL_ANL_REL_V2",
317
411
  level: "warn",
318
- message: `Relation "${fieldName}" on "${from}" names no target table and was skipped.`
412
+ message: `Relation "${fieldName}" on "${from}" names no target table and was skipped.`,
413
+ path: from
319
414
  });
320
415
  continue;
321
416
  }
322
- const via = getSymbolOf(r.throughTable, "drizzle:Name") ?? getSymbolOf(r.through?.sourceTable, "drizzle:Name") ?? void 0;
417
+ const via = qualifiedNameOfDrizzleTable(r.throughTable) ?? qualifiedNameOfDrizzleTable(r.through?.sourceTable) ?? void 0;
323
418
  if (via) out.push({ kind: "manyToMany", from, to, via });
324
419
  else out.push({ kind: r.relationType === "many" ? "many" : "one", from, to });
325
420
  }
326
421
  }
327
422
  return out;
328
423
  }
424
+ function unknownColumnHint(reason) {
425
+ if (reason === "custom") {
426
+ return "A customType has no runtime shape to read. Declare it with .$type<T>() and turn on typedColumns to give the validator the type.";
427
+ }
428
+ if (reason === "gel-temporal") {
429
+ return "A Gel temporal column holds an instance of a class from the `gel` package, which DRZL cannot import, so it is left untyped on purpose rather than guessed at. Turn on typedColumns to recover the declared type, and validate the value yourself.";
430
+ }
431
+ return "Open an issue naming the column type so it can be modelled, or declare it with .$type<T>() and turn on typedColumns.";
432
+ }
329
433
  var _SchemaAnalyzer = class _SchemaAnalyzer {
330
434
  constructor(schemaPath) {
331
435
  this.schemaPath = schemaPath;
@@ -376,6 +480,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
376
480
  code: "DRZL_ANL_EXTRACONFIG",
377
481
  level: "warn",
378
482
  message: `Could not evaluate the extra-config callback for table "${tableName}": ${e.message}`,
483
+ path: tableName,
379
484
  hint: "Indexes, composite keys, checks and table-level foreign keys will be missing for this table."
380
485
  });
381
486
  return [];
@@ -429,9 +534,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
429
534
  };
430
535
  const foreignColumnsObj = this.getSymbol(ref.foreignTable, "drizzle:Columns") ?? {};
431
536
  const toForeignTs = this.dbToTsNames(foreignColumnsObj);
537
+ const foreignSchema = this.getSymbol(ref.foreignTable, "drizzle:Schema");
432
538
  return {
433
539
  columns: (ref.columns ?? []).map((c) => toTs(c?.name)),
434
540
  foreignTable: this.getSymbol(ref.foreignTable, "drizzle:Name") ?? "unknown",
541
+ ...foreignSchema ? { foreignSchema } : {},
435
542
  foreignColumns: (ref.foreignColumns ?? []).map((c) => toForeignTs(c?.name)),
436
543
  onDelete: action(fk?.onDelete, fk?._onDelete),
437
544
  onUpdate: action(fk?.onUpdate, fk?._onUpdate),
@@ -480,7 +587,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
480
587
  * on each returned value, so the stand-in results must carry that method or the call throws.
481
588
  */
482
589
  readRelationsObject(val, exportName, issues) {
483
- const from = this.getSymbol(val.table, "drizzle:Name") ?? exportName;
590
+ const from = qualifiedNameOfDrizzleTable(val.table) ?? exportName;
484
591
  const make = (kind) => (table, cfg) => ({
485
592
  kind,
486
593
  referencedTable: table,
@@ -494,7 +601,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
494
601
  const built = val.config({ one: make("one"), many: make("many") });
495
602
  const out = [];
496
603
  for (const rel of Object.values(built ?? {})) {
497
- const to = this.getSymbol(rel?.referencedTable, "drizzle:Name");
604
+ const to = qualifiedNameOfDrizzleTable(rel?.referencedTable);
498
605
  if (to) out.push({ kind: rel.kind, from, to });
499
606
  }
500
607
  return out;
@@ -503,6 +610,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
503
610
  code: "DRZL_ANL_RELATIONS",
504
611
  level: "warn",
505
612
  message: `Could not read the relations declared in "${exportName}": ${e.message}`,
613
+ path: from,
506
614
  hint: "Relations for this table will be missing from the analysis."
507
615
  });
508
616
  return [];
@@ -523,10 +631,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
523
631
  if (fks.length < 2) continue;
524
632
  const fkCols = new Set(fks.flatMap((f) => f.columns));
525
633
  if (!t.columns.every((c) => fkCols.has(c.name))) continue;
526
- const targets = [...new Set(fks.map((f) => f.foreignTable))];
634
+ const targets = [...new Set(fks.map(qualifiedForeignTable))];
527
635
  if (targets.length !== 2) continue;
528
- out.push({ kind: "manyToMany", from: targets[0], to: targets[1], via: t.name });
529
- out.push({ kind: "manyToMany", from: targets[1], to: targets[0], via: t.name });
636
+ const via = qualifiedTableName(t);
637
+ out.push({ kind: "manyToMany", from: targets[0], to: targets[1], via });
638
+ out.push({ kind: "manyToMany", from: targets[1], to: targets[0], via });
530
639
  }
531
640
  return out;
532
641
  }
@@ -553,6 +662,24 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
553
662
  if (inexact) [out.min, out.max] = inexact;
554
663
  out.integer = false;
555
664
  }
665
+ const nonFinite = _SchemaAnalyzer.PG_NON_FINITE[ctor];
666
+ if (nonFinite) {
667
+ out.allowsNaN = nonFinite.nan;
668
+ out.allowsInfinity = nonFinite.infinity;
669
+ }
670
+ if (DECIMAL_NUMBER_MODE.test(ctor)) {
671
+ const range2 = decimalModeRange(column, ctor, "number");
672
+ if (range2) [out.min, out.max] = range2;
673
+ out.integer = false;
674
+ if (ctor === "PgNumericNumber") {
675
+ out.allowsNaN = true;
676
+ out.allowsInfinity = !declaredDecimalRange(column);
677
+ }
678
+ } else if (DECIMAL_BIGINT_MODE.test(ctor)) {
679
+ const range2 = decimalModeRange(column, ctor, "bigint");
680
+ if (range2) [out.min, out.max] = range2;
681
+ out.integer = true;
682
+ }
556
683
  if (/^(Pg)?UUID$/i.test(ctor) || /Uuid$/i.test(ctor)) out.format = "uuid";
557
684
  return out;
558
685
  }
@@ -564,12 +691,36 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
564
691
  tsType: column?.config?.mode === "timestamp" ? "Date" : "number",
565
692
  dbType: "INTEGER"
566
693
  };
694
+ // Both timestamp modes of `integer()`, which are one class and one type. `timestamp` and
695
+ // `timestamp_ms` differ in the scale of the number on the wire, seconds against
696
+ // milliseconds, and `mapFromDriverValue` consumes that difference and hands back a `Date`
697
+ // either way; nothing downstream of the analyzer ever sees the integer. So an arm keyed on
698
+ // the class covers both, where the mode check that used to answer this fell through the
699
+ // switch to a default arm testing `config.mode === 'timestamp'` and named only the first.
700
+ // The second came back `unknown`, and every generator emitted a schema accepting anything.
701
+ //
702
+ // `DATE` rather than the `INTEGER` that mode check returned, so the two majors describe the
703
+ // column identically. `dbType` is read in exactly one place outside this file,
704
+ // `isIntegerColumn`, which the generators consult only for a `tsType` of `number`, so the
705
+ // relabel reaches no output. Measured rather than argued: emitting a `Date` column under
706
+ // both labels, nullable and not, through all five generators gives ten byte-identical pairs.
707
+ case "SQLiteTimestamp":
708
+ return { tsType: "Date", dbType: "DATE" };
567
709
  case "SQLiteText":
568
710
  return { tsType: "string", dbType: "TEXT" };
569
711
  case "SQLiteReal":
570
712
  return { tsType: "number", dbType: "REAL" };
713
+ // No 0.4x column is a `SQLiteBlob`: `sqlite-core` builds a `SQLiteBlobBuffer`, a
714
+ // `SQLiteBlobJson` or a `SQLiteBigInt`, one per mode, and exports no class of this name at
715
+ // all. The arm answers the hand-built column in sqlite-types.spec.ts and nothing drizzle
716
+ // produces, which is why a real `blob()` reached neither it nor anything else.
571
717
  case "SQLiteBlob":
572
718
  return { tsType: "Uint8Array", dbType: "BLOB" };
719
+ // The class a real `blob()` and `blob({ mode: 'buffer' })` both build. See `BUFFER_CLASSES`
720
+ // for the measurement; the answers here are v1's own for the same column, so this is the
721
+ // two majors agreeing rather than a new opinion.
722
+ case "SQLiteBlobBuffer":
723
+ return { tsType: "Buffer", dbType: "BYTEA" };
573
724
  // SQLite spells a mode as a distinct class rather than as config, so `text({mode:'json'})`
574
725
  // is a `SQLiteTextJson` and matched no arm at all: the column came back UNKNOWN, which is
575
726
  // wider than the `any` a json column at least used to get.
@@ -608,6 +759,34 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
608
759
  case "MySqlEnumColumn":
609
760
  case "SingleStoreEnumColumn":
610
761
  return { tsType: "string", dbType: "TEXT" };
762
+ // The pgvector family, found by the analyzer fuzzer: all three came back `unknown` on this
763
+ // path, so their validators accepted anything. The answers are drizzle's own mappers rather
764
+ // than the type names, and the three do not agree with each other:
765
+ //
766
+ // vector(3) SELECT gives [1,2,3] INSERT sends "[1,2,3]"
767
+ // halfvec(3) SELECT gives [1,2,3] INSERT sends "[1,2,3]"
768
+ // sparsevec(3) SELECT gives "{1:1.5,3:2}/3" INSERT sends "{1:1.5,3:2}/3"
769
+ //
770
+ // So the two dense ones are number arrays and the sparse one is a string. Typing `sparsevec`
771
+ // as a vector for symmetry would reject every row the database returns, which is the defect
772
+ // this family was filed under to begin with. The `shape` carries the dimension count where
773
+ // one is declared, as the codec path already did for `vector`.
774
+ case "PgVector":
775
+ case "PgHalfVector":
776
+ case "SingleStoreVector":
777
+ return { tsType: "number[]", dbType: "VECTOR" };
778
+ // `BIT` rather than `TEXT`, which a first version of this arm returned. v1's codec says `BIT`
779
+ // for the same column, and the cross-major diff said so: naming the class made ten of its
780
+ // twelve entries go stale and left `c_bit.dbType` and its nullable twin standing, which is
781
+ // that check distinguishing a fix from a half fix.
782
+ case "PgBinaryVector":
783
+ return { tsType: "string", dbType: "BIT" };
784
+ case "PgGeometry":
785
+ return { tsType: "[number, number]", dbType: "GEOMETRY" };
786
+ case "PgGeometryObject":
787
+ return { tsType: "{ x: number; y: number }", dbType: "GEOMETRY" };
788
+ case "PgSparseVector":
789
+ return { tsType: "string", dbType: "TEXT" };
611
790
  case "PgInteger":
612
791
  case "PgSmallInt":
613
792
  return { tsType: "number", dbType: "INTEGER" };
@@ -768,7 +947,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
768
947
  if (/^Gel/i.test(ctor)) {
769
948
  if (/BigInt64/i.test(ctor)) return { tsType: "bigint", dbType: "BIGINT" };
770
949
  if (/Int53|Integer|SmallInt/i.test(ctor)) return { tsType: "number", dbType: "INTEGER" };
771
- if (/Real|DoublePrecision/i.test(ctor)) return { tsType: "number", dbType: "NUMERIC" };
950
+ if (/DoublePrecision/i.test(ctor)) return { tsType: "number", dbType: "DOUBLE" };
951
+ if (/Real/i.test(ctor)) return { tsType: "number", dbType: "REAL" };
772
952
  if (/Decimal/i.test(ctor)) return { tsType: "string", dbType: "NUMERIC" };
773
953
  if (/UUID/i.test(ctor)) return { tsType: "string", dbType: "UUID" };
774
954
  if (/Json/i.test(ctor)) return { tsType: "any", dbType: "JSON" };
@@ -777,7 +957,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
777
957
  if (/Bool/i.test(ctor)) return { tsType: "boolean", dbType: "BOOLEAN" };
778
958
  if (/TimestampTz/i.test(ctor)) return { tsType: "Date", dbType: "TIMESTAMPTZ" };
779
959
  if (/Timestamp|LocalDateString|LocalTime|DateDuration|RelDuration|Duration/i.test(ctor))
780
- return { tsType: "unknown", dbType: "UNKNOWN" };
960
+ return { tsType: "unknown", dbType: "UNKNOWN", unnameable: "gel-temporal" };
781
961
  }
782
962
  return { tsType: "unknown", dbType: "UNKNOWN" };
783
963
  }
@@ -793,7 +973,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
793
973
  const uniqueGroups = /* @__PURE__ */ new Map();
794
974
  for (const [colName, outerCol] of Object.entries(columnsObj)) {
795
975
  const { element: col, dimensions: arrayDims } = unwrapArrayColumn(outerCol);
796
- let { tsType, dbType } = this.mapColumnType(col);
976
+ const mapped = this.mapColumnType(col);
977
+ let { tsType, dbType } = mapped;
797
978
  if (tsType === "unknown" && /At$/.test(colName)) {
798
979
  tsType = "Date";
799
980
  dbType = "INTEGER";
@@ -819,11 +1000,13 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
819
1000
  const v1 = describeV1Column(col);
820
1001
  const constraints = this.columnConstraints(col);
821
1002
  if (v1?.shape) delete constraints.maxLength;
822
- const sqlKind = String(outerCol?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? "");
1003
+ const sqlKind = String(
1004
+ outerCol?.constructor?.[/* @__PURE__ */ Symbol.for("drizzle:entityKind")] ?? ""
1005
+ );
823
1006
  const sqlType = sqlKind.startsWith("MySql") && typeof col?.getSQLType === "function" ? String(col.getSQLType()).toLowerCase() : void 0;
824
1007
  const byteCap = sqlType ? MYSQL_TEXT_CAPS[sqlType] : void 0;
825
1008
  const ctorName = String(col?.constructor?.name ?? "");
826
- const fallbackShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : BYTE_STRING_CLASSES.has(ctorName) ? { kind: "byteString", length: declaredLength(col) } : GEOMETRIC_CLASS_SHAPES[ctorName];
1009
+ const fallbackShape = dbType === "JSON" || dbType === "JSONB" || col?.config?.mode === "json" ? { kind: "json" } : BYTE_STRING_CLASSES.has(ctorName) ? { kind: "byteString", length: declaredLength(col) } : BUFFER_CLASSES.has(ctorName) ? { kind: "buffer" } : NUMBER_VECTOR_CLASSES.has(ctorName) ? { kind: "numberVector", length: declaredLength(col) } : BIT_STRING_CLASSES.has(ctorName) ? { kind: "bitstring", length: declaredLength(col), exact: true } : GEOMETRIC_CLASS_SHAPES[ctorName];
827
1010
  if (fallbackShape?.kind === "byteString") delete constraints.maxLength;
828
1011
  const shape = (v1?.shape ?? fallbackShape)?.kind;
829
1012
  const finalTs = v1?.tsType ?? tsType;
@@ -834,13 +1017,26 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
834
1017
  code: "DRZL_ANL_UNKNOWN_COLUMN",
835
1018
  level: "warn",
836
1019
  message: `Column "${colName}" on table "${tsName}" has no known type${sqlType2 ? ` (SQL type ${sqlType2})` : ""}, so its validator will accept any value.`,
837
- 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."
1020
+ path: `${tsName}.${colName}`,
1021
+ hint: unknownColumnHint(shape === "custom" ? "custom" : mapped.unnameable)
838
1022
  });
839
1023
  }
1024
+ const dims = arrayDims || v1?.arrayDimensions || 0;
1025
+ const declaredSqlType = (() => {
1026
+ let raw;
1027
+ try {
1028
+ raw = typeof outerCol?.getSQLType === "function" ? outerCol.getSQLType() : void 0;
1029
+ } catch {
1030
+ return void 0;
1031
+ }
1032
+ if (typeof raw !== "string" || !raw) return void 0;
1033
+ return raw.endsWith("]") ? raw : raw + "[]".repeat(dims);
1034
+ })();
840
1035
  columns.push({
841
1036
  name: colName,
842
1037
  tsType,
843
1038
  dbType,
1039
+ ...declaredSqlType ? { sqlType: declaredSqlType } : {},
844
1040
  nullable,
845
1041
  hasDefault,
846
1042
  isGenerated,
@@ -917,6 +1113,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
917
1113
  if (!col) continue;
918
1114
  col.references = {
919
1115
  table: fk.foreignTable,
1116
+ ...fk.foreignSchema ? { schema: fk.foreignSchema } : {},
920
1117
  column: fk.foreignColumns[0],
921
1118
  onDelete: fk.onDelete,
922
1119
  onUpdate: fk.onUpdate
@@ -989,9 +1186,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
989
1186
  }
990
1187
  }
991
1188
  if (opts.includeRelations) {
1189
+ const self = qualifiedTableName(table);
992
1190
  for (const fk of table.foreignKeys ?? []) {
993
- relations.push({ kind: "one", from: table.name, to: fk.foreignTable });
994
- relations.push({ kind: "many", from: fk.foreignTable, to: table.name });
1191
+ const target = qualifiedForeignTable(fk);
1192
+ relations.push({ kind: "one", from: self, to: target });
1193
+ relations.push({ kind: "many", from: target, to: self });
995
1194
  }
996
1195
  }
997
1196
  } else if (this.isRelationsObject(val)) {
@@ -1019,7 +1218,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
1019
1218
  issues.push({
1020
1219
  code: "DRZL_ANL_TABLE",
1021
1220
  level: "warn",
1022
- message: `Failed to analyze export ${name}: ${String(e)}`
1221
+ message: `Failed to analyze export ${name}: ${String(e)}`,
1222
+ path: name
1023
1223
  });
1024
1224
  }
1025
1225
  }
@@ -1064,11 +1264,21 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
1064
1264
  relations.push(...this.inferManyToMany(tables));
1065
1265
  }
1066
1266
  if (opts.includeRelations && opts.includeHeuristicRelations) {
1067
- const tableNames = new Set(tables.map((t) => t.name));
1068
- const findTarget = (base) => {
1069
- if (tableNames.has(base)) return base;
1070
- if (tableNames.has(base + "s")) return base + "s";
1071
- if (tableNames.has(base + "es")) return base + "es";
1267
+ const byBareName = /* @__PURE__ */ new Map();
1268
+ for (const t of tables) {
1269
+ const list = byBareName.get(t.name);
1270
+ if (list) list.push(t);
1271
+ else byBareName.set(t.name, [t]);
1272
+ }
1273
+ const findTarget = (base, from) => {
1274
+ for (const candidate of [base, base + "s", base + "es"]) {
1275
+ const hits = byBareName.get(candidate);
1276
+ if (!hits?.length) continue;
1277
+ const sameSchema = hits.filter((t) => t.schema === from.schema);
1278
+ if (sameSchema.length === 1) return qualifiedTableName(sameSchema[0]);
1279
+ if (hits.length === 1) return qualifiedTableName(hits[0]);
1280
+ return void 0;
1281
+ }
1072
1282
  return void 0;
1073
1283
  };
1074
1284
  for (const t of tables) {
@@ -1076,8 +1286,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
1076
1286
  if (c.references) continue;
1077
1287
  if (c.name.endsWith("Id")) {
1078
1288
  const base = c.name.slice(0, -2);
1079
- const target = findTarget(base);
1080
- if (target) relations.push({ kind: "one", from: t.name, to: target });
1289
+ const target = findTarget(base, t);
1290
+ if (target) relations.push({ kind: "one", from: qualifiedTableName(t), to: target });
1081
1291
  }
1082
1292
  }
1083
1293
  }
@@ -1186,10 +1396,58 @@ _SchemaAnalyzer.INEXACT_RANGES = {
1186
1396
  SQLiteReal: null,
1187
1397
  SingleStoreDouble: null,
1188
1398
  SingleStoreReal: null,
1189
- // `numeric({ mode: 'number' })`, which v1 reaches through the bare-number arm of
1190
- // `describeV1Column`. This one is about what a JS number can carry rather than about the
1191
- // column, which Postgres caps far lower: it refuses 2147483648 into a `numeric(10,2)`.
1192
- PgNumericNumber: JS_SAFE_INTEGER_BOUNDS
1399
+ // Gel, whose `real` is a `std::float32` and whose `doublePrecision` is a `std::float64`. Both
1400
+ // used to be answered by a `/Real|DoublePrecision/i` arm that said NUMERIC and stated nothing
1401
+ // else at all, so a `real` column accepted 1e300 and the server refused it.
1402
+ //
1403
+ // Measured on a live Gel 7.1 (`geldata/gel:7`, sys::get_version_as_str() -> 7.1+08db576)
1404
+ // through the `gel` client, casting each literal so the server parses it, and again through a
1405
+ // stored property on a real object type. The float32 edge is Postgres's exactly, to the double:
1406
+ //
1407
+ // 3.4028234663852886e38 accepted, returned unchanged
1408
+ // 3.4028235677973366e38 accepted, and stored as 3.4028234663852886e38
1409
+ // 3.402823567797337e38 refused, "is out of range for type std::float32"
1410
+ // 1e300 refused, the same way
1411
+ //
1412
+ // The same value accepted, the same next double up refused, and the same rounding down of the
1413
+ // midpoint, so it takes the constant already here rather than a second name for one number.
1414
+ // float64 took 1e300 and Number.MAX_VALUE faithfully, for the reason no 8 byte float has a
1415
+ // truthful finite bound.
1416
+ GelReal: PG_FLOAT4_RANGE,
1417
+ GelDoublePrecision: null
1418
+ };
1419
+ // `numeric`/`decimal` in either of its two numeric modes is deliberately not in the table above.
1420
+ // Its bound is not a fixed magnitude per class but the precision each column declares for itself,
1421
+ // which no table keyed on a class name can hold; see `declaredDecimalRange`.
1422
+ /**
1423
+ * The Postgres number columns that hold a non-finite double, and which of the three each holds.
1424
+ *
1425
+ * The class-name half of what `describeV1Column` reads off the codec, and the two must agree: a
1426
+ * fact stated on one path and not the other is a schema that changes when the user upgrades
1427
+ * drizzle, which the cross-major diff in `verify-packed.sh` fails on. These three class names are
1428
+ * the same on both majors, read off real `pgTable` columns on 0.45.2 and on 1.0.0-rc.4, so this
1429
+ * table also answers for a v1 column and the two answers are identical rather than merely
1430
+ * compatible. non-finite-numbers.spec.ts asserts that agreement through the real analyzer.
1431
+ *
1432
+ * No MySQL, SingleStore or SQLite class belongs here: MySQL refuses all three on a `float`/
1433
+ * `double` and stores `0.00` for a `decimal`, and SQLite returns both infinities while silently
1434
+ * turning `NaN` into NULL, which is a different answer that has to arrive whole.
1435
+ *
1436
+ * Gel does belong, and is the fourth and fifth entries. Measured on a live Gel 7.1 rather than
1437
+ * inferred from it being Postgres-backed: both `std::float32` and `std::float64` stored `nan`,
1438
+ * `inf` and `-inf` and handed all three back as `NaN`, `Infinity` and `-Infinity`, through a cast
1439
+ * and again through a stored property. Without them every row of such a column failed validation.
1440
+ *
1441
+ * `PgNumeric` is absent because its value is a string, and its pattern already accepts `NaN` and
1442
+ * `Infinity`. `PgNumericNumber` is absent because its answer is no longer flat: it takes `NaN` at
1443
+ * any width and an infinity only where no precision is declared, which is a per-column question
1444
+ * this table cannot ask. `columnConstraints` answers it beside the bound that decides it.
1445
+ */
1446
+ _SchemaAnalyzer.PG_NON_FINITE = {
1447
+ PgReal: { nan: true, infinity: true },
1448
+ PgDoublePrecision: { nan: true, infinity: true },
1449
+ GelReal: { nan: true, infinity: true },
1450
+ GelDoublePrecision: { nan: true, infinity: true }
1193
1451
  };
1194
1452
  var SchemaAnalyzer = _SchemaAnalyzer;
1195
1453
  var index_default = SchemaAnalyzer;
@@ -1200,5 +1458,7 @@ var index_default = SchemaAnalyzer;
1200
1458
  isDrizzleView,
1201
1459
  isReadOnlyRelation,
1202
1460
  isRelationsV2,
1461
+ qualifiedForeignTable,
1462
+ qualifiedTableName,
1203
1463
  readRelationsV2
1204
1464
  });