Skip to content

Commit

Permalink
Strengthen assertions for tests that check preservation of UNIQUE c…
Browse files Browse the repository at this point in the history
…onstraints (#277)

Strengthen the tests that check for preservation of `UNIQUE` constraints
when a column is duplicated so that they can check for the failure case
in #273.

It's not enough to test that the column does not accept duplicate values
after the migration completes. Both a unique index and a unique
constraint will have that effect but we need to ensure that the
**constraint** is present on the table once the migration completes.

Duplicating a `UNIQUE` constraint is a two-step process, first creating
a `UNIQUE` index on migration start and then upgrading the index to a
constraint on migration completion. #273 occurs when this upgrade fails
and the column is left with the unique index but not the constraint.
Subsequent migrations will then fail to duplicate the non-existent
unique constraint.

Part of #273
  • Loading branch information
andrew-farries committed Feb 6, 2024
1 parent 9586b44 commit 5686ddc
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 45 deletions.
30 changes: 22 additions & 8 deletions pkg/migrations/op_change_type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,16 +411,27 @@ func TestChangeColumnType(t *testing.T) {
Pk: ptr(true),
},
{
Name: "username",
Type: "text",
Unique: ptr(true),
Name: "username",
Type: "text",
},
},
},
},
},
{
Name: "02_change_type",
Name: "02_set_unique",
Operations: migrations.Operations{
&migrations.OpAlterColumn{
Table: "users",
Column: "username",
Unique: &migrations.UniqueConstraint{Name: "unique_username"},
Up: ptr("username"),
Down: ptr("username"),
},
},
},
{
Name: "03_change_type",
Operations: migrations.Operations{
&migrations.OpAlterColumn{
Table: "users",
Expand All @@ -434,25 +445,28 @@ func TestChangeColumnType(t *testing.T) {
},
afterStart: func(t *testing.T, db *sql.DB, schema string) {
// Inserting an initial row succeeds
MustInsert(t, db, schema, "02_change_type", "users", map[string]string{
MustInsert(t, db, schema, "03_change_type", "users", map[string]string{
"username": "alice",
})

// Inserting a row with a duplicate `username` value fails
MustNotInsert(t, db, schema, "02_change_type", "users", map[string]string{
MustNotInsert(t, db, schema, "03_change_type", "users", map[string]string{
"username": "alice",
}, testutils.UniqueViolationErrorCode)
},
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
},
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
// The table has a unique constraint defined on it
UniqueConstraintMustExist(t, db, schema, "users", "unique_username")

// Inserting a row with a duplicate `username` value fails
MustNotInsert(t, db, schema, "02_change_type", "users", map[string]string{
MustNotInsert(t, db, schema, "03_change_type", "users", map[string]string{
"username": "alice",
}, testutils.UniqueViolationErrorCode)

// Inserting a row with a different `username` value succeeds
MustInsert(t, db, schema, "02_change_type", "users", map[string]string{
MustInsert(t, db, schema, "03_change_type", "users", map[string]string{
"username": "bob",
})
},
Expand Down
27 changes: 27 additions & 0 deletions pkg/migrations/op_common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,13 @@ func CheckConstraintMustExist(t *testing.T, db *sql.DB, schema, table, constrain
}
}

func UniqueConstraintMustExist(t *testing.T, db *sql.DB, schema, table, constraint string) {
t.Helper()
if !uniqueConstraintExists(t, db, schema, table, constraint) {
t.Fatalf("Expected unique constraint %q to exist", constraint)
}
}

func ValidatedForeignKeyMustExist(t *testing.T, db *sql.DB, schema, table, constraint string) {
t.Helper()
if !foreignKeyExists(t, db, schema, table, constraint, true) {
Expand Down Expand Up @@ -286,6 +293,26 @@ func checkConstraintExists(t *testing.T, db *sql.DB, schema, table, constraint s
return exists
}

func uniqueConstraintExists(t *testing.T, db *sql.DB, schema, table, constraint string) bool {
t.Helper()

var exists bool
err := db.QueryRow(`
SELECT EXISTS (
SELECT 1
FROM pg_catalog.pg_constraint
WHERE conrelid = $1::regclass
AND conname = $2
AND contype = 'u'
)`,
fmt.Sprintf("%s.%s", schema, table), constraint).Scan(&exists)
if err != nil {
t.Fatal(err)
}

return exists
}

func foreignKeyExists(t *testing.T, db *sql.DB, schema, table, constraint string, validated bool) bool {
t.Helper()

Expand Down
28 changes: 21 additions & 7 deletions pkg/migrations/op_drop_constraint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -690,14 +690,25 @@ func TestDropConstraint(t *testing.T) {
Name: "title",
Type: "text",
Nullable: ptr(true),
Unique: ptr(true),
},
},
},
},
},
{
Name: "02_add_check_constraint",
Name: "02_set_unique",
Operations: migrations.Operations{
&migrations.OpAlterColumn{
Table: "posts",
Column: "title",
Unique: &migrations.UniqueConstraint{Name: "unique_title"},
Up: ptr("title"),
Down: ptr("title"),
},
},
},
{
Name: "03_add_check_constraint",
Operations: migrations.Operations{
&migrations.OpAlterColumn{
Table: "posts",
Expand All @@ -712,7 +723,7 @@ func TestDropConstraint(t *testing.T) {
},
},
{
Name: "03_drop_check_constraint",
Name: "04_drop_check_constraint",
Operations: migrations.Operations{
&migrations.OpDropConstraint{
Table: "posts",
Expand All @@ -726,25 +737,28 @@ func TestDropConstraint(t *testing.T) {
},
afterStart: func(t *testing.T, db *sql.DB, schema string) {
// Inserting an initial row into the `posts` table succeeds
MustInsert(t, db, schema, "03_drop_check_constraint", "posts", map[string]string{
MustInsert(t, db, schema, "04_drop_check_constraint", "posts", map[string]string{
"title": "post by alice",
})

// Inserting another row with a duplicate `title` value fails
MustNotInsert(t, db, schema, "03_drop_check_constraint", "posts", map[string]string{
MustNotInsert(t, db, schema, "04_drop_check_constraint", "posts", map[string]string{
"title": "post by alice",
}, testutils.UniqueViolationErrorCode)
},
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
},
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
// The table has a unique constraint defined on it
UniqueConstraintMustExist(t, db, schema, "posts", "unique_title")

// Inserting a row with a duplicate `title` value fails
MustNotInsert(t, db, schema, "03_drop_check_constraint", "posts", map[string]string{
MustNotInsert(t, db, schema, "04_drop_check_constraint", "posts", map[string]string{
"title": "post by alice",
}, testutils.UniqueViolationErrorCode)

// Inserting a row with a different `title` value succeeds
MustInsert(t, db, schema, "03_drop_check_constraint", "posts", map[string]string{
MustInsert(t, db, schema, "04_drop_check_constraint", "posts", map[string]string{
"title": "post by bob",
})
},
Expand Down
26 changes: 20 additions & 6 deletions pkg/migrations/op_drop_not_null_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,14 +434,25 @@ func TestDropNotNull(t *testing.T) {
Name: "name",
Type: "text",
Nullable: ptr(false),
Unique: ptr(true),
},
},
},
},
},
{
Name: "02_set_not_null",
Name: "02_set_unique",
Operations: migrations.Operations{
&migrations.OpAlterColumn{
Table: "users",
Column: "name",
Unique: &migrations.UniqueConstraint{Name: "unique_name"},
Up: ptr("name"),
Down: ptr("name"),
},
},
},
{
Name: "03_set_not_null",
Operations: migrations.Operations{
&migrations.OpAlterColumn{
Table: "users",
Expand All @@ -455,25 +466,28 @@ func TestDropNotNull(t *testing.T) {
},
afterStart: func(t *testing.T, db *sql.DB, schema string) {
// Inserting an initial row succeeds
MustInsert(t, db, schema, "02_set_not_null", "users", map[string]string{
MustInsert(t, db, schema, "03_set_not_null", "users", map[string]string{
"name": "alice",
})

// Inserting a row with a duplicate `name` value fails
MustNotInsert(t, db, schema, "02_set_not_null", "users", map[string]string{
MustNotInsert(t, db, schema, "03_set_not_null", "users", map[string]string{
"name": "alice",
}, testutils.UniqueViolationErrorCode)
},
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
},
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
// The table has a unique constraint defined on it
UniqueConstraintMustExist(t, db, schema, "users", "unique_name")

// Inserting a row with a duplicate `name` value fails
MustNotInsert(t, db, schema, "02_set_not_null", "users", map[string]string{
MustNotInsert(t, db, schema, "03_set_not_null", "users", map[string]string{
"name": "alice",
}, testutils.UniqueViolationErrorCode)

// Inserting a row with a different `name` value succeeds
MustInsert(t, db, schema, "02_set_not_null", "users", map[string]string{
MustInsert(t, db, schema, "03_set_not_null", "users", map[string]string{
"name": "bob",
})
},
Expand Down
30 changes: 22 additions & 8 deletions pkg/migrations/op_set_check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,16 +428,27 @@ func TestSetCheckConstraint(t *testing.T) {
Pk: ptr(true),
},
{
Name: "title",
Type: "text",
Unique: ptr(true),
Name: "title",
Type: "text",
},
},
},
},
},
{
Name: "02_add_check_constraint",
Name: "02_set_unique",
Operations: migrations.Operations{
&migrations.OpAlterColumn{
Table: "posts",
Column: "title",
Unique: &migrations.UniqueConstraint{Name: "unique_title"},
Up: ptr("title"),
Down: ptr("title"),
},
},
},
{
Name: "03_add_check_constraint",
Operations: migrations.Operations{
&migrations.OpAlterColumn{
Table: "posts",
Expand All @@ -454,25 +465,28 @@ func TestSetCheckConstraint(t *testing.T) {
},
afterStart: func(t *testing.T, db *sql.DB, schema string) {
// Inserting an initial row succeeds
MustInsert(t, db, schema, "02_add_check_constraint", "posts", map[string]string{
MustInsert(t, db, schema, "03_add_check_constraint", "posts", map[string]string{
"title": "post by alice",
})

// Inserting a row with a duplicate `title` value fails
MustNotInsert(t, db, schema, "02_add_check_constraint", "posts", map[string]string{
MustNotInsert(t, db, schema, "03_add_check_constraint", "posts", map[string]string{
"title": "post by alice",
}, testutils.UniqueViolationErrorCode)
},
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
},
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
// The table has a unique constraint defined on it
UniqueConstraintMustExist(t, db, schema, "posts", "unique_title")

// Inserting a row with a duplicate `title` value fails
MustNotInsert(t, db, schema, "02_add_check_constraint", "posts", map[string]string{
MustNotInsert(t, db, schema, "03_add_check_constraint", "posts", map[string]string{
"title": "post by alice",
}, testutils.UniqueViolationErrorCode)

// Inserting a row with a different `title` value succeeds
MustInsert(t, db, schema, "02_add_check_constraint", "posts", map[string]string{
MustInsert(t, db, schema, "03_add_check_constraint", "posts", map[string]string{
"title": "post by bob",
})
},
Expand Down
34 changes: 24 additions & 10 deletions pkg/migrations/op_set_fk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,16 +552,27 @@ func TestSetForeignKey(t *testing.T) {
Type: "text",
},
{
Name: "user_id",
Type: "integer",
Unique: ptr(true),
Name: "user_id",
Type: "integer",
},
},
},
},
},
{
Name: "02_add_fk_constraint",
Name: "02_set_unique",
Operations: migrations.Operations{
&migrations.OpAlterColumn{
Table: "posts",
Column: "user_id",
Unique: &migrations.UniqueConstraint{Name: "unique_user_id"},
Up: ptr("user_id"),
Down: ptr("user_id"),
},
},
},
{
Name: "03_add_fk_constraint",
Operations: migrations.Operations{
&migrations.OpAlterColumn{
Table: "posts",
Expand All @@ -579,40 +590,43 @@ func TestSetForeignKey(t *testing.T) {
},
afterStart: func(t *testing.T, db *sql.DB, schema string) {
// Set up the users table with a reference row
MustInsert(t, db, schema, "02_add_fk_constraint", "users", map[string]string{
MustInsert(t, db, schema, "03_add_fk_constraint", "users", map[string]string{
"name": "alice",
"id": "1",
})

// Inserting an initial row succeeds
MustInsert(t, db, schema, "02_add_fk_constraint", "posts", map[string]string{
MustInsert(t, db, schema, "03_add_fk_constraint", "posts", map[string]string{
"title": "post by alice",
"user_id": "1",
})

// Inserting a row with a duplicate `user_id` fails.
MustNotInsert(t, db, schema, "02_add_fk_constraint", "posts", map[string]string{
MustNotInsert(t, db, schema, "03_add_fk_constraint", "posts", map[string]string{
"title": "post by alice 2",
"user_id": "1",
}, testutils.UniqueViolationErrorCode)
},
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
},
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
// The 'posts' table has a unique constraint defined on it
UniqueConstraintMustExist(t, db, schema, "posts", "unique_user_id")

// Inserting a row with a duplicate `user_id` fails
MustNotInsert(t, db, schema, "02_add_fk_constraint", "posts", map[string]string{
MustNotInsert(t, db, schema, "03_add_fk_constraint", "posts", map[string]string{
"title": "post by alice 3",
"user_id": "1",
}, testutils.UniqueViolationErrorCode)

// Set up the users table with another reference row
MustInsert(t, db, schema, "02_add_fk_constraint", "users", map[string]string{
MustInsert(t, db, schema, "03_add_fk_constraint", "users", map[string]string{
"name": "bob",
"id": "2",
})

// Inserting a row with a different `user_id` succeeds
MustInsert(t, db, schema, "02_add_fk_constraint", "posts", map[string]string{
MustInsert(t, db, schema, "03_add_fk_constraint", "posts", map[string]string{
"title": "post by bob",
"user_id": "2",
})
Expand Down
Loading

0 comments on commit 5686ddc

Please sign in to comment.