fix(store): cascade delete related rows when removing a site
CI / test (pull_request) Successful in 2m32s
CI / lint (pull_request) Successful in 56s
CI / vulncheck (pull_request) Successful in 51s

DeleteSite now removes maintenance_windows, check_history, and
state_changes for the site within a transaction before deleting
the site itself. Prevents orphaned rows.

Closes #71
This commit was merged in pull request #92.
This commit is contained in:
2026-06-05 12:55:43 -04:00
parent 965a864343
commit 65a83368bf
2 changed files with 68 additions and 2 deletions
+17 -1
View File
@@ -152,10 +152,26 @@ func (s *SQLStore) UpdateSitePaused(id int, paused bool) 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 {
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")
return nil
}
+51 -1
View File
@@ -1,8 +1,10 @@
package store
import (
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
"testing"
"time"
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
)
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)
}
}
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)
}
}
}