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
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

This commit is contained in:
Virgil 2026-04-03 08:32:50 +00:00
commit 95a27f97d5
6 changed files with 89 additions and 2 deletions

View file

@ -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.

View file

@ -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 |

View file

@ -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")

View file

@ -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()

View file

@ -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) {

View file

@ -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()