diff --git a/.golangci.yml b/.golangci.yml index 774475b..4d4d877 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,6 +4,7 @@ run: linters: enable: + - depguard - govet - errcheck - staticcheck @@ -17,6 +18,17 @@ linters: - exhaustive - wrapcheck +linters-settings: + depguard: + rules: + legacy-module-paths: + list-mode: lax + files: + - $all + deny: + - pkg: forge.lthn.ai/ + desc: use dappco.re/ module paths instead + issues: exclude-use-default: false max-same-issues: 0 diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..b9b9094 --- /dev/null +++ b/doc.go @@ -0,0 +1,7 @@ +// Package store provides a SQLite-backed key-value store with group namespaces, +// TTL expiry, quota-enforced scoped views, and reactive change notifications. +// +// Use New to open a store, then Set/Get for CRUD operations. Use +// NewScoped/NewScopedWithQuota when group names need tenant isolation or +// per-namespace quotas. +package store diff --git a/events_test.go b/events_test.go index f93d44b..995e83a 100644 --- a/events_test.go +++ b/events_test.go @@ -375,7 +375,7 @@ func TestWatch_Good_ScopedStoreEvents(t *testing.T) { // EventType.String() // --------------------------------------------------------------------------- -func TestEventType_String(t *testing.T) { +func TestEventType_Good_String(t *testing.T) { assert.Equal(t, "set", EventSet.String()) assert.Equal(t, "delete", EventDelete.String()) assert.Equal(t, "delete_group", EventDeleteGroup.String()) diff --git a/go.sum b/go.sum index 1cf12e5..0dc6285 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc= +dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/store.go b/store.go index c1f977f..576b66b 100644 --- a/store.go +++ b/store.go @@ -14,9 +14,11 @@ import ( ) // ErrNotFound is returned when a key does not exist in the store. +// Use errors.Is(err, ErrNotFound) to test for it. var ErrNotFound = coreerr.E("store", "not found", nil) // ErrQuotaExceeded is returned when a namespace quota limit is reached. +// Use errors.Is(err, ErrQuotaExceeded) to test for it. var ErrQuotaExceeded = coreerr.E("store", "quota exceeded", nil) // Store is a group-namespaced key-value store backed by SQLite. @@ -343,6 +345,7 @@ func (s *Store) GroupsSeq(prefix string) iter.Seq2[string, error] { } } +// escapeLike escapes SQLite LIKE wildcards and the escape character itself. func escapeLike(s string) string { s = strings.ReplaceAll(s, "^", "^^") s = strings.ReplaceAll(s, "%", "^%") diff --git a/store_test.go b/store_test.go index 4bfbb71..c0452e0 100644 --- a/store_test.go +++ b/store_test.go @@ -445,7 +445,7 @@ func TestClose_Good_OperationsFailAfterClose(t *testing.T) { // Edge cases // --------------------------------------------------------------------------- -func TestEdgeCases(t *testing.T) { +func TestSetGet_Good_EdgeCases(t *testing.T) { s, _ := New(":memory:") defer s.Close() @@ -491,7 +491,7 @@ func TestEdgeCases(t *testing.T) { // Group isolation // --------------------------------------------------------------------------- -func TestGroupIsolation(t *testing.T) { +func TestStore_Good_GroupIsolation(t *testing.T) { s, _ := New(":memory:") defer s.Close() @@ -520,7 +520,7 @@ func TestGroupIsolation(t *testing.T) { // Concurrent access // --------------------------------------------------------------------------- -func TestConcurrent_ReadWrite(t *testing.T) { +func TestConcurrent_Good_ReadWrite(t *testing.T) { dbPath := filepath.Join(t.TempDir(), "concurrent.db") s, err := New(dbPath) require.NoError(t, err) @@ -581,7 +581,7 @@ func TestConcurrent_ReadWrite(t *testing.T) { } } -func TestConcurrent_GetAll(t *testing.T) { +func TestConcurrent_Good_GetAll(t *testing.T) { s, err := New(filepath.Join(t.TempDir(), "getall.db")) require.NoError(t, err) defer s.Close() @@ -607,7 +607,7 @@ func TestConcurrent_GetAll(t *testing.T) { wg.Wait() } -func TestConcurrent_DeleteGroup(t *testing.T) { +func TestConcurrent_Good_DeleteGroup(t *testing.T) { s, err := New(filepath.Join(t.TempDir(), "delgrp.db")) require.NoError(t, err) defer s.Close() @@ -631,7 +631,7 @@ func TestConcurrent_DeleteGroup(t *testing.T) { // ErrNotFound wrapping verification // --------------------------------------------------------------------------- -func TestErrNotFound_Is(t *testing.T) { +func TestErrNotFound_Good_Is(t *testing.T) { s, _ := New(":memory:") defer s.Close() @@ -900,7 +900,7 @@ func TestPurgeExpired_Good_BackgroundPurge(t *testing.T) { // Schema migration — reopening an existing database // --------------------------------------------------------------------------- -func TestSchemaUpgrade_ExistingDB(t *testing.T) { +func TestSchemaUpgrade_Good_ExistingDB(t *testing.T) { dbPath := filepath.Join(t.TempDir(), "upgrade.db") // Open, write, close. @@ -925,7 +925,7 @@ func TestSchemaUpgrade_ExistingDB(t *testing.T) { assert.Equal(t, "ttl-val", val2) } -func TestSchemaUpgrade_PreTTLDatabase(t *testing.T) { +func TestSchemaUpgrade_Good_PreTTLDatabase(t *testing.T) { // Simulate a database created before TTL support (no expires_at column). dbPath := filepath.Join(t.TempDir(), "pre-ttl.db") db, err := sql.Open("sqlite", dbPath) @@ -965,7 +965,7 @@ func TestSchemaUpgrade_PreTTLDatabase(t *testing.T) { // Concurrent TTL access // --------------------------------------------------------------------------- -func TestConcurrent_TTL(t *testing.T) { +func TestConcurrent_Good_TTL(t *testing.T) { s, err := New(filepath.Join(t.TempDir(), "concurrent-ttl.db")) require.NoError(t, err) defer s.Close()