From 70a71a29a47b91eeb6a32217c65420e92bcb9605 Mon Sep 17 00:00:00 2001 From: Adam Crossland Date: Thu, 19 Feb 2026 12:47:12 -0500 Subject: [PATCH 1/4] [add_named_kv] implement named kv stores This change adds the ability to have kv stores that are not scoped to a function, allowing data to be shared between multiple functions. To make this possible, this change adds two new functions to the kv interface: openNamed and closeNamed. The openNamed function takes a string parameter that specifies a shared kv store to use for all futher kv.get, kv.set and kv.delete calls until the kv.closeNamed function is called, which returns to using to the default, function-scoped kv store. For example, let's say that I want to have an HTTP handler function that increments a counter each time that it is called: function handler(ctx, event) kv.openNamed("shared") local count = kv.get("visitCounter") or 0 count = count + 1 kv.set("visitCounter", count) return { statusCode = 200, headers = { ["Content-Type"] = "application/json" }, body = json.encode({ message = "Visit count = " .. count }) } end I also want to have a function that runs every minute to email a report of the number of times that the first function has been called: function handler(ctx, event) -- Get data to report kv.openNamed("shared") local count = kv.get("visitCounter") or 0 kv.closeNamed() -- Send email via Resend local result, err = email.send({ from = "reports@fakeurl.dev", to = "reportreceivingperson@gmail.com", subject = "Current visit counter value", html = "

Current visit counter value is " .. count .. "

", }) if err then log.error("Email error: " .. err) return { statusCode = 500, headers = { ["Content-Type"] = "application/json" }, body = json.encode({ error = err }) } end log.info("Email sent: " .. result.id) return { statusCode = 200, headers = { ["Content-Type"] = "application/json" }, body = json.encode({ success = true, email_id = result.id }) } end By using kv.openNamed("shared") and kv.closeNamed(), both functions can access the same kv store. --- frontend/js/components/api-reference.js | 10 +++ frontend/js/components/code-editor.js | 10 +++ frontend/js/i18n/locales/en.js | 2 + frontend/llms.txt | 9 +++ internal/runner/lua_kv.go | 19 ++++++ internal/runner/runner_test.go | 28 +++++++- internal/services/kv/kv.go | 89 ++++++++++++++++++++++--- internal/services/kv/kv_test.go | 37 ++++++++++ 8 files changed, 193 insertions(+), 11 deletions(-) diff --git a/frontend/js/components/api-reference.js b/frontend/js/components/api-reference.js index 773ea6b..dffc2ca 100644 --- a/frontend/js/components/api-reference.js +++ b/frontend/js/components/api-reference.js @@ -341,6 +341,16 @@ export function getLuaAPISections() { name: "kv.delete(key)", type: "function", description: t("luaApi.io.items.kvDelete"), + }, + { + name: "kv.openNamed(name)", + type: "function", + description: t("luaApi.io.items.kvOpenNamed"), + }, + { + name: "kv.closeNamed()", + type: "function", + description: t("luaApi.io.items.kvCloseNamed"), }, ], }, diff --git a/frontend/js/components/code-editor.js b/frontend/js/components/code-editor.js index 0f95072..03093fd 100644 --- a/frontend/js/components/code-editor.js +++ b/frontend/js/components/code-editor.js @@ -117,6 +117,16 @@ const API_DOCS = { snippet: 'kv.delete("${1:key}")', description: "Delete a key from the store", }, + "kv.openNamed": { + signature: "kv.openNamed(name: string)", + snippet: 'kv.openNamed("${1:name}")', + description: "Open a named key-value store", + }, + "kv.closeNamed": { + signature: "kv.closeNamed()", + snippet: 'kv.closeNamed()', + description: "Close a named key-value store", + }, "env.get": { signature: "env.get(key: string): string | nil", snippet: 'env.get("${1:key}")', diff --git a/frontend/js/i18n/locales/en.js b/frontend/js/i18n/locales/en.js index 69816b2..678dd0c 100644 --- a/frontend/js/i18n/locales/en.js +++ b/frontend/js/i18n/locales/en.js @@ -506,6 +506,8 @@ export default { kvGet: "Get value from store", kvSet: "Set key-value pair", kvDelete: "Delete key from store", + kvOpenNamed: "Open named store", + kvCloseNamed: "Close named store", envGet: "Get environment variable", httpGet: "GET request", httpPost: "POST request", diff --git a/frontend/llms.txt b/frontend/llms.txt index ac5c373..7b01398 100644 --- a/frontend/llms.txt +++ b/frontend/llms.txt @@ -74,11 +74,20 @@ Persistent storage scoped to function ID: - kv.get(key: string): string | nil - Retrieve value, returns nil if not found - kv.set(key: string, value: string): boolean - Set key-value pair, returns success - kv.delete(key: string): boolean - Delete key, returns success +- kv.openNamed(storeName: string): error - Switch to a named store +- kv.closeNamed() - Switch from named store to default, function-scoped store Example: ```lua local count = kv.get("counter") or "0" kv.set("counter", tostring(tonumber(count) + 1)) + +kv.openNamed("shared") +kv.set("counter", "42") +local sharedCount = kv.get("counter") or "0" -- should be 42 +kv.closeNamed() + +local originalCount = kv.get("counter") or "0" -- should be 1 ``` ### Environment Variables (env) diff --git a/internal/runner/lua_kv.go b/internal/runner/lua_kv.go index 34af00f..ad554a1 100644 --- a/internal/runner/lua_kv.go +++ b/internal/runner/lua_kv.go @@ -46,5 +46,24 @@ func registerKV(L *lua.LState, kvStore kv.Store, functionID string) { return 1 })) + // kv.openNamed(storeName) + L.SetField(kvTable, "openNamed", L.NewFunction(func(L *lua.LState) int { + storeName := L.CheckString(1) + err := kvStore.OpenNamed(storeName) + if err != nil { + L.Push(lua.LFalse) + return 1 + } + L.Push(lua.LTrue) + return 1 + })) + + // kv.closeNamed() + L.SetField(kvTable, "closeNamed", L.NewFunction(func(L *lua.LState) int { + kvStore.CloseNamed() + L.Push(lua.LTrue) + return 1 + })) + L.SetGlobal("kv", kvTable) } diff --git a/internal/runner/runner_test.go b/internal/runner/runner_test.go index d50029b..2e61bfa 100644 --- a/internal/runner/runner_test.go +++ b/internal/runner/runner_test.go @@ -7,8 +7,8 @@ import ( "testing" "time" - "github.com/dimiro1/lunar/internal/services/env" "github.com/dimiro1/lunar/internal/events" + "github.com/dimiro1/lunar/internal/services/env" internalhttp "github.com/dimiro1/lunar/internal/services/http" "github.com/dimiro1/lunar/internal/services/kv" "github.com/dimiro1/lunar/internal/services/logger" @@ -285,6 +285,32 @@ end if val != "value1" { t.Errorf("expected value 'value1', got %q", val) } + + // Test with named store + luaCodeNamed := ` +function handler(ctx, event) + kv.set("key1", "value1") -- This should go to the default store + kv.openNamed("mystore") + kv.set("key1", "value2") + local val = kv.get("key1") + kv.closeNamed() + local orginalVal = kv.get("key1") -- Should retrieve from default store + + return { + statusCode = 200, + body = "Retrieved: " .. val .. ", " .. orginalVal + } +end +` + + resp, err = Run(context.Background(), deps, Request{Context: execCtx, Event: event, Code: luaCodeNamed}) + if err != nil { + t.Fatalf("Run failed: %v", err) + } + + if resp.HTTP.Body != "Retrieved: value2, value1" { + t.Errorf("expected body 'Retrieved: value2', got %q", resp.HTTP.Body) + } } func TestRun_Env(t *testing.T) { diff --git a/internal/services/kv/kv.go b/internal/services/kv/kv.go index 716ba93..14012e8 100644 --- a/internal/services/kv/kv.go +++ b/internal/services/kv/kv.go @@ -21,11 +21,14 @@ type Store interface { Get(functionID, key string) (string, error) Set(functionID, key, value string) error Delete(functionID, key string) error + OpenNamed(storeName string) error + CloseNamed() } // MemoryStore is an in-memory implementation of Store type MemoryStore struct { - data map[string]map[string]string // functionID -> key -> value + data map[string]map[string]string // functionID -> key -> value + StoreName string } // NewMemoryStore creates a new in-memory KV store @@ -35,9 +38,19 @@ func NewMemoryStore() *MemoryStore { } } +// Open opens a new in-memory KV store (for interface compatibility) +func (m *MemoryStore) Open(storeName string) (Store, error) { + return NewMemoryStore(), nil +} + // Get retrieves a value by functionID and key func (m *MemoryStore) Get(functionID, key string) (string, error) { - ns, exists := m.data[functionID] + storeName := functionID + if m.StoreName != "" { + storeName = m.StoreName + } + + ns, exists := m.data[storeName] if !exists { return "", &Error{Message: fmt.Sprintf("key not found: %s", key)} } @@ -51,24 +64,50 @@ func (m *MemoryStore) Get(functionID, key string) (string, error) { // Set stores a key-value pair for a functionID func (m *MemoryStore) Set(functionID, key, value string) error { - if _, exists := m.data[functionID]; !exists { - m.data[functionID] = make(map[string]string) + storeName := functionID + if m.StoreName != "" { + storeName = m.StoreName } - m.data[functionID][key] = value + + if _, exists := m.data[storeName]; !exists { + m.data[storeName] = make(map[string]string) + } + m.data[storeName][key] = value return nil } // Delete removes a key-value pair for a functionID func (m *MemoryStore) Delete(functionID, key string) error { - if ns, exists := m.data[functionID]; exists { + storeName := functionID + if m.StoreName != "" { + storeName = m.StoreName + } + + if ns, exists := m.data[storeName]; exists { delete(ns, key) } return nil } +// OpenNamed opens a new in-memory KV store with a given name (for interface compatibility) +func (m *MemoryStore) OpenNamed(storeName string) error { + if storeName == "" { + return &Error{Message: "storeName cannot be empty"} + } + + m.StoreName = storeName + return nil +} + +// CloseNamed clears the StoreName for the MemoryStore instance (for interface compatibility) +func (m *MemoryStore) CloseNamed() { + m.StoreName = "" +} + // SQLiteStore is a SQLite-backed implementation of Store type SQLiteStore struct { - db *sql.DB + db *sql.DB + StoreName string } // NewSQLiteStore creates a new SQLite-backed KV store @@ -78,10 +117,15 @@ func NewSQLiteStore(db *sql.DB) *SQLiteStore { // Get retrieves a value by functionID and key func (s *SQLiteStore) Get(functionID, key string) (string, error) { + storeName := functionID + if s.StoreName != "" { + storeName = s.StoreName + } + var value string err := s.db.QueryRow( "SELECT value FROM kv_store WHERE function_id = ? AND key = ?", - functionID, key, + storeName, key, ).Scan(&value) if errors.Is(err, sql.ErrNoRows) { @@ -96,9 +140,14 @@ func (s *SQLiteStore) Get(functionID, key string) (string, error) { // Set stores a key-value pair for a functionID func (s *SQLiteStore) Set(functionID, key, value string) error { + storeName := functionID + if s.StoreName != "" { + storeName = s.StoreName + } + _, err := s.db.Exec( "INSERT OR REPLACE INTO kv_store (function_id, key, value) VALUES (?, ?, ?)", - functionID, key, value, + storeName, key, value, ) if err != nil { return fmt.Errorf("failed to set value: %w", err) @@ -108,12 +157,32 @@ func (s *SQLiteStore) Set(functionID, key, value string) error { // Delete removes a key-value pair for a functionID func (s *SQLiteStore) Delete(functionID, key string) error { + storeName := functionID + if s.StoreName != "" { + storeName = s.StoreName + } + _, err := s.db.Exec( "DELETE FROM kv_store WHERE function_id = ? AND key = ?", - functionID, key, + storeName, key, ) if err != nil { return fmt.Errorf("failed to delete value: %w", err) } return nil } + +// OpenNamed sets the StoreName for the SQLiteStore instance, allowing for namespacing of stores +func (s *SQLiteStore) OpenNamed(storeName string) error { + if storeName == "" { + return &Error{Message: "storeName cannot be empty"} + } + + s.StoreName = storeName + return nil +} + +// CloseNamed clears the StoreName for the SQLiteStore instance +func (s *SQLiteStore) CloseNamed() { + s.StoreName = "" +} diff --git a/internal/services/kv/kv_test.go b/internal/services/kv/kv_test.go index e20422d..66b4e43 100644 --- a/internal/services/kv/kv_test.go +++ b/internal/services/kv/kv_test.go @@ -134,6 +134,43 @@ func TestSQLiteStore_DeleteNonExistent(t *testing.T) { } } +func TestSQLiteStore_GetAndSetWithNamedStore(t *testing.T) { + db := setupTestDB(t) + store := NewSQLiteStore(db) + + // Open a named store + err := store.OpenNamed("test-store") + if err != nil { + t.Fatalf("Failed to open named store: %v", err) + } + + // Set a value in the named store. The functionID is ignored when StoreName is set, + // so we can use any functionID here. + err = store.Set("func-123", "key1", "value1") + if err != nil { + t.Fatalf("Failed to set value in named store: %v", err) + } + + // Get the value from the named store. The functionID is ignored when StoreName is set, + // so we can use any functionID here. + value, err := store.Get("func-123", "key1") + if err != nil { + t.Fatalf("Failed to get value from named store: %v", err) + } + + if value != "value1" { + t.Errorf("Expected value 'value1' from named store, got '%s'", value) + } + + // Close the named store. This just clears the StoreName, so subsequent calls will use functionID again. + // It is important to test that the named store doesn't affect the default functionID-based storage. + store.CloseNamed() + value, err = store.Get("func-123", "key1") + if err == nil { + t.Error("Expected error for key in default store after closing named store, got nil") + } +} + func TestSQLiteStore_FunctionIsolation(t *testing.T) { db := setupTestDB(t) store := NewSQLiteStore(db) From b894f6e85f626fa1b62d583171323ed21211ede3 Mon Sep 17 00:00:00 2001 From: Adam Crossland Date: Thu, 19 Feb 2026 16:01:39 -0500 Subject: [PATCH 2/4] Change storeName to be a private property There shouldn't be any reason for that to be accessed outside of this file. --- internal/services/kv/kv.go | 42 +++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/internal/services/kv/kv.go b/internal/services/kv/kv.go index 14012e8..90c9efa 100644 --- a/internal/services/kv/kv.go +++ b/internal/services/kv/kv.go @@ -28,7 +28,7 @@ type Store interface { // MemoryStore is an in-memory implementation of Store type MemoryStore struct { data map[string]map[string]string // functionID -> key -> value - StoreName string + storeName string // Optional store name for named stores } // NewMemoryStore creates a new in-memory KV store @@ -46,8 +46,8 @@ func (m *MemoryStore) Open(storeName string) (Store, error) { // Get retrieves a value by functionID and key func (m *MemoryStore) Get(functionID, key string) (string, error) { storeName := functionID - if m.StoreName != "" { - storeName = m.StoreName + if m.storeName != "" { + storeName = m.storeName } ns, exists := m.data[storeName] @@ -65,8 +65,8 @@ func (m *MemoryStore) Get(functionID, key string) (string, error) { // Set stores a key-value pair for a functionID func (m *MemoryStore) Set(functionID, key, value string) error { storeName := functionID - if m.StoreName != "" { - storeName = m.StoreName + if m.storeName != "" { + storeName = m.storeName } if _, exists := m.data[storeName]; !exists { @@ -79,8 +79,8 @@ func (m *MemoryStore) Set(functionID, key, value string) error { // Delete removes a key-value pair for a functionID func (m *MemoryStore) Delete(functionID, key string) error { storeName := functionID - if m.StoreName != "" { - storeName = m.StoreName + if m.storeName != "" { + storeName = m.storeName } if ns, exists := m.data[storeName]; exists { @@ -95,19 +95,19 @@ func (m *MemoryStore) OpenNamed(storeName string) error { return &Error{Message: "storeName cannot be empty"} } - m.StoreName = storeName + m.storeName = storeName return nil } -// CloseNamed clears the StoreName for the MemoryStore instance (for interface compatibility) +// CloseNamed clears the storeName for the MemoryStore instance (for interface compatibility) func (m *MemoryStore) CloseNamed() { - m.StoreName = "" + m.storeName = "" } // SQLiteStore is a SQLite-backed implementation of Store type SQLiteStore struct { db *sql.DB - StoreName string + storeName string // Optional store name for named stores } // NewSQLiteStore creates a new SQLite-backed KV store @@ -118,8 +118,8 @@ func NewSQLiteStore(db *sql.DB) *SQLiteStore { // Get retrieves a value by functionID and key func (s *SQLiteStore) Get(functionID, key string) (string, error) { storeName := functionID - if s.StoreName != "" { - storeName = s.StoreName + if s.storeName != "" { + storeName = s.storeName } var value string @@ -141,8 +141,8 @@ func (s *SQLiteStore) Get(functionID, key string) (string, error) { // Set stores a key-value pair for a functionID func (s *SQLiteStore) Set(functionID, key, value string) error { storeName := functionID - if s.StoreName != "" { - storeName = s.StoreName + if s.storeName != "" { + storeName = s.storeName } _, err := s.db.Exec( @@ -158,8 +158,8 @@ func (s *SQLiteStore) Set(functionID, key, value string) error { // Delete removes a key-value pair for a functionID func (s *SQLiteStore) Delete(functionID, key string) error { storeName := functionID - if s.StoreName != "" { - storeName = s.StoreName + if s.storeName != "" { + storeName = s.storeName } _, err := s.db.Exec( @@ -172,17 +172,17 @@ func (s *SQLiteStore) Delete(functionID, key string) error { return nil } -// OpenNamed sets the StoreName for the SQLiteStore instance, allowing for namespacing of stores +// OpenNamed sets the storeName for the SQLiteStore instance, allowing for namespacing of stores func (s *SQLiteStore) OpenNamed(storeName string) error { if storeName == "" { return &Error{Message: "storeName cannot be empty"} } - s.StoreName = storeName + s.storeName = storeName return nil } -// CloseNamed clears the StoreName for the SQLiteStore instance +// CloseNamed clears the storeName for the SQLiteStore instance func (s *SQLiteStore) CloseNamed() { - s.StoreName = "" + s.storeName = "" } From 20dd2c908c8a347673338b662a77c14f6a9352cf Mon Sep 17 00:00:00 2001 From: Adam Crossland Date: Sat, 21 Feb 2026 14:55:15 -0500 Subject: [PATCH 3/4] reimplement shared kvstore This change adds a global kv store that can be accessed by multiple functions. To make this possible, this change adds three new functions to the kv interface: getGlobal, setGlobal and deleteGlobal. These functions map the behavior of get, set, and delete. For example, let's say that I want to have an HTTP handler function that increments a counter each time that it is called: function handler(ctx, event) local count = kv.getGlobal("visitCounter") or 0 count = count + 1 kv.setGlobal("visitCounter", count) return { statusCode = 200, headers = { ["Content-Type"] = "application/json" }, body = json.encode({ message = "Visit count = " .. count }) } end I also want to have a function that runs every minute to email a report of the number of times that the first function has been called: function handler(ctx, event) -- Get data to report local count = kv.getGlobal("visitCounter") or 0 -- Send email via Resend local result, err = email.send({ from = "reports@fakeurl.dev", to = "reportreceivingperson@gmail.com", subject = "Current visit counter value", html = "

Current visit counter value is " .. count .. "

", }) if err then log.error("Email error: " .. err) return { statusCode = 500, headers = { ["Content-Type"] = "application/json" }, body = json.encode({ error = err }) } end log.info("Email sent: " .. result.id) return { statusCode = 200, headers = { ["Content-Type"] = "application/json" }, body = json.encode({ success = true, email_id = result.id }) } end --- frontend/js/components/api-reference.js | 13 ++- frontend/js/components/code-editor.js | 23 +++-- frontend/js/i18n/locales/en.js | 5 +- frontend/llms.txt | 16 ++-- internal/runner/lua_kv.go | 32 +++++-- internal/runner/runner_test.go | 14 ++- internal/services/kv/kv.go | 118 +++++++++++------------- internal/services/kv/kv_test.go | 38 ++++---- 8 files changed, 135 insertions(+), 124 deletions(-) diff --git a/frontend/js/components/api-reference.js b/frontend/js/components/api-reference.js index dffc2ca..5aaae7c 100644 --- a/frontend/js/components/api-reference.js +++ b/frontend/js/components/api-reference.js @@ -343,14 +343,19 @@ export function getLuaAPISections() { description: t("luaApi.io.items.kvDelete"), }, { - name: "kv.openNamed(name)", + name: "kv.getGlobal(key)", type: "function", - description: t("luaApi.io.items.kvOpenNamed"), + description: t("luaApi.io.items.kvGetGlobal"), }, { - name: "kv.closeNamed()", + name: "kv.setGlobal(key, value)", type: "function", - description: t("luaApi.io.items.kvCloseNamed"), + description: t("luaApi.io.items.kvSetGlobal"), + }, + { + name: "kv.deleteGlobal(key)", + type: "function", + description: t("luaApi.io.items.kvDeleteGlobal"), }, ], }, diff --git a/frontend/js/components/code-editor.js b/frontend/js/components/code-editor.js index 03093fd..079973b 100644 --- a/frontend/js/components/code-editor.js +++ b/frontend/js/components/code-editor.js @@ -117,15 +117,20 @@ const API_DOCS = { snippet: 'kv.delete("${1:key}")', description: "Delete a key from the store", }, - "kv.openNamed": { - signature: "kv.openNamed(name: string)", - snippet: 'kv.openNamed("${1:name}")', - description: "Open a named key-value store", - }, - "kv.closeNamed": { - signature: "kv.closeNamed()", - snippet: 'kv.closeNamed()', - description: "Close a named key-value store", + "kv.getGlobal": { + signature: "kv.getGlobal(key: string): string | nil", + snippet: 'kv.getGlobal("${1:key}")', + description: "Get a value from the global key-value store. Returns nil if key does not exist.", + }, + "kv.setGlobal": { + signature: "kv.setGlobal(key: string, value: string)", + snippet: 'kv.setGlobal("${1:key}", "${2:value}")', + description: "Set a key-value pair in the global store", + }, + "kv.deleteGlobal": { + signature: "kv.deleteGlobal(key: string)", + snippet: 'kv.deleteGlobal("${1:key}")', + description: "Delete a key from the global store", }, "env.get": { signature: "env.get(key: string): string | nil", diff --git a/frontend/js/i18n/locales/en.js b/frontend/js/i18n/locales/en.js index 678dd0c..deb3c7a 100644 --- a/frontend/js/i18n/locales/en.js +++ b/frontend/js/i18n/locales/en.js @@ -506,8 +506,9 @@ export default { kvGet: "Get value from store", kvSet: "Set key-value pair", kvDelete: "Delete key from store", - kvOpenNamed: "Open named store", - kvCloseNamed: "Close named store", + kvGetGlobal: "Get value from global store", + kvSetGlobal: "Set key-value pair in global store", + kvDeleteGlobal: "Delete key from global store", envGet: "Get environment variable", httpGet: "GET request", httpPost: "POST request", diff --git a/frontend/llms.txt b/frontend/llms.txt index 7b01398..0fe7bcf 100644 --- a/frontend/llms.txt +++ b/frontend/llms.txt @@ -74,20 +74,16 @@ Persistent storage scoped to function ID: - kv.get(key: string): string | nil - Retrieve value, returns nil if not found - kv.set(key: string, value: string): boolean - Set key-value pair, returns success - kv.delete(key: string): boolean - Delete key, returns success -- kv.openNamed(storeName: string): error - Switch to a named store -- kv.closeNamed() - Switch from named store to default, function-scoped store - +- kv.getGlobal(key: string): error - Retrieve value from global store, returns nil if not found +- kv.setGlobal(key: string, value: string): boolean - Set key-value pair, returns success +- kv.deleteGlobal(key: string): boolean - Delete key from global store, returns success Example: ```lua -local count = kv.get("counter") or "0" +local count = kv.get("counter") or 0 kv.set("counter", tostring(tonumber(count) + 1)) -kv.openNamed("shared") -kv.set("counter", "42") -local sharedCount = kv.get("counter") or "0" -- should be 42 -kv.closeNamed() - -local originalCount = kv.get("counter") or "0" -- should be 1 +local sharedCount = kv.getGlobal("sharedCounter") or 0 +kv.setGlobal("sharedCounter", sharedGlobal + 1) ``` ### Environment Variables (env) diff --git a/internal/runner/lua_kv.go b/internal/runner/lua_kv.go index ad554a1..936d549 100644 --- a/internal/runner/lua_kv.go +++ b/internal/runner/lua_kv.go @@ -46,10 +46,23 @@ func registerKV(L *lua.LState, kvStore kv.Store, functionID string) { return 1 })) - // kv.openNamed(storeName) - L.SetField(kvTable, "openNamed", L.NewFunction(func(L *lua.LState) int { - storeName := L.CheckString(1) - err := kvStore.OpenNamed(storeName) + // kv.getGlobal(key) + L.SetField(kvTable, "getGlobal", L.NewFunction(func(L *lua.LState) int { + key := L.CheckString(1) + value, err := kvStore.GetGlobal(key) + if err != nil { + L.Push(lua.LNil) + return 1 + } + L.Push(lua.LString(value)) + return 1 + })) + + // kv.setGlobal(key, value) + L.SetField(kvTable, "setGlobal", L.NewFunction(func(L *lua.LState) int { + key := L.CheckString(1) + value := L.CheckString(2) + err := kvStore.SetGlobal(key, value) if err != nil { L.Push(lua.LFalse) return 1 @@ -58,9 +71,14 @@ func registerKV(L *lua.LState, kvStore kv.Store, functionID string) { return 1 })) - // kv.closeNamed() - L.SetField(kvTable, "closeNamed", L.NewFunction(func(L *lua.LState) int { - kvStore.CloseNamed() + // kv.deleteGlobal(key) + L.SetField(kvTable, "deleteGlobal", L.NewFunction(func(L *lua.LState) int { + key := L.CheckString(1) + err := kvStore.DeleteGlobal(key) + if err != nil { + L.Push(lua.LFalse) + return 1 + } L.Push(lua.LTrue) return 1 })) diff --git a/internal/runner/runner_test.go b/internal/runner/runner_test.go index 2e61bfa..3816863 100644 --- a/internal/runner/runner_test.go +++ b/internal/runner/runner_test.go @@ -290,15 +290,13 @@ end luaCodeNamed := ` function handler(ctx, event) kv.set("key1", "value1") -- This should go to the default store - kv.openNamed("mystore") - kv.set("key1", "value2") - local val = kv.get("key1") - kv.closeNamed() - local orginalVal = kv.get("key1") -- Should retrieve from default store + kv.setGlobal("key1", "value2") -- This should go to the global store + local val = kv.get("key1") -- Should retrieve from default store + local globalVal = kv.getGlobal("key1") -- Should retrieve from global store return { statusCode = 200, - body = "Retrieved: " .. val .. ", " .. orginalVal + body = "Retrieved: " .. val .. ", " .. globalVal } end ` @@ -308,8 +306,8 @@ end t.Fatalf("Run failed: %v", err) } - if resp.HTTP.Body != "Retrieved: value2, value1" { - t.Errorf("expected body 'Retrieved: value2', got %q", resp.HTTP.Body) + if resp.HTTP.Body != "Retrieved: value1, value2" { + t.Errorf("expected body 'Retrieved: value1, value2', got %q", resp.HTTP.Body) } } diff --git a/internal/services/kv/kv.go b/internal/services/kv/kv.go index 90c9efa..4c39b66 100644 --- a/internal/services/kv/kv.go +++ b/internal/services/kv/kv.go @@ -21,14 +21,14 @@ type Store interface { Get(functionID, key string) (string, error) Set(functionID, key, value string) error Delete(functionID, key string) error - OpenNamed(storeName string) error - CloseNamed() + GetGlobal(key string) (string, error) + SetGlobal(key, value string) error + DeleteGlobal(key string) error } // MemoryStore is an in-memory implementation of Store type MemoryStore struct { - data map[string]map[string]string // functionID -> key -> value - storeName string // Optional store name for named stores + data map[string]map[string]string // functionID -> key -> value } // NewMemoryStore creates a new in-memory KV store @@ -43,14 +43,8 @@ func (m *MemoryStore) Open(storeName string) (Store, error) { return NewMemoryStore(), nil } -// Get retrieves a value by functionID and key func (m *MemoryStore) Get(functionID, key string) (string, error) { - storeName := functionID - if m.storeName != "" { - storeName = m.storeName - } - - ns, exists := m.data[storeName] + ns, exists := m.data[functionID] if !exists { return "", &Error{Message: fmt.Sprintf("key not found: %s", key)} } @@ -64,50 +58,51 @@ func (m *MemoryStore) Get(functionID, key string) (string, error) { // Set stores a key-value pair for a functionID func (m *MemoryStore) Set(functionID, key, value string) error { - storeName := functionID - if m.storeName != "" { - storeName = m.storeName + if _, exists := m.data[functionID]; !exists { + m.data[functionID] = make(map[string]string) } - - if _, exists := m.data[storeName]; !exists { - m.data[storeName] = make(map[string]string) - } - m.data[storeName][key] = value + m.data[functionID][key] = value return nil } // Delete removes a key-value pair for a functionID func (m *MemoryStore) Delete(functionID, key string) error { - storeName := functionID - if m.storeName != "" { - storeName = m.storeName - } - - if ns, exists := m.data[storeName]; exists { + if ns, exists := m.data[functionID]; exists { delete(ns, key) } return nil } -// OpenNamed opens a new in-memory KV store with a given name (for interface compatibility) -func (m *MemoryStore) OpenNamed(storeName string) error { - if storeName == "" { - return &Error{Message: "storeName cannot be empty"} +// GetGlobal retrieves a value from the global key-value store +func (m *MemoryStore) GetGlobal(key string) (string, error) { + if key == "" { + return "", &Error{Message: "key cannot be empty"} } - m.storeName = storeName - return nil + return m.Get("", key) } -// CloseNamed clears the storeName for the MemoryStore instance (for interface compatibility) -func (m *MemoryStore) CloseNamed() { - m.storeName = "" +// SetGlobal sets a value in the global key-value store +func (m *MemoryStore) SetGlobal(key, value string) error { + if key == "" { + return &Error{Message: "key cannot be empty"} + } + + return m.Set("", key, value) +} + +// DeleteGlobal removes a key-value pair from the global key-value store +func (m *MemoryStore) DeleteGlobal(key string) error { + if key == "" { + return &Error{Message: "key cannot be empty"} + } + + return m.Delete("", key) } // SQLiteStore is a SQLite-backed implementation of Store type SQLiteStore struct { - db *sql.DB - storeName string // Optional store name for named stores + db *sql.DB } // NewSQLiteStore creates a new SQLite-backed KV store @@ -117,15 +112,10 @@ func NewSQLiteStore(db *sql.DB) *SQLiteStore { // Get retrieves a value by functionID and key func (s *SQLiteStore) Get(functionID, key string) (string, error) { - storeName := functionID - if s.storeName != "" { - storeName = s.storeName - } - var value string err := s.db.QueryRow( "SELECT value FROM kv_store WHERE function_id = ? AND key = ?", - storeName, key, + functionID, key, ).Scan(&value) if errors.Is(err, sql.ErrNoRows) { @@ -140,14 +130,9 @@ func (s *SQLiteStore) Get(functionID, key string) (string, error) { // Set stores a key-value pair for a functionID func (s *SQLiteStore) Set(functionID, key, value string) error { - storeName := functionID - if s.storeName != "" { - storeName = s.storeName - } - _, err := s.db.Exec( "INSERT OR REPLACE INTO kv_store (function_id, key, value) VALUES (?, ?, ?)", - storeName, key, value, + functionID, key, value, ) if err != nil { return fmt.Errorf("failed to set value: %w", err) @@ -157,14 +142,9 @@ func (s *SQLiteStore) Set(functionID, key, value string) error { // Delete removes a key-value pair for a functionID func (s *SQLiteStore) Delete(functionID, key string) error { - storeName := functionID - if s.storeName != "" { - storeName = s.storeName - } - _, err := s.db.Exec( "DELETE FROM kv_store WHERE function_id = ? AND key = ?", - storeName, key, + functionID, key, ) if err != nil { return fmt.Errorf("failed to delete value: %w", err) @@ -172,17 +152,29 @@ func (s *SQLiteStore) Delete(functionID, key string) error { return nil } -// OpenNamed sets the storeName for the SQLiteStore instance, allowing for namespacing of stores -func (s *SQLiteStore) OpenNamed(storeName string) error { - if storeName == "" { - return &Error{Message: "storeName cannot be empty"} +// GetGlobal retrieves a value from the global key-value store +func (s *SQLiteStore) GetGlobal(key string) (string, error) { + if key == "" { + return "", &Error{Message: "key cannot be empty"} } - s.storeName = storeName - return nil + return s.Get("", key) } -// CloseNamed clears the storeName for the SQLiteStore instance -func (s *SQLiteStore) CloseNamed() { - s.storeName = "" +// SetGlobal sets a value in the global key-value store +func (s *SQLiteStore) SetGlobal(key, value string) error { + if key == "" { + return &Error{Message: "key cannot be empty"} + } + + return s.Set("", key, value) +} + +// DeleteGlobal removes a key-value pair from the global key-value store +func (s *SQLiteStore) DeleteGlobal(key string) error { + if key == "" { + return &Error{Message: "key cannot be empty"} + } + + return s.Delete("", key) } diff --git a/internal/services/kv/kv_test.go b/internal/services/kv/kv_test.go index 66b4e43..147231f 100644 --- a/internal/services/kv/kv_test.go +++ b/internal/services/kv/kv_test.go @@ -134,40 +134,36 @@ func TestSQLiteStore_DeleteNonExistent(t *testing.T) { } } -func TestSQLiteStore_GetAndSetWithNamedStore(t *testing.T) { +func TestSQLiteStore_GetAndSetWithGlobalStore(t *testing.T) { db := setupTestDB(t) store := NewSQLiteStore(db) - // Open a named store - err := store.OpenNamed("test-store") + // Set a value in the global store. + err := store.SetGlobal("key1", "value1") if err != nil { - t.Fatalf("Failed to open named store: %v", err) + t.Fatalf("Failed to set value in global store: %v", err) } - // Set a value in the named store. The functionID is ignored when StoreName is set, - // so we can use any functionID here. - err = store.Set("func-123", "key1", "value1") + // Get the value from the global store. + value, err := store.GetGlobal("key1") if err != nil { - t.Fatalf("Failed to set value in named store: %v", err) + t.Fatalf("Failed to get value from global store: %v", err) } - // Get the value from the named store. The functionID is ignored when StoreName is set, - // so we can use any functionID here. - value, err := store.Get("func-123", "key1") - if err != nil { - t.Fatalf("Failed to get value from named store: %v", err) + if value != "value1" { + t.Errorf("Expected value 'value1' from global store, got '%s'", value) } - if value != "value1" { - t.Errorf("Expected value 'value1' from named store, got '%s'", value) + // Delete a key from the global store + err = store.DeleteGlobal("key1") + if err != nil { + t.Errorf("Expected no error for deleting key from global store, got %v", err) } - // Close the named store. This just clears the StoreName, so subsequent calls will use functionID again. - // It is important to test that the named store doesn't affect the default functionID-based storage. - store.CloseNamed() - value, err = store.Get("func-123", "key1") - if err == nil { - t.Error("Expected error for key in default store after closing named store, got nil") + // Delete a non-existent key from the global store + err = store.DeleteGlobal("key1") + if err != nil { + t.Errorf("Expected no error for deleting key from global store, got %v", err) } } From 2faeba4ec5c7531bfbcbb1fcca7e033664815869 Mon Sep 17 00:00:00 2001 From: Adam Crossland Date: Sun, 22 Feb 2026 08:59:17 -0500 Subject: [PATCH 4/4] remove unused function --- internal/services/kv/kv.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/internal/services/kv/kv.go b/internal/services/kv/kv.go index 4c39b66..a04978b 100644 --- a/internal/services/kv/kv.go +++ b/internal/services/kv/kv.go @@ -38,11 +38,6 @@ func NewMemoryStore() *MemoryStore { } } -// Open opens a new in-memory KV store (for interface compatibility) -func (m *MemoryStore) Open(storeName string) (Store, error) { - return NewMemoryStore(), nil -} - func (m *MemoryStore) Get(functionID, key string) (string, error) { ns, exists := m.data[functionID] if !exists {