go-store/docs/development.md
Snider c570f08eba docs: graduate TODO/FINDINGS into production documentation
Replace internal task tracking (TODO.md, FINDINGS.md) with structured
documentation in docs/. Trim CLAUDE.md to agent instructions only.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-20 15:01:55 +00:00

157 lines
6.4 KiB
Markdown

# Development Guide — go-store
## Prerequisites
- Go 1.25 or later
- No CGO required (`modernc.org/sqlite` is a pure-Go SQLite implementation)
- No external tools beyond the Go toolchain
## Build and Test
The package is a standard Go module. All standard `go` commands apply.
```bash
# Run all tests
go test ./...
# Run with the race detector (required before any commit touching concurrency)
go test -race ./...
# Run a single test by name
go test -v -run TestWatch_Good_SpecificKey ./...
# Run tests with coverage
go test -cover ./...
# Generate a coverage profile and view it in the browser
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# Run benchmarks
go test -bench=. -benchmem ./...
# Run a specific benchmark
go test -bench=BenchmarkSet -benchmem ./...
```
**Coverage target: 95%.** The remaining uncovered lines are defensive error paths (scan errors, rows iteration errors on corrupted databases) covered by `coverage_test.go`. Do not remove these checks to chase coverage — they protect against driver and OS-level failures that are not reachable through integration tests against a healthy SQLite database.
## Test Patterns
Tests follow the `_Good`, `_Bad`, `_Ugly` suffix convention used across the Core Go ecosystem:
- `_Good` — happy-path behaviour, including edge cases that should succeed
- `_Bad` — expected error conditions (closed store, invalid input, quota exceeded)
- `_Ugly` — not currently used in this package (reserved for panic/edge cases)
Tests are grouped into sections by the method under test, marked with comment banners:
```go
// ---------------------------------------------------------------------------
// Watch — specific key
// ---------------------------------------------------------------------------
func TestWatch_Good_SpecificKey(t *testing.T) { ... }
func TestWatch_Good_WildcardKey(t *testing.T) { ... }
```
**In-memory stores for unit tests.** Use `New(":memory:")` for all tests that do not require persistence. In-memory stores are faster and leave no filesystem artefacts.
**File-backed stores for concurrency and persistence tests.** Use `filepath.Join(t.TempDir(), "name.db")` for tests that verify WAL mode, persistence across open/close cycles, or concurrent writes. `t.TempDir()` is cleaned up automatically at the end of the test.
**Avoid `time.Sleep` except for TTL tests.** TTL expiry tests require short sleeps. Use the minimum duration that reliably demonstrates expiry (typically 1ms TTL + 5ms sleep). Do not use sleeps as synchronisation barriers for goroutines — use `sync.WaitGroup` instead.
**Race detector is mandatory for all concurrency tests.** Run `go test -race ./...` before marking any concurrency work as complete. The test suite must be clean under the race detector.
**Testify assertions.** The test suite uses `github.com/stretchr/testify/assert` and `github.com/stretchr/testify/require`. Use `require` for preconditions (test should abort on failure) and `assert` for verifications (test should continue and report all failures).
## Coding Standards
### Language
Use UK English throughout all documentation, comments, and error messages. This applies to spelling (colour, organisation, behaviour, serialise, initialise) and terminology. American spellings are not acceptable.
### Code Style
- `gofmt` formatting is mandatory. Run `go fmt ./...` before committing.
- `go vet ./...` must report no warnings.
- All error strings begin with the package function context: `"store.Method: what failed"`. This convention makes errors self-identifying in log output without requiring a stack trace.
- Exported identifiers must have Go doc comments.
- Internal helpers (unexported) should have comments explaining non-obvious behaviour.
### Licence Header
All source files must include the EUPL-1.2 licence identifier. The licence is specified in the module metadata and applies to all contributions.
### Dependencies
go-store is intentionally minimal. Before adding any new dependency:
1. Verify it cannot be replaced with a standard library alternative.
2. Verify it is pure Go (no CGO) to preserve cross-compilation.
3. Verify it has a compatible open-source licence (EUPL-1.2 compatible).
The only permitted runtime dependency is `modernc.org/sqlite`. Test-only dependencies (`github.com/stretchr/testify`) are acceptable.
## Commit Guidelines
Use conventional commit format:
```
type(scope): description
```
Common types: `feat`, `fix`, `test`, `docs`, `refactor`, `perf`, `chore`.
Examples:
```
feat(store): add PurgeExpired public method
fix(events): prevent deadlock when callback calls store methods
test(scope): add quota enforcement for new groups
docs(architecture): document WAL single-connection constraint
perf(store): replace linear watcher scan with index lookup
```
Every commit must include the co-author trailer:
```
Co-Authored-By: Virgil <virgil@lethean.io>
```
All tests must pass before committing:
```bash
go test -race ./...
go vet ./...
```
## Benchmarks and Performance
Reference benchmark results (Apple M-series, in-memory store):
```
BenchmarkSet-32 119280 10290 ns/op 328 B/op 12 allocs/op
BenchmarkGet-32 335707 3589 ns/op 576 B/op 21 allocs/op
BenchmarkGetAll-32 (10K keys) 258 4741451 ns/op 2268787 B/op 80095 allocs/op
BenchmarkSet_FileBacked-32 4525 265868 ns/op 327 B/op 12 allocs/op
```
Derived throughput:
- In-memory `Set`: approximately 97,000 ops/sec
- In-memory `Get`: approximately 279,000 ops/sec
- File-backed `Set`: approximately 3,800 ops/sec (dominated by fsync)
- `GetAll` with 10,000 keys: approximately 2.3 MB allocated per call
`GetAll` allocations scale linearly with the number of keys (one map entry per row). Applications fetching very large groups should consider pagination at a higher layer or restructuring data into multiple smaller groups.
## Adding a New Method
1. Implement the method on `*Store` in `store.go` (or `scope.go` if it is namespace-scoped).
2. If it is a mutating operation, call `s.notify(Event{...})` after the successful database write.
3. Add a corresponding delegation method to `ScopedStore` in `scope.go` that prefixes the group.
4. Write tests covering the happy path, error conditions, and closed-store behaviour.
5. Update quota checks in `checkQuota` if the operation affects key or group counts.
6. Run `go test -race ./...` and `go vet ./...`.
7. Update `docs/architecture.md` if the method introduces a new concept or changes an existing one.