# 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 ``` 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.