115 lines
3.9 KiB
Go
115 lines
3.9 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// readonly_test.go pins the OpenReadOnly contract (пакет №4): no flock (status
|
|
// works during a live run), hard read-only connections, loud errors on a missing
|
|
// DB and on a schema the binary does not match.
|
|
|
|
// TestOpenReadOnlyConcurrentWithWriter is the headline: a READ-ONLY open must
|
|
// succeed while a WRITER owns the project (the exclusive flock is held), reads
|
|
// must see the writer's committed rows, and a stray write through the read-only
|
|
// store must fail loudly instead of corrupting the live writer's state.
|
|
func TestOpenReadOnlyConcurrentWithWriter(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "book.db")
|
|
w, err := Open(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer w.Close()
|
|
if err := w.InsertRequestLog(RequestLog{BookID: "b1", Chapter: 1, Stage: "draft", Role: "translator", OK: true}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Writer still open (flock held) — the old Open here would fail with
|
|
// "project database is in use by another tmctl process".
|
|
ro, err := OpenReadOnly(path)
|
|
if err != nil {
|
|
t.Fatalf("read-only open must not need the writer's flock: %v", err)
|
|
}
|
|
defer ro.Close()
|
|
|
|
rows, err := ro.RequestLogRows("b1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rows) != 1 || rows[0].Stage != "draft" {
|
|
t.Fatalf("read-only store must see the writer's committed rows, got %+v", rows)
|
|
}
|
|
|
|
// A stray write must be a loud SQLITE_READONLY error, never a silent success
|
|
// (the read-only Store aliases its write pool to the query_only connections).
|
|
if err := ro.InsertRequestLog(RequestLog{BookID: "b1", Chapter: 2, Stage: "edit"}); err == nil {
|
|
t.Fatal("a write through the read-only store must fail")
|
|
}
|
|
|
|
// The writer keeps working while the reader is open (WAL concurrent access).
|
|
if err := w.InsertRequestLog(RequestLog{BookID: "b1", Chapter: 2, Stage: "edit", Role: "editor", OK: true}); err != nil {
|
|
t.Fatalf("writer must not be disturbed by an open reader: %v", err)
|
|
}
|
|
rows, err = ro.RequestLogRows("b1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rows) != 2 {
|
|
t.Fatalf("reader must see the writer's new committed row, got %d rows", len(rows))
|
|
}
|
|
}
|
|
|
|
// TestOpenReadOnlyMissingDB: a read of a never-run project must not silently
|
|
// create an empty database — the error wraps os.ErrNotExist so the runner can
|
|
// fall back to the creating Open on first touch.
|
|
func TestOpenReadOnlyMissingDB(t *testing.T) {
|
|
_, err := OpenReadOnly(filepath.Join(t.TempDir(), "nope.db"))
|
|
if err == nil {
|
|
t.Fatal("read-only open of a missing DB must fail")
|
|
}
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("error must wrap os.ErrNotExist for the first-touch fallback, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestOpenReadOnlySchemaMismatch: read-only open never migrates, so a database
|
|
// behind (or ahead of) this binary's schema is a loud, actionable error — not a
|
|
// query that fails later on a missing column.
|
|
func TestOpenReadOnlySchemaMismatch(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "book.db")
|
|
w, err := Open(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.Close()
|
|
|
|
// Roll the recorded version back one step (simulates a DB from an older binary).
|
|
db, err := sql.Open("sqlite", "file:"+path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.Exec(`DELETE FROM schema_version WHERE version = (SELECT MAX(version) FROM schema_version)`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
db.Close()
|
|
|
|
if _, err := OpenReadOnly(path); err == nil {
|
|
t.Fatal("read-only open of an older-schema DB must fail loudly (it never migrates)")
|
|
}
|
|
|
|
// Ahead of the binary: an unknown future version must fail too.
|
|
db, err = sql.Open("sqlite", "file:"+path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.Exec(`INSERT INTO schema_version (version) VALUES (?), (?)`, len(migrations), len(migrations)+7); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
db.Close()
|
|
if _, err := OpenReadOnly(path); err == nil {
|
|
t.Fatal("read-only open of a newer-schema DB must fail loudly")
|
|
}
|
|
}
|