diff --git a/database/mock/store.go b/database/mock/store.go index cd9465d282..fead638000 100644 --- a/database/mock/store.go +++ b/database/mock/store.go @@ -396,6 +396,20 @@ func (mr *MockStoreMockRecorder) DeleteProfileForEntity(arg0, arg1 any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteProfileForEntity", reflect.TypeOf((*MockStore)(nil).DeleteProfileForEntity), arg0, arg1) } +// DeleteProfilesInProject mocks base method. +func (m *MockStore) DeleteProfilesInProject(arg0 context.Context, arg1 uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteProfilesInProject", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteProfilesInProject indicates an expected call of DeleteProfilesInProject. +func (mr *MockStoreMockRecorder) DeleteProfilesInProject(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteProfilesInProject", reflect.TypeOf((*MockStore)(nil).DeleteProfilesInProject), arg0, arg1) +} + // DeleteProject mocks base method. func (m *MockStore) DeleteProject(arg0 context.Context, arg1 uuid.UUID) ([]db.DeleteProjectRow, error) { m.ctrl.T.Helper() diff --git a/database/query/profiles.sql b/database/query/profiles.sql index df12c01b5b..08af717d28 100644 --- a/database/query/profiles.sql +++ b/database/query/profiles.sql @@ -106,7 +106,6 @@ JOIN entity_profile_rules ON entity_profiles.id = entity_profile_rules.entity_pr WHERE entity_profile_rules.rule_type_id = $1 GROUP BY profiles.id; - -- name: CountProfilesByEntityType :many SELECT COUNT(p.id) AS num_profiles, ep.entity AS profile_entity FROM profiles AS p @@ -115,3 +114,7 @@ GROUP BY ep.entity; -- name: CountProfilesByName :one SELECT COUNT(*) AS num_named_profiles FROM profiles WHERE lower(name) = lower(sqlc.arg(name)); + +-- used when cleaning up a project to avoid FK dependency issues between rule_types and rule_instances +-- name: DeleteProfilesInProject :exec +DELETE FROM profiles WHERE project_id = $1; \ No newline at end of file diff --git a/internal/db/profiles.sql.go b/internal/db/profiles.sql.go index 8a984351e4..4cc4b59dcb 100644 --- a/internal/db/profiles.sql.go +++ b/internal/db/profiles.sql.go @@ -174,6 +174,16 @@ func (q *Queries) DeleteProfileForEntity(ctx context.Context, arg DeleteProfileF return err } +const deleteProfilesInProject = `-- name: DeleteProfilesInProject :exec +DELETE FROM profiles WHERE project_id = $1 +` + +// used when cleaning up a project to avoid FK dependency issues between rule_types and rule_instances +func (q *Queries) DeleteProfilesInProject(ctx context.Context, projectID uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteProfilesInProject, projectID) + return err +} + const deleteRuleInstantiation = `-- name: DeleteRuleInstantiation :exec DELETE FROM entity_profile_rules WHERE entity_profile_id = $1 AND rule_type_id = $2 ` diff --git a/internal/db/querier.go b/internal/db/querier.go index 5d26672009..3d568f0ef0 100644 --- a/internal/db/querier.go +++ b/internal/db/querier.go @@ -35,6 +35,8 @@ type Querier interface { DeleteNonUpdatedRules(ctx context.Context, arg DeleteNonUpdatedRulesParams) error DeleteProfile(ctx context.Context, arg DeleteProfileParams) error DeleteProfileForEntity(ctx context.Context, arg DeleteProfileForEntityParams) error + // used when cleaning up a project to avoid FK dependency issues between rule_types and rule_instances + DeleteProfilesInProject(ctx context.Context, projectID uuid.UUID) error DeleteProject(ctx context.Context, id uuid.UUID) ([]DeleteProjectRow, error) DeleteProvider(ctx context.Context, arg DeleteProviderParams) error DeletePullRequest(ctx context.Context, arg DeletePullRequestParams) error diff --git a/internal/projects/deleter.go b/internal/projects/deleter.go index b40d901595..75dfc8b886 100644 --- a/internal/projects/deleter.go +++ b/internal/projects/deleter.go @@ -145,6 +145,13 @@ func (p *projectDeleter) DeleteProject( } } + // Delete the project's profiles. This avoids foreign key issues with + // rule_instances when deleting rows from rule_type. + err = querier.DeleteProfilesInProject(ctx, proj) + if err != nil { + return fmt.Errorf("unable to delete profiles: %w", err) + } + // no role assignments for this project // we can safely delete it. l.Debug().Msg("deleting project from database") diff --git a/internal/projects/deleter_test.go b/internal/projects/deleter_test.go index 02377fa615..30ea54392e 100644 --- a/internal/projects/deleter_test.go +++ b/internal/projects/deleter_test.go @@ -45,6 +45,7 @@ func TestDeleteProjectOneProjectWithNoParents(t *testing.T) { mockStore := mockdb.NewMockStore(ctrl) mockStore.EXPECT().GetProjectByID(gomock.Any(), proj).Return( db.Project{ID: proj}, nil) + mockStore.EXPECT().DeleteProfilesInProject(gomock.Any(), proj) mockStore.EXPECT().DeleteProject(gomock.Any(), proj). Return([]db.DeleteProjectRow{ {ID: proj}, @@ -90,6 +91,7 @@ func TestDeleteProjectWithOneParent(t *testing.T) { }, nil) mockStore.EXPECT().ListProvidersByProjectID(gomock.Any(), []uuid.UUID{proj}). Return([]db.Provider{}, nil) + mockStore.EXPECT().DeleteProfilesInProject(gomock.Any(), proj) mockStore.EXPECT().DeleteProject(gomock.Any(), proj). Return([]db.DeleteProjectRow{ { @@ -141,6 +143,7 @@ func TestDeleteProjectProjectInThreeNodeHierarchy(t *testing.T) { }, nil) mockStore.EXPECT().ListProvidersByProjectID(gomock.Any(), []uuid.UUID{proj}). Return([]db.Provider{}, nil) + mockStore.EXPECT().DeleteProfilesInProject(gomock.Any(), proj) mockStore.EXPECT().DeleteProject(gomock.Any(), proj). Return([]db.DeleteProjectRow{ { @@ -198,6 +201,7 @@ func TestDeleteMiddleProjectInThreeNodeHierarchy(t *testing.T) { }, nil) mockStore.EXPECT().ListProvidersByProjectID(gomock.Any(), []uuid.UUID{proj}). Return([]db.Provider{}, nil) + mockStore.EXPECT().DeleteProfilesInProject(gomock.Any(), proj).Return(nil) mockStore.EXPECT().DeleteProject(gomock.Any(), proj). Return([]db.DeleteProjectRow{ { @@ -247,6 +251,7 @@ func TestDeleteProjectWithProvider(t *testing.T) { mockStore := mockdb.NewMockStore(ctrl) mockStore.EXPECT().GetProjectByID(gomock.Any(), proj).Return( db.Project{ID: proj}, nil) + mockStore.EXPECT().DeleteProfilesInProject(gomock.Any(), proj) mockStore.EXPECT().DeleteProject(gomock.Any(), proj). Return([]db.DeleteProjectRow{ {ID: proj}, @@ -302,6 +307,7 @@ func TestCleanupUnmanaged(t *testing.T) { mockStore.EXPECT().GetProjectByID(gomock.Any(), projThree).Return( db.Project{ID: projThree}, nil).Times(2) // Project 3 has no other admins, so it will be deleted. + mockStore.EXPECT().DeleteProfilesInProject(gomock.Any(), projThree).Return(nil) mockStore.EXPECT().DeleteProject(gomock.Any(), projThree).Return( []db.DeleteProjectRow{ {ID: projThree},