fix(dbml): resolve commented cross-file // Ref: lines
Commented refs are collected per file and resolved against the combined model after all inputs are loaded (directory, --from-list, merge, jobs). Matched refs become FKs and relationships; duplicates of existing FKs are skipped; missing targets are skipped with a warning; column type mismatches warn. Also keep reused index names within a DBML table instead of overwriting, give a second FK to the same table a distinct relationship name, and make the pgsql writer match relationships to FKs by name first.
This commit is contained in:
+69
-14
@@ -48,7 +48,22 @@ func (r *Reader) ReadDatabase() (*models.Database, error) {
|
||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
return r.parseDBML(string(content))
|
||||
db, err := r.parseDBML(string(content))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.resolveCommentedRefs(db)
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// resolveCommentedRefs resolves the commented refs whose tables are loaded.
|
||||
// Unmatched refs stay pending for a later pass over a combined model.
|
||||
func (r *Reader) resolveCommentedRefs(db *models.Database) {
|
||||
for _, w := range ResolveCommentedRefs(db, false) {
|
||||
if r.options.Progress != nil {
|
||||
r.options.Progress("warning: " + w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReadSchema reads and parses DBML input, returning a Schema model
|
||||
@@ -125,6 +140,7 @@ func (r *Reader) readDirectoryDBML(dirPath string) (*models.Database, error) {
|
||||
}
|
||||
}
|
||||
|
||||
r.resolveCommentedRefs(db)
|
||||
return db, nil
|
||||
}
|
||||
|
||||
@@ -440,6 +456,10 @@ func mergeDatabase(baseDB, fileDB *models.Database) {
|
||||
// Merge domains
|
||||
baseDB.Domains = append(baseDB.Domains, fileDB.Domains...)
|
||||
|
||||
for _, ref := range PendingCommentedRefs(fileDB) {
|
||||
addPendingCommentedRef(baseDB, ref)
|
||||
}
|
||||
|
||||
// Use first non-empty description
|
||||
if baseDB.Description == "" && fileDB.Description != "" {
|
||||
baseDB.Description = fileDB.Description
|
||||
@@ -493,8 +513,12 @@ func (r *Reader) parseDBML(content string) (*models.Database, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip empty lines and comments
|
||||
// Skip empty lines and comments. A commented `// Ref:` is kept as a
|
||||
// pending cross-file ref, resolved once every file is loaded.
|
||||
if line == "" || strings.HasPrefix(line, "//") {
|
||||
if ref, ok := commentedRef(line); ok {
|
||||
addPendingCommentedRef(db, ref)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -581,7 +605,13 @@ func (r *Reader) parseDBML(content string) (*models.Database, error) {
|
||||
|
||||
index := r.parseIndex(line, currentTable.Name, currentSchema)
|
||||
if index != nil {
|
||||
currentTable.Indexes[index.Name] = index
|
||||
// Keep a reused name under a unique map key so the duplicate is
|
||||
// not silently dropped; the inspector reports it.
|
||||
key := index.Name
|
||||
for n := 2; currentTable.Indexes[key] != nil; n++ {
|
||||
key = fmt.Sprintf("%s#%d", index.Name, n)
|
||||
}
|
||||
currentTable.Indexes[key] = index
|
||||
lastIndex = index
|
||||
}
|
||||
continue
|
||||
@@ -659,20 +689,12 @@ func (r *Reader) parseDBML(content string) (*models.Database, error) {
|
||||
// for DBML refs so diffing equivalent schemas compares the same model.
|
||||
for _, schema := range schemaMap {
|
||||
for _, table := range schema.Tables {
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, name := range sortedConstraintNames(table.Constraints) {
|
||||
constraint := table.Constraints[name]
|
||||
if constraint.Type != models.ForeignKeyConstraint {
|
||||
continue
|
||||
}
|
||||
name := fmt.Sprintf("%s_to_%s", table.Name, constraint.ReferencedTable)
|
||||
relationship := models.InitRelationship(name, models.OneToMany)
|
||||
relationship.FromTable = table.Name
|
||||
relationship.FromSchema = table.Schema
|
||||
relationship.FromColumns = append([]string(nil), constraint.Columns...)
|
||||
relationship.ToTable = constraint.ReferencedTable
|
||||
relationship.ToSchema = constraint.ReferencedSchema
|
||||
relationship.ToColumns = append([]string(nil), constraint.ReferencedColumns...)
|
||||
relationship.ForeignKey = constraint.Name
|
||||
table.Relationships[name] = relationship
|
||||
addFKRelationship(table, constraint)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -685,6 +707,39 @@ func (r *Reader) parseDBML(content string) (*models.Database, error) {
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// sortedConstraintNames returns constraint keys in sorted order so derived
|
||||
// relationship names do not depend on map iteration order.
|
||||
func sortedConstraintNames(constraints map[string]*models.Constraint) []string {
|
||||
names := make([]string, 0, len(constraints))
|
||||
for name := range constraints {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
// addFKRelationship derives the relationship for a foreign key, matching how
|
||||
// the PostgreSQL reader models FKs.
|
||||
func addFKRelationship(table *models.Table, constraint *models.Constraint) {
|
||||
name := fmt.Sprintf("%s_to_%s", table.Name, constraint.ReferencedTable)
|
||||
if existing, taken := table.Relationships[name]; taken && existing.ForeignKey != constraint.Name {
|
||||
// A second FK to the same table must not overwrite the first.
|
||||
name = fmt.Sprintf("%s_%s", name, strings.Join(constraint.Columns, "_"))
|
||||
}
|
||||
relationship := models.InitRelationship(name, models.OneToMany)
|
||||
relationship.FromTable = table.Name
|
||||
relationship.FromSchema = table.Schema
|
||||
relationship.FromColumns = append([]string(nil), constraint.Columns...)
|
||||
relationship.ToTable = constraint.ReferencedTable
|
||||
relationship.ToSchema = constraint.ReferencedSchema
|
||||
relationship.ToColumns = append([]string(nil), constraint.ReferencedColumns...)
|
||||
relationship.ForeignKey = constraint.Name
|
||||
if table.Relationships == nil {
|
||||
table.Relationships = make(map[string]*models.Relationship)
|
||||
}
|
||||
table.Relationships[name] = relationship
|
||||
}
|
||||
|
||||
// setTableNote preserves multiple table notes. The first maps to Description
|
||||
// and the second to Comment, matching the model fields used by code writers.
|
||||
func setTableNote(table *models.Table, note string) {
|
||||
|
||||
Reference in New Issue
Block a user