feat: Enhance PostgreSQL type handling and migration scripts

- Introduced equivalent base types and variants for PostgreSQL types to normalize type comparisons.
- Added functions for normalizing SQL types and retrieving equivalent type variants.
- Updated migration writer to handle type alterations with checks for existing types.
- Implemented logic to create necessary extensions (e.g., pg_trgm) based on schema requirements.
- Enhanced tests to cover new functionality for type normalization and migration handling.
- Improved handling of GIN indexes to use appropriate operator classes based on column types.
This commit is contained in:
2026-05-05 14:50:34 +02:00
parent 72200ea72e
commit 2d97a47ee1
14 changed files with 1042 additions and 65 deletions

View File

@@ -22,6 +22,16 @@ type MergeResult struct {
EnumsAdded int
ViewsAdded int
SequencesAdded int
TypeConflicts []ColumnTypeConflict
}
// ColumnTypeConflict describes a column that exists in both schemas but with incompatible types.
type ColumnTypeConflict struct {
Schema string
Table string
Column string
TargetType string
SourceType string
}
// MergeOptions contains options for merge operations
@@ -146,11 +156,19 @@ func (r *MergeResult) mergeColumns(table *models.Table, srcTable *models.Table)
// Merge columns
for colName, srcCol := range srcTable.Columns {
if _, exists := existingColumns[colName]; !exists {
if tgtCol, exists := existingColumns[colName]; !exists {
// Column doesn't exist, add it
newCol := cloneColumn(srcCol)
table.Columns[colName] = newCol
r.ColumnsAdded++
} else if columnTypeConflict(tgtCol, srcCol) {
r.TypeConflicts = append(r.TypeConflicts, ColumnTypeConflict{
Schema: firstNonEmpty(table.Schema, srcTable.Schema, srcCol.Schema),
Table: firstNonEmpty(table.Name, srcTable.Name, srcCol.Table),
Column: firstNonEmpty(tgtCol.Name, srcCol.Name, colName),
TargetType: describeColumnType(tgtCol),
SourceType: describeColumnType(srcCol),
})
}
}
}
@@ -426,6 +444,52 @@ func cloneColumn(col *models.Column) *models.Column {
return newCol
}
func columnTypeConflict(target, source *models.Column) bool {
if target == nil || source == nil {
return false
}
return normalizeType(target.Type) != normalizeType(source.Type) ||
target.Length != source.Length ||
target.Precision != source.Precision ||
target.Scale != source.Scale
}
func normalizeType(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
func describeColumnType(col *models.Column) string {
if col == nil {
return ""
}
typeName := strings.TrimSpace(col.Type)
if typeName == "" {
return ""
}
switch {
case col.Precision > 0 && col.Scale > 0:
return fmt.Sprintf("%s(%d,%d)", typeName, col.Precision, col.Scale)
case col.Precision > 0:
return fmt.Sprintf("%s(%d)", typeName, col.Precision)
case col.Length > 0:
return fmt.Sprintf("%s(%d)", typeName, col.Length)
default:
return typeName
}
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}
func cloneConstraint(constraint *models.Constraint) *models.Constraint {
if constraint == nil {
return nil
@@ -609,6 +673,7 @@ func GetMergeSummary(result *MergeResult) string {
fmt.Sprintf("Enums added: %d", result.EnumsAdded),
fmt.Sprintf("Relations added: %d", result.RelationsAdded),
fmt.Sprintf("Domains added: %d", result.DomainsAdded),
fmt.Sprintf("Type conflicts: %d", len(result.TypeConflicts)),
}
totalAdded := result.SchemasAdded + result.TablesAdded + result.ColumnsAdded +
@@ -625,3 +690,35 @@ func GetMergeSummary(result *MergeResult) string {
return summary
}
// GetColumnTypeConflictSummary returns a short, human-readable conflict summary.
func GetColumnTypeConflictSummary(result *MergeResult, limit int) string {
if result == nil || len(result.TypeConflicts) == 0 {
return ""
}
if limit <= 0 {
limit = len(result.TypeConflicts)
}
lines := make([]string, 0, min(limit, len(result.TypeConflicts))+1)
lines = append(lines, "column type conflicts detected:")
for i, conflict := range result.TypeConflicts {
if i >= limit {
break
}
lines = append(lines, fmt.Sprintf(" - %s.%s.%s: target=%s source=%s",
conflict.Schema, conflict.Table, conflict.Column, conflict.TargetType, conflict.SourceType))
}
if len(result.TypeConflicts) > limit {
lines = append(lines, fmt.Sprintf(" ... and %d more", len(result.TypeConflicts)-limit))
}
return strings.Join(lines, "\n")
}
func min(a, b int) int {
if a < b {
return a
}
return b
}