Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d44945b475 | ||
|
|
3b88c386a1 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Maintainer: Hein (Warky Devs) <hein@warky.dev>
|
||||
pkgname=relspec
|
||||
pkgver=1.0.65
|
||||
pkgver=1.0.66
|
||||
pkgrel=1
|
||||
pkgdesc="RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs."
|
||||
arch=('x86_64' 'aarch64')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: relspec
|
||||
Version: 1.0.65
|
||||
Version: 1.0.66
|
||||
Release: 1%{?dist}
|
||||
Summary: RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs.
|
||||
|
||||
|
||||
+44
-16
@@ -2,10 +2,22 @@ package diff
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
|
||||
// sortedKeys returns a map's keys sorted alphabetically, so callers get a
|
||||
// deterministic iteration order instead of Go's randomized map order.
|
||||
func sortedKeys[T any](m map[string]T) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// CompareDatabases compares two database models and returns the differences
|
||||
func CompareDatabases(source, target *models.Database) *DiffResult {
|
||||
result := &DiffResult{
|
||||
@@ -34,7 +46,8 @@ func compareSchemas(source, target []*models.Schema) *SchemaDiff {
|
||||
}
|
||||
|
||||
// Find missing and modified schemas
|
||||
for name, srcSchema := range sourceMap {
|
||||
for _, name := range sortedKeys(sourceMap) {
|
||||
srcSchema := sourceMap[name]
|
||||
if tgtSchema, exists := targetMap[name]; !exists {
|
||||
diff.Missing = append(diff.Missing, srcSchema)
|
||||
} else {
|
||||
@@ -45,7 +58,8 @@ func compareSchemas(source, target []*models.Schema) *SchemaDiff {
|
||||
}
|
||||
|
||||
// Find extra schemas
|
||||
for name, tgtSchema := range targetMap {
|
||||
for _, name := range sortedKeys(targetMap) {
|
||||
tgtSchema := targetMap[name]
|
||||
if _, exists := sourceMap[name]; !exists {
|
||||
diff.Extra = append(diff.Extra, tgtSchema)
|
||||
}
|
||||
@@ -106,7 +120,8 @@ func compareTables(source, target []*models.Table) *TableDiff {
|
||||
}
|
||||
|
||||
// Find missing and modified tables
|
||||
for name, srcTable := range sourceMap {
|
||||
for _, name := range sortedKeys(sourceMap) {
|
||||
srcTable := sourceMap[name]
|
||||
if tgtTable, exists := targetMap[name]; !exists {
|
||||
diff.Missing = append(diff.Missing, srcTable)
|
||||
} else {
|
||||
@@ -117,7 +132,8 @@ func compareTables(source, target []*models.Table) *TableDiff {
|
||||
}
|
||||
|
||||
// Find extra tables
|
||||
for name, tgtTable := range targetMap {
|
||||
for _, name := range sortedKeys(targetMap) {
|
||||
tgtTable := targetMap[name]
|
||||
if _, exists := sourceMap[name]; !exists {
|
||||
diff.Extra = append(diff.Extra, tgtTable)
|
||||
}
|
||||
@@ -176,7 +192,8 @@ func compareColumns(source, target map[string]*models.Column) *ColumnDiff {
|
||||
}
|
||||
|
||||
// Find missing and modified columns
|
||||
for name, srcCol := range source {
|
||||
for _, name := range sortedKeys(source) {
|
||||
srcCol := source[name]
|
||||
if tgtCol, exists := target[name]; !exists {
|
||||
diff.Missing = append(diff.Missing, srcCol)
|
||||
} else {
|
||||
@@ -192,7 +209,8 @@ func compareColumns(source, target map[string]*models.Column) *ColumnDiff {
|
||||
}
|
||||
|
||||
// Find extra columns
|
||||
for name, tgtCol := range target {
|
||||
for _, name := range sortedKeys(target) {
|
||||
tgtCol := target[name]
|
||||
if _, exists := source[name]; !exists {
|
||||
diff.Extra = append(diff.Extra, tgtCol)
|
||||
}
|
||||
@@ -240,7 +258,8 @@ func compareIndexes(source, target map[string]*models.Index) *IndexDiff {
|
||||
}
|
||||
|
||||
// Find missing and modified indexes
|
||||
for name, srcIdx := range source {
|
||||
for _, name := range sortedKeys(source) {
|
||||
srcIdx := source[name]
|
||||
if tgtIdx, exists := target[name]; !exists {
|
||||
diff.Missing = append(diff.Missing, srcIdx)
|
||||
} else {
|
||||
@@ -256,7 +275,8 @@ func compareIndexes(source, target map[string]*models.Index) *IndexDiff {
|
||||
}
|
||||
|
||||
// Find extra indexes
|
||||
for name, tgtIdx := range target {
|
||||
for _, name := range sortedKeys(target) {
|
||||
tgtIdx := target[name]
|
||||
if _, exists := source[name]; !exists {
|
||||
diff.Extra = append(diff.Extra, tgtIdx)
|
||||
}
|
||||
@@ -292,7 +312,8 @@ func compareConstraints(source, target map[string]*models.Constraint) *Constrain
|
||||
}
|
||||
|
||||
// Find missing and modified constraints
|
||||
for name, srcCon := range source {
|
||||
for _, name := range sortedKeys(source) {
|
||||
srcCon := source[name]
|
||||
if tgtCon, exists := target[name]; !exists {
|
||||
diff.Missing = append(diff.Missing, srcCon)
|
||||
} else {
|
||||
@@ -308,7 +329,8 @@ func compareConstraints(source, target map[string]*models.Constraint) *Constrain
|
||||
}
|
||||
|
||||
// Find extra constraints
|
||||
for name, tgtCon := range target {
|
||||
for _, name := range sortedKeys(target) {
|
||||
tgtCon := target[name]
|
||||
if _, exists := source[name]; !exists {
|
||||
diff.Extra = append(diff.Extra, tgtCon)
|
||||
}
|
||||
@@ -350,7 +372,8 @@ func compareRelationships(source, target map[string]*models.Relationship) *Relat
|
||||
}
|
||||
|
||||
// Find missing and modified relationships
|
||||
for name, srcRel := range source {
|
||||
for _, name := range sortedKeys(source) {
|
||||
srcRel := source[name]
|
||||
if tgtRel, exists := target[name]; !exists {
|
||||
diff.Missing = append(diff.Missing, srcRel)
|
||||
} else {
|
||||
@@ -366,7 +389,8 @@ func compareRelationships(source, target map[string]*models.Relationship) *Relat
|
||||
}
|
||||
|
||||
// Find extra relationships
|
||||
for name, tgtRel := range target {
|
||||
for _, name := range sortedKeys(target) {
|
||||
tgtRel := target[name]
|
||||
if _, exists := source[name]; !exists {
|
||||
diff.Extra = append(diff.Extra, tgtRel)
|
||||
}
|
||||
@@ -415,7 +439,8 @@ func compareViews(source, target []*models.View) *ViewDiff {
|
||||
}
|
||||
|
||||
// Find missing and modified views
|
||||
for name, srcView := range sourceMap {
|
||||
for _, name := range sortedKeys(sourceMap) {
|
||||
srcView := sourceMap[name]
|
||||
if tgtView, exists := targetMap[name]; !exists {
|
||||
diff.Missing = append(diff.Missing, srcView)
|
||||
} else {
|
||||
@@ -431,7 +456,8 @@ func compareViews(source, target []*models.View) *ViewDiff {
|
||||
}
|
||||
|
||||
// Find extra views
|
||||
for name, tgtView := range targetMap {
|
||||
for _, name := range sortedKeys(targetMap) {
|
||||
tgtView := targetMap[name]
|
||||
if _, exists := sourceMap[name]; !exists {
|
||||
diff.Extra = append(diff.Extra, tgtView)
|
||||
}
|
||||
@@ -468,7 +494,8 @@ func compareSequences(source, target []*models.Sequence) *SequenceDiff {
|
||||
}
|
||||
|
||||
// Find missing and modified sequences
|
||||
for name, srcSeq := range sourceMap {
|
||||
for _, name := range sortedKeys(sourceMap) {
|
||||
srcSeq := sourceMap[name]
|
||||
if tgtSeq, exists := targetMap[name]; !exists {
|
||||
diff.Missing = append(diff.Missing, srcSeq)
|
||||
} else {
|
||||
@@ -484,7 +511,8 @@ func compareSequences(source, target []*models.Sequence) *SequenceDiff {
|
||||
}
|
||||
|
||||
// Find extra sequences
|
||||
for name, tgtSeq := range targetMap {
|
||||
for _, name := range sortedKeys(targetMap) {
|
||||
tgtSeq := targetMap[name]
|
||||
if _, exists := sourceMap[name]; !exists {
|
||||
diff.Extra = append(diff.Extra, tgtSeq)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package diff
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
@@ -140,6 +141,46 @@ func TestCompareColumns(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCompareColumns_Deterministic verifies that Missing/Extra entries are
|
||||
// always reported in the same (alphabetical) order across repeated calls,
|
||||
// instead of following Go's randomized map iteration order over the
|
||||
// source/target column maps.
|
||||
func TestCompareColumns_Deterministic(t *testing.T) {
|
||||
source := map[string]*models.Column{
|
||||
"zeta": {Name: "zeta", Type: "text"},
|
||||
"alpha": {Name: "alpha", Type: "text"},
|
||||
"mu": {Name: "mu", Type: "text"},
|
||||
}
|
||||
target := map[string]*models.Column{
|
||||
"omega": {Name: "omega", Type: "text"},
|
||||
"delta": {Name: "delta", Type: "text"},
|
||||
"charlie": {Name: "charlie", Type: "text"},
|
||||
}
|
||||
|
||||
wantMissing := []string{"alpha", "mu", "zeta"}
|
||||
wantExtra := []string{"charlie", "delta", "omega"}
|
||||
|
||||
for i := 0; i < 25; i++ {
|
||||
got := compareColumns(source, target)
|
||||
|
||||
gotMissing := make([]string, len(got.Missing))
|
||||
for j, c := range got.Missing {
|
||||
gotMissing[j] = c.Name
|
||||
}
|
||||
gotExtra := make([]string, len(got.Extra))
|
||||
for j, c := range got.Extra {
|
||||
gotExtra[j] = c.Name
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(gotMissing, wantMissing) {
|
||||
t.Fatalf("compareColumns() Missing = %v, want %v (run %d)", gotMissing, wantMissing, i)
|
||||
}
|
||||
if !reflect.DeepEqual(gotExtra, wantExtra) {
|
||||
t.Fatalf("compareColumns() Extra = %v, want %v (run %d)", gotExtra, wantExtra, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareColumnDetails(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -2,6 +2,7 @@ package inspector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
@@ -54,8 +55,15 @@ func NewInspector(db *models.Database, config *Config) *Inspector {
|
||||
func (i *Inspector) Inspect() (*InspectorReport, error) {
|
||||
results := []ValidationResult{}
|
||||
|
||||
// Run all enabled validators
|
||||
for ruleName, rule := range i.config.Rules {
|
||||
// Run all enabled validators in deterministic (alphabetical) rule-name order
|
||||
ruleNames := make([]string, 0, len(i.config.Rules))
|
||||
for ruleName := range i.config.Rules {
|
||||
ruleNames = append(ruleNames, ruleName)
|
||||
}
|
||||
sort.Strings(ruleNames)
|
||||
|
||||
for _, ruleName := range ruleNames {
|
||||
rule := i.config.Rules[ruleName]
|
||||
if !rule.IsEnabled() {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -51,6 +51,45 @@ func TestInspect(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestInspect_Deterministic verifies that repeated Inspect() calls against
|
||||
// the same database and config produce violations in the same order, instead
|
||||
// of following Go's randomized map iteration order over config.Rules and the
|
||||
// per-table Columns/Constraints/Indexes maps.
|
||||
func TestInspect_Deterministic(t *testing.T) {
|
||||
db := createTestDatabase()
|
||||
config := GetDefaultConfig()
|
||||
|
||||
inspector := NewInspector(db, config)
|
||||
|
||||
first, err := inspector.Inspect()
|
||||
if err != nil {
|
||||
t.Fatalf("Inspect() returned error: %v", err)
|
||||
}
|
||||
|
||||
wantOrder := make([]string, len(first.Violations))
|
||||
for i, v := range first.Violations {
|
||||
wantOrder[i] = v.RuleName + "|" + v.Location
|
||||
}
|
||||
|
||||
for i := 0; i < 25; i++ {
|
||||
report, err := inspector.Inspect()
|
||||
if err != nil {
|
||||
t.Fatalf("Inspect() returned error on run %d: %v", i, err)
|
||||
}
|
||||
|
||||
if len(report.Violations) != len(wantOrder) {
|
||||
t.Fatalf("run %d: got %d violations, want %d", i, len(report.Violations), len(wantOrder))
|
||||
}
|
||||
|
||||
for j, v := range report.Violations {
|
||||
got := v.RuleName + "|" + v.Location
|
||||
if got != wantOrder[j] {
|
||||
t.Fatalf("run %d: violation[%d] = %q, want %q", i, j, got, wantOrder[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectWithDisabledRules(t *testing.T) {
|
||||
db := createTestDatabase()
|
||||
config := GetDefaultConfig()
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -199,12 +200,18 @@ func (f *MarkdownFormatter) formatContext(context map[string]interface{}) string
|
||||
"column": true,
|
||||
}
|
||||
|
||||
for key, value := range context {
|
||||
keys := make([]string, 0, len(context))
|
||||
for key := range context {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
if skipKeys[key] {
|
||||
continue
|
||||
}
|
||||
|
||||
parts = append(parts, fmt.Sprintf("%s=%v", key, value))
|
||||
parts = append(parts, fmt.Sprintf("%s=%v", key, context[key]))
|
||||
}
|
||||
|
||||
return strings.Join(parts, ", ")
|
||||
|
||||
+54
-12
@@ -2,12 +2,54 @@ package inspector
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/pgsql"
|
||||
)
|
||||
|
||||
// sortedKeys returns a map's keys sorted alphabetically, so validators report
|
||||
// violations in a deterministic order instead of Go's randomized map order.
|
||||
func sortedKeys[T any](m map[string]T) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// sortColumns returns columns sorted by Sequence then Name for deterministic output.
|
||||
func sortColumns(columns map[string]*models.Column) []*models.Column {
|
||||
result := make([]*models.Column, 0, len(columns))
|
||||
for _, col := range columns {
|
||||
result = append(result, col)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// sortConstraints returns constraints sorted by Sequence then Name for deterministic output.
|
||||
func sortConstraints(constraints map[string]*models.Constraint) []*models.Constraint {
|
||||
result := make([]*models.Constraint, 0, len(constraints))
|
||||
for _, c := range constraints {
|
||||
result = append(result, c)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// validatePrimaryKeyNaming checks that primary key column names match a pattern
|
||||
func validatePrimaryKeyNaming(db *models.Database, rule Rule, ruleName string) []ValidationResult {
|
||||
results := []ValidationResult{}
|
||||
@@ -18,7 +60,7 @@ func validatePrimaryKeyNaming(db *models.Database, rule Rule, ruleName string) [
|
||||
|
||||
for _, schema := range db.Schemas {
|
||||
for _, table := range schema.Tables {
|
||||
for _, col := range table.Columns {
|
||||
for _, col := range sortColumns(table.Columns) {
|
||||
if col.IsPrimaryKey {
|
||||
location := formatLocation(schema.Name, table.Name, col.Name)
|
||||
passed := pattern.MatchString(col.Name)
|
||||
@@ -49,7 +91,7 @@ func validatePrimaryKeyDatatype(db *models.Database, rule Rule, ruleName string)
|
||||
|
||||
for _, schema := range db.Schemas {
|
||||
for _, table := range schema.Tables {
|
||||
for _, col := range table.Columns {
|
||||
for _, col := range sortColumns(table.Columns) {
|
||||
if col.IsPrimaryKey {
|
||||
location := formatLocation(schema.Name, table.Name, col.Name)
|
||||
|
||||
@@ -84,7 +126,7 @@ func validatePrimaryKeyAutoIncrement(db *models.Database, rule Rule, ruleName st
|
||||
|
||||
for _, schema := range db.Schemas {
|
||||
for _, table := range schema.Tables {
|
||||
for _, col := range table.Columns {
|
||||
for _, col := range sortColumns(table.Columns) {
|
||||
if col.IsPrimaryKey {
|
||||
location := formatLocation(schema.Name, table.Name, col.Name)
|
||||
|
||||
@@ -125,7 +167,7 @@ func validateForeignKeyColumnNaming(db *models.Database, rule Rule, ruleName str
|
||||
for _, schema := range db.Schemas {
|
||||
for _, table := range schema.Tables {
|
||||
// Check foreign key constraints
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type == models.ForeignKeyConstraint {
|
||||
for _, colName := range constraint.Columns {
|
||||
location := formatLocation(schema.Name, table.Name, colName)
|
||||
@@ -163,7 +205,7 @@ func validateForeignKeyConstraintNaming(db *models.Database, rule Rule, ruleName
|
||||
|
||||
for _, schema := range db.Schemas {
|
||||
for _, table := range schema.Tables {
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type == models.ForeignKeyConstraint {
|
||||
location := formatLocation(schema.Name, table.Name, "")
|
||||
passed := pattern.MatchString(constraint.Name)
|
||||
@@ -209,7 +251,7 @@ func validateForeignKeyIndex(db *models.Database, rule Rule, ruleName string) []
|
||||
}
|
||||
|
||||
// Check if each FK column has an index
|
||||
for fkCol := range fkColumns {
|
||||
for _, fkCol := range sortedKeys(fkColumns) {
|
||||
hasIndex := false
|
||||
|
||||
// Check table indexes
|
||||
@@ -282,7 +324,7 @@ func validateColumnNamingCase(db *models.Database, rule Rule, ruleName string) [
|
||||
|
||||
for _, schema := range db.Schemas {
|
||||
for _, table := range schema.Tables {
|
||||
for _, col := range table.Columns {
|
||||
for _, col := range sortColumns(table.Columns) {
|
||||
location := formatLocation(schema.Name, table.Name, col.Name)
|
||||
passed := pattern.MatchString(col.Name)
|
||||
|
||||
@@ -339,7 +381,7 @@ func validateColumnNameLength(db *models.Database, rule Rule, ruleName string) [
|
||||
|
||||
for _, schema := range db.Schemas {
|
||||
for _, table := range schema.Tables {
|
||||
for _, col := range table.Columns {
|
||||
for _, col := range sortColumns(table.Columns) {
|
||||
location := formatLocation(schema.Name, table.Name, col.Name)
|
||||
passed := len(col.Name) <= rule.MaxLength
|
||||
|
||||
@@ -396,7 +438,7 @@ func validateReservedKeywords(db *models.Database, rule Rule, ruleName string) [
|
||||
|
||||
// Check column names
|
||||
if rule.CheckColumns {
|
||||
for _, col := range table.Columns {
|
||||
for _, col := range sortColumns(table.Columns) {
|
||||
location := formatLocation(schema.Name, table.Name, col.Name)
|
||||
passed := !keywords[strings.ToUpper(col.Name)]
|
||||
|
||||
@@ -479,7 +521,7 @@ func validateOrphanedForeignKey(db *models.Database, rule Rule, ruleName string)
|
||||
// Check all foreign key constraints
|
||||
for _, schema := range db.Schemas {
|
||||
for _, table := range schema.Tables {
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type == models.ForeignKeyConstraint {
|
||||
// Build referenced table key
|
||||
refSchema := constraint.ReferencedSchema
|
||||
@@ -522,7 +564,7 @@ func validateCircularDependency(db *models.Database, rule Rule, ruleName string)
|
||||
for _, table := range schema.Tables {
|
||||
tableKey := schema.Name + "." + table.Name
|
||||
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type == models.ForeignKeyConstraint {
|
||||
refSchema := constraint.ReferencedSchema
|
||||
if refSchema == "" {
|
||||
@@ -537,7 +579,7 @@ func validateCircularDependency(db *models.Database, rule Rule, ruleName string)
|
||||
}
|
||||
|
||||
// Check for cycles using DFS
|
||||
for tableKey := range dependencies {
|
||||
for _, tableKey := range sortedKeys(dependencies) {
|
||||
visited := make(map[string]bool)
|
||||
recStack := make(map[string]bool)
|
||||
|
||||
|
||||
+12
-2
@@ -5,6 +5,7 @@ package merge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -156,8 +157,17 @@ func (r *MergeResult) mergeColumns(table *models.Table, srcTable *models.Table)
|
||||
existingColumns[colName] = table.Columns[colName]
|
||||
}
|
||||
|
||||
// Merge columns
|
||||
for colName, srcCol := range srcTable.Columns {
|
||||
// Merge columns in deterministic (alphabetical) order so that, when a
|
||||
// TypeConflicts entry is recorded, its position in the report doesn't
|
||||
// depend on Go's randomized map iteration order.
|
||||
srcColNames := make([]string, 0, len(srcTable.Columns))
|
||||
for colName := range srcTable.Columns {
|
||||
srcColNames = append(srcColNames, colName)
|
||||
}
|
||||
sort.Strings(srcColNames)
|
||||
|
||||
for _, colName := range srcColNames {
|
||||
srcCol := srcTable.Columns[colName]
|
||||
if tgtCol, exists := existingColumns[colName]; !exists {
|
||||
// Column doesn't exist, add it
|
||||
newCol := cloneColumn(srcCol)
|
||||
|
||||
+23
-1
@@ -1,6 +1,9 @@
|
||||
package models
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Flat/Denormalized Views
|
||||
//
|
||||
@@ -56,6 +59,10 @@ func (d *Database) ToFlatColumns() []*FlatColumn {
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(flatColumns, func(i, j int) bool {
|
||||
return flatColumns[i].FullyQualifiedName < flatColumns[j].FullyQualifiedName
|
||||
})
|
||||
|
||||
return flatColumns
|
||||
}
|
||||
|
||||
@@ -148,6 +155,10 @@ func (d *Database) ToFlatConstraints() []*FlatConstraint {
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(flatConstraints, func(i, j int) bool {
|
||||
return flatConstraints[i].FullyQualifiedName < flatConstraints[j].FullyQualifiedName
|
||||
})
|
||||
|
||||
return flatConstraints
|
||||
}
|
||||
|
||||
@@ -198,5 +209,16 @@ func (d *Database) ToFlatRelationships() []*FlatRelationship {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
+24
-4
@@ -5,6 +5,7 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -141,15 +142,28 @@ func (d *Table) SQLName() string {
|
||||
|
||||
// GetPrimaryKey returns the primary key column for the table, or nil if none exists.
|
||||
func (m Table) GetPrimaryKey() *Column {
|
||||
var pk *Column
|
||||
for _, column := range m.Columns {
|
||||
if column.IsPrimaryKey {
|
||||
return column
|
||||
if !column.IsPrimaryKey {
|
||||
continue
|
||||
}
|
||||
if pk == nil || columnLess(column, pk) {
|
||||
pk = column
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return pk
|
||||
}
|
||||
|
||||
// GetForeignKeys returns all foreign key constraints for the table.
|
||||
// columnLess reports whether a should sort before b, by Sequence then Name.
|
||||
func columnLess(a, b *Column) bool {
|
||||
if a.Sequence > 0 && b.Sequence > 0 {
|
||||
return a.Sequence < b.Sequence
|
||||
}
|
||||
return a.Name < b.Name
|
||||
}
|
||||
|
||||
// GetForeignKeys returns all foreign key constraints for the table, sorted
|
||||
// deterministically by Sequence then Name.
|
||||
func (m Table) GetForeignKeys() []*Constraint {
|
||||
keys := make([]*Constraint, 0)
|
||||
|
||||
@@ -158,6 +172,12 @@ func (m Table) GetForeignKeys() []*Constraint {
|
||||
keys = append(keys, c)
|
||||
}
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
if keys[i].Sequence > 0 && keys[j].Sequence > 0 {
|
||||
return keys[i].Sequence < keys[j].Sequence
|
||||
}
|
||||
return keys[i].Name < keys[j].Name
|
||||
})
|
||||
return keys
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
@@ -373,7 +374,13 @@ func (r *Reader) convertKey(dctxKey *models.DCTXKey, table *models.Table, fieldG
|
||||
if len(columns) == 0 {
|
||||
if dctxKey.Primary {
|
||||
// Look for common primary key column patterns
|
||||
colNames := make([]string, 0, len(table.Columns))
|
||||
for colName := range table.Columns {
|
||||
colNames = append(colNames, colName)
|
||||
}
|
||||
sort.Strings(colNames)
|
||||
|
||||
for _, colName := range colNames {
|
||||
colNameLower := strings.ToLower(colName)
|
||||
if strings.HasPrefix(colNameLower, "rid_") || strings.HasSuffix(colNameLower, "id") {
|
||||
columns = append(columns, colName)
|
||||
|
||||
@@ -820,17 +820,31 @@ func (r *Reader) createImplicitJoinTable(model1, model2 string, tableMap map[str
|
||||
tableMap[joinTableName] = joinTable
|
||||
}
|
||||
|
||||
// getPrimaryKeyColumn returns the primary key column of a table
|
||||
// getPrimaryKeyColumn returns the primary key column of a table. For tables
|
||||
// with a composite primary key, the column with the lowest Sequence (or,
|
||||
// failing that, the alphabetically first Name) is returned deterministically.
|
||||
func (r *Reader) getPrimaryKeyColumn(table *models.Table) *models.Column {
|
||||
if table == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var pk *models.Column
|
||||
for _, col := range table.Columns {
|
||||
if col.IsPrimaryKey {
|
||||
return col
|
||||
if !col.IsPrimaryKey {
|
||||
continue
|
||||
}
|
||||
if pk == nil {
|
||||
pk = col
|
||||
continue
|
||||
}
|
||||
if col.Sequence > 0 && pk.Sequence > 0 {
|
||||
if col.Sequence < pk.Sequence {
|
||||
pk = col
|
||||
}
|
||||
} else if col.Name < pk.Name {
|
||||
pk = col
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return pk
|
||||
}
|
||||
|
||||
@@ -806,17 +806,31 @@ func (r *Reader) createManyToManyJoinTable(entity1, entity2 string, tableMap map
|
||||
tableMap[joinTableName] = joinTable
|
||||
}
|
||||
|
||||
// getPrimaryKeyColumn returns the primary key column of a table
|
||||
// getPrimaryKeyColumn returns the primary key column of a table. For tables
|
||||
// with a composite primary key, the column with the lowest Sequence (or,
|
||||
// failing that, the alphabetically first Name) is returned deterministically.
|
||||
func (r *Reader) getPrimaryKeyColumn(table *models.Table) *models.Column {
|
||||
if table == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var pk *models.Column
|
||||
for _, col := range table.Columns {
|
||||
if col.IsPrimaryKey {
|
||||
return col
|
||||
if !col.IsPrimaryKey {
|
||||
continue
|
||||
}
|
||||
if pk == nil {
|
||||
pk = col
|
||||
continue
|
||||
}
|
||||
if col.Sequence > 0 && pk.Sequence > 0 {
|
||||
if col.Sequence < pk.Sequence {
|
||||
pk = col
|
||||
}
|
||||
} else if col.Name < pk.Name {
|
||||
pk = col
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return pk
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
|
||||
@@ -69,5 +70,6 @@ func getColumnNames(table *models.Table) []string {
|
||||
for name := range table.Columns {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package ui
|
||||
|
||||
import "git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
|
||||
// Relationship data operations - business logic for relationship management
|
||||
|
||||
@@ -111,5 +115,6 @@ func (se *SchemaEditor) GetRelationshipNames(schemaIndex, tableIndex int) []stri
|
||||
for name := range table.Relationships {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
@@ -220,8 +220,11 @@ func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper, fl
|
||||
Prefix: GeneratePrefix(table.Name),
|
||||
}
|
||||
|
||||
// Convert columns to fields (sorted by sequence or name)
|
||||
columns := sortColumns(table.Columns)
|
||||
|
||||
// Find primary key
|
||||
for _, col := range table.Columns {
|
||||
for _, col := range columns {
|
||||
if col.IsPrimaryKey {
|
||||
// Sanitize column name to remove backticks
|
||||
safeName := writers.SanitizeStructTagValue(col.Name)
|
||||
@@ -240,8 +243,6 @@ func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper, fl
|
||||
}
|
||||
}
|
||||
|
||||
// Convert columns to fields (sorted by sequence or name)
|
||||
columns := sortColumns(table.Columns)
|
||||
for _, col := range columns {
|
||||
field := columnToField(col, table, typeMapper)
|
||||
// Check for name collision with generated methods and rename if needed
|
||||
@@ -335,6 +336,21 @@ func sortConstraints(constraints map[string]*models.Constraint) []*models.Constr
|
||||
return result
|
||||
}
|
||||
|
||||
// sortIndexes sorts indexes by sequence, then by name
|
||||
func sortIndexes(indexes map[string]*models.Index) []*models.Index {
|
||||
result := make([]*models.Index, 0, len(indexes))
|
||||
for _, idx := range indexes {
|
||||
result = append(result, idx)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// sortColumns sorts columns by sequence, then by name
|
||||
func sortColumns(columns map[string]*models.Column) []*models.Column {
|
||||
result := make([]*models.Column, 0, len(columns))
|
||||
|
||||
@@ -383,7 +383,7 @@ func (tm *TypeMapper) BuildBunTag(column *models.Column, table *models.Table) st
|
||||
|
||||
// Check for indexes (unique indexes should be added to tag)
|
||||
if table != nil {
|
||||
for _, index := range table.Indexes {
|
||||
for _, index := range sortIndexes(table.Indexes) {
|
||||
if !index.Unique {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -836,6 +836,43 @@ func TestTypeMapper_BuildBunTag(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestTypeMapper_BuildBunTag_MultipleUniqueIndexesDeterministic verifies that
|
||||
// when a column belongs to more than one unique index, the "unique:" tag
|
||||
// fragments always appear in the same order across repeated calls, instead
|
||||
// of following Go's randomized map iteration order over Table.Indexes.
|
||||
func TestTypeMapper_BuildBunTag_MultipleUniqueIndexesDeterministic(t *testing.T) {
|
||||
mapper := NewTypeMapper("", "")
|
||||
table := &models.Table{
|
||||
Name: "accounts",
|
||||
Indexes: map[string]*models.Index{
|
||||
"idx_z_accounts_email_tenant": {
|
||||
Name: "idx_z_accounts_email_tenant",
|
||||
Columns: []string{"email", "tenant_id"},
|
||||
Unique: true,
|
||||
},
|
||||
"idx_a_accounts_email_region": {
|
||||
Name: "idx_a_accounts_email_region",
|
||||
Columns: []string{"email", "region_id"},
|
||||
Unique: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
column := &models.Column{Name: "email", Type: "varchar", Length: 255, NotNull: true}
|
||||
|
||||
first := mapper.BuildBunTag(column, table)
|
||||
for i := 0; i < 50; i++ {
|
||||
got := mapper.BuildBunTag(column, table)
|
||||
if got != first {
|
||||
t.Fatalf("BuildBunTag() is non-deterministic across calls: %q vs %q", first, got)
|
||||
}
|
||||
}
|
||||
|
||||
wantOrder := "unique:idx_a_accounts_email_region,unique:idx_z_accounts_email_tenant,"
|
||||
if !strings.Contains(first, wantOrder) {
|
||||
t.Errorf("BuildBunTag() = %q, want unique tags sorted by index name: %q", first, wantOrder)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTypeMapper_BuildBunTag_ArraysAreNativeInEveryMode verifies that array
|
||||
// columns always use a plain "text[]"-style type and the native Go slice
|
||||
// type plus an explicit "array" tag, regardless of NullableTypes style
|
||||
|
||||
@@ -3,6 +3,7 @@ package dbml
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
@@ -78,7 +79,7 @@ func (w *Writer) databaseToDBML(d *models.Database) string {
|
||||
sb.WriteString("\n// Relationships\n")
|
||||
for _, schema := range d.Schemas {
|
||||
for _, table := range schema.Tables {
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type == models.ForeignKeyConstraint {
|
||||
sb.WriteString(w.constraintToDBML(constraint, table))
|
||||
}
|
||||
@@ -112,7 +113,7 @@ func (w *Writer) tableToDBML(t *models.Table) string {
|
||||
tableName := fmt.Sprintf("%s.%s", t.Schema, t.Name)
|
||||
fmt.Fprintf(&sb, "Table %s {\n", tableName)
|
||||
|
||||
for _, column := range t.Columns {
|
||||
for _, column := range sortColumns(t.Columns) {
|
||||
fmt.Fprintf(&sb, " %s %s", column.Name, column.Type)
|
||||
|
||||
var attrs []string
|
||||
@@ -149,7 +150,7 @@ func (w *Writer) tableToDBML(t *models.Table) string {
|
||||
|
||||
if len(t.Indexes) > 0 {
|
||||
sb.WriteString("\n indexes {\n")
|
||||
for _, index := range t.Indexes {
|
||||
for _, index := range sortIndexes(t.Indexes) {
|
||||
var indexAttrs []string
|
||||
if index.Unique {
|
||||
indexAttrs = append(indexAttrs, "unique")
|
||||
@@ -230,3 +231,48 @@ func (w *Writer) constraintToDBML(c *models.Constraint, t *models.Table) string
|
||||
|
||||
return refLine + "\n"
|
||||
}
|
||||
|
||||
// sortColumns returns columns sorted by Sequence then Name for deterministic output.
|
||||
func sortColumns(columns map[string]*models.Column) []*models.Column {
|
||||
result := make([]*models.Column, 0, len(columns))
|
||||
for _, col := range columns {
|
||||
result = append(result, col)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// sortConstraints returns constraints sorted by Sequence then Name for deterministic output.
|
||||
func sortConstraints(constraints map[string]*models.Constraint) []*models.Constraint {
|
||||
result := make([]*models.Constraint, 0, len(constraints))
|
||||
for _, c := range constraints {
|
||||
result = append(result, c)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// sortIndexes returns indexes sorted by Sequence then Name for deterministic output.
|
||||
func sortIndexes(indexes map[string]*models.Index) []*models.Index {
|
||||
result := make([]*models.Index, 0, len(indexes))
|
||||
for _, idx := range indexes {
|
||||
result = append(result, idx)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -66,7 +66,14 @@ func (w *Writer) WriteSchema(schema *models.Schema) error {
|
||||
|
||||
// Add table-level relationships
|
||||
for _, table := range tableSlice {
|
||||
for _, rel := range table.Relationships {
|
||||
relNames := make([]string, 0, len(table.Relationships))
|
||||
for name := range table.Relationships {
|
||||
relNames = append(relNames, name)
|
||||
}
|
||||
sort.Strings(relNames)
|
||||
|
||||
for _, relName := range relNames {
|
||||
rel := table.Relationships[relName]
|
||||
// Check if this relationship is already in the list (avoid duplicates)
|
||||
isDuplicate := false
|
||||
for _, existing := range allRelations {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
@@ -175,7 +176,7 @@ func (w *Writer) databaseToDrawDB(d *models.Database) *DrawDBSchema {
|
||||
// Add relationships
|
||||
for _, schemaModel := range d.Schemas {
|
||||
for _, table := range schemaModel.Tables {
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type == models.ForeignKeyConstraint && constraint.ReferencedTable != "" {
|
||||
startTableKey := fmt.Sprintf("%s.%s", schemaModel.Name, table.Name)
|
||||
endTableKey := fmt.Sprintf("%s.%s", constraint.ReferencedSchema, constraint.ReferencedTable)
|
||||
@@ -306,7 +307,7 @@ func (w *Writer) convertTableToDrawDB(table *models.Table, schemaName string, ta
|
||||
}
|
||||
|
||||
// Add fields
|
||||
for _, column := range table.Columns {
|
||||
for _, column := range sortColumns(table.Columns) {
|
||||
field := &DrawDBField{
|
||||
ID: fieldID,
|
||||
Name: column.Name,
|
||||
@@ -339,7 +340,7 @@ func (w *Writer) convertTableToDrawDB(table *models.Table, schemaName string, ta
|
||||
|
||||
// Add indexes
|
||||
indexID := 0
|
||||
for _, index := range table.Indexes {
|
||||
for _, index := range sortIndexes(table.Indexes) {
|
||||
drawIndex := &DrawDBIndex{
|
||||
ID: indexID,
|
||||
Name: index.Name,
|
||||
@@ -393,3 +394,48 @@ func getColorForIndex(index int) string {
|
||||
}
|
||||
return colors[index%len(colors)]
|
||||
}
|
||||
|
||||
// sortColumns returns columns sorted by Sequence then Name for deterministic output.
|
||||
func sortColumns(columns map[string]*models.Column) []*models.Column {
|
||||
result := make([]*models.Column, 0, len(columns))
|
||||
for _, col := range columns {
|
||||
result = append(result, col)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// sortConstraints returns constraints sorted by Sequence then Name for deterministic output.
|
||||
func sortConstraints(constraints map[string]*models.Constraint) []*models.Constraint {
|
||||
result := make([]*models.Constraint, 0, len(constraints))
|
||||
for _, c := range constraints {
|
||||
result = append(result, c)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// sortIndexes returns indexes sorted by Sequence then Name for deterministic output.
|
||||
func sortIndexes(indexes map[string]*models.Index) []*models.Index {
|
||||
result := make([]*models.Index, 0, len(indexes))
|
||||
for _, idx := range indexes {
|
||||
result = append(result, idx)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
@@ -250,7 +251,7 @@ func (w *Writer) buildTableData(table *models.Table, schema *models.Schema, db *
|
||||
indexColumnFields := make(map[string]bool)
|
||||
|
||||
// Add indexes (excluding single-column unique indexes, which are handled inline)
|
||||
for _, index := range table.Indexes {
|
||||
for _, index := range sortIndexes(table.Indexes) {
|
||||
// Skip single-column unique indexes (handled by .unique() modifier)
|
||||
if index.Unique && len(index.Columns) == 1 {
|
||||
continue
|
||||
@@ -270,7 +271,7 @@ func (w *Writer) buildTableData(table *models.Table, schema *models.Schema, db *
|
||||
}
|
||||
|
||||
// Add multi-column unique constraints as unique indexes
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type == models.UniqueConstraint && len(constraint.Columns) > 1 {
|
||||
// Create a unique index for this constraint
|
||||
indexData := &IndexData{
|
||||
@@ -316,6 +317,36 @@ func (w *Writer) buildTableData(table *models.Table, schema *models.Schema, db *
|
||||
return tableData
|
||||
}
|
||||
|
||||
// sortIndexes returns indexes sorted by Sequence then Name for deterministic output.
|
||||
func sortIndexes(indexes map[string]*models.Index) []*models.Index {
|
||||
result := make([]*models.Index, 0, len(indexes))
|
||||
for _, idx := range indexes {
|
||||
result = append(result, idx)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// sortConstraints returns constraints sorted by Sequence then Name for deterministic output.
|
||||
func sortConstraints(constraints map[string]*models.Constraint) []*models.Constraint {
|
||||
result := make([]*models.Constraint, 0, len(constraints))
|
||||
for _, c := range constraints {
|
||||
result = append(result, c)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// sortStrings sorts a slice of strings in place
|
||||
func sortStrings(strs []string) {
|
||||
for i := 0; i < len(strs); i++ {
|
||||
@@ -422,7 +453,8 @@ func (w *Writer) getTableEnumNames(table *models.Table, schema *models.Schema, e
|
||||
enumNames := make([]string, 0)
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, col := range table.Columns {
|
||||
for _, colName := range w.getSortedColumnNames(table) {
|
||||
col := table.Columns[colName]
|
||||
if enumMap[col.Type] || enumMap[strings.ToLower(col.Type)] {
|
||||
// Find the enum in schema
|
||||
for _, enum := range schema.Enums {
|
||||
|
||||
@@ -134,8 +134,11 @@ func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper, fl
|
||||
Prefix: GeneratePrefix(table.Name),
|
||||
}
|
||||
|
||||
// Convert columns to fields (sorted by sequence or name)
|
||||
columns := sortColumns(table.Columns)
|
||||
|
||||
// Find primary key
|
||||
for _, col := range table.Columns {
|
||||
for _, col := range columns {
|
||||
if col.IsPrimaryKey {
|
||||
// Sanitize column name to remove backticks
|
||||
safeName := writers.SanitizeStructTagValue(col.Name)
|
||||
@@ -153,8 +156,6 @@ func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper, fl
|
||||
}
|
||||
}
|
||||
|
||||
// Convert columns to fields (sorted by sequence or name)
|
||||
columns := sortColumns(table.Columns)
|
||||
for _, col := range columns {
|
||||
field := columnToField(col, table, typeMapper)
|
||||
// Check for name collision with generated methods and rename if needed
|
||||
@@ -248,6 +249,21 @@ func sortConstraints(constraints map[string]*models.Constraint) []*models.Constr
|
||||
return result
|
||||
}
|
||||
|
||||
// sortIndexes sorts indexes by sequence, then by name
|
||||
func sortIndexes(indexes map[string]*models.Index) []*models.Index {
|
||||
result := make([]*models.Index, 0, len(indexes))
|
||||
for _, idx := range indexes {
|
||||
result = append(result, idx)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// sortColumns sorts columns by sequence, then by name
|
||||
func sortColumns(columns map[string]*models.Column) []*models.Column {
|
||||
result := make([]*models.Column, 0, len(columns))
|
||||
|
||||
@@ -415,7 +415,7 @@ func (tm *TypeMapper) BuildGormTag(column *models.Column, table *models.Table) s
|
||||
|
||||
// Check for unique constraint
|
||||
if table != nil {
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type == models.UniqueConstraint {
|
||||
for _, col := range constraint.Columns {
|
||||
if col == column.Name {
|
||||
@@ -431,7 +431,7 @@ func (tm *TypeMapper) BuildGormTag(column *models.Column, table *models.Table) s
|
||||
}
|
||||
|
||||
// Check for index
|
||||
for _, index := range table.Indexes {
|
||||
for _, index := range sortIndexes(table.Indexes) {
|
||||
for _, col := range index.Columns {
|
||||
if col == column.Name {
|
||||
if index.Unique {
|
||||
|
||||
@@ -757,3 +757,48 @@ func TestTypeMapper_BuildGormTag_PreservesExplicitTypeModifiers(t *testing.T) {
|
||||
t.Fatalf("type modifier appears duplicated in %q", tag)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTypeMapper_BuildGormTag_MultipleUniqueIndexesDeterministic verifies
|
||||
// that when a column belongs to a unique constraint and more than one
|
||||
// unique index, the "uniqueIndex:" tag fragments always appear in the same
|
||||
// order across repeated calls, instead of following Go's randomized map
|
||||
// iteration order over Table.Constraints and Table.Indexes.
|
||||
func TestTypeMapper_BuildGormTag_MultipleUniqueIndexesDeterministic(t *testing.T) {
|
||||
mapper := NewTypeMapper("")
|
||||
table := &models.Table{
|
||||
Name: "accounts",
|
||||
Constraints: map[string]*models.Constraint{
|
||||
"uq_z_accounts_email": {
|
||||
Name: "uq_z_accounts_email",
|
||||
Type: models.UniqueConstraint,
|
||||
Columns: []string{"email"},
|
||||
},
|
||||
},
|
||||
Indexes: map[string]*models.Index{
|
||||
"idx_z_accounts_email_tenant": {
|
||||
Name: "idx_z_accounts_email_tenant",
|
||||
Columns: []string{"email", "tenant_id"},
|
||||
Unique: true,
|
||||
},
|
||||
"idx_a_accounts_email_region": {
|
||||
Name: "idx_a_accounts_email_region",
|
||||
Columns: []string{"email", "region_id"},
|
||||
Unique: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
column := &models.Column{Name: "email", Type: "varchar", Length: 255, NotNull: true}
|
||||
|
||||
first := mapper.BuildGormTag(column, table)
|
||||
for i := 0; i < 50; i++ {
|
||||
got := mapper.BuildGormTag(column, table)
|
||||
if got != first {
|
||||
t.Fatalf("BuildGormTag() is non-deterministic across calls: %q vs %q", first, got)
|
||||
}
|
||||
}
|
||||
|
||||
wantOrder := "uniqueIndex:uq_z_accounts_email;uniqueIndex:idx_a_accounts_email_region;uniqueIndex:idx_z_accounts_email_tenant"
|
||||
if !strings.Contains(first, wantOrder) {
|
||||
t.Errorf("BuildGormTag() = %q, want uniqueIndex tags sorted (constraint before indexes, indexes by name): %q", first, wantOrder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +233,8 @@ func (w *Writer) tableToGraphQL(table *models.Table, db *models.Database, schema
|
||||
// Add relation fields
|
||||
relationFields = w.generateRelationFields(table, db, schema)
|
||||
|
||||
// Write fields in order: ID, scalars (sorted), relations (sorted)
|
||||
// Write fields in order: ID (sorted), scalars (sorted), relations (sorted)
|
||||
sort.Strings(idFields)
|
||||
for _, field := range idFields {
|
||||
sb.WriteString(field + "\n")
|
||||
}
|
||||
|
||||
@@ -239,7 +239,8 @@ func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *mod
|
||||
}
|
||||
|
||||
// Check each constraint in current database
|
||||
for constraintName, currentConstraint := range currentTable.Constraints {
|
||||
for _, currentConstraint := range sortConstraints(currentTable.Constraints) {
|
||||
constraintName := currentConstraint.Name
|
||||
modelConstraint, existsInModel := modelTable.Constraints[constraintName]
|
||||
|
||||
shouldDrop := false
|
||||
@@ -252,7 +253,8 @@ func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *mod
|
||||
if shouldDrop && currentConstraint.Type == models.PrimaryKeyConstraint {
|
||||
// Drop FK constraints that depend on this PK before dropping the PK itself.
|
||||
for _, otherTable := range current.Tables {
|
||||
for fkName, fkConstraint := range otherTable.Constraints {
|
||||
for _, fkConstraint := range sortConstraints(otherTable.Constraints) {
|
||||
fkName := fkConstraint.Name
|
||||
if fkConstraint.Type != models.ForeignKeyConstraint {
|
||||
continue
|
||||
}
|
||||
@@ -310,7 +312,8 @@ func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *mod
|
||||
}
|
||||
|
||||
// Check indexes
|
||||
for indexName, currentIndex := range currentTable.Indexes {
|
||||
for _, currentIndex := range sortIndexes(currentTable.Indexes) {
|
||||
indexName := currentIndex.Name
|
||||
modelIndex, existsInModel := modelTable.Indexes[indexName]
|
||||
|
||||
shouldDrop := false
|
||||
@@ -401,7 +404,7 @@ func (w *MigrationWriter) generateAlterTableScripts(schema *models.Schema, model
|
||||
}
|
||||
|
||||
// Check each model column
|
||||
for _, modelCol := range modelTable.Columns {
|
||||
for _, modelCol := range sortColumns(modelTable.Columns) {
|
||||
currentCol, exists := currentColumns[strings.ToLower(modelCol.Name)]
|
||||
|
||||
if !exists {
|
||||
@@ -518,7 +521,8 @@ func (w *MigrationWriter) generateIndexScripts(model *models.Schema, current *mo
|
||||
|
||||
// Process primary keys first - check explicit constraints
|
||||
foundExplicitPK := false
|
||||
for constraintName, constraint := range modelTable.Constraints {
|
||||
for _, constraint := range sortConstraints(modelTable.Constraints) {
|
||||
constraintName := constraint.Name
|
||||
if constraint.Type == models.PrimaryKeyConstraint {
|
||||
foundExplicitPK = true
|
||||
shouldCreate := true
|
||||
@@ -603,7 +607,8 @@ func (w *MigrationWriter) generateIndexScripts(model *models.Schema, current *mo
|
||||
}
|
||||
|
||||
// Process indexes
|
||||
for indexName, modelIndex := range modelTable.Indexes {
|
||||
for _, modelIndex := range sortIndexes(modelTable.Indexes) {
|
||||
indexName := modelIndex.Name
|
||||
// Skip primary key indexes
|
||||
if strings.HasPrefix(strings.ToLower(indexName), "pk_") {
|
||||
continue
|
||||
@@ -697,7 +702,8 @@ func (w *MigrationWriter) generateForeignKeyScripts(model *models.Schema, curren
|
||||
currentTable := currentTables[strings.ToLower(modelTable.Name)]
|
||||
|
||||
// Process each constraint
|
||||
for constraintName, constraint := range modelTable.Constraints {
|
||||
for _, constraint := range sortConstraints(modelTable.Constraints) {
|
||||
constraintName := constraint.Name
|
||||
if constraint.Type != models.ForeignKeyConstraint {
|
||||
continue
|
||||
}
|
||||
@@ -787,7 +793,7 @@ func (w *MigrationWriter) generateCommentScripts(model *models.Schema, current *
|
||||
}
|
||||
|
||||
// Column comments
|
||||
for _, col := range modelTable.Columns {
|
||||
for _, col := range sortColumns(modelTable.Columns) {
|
||||
if col.Description != "" {
|
||||
sql, err := w.executor.ExecuteCommentColumn(CommentColumnData{
|
||||
SchemaName: model.Name,
|
||||
|
||||
@@ -545,7 +545,7 @@ func BuildAuditFunctionData(
|
||||
|
||||
// Build list of audited columns
|
||||
auditedColumns := make([]*models.Column, 0)
|
||||
for _, col := range table.Columns {
|
||||
for _, col := range sortColumns(table.Columns) {
|
||||
if col.Name == pk.Name {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, erro
|
||||
for _, table := range schema.Tables {
|
||||
// First check for explicit PrimaryKeyConstraint
|
||||
var pkConstraint *models.Constraint
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type == models.PrimaryKeyConstraint {
|
||||
pkConstraint = constraint
|
||||
break
|
||||
@@ -255,7 +255,7 @@ func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, erro
|
||||
|
||||
// Phase 5: Indexes
|
||||
for _, table := range schema.Tables {
|
||||
for _, index := range table.Indexes {
|
||||
for _, index := range sortIndexes(table.Indexes) {
|
||||
// Skip primary key indexes
|
||||
if strings.HasSuffix(index.Name, "_pkey") {
|
||||
continue
|
||||
@@ -298,7 +298,7 @@ func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, erro
|
||||
|
||||
// Phase 5.5: Unique constraints
|
||||
for _, table := range schema.Tables {
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type != models.UniqueConstraint {
|
||||
continue
|
||||
}
|
||||
@@ -321,7 +321,7 @@ func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, erro
|
||||
|
||||
// Phase 5.7: Check constraints
|
||||
for _, table := range schema.Tables {
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type != models.CheckConstraint {
|
||||
continue
|
||||
}
|
||||
@@ -344,7 +344,7 @@ func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, erro
|
||||
|
||||
// Phase 6: Foreign keys
|
||||
for _, table := range schema.Tables {
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type != models.ForeignKeyConstraint {
|
||||
continue
|
||||
}
|
||||
@@ -394,7 +394,7 @@ func (w *Writer) GenerateSchemaStatements(schema *models.Schema) ([]string, erro
|
||||
statements = append(statements, stmt)
|
||||
}
|
||||
|
||||
for _, column := range table.Columns {
|
||||
for _, column := range sortColumns(table.Columns) {
|
||||
if column.Comment != "" {
|
||||
stmt := fmt.Sprintf("COMMENT ON COLUMN %s.%s IS '%s'",
|
||||
w.qualTable(schema.SQLName(), table.SQLName()), column.SQLName(), escapeQuote(column.Comment))
|
||||
@@ -866,10 +866,9 @@ func (w *Writer) writePrimaryKeys(schema *models.Schema) error {
|
||||
for _, table := range schema.Tables {
|
||||
// Find primary key constraint
|
||||
var pkConstraint *models.Constraint
|
||||
for name, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type == models.PrimaryKeyConstraint {
|
||||
pkConstraint = constraint
|
||||
_ = name // Use the name variable
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1475,6 +1474,51 @@ func resolveIndexColumn(table *models.Table, colName string) (*models.Column, bo
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// sortColumns returns columns sorted by Sequence then Name for deterministic output.
|
||||
func sortColumns(columns map[string]*models.Column) []*models.Column {
|
||||
result := make([]*models.Column, 0, len(columns))
|
||||
for _, col := range columns {
|
||||
result = append(result, col)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// sortConstraints returns constraints sorted by Sequence then Name for deterministic output.
|
||||
func sortConstraints(constraints map[string]*models.Constraint) []*models.Constraint {
|
||||
result := make([]*models.Constraint, 0, len(constraints))
|
||||
for _, c := range constraints {
|
||||
result = append(result, c)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// sortIndexes returns indexes sorted by Sequence then Name for deterministic output.
|
||||
func sortIndexes(indexes map[string]*models.Index) []*models.Index {
|
||||
result := make([]*models.Index, 0, len(indexes))
|
||||
for _, idx := range indexes {
|
||||
result = append(result, idx)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// formatStringList formats a list of strings as a SQL-safe comma-separated quoted list
|
||||
func formatStringList(items []string) string {
|
||||
quoted := make([]string, len(items))
|
||||
|
||||
@@ -549,14 +549,14 @@ func (w *Writer) generateBlockAttributes(table *models.Table) string {
|
||||
}
|
||||
|
||||
// @@unique for multi-column unique constraints
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type == models.UniqueConstraint && len(constraint.Columns) > 1 {
|
||||
fmt.Fprintf(&sb, " @@unique([%s])\n", strings.Join(constraint.Columns, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// @@index for indexes
|
||||
for _, index := range table.Indexes {
|
||||
for _, index := range sortIndexes(table.Indexes) {
|
||||
if !index.Unique { // Unique indexes are handled by @@unique
|
||||
fmt.Fprintf(&sb, " @@index([%s])\n", strings.Join(index.Columns, ", "))
|
||||
}
|
||||
@@ -564,3 +564,33 @@ func (w *Writer) generateBlockAttributes(table *models.Table) string {
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// sortConstraints returns constraints sorted by Sequence then Name for deterministic output.
|
||||
func sortConstraints(constraints map[string]*models.Constraint) []*models.Constraint {
|
||||
result := make([]*models.Constraint, 0, len(constraints))
|
||||
for _, c := range constraints {
|
||||
result = append(result, c)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// sortIndexes returns indexes sorted by Sequence then Name for deterministic output.
|
||||
func sortIndexes(indexes map[string]*models.Index) []*models.Index {
|
||||
result := make([]*models.Index, 0, len(indexes))
|
||||
for _, idx := range indexes {
|
||||
result = append(result, idx)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"sort"
|
||||
"text/template"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
@@ -133,15 +134,11 @@ func (te *TemplateExecutor) ExecuteCreateForeignKey(data ConstraintTemplateData)
|
||||
|
||||
// BuildTableTemplateData builds TableTemplateData from a models.Table
|
||||
func BuildTableTemplateData(schema string, table *models.Table) TableTemplateData {
|
||||
// Get sorted columns
|
||||
columns := make([]*models.Column, 0, len(table.Columns))
|
||||
for _, col := range table.Columns {
|
||||
columns = append(columns, col)
|
||||
}
|
||||
columns := sortColumns(table.Columns)
|
||||
|
||||
// Find primary key constraint
|
||||
var pk *models.Constraint
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type == models.PrimaryKeyConstraint {
|
||||
pk = constraint
|
||||
break
|
||||
@@ -151,7 +148,7 @@ func BuildTableTemplateData(schema string, table *models.Table) TableTemplateDat
|
||||
// If no explicit primary key constraint, build one from columns with IsPrimaryKey=true
|
||||
if pk == nil {
|
||||
pkCols := []string{}
|
||||
for _, col := range table.Columns {
|
||||
for _, col := range columns {
|
||||
if col.IsPrimaryKey {
|
||||
pkCols = append(pkCols, col.Name)
|
||||
}
|
||||
@@ -172,3 +169,48 @@ func BuildTableTemplateData(schema string, table *models.Table) TableTemplateDat
|
||||
PrimaryKey: pk,
|
||||
}
|
||||
}
|
||||
|
||||
// sortColumns returns columns sorted by Sequence then Name for deterministic output.
|
||||
func sortColumns(columns map[string]*models.Column) []*models.Column {
|
||||
result := make([]*models.Column, 0, len(columns))
|
||||
for _, col := range columns {
|
||||
result = append(result, col)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// sortConstraints returns constraints sorted by Sequence then Name for deterministic output.
|
||||
func sortConstraints(constraints map[string]*models.Constraint) []*models.Constraint {
|
||||
result := make([]*models.Constraint, 0, len(constraints))
|
||||
for _, c := range constraints {
|
||||
result = append(result, c)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// sortIndexes returns indexes sorted by Sequence then Name for deterministic output.
|
||||
func sortIndexes(indexes map[string]*models.Index) []*models.Index {
|
||||
result := make([]*models.Index, 0, len(indexes))
|
||||
for _, idx := range indexes {
|
||||
result = append(result, idx)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Sequence > 0 && result[j].Sequence > 0 {
|
||||
return result[i].Sequence < result[j].Sequence
|
||||
}
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ func (w *Writer) writeTable(schema string, table *models.Table) error {
|
||||
|
||||
// writeIndexes writes indexes for a table
|
||||
func (w *Writer) writeIndexes(schema string, table *models.Table) error {
|
||||
for _, index := range table.Indexes {
|
||||
for _, index := range sortIndexes(table.Indexes) {
|
||||
// Skip primary key indexes
|
||||
if strings.HasSuffix(index.Name, "_pkey") {
|
||||
continue
|
||||
@@ -174,7 +174,7 @@ func (w *Writer) writeIndexes(schema string, table *models.Table) error {
|
||||
|
||||
// writeUniqueConstraints writes unique constraints as unique indexes
|
||||
func (w *Writer) writeUniqueConstraints(schema string, table *models.Table) error {
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type != models.UniqueConstraint {
|
||||
continue
|
||||
}
|
||||
@@ -195,7 +195,7 @@ func (w *Writer) writeUniqueConstraints(schema string, table *models.Table) erro
|
||||
}
|
||||
|
||||
// Also handle unique indexes from the Indexes map
|
||||
for _, index := range table.Indexes {
|
||||
for _, index := range sortIndexes(table.Indexes) {
|
||||
if !index.Unique {
|
||||
continue
|
||||
}
|
||||
@@ -232,7 +232,7 @@ func (w *Writer) writeUniqueConstraints(schema string, table *models.Table) erro
|
||||
|
||||
// writeCheckConstraints writes check constraints as comments
|
||||
func (w *Writer) writeCheckConstraints(schema string, table *models.Table) error {
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type != models.CheckConstraint {
|
||||
continue
|
||||
}
|
||||
@@ -257,7 +257,7 @@ func (w *Writer) writeCheckConstraints(schema string, table *models.Table) error
|
||||
|
||||
// writeForeignKeys writes foreign keys as comments
|
||||
func (w *Writer) writeForeignKeys(schema string, table *models.Table) error {
|
||||
for _, constraint := range table.Constraints {
|
||||
for _, constraint := range sortConstraints(table.Constraints) {
|
||||
if constraint.Type != models.ForeignKeyConstraint {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -531,7 +531,13 @@ func (w *Writer) generateInverseRelations(table *models.Table, schema *models.Sc
|
||||
|
||||
// generateManyToManyRelations generates @ManyToMany fields
|
||||
func (w *Writer) generateManyToManyRelations(table *models.Table, schema *models.Schema, joinTables map[string]bool, sb *strings.Builder) {
|
||||
for joinTableName := range joinTables {
|
||||
joinTableNames := make([]string, 0, len(joinTables))
|
||||
for name := range joinTables {
|
||||
joinTableNames = append(joinTableNames, name)
|
||||
}
|
||||
sort.Strings(joinTableNames)
|
||||
|
||||
for _, joinTableName := range joinTableNames {
|
||||
joinTable := w.findTable(joinTableName, schema)
|
||||
if joinTable == nil {
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user