Skip to content

Commit

Permalink
Add test for FK preservation when changing type
Browse files Browse the repository at this point in the history
Ensure that changing a column's type preserves any foreign keys defined
on the column.
  • Loading branch information
andrew-farries committed Jan 15, 2024
1 parent 86d40c1 commit 7f919a0
Showing 1 changed file with 199 additions and 121 deletions.
320 changes: 199 additions & 121 deletions pkg/migrations/op_change_type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,137 +14,215 @@ import (
func TestChangeColumnType(t *testing.T) {
t.Parallel()

ExecuteTests(t, TestCases{{
name: "change column type",
migrations: []migrations.Migration{
{
Name: "01_add_table",
Operations: migrations.Operations{
&migrations.OpCreateTable{
Name: "reviews",
Columns: []migrations.Column{
{
Name: "id",
Type: "serial",
Pk: true,
},
{
Name: "username",
Type: "text",
ExecuteTests(t, TestCases{
{
name: "change column type",
migrations: []migrations.Migration{
{
Name: "01_add_table",
Operations: migrations.Operations{
&migrations.OpCreateTable{
Name: "reviews",
Columns: []migrations.Column{
{
Name: "id",
Type: "serial",
Pk: true,
},
{
Name: "username",
Type: "text",
},
{
Name: "product",
Type: "text",
},
{
Name: "rating",
Type: "text",
Default: ptr("0"),
},
},
{
Name: "product",
Type: "text",
},
},
},
{
Name: "02_change_type",
Operations: migrations.Operations{
&migrations.OpAlterColumn{
Table: "reviews",
Column: "rating",
Type: "integer",
Up: "CAST (rating AS integer)",
Down: "CAST (rating AS text)",
},
},
},
},
afterStart: func(t *testing.T, db *sql.DB) {
newVersionSchema := roll.VersionedSchemaName("public", "02_change_type")

// The new (temporary) `rating` column should exist on the underlying table.
ColumnMustExist(t, db, "public", "reviews", migrations.TemporaryName("rating"))

// The `rating` column in the new view must have the correct type.
ColumnMustHaveType(t, db, newVersionSchema, "reviews", "rating", "integer")

// Inserting into the new `rating` column should work.
MustInsert(t, db, "public", "02_change_type", "reviews", map[string]string{
"username": "alice",
"product": "apple",
"rating": "5",
})

// The value inserted into the new `rating` column has been backfilled into
// the old `rating` column.
rows := MustSelect(t, db, "public", "01_add_table", "reviews")
assert.Equal(t, []map[string]any{
{"id": 1, "username": "alice", "product": "apple", "rating": "5"},
}, rows)

// Inserting into the old `rating` column should work.
MustInsert(t, db, "public", "01_add_table", "reviews", map[string]string{
"username": "bob",
"product": "banana",
"rating": "8",
})

// The value inserted into the old `rating` column has been backfilled into
// the new `rating` column.
rows = MustSelect(t, db, "public", "02_change_type", "reviews")
assert.Equal(t, []map[string]any{
{"id": 1, "username": "alice", "product": "apple", "rating": 5},
{"id": 2, "username": "bob", "product": "banana", "rating": 8},
}, rows)
},
afterRollback: func(t *testing.T, db *sql.DB) {
// The new (temporary) `rating` column should not exist on the underlying table.
ColumnMustNotExist(t, db, "public", "reviews", migrations.TemporaryName("rating"))

// The up function no longer exists.
FunctionMustNotExist(t, db, "public", migrations.TriggerFunctionName("reviews", "rating"))
// The down function no longer exists.
FunctionMustNotExist(t, db, "public", migrations.TriggerFunctionName("reviews", migrations.TemporaryName("rating")))

// The up trigger no longer exists.
TriggerMustNotExist(t, db, "public", "reviews", migrations.TriggerName("reviews", "rating"))
// The down trigger no longer exists.
TriggerMustNotExist(t, db, "public", "reviews", migrations.TriggerName("reviews", migrations.TemporaryName("rating")))
},
afterComplete: func(t *testing.T, db *sql.DB) {
newVersionSchema := roll.VersionedSchemaName("public", "02_change_type")

// The new (temporary) `rating` column should not exist on the underlying table.
ColumnMustNotExist(t, db, "public", "reviews", migrations.TemporaryName("rating"))

// The `rating` column in the new view must have the correct type.
ColumnMustHaveType(t, db, newVersionSchema, "reviews", "rating", "integer")

// Inserting into the new view should work.
MustInsert(t, db, "public", "02_change_type", "reviews", map[string]string{
"username": "carl",
"product": "carrot",
"rating": "3",
})

// Selecting from the new view should succeed.
rows := MustSelect(t, db, "public", "02_change_type", "reviews")
assert.Equal(t, []map[string]any{
{"id": 1, "username": "alice", "product": "apple", "rating": 5},
{"id": 2, "username": "bob", "product": "banana", "rating": 8},
{"id": 3, "username": "carl", "product": "carrot", "rating": 3},
}, rows)

// The up function no longer exists.
FunctionMustNotExist(t, db, "public", migrations.TriggerFunctionName("reviews", "rating"))
// The down function no longer exists.
FunctionMustNotExist(t, db, "public", migrations.TriggerFunctionName("reviews", migrations.TemporaryName("rating")))

// The up trigger no longer exists.
TriggerMustNotExist(t, db, "public", "reviews", migrations.TriggerName("reviews", "rating"))
// The down trigger no longer exists.
TriggerMustNotExist(t, db, "public", "reviews", migrations.TriggerName("reviews", migrations.TemporaryName("rating")))
},
},
{
name: "changing column type preserves any foreign key constraints on the column",
migrations: []migrations.Migration{
{
Name: "01_add_departments_table",
Operations: migrations.Operations{
&migrations.OpCreateTable{
Name: "departments",
Columns: []migrations.Column{
{
Name: "id",
Type: "serial",
Pk: true,
},
{
Name: "name",
Type: "text",
Nullable: false,
},
},
{
Name: "rating",
Type: "text",
Default: ptr("0"),
},
},
},
{
Name: "02_add_employees_table",
Operations: migrations.Operations{
&migrations.OpCreateTable{
Name: "employees",
Columns: []migrations.Column{
{
Name: "id",
Type: "serial",
Pk: true,
},
{
Name: "name",
Type: "text",
Nullable: false,
},
{
Name: "department_id",
Type: "integer",
References: &migrations.ForeignKeyReference{
Name: "fk_employee_department",
Table: "departments",
Column: "id",
},
},
},
},
},
},
},
{
Name: "02_change_type",
Operations: migrations.Operations{
&migrations.OpAlterColumn{
Table: "reviews",
Column: "rating",
Type: "integer",
Up: "CAST (rating AS integer)",
Down: "CAST (rating AS text)",
{
Name: "03_change_type",
Operations: migrations.Operations{
&migrations.OpAlterColumn{
Table: "employees",
Column: "department_id",
Type: "bigint",
Up: "department_id",
Down: "department_id",
},
},
},
},
afterStart: func(t *testing.T, db *sql.DB) {
// A temporary FK constraint has been created on the temporary column
ConstraintMustExist(t, db, "public", "employees", migrations.TemporaryName("fk_employee_department"))
},
afterRollback: func(t *testing.T, db *sql.DB) {
},
afterComplete: func(t *testing.T, db *sql.DB) {
// The foreign key constraint still exists on the column
ConstraintMustExist(t, db, "public", "employees", "fk_employee_department")
},
},
afterStart: func(t *testing.T, db *sql.DB) {
newVersionSchema := roll.VersionedSchemaName("public", "02_change_type")

// The new (temporary) `rating` column should exist on the underlying table.
ColumnMustExist(t, db, "public", "reviews", migrations.TemporaryName("rating"))

// The `rating` column in the new view must have the correct type.
ColumnMustHaveType(t, db, newVersionSchema, "reviews", "rating", "integer")

// Inserting into the new `rating` column should work.
MustInsert(t, db, "public", "02_change_type", "reviews", map[string]string{
"username": "alice",
"product": "apple",
"rating": "5",
})

// The value inserted into the new `rating` column has been backfilled into
// the old `rating` column.
rows := MustSelect(t, db, "public", "01_add_table", "reviews")
assert.Equal(t, []map[string]any{
{"id": 1, "username": "alice", "product": "apple", "rating": "5"},
}, rows)

// Inserting into the old `rating` column should work.
MustInsert(t, db, "public", "01_add_table", "reviews", map[string]string{
"username": "bob",
"product": "banana",
"rating": "8",
})

// The value inserted into the old `rating` column has been backfilled into
// the new `rating` column.
rows = MustSelect(t, db, "public", "02_change_type", "reviews")
assert.Equal(t, []map[string]any{
{"id": 1, "username": "alice", "product": "apple", "rating": 5},
{"id": 2, "username": "bob", "product": "banana", "rating": 8},
}, rows)
},
afterRollback: func(t *testing.T, db *sql.DB) {
// The new (temporary) `rating` column should not exist on the underlying table.
ColumnMustNotExist(t, db, "public", "reviews", migrations.TemporaryName("rating"))

// The up function no longer exists.
FunctionMustNotExist(t, db, "public", migrations.TriggerFunctionName("reviews", "rating"))
// The down function no longer exists.
FunctionMustNotExist(t, db, "public", migrations.TriggerFunctionName("reviews", migrations.TemporaryName("rating")))

// The up trigger no longer exists.
TriggerMustNotExist(t, db, "public", "reviews", migrations.TriggerName("reviews", "rating"))
// The down trigger no longer exists.
TriggerMustNotExist(t, db, "public", "reviews", migrations.TriggerName("reviews", migrations.TemporaryName("rating")))
},
afterComplete: func(t *testing.T, db *sql.DB) {
newVersionSchema := roll.VersionedSchemaName("public", "02_change_type")

// The new (temporary) `rating` column should not exist on the underlying table.
ColumnMustNotExist(t, db, "public", "reviews", migrations.TemporaryName("rating"))

// The `rating` column in the new view must have the correct type.
ColumnMustHaveType(t, db, newVersionSchema, "reviews", "rating", "integer")

// Inserting into the new view should work.
MustInsert(t, db, "public", "02_change_type", "reviews", map[string]string{
"username": "carl",
"product": "carrot",
"rating": "3",
})

// Selecting from the new view should succeed.
rows := MustSelect(t, db, "public", "02_change_type", "reviews")
assert.Equal(t, []map[string]any{
{"id": 1, "username": "alice", "product": "apple", "rating": 5},
{"id": 2, "username": "bob", "product": "banana", "rating": 8},
{"id": 3, "username": "carl", "product": "carrot", "rating": 3},
}, rows)

// The up function no longer exists.
FunctionMustNotExist(t, db, "public", migrations.TriggerFunctionName("reviews", "rating"))
// The down function no longer exists.
FunctionMustNotExist(t, db, "public", migrations.TriggerFunctionName("reviews", migrations.TemporaryName("rating")))

// The up trigger no longer exists.
TriggerMustNotExist(t, db, "public", "reviews", migrations.TriggerName("reviews", "rating"))
// The down trigger no longer exists.
TriggerMustNotExist(t, db, "public", "reviews", migrations.TriggerName("reviews", migrations.TemporaryName("rating")))
},
}})
})
}

func TestChangeColumnTypeValidation(t *testing.T) {
Expand Down

0 comments on commit 7f919a0

Please sign in to comment.