Skip to content

Commit a9b35b5

Browse files
Add support for alter_table set unique operations in multi-operation migrations (#609)
Ensure that multi-operation migrations combining `alter_column` operations setting columns to `UNIQUE` works in combination with other operations. Add testcases for: * rename table, set column unique * rename table, rename column, set column unique * create table, set column unique Previously these migrations would fail as the `alter_column` operation was unaware of the changes made by the preceding operation. Part of #239
1 parent cec0d80 commit a9b35b5

File tree

2 files changed

+226
-5
lines changed

2 files changed

+226
-5
lines changed

pkg/migrations/op_set_unique.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ var _ Operation = (*OpSetUnique)(nil)
2323

2424
func (o *OpSetUnique) Start(ctx context.Context, conn db.DB, latestSchema string, tr SQLTransformer, s *schema.Schema, cbs ...CallbackFn) (*schema.Table, error) {
2525
table := s.GetTable(o.Table)
26+
column := table.GetColumn(o.Column)
2627

2728
// Add a unique index to the new column
28-
if err := o.addUniqueIndex(ctx, conn); err != nil {
29+
if err := addUniqueIndex(ctx, conn, table.Name, column.Name, o.Name); err != nil {
2930
return nil, fmt.Errorf("failed to add unique index: %w", err)
3031
}
3132

@@ -73,12 +74,12 @@ func (o *OpSetUnique) Validate(ctx context.Context, s *schema.Schema) error {
7374
return nil
7475
}
7576

76-
func (o *OpSetUnique) addUniqueIndex(ctx context.Context, conn db.DB) error {
77+
func addUniqueIndex(ctx context.Context, conn db.DB, table, column, name string) error {
7778
// create unique index concurrently
7879
_, err := conn.ExecContext(ctx, fmt.Sprintf("CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS %s ON %s (%s)",
79-
pq.QuoteIdentifier(o.Name),
80-
pq.QuoteIdentifier(o.Table),
81-
pq.QuoteIdentifier(TemporaryName(o.Column))))
80+
pq.QuoteIdentifier(name),
81+
pq.QuoteIdentifier(table),
82+
pq.QuoteIdentifier(column)))
8283

8384
return err
8485
}

pkg/migrations/op_set_unique_test.go

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,3 +565,223 @@ func TestSetColumnUnique(t *testing.T) {
565565
},
566566
})
567567
}
568+
569+
func TestSetUniqueInMultiOperationMigrations(t *testing.T) {
570+
t.Parallel()
571+
572+
ExecuteTests(t, TestCases{
573+
{
574+
name: "rename table, set unique",
575+
migrations: []migrations.Migration{
576+
{
577+
Name: "01_create_table",
578+
Operations: migrations.Operations{
579+
&migrations.OpCreateTable{
580+
Name: "items",
581+
Columns: []migrations.Column{
582+
{
583+
Name: "id",
584+
Type: "serial",
585+
Pk: true,
586+
},
587+
{
588+
Name: "name",
589+
Type: "varchar(255)",
590+
Nullable: true,
591+
},
592+
},
593+
},
594+
},
595+
},
596+
{
597+
Name: "02_multi_operation",
598+
Operations: migrations.Operations{
599+
&migrations.OpRenameTable{
600+
From: "items",
601+
To: "products",
602+
},
603+
&migrations.OpAlterColumn{
604+
Table: "products",
605+
Column: "name",
606+
Unique: &migrations.UniqueConstraint{
607+
Name: "products_name_unique",
608+
},
609+
Up: "name || '-' || floor(random()*100000)::text",
610+
Down: "name",
611+
},
612+
},
613+
},
614+
},
615+
afterStart: func(t *testing.T, db *sql.DB, schema string) {
616+
// Can insert a row that meets the constraint into the new view
617+
MustInsert(t, db, schema, "02_multi_operation", "products", map[string]string{
618+
"name": "apple",
619+
})
620+
621+
// Can't insert a row that violates the constraint into the new view
622+
MustNotInsert(t, db, schema, "02_multi_operation", "products", map[string]string{
623+
"name": "apple",
624+
}, testutils.UniqueViolationErrorCode)
625+
626+
// Can insert a row that violates the constraint into the old view
627+
MustInsert(t, db, schema, "01_create_table", "items", map[string]string{
628+
"name": "apple",
629+
})
630+
},
631+
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
632+
// The table has been cleaned up
633+
TableMustBeCleanedUp(t, db, schema, "items", "name")
634+
},
635+
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
636+
// Can insert a row that meets the constraint into the new view
637+
MustInsert(t, db, schema, "02_multi_operation", "products", map[string]string{
638+
"name": "banana",
639+
})
640+
641+
// Can't insert a row that violates the constraint into the new view
642+
MustNotInsert(t, db, schema, "02_multi_operation", "products", map[string]string{
643+
"name": "banana",
644+
}, testutils.UniqueViolationErrorCode)
645+
},
646+
},
647+
{
648+
name: "rename table, rename column, set unique",
649+
migrations: []migrations.Migration{
650+
{
651+
Name: "01_create_table",
652+
Operations: migrations.Operations{
653+
&migrations.OpCreateTable{
654+
Name: "items",
655+
Columns: []migrations.Column{
656+
{
657+
Name: "id",
658+
Type: "serial",
659+
Pk: true,
660+
},
661+
{
662+
Name: "name",
663+
Type: "varchar(255)",
664+
Nullable: true,
665+
},
666+
},
667+
},
668+
},
669+
},
670+
{
671+
Name: "02_multi_operation",
672+
Operations: migrations.Operations{
673+
&migrations.OpRenameTable{
674+
From: "items",
675+
To: "products",
676+
},
677+
&migrations.OpRenameColumn{
678+
Table: "products",
679+
From: "name",
680+
To: "item_name",
681+
},
682+
&migrations.OpAlterColumn{
683+
Table: "products",
684+
Column: "item_name",
685+
Unique: &migrations.UniqueConstraint{
686+
Name: "products_name_unique",
687+
},
688+
Up: "item_name || '-' || floor(random()*100000)::text",
689+
Down: "item_name",
690+
},
691+
},
692+
},
693+
},
694+
afterStart: func(t *testing.T, db *sql.DB, schema string) {
695+
// Can insert a row that meets the constraint into the new view
696+
MustInsert(t, db, schema, "02_multi_operation", "products", map[string]string{
697+
"item_name": "apple",
698+
})
699+
700+
// Can't insert a row that violates the constraint into the new view
701+
MustNotInsert(t, db, schema, "02_multi_operation", "products", map[string]string{
702+
"item_name": "apple",
703+
}, testutils.UniqueViolationErrorCode)
704+
705+
// Can insert a row that violates the constraint into the old view
706+
MustInsert(t, db, schema, "01_create_table", "items", map[string]string{
707+
"name": "apple",
708+
})
709+
},
710+
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
711+
// The table has been cleaned up
712+
TableMustBeCleanedUp(t, db, schema, "items", "name")
713+
},
714+
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
715+
// Can insert a row that meets the constraint into the new view
716+
MustInsert(t, db, schema, "02_multi_operation", "products", map[string]string{
717+
"item_name": "banana",
718+
})
719+
720+
// Can't insert a row that violates the constraint into the new view
721+
MustNotInsert(t, db, schema, "02_multi_operation", "products", map[string]string{
722+
"item_name": "banana",
723+
}, testutils.UniqueViolationErrorCode)
724+
},
725+
},
726+
{
727+
name: "create table, set unique",
728+
migrations: []migrations.Migration{
729+
{
730+
Name: "01_multi_operation",
731+
Operations: migrations.Operations{
732+
&migrations.OpCreateTable{
733+
Name: "items",
734+
Columns: []migrations.Column{
735+
{
736+
Name: "id",
737+
Type: "serial",
738+
Pk: true,
739+
},
740+
{
741+
Name: "name",
742+
Type: "varchar(255)",
743+
Nullable: true,
744+
},
745+
},
746+
},
747+
&migrations.OpAlterColumn{
748+
Table: "items",
749+
Column: "name",
750+
Unique: &migrations.UniqueConstraint{
751+
Name: "items_name_unique",
752+
},
753+
Up: "name || '-' || floor(random()*100000)::text",
754+
Down: "name",
755+
},
756+
},
757+
},
758+
},
759+
afterStart: func(t *testing.T, db *sql.DB, schema string) {
760+
// Can insert a row into the new (only) schema that meets the constraint
761+
MustInsert(t, db, schema, "01_multi_operation", "items", map[string]string{
762+
"name": "apple",
763+
})
764+
765+
// Can't insert a row into the new (only) schema that violates the constraint
766+
MustNotInsert(t, db, schema, "01_multi_operation", "items", map[string]string{
767+
"name": "apple",
768+
}, testutils.UniqueViolationErrorCode)
769+
},
770+
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
771+
// The table has been dropped
772+
TableMustNotExist(t, db, schema, "items")
773+
},
774+
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
775+
// Can insert a row into the new (only) schema that meets the constraint
776+
MustInsert(t, db, schema, "01_multi_operation", "items", map[string]string{
777+
"name": "apple",
778+
})
779+
780+
// Can't insert a row into the new (only) schema that violates the constraint
781+
MustNotInsert(t, db, schema, "01_multi_operation", "items", map[string]string{
782+
"name": "apple",
783+
}, testutils.UniqueViolationErrorCode)
784+
},
785+
},
786+
})
787+
}

0 commit comments

Comments
 (0)