@drzl/analyzer 1.17.7 → 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);
@@ -385,27 +393,43 @@ function isRelationsV2(val) {
385
393
  )
386
394
  );
387
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
+ }
388
402
  function readRelationsV2(val, issues = []) {
389
403
  const out = [];
390
404
  for (const [tableKey, entry] of Object.entries(val)) {
391
- const from = getSymbolOf(entry.table, "drizzle:Name") ?? entry.name ?? tableKey;
405
+ const from = qualifiedNameOfDrizzleTable(entry.table) ?? entry.name ?? tableKey;
392
406
  for (const [fieldName, r] of Object.entries(entry.relations ?? {})) {
393
- const to = r?.targetTableName;
407
+ const to = qualifiedNameOfDrizzleTable(r?.targetTable) ?? r?.targetTableName;
394
408
  if (typeof to !== "string" || !to) {
395
409
  issues.push({
396
410
  code: "DRZL_ANL_REL_V2",
397
411
  level: "warn",
398
- 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
399
414
  });
400
415
  continue;
401
416
  }
402
- 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;
403
418
  if (via) out.push({ kind: "manyToMany", from, to, via });
404
419
  else out.push({ kind: r.relationType === "many" ? "many" : "one", from, to });
405
420
  }
406
421
  }
407
422
  return out;
408
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
+ }
409
433
  var _SchemaAnalyzer = class _SchemaAnalyzer {
410
434
  constructor(schemaPath) {
411
435
  this.schemaPath = schemaPath;
@@ -456,6 +480,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
456
480
  code: "DRZL_ANL_EXTRACONFIG",
457
481
  level: "warn",
458
482
  message: `Could not evaluate the extra-config callback for table "${tableName}": ${e.message}`,
483
+ path: tableName,
459
484
  hint: "Indexes, composite keys, checks and table-level foreign keys will be missing for this table."
460
485
  });
461
486
  return [];
@@ -509,9 +534,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
509
534
  };
510
535
  const foreignColumnsObj = this.getSymbol(ref.foreignTable, "drizzle:Columns") ?? {};
511
536
  const toForeignTs = this.dbToTsNames(foreignColumnsObj);
537
+ const foreignSchema = this.getSymbol(ref.foreignTable, "drizzle:Schema");
512
538
  return {
513
539
  columns: (ref.columns ?? []).map((c) => toTs(c?.name)),
514
540
  foreignTable: this.getSymbol(ref.foreignTable, "drizzle:Name") ?? "unknown",
541
+ ...foreignSchema ? { foreignSchema } : {},
515
542
  foreignColumns: (ref.foreignColumns ?? []).map((c) => toForeignTs(c?.name)),
516
543
  onDelete: action(fk?.onDelete, fk?._onDelete),
517
544
  onUpdate: action(fk?.onUpdate, fk?._onUpdate),
@@ -560,7 +587,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
560
587
  * on each returned value, so the stand-in results must carry that method or the call throws.
561
588
  */
562
589
  readRelationsObject(val, exportName, issues) {
563
- const from = this.getSymbol(val.table, "drizzle:Name") ?? exportName;
590
+ const from = qualifiedNameOfDrizzleTable(val.table) ?? exportName;
564
591
  const make = (kind) => (table, cfg) => ({
565
592
  kind,
566
593
  referencedTable: table,
@@ -574,7 +601,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
574
601
  const built = val.config({ one: make("one"), many: make("many") });
575
602
  const out = [];
576
603
  for (const rel of Object.values(built ?? {})) {
577
- const to = this.getSymbol(rel?.referencedTable, "drizzle:Name");
604
+ const to = qualifiedNameOfDrizzleTable(rel?.referencedTable);
578
605
  if (to) out.push({ kind: rel.kind, from, to });
579
606
  }
580
607
  return out;
@@ -583,6 +610,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
583
610
  code: "DRZL_ANL_RELATIONS",
584
611
  level: "warn",
585
612
  message: `Could not read the relations declared in "${exportName}": ${e.message}`,
613
+ path: from,
586
614
  hint: "Relations for this table will be missing from the analysis."
587
615
  });
588
616
  return [];
@@ -603,10 +631,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
603
631
  if (fks.length < 2) continue;
604
632
  const fkCols = new Set(fks.flatMap((f) => f.columns));
605
633
  if (!t.columns.every((c) => fkCols.has(c.name))) continue;
606
- const targets = [...new Set(fks.map((f) => f.foreignTable))];
634
+ const targets = [...new Set(fks.map(qualifiedForeignTable))];
607
635
  if (targets.length !== 2) continue;
608
- out.push({ kind: "manyToMany", from: targets[0], to: targets[1], via: t.name });
609
- 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 });
610
639
  }
611
640
  return out;
612
641
  }
@@ -928,7 +957,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
928
957
  if (/Bool/i.test(ctor)) return { tsType: "boolean", dbType: "BOOLEAN" };
929
958
  if (/TimestampTz/i.test(ctor)) return { tsType: "Date", dbType: "TIMESTAMPTZ" };
930
959
  if (/Timestamp|LocalDateString|LocalTime|DateDuration|RelDuration|Duration/i.test(ctor))
931
- return { tsType: "unknown", dbType: "UNKNOWN" };
960
+ return { tsType: "unknown", dbType: "UNKNOWN", unnameable: "gel-temporal" };
932
961
  }
933
962
  return { tsType: "unknown", dbType: "UNKNOWN" };
934
963
  }
@@ -944,7 +973,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
944
973
  const uniqueGroups = /* @__PURE__ */ new Map();
945
974
  for (const [colName, outerCol] of Object.entries(columnsObj)) {
946
975
  const { element: col, dimensions: arrayDims } = unwrapArrayColumn(outerCol);
947
- let { tsType, dbType } = this.mapColumnType(col);
976
+ const mapped = this.mapColumnType(col);
977
+ let { tsType, dbType } = mapped;
948
978
  if (tsType === "unknown" && /At$/.test(colName)) {
949
979
  tsType = "Date";
950
980
  dbType = "INTEGER";
@@ -987,13 +1017,26 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
987
1017
  code: "DRZL_ANL_UNKNOWN_COLUMN",
988
1018
  level: "warn",
989
1019
  message: `Column "${colName}" on table "${tsName}" has no known type${sqlType2 ? ` (SQL type ${sqlType2})` : ""}, so its validator will accept any value.`,
990
- 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)
991
1022
  });
992
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
+ })();
993
1035
  columns.push({
994
1036
  name: colName,
995
1037
  tsType,
996
1038
  dbType,
1039
+ ...declaredSqlType ? { sqlType: declaredSqlType } : {},
997
1040
  nullable,
998
1041
  hasDefault,
999
1042
  isGenerated,
@@ -1070,6 +1113,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
1070
1113
  if (!col) continue;
1071
1114
  col.references = {
1072
1115
  table: fk.foreignTable,
1116
+ ...fk.foreignSchema ? { schema: fk.foreignSchema } : {},
1073
1117
  column: fk.foreignColumns[0],
1074
1118
  onDelete: fk.onDelete,
1075
1119
  onUpdate: fk.onUpdate
@@ -1142,9 +1186,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
1142
1186
  }
1143
1187
  }
1144
1188
  if (opts.includeRelations) {
1189
+ const self = qualifiedTableName(table);
1145
1190
  for (const fk of table.foreignKeys ?? []) {
1146
- relations.push({ kind: "one", from: table.name, to: fk.foreignTable });
1147
- 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 });
1148
1194
  }
1149
1195
  }
1150
1196
  } else if (this.isRelationsObject(val)) {
@@ -1172,7 +1218,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
1172
1218
  issues.push({
1173
1219
  code: "DRZL_ANL_TABLE",
1174
1220
  level: "warn",
1175
- message: `Failed to analyze export ${name}: ${String(e)}`
1221
+ message: `Failed to analyze export ${name}: ${String(e)}`,
1222
+ path: name
1176
1223
  });
1177
1224
  }
1178
1225
  }
@@ -1217,11 +1264,21 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
1217
1264
  relations.push(...this.inferManyToMany(tables));
1218
1265
  }
1219
1266
  if (opts.includeRelations && opts.includeHeuristicRelations) {
1220
- const tableNames = new Set(tables.map((t) => t.name));
1221
- const findTarget = (base) => {
1222
- if (tableNames.has(base)) return base;
1223
- if (tableNames.has(base + "s")) return base + "s";
1224
- 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
+ }
1225
1282
  return void 0;
1226
1283
  };
1227
1284
  for (const t of tables) {
@@ -1229,8 +1286,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
1229
1286
  if (c.references) continue;
1230
1287
  if (c.name.endsWith("Id")) {
1231
1288
  const base = c.name.slice(0, -2);
1232
- const target = findTarget(base);
1233
- 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 });
1234
1291
  }
1235
1292
  }
1236
1293
  }
@@ -1401,5 +1458,7 @@ var index_default = SchemaAnalyzer;
1401
1458
  isDrizzleView,
1402
1459
  isReadOnlyRelation,
1403
1460
  isRelationsV2,
1461
+ qualifiedForeignTable,
1462
+ qualifiedTableName,
1404
1463
  readRelationsV2
1405
1464
  });
package/dist/index.d.cts CHANGED
@@ -8,28 +8,91 @@ interface Issue {
8
8
  level: 'info' | 'warn' | 'error';
9
9
  message: string;
10
10
  hint?: string;
11
+ /**
12
+ * Where the issue is, as `table` or `table.column`.
13
+ *
14
+ * Declared since this interface existed and set by nothing, so every consumer wanting to group
15
+ * warnings by table had to read the names back out of the English in `message`. `drzl doctor` is
16
+ * the first such consumer and a report built by regex over prose breaks the first time a message
17
+ * is reworded, so the names are stated here instead.
18
+ *
19
+ * Still optional: an issue about the schema as a whole, such as an unidentifiable dialect, is
20
+ * about no table and says so by omitting this.
21
+ */
11
22
  path?: string;
12
23
  }
24
+ /**
25
+ * Why a column has no type, where "nobody has modelled it" is not the answer.
26
+ *
27
+ * The distinction exists because it changes the advice. A column class this file has no arm for is
28
+ * a gap someone can close, and the warning tells its author to say so. A Gel temporal column is
29
+ * not: the value is an instance of a class from the `gel` package, DRZL cannot import that package,
30
+ * and no generator could emit a check for it even knowing the name. Leaving it `unknown` is the
31
+ * measured answer rather than an omission, and its warning should say that instead of asking for a
32
+ * bug report that is already closed.
33
+ */
34
+ type UnnameableReason = 'gel-temporal';
13
35
  interface ColumnRef {
14
36
  table: string;
15
37
  column: string;
16
38
  }
39
+ /**
40
+ * A link between two tables, each end named by `qualifiedTableName`.
41
+ *
42
+ * Qualified rather than bare, because a bare database name identifies a table only while no two
43
+ * SQL schemas hold it: `to: 'users'` cannot say whether it means `public.users` or
44
+ * `reporting.users`, and every consumer resolves these strings back to a table object. A table in
45
+ * the default schema has no prefix, so nothing about a single-schema analysis changes.
46
+ */
17
47
  interface Relation {
18
48
  kind: 'one' | 'many' | 'manyToMany';
49
+ /** Qualified table name: `users`, or `reporting.users`. */
19
50
  from: string;
51
+ /** Qualified table name: `users`, or `reporting.users`. */
20
52
  to: string;
53
+ /** Qualified name of the join table, for m2m. */
21
54
  via?: string;
22
55
  }
23
56
  interface Column {
24
57
  name: string;
25
58
  tsType: string;
59
+ /**
60
+ * A coarse label for the column's kind, not its type.
61
+ *
62
+ * `varchar`, `char` and `text` are all `TEXT` here, deliberately: exactly one consumer reads
63
+ * this, `isIntegerColumn`, and it only asks whether a number is whole. Use `sqlType` for the
64
+ * question this name suggests it answers.
65
+ */
26
66
  dbType: string;
67
+ /**
68
+ * The column's type as the database declares it, from Drizzle's own `getSQLType()`:
69
+ * `varchar(255)`, `numeric(10, 2)`, `timestamp with time zone`, `text[]`, or an enum's type
70
+ * name.
71
+ *
72
+ * The one fact about a column that no validator schema can carry, and the one every other fact
73
+ * here is a consequence of. A generator that emits metadata beside its schemas has nothing else
74
+ * to put in it: `dbType` is a label rather than a type, and the declared width lives in
75
+ * `maxLength`, the precision in `min`/`max`, and neither of those says which type produced it.
76
+ *
77
+ * The two Drizzle majors disagree about an array and are reconciled here, measured rather than
78
+ * assumed: 0.4x wraps the column in a `PgArray` whose own answer is already `text[]`, while v1
79
+ * leaves the class alone and raises `dimensions`, so its answer is the bare `text`. The suffix
80
+ * is added from `arrayDimensions` when the type does not already carry one, so a consumer
81
+ * cannot tell which major produced its metadata.
82
+ *
83
+ * Absent where the builder has no `getSQLType` or it throws. Nothing is guessed from the class
84
+ * name: an invented type string reads exactly like a real one, and there is no way for a
85
+ * consumer to tell them apart.
86
+ */
87
+ sqlType?: string;
27
88
  nullable: boolean;
28
89
  hasDefault: boolean;
29
90
  isGenerated: boolean;
30
91
  defaultExpression?: string;
31
92
  references?: {
32
93
  table: string;
94
+ /** SQL schema of the referenced table, absent for the default one. See `ForeignKey`. */
95
+ schema?: string;
33
96
  column: string;
34
97
  onDelete?: string;
35
98
  onUpdate?: string;
@@ -272,6 +335,17 @@ interface ForeignKey {
272
335
  name?: string;
273
336
  columns: string[];
274
337
  foreignTable: string;
338
+ /**
339
+ * The SQL schema the referenced table lives in, absent for the default one, exactly as
340
+ * `Table.schema` is.
341
+ *
342
+ * `foreignTable` is a bare database name and Postgres lets two schemas hold the same one, so a
343
+ * key pointing at `reporting.users` recorded the identical string a key pointing at
344
+ * `public.users` records. Every consumer that resolves a key back to a table object did so by
345
+ * that string, and therefore resolved to whichever of the two it saw first. Use
346
+ * `qualifiedForeignTable` rather than reading the two fields apart.
347
+ */
348
+ foreignSchema?: string;
275
349
  foreignColumns: string[];
276
350
  onDelete?: string;
277
351
  onUpdate?: string;
@@ -279,6 +353,15 @@ interface ForeignKey {
279
353
  interface Table {
280
354
  name: string;
281
355
  tsName: string;
356
+ /**
357
+ * The SQL schema the table was declared in, from `pgSchema('reporting').table(...)` and the
358
+ * MySQL and SingleStore equivalents. Absent for a table declared with plain `pgTable`, which is
359
+ * the only spelling of the default schema there is: Drizzle refuses `pgSchema('public')`
360
+ * outright, with "Postgres is using public schema by default".
361
+ *
362
+ * `name` stays bare, so two tables in two schemas share one. `qualifiedTableName` is what tells
363
+ * them apart, and is what every name-addressed surface in DRZL matches against.
364
+ */
282
365
  schema?: string;
283
366
  columns: Column[];
284
367
  primaryKey?: Key;
@@ -298,6 +381,28 @@ interface Enum {
298
381
  name: string;
299
382
  values: string[];
300
383
  }
384
+ /**
385
+ * The one name that identifies a table across every SQL schema in an analysis.
386
+ *
387
+ * `reporting.users` where the table names a schema, and the bare `users` where it does not. The
388
+ * bare form for the default schema is deliberate and is what makes this safe to reach for
389
+ * everywhere: on a schema module that never calls `pgSchema`, and that is nearly all of them, this
390
+ * returns exactly `table.name`, so every file name, every export, every config pattern and every
391
+ * emitted path is byte for byte what it was.
392
+ *
393
+ * `public.users` is not produced here. Drizzle refuses `pgSchema('public')`, so no analysis can
394
+ * ever carry `schema: 'public'`, and a table with no schema *is* the public one. `public.` exists
395
+ * only as a spelling a config may use, resolved by `@drzl/cli`.
396
+ */
397
+ declare function qualifiedTableName(table: {
398
+ name: string;
399
+ schema?: string;
400
+ }): string;
401
+ /** The same name, for the far end of a foreign key. */
402
+ declare function qualifiedForeignTable(fk: {
403
+ foreignTable: string;
404
+ foreignSchema?: string;
405
+ }): string;
301
406
  interface Analysis {
302
407
  drizzleVersion?: string;
303
408
  dialect: Dialect;
@@ -532,4 +637,4 @@ declare class SchemaAnalyzer {
532
637
  analyze(opts?: AnalyzeOptions): Promise<Analysis>;
533
638
  }
534
639
 
535
- 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 };
640
+ export { type Analysis, type AnalyzeOptions, type Check, type Column, type ColumnRef, type ColumnShape, type Dialect, type Enum, type ForeignKey, type Index, type Issue, type Key, type Relation, SchemaAnalyzer, type Table, type UnnameableReason, SchemaAnalyzer as default, describeV1Column, isDrizzleView, isReadOnlyRelation, isRelationsV2, qualifiedForeignTable, qualifiedTableName, readRelationsV2 };
package/dist/index.d.ts CHANGED
@@ -8,28 +8,91 @@ interface Issue {
8
8
  level: 'info' | 'warn' | 'error';
9
9
  message: string;
10
10
  hint?: string;
11
+ /**
12
+ * Where the issue is, as `table` or `table.column`.
13
+ *
14
+ * Declared since this interface existed and set by nothing, so every consumer wanting to group
15
+ * warnings by table had to read the names back out of the English in `message`. `drzl doctor` is
16
+ * the first such consumer and a report built by regex over prose breaks the first time a message
17
+ * is reworded, so the names are stated here instead.
18
+ *
19
+ * Still optional: an issue about the schema as a whole, such as an unidentifiable dialect, is
20
+ * about no table and says so by omitting this.
21
+ */
11
22
  path?: string;
12
23
  }
24
+ /**
25
+ * Why a column has no type, where "nobody has modelled it" is not the answer.
26
+ *
27
+ * The distinction exists because it changes the advice. A column class this file has no arm for is
28
+ * a gap someone can close, and the warning tells its author to say so. A Gel temporal column is
29
+ * not: the value is an instance of a class from the `gel` package, DRZL cannot import that package,
30
+ * and no generator could emit a check for it even knowing the name. Leaving it `unknown` is the
31
+ * measured answer rather than an omission, and its warning should say that instead of asking for a
32
+ * bug report that is already closed.
33
+ */
34
+ type UnnameableReason = 'gel-temporal';
13
35
  interface ColumnRef {
14
36
  table: string;
15
37
  column: string;
16
38
  }
39
+ /**
40
+ * A link between two tables, each end named by `qualifiedTableName`.
41
+ *
42
+ * Qualified rather than bare, because a bare database name identifies a table only while no two
43
+ * SQL schemas hold it: `to: 'users'` cannot say whether it means `public.users` or
44
+ * `reporting.users`, and every consumer resolves these strings back to a table object. A table in
45
+ * the default schema has no prefix, so nothing about a single-schema analysis changes.
46
+ */
17
47
  interface Relation {
18
48
  kind: 'one' | 'many' | 'manyToMany';
49
+ /** Qualified table name: `users`, or `reporting.users`. */
19
50
  from: string;
51
+ /** Qualified table name: `users`, or `reporting.users`. */
20
52
  to: string;
53
+ /** Qualified name of the join table, for m2m. */
21
54
  via?: string;
22
55
  }
23
56
  interface Column {
24
57
  name: string;
25
58
  tsType: string;
59
+ /**
60
+ * A coarse label for the column's kind, not its type.
61
+ *
62
+ * `varchar`, `char` and `text` are all `TEXT` here, deliberately: exactly one consumer reads
63
+ * this, `isIntegerColumn`, and it only asks whether a number is whole. Use `sqlType` for the
64
+ * question this name suggests it answers.
65
+ */
26
66
  dbType: string;
67
+ /**
68
+ * The column's type as the database declares it, from Drizzle's own `getSQLType()`:
69
+ * `varchar(255)`, `numeric(10, 2)`, `timestamp with time zone`, `text[]`, or an enum's type
70
+ * name.
71
+ *
72
+ * The one fact about a column that no validator schema can carry, and the one every other fact
73
+ * here is a consequence of. A generator that emits metadata beside its schemas has nothing else
74
+ * to put in it: `dbType` is a label rather than a type, and the declared width lives in
75
+ * `maxLength`, the precision in `min`/`max`, and neither of those says which type produced it.
76
+ *
77
+ * The two Drizzle majors disagree about an array and are reconciled here, measured rather than
78
+ * assumed: 0.4x wraps the column in a `PgArray` whose own answer is already `text[]`, while v1
79
+ * leaves the class alone and raises `dimensions`, so its answer is the bare `text`. The suffix
80
+ * is added from `arrayDimensions` when the type does not already carry one, so a consumer
81
+ * cannot tell which major produced its metadata.
82
+ *
83
+ * Absent where the builder has no `getSQLType` or it throws. Nothing is guessed from the class
84
+ * name: an invented type string reads exactly like a real one, and there is no way for a
85
+ * consumer to tell them apart.
86
+ */
87
+ sqlType?: string;
27
88
  nullable: boolean;
28
89
  hasDefault: boolean;
29
90
  isGenerated: boolean;
30
91
  defaultExpression?: string;
31
92
  references?: {
32
93
  table: string;
94
+ /** SQL schema of the referenced table, absent for the default one. See `ForeignKey`. */
95
+ schema?: string;
33
96
  column: string;
34
97
  onDelete?: string;
35
98
  onUpdate?: string;
@@ -272,6 +335,17 @@ interface ForeignKey {
272
335
  name?: string;
273
336
  columns: string[];
274
337
  foreignTable: string;
338
+ /**
339
+ * The SQL schema the referenced table lives in, absent for the default one, exactly as
340
+ * `Table.schema` is.
341
+ *
342
+ * `foreignTable` is a bare database name and Postgres lets two schemas hold the same one, so a
343
+ * key pointing at `reporting.users` recorded the identical string a key pointing at
344
+ * `public.users` records. Every consumer that resolves a key back to a table object did so by
345
+ * that string, and therefore resolved to whichever of the two it saw first. Use
346
+ * `qualifiedForeignTable` rather than reading the two fields apart.
347
+ */
348
+ foreignSchema?: string;
275
349
  foreignColumns: string[];
276
350
  onDelete?: string;
277
351
  onUpdate?: string;
@@ -279,6 +353,15 @@ interface ForeignKey {
279
353
  interface Table {
280
354
  name: string;
281
355
  tsName: string;
356
+ /**
357
+ * The SQL schema the table was declared in, from `pgSchema('reporting').table(...)` and the
358
+ * MySQL and SingleStore equivalents. Absent for a table declared with plain `pgTable`, which is
359
+ * the only spelling of the default schema there is: Drizzle refuses `pgSchema('public')`
360
+ * outright, with "Postgres is using public schema by default".
361
+ *
362
+ * `name` stays bare, so two tables in two schemas share one. `qualifiedTableName` is what tells
363
+ * them apart, and is what every name-addressed surface in DRZL matches against.
364
+ */
282
365
  schema?: string;
283
366
  columns: Column[];
284
367
  primaryKey?: Key;
@@ -298,6 +381,28 @@ interface Enum {
298
381
  name: string;
299
382
  values: string[];
300
383
  }
384
+ /**
385
+ * The one name that identifies a table across every SQL schema in an analysis.
386
+ *
387
+ * `reporting.users` where the table names a schema, and the bare `users` where it does not. The
388
+ * bare form for the default schema is deliberate and is what makes this safe to reach for
389
+ * everywhere: on a schema module that never calls `pgSchema`, and that is nearly all of them, this
390
+ * returns exactly `table.name`, so every file name, every export, every config pattern and every
391
+ * emitted path is byte for byte what it was.
392
+ *
393
+ * `public.users` is not produced here. Drizzle refuses `pgSchema('public')`, so no analysis can
394
+ * ever carry `schema: 'public'`, and a table with no schema *is* the public one. `public.` exists
395
+ * only as a spelling a config may use, resolved by `@drzl/cli`.
396
+ */
397
+ declare function qualifiedTableName(table: {
398
+ name: string;
399
+ schema?: string;
400
+ }): string;
401
+ /** The same name, for the far end of a foreign key. */
402
+ declare function qualifiedForeignTable(fk: {
403
+ foreignTable: string;
404
+ foreignSchema?: string;
405
+ }): string;
301
406
  interface Analysis {
302
407
  drizzleVersion?: string;
303
408
  dialect: Dialect;
@@ -532,4 +637,4 @@ declare class SchemaAnalyzer {
532
637
  analyze(opts?: AnalyzeOptions): Promise<Analysis>;
533
638
  }
534
639
 
535
- 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 };
640
+ export { type Analysis, type AnalyzeOptions, type Check, type Column, type ColumnRef, type ColumnShape, type Dialect, type Enum, type ForeignKey, type Index, type Issue, type Key, type Relation, SchemaAnalyzer, type Table, type UnnameableReason, SchemaAnalyzer as default, describeV1Column, isDrizzleView, isReadOnlyRelation, isRelationsV2, qualifiedForeignTable, qualifiedTableName, readRelationsV2 };
package/dist/index.js CHANGED
@@ -1,4 +1,10 @@
1
1
  // src/index.ts
2
+ function qualifiedTableName(table) {
3
+ return table.schema ? `${table.schema}.${table.name}` : table.name;
4
+ }
5
+ function qualifiedForeignTable(fk) {
6
+ return fk.foreignSchema ? `${fk.foreignSchema}.${fk.foreignTable}` : fk.foreignTable;
7
+ }
2
8
  function renderSqlLiteral(v) {
3
9
  if (v === null || v === void 0) return "NULL";
4
10
  if (typeof v === "number" || typeof v === "bigint") return String(v);
@@ -344,27 +350,43 @@ function isRelationsV2(val) {
344
350
  )
345
351
  );
346
352
  }
353
+ function qualifiedNameOfDrizzleTable(tbl) {
354
+ const name = getSymbolOf(tbl, "drizzle:Name");
355
+ if (typeof name !== "string" || !name) return void 0;
356
+ const schema = getSymbolOf(tbl, "drizzle:Schema");
357
+ return typeof schema === "string" && schema ? `${schema}.${name}` : name;
358
+ }
347
359
  function readRelationsV2(val, issues = []) {
348
360
  const out = [];
349
361
  for (const [tableKey, entry] of Object.entries(val)) {
350
- const from = getSymbolOf(entry.table, "drizzle:Name") ?? entry.name ?? tableKey;
362
+ const from = qualifiedNameOfDrizzleTable(entry.table) ?? entry.name ?? tableKey;
351
363
  for (const [fieldName, r] of Object.entries(entry.relations ?? {})) {
352
- const to = r?.targetTableName;
364
+ const to = qualifiedNameOfDrizzleTable(r?.targetTable) ?? r?.targetTableName;
353
365
  if (typeof to !== "string" || !to) {
354
366
  issues.push({
355
367
  code: "DRZL_ANL_REL_V2",
356
368
  level: "warn",
357
- message: `Relation "${fieldName}" on "${from}" names no target table and was skipped.`
369
+ message: `Relation "${fieldName}" on "${from}" names no target table and was skipped.`,
370
+ path: from
358
371
  });
359
372
  continue;
360
373
  }
361
- const via = getSymbolOf(r.throughTable, "drizzle:Name") ?? getSymbolOf(r.through?.sourceTable, "drizzle:Name") ?? void 0;
374
+ const via = qualifiedNameOfDrizzleTable(r.throughTable) ?? qualifiedNameOfDrizzleTable(r.through?.sourceTable) ?? void 0;
362
375
  if (via) out.push({ kind: "manyToMany", from, to, via });
363
376
  else out.push({ kind: r.relationType === "many" ? "many" : "one", from, to });
364
377
  }
365
378
  }
366
379
  return out;
367
380
  }
381
+ function unknownColumnHint(reason) {
382
+ if (reason === "custom") {
383
+ return "A customType has no runtime shape to read. Declare it with .$type<T>() and turn on typedColumns to give the validator the type.";
384
+ }
385
+ if (reason === "gel-temporal") {
386
+ 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.";
387
+ }
388
+ return "Open an issue naming the column type so it can be modelled, or declare it with .$type<T>() and turn on typedColumns.";
389
+ }
368
390
  var _SchemaAnalyzer = class _SchemaAnalyzer {
369
391
  constructor(schemaPath) {
370
392
  this.schemaPath = schemaPath;
@@ -415,6 +437,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
415
437
  code: "DRZL_ANL_EXTRACONFIG",
416
438
  level: "warn",
417
439
  message: `Could not evaluate the extra-config callback for table "${tableName}": ${e.message}`,
440
+ path: tableName,
418
441
  hint: "Indexes, composite keys, checks and table-level foreign keys will be missing for this table."
419
442
  });
420
443
  return [];
@@ -468,9 +491,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
468
491
  };
469
492
  const foreignColumnsObj = this.getSymbol(ref.foreignTable, "drizzle:Columns") ?? {};
470
493
  const toForeignTs = this.dbToTsNames(foreignColumnsObj);
494
+ const foreignSchema = this.getSymbol(ref.foreignTable, "drizzle:Schema");
471
495
  return {
472
496
  columns: (ref.columns ?? []).map((c) => toTs(c?.name)),
473
497
  foreignTable: this.getSymbol(ref.foreignTable, "drizzle:Name") ?? "unknown",
498
+ ...foreignSchema ? { foreignSchema } : {},
474
499
  foreignColumns: (ref.foreignColumns ?? []).map((c) => toForeignTs(c?.name)),
475
500
  onDelete: action(fk?.onDelete, fk?._onDelete),
476
501
  onUpdate: action(fk?.onUpdate, fk?._onUpdate),
@@ -519,7 +544,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
519
544
  * on each returned value, so the stand-in results must carry that method or the call throws.
520
545
  */
521
546
  readRelationsObject(val, exportName, issues) {
522
- const from = this.getSymbol(val.table, "drizzle:Name") ?? exportName;
547
+ const from = qualifiedNameOfDrizzleTable(val.table) ?? exportName;
523
548
  const make = (kind) => (table, cfg) => ({
524
549
  kind,
525
550
  referencedTable: table,
@@ -533,7 +558,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
533
558
  const built = val.config({ one: make("one"), many: make("many") });
534
559
  const out = [];
535
560
  for (const rel of Object.values(built ?? {})) {
536
- const to = this.getSymbol(rel?.referencedTable, "drizzle:Name");
561
+ const to = qualifiedNameOfDrizzleTable(rel?.referencedTable);
537
562
  if (to) out.push({ kind: rel.kind, from, to });
538
563
  }
539
564
  return out;
@@ -542,6 +567,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
542
567
  code: "DRZL_ANL_RELATIONS",
543
568
  level: "warn",
544
569
  message: `Could not read the relations declared in "${exportName}": ${e.message}`,
570
+ path: from,
545
571
  hint: "Relations for this table will be missing from the analysis."
546
572
  });
547
573
  return [];
@@ -562,10 +588,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
562
588
  if (fks.length < 2) continue;
563
589
  const fkCols = new Set(fks.flatMap((f) => f.columns));
564
590
  if (!t.columns.every((c) => fkCols.has(c.name))) continue;
565
- const targets = [...new Set(fks.map((f) => f.foreignTable))];
591
+ const targets = [...new Set(fks.map(qualifiedForeignTable))];
566
592
  if (targets.length !== 2) continue;
567
- out.push({ kind: "manyToMany", from: targets[0], to: targets[1], via: t.name });
568
- out.push({ kind: "manyToMany", from: targets[1], to: targets[0], via: t.name });
593
+ const via = qualifiedTableName(t);
594
+ out.push({ kind: "manyToMany", from: targets[0], to: targets[1], via });
595
+ out.push({ kind: "manyToMany", from: targets[1], to: targets[0], via });
569
596
  }
570
597
  return out;
571
598
  }
@@ -887,7 +914,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
887
914
  if (/Bool/i.test(ctor)) return { tsType: "boolean", dbType: "BOOLEAN" };
888
915
  if (/TimestampTz/i.test(ctor)) return { tsType: "Date", dbType: "TIMESTAMPTZ" };
889
916
  if (/Timestamp|LocalDateString|LocalTime|DateDuration|RelDuration|Duration/i.test(ctor))
890
- return { tsType: "unknown", dbType: "UNKNOWN" };
917
+ return { tsType: "unknown", dbType: "UNKNOWN", unnameable: "gel-temporal" };
891
918
  }
892
919
  return { tsType: "unknown", dbType: "UNKNOWN" };
893
920
  }
@@ -903,7 +930,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
903
930
  const uniqueGroups = /* @__PURE__ */ new Map();
904
931
  for (const [colName, outerCol] of Object.entries(columnsObj)) {
905
932
  const { element: col, dimensions: arrayDims } = unwrapArrayColumn(outerCol);
906
- let { tsType, dbType } = this.mapColumnType(col);
933
+ const mapped = this.mapColumnType(col);
934
+ let { tsType, dbType } = mapped;
907
935
  if (tsType === "unknown" && /At$/.test(colName)) {
908
936
  tsType = "Date";
909
937
  dbType = "INTEGER";
@@ -946,13 +974,26 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
946
974
  code: "DRZL_ANL_UNKNOWN_COLUMN",
947
975
  level: "warn",
948
976
  message: `Column "${colName}" on table "${tsName}" has no known type${sqlType2 ? ` (SQL type ${sqlType2})` : ""}, so its validator will accept any value.`,
949
- 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."
977
+ path: `${tsName}.${colName}`,
978
+ hint: unknownColumnHint(shape === "custom" ? "custom" : mapped.unnameable)
950
979
  });
951
980
  }
981
+ const dims = arrayDims || v1?.arrayDimensions || 0;
982
+ const declaredSqlType = (() => {
983
+ let raw;
984
+ try {
985
+ raw = typeof outerCol?.getSQLType === "function" ? outerCol.getSQLType() : void 0;
986
+ } catch {
987
+ return void 0;
988
+ }
989
+ if (typeof raw !== "string" || !raw) return void 0;
990
+ return raw.endsWith("]") ? raw : raw + "[]".repeat(dims);
991
+ })();
952
992
  columns.push({
953
993
  name: colName,
954
994
  tsType,
955
995
  dbType,
996
+ ...declaredSqlType ? { sqlType: declaredSqlType } : {},
956
997
  nullable,
957
998
  hasDefault,
958
999
  isGenerated,
@@ -1029,6 +1070,7 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
1029
1070
  if (!col) continue;
1030
1071
  col.references = {
1031
1072
  table: fk.foreignTable,
1073
+ ...fk.foreignSchema ? { schema: fk.foreignSchema } : {},
1032
1074
  column: fk.foreignColumns[0],
1033
1075
  onDelete: fk.onDelete,
1034
1076
  onUpdate: fk.onUpdate
@@ -1101,9 +1143,11 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
1101
1143
  }
1102
1144
  }
1103
1145
  if (opts.includeRelations) {
1146
+ const self = qualifiedTableName(table);
1104
1147
  for (const fk of table.foreignKeys ?? []) {
1105
- relations.push({ kind: "one", from: table.name, to: fk.foreignTable });
1106
- relations.push({ kind: "many", from: fk.foreignTable, to: table.name });
1148
+ const target = qualifiedForeignTable(fk);
1149
+ relations.push({ kind: "one", from: self, to: target });
1150
+ relations.push({ kind: "many", from: target, to: self });
1107
1151
  }
1108
1152
  }
1109
1153
  } else if (this.isRelationsObject(val)) {
@@ -1131,7 +1175,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
1131
1175
  issues.push({
1132
1176
  code: "DRZL_ANL_TABLE",
1133
1177
  level: "warn",
1134
- message: `Failed to analyze export ${name}: ${String(e)}`
1178
+ message: `Failed to analyze export ${name}: ${String(e)}`,
1179
+ path: name
1135
1180
  });
1136
1181
  }
1137
1182
  }
@@ -1176,11 +1221,21 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
1176
1221
  relations.push(...this.inferManyToMany(tables));
1177
1222
  }
1178
1223
  if (opts.includeRelations && opts.includeHeuristicRelations) {
1179
- const tableNames = new Set(tables.map((t) => t.name));
1180
- const findTarget = (base) => {
1181
- if (tableNames.has(base)) return base;
1182
- if (tableNames.has(base + "s")) return base + "s";
1183
- if (tableNames.has(base + "es")) return base + "es";
1224
+ const byBareName = /* @__PURE__ */ new Map();
1225
+ for (const t of tables) {
1226
+ const list = byBareName.get(t.name);
1227
+ if (list) list.push(t);
1228
+ else byBareName.set(t.name, [t]);
1229
+ }
1230
+ const findTarget = (base, from) => {
1231
+ for (const candidate of [base, base + "s", base + "es"]) {
1232
+ const hits = byBareName.get(candidate);
1233
+ if (!hits?.length) continue;
1234
+ const sameSchema = hits.filter((t) => t.schema === from.schema);
1235
+ if (sameSchema.length === 1) return qualifiedTableName(sameSchema[0]);
1236
+ if (hits.length === 1) return qualifiedTableName(hits[0]);
1237
+ return void 0;
1238
+ }
1184
1239
  return void 0;
1185
1240
  };
1186
1241
  for (const t of tables) {
@@ -1188,8 +1243,8 @@ var _SchemaAnalyzer = class _SchemaAnalyzer {
1188
1243
  if (c.references) continue;
1189
1244
  if (c.name.endsWith("Id")) {
1190
1245
  const base = c.name.slice(0, -2);
1191
- const target = findTarget(base);
1192
- if (target) relations.push({ kind: "one", from: t.name, to: target });
1246
+ const target = findTarget(base, t);
1247
+ if (target) relations.push({ kind: "one", from: qualifiedTableName(t), to: target });
1193
1248
  }
1194
1249
  }
1195
1250
  }
@@ -1360,5 +1415,7 @@ export {
1360
1415
  isDrizzleView,
1361
1416
  isReadOnlyRelation,
1362
1417
  isRelationsV2,
1418
+ qualifiedForeignTable,
1419
+ qualifiedTableName,
1363
1420
  readRelationsV2
1364
1421
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drzl/analyzer",
3
- "version": "1.17.7",
3
+ "version": "1.20.0",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",