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>
157 lines
6.4 KiB
Markdown
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.
|