Table.Columns/Constraints/Indexes/Relationships are Go maps, and every writer, reader, diff, inspector, and merge code path that iterated them directly was subject to Go's randomized map order, so identical input could produce different output (or a different in-report violation/diff order) on every run. Most visibly this showed up as bun/gorm `unique:` struct tags changing order across consecutive `make models` runs with no source change. Fixed by sorting map iteration (by Sequence then Name, or alphabetically for string-keyed maps) everywhere the order affects generated output or first-match tie-break logic, across the bun, gorm, sqlite, dbml, drawdb, pgsql, prisma, graphql, typeorm, drizzle, and dctx writers; the dctx, prisma, and typeorm readers; the shared models.GetPrimaryKey/ GetForeignKeys helpers; pkg/diff, pkg/inspector, and pkg/merge; and the TUI column/relationship pickers in pkg/ui.
225 lines
11 KiB
Go
225 lines
11 KiB
Go
package models
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
)
|
|
|
|
// Flat/Denormalized Views
|
|
//
|
|
// This file provides flattened data structures with fully qualified names
|
|
// for easier querying and analysis of database schemas without navigating
|
|
// nested hierarchies.
|
|
|
|
// FlatColumn represents a column with full database context in a single structure.
|
|
// It includes fully qualified names for easy identification and querying.
|
|
type FlatColumn struct {
|
|
DatabaseName string `json:"database_name" yaml:"database_name" xml:"database_name"`
|
|
SchemaName string `json:"schema_name" yaml:"schema_name" xml:"schema_name"`
|
|
TableName string `json:"table_name" yaml:"table_name" xml:"table_name"`
|
|
ColumnName string `json:"column_name" yaml:"column_name" xml:"column_name"`
|
|
FullyQualifiedName string `json:"fully_qualified_name" yaml:"fully_qualified_name" xml:"fully_qualified_name"` // database.schema.table.column
|
|
Type string `json:"type" yaml:"type" xml:"type"`
|
|
Length int `json:"length,omitempty" yaml:"length,omitempty" xml:"length,omitempty"`
|
|
Precision int `json:"precision,omitempty" yaml:"precision,omitempty" xml:"precision,omitempty"`
|
|
Scale int `json:"scale,omitempty" yaml:"scale,omitempty" xml:"scale,omitempty"`
|
|
NotNull bool `json:"not_null" yaml:"not_null" xml:"not_null"`
|
|
Default any `json:"default,omitempty" yaml:"default,omitempty" xml:"default,omitempty"`
|
|
AutoIncrement bool `json:"auto_increment" yaml:"auto_increment" xml:"auto_increment"`
|
|
IsPrimaryKey bool `json:"is_primary_key" yaml:"is_primary_key" xml:"is_primary_key"`
|
|
Description string `json:"description,omitempty" yaml:"description,omitempty" xml:"description,omitempty"`
|
|
Comment string `json:"comment,omitempty" yaml:"comment,omitempty" xml:"comment,omitempty"`
|
|
}
|
|
|
|
// ToFlatColumns converts a Database to a slice of FlatColumns for denormalized access to all columns.
|
|
func (d *Database) ToFlatColumns() []*FlatColumn {
|
|
flatColumns := make([]*FlatColumn, 0)
|
|
|
|
for _, schema := range d.Schemas {
|
|
for _, table := range schema.Tables {
|
|
for _, column := range table.Columns {
|
|
flatColumns = append(flatColumns, &FlatColumn{
|
|
DatabaseName: d.Name,
|
|
SchemaName: schema.Name,
|
|
TableName: table.Name,
|
|
ColumnName: column.Name,
|
|
FullyQualifiedName: fmt.Sprintf("%s.%s.%s.%s", d.Name, schema.Name, table.Name, column.Name),
|
|
Type: column.Type,
|
|
Length: column.Length,
|
|
Precision: column.Precision,
|
|
Scale: column.Scale,
|
|
NotNull: column.NotNull,
|
|
Default: column.Default,
|
|
AutoIncrement: column.AutoIncrement,
|
|
IsPrimaryKey: column.IsPrimaryKey,
|
|
Description: column.Description,
|
|
Comment: column.Comment,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Slice(flatColumns, func(i, j int) bool {
|
|
return flatColumns[i].FullyQualifiedName < flatColumns[j].FullyQualifiedName
|
|
})
|
|
|
|
return flatColumns
|
|
}
|
|
|
|
// FlatTable represents a table with full database context and aggregated counts.
|
|
type FlatTable struct {
|
|
DatabaseName string `json:"database_name" yaml:"database_name" xml:"database_name"`
|
|
SchemaName string `json:"schema_name" yaml:"schema_name" xml:"schema_name"`
|
|
TableName string `json:"table_name" yaml:"table_name" xml:"table_name"`
|
|
FullyQualifiedName string `json:"fully_qualified_name" yaml:"fully_qualified_name" xml:"fully_qualified_name"` // database.schema.table
|
|
Description string `json:"description,omitempty" yaml:"description,omitempty" xml:"description,omitempty"`
|
|
Comment string `json:"comment,omitempty" yaml:"comment,omitempty" xml:"comment,omitempty"`
|
|
Tablespace string `json:"tablespace,omitempty" yaml:"tablespace,omitempty" xml:"tablespace,omitempty"`
|
|
ColumnCount int `json:"column_count" yaml:"column_count" xml:"column_count"`
|
|
ConstraintCount int `json:"constraint_count" yaml:"constraint_count" xml:"constraint_count"`
|
|
IndexCount int `json:"index_count" yaml:"index_count" xml:"index_count"`
|
|
}
|
|
|
|
// ToFlatTables converts a Database to a slice of FlatTables for denormalized access to all tables.
|
|
func (d *Database) ToFlatTables() []*FlatTable {
|
|
flatTables := make([]*FlatTable, 0)
|
|
|
|
for _, schema := range d.Schemas {
|
|
for _, table := range schema.Tables {
|
|
flatTables = append(flatTables, &FlatTable{
|
|
DatabaseName: d.Name,
|
|
SchemaName: schema.Name,
|
|
TableName: table.Name,
|
|
FullyQualifiedName: fmt.Sprintf("%s.%s.%s", d.Name, schema.Name, table.Name),
|
|
Description: table.Description,
|
|
Comment: table.Comment,
|
|
Tablespace: table.Tablespace,
|
|
ColumnCount: len(table.Columns),
|
|
ConstraintCount: len(table.Constraints),
|
|
IndexCount: len(table.Indexes),
|
|
})
|
|
}
|
|
}
|
|
|
|
return flatTables
|
|
}
|
|
|
|
// FlatConstraint represents a constraint with full database context and resolved references.
|
|
type FlatConstraint struct {
|
|
DatabaseName string `json:"database_name" yaml:"database_name" xml:"database_name"`
|
|
SchemaName string `json:"schema_name" yaml:"schema_name" xml:"schema_name"`
|
|
TableName string `json:"table_name" yaml:"table_name" xml:"table_name"`
|
|
ConstraintName string `json:"constraint_name" yaml:"constraint_name" xml:"constraint_name"`
|
|
FullyQualifiedName string `json:"fully_qualified_name" yaml:"fully_qualified_name" xml:"fully_qualified_name"` // database.schema.table.constraint
|
|
Type ConstraintType `json:"type" yaml:"type" xml:"type"`
|
|
Columns []string `json:"columns" yaml:"columns" xml:"columns"`
|
|
Expression string `json:"expression,omitempty" yaml:"expression,omitempty" xml:"expression,omitempty"`
|
|
ReferencedTable string `json:"referenced_table,omitempty" yaml:"referenced_table,omitempty" xml:"referenced_table,omitempty"`
|
|
ReferencedSchema string `json:"referenced_schema,omitempty" yaml:"referenced_schema,omitempty" xml:"referenced_schema,omitempty"`
|
|
ReferencedColumns []string `json:"referenced_columns,omitempty" yaml:"referenced_columns,omitempty" xml:"referenced_columns,omitempty"`
|
|
ReferencedFQN string `json:"referenced_fqn,omitempty" yaml:"referenced_fqn,omitempty" xml:"referenced_fqn,omitempty"` // Fully qualified reference
|
|
OnDelete string `json:"on_delete,omitempty" yaml:"on_delete,omitempty" xml:"on_delete,omitempty"`
|
|
OnUpdate string `json:"on_update,omitempty" yaml:"on_update,omitempty" xml:"on_update,omitempty"`
|
|
}
|
|
|
|
// ToFlatConstraints converts a Database to a slice of FlatConstraints for denormalized access to all constraints.
|
|
func (d *Database) ToFlatConstraints() []*FlatConstraint {
|
|
flatConstraints := make([]*FlatConstraint, 0)
|
|
|
|
for _, schema := range d.Schemas {
|
|
for _, table := range schema.Tables {
|
|
for _, constraint := range table.Constraints {
|
|
fc := &FlatConstraint{
|
|
DatabaseName: d.Name,
|
|
SchemaName: schema.Name,
|
|
TableName: table.Name,
|
|
ConstraintName: constraint.Name,
|
|
FullyQualifiedName: fmt.Sprintf("%s.%s.%s.%s", d.Name, schema.Name, table.Name, constraint.Name),
|
|
Type: constraint.Type,
|
|
Columns: constraint.Columns,
|
|
Expression: constraint.Expression,
|
|
ReferencedTable: constraint.ReferencedTable,
|
|
ReferencedSchema: constraint.ReferencedSchema,
|
|
ReferencedColumns: constraint.ReferencedColumns,
|
|
OnDelete: constraint.OnDelete,
|
|
OnUpdate: constraint.OnUpdate,
|
|
}
|
|
|
|
// Build fully qualified reference name for foreign keys
|
|
if constraint.Type == ForeignKeyConstraint && constraint.ReferencedTable != "" {
|
|
fc.ReferencedFQN = fmt.Sprintf("%s.%s.%s", d.Name, constraint.ReferencedSchema, constraint.ReferencedTable)
|
|
}
|
|
|
|
flatConstraints = append(flatConstraints, fc)
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Slice(flatConstraints, func(i, j int) bool {
|
|
return flatConstraints[i].FullyQualifiedName < flatConstraints[j].FullyQualifiedName
|
|
})
|
|
|
|
return flatConstraints
|
|
}
|
|
|
|
// FlatRelationship represents a relationship with full database context and fully qualified table names.
|
|
type FlatRelationship struct {
|
|
DatabaseName string `json:"database_name" yaml:"database_name" xml:"database_name"`
|
|
RelationshipName string `json:"relationship_name" yaml:"relationship_name" xml:"relationship_name"`
|
|
Type RelationType `json:"type" yaml:"type" xml:"type"`
|
|
FromFQN string `json:"from_fqn" yaml:"from_fqn" xml:"from_fqn"` // database.schema.table
|
|
ToFQN string `json:"to_fqn" yaml:"to_fqn" xml:"to_fqn"` // database.schema.table
|
|
FromTable string `json:"from_table" yaml:"from_table" xml:"from_table"`
|
|
FromSchema string `json:"from_schema" yaml:"from_schema" xml:"from_schema"`
|
|
ToTable string `json:"to_table" yaml:"to_table" xml:"to_table"`
|
|
ToSchema string `json:"to_schema" yaml:"to_schema" xml:"to_schema"`
|
|
ForeignKey string `json:"foreign_key" yaml:"foreign_key" xml:"foreign_key"`
|
|
ThroughTableFQN string `json:"through_table_fqn,omitempty" yaml:"through_table_fqn,omitempty" xml:"through_table_fqn,omitempty"`
|
|
Description string `json:"description,omitempty" yaml:"description,omitempty" xml:"description,omitempty"`
|
|
}
|
|
|
|
// ToFlatRelationships converts a Database to a slice of FlatRelationships for denormalized access to all relationships.
|
|
func (d *Database) ToFlatRelationships() []*FlatRelationship {
|
|
flatRelationships := make([]*FlatRelationship, 0)
|
|
|
|
for _, schema := range d.Schemas {
|
|
for _, table := range schema.Tables {
|
|
for _, relationship := range table.Relationships {
|
|
fr := &FlatRelationship{
|
|
DatabaseName: d.Name,
|
|
RelationshipName: relationship.Name,
|
|
Type: relationship.Type,
|
|
FromFQN: fmt.Sprintf("%s.%s.%s", d.Name, relationship.FromSchema, relationship.FromTable),
|
|
ToFQN: fmt.Sprintf("%s.%s.%s", d.Name, relationship.ToSchema, relationship.ToTable),
|
|
FromTable: relationship.FromTable,
|
|
FromSchema: relationship.FromSchema,
|
|
ToTable: relationship.ToTable,
|
|
ToSchema: relationship.ToSchema,
|
|
ForeignKey: relationship.ForeignKey,
|
|
Description: relationship.Description,
|
|
}
|
|
|
|
// Add through table FQN for many-to-many relationships
|
|
if relationship.ThroughTable != "" {
|
|
fr.ThroughTableFQN = fmt.Sprintf("%s.%s.%s", d.Name, relationship.ThroughSchema, relationship.ThroughTable)
|
|
}
|
|
|
|
flatRelationships = append(flatRelationships, fr)
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Slice(flatRelationships, func(i, j int) bool {
|
|
a, b := flatRelationships[i], flatRelationships[j]
|
|
if a.FromFQN != b.FromFQN {
|
|
return a.FromFQN < b.FromFQN
|
|
}
|
|
if a.RelationshipName != b.RelationshipName {
|
|
return a.RelationshipName < b.RelationshipName
|
|
}
|
|
return a.ToFQN < b.ToFQN
|
|
})
|
|
|
|
return flatRelationships
|
|
}
|