• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rogerpadilla / uql / 20834688025

08 Jan 2026 10:59PM UTC coverage: 94.064% (-4.3%) from 98.408%
20834688025

Pull #81

github

web-flow
Merge 1f9607626 into 097cb85cf
Pull Request #81: feat: implementation of unified ORM schema synchronization system via Schema AST

2290 of 2605 branches covered (87.91%)

Branch coverage included in aggregate %.

1766 of 1890 new or added lines in 37 files covered. (93.44%)

13 existing lines in 1 file now uncovered.

5079 of 5229 relevant lines covered (97.13%)

181.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.35
/packages/core/src/migrate/codegen/smartRelationDetector.ts
1
/**
2
 * Smart Relation Detector
3
 *
4
 * Uses heuristics and schema analysis to detect relationships between tables:
5
 * - Explicit foreign keys (highest confidence)
6
 * - Junction tables for ManyToMany
7
 * - Unique FK columns for OneToOne
8
 */
9

10
import type { SchemaAST } from '../../schema/schemaAST.js';
11
import type { DetectedRelation } from '../../schema/types.js';
12
import { singularize } from '../../util/string.util.js';
13

14
/**
15
 * Configuration for relation detection.
16
 */
17
export interface RelationDetectorOptions {
18
  /** Minimum confidence threshold (0-1) to include in results */
19
  minConfidence?: number;
20
  /** Custom singularize function */
21
  singularize?: (name: string) => string;
22
}
23

24
/**
25
 * Detects relationships in a SchemaAST using multiple heuristics.
26
 */
27
export class SmartRelationDetector {
28
  private readonly options: Required<RelationDetectorOptions>;
29

30
  constructor(
31
    private readonly ast: SchemaAST,
7✔
32
    options: RelationDetectorOptions = {},
7✔
33
  ) {
34
    this.options = {
7✔
35
      minConfidence: options.minConfidence ?? 0.5,
13✔
36
      singularize: options.singularize ?? this.defaultSingularize.bind(this),
14✔
37
    };
38
  }
39

40
  /**
41
   * Detect all relationships in the schema.
42
   */
43
  detectAll(): DetectedRelation[] {
44
    const relations: DetectedRelation[] = [];
4✔
45

46
    // 1. Add explicit FK relationships (confidence: 1.0)
47
    for (const rel of this.ast.relationships) {
4✔
48
      relations.push({
4✔
49
        type: rel.type,
50
        from: {
51
          table: rel.from.table,
52
          columns: rel.from.columns,
53
        },
54
        to: {
55
          table: rel.to.table,
56
          columns: rel.to.columns,
57
        },
58
        through: rel.through,
59
        confidence: 1.0,
60
        source: 'explicit_fk',
61
      });
62
    }
63

64
    // 2. Detect junction tables (ManyToMany)
65
    const junctionRelations = this.detectJunctionTables();
4✔
66
    relations.push(...junctionRelations);
4✔
67

68
    // 3. Detect unique FK -> OneToOne upgrades
69
    const oneToOneUpgrades = this.detectOneToOneRelations(relations);
4✔
70
    relations.push(...oneToOneUpgrades);
4✔
71

72
    // Filter by confidence
73
    return relations.filter((r) => r.confidence >= this.options.minConfidence);
7✔
74
  }
75

76
  /**
77
   * Detect junction tables that represent ManyToMany relationships.
78
   */
79
  private detectJunctionTables(): DetectedRelation[] {
80
    const relations: DetectedRelation[] = [];
4✔
81

82
    for (const table of this.ast.tables.values()) {
4✔
83
      if (!this.ast.isJunctionTable(table)) continue;
7✔
84

85
      const outgoingRels = table.outgoingRelations;
1✔
86
      if (outgoingRels.length !== 2) continue;
1!
87

88
      const [rel1, rel2] = outgoingRels;
1✔
89

90
      // Create ManyToMany relation
91
      relations.push({
1✔
92
        type: 'ManyToMany',
93
        from: {
94
          table: rel1.to.table,
95
          columns: rel1.to.columns,
96
        },
97
        to: {
98
          table: rel2.to.table,
99
          columns: rel2.to.columns,
100
        },
101
        through: table,
102
        confidence: 0.95,
103
        source: 'junction_table',
104
      });
105

106
      // Also create inverse relation
107
      relations.push({
1✔
108
        type: 'ManyToMany',
109
        from: {
110
          table: rel2.to.table,
111
          columns: rel2.to.columns,
112
        },
113
        to: {
114
          table: rel1.to.table,
115
          columns: rel1.to.columns,
116
        },
117
        through: table,
118
        confidence: 0.95,
119
        source: 'junction_table',
120
      });
121
    }
122

123
    return relations;
4✔
124
  }
125

126
  /**
127
   * Detect relations where unique FK should upgrade to OneToOne.
128
   */
129
  private detectOneToOneRelations(existingRelations: DetectedRelation[]): DetectedRelation[] {
130
    const upgrades: DetectedRelation[] = [];
4✔
131

132
    for (const rel of existingRelations) {
4✔
133
      if (rel.source !== 'explicit_fk') continue;
6✔
134

135
      const fromCol = rel.from.columns[0];
4✔
136
      if (fromCol?.isUnique && rel.type === 'ManyToOne') {
4✔
137
        // Upgrade to OneToOne
138
        upgrades.push({
1✔
139
          type: 'OneToOne',
140
          from: rel.from,
141
          to: rel.to,
142
          through: rel.through,
143
          confidence: 0.9,
144
          source: 'unique_fk',
145
        });
146
      }
147
    }
148

149
    return upgrades;
4✔
150
  }
151

152
  /**
153
   * Default singularize function (delegates to shared utility).
154
   */
155
  private defaultSingularize(name: string): string {
NEW
156
    return singularize(name);
×
157
  }
158
}
159

160
/**
161
 * Create a SmartRelationDetector for the given AST.
162
 */
163
export function createRelationDetector(ast: SchemaAST, options?: RelationDetectorOptions): SmartRelationDetector {
164
  return new SmartRelationDetector(ast, options);
1✔
165
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc