diff --git a/docs/architecture.md b/docs/architecture.md index 6e5a088..b8f33de 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -215,7 +215,7 @@ Namespace strings must match `^[a-zA-Z0-9-]+$`. Invalid namespaces are rejected ### Quota Enforcement -`NewScopedWithQuota(store, namespace, QuotaConfig)` adds per-namespace limits. For example, `store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}` caps a namespace at 100 keys and 10 groups: +`NewScopedConfigured(store.ScopedStoreConfig{...})` is the preferred way to set per-namespace limits because the quota values stay visible at the call site. For example, `store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}` caps a namespace at 100 keys and 10 groups: ```go type QuotaConfig struct { diff --git a/docs/index.md b/docs/index.md index 74e3ed2..8fe0fda 100644 --- a/docs/index.md +++ b/docs/index.md @@ -126,7 +126,7 @@ The entire package lives in a single Go package (`package store`) with the follo | `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, transaction support | | `transaction.go` | `Store.Transaction`, transaction-scoped write helpers, staged event dispatch | | `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 | +| `scope.go` | `ScopedStore` wrapper for namespace isolation, `QuotaConfig` struct, `NewScoped`/`NewScopedConfigured` constructors, namespace-local helper delegation, quota enforcement logic | | `journal.go` | Journal persistence, Flux-like querying, JSON row inflation, journal schema helpers | | `workspace.go` | Workspace buffers, aggregation, query analysis, commit flow, and orphan recovery | | `compact.go` | Cold archive generation to JSONL gzip or zstd | diff --git a/scope_test.go b/scope_test.go index e732b0c..9c52c83 100644 --- a/scope_test.go +++ b/scope_test.go @@ -61,44 +61,59 @@ func TestScope_NewScoped_Bad_InvalidChars(t *testing.T) { } } -func TestScope_NewScopedWithQuota_Bad_InvalidNamespace(t *testing.T) { +func TestScope_NewScopedConfigured_Bad_InvalidNamespaceFromQuotaConfig(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - _, err := NewScopedWithQuota(storeInstance, "tenant_a", QuotaConfig{MaxKeys: 1}) + _, err := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant_a", + Quota: QuotaConfig{MaxKeys: 1}, + }) require.Error(t, err) assert.Contains(t, err.Error(), "store.NewScoped") } -func TestScope_NewScopedWithQuota_Bad_NilStore(t *testing.T) { - _, err := NewScopedWithQuota(nil, "tenant-a", QuotaConfig{MaxKeys: 1}) +func TestScope_NewScopedConfigured_Bad_NilStoreFromQuotaConfig(t *testing.T) { + _, err := NewScopedConfigured(nil, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: 1}, + }) require.Error(t, err) assert.Contains(t, err.Error(), "store instance is nil") } -func TestScope_NewScopedWithQuota_Bad_NegativeMaxKeys(t *testing.T) { +func TestScope_NewScopedConfigured_Bad_NegativeMaxKeys(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - _, err := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxKeys: -1}) + _, err := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: -1}, + }) require.Error(t, err) assert.Contains(t, err.Error(), "zero or positive") } -func TestScope_NewScopedWithQuota_Bad_NegativeMaxGroups(t *testing.T) { +func TestScope_NewScopedConfigured_Bad_NegativeMaxGroups(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - _, err := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxGroups: -1}) + _, err := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxGroups: -1}, + }) require.Error(t, err) assert.Contains(t, err.Error(), "zero or positive") } -func TestScope_NewScopedWithQuota_Good_InlineQuotaFields(t *testing.T) { +func TestScope_NewScopedConfigured_Good_InlineQuotaFields(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - scopedStore, err := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxKeys: 4, MaxGroups: 2}) + scopedStore, err := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: 4, MaxGroups: 2}, + }) require.NoError(t, err) assert.Equal(t, 4, scopedStore.MaxKeys) @@ -570,7 +585,10 @@ func TestScope_Quota_Good_MaxKeys(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - scopedStore, err := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxKeys: 5}) + scopedStore, err := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: 5}, + }) require.NoError(t, err) // Insert 5 keys across different groups — should be fine. @@ -593,7 +611,10 @@ func TestScope_Quota_Bad_QuotaCheckQueryError(t *testing.T) { cancelPurge: func() {}, } - scopedStore, err := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxKeys: 1}) + scopedStore, err := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: 1}, + }) require.NoError(t, err) err = scopedStore.SetIn("config", "theme", "dark") @@ -605,7 +626,10 @@ func TestScope_Quota_Good_MaxKeys_AcrossGroups(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - scopedStore, _ := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxKeys: 3}) + scopedStore, _ := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: 3}, + }) require.NoError(t, scopedStore.SetIn("g1", "a", "1")) require.NoError(t, scopedStore.SetIn("g2", "b", "2")) @@ -620,7 +644,10 @@ func TestScope_Quota_Good_UpsertDoesNotCount(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - scopedStore, _ := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxKeys: 3}) + scopedStore, _ := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: 3}, + }) require.NoError(t, scopedStore.SetIn("g", "a", "1")) require.NoError(t, scopedStore.SetIn("g", "b", "2")) @@ -638,7 +665,10 @@ func TestScope_Quota_Good_ExpiredUpsertDoesNotEmitDeleteEvent(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - scopedStore, _ := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxKeys: 1}) + scopedStore, _ := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: 1}, + }) events := storeInstance.Watch("tenant-a:g") defer storeInstance.Unwatch("tenant-a:g", events) @@ -674,7 +704,10 @@ func TestScope_Quota_Good_DeleteAndReInsert(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - scopedStore, _ := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxKeys: 3}) + scopedStore, _ := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: 3}, + }) require.NoError(t, scopedStore.SetIn("g", "a", "1")) require.NoError(t, scopedStore.SetIn("g", "b", "2")) @@ -689,7 +722,10 @@ func TestScope_Quota_Good_ZeroMeansUnlimited(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - scopedStore, _ := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxKeys: 0, MaxGroups: 0}) + scopedStore, _ := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: 0, MaxGroups: 0}, + }) // Should be able to insert many keys and groups without error. for i := range 100 { @@ -701,7 +737,10 @@ func TestScope_Quota_Good_ExpiredKeysExcluded(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - scopedStore, _ := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxKeys: 3}) + scopedStore, _ := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: 3}, + }) // Insert 3 keys, 2 with short TTL. require.NoError(t, scopedStore.SetWithTTL("g", "temp1", "v", 1*time.Millisecond)) @@ -723,7 +762,10 @@ func TestScope_Quota_Good_SetWithTTL_Enforced(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - scopedStore, _ := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxKeys: 2}) + scopedStore, _ := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: 2}, + }) require.NoError(t, scopedStore.SetWithTTL("g", "a", "1", time.Hour)) require.NoError(t, scopedStore.SetWithTTL("g", "b", "2", time.Hour)) @@ -740,7 +782,10 @@ func TestScope_Quota_Good_MaxGroups(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - scopedStore, _ := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxGroups: 3}) + scopedStore, _ := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxGroups: 3}, + }) require.NoError(t, scopedStore.SetIn("g1", "k", "v")) require.NoError(t, scopedStore.SetIn("g2", "k", "v")) @@ -756,7 +801,10 @@ func TestScope_Quota_Good_MaxGroups_ExistingGroupOK(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - scopedStore, _ := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxGroups: 2}) + scopedStore, _ := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxGroups: 2}, + }) require.NoError(t, scopedStore.SetIn("g1", "a", "1")) require.NoError(t, scopedStore.SetIn("g2", "b", "2")) @@ -770,7 +818,10 @@ func TestScope_Quota_Good_MaxGroups_DeleteAndRecreate(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - scopedStore, _ := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxGroups: 2}) + scopedStore, _ := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxGroups: 2}, + }) require.NoError(t, scopedStore.SetIn("g1", "k", "v")) require.NoError(t, scopedStore.SetIn("g2", "k", "v")) @@ -784,7 +835,10 @@ func TestScope_Quota_Good_MaxGroups_ZeroUnlimited(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - scopedStore, _ := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxGroups: 0}) + scopedStore, _ := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxGroups: 0}, + }) for i := range 50 { require.NoError(t, scopedStore.SetIn(keyName(i), "k", "v")) @@ -795,7 +849,10 @@ func TestScope_Quota_Good_MaxGroups_ExpiredGroupExcluded(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - scopedStore, _ := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxGroups: 2}) + scopedStore, _ := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxGroups: 2}, + }) // Create 2 groups, one with only TTL keys. require.NoError(t, scopedStore.SetWithTTL("g1", "k", "v", 1*time.Millisecond)) @@ -811,7 +868,10 @@ func TestScope_Quota_Good_BothLimits(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - scopedStore, _ := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxKeys: 10, MaxGroups: 2}) + scopedStore, _ := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: 10, MaxGroups: 2}, + }) require.NoError(t, scopedStore.SetIn("g1", "a", "1")) require.NoError(t, scopedStore.SetIn("g2", "b", "2")) @@ -828,8 +888,14 @@ func TestScope_Quota_Good_DoesNotAffectOtherNamespaces(t *testing.T) { storeInstance, _ := New(":memory:") defer storeInstance.Close() - alphaStore, _ := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxKeys: 2}) - betaStore, _ := NewScopedWithQuota(storeInstance, "tenant-b", QuotaConfig{MaxKeys: 2}) + alphaStore, _ := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: 2}, + }) + betaStore, _ := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-b", + Quota: QuotaConfig{MaxKeys: 2}, + }) require.NoError(t, alphaStore.SetIn("g", "a1", "v")) require.NoError(t, alphaStore.SetIn("g", "a2", "v")) diff --git a/transaction_test.go b/transaction_test.go index e3d2ae9..795be13 100644 --- a/transaction_test.go +++ b/transaction_test.go @@ -177,7 +177,10 @@ func TestTransaction_ScopedStoreTransaction_Good_CommitsNamespacedWrites(t *test storeInstance, _ := New(":memory:") defer storeInstance.Close() - scopedStore, err := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxKeys: 4, MaxGroups: 2}) + scopedStore, err := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: 4, MaxGroups: 2}, + }) require.NoError(t, err) err = scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { @@ -239,7 +242,10 @@ func TestTransaction_ScopedStoreTransaction_Good_QuotaUsesPendingWrites(t *testi storeInstance, _ := New(":memory:") defer storeInstance.Close() - scopedStore, err := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxKeys: 2, MaxGroups: 2}) + scopedStore, err := NewScopedConfigured(storeInstance, ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: 2, MaxGroups: 2}, + }) require.NoError(t, err) err = scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error {