- Introduce a new screen for importing and merging database schemas. - Implement merge logic to combine schemas, tables, columns, and other objects. - Add options to skip specific object types during the merge process. - Update main menu to include the new import and merge option.
575 lines
15 KiB
Go
575 lines
15 KiB
Go
// Package merge provides utilities for merging database schemas.
|
|
// It allows combining schemas from multiple sources while avoiding duplicates,
|
|
// supporting only additive operations (no deletion or modification of existing items).
|
|
package merge
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
|
)
|
|
|
|
// MergeResult represents the result of a merge operation
|
|
type MergeResult struct {
|
|
SchemasAdded int
|
|
TablesAdded int
|
|
ColumnsAdded int
|
|
RelationsAdded int
|
|
DomainsAdded int
|
|
EnumsAdded int
|
|
ViewsAdded int
|
|
SequencesAdded int
|
|
}
|
|
|
|
// MergeOptions contains options for merge operations
|
|
type MergeOptions struct {
|
|
SkipDomains bool
|
|
SkipRelations bool
|
|
SkipEnums bool
|
|
SkipViews bool
|
|
SkipSequences bool
|
|
SkipTableNames map[string]bool // Tables to skip during merge (keyed by table name)
|
|
}
|
|
|
|
// MergeDatabases merges the source database into the target database.
|
|
// Only adds missing items; existing items are not modified.
|
|
func MergeDatabases(target, source *models.Database, opts *MergeOptions) *MergeResult {
|
|
if opts == nil {
|
|
opts = &MergeOptions{}
|
|
}
|
|
|
|
result := &MergeResult{}
|
|
|
|
if target == nil || source == nil {
|
|
return result
|
|
}
|
|
|
|
// Merge schemas and their contents
|
|
result.merge(target, source, opts)
|
|
|
|
return result
|
|
}
|
|
|
|
func (r *MergeResult) merge(target, source *models.Database, opts *MergeOptions) {
|
|
// Create maps of existing schemas for quick lookup
|
|
existingSchemas := make(map[string]*models.Schema)
|
|
for _, schema := range target.Schemas {
|
|
existingSchemas[schema.SQLName()] = schema
|
|
}
|
|
|
|
// Merge schemas
|
|
for _, srcSchema := range source.Schemas {
|
|
schemaName := srcSchema.SQLName()
|
|
if tgtSchema, exists := existingSchemas[schemaName]; exists {
|
|
// Schema exists, merge its contents
|
|
r.mergeSchemaContents(tgtSchema, srcSchema, opts)
|
|
} else {
|
|
// Schema doesn't exist, add it
|
|
newSchema := cloneSchema(srcSchema)
|
|
target.Schemas = append(target.Schemas, newSchema)
|
|
r.SchemasAdded++
|
|
}
|
|
}
|
|
|
|
// Merge domains if not skipped
|
|
if !opts.SkipDomains {
|
|
r.mergeDomains(target, source)
|
|
}
|
|
}
|
|
|
|
func (r *MergeResult) mergeSchemaContents(target, source *models.Schema, opts *MergeOptions) {
|
|
// Merge tables
|
|
r.mergeTables(target, source, opts)
|
|
|
|
// Merge views if not skipped
|
|
if !opts.SkipViews {
|
|
r.mergeViews(target, source)
|
|
}
|
|
|
|
// Merge sequences if not skipped
|
|
if !opts.SkipSequences {
|
|
r.mergeSequences(target, source)
|
|
}
|
|
|
|
// Merge enums if not skipped
|
|
if !opts.SkipEnums {
|
|
r.mergeEnums(target, source)
|
|
}
|
|
|
|
// Merge relations if not skipped
|
|
if !opts.SkipRelations {
|
|
r.mergeRelations(target, source)
|
|
}
|
|
}
|
|
|
|
func (r *MergeResult) mergeTables(schema *models.Schema, source *models.Schema, opts *MergeOptions) {
|
|
// Create map of existing tables
|
|
existingTables := make(map[string]*models.Table)
|
|
for _, table := range schema.Tables {
|
|
existingTables[table.SQLName()] = table
|
|
}
|
|
|
|
// Merge tables
|
|
for _, srcTable := range source.Tables {
|
|
tableName := srcTable.SQLName()
|
|
|
|
// Skip if table is in the skip list (case-insensitive)
|
|
if opts != nil && opts.SkipTableNames != nil && opts.SkipTableNames[strings.ToLower(tableName)] {
|
|
continue
|
|
}
|
|
|
|
if tgtTable, exists := existingTables[tableName]; exists {
|
|
// Table exists, merge its columns
|
|
r.mergeColumns(tgtTable, srcTable)
|
|
} else {
|
|
// Table doesn't exist, add it
|
|
newTable := cloneTable(srcTable)
|
|
schema.Tables = append(schema.Tables, newTable)
|
|
r.TablesAdded++
|
|
// Count columns in the newly added table
|
|
r.ColumnsAdded += len(newTable.Columns)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *MergeResult) mergeColumns(table *models.Table, srcTable *models.Table) {
|
|
// Create map of existing columns
|
|
existingColumns := make(map[string]*models.Column)
|
|
for colName := range table.Columns {
|
|
existingColumns[colName] = table.Columns[colName]
|
|
}
|
|
|
|
// Merge columns
|
|
for colName, srcCol := range srcTable.Columns {
|
|
if _, exists := existingColumns[colName]; !exists {
|
|
// Column doesn't exist, add it
|
|
newCol := cloneColumn(srcCol)
|
|
table.Columns[colName] = newCol
|
|
r.ColumnsAdded++
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *MergeResult) mergeViews(schema *models.Schema, source *models.Schema) {
|
|
// Create map of existing views
|
|
existingViews := make(map[string]*models.View)
|
|
for _, view := range schema.Views {
|
|
existingViews[view.SQLName()] = view
|
|
}
|
|
|
|
// Merge views
|
|
for _, srcView := range source.Views {
|
|
viewName := srcView.SQLName()
|
|
if _, exists := existingViews[viewName]; !exists {
|
|
// View doesn't exist, add it
|
|
newView := cloneView(srcView)
|
|
schema.Views = append(schema.Views, newView)
|
|
r.ViewsAdded++
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *MergeResult) mergeSequences(schema *models.Schema, source *models.Schema) {
|
|
// Create map of existing sequences
|
|
existingSequences := make(map[string]*models.Sequence)
|
|
for _, seq := range schema.Sequences {
|
|
existingSequences[seq.SQLName()] = seq
|
|
}
|
|
|
|
// Merge sequences
|
|
for _, srcSeq := range source.Sequences {
|
|
seqName := srcSeq.SQLName()
|
|
if _, exists := existingSequences[seqName]; !exists {
|
|
// Sequence doesn't exist, add it
|
|
newSeq := cloneSequence(srcSeq)
|
|
schema.Sequences = append(schema.Sequences, newSeq)
|
|
r.SequencesAdded++
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *MergeResult) mergeEnums(schema *models.Schema, source *models.Schema) {
|
|
// Create map of existing enums
|
|
existingEnums := make(map[string]*models.Enum)
|
|
for _, enum := range schema.Enums {
|
|
existingEnums[enum.SQLName()] = enum
|
|
}
|
|
|
|
// Merge enums
|
|
for _, srcEnum := range source.Enums {
|
|
enumName := srcEnum.SQLName()
|
|
if _, exists := existingEnums[enumName]; !exists {
|
|
// Enum doesn't exist, add it
|
|
newEnum := cloneEnum(srcEnum)
|
|
schema.Enums = append(schema.Enums, newEnum)
|
|
r.EnumsAdded++
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *MergeResult) mergeRelations(schema *models.Schema, source *models.Schema) {
|
|
// Create map of existing relations
|
|
existingRelations := make(map[string]*models.Relationship)
|
|
for _, rel := range schema.Relations {
|
|
existingRelations[rel.SQLName()] = rel
|
|
}
|
|
|
|
// Merge relations
|
|
for _, srcRel := range source.Relations {
|
|
if _, exists := existingRelations[srcRel.SQLName()]; !exists {
|
|
// Relation doesn't exist, add it
|
|
newRel := cloneRelation(srcRel)
|
|
schema.Relations = append(schema.Relations, newRel)
|
|
r.RelationsAdded++
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *MergeResult) mergeDomains(target *models.Database, source *models.Database) {
|
|
// Create map of existing domains
|
|
existingDomains := make(map[string]*models.Domain)
|
|
for _, domain := range target.Domains {
|
|
existingDomains[domain.SQLName()] = domain
|
|
}
|
|
|
|
// Merge domains
|
|
for _, srcDomain := range source.Domains {
|
|
domainName := srcDomain.SQLName()
|
|
if _, exists := existingDomains[domainName]; !exists {
|
|
// Domain doesn't exist, add it
|
|
newDomain := cloneDomain(srcDomain)
|
|
target.Domains = append(target.Domains, newDomain)
|
|
r.DomainsAdded++
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clone functions to create deep copies of models
|
|
|
|
func cloneSchema(schema *models.Schema) *models.Schema {
|
|
if schema == nil {
|
|
return nil
|
|
}
|
|
newSchema := &models.Schema{
|
|
Name: schema.Name,
|
|
Description: schema.Description,
|
|
Owner: schema.Owner,
|
|
Comment: schema.Comment,
|
|
Sequence: schema.Sequence,
|
|
UpdatedAt: schema.UpdatedAt,
|
|
Tables: make([]*models.Table, 0),
|
|
Views: make([]*models.View, 0),
|
|
Sequences: make([]*models.Sequence, 0),
|
|
Enums: make([]*models.Enum, 0),
|
|
Relations: make([]*models.Relationship, 0),
|
|
}
|
|
|
|
if schema.Permissions != nil {
|
|
newSchema.Permissions = make(map[string]string)
|
|
for k, v := range schema.Permissions {
|
|
newSchema.Permissions[k] = v
|
|
}
|
|
}
|
|
|
|
if schema.Metadata != nil {
|
|
newSchema.Metadata = make(map[string]interface{})
|
|
for k, v := range schema.Metadata {
|
|
newSchema.Metadata[k] = v
|
|
}
|
|
}
|
|
|
|
if schema.Scripts != nil {
|
|
newSchema.Scripts = make([]*models.Script, len(schema.Scripts))
|
|
copy(newSchema.Scripts, schema.Scripts)
|
|
}
|
|
|
|
// Clone tables
|
|
for _, table := range schema.Tables {
|
|
newSchema.Tables = append(newSchema.Tables, cloneTable(table))
|
|
}
|
|
|
|
// Clone views
|
|
for _, view := range schema.Views {
|
|
newSchema.Views = append(newSchema.Views, cloneView(view))
|
|
}
|
|
|
|
// Clone sequences
|
|
for _, seq := range schema.Sequences {
|
|
newSchema.Sequences = append(newSchema.Sequences, cloneSequence(seq))
|
|
}
|
|
|
|
// Clone enums
|
|
for _, enum := range schema.Enums {
|
|
newSchema.Enums = append(newSchema.Enums, cloneEnum(enum))
|
|
}
|
|
|
|
// Clone relations
|
|
for _, rel := range schema.Relations {
|
|
newSchema.Relations = append(newSchema.Relations, cloneRelation(rel))
|
|
}
|
|
|
|
return newSchema
|
|
}
|
|
|
|
func cloneTable(table *models.Table) *models.Table {
|
|
if table == nil {
|
|
return nil
|
|
}
|
|
newTable := &models.Table{
|
|
Name: table.Name,
|
|
Description: table.Description,
|
|
Schema: table.Schema,
|
|
Comment: table.Comment,
|
|
Sequence: table.Sequence,
|
|
UpdatedAt: table.UpdatedAt,
|
|
Columns: make(map[string]*models.Column),
|
|
Constraints: make(map[string]*models.Constraint),
|
|
Indexes: make(map[string]*models.Index),
|
|
}
|
|
|
|
if table.Metadata != nil {
|
|
newTable.Metadata = make(map[string]interface{})
|
|
for k, v := range table.Metadata {
|
|
newTable.Metadata[k] = v
|
|
}
|
|
}
|
|
|
|
// Clone columns
|
|
for colName, col := range table.Columns {
|
|
newTable.Columns[colName] = cloneColumn(col)
|
|
}
|
|
|
|
// Clone constraints
|
|
for constName, constraint := range table.Constraints {
|
|
newTable.Constraints[constName] = cloneConstraint(constraint)
|
|
}
|
|
|
|
// Clone indexes
|
|
for idxName, index := range table.Indexes {
|
|
newTable.Indexes[idxName] = cloneIndex(index)
|
|
}
|
|
|
|
return newTable
|
|
}
|
|
|
|
func cloneColumn(col *models.Column) *models.Column {
|
|
if col == nil {
|
|
return nil
|
|
}
|
|
newCol := &models.Column{
|
|
Name: col.Name,
|
|
Type: col.Type,
|
|
Description: col.Description,
|
|
Comment: col.Comment,
|
|
IsPrimaryKey: col.IsPrimaryKey,
|
|
NotNull: col.NotNull,
|
|
Default: col.Default,
|
|
Precision: col.Precision,
|
|
Scale: col.Scale,
|
|
Length: col.Length,
|
|
Sequence: col.Sequence,
|
|
AutoIncrement: col.AutoIncrement,
|
|
Collation: col.Collation,
|
|
}
|
|
|
|
return newCol
|
|
}
|
|
|
|
func cloneConstraint(constraint *models.Constraint) *models.Constraint {
|
|
if constraint == nil {
|
|
return nil
|
|
}
|
|
newConstraint := &models.Constraint{
|
|
Type: constraint.Type,
|
|
Columns: make([]string, len(constraint.Columns)),
|
|
ReferencedTable: constraint.ReferencedTable,
|
|
ReferencedSchema: constraint.ReferencedSchema,
|
|
ReferencedColumns: make([]string, len(constraint.ReferencedColumns)),
|
|
OnUpdate: constraint.OnUpdate,
|
|
OnDelete: constraint.OnDelete,
|
|
Expression: constraint.Expression,
|
|
Name: constraint.Name,
|
|
Deferrable: constraint.Deferrable,
|
|
InitiallyDeferred: constraint.InitiallyDeferred,
|
|
Sequence: constraint.Sequence,
|
|
}
|
|
copy(newConstraint.Columns, constraint.Columns)
|
|
copy(newConstraint.ReferencedColumns, constraint.ReferencedColumns)
|
|
return newConstraint
|
|
}
|
|
|
|
func cloneIndex(index *models.Index) *models.Index {
|
|
if index == nil {
|
|
return nil
|
|
}
|
|
newIndex := &models.Index{
|
|
Name: index.Name,
|
|
Description: index.Description,
|
|
Table: index.Table,
|
|
Schema: index.Schema,
|
|
Columns: make([]string, len(index.Columns)),
|
|
Unique: index.Unique,
|
|
Type: index.Type,
|
|
Where: index.Where,
|
|
Concurrent: index.Concurrent,
|
|
Include: make([]string, len(index.Include)),
|
|
Comment: index.Comment,
|
|
Sequence: index.Sequence,
|
|
}
|
|
copy(newIndex.Columns, index.Columns)
|
|
copy(newIndex.Include, index.Include)
|
|
return newIndex
|
|
}
|
|
|
|
func cloneView(view *models.View) *models.View {
|
|
if view == nil {
|
|
return nil
|
|
}
|
|
newView := &models.View{
|
|
Name: view.Name,
|
|
Description: view.Description,
|
|
Schema: view.Schema,
|
|
Definition: view.Definition,
|
|
Comment: view.Comment,
|
|
Sequence: view.Sequence,
|
|
Columns: make(map[string]*models.Column),
|
|
}
|
|
|
|
if view.Metadata != nil {
|
|
newView.Metadata = make(map[string]interface{})
|
|
for k, v := range view.Metadata {
|
|
newView.Metadata[k] = v
|
|
}
|
|
}
|
|
|
|
// Clone columns
|
|
for colName, col := range view.Columns {
|
|
newView.Columns[colName] = cloneColumn(col)
|
|
}
|
|
|
|
return newView
|
|
}
|
|
|
|
func cloneSequence(seq *models.Sequence) *models.Sequence {
|
|
if seq == nil {
|
|
return nil
|
|
}
|
|
newSeq := &models.Sequence{
|
|
Name: seq.Name,
|
|
Description: seq.Description,
|
|
Schema: seq.Schema,
|
|
StartValue: seq.StartValue,
|
|
MinValue: seq.MinValue,
|
|
MaxValue: seq.MaxValue,
|
|
IncrementBy: seq.IncrementBy,
|
|
CacheSize: seq.CacheSize,
|
|
Cycle: seq.Cycle,
|
|
OwnedByTable: seq.OwnedByTable,
|
|
OwnedByColumn: seq.OwnedByColumn,
|
|
Comment: seq.Comment,
|
|
Sequence: seq.Sequence,
|
|
}
|
|
return newSeq
|
|
}
|
|
|
|
func cloneEnum(enum *models.Enum) *models.Enum {
|
|
if enum == nil {
|
|
return nil
|
|
}
|
|
newEnum := &models.Enum{
|
|
Name: enum.Name,
|
|
Values: make([]string, len(enum.Values)),
|
|
Schema: enum.Schema,
|
|
}
|
|
copy(newEnum.Values, enum.Values)
|
|
return newEnum
|
|
}
|
|
|
|
func cloneRelation(rel *models.Relationship) *models.Relationship {
|
|
if rel == nil {
|
|
return nil
|
|
}
|
|
newRel := &models.Relationship{
|
|
Name: rel.Name,
|
|
Type: rel.Type,
|
|
FromTable: rel.FromTable,
|
|
FromSchema: rel.FromSchema,
|
|
FromColumns: make([]string, len(rel.FromColumns)),
|
|
ToTable: rel.ToTable,
|
|
ToSchema: rel.ToSchema,
|
|
ToColumns: make([]string, len(rel.ToColumns)),
|
|
ForeignKey: rel.ForeignKey,
|
|
ThroughTable: rel.ThroughTable,
|
|
ThroughSchema: rel.ThroughSchema,
|
|
Description: rel.Description,
|
|
Sequence: rel.Sequence,
|
|
}
|
|
|
|
if rel.Properties != nil {
|
|
newRel.Properties = make(map[string]string)
|
|
for k, v := range rel.Properties {
|
|
newRel.Properties[k] = v
|
|
}
|
|
}
|
|
|
|
copy(newRel.FromColumns, rel.FromColumns)
|
|
copy(newRel.ToColumns, rel.ToColumns)
|
|
return newRel
|
|
}
|
|
|
|
func cloneDomain(domain *models.Domain) *models.Domain {
|
|
if domain == nil {
|
|
return nil
|
|
}
|
|
newDomain := &models.Domain{
|
|
Name: domain.Name,
|
|
Description: domain.Description,
|
|
Comment: domain.Comment,
|
|
Sequence: domain.Sequence,
|
|
Tables: make([]*models.DomainTable, len(domain.Tables)),
|
|
}
|
|
|
|
if domain.Metadata != nil {
|
|
newDomain.Metadata = make(map[string]interface{})
|
|
for k, v := range domain.Metadata {
|
|
newDomain.Metadata[k] = v
|
|
}
|
|
}
|
|
|
|
copy(newDomain.Tables, domain.Tables)
|
|
return newDomain
|
|
}
|
|
|
|
// GetMergeSummary returns a human-readable summary of the merge result
|
|
func GetMergeSummary(result *MergeResult) string {
|
|
if result == nil {
|
|
return "No merge result available"
|
|
}
|
|
|
|
lines := []string{
|
|
"=== Merge Summary ===",
|
|
fmt.Sprintf("Schemas added: %d", result.SchemasAdded),
|
|
fmt.Sprintf("Tables added: %d", result.TablesAdded),
|
|
fmt.Sprintf("Columns added: %d", result.ColumnsAdded),
|
|
fmt.Sprintf("Views added: %d", result.ViewsAdded),
|
|
fmt.Sprintf("Sequences added: %d", result.SequencesAdded),
|
|
fmt.Sprintf("Enums added: %d", result.EnumsAdded),
|
|
fmt.Sprintf("Relations added: %d", result.RelationsAdded),
|
|
fmt.Sprintf("Domains added: %d", result.DomainsAdded),
|
|
}
|
|
|
|
totalAdded := result.SchemasAdded + result.TablesAdded + result.ColumnsAdded +
|
|
result.ViewsAdded + result.SequencesAdded + result.EnumsAdded +
|
|
result.RelationsAdded + result.DomainsAdded
|
|
|
|
lines = append(lines, fmt.Sprintf("Total items added: %d", totalAdded))
|
|
|
|
summary := ""
|
|
for _, line := range lines {
|
|
summary += line + "\n"
|
|
}
|
|
|
|
return summary
|
|
}
|