Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/ceilings.md
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,13 @@ work when nobody knows it and that the call site never shows. The mechanism and
version-1 step is the one step every database that already exists has long since run, so a column added
there reaches none of them and every read of that table fails. 0.53.0 shipped a column that way and loaded
no projects at all.
- **The workspace database is three files while lich runs** (`internal/store/store.go`, `open`): WAL mode
appends commits to `lich.db-wal` beside `lich.db`, with `lich.db-shm` as its index, and folds them back into
`lich.db` at checkpoints and on a clean close. Copying `lich.db` alone off a running lich, as a backup or a
move, leaves the commits since the last checkpoint behind; copy all three, or close lich first. With
`synchronous(NORMAL)` a power loss can drop the last commits, never corrupt the file. The rollback journal it
replaced created, synced and deleted a file on every commit, tens of milliseconds each on Windows, which
was most of the backend suite's time there.
- **The history's branch is read live, so a row whose checkout is gone has none** (`internal/project.BranchesOf`):
the branch a row shows is not the one it stores — a worktree keeps the name it was created with while an
agent moves the branch inside it, so the stored snapshot dates the close and only git can say what the
Expand Down
7 changes: 5 additions & 2 deletions internal/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,13 +458,16 @@ func New() (*Service, error) {
// open opens the database at path, creating parent directories and applying the
// schema. foreign_keys is enabled per connection via the DSN so ON DELETE
// CASCADE fires; a single open connection serializes writes and sidesteps SQLite
// lock contention in this low-concurrency desktop app.
// lock contention in this low-concurrency desktop app. WAL with synchronous
// NORMAL appends each commit to lich.db-wal instead of creating, syncing and
// deleting a rollback journal per write, which on Windows costs tens of
// milliseconds a commit; a power loss can drop the last commits, never corrupt.
func open(path string) (*Service, error) {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return nil, fmt.Errorf("create data directory: %w", err)
}

dsn := fmt.Sprintf("file:%s?_pragma=foreign_keys(1)&_pragma=busy_timeout(%d)", path, busyTimeoutMS)
dsn := fmt.Sprintf("file:%s?_pragma=foreign_keys(1)&_pragma=busy_timeout(%d)&_pragma=journal_mode(WAL)&_pragma=synchronous(NORMAL)", path, busyTimeoutMS)
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
Expand Down
21 changes: 21 additions & 0 deletions internal/store/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ func newTestStore(t testing.TB) *Service {
return svc
}

// TestOpenCommitsWithoutAJournalFilePerWrite pins the journal setup: the
// default rollback journal creates, syncs and deletes a file on every commit,
// which costs tens of milliseconds each on Windows.
func TestOpenCommitsWithoutAJournalFilePerWrite(t *testing.T) {
svc := newTestStore(t)
var mode string
var synchronous int
if err := svc.db.QueryRow("PRAGMA journal_mode").Scan(&mode); err != nil {
t.Fatal(err)
}
if err := svc.db.QueryRow("PRAGMA synchronous").Scan(&synchronous); err != nil {
t.Fatal(err)
}
if mode != "wal" {
t.Errorf("journal_mode = %q, want wal", mode)
}
if synchronous != 1 {
t.Errorf("synchronous = %d, want 1 (NORMAL)", synchronous)
}
}

// TestSessionExistsSpansParkedRows: the answer is what internal/drop asks
// before ageing a dropped-file copy out, and a parked session is one a resume
// walks straight back into, so its copies are still worth keeping and parking
Expand Down
Loading