Skip to content

Commit ae55c6b

Browse files
committed
Alter PK column correctly even if it is part of a foreign key
1 parent a585f84 commit ae55c6b

File tree

3 files changed

+194
-0
lines changed

3 files changed

+194
-0
lines changed

pkg/migrations/dbactions.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,3 +736,75 @@ func (a *rawSQLAction) Execute(ctx context.Context) error {
736736
_, err := a.conn.ExecContext(ctx, a.sql)
737737
return err
738738
}
739+
740+
type alterReferencesAction struct {
741+
conn db.DB
742+
table string
743+
column string
744+
}
745+
746+
func NewAlterReferencesAction(conn db.DB, table, column string) *alterReferencesAction {
747+
return &alterReferencesAction{
748+
conn: conn,
749+
table: table,
750+
column: column,
751+
}
752+
}
753+
754+
func (a *alterReferencesAction) Execute(ctx context.Context) error {
755+
definitions, err := a.constraintDefinitions(ctx)
756+
if err != nil {
757+
return err
758+
}
759+
for _, def := range definitions {
760+
// Drop the existing constraint
761+
_, err := a.conn.ExecContext(ctx, fmt.Sprintf("ALTER TABLE %s DROP CONSTRAINT %s",
762+
pq.QuoteIdentifier(def.table),
763+
pq.QuoteIdentifier(def.name),
764+
))
765+
if err != nil {
766+
return fmt.Errorf("dropping constraint %s on %s: %w", def.name, def.table, err)
767+
}
768+
769+
// Recreate the constraint with the table and new column
770+
newDef := strings.ReplaceAll(def.def, a.column, TemporaryName(a.column))
771+
_, err = a.conn.ExecContext(ctx, fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT %s %s",
772+
pq.QuoteIdentifier(def.table),
773+
pq.QuoteIdentifier(def.name),
774+
newDef,
775+
))
776+
if err != nil {
777+
return fmt.Errorf("altering references for %s.%s: %w", a.table, a.column, err)
778+
}
779+
}
780+
return nil
781+
}
782+
783+
type constraintDefinition struct {
784+
name string
785+
table string
786+
def string
787+
}
788+
789+
func (a *alterReferencesAction) constraintDefinitions(ctx context.Context) ([]constraintDefinition, error) {
790+
rows, err := a.conn.QueryContext(ctx, fmt.Sprintf(`
791+
SELECT conname, r.conrelid::regclass, pg_catalog.pg_get_constraintdef(r.oid, true) as condef
792+
FROM pg_catalog.pg_constraint r
793+
WHERE confrelid = %s::regclass AND r.contype = 'f'`,
794+
pq.QuoteLiteral(a.table),
795+
))
796+
if err != nil {
797+
return nil, fmt.Errorf("querying referencing constraints for %s.%s: %w", a.table, a.column, err)
798+
}
799+
defer rows.Close()
800+
801+
defs := make([]constraintDefinition, 0)
802+
for rows.Next() {
803+
var def constraintDefinition
804+
if err := rows.Scan(&def.name, &def.table, &def.def); err != nil {
805+
return nil, fmt.Errorf("scanning referencing constraints for %s.%s: %w", a.table, a.column, err)
806+
}
807+
defs = append(defs, def)
808+
}
809+
return defs, rows.Err()
810+
}

pkg/migrations/op_alter_column.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ func (o *OpAlterColumn) Complete(l Logger, conn db.DB, s *schema.Schema) ([]DBAc
118118

119119
return append(dbActions, []DBAction{
120120
NewAlterSequenceOwnerAction(conn, o.Table, o.Column, TemporaryName(o.Column)),
121+
NewAlterReferencesAction(conn, o.Table, o.Column),
121122
NewDropColumnAction(conn, o.Table, o.Column),
122123
NewDropFunctionAction(conn,
123124
backfill.TriggerFunctionName(o.Table, o.Column),

pkg/migrations/op_alter_column_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,127 @@ func TestAlterPrimaryKeyColumns(t *testing.T) {
674674
})
675675
},
676676
},
677+
{
678+
name: "alter a single primary key column when the column is used in a foreign key constraint",
679+
migrations: []migrations.Migration{
680+
{
681+
Name: "01_create_table",
682+
Operations: migrations.Operations{
683+
&migrations.OpCreateTable{
684+
Name: "events",
685+
Columns: []migrations.Column{
686+
{
687+
Name: "id",
688+
Type: "serial",
689+
Pk: true,
690+
},
691+
{
692+
Name: "name",
693+
Type: "varchar(255)",
694+
Nullable: true,
695+
},
696+
},
697+
},
698+
&migrations.OpCreateTable{
699+
Name: "people",
700+
Columns: []migrations.Column{
701+
{
702+
Name: "id",
703+
Type: "int",
704+
Pk: true,
705+
},
706+
{
707+
Name: "name",
708+
Type: "varchar(255)",
709+
Nullable: true,
710+
},
711+
{
712+
Name: "manages",
713+
Type: "serial",
714+
Nullable: false,
715+
References: &migrations.ForeignKeyReference{
716+
Table: "events",
717+
Column: "id",
718+
Name: "person_manages_event_fk",
719+
},
720+
},
721+
},
722+
},
723+
},
724+
},
725+
{
726+
Name: "02_alter_column",
727+
Operations: migrations.Operations{
728+
&migrations.OpAlterColumn{
729+
Table: "people",
730+
Column: "id",
731+
Type: ptr("bigint"),
732+
Up: "CAST(id AS bigint)",
733+
Down: "SELECT CASE WHEN id < 2147483647 THEN CAST(id AS int) ELSE 0 END",
734+
},
735+
},
736+
},
737+
},
738+
afterStart: func(t *testing.T, db *sql.DB, schema string) {
739+
PrimaryKeyConstraintMustExist(t, db, schema, "people", "people_pkey")
740+
ValidatedForeignKeyMustExist(t, db, schema, "people", "person_manages_event_fk")
741+
742+
bigint := "31474836471" // A value larger than int can hold
743+
integer := "100"
744+
745+
// Inserting a row with integer id into the old schema should succeed
746+
MustInsert(t, db, schema, "01_create_table", "events", map[string]string{
747+
"name": "pgroll v1 release party",
748+
})
749+
MustInsert(t, db, schema, "01_create_table", "people", map[string]string{
750+
"id": integer,
751+
"name": "alice",
752+
"manages": "1",
753+
})
754+
// Inserting a row with integer bigint id into the old schema should fail
755+
MustNotInsert(t, db, schema, "01_create_table", "people", map[string]string{
756+
"id": bigint,
757+
"name": "alice",
758+
"manages": "1",
759+
}, testutils.NumericValueOutOfRangeErrorCode)
760+
761+
// Inserting a row with bigint id into the new schema should succeed
762+
MustInsert(t, db, schema, "02_alter_column", "people", map[string]string{
763+
"id": bigint,
764+
"name": "alice",
765+
"manages": "1",
766+
})
767+
// Inserting a row into the `people` table with a `manages` field that
768+
// violates the FK constraint fails
769+
MustNotInsert(t, db, schema, "02_alter_column", "people", map[string]string{
770+
"id": "10",
771+
"name": "alice",
772+
"manages": "2",
773+
}, testutils.FKViolationErrorCode)
774+
},
775+
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
776+
PrimaryKeyConstraintMustExist(t, db, schema, "people", "people_pkey")
777+
ValidatedForeignKeyMustExist(t, db, schema, "people", "person_manages_event_fk")
778+
},
779+
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
780+
PrimaryKeyConstraintMustExist(t, db, schema, "people", "people_pkey")
781+
ValidatedForeignKeyMustExist(t, db, schema, "people", "person_manages_event_fk")
782+
783+
// Inserting a row with integer bigint into the new schema should succeed
784+
MustInsert(t, db, schema, "02_alter_column", "people", map[string]string{
785+
"id": "31474836472",
786+
"name": "bob",
787+
"manages": "1",
788+
})
789+
// Inserting a row into the `people` table with a `manages` field that
790+
// violates the FK constraint fails
791+
MustNotInsert(t, db, schema, "02_alter_column", "people", map[string]string{
792+
"id": "31474836474",
793+
"name": "alice",
794+
"manages": "2",
795+
}, testutils.FKViolationErrorCode)
796+
},
797+
},
677798
})
678799
}
679800

0 commit comments

Comments
 (0)