diff --git a/README.md b/README.md index 0940ba2..bdaa78c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Group-namespaced SQLite key-value store with TTL expiry, namespace isolation, qu **Module**: `dappco.re/go/core/store` **Licence**: EUPL-1.2 -**Language**: Go 1.25 +**Language**: Go 1.26 ## Quick Start @@ -25,14 +25,21 @@ import ( func main() { storeInstance, err := store.New("/path/to/store.db") // or store.New(":memory:") if err != nil { - panic(err) + return } defer storeInstance.Close() - _ = storeInstance.Set("config", "theme", "dark") - _ = storeInstance.SetWithTTL("session", "token", "abc123", 24*time.Hour) + if err := storeInstance.Set("config", "theme", "dark"); err != nil { + return + } + if err := storeInstance.SetWithTTL("session", "token", "abc123", 24*time.Hour); err != nil { + return + } themeValue, err := storeInstance.Get("config", "theme") - fmt.Println(themeValue, err) + if err != nil { + return + } + fmt.Println(themeValue) // Watch for mutations watcher := storeInstance.Watch("config", "*") @@ -44,8 +51,13 @@ func main() { }() // Scoped store for tenant isolation - scopedStore, _ := store.NewScoped(storeInstance, "tenant-42") - _ = scopedStore.Set("prefs", "locale", "en-GB") + scopedStore, err := store.NewScoped(storeInstance, "tenant-42") + if err != nil { + return + } + if err := scopedStore.Set("prefs", "locale", "en-GB"); err != nil { + return + } } ``` diff --git a/doc.go b/doc.go index 87ce322..76120d7 100644 --- a/doc.go +++ b/doc.go @@ -3,15 +3,36 @@ // // Usage example: // -// storeInstance, _ := store.New(":memory:") -// defer storeInstance.Close() +// func main() { +// storeInstance, err := store.New(":memory:") +// if err != nil { +// return +// } +// defer storeInstance.Close() // -// _ = storeInstance.Set("config", "theme", "dark") -// themeValue, _ := storeInstance.Get("config", "theme") +// if err := storeInstance.Set("config", "theme", "dark"); err != nil { +// return +// } +// themeValue, err := storeInstance.Get("config", "theme") +// if err != nil { +// return +// } +// core.Println(themeValue) // -// scopedStore, _ := store.NewScoped(storeInstance, "tenant-a") -// _ = scopedStore.Set("config", "theme", "dark") +// scopedStore, err := store.NewScoped(storeInstance, "tenant-a") +// if err != nil { +// return +// } +// if err := scopedStore.Set("config", "theme", "dark"); err != nil { +// return +// } // -// quotaScopedStore, _ := store.NewScopedWithQuota(storeInstance, "tenant-b", store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}) -// _ = quotaScopedStore.Set("prefs", "locale", "en-GB") +// quotaScopedStore, err := store.NewScopedWithQuota(storeInstance, "tenant-b", store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}) +// if err != nil { +// return +// } +// if err := quotaScopedStore.Set("prefs", "locale", "en-GB"); err != nil { +// return +// } +// } package store diff --git a/docs/architecture.md b/docs/architecture.md index e2afb0d..cb189fa 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -103,9 +103,16 @@ Both return `NotFoundError` if the key does not exist or has expired. `Render(templateSource, group)` is a convenience method that fetches all non-expired key-value pairs from a group and renders a Go `text/template` against them. The template data is a `map[string]string` keyed by the field name. ```go -storeInstance.Set("miner", "pool", "pool.lthn.io:3333") -storeInstance.Set("miner", "wallet", "iz...") -renderedTemplate, _ := storeInstance.Render(`{"pool":"{{ .pool }}","wallet":"{{ .wallet }}"}`, "miner") +if err := storeInstance.Set("miner", "pool", "pool.lthn.io:3333"); err != nil { + return +} +if err := storeInstance.Set("miner", "wallet", "iz..."); err != nil { + return +} +renderedTemplate, err := storeInstance.Render(`{"pool":"{{ .pool }}","wallet":"{{ .wallet }}"}`, "miner") +if err != nil { + return +} // renderedTemplate: {"pool":"pool.lthn.io:3333","wallet":"iz..."} ``` @@ -187,8 +194,13 @@ Watcher matching is handled by the `watcherMatches` helper, which checks the gro `ScopedStore` wraps a `*Store` and automatically prefixes all group names with `namespace + ":"`. This prevents key collisions when multiple tenants share a single underlying database. ```go -scopedStore, _ := store.NewScoped(storeInstance, "tenant-42") -scopedStore.Set("config", "theme", "dark") +scopedStore, err := store.NewScoped(storeInstance, "tenant-42") +if err != nil { + return +} +if err := scopedStore.Set("config", "theme", "dark"); err != nil { + return +} // Stored in underlying store as group="tenant-42:config", key="theme" ``` diff --git a/docs/index.md b/docs/index.md index 74ee339..e44afa7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -29,37 +29,65 @@ func main() { // Open a store. Use ":memory:" for ephemeral data or a file path for persistence. storeInstance, err := store.New("/tmp/app.db") if err != nil { - panic(err) + return } defer storeInstance.Close() // Basic CRUD - _ = storeInstance.Set("config", "theme", "dark") - themeValue, _ := storeInstance.Get("config", "theme") + if err := storeInstance.Set("config", "theme", "dark"); err != nil { + return + } + themeValue, err := storeInstance.Get("config", "theme") + if err != nil { + return + } core.Println(themeValue) // "dark" // TTL expiry -- key disappears after the duration elapses - _ = storeInstance.SetWithTTL("session", "token", "abc123", 24*time.Hour) + if err := storeInstance.SetWithTTL("session", "token", "abc123", 24*time.Hour); err != nil { + return + } // Fetch all keys in a group - configEntries, _ := storeInstance.GetAll("config") + configEntries, err := storeInstance.GetAll("config") + if err != nil { + return + } core.Println(configEntries) // map[theme:dark] // Template rendering from stored values - storeInstance.Set("mail", "host", "smtp.example.com") - storeInstance.Set("mail", "port", "587") - renderedTemplate, _ := storeInstance.Render(`{{ .host }}:{{ .port }}`, "mail") + if err := storeInstance.Set("mail", "host", "smtp.example.com"); err != nil { + return + } + if err := storeInstance.Set("mail", "port", "587"); err != nil { + return + } + renderedTemplate, err := storeInstance.Render(`{{ .host }}:{{ .port }}`, "mail") + if err != nil { + return + } core.Println(renderedTemplate) // "smtp.example.com:587" // Namespace isolation for multi-tenant use - scopedStore, _ := store.NewScoped(storeInstance, "tenant-42") - _ = scopedStore.Set("prefs", "locale", "en-GB") + scopedStore, err := store.NewScoped(storeInstance, "tenant-42") + if err != nil { + return + } + if err := scopedStore.Set("prefs", "locale", "en-GB"); err != nil { + return + } // Stored internally as group "tenant-42:prefs", key "locale" // Quota enforcement quota := store.QuotaConfig{MaxKeys: 100, MaxGroups: 5} - quotaScopedStore, _ := store.NewScopedWithQuota(storeInstance, "tenant-99", quota) - err = quotaScopedStore.Set("g", "k", "v") // returns store.QuotaExceededError if limits are hit + quotaScopedStore, err := store.NewScopedWithQuota(storeInstance, "tenant-99", quota) + if err != nil { + return + } + // returns store.QuotaExceededError if limits are hit + if err := quotaScopedStore.Set("g", "k", "v"); err != nil { + return + } // Watch for mutations via a buffered channel watcher := storeInstance.Watch("config", "*") diff --git a/events.go b/events.go index a41f70c..84bef9f 100644 --- a/events.go +++ b/events.go @@ -43,9 +43,9 @@ type Event struct { Timestamp time.Time } -// Usage example: `watcher := storeInstance.Watch("config", "*"); defer storeInstance.Unwatch(watcher); for event := range watcher.Events { _ = event }` +// Usage example: `watcher := storeInstance.Watch("config", "*"); defer storeInstance.Unwatch(watcher); for event := range watcher.Events { if event.Type == EventDeleteGroup { return } }` type Watcher struct { - // Usage example: `for event := range watcher.Events { _ = event }` + // Usage example: `for event := range watcher.Events { if event.Key == "theme" { return } }` Events <-chan Event // eventChannel is the internal write channel (same underlying channel as Events). diff --git a/scope.go b/scope.go index 675ccc0..8ff8326 100644 --- a/scope.go +++ b/scope.go @@ -17,14 +17,14 @@ type QuotaConfig struct { MaxGroups int // maximum distinct groups in the namespace } -// Usage example: `scopedStore, _ := store.NewScoped(storeInstance, "tenant-a"); _ = scopedStore.Set("config", "theme", "dark")` +// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }; if err := scopedStore.Set("config", "theme", "dark"); err != nil { return }` type ScopedStore struct { storeInstance *Store namespace string quota QuotaConfig } -// Usage example: `scopedStore, _ := store.NewScoped(storeInstance, "tenant-a")` +// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }` func NewScoped(storeInstance *Store, namespace string) (*ScopedStore, error) { if !validNamespace.MatchString(namespace) { return nil, core.E("store.NewScoped", core.Sprintf("namespace %q is invalid (must be non-empty, alphanumeric + hyphens)", namespace), nil) @@ -33,7 +33,7 @@ func NewScoped(storeInstance *Store, namespace string) (*ScopedStore, error) { return scopedStore, nil } -// Usage example: `scopedStore, _ := store.NewScopedWithQuota(storeInstance, "tenant-a", store.QuotaConfig{MaxKeys: 100, MaxGroups: 10})` +// Usage example: `scopedStore, err := store.NewScopedWithQuota(storeInstance, "tenant-a", store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}); if err != nil { return }` func NewScopedWithQuota(storeInstance *Store, namespace string, quota QuotaConfig) (*ScopedStore, error) { scopedStore, err := NewScoped(storeInstance, namespace) if err != nil { @@ -47,7 +47,7 @@ func (scopedStore *ScopedStore) namespacedGroup(group string) string { return scopedStore.namespace + ":" + group } -// Usage example: `scopedStore, _ := store.NewScoped(storeInstance, "tenant-a"); namespace := scopedStore.Namespace()` +// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }; namespace := scopedStore.Namespace(); core.Println(namespace)` func (scopedStore *ScopedStore) Namespace() string { return scopedStore.namespace } @@ -57,7 +57,7 @@ func (scopedStore *ScopedStore) Get(group, key string) (string, error) { return scopedStore.storeInstance.Get(scopedStore.namespacedGroup(group), key) } -// Usage example: `_ = scopedStore.Set("config", "theme", "dark")` +// Usage example: `if err := scopedStore.Set("config", "theme", "dark"); err != nil { return }` func (scopedStore *ScopedStore) Set(group, key, value string) error { if err := scopedStore.checkQuota("store.ScopedStore.Set", group, key); err != nil { return err @@ -65,7 +65,7 @@ func (scopedStore *ScopedStore) Set(group, key, value string) error { return scopedStore.storeInstance.Set(scopedStore.namespacedGroup(group), key, value) } -// Usage example: `_ = scopedStore.SetWithTTL("sessions", "token", "abc123", time.Hour)` +// Usage example: `if err := scopedStore.SetWithTTL("sessions", "token", "abc123", time.Hour); err != nil { return }` func (scopedStore *ScopedStore) SetWithTTL(group, key, value string, ttl time.Duration) error { if err := scopedStore.checkQuota("store.ScopedStore.SetWithTTL", group, key); err != nil { return err @@ -73,12 +73,12 @@ func (scopedStore *ScopedStore) SetWithTTL(group, key, value string, ttl time.Du return scopedStore.storeInstance.SetWithTTL(scopedStore.namespacedGroup(group), key, value, ttl) } -// Usage example: `_ = scopedStore.Delete("config", "theme")` +// Usage example: `if err := scopedStore.Delete("config", "theme"); err != nil { return }` func (scopedStore *ScopedStore) Delete(group, key string) error { return scopedStore.storeInstance.Delete(scopedStore.namespacedGroup(group), key) } -// Usage example: `_ = scopedStore.DeleteGroup("cache")` +// Usage example: `if err := scopedStore.DeleteGroup("cache"); err != nil { return }` func (scopedStore *ScopedStore) DeleteGroup(group string) error { return scopedStore.storeInstance.DeleteGroup(scopedStore.namespacedGroup(group)) } @@ -88,7 +88,7 @@ func (scopedStore *ScopedStore) GetAll(group string) (map[string]string, error) return scopedStore.storeInstance.GetAll(scopedStore.namespacedGroup(group)) } -// Usage example: `for entry, err := range scopedStore.All("config") { if err != nil { break }; _ = entry }` +// Usage example: `for entry, err := range scopedStore.All("config") { if err != nil { break }; core.Println(entry.Key, entry.Value) }` func (scopedStore *ScopedStore) All(group string) iter.Seq2[KeyValue, error] { return scopedStore.storeInstance.All(scopedStore.namespacedGroup(group)) } diff --git a/store.go b/store.go index e8c976a..be5b91e 100644 --- a/store.go +++ b/store.go @@ -27,7 +27,7 @@ const ( entryValueColumn = "entry_value" ) -// Usage example: `storeInstance, _ := store.New(":memory:"); _ = storeInstance.Set("config", "theme", "dark")` +// Usage example: `storeInstance, err := store.New(":memory:"); if err != nil { return }; if err := storeInstance.Set("config", "theme", "dark"); err != nil { return }` type Store struct { database *sql.DB cancelPurge context.CancelFunc @@ -42,7 +42,7 @@ type Store struct { nextRegistrationID uint64 // monotonic ID for watchers and callbacks } -// Usage example: `storeInstance, _ := store.New(":memory:")` +// Usage example: `storeInstance, err := store.New(":memory:"); if err != nil { return }` func New(databasePath string) (*Store, error) { sqliteDatabase, err := sql.Open("sqlite", databasePath) if err != nil { @@ -72,7 +72,7 @@ func New(databasePath string) (*Store, error) { return storeInstance, nil } -// Usage example: `storeInstance, _ := store.New(":memory:"); defer storeInstance.Close()` +// Usage example: `storeInstance, err := store.New(":memory:"); if err != nil { return }; defer storeInstance.Close()` func (storeInstance *Store) Close() error { storeInstance.cancelPurge() storeInstance.purgeWaitGroup.Wait() @@ -97,13 +97,15 @@ func (storeInstance *Store) Get(group, key string) (string, error) { return "", core.E("store.Get", "query", err) } if expiresAt.Valid && expiresAt.Int64 <= time.Now().UnixMilli() { - _, _ = storeInstance.database.Exec("DELETE FROM "+entriesTableName+" WHERE "+entryGroupColumn+" = ? AND "+entryKeyColumn+" = ?", group, key) + if _, err := storeInstance.database.Exec("DELETE FROM "+entriesTableName+" WHERE "+entryGroupColumn+" = ? AND "+entryKeyColumn+" = ?", group, key); err != nil { + return "", core.E("store.Get", "lazy delete", err) + } return "", core.E("store.Get", core.Concat(group, "/", key), NotFoundError) } return value, nil } -// Usage example: `_ = storeInstance.Set("config", "theme", "dark")` +// Usage example: `if err := storeInstance.Set("config", "theme", "dark"); err != nil { return }` func (storeInstance *Store) Set(group, key, value string) error { _, err := storeInstance.database.Exec( "INSERT INTO "+entriesTableName+" ("+entryGroupColumn+", "+entryKeyColumn+", "+entryValueColumn+", expires_at) VALUES (?, ?, ?, NULL) "+ @@ -117,7 +119,7 @@ func (storeInstance *Store) Set(group, key, value string) error { return nil } -// Usage example: `_ = storeInstance.SetWithTTL("session", "token", "abc123", time.Minute)` +// Usage example: `if err := storeInstance.SetWithTTL("session", "token", "abc123", time.Minute); err != nil { return }` func (storeInstance *Store) SetWithTTL(group, key, value string, ttl time.Duration) error { expiresAt := time.Now().Add(ttl).UnixMilli() _, err := storeInstance.database.Exec( @@ -132,7 +134,7 @@ func (storeInstance *Store) SetWithTTL(group, key, value string, ttl time.Durati return nil } -// Usage example: `_ = storeInstance.Delete("config", "theme")` +// Usage example: `if err := storeInstance.Delete("config", "theme"); err != nil { return }` func (storeInstance *Store) Delete(group, key string) error { _, err := storeInstance.database.Exec("DELETE FROM "+entriesTableName+" WHERE "+entryGroupColumn+" = ? AND "+entryKeyColumn+" = ?", group, key) if err != nil { @@ -155,7 +157,7 @@ func (storeInstance *Store) Count(group string) (int, error) { return count, nil } -// Usage example: `_ = storeInstance.DeleteGroup("cache")` +// Usage example: `if err := storeInstance.DeleteGroup("cache"); err != nil { return }` func (storeInstance *Store) DeleteGroup(group string) error { _, err := storeInstance.database.Exec("DELETE FROM "+entriesTableName+" WHERE "+entryGroupColumn+" = ?", group) if err != nil { @@ -165,7 +167,7 @@ func (storeInstance *Store) DeleteGroup(group string) error { return nil } -// Usage example: `for entry, err := range storeInstance.All("config") { if err != nil { break }; _ = entry }` +// Usage example: `for entry, err := range storeInstance.All("config") { if err != nil { break }; core.Println(entry.Key, entry.Value) }` type KeyValue struct { Key, Value string } @@ -182,7 +184,7 @@ func (storeInstance *Store) GetAll(group string) (map[string]string, error) { return entriesByKey, nil } -// Usage example: `for entry, err := range storeInstance.All("config") { if err != nil { break }; _ = entry }` +// Usage example: `for entry, err := range storeInstance.All("config") { if err != nil { break }; core.Println(entry.Key, entry.Value) }` func (storeInstance *Store) All(group string) iter.Seq2[KeyValue, error] { return func(yield func(KeyValue, error) bool) { rows, err := storeInstance.database.Query( @@ -213,7 +215,7 @@ func (storeInstance *Store) All(group string) iter.Seq2[KeyValue, error] { } } -// Usage example: `parts, _ := storeInstance.GetSplit("config", "hosts", ",")` +// Usage example: `parts, err := storeInstance.GetSplit("config", "hosts", ","); if err != nil { return }; for part := range parts { core.Println(part) }` func (storeInstance *Store) GetSplit(group, key, separator string) (iter.Seq[string], error) { value, err := storeInstance.Get(group, key) if err != nil { @@ -222,7 +224,7 @@ func (storeInstance *Store) GetSplit(group, key, separator string) (iter.Seq[str return splitSeq(value, separator), nil } -// Usage example: `fields, _ := storeInstance.GetFields("config", "flags")` +// Usage example: `fields, err := storeInstance.GetFields("config", "flags"); if err != nil { return }; for field := range fields { core.Println(field) }` func (storeInstance *Store) GetFields(group, key string) (iter.Seq[string], error) { value, err := storeInstance.Get(group, key) if err != nil { @@ -285,7 +287,7 @@ func (storeInstance *Store) Groups(groupPrefix string) ([]string, error) { return groupNames, nil } -// Usage example: `for tenantGroupName, err := range storeInstance.GroupsSeq("tenant-a:") { if err != nil { break }; _ = tenantGroupName }` +// Usage example: `for tenantGroupName, err := range storeInstance.GroupsSeq("tenant-a:") { if err != nil { break }; core.Println(tenantGroupName) }` func (storeInstance *Store) GroupsSeq(groupPrefix string) iter.Seq2[string, error] { return func(yield func(string, error) bool) { var rows *sql.Rows @@ -489,7 +491,9 @@ func migrateLegacyEntriesTable(database *sql.DB) error { committed := false defer func() { if !committed { - _ = transaction.Rollback() + if rollbackErr := transaction.Rollback(); rollbackErr != nil { + // Ignore rollback failures; the original error is already being returned. + } } }()