package storage import ( "database/sql" "fmt" "strings" ) // SnapshotTo writes a consistent point-in-time copy of the database to destPath // using SQLite's VACUUM INTO (never touching/locking the live file). When // includeBlobs is false, the image_data blobs are stripped from the copy, // leaving only metadata — which is ~99% smaller. // // destPath must not already exist (VACUUM INTO requires this). func (s *Store) SnapshotTo(destPath string, includeBlobs bool) error { if _, err := s.db.Exec(`VACUUM INTO '` + escapeSQLiteString(destPath) + `'`); err != nil { return fmt.Errorf("vacuum into snapshot: %w", err) } if includeBlobs { return nil } // Strip blobs from the copy only. image_data is NOT NULL, so use an empty blob. // Both receipts and their attachments carry blobs. snap, err := sql.Open("sqlite", destPath) if err != nil { return fmt.Errorf("open snapshot: %w", err) } defer snap.Close() for _, table := range []string{"receipts", "attachments"} { if _, err := snap.Exec(`UPDATE ` + table + ` SET image_data = X''`); err != nil { return fmt.Errorf("strip %s blobs: %w", table, err) } } if _, err := snap.Exec(`VACUUM`); err != nil { return fmt.Errorf("vacuum stripped snapshot: %w", err) } return nil } // escapeSQLiteString escapes a string for use inside single quotes in SQL. func escapeSQLiteString(s string) string { return strings.ReplaceAll(s, "'", "''") }