Skip to content

Commit

Permalink
Implement a foreign key for subjects
Browse files Browse the repository at this point in the history
We want to be able to nest subjects and build a hierarchical
relationship between them. This commit does that by adding a database
migration that adds a new column for parent_id and adds a FK
relationship to it.

This approach was discussed in #84
  • Loading branch information
rhmdnd committed Jul 21, 2022
1 parent 91fc82f commit 02961d5
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 4 deletions.
3 changes: 3 additions & 0 deletions migrations/000002_add_subject_parent_id.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE subjects DROP CONSTRAINT fk_subjects_parent_id;

ALTER TABLE subjects DROP COLUMN parent_id;
5 changes: 5 additions & 0 deletions migrations/000002_add_subject_parent_id.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE subjects
ADD COLUMN parent_id UUID;

ALTER TABLE subjects
ADD CONSTRAINT fk_subjects_parent_id FOREIGN KEY (parent_id) REFERENCES subjects (id);
9 changes: 6 additions & 3 deletions tests/helpers.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package tests

import "database/sql"

type Subject struct {
ID string
Name string
Type string
ID string
Name string
Type string
ParentID sql.NullString
}
109 changes: 108 additions & 1 deletion tests/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ func TestInsertSubjectSucceeds(t *testing.T) { // nolint:paralleltest // databas
assert.Equal(t, expectedSubject.ID, subject.ID, "expected %s got %s", expectedSubject.ID, subject.ID)
assert.Equal(t, expectedSubject.Name, subject.Name, "expected %s got %s", expectedSubject.Name, subject.Name)
assert.Equal(t, expectedSubject.Type, subject.Type, "expected %s got %s", expectedSubject.Type, subject.Type)
parentIDValue, err := subject.ParentID.Value()
assert.False(t, subject.ParentID.Valid)
assert.Nil(t, err)
assert.Nil(t, parentIDValue)

// Drop the database instead of downgrading since we don't need the
// data anyway
Expand Down Expand Up @@ -187,6 +191,66 @@ func TestInsertSubjectWithLongTypeFails(t *testing.T) { // nolint:paralleltest /
}
}

func TestInsertSubjectWithParent(t *testing.T) { // nolint:paralleltest // database tests should run serially
m := getMigrationHelper(t)
if err := m.Up(); err != nil {
t.Fatalf("Unable to upgrade database: %s", err)
}

// Create a subject to act as the parent
parentID := getUUIDString()
subjectTypeStr := getUUIDString()
p := Subject{ID: parentID, Name: clusterName, Type: subjectTypeStr}
gormDB := getGormHelper()
gormDB.Create(&p)

// Create another subject referencing the parent
id := getUUIDString()
subjectTypeStr = getUUIDString()
s := Subject{ID: id, Name: clusterName, Type: subjectTypeStr, ParentID: sql.NullString{String: parentID, Valid: true}}
gormDB.Create(&s)

a := Subject{}
gormDB.First(&a, "id = ?", id)

e := Subject{ID: id, Name: clusterName, Type: subjectTypeStr, ParentID: sql.NullString{String: parentID, Valid: true}}
assert.Equal(t, e.ID, a.ID, "expected %s got %s", e.ID, a.ID)
assert.Equal(t, e.Name, a.Name, "expected %s got %s", e.Name, a.Name)
assert.Equal(t, e.Type, a.Type, "expected %s got %s", e.Type, a.Type)
assert.True(t, a.ParentID.Valid)
assert.Equal(t, e.ParentID.String, a.ParentID.String, "expected %s got %s", e.ParentID.String, a.ParentID.String)

// Drop the database instead of downgrading since we don't need the
// data anyway
if err := m.Drop(); err != nil {
t.Fatalf("Unable to drop database: %s", err)
}
}

func TestInsertSubjectWithNonExistentParent(t *testing.T) { // nolint:paralleltest // database tests should run serially
m := getMigrationHelper(t)
if err := m.Up(); err != nil {
t.Fatalf("Unable to upgrade database: %s", err)
}

// Generate a fake parent ID that doesn't actually exist
parentID := getUUIDString()

// Create another subject referencing the parent
id := getUUIDString()
subjectTypeStr := getUUIDString()
s := Subject{ID: id, Name: clusterName, Type: subjectTypeStr, ParentID: sql.NullString{String: parentID, Valid: true}}
gormDB := getGormHelper()
err := gormDB.Create(&s).Error
assert.NotEmpty(t, err, "Shouldn't be able to insert values the violate foreign key constraints")

// Drop the database instead of downgrading since we don't need the
// data anyway
if err := m.Drop(); err != nil {
t.Fatalf("Unable to drop database: %s", err)
}
}

func TestMigration(t *testing.T) { // nolint:paralleltest // database tests should run serially
m := getMigrationHelper(t)

Expand All @@ -205,7 +269,7 @@ func TestMigration(t *testing.T) { // nolint:paralleltest // database tests shou
// Upgrade the database and make sure all upgrades apply cleanly.
err = m.Up()
version, dirty, _ = m.Version()
expectedVersion = uint(1)
expectedVersion = uint(2)
assert.Equal(t, expectedVersion, version, "Database version mismatch: want %d but got %d", expectedVersion, version)
assert.Equal(t, false, dirty, "Database state mismatch: want %t but got %t", false, dirty)
assert.Equal(t, err, nil, "Error upgrading the database: %s", err)
Expand All @@ -218,3 +282,46 @@ func TestMigration(t *testing.T) { // nolint:paralleltest // database tests shou
assert.Equal(t, false, dirty, "Database state mismatch: want %t but got %t", false, dirty)
assert.Equal(t, err, nil, "Error downgrading the database: %s", err)
}

func TestSubjectParentIDMigration(t *testing.T) { // nolint:paralleltest // database tests should run serially
m := getMigrationHelper(t)
gormDB := getGormHelper()

type subject struct{}
tableName := "subjects"
columnName := "parent_id"

m.Migrate(1)
result := gormDB.Migrator().HasTable(tableName)
assert.True(t, result, "Table doesn't exist: %s", tableName)

for _, s := range []string{"id", "name", "type"} {
result = gormDB.Migrator().HasColumn(&subject{}, s)
assert.True(t, result, "Table doesn't have column: %s", s)
}
result = gormDB.Migrator().HasColumn(&subject{}, columnName)
assert.False(t, result, "Table has column: %s", columnName)

// Ensure the upgrade adds the parent_id colume and the constraint
m.Migrate(2)
result = gormDB.Migrator().HasTable(tableName)
assert.True(t, result, "Table doesn't exist: %s", tableName)

for _, s := range []string{"id", "name", "type", "parent_id"} {
result = gormDB.Migrator().HasColumn(&subject{}, s)
assert.True(t, result, "Table doesn't have column: %s", s)
}

constraintName := "fk_subjects_parent_id"
result = gormDB.Migrator().HasConstraint(&subject{}, constraintName)
assert.True(t, result, "Table doesn't have constraint: %s", constraintName)

// Make sure the downgrade removes the parent_id colume and the
// constraint
m.Migrate(1)
result = gormDB.Migrator().HasColumn(&subject{}, columnName)
assert.False(t, result, "Table has column: %s", columnName)

result = gormDB.Migrator().HasConstraint(&subject{}, constraintName)
assert.False(t, result, "Table has constraint: %s", constraintName)
}

0 comments on commit 02961d5

Please sign in to comment.