@drzl/analyzer 1.2.0 → 1.4.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
@@ -52,6 +52,197 @@ var SchemaAnalyzer = class {
52
52
  }
53
53
  return table[Symbol.for(key)];
54
54
  }
55
+ /**
56
+ * Drizzle keys the Columns object by TypeScript property name, but every other piece of
57
+ * metadata (foreign keys, indexes, composite primary keys) reports the *database* column
58
+ * name. We report the TS name, since that is what a generated schema has to spell, so
59
+ * anything coming from that other metadata has to be translated back.
60
+ *
61
+ * Falls through to the input when a name is unknown, which keeps raw SQL expressions and
62
+ * columns belonging to another table readable rather than dropping them.
63
+ */
64
+ dbToTsNames(columnsObj) {
65
+ const map = /* @__PURE__ */ new Map();
66
+ for (const [tsName, col] of Object.entries(columnsObj ?? {})) {
67
+ const dbName = col?.name;
68
+ if (typeof dbName === "string") map.set(dbName, tsName);
69
+ }
70
+ return (dbName) => {
71
+ const raw = typeof dbName === "string" ? dbName : dbName?.name ?? String(dbName);
72
+ return map.get(raw) ?? raw;
73
+ };
74
+ }
75
+ /**
76
+ * Entries from a table's third argument, the extra-config callback.
77
+ *
78
+ * Drizzle invokes this callback with the table's ExtraConfigColumns, NOT with the table.
79
+ * Passing the table throws, and because the whole block used to sit under a bare `catch {}`
80
+ * the throw was swallowed and every index, unique index, composite primary key, check
81
+ * constraint and table-level foreign key silently vanished from the analysis.
82
+ *
83
+ * Both shapes are accepted: modern Drizzle returns an array, older versions an object.
84
+ */
85
+ extraConfigEntries(tbl, issues, tableName) {
86
+ const builder = this.getSymbol(tbl, "drizzle:ExtraConfigBuilder");
87
+ if (typeof builder !== "function") return [];
88
+ const cols = this.getSymbol(tbl, "drizzle:ExtraConfigColumns") ?? tbl;
89
+ try {
90
+ const built = builder(cols);
91
+ if (!built) return [];
92
+ return Array.isArray(built) ? built : Object.values(built);
93
+ } catch (e) {
94
+ issues.push({
95
+ code: "DRZL_ANL_EXTRACONFIG",
96
+ level: "warn",
97
+ message: `Could not evaluate the extra-config callback for table "${tableName}": ${e.message}`,
98
+ hint: "Indexes, composite keys, checks and table-level foreign keys will be missing for this table."
99
+ });
100
+ return [];
101
+ }
102
+ }
103
+ /**
104
+ * Foreign keys declared inline with `.references()`.
105
+ *
106
+ * Drizzle stores these per dialect under `drizzle:PgInlineForeignKeys`,
107
+ * `drizzle:MySqlInlineForeignKeys` and `drizzle:SQLiteInlineForeignKeys`. Matching the
108
+ * suffix rather than listing the three keeps new dialects working without a change here.
109
+ * SingleStore has no entry because it does not support foreign keys at all, which is why
110
+ * `.references()` is not even a function there.
111
+ */
112
+ inlineForeignKeys(tbl) {
113
+ try {
114
+ for (const s of Object.getOwnPropertySymbols(tbl)) {
115
+ if (/InlineForeignKeys$/.test(s.description ?? "")) {
116
+ const v = tbl[s];
117
+ if (Array.isArray(v)) return v;
118
+ }
119
+ }
120
+ } catch {
121
+ }
122
+ return [];
123
+ }
124
+ /**
125
+ * Normalise one foreign key, inline or table-level, into a common shape.
126
+ *
127
+ * `.reference()` yields the resolved columns on both. The referential actions do not live
128
+ * in the same place: a built ForeignKey exposes `onDelete`/`onUpdate` as strings, while an
129
+ * unbuilt ForeignKeyBuilder exposes them as the chainable setter functions and keeps the
130
+ * values in `_onDelete`/`_onUpdate`. Reading the wrong one yields a function where a string
131
+ * is expected, so check the type rather than the property's presence.
132
+ *
133
+ * Postgres reports 'no action' where MySQL and SQLite report nothing, for identical schemas.
134
+ * Since 'no action' *is* the default, it is normalised away so the same schema analyses the
135
+ * same across dialects.
136
+ */
137
+ readForeignKey(fk, toTs) {
138
+ let ref;
139
+ try {
140
+ ref = fk?.reference?.();
141
+ } catch {
142
+ return void 0;
143
+ }
144
+ if (!ref?.foreignTable) return void 0;
145
+ const action = (v, fallback) => {
146
+ const raw = typeof v === "string" ? v : typeof fallback === "string" ? fallback : void 0;
147
+ return raw && raw.toLowerCase() !== "no action" ? raw : void 0;
148
+ };
149
+ const foreignColumnsObj = this.getSymbol(ref.foreignTable, "drizzle:Columns") ?? {};
150
+ const toForeignTs = this.dbToTsNames(foreignColumnsObj);
151
+ return {
152
+ columns: (ref.columns ?? []).map((c) => toTs(c?.name)),
153
+ foreignTable: this.getSymbol(ref.foreignTable, "drizzle:Name") ?? "unknown",
154
+ foreignColumns: (ref.foreignColumns ?? []).map((c) => toForeignTs(c?.name)),
155
+ onDelete: action(fk?.onDelete, fk?._onDelete),
156
+ onUpdate: action(fk?.onUpdate, fk?._onUpdate),
157
+ name: typeof fk?.getName === "function" ? void 0 : fk?.name
158
+ };
159
+ }
160
+ /**
161
+ * Render a Drizzle SQL template back into readable text.
162
+ *
163
+ * A `sql` tagged template is stored as alternating chunks: literal fragments holding a
164
+ * string array, and column references. `String()` on that array yields "[object Object]",
165
+ * so a check constraint's expression has to be assembled rather than stringified.
166
+ * Anything unrecognised becomes `?`, which is honest about the gap without inventing SQL.
167
+ */
168
+ renderSql(value, toTs) {
169
+ const chunks = value?.queryChunks;
170
+ if (!Array.isArray(chunks)) return String(value ?? "");
171
+ return chunks.map((c) => {
172
+ if (Array.isArray(c?.value)) return c.value.join("");
173
+ if (typeof c?.name === "string") return toTs(c.name);
174
+ if (c?.queryChunks) return this.renderSql(c, toTs);
175
+ return "?";
176
+ }).join("").trim();
177
+ }
178
+ /** A value produced by Drizzle's `relations()` helper: a source table plus a callback. */
179
+ isRelationsObject(val) {
180
+ return !!val && typeof val === "object" && typeof val.config === "function" && !!this.getSymbol(val.table, "drizzle:Columns");
181
+ }
182
+ /**
183
+ * Read the relations declared by `relations(table, ({ one, many }) => ...)`.
184
+ *
185
+ * The previous implementation looked for `val.config.relations`. That property does not
186
+ * exist: `config` is a *function*, so the expression was always undefined and the branch
187
+ * never ran. Nothing failed loudly, relations simply came back empty forever.
188
+ *
189
+ * `config` cannot just be read, it has to be invoked with the builder Drizzle would pass.
190
+ * Rather than depend on drizzle-orm to obtain the real builder, which this package
191
+ * deliberately does not do anywhere else, a stand-in supplies the only two functions a
192
+ * user's callback can call. `relations()` wraps that callback and calls `.withFieldName()`
193
+ * on each returned value, so the stand-in results must carry that method or the call throws.
194
+ */
195
+ readRelationsObject(val, exportName, issues) {
196
+ const from = this.getSymbol(val.table, "drizzle:Name") ?? exportName;
197
+ const make = (kind) => (table, cfg) => ({
198
+ kind,
199
+ referencedTable: table,
200
+ cfg,
201
+ withFieldName(n) {
202
+ this.fieldName = n;
203
+ return this;
204
+ }
205
+ });
206
+ try {
207
+ const built = val.config({ one: make("one"), many: make("many") });
208
+ const out = [];
209
+ for (const rel of Object.values(built ?? {})) {
210
+ const to = this.getSymbol(rel?.referencedTable, "drizzle:Name");
211
+ if (to) out.push({ kind: rel.kind, from, to });
212
+ }
213
+ return out;
214
+ } catch (e) {
215
+ issues.push({
216
+ code: "DRZL_ANL_RELATIONS",
217
+ level: "warn",
218
+ message: `Could not read the relations declared in "${exportName}": ${e.message}`,
219
+ hint: "Relations for this table will be missing from the analysis."
220
+ });
221
+ return [];
222
+ }
223
+ }
224
+ /**
225
+ * Infer many-to-many links through a join table.
226
+ *
227
+ * A join table is taken to be one whose every column participates in a foreign key, and
228
+ * which points at exactly two distinct tables. Requiring *all* columns to be foreign keys
229
+ * is deliberate: a table carrying its own data is a real entity, not plumbing, and calling
230
+ * it a join table would invent a relation the author never declared.
231
+ */
232
+ inferManyToMany(tables) {
233
+ const out = [];
234
+ for (const t of tables) {
235
+ const fks = t.foreignKeys ?? [];
236
+ if (fks.length < 2) continue;
237
+ const fkCols = new Set(fks.flatMap((f) => f.columns));
238
+ if (!t.columns.every((c) => fkCols.has(c.name))) continue;
239
+ const targets = [...new Set(fks.map((f) => f.foreignTable))];
240
+ if (targets.length !== 2) continue;
241
+ out.push({ kind: "manyToMany", from: targets[0], to: targets[1], via: t.name });
242
+ out.push({ kind: "manyToMany", from: targets[1], to: targets[0], via: t.name });
243
+ }
244
+ return out;
245
+ }
55
246
  mapColumnType(column) {
56
247
  const ctor = column?.constructor?.name ?? "";
57
248
  switch (ctor) {
@@ -67,14 +258,17 @@ var SchemaAnalyzer = class {
67
258
  case "SQLiteBlob":
68
259
  return { tsType: "Uint8Array", dbType: "BLOB" };
69
260
  case "SQLiteNumeric":
70
- return { tsType: "number", dbType: "NUMERIC" };
261
+ return { tsType: "string", dbType: "NUMERIC" };
71
262
  case "SQLiteBoolean":
72
263
  return { tsType: "boolean", dbType: "INTEGER" };
73
264
  case "PgInteger":
74
265
  case "PgSmallInt":
75
266
  return { tsType: "number", dbType: "INTEGER" };
76
267
  case "PgBigInt":
77
- return { tsType: "bigint", dbType: "BIGINT" };
268
+ return {
269
+ tsType: column?.config?.mode === "number" ? "number" : "bigint",
270
+ dbType: "BIGINT"
271
+ };
78
272
  case "PgSerial":
79
273
  case "PgSmallSerial":
80
274
  case "PgBigSerial":
@@ -92,9 +286,10 @@ var SchemaAnalyzer = class {
92
286
  case "PgDate":
93
287
  return { tsType: "Date", dbType: "TIMESTAMP" };
94
288
  case "PgNumeric":
289
+ return { tsType: "string", dbType: "NUMERIC" };
95
290
  case "PgFloat":
96
291
  case "PgDoublePrecision":
97
- return { tsType: "number", dbType: "NUMERIC" };
292
+ return { tsType: "number", dbType: "DOUBLE" };
98
293
  case "PgJson":
99
294
  case "PgJsonb":
100
295
  return { tsType: "any", dbType: ctor === "PgJsonb" ? "JSONB" : "JSON" };
@@ -173,12 +368,13 @@ var SchemaAnalyzer = class {
173
368
  return { tsType: "unknown", dbType: "UNKNOWN" };
174
369
  }
175
370
  }
176
- analyzeTable(tsName, tbl) {
371
+ analyzeTable(tsName, tbl, issues = []) {
177
372
  const columnsObj = this.getSymbol(tbl, "drizzle:Columns") ?? {};
178
373
  const columns = [];
179
374
  const unique = [];
180
375
  const indexes = [];
181
376
  const checks = [];
377
+ const foreignKeys = [];
182
378
  const pkCols = [];
183
379
  const uniqueGroups = /* @__PURE__ */ new Map();
184
380
  for (const [colName, col] of Object.entries(columnsObj)) {
@@ -191,13 +387,7 @@ var SchemaAnalyzer = class {
191
387
  const nullable = !col?.notNull && !col?.config?.notNull;
192
388
  const isGenerated = !!(col?.autoIncrement || col?.isGenerated);
193
389
  const hasDefault = col?.default !== void 0 || col?.config?.default !== void 0 || isGenerated;
194
- const ref = col?.references;
195
- const references = ref ? {
196
- table: ref.table ?? "unknown",
197
- column: ref.column ?? "id",
198
- onDelete: ref.onDelete,
199
- onUpdate: ref.onUpdate
200
- } : void 0;
390
+ const references = void 0;
201
391
  const isUnique = !!(col?.isUnique || col?.config?.isUnique);
202
392
  const isPk = !!(col?.primary || col?.config?.primaryKey);
203
393
  if (isPk) pkCols.push(colName);
@@ -222,10 +412,11 @@ var SchemaAnalyzer = class {
222
412
  }
223
413
  const name = this.getSymbol(tbl, "drizzle:Name") || tsName;
224
414
  const schema = this.getSymbol(tbl, "drizzle:Schema");
415
+ const toTs = this.dbToTsNames(columnsObj);
225
416
  try {
226
- const pkDef = tbl[Symbol.for("drizzle:PrimaryKey")];
417
+ const pkDef = this.getSymbol(tbl, "drizzle:PrimaryKey");
227
418
  if (pkDef && Array.isArray(pkDef.columns)) {
228
- const cols = pkDef.columns.map((c) => c?.name ?? String(c)).filter(Boolean);
419
+ const cols = pkDef.columns.map((c) => toTs(c?.name)).filter(Boolean);
229
420
  if (cols.length) {
230
421
  pkCols.splice(0, pkCols.length, ...cols);
231
422
  }
@@ -233,36 +424,50 @@ var SchemaAnalyzer = class {
233
424
  } catch {
234
425
  }
235
426
  try {
236
- const idxDef = tbl[Symbol.for("drizzle:Indexes")];
427
+ const idxDef = this.getSymbol(tbl, "drizzle:Indexes");
237
428
  if (Array.isArray(idxDef)) {
238
429
  for (const i of idxDef) {
239
- const cols = (i?.columns ?? []).map((c) => c?.name ?? String(c)).filter(Boolean);
430
+ const cols = (i?.columns ?? []).map((c) => toTs(c?.name)).filter(Boolean);
240
431
  if (cols.length) indexes.push({ columns: cols, name: i?.name });
241
432
  if (i?.unique && cols.length) unique.push({ columns: cols });
242
433
  }
243
434
  }
244
435
  } catch {
245
436
  }
246
- try {
247
- const builder = tbl[Symbol.for("drizzle:ExtraConfigBuilder")];
248
- if (typeof builder === "function") {
249
- const built = builder(tbl);
250
- const entries = Array.isArray(built) ? built.map((v, i) => [v?.config?.name ?? v?.name ?? `idx_${i}`, v]) : Object.entries(built ?? {});
251
- for (const [key, val] of entries) {
252
- const cfg = val?.config ?? val;
253
- const cols = (cfg?.columns ?? []).map((c) => c?.name ?? String(c)).filter(Boolean);
254
- const uniqueFlag = !!(cfg?.unique || /unique/i.test(val?.constructor?.name ?? "") || /unique/i.test(key));
255
- const idxName = cfg?.name ?? val?.name ?? key;
256
- const expr = cfg?.where || cfg?.expression;
257
- if (expr && !cols.length) {
258
- checks.push({ name: idxName, expression: String(expr) });
259
- } else if (cols.length) {
260
- indexes.push({ columns: cols, name: idxName });
261
- if (uniqueFlag) unique.push({ columns: cols });
262
- }
263
- }
437
+ for (const entry of this.extraConfigEntries(tbl, issues, name)) {
438
+ if (typeof entry?.reference === "function") {
439
+ const fk = this.readForeignKey(entry, toTs);
440
+ if (fk) foreignKeys.push(fk);
441
+ continue;
264
442
  }
265
- } catch {
443
+ if (entry?.value?.queryChunks && entry?.name !== void 0) {
444
+ checks.push({ name: entry.name, expression: this.renderSql(entry.value, toTs) });
445
+ continue;
446
+ }
447
+ const cfg = entry?.config ?? entry ?? {};
448
+ const cols = (cfg.columns ?? []).map((c) => toTs(c?.name)).filter(Boolean);
449
+ if (!cols.length) continue;
450
+ if (cfg.unique === void 0) {
451
+ pkCols.splice(0, pkCols.length, ...cols);
452
+ continue;
453
+ }
454
+ indexes.push({ columns: cols, name: cfg.name });
455
+ if (cfg.unique) unique.push({ columns: cols, name: cfg.name });
456
+ }
457
+ for (const fk of this.inlineForeignKeys(tbl)) {
458
+ const read = this.readForeignKey(fk, toTs);
459
+ if (read) foreignKeys.push(read);
460
+ }
461
+ for (const fk of foreignKeys) {
462
+ if (fk.columns.length !== 1 || fk.foreignColumns.length !== 1) continue;
463
+ const col = columns.find((c) => c.name === fk.columns[0]);
464
+ if (!col) continue;
465
+ col.references = {
466
+ table: fk.foreignTable,
467
+ column: fk.foreignColumns[0],
468
+ onDelete: fk.onDelete,
469
+ onUpdate: fk.onUpdate
470
+ };
266
471
  }
267
472
  return {
268
473
  name,
@@ -276,6 +481,7 @@ var SchemaAnalyzer = class {
276
481
  ],
277
482
  indexes: [...pkCols.length ? [{ columns: pkCols }] : [], ...indexes],
278
483
  checks,
484
+ foreignKeys,
279
485
  meta: {}
280
486
  };
281
487
  }
@@ -311,39 +517,29 @@ var SchemaAnalyzer = class {
311
517
  const tables = [];
312
518
  const relations = [];
313
519
  const enums = [];
520
+ const columnEnums = [];
314
521
  for (const [name, val] of Object.entries(exportsObj)) {
315
522
  try {
316
523
  const cols = this.getSymbol(val, "drizzle:Columns");
317
524
  if (cols && typeof cols === "object") {
318
- const table = this.analyzeTable(name, val);
525
+ const table = this.analyzeTable(name, val, issues);
319
526
  tables.push(table);
320
- if (opts.includeRelations) {
321
- for (const col of table.columns) {
322
- if (col.references) {
323
- relations.push({
324
- kind: "many",
325
- from: table.name,
326
- to: col.references.table
327
- });
328
- }
329
- const enumVals = cols[col.name]?.enumValues;
330
- if (enumVals && enumVals.length) {
331
- const enumName = `${table.name}_${col.name}_enum`;
332
- if (!enums.find((e) => e.name === enumName))
333
- enums.push({ name: enumName, values: enumVals });
334
- }
527
+ for (const col of table.columns) {
528
+ const enumVals = cols[col.name]?.enumValues;
529
+ if (enumVals && enumVals.length) {
530
+ columnEnums.push({ name: `${table.name}_${col.name}_enum`, values: enumVals });
335
531
  }
336
532
  }
337
- } else if (val?.config?.relations) {
338
- const cfg = val.config.relations;
339
- const base = name.replace(/Relations$/, "");
340
- for (const entry of Object.values(cfg)) {
341
- const target = entry?.referencedTable;
342
- const targetName = target ? this.getSymbol(target, "drizzle:Name") : void 0;
343
- if (targetName) {
344
- relations.push({ kind: "many", from: base, to: targetName });
533
+ if (opts.includeRelations) {
534
+ for (const fk of table.foreignKeys ?? []) {
535
+ relations.push({ kind: "one", from: table.name, to: fk.foreignTable });
536
+ relations.push({ kind: "many", from: fk.foreignTable, to: table.name });
345
537
  }
346
538
  }
539
+ } else if (this.isRelationsObject(val)) {
540
+ if (opts.includeRelations) {
541
+ relations.push(...this.readRelationsObject(val, name, issues));
542
+ }
347
543
  } else {
348
544
  const ev = val?.enumValues;
349
545
  if (Array.isArray(ev) && ev.every((x) => typeof x === "string")) {
@@ -365,6 +561,12 @@ var SchemaAnalyzer = class {
365
561
  });
366
562
  }
367
563
  }
564
+ for (const candidate of columnEnums) {
565
+ const key = JSON.stringify(candidate.values);
566
+ if (enums.some((e) => JSON.stringify(e.values) === key)) continue;
567
+ if (enums.some((e) => e.name === candidate.name)) continue;
568
+ enums.push(candidate);
569
+ }
368
570
  let dialect = "unknown";
369
571
  const ctorNames = /* @__PURE__ */ new Set();
370
572
  for (const [_, val] of Object.entries(exportsObj)) {
@@ -388,6 +590,9 @@ var SchemaAnalyzer = class {
388
590
  );
389
591
  if (looksSqlite) dialect = "sqlite";
390
592
  }
593
+ if (opts.includeRelations) {
594
+ relations.push(...this.inferManyToMany(tables));
595
+ }
391
596
  if (opts.includeRelations && opts.includeHeuristicRelations) {
392
597
  const tableNames = new Set(tables.map((t) => t.name));
393
598
  const findTarget = (base) => {
@@ -398,19 +603,27 @@ var SchemaAnalyzer = class {
398
603
  };
399
604
  for (const t of tables) {
400
605
  for (const c of t.columns) {
606
+ if (c.references) continue;
401
607
  if (c.name.endsWith("Id")) {
402
608
  const base = c.name.slice(0, -2);
403
609
  const target = findTarget(base);
404
- if (target) relations.push({ kind: "many", from: t.name, to: target });
610
+ if (target) relations.push({ kind: "one", from: t.name, to: target });
405
611
  }
406
612
  }
407
613
  }
408
614
  }
615
+ const seen = /* @__PURE__ */ new Set();
616
+ const deduped = relations.filter((r) => {
617
+ const key = `${r.kind}|${r.from}|${r.to}|${r.via ?? ""}`;
618
+ if (seen.has(key)) return false;
619
+ seen.add(key);
620
+ return true;
621
+ });
409
622
  return {
410
623
  dialect,
411
624
  tables,
412
625
  enums,
413
- relations,
626
+ relations: deduped,
414
627
  issues
415
628
  };
416
629
  }
package/dist/index.d.cts CHANGED
@@ -44,6 +44,21 @@ interface Check {
44
44
  name?: string;
45
45
  expression?: string;
46
46
  }
47
+ /**
48
+ * A foreign key as declared, which may span several columns. Single-column keys are also
49
+ * mirrored onto `Column.references` for convenience; composite ones exist only here, because
50
+ * they cannot be attributed to any one column.
51
+ *
52
+ * Column names are TypeScript property names, matching `Column.name`.
53
+ */
54
+ interface ForeignKey {
55
+ name?: string;
56
+ columns: string[];
57
+ foreignTable: string;
58
+ foreignColumns: string[];
59
+ onDelete?: string;
60
+ onUpdate?: string;
61
+ }
47
62
  interface Table {
48
63
  name: string;
49
64
  tsName: string;
@@ -53,6 +68,7 @@ interface Table {
53
68
  unique: Key[];
54
69
  indexes: Index[];
55
70
  checks?: Check[];
71
+ foreignKeys?: ForeignKey[];
56
72
  meta?: Record<string, unknown>;
57
73
  }
58
74
  interface Enum {
@@ -76,9 +92,88 @@ declare class SchemaAnalyzer {
76
92
  private readonly schemaPath;
77
93
  constructor(schemaPath: string);
78
94
  private getSymbol;
95
+ /**
96
+ * Drizzle keys the Columns object by TypeScript property name, but every other piece of
97
+ * metadata (foreign keys, indexes, composite primary keys) reports the *database* column
98
+ * name. We report the TS name, since that is what a generated schema has to spell, so
99
+ * anything coming from that other metadata has to be translated back.
100
+ *
101
+ * Falls through to the input when a name is unknown, which keeps raw SQL expressions and
102
+ * columns belonging to another table readable rather than dropping them.
103
+ */
104
+ private dbToTsNames;
105
+ /**
106
+ * Entries from a table's third argument, the extra-config callback.
107
+ *
108
+ * Drizzle invokes this callback with the table's ExtraConfigColumns, NOT with the table.
109
+ * Passing the table throws, and because the whole block used to sit under a bare `catch {}`
110
+ * the throw was swallowed and every index, unique index, composite primary key, check
111
+ * constraint and table-level foreign key silently vanished from the analysis.
112
+ *
113
+ * Both shapes are accepted: modern Drizzle returns an array, older versions an object.
114
+ */
115
+ private extraConfigEntries;
116
+ /**
117
+ * Foreign keys declared inline with `.references()`.
118
+ *
119
+ * Drizzle stores these per dialect under `drizzle:PgInlineForeignKeys`,
120
+ * `drizzle:MySqlInlineForeignKeys` and `drizzle:SQLiteInlineForeignKeys`. Matching the
121
+ * suffix rather than listing the three keeps new dialects working without a change here.
122
+ * SingleStore has no entry because it does not support foreign keys at all, which is why
123
+ * `.references()` is not even a function there.
124
+ */
125
+ private inlineForeignKeys;
126
+ /**
127
+ * Normalise one foreign key, inline or table-level, into a common shape.
128
+ *
129
+ * `.reference()` yields the resolved columns on both. The referential actions do not live
130
+ * in the same place: a built ForeignKey exposes `onDelete`/`onUpdate` as strings, while an
131
+ * unbuilt ForeignKeyBuilder exposes them as the chainable setter functions and keeps the
132
+ * values in `_onDelete`/`_onUpdate`. Reading the wrong one yields a function where a string
133
+ * is expected, so check the type rather than the property's presence.
134
+ *
135
+ * Postgres reports 'no action' where MySQL and SQLite report nothing, for identical schemas.
136
+ * Since 'no action' *is* the default, it is normalised away so the same schema analyses the
137
+ * same across dialects.
138
+ */
139
+ private readForeignKey;
140
+ /**
141
+ * Render a Drizzle SQL template back into readable text.
142
+ *
143
+ * A `sql` tagged template is stored as alternating chunks: literal fragments holding a
144
+ * string array, and column references. `String()` on that array yields "[object Object]",
145
+ * so a check constraint's expression has to be assembled rather than stringified.
146
+ * Anything unrecognised becomes `?`, which is honest about the gap without inventing SQL.
147
+ */
148
+ private renderSql;
149
+ /** A value produced by Drizzle's `relations()` helper: a source table plus a callback. */
150
+ private isRelationsObject;
151
+ /**
152
+ * Read the relations declared by `relations(table, ({ one, many }) => ...)`.
153
+ *
154
+ * The previous implementation looked for `val.config.relations`. That property does not
155
+ * exist: `config` is a *function*, so the expression was always undefined and the branch
156
+ * never ran. Nothing failed loudly, relations simply came back empty forever.
157
+ *
158
+ * `config` cannot just be read, it has to be invoked with the builder Drizzle would pass.
159
+ * Rather than depend on drizzle-orm to obtain the real builder, which this package
160
+ * deliberately does not do anywhere else, a stand-in supplies the only two functions a
161
+ * user's callback can call. `relations()` wraps that callback and calls `.withFieldName()`
162
+ * on each returned value, so the stand-in results must carry that method or the call throws.
163
+ */
164
+ private readRelationsObject;
165
+ /**
166
+ * Infer many-to-many links through a join table.
167
+ *
168
+ * A join table is taken to be one whose every column participates in a foreign key, and
169
+ * which points at exactly two distinct tables. Requiring *all* columns to be foreign keys
170
+ * is deliberate: a table carrying its own data is a real entity, not plumbing, and calling
171
+ * it a join table would invent a relation the author never declared.
172
+ */
173
+ private inferManyToMany;
79
174
  private mapColumnType;
80
175
  private analyzeTable;
81
176
  analyze(opts?: AnalyzeOptions): Promise<Analysis>;
82
177
  }
83
178
 
84
- export { type Analysis, type AnalyzeOptions, type Check, type Column, type ColumnRef, type Dialect, type Enum, type Index, type Issue, type Key, type Relation, SchemaAnalyzer, type Table, SchemaAnalyzer as default };
179
+ export { type Analysis, type AnalyzeOptions, type Check, type Column, type ColumnRef, type Dialect, type Enum, type ForeignKey, type Index, type Issue, type Key, type Relation, SchemaAnalyzer, type Table, SchemaAnalyzer as default };
package/dist/index.d.ts CHANGED
@@ -44,6 +44,21 @@ interface Check {
44
44
  name?: string;
45
45
  expression?: string;
46
46
  }
47
+ /**
48
+ * A foreign key as declared, which may span several columns. Single-column keys are also
49
+ * mirrored onto `Column.references` for convenience; composite ones exist only here, because
50
+ * they cannot be attributed to any one column.
51
+ *
52
+ * Column names are TypeScript property names, matching `Column.name`.
53
+ */
54
+ interface ForeignKey {
55
+ name?: string;
56
+ columns: string[];
57
+ foreignTable: string;
58
+ foreignColumns: string[];
59
+ onDelete?: string;
60
+ onUpdate?: string;
61
+ }
47
62
  interface Table {
48
63
  name: string;
49
64
  tsName: string;
@@ -53,6 +68,7 @@ interface Table {
53
68
  unique: Key[];
54
69
  indexes: Index[];
55
70
  checks?: Check[];
71
+ foreignKeys?: ForeignKey[];
56
72
  meta?: Record<string, unknown>;
57
73
  }
58
74
  interface Enum {
@@ -76,9 +92,88 @@ declare class SchemaAnalyzer {
76
92
  private readonly schemaPath;
77
93
  constructor(schemaPath: string);
78
94
  private getSymbol;
95
+ /**
96
+ * Drizzle keys the Columns object by TypeScript property name, but every other piece of
97
+ * metadata (foreign keys, indexes, composite primary keys) reports the *database* column
98
+ * name. We report the TS name, since that is what a generated schema has to spell, so
99
+ * anything coming from that other metadata has to be translated back.
100
+ *
101
+ * Falls through to the input when a name is unknown, which keeps raw SQL expressions and
102
+ * columns belonging to another table readable rather than dropping them.
103
+ */
104
+ private dbToTsNames;
105
+ /**
106
+ * Entries from a table's third argument, the extra-config callback.
107
+ *
108
+ * Drizzle invokes this callback with the table's ExtraConfigColumns, NOT with the table.
109
+ * Passing the table throws, and because the whole block used to sit under a bare `catch {}`
110
+ * the throw was swallowed and every index, unique index, composite primary key, check
111
+ * constraint and table-level foreign key silently vanished from the analysis.
112
+ *
113
+ * Both shapes are accepted: modern Drizzle returns an array, older versions an object.
114
+ */
115
+ private extraConfigEntries;
116
+ /**
117
+ * Foreign keys declared inline with `.references()`.
118
+ *
119
+ * Drizzle stores these per dialect under `drizzle:PgInlineForeignKeys`,
120
+ * `drizzle:MySqlInlineForeignKeys` and `drizzle:SQLiteInlineForeignKeys`. Matching the
121
+ * suffix rather than listing the three keeps new dialects working without a change here.
122
+ * SingleStore has no entry because it does not support foreign keys at all, which is why
123
+ * `.references()` is not even a function there.
124
+ */
125
+ private inlineForeignKeys;
126
+ /**
127
+ * Normalise one foreign key, inline or table-level, into a common shape.
128
+ *
129
+ * `.reference()` yields the resolved columns on both. The referential actions do not live
130
+ * in the same place: a built ForeignKey exposes `onDelete`/`onUpdate` as strings, while an
131
+ * unbuilt ForeignKeyBuilder exposes them as the chainable setter functions and keeps the
132
+ * values in `_onDelete`/`_onUpdate`. Reading the wrong one yields a function where a string
133
+ * is expected, so check the type rather than the property's presence.
134
+ *
135
+ * Postgres reports 'no action' where MySQL and SQLite report nothing, for identical schemas.
136
+ * Since 'no action' *is* the default, it is normalised away so the same schema analyses the
137
+ * same across dialects.
138
+ */
139
+ private readForeignKey;
140
+ /**
141
+ * Render a Drizzle SQL template back into readable text.
142
+ *
143
+ * A `sql` tagged template is stored as alternating chunks: literal fragments holding a
144
+ * string array, and column references. `String()` on that array yields "[object Object]",
145
+ * so a check constraint's expression has to be assembled rather than stringified.
146
+ * Anything unrecognised becomes `?`, which is honest about the gap without inventing SQL.
147
+ */
148
+ private renderSql;
149
+ /** A value produced by Drizzle's `relations()` helper: a source table plus a callback. */
150
+ private isRelationsObject;
151
+ /**
152
+ * Read the relations declared by `relations(table, ({ one, many }) => ...)`.
153
+ *
154
+ * The previous implementation looked for `val.config.relations`. That property does not
155
+ * exist: `config` is a *function*, so the expression was always undefined and the branch
156
+ * never ran. Nothing failed loudly, relations simply came back empty forever.
157
+ *
158
+ * `config` cannot just be read, it has to be invoked with the builder Drizzle would pass.
159
+ * Rather than depend on drizzle-orm to obtain the real builder, which this package
160
+ * deliberately does not do anywhere else, a stand-in supplies the only two functions a
161
+ * user's callback can call. `relations()` wraps that callback and calls `.withFieldName()`
162
+ * on each returned value, so the stand-in results must carry that method or the call throws.
163
+ */
164
+ private readRelationsObject;
165
+ /**
166
+ * Infer many-to-many links through a join table.
167
+ *
168
+ * A join table is taken to be one whose every column participates in a foreign key, and
169
+ * which points at exactly two distinct tables. Requiring *all* columns to be foreign keys
170
+ * is deliberate: a table carrying its own data is a real entity, not plumbing, and calling
171
+ * it a join table would invent a relation the author never declared.
172
+ */
173
+ private inferManyToMany;
79
174
  private mapColumnType;
80
175
  private analyzeTable;
81
176
  analyze(opts?: AnalyzeOptions): Promise<Analysis>;
82
177
  }
83
178
 
84
- export { type Analysis, type AnalyzeOptions, type Check, type Column, type ColumnRef, type Dialect, type Enum, type Index, type Issue, type Key, type Relation, SchemaAnalyzer, type Table, SchemaAnalyzer as default };
179
+ export { type Analysis, type AnalyzeOptions, type Check, type Column, type ColumnRef, type Dialect, type Enum, type ForeignKey, type Index, type Issue, type Key, type Relation, SchemaAnalyzer, type Table, SchemaAnalyzer as default };
package/dist/index.js CHANGED
@@ -16,6 +16,197 @@ var SchemaAnalyzer = class {
16
16
  }
17
17
  return table[Symbol.for(key)];
18
18
  }
19
+ /**
20
+ * Drizzle keys the Columns object by TypeScript property name, but every other piece of
21
+ * metadata (foreign keys, indexes, composite primary keys) reports the *database* column
22
+ * name. We report the TS name, since that is what a generated schema has to spell, so
23
+ * anything coming from that other metadata has to be translated back.
24
+ *
25
+ * Falls through to the input when a name is unknown, which keeps raw SQL expressions and
26
+ * columns belonging to another table readable rather than dropping them.
27
+ */
28
+ dbToTsNames(columnsObj) {
29
+ const map = /* @__PURE__ */ new Map();
30
+ for (const [tsName, col] of Object.entries(columnsObj ?? {})) {
31
+ const dbName = col?.name;
32
+ if (typeof dbName === "string") map.set(dbName, tsName);
33
+ }
34
+ return (dbName) => {
35
+ const raw = typeof dbName === "string" ? dbName : dbName?.name ?? String(dbName);
36
+ return map.get(raw) ?? raw;
37
+ };
38
+ }
39
+ /**
40
+ * Entries from a table's third argument, the extra-config callback.
41
+ *
42
+ * Drizzle invokes this callback with the table's ExtraConfigColumns, NOT with the table.
43
+ * Passing the table throws, and because the whole block used to sit under a bare `catch {}`
44
+ * the throw was swallowed and every index, unique index, composite primary key, check
45
+ * constraint and table-level foreign key silently vanished from the analysis.
46
+ *
47
+ * Both shapes are accepted: modern Drizzle returns an array, older versions an object.
48
+ */
49
+ extraConfigEntries(tbl, issues, tableName) {
50
+ const builder = this.getSymbol(tbl, "drizzle:ExtraConfigBuilder");
51
+ if (typeof builder !== "function") return [];
52
+ const cols = this.getSymbol(tbl, "drizzle:ExtraConfigColumns") ?? tbl;
53
+ try {
54
+ const built = builder(cols);
55
+ if (!built) return [];
56
+ return Array.isArray(built) ? built : Object.values(built);
57
+ } catch (e) {
58
+ issues.push({
59
+ code: "DRZL_ANL_EXTRACONFIG",
60
+ level: "warn",
61
+ message: `Could not evaluate the extra-config callback for table "${tableName}": ${e.message}`,
62
+ hint: "Indexes, composite keys, checks and table-level foreign keys will be missing for this table."
63
+ });
64
+ return [];
65
+ }
66
+ }
67
+ /**
68
+ * Foreign keys declared inline with `.references()`.
69
+ *
70
+ * Drizzle stores these per dialect under `drizzle:PgInlineForeignKeys`,
71
+ * `drizzle:MySqlInlineForeignKeys` and `drizzle:SQLiteInlineForeignKeys`. Matching the
72
+ * suffix rather than listing the three keeps new dialects working without a change here.
73
+ * SingleStore has no entry because it does not support foreign keys at all, which is why
74
+ * `.references()` is not even a function there.
75
+ */
76
+ inlineForeignKeys(tbl) {
77
+ try {
78
+ for (const s of Object.getOwnPropertySymbols(tbl)) {
79
+ if (/InlineForeignKeys$/.test(s.description ?? "")) {
80
+ const v = tbl[s];
81
+ if (Array.isArray(v)) return v;
82
+ }
83
+ }
84
+ } catch {
85
+ }
86
+ return [];
87
+ }
88
+ /**
89
+ * Normalise one foreign key, inline or table-level, into a common shape.
90
+ *
91
+ * `.reference()` yields the resolved columns on both. The referential actions do not live
92
+ * in the same place: a built ForeignKey exposes `onDelete`/`onUpdate` as strings, while an
93
+ * unbuilt ForeignKeyBuilder exposes them as the chainable setter functions and keeps the
94
+ * values in `_onDelete`/`_onUpdate`. Reading the wrong one yields a function where a string
95
+ * is expected, so check the type rather than the property's presence.
96
+ *
97
+ * Postgres reports 'no action' where MySQL and SQLite report nothing, for identical schemas.
98
+ * Since 'no action' *is* the default, it is normalised away so the same schema analyses the
99
+ * same across dialects.
100
+ */
101
+ readForeignKey(fk, toTs) {
102
+ let ref;
103
+ try {
104
+ ref = fk?.reference?.();
105
+ } catch {
106
+ return void 0;
107
+ }
108
+ if (!ref?.foreignTable) return void 0;
109
+ const action = (v, fallback) => {
110
+ const raw = typeof v === "string" ? v : typeof fallback === "string" ? fallback : void 0;
111
+ return raw && raw.toLowerCase() !== "no action" ? raw : void 0;
112
+ };
113
+ const foreignColumnsObj = this.getSymbol(ref.foreignTable, "drizzle:Columns") ?? {};
114
+ const toForeignTs = this.dbToTsNames(foreignColumnsObj);
115
+ return {
116
+ columns: (ref.columns ?? []).map((c) => toTs(c?.name)),
117
+ foreignTable: this.getSymbol(ref.foreignTable, "drizzle:Name") ?? "unknown",
118
+ foreignColumns: (ref.foreignColumns ?? []).map((c) => toForeignTs(c?.name)),
119
+ onDelete: action(fk?.onDelete, fk?._onDelete),
120
+ onUpdate: action(fk?.onUpdate, fk?._onUpdate),
121
+ name: typeof fk?.getName === "function" ? void 0 : fk?.name
122
+ };
123
+ }
124
+ /**
125
+ * Render a Drizzle SQL template back into readable text.
126
+ *
127
+ * A `sql` tagged template is stored as alternating chunks: literal fragments holding a
128
+ * string array, and column references. `String()` on that array yields "[object Object]",
129
+ * so a check constraint's expression has to be assembled rather than stringified.
130
+ * Anything unrecognised becomes `?`, which is honest about the gap without inventing SQL.
131
+ */
132
+ renderSql(value, toTs) {
133
+ const chunks = value?.queryChunks;
134
+ if (!Array.isArray(chunks)) return String(value ?? "");
135
+ return chunks.map((c) => {
136
+ if (Array.isArray(c?.value)) return c.value.join("");
137
+ if (typeof c?.name === "string") return toTs(c.name);
138
+ if (c?.queryChunks) return this.renderSql(c, toTs);
139
+ return "?";
140
+ }).join("").trim();
141
+ }
142
+ /** A value produced by Drizzle's `relations()` helper: a source table plus a callback. */
143
+ isRelationsObject(val) {
144
+ return !!val && typeof val === "object" && typeof val.config === "function" && !!this.getSymbol(val.table, "drizzle:Columns");
145
+ }
146
+ /**
147
+ * Read the relations declared by `relations(table, ({ one, many }) => ...)`.
148
+ *
149
+ * The previous implementation looked for `val.config.relations`. That property does not
150
+ * exist: `config` is a *function*, so the expression was always undefined and the branch
151
+ * never ran. Nothing failed loudly, relations simply came back empty forever.
152
+ *
153
+ * `config` cannot just be read, it has to be invoked with the builder Drizzle would pass.
154
+ * Rather than depend on drizzle-orm to obtain the real builder, which this package
155
+ * deliberately does not do anywhere else, a stand-in supplies the only two functions a
156
+ * user's callback can call. `relations()` wraps that callback and calls `.withFieldName()`
157
+ * on each returned value, so the stand-in results must carry that method or the call throws.
158
+ */
159
+ readRelationsObject(val, exportName, issues) {
160
+ const from = this.getSymbol(val.table, "drizzle:Name") ?? exportName;
161
+ const make = (kind) => (table, cfg) => ({
162
+ kind,
163
+ referencedTable: table,
164
+ cfg,
165
+ withFieldName(n) {
166
+ this.fieldName = n;
167
+ return this;
168
+ }
169
+ });
170
+ try {
171
+ const built = val.config({ one: make("one"), many: make("many") });
172
+ const out = [];
173
+ for (const rel of Object.values(built ?? {})) {
174
+ const to = this.getSymbol(rel?.referencedTable, "drizzle:Name");
175
+ if (to) out.push({ kind: rel.kind, from, to });
176
+ }
177
+ return out;
178
+ } catch (e) {
179
+ issues.push({
180
+ code: "DRZL_ANL_RELATIONS",
181
+ level: "warn",
182
+ message: `Could not read the relations declared in "${exportName}": ${e.message}`,
183
+ hint: "Relations for this table will be missing from the analysis."
184
+ });
185
+ return [];
186
+ }
187
+ }
188
+ /**
189
+ * Infer many-to-many links through a join table.
190
+ *
191
+ * A join table is taken to be one whose every column participates in a foreign key, and
192
+ * which points at exactly two distinct tables. Requiring *all* columns to be foreign keys
193
+ * is deliberate: a table carrying its own data is a real entity, not plumbing, and calling
194
+ * it a join table would invent a relation the author never declared.
195
+ */
196
+ inferManyToMany(tables) {
197
+ const out = [];
198
+ for (const t of tables) {
199
+ const fks = t.foreignKeys ?? [];
200
+ if (fks.length < 2) continue;
201
+ const fkCols = new Set(fks.flatMap((f) => f.columns));
202
+ if (!t.columns.every((c) => fkCols.has(c.name))) continue;
203
+ const targets = [...new Set(fks.map((f) => f.foreignTable))];
204
+ if (targets.length !== 2) continue;
205
+ out.push({ kind: "manyToMany", from: targets[0], to: targets[1], via: t.name });
206
+ out.push({ kind: "manyToMany", from: targets[1], to: targets[0], via: t.name });
207
+ }
208
+ return out;
209
+ }
19
210
  mapColumnType(column) {
20
211
  const ctor = column?.constructor?.name ?? "";
21
212
  switch (ctor) {
@@ -31,14 +222,17 @@ var SchemaAnalyzer = class {
31
222
  case "SQLiteBlob":
32
223
  return { tsType: "Uint8Array", dbType: "BLOB" };
33
224
  case "SQLiteNumeric":
34
- return { tsType: "number", dbType: "NUMERIC" };
225
+ return { tsType: "string", dbType: "NUMERIC" };
35
226
  case "SQLiteBoolean":
36
227
  return { tsType: "boolean", dbType: "INTEGER" };
37
228
  case "PgInteger":
38
229
  case "PgSmallInt":
39
230
  return { tsType: "number", dbType: "INTEGER" };
40
231
  case "PgBigInt":
41
- return { tsType: "bigint", dbType: "BIGINT" };
232
+ return {
233
+ tsType: column?.config?.mode === "number" ? "number" : "bigint",
234
+ dbType: "BIGINT"
235
+ };
42
236
  case "PgSerial":
43
237
  case "PgSmallSerial":
44
238
  case "PgBigSerial":
@@ -56,9 +250,10 @@ var SchemaAnalyzer = class {
56
250
  case "PgDate":
57
251
  return { tsType: "Date", dbType: "TIMESTAMP" };
58
252
  case "PgNumeric":
253
+ return { tsType: "string", dbType: "NUMERIC" };
59
254
  case "PgFloat":
60
255
  case "PgDoublePrecision":
61
- return { tsType: "number", dbType: "NUMERIC" };
256
+ return { tsType: "number", dbType: "DOUBLE" };
62
257
  case "PgJson":
63
258
  case "PgJsonb":
64
259
  return { tsType: "any", dbType: ctor === "PgJsonb" ? "JSONB" : "JSON" };
@@ -137,12 +332,13 @@ var SchemaAnalyzer = class {
137
332
  return { tsType: "unknown", dbType: "UNKNOWN" };
138
333
  }
139
334
  }
140
- analyzeTable(tsName, tbl) {
335
+ analyzeTable(tsName, tbl, issues = []) {
141
336
  const columnsObj = this.getSymbol(tbl, "drizzle:Columns") ?? {};
142
337
  const columns = [];
143
338
  const unique = [];
144
339
  const indexes = [];
145
340
  const checks = [];
341
+ const foreignKeys = [];
146
342
  const pkCols = [];
147
343
  const uniqueGroups = /* @__PURE__ */ new Map();
148
344
  for (const [colName, col] of Object.entries(columnsObj)) {
@@ -155,13 +351,7 @@ var SchemaAnalyzer = class {
155
351
  const nullable = !col?.notNull && !col?.config?.notNull;
156
352
  const isGenerated = !!(col?.autoIncrement || col?.isGenerated);
157
353
  const hasDefault = col?.default !== void 0 || col?.config?.default !== void 0 || isGenerated;
158
- const ref = col?.references;
159
- const references = ref ? {
160
- table: ref.table ?? "unknown",
161
- column: ref.column ?? "id",
162
- onDelete: ref.onDelete,
163
- onUpdate: ref.onUpdate
164
- } : void 0;
354
+ const references = void 0;
165
355
  const isUnique = !!(col?.isUnique || col?.config?.isUnique);
166
356
  const isPk = !!(col?.primary || col?.config?.primaryKey);
167
357
  if (isPk) pkCols.push(colName);
@@ -186,10 +376,11 @@ var SchemaAnalyzer = class {
186
376
  }
187
377
  const name = this.getSymbol(tbl, "drizzle:Name") || tsName;
188
378
  const schema = this.getSymbol(tbl, "drizzle:Schema");
379
+ const toTs = this.dbToTsNames(columnsObj);
189
380
  try {
190
- const pkDef = tbl[Symbol.for("drizzle:PrimaryKey")];
381
+ const pkDef = this.getSymbol(tbl, "drizzle:PrimaryKey");
191
382
  if (pkDef && Array.isArray(pkDef.columns)) {
192
- const cols = pkDef.columns.map((c) => c?.name ?? String(c)).filter(Boolean);
383
+ const cols = pkDef.columns.map((c) => toTs(c?.name)).filter(Boolean);
193
384
  if (cols.length) {
194
385
  pkCols.splice(0, pkCols.length, ...cols);
195
386
  }
@@ -197,36 +388,50 @@ var SchemaAnalyzer = class {
197
388
  } catch {
198
389
  }
199
390
  try {
200
- const idxDef = tbl[Symbol.for("drizzle:Indexes")];
391
+ const idxDef = this.getSymbol(tbl, "drizzle:Indexes");
201
392
  if (Array.isArray(idxDef)) {
202
393
  for (const i of idxDef) {
203
- const cols = (i?.columns ?? []).map((c) => c?.name ?? String(c)).filter(Boolean);
394
+ const cols = (i?.columns ?? []).map((c) => toTs(c?.name)).filter(Boolean);
204
395
  if (cols.length) indexes.push({ columns: cols, name: i?.name });
205
396
  if (i?.unique && cols.length) unique.push({ columns: cols });
206
397
  }
207
398
  }
208
399
  } catch {
209
400
  }
210
- try {
211
- const builder = tbl[Symbol.for("drizzle:ExtraConfigBuilder")];
212
- if (typeof builder === "function") {
213
- const built = builder(tbl);
214
- const entries = Array.isArray(built) ? built.map((v, i) => [v?.config?.name ?? v?.name ?? `idx_${i}`, v]) : Object.entries(built ?? {});
215
- for (const [key, val] of entries) {
216
- const cfg = val?.config ?? val;
217
- const cols = (cfg?.columns ?? []).map((c) => c?.name ?? String(c)).filter(Boolean);
218
- const uniqueFlag = !!(cfg?.unique || /unique/i.test(val?.constructor?.name ?? "") || /unique/i.test(key));
219
- const idxName = cfg?.name ?? val?.name ?? key;
220
- const expr = cfg?.where || cfg?.expression;
221
- if (expr && !cols.length) {
222
- checks.push({ name: idxName, expression: String(expr) });
223
- } else if (cols.length) {
224
- indexes.push({ columns: cols, name: idxName });
225
- if (uniqueFlag) unique.push({ columns: cols });
226
- }
227
- }
401
+ for (const entry of this.extraConfigEntries(tbl, issues, name)) {
402
+ if (typeof entry?.reference === "function") {
403
+ const fk = this.readForeignKey(entry, toTs);
404
+ if (fk) foreignKeys.push(fk);
405
+ continue;
228
406
  }
229
- } catch {
407
+ if (entry?.value?.queryChunks && entry?.name !== void 0) {
408
+ checks.push({ name: entry.name, expression: this.renderSql(entry.value, toTs) });
409
+ continue;
410
+ }
411
+ const cfg = entry?.config ?? entry ?? {};
412
+ const cols = (cfg.columns ?? []).map((c) => toTs(c?.name)).filter(Boolean);
413
+ if (!cols.length) continue;
414
+ if (cfg.unique === void 0) {
415
+ pkCols.splice(0, pkCols.length, ...cols);
416
+ continue;
417
+ }
418
+ indexes.push({ columns: cols, name: cfg.name });
419
+ if (cfg.unique) unique.push({ columns: cols, name: cfg.name });
420
+ }
421
+ for (const fk of this.inlineForeignKeys(tbl)) {
422
+ const read = this.readForeignKey(fk, toTs);
423
+ if (read) foreignKeys.push(read);
424
+ }
425
+ for (const fk of foreignKeys) {
426
+ if (fk.columns.length !== 1 || fk.foreignColumns.length !== 1) continue;
427
+ const col = columns.find((c) => c.name === fk.columns[0]);
428
+ if (!col) continue;
429
+ col.references = {
430
+ table: fk.foreignTable,
431
+ column: fk.foreignColumns[0],
432
+ onDelete: fk.onDelete,
433
+ onUpdate: fk.onUpdate
434
+ };
230
435
  }
231
436
  return {
232
437
  name,
@@ -240,6 +445,7 @@ var SchemaAnalyzer = class {
240
445
  ],
241
446
  indexes: [...pkCols.length ? [{ columns: pkCols }] : [], ...indexes],
242
447
  checks,
448
+ foreignKeys,
243
449
  meta: {}
244
450
  };
245
451
  }
@@ -275,39 +481,29 @@ var SchemaAnalyzer = class {
275
481
  const tables = [];
276
482
  const relations = [];
277
483
  const enums = [];
484
+ const columnEnums = [];
278
485
  for (const [name, val] of Object.entries(exportsObj)) {
279
486
  try {
280
487
  const cols = this.getSymbol(val, "drizzle:Columns");
281
488
  if (cols && typeof cols === "object") {
282
- const table = this.analyzeTable(name, val);
489
+ const table = this.analyzeTable(name, val, issues);
283
490
  tables.push(table);
284
- if (opts.includeRelations) {
285
- for (const col of table.columns) {
286
- if (col.references) {
287
- relations.push({
288
- kind: "many",
289
- from: table.name,
290
- to: col.references.table
291
- });
292
- }
293
- const enumVals = cols[col.name]?.enumValues;
294
- if (enumVals && enumVals.length) {
295
- const enumName = `${table.name}_${col.name}_enum`;
296
- if (!enums.find((e) => e.name === enumName))
297
- enums.push({ name: enumName, values: enumVals });
298
- }
491
+ for (const col of table.columns) {
492
+ const enumVals = cols[col.name]?.enumValues;
493
+ if (enumVals && enumVals.length) {
494
+ columnEnums.push({ name: `${table.name}_${col.name}_enum`, values: enumVals });
299
495
  }
300
496
  }
301
- } else if (val?.config?.relations) {
302
- const cfg = val.config.relations;
303
- const base = name.replace(/Relations$/, "");
304
- for (const entry of Object.values(cfg)) {
305
- const target = entry?.referencedTable;
306
- const targetName = target ? this.getSymbol(target, "drizzle:Name") : void 0;
307
- if (targetName) {
308
- relations.push({ kind: "many", from: base, to: targetName });
497
+ if (opts.includeRelations) {
498
+ for (const fk of table.foreignKeys ?? []) {
499
+ relations.push({ kind: "one", from: table.name, to: fk.foreignTable });
500
+ relations.push({ kind: "many", from: fk.foreignTable, to: table.name });
309
501
  }
310
502
  }
503
+ } else if (this.isRelationsObject(val)) {
504
+ if (opts.includeRelations) {
505
+ relations.push(...this.readRelationsObject(val, name, issues));
506
+ }
311
507
  } else {
312
508
  const ev = val?.enumValues;
313
509
  if (Array.isArray(ev) && ev.every((x) => typeof x === "string")) {
@@ -329,6 +525,12 @@ var SchemaAnalyzer = class {
329
525
  });
330
526
  }
331
527
  }
528
+ for (const candidate of columnEnums) {
529
+ const key = JSON.stringify(candidate.values);
530
+ if (enums.some((e) => JSON.stringify(e.values) === key)) continue;
531
+ if (enums.some((e) => e.name === candidate.name)) continue;
532
+ enums.push(candidate);
533
+ }
332
534
  let dialect = "unknown";
333
535
  const ctorNames = /* @__PURE__ */ new Set();
334
536
  for (const [_, val] of Object.entries(exportsObj)) {
@@ -352,6 +554,9 @@ var SchemaAnalyzer = class {
352
554
  );
353
555
  if (looksSqlite) dialect = "sqlite";
354
556
  }
557
+ if (opts.includeRelations) {
558
+ relations.push(...this.inferManyToMany(tables));
559
+ }
355
560
  if (opts.includeRelations && opts.includeHeuristicRelations) {
356
561
  const tableNames = new Set(tables.map((t) => t.name));
357
562
  const findTarget = (base) => {
@@ -362,19 +567,27 @@ var SchemaAnalyzer = class {
362
567
  };
363
568
  for (const t of tables) {
364
569
  for (const c of t.columns) {
570
+ if (c.references) continue;
365
571
  if (c.name.endsWith("Id")) {
366
572
  const base = c.name.slice(0, -2);
367
573
  const target = findTarget(base);
368
- if (target) relations.push({ kind: "many", from: t.name, to: target });
574
+ if (target) relations.push({ kind: "one", from: t.name, to: target });
369
575
  }
370
576
  }
371
577
  }
372
578
  }
579
+ const seen = /* @__PURE__ */ new Set();
580
+ const deduped = relations.filter((r) => {
581
+ const key = `${r.kind}|${r.from}|${r.to}|${r.via ?? ""}`;
582
+ if (seen.has(key)) return false;
583
+ seen.add(key);
584
+ return true;
585
+ });
373
586
  return {
374
587
  dialect,
375
588
  tables,
376
589
  enums,
377
- relations,
590
+ relations: deduped,
378
591
  issues
379
592
  };
380
593
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drzl/analyzer",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -21,6 +21,7 @@
21
21
  "jiti": "^2.5.1"
22
22
  },
23
23
  "devDependencies": {
24
+ "drizzle-orm": "^0.45.2",
24
25
  "tsup": "^8.5.0",
25
26
  "typescript": "^5.9.2"
26
27
  },