package db import ( "bytes" "database/sql" "fmt" "net/url" "os" "path/filepath" "strconv" ) const ( // ModeReadOnly is the mode to open a database in read-only mode. ModeReadOnly = true // ModeReadWrite is the mode to open a database in read-write mode. ModeReadWrite = false ) // MakeDSN returns a SQLite DSN for the given path, with the given options. func MakeDSN(path string, readOnly, fkEnabled, walEnabled bool) string { opts := url.Values{} if readOnly { opts.Add("mode", "ro") } opts.Add("_fk", strconv.FormatBool(fkEnabled)) opts.Add("_journal", "WAL") if !walEnabled { opts.Set("_journal", "DELETE") } opts.Add("_sync", "0") return fmt.Sprintf("file:%s?%s", path, opts.Encode()) } // WALPath returns the path to the WAL file for the given database path. func WALPath(dbPath string) string { return dbPath + "-wal" } // IsValidSQLiteFile checks that the supplied path looks like a SQLite file. // A non-existent file is considered invalid. func IsValidSQLiteFile(path string) bool { f, err := os.Open(path) if err != nil { return false } defer f.Close() b := make([]byte, 16) if _, err := f.Read(b); err != nil { return false } return IsValidSQLiteData(b) } // IsValidSQLiteData checks that the supplied data looks like a SQLite data. // See https://www.sqlite.org/fileformat.html. func IsValidSQLiteData(b []byte) bool { return len(b) > 13 && string(b[0:13]) == "SQLite format" } // IsValidSQLiteWALFile checks that the supplied path looks like a SQLite // WAL file. See https://www.sqlite.org/fileformat2.html#walformat. A // non-existent file is considered invalid. func IsValidSQLiteWALFile(path string) bool { f, err := os.Open(path) if err != nil { return false } defer f.Close() b := make([]byte, 4) if _, err := f.Read(b); err != nil { return false } return IsValidSQLiteWALData(b) } // IsValidSQLiteWALData checks that the supplied data looks like a SQLite // WAL file. func IsValidSQLiteWALData(b []byte) bool { if len(b) < 4 { return false } header1 := []byte{0x37, 0x7f, 0x06, 0x82} header2 := []byte{0x37, 0x7f, 0x06, 0x83} header := b[:4] return bytes.Equal(header, header1) || bytes.Equal(header, header2) } // 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 } // IsDELETEModeEnabledSQLiteFile checks that the supplied path looks like a SQLite // with DELETE mode enabled. func IsDELETEModeEnabledSQLiteFile(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 IsDELETEModeEnabled(b) } // IsDELETEModeEnabled checks that the supplied data looks like a SQLite file // with DELETE mode enabled. func IsDELETEModeEnabled(b []byte) bool { return len(b) >= 20 && b[18] == 1 && b[19] == 1 } // EnsureDeleteMode ensures the database at the given path is in DELETE mode. func EnsureDeleteMode(path string) error { if IsDELETEModeEnabledSQLiteFile(path) { return nil } rwDSN := fmt.Sprintf("file:%s", path) conn, err := sql.Open("sqlite3", rwDSN) if err != nil { return fmt.Errorf("open: %s", err.Error()) } defer conn.Close() _, err = conn.Exec("PRAGMA journal_mode=DELETE") return err } // RemoveFiles removes the SQLite database file, and any associated WAL and SHM files. func RemoveFiles(path string) error { if err := os.Remove(path); err != nil && !os.IsNotExist(err) { return err } if err := os.Remove(path + "-wal"); err != nil && !os.IsNotExist(err) { return err } if err := os.Remove(path + "-shm"); err != nil && !os.IsNotExist(err) { return err } return nil } // CheckIntegrity runs a PRAGMA integrity_check on the database at the given path. // If full is true, a full integrity check is performed, otherwise a quick check. func CheckIntegrity(path string, full bool) (bool, error) { db, err := Open(path, false, false) if err != nil { return false, err } defer db.Close() sql := "PRAGMA quick_check" if full { sql = "PRAGMA integrity_check" } rows, err := db.rwDB.Query(sql) if err != nil { return false, err } var result string if !rows.Next() { return false, nil } if err := rows.Scan(&result); err != nil { return false, err } return result == "ok", nil } // ReplayWAL replays the given WAL files into the database at the given path, // in the order given by the slice. The supplied WAL files must be in the same // directory as the database file and are deleted as a result of the replay operation. // If deleteMode is true, the database file will be in DELETE mode after the replay // operation, otherwise it will be in WAL mode. Finally, regardless of deleteMode, // there will be no "true" WAL file after the replay operation. func ReplayWAL(path string, wals []string, deleteMode bool) error { for _, wal := range wals { if filepath.Dir(wal) != filepath.Dir(path) { return ErrWALReplayDirectoryMismatch } } if !IsValidSQLiteFile(path) { return fmt.Errorf("invalid database file %s", path) } for _, wal := range wals { if !IsValidSQLiteWALFile(wal) { return fmt.Errorf("invalid WAL file %s", wal) } if err := os.Rename(wal, path+"-wal"); err != nil { return fmt.Errorf("rename WAL %s: %s", wal, err.Error()) } db, err := Open(path, false, true) if err != nil { return err } if err := db.Checkpoint(CheckpointTruncate); err != nil { return fmt.Errorf("checkpoint WAL %s: %s", wal, err.Error()) } // Closing the database will remove the WAL file which was just // checkpointed into the database file. if err := db.Close(); err != nil { return err } } if deleteMode { db, err := Open(path, false, false) if err != nil { return err } if err := db.Close(); err != nil { return err } } // Ensure the database file is sync'ed to disk. fd, err := os.OpenFile(path, os.O_RDWR, 0666) if err != nil { return err } if err := fd.Sync(); err != nil { return err } return fd.Close() }