From 108d166990bdd938c230aef3fa05995174140c7e Mon Sep 17 00:00:00 2001 From: Sandro Date: Thu, 29 Oct 2026 06:43:51 +0100 Subject: [PATCH] chore(db): stop wiping databases with an older schema version All installations are past schema version 2, so the one-time reset is gone; data is never deleted on start (demo mode still resets). user_version is kept for future migrations. Legal test uses an in-memory DB (cleanup race). Co-Authored-By: Claude Opus 5.5 --- Doku/database.md | 2 +- Doku/flaws.md | 2 +- internal/api/server_test.go | 3 ++- internal/storage/sqlite.go | 21 +++------------------ internal/storage/visibility_test.go | 9 +++++---- 5 files changed, 12 insertions(+), 25 deletions(-) diff --git a/Doku/database.md b/Doku/database.md index 5c0f51f..d991666 100644 --- a/Doku/database.md +++ b/Doku/database.md @@ -80,7 +80,7 @@ teams 1─n team_members n─1 users - `users.username` wird aus dem E-Mail-Präfix aufgefüllt. - Notiz-Synchronisation (`migrateDocSync`): neue Spalten an `blocks` (inkl. `deleted_at`; vorhandene Grabsteine erhalten den Migrationszeitpunkt) und `documents` (`head_seq`, `title_version`, `log_floor`), Tabellen `doc_ops` und `doc_op_results`. Blöcke im alten flachen Format (nur `level`/`position`) werden einmalig in den Baum umgewandelt. - Standardwerte in `system_settings` (`INSERT OR IGNORE`): `sharing_mode=organizations`, `allow_registration=false`, `instance_name=MindSift`, SMTP leer. -- **Schema-Version:** `PRAGMA user_version` hält `schemaVersion` (derzeit 2). Ist die Datenbank älter, werden vor allem anderen **alle Tabellen gelöscht** und neu angelegt. Bewusst so, solange MindSift nicht produktiv läuft. +- **Schema-Version:** `PRAGMA user_version` hält `schemaVersion` (derzeit 2), damit spätere Versionen erkennen, welche Migrationen eine Datenbank schon hat. Daten werden beim Start nie gelöscht (das einmalige Zurücksetzen auf Version 2 ist entfernt, alle Installationen sind darüber hinweg). Nur der Demo-Modus leert die Datenbank bei jedem Start. ## Sicherung diff --git a/Doku/flaws.md b/Doku/flaws.md index eaace45..d6fc9d2 100644 --- a/Doku/flaws.md +++ b/Doku/flaws.md @@ -115,7 +115,7 @@ Schweregrade: **Hoch** (Datenabfluss oder Rechteumgehung möglich), **Mittel** ( - **Maßnahme:** Fälligkeit mit Uhrzeit speichern, `alarm_sent` bei Änderung von `due_date` zurücksetzen, Wiederholungen begrenzen. ### FLAW-13: Migrationen -- **Teilweise behoben:** Das Schema hat eine Version (`PRAGMA user_version`, `schemaVersion` in `sqlite.go`). Ist eine Datenbank älter, wird sie beim Start **komplett gelöscht** und neu angelegt (vor dem produktiven Einsatz bewusst so gewählt). Die übrigen Punkte unten gelten weiter. +- **Teilweise behoben:** Das Schema hat eine Version (`PRAGMA user_version`, `schemaVersion` in `sqlite.go`), auf die künftige Migrationen aufsetzen können. Die übrigen Punkte unten gelten weiter. - **Fundstellen:** `Store.migrate` in [`internal/storage/sqlite.go`](../internal/storage/sqlite.go), [`migrations/001_init.sql`](../migrations/001_init.sql). - **Probleme:** - Es gibt keine Versionstabelle. Alle `ALTER TABLE`-Anweisungen laufen bei jedem Start erneut; Fehler werden pauschal ignoriert (`_, _ = s.db.Exec(q)`), echte Fehler fallen nicht auf. diff --git a/internal/api/server_test.go b/internal/api/server_test.go index 9c4df1b..92788e4 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -1369,7 +1369,8 @@ func TestFirstBootSetupWithSampleData(t *testing.T) { // Impressum and Datenschutz must be reachable without login; unset texts stay hidden. func TestLegalTextsArePublic(t *testing.T) { - dbPath := filepath.Join(t.TempDir(), "legal.db") + // In memory: no files the cleanup could race with + dbPath := ":memory:" store, err := storage.NewStore(dbPath) if err != nil { t.Fatalf("NewStore: %v", err) diff --git a/internal/storage/sqlite.go b/internal/storage/sqlite.go index 42e8140..e482122 100644 --- a/internal/storage/sqlite.go +++ b/internal/storage/sqlite.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "log/slog" "os" "path/filepath" "regexp" @@ -58,22 +57,11 @@ func (s *Store) Close() error { return s.db.Close() } -// schemaVersion is stored in PRAGMA user_version. Databases from an older version are wiped and rebuilt -// on start: MindSift is not in production yet, so breaking changes need no migration path. +// schemaVersion is stored in PRAGMA user_version so later releases can tell which migrations a database +// already has. (Version 2 once wiped older databases on start; all installations are past that now.) const schemaVersion = 2 -func (s *Store) resetOutdatedSchema() error { - var version int - if err := s.db.QueryRow(`PRAGMA user_version`).Scan(&version); err != nil { - return err - } - if version >= schemaVersion { - return nil - } - slog.Warn("Veraltetes Datenbankschema: alle Daten werden gelöscht und neu angelegt", "version", version, "neu", schemaVersion) - return s.dropAllTables() -} - +// dropAllTables removes every table; used by the demo mode reset. func (s *Store) dropAllTables() error { conn, err := s.db.Conn(context.Background()) if err != nil { @@ -107,9 +95,6 @@ func (s *Store) dropAllTables() error { } func (s *Store) migrate() error { - if err := s.resetOutdatedSchema(); err != nil { - return fmt.Errorf("reset outdated schema: %w", err) - } schema := ` PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; diff --git a/internal/storage/visibility_test.go b/internal/storage/visibility_test.go index cd9666f..801d03c 100644 --- a/internal/storage/visibility_test.go +++ b/internal/storage/visibility_test.go @@ -144,8 +144,9 @@ func TestVisibilityFollowsOrganizations(t *testing.T) { } } -func TestOutdatedSchemaIsWiped(t *testing.T) { - path := filepath.Join(t.TempDir(), "old.db") +// Existing data must survive a restart, whatever user_version the database carries. +func TestDataSurvivesRestart(t *testing.T) { + path := filepath.Join(t.TempDir(), "keep.db") store, err := NewStore(path) if err != nil { t.Fatalf("NewStore failed: %v", err) @@ -159,8 +160,8 @@ func TestOutdatedSchemaIsWiped(t *testing.T) { t.Fatalf("reopen failed: %v", err) } defer store.Close() - if n, _ := store.CountUsers(); n != 0 { - t.Fatalf("outdated database should start empty, found %d users", n) + if n, _ := store.CountUsers(); n != 1 { + t.Fatalf("data must be kept on restart, found %d users", n) } var v int _ = store.db.QueryRow(`PRAGMA user_version`).Scan(&v)