sql writer
This commit is contained in:
@@ -11,13 +11,7 @@ import (
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
)
|
||||
|
||||
// MigrationWriter generates differential migration SQL scripts
|
||||
type MigrationWriter struct {
|
||||
options *writers.WriterOptions
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
// MigrationScript represents a single migration script with priority and sequence
|
||||
// MigrationScript represents a single migration script with priority
|
||||
type MigrationScript struct {
|
||||
ObjectName string
|
||||
ObjectType string
|
||||
@@ -27,14 +21,27 @@ type MigrationScript struct {
|
||||
Body string
|
||||
}
|
||||
|
||||
// NewMigrationWriter creates a new migration writer
|
||||
func NewMigrationWriter(options *writers.WriterOptions) *MigrationWriter {
|
||||
return &MigrationWriter{
|
||||
options: options,
|
||||
}
|
||||
// MigrationWriter generates differential migration SQL scripts using templates
|
||||
type MigrationWriter struct {
|
||||
options *writers.WriterOptions
|
||||
writer io.Writer
|
||||
executor *TemplateExecutor
|
||||
}
|
||||
|
||||
// WriteMigration generates migration scripts by comparing model (desired) vs current (actual) database
|
||||
// NewMigrationWriter creates a new templated migration writer
|
||||
func NewMigrationWriter(options *writers.WriterOptions) (*MigrationWriter, error) {
|
||||
executor, err := NewTemplateExecutor()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create template executor: %w", err)
|
||||
}
|
||||
|
||||
return &MigrationWriter{
|
||||
options: options,
|
||||
executor: executor,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WriteMigration generates migration scripts using templates
|
||||
func (w *MigrationWriter) WriteMigration(model *models.Database, current *models.Database) error {
|
||||
var writer io.Writer
|
||||
var file *os.File
|
||||
@@ -56,9 +63,26 @@ func (w *MigrationWriter) WriteMigration(model *models.Database, current *models
|
||||
|
||||
w.writer = writer
|
||||
|
||||
// Check if audit is configured in metadata
|
||||
var auditConfig *AuditConfig
|
||||
if w.options.Metadata != nil {
|
||||
if ac, ok := w.options.Metadata["audit_config"].(*AuditConfig); ok {
|
||||
auditConfig = ac
|
||||
}
|
||||
}
|
||||
|
||||
// Generate all migration scripts
|
||||
scripts := make([]MigrationScript, 0)
|
||||
|
||||
// Generate audit tables if needed (priority 90)
|
||||
if auditConfig != nil && len(auditConfig.EnabledTables) > 0 {
|
||||
auditTableScript, err := w.generateAuditTablesScript(auditConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate audit tables: %w", err)
|
||||
}
|
||||
scripts = append(scripts, auditTableScript...)
|
||||
}
|
||||
|
||||
// Process each schema in the model
|
||||
for _, modelSchema := range model.Schemas {
|
||||
// Find corresponding schema in current database
|
||||
@@ -71,8 +95,20 @@ func (w *MigrationWriter) WriteMigration(model *models.Database, current *models
|
||||
}
|
||||
|
||||
// Generate schema-level scripts
|
||||
schemaScripts := w.generateSchemaScripts(modelSchema, currentSchema)
|
||||
schemaScripts, err := w.generateSchemaScripts(modelSchema, currentSchema)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate schema scripts: %w", err)
|
||||
}
|
||||
scripts = append(scripts, schemaScripts...)
|
||||
|
||||
// Generate audit scripts for this schema (if configured)
|
||||
if auditConfig != nil {
|
||||
auditScripts, err := w.generateAuditScripts(modelSchema, auditConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate audit scripts: %w", err)
|
||||
}
|
||||
scripts = append(scripts, auditScripts...)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort scripts by priority and sequence
|
||||
@@ -98,37 +134,52 @@ func (w *MigrationWriter) WriteMigration(model *models.Database, current *models
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateSchemaScripts generates migration scripts for a schema
|
||||
func (w *MigrationWriter) generateSchemaScripts(model *models.Schema, current *models.Schema) []MigrationScript {
|
||||
// generateSchemaScripts generates migration scripts for a schema using templates
|
||||
func (w *MigrationWriter) generateSchemaScripts(model *models.Schema, current *models.Schema) ([]MigrationScript, error) {
|
||||
scripts := make([]MigrationScript, 0)
|
||||
|
||||
// Phase 1: Drop constraints and indexes that changed (Priority 11-50)
|
||||
if current != nil {
|
||||
scripts = append(scripts, w.generateDropScripts(model, current)...)
|
||||
}
|
||||
|
||||
// Phase 2: Rename tables and columns (Priority 60-90)
|
||||
if current != nil {
|
||||
scripts = append(scripts, w.generateRenameScripts(model, current)...)
|
||||
dropScripts, err := w.generateDropScripts(model, current)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate drop scripts: %w", err)
|
||||
}
|
||||
scripts = append(scripts, dropScripts...)
|
||||
}
|
||||
|
||||
// Phase 3: Create/Alter tables and columns (Priority 100-145)
|
||||
scripts = append(scripts, w.generateTableScripts(model, current)...)
|
||||
tableScripts, err := w.generateTableScripts(model, current)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate table scripts: %w", err)
|
||||
}
|
||||
scripts = append(scripts, tableScripts...)
|
||||
|
||||
// Phase 4: Create indexes (Priority 160-180)
|
||||
scripts = append(scripts, w.generateIndexScripts(model, current)...)
|
||||
indexScripts, err := w.generateIndexScripts(model, current)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate index scripts: %w", err)
|
||||
}
|
||||
scripts = append(scripts, indexScripts...)
|
||||
|
||||
// Phase 5: Create foreign keys (Priority 195)
|
||||
scripts = append(scripts, w.generateForeignKeyScripts(model, current)...)
|
||||
fkScripts, err := w.generateForeignKeyScripts(model, current)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate foreign key scripts: %w", err)
|
||||
}
|
||||
scripts = append(scripts, fkScripts...)
|
||||
|
||||
// Phase 6: Add comments (Priority 200+)
|
||||
scripts = append(scripts, w.generateCommentScripts(model, current)...)
|
||||
commentScripts, err := w.generateCommentScripts(model, current)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate comment scripts: %w", err)
|
||||
}
|
||||
scripts = append(scripts, commentScripts...)
|
||||
|
||||
return scripts
|
||||
return scripts, nil
|
||||
}
|
||||
|
||||
// generateDropScripts generates DROP scripts for removed/changed objects
|
||||
func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *models.Schema) []MigrationScript {
|
||||
// generateDropScripts generates DROP scripts using templates
|
||||
func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *models.Schema) ([]MigrationScript, error) {
|
||||
scripts := make([]MigrationScript, 0)
|
||||
|
||||
// Build map of model tables for quick lookup
|
||||
@@ -142,35 +193,37 @@ func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *mod
|
||||
modelTable, existsInModel := modelTables[strings.ToLower(currentTable.Name)]
|
||||
|
||||
if !existsInModel {
|
||||
// Table will be dropped, skip individual constraint drops
|
||||
continue
|
||||
}
|
||||
|
||||
// Check each constraint in current database
|
||||
for constraintName, currentConstraint := range currentTable.Constraints {
|
||||
// Check if constraint exists in model
|
||||
modelConstraint, existsInModel := modelTable.Constraints[constraintName]
|
||||
|
||||
shouldDrop := false
|
||||
|
||||
if !existsInModel {
|
||||
shouldDrop = true
|
||||
} else if !constraintsEqual(modelConstraint, currentConstraint) {
|
||||
// Constraint changed, drop and recreate
|
||||
shouldDrop = true
|
||||
}
|
||||
|
||||
if shouldDrop {
|
||||
sql, err := w.executor.ExecuteDropConstraint(DropConstraintData{
|
||||
SchemaName: current.Name,
|
||||
TableName: currentTable.Name,
|
||||
ConstraintName: constraintName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
script := MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s.%s", current.Name, currentTable.Name, constraintName),
|
||||
ObjectType: "drop constraint",
|
||||
Schema: current.Name,
|
||||
Priority: 11,
|
||||
Sequence: len(scripts),
|
||||
Body: fmt.Sprintf(
|
||||
"ALTER TABLE %s.%s DROP CONSTRAINT IF EXISTS %s;",
|
||||
current.Name, currentTable.Name, constraintName,
|
||||
),
|
||||
Body: sql,
|
||||
}
|
||||
scripts = append(scripts, script)
|
||||
}
|
||||
@@ -181,7 +234,6 @@ func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *mod
|
||||
modelIndex, existsInModel := modelTable.Indexes[indexName]
|
||||
|
||||
shouldDrop := false
|
||||
|
||||
if !existsInModel {
|
||||
shouldDrop = true
|
||||
} else if !indexesEqual(modelIndex, currentIndex) {
|
||||
@@ -189,42 +241,32 @@ func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *mod
|
||||
}
|
||||
|
||||
if shouldDrop {
|
||||
sql, err := w.executor.ExecuteDropIndex(DropIndexData{
|
||||
SchemaName: current.Name,
|
||||
IndexName: indexName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
script := MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s.%s", current.Name, currentTable.Name, indexName),
|
||||
ObjectType: "drop index",
|
||||
Schema: current.Name,
|
||||
Priority: 20,
|
||||
Sequence: len(scripts),
|
||||
Body: fmt.Sprintf(
|
||||
"DROP INDEX IF EXISTS %s.%s CASCADE;",
|
||||
current.Name, indexName,
|
||||
),
|
||||
Body: sql,
|
||||
}
|
||||
scripts = append(scripts, script)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scripts
|
||||
return scripts, nil
|
||||
}
|
||||
|
||||
// generateRenameScripts generates RENAME scripts for renamed objects
|
||||
func (w *MigrationWriter) generateRenameScripts(model *models.Schema, current *models.Schema) []MigrationScript {
|
||||
scripts := make([]MigrationScript, 0)
|
||||
|
||||
// For now, we don't attempt to detect renames automatically
|
||||
// This would require GUID matching or other heuristics
|
||||
// Users would need to handle renames manually or through metadata
|
||||
|
||||
// Suppress unused parameter warnings
|
||||
_ = model
|
||||
_ = current
|
||||
|
||||
return scripts
|
||||
}
|
||||
|
||||
// generateTableScripts generates CREATE/ALTER TABLE scripts
|
||||
func (w *MigrationWriter) generateTableScripts(model *models.Schema, current *models.Schema) []MigrationScript {
|
||||
// generateTableScripts generates CREATE/ALTER TABLE scripts using templates
|
||||
func (w *MigrationWriter) generateTableScripts(model *models.Schema, current *models.Schema) ([]MigrationScript, error) {
|
||||
scripts := make([]MigrationScript, 0)
|
||||
|
||||
// Build map of current tables
|
||||
@@ -241,59 +283,35 @@ func (w *MigrationWriter) generateTableScripts(model *models.Schema, current *mo
|
||||
|
||||
if !exists {
|
||||
// Table doesn't exist, create it
|
||||
script := w.generateCreateTableScript(model, modelTable)
|
||||
sql, err := w.executor.ExecuteCreateTable(BuildCreateTableData(model.Name, modelTable))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
script := MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s", model.Name, modelTable.Name),
|
||||
ObjectType: "create table",
|
||||
Schema: model.Name,
|
||||
Priority: 100,
|
||||
Sequence: len(scripts),
|
||||
Body: sql,
|
||||
}
|
||||
scripts = append(scripts, script)
|
||||
} else {
|
||||
// Table exists, check for column changes
|
||||
alterScripts := w.generateAlterTableScripts(model, modelTable, currentTable)
|
||||
alterScripts, err := w.generateAlterTableScripts(model, modelTable, currentTable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scripts = append(scripts, alterScripts...)
|
||||
}
|
||||
}
|
||||
|
||||
return scripts
|
||||
return scripts, nil
|
||||
}
|
||||
|
||||
// generateCreateTableScript generates a CREATE TABLE script
|
||||
func (w *MigrationWriter) generateCreateTableScript(schema *models.Schema, table *models.Table) MigrationScript {
|
||||
var body strings.Builder
|
||||
|
||||
body.WriteString(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (\n", schema.Name, table.Name))
|
||||
|
||||
// Get sorted columns
|
||||
columns := getSortedColumns(table.Columns)
|
||||
columnDefs := make([]string, 0, len(columns))
|
||||
|
||||
for _, col := range columns {
|
||||
colDef := fmt.Sprintf(" %s %s", col.Name, col.Type)
|
||||
|
||||
// Add default value if present
|
||||
if col.Default != nil {
|
||||
colDef += fmt.Sprintf(" DEFAULT %v", col.Default)
|
||||
}
|
||||
|
||||
// Add NOT NULL if needed
|
||||
if col.NotNull {
|
||||
colDef += " NOT NULL"
|
||||
}
|
||||
|
||||
columnDefs = append(columnDefs, colDef)
|
||||
}
|
||||
|
||||
body.WriteString(strings.Join(columnDefs, ",\n"))
|
||||
body.WriteString("\n);")
|
||||
|
||||
return MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s", schema.Name, table.Name),
|
||||
ObjectType: "create table",
|
||||
Schema: schema.Name,
|
||||
Priority: 100,
|
||||
Sequence: 0,
|
||||
Body: body.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// generateAlterTableScripts generates ALTER TABLE scripts for column changes
|
||||
func (w *MigrationWriter) generateAlterTableScripts(schema *models.Schema, modelTable *models.Table, currentTable *models.Table) []MigrationScript {
|
||||
// generateAlterTableScripts generates ALTER TABLE scripts using templates
|
||||
func (w *MigrationWriter) generateAlterTableScripts(schema *models.Schema, modelTable *models.Table, currentTable *models.Table) ([]MigrationScript, error) {
|
||||
scripts := make([]MigrationScript, 0)
|
||||
|
||||
// Build map of current columns
|
||||
@@ -308,85 +326,93 @@ func (w *MigrationWriter) generateAlterTableScripts(schema *models.Schema, model
|
||||
|
||||
if !exists {
|
||||
// Column doesn't exist, add it
|
||||
defaultVal := ""
|
||||
if modelCol.Default != nil {
|
||||
defaultVal = fmt.Sprintf("%v", modelCol.Default)
|
||||
}
|
||||
|
||||
sql, err := w.executor.ExecuteAddColumn(AddColumnData{
|
||||
SchemaName: schema.Name,
|
||||
TableName: modelTable.Name,
|
||||
ColumnName: modelCol.Name,
|
||||
ColumnType: modelCol.Type,
|
||||
Default: defaultVal,
|
||||
NotNull: modelCol.NotNull,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
script := MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s.%s", schema.Name, modelTable.Name, modelCol.Name),
|
||||
ObjectType: "create column",
|
||||
Schema: schema.Name,
|
||||
Priority: 120,
|
||||
Sequence: len(scripts),
|
||||
Body: fmt.Sprintf(
|
||||
"ALTER TABLE %s.%s\n ADD COLUMN IF NOT EXISTS %s %s%s%s;",
|
||||
schema.Name, modelTable.Name, modelCol.Name, modelCol.Type,
|
||||
func() string {
|
||||
if modelCol.Default != nil {
|
||||
return fmt.Sprintf(" DEFAULT %v", modelCol.Default)
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
func() string {
|
||||
if modelCol.NotNull {
|
||||
return " NOT NULL"
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
),
|
||||
Body: sql,
|
||||
}
|
||||
scripts = append(scripts, script)
|
||||
} else if !columnsEqual(modelCol, currentCol) {
|
||||
// Column exists but type or properties changed
|
||||
// Column exists but properties changed
|
||||
if modelCol.Type != currentCol.Type {
|
||||
sql, err := w.executor.ExecuteAlterColumnType(AlterColumnTypeData{
|
||||
SchemaName: schema.Name,
|
||||
TableName: modelTable.Name,
|
||||
ColumnName: modelCol.Name,
|
||||
NewType: modelCol.Type,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
script := MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s.%s", schema.Name, modelTable.Name, modelCol.Name),
|
||||
ObjectType: "alter column type",
|
||||
Schema: schema.Name,
|
||||
Priority: 120,
|
||||
Sequence: len(scripts),
|
||||
Body: fmt.Sprintf(
|
||||
"ALTER TABLE %s.%s\n ALTER COLUMN %s TYPE %s;",
|
||||
schema.Name, modelTable.Name, modelCol.Name, modelCol.Type,
|
||||
),
|
||||
Body: sql,
|
||||
}
|
||||
scripts = append(scripts, script)
|
||||
}
|
||||
|
||||
// Check default value changes
|
||||
if fmt.Sprintf("%v", modelCol.Default) != fmt.Sprintf("%v", currentCol.Default) {
|
||||
if modelCol.Default != nil {
|
||||
script := MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s.%s", schema.Name, modelTable.Name, modelCol.Name),
|
||||
ObjectType: "alter column default",
|
||||
Schema: schema.Name,
|
||||
Priority: 145,
|
||||
Sequence: len(scripts),
|
||||
Body: fmt.Sprintf(
|
||||
"ALTER TABLE %s.%s\n ALTER COLUMN %s SET DEFAULT %v;",
|
||||
schema.Name, modelTable.Name, modelCol.Name, modelCol.Default,
|
||||
),
|
||||
}
|
||||
scripts = append(scripts, script)
|
||||
} else {
|
||||
script := MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s.%s", schema.Name, modelTable.Name, modelCol.Name),
|
||||
ObjectType: "alter column default",
|
||||
Schema: schema.Name,
|
||||
Priority: 145,
|
||||
Sequence: len(scripts),
|
||||
Body: fmt.Sprintf(
|
||||
"ALTER TABLE %s.%s\n ALTER COLUMN %s DROP DEFAULT;",
|
||||
schema.Name, modelTable.Name, modelCol.Name,
|
||||
),
|
||||
}
|
||||
scripts = append(scripts, script)
|
||||
setDefault := modelCol.Default != nil
|
||||
defaultVal := ""
|
||||
if setDefault {
|
||||
defaultVal = fmt.Sprintf("%v", modelCol.Default)
|
||||
}
|
||||
|
||||
sql, err := w.executor.ExecuteAlterColumnDefault(AlterColumnDefaultData{
|
||||
SchemaName: schema.Name,
|
||||
TableName: modelTable.Name,
|
||||
ColumnName: modelCol.Name,
|
||||
SetDefault: setDefault,
|
||||
DefaultValue: defaultVal,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
script := MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s.%s", schema.Name, modelTable.Name, modelCol.Name),
|
||||
ObjectType: "alter column default",
|
||||
Schema: schema.Name,
|
||||
Priority: 145,
|
||||
Sequence: len(scripts),
|
||||
Body: sql,
|
||||
}
|
||||
scripts = append(scripts, script)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scripts
|
||||
return scripts, nil
|
||||
}
|
||||
|
||||
// generateIndexScripts generates CREATE INDEX scripts
|
||||
func (w *MigrationWriter) generateIndexScripts(model *models.Schema, current *models.Schema) []MigrationScript {
|
||||
// generateIndexScripts generates CREATE INDEX scripts using templates
|
||||
func (w *MigrationWriter) generateIndexScripts(model *models.Schema, current *models.Schema) ([]MigrationScript, error) {
|
||||
scripts := make([]MigrationScript, 0)
|
||||
|
||||
// Build map of current tables
|
||||
@@ -401,47 +427,7 @@ func (w *MigrationWriter) generateIndexScripts(model *models.Schema, current *mo
|
||||
for _, modelTable := range model.Tables {
|
||||
currentTable := currentTables[strings.ToLower(modelTable.Name)]
|
||||
|
||||
// Process each index in model
|
||||
for indexName, modelIndex := range modelTable.Indexes {
|
||||
shouldCreate := true
|
||||
|
||||
// Check if index exists in current
|
||||
if currentTable != nil {
|
||||
if currentIndex, exists := currentTable.Indexes[indexName]; exists {
|
||||
if indexesEqual(modelIndex, currentIndex) {
|
||||
shouldCreate = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if shouldCreate {
|
||||
unique := ""
|
||||
if modelIndex.Unique {
|
||||
unique = "UNIQUE "
|
||||
}
|
||||
|
||||
indexType := "btree"
|
||||
if modelIndex.Type != "" {
|
||||
indexType = modelIndex.Type
|
||||
}
|
||||
|
||||
script := MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s.%s", model.Name, modelTable.Name, indexName),
|
||||
ObjectType: "create index",
|
||||
Schema: model.Name,
|
||||
Priority: 180,
|
||||
Sequence: len(scripts),
|
||||
Body: fmt.Sprintf(
|
||||
"CREATE %sINDEX IF NOT EXISTS %s\n ON %s.%s USING %s (%s);",
|
||||
unique, indexName, model.Name, modelTable.Name, indexType,
|
||||
strings.Join(modelIndex.Columns, ", "),
|
||||
),
|
||||
}
|
||||
scripts = append(scripts, script)
|
||||
}
|
||||
}
|
||||
|
||||
// Add primary key constraint if it exists
|
||||
// Process primary keys first
|
||||
for constraintName, constraint := range modelTable.Constraints {
|
||||
if constraint.Type == models.PrimaryKeyConstraint {
|
||||
shouldCreate := true
|
||||
@@ -455,39 +441,82 @@ func (w *MigrationWriter) generateIndexScripts(model *models.Schema, current *mo
|
||||
}
|
||||
|
||||
if shouldCreate {
|
||||
sql, err := w.executor.ExecuteCreatePrimaryKey(CreatePrimaryKeyData{
|
||||
SchemaName: model.Name,
|
||||
TableName: modelTable.Name,
|
||||
ConstraintName: constraintName,
|
||||
Columns: strings.Join(constraint.Columns, ", "),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
script := MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s.%s", model.Name, modelTable.Name, constraintName),
|
||||
ObjectType: "create primary key",
|
||||
Schema: model.Name,
|
||||
Priority: 160,
|
||||
Sequence: len(scripts),
|
||||
Body: fmt.Sprintf(
|
||||
"DO $$\nBEGIN\n IF NOT EXISTS (\n"+
|
||||
" SELECT 1 FROM information_schema.table_constraints\n"+
|
||||
" WHERE table_schema = '%s'\n"+
|
||||
" AND table_name = '%s'\n"+
|
||||
" AND constraint_name = '%s'\n"+
|
||||
" ) THEN\n"+
|
||||
" ALTER TABLE %s.%s\n"+
|
||||
" ADD CONSTRAINT %s PRIMARY KEY (%s);\n"+
|
||||
" END IF;\n"+
|
||||
"END;\n$$;",
|
||||
model.Name, modelTable.Name, constraintName,
|
||||
model.Name, modelTable.Name, constraintName,
|
||||
strings.Join(constraint.Columns, ", "),
|
||||
),
|
||||
Body: sql,
|
||||
}
|
||||
scripts = append(scripts, script)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process indexes
|
||||
for indexName, modelIndex := range modelTable.Indexes {
|
||||
// Skip primary key indexes
|
||||
if strings.HasPrefix(strings.ToLower(indexName), "pk_") {
|
||||
continue
|
||||
}
|
||||
|
||||
shouldCreate := true
|
||||
|
||||
if currentTable != nil {
|
||||
if currentIndex, exists := currentTable.Indexes[indexName]; exists {
|
||||
if indexesEqual(modelIndex, currentIndex) {
|
||||
shouldCreate = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if shouldCreate {
|
||||
indexType := "btree"
|
||||
if modelIndex.Type != "" {
|
||||
indexType = modelIndex.Type
|
||||
}
|
||||
|
||||
sql, err := w.executor.ExecuteCreateIndex(CreateIndexData{
|
||||
SchemaName: model.Name,
|
||||
TableName: modelTable.Name,
|
||||
IndexName: indexName,
|
||||
IndexType: indexType,
|
||||
Columns: strings.Join(modelIndex.Columns, ", "),
|
||||
Unique: modelIndex.Unique,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
script := MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s.%s", model.Name, modelTable.Name, indexName),
|
||||
ObjectType: "create index",
|
||||
Schema: model.Name,
|
||||
Priority: 180,
|
||||
Sequence: len(scripts),
|
||||
Body: sql,
|
||||
}
|
||||
scripts = append(scripts, script)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scripts
|
||||
return scripts, nil
|
||||
}
|
||||
|
||||
// generateForeignKeyScripts generates ADD CONSTRAINT FOREIGN KEY scripts
|
||||
func (w *MigrationWriter) generateForeignKeyScripts(model *models.Schema, current *models.Schema) []MigrationScript {
|
||||
// generateForeignKeyScripts generates ADD CONSTRAINT FOREIGN KEY scripts using templates
|
||||
func (w *MigrationWriter) generateForeignKeyScripts(model *models.Schema, current *models.Schema) ([]MigrationScript, error) {
|
||||
scripts := make([]MigrationScript, 0)
|
||||
|
||||
// Build map of current tables
|
||||
@@ -510,7 +539,6 @@ func (w *MigrationWriter) generateForeignKeyScripts(model *models.Schema, curren
|
||||
|
||||
shouldCreate := true
|
||||
|
||||
// Check if constraint exists in current
|
||||
if currentTable != nil {
|
||||
if currentConstraint, exists := currentTable.Constraints[constraintName]; exists {
|
||||
if constraintsEqual(constraint, currentConstraint) {
|
||||
@@ -530,59 +558,62 @@ func (w *MigrationWriter) generateForeignKeyScripts(model *models.Schema, curren
|
||||
onUpdate = strings.ToUpper(constraint.OnUpdate)
|
||||
}
|
||||
|
||||
sql, err := w.executor.ExecuteCreateForeignKey(CreateForeignKeyData{
|
||||
SchemaName: model.Name,
|
||||
TableName: modelTable.Name,
|
||||
ConstraintName: constraintName,
|
||||
SourceColumns: strings.Join(constraint.Columns, ", "),
|
||||
TargetSchema: constraint.ReferencedSchema,
|
||||
TargetTable: constraint.ReferencedTable,
|
||||
TargetColumns: strings.Join(constraint.ReferencedColumns, ", "),
|
||||
OnDelete: onDelete,
|
||||
OnUpdate: onUpdate,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
script := MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s.%s", model.Name, modelTable.Name, constraintName),
|
||||
ObjectType: "create foreign key",
|
||||
Schema: model.Name,
|
||||
Priority: 195,
|
||||
Sequence: len(scripts),
|
||||
Body: fmt.Sprintf(
|
||||
"ALTER TABLE %s.%s\n"+
|
||||
" DROP CONSTRAINT IF EXISTS %s;\n\n"+
|
||||
"ALTER TABLE %s.%s\n"+
|
||||
" ADD CONSTRAINT %s\n"+
|
||||
" FOREIGN KEY (%s)\n"+
|
||||
" REFERENCES %s.%s (%s)\n"+
|
||||
" ON DELETE %s\n"+
|
||||
" ON UPDATE %s\n"+
|
||||
" DEFERRABLE;",
|
||||
model.Name, modelTable.Name, constraintName,
|
||||
model.Name, modelTable.Name, constraintName,
|
||||
strings.Join(constraint.Columns, ", "),
|
||||
constraint.ReferencedSchema, constraint.ReferencedTable,
|
||||
strings.Join(constraint.ReferencedColumns, ", "),
|
||||
onDelete, onUpdate,
|
||||
),
|
||||
Body: sql,
|
||||
}
|
||||
scripts = append(scripts, script)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scripts
|
||||
return scripts, nil
|
||||
}
|
||||
|
||||
// generateCommentScripts generates COMMENT ON scripts
|
||||
func (w *MigrationWriter) generateCommentScripts(model *models.Schema, current *models.Schema) []MigrationScript {
|
||||
// generateCommentScripts generates COMMENT ON scripts using templates
|
||||
func (w *MigrationWriter) generateCommentScripts(model *models.Schema, current *models.Schema) ([]MigrationScript, error) {
|
||||
scripts := make([]MigrationScript, 0)
|
||||
|
||||
// Suppress unused parameter warning (current not used yet, could be used for diffing)
|
||||
_ = current
|
||||
_ = current // TODO: Compare with current schema to only add new/changed comments
|
||||
|
||||
// Process each model table
|
||||
for _, modelTable := range model.Tables {
|
||||
// Table comment
|
||||
if modelTable.Description != "" {
|
||||
sql, err := w.executor.ExecuteCommentTable(CommentTableData{
|
||||
SchemaName: model.Name,
|
||||
TableName: modelTable.Name,
|
||||
Comment: escapeQuote(modelTable.Description),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
script := MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s", model.Name, modelTable.Name),
|
||||
ObjectType: "comment on table",
|
||||
Schema: model.Name,
|
||||
Priority: 200,
|
||||
Sequence: len(scripts),
|
||||
Body: fmt.Sprintf(
|
||||
"COMMENT ON TABLE %s.%s IS '%s';",
|
||||
model.Name, modelTable.Name, escapeQuote(modelTable.Description),
|
||||
),
|
||||
Body: sql,
|
||||
}
|
||||
scripts = append(scripts, script)
|
||||
}
|
||||
@@ -590,79 +621,218 @@ func (w *MigrationWriter) generateCommentScripts(model *models.Schema, current *
|
||||
// Column comments
|
||||
for _, col := range modelTable.Columns {
|
||||
if col.Description != "" {
|
||||
sql, err := w.executor.ExecuteCommentColumn(CommentColumnData{
|
||||
SchemaName: model.Name,
|
||||
TableName: modelTable.Name,
|
||||
ColumnName: col.Name,
|
||||
Comment: escapeQuote(col.Description),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
script := MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s.%s", model.Name, modelTable.Name, col.Name),
|
||||
ObjectType: "comment on column",
|
||||
Schema: model.Name,
|
||||
Priority: 200,
|
||||
Sequence: len(scripts),
|
||||
Body: fmt.Sprintf(
|
||||
"COMMENT ON COLUMN %s.%s.%s IS '%s';",
|
||||
model.Name, modelTable.Name, col.Name, escapeQuote(col.Description),
|
||||
),
|
||||
Body: sql,
|
||||
}
|
||||
scripts = append(scripts, script)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scripts
|
||||
return scripts, nil
|
||||
}
|
||||
|
||||
// Comparison helper functions
|
||||
// generateAuditTablesScript generates audit table creation scripts using templates
|
||||
func (w *MigrationWriter) generateAuditTablesScript(auditConfig *AuditConfig) ([]MigrationScript, error) {
|
||||
scripts := make([]MigrationScript, 0)
|
||||
|
||||
func constraintsEqual(a, b *models.Constraint) bool {
|
||||
if a.Type != b.Type {
|
||||
auditSchema := auditConfig.AuditSchema
|
||||
if auditSchema == "" {
|
||||
auditSchema = "public"
|
||||
}
|
||||
|
||||
sql, err := w.executor.ExecuteAuditTables(AuditTablesData{
|
||||
AuditSchema: auditSchema,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
script := MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.atevent+atdetail", auditSchema),
|
||||
ObjectType: "create audit tables",
|
||||
Schema: auditSchema,
|
||||
Priority: 90,
|
||||
Sequence: 0,
|
||||
Body: sql,
|
||||
}
|
||||
scripts = append(scripts, script)
|
||||
|
||||
return scripts, nil
|
||||
}
|
||||
|
||||
// generateAuditScripts generates audit functions and triggers using templates
|
||||
func (w *MigrationWriter) generateAuditScripts(schema *models.Schema, auditConfig *AuditConfig) ([]MigrationScript, error) {
|
||||
scripts := make([]MigrationScript, 0)
|
||||
|
||||
// Process each table in the schema
|
||||
for _, table := range schema.Tables {
|
||||
if !auditConfig.IsTableAudited(schema.Name, table.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
config := auditConfig.GetTableConfig(schema.Name, table.Name)
|
||||
if config == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find primary key
|
||||
pk := table.GetPrimaryKey()
|
||||
if pk == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
auditSchema := auditConfig.AuditSchema
|
||||
if auditSchema == "" {
|
||||
auditSchema = schema.Name
|
||||
}
|
||||
|
||||
// Generate audit function
|
||||
funcName := fmt.Sprintf("ft_audit_%s", table.Name)
|
||||
funcData := BuildAuditFunctionData(schema.Name, table, pk, config, auditSchema, auditConfig.UserFunction)
|
||||
|
||||
funcSQL, err := w.executor.ExecuteAuditFunction(funcData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
functionScript := MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s", schema.Name, funcName),
|
||||
ObjectType: "create audit function",
|
||||
Schema: schema.Name,
|
||||
Priority: 345,
|
||||
Sequence: len(scripts),
|
||||
Body: funcSQL,
|
||||
}
|
||||
scripts = append(scripts, functionScript)
|
||||
|
||||
// Generate audit trigger
|
||||
triggerName := fmt.Sprintf("t_audit_%s", table.Name)
|
||||
events := make([]string, 0)
|
||||
if config.AuditInsert {
|
||||
events = append(events, "INSERT")
|
||||
}
|
||||
if config.AuditUpdate {
|
||||
events = append(events, "UPDATE")
|
||||
}
|
||||
if config.AuditDelete {
|
||||
events = append(events, "DELETE")
|
||||
}
|
||||
|
||||
if len(events) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
triggerSQL, err := w.executor.ExecuteAuditTrigger(AuditTriggerData{
|
||||
SchemaName: schema.Name,
|
||||
TableName: table.Name,
|
||||
TriggerName: triggerName,
|
||||
FunctionName: funcName,
|
||||
Events: strings.Join(events, " OR "),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
triggerScript := MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s", schema.Name, triggerName),
|
||||
ObjectType: "create audit trigger",
|
||||
Schema: schema.Name,
|
||||
Priority: 355,
|
||||
Sequence: len(scripts),
|
||||
Body: triggerSQL,
|
||||
}
|
||||
scripts = append(scripts, triggerScript)
|
||||
}
|
||||
|
||||
return scripts, nil
|
||||
}
|
||||
|
||||
// Helper functions for comparing database objects
|
||||
|
||||
// columnsEqual checks if two columns have the same definition
|
||||
func columnsEqual(col1, col2 *models.Column) bool {
|
||||
if col1 == nil || col2 == nil {
|
||||
return false
|
||||
}
|
||||
if len(a.Columns) != len(b.Columns) {
|
||||
return strings.EqualFold(col1.Type, col2.Type) &&
|
||||
col1.NotNull == col2.NotNull &&
|
||||
fmt.Sprintf("%v", col1.Default) == fmt.Sprintf("%v", col2.Default)
|
||||
}
|
||||
|
||||
// constraintsEqual checks if two constraints are equal
|
||||
func constraintsEqual(c1, c2 *models.Constraint) bool {
|
||||
if c1 == nil || c2 == nil {
|
||||
return false
|
||||
}
|
||||
for i := range a.Columns {
|
||||
if !strings.EqualFold(a.Columns[i], b.Columns[i]) {
|
||||
if c1.Type != c2.Type {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare columns
|
||||
if len(c1.Columns) != len(c2.Columns) {
|
||||
return false
|
||||
}
|
||||
for i, col := range c1.Columns {
|
||||
if !strings.EqualFold(col, c2.Columns[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if a.Type == models.ForeignKeyConstraint {
|
||||
if a.ReferencedTable != b.ReferencedTable || a.ReferencedSchema != b.ReferencedSchema {
|
||||
|
||||
// For foreign keys, also compare referenced table and columns
|
||||
if c1.Type == models.ForeignKeyConstraint {
|
||||
if !strings.EqualFold(c1.ReferencedTable, c2.ReferencedTable) {
|
||||
return false
|
||||
}
|
||||
if len(a.ReferencedColumns) != len(b.ReferencedColumns) {
|
||||
if len(c1.ReferencedColumns) != len(c2.ReferencedColumns) {
|
||||
return false
|
||||
}
|
||||
for i := range a.ReferencedColumns {
|
||||
if !strings.EqualFold(a.ReferencedColumns[i], b.ReferencedColumns[i]) {
|
||||
for i, col := range c1.ReferencedColumns {
|
||||
if !strings.EqualFold(col, c2.ReferencedColumns[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if c1.OnDelete != c2.OnDelete || c1.OnUpdate != c2.OnUpdate {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func indexesEqual(a, b *models.Index) bool {
|
||||
if a.Unique != b.Unique {
|
||||
// indexesEqual checks if two indexes are equal
|
||||
func indexesEqual(idx1, idx2 *models.Index) bool {
|
||||
if idx1 == nil || idx2 == nil {
|
||||
return false
|
||||
}
|
||||
if len(a.Columns) != len(b.Columns) {
|
||||
if idx1.Unique != idx2.Unique {
|
||||
return false
|
||||
}
|
||||
for i := range a.Columns {
|
||||
if !strings.EqualFold(a.Columns[i], b.Columns[i]) {
|
||||
if !strings.EqualFold(idx1.Type, idx2.Type) {
|
||||
return false
|
||||
}
|
||||
if len(idx1.Columns) != len(idx2.Columns) {
|
||||
return false
|
||||
}
|
||||
for i, col := range idx1.Columns {
|
||||
if !strings.EqualFold(col, idx2.Columns[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func columnsEqual(a, b *models.Column) bool {
|
||||
if a.Type != b.Type {
|
||||
return false
|
||||
}
|
||||
if a.NotNull != b.NotNull {
|
||||
return false
|
||||
}
|
||||
if fmt.Sprintf("%v", a.Default) != fmt.Sprintf("%v", b.Default) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user