From 4a7ac2850f1b082a096b8f88453361972b9f26f5 Mon Sep 17 00:00:00 2001 From: Philip O'Toole Date: Mon, 12 Jun 2023 12:32:07 -0400 Subject: [PATCH] rqlite doesn't (yet) support WAL mode SQLite data --- db/db.go | 23 ++++++++++++++++ db/db_ondisk_test.go | 62 ++++++++++++++++++++++++++++++++++++++++++++ http/service.go | 8 ++++++ store/store.go | 4 +++ 4 files changed, 97 insertions(+) diff --git a/db/db.go b/db/db.go index 7ea7bd0f..60f7dfe5 100644 --- a/db/db.go +++ b/db/db.go @@ -105,6 +105,29 @@ func IsValidSQLiteData(b []byte) bool { return len(b) > 13 && string(b[0:13]) == "SQLite format" } +// IsWALModeEnabledSQLiteFile checks that the supplied path looks like a SQLite +// with WAL mode enabled. +func IsWALModeEnabledSQLiteFile(path string) bool { + f, err := os.Open(path) + if err != nil { + return false + } + defer f.Close() + + b := make([]byte, 20) + if _, err := f.Read(b); err != nil { + return false + } + + return IsWALModeEnabled(b) +} + +// IsWALModeEnabled checks that the supplied data looks like a SQLite data +// with WAL mode enabled. +func IsWALModeEnabled(b []byte) bool { + return len(b) >= 20 && b[18] == 2 && b[19] == 2 +} + // Open opens a file-based database, creating it if it does not exist. After this // function returns, an actual SQLite file will always exist. func Open(dbPath string, fkEnabled bool) (*DB, error) { diff --git a/db/db_ondisk_test.go b/db/db_ondisk_test.go index c9ebe6f3..83560f43 100644 --- a/db/db_ondisk_test.go +++ b/db/db_ondisk_test.go @@ -38,6 +38,68 @@ func Test_IsValidSQLiteOnDisk(t *testing.T) { } } +func Test_IsWALModeEnablednDiskDELETE(t *testing.T) { + path := mustTempFile() + defer os.Remove(path) + + dsn := fmt.Sprintf("file:%s", path) + db, err := sql.Open("sqlite3", dsn) + if err != nil { + t.Fatalf("failed to create SQLite database: %s", err.Error()) + } + _, err = db.Exec("CREATE TABLE foo (name TEXT)") + if err != nil { + t.Fatalf("failed to create table: %s", err.Error()) + } + if err := db.Close(); err != nil { + t.Fatalf("failed to close database: %s", err.Error()) + } + + if IsWALModeEnabledSQLiteFile(path) { + t.Fatalf("non WAL file marked as WAL") + } + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to read SQLite file: %s", err.Error()) + } + if IsWALModeEnabled(data) { + t.Fatalf("non WAL data marked as WAL") + } +} + +func Test_IsWALModeEnablednDiskWAL(t *testing.T) { + path := mustTempFile() + defer os.Remove(path) + + dsn := fmt.Sprintf("file:%s", path) + db, err := sql.Open("sqlite3", dsn) + if err != nil { + t.Fatalf("failed to create SQLite database: %s", err.Error()) + } + _, err = db.Exec("CREATE TABLE foo (name TEXT)") + if err != nil { + t.Fatalf("failed to create table: %s", err.Error()) + } + _, err = db.Exec("PRAGMA journal_mode=WAL") + if err != nil { + t.Fatalf("failed to enable WAL mode: %s", err.Error()) + } + if err := db.Close(); err != nil { + t.Fatalf("failed to close database: %s", err.Error()) + } + + if !IsWALModeEnabledSQLiteFile(path) { + t.Fatalf("WAL file marked as non-WAL") + } + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to read SQLite file: %s", err.Error()) + } + if !IsWALModeEnabled(data) { + t.Fatalf("WAL data marked as non-WAL") + } +} + func Test_FileCreationOnDisk(t *testing.T) { dir := t.TempDir() dbPath := path.Join(dir, "test_db") diff --git a/http/service.go b/http/service.go index 09a1c6b8..5046ed9c 100644 --- a/http/service.go +++ b/http/service.go @@ -858,6 +858,14 @@ func (s *Service) handleLoad(w http.ResponseWriter, r *http.Request) { lr := &command.LoadRequest{ Data: b, } + + if db.IsWALModeEnabled(b) { + s.logger.Printf("SQLite database file is in WAL mode - rejecting load request") + http.Error(w, `SQLite database file is in WAL mode - convert it to DELETE mode via 'PRAGMA journal_mode=DELETE'`, + http.StatusBadRequest) + return + } + err := s.store.Load(lr) if err != nil && err != store.ErrNotLeader { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/store/store.go b/store/store.go index 861ae2e2..24f6ca12 100644 --- a/store/store.go +++ b/store/store.go @@ -310,6 +310,10 @@ func (s *Store) SetRestorePath(path string) error { if !sql.IsValidSQLiteFile(path) { return fmt.Errorf("file %s is not a valid SQLite file", path) } + if sql.IsWALModeEnabledSQLiteFile(path) { + return fmt.Errorf("file %s is in WAL mode - convert to DELETE mode", path) + } + s.RegisterReadyChannel(s.restoreDoneCh) s.restorePath = path return nil