fix(store): cascade delete related rows when removing a site #92

Merged
lerko merged 1 commits from fix/orphaned-maintenance-windows into main 2026-06-05 20:23:11 +00:00
2 changed files with 68 additions and 2 deletions
Showing only changes of commit 65a83368bf - Show all commits
+17 -1
View File
@@ -152,10 +152,26 @@ func (s *SQLStore) UpdateSitePaused(id int, paused bool) error {
} }
func (s *SQLStore) DeleteSite(id int) error { func (s *SQLStore) DeleteSite(id int) error {
_, err := s.db.Exec(s.q("DELETE FROM sites WHERE id=?"), id) tx, err := s.db.Begin()
if err != nil { if err != nil {
return err return err
} }
defer func() { _ = tx.Rollback() }()
for _, q := range []string{
"DELETE FROM maintenance_windows WHERE monitor_id = ?",
"DELETE FROM check_history WHERE site_id = ?",
"DELETE FROM state_changes WHERE site_id = ?",
"DELETE FROM sites WHERE id = ?",
} {
if _, err := tx.Exec(s.q(q), id); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
s.dialect.ResetSequenceOnEmpty(s.db, "sites") s.dialect.ResetSequenceOnEmpty(s.db, "sites")
return nil return nil
} }
+51 -1
View File
@@ -1,8 +1,10 @@
package store package store
import ( import (
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
"testing" "testing"
"time"
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
) )
func newTestStore(t *testing.T) *SQLStore { func newTestStore(t *testing.T) *SQLStore {
@@ -229,3 +231,51 @@ func TestCheckHistory(t *testing.T) {
t.Errorf("expected 1 up record for site 1, got %d", upCount) t.Errorf("expected 1 up record for site 1, got %d", upCount)
} }
} }
func TestDeleteSiteCascade(t *testing.T) {
s := newTestStore(t)
site := models.Site{Name: "Cascade Test", URL: "https://example.com", Interval: 30}
if err := s.AddSite(site); err != nil {
t.Fatalf("AddSite: %v", err)
}
sites, _ := s.GetSites()
siteID := sites[0].ID
if err := s.SaveCheck(siteID, 1000, true); err != nil {
t.Fatalf("SaveCheck: %v", err)
}
if err := s.SaveStateChange(siteID, "UP", "DOWN", "timeout"); err != nil {
t.Fatalf("SaveStateChange: %v", err)
}
mw := models.MaintenanceWindow{
MonitorID: siteID,
Title: "Test MW",
Type: "maintenance",
StartTime: time.Now(),
}
if err := s.AddMaintenanceWindow(mw); err != nil {
t.Fatalf("AddMaintenanceWindow: %v", err)
}
if err := s.DeleteSite(siteID); err != nil {
t.Fatalf("DeleteSite: %v", err)
}
history, _ := s.LoadAllHistory(100)
if len(history[siteID]) != 0 {
t.Errorf("expected 0 check_history rows, got %d", len(history[siteID]))
}
changes, _ := s.GetStateChanges(siteID, 100)
if len(changes) != 0 {
t.Errorf("expected 0 state_changes rows, got %d", len(changes))
}
windows, _ := s.GetActiveMaintenanceWindows()
for _, w := range windows {
if w.MonitorID == siteID {
t.Errorf("orphaned maintenance window found: id=%d", w.ID)
}
}
}