On this page
API reference for the db package — SQLite database tracking all saferm deletions, with WAL mode, busy_timeout and a bounded retry for lock contention.
#internal/db
#internal/db
Package db manages the SQLite database tracking all saferm deletions.
Concurrency safety across simultaneous sessions comes in three layers: WAL mode, SQLite's own busy_timeout, and a bounded retry on top of both -- every operation that meets SQLITE_BUSY or SQLITE_LOCKED is run again, up to five attempts with a 50ms linear backoff, reported through a RetryNotifier. Contention that outlives the whole budget is returned as a *ContentionError, a type distinct from every other database failure, so a caller can tell "another process holds the write lock, try again" from "this archive is broken".
#SchemaSQL
const SchemaSQL = `#ErrNotFound
var ErrNotFound = errors.New("record not found")ErrNotFound is returned when a queried record does not exist.
#ErrOriginVersionWithoutName
var ErrOriginVersionWithoutName = errors.New("origin_version requires origin_name")ErrOriginVersionWithoutName is returned by Insert when a record carries an origin version but no origin name.
SQLite cannot add a CHECK constraint to an existing table, so the invariant is enforced here instead of in the schema: one enforcement path, identical on a fresh database and on one that reached this schema through the migration ladder, and no table rebuild. The cost is stated rather than hidden -- a hand-edited database, or an older binary writing into a newer one, bypasses it.
#ErrOriginEmpty
var ErrOriginEmpty = errors.New("origin fields must be absent or non-empty")ErrOriginEmpty is returned by Insert when an origin field is present but empty. Both fields are nullable and never empty: an empty string would be a third state beside "a tool claimed this" and "none did", and nothing can read it as either.
#RetryNotifier
type RetryNotifier func(attempt, maxAttempts int, delay time.Duration, err error)RetryNotifier is called once before each contention retry, so a caller can report the wait under --verbose. It is never called for a failure that is not contention, and never for the final attempt (there is no wait after it).
#ContentionError
type ContentionError structContentionError reports that an operation was still meeting a locked database after the whole retry budget was spent. It is deliberately its own type: a caller that collapses it into a generic database failure loses the one piece of information that distinguishes "another process is busy, try later" from "this database is broken".
#DB
type DB structDB wraps a *sql.DB connection to the saferm SQLite database.
#DeletionRecord
type DeletionRecord structDeletionRecord represents a single archived deletion in the database.
#IsContention
func IsContention(err error) boolIsContention reports whether err is SQLITE_BUSY/SQLITE_LOCKED-class contention -- a lock held by another connection, which retrying can clear.
The driver reports a result code alongside the message, so the classification reads that code rather than matching on English. SQLite's extended codes carry the primary code in their low byte (SQLITE_BUSY_SNAPSHOT is SQLITE_BUSY | (2 << 8), and so on), so the low byte is what is compared -- every extended flavour of BUSY and LOCKED classifies with its primary.
#IsContentionExhausted
func IsContentionExhausted(err error) boolIsContentionExhausted reports whether err is a ContentionError -- contention that outlived the retry budget. It is what maps a database failure onto saferm's distinct contention exit code.
#Open
func Open(dbPath string, notify RetryNotifier) (*DB, error)Open opens (or creates) the SQLite database at dbPath with WAL mode and busy_timeout, then runs the schema DDL.
notify, when non-nil, is called before each contention retry -- for every operation on the returned DB as well as for the schema work below, which is why it is supplied here rather than set afterwards. Pass nil for no reporting.
#ContentionError.Error
func (e *ContentionError) Error() string#ContentionError.Unwrap
func (e *ContentionError) Unwrap() error { return e.Err }#DB.Close
func (d *DB) Close() errorClose closes the underlying database connection.
#DB.Insert
func (d *DB) Insert(rec *DeletionRecord) (int64, error)Insert inserts a DeletionRecord and returns the auto-increment ID.
The origin invariants are checked here, before anything is written: see ErrOriginVersionWithoutName for why they live in code rather than in the schema.
#DB.QueryByID
func (d *DB) QueryByID(id int64) (*DeletionRecord, error)QueryByID retrieves a single record by ID. Returns ErrNotFound if it does not exist.
#DB.QueryByUUID
func (d *DB) QueryByUUID(uuid string) (*DeletionRecord, error)QueryByUUID retrieves a single record by its archive uuid. Returns ErrNotFound if it does not exist.
The uuid is the identifier a record keeps: the numeric id is this database's autoincrement counter, while the uuid names the archived entry on disk and is what delete hands back to its caller.
#DB.QueryByPath
func (d *DB) QueryByPath(path string) ([]*DeletionRecord, error)QueryByPath returns all non-restored records matching the given original_path, ordered by deleted_at DESC (newest first).
#DB.QueryAll
func (d *DB) QueryAll(includeAll bool) ([]*DeletionRecord, error)QueryAll returns all records ordered by deleted_at DESC. If includeAll is false, restored and purged records are excluded.
#DB.MarkRestored
func (d *DB) MarkRestored(id int64, restoredTo string) errorMarkRestored sets restored_at to now and restored_to to the given path. Returns ErrNotFound if the record does not exist.
#DB.MarkPurged
func (d *DB) MarkPurged(id int64) errorMarkPurged sets purged_at to now, preserving the metadata record. Returns ErrNotFound if the record does not exist.
#DB.QueryOlderThan
func (d *DB) QueryOlderThan(before time.Time) ([]*DeletionRecord, error)QueryOlderThan returns all non-restored, non-purged records deleted before the given time.