Skip to content

Commit 177de22

Browse files
authored
Fix adding extra unique constraint to columns that are already unique (#579)
Previously, `pgroll` failed to add unique constraint to columns that were already included in another unique constraint in some cases. The bug is triggered when the name of the column that is not yet unique comes earlier than the already unique column. The error message we get: ``` Failed to complete migration: unable to execute complete operation: failed to create unique constraint from index "_pgroll_dup_fruits_name_key": pq: index "fruits_name_key" does not exist ``` The issue is during renaming phase. `pgroll` checked if the unique index is duplicated (thus, has to be renamed). Then `pgroll` checked if the column (being renamed) is included in the index. If it was part of the index, `pgroll` renamed the index to the final name. If that unique index happened to be a unique constraint `pgroll` transformed the index into a unique constraint. Good, yay. However, if the unique index is duplicated, but the column (that was being renamed) was not part of the index definition, `pgroll` did not rename the index. If this unique index is a constraint, `pgroll` still tried to transform the index with the original name into a constraint. So we got the error message above. Now this problem is fixed. Related to #227
1 parent e689a12 commit 177de22

File tree

2 files changed

+112
-15
lines changed

2 files changed

+112
-15
lines changed

pkg/migrations/op_create_constraint_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,106 @@ func TestCreateConstraint(t *testing.T) {
495495
}, rows)
496496
},
497497
},
498+
{
499+
name: "create unique constraint on a unique column and another column",
500+
migrations: []migrations.Migration{
501+
{
502+
Name: "01_add_table",
503+
Operations: migrations.Operations{
504+
&migrations.OpCreateTable{
505+
Name: "users",
506+
Columns: []migrations.Column{
507+
{
508+
Name: "id",
509+
Type: "serial",
510+
Pk: true,
511+
},
512+
{
513+
Name: "name",
514+
Type: "varchar(255)",
515+
Nullable: false,
516+
},
517+
{
518+
Name: "email",
519+
Type: "varchar(255)",
520+
Nullable: true,
521+
},
522+
},
523+
Constraints: []migrations.Constraint{
524+
{
525+
Name: "unique_name",
526+
Type: "unique",
527+
Columns: []string{"name"},
528+
},
529+
},
530+
},
531+
},
532+
},
533+
{
534+
Name: "02_create_constraint",
535+
Operations: migrations.Operations{
536+
&migrations.OpCreateConstraint{
537+
Name: "unique_name_email",
538+
Table: "users",
539+
Type: "unique",
540+
Columns: []string{"email", "name"},
541+
Up: map[string]string{
542+
"name": "name || random()",
543+
"email": "email || random()",
544+
},
545+
Down: map[string]string{
546+
"name": "name",
547+
"email": "email",
548+
},
549+
},
550+
},
551+
},
552+
},
553+
afterStart: func(t *testing.T, db *sql.DB, schema string) {
554+
// The index has been created on the underlying table.
555+
IndexMustExist(t, db, schema, "users", "unique_name")
556+
IndexMustExist(t, db, schema, "users", "unique_name_email")
557+
558+
// Inserting values into the old schema that violate uniqueness should succeed.
559+
MustInsert(t, db, schema, "01_add_table", "users", map[string]string{
560+
"name": "alice",
561+
"email": "email",
562+
})
563+
MustInsert(t, db, schema, "01_add_table", "users", map[string]string{
564+
"name": "bob",
565+
"email": "email",
566+
})
567+
568+
// Inserting values into the new schema that violate uniqueness should fail.
569+
MustInsert(t, db, schema, "02_create_constraint", "users", map[string]string{
570+
"name": "cat",
571+
"email": "email",
572+
})
573+
MustNotInsert(t, db, schema, "02_create_constraint", "users", map[string]string{
574+
"name": "cat",
575+
"email": "email",
576+
}, testutils.UniqueViolationErrorCode)
577+
},
578+
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
579+
// The index has been dropped from the the underlying table.
580+
IndexMustNotExist(t, db, schema, "users", "unique_name_email")
581+
582+
// Functions, triggers and temporary columns are dropped.
583+
TableMustBeCleanedUp(t, db, schema, "users", "name", "email")
584+
},
585+
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
586+
// Functions, triggers and temporary columns are dropped.
587+
TableMustBeCleanedUp(t, db, schema, "users", "name", "email")
588+
589+
// Inserting values into the new schema that violate uniqueness should fail.
590+
MustInsert(t, db, schema, "02_create_constraint", "users", map[string]string{
591+
"name": "carol",
592+
})
593+
MustNotInsert(t, db, schema, "02_create_constraint", "users", map[string]string{
594+
"name": "carol",
595+
}, testutils.UniqueViolationErrorCode)
596+
},
597+
},
498598
})
499599
}
500600

pkg/migrations/rename.go

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -123,26 +123,24 @@ func RenameDuplicatedColumn(ctx context.Context, conn db.DB, table *schema.Table
123123
// Rename any indexes on the duplicated column and use unique indexes to
124124
// create `UNIQUE` constraints.
125125
for _, idx := range table.Indexes {
126-
if !IsDuplicatedName(idx.Name) {
126+
if !IsDuplicatedName(idx.Name) || !slices.Contains(idx.Columns, TemporaryName(column.Name)) {
127127
continue
128128
}
129129

130-
if slices.Contains(idx.Columns, TemporaryName(column.Name)) {
131-
// Rename the index to its original name
132-
renameIndexSQL := fmt.Sprintf(cRenameIndexSQL,
133-
pq.QuoteIdentifier(idx.Name),
134-
pq.QuoteIdentifier(StripDuplicationPrefix(idx.Name)),
135-
)
130+
// Rename the index to its original name
131+
renameIndexSQL := fmt.Sprintf(cRenameIndexSQL,
132+
pq.QuoteIdentifier(idx.Name),
133+
pq.QuoteIdentifier(StripDuplicationPrefix(idx.Name)),
134+
)
136135

137-
_, err = conn.ExecContext(ctx, renameIndexSQL)
138-
if err != nil {
139-
return fmt.Errorf("failed to rename index %q: %w", idx.Name, err)
140-
}
141-
142-
// Index no longer exists, remove it from the table
143-
delete(table.Indexes, idx.Name)
136+
_, err = conn.ExecContext(ctx, renameIndexSQL)
137+
if err != nil {
138+
return fmt.Errorf("failed to rename index %q: %w", idx.Name, err)
144139
}
145140

141+
// Index no longer exists, remove it from the table
142+
delete(table.Indexes, idx.Name)
143+
146144
if _, ok := table.UniqueConstraints[StripDuplicationPrefix(idx.Name)]; idx.Unique && ok {
147145
// Create a unique constraint using the unique index
148146
createUniqueConstraintSQL := fmt.Sprintf(cCreateUniqueConstraintSQL,
@@ -156,7 +154,6 @@ func RenameDuplicatedColumn(ctx context.Context, conn db.DB, table *schema.Table
156154
return fmt.Errorf("failed to create unique constraint from index %q: %w", idx.Name, err)
157155
}
158156
}
159-
160157
}
161158

162159
return nil

0 commit comments

Comments
 (0)