From 7f919a01a1f272da60457c5d739b477939e614b0 Mon Sep 17 00:00:00 2001 From: Andrew Farries Date: Mon, 15 Jan 2024 10:21:49 +0000 Subject: [PATCH] Add test for FK preservation when changing type Ensure that changing a column's type preserves any foreign keys defined on the column. --- pkg/migrations/op_change_type_test.go | 320 ++++++++++++++++---------- 1 file changed, 199 insertions(+), 121 deletions(-) diff --git a/pkg/migrations/op_change_type_test.go b/pkg/migrations/op_change_type_test.go index 16df8886..b8208048 100644 --- a/pkg/migrations/op_change_type_test.go +++ b/pkg/migrations/op_change_type_test.go @@ -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) {