2026-02-20 08:19:11 +00:00
package store
import (
2026-04-04 19:12:24 +00:00
"database/sql"
2026-02-23 05:21:39 +00:00
"iter"
2026-02-20 08:19:11 +00:00
"regexp"
"time"
2026-03-16 21:29:42 +00:00
2026-03-26 13:58:50 +00:00
core "dappco.re/go/core"
2026-02-20 08:19:11 +00:00
)
2026-03-30 17:37:50 +00:00
// validNamespace.MatchString("tenant-a") is true; validNamespace.MatchString("tenant_a") is false.
2026-02-20 08:19:11 +00:00
var validNamespace = regexp . MustCompile ( ` ^[a-zA-Z0-9-]+$ ` )
2026-03-30 20:46:43 +00:00
const defaultScopedGroupName = "default"
2026-04-04 16:06:43 +00:00
// QuotaConfig sets per-namespace key and group limits.
2026-03-26 19:17:11 +00:00
// Usage example: `quota := store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}`
2026-02-20 08:19:11 +00:00
type QuotaConfig struct {
2026-03-30 16:54:02 +00:00
// Usage example: `store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}` limits a namespace to 100 keys.
MaxKeys int
// Usage example: `store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}` limits a namespace to 10 groups.
MaxGroups int
2026-02-20 08:19:11 +00:00
}
2026-04-04 15:47:56 +00:00
// Usage example: `if err := (store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}).Validate(); err != nil { return }`
func ( quotaConfig QuotaConfig ) Validate ( ) error {
if quotaConfig . MaxKeys < 0 || quotaConfig . MaxGroups < 0 {
return core . E (
"store.QuotaConfig.Validate" ,
core . Sprintf ( "quota values must be zero or positive; got MaxKeys=%d MaxGroups=%d" , quotaConfig . MaxKeys , quotaConfig . MaxGroups ) ,
nil ,
)
}
return nil
}
2026-04-04 16:06:43 +00:00
// ScopedStoreConfig combines namespace selection with optional quota limits.
2026-04-04 09:34:54 +00:00
// Usage example: `config := store.ScopedStoreConfig{Namespace: "tenant-a", Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}`
type ScopedStoreConfig struct {
// Usage example: `config := store.ScopedStoreConfig{Namespace: "tenant-a"}`
Namespace string
// Usage example: `config := store.ScopedStoreConfig{Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}`
Quota QuotaConfig
}
2026-04-04 10:46:46 +00:00
// Usage example: `if err := (store.ScopedStoreConfig{Namespace: "tenant-a", Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}).Validate(); err != nil { return }`
2026-04-04 11:26:16 +00:00
func ( scopedConfig ScopedStoreConfig ) Validate ( ) error {
if ! validNamespace . MatchString ( scopedConfig . Namespace ) {
2026-04-04 10:46:46 +00:00
return core . E (
"store.ScopedStoreConfig.Validate" ,
2026-04-04 11:26:16 +00:00
core . Sprintf ( "namespace %q is invalid; use names like %q or %q" , scopedConfig . Namespace , "tenant-a" , "tenant-42" ) ,
2026-04-04 10:46:46 +00:00
nil ,
)
}
2026-04-04 15:47:56 +00:00
if err := scopedConfig . Quota . Validate ( ) ; err != nil {
return core . E ( "store.ScopedStoreConfig.Validate" , "quota" , err )
2026-04-04 10:46:46 +00:00
}
return nil
}
2026-04-04 16:06:43 +00:00
// ScopedStore prefixes group names with namespace + ":" before delegating to Store.
2026-04-04 17:43:55 +00:00
//
2026-04-04 16:37:56 +00:00
// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }; if err := scopedStore.SetIn("config", "colour", "blue"); err != nil { return }`
2026-04-04 16:06:43 +00:00
type ScopedStore struct {
storeInstance * Store
namespace string
// Usage example: `scopedStore.MaxKeys = 100`
MaxKeys int
// Usage example: `scopedStore.MaxGroups = 10`
MaxGroups int
2026-04-03 06:31:35 +00:00
}
2026-04-04 16:06:43 +00:00
// NewScoped validates a namespace and prefixes groups with namespace + ":".
// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }`
func NewScoped ( storeInstance * Store , namespace string ) ( * ScopedStore , error ) {
2026-03-30 19:29:48 +00:00
if storeInstance == nil {
2026-04-04 16:06:43 +00:00
return nil , core . E ( "store.NewScoped" , "store instance is nil" , nil )
2026-03-30 19:29:48 +00:00
}
2026-02-20 08:19:11 +00:00
if ! validNamespace . MatchString ( namespace ) {
2026-04-04 16:06:43 +00:00
return nil , core . E ( "store.NewScoped" , core . Sprintf ( "namespace %q is invalid; use names like %q or %q" , namespace , "tenant-a" , "tenant-42" ) , nil )
2026-02-20 08:19:11 +00:00
}
2026-04-04 16:06:43 +00:00
scopedStore := & ScopedStore { storeInstance : storeInstance , namespace : namespace }
return scopedStore , nil
2026-02-20 08:19:11 +00:00
}
2026-04-04 16:06:43 +00:00
// NewScopedConfigured validates the namespace and optional quota settings before constructing a ScopedStore.
2026-04-04 09:34:54 +00:00
// Usage example: `scopedStore, err := store.NewScopedConfigured(storeInstance, store.ScopedStoreConfig{Namespace: "tenant-a", Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}); if err != nil { return }`
2026-04-04 11:26:16 +00:00
func NewScopedConfigured ( storeInstance * Store , scopedConfig ScopedStoreConfig ) ( * ScopedStore , error ) {
2026-04-04 10:46:46 +00:00
if storeInstance == nil {
return nil , core . E ( "store.NewScopedConfigured" , "store instance is nil" , nil )
2026-02-20 08:19:11 +00:00
}
2026-04-04 11:26:16 +00:00
if err := scopedConfig . Validate ( ) ; err != nil {
2026-04-04 10:46:46 +00:00
return nil , core . E ( "store.NewScopedConfigured" , "validate config" , err )
2026-03-30 20:03:19 +00:00
}
2026-04-04 16:06:43 +00:00
scopedStore , err := NewScoped ( storeInstance , scopedConfig . Namespace )
if err != nil {
return nil , err
}
2026-04-04 11:26:16 +00:00
scopedStore . MaxKeys = scopedConfig . Quota . MaxKeys
scopedStore . MaxGroups = scopedConfig . Quota . MaxGroups
2026-03-30 14:54:34 +00:00
return scopedStore , nil
2026-02-20 08:19:11 +00:00
}
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) namespacedGroup ( group string ) string {
return scopedStore . namespace + ":" + group
2026-02-20 08:19:11 +00:00
}
2026-03-30 19:14:09 +00:00
func ( scopedStore * ScopedStore ) namespacePrefix ( ) string {
return scopedStore . namespace + ":"
}
2026-04-04 16:06:43 +00:00
func ( scopedStore * ScopedStore ) defaultGroup ( ) string {
return defaultScopedGroupName
}
2026-03-30 19:14:09 +00:00
func ( scopedStore * ScopedStore ) trimNamespacePrefix ( groupName string ) string {
return core . TrimPrefix ( groupName , scopedStore . namespacePrefix ( ) )
}
2026-04-04 16:06:43 +00:00
// Namespace returns the namespace string.
// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }; namespace := scopedStore.Namespace(); fmt.Println(namespace)`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) Namespace ( ) string {
return scopedStore . namespace
2026-02-20 08:19:11 +00:00
}
2026-03-30 20:46:43 +00:00
// Usage example: `colourValue, err := scopedStore.Get("colour")`
2026-04-04 16:37:56 +00:00
func ( scopedStore * ScopedStore ) Get ( key string ) ( string , error ) {
return scopedStore . storeInstance . Get ( scopedStore . namespacedGroup ( scopedStore . defaultGroup ( ) ) , key )
2026-04-03 05:13:46 +00:00
}
2026-04-04 16:06:43 +00:00
// GetFrom reads a key from an explicit namespaced group.
2026-04-03 05:13:46 +00:00
// Usage example: `colourValue, err := scopedStore.GetFrom("config", "colour")`
func ( scopedStore * ScopedStore ) GetFrom ( group , key string ) ( string , error ) {
2026-04-04 16:37:56 +00:00
return scopedStore . storeInstance . Get ( scopedStore . namespacedGroup ( group ) , key )
2026-02-20 08:19:11 +00:00
}
2026-03-30 20:46:43 +00:00
// Usage example: `if err := scopedStore.Set("colour", "blue"); err != nil { return }`
2026-04-04 16:37:56 +00:00
func ( scopedStore * ScopedStore ) Set ( key , value string ) error {
defaultGroup := scopedStore . defaultGroup ( )
if err := scopedStore . checkQuota ( "store.ScopedStore.Set" , defaultGroup , key ) ; err != nil {
2026-04-03 06:31:35 +00:00
return err
}
2026-04-04 16:37:56 +00:00
return scopedStore . storeInstance . Set ( scopedStore . namespacedGroup ( defaultGroup ) , key , value )
2026-04-04 16:06:43 +00:00
}
// SetIn writes a key to an explicit namespaced group.
// Usage example: `if err := scopedStore.SetIn("config", "colour", "blue"); err != nil { return }`
func ( scopedStore * ScopedStore ) SetIn ( group , key , value string ) error {
2026-04-04 16:37:56 +00:00
if err := scopedStore . checkQuota ( "store.ScopedStore.SetIn" , group , key ) ; err != nil {
return err
}
return scopedStore . storeInstance . Set ( scopedStore . namespacedGroup ( group ) , key , value )
2026-02-20 08:19:11 +00:00
}
2026-03-30 16:41:56 +00:00
// Usage example: `if err := scopedStore.SetWithTTL("sessions", "token", "abc123", time.Hour); err != nil { return }`
2026-03-30 18:37:07 +00:00
func ( scopedStore * ScopedStore ) SetWithTTL ( group , key , value string , timeToLive time . Duration ) error {
2026-03-30 16:27:54 +00:00
if err := scopedStore . checkQuota ( "store.ScopedStore.SetWithTTL" , group , key ) ; err != nil {
2026-02-20 08:19:11 +00:00
return err
}
2026-04-04 16:06:43 +00:00
return scopedStore . storeInstance . SetWithTTL ( scopedStore . namespacedGroup ( group ) , key , value , timeToLive )
2026-02-20 08:19:11 +00:00
}
2026-03-30 18:49:17 +00:00
// Usage example: `if err := scopedStore.Delete("config", "colour"); err != nil { return }`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) Delete ( group , key string ) error {
2026-04-04 16:06:43 +00:00
return scopedStore . storeInstance . Delete ( scopedStore . namespacedGroup ( group ) , key )
2026-02-20 08:19:11 +00:00
}
2026-03-30 16:41:56 +00:00
// Usage example: `if err := scopedStore.DeleteGroup("cache"); err != nil { return }`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) DeleteGroup ( group string ) error {
2026-04-04 16:06:43 +00:00
return scopedStore . storeInstance . DeleteGroup ( scopedStore . namespacedGroup ( group ) )
2026-04-03 07:14:22 +00:00
}
2026-04-04 16:09:14 +00:00
// Usage example: `if err := scopedStore.DeletePrefix("cache"); err != nil { return }`
// Usage example: `if err := scopedStore.DeletePrefix(""); err != nil { return }`
func ( scopedStore * ScopedStore ) DeletePrefix ( groupPrefix string ) error {
return scopedStore . storeInstance . DeletePrefix ( scopedStore . namespacedGroup ( groupPrefix ) )
}
2026-03-30 18:49:17 +00:00
// Usage example: `colourEntries, err := scopedStore.GetAll("config")`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) GetAll ( group string ) ( map [ string ] string , error ) {
2026-04-04 16:06:43 +00:00
return scopedStore . storeInstance . GetAll ( scopedStore . namespacedGroup ( group ) )
2026-04-03 08:32:35 +00:00
}
2026-04-04 17:53:21 +00:00
// Usage example: `page, err := scopedStore.GetPage("config", 0, 25); if err != nil { return }; for _, entry := range page { fmt.Println(entry.Key, entry.Value) }`
func ( scopedStore * ScopedStore ) GetPage ( group string , offset , limit int ) ( [ ] KeyValue , error ) {
return scopedStore . storeInstance . GetPage ( scopedStore . namespacedGroup ( group ) , offset , limit )
}
2026-03-30 17:32:09 +00:00
// Usage example: `for entry, err := range scopedStore.All("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) All ( group string ) iter . Seq2 [ KeyValue , error ] {
2026-04-04 16:06:43 +00:00
return scopedStore . storeInstance . All ( scopedStore . namespacedGroup ( group ) )
2026-02-23 05:21:39 +00:00
}
2026-04-03 05:13:46 +00:00
// Usage example: `for entry, err := range scopedStore.AllSeq("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }`
func ( scopedStore * ScopedStore ) AllSeq ( group string ) iter . Seq2 [ KeyValue , error ] {
2026-04-04 16:06:43 +00:00
return scopedStore . All ( group )
2026-04-03 05:13:46 +00:00
}
2026-03-30 16:13:55 +00:00
// Usage example: `keyCount, err := scopedStore.Count("config")`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) Count ( group string ) ( int , error ) {
2026-04-04 16:06:43 +00:00
return scopedStore . storeInstance . Count ( scopedStore . namespacedGroup ( group ) )
2026-02-20 08:19:11 +00:00
}
2026-03-30 19:14:09 +00:00
// Usage example: `keyCount, err := scopedStore.CountAll("config")`
2026-03-30 20:46:43 +00:00
// Usage example: `keyCount, err := scopedStore.CountAll()`
func ( scopedStore * ScopedStore ) CountAll ( groupPrefix ... string ) ( int , error ) {
2026-04-04 18:42:19 +00:00
return scopedStore . storeInstance . CountAll ( scopedStore . namespacedGroup ( firstOrEmptyString ( groupPrefix ) ) )
2026-03-30 19:14:09 +00:00
}
// Usage example: `groupNames, err := scopedStore.Groups("config")`
2026-03-30 20:46:43 +00:00
// Usage example: `groupNames, err := scopedStore.Groups()`
func ( scopedStore * ScopedStore ) Groups ( groupPrefix ... string ) ( [ ] string , error ) {
2026-04-04 18:42:19 +00:00
groupNames , err := scopedStore . storeInstance . Groups ( scopedStore . namespacedGroup ( firstOrEmptyString ( groupPrefix ) ) )
2026-03-30 19:14:09 +00:00
if err != nil {
return nil , err
}
for i , groupName := range groupNames {
groupNames [ i ] = scopedStore . trimNamespacePrefix ( groupName )
}
return groupNames , nil
}
// Usage example: `for groupName, err := range scopedStore.GroupsSeq("config") { if err != nil { break }; fmt.Println(groupName) }`
2026-03-30 20:46:43 +00:00
// Usage example: `for groupName, err := range scopedStore.GroupsSeq() { if err != nil { break }; fmt.Println(groupName) }`
func ( scopedStore * ScopedStore ) GroupsSeq ( groupPrefix ... string ) iter . Seq2 [ string , error ] {
2026-03-30 19:14:09 +00:00
return func ( yield func ( string , error ) bool ) {
namespacePrefix := scopedStore . namespacePrefix ( )
2026-04-04 18:42:19 +00:00
for groupName , err := range scopedStore . storeInstance . GroupsSeq ( scopedStore . namespacedGroup ( firstOrEmptyString ( groupPrefix ) ) ) {
2026-03-30 19:14:09 +00:00
if err != nil {
if ! yield ( "" , err ) {
return
}
continue
}
if ! yield ( core . TrimPrefix ( groupName , namespacePrefix ) , nil ) {
return
}
}
}
}
2026-03-30 16:13:55 +00:00
// Usage example: `renderedTemplate, err := scopedStore.Render("Hello {{ .name }}", "user")`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) Render ( templateSource , group string ) ( string , error ) {
2026-04-04 16:06:43 +00:00
return scopedStore . storeInstance . Render ( templateSource , scopedStore . namespacedGroup ( group ) )
2026-02-20 08:19:11 +00:00
}
2026-03-30 19:14:09 +00:00
// Usage example: `parts, err := scopedStore.GetSplit("config", "hosts", ","); if err != nil { return }; for part := range parts { fmt.Println(part) }`
func ( scopedStore * ScopedStore ) GetSplit ( group , key , separator string ) ( iter . Seq [ string ] , error ) {
2026-04-04 16:06:43 +00:00
return scopedStore . storeInstance . GetSplit ( scopedStore . namespacedGroup ( group ) , key , separator )
2026-03-30 19:14:09 +00:00
}
// Usage example: `fields, err := scopedStore.GetFields("config", "flags"); if err != nil { return }; for field := range fields { fmt.Println(field) }`
func ( scopedStore * ScopedStore ) GetFields ( group , key string ) ( iter . Seq [ string ] , error ) {
2026-04-04 16:06:43 +00:00
return scopedStore . storeInstance . GetFields ( scopedStore . namespacedGroup ( group ) , key )
2026-04-03 06:55:37 +00:00
}
2026-03-30 19:14:09 +00:00
// Usage example: `removedRows, err := scopedStore.PurgeExpired(); if err != nil { return }; fmt.Println(removedRows)`
func ( scopedStore * ScopedStore ) PurgeExpired ( ) ( int64 , error ) {
2026-04-04 18:20:52 +00:00
if scopedStore == nil {
return 0 , core . E ( "store.ScopedStore.PurgeExpired" , "scoped store is nil" , nil )
}
if err := scopedStore . storeInstance . ensureReady ( "store.ScopedStore.PurgeExpired" ) ; err != nil {
return 0 , err
}
removedRows , err := purgeExpiredMatchingGroupPrefix ( scopedStore . storeInstance . sqliteDatabase , scopedStore . namespacePrefix ( ) )
2026-03-30 20:03:19 +00:00
if err != nil {
return 0 , core . E ( "store.ScopedStore.PurgeExpired" , "delete expired rows" , err )
}
return removedRows , nil
2026-03-30 19:14:09 +00:00
}
2026-04-04 19:24:47 +00:00
// Usage example: `unregister := scopedStore.OnChange(func(event store.Event) { fmt.Println(event.Group, event.Key, event.Value) })`
// The callback receives the namespace-local group name, so a write to
// `tenant-a:config` is reported as `config`.
func ( scopedStore * ScopedStore ) OnChange ( callback func ( Event ) ) func ( ) {
if scopedStore == nil || callback == nil {
return func ( ) { }
}
if scopedStore . storeInstance == nil {
return func ( ) { }
}
namespacePrefix := scopedStore . namespacePrefix ( )
return scopedStore . storeInstance . OnChange ( func ( event Event ) {
if ! core . HasPrefix ( event . Group , namespacePrefix ) {
return
}
event . Group = core . TrimPrefix ( event . Group , namespacePrefix )
callback ( event )
} )
}
2026-04-04 16:09:14 +00:00
// ScopedStoreTransaction exposes namespace-local transaction helpers so callers
// can work inside a scoped namespace without manually prefixing group names.
//
// Usage example: `err := scopedStore.Transaction(func(transaction *store.ScopedStoreTransaction) error { return transaction.Set("theme", "dark") })`
type ScopedStoreTransaction struct {
scopedStore * ScopedStore
storeTransaction * StoreTransaction
}
// Usage example: `err := scopedStore.Transaction(func(transaction *store.ScopedStoreTransaction) error { return transaction.Set("theme", "dark") })`
func ( scopedStore * ScopedStore ) Transaction ( operation func ( * ScopedStoreTransaction ) error ) error {
if scopedStore == nil {
return core . E ( "store.ScopedStore.Transaction" , "scoped store is nil" , nil )
}
if operation == nil {
return core . E ( "store.ScopedStore.Transaction" , "operation is nil" , nil )
}
return scopedStore . storeInstance . Transaction ( func ( storeTransaction * StoreTransaction ) error {
return operation ( & ScopedStoreTransaction {
scopedStore : scopedStore ,
storeTransaction : storeTransaction ,
} )
} )
}
func ( scopedStoreTransaction * ScopedStoreTransaction ) ensureReady ( operation string ) error {
if scopedStoreTransaction == nil {
return core . E ( operation , "scoped transaction is nil" , nil )
}
if scopedStoreTransaction . scopedStore == nil {
return core . E ( operation , "scoped transaction store is nil" , nil )
}
if scopedStoreTransaction . storeTransaction == nil {
return core . E ( operation , "scoped transaction database is nil" , nil )
}
if err := scopedStoreTransaction . scopedStore . storeInstance . ensureReady ( operation ) ; err != nil {
return err
}
return scopedStoreTransaction . storeTransaction . ensureReady ( operation )
}
// Usage example: `colourValue, err := scopedStoreTransaction.Get("colour")`
2026-04-04 16:37:56 +00:00
func ( scopedStoreTransaction * ScopedStoreTransaction ) Get ( key string ) ( string , error ) {
2026-04-04 16:09:14 +00:00
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.Get" ) ; err != nil {
return "" , err
}
2026-04-04 16:37:56 +00:00
return scopedStoreTransaction . storeTransaction . Get (
scopedStoreTransaction . scopedStore . namespacedGroup ( scopedStoreTransaction . scopedStore . defaultGroup ( ) ) ,
key ,
)
2026-04-04 16:09:14 +00:00
}
// Usage example: `colourValue, err := scopedStoreTransaction.GetFrom("config", "colour")`
func ( scopedStoreTransaction * ScopedStoreTransaction ) GetFrom ( group , key string ) ( string , error ) {
2026-04-04 16:37:56 +00:00
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.GetFrom" ) ; err != nil {
return "" , err
}
return scopedStoreTransaction . storeTransaction . Get ( scopedStoreTransaction . scopedStore . namespacedGroup ( group ) , key )
2026-04-04 16:09:14 +00:00
}
// Usage example: `if err := scopedStoreTransaction.Set("theme", "dark"); err != nil { return err }`
2026-04-04 16:37:56 +00:00
func ( scopedStoreTransaction * ScopedStoreTransaction ) Set ( key , value string ) error {
2026-04-04 16:09:14 +00:00
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.Set" ) ; err != nil {
return err
}
2026-04-04 16:37:56 +00:00
defaultGroup := scopedStoreTransaction . scopedStore . defaultGroup ( )
if err := scopedStoreTransaction . checkQuota ( "store.ScopedStoreTransaction.Set" , defaultGroup , key ) ; err != nil {
2026-04-04 16:09:14 +00:00
return err
}
2026-04-04 16:37:56 +00:00
return scopedStoreTransaction . storeTransaction . Set (
scopedStoreTransaction . scopedStore . namespacedGroup ( defaultGroup ) ,
key ,
value ,
)
2026-04-04 16:09:14 +00:00
}
// Usage example: `if err := scopedStoreTransaction.SetIn("config", "colour", "blue"); err != nil { return err }`
func ( scopedStoreTransaction * ScopedStoreTransaction ) SetIn ( group , key , value string ) error {
2026-04-04 16:37:56 +00:00
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.SetIn" ) ; err != nil {
return err
}
if err := scopedStoreTransaction . checkQuota ( "store.ScopedStoreTransaction.SetIn" , group , key ) ; err != nil {
return err
}
return scopedStoreTransaction . storeTransaction . Set ( scopedStoreTransaction . scopedStore . namespacedGroup ( group ) , key , value )
2026-04-04 16:09:14 +00:00
}
// Usage example: `if err := scopedStoreTransaction.SetWithTTL("sessions", "token", "abc123", time.Hour); err != nil { return err }`
func ( scopedStoreTransaction * ScopedStoreTransaction ) SetWithTTL ( group , key , value string , timeToLive time . Duration ) error {
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.SetWithTTL" ) ; err != nil {
return err
}
if err := scopedStoreTransaction . checkQuota ( "store.ScopedStoreTransaction.SetWithTTL" , group , key ) ; err != nil {
return err
}
return scopedStoreTransaction . storeTransaction . SetWithTTL ( scopedStoreTransaction . scopedStore . namespacedGroup ( group ) , key , value , timeToLive )
}
// Usage example: `if err := scopedStoreTransaction.Delete("config", "colour"); err != nil { return err }`
func ( scopedStoreTransaction * ScopedStoreTransaction ) Delete ( group , key string ) error {
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.Delete" ) ; err != nil {
return err
}
return scopedStoreTransaction . storeTransaction . Delete ( scopedStoreTransaction . scopedStore . namespacedGroup ( group ) , key )
}
// Usage example: `if err := scopedStoreTransaction.DeleteGroup("cache"); err != nil { return err }`
func ( scopedStoreTransaction * ScopedStoreTransaction ) DeleteGroup ( group string ) error {
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.DeleteGroup" ) ; err != nil {
return err
}
return scopedStoreTransaction . storeTransaction . DeleteGroup ( scopedStoreTransaction . scopedStore . namespacedGroup ( group ) )
}
// Usage example: `if err := scopedStoreTransaction.DeletePrefix("cache"); err != nil { return err }`
// Usage example: `if err := scopedStoreTransaction.DeletePrefix(""); err != nil { return err }`
func ( scopedStoreTransaction * ScopedStoreTransaction ) DeletePrefix ( groupPrefix string ) error {
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.DeletePrefix" ) ; err != nil {
return err
}
return scopedStoreTransaction . storeTransaction . DeletePrefix ( scopedStoreTransaction . scopedStore . namespacedGroup ( groupPrefix ) )
}
// Usage example: `colourEntries, err := scopedStoreTransaction.GetAll("config")`
func ( scopedStoreTransaction * ScopedStoreTransaction ) GetAll ( group string ) ( map [ string ] string , error ) {
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.GetAll" ) ; err != nil {
return nil , err
}
return scopedStoreTransaction . storeTransaction . GetAll ( scopedStoreTransaction . scopedStore . namespacedGroup ( group ) )
}
2026-04-04 17:53:21 +00:00
// Usage example: `page, err := scopedStoreTransaction.GetPage("config", 0, 25); if err != nil { return }; for _, entry := range page { fmt.Println(entry.Key, entry.Value) }`
func ( scopedStoreTransaction * ScopedStoreTransaction ) GetPage ( group string , offset , limit int ) ( [ ] KeyValue , error ) {
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.GetPage" ) ; err != nil {
return nil , err
}
return scopedStoreTransaction . storeTransaction . GetPage ( scopedStoreTransaction . scopedStore . namespacedGroup ( group ) , offset , limit )
}
2026-04-04 16:09:14 +00:00
// Usage example: `for entry, err := range scopedStoreTransaction.All("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }`
func ( scopedStoreTransaction * ScopedStoreTransaction ) All ( group string ) iter . Seq2 [ KeyValue , error ] {
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.All" ) ; err != nil {
return func ( yield func ( KeyValue , error ) bool ) {
yield ( KeyValue { } , err )
}
}
return scopedStoreTransaction . storeTransaction . All ( scopedStoreTransaction . scopedStore . namespacedGroup ( group ) )
}
// Usage example: `for entry, err := range scopedStoreTransaction.AllSeq("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }`
func ( scopedStoreTransaction * ScopedStoreTransaction ) AllSeq ( group string ) iter . Seq2 [ KeyValue , error ] {
return scopedStoreTransaction . All ( group )
}
// Usage example: `keyCount, err := scopedStoreTransaction.Count("config")`
func ( scopedStoreTransaction * ScopedStoreTransaction ) Count ( group string ) ( int , error ) {
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.Count" ) ; err != nil {
return 0 , err
}
return scopedStoreTransaction . storeTransaction . Count ( scopedStoreTransaction . scopedStore . namespacedGroup ( group ) )
}
// Usage example: `keyCount, err := scopedStoreTransaction.CountAll("config")`
// Usage example: `keyCount, err := scopedStoreTransaction.CountAll()`
func ( scopedStoreTransaction * ScopedStoreTransaction ) CountAll ( groupPrefix ... string ) ( int , error ) {
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.CountAll" ) ; err != nil {
return 0 , err
}
2026-04-04 18:42:19 +00:00
return scopedStoreTransaction . storeTransaction . CountAll ( scopedStoreTransaction . scopedStore . namespacedGroup ( firstOrEmptyString ( groupPrefix ) ) )
2026-04-04 16:09:14 +00:00
}
// Usage example: `groupNames, err := scopedStoreTransaction.Groups("config")`
// Usage example: `groupNames, err := scopedStoreTransaction.Groups()`
func ( scopedStoreTransaction * ScopedStoreTransaction ) Groups ( groupPrefix ... string ) ( [ ] string , error ) {
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.Groups" ) ; err != nil {
return nil , err
}
2026-04-04 18:42:19 +00:00
groupNames , err := scopedStoreTransaction . storeTransaction . Groups ( scopedStoreTransaction . scopedStore . namespacedGroup ( firstOrEmptyString ( groupPrefix ) ) )
2026-04-04 16:09:14 +00:00
if err != nil {
return nil , err
}
for i , groupName := range groupNames {
groupNames [ i ] = scopedStoreTransaction . scopedStore . trimNamespacePrefix ( groupName )
}
return groupNames , nil
}
// Usage example: `for groupName, err := range scopedStoreTransaction.GroupsSeq("config") { if err != nil { break }; fmt.Println(groupName) }`
// Usage example: `for groupName, err := range scopedStoreTransaction.GroupsSeq() { if err != nil { break }; fmt.Println(groupName) }`
func ( scopedStoreTransaction * ScopedStoreTransaction ) GroupsSeq ( groupPrefix ... string ) iter . Seq2 [ string , error ] {
return func ( yield func ( string , error ) bool ) {
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.GroupsSeq" ) ; err != nil {
yield ( "" , err )
return
}
namespacePrefix := scopedStoreTransaction . scopedStore . namespacePrefix ( )
2026-04-04 18:42:19 +00:00
for groupName , err := range scopedStoreTransaction . storeTransaction . GroupsSeq ( scopedStoreTransaction . scopedStore . namespacedGroup ( firstOrEmptyString ( groupPrefix ) ) ) {
2026-04-04 16:09:14 +00:00
if err != nil {
if ! yield ( "" , err ) {
return
}
continue
}
if ! yield ( core . TrimPrefix ( groupName , namespacePrefix ) , nil ) {
return
}
}
}
}
// Usage example: `renderedTemplate, err := scopedStoreTransaction.Render("Hello {{ .name }}", "user")`
func ( scopedStoreTransaction * ScopedStoreTransaction ) Render ( templateSource , group string ) ( string , error ) {
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.Render" ) ; err != nil {
return "" , err
}
return scopedStoreTransaction . storeTransaction . Render ( templateSource , scopedStoreTransaction . scopedStore . namespacedGroup ( group ) )
}
// Usage example: `parts, err := scopedStoreTransaction.GetSplit("config", "hosts", ","); if err != nil { return }; for part := range parts { fmt.Println(part) }`
func ( scopedStoreTransaction * ScopedStoreTransaction ) GetSplit ( group , key , separator string ) ( iter . Seq [ string ] , error ) {
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.GetSplit" ) ; err != nil {
return nil , err
}
return scopedStoreTransaction . storeTransaction . GetSplit ( scopedStoreTransaction . scopedStore . namespacedGroup ( group ) , key , separator )
}
// Usage example: `fields, err := scopedStoreTransaction.GetFields("config", "flags"); if err != nil { return }; for field := range fields { fmt.Println(field) }`
func ( scopedStoreTransaction * ScopedStoreTransaction ) GetFields ( group , key string ) ( iter . Seq [ string ] , error ) {
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.GetFields" ) ; err != nil {
return nil , err
}
return scopedStoreTransaction . storeTransaction . GetFields ( scopedStoreTransaction . scopedStore . namespacedGroup ( group ) , key )
}
2026-04-04 18:20:52 +00:00
// Usage example: `removedRows, err := scopedStoreTransaction.PurgeExpired(); if err != nil { return err }; fmt.Println(removedRows)`
func ( scopedStoreTransaction * ScopedStoreTransaction ) PurgeExpired ( ) ( int64 , error ) {
if err := scopedStoreTransaction . ensureReady ( "store.ScopedStoreTransaction.PurgeExpired" ) ; err != nil {
return 0 , err
}
removedRows , err := purgeExpiredMatchingGroupPrefix ( scopedStoreTransaction . storeTransaction . sqliteTransaction , scopedStoreTransaction . scopedStore . namespacePrefix ( ) )
if err != nil {
return 0 , core . E ( "store.ScopedStoreTransaction.PurgeExpired" , "delete expired rows" , err )
}
return removedRows , nil
}
2026-04-04 16:09:14 +00:00
func ( scopedStoreTransaction * ScopedStoreTransaction ) checkQuota ( operation , group , key string ) error {
if scopedStoreTransaction . scopedStore . MaxKeys == 0 && scopedStoreTransaction . scopedStore . MaxGroups == 0 {
return nil
}
namespacedGroup := scopedStoreTransaction . scopedStore . namespacedGroup ( group )
namespacePrefix := scopedStoreTransaction . scopedStore . namespacePrefix ( )
2026-04-04 19:12:24 +00:00
exists , err := liveEntryExists ( scopedStoreTransaction . storeTransaction . sqliteTransaction , namespacedGroup , key )
if err != nil {
2026-04-04 16:09:14 +00:00
return core . E ( operation , "quota check" , err )
}
2026-04-04 19:12:24 +00:00
if exists {
return nil
}
2026-04-04 16:09:14 +00:00
if scopedStoreTransaction . scopedStore . MaxKeys > 0 {
keyCount , err := scopedStoreTransaction . storeTransaction . CountAll ( namespacePrefix )
if err != nil {
return core . E ( operation , "quota check" , err )
}
if keyCount >= scopedStoreTransaction . scopedStore . MaxKeys {
return core . E ( operation , core . Sprintf ( "key limit (%d)" , scopedStoreTransaction . scopedStore . MaxKeys ) , QuotaExceededError )
}
}
if scopedStoreTransaction . scopedStore . MaxGroups > 0 {
existingGroupCount , err := scopedStoreTransaction . storeTransaction . Count ( namespacedGroup )
if err != nil {
return core . E ( operation , "quota check" , err )
}
if existingGroupCount == 0 {
knownGroupCount := 0
for _ , iterationErr := range scopedStoreTransaction . storeTransaction . GroupsSeq ( namespacePrefix ) {
if iterationErr != nil {
return core . E ( operation , "quota check" , iterationErr )
}
knownGroupCount ++
}
if knownGroupCount >= scopedStoreTransaction . scopedStore . MaxGroups {
return core . E ( operation , core . Sprintf ( "group limit (%d)" , scopedStoreTransaction . scopedStore . MaxGroups ) , QuotaExceededError )
}
}
}
return nil
}
2026-03-30 18:49:17 +00:00
// checkQuota("store.ScopedStore.Set", "config", "colour") returns nil when the
2026-03-30 17:37:50 +00:00
// namespace still has quota available and QuotaExceededError when a new key or
// group would exceed the configured limit. Existing keys are treated as
// upserts and do not consume quota.
2026-03-30 16:27:54 +00:00
func ( scopedStore * ScopedStore ) checkQuota ( operation , group , key string ) error {
2026-04-04 16:06:43 +00:00
if scopedStore . MaxKeys == 0 && scopedStore . MaxGroups == 0 {
2026-02-20 08:19:11 +00:00
return nil
}
2026-04-04 16:06:43 +00:00
namespacedGroup := scopedStore . namespacedGroup ( group )
namespacePrefix := scopedStore . namespacePrefix ( )
2026-04-04 19:12:24 +00:00
exists , err := liveEntryExists ( scopedStore . storeInstance . sqliteDatabase , namespacedGroup , key )
if err != nil {
2026-04-04 16:06:43 +00:00
// A database error occurred, not just a "not found" result.
2026-03-30 16:27:54 +00:00
return core . E ( operation , "quota check" , err )
2026-03-09 08:20:38 +00:00
}
2026-04-04 19:12:24 +00:00
if exists {
// Key exists — this is an upsert, no quota check needed.
return nil
}
2026-02-20 08:19:11 +00:00
2026-04-04 16:06:43 +00:00
// Check MaxKeys quota.
if scopedStore . MaxKeys > 0 {
keyCount , err := scopedStore . storeInstance . CountAll ( namespacePrefix )
2026-02-20 08:19:11 +00:00
if err != nil {
2026-03-30 16:27:54 +00:00
return core . E ( operation , "quota check" , err )
2026-02-20 08:19:11 +00:00
}
2026-04-04 16:06:43 +00:00
if keyCount >= scopedStore . MaxKeys {
return core . E ( operation , core . Sprintf ( "key limit (%d)" , scopedStore . MaxKeys ) , QuotaExceededError )
2026-02-20 08:19:11 +00:00
}
}
2026-04-04 16:06:43 +00:00
// Check MaxGroups quota — only if this would create a new group.
if scopedStore . MaxGroups > 0 {
existingGroupCount , err := scopedStore . storeInstance . Count ( namespacedGroup )
2026-02-20 08:19:11 +00:00
if err != nil {
2026-03-30 16:27:54 +00:00
return core . E ( operation , "quota check" , err )
2026-02-20 08:19:11 +00:00
}
2026-03-30 14:54:34 +00:00
if existingGroupCount == 0 {
2026-04-04 16:06:43 +00:00
// This group is new — check if adding it would exceed the group limit.
knownGroupCount := 0
for _ , iterationErr := range scopedStore . storeInstance . GroupsSeq ( namespacePrefix ) {
if iterationErr != nil {
return core . E ( operation , "quota check" , iterationErr )
}
knownGroupCount ++
2026-02-20 08:19:11 +00:00
}
2026-04-04 16:06:43 +00:00
if knownGroupCount >= scopedStore . MaxGroups {
return core . E ( operation , core . Sprintf ( "group limit (%d)" , scopedStore . MaxGroups ) , QuotaExceededError )
2026-02-20 08:19:11 +00:00
}
}
}
return nil
}
2026-04-04 19:12:24 +00:00
func liveEntryExists ( queryable keyExistenceQuery , group , key string ) ( bool , error ) {
var exists int
err := queryable . QueryRow (
"SELECT 1 FROM " + entriesTableName + " WHERE " + entryGroupColumn + " = ? AND " + entryKeyColumn + " = ? AND (expires_at IS NULL OR expires_at > ?) LIMIT 1" ,
group ,
key ,
time . Now ( ) . UnixMilli ( ) ,
) . Scan ( & exists )
if err == nil {
return true , nil
}
if err == sql . ErrNoRows {
return false , nil
}
return false , err
}
type keyExistenceQuery interface {
QueryRow ( query string , args ... any ) * sql . Row
}