SQLite can't ALTER TABLE ADD CONSTRAINT, so foreign keys are now written as inline FOREIGN KEY clauses in CREATE TABLE instead of commented-out ALTER statements. The default schema (public/main) now produces bare table names instead of a "public_" prefix; other schemas are still prefixed to avoid collisions. Also adds direct-to-file execution: the sqlite writer can now apply generated DDL straight to a .db file via Metadata["connection_string"], wired into `relspec merge --output-conn`.
243 lines
6.9 KiB
Go
243 lines
6.9 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
|
|
ForeignKeys []ForeignKeyTemplateData
|
|
}
|
|
|
|
// ForeignKeyTemplateData contains data for an inline FOREIGN KEY clause
|
|
type ForeignKeyTemplateData struct {
|
|
Name string
|
|
Columns []string
|
|
ForeignSchema string
|
|
ForeignTable string
|
|
ForeignColumns []string
|
|
OnDelete string
|
|
OnUpdate string
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collect foreign keys for inline FOREIGN KEY clauses
|
|
var fks []ForeignKeyTemplateData
|
|
for _, constraint := range sortConstraints(table.Constraints) {
|
|
if constraint.Type != models.ForeignKeyConstraint {
|
|
continue
|
|
}
|
|
|
|
refSchema := tableSchemaName(constraint.ReferencedSchema)
|
|
if refSchema == "" {
|
|
refSchema = schema
|
|
}
|
|
|
|
fks = append(fks, ForeignKeyTemplateData{
|
|
Name: constraint.Name,
|
|
Columns: constraint.Columns,
|
|
ForeignSchema: refSchema,
|
|
ForeignTable: constraint.ReferencedTable,
|
|
ForeignColumns: constraint.ReferencedColumns,
|
|
OnDelete: constraint.OnDelete,
|
|
OnUpdate: constraint.OnUpdate,
|
|
})
|
|
}
|
|
|
|
return TableTemplateData{
|
|
Schema: schema,
|
|
Name: table.Name,
|
|
Columns: columns,
|
|
PrimaryKey: pk,
|
|
ForeignKeys: fks,
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|