refactor(store): tighten AX examples and error handling
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
c15862a81d
commit
5df38516cc
7 changed files with 134 additions and 57 deletions
26
README.md
26
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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
37
doc.go
37
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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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", "*")
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
18
scope.go
18
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))
|
||||
}
|
||||
|
|
|
|||
32
store.go
32
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.
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue