6.6 KiB
6.6 KiB
go-store RFC - AX-Aligned SQLite Store
An agent should be able to use this store from this document alone.
Module: dappco.re/go/core/store
Repository: core/go-store
Package: store
1. Overview
go-store is a single-package SQLite-backed key-value store with TTL expiry, namespace isolation, quota enforcement, and reactive mutation events.
The public surface is intentionally small. Names are descriptive, comments show concrete usage, and the implementation keeps a single SQLite connection so pragma settings stay consistent.
2. File Layout
| File | Purpose |
|---|---|
doc.go |
Package comment with concrete usage examples |
store.go |
Store, CRUD, TTL, background purge, bulk reads, prefix counts, group discovery, string splitting helpers, template rendering |
events.go |
EventType, Event, Watcher, Watch, Unwatch, OnChange, internal dispatch |
scope.go |
ScopedStore, QuotaConfig, namespace validation, quota enforcement |
*_test.go |
Behavioural tests for CRUD, TTL, events, quotas, and defensive error paths |
3. Core API
Store
New(databasePath string) (*Store, error)opens a SQLite database, applies WAL and busy-timeout pragmas, and pins access to one connection.Close() errorstops the background purge goroutine and closes the database.Get(group, key string) (string, error)returns a stored value orNotFoundError.Set(group, key, value string) errorstores a value and clears any existing TTL.SetWithTTL(group, key, value string, ttl time.Duration) errorstores a value that expires after the supplied duration.Delete(group, key string) errorremoves one key.DeleteGroup(group string) errorremoves every key in a group.Count(group string) (int, error)counts non-expired keys in one group.CountAll(groupPrefix string) (int, error)counts non-expired keys across groups that match a prefix.GetAll(group string) (map[string]string, error)returns all non-expired keys in one group.All(group string) iter.Seq2[KeyValue, error]streams all non-expired key-value pairs in one group.Groups(groupPrefix string) ([]string, error)returns distinct non-expired group names that match a prefix.GroupsSeq(groupPrefix string) iter.Seq2[string, error]streams matching group names.GetSplit(group, key, separator string) (iter.Seq[string], error)splits a stored value by a custom separator.GetFields(group, key string) (iter.Seq[string], error)splits a stored value on whitespace.Render(templateSource, group string) (string, error)renders a Gotext/templateusing group data.PurgeExpired() (int64, error)removes expired rows immediately.
ScopedStore
NewScoped(storeInstance *Store, namespace string) (*ScopedStore, error)validates a namespace and prefixes groups withnamespace + ":".NewScopedWithQuota(storeInstance *Store, namespace string, quota QuotaConfig) (*ScopedStore, error)adds per-namespace key and group limits.Namespace() stringreturns the namespace string.ScopedStoreexposes the same read and write methods asStore, with group names prefixed automatically.
Events
EventTypevalues:EventSet,EventDelete,EventDeleteGroup.EventType.String()returnsset,delete,delete_group, orunknown.EventcarriesType,Group,Key,Value, andTimestamp.WatcherexposesEvents <-chan Event.Watch(group, key string) *Watcherregisters a buffered watcher.Unwatch(watcher *Watcher)removes a watcher and closes its channel.OnChange(callback func(Event)) func()registers a synchronous callback and returns an idempotent unregister function.
Quotas and Errors
QuotaConfig{MaxKeys, MaxGroups int}sets per-namespace limits; zero means unlimited.NotFoundErroris returned when a key does not exist or has expired.QuotaExceededErroris returned when a namespace quota would be exceeded.KeyValueis the item type returned byAll.
4. Behavioural Rules
- Names use full words where practical:
Store,ScopedStore,QuotaConfig,Watcher,Namespace. - Public comments show concrete usage instead of restating the signature.
- Examples use UK English, for example
colourandbehaviour. - The store keeps a single SQLite connection open for all operations. SQLite pragmas are per-connection, so pooling would make behaviour unpredictable.
- TTL is enforced in three layers: lazy delete on
Get, query-time filtering on bulk reads, and a background purge goroutine. - Event delivery to watchers is non-blocking. If a watcher channel is full, the event is dropped rather than blocking the writer.
- Callbacks registered with
OnChangeare invoked synchronously after the database write. Callbacks can safely register or unregister other subscriptions because the watcher and callback registries use separate locks. - Quota checks happen before writes. Existing keys count as upserts and do not consume quota.
- Namespace strings must match
^[a-zA-Z0-9-]+$.
5. Concrete Usage
package main
import (
"fmt"
"time"
"dappco.re/go/core/store"
)
func main() {
storeInstance, err := store.New(":memory:")
if err != nil {
return
}
defer storeInstance.Close()
if err := storeInstance.Set("config", "colour", "blue"); err != nil {
return
}
if err := storeInstance.SetWithTTL("session", "token", "abc123", 5*time.Minute); err != nil {
return
}
colourValue, err := storeInstance.Get("config", "colour")
if err != nil {
return
}
fmt.Println(colourValue)
for entry, err := range storeInstance.All("config") {
if err != nil {
return
}
fmt.Println(entry.Key, entry.Value)
}
for groupName, err := range storeInstance.GroupsSeq("tenant-a:") {
if err != nil {
return
}
fmt.Println(groupName)
}
watcher := storeInstance.Watch("config", "*")
defer storeInstance.Unwatch(watcher)
go func() {
for event := range watcher.Events {
fmt.Println(event.Type, event.Group, event.Key, event.Value)
}
}()
unregister := storeInstance.OnChange(func(event store.Event) {
fmt.Println("changed", event.Group, event.Key, event.Value)
})
defer unregister()
scopedStore, err := store.NewScopedWithQuota(
storeInstance,
"tenant-a",
store.QuotaConfig{MaxKeys: 100, MaxGroups: 10},
)
if err != nil {
return
}
if err := scopedStore.Set("prefs", "locale", "en-GB"); err != nil {
return
}
}
6. Reference Paths
| Resource | Location |
|---|---|
| Package comment | doc.go |
| Core store implementation | store.go |
| Events and callbacks | events.go |
| Namespace and quota logic | scope.go |
| Architecture notes | docs/architecture.md |
| Agent conventions | CODEX.md |