Merge pull request '[agent/codex:gpt-5.4-mini] Read ~/spec/code/core/go/store/RFC.md fully. Find features d...' (#82) from agent/read---spec-code-core-go-store-rfc-md-fu into dev
This commit is contained in:
commit
95a27f97d5
6 changed files with 89 additions and 2 deletions
|
|
@ -214,7 +214,6 @@ These tests exercise correct defensive code. They must continue to pass but are
|
|||
|
||||
These are design notes, not committed work:
|
||||
|
||||
- **Pagination for `GetAll`.** A `GetPage(group string, offset, limit int)` method would support large groups without full in-memory materialisation.
|
||||
- **Indexed prefix keys.** An additional index on `(group_name, entry_key)` prefix would accelerate prefix scans without a full-table scan.
|
||||
- **TTL background purge interval as constructor configuration.** Currently only settable by mutating `storeInstance.purgeInterval` directly in tests. A constructor-level `PurgeInterval` field or config entry would make this part of the public API.
|
||||
- **Cross-group atomic operations.** Exposing a `Transaction(func(tx *StoreTx) error)` API would allow callers to compose atomic multi-group operations.
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ The entire package lives in a single Go package (`package store`) with the follo
|
|||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `doc.go` | Package comment with concrete usage examples |
|
||||
| `store.go` | Core `Store` type, CRUD operations (`Get`, `Set`, `SetWithTTL`, `Delete`, `DeleteGroup`, `DeletePrefix`), bulk queries (`GetAll`, `All`, `Count`, `CountAll`, `Groups`, `GroupsSeq`), string splitting helpers (`GetSplit`, `GetFields`), template rendering (`Render`), TTL expiry, background purge goroutine |
|
||||
| `store.go` | Core `Store` type, CRUD operations (`Get`, `Set`, `SetWithTTL`, `Delete`, `DeleteGroup`, `DeletePrefix`), bulk queries (`GetAll`, `GetPage`, `All`, `Count`, `CountAll`, `Groups`, `GroupsSeq`), string splitting helpers (`GetSplit`, `GetFields`), template rendering (`Render`), TTL expiry, background purge goroutine |
|
||||
| `events.go` | `EventType` constants, `Event` struct, `Watch`/`Unwatch` channel subscriptions, `OnChange` callback registration, internal `notify` dispatch |
|
||||
| `scope.go` | `ScopedStore` wrapper for namespace isolation, `QuotaConfig` struct, `NewScoped`/`NewScopedWithQuota` constructors, namespace-local helper delegation, quota enforcement logic |
|
||||
| `journal.go` | Journal persistence, Flux-like querying, JSON row inflation, journal schema helpers |
|
||||
|
|
|
|||
9
scope.go
9
scope.go
|
|
@ -190,6 +190,15 @@ func (scopedStore *ScopedStore) GetAll(group string) (map[string]string, error)
|
|||
return storeInstance.GetAll(scopedStore.namespacedGroup(group))
|
||||
}
|
||||
|
||||
// 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) {
|
||||
storeInstance, err := scopedStore.storeInstance("store.GetPage")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return storeInstance.GetPage(scopedStore.namespacedGroup(group), offset, limit)
|
||||
}
|
||||
|
||||
// Usage example: `for entry, err := range scopedStore.All("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }`
|
||||
func (scopedStore *ScopedStore) All(group string) iter.Seq2[KeyValue, error] {
|
||||
storeInstance, err := scopedStore.storeInstance("store.All")
|
||||
|
|
|
|||
|
|
@ -163,6 +163,21 @@ func TestScope_ScopedStore_Good_AllSeq(t *testing.T) {
|
|||
assert.ElementsMatch(t, []string{"first", "second"}, keys)
|
||||
}
|
||||
|
||||
func TestScope_ScopedStore_Good_GetPage(t *testing.T) {
|
||||
storeInstance, _ := New(":memory:")
|
||||
defer storeInstance.Close()
|
||||
|
||||
scopedStore := mustScoped(t, storeInstance, "tenant-a")
|
||||
require.NoError(t, scopedStore.SetIn("items", "charlie", "3"))
|
||||
require.NoError(t, scopedStore.SetIn("items", "alpha", "1"))
|
||||
require.NoError(t, scopedStore.SetIn("items", "bravo", "2"))
|
||||
|
||||
page, err := scopedStore.GetPage("items", 0, 2)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, page, 2)
|
||||
assert.Equal(t, []KeyValue{{Key: "alpha", Value: "1"}, {Key: "bravo", Value: "2"}}, page)
|
||||
}
|
||||
|
||||
func TestScope_ScopedStore_Good_PrefixedInUnderlyingStore(t *testing.T) {
|
||||
storeInstance, _ := New(":memory:")
|
||||
defer storeInstance.Close()
|
||||
|
|
|
|||
35
store.go
35
store.go
|
|
@ -434,6 +434,41 @@ func (storeInstance *Store) GetAll(group string) (map[string]string, error) {
|
|||
return entriesByKey, nil
|
||||
}
|
||||
|
||||
// Usage example: `page, err := storeInstance.GetPage("config", 0, 25); if err != nil { return }; for _, entry := range page { fmt.Println(entry.Key, entry.Value) }`
|
||||
func (storeInstance *Store) GetPage(group string, offset, limit int) ([]KeyValue, error) {
|
||||
if err := storeInstance.ensureReady("store.GetPage"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if offset < 0 {
|
||||
return nil, core.E("store.GetPage", "offset must be zero or positive", nil)
|
||||
}
|
||||
if limit < 0 {
|
||||
return nil, core.E("store.GetPage", "limit must be zero or positive", nil)
|
||||
}
|
||||
|
||||
rows, err := storeInstance.database.Query(
|
||||
"SELECT "+entryKeyColumn+", "+entryValueColumn+" FROM "+entriesTableName+" WHERE "+entryGroupColumn+" = ? AND (expires_at IS NULL OR expires_at > ?) ORDER BY "+entryKeyColumn+" LIMIT ? OFFSET ?",
|
||||
group, time.Now().UnixMilli(), limit, offset,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, core.E("store.GetPage", "query rows", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
page := make([]KeyValue, 0, limit)
|
||||
for rows.Next() {
|
||||
var entry KeyValue
|
||||
if err := rows.Scan(&entry.Key, &entry.Value); err != nil {
|
||||
return nil, core.E("store.GetPage", "scan row", err)
|
||||
}
|
||||
page = append(page, entry)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, core.E("store.GetPage", "rows iteration", err)
|
||||
}
|
||||
return page, nil
|
||||
}
|
||||
|
||||
// Usage example: `for entry, err := range storeInstance.AllSeq("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }`
|
||||
func (storeInstance *Store) AllSeq(group string) iter.Seq2[KeyValue, error] {
|
||||
return func(yield func(KeyValue, error) bool) {
|
||||
|
|
|
|||
|
|
@ -394,6 +394,35 @@ func TestStore_GetAll_Good_Empty(t *testing.T) {
|
|||
assert.Empty(t, all)
|
||||
}
|
||||
|
||||
func TestStore_GetPage_Good(t *testing.T) {
|
||||
storeInstance, _ := New(":memory:")
|
||||
defer storeInstance.Close()
|
||||
|
||||
require.NoError(t, storeInstance.Set("grp", "charlie", "3"))
|
||||
require.NoError(t, storeInstance.Set("grp", "alpha", "1"))
|
||||
require.NoError(t, storeInstance.Set("grp", "bravo", "2"))
|
||||
|
||||
page, err := storeInstance.GetPage("grp", 1, 2)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, page, 2)
|
||||
assert.Equal(t, []KeyValue{{Key: "bravo", Value: "2"}, {Key: "charlie", Value: "3"}}, page)
|
||||
}
|
||||
|
||||
func TestStore_GetPage_Good_EmptyAndBounds(t *testing.T) {
|
||||
storeInstance, _ := New(":memory:")
|
||||
defer storeInstance.Close()
|
||||
|
||||
page, err := storeInstance.GetPage("grp", 0, 0)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, page)
|
||||
|
||||
_, err = storeInstance.GetPage("grp", -1, 1)
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = storeInstance.GetPage("grp", 0, -1)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestStore_GetAll_Bad_ClosedStore(t *testing.T) {
|
||||
storeInstance, _ := New(":memory:")
|
||||
storeInstance.Close()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue