feat(pgsql): unique constraints on apply, matching unique index names, FK deferral
- migration writer now emits unique constraints (direct apply and live diff previously dropped them); idempotent via the existing DO block - unique constraint adopts a same-named model unique index with ADD CONSTRAINT ... UNIQUE USING INDEX instead of building a second index - dbml reader names unnamed unique indexes ukey_<table>_<cols> to match the unique constraint naming - foreign key deferral: Constraint.Deferrable/InitiallyDeferred are *bool (nil = unspecified, never diffed); writers emit DEFERRABLE, DEFERRABLE INITIALLY DEFERRED and NOT DEFERRABLE; live reader reads condeferrable and condeferred; dbml reader/writer support deferrable and initially deferred - tests and README/naming docs updated
This commit is contained in:
@@ -222,6 +222,13 @@ func (w *MigrationWriter) generateSchemaScripts(model, current *models.Schema) (
|
||||
}
|
||||
scripts = append(scripts, indexScripts...)
|
||||
|
||||
// Phase 4.5: Create unique constraints (Priority 185)
|
||||
uniqueScripts, err := w.generateUniqueConstraintScripts(model, current)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate unique constraint scripts: %w", err)
|
||||
}
|
||||
scripts = append(scripts, uniqueScripts...)
|
||||
|
||||
// Phase 5: Create foreign keys (Priority 195)
|
||||
fkScripts, err := w.generateForeignKeyScripts(model, current, droppedFKs)
|
||||
if err != nil {
|
||||
@@ -751,6 +758,92 @@ func buildIndexColumnExpressionsFiltered(table *models.Table, index *models.Inde
|
||||
return columnExprs
|
||||
}
|
||||
|
||||
// generateUniqueConstraintScripts generates ADD CONSTRAINT UNIQUE scripts for model unique
|
||||
// constraints that are missing from, or differ in, the live schema.
|
||||
func (w *MigrationWriter) generateUniqueConstraintScripts(model, current *models.Schema) ([]MigrationScript, error) {
|
||||
scripts := make([]MigrationScript, 0)
|
||||
|
||||
currentTables := make(map[string]*models.Table)
|
||||
if current != nil {
|
||||
for _, table := range current.Tables {
|
||||
currentTables[strings.ToLower(table.Name)] = table
|
||||
}
|
||||
}
|
||||
|
||||
for _, modelTable := range model.Tables {
|
||||
currentTable := currentTables[strings.ToLower(modelTable.Name)]
|
||||
|
||||
for _, constraint := range sortConstraints(modelTable.Constraints) {
|
||||
if constraint.Type != models.UniqueConstraint {
|
||||
continue
|
||||
}
|
||||
|
||||
if currentTable != nil {
|
||||
if currentConstraint, exists := lookupConstraint(currentTable.Constraints, constraint.Name); exists && constraintsEqual(constraint, currentConstraint) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
columnExprs := make([]string, 0, len(constraint.Columns))
|
||||
for _, colName := range constraint.Columns {
|
||||
if col, ok := modelTable.Columns[colName]; ok {
|
||||
columnExprs = append(columnExprs, col.SQLName())
|
||||
}
|
||||
}
|
||||
if len(columnExprs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
sql, err := w.executor.ExecuteCreateUniqueConstraint(CreateUniqueConstraintData{
|
||||
SchemaName: model.Name,
|
||||
TableName: modelTable.Name,
|
||||
ConstraintName: constraint.Name,
|
||||
Columns: strings.Join(columnExprs, ", "),
|
||||
UsingIndex: adoptableUniqueIndex(modelTable, constraint),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scripts = append(scripts, MigrationScript{
|
||||
ObjectName: fmt.Sprintf("%s.%s.%s", model.Name, modelTable.Name, constraint.Name),
|
||||
ObjectType: "create unique constraint",
|
||||
Schema: model.Name,
|
||||
Priority: 185,
|
||||
Sequence: len(scripts),
|
||||
Body: sql,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return scripts, nil
|
||||
}
|
||||
|
||||
// adoptableUniqueIndex returns the name of the model's unique index that the given unique
|
||||
// constraint can be built on with ADD CONSTRAINT ... UNIQUE USING INDEX, so the constraint does
|
||||
// not create a second index (or collide with the index's name). The index must carry the
|
||||
// constraint's name, be a plain non-partial btree index and cover exactly the constraint's
|
||||
// columns. Index creation runs before constraint creation, so this holds whether or not the
|
||||
// live database already has the index. Returns "" when none qualifies.
|
||||
func adoptableUniqueIndex(table *models.Table, constraint *models.Constraint) string {
|
||||
index, ok := table.Indexes[constraint.Name]
|
||||
if !ok || !index.Unique || index.Where != "" || len(index.Include) > 0 {
|
||||
return ""
|
||||
}
|
||||
if index.Type != "" && !strings.EqualFold(index.Type, "btree") {
|
||||
return ""
|
||||
}
|
||||
if len(index.Columns) != len(constraint.Columns) {
|
||||
return ""
|
||||
}
|
||||
for i, col := range index.Columns {
|
||||
if !strings.EqualFold(col, constraint.Columns[i]) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return index.Name
|
||||
}
|
||||
|
||||
// generateForeignKeyScripts generates ADD CONSTRAINT FOREIGN KEY scripts using templates.
|
||||
// forceRecreate is a set of FK constraint keys (schema.table.name) that must be recreated
|
||||
// even if unchanged, because their referenced PK was dropped and recreated.
|
||||
@@ -800,6 +893,12 @@ func (w *MigrationWriter) generateForeignKeyScripts(model, current *models.Schem
|
||||
onUpdate = strings.ToUpper(constraint.OnUpdate)
|
||||
}
|
||||
|
||||
// An unspecified deferral keeps the historical default of DEFERRABLE.
|
||||
deferral := constraint.DeferralClause()
|
||||
if deferral == "" {
|
||||
deferral = "DEFERRABLE"
|
||||
}
|
||||
|
||||
sql, err := w.executor.ExecuteCreateForeignKey(CreateForeignKeyData{
|
||||
SchemaName: model.Name,
|
||||
TableName: modelTable.Name,
|
||||
@@ -810,6 +909,7 @@ func (w *MigrationWriter) generateForeignKeyScripts(model, current *models.Schem
|
||||
TargetColumns: strings.Join(constraint.ReferencedColumns, ", "),
|
||||
OnDelete: onDelete,
|
||||
OnUpdate: onUpdate,
|
||||
Deferral: deferral,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1149,6 +1249,9 @@ func constraintsEqual(c1, c2 *models.Constraint) bool {
|
||||
if !fkActionsEqual(c1.OnDelete, c2.OnDelete) || !fkActionsEqual(c1.OnUpdate, c2.OnUpdate) {
|
||||
return false
|
||||
}
|
||||
if !models.DeferralMatches(c1, c2) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
Reference in New Issue
Block a user