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.
292 lines
7.6 KiB
Go
292 lines
7.6 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
|
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
|
)
|
|
|
|
// Writer implements the Writer interface for SQLite SQL output
|
|
type Writer struct {
|
|
options *writers.WriterOptions
|
|
writer io.Writer
|
|
executor *TemplateExecutor
|
|
}
|
|
|
|
// NewWriter creates a new SQLite SQL writer
|
|
// SQLite doesn't support schemas, so FlattenSchema is automatically enabled
|
|
func NewWriter(options *writers.WriterOptions) *Writer {
|
|
// Force schema flattening for SQLite
|
|
options.FlattenSchema = true
|
|
|
|
executor, _ := NewTemplateExecutor(options)
|
|
return &Writer{
|
|
options: options,
|
|
executor: executor,
|
|
}
|
|
}
|
|
|
|
// WriteDatabase writes the entire database schema as SQLite SQL
|
|
func (w *Writer) WriteDatabase(db *models.Database) error {
|
|
var writer io.Writer
|
|
var file *os.File
|
|
var err error
|
|
|
|
// Use existing writer if already set (for testing)
|
|
if w.writer != nil {
|
|
writer = w.writer
|
|
} else if w.options.OutputPath != "" {
|
|
// Determine output destination
|
|
file, err = os.Create(w.options.OutputPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create output file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
writer = file
|
|
} else {
|
|
writer = os.Stdout
|
|
}
|
|
|
|
w.writer = writer
|
|
|
|
// Write header comment
|
|
fmt.Fprintf(w.writer, "-- SQLite Database Schema\n")
|
|
fmt.Fprintf(w.writer, "-- Database: %s\n", db.Name)
|
|
fmt.Fprintf(w.writer, "-- Generated by RelSpec\n")
|
|
fmt.Fprintf(w.writer, "-- Note: Schema names have been flattened (e.g., public.users -> public_users)\n\n")
|
|
|
|
// Enable foreign keys
|
|
pragma, err := w.executor.ExecutePragmaForeignKeys()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate pragma statement: %w", err)
|
|
}
|
|
fmt.Fprintf(w.writer, "%s\n", pragma)
|
|
|
|
// Process each schema in the database
|
|
for _, schema := range db.Schemas {
|
|
if err := w.WriteSchema(schema); err != nil {
|
|
return fmt.Errorf("failed to write schema %s: %w", schema.Name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// WriteSchema writes a single schema as SQLite SQL
|
|
func (w *Writer) WriteSchema(schema *models.Schema) error {
|
|
// SQLite doesn't have schemas, so we just write a comment
|
|
if schema.Name != "" {
|
|
fmt.Fprintf(w.writer, "-- Schema: %s (flattened into table names)\n\n", schema.Name)
|
|
}
|
|
|
|
// Phase 1: Create tables
|
|
for _, table := range schema.Tables {
|
|
if err := w.writeTable(schema.Name, table); err != nil {
|
|
return fmt.Errorf("failed to write table %s: %w", table.Name, err)
|
|
}
|
|
}
|
|
|
|
// Phase 2: Create indexes
|
|
for _, table := range schema.Tables {
|
|
if err := w.writeIndexes(schema.Name, table); err != nil {
|
|
return fmt.Errorf("failed to write indexes for table %s: %w", table.Name, err)
|
|
}
|
|
}
|
|
|
|
// Phase 3: Create unique constraints (as unique indexes)
|
|
for _, table := range schema.Tables {
|
|
if err := w.writeUniqueConstraints(schema.Name, table); err != nil {
|
|
return fmt.Errorf("failed to write unique constraints for table %s: %w", table.Name, err)
|
|
}
|
|
}
|
|
|
|
// Phase 4: Check constraints (as comments, since SQLite requires them in CREATE TABLE)
|
|
for _, table := range schema.Tables {
|
|
if err := w.writeCheckConstraints(schema.Name, table); err != nil {
|
|
return fmt.Errorf("failed to write check constraints for table %s: %w", table.Name, err)
|
|
}
|
|
}
|
|
|
|
// Phase 5: Foreign keys (as comments for compatibility)
|
|
for _, table := range schema.Tables {
|
|
if err := w.writeForeignKeys(schema.Name, table); err != nil {
|
|
return fmt.Errorf("failed to write foreign keys for table %s: %w", table.Name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// WriteTable writes a single table as SQLite SQL
|
|
func (w *Writer) WriteTable(table *models.Table) error {
|
|
return w.writeTable("", table)
|
|
}
|
|
|
|
// writeTable is the internal implementation
|
|
func (w *Writer) writeTable(schema string, table *models.Table) error {
|
|
// Build table template data
|
|
data := BuildTableTemplateData(schema, table)
|
|
|
|
// Execute template
|
|
sql, err := w.executor.ExecuteCreateTable(data)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute create table template: %w", err)
|
|
}
|
|
|
|
fmt.Fprintf(w.writer, "%s\n", sql)
|
|
return nil
|
|
}
|
|
|
|
// writeIndexes writes indexes for a table
|
|
func (w *Writer) writeIndexes(schema string, table *models.Table) error {
|
|
for _, index := range sortIndexes(table.Indexes) {
|
|
// Skip primary key indexes
|
|
if strings.HasSuffix(index.Name, "_pkey") {
|
|
continue
|
|
}
|
|
|
|
// Skip unique indexes (handled separately as unique constraints)
|
|
if index.Unique {
|
|
continue
|
|
}
|
|
|
|
data := IndexTemplateData{
|
|
Schema: schema,
|
|
Table: table.Name,
|
|
Name: index.Name,
|
|
Columns: index.Columns,
|
|
}
|
|
|
|
sql, err := w.executor.ExecuteCreateIndex(data)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute create index template: %w", err)
|
|
}
|
|
|
|
fmt.Fprintf(w.writer, "%s\n", sql)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// writeUniqueConstraints writes unique constraints as unique indexes
|
|
func (w *Writer) writeUniqueConstraints(schema string, table *models.Table) error {
|
|
for _, constraint := range sortConstraints(table.Constraints) {
|
|
if constraint.Type != models.UniqueConstraint {
|
|
continue
|
|
}
|
|
|
|
data := ConstraintTemplateData{
|
|
Schema: schema,
|
|
Table: table.Name,
|
|
Name: constraint.Name,
|
|
Columns: constraint.Columns,
|
|
}
|
|
|
|
sql, err := w.executor.ExecuteCreateUniqueConstraint(data)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute create unique constraint template: %w", err)
|
|
}
|
|
|
|
fmt.Fprintf(w.writer, "%s\n", sql)
|
|
}
|
|
|
|
// Also handle unique indexes from the Indexes map
|
|
for _, index := range sortIndexes(table.Indexes) {
|
|
if !index.Unique {
|
|
continue
|
|
}
|
|
|
|
// Skip if already handled as a constraint
|
|
alreadyHandled := false
|
|
for _, constraint := range table.Constraints {
|
|
if constraint.Type == models.UniqueConstraint && constraint.Name == index.Name {
|
|
alreadyHandled = true
|
|
break
|
|
}
|
|
}
|
|
if alreadyHandled {
|
|
continue
|
|
}
|
|
|
|
data := ConstraintTemplateData{
|
|
Schema: schema,
|
|
Table: table.Name,
|
|
Name: index.Name,
|
|
Columns: index.Columns,
|
|
}
|
|
|
|
sql, err := w.executor.ExecuteCreateUniqueConstraint(data)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute create unique index template: %w", err)
|
|
}
|
|
|
|
fmt.Fprintf(w.writer, "%s\n", sql)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// writeCheckConstraints writes check constraints as comments
|
|
func (w *Writer) writeCheckConstraints(schema string, table *models.Table) error {
|
|
for _, constraint := range sortConstraints(table.Constraints) {
|
|
if constraint.Type != models.CheckConstraint {
|
|
continue
|
|
}
|
|
|
|
data := ConstraintTemplateData{
|
|
Schema: schema,
|
|
Table: table.Name,
|
|
Name: constraint.Name,
|
|
Expression: constraint.Expression,
|
|
}
|
|
|
|
sql, err := w.executor.ExecuteCreateCheckConstraint(data)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute create check constraint template: %w", err)
|
|
}
|
|
|
|
fmt.Fprintf(w.writer, "%s\n", sql)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// writeForeignKeys writes foreign keys as comments
|
|
func (w *Writer) writeForeignKeys(schema string, table *models.Table) error {
|
|
for _, constraint := range sortConstraints(table.Constraints) {
|
|
if constraint.Type != models.ForeignKeyConstraint {
|
|
continue
|
|
}
|
|
|
|
refSchema := constraint.ReferencedSchema
|
|
if refSchema == "" {
|
|
refSchema = schema
|
|
}
|
|
|
|
data := ConstraintTemplateData{
|
|
Schema: schema,
|
|
Table: table.Name,
|
|
Name: constraint.Name,
|
|
Columns: constraint.Columns,
|
|
ForeignSchema: refSchema,
|
|
ForeignTable: constraint.ReferencedTable,
|
|
ForeignColumns: constraint.ReferencedColumns,
|
|
OnDelete: constraint.OnDelete,
|
|
OnUpdate: constraint.OnUpdate,
|
|
}
|
|
|
|
sql, err := w.executor.ExecuteCreateForeignKey(data)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute create foreign key template: %w", err)
|
|
}
|
|
|
|
fmt.Fprintf(w.writer, "%s\n", sql)
|
|
}
|
|
|
|
return nil
|
|
}
|