Lets an external reader (sqlite3 CLI, DB Browser, DataGrip) inspect the database file concurrently without "database is locked" errors while geniusrund is running. Doesn't change in-process concurrency -- queries are already fully serialized via SetMaxOpenConns(1). auth: fix flaky tampered-cookie tests Both tests corrupted a signed cookie by blindly overwriting its last character with "x", which is occasionally a no-op if that character (part of the token's signature, so effectively randomized by the embedded timestamp) already happened to be "x" -- silently passing without having tampered with anything. Confirmed via 15 repeated runs (3 spurious passes) before the fix and 30 clean runs after. flipLastChar now guarantees the byte actually changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
70 lines
2.2 KiB
Go
70 lines
2.2 KiB
Go
// Package store is geniusrun's SQLite persistence layer: activities, laps,
|
|
// per-second samples, workout kind rule config, and classification history.
|
|
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
_ "embed"
|
|
"fmt"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
//go:embed schema.sql
|
|
var schemaSQL string
|
|
|
|
// DB wraps a *sql.DB opened against a geniusrun SQLite database file, with
|
|
// the schema already applied.
|
|
type DB struct {
|
|
*sql.DB
|
|
}
|
|
|
|
// Open opens (creating if needed) the SQLite database at path and applies
|
|
// schema.sql if it hasn't been applied yet. There is no migration history --
|
|
// this is a pre-production app with no compatibility obligation to older
|
|
// database files. Edit schema.sql directly to change the schema.
|
|
func Open(path string) (*DB, error) {
|
|
// WAL mode lets an external reader (sqlite3 CLI, DB Browser, DataGrip)
|
|
// inspect the file concurrently without "database is locked" errors
|
|
// while geniusrund is running -- the app's own queries are already
|
|
// fully serialized via SetMaxOpenConns(1) below, so this doesn't change
|
|
// in-process concurrency, only cross-process access to the same file.
|
|
sqlDB, err := sql.Open("sqlite", path+"?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open sqlite database: %w", err)
|
|
}
|
|
// SQLite only supports one writer at a time; a single connection avoids
|
|
// "database is locked" errors under any concurrent access from the app.
|
|
sqlDB.SetMaxOpenConns(1)
|
|
|
|
db := &DB{DB: sqlDB}
|
|
if err := db.applySchema(); err != nil {
|
|
sqlDB.Close()
|
|
return nil, err
|
|
}
|
|
return db, nil
|
|
}
|
|
|
|
// applySchema runs schema.sql once, the first time this database file is
|
|
// opened -- detected by checking whether the users table already exists.
|
|
func (db *DB) applySchema() error {
|
|
var alreadyApplied int
|
|
if err := db.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='users'`).Scan(&alreadyApplied); err != nil {
|
|
return fmt.Errorf("check existing schema: %w", err)
|
|
}
|
|
if alreadyApplied > 0 {
|
|
return nil
|
|
}
|
|
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
return fmt.Errorf("begin schema tx: %w", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
if _, err := tx.Exec(schemaSQL); err != nil {
|
|
return fmt.Errorf("apply schema: %w", err)
|
|
}
|
|
return tx.Commit()
|
|
}
|