From 9445d24b9e97cea26909cf2ccc8a4ecea8316dfe Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Sat, 18 Apr 2026 12:12:30 -0400 Subject: [PATCH 1/2] index car_blocks.file_id to fix FK cascade scans without this index, ON DELETE SET NULL from files.id to car_blocks.file_id causes a full-table sequential scan of car_blocks for every file being deleted. on a billion-row car_blocks table this makes file deletion effectively impossible -- the healthcheck reaper in prod wedged for 5 months because of exactly this. add gorm:"index" to CarBlock.FileID (CarID already has one). add a regression test that asserts both FK columns plus cid are indexed, so this doesn't silently regress again. --- model/migrate_test.go | 14 ++++++++++++++ model/preparation.go | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/model/migrate_test.go b/model/migrate_test.go index 62491bb58..5ba69be0c 100644 --- a/model/migrate_test.go +++ b/model/migrate_test.go @@ -77,6 +77,20 @@ func TestFKSetNullOnDelete(t *testing.T) { }) } +// indexes on CarBlock FK columns must exist or SET NULL cascades from parent +// deletion fall back to seq scans of the entire car_blocks table. this has bit +// us before -- the reaper wedged for months because file_id was unindexed. +func TestCarBlockFKIndexes(t *testing.T) { + testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { + require.True(t, db.Migrator().HasIndex(&model.CarBlock{}, "CarID"), + "car_blocks.car_id must be indexed for FK cascade performance") + require.True(t, db.Migrator().HasIndex(&model.CarBlock{}, "FileID"), + "car_blocks.file_id must be indexed for FK cascade performance") + require.True(t, db.Migrator().HasIndex(&model.CarBlock{}, "CID"), + "car_blocks.cid must be indexed for gateway lookups") + }) +} + func TestInferPieceTypes(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { prep := model.Preparation{Name: "test", MaxSize: 1024, PieceSize: 1024} diff --git a/model/preparation.go b/model/preparation.go index 7a4484357..fd19ab1a4 100644 --- a/model/preparation.go +++ b/model/preparation.go @@ -304,7 +304,7 @@ type CarBlock struct { // Associations - SET NULL for fast prep deletion, async cleanup CarID *CarID `cbor:"-" gorm:"index" json:"carId"` Car *Car `cbor:"-" gorm:"foreignKey:CarID;constraint:OnDelete:SET NULL" json:"car,omitempty" swaggerignore:"true"` - FileID *FileID `cbor:"7,keyasint,omitempty" json:"fileId"` + FileID *FileID `cbor:"7,keyasint,omitempty" gorm:"index" json:"fileId"` File *File `cbor:"-" gorm:"foreignKey:FileID;constraint:OnDelete:SET NULL" json:"file,omitempty" swaggerignore:"true"` } From 40e89f6fa85cf4e67712795e9e091eab8efe8516 Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Sat, 18 Apr 2026 18:36:45 +0200 Subject: [PATCH 2/2] Update migrate_test.go --- model/migrate_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/model/migrate_test.go b/model/migrate_test.go index 5ba69be0c..2c2ec6271 100644 --- a/model/migrate_test.go +++ b/model/migrate_test.go @@ -77,9 +77,6 @@ func TestFKSetNullOnDelete(t *testing.T) { }) } -// indexes on CarBlock FK columns must exist or SET NULL cascades from parent -// deletion fall back to seq scans of the entire car_blocks table. this has bit -// us before -- the reaper wedged for months because file_id was unindexed. func TestCarBlockFKIndexes(t *testing.T) { testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) { require.True(t, db.Migrator().HasIndex(&model.CarBlock{}, "CarID"),