Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions management/server/migration/migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,102 @@ func MigrateJsonToTable[T any](ctx context.Context, db *gorm.DB, columnName stri
return nil
}

// hasForeignKey checks whether a foreign key constraint exists on the given table and column.
func hasForeignKey(db *gorm.DB, table, column string) bool {
var count int64

switch db.Name() {
case "postgres":
db.Raw(`
SELECT COUNT(*) FROM information_schema.key_column_usage kcu
JOIN information_schema.table_constraints tc
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
WHERE tc.constraint_type = 'FOREIGN KEY'
AND kcu.table_name = ?
AND kcu.column_name = ?
`, table, column).Scan(&count)
case "mysql":
db.Raw(`
SELECT COUNT(*) FROM information_schema.key_column_usage
WHERE table_schema = DATABASE()
AND table_name = ?
AND column_name = ?
AND referenced_table_name IS NOT NULL
`, table, column).Scan(&count)
default: // sqlite
type fkInfo struct {
From string
}
var fks []fkInfo
db.Raw(fmt.Sprintf("PRAGMA foreign_key_list(%s)", table)).Scan(&fks)
for _, fk := range fks {
if fk.From == column {
return true
}
}
return false
}

return count > 0
Comment on lines +493 to +529
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the hasForeignKey function and surrounding context
rg -n "func hasForeignKey" management/server/migration/ -A 50

Repository: netbirdio/netbird

Length of output: 3828


🏁 Script executed:

# Also check for CleanupOrphanedResources to see how hasForeignKey is called
rg -n "CleanupOrphanedResources" management/server/migration/ -B 5 -A 20

Repository: netbirdio/netbird

Length of output: 15120


🏁 Script executed:

# Check if hasForeignKey is called elsewhere in the codebase
rg -n "hasForeignKey" --type go

Repository: netbirdio/netbird

Length of output: 397


🏁 Script executed:

# Get the context around the hasForeignKey call at line 567
sed -n '560,580p' management/server/migration/migration.go

Repository: netbirdio/netbird

Length of output: 816


🏁 Script executed:

# Check if there's error checking after hasForeignKey call
sed -n '560,585p' management/server/migration/migration.go

Repository: netbirdio/netbird

Length of output: 990


🏁 Script executed:

# Verify the exact error handling in hasForeignKey - check if Error is ever checked
sed -n '493,530p' management/server/migration/migration.go | grep -i "error\|\.Error"

Repository: netbirdio/netbird

Length of output: 43


Change hasForeignKey to return (bool, error) so metadata lookup failures abort instead of silently allowing data loss.

hasForeignKey() ignores .Scan().Error for every dialect. If metadata introspection fails (connection issue, permission denial, SQL syntax error), the function returns false, and CleanupOrphanedResources() proceeds with the destructive DELETE as if no FK existed. Return (bool, error) and have the caller check and propagate errors.

Proposed fix
-func hasForeignKey(db *gorm.DB, table, column string) bool {
+func hasForeignKey(db *gorm.DB, table, column string) (bool, error) {
 	var count int64

 	switch db.Name() {
 	case "postgres":
-		db.Raw(`
+		if err := db.Raw(`
 			SELECT COUNT(*) FROM information_schema.key_column_usage kcu
 			JOIN information_schema.table_constraints tc
 			  ON tc.constraint_name = kcu.constraint_name
 			  AND tc.table_schema = kcu.table_schema
 			WHERE tc.constraint_type = 'FOREIGN KEY'
 			  AND kcu.table_name = ?
 			  AND kcu.column_name = ?
-		`, table, column).Scan(&count)
+		`, table, column).Scan(&count).Error; err != nil {
+			return false, fmt.Errorf("inspect foreign keys for %s.%s: %w", table, column, err)
+		}
 	case "mysql":
-		db.Raw(`
+		if err := db.Raw(`
 			SELECT COUNT(*) FROM information_schema.key_column_usage
 			WHERE table_schema = DATABASE()
 			  AND table_name = ?
 			  AND column_name = ?
 			  AND referenced_table_name IS NOT NULL
-		`, table, column).Scan(&count)
+		`, table, column).Scan(&count).Error; err != nil {
+			return false, fmt.Errorf("inspect foreign keys for %s.%s: %w", table, column, err)
+		}
 	default: // sqlite
 		type fkInfo struct {
 			From string
 		}
 		var fks []fkInfo
-		db.Raw(fmt.Sprintf("PRAGMA foreign_key_list(%s)", table)).Scan(&fks)
+		if err := db.Raw(fmt.Sprintf("PRAGMA foreign_key_list(%s)", table)).Scan(&fks).Error; err != nil {
+			return false, fmt.Errorf("inspect foreign keys for %s.%s: %w", table, column, err)
+		}
 		for _, fk := range fks {
 			if fk.From == column {
-				return true
+				return true, nil
 			}
 		}
-		return false
+		return false, nil
 	}

-	return count > 0
+	return count > 0, nil
 }
-	if hasForeignKey(db, childTable, fkColumn) {
+	hasFK, err := hasForeignKey(db, childTable, fkColumn)
+	if err != nil {
+		return err
+	}
+	if hasFK {
 		log.WithContext(ctx).Debugf("foreign key constraint for %s already exists on %s, no cleanup needed", fkColumn, childTable)
 		return nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/migration/migration.go` around lines 493 - 529, Update
hasForeignKey to return (bool, error) and propagate DB errors instead of
swallowing them: change the signature of hasForeignKey(db *gorm.DB, table,
column string) to hasForeignKey(db *gorm.DB, table, column string) (bool,
error), capture and check the error returned by each db.Raw(...).Scan(...) call
(for postgres/mysql and for sqlite where you call db.Raw(fmt.Sprintf("PRAGMA
foreign_key_list(%s)", table)).Scan(&fks)) and return false with that error on
failure; for sqlite still inspect fkInfo.From entries and return (true, nil)
when found or (false, nil) when none found; then update callers (e.g.,
CleanupOrphanedResources) to handle the error return and abort/propagate instead
of assuming false.

}

// CleanupOrphanedResources deletes rows from the table of model T where the foreign
// key column (fkColumn) references a row in the table of model R that no longer exists.
func CleanupOrphanedResources[T any, R any](ctx context.Context, db *gorm.DB, fkColumn string) error {
var model T
var refModel R

if !db.Migrator().HasTable(&model) {
log.WithContext(ctx).Debugf("table for %T does not exist, no cleanup needed", model)
return nil
}

if !db.Migrator().HasTable(&refModel) {
log.WithContext(ctx).Debugf("referenced table for %T does not exist, no cleanup needed", refModel)
return nil
}

stmtT := &gorm.Statement{DB: db}
if err := stmtT.Parse(&model); err != nil {
return fmt.Errorf("parse model %T: %w", model, err)
}
childTable := stmtT.Schema.Table

stmtR := &gorm.Statement{DB: db}
if err := stmtR.Parse(&refModel); err != nil {
return fmt.Errorf("parse reference model %T: %w", refModel, err)
}
parentTable := stmtR.Schema.Table

if !db.Migrator().HasColumn(&model, fkColumn) {
log.WithContext(ctx).Debugf("column %s does not exist in table %s, no cleanup needed", fkColumn, childTable)
return nil
}

// If a foreign key constraint already exists on the column, the DB itself
// enforces referential integrity and orphaned rows cannot exist.
if hasForeignKey(db, childTable, fkColumn) {
log.WithContext(ctx).Debugf("foreign key constraint for %s already exists on %s, no cleanup needed", fkColumn, childTable)
return nil
}

result := db.Exec(
fmt.Sprintf(
"DELETE FROM %s WHERE %s NOT IN (SELECT id FROM %s)",
childTable, fkColumn, parentTable,
),
)
if result.Error != nil {
return fmt.Errorf("cleanup orphaned rows in %s: %w", childTable, result.Error)
}

log.WithContext(ctx).Infof("Cleaned up %d orphaned rows from %s where %s had no matching row in %s",
result.RowsAffected, childTable, fkColumn, parentTable)

return nil
}

func RemoveDuplicatePeerKeys(ctx context.Context, db *gorm.DB) error {
if !db.Migrator().HasTable("peers") {
log.WithContext(ctx).Debug("peers table does not exist, skipping duplicate key cleanup")
Expand Down
194 changes: 194 additions & 0 deletions management/server/migration/migration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,3 +441,197 @@ func TestRemoveDuplicatePeerKeys_NoTable(t *testing.T) {
err := migration.RemoveDuplicatePeerKeys(context.Background(), db)
require.NoError(t, err, "Should not fail when table does not exist")
}

type testParent struct {
ID string `gorm:"primaryKey"`
}

func (testParent) TableName() string {
return "test_parents"
}

type testChild struct {
ID string `gorm:"primaryKey"`
ParentID string
}

func (testChild) TableName() string {
return "test_children"
}

type testChildWithFK struct {
ID string `gorm:"primaryKey"`
ParentID string `gorm:"index"`
Parent *testParent `gorm:"foreignKey:ParentID"`
}

func (testChildWithFK) TableName() string {
return "test_children"
}

func setupOrphanTestDB(t *testing.T, models ...any) *gorm.DB {
t.Helper()
db := setupDatabase(t)
for _, m := range models {
_ = db.Migrator().DropTable(m)
}
err := db.AutoMigrate(models...)
require.NoError(t, err, "Failed to auto-migrate tables")
return db
}

func TestCleanupOrphanedResources_NoChildTable(t *testing.T) {
db := setupDatabase(t)
_ = db.Migrator().DropTable(&testChild{})
_ = db.Migrator().DropTable(&testParent{})

err := migration.CleanupOrphanedResources[testChild, testParent](context.Background(), db, "parent_id")
require.NoError(t, err, "Should not fail when child table does not exist")
}

func TestCleanupOrphanedResources_NoParentTable(t *testing.T) {
db := setupDatabase(t)
_ = db.Migrator().DropTable(&testParent{})
_ = db.Migrator().DropTable(&testChild{})

err := db.AutoMigrate(&testChild{})
require.NoError(t, err)

err = migration.CleanupOrphanedResources[testChild, testParent](context.Background(), db, "parent_id")
require.NoError(t, err, "Should not fail when parent table does not exist")
}

func TestCleanupOrphanedResources_EmptyTables(t *testing.T) {
db := setupOrphanTestDB(t, &testParent{}, &testChild{})

err := migration.CleanupOrphanedResources[testChild, testParent](context.Background(), db, "parent_id")
require.NoError(t, err, "Should not fail on empty tables")

var count int64
db.Model(&testChild{}).Count(&count)
assert.Equal(t, int64(0), count)
}

func TestCleanupOrphanedResources_NoOrphans(t *testing.T) {
db := setupOrphanTestDB(t, &testParent{}, &testChild{})

require.NoError(t, db.Create(&testParent{ID: "p1"}).Error)
require.NoError(t, db.Create(&testParent{ID: "p2"}).Error)
require.NoError(t, db.Create(&testChild{ID: "c1", ParentID: "p1"}).Error)
require.NoError(t, db.Create(&testChild{ID: "c2", ParentID: "p2"}).Error)

err := migration.CleanupOrphanedResources[testChild, testParent](context.Background(), db, "parent_id")
require.NoError(t, err)

var count int64
db.Model(&testChild{}).Count(&count)
assert.Equal(t, int64(2), count, "All children should remain when no orphans")
}

func TestCleanupOrphanedResources_AllOrphans(t *testing.T) {
db := setupOrphanTestDB(t, &testParent{}, &testChild{})

require.NoError(t, db.Exec("INSERT INTO test_children (id, parent_id) VALUES (?, ?)", "c1", "gone1").Error)
require.NoError(t, db.Exec("INSERT INTO test_children (id, parent_id) VALUES (?, ?)", "c2", "gone2").Error)
require.NoError(t, db.Exec("INSERT INTO test_children (id, parent_id) VALUES (?, ?)", "c3", "gone3").Error)

err := migration.CleanupOrphanedResources[testChild, testParent](context.Background(), db, "parent_id")
require.NoError(t, err)

var count int64
db.Model(&testChild{}).Count(&count)
assert.Equal(t, int64(0), count, "All orphaned children should be deleted")
}

func TestCleanupOrphanedResources_MixedValidAndOrphaned(t *testing.T) {
db := setupOrphanTestDB(t, &testParent{}, &testChild{})

require.NoError(t, db.Create(&testParent{ID: "p1"}).Error)
require.NoError(t, db.Create(&testParent{ID: "p2"}).Error)

require.NoError(t, db.Create(&testChild{ID: "c1", ParentID: "p1"}).Error)
require.NoError(t, db.Create(&testChild{ID: "c2", ParentID: "p2"}).Error)
require.NoError(t, db.Create(&testChild{ID: "c3", ParentID: "p1"}).Error)

require.NoError(t, db.Exec("INSERT INTO test_children (id, parent_id) VALUES (?, ?)", "c4", "gone1").Error)
require.NoError(t, db.Exec("INSERT INTO test_children (id, parent_id) VALUES (?, ?)", "c5", "gone2").Error)

err := migration.CleanupOrphanedResources[testChild, testParent](context.Background(), db, "parent_id")
require.NoError(t, err)

var remaining []testChild
require.NoError(t, db.Order("id").Find(&remaining).Error)

assert.Len(t, remaining, 3, "Only valid children should remain")
assert.Equal(t, "c1", remaining[0].ID)
assert.Equal(t, "c2", remaining[1].ID)
assert.Equal(t, "c3", remaining[2].ID)
}

func TestCleanupOrphanedResources_Idempotent(t *testing.T) {
db := setupOrphanTestDB(t, &testParent{}, &testChild{})

require.NoError(t, db.Create(&testParent{ID: "p1"}).Error)
require.NoError(t, db.Create(&testChild{ID: "c1", ParentID: "p1"}).Error)
require.NoError(t, db.Exec("INSERT INTO test_children (id, parent_id) VALUES (?, ?)", "c2", "gone").Error)

ctx := context.Background()

err := migration.CleanupOrphanedResources[testChild, testParent](ctx, db, "parent_id")
require.NoError(t, err)

var count int64
db.Model(&testChild{}).Count(&count)
assert.Equal(t, int64(1), count)

err = migration.CleanupOrphanedResources[testChild, testParent](ctx, db, "parent_id")
require.NoError(t, err)

db.Model(&testChild{}).Count(&count)
assert.Equal(t, int64(1), count, "Count should remain the same after second run")
}

func TestCleanupOrphanedResources_SkipsWhenForeignKeyExists(t *testing.T) {
engine := os.Getenv("NETBIRD_STORE_ENGINE")
if engine != "postgres" && engine != "mysql" {
t.Skip("FK constraint early-exit test requires postgres or mysql")
}

db := setupDatabase(t)
_ = db.Migrator().DropTable(&testChildWithFK{})
_ = db.Migrator().DropTable(&testParent{})

err := db.AutoMigrate(&testParent{}, &testChildWithFK{})
require.NoError(t, err)

require.NoError(t, db.Create(&testParent{ID: "p1"}).Error)
require.NoError(t, db.Create(&testParent{ID: "p2"}).Error)
require.NoError(t, db.Create(&testChildWithFK{ID: "c1", ParentID: "p1"}).Error)
require.NoError(t, db.Create(&testChildWithFK{ID: "c2", ParentID: "p2"}).Error)

switch engine {
case "postgres":
require.NoError(t, db.Exec("ALTER TABLE test_children DROP CONSTRAINT fk_test_children_parent").Error)
require.NoError(t, db.Exec("DELETE FROM test_parents WHERE id = ?", "p2").Error)
require.NoError(t, db.Exec(
"ALTER TABLE test_children ADD CONSTRAINT fk_test_children_parent "+
"FOREIGN KEY (parent_id) REFERENCES test_parents(id) NOT VALID",
).Error)
case "mysql":
require.NoError(t, db.Exec("SET FOREIGN_KEY_CHECKS = 0").Error)
require.NoError(t, db.Exec("ALTER TABLE test_children DROP FOREIGN KEY fk_test_children_parent").Error)
require.NoError(t, db.Exec("DELETE FROM test_parents WHERE id = ?", "p2").Error)
require.NoError(t, db.Exec(
"ALTER TABLE test_children ADD CONSTRAINT fk_test_children_parent "+
"FOREIGN KEY (parent_id) REFERENCES test_parents(id)",
).Error)
require.NoError(t, db.Exec("SET FOREIGN_KEY_CHECKS = 1").Error)
Comment on lines +621 to +628
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify current MySQL FK test uses unpinned sequential Exec calls.
rg -n -C3 'SET FOREIGN_KEY_CHECKS|DROP FOREIGN KEY|ADD CONSTRAINT fk_test_children_parent' management/server/migration/migration_test.go
rg -n -C2 'Transaction\\(|\\.DB\\(\\)|Conn\\(' management/server/migration/migration_test.go

Repository: netbirdio/netbird

Length of output: 1260


🌐 Web query:

In MySQL, is FOREIGN_KEY_CHECKS session-scoped, and in Go database/sql can consecutive Exec calls on *sql.DB run on different pooled connections unless pinned via sql.Conn or a transaction?

💡 Result:

Yes, FOREIGN_KEY_CHECKS is session-scoped in MySQL. It can be set per session with SET foreign_key_checks=0 (or 1), affecting only that connection/session. There is also a global scope (SET GLOBAL), but session settings override for the current connection, and new sessions inherit from global. Consecutive Exec calls on *sql.DB can run on different pooled connections, as *sql.DB manages a connection pool and each call acquires a connection from the pool (possibly different ones sequentially). To pin to the same connection, use sql.Conn (via db.Conn(ctx)) or a transaction (tx := db.BeginTx(ctx, nil)), where operations are guaranteed on the same connection.

Citations:


Pin MySQL FK-check statements to one connection to avoid flaky behavior.

FOREIGN_KEY_CHECKS is session-scoped in MySQL. Each db.Exec() call on *sql.DB can acquire a different connection from the pool, making it nondeterministic whether checks remain disabled across all statements on lines 621–628.

Suggested fix (pin to a dedicated SQL connection for the mysql branch)
 case "mysql":
-		require.NoError(t, db.Exec("SET FOREIGN_KEY_CHECKS = 0").Error)
-		require.NoError(t, db.Exec("ALTER TABLE test_children DROP FOREIGN KEY fk_test_children_parent").Error)
-		require.NoError(t, db.Exec("DELETE FROM test_parents WHERE id = ?", "p2").Error)
-		require.NoError(t, db.Exec(
-			"ALTER TABLE test_children ADD CONSTRAINT fk_test_children_parent "+
-				"FOREIGN KEY (parent_id) REFERENCES test_parents(id)",
-		).Error)
-		require.NoError(t, db.Exec("SET FOREIGN_KEY_CHECKS = 1").Error)
+		sqlDB, err := db.DB()
+		require.NoError(t, err)
+		conn, err := sqlDB.Conn(context.Background())
+		require.NoError(t, err)
+		t.Cleanup(func() {
+			_, _ = conn.ExecContext(context.Background(), "SET FOREIGN_KEY_CHECKS = 1")
+			_ = conn.Close()
+		})
+
+		_, err = conn.ExecContext(context.Background(), "SET FOREIGN_KEY_CHECKS = 0")
+		require.NoError(t, err)
+		_, err = conn.ExecContext(context.Background(), "ALTER TABLE test_children DROP FOREIGN KEY fk_test_children_parent")
+		require.NoError(t, err)
+		_, err = conn.ExecContext(context.Background(), "DELETE FROM test_parents WHERE id = ?", "p2")
+		require.NoError(t, err)
+		_, err = conn.ExecContext(
+			context.Background(),
+			"ALTER TABLE test_children ADD CONSTRAINT fk_test_children_parent "+
+				"FOREIGN KEY (parent_id) REFERENCES test_parents(id)",
+		)
+		require.NoError(t, err)
+		_, err = conn.ExecContext(context.Background(), "SET FOREIGN_KEY_CHECKS = 1")
+		require.NoError(t, err)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
require.NoError(t, db.Exec("SET FOREIGN_KEY_CHECKS = 0").Error)
require.NoError(t, db.Exec("ALTER TABLE test_children DROP FOREIGN KEY fk_test_children_parent").Error)
require.NoError(t, db.Exec("DELETE FROM test_parents WHERE id = ?", "p2").Error)
require.NoError(t, db.Exec(
"ALTER TABLE test_children ADD CONSTRAINT fk_test_children_parent "+
"FOREIGN KEY (parent_id) REFERENCES test_parents(id)",
).Error)
require.NoError(t, db.Exec("SET FOREIGN_KEY_CHECKS = 1").Error)
case "mysql":
sqlDB, err := db.DB()
require.NoError(t, err)
conn, err := sqlDB.Conn(context.Background())
require.NoError(t, err)
t.Cleanup(func() {
_, _ = conn.ExecContext(context.Background(), "SET FOREIGN_KEY_CHECKS = 1")
_ = conn.Close()
})
_, err = conn.ExecContext(context.Background(), "SET FOREIGN_KEY_CHECKS = 0")
require.NoError(t, err)
_, err = conn.ExecContext(context.Background(), "ALTER TABLE test_children DROP FOREIGN KEY fk_test_children_parent")
require.NoError(t, err)
_, err = conn.ExecContext(context.Background(), "DELETE FROM test_parents WHERE id = ?", "p2")
require.NoError(t, err)
_, err = conn.ExecContext(
context.Background(),
"ALTER TABLE test_children ADD CONSTRAINT fk_test_children_parent "+
"FOREIGN KEY (parent_id) REFERENCES test_parents(id)",
)
require.NoError(t, err)
_, err = conn.ExecContext(context.Background(), "SET FOREIGN_KEY_CHECKS = 1")
require.NoError(t, err)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/migration/migration_test.go` around lines 621 - 628, The
FOREIGN_KEY_CHECKS toggles are session-scoped so calling db.Exec separately can
hit different connections; wrap the sequence in a single pinned connection (use
db.Transaction or db.Begin) and run all Exec calls inside that transaction to
ensure they execute on the same connection. Concretely, replace the separate
db.Exec calls with db.Transaction(func(tx *gorm.DB) error { tx.Exec("SET
FOREIGN_KEY_CHECKS = 0"); tx.Exec("ALTER TABLE test_children DROP FOREIGN KEY
fk_test_children_parent"); tx.Exec("DELETE FROM test_parents WHERE id = ?",
"p2"); tx.Exec("ALTER TABLE test_children ADD CONSTRAINT fk_test_children_parent
FOREIGN KEY (parent_id) REFERENCES test_parents(id)"); tx.Exec("SET
FOREIGN_KEY_CHECKS = 1"); return tx.Error }) (or use db.DB().Conn(ctx) and exec
on the returned *sql.Conn) so all statements run on the same connection.

}

err = migration.CleanupOrphanedResources[testChildWithFK, testParent](context.Background(), db, "parent_id")
require.NoError(t, err)

var count int64
db.Model(&testChildWithFK{}).Count(&count)
assert.Equal(t, int64(2), count, "Both rows should survive — migration must skip when FK constraint exists")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
6 changes: 6 additions & 0 deletions management/server/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,12 @@ func getMigrationsPreAuto(ctx context.Context) []migrationFunc {
func(db *gorm.DB) error {
return migration.RemoveDuplicatePeerKeys(ctx, db)
},
func(db *gorm.DB) error {
return migration.CleanupOrphanedResources[rpservice.Service, types.Account](ctx, db, "account_id")
},
func(db *gorm.DB) error {
return migration.CleanupOrphanedResources[domain.Domain, types.Account](ctx, db, "account_id")
},
}
}

Expand Down
Loading