ScopedStore wraps Store and auto-prefixes groups with a namespace to prevent key collisions across tenants. QuotaConfig enforces per-namespace MaxKeys and MaxGroups limits (zero = unlimited). Upserts and expired keys are excluded from quota counts. New Store methods: CountAll(prefix) and Groups(prefix) for cross-group queries. All 93 tests pass with race detector, coverage 94.7%. Co-Authored-By: Virgil <virgil@lethean.io> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5 KiB
5 KiB
TODO.md -- go-store
Dispatched from core/go orchestration. Pick up tasks in order.
Phase 0: Hardening & Test Coverage
- Expand test coverage -- concurrent Set/Get with 10 goroutines (race test), Render() with invalid template syntax, Render() with missing template vars, Get() on non-existent group vs non-existent key, DeleteGroup() then verify GetAll() returns empty, Count() after bulk inserts, :memory: vs file-backed store, WAL mode verification. Coverage: 73.1% -> 90.9%.
- Edge cases -- empty key, empty value, empty group, very long key (10K chars), binary-ish value (null bytes), Unicode keys and values, CJK, Arabic, SQL injection attempts, special characters.
- Benchmark -- BenchmarkSet, BenchmarkGet, BenchmarkGetAll with 10K keys, BenchmarkSet_FileBacked.
go vet ./...clean -- no warnings.- Concurrency fix -- Added
db.SetMaxOpenConns(1)andPRAGMA busy_timeout=5000to prevent SQLITE_BUSY errors under concurrent writes.
Phase 1: TTL Support
- Add optional expiry timestamp for keys (
expires_at INTEGERcolumn) - Background goroutine to purge expired entries (configurable interval, default 60s)
SetWithTTL(group, key, value, duration)API- Lazy expiry check on
Getas fallback PurgeExpired()public method for manual purgeCount,GetAll,Renderexclude expired entries- Schema migration for pre-TTL databases (ALTER TABLE ADD COLUMN)
- Tests for all TTL functionality including concurrent TTL access
Phase 2: Namespace Isolation
Scoped store wrapper that auto-prefixes groups with a namespace to prevent key collisions across tenants. Pure Go, no new deps.
2.1 ScopedStore Wrapper
- Create
scope.go— A lightweight wrapper around*Storethat auto-prefixes all group names:type ScopedStore struct { store *Store; namespace string }— holds reference to underlying store and namespace prefixfunc NewScoped(store *Store, namespace string) *ScopedStore— constructor. Validates namespace is non-empty, alphanumeric + hyphens only.func (s *ScopedStore) prefix(group string) string— returnsnamespace + ":" + group- Implement all
Storemethods onScopedStorethat delegate to the underlying store with prefixed groups:Get(group, key),Set(group, key, value),SetWithTTL(group, key, value, ttl)Delete(group, key),DeleteGroup(group)GetAll(group),Count(group)Render(group, key, data)
- Each method simply calls
s.store.Method(s.prefix(group), key, ...)— thin delegation, no logic duplication.
2.2 Quota Enforcement
- Add
QuotaConfigto ScopedStore — Optional quota limits per namespace:type QuotaConfig struct { MaxKeys int; MaxGroups int }— zero means unlimitedfunc NewScopedWithQuota(store *Store, namespace string, quota QuotaConfig) *ScopedStorevar ErrQuotaExceeded = errors.New("store: quota exceeded")
- Enforce on Set() — Before inserting, check
Count()across all groups with the namespace prefix. IfMaxKeys > 0and current count >= MaxKeys, returnErrQuotaExceeded. Only check on new keys (UPSERT existing keys doesn't increase count). - Enforce on group creation — Track distinct groups with the namespace prefix. If
MaxGroups > 0and adding a new group would exceed the limit, returnErrQuotaExceeded. - Add
CountAll() (int, error)to Store — Returns total key count across ALL groups matching a prefix. SQL:SELECT COUNT(*) FROM kv WHERE grp LIKE ? AND (expires_at IS NULL OR expires_at > ?)withnamespace + ":%". - Add
Groups() ([]string, error)to Store — Returns distinct group names. SQL:SELECT DISTINCT grp FROM kv WHERE (expires_at IS NULL OR expires_at > ?). Useful for quota checks and admin tooling.
2.3 Tests
- ScopedStore basic tests — Set/Get/Delete through ScopedStore, verify underlying store has prefixed groups, two namespaces don't collide, GetAll returns only scoped group's keys
- Quota tests — (a) MaxKeys=5, insert 5 keys → OK, insert 6th → ErrQuotaExceeded, (b) UPSERT existing key doesn't count towards quota, (c) Delete + re-insert stays within quota, (d) MaxGroups=3, create 3 groups → OK, 4th → ErrQuotaExceeded, (e) zero quota = unlimited, (f) TTL-expired keys don't count towards quota
- CountAll/Groups tests — (a) CountAll with mixed namespaces, (b) Groups returns distinct list, (c) expired keys excluded from both
- Existing tests still pass — No changes to Store API, backward compatible. Coverage: 90.9% → 94.7%.
Phase 3: Event Hooks
- Notify on
Set/Deletefor reactive patterns - Channel-based subscription:
Watch(group, key) <-chan Event - Support wildcard watches (
Watch(group, "*")) - Integration hook for go-ws to broadcast store changes via WebSocket
Workflow
- Virgil in core/go writes tasks here after research
- This repo's dedicated session picks up tasks in phase order
- Mark
[x]when done, note commit hash