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.
217 lines
6.3 KiB
Go
217 lines
6.3 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"bytes"
|
|
"embed"
|
|
"fmt"
|
|
"sort"
|
|
"text/template"
|
|
|
|
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
|
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
|
)
|
|
|
|
//go:embed templates/*.tmpl
|
|
var templateFS embed.FS
|
|
|
|
// TemplateExecutor manages and executes SQLite SQL templates
|
|
type TemplateExecutor struct {
|
|
templates *template.Template
|
|
options *writers.WriterOptions
|
|
}
|
|
|
|
// NewTemplateExecutor creates a new template executor for SQLite
|
|
func NewTemplateExecutor(opts *writers.WriterOptions) (*TemplateExecutor, error) {
|
|
// Create template with SQLite-specific functions
|
|
funcMap := GetTemplateFuncs(opts)
|
|
|
|
tmpl, err := template.New("").Funcs(funcMap).ParseFS(templateFS, "templates/*.tmpl")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse templates: %w", err)
|
|
}
|
|
|
|
return &TemplateExecutor{
|
|
templates: tmpl,
|
|
options: opts,
|
|
}, nil
|
|
}
|
|
|
|
// Template data structures
|
|
|
|
// TableTemplateData contains data for table template
|
|
type TableTemplateData struct {
|
|
Schema string
|
|
Name string
|
|
Columns []*models.Column
|
|
PrimaryKey *models.Constraint
|
|
}
|
|
|
|
// IndexTemplateData contains data for index template
|
|
type IndexTemplateData struct {
|
|
Schema string
|
|
Table string
|
|
Name string
|
|
Columns []string
|
|
}
|
|
|
|
// ConstraintTemplateData contains data for constraint templates
|
|
type ConstraintTemplateData struct {
|
|
Schema string
|
|
Table string
|
|
Name string
|
|
Columns []string
|
|
Expression string
|
|
ForeignSchema string
|
|
ForeignTable string
|
|
ForeignColumns []string
|
|
OnDelete string
|
|
OnUpdate string
|
|
}
|
|
|
|
// Execute methods
|
|
|
|
// ExecutePragmaForeignKeys executes the pragma foreign keys template
|
|
func (te *TemplateExecutor) ExecutePragmaForeignKeys() (string, error) {
|
|
var buf bytes.Buffer
|
|
err := te.templates.ExecuteTemplate(&buf, "pragma_foreign_keys.tmpl", nil)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to execute pragma_foreign_keys template: %w", err)
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
// ExecuteCreateTable executes the create table template
|
|
func (te *TemplateExecutor) ExecuteCreateTable(data TableTemplateData) (string, error) {
|
|
var buf bytes.Buffer
|
|
err := te.templates.ExecuteTemplate(&buf, "create_table.tmpl", data)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to execute create_table template: %w", err)
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
// ExecuteCreateIndex executes the create index template
|
|
func (te *TemplateExecutor) ExecuteCreateIndex(data IndexTemplateData) (string, error) {
|
|
var buf bytes.Buffer
|
|
err := te.templates.ExecuteTemplate(&buf, "create_index.tmpl", data)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to execute create_index template: %w", err)
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
// ExecuteCreateUniqueConstraint executes the create unique constraint template
|
|
func (te *TemplateExecutor) ExecuteCreateUniqueConstraint(data ConstraintTemplateData) (string, error) {
|
|
var buf bytes.Buffer
|
|
err := te.templates.ExecuteTemplate(&buf, "create_unique_constraint.tmpl", data)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to execute create_unique_constraint template: %w", err)
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
// ExecuteCreateCheckConstraint executes the create check constraint template
|
|
func (te *TemplateExecutor) ExecuteCreateCheckConstraint(data ConstraintTemplateData) (string, error) {
|
|
var buf bytes.Buffer
|
|
err := te.templates.ExecuteTemplate(&buf, "create_check_constraint.tmpl", data)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to execute create_check_constraint template: %w", err)
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
// ExecuteCreateForeignKey executes the create foreign key template
|
|
func (te *TemplateExecutor) ExecuteCreateForeignKey(data ConstraintTemplateData) (string, error) {
|
|
var buf bytes.Buffer
|
|
err := te.templates.ExecuteTemplate(&buf, "create_foreign_key.tmpl", data)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to execute create_foreign_key template: %w", err)
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
// Helper functions to build template data from models
|
|
|
|
// BuildTableTemplateData builds TableTemplateData from a models.Table
|
|
func BuildTableTemplateData(schema string, table *models.Table) TableTemplateData {
|
|
columns := sortColumns(table.Columns)
|
|
|
|
// Find primary key constraint
|
|
var pk *models.Constraint
|
|
for _, constraint := range sortConstraints(table.Constraints) {
|
|
if constraint.Type == models.PrimaryKeyConstraint {
|
|
pk = constraint
|
|
break
|
|
}
|
|
}
|
|
|
|
// If no explicit primary key constraint, build one from columns with IsPrimaryKey=true
|
|
if pk == nil {
|
|
pkCols := []string{}
|
|
for _, col := range columns {
|
|
if col.IsPrimaryKey {
|
|
pkCols = append(pkCols, col.Name)
|
|
}
|
|
}
|
|
if len(pkCols) > 0 {
|
|
pk = &models.Constraint{
|
|
Name: "pk_" + table.Name,
|
|
Type: models.PrimaryKeyConstraint,
|
|
Columns: pkCols,
|
|
}
|
|
}
|
|
}
|
|
|
|
return TableTemplateData{
|
|
Schema: schema,
|
|
Name: table.Name,
|
|
Columns: columns,
|
|
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
|
|
}
|