mirror of
https://github.com/EchoVault/SugarDB.git
synced 2025-09-27 04:16:06 +08:00
Extend SugarDB Commands Using Lua Scripts (#155)
* Extend SugarDB by creating new commands using Lua - @kelvinmwinuka
This commit is contained in:
@@ -8,8 +8,8 @@ COPY . ./
|
||||
ENV CGO_ENABLED=1 CC=gcc GOOS=linux GOARCH=amd64
|
||||
|
||||
ENV DEST=volumes/modules
|
||||
RUN CGO_ENABLED=$CGO_ENABLED CC=$CC GOOS=$GOOS GOARCH=$GOARCH go build -buildmode=plugin -o $DEST/module_set/module_set.so ./internal/volumes/modules/module_set/module_set.go
|
||||
RUN CGO_ENABLED=$CGO_ENABLED CC=$CC GOOS=$GOOS GOARCH=$GOARCH go build -buildmode=plugin -o $DEST/module_get/module_get.so ./internal/volumes/modules/module_get/module_get.go
|
||||
RUN CGO_ENABLED=$CGO_ENABLED CC=$CC GOOS=$GOOS GOARCH=$GOARCH go build -buildmode=plugin -o $DEST/module_set/module_set.so ./internal/volumes/modules/go/module_set/module_set.go
|
||||
RUN CGO_ENABLED=$CGO_ENABLED CC=$CC GOOS=$GOOS GOARCH=$GOARCH go build -buildmode=plugin -o $DEST/module_get/module_get.so ./internal/volumes/modules/go/module_get/module_get.go
|
||||
|
||||
ENV DEST=bin
|
||||
RUN CGO_ENABLED=$CGO_ENABLED CC=$CC GOOS=$GOOS GOARCH=$GOARCH go build -o $DEST/server ./cmd/...
|
||||
|
8
Makefile
8
Makefile
@@ -2,14 +2,14 @@ run:
|
||||
docker-compose up --build
|
||||
|
||||
build-local:
|
||||
CGO_ENABLED=1 go build -buildmode=plugin -o ./bin/modules/module_set/module_set.so ./internal/volumes/modules/module_set/module_set.go && \
|
||||
CGO_ENABLED=1 go build -buildmode=plugin -o ./bin/modules/module_get/module_get.so ./internal/volumes/modules/module_get/module_get.go && \
|
||||
CGO_ENABLED=1 go build -buildmode=plugin -o ./bin/modules/module_set/module_set.so ./internal/volumes/modules/go/module_set/module_set.go && \
|
||||
CGO_ENABLED=1 go build -buildmode=plugin -o ./bin/modules/module_get/module_get.so ./internal/volumes/modules/go/module_get/module_get.go && \
|
||||
CGO_ENABLED=1 go build -o ./bin ./...
|
||||
|
||||
|
||||
build-modules-test:
|
||||
CGO_ENABLED=1 go build --race=$(RACE) -buildmode=plugin -o $(OUT)/modules/module_set/module_set.so ./internal/volumes/modules/module_set/module_set.go && \
|
||||
CGO_ENABLED=1 go build --race=$(RACE) -buildmode=plugin -o $(OUT)/modules/module_get/module_get.so ./internal/volumes/modules/module_get/module_get.go
|
||||
CGO_ENABLED=1 go build --race=$(RACE) -buildmode=plugin -o $(OUT)/modules/module_set/module_set.so ./internal/volumes/modules/go/module_set/module_set.go && \
|
||||
CGO_ENABLED=1 go build --race=$(RACE) -buildmode=plugin -o $(OUT)/modules/module_get/module_get.so ./internal/volumes/modules/go/module_get/module_get.go
|
||||
|
||||
test:
|
||||
env RACE=false OUT=internal/modules/admin/testdata make build-modules-test && \
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,7 @@ services:
|
||||
volumes:
|
||||
- ./internal/volumes/config:/etc/sugardb/config
|
||||
- ./internal/volumes/nodes/standalone_node:/var/lib/sugardb
|
||||
- ./internal/volumes/modules/lua:/var/lib/sugardb/scripts/lua
|
||||
networks:
|
||||
- testnet
|
||||
|
||||
|
527
docs/docs/extension/lua.mdx
Normal file
527
docs/docs/extension/lua.mdx
Normal file
@@ -0,0 +1,527 @@
|
||||
---
|
||||
title: Lua Modules
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
import LoadModuleDocs from "@site/src/components/load_module"
|
||||
import CodeBlock from "@theme/CodeBlock"
|
||||
|
||||
# Lua Modules
|
||||
|
||||
SugarDB allows you to create new command modules using Lua scripts. These scripts are loaded into SugarDB at runtime and can be triggered by both embedded clients and TCP clients just like native commands.
|
||||
|
||||
## Creating a Lua Script Module
|
||||
|
||||
A Lua script has the following anatomy:
|
||||
|
||||
```lua
|
||||
-- The keyword to trigger the command
|
||||
command = "LUA.EXAMPLE"
|
||||
|
||||
--[[
|
||||
The string array of categories this command belongs to.
|
||||
This array can contain both built-in categories and new custom categories.
|
||||
]]
|
||||
categories = {"generic", "write", "fast"}
|
||||
|
||||
-- The description of the command
|
||||
description = "(LUA.EXAMPLE) Example lua command that sets various data types to keys"
|
||||
|
||||
-- Whether the command should be synced across the RAFT cluster
|
||||
sync = true
|
||||
|
||||
--[[
|
||||
keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
|
||||
The returned data from this function is used in the Access Control Layer to determine if the current connection is
|
||||
authorized to execute this command. The function must return a table that specifies which keys in this command
|
||||
are read keys and which ones are write keys.
|
||||
Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4", "key5"}}
|
||||
|
||||
1. "command" is a string array representing the command that triggered this key extraction function.
|
||||
|
||||
2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
|
||||
These args are passed to the key extraction function everytime it's invoked.
|
||||
]]
|
||||
function keyExtractionFunc (command, args)
|
||||
for k,v in pairs(args) do
|
||||
print(k, v)
|
||||
end
|
||||
if (#command ~= 1) then
|
||||
error("wrong number of args, expected 0")
|
||||
end
|
||||
return { ["readKeys"] = {}, ["writeKeys"] = {} }
|
||||
end
|
||||
|
||||
--[[
|
||||
handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
|
||||
SugarDB. The function must return a valid RESP response or throw an error.
|
||||
The handler function accepts the following args:
|
||||
|
||||
1. "context" is a table that contains some information about the environment this command has been executed in.
|
||||
Example: {["protocol"] = 2, ["database"] = 0}
|
||||
This object contains the following properties:
|
||||
i) protocol - the protocol version of the client that executed the command (either 2 or 3).
|
||||
ii) database - the active database index of the client that executed the command.
|
||||
|
||||
2. "command" is the string array representing the command that triggered this handler function.
|
||||
|
||||
3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
|
||||
This function accepts a string array of keys to check and returns a table with each key having a corresponding
|
||||
boolean value indicating whether it exists.
|
||||
Examples:
|
||||
i) Example invocation: keyExists({"key1", "key2", "key3"})
|
||||
ii) Example return: {["key1"] = true, ["key2"] = false, ["key3"] = true}
|
||||
|
||||
4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
|
||||
The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
|
||||
containing the corresponding value from the store.
|
||||
The possible data types for the values are: number, string, nil, hash, set, zset
|
||||
Examples:
|
||||
i) Example invocation: getValues({"key1", "key2", "key3"})
|
||||
ii) Example return: {["key1"] = 3.142, ["key2"] = nil, ["key3"] = "Pi"}
|
||||
|
||||
5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
|
||||
This function accepts a table with keys and the corresponding values to set for each key in the active database
|
||||
in the store.
|
||||
The accepted data types for the values are: number, string, nil, hash, set, zset.
|
||||
The setValues function does not return anything.
|
||||
Examples:
|
||||
i) Example invocation: setValues({["key1"] = 3.142, ["key2"] = nil, ["key3"] = "Pi"})
|
||||
|
||||
6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
|
||||
handler everytime it's invoked.
|
||||
]]
|
||||
function handlerFunc(ctx, command, keysExist, getValues, setValues, args)
|
||||
-- Set various data types to keys
|
||||
local keyValues = {
|
||||
["numberKey"] = 42,
|
||||
["stringKey"] = "Hello, SugarDB!",
|
||||
["nilKey"] = nil,
|
||||
}
|
||||
|
||||
-- Store the values in the database
|
||||
setValues(keyValues)
|
||||
|
||||
-- Verify the values have been set correctly
|
||||
local keysToGet = {"numberKey", "stringKey", "nilKey"}
|
||||
local retrievedValues = getValues(keysToGet)
|
||||
|
||||
-- Create a table to track mismatches
|
||||
local mismatches = {}
|
||||
for key, expectedValue in pairs(keyValues) do
|
||||
local retrievedValue = retrievedValues[key]
|
||||
if retrievedValue ~= expectedValue then
|
||||
table.insert(mismatches, string.format("Key '%s': expected '%s', got '%s'", key, tostring(expectedValue), tostring(retrievedValue)))
|
||||
end
|
||||
end
|
||||
|
||||
-- If mismatches exist, return an error
|
||||
if #mismatches > 0 then
|
||||
error("values mismatch")
|
||||
end
|
||||
|
||||
-- If all values match, return OK
|
||||
return "+OK\r\n"
|
||||
end
|
||||
```
|
||||
|
||||
## Loading Lua Modules
|
||||
<LoadModuleDocs module="lua" />
|
||||
|
||||
## Standard Data Types
|
||||
|
||||
Sugar DB supports the following standard data types in Lua scripts:
|
||||
|
||||
- string
|
||||
- number (integers and floating-point numbers)
|
||||
- nil
|
||||
- arrays (tables with integer keys)
|
||||
|
||||
These data types can be used to stored using the setValues function and
|
||||
retrieved using the getValues function.
|
||||
|
||||
## Custom Data Types
|
||||
|
||||
In addition to the standard data types, SugarDB also supports custom data types in Lua scripts.
|
||||
These custom data types include:
|
||||
|
||||
- Hashes
|
||||
- Sets
|
||||
- Sorted Sets
|
||||
|
||||
Just like the standard types, these custom data types can be stored and retrieved using the setValues
|
||||
and getValues functions respectively.
|
||||
|
||||
### Hashes
|
||||
|
||||
The hash data type is a custom data type in SugarDB designed for storing and managing key-value pairs.
|
||||
It supports several methods for interacting with the hash, including adding, updating, retrieving, deleting, and checking values.
|
||||
This section explains how to make use of the hash data type in your Lua scripts.
|
||||
|
||||
#### Creating a Hash
|
||||
|
||||
```lua
|
||||
local myHash = hash.new()
|
||||
```
|
||||
|
||||
#### Hash methods
|
||||
|
||||
`set` - Adds or updates key-value pairs in the hash. If the key exists,
|
||||
the value is updated; otherwise, it is added.
|
||||
|
||||
```lua
|
||||
local myHash = hash.new()
|
||||
local numUpdated = myHash:set({
|
||||
{key1 = "value1"},
|
||||
{key2 = "value2"}
|
||||
})
|
||||
print(numUpdated) -- Output: 2
|
||||
```
|
||||
|
||||
`setnx` - Adds key-value pairs to the hash only if the key does not already exist.
|
||||
|
||||
```lua
|
||||
local myHash = hash.new()
|
||||
myHash:set({{key1 = "value1"}})
|
||||
local numAdded = myHash:setnx({
|
||||
{key1 = "newValue"}, -- Will not overwrite because key1 exists
|
||||
{key2 = "value2"} -- Will be added
|
||||
})
|
||||
print(numAdded) -- Output: 1
|
||||
```
|
||||
|
||||
`get` - Retrieves the values for the specified keys. Returns nil for keys that do not exist.
|
||||
|
||||
```lua
|
||||
local myHash = hash.new()
|
||||
myHash:set({{key1 = "value1"}, {key2 = "value2"}})
|
||||
local values = myHash:get({"key1", "key2", "key3"})
|
||||
for k, v in pairs(values) do
|
||||
print(k, v) -- Output: key1 value1, key2 value2, key3 nil
|
||||
end
|
||||
```
|
||||
|
||||
`len` - Returns the number of key-value pairs in the hash.
|
||||
|
||||
```lua
|
||||
local myHash = hash.new()
|
||||
myHash:set({{key1 = "value1"}, {key2 = "value2"}})
|
||||
print(myHash:len()) -- Output: 2
|
||||
```
|
||||
|
||||
`all` - Returns a table containing all key-value pairs in the hash.
|
||||
|
||||
```lua
|
||||
local myHash = hash.new()
|
||||
myHash:set({{key1 = "value1"}, {key2 = "value2"}})
|
||||
local allValues = myHash:all()
|
||||
for k, v in pairs(allValues) do
|
||||
print(k, v) -- Output: key1 value1, key2 value2
|
||||
end
|
||||
```
|
||||
|
||||
`exists` - Checks if specified keys exist in the hash.
|
||||
|
||||
```lua
|
||||
local myHash = hash.new()
|
||||
myHash:set({{key1 = "value1"}})
|
||||
local existence = myHash:exists({"key1", "key2"})
|
||||
for k, v in pairs(existence) do
|
||||
print(k, v) -- Output: key1 true, key2 false
|
||||
end
|
||||
```
|
||||
|
||||
`del` - Deletes the specified keys from the hash. Returns the number of keys deleted.
|
||||
|
||||
```lua
|
||||
local myHash = hash.new()
|
||||
myHash:set({{key1 = "value1"}, {key2 = "value2"}})
|
||||
local numDeleted = myHash:del({"key1", "key3"})
|
||||
print(numDeleted) -- Output: 1
|
||||
```
|
||||
|
||||
|
||||
### Sets
|
||||
|
||||
The `set` data type is a custom data type in SugarDB designed for managing unique elements.
|
||||
It supports operations like adding, removing, checking for membership, and performing set operations such as subtraction.
|
||||
This section explains how to use the `set` data type in your Lua scripts.
|
||||
|
||||
#### Creating a Set
|
||||
|
||||
```lua
|
||||
local mySet = set.new({"apple", "banana", "cherry"})
|
||||
```
|
||||
|
||||
#### Set methods
|
||||
|
||||
`add` - Adds one or more elements to the set. Returns the number of elements added.
|
||||
|
||||
```lua
|
||||
local mySet = set.new()
|
||||
local addedCount = mySet:add({"apple", "banana"})
|
||||
print(addedCount) -- Output: 2
|
||||
```
|
||||
|
||||
`pop` - Removes and returns a specified number of random elements from the set.
|
||||
|
||||
```lua
|
||||
local mySet = set.new({"apple", "banana", "cherry"})
|
||||
local popped = mySet:pop(2)
|
||||
for i, v in ipairs(popped) do
|
||||
print(i, v) -- Output: Two random elements
|
||||
end
|
||||
```
|
||||
|
||||
`contains` - Checks if a specific element exists in the set.
|
||||
|
||||
```lua
|
||||
local mySet = set.new({"apple", "banana"})
|
||||
print(mySet:contains("apple")) -- Output: true
|
||||
print(mySet:contains("cherry")) -- Output: false
|
||||
```
|
||||
|
||||
`cardinality` - Returns the number of elements in the set.
|
||||
|
||||
```lua
|
||||
local mySet = set.new({"apple", "banana"})
|
||||
print(mySet:cardinality()) -- Output: 2
|
||||
```
|
||||
|
||||
`remove` - Removes one or more specified elements from the set. Returns the number of elements removed.
|
||||
|
||||
```lua
|
||||
local mySet = set.new({"apple", "banana", "cherry"})
|
||||
local removedCount = mySet:remove({"banana", "cherry"})
|
||||
print(removedCount) -- Output: 2
|
||||
```
|
||||
|
||||
`move` - Moves an element from one set to another. Returns true if the element was successfully moved.
|
||||
|
||||
```lua
|
||||
local set1 = set.new({"apple", "banana"})
|
||||
local set2 = set.new({"cherry"})
|
||||
local success = set1:move(set2, "banana")
|
||||
print(success) -- Output: true
|
||||
```
|
||||
|
||||
`subtract` - Returns a new set that is the result of subtracting other sets from the current set.
|
||||
|
||||
```lua
|
||||
local set1 = set.new({"apple", "banana", "cherry"})
|
||||
local set2 = set.new({"banana"})
|
||||
local resultSet = set1:subtract({set2})
|
||||
local allElems = resultSet:all()
|
||||
for i, v in ipairs(allElems) do
|
||||
print(i, v) -- Output: "apple", "cherry"
|
||||
end
|
||||
```
|
||||
|
||||
`all` - Returns a table containing all elements in the set.
|
||||
|
||||
```lua
|
||||
local mySet = set.new({"apple", "banana", "cherry"})
|
||||
local allElems = mySet:all()
|
||||
for i, v in ipairs(allElems) do
|
||||
print(i, v) -- Output: "apple", "banana", "cherry"
|
||||
end
|
||||
```
|
||||
|
||||
`random` - Returns a table of randomly selected elements from the set. The number of elements to return is specified as an argument.
|
||||
|
||||
```lua
|
||||
local mySet = set.new({"apple", "banana", "cherry", "date"})
|
||||
local randomElems = mySet:random(2)
|
||||
for i, v in ipairs(randomElems) do
|
||||
print(i, v) -- Output: Two random elements
|
||||
end
|
||||
```
|
||||
|
||||
### Sorted Sets
|
||||
|
||||
A zset is a sorted set that stores zmember elements, ordered by their score.
|
||||
The zset type provides methods to manipulate and query the set. A zset is made up of
|
||||
zmember elements, each of which has a value and a score.
|
||||
|
||||
#### zmember
|
||||
|
||||
A zmember represents an element in a zset (sorted set). Each zmember consists of:
|
||||
- value: A unique identifier for the member (e.g., a string).
|
||||
- score: A numeric value used to sort the member in the sorted set.
|
||||
|
||||
You can create a zmember using the `zmember.new` method:
|
||||
|
||||
```lua
|
||||
local m = zmember.new({value = "example", score = 42})
|
||||
```
|
||||
|
||||
The zmember type provides methods to retrieve or modify these properties.
|
||||
|
||||
To set/get the value of a zmember, use the `value` method:
|
||||
|
||||
```lua
|
||||
-- Get the value
|
||||
local value = m:value()
|
||||
|
||||
-- Set the value
|
||||
m:value("new_value")
|
||||
```
|
||||
|
||||
To set/get the score, use the `score` method:
|
||||
|
||||
```lua
|
||||
-- Get the score
|
||||
local score = m:score()
|
||||
|
||||
-- Set the score
|
||||
m:score(99.5)
|
||||
```
|
||||
|
||||
#### Creating a Sorted Set
|
||||
|
||||
```lua
|
||||
-- Create a new zset with no zmembers
|
||||
local zset1 = zset.new()
|
||||
|
||||
-- Create a new zset with two zmembers
|
||||
local zset2 = zset.new({
|
||||
zmember.new({value = "a", score = 10}),
|
||||
zmember.new({value = "b", score = 20}),
|
||||
})
|
||||
```
|
||||
|
||||
#### Sorted Set Methods
|
||||
|
||||
`add` - Adds one or more zmember elements to the zset.
|
||||
Optionally, you can specify update policies using the optional modifiers.
|
||||
|
||||
Optional Modifiers:
|
||||
- "exists": Specifies whether to only update existing members ("xx") or only add new members ("nx"). Defaults to no restriction.
|
||||
- "comparison": Specifies a comparison method for updating scores (e.g., "min", "max").
|
||||
- "changed": If true, returns the count of changed elements.
|
||||
- "incr": If true, increments the score of the specified member by the given score instead of replacing it.
|
||||
|
||||
Basic usage:
|
||||
|
||||
```lua
|
||||
-- Create members
|
||||
local m1 = zmember.new({value = "item1", score = 10})
|
||||
local m2 = zmember.new({value = "item2", score = 20})
|
||||
|
||||
-- Create zset and add members
|
||||
local zset = zset.new()
|
||||
zset:add({m1, m2})
|
||||
|
||||
-- Check cardinality
|
||||
print(zset:cardinality()) -- Outputs: 2
|
||||
```
|
||||
|
||||
Usage with optional modifiers:
|
||||
|
||||
```lua
|
||||
-- Create zset
|
||||
local zset = zset.new({
|
||||
zmember.new({value = "a", score = 10}),
|
||||
zmember.new({value = "b", score = 20}),
|
||||
})
|
||||
|
||||
-- Attempt to add members with different policies
|
||||
local new_members = {
|
||||
zmember.new({value = "a", score = 5}), -- Existing member
|
||||
zmember.new({value = "c", score = 15}), -- New member
|
||||
}
|
||||
|
||||
-- Use policies to update and add
|
||||
local options = {
|
||||
exists = "xx", -- Only update existing members
|
||||
comparison = "max", -- Keep the maximum score for existing members
|
||||
changed = true, -- Return the count of changed elements
|
||||
}
|
||||
local changed_count = zset:add(new_members, options)
|
||||
|
||||
-- Display results
|
||||
print("Changed count:", changed_count) -- Outputs: 1 (only "a" is updated)
|
||||
print("Updated zset:")
|
||||
for _, member in ipairs(zset:all()) do
|
||||
print(member:value(), member:score())
|
||||
end
|
||||
|
||||
-- Adding with different policies
|
||||
local incr_options = {
|
||||
exists = "nx", -- Only add new members
|
||||
incr = true, -- Increment the score of the added members
|
||||
}
|
||||
zset:add({zmember.new({value = "d", score = 10})}, incr_options)
|
||||
|
||||
-- Display updated zset
|
||||
print("After adding with increment:")
|
||||
for _, member in ipairs(zset:all()) do
|
||||
print(member:value(), member:score())
|
||||
end
|
||||
```
|
||||
|
||||
`update` - Updates one or more zmember elements in the zset.
|
||||
If the member doesn’t exist, the behavior depends on the provided update options.
|
||||
|
||||
Optional Modifiers:
|
||||
- "exists": Specifies whether to only update existing members ("xx") or only add new members ("nx"). Defaults to no restriction.
|
||||
- "comparison": Specifies a comparison method for updating scores (e.g., "min", "max").
|
||||
- "changed": If true, returns the count of changed elements.
|
||||
- "incr": If true, increments the score of the specified member by the given score instead of replacing it.
|
||||
|
||||
```lua
|
||||
-- Create members
|
||||
local m1 = zmember.new({value = "item1", score = 10})
|
||||
local m2 = zmember.new({value = "item2", score = 20})
|
||||
|
||||
-- Create zset and add members
|
||||
local zset = zset.new({m1, m2})
|
||||
|
||||
-- Update a member
|
||||
local m_update = zmember.new({value = "item1", score = 15})
|
||||
local changed_count = zset:update({m_update}, {exists = true, comparison = "max", changed = true})
|
||||
print("Changed count:", changed_count) -- Outputs the number of elements updated
|
||||
```
|
||||
|
||||
`remove` - Removes a member from the zset by its value.
|
||||
|
||||
```lua
|
||||
local removed = zset:remove("a") -- Returns true if removed
|
||||
```
|
||||
|
||||
`cardinality` - Returns the number of zmembers in the zset.
|
||||
|
||||
```lua
|
||||
local count = zset:cardinality()
|
||||
```
|
||||
|
||||
`contains` - Checks if a zmember with the specified value exists in the zset.
|
||||
|
||||
```lua
|
||||
local exists = zset:contains("b") -- Returns true if exists
|
||||
```
|
||||
|
||||
`random` - Returns a random zmember from the zset.
|
||||
|
||||
```lua
|
||||
local members = zset:random(2) -- Returns up to 2 random members
|
||||
```
|
||||
|
||||
`all` - Returns all zmembers in the zset.
|
||||
|
||||
```lua
|
||||
local members = zset:all()
|
||||
for _, member in ipairs(members) do
|
||||
print(member:value(), member:score())
|
||||
end
|
||||
```
|
||||
|
||||
`subtract` - Returns a new zset that is the result of subtracting other zsets from the current one.
|
||||
|
||||
```lua
|
||||
local other_zset = zset.new({
|
||||
zmember.new({value = "b", score = 20}),
|
||||
})
|
||||
local result_zset = zset:subtract({other_zset})
|
||||
```
|
@@ -1,3 +1,11 @@
|
||||
---
|
||||
title: Shared Object Files
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
import LoadModuleDocs from "@site/src/components/load_module"
|
||||
|
||||
# Shared Object Files
|
||||
|
||||
SugarDB allows you to extend its list of commands using shared object files. You can write Go scripts that are compiled in plugin mode to achieve this.
|
||||
@@ -110,128 +118,8 @@ Pass the -buildmode=plugin flag when compiling the plugin and the -o flag to spe
|
||||
CGO_ENABLED=1 CC=gcc GOOS=linux GOARCH=amd64 go build -buildmode=plugin -o module_set.so module_set.go
|
||||
```
|
||||
|
||||
## Loading Module
|
||||
|
||||
You can load modules in 3 ways:
|
||||
|
||||
### 1. At startup with the `--loadmodule` flag.
|
||||
|
||||
Upon startup you can provide the flag `--loadmodule="<path>/<to>/<module>.so"`. This is the path to the module's .so file. You can pass this flag multiple times to load multiple modules on startup.
|
||||
|
||||
### 2. At runtime with the `MODULE LOAD` command.
|
||||
|
||||
You can load modules dynamically at runtime using the `MODULE LOAD` command as follows:
|
||||
|
||||
```
|
||||
MODULE LOAD <path>/<to>/<module>.so
|
||||
```
|
||||
|
||||
This command only takes one path so if you have multiple modules to load, You will have to load them one at a time.
|
||||
|
||||
### 3. At runtime the the `LoadModule` method.
|
||||
|
||||
You can load a module .so file dynamically at runtime using the <a target="_blank" href="https://pkg.go.dev/github.com/echovault/echovault@v0.10.1/echovault#EchoVault.LoadModule">`LoadModule`</a> method in the embedded API.
|
||||
|
||||
```go
|
||||
err = server.LoadModule("<path>/<to>/<module>.so")
|
||||
```
|
||||
|
||||
### Loading Module with Args
|
||||
|
||||
You might have notices the `args ...string` variadic parameter when creating a module. This a list of args that are passed to the module's key extraction and handler functions.
|
||||
|
||||
The values passed here are established once when loading the module, and the same values will be passed to the respective functions everytime the command is executed.
|
||||
|
||||
If you don't provide any args, an empty slice will be passed in the args parameter. Otehrwise, a slice containing your defined args will be used.
|
||||
|
||||
To load a module with args using the embedded API:
|
||||
|
||||
```go
|
||||
err = server.LoadModule("<path>/<to>/<module>.so", "list", "of", "args")
|
||||
```
|
||||
|
||||
To load a module with args using the `MODULE LOAD` command:
|
||||
|
||||
```
|
||||
MODULE LOAD <path>/<to>/<module>.so "list" "of" "args"
|
||||
```
|
||||
|
||||
NOTE: You cannot pass args when loading modules at startup with the `--loadmodule` flag.
|
||||
|
||||
## List Modules
|
||||
|
||||
You can list the current modules loaded in the SugarDB instance using both the Client-Server and embedded APIs.
|
||||
|
||||
To check the loaded modules using the embedded API, use the <a target="_blank" href="https://pkg.go.dev/github.com/echovault/echovault@v0.10.1/echovault#EchoVault.ListModules">`ListModules`</a> method:
|
||||
|
||||
```go
|
||||
modules := server.ListModules()
|
||||
```
|
||||
|
||||
This method returns a string slice containing all the loaded modules in the SugarDB instance.
|
||||
|
||||
You can also list the loaded modules over the TCP API using the `MODULE LIST` command.
|
||||
|
||||
Here's an example response of the loaded modules:
|
||||
|
||||
```
|
||||
1) "acl"
|
||||
2) "admin"
|
||||
3) "connection"
|
||||
4) "generic"
|
||||
5) "hash"
|
||||
6) "list"
|
||||
7) "pubsub"
|
||||
8) "set"
|
||||
9) "sortedset"
|
||||
10) "string"
|
||||
11) "./modules/module_set/module_set.so"
|
||||
```
|
||||
|
||||
Notice that the modules loaded from .so files have their respective file names as the module name.
|
||||
|
||||
## Execute Module Command
|
||||
|
||||
Here's an example of executing the `Module.Set` command with the embedded API:
|
||||
|
||||
Here's an example of executing the COPYDEFAULT custom command that we created previously:
|
||||
|
||||
```go
|
||||
// Execute the custom COPYDEFAULT command
|
||||
res, err := server.ExecuteCommand("Module.Set", "key1", "10")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
fmt.Println(string(res))
|
||||
}
|
||||
```
|
||||
|
||||
Here's how we would exectute the same command over the TCP client-server interface:
|
||||
|
||||
```
|
||||
Module.Set key1 10
|
||||
```
|
||||
|
||||
## Unload Module
|
||||
|
||||
You can unload modules from the SugarDB instance using both the embedded and TCP APIs.
|
||||
|
||||
Here's an example of unloading a module using the embedded API:
|
||||
|
||||
```go
|
||||
// Unload custom module
|
||||
server.UnloadModule("./modules/module_set/module_set.so")
|
||||
// Unload built-in module
|
||||
server.UnloadModule("sortedset")
|
||||
```
|
||||
|
||||
Here's an example of unloading a module using the TCP interface:
|
||||
|
||||
```
|
||||
MODULE UNLOAD ./modules/module_set/module_set.so
|
||||
```
|
||||
|
||||
When unloading a module, the name should be equal to what's returned from the `ListModules` method or the `ModuleList` command.
|
||||
## Loading Modules
|
||||
<LoadModuleDocs module="go" />
|
||||
|
||||
## Important considerations
|
||||
|
@@ -36,7 +36,7 @@ const config: Config = {
|
||||
plugins: [
|
||||
function hotReload() {
|
||||
return {
|
||||
name: 'hot-reload',
|
||||
name: "hot-reload",
|
||||
configureWebpack() {
|
||||
return {
|
||||
watchOptions: {
|
||||
@@ -132,6 +132,7 @@ const config: Config = {
|
||||
copyright: `Copyright © ${new Date().getFullYear()} SugarDB.`,
|
||||
},
|
||||
prism: {
|
||||
additionalLanguages: ["lua"],
|
||||
theme: prismThemes.github,
|
||||
darkTheme: prismThemes.dracula,
|
||||
},
|
||||
|
@@ -15,11 +15,12 @@
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.2.1",
|
||||
"@docusaurus/preset-classic": "3.2.1",
|
||||
"@docusaurus/core": "3.6.3",
|
||||
"@docusaurus/plugin-content-docs": "3.6.3",
|
||||
"@docusaurus/preset-classic": "3.6.3",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"prism-react-renderer": "^2.4.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
@@ -27,6 +28,7 @@
|
||||
"@docusaurus/module-type-aliases": "3.2.1",
|
||||
"@docusaurus/tsconfig": "3.2.1",
|
||||
"@docusaurus/types": "3.2.1",
|
||||
"@types/react": "^18.3.12",
|
||||
"typescript": "~5.2.2"
|
||||
},
|
||||
"browserslist": {
|
||||
|
6145
docs/pnpm-lock.yaml
generated
6145
docs/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
156
docs/src/components/load_module.tsx
Normal file
156
docs/src/components/load_module.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import React from "react";
|
||||
import CodeBlock from "@theme/CodeBlock";
|
||||
|
||||
const LoadModuleDocs = ({ module }: { module: "lua" | "go" }) => {
|
||||
const module_path =
|
||||
module === "lua" ? "path/to/module/module.lua" : "path/to/module/module.so";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>You can load modules in 3 ways:</p>
|
||||
|
||||
<h3 id="section-3-1">1. At startup with the `--loadmodule` flag.</h3>
|
||||
<p>
|
||||
Upon startup you can provide the flag {module_path}. This is the path to
|
||||
the module's file. You can pass this flag multiple times to load
|
||||
multiple modules on startup.
|
||||
</p>
|
||||
|
||||
<h3 id="section-3-2">2. At runtime with the `MODULE LOAD` command.</h3>
|
||||
<p>
|
||||
You can load modules dynamically at runtime using the `MODULE LOAD`
|
||||
command as follows:
|
||||
</p>
|
||||
<CodeBlock language={"sh"}>{`MODULE LOAD ${module_path}`}</CodeBlock>
|
||||
<p>
|
||||
This command only takes one path so if you have multiple modules to
|
||||
load, You will have to load them one at a time.
|
||||
</p>
|
||||
|
||||
<h3>3. At runtime the `LoadModule` method.</h3>
|
||||
<p>
|
||||
You can load a module .so file dynamically at runtime using the{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://pkg.go.dev/github.com/echovault/echovault@v0.10.1/echovault#EchoVault.LoadModule"
|
||||
>
|
||||
`LoadModule`
|
||||
</a>{" "}
|
||||
method in the embedded API.
|
||||
</p>
|
||||
<CodeBlock
|
||||
language={"go"}
|
||||
>{`err = server.LoadModule("${module_path}")`}</CodeBlock>
|
||||
|
||||
<h3>Loading Module with Args</h3>
|
||||
<p>
|
||||
You might have notices the `args ...string` variadic parameter when
|
||||
creating a module. This a list of args that are passed to the module's
|
||||
key extraction and handler functions.
|
||||
</p>
|
||||
<p>
|
||||
The values passed here are established once when loading the module, and
|
||||
the same values will be passed to the respective functions everytime the
|
||||
command is executed.
|
||||
</p>
|
||||
<p>
|
||||
If you don't provide any args, an empty slice will be passed in the args
|
||||
parameter. Otehrwise, a slice containing your defined args will be used.
|
||||
</p>
|
||||
<p>To load a module with args using the embedded API: </p>
|
||||
<CodeBlock language={"go"}>
|
||||
{`err = server.LoadModule("${module_path}", "list", "of", "args")`}
|
||||
</CodeBlock>
|
||||
<p>To load a module with args using the `MODULE LOAD` command:</p>
|
||||
<CodeBlock language={"sh"}>
|
||||
{`MODULE LOAD ${module_path} arg1 arg2 arg3`}
|
||||
</CodeBlock>
|
||||
<p>
|
||||
NOTE: You cannot pass args when loading modules at startup with the
|
||||
`--loadmodule` flag.
|
||||
</p>
|
||||
|
||||
<h2>List Modules</h2>
|
||||
<p>
|
||||
You can list the current modules loaded in the SugarDB instance using
|
||||
both the Client-Server and embedded APIs.
|
||||
</p>
|
||||
<p>
|
||||
To check the loaded modules using the embedded API, use the{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://pkg.go.dev/github.com/echovault/echovault@v0.10.1/echovault#EchoVault.ListModules"
|
||||
>
|
||||
`ListModules`
|
||||
</a>{" "}
|
||||
method:
|
||||
</p>
|
||||
<CodeBlock language={"go"}>{`modules := server.ListModules()`}</CodeBlock>
|
||||
<p>
|
||||
This method returns a string slice containing all the loaded modules in
|
||||
the SugarDB instance.
|
||||
</p>
|
||||
<p>
|
||||
You can also list the loaded modules over the TCP API using the `MODULE
|
||||
LIST` command.
|
||||
</p>
|
||||
<p>Here's an example response of the loaded modules:</p>
|
||||
<CodeBlock language={"sh"}>{`1) "acl"
|
||||
2) "admin"
|
||||
3) "connection"
|
||||
4) "generic"
|
||||
5) "hash"
|
||||
6) "list"
|
||||
7) "pubsub"
|
||||
8) "set"
|
||||
9) "sortedset"
|
||||
10) "string"
|
||||
11) "${module_path}"`}</CodeBlock>
|
||||
<p>
|
||||
Notice that the modules loaded from .so files have their respective file
|
||||
names as the module name.
|
||||
</p>
|
||||
|
||||
<h2>Execute Module Command</h2>
|
||||
<p>
|
||||
Here's an example of executing the `Module.Set` command with the
|
||||
embedded API:
|
||||
</p>
|
||||
<p>
|
||||
Here's an example of executing the COPYDEFAULT custom command that we
|
||||
created previously:
|
||||
</p>
|
||||
<CodeBlock language={"go"}>{`// Execute the custom COPYDEFAULT command
|
||||
res, err := server.ExecuteCommand("Module.Set", "key1", "10")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
fmt.Println(string(res))
|
||||
}`}</CodeBlock>
|
||||
<p>
|
||||
Here's how we would exectute the same command over the TCP client-server
|
||||
interface:
|
||||
</p>
|
||||
<CodeBlock language={"sh"}>{`Module.Set key1 10`}</CodeBlock>
|
||||
|
||||
<h2>Unload Module</h2>
|
||||
<p>
|
||||
You can unload modules from the SugarDB instance using both the embedded
|
||||
and TCP APIs.
|
||||
</p>
|
||||
<p>Here's an example of unloading a module using the embedded API:</p>
|
||||
<CodeBlock language="go">{`// Unload custom module
|
||||
server.UnloadModule("${module_path}")
|
||||
// Unload built-in module
|
||||
server.UnloadModule("sortedset")`}</CodeBlock>
|
||||
<p>Here's an example of unloading a module using the TCP interface:</p>
|
||||
<CodeBlock language="sh">{`MODULE UNLOAD ${module_path}`}</CodeBlock>
|
||||
<p>
|
||||
When unloading a module, the name should be equal to what's returned
|
||||
from the `ListModules` method or the `ModuleList` command.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadModuleDocs;
|
8461
docs/yarn.lock
8461
docs/yarn.lock
File diff suppressed because it is too large
Load Diff
18
go.mod
18
go.mod
@@ -1,15 +1,16 @@
|
||||
module github.com/echovault/sugardb
|
||||
|
||||
go 1.22.0
|
||||
go 1.23.3
|
||||
|
||||
require (
|
||||
github.com/go-test/deep v1.1.1
|
||||
github.com/gobwas/glob v0.2.3
|
||||
github.com/hashicorp/memberlist v0.5.0
|
||||
github.com/hashicorp/raft v1.5.0
|
||||
github.com/hashicorp/memberlist v0.5.1
|
||||
github.com/hashicorp/raft v1.7.1
|
||||
github.com/hashicorp/raft-boltdb v0.0.0-20230125174641-2a8082862702
|
||||
github.com/sethvargo/go-retry v0.2.4
|
||||
github.com/sethvargo/go-retry v0.3.0
|
||||
github.com/tidwall/resp v0.1.1
|
||||
github.com/yuin/gopher-lua v1.1.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -19,9 +20,10 @@ require (
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.2 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0 // indirect
|
||||
github.com/hashicorp/go-msgpack v0.5.5 // indirect
|
||||
github.com/hashicorp/go-msgpack/v2 v2.1.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.0.0 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.0 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.0 // indirect
|
||||
@@ -29,7 +31,7 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/miekg/dns v1.1.26 // indirect
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 // indirect
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 // indirect
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/net v0.16.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
)
|
||||
|
48
go.sum
48
go.sum
@@ -4,7 +4,6 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=
|
||||
github.com/armon/go-metrics v0.3.8/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
|
||||
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
|
||||
@@ -44,13 +43,14 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
||||
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-hclog v1.6.2 h1:NOtoftovWkDheyUM/8JW3QMiXyxJK3uHRK7wV04nD2I=
|
||||
github.com/hashicorp/go-hclog v1.6.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
|
||||
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-msgpack/v2 v2.1.2 h1:4Ee8FTp834e+ewB71RDrQ0VKpyFdrKOjvYtnQ/ltVj0=
|
||||
github.com/hashicorp/go-msgpack/v2 v2.1.2/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhzywQsSRV5033mMu4=
|
||||
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
@@ -60,11 +60,11 @@ github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCS
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
|
||||
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
|
||||
github.com/hashicorp/memberlist v0.5.1 h1:mk5dRuzeDNis2bi6LLoQIXfMH7JQvAzt3mQD0vNZZUo=
|
||||
github.com/hashicorp/memberlist v0.5.1/go.mod h1:zGDXV6AqbDTKTM6yxW0I4+JtFzZAJVoIPvss4hV8F24=
|
||||
github.com/hashicorp/raft v1.1.0/go.mod h1:4Ak7FSPnuvmb0GV6vgIAJ4vYT4bek9bb6Q+7HVbyzqM=
|
||||
github.com/hashicorp/raft v1.5.0 h1:uNs9EfJ4FwiArZRxxfd/dQ5d33nV31/CdCHArH89hT8=
|
||||
github.com/hashicorp/raft v1.5.0/go.mod h1:pKHB2mf/Y25u3AHNSXVRv+yT+WAnmeTX0BwVppVQV+M=
|
||||
github.com/hashicorp/raft v1.7.1 h1:ytxsNx4baHsRZrhUcbt3+79zc4ly8qm7pi0393pSchY=
|
||||
github.com/hashicorp/raft v1.7.1/go.mod h1:hUeiEwQQR/Nk2iKDD0dkEhklSsu3jcAcqvPzPoZSAEM=
|
||||
github.com/hashicorp/raft-boltdb v0.0.0-20230125174641-2a8082862702 h1:RLKEcCuKcZ+qp2VlaaZsYZfLOmIiuJNpEi48Rl8u9cQ=
|
||||
github.com/hashicorp/raft-boltdb v0.0.0-20230125174641-2a8082862702/go.mod h1:nTakvJ4XYq45UXtn0DbwR4aU9ZdjlnIenpbs6Cd+FM0=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
@@ -72,8 +72,9 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
@@ -91,7 +92,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -114,36 +114,36 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec=
|
||||
github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw=
|
||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
|
||||
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -161,8 +161,8 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -171,12 +171,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@@ -485,6 +485,7 @@ func Commands() []internal.Command {
|
||||
Categories: []string{},
|
||||
Description: "Access-Control-List commands",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0),
|
||||
|
@@ -197,6 +197,7 @@ func Commands() []internal.Command {
|
||||
Categories: []string{constants.AdminCategory, constants.SlowCategory},
|
||||
Description: "Get a list of all the commands in available on the echovault with categories and descriptions.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0),
|
||||
@@ -210,6 +211,7 @@ func Commands() []internal.Command {
|
||||
Categories: []string{},
|
||||
Description: "Commands pertaining to echovault commands",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0),
|
||||
@@ -264,6 +266,7 @@ Get the list of command names. Allows for filtering by ACL category or glob patt
|
||||
Categories: []string{constants.AdminCategory, constants.SlowCategory, constants.DangerousCategory},
|
||||
Description: "(SAVE) Trigger a snapshot save.",
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0),
|
||||
@@ -282,6 +285,7 @@ Get the list of command names. Allows for filtering by ACL category or glob patt
|
||||
Categories: []string{constants.AdminCategory, constants.FastCategory, constants.DangerousCategory},
|
||||
Description: "(LASTSAVE) Get unix timestamp for the latest snapshot in milliseconds.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0),
|
||||
@@ -301,6 +305,7 @@ Get the list of command names. Allows for filtering by ACL category or glob patt
|
||||
Categories: []string{constants.AdminCategory, constants.SlowCategory, constants.DangerousCategory},
|
||||
Description: "(REWRITEAOF) Trigger re-writing of append process.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0),
|
||||
@@ -318,6 +323,7 @@ Get the list of command names. Allows for filtering by ACL category or glob patt
|
||||
Module: constants.AdminModule,
|
||||
Categories: []string{},
|
||||
Description: "Module commands",
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0),
|
||||
|
@@ -379,6 +379,84 @@ func Test_AdminCommands(t *testing.T) {
|
||||
wantTestRes: "OK",
|
||||
wantTestErr: nil,
|
||||
},
|
||||
{
|
||||
name: "6. Load LUA example module",
|
||||
execCommand: []resp.Value{
|
||||
resp.StringValue("MODULE"),
|
||||
resp.StringValue("LOAD"),
|
||||
resp.StringValue(path.Join("..", "..", "volumes", "modules", "lua", "example.lua")),
|
||||
},
|
||||
wantExecRes: "OK",
|
||||
testCommand: []resp.Value{
|
||||
resp.StringValue("LUA.EXAMPLE"),
|
||||
},
|
||||
wantTestRes: "OK",
|
||||
wantTestErr: nil,
|
||||
},
|
||||
{
|
||||
name: "7. Load LUA hash module",
|
||||
execCommand: []resp.Value{
|
||||
resp.StringValue("MODULE"),
|
||||
resp.StringValue("LOAD"),
|
||||
resp.StringValue(path.Join("..", "..", "volumes", "modules", "lua", "hash.lua")),
|
||||
},
|
||||
wantExecRes: "OK",
|
||||
testCommand: []resp.Value{
|
||||
resp.StringValue("LUA.HASH"),
|
||||
resp.StringValue("LUA.HASH_KEY_1"),
|
||||
},
|
||||
wantTestRes: "OK",
|
||||
wantTestErr: nil,
|
||||
},
|
||||
{
|
||||
name: "8. Load LUA set module",
|
||||
execCommand: []resp.Value{
|
||||
resp.StringValue("MODULE"),
|
||||
resp.StringValue("LOAD"),
|
||||
resp.StringValue(path.Join("..", "..", "volumes", "modules", "lua", "set.lua")),
|
||||
},
|
||||
wantExecRes: "OK",
|
||||
testCommand: []resp.Value{
|
||||
resp.StringValue("LUA.SET"),
|
||||
resp.StringValue("LUA.SET_KEY_1"),
|
||||
resp.StringValue("LUA.SET_KEY_2"),
|
||||
resp.StringValue("LUA.SET_KEY_3"),
|
||||
},
|
||||
wantTestRes: "OK",
|
||||
wantTestErr: nil,
|
||||
},
|
||||
{
|
||||
name: "9. Load LUA zset module",
|
||||
execCommand: []resp.Value{
|
||||
resp.StringValue("MODULE"),
|
||||
resp.StringValue("LOAD"),
|
||||
resp.StringValue(path.Join("..", "..", "volumes", "modules", "lua", "zset.lua")),
|
||||
},
|
||||
wantExecRes: "OK",
|
||||
testCommand: []resp.Value{
|
||||
resp.StringValue("LUA.ZSET"),
|
||||
resp.StringValue("LUA.ZSET_KEY_1"),
|
||||
resp.StringValue("LUA.ZSET_KEY_2"),
|
||||
resp.StringValue("LUA.ZSET_KEY_3"),
|
||||
},
|
||||
wantTestRes: "OK",
|
||||
wantTestErr: nil,
|
||||
},
|
||||
{
|
||||
name: "10. Load LUA list module",
|
||||
execCommand: []resp.Value{
|
||||
resp.StringValue("MODULE"),
|
||||
resp.StringValue("LOAD"),
|
||||
resp.StringValue(path.Join("..", "..", "volumes", "modules", "lua", "list.lua")),
|
||||
},
|
||||
wantExecRes: "OK",
|
||||
testCommand: []resp.Value{
|
||||
resp.StringValue("LUA.LIST"),
|
||||
resp.StringValue("LUA.LIST_KEY_1"),
|
||||
},
|
||||
wantTestRes: "OK",
|
||||
wantTestErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
conn, err := internal.GetConnection("localhost", port)
|
||||
|
@@ -181,6 +181,7 @@ func Commands() []internal.Command {
|
||||
Authenticates the connection. If the username is not provided, the connection will be authenticated against the
|
||||
default ACL user. Otherwise, it is authenticated against the ACL user with the provided username.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0),
|
||||
@@ -198,6 +199,7 @@ default ACL user. Otherwise, it is authenticated against the ACL user with the p
|
||||
Ping the echovault server. If a message is provided, the message will be echoed back to the client.
|
||||
Otherwise, the server will return "PONG".`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0),
|
||||
@@ -213,6 +215,7 @@ Otherwise, the server will return "PONG".`,
|
||||
Categories: []string{constants.ConnectionCategory, constants.FastCategory},
|
||||
Description: `(ECHO message) Echo the message back to the client.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0),
|
||||
@@ -230,6 +233,7 @@ Otherwise, the server will return "PONG".`,
|
||||
Switch to a different protocol, optionally authenticating and setting the connection's name.
|
||||
This command returns a contextual client report.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0),
|
||||
@@ -245,6 +249,7 @@ This command returns a contextual client report.`,
|
||||
Categories: []string{constants.FastCategory, constants.ConnectionCategory},
|
||||
Description: `(SELECT index) Change the logical database that the current connection is operating from.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0),
|
||||
@@ -268,6 +273,7 @@ This command swaps two databases,
|
||||
so that immediately all the clients connected to a given database will see the data of the other database,
|
||||
and the other way around.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0),
|
||||
|
@@ -686,7 +686,7 @@ func handleRenamenx(params internal.HandlerFuncParams) ([]byte, error) {
|
||||
|
||||
keyExistsCheck := params.KeysExist(params.Context, []string{newKey})
|
||||
if keyExistsCheck[newKey] {
|
||||
return nil, errors.New("Key already exists!")
|
||||
return nil, fmt.Errorf("key %s already exists", newKey)
|
||||
}
|
||||
|
||||
return handleRename(params)
|
||||
@@ -707,9 +707,9 @@ func handleFlush(params internal.HandlerFuncParams) ([]byte, error) {
|
||||
return []byte(constants.OkResponse), nil
|
||||
}
|
||||
|
||||
func handleRandomkey(params internal.HandlerFuncParams) ([]byte, error) {
|
||||
func handleRandomKey(params internal.HandlerFuncParams) ([]byte, error) {
|
||||
|
||||
key := params.Randomkey(params.Context)
|
||||
key := params.RandomKey(params.Context)
|
||||
|
||||
return []byte(fmt.Sprintf("+%v\r\n", key)), nil
|
||||
}
|
||||
@@ -853,7 +853,7 @@ func handleTouch(params internal.HandlerFuncParams) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
touchedKeys, err := params.Touchkey(params.Context, keys.ReadKeys)
|
||||
touchedKeys, err := params.TouchKey(params.Context, keys.ReadKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -996,6 +996,7 @@ PX - Expire the key after the specified number of milliseconds (positive integer
|
||||
EXAT - Expire at the exact time in unix seconds (positive integer).
|
||||
PXAT - Expire at the exat time in unix milliseconds (positive integer).`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: setKeyFunc,
|
||||
HandlerFunc: handleSet,
|
||||
},
|
||||
@@ -1005,6 +1006,7 @@ PXAT - Expire at the exat time in unix milliseconds (positive integer).`,
|
||||
Categories: []string{constants.WriteCategory, constants.SlowCategory},
|
||||
Description: "(MSET key value [key value ...]) Automatically set or modify multiple key/value pairs.",
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: msetKeyFunc,
|
||||
HandlerFunc: handleMSet,
|
||||
},
|
||||
@@ -1014,6 +1016,7 @@ PXAT - Expire at the exat time in unix milliseconds (positive integer).`,
|
||||
Categories: []string{constants.ReadCategory, constants.FastCategory},
|
||||
Description: "(GET key) Get the value at the specified key.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: getKeyFunc,
|
||||
HandlerFunc: handleGet,
|
||||
},
|
||||
@@ -1023,6 +1026,7 @@ PXAT - Expire at the exat time in unix milliseconds (positive integer).`,
|
||||
Categories: []string{constants.ReadCategory, constants.FastCategory},
|
||||
Description: "(MGET key [key ...]) Get multiple values from the specified keys.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: mgetKeyFunc,
|
||||
HandlerFunc: handleMGet,
|
||||
},
|
||||
@@ -1032,6 +1036,7 @@ PXAT - Expire at the exat time in unix milliseconds (positive integer).`,
|
||||
Categories: []string{constants.KeyspaceCategory, constants.WriteCategory, constants.FastCategory},
|
||||
Description: "(DEL key [key ...]) Removes one or more keys from the store.",
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: delKeyFunc,
|
||||
HandlerFunc: handleDel,
|
||||
},
|
||||
@@ -1042,6 +1047,7 @@ PXAT - Expire at the exat time in unix milliseconds (positive integer).`,
|
||||
Description: `(PERSIST key) Removes the TTl associated with a key,
|
||||
turning it from a volatile key to a persistent key.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: persistKeyFunc,
|
||||
HandlerFunc: handlePersist,
|
||||
},
|
||||
@@ -1053,6 +1059,7 @@ turning it from a volatile key to a persistent key.`,
|
||||
Return -1 if the key exists but has no associated expiry time.
|
||||
Returns -2 if the key does not exist.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: expireTimeKeyFunc,
|
||||
HandlerFunc: handleExpireTime,
|
||||
},
|
||||
@@ -1064,6 +1071,7 @@ Returns -2 if the key does not exist.`,
|
||||
Return -1 if the key exists but has no associated expiry time.
|
||||
Returns -2 if the key does not exist.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: expireTimeKeyFunc,
|
||||
HandlerFunc: handleExpireTime,
|
||||
},
|
||||
@@ -1075,6 +1083,7 @@ Returns -2 if the key does not exist.`,
|
||||
If the key exists but does not have an associated expiry time, -1 is returned.
|
||||
If the key does not exist, -2 is returned.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: ttlKeyFunc,
|
||||
HandlerFunc: handleTTL,
|
||||
},
|
||||
@@ -1086,6 +1095,7 @@ If the key does not exist, -2 is returned.`,
|
||||
If the key exists but does not have an associated expiry time, -1 is returned.
|
||||
If the key does not exist, -2 is returned.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: ttlKeyFunc,
|
||||
HandlerFunc: handleTTL,
|
||||
},
|
||||
@@ -1100,6 +1110,7 @@ XX - Only set the expiry time if the key already has an expiry time.
|
||||
GT - Only set the expiry time if the new expiry time is greater than the current one.
|
||||
LT - Only set the expiry time if the new expiry time is less than the current one.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: expireKeyFunc,
|
||||
HandlerFunc: handleExpire,
|
||||
},
|
||||
@@ -1114,6 +1125,7 @@ XX - Only set the expiry time if the key already has an expiry time.
|
||||
GT - Only set the expiry time if the new expiry time is greater than the current one.
|
||||
LT - Only set the expiry time if the new expiry time is less than the current one.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: expireKeyFunc,
|
||||
HandlerFunc: handleExpire,
|
||||
},
|
||||
@@ -1129,6 +1141,7 @@ XX - Only set the expiry time if the key already has an expiry time.
|
||||
GT - Only set the expiry time if the new expiry time is greater than the current one.
|
||||
LT - Only set the expiry time if the new expiry time is less than the current one.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: expireAtKeyFunc,
|
||||
HandlerFunc: handleExpireAt,
|
||||
},
|
||||
@@ -1144,6 +1157,7 @@ XX - Only set the expiry time if the key already has an expiry time.
|
||||
GT - Only set the expiry time if the new expiry time is greater than the current one.
|
||||
LT - Only set the expiry time if the new expiry time is less than the current one.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: expireAtKeyFunc,
|
||||
HandlerFunc: handleExpireAt,
|
||||
},
|
||||
@@ -1156,6 +1170,7 @@ Increments the number stored at key by one. If the key does not exist, it is set
|
||||
An error is returned if the key contains a value of the wrong type or contains a string that cannot be represented as integer.
|
||||
This operation is limited to 64 bit signed integers.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: incrKeyFunc,
|
||||
HandlerFunc: handleIncr,
|
||||
},
|
||||
@@ -1169,6 +1184,7 @@ If the key does not exist, it is set to 0 before performing the operation.
|
||||
An error is returned if the key contains a value of the wrong type or contains a string that cannot be represented as integer.
|
||||
This operation is limited to 64 bit signed integers.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: decrKeyFunc,
|
||||
HandlerFunc: handleDecr,
|
||||
},
|
||||
@@ -1180,6 +1196,7 @@ This operation is limited to 64 bit signed integers.`,
|
||||
Increments the number stored at key by increment. If the key does not exist, it is set to 0 before performing the operation.
|
||||
An error is returned if the key contains a value of the wrong type or contains a string that can not be represented as integer.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: incrByKeyFunc,
|
||||
HandlerFunc: handleIncrBy,
|
||||
},
|
||||
@@ -1191,6 +1208,7 @@ An error is returned if the key contains a value of the wrong type or contains a
|
||||
Increments the number stored at key by increment. If the key does not exist, it is set to 0 before performing the operation.
|
||||
An error is returned if the key contains a value of the wrong type or contains a string that cannot be represented as float.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: incrByFloatKeyFunc,
|
||||
HandlerFunc: handleIncrByFloat,
|
||||
},
|
||||
@@ -1203,6 +1221,7 @@ The DECRBY command reduces the value stored at the specified key by the specifie
|
||||
If the key does not exist, it is initialized with a value of 0 before performing the operation.
|
||||
If the key's value is not of the correct type or cannot be represented as an integer, an error is returned.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: decrByKeyFunc,
|
||||
HandlerFunc: handleDecrBy,
|
||||
},
|
||||
@@ -1213,6 +1232,7 @@ If the key's value is not of the correct type or cannot be represented as an int
|
||||
Description: `(RENAME key newkey)
|
||||
Renames key to newkey. If newkey already exists, it is overwritten. If key does not exist, an error is returned.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: renameKeyFunc,
|
||||
HandlerFunc: handleRename,
|
||||
},
|
||||
@@ -1227,6 +1247,7 @@ Renames key to newkey. If newkey already exists, it is overwritten. If key does
|
||||
},
|
||||
Description: `(FLUSHALL) Delete all the keys in all the existing databases. This command is always synchronous.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0),
|
||||
@@ -1246,6 +1267,7 @@ Renames key to newkey. If newkey already exists, it is overwritten. If key does
|
||||
Description: `(FLUSHDB)
|
||||
Delete all the keys in the currently selected database. This command is always synchronous.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0), ReadKeys: make([]string, 0), WriteKeys: make([]string, 0),
|
||||
@@ -1259,8 +1281,9 @@ Delete all the keys in the currently selected database. This command is always s
|
||||
Categories: []string{constants.KeyspaceCategory, constants.ReadCategory, constants.SlowCategory},
|
||||
Description: "(RANDOMKEY) Returns a random key from the current selected database.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: randomKeyFunc,
|
||||
HandlerFunc: handleRandomkey,
|
||||
HandlerFunc: handleRandomKey,
|
||||
},
|
||||
{
|
||||
Command: "getdel",
|
||||
@@ -1268,6 +1291,7 @@ Delete all the keys in the currently selected database. This command is always s
|
||||
Categories: []string{constants.WriteCategory, constants.FastCategory},
|
||||
Description: "(GETDEL key) Get the value of key and delete the key. This command is similar to [GET], but deletes key on success.",
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: getDelKeyFunc,
|
||||
HandlerFunc: handleGetdel,
|
||||
},
|
||||
@@ -1277,6 +1301,7 @@ Delete all the keys in the currently selected database. This command is always s
|
||||
Categories: []string{constants.WriteCategory, constants.FastCategory},
|
||||
Description: "(GETEX key [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST]) Get the value of key and optionally set its expiration. GETEX is similar to [GET], but is a write command with additional options.",
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: getExKeyFunc,
|
||||
HandlerFunc: handleGetex,
|
||||
},
|
||||
@@ -1286,6 +1311,7 @@ Delete all the keys in the currently selected database. This command is always s
|
||||
Categories: []string{constants.KeyspaceCategory, constants.ReadCategory, constants.FastCategory},
|
||||
Description: "(TYPE key) Returns the string representation of the type of the value stored at key. The different types that can be returned are: string, integer, float, list, set, zset, and hash.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: typeKeyFunc,
|
||||
HandlerFunc: handleType,
|
||||
},
|
||||
@@ -1296,6 +1322,7 @@ Delete all the keys in the currently selected database. This command is always s
|
||||
Description: `(TOUCH keys [key ...]) Alters the last access time or access count of the key(s) depending on whether LFU or LRU strategy was used.
|
||||
A key is ignored if it does not exist. This commands returns the number of keys that were touched.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: touchKeyFunc,
|
||||
HandlerFunc: handleTouch,
|
||||
},
|
||||
@@ -1306,6 +1333,7 @@ A key is ignored if it does not exist. This commands returns the number of keys
|
||||
Description: `(OBJECTFREQ key) Get the access frequency count of an object stored at <key>.
|
||||
The command is only available when the maxmemory-policy configuration directive is set to one of the LFU policies.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: objFreqKeyFunc,
|
||||
HandlerFunc: handleObjFreq,
|
||||
},
|
||||
@@ -1316,6 +1344,7 @@ The command is only available when the maxmemory-policy configuration directive
|
||||
Description: `(OBJECTIDLETIME key) Get the time in seconds since the last access to the value stored at <key>.
|
||||
The command is only available when the maxmemory-policy configuration directive is set to one of the LRU policies.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: objIdleTimeKeyFunc,
|
||||
HandlerFunc: handleObjIdleTime,
|
||||
},
|
||||
|
@@ -2672,7 +2672,7 @@ func Test_Generic(t *testing.T) {
|
||||
presetValue: "value3",
|
||||
command: []resp.Value{resp.StringValue("RENAMENX"), resp.StringValue("renamenxOldKey3"), resp.StringValue("renamenxNewKey1")},
|
||||
expectedResponse: "",
|
||||
expectedError: errors.New("Key already exists!"),
|
||||
expectedError: errors.New("key renamenxNewKey1 already exists"),
|
||||
},
|
||||
{
|
||||
name: "4. Command too short",
|
||||
|
@@ -838,6 +838,7 @@ func Commands() []internal.Command {
|
||||
Description: `(HSET key field value [field value ...])
|
||||
Set update each field of the hash with the corresponding value.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: hsetKeyFunc,
|
||||
HandlerFunc: handleHSET,
|
||||
},
|
||||
@@ -848,6 +849,7 @@ Set update each field of the hash with the corresponding value.`,
|
||||
Description: `(HSETNX key field value [field value ...])
|
||||
Set hash field value only if the field does not exist.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: hsetnxKeyFunc,
|
||||
HandlerFunc: handleHSET,
|
||||
},
|
||||
@@ -858,6 +860,7 @@ Set hash field value only if the field does not exist.`,
|
||||
Description: `(HGET key field [field ...])
|
||||
Retrieve the value of each of the listed fields from the hash.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: hgetKeyFunc,
|
||||
HandlerFunc: handleHGET,
|
||||
},
|
||||
@@ -868,6 +871,7 @@ Retrieve the value of each of the listed fields from the hash.`,
|
||||
Description: `(HMGET key field [field ...])
|
||||
Retrieve the value of each of the listed fields from the hash.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: hmgetKeyFunc,
|
||||
HandlerFunc: handleHMGET,
|
||||
},
|
||||
@@ -878,6 +882,7 @@ Retrieve the value of each of the listed fields from the hash.`,
|
||||
Description: `(HSTRLEN key field [field ...])
|
||||
Return the string length of the values stored at the specified fields. 0 if the value does not exist.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: hstrlenKeyFunc,
|
||||
HandlerFunc: handleHSTRLEN,
|
||||
},
|
||||
@@ -887,6 +892,7 @@ Return the string length of the values stored at the specified fields. 0 if the
|
||||
Categories: []string{constants.HashCategory, constants.ReadCategory, constants.SlowCategory},
|
||||
Description: `(HVALS key) Returns all the values of the hash at key.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: hvalsKeyFunc,
|
||||
HandlerFunc: handleHVALS,
|
||||
},
|
||||
@@ -896,6 +902,7 @@ Return the string length of the values stored at the specified fields. 0 if the
|
||||
Categories: []string{constants.HashCategory, constants.ReadCategory, constants.SlowCategory},
|
||||
Description: `(HRANDFIELD key [count [WITHVALUES]]) Returns one or more random fields from the hash.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: hrandfieldKeyFunc,
|
||||
HandlerFunc: handleHRANDFIELD,
|
||||
},
|
||||
@@ -905,6 +912,7 @@ Return the string length of the values stored at the specified fields. 0 if the
|
||||
Categories: []string{constants.HashCategory, constants.ReadCategory, constants.FastCategory},
|
||||
Description: `(HLEN key) Returns the number of fields in the hash.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: hlenKeyFunc,
|
||||
HandlerFunc: handleHLEN,
|
||||
},
|
||||
@@ -914,6 +922,7 @@ Return the string length of the values stored at the specified fields. 0 if the
|
||||
Categories: []string{constants.HashCategory, constants.ReadCategory, constants.SlowCategory},
|
||||
Description: `(HKEYS key) Returns all the fields in a hash.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: hkeysKeyFunc,
|
||||
HandlerFunc: handleHKEYS,
|
||||
},
|
||||
@@ -923,6 +932,7 @@ Return the string length of the values stored at the specified fields. 0 if the
|
||||
Categories: []string{constants.HashCategory, constants.WriteCategory, constants.FastCategory},
|
||||
Description: `(HINCRBYFLOAT key field increment) Increment the hash value by the float increment.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: hincrbyKeyFunc,
|
||||
HandlerFunc: handleHINCRBY,
|
||||
},
|
||||
@@ -932,6 +942,7 @@ Return the string length of the values stored at the specified fields. 0 if the
|
||||
Categories: []string{constants.HashCategory, constants.WriteCategory, constants.FastCategory},
|
||||
Description: `(HINCRBY key field increment) Increment the hash value by the integer increment`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: hincrbyKeyFunc,
|
||||
HandlerFunc: handleHINCRBY,
|
||||
},
|
||||
@@ -941,6 +952,7 @@ Return the string length of the values stored at the specified fields. 0 if the
|
||||
Categories: []string{constants.HashCategory, constants.ReadCategory, constants.SlowCategory},
|
||||
Description: `(HGETALL key) Get all fields and values of a hash.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: hgetallKeyFunc,
|
||||
HandlerFunc: handleHGETALL,
|
||||
},
|
||||
@@ -950,6 +962,7 @@ Return the string length of the values stored at the specified fields. 0 if the
|
||||
Categories: []string{constants.HashCategory, constants.ReadCategory, constants.FastCategory},
|
||||
Description: `(HEXISTS key field) Returns if field is an existing field in the hash.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: hexistsKeyFunc,
|
||||
HandlerFunc: handleHEXISTS,
|
||||
},
|
||||
@@ -959,6 +972,7 @@ Return the string length of the values stored at the specified fields. 0 if the
|
||||
Categories: []string{constants.HashCategory, constants.WriteCategory, constants.FastCategory},
|
||||
Description: `(HDEL key field [field ...]) Deletes the specified fields from the hash.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: hdelKeyFunc,
|
||||
HandlerFunc: handleHDEL,
|
||||
},
|
||||
|
@@ -507,6 +507,7 @@ func Commands() []internal.Command {
|
||||
Description: `(LPUSH key element [element ...])
|
||||
Prepends one or more values to the beginning of a list, creates the list if it does not exist.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: lpushKeyFunc,
|
||||
HandlerFunc: handleLPush,
|
||||
},
|
||||
@@ -517,6 +518,7 @@ Prepends one or more values to the beginning of a list, creates the list if it d
|
||||
Description: `(LPUSHX key element [element ...])
|
||||
Prepends a value to the beginning of a list only if the list exists.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: lpushKeyFunc,
|
||||
HandlerFunc: handleLPush,
|
||||
},
|
||||
@@ -529,6 +531,7 @@ Removes count elements from the beginning of the list and returns an array of th
|
||||
Returns a bulk string of the first element when called without count.
|
||||
Returns an array of n elements from the beginning of the list when called with a count when n=count. `,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: popKeyFunc,
|
||||
HandlerFunc: handlePop,
|
||||
},
|
||||
@@ -538,6 +541,7 @@ Returns an array of n elements from the beginning of the list when called with a
|
||||
Categories: []string{constants.ListCategory, constants.ReadCategory, constants.FastCategory},
|
||||
Description: "(LLEN key) Return the length of a list.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: llenKeyFunc,
|
||||
HandlerFunc: handleLLen,
|
||||
},
|
||||
@@ -547,6 +551,7 @@ Returns an array of n elements from the beginning of the list when called with a
|
||||
Categories: []string{constants.ListCategory, constants.ReadCategory, constants.SlowCategory},
|
||||
Description: "(LRANGE key start end) Return a range of elements between the given indices.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: lrangeKeyFunc,
|
||||
HandlerFunc: handleLRange,
|
||||
},
|
||||
@@ -556,6 +561,7 @@ Returns an array of n elements from the beginning of the list when called with a
|
||||
Categories: []string{constants.ListCategory, constants.ReadCategory, constants.FastCategory},
|
||||
Description: "(LINDEX key index) Gets list element by index.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: lindexKeyFunc,
|
||||
HandlerFunc: handleLIndex,
|
||||
},
|
||||
@@ -565,6 +571,7 @@ Returns an array of n elements from the beginning of the list when called with a
|
||||
Categories: []string{constants.ListCategory, constants.WriteCategory, constants.FastCategory},
|
||||
Description: "(LSET key index element) Sets the value of an element in a list by its index.",
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: lsetKeyFunc,
|
||||
HandlerFunc: handleLSet,
|
||||
},
|
||||
@@ -574,6 +581,7 @@ Returns an array of n elements from the beginning of the list when called with a
|
||||
Categories: []string{constants.ListCategory, constants.WriteCategory, constants.SlowCategory},
|
||||
Description: "(LTRIM key start end) Trims a list using the specified range.",
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: ltrimKeyFunc,
|
||||
HandlerFunc: handleLTrim,
|
||||
},
|
||||
@@ -583,6 +591,7 @@ Returns an array of n elements from the beginning of the list when called with a
|
||||
Categories: []string{constants.ListCategory, constants.WriteCategory, constants.SlowCategory},
|
||||
Description: "(LREM key count element) Remove <count> elements from list.",
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: lremKeyFunc,
|
||||
HandlerFunc: handleLRem,
|
||||
},
|
||||
@@ -593,6 +602,7 @@ Returns an array of n elements from the beginning of the list when called with a
|
||||
Description: `(LMOVE source destination <LEFT | RIGHT> <LEFT | RIGHT>)
|
||||
Move element from one list to the other specifying left/right for both lists.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: lmoveKeyFunc,
|
||||
HandlerFunc: handleLMove,
|
||||
},
|
||||
@@ -605,6 +615,7 @@ Removes count elements from the end of the list and returns an array of the elem
|
||||
Returns a bulk string of the last element when called without count.
|
||||
Returns an array of n elements from the end of the list when called with a count when n=count.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: popKeyFunc,
|
||||
HandlerFunc: handlePop,
|
||||
},
|
||||
@@ -614,6 +625,7 @@ Returns an array of n elements from the end of the list when called with a count
|
||||
Categories: []string{constants.ListCategory, constants.WriteCategory, constants.FastCategory},
|
||||
Description: "(RPUSH key element [element ...]) Appends one or multiple elements to the end of a list.",
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: rpushKeyFunc,
|
||||
HandlerFunc: handleRPush,
|
||||
},
|
||||
@@ -623,6 +635,7 @@ Returns an array of n elements from the end of the list when called with a count
|
||||
Categories: []string{constants.ListCategory, constants.WriteCategory, constants.FastCategory},
|
||||
Description: "(RPUSHX key element [element ...]) Appends an element to the end of a list, only if the list exists.",
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: rpushKeyFunc,
|
||||
HandlerFunc: handleRPush,
|
||||
},
|
||||
|
@@ -108,6 +108,7 @@ func Commands() []internal.Command {
|
||||
Categories: []string{constants.PubSubCategory, constants.ConnectionCategory, constants.SlowCategory},
|
||||
Description: "(SUBSCRIBE channel [channel ...]) Subscribe to one or more channels.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
// Treat the channels as keys
|
||||
if len(cmd) < 2 {
|
||||
@@ -127,6 +128,7 @@ func Commands() []internal.Command {
|
||||
Categories: []string{constants.PubSubCategory, constants.ConnectionCategory, constants.SlowCategory},
|
||||
Description: "(PSUBSCRIBE pattern [pattern ...]) Subscribe to one or more glob patterns.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
// Treat the patterns as keys
|
||||
if len(cmd) < 2 {
|
||||
@@ -146,6 +148,7 @@ func Commands() []internal.Command {
|
||||
Categories: []string{constants.PubSubCategory, constants.FastCategory},
|
||||
Description: "(PUBLISH channel message) Publish a message to the specified channel.",
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
// Treat the channel as a key
|
||||
if len(cmd) != 3 {
|
||||
@@ -167,6 +170,7 @@ func Commands() []internal.Command {
|
||||
If the channel list is not provided, then the connection will be unsubscribed from all the channels that
|
||||
it's currently subscribe to.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
// Treat the channels as keys
|
||||
return internal.KeyExtractionFuncResult{
|
||||
@@ -185,6 +189,7 @@ it's currently subscribe to.`,
|
||||
If the pattern list is not provided, then the connection will be unsubscribed from all the patterns that
|
||||
it's currently subscribe to.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: cmd[1:],
|
||||
@@ -200,6 +205,7 @@ it's currently subscribe to.`,
|
||||
Categories: []string{},
|
||||
Description: "",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0),
|
||||
|
@@ -576,6 +576,7 @@ func Commands() []internal.Command {
|
||||
Description: `(SADD key member [member...])
|
||||
Add one or more members to the set. If the set does not exist, it's created.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: saddKeyFunc,
|
||||
HandlerFunc: handleSADD,
|
||||
},
|
||||
@@ -585,6 +586,7 @@ Add one or more members to the set. If the set does not exist, it's created.`,
|
||||
Categories: []string{constants.SetCategory, constants.WriteCategory, constants.FastCategory},
|
||||
Description: "(SCARD key) Returns the cardinality of the set.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: scardKeyFunc,
|
||||
HandlerFunc: handleSCARD,
|
||||
},
|
||||
@@ -596,6 +598,7 @@ Add one or more members to the set. If the set does not exist, it's created.`,
|
||||
If the first key provided is the only valid set, then this key's set will be returned as the result.
|
||||
All keys that are non-existed or hold values that are not sets will be skipped.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: sdiffKeyFunc,
|
||||
HandlerFunc: handleSDIFF,
|
||||
},
|
||||
@@ -606,6 +609,7 @@ All keys that are non-existed or hold values that are not sets will be skipped.`
|
||||
Description: `(SDIFFSTORE destination key [key...]) Works the same as SDIFF but also stores the result at 'destination'.
|
||||
Returns the cardinality of the new set.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: sdiffstoreKeyFunc,
|
||||
HandlerFunc: handleSDIFFSTORE,
|
||||
},
|
||||
@@ -615,6 +619,7 @@ Returns the cardinality of the new set.`,
|
||||
Categories: []string{constants.SetCategory, constants.WriteCategory, constants.SlowCategory},
|
||||
Description: "(SINTER key [key...]) Returns the intersection of multiple sets.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: sinterKeyFunc,
|
||||
HandlerFunc: handleSINTER,
|
||||
},
|
||||
@@ -625,6 +630,7 @@ Returns the cardinality of the new set.`,
|
||||
Description: `(SINTERCARD key [key...] [LIMIT limit])
|
||||
Returns the cardinality of the intersection between multiple sets.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: sintercardKeyFunc,
|
||||
HandlerFunc: handleSINTERCARD,
|
||||
},
|
||||
@@ -634,6 +640,7 @@ Returns the cardinality of the intersection between multiple sets.`,
|
||||
Categories: []string{constants.SetCategory, constants.WriteCategory, constants.SlowCategory},
|
||||
Description: "(SINTERSTORE destination key [key...]) Stores the intersection of multiple sets at the destination key.",
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: sinterstoreKeyFunc,
|
||||
HandlerFunc: handleSINTERSTORE,
|
||||
},
|
||||
@@ -643,6 +650,7 @@ Returns the cardinality of the intersection between multiple sets.`,
|
||||
Categories: []string{constants.SetCategory, constants.ReadCategory, constants.FastCategory},
|
||||
Description: "(SISMEMBER key member) Returns if member is contained in the set.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: sismemberKeyFunc,
|
||||
HandlerFunc: handleSISMEMBER,
|
||||
},
|
||||
@@ -652,6 +660,7 @@ Returns the cardinality of the intersection between multiple sets.`,
|
||||
Categories: []string{constants.SetCategory, constants.ReadCategory, constants.SlowCategory},
|
||||
Description: "(SMEMBERS key) Returns all members of a set.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: smembersKeyFunc,
|
||||
HandlerFunc: handleSMEMBERS,
|
||||
},
|
||||
@@ -661,6 +670,7 @@ Returns the cardinality of the intersection between multiple sets.`,
|
||||
Categories: []string{constants.SetCategory, constants.ReadCategory, constants.FastCategory},
|
||||
Description: "(SMISMEMBER key member [member...]) Returns if multiple members are in the set.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: smismemberKeyFunc,
|
||||
HandlerFunc: handleSMISMEMBER,
|
||||
},
|
||||
@@ -671,6 +681,7 @@ Returns the cardinality of the intersection between multiple sets.`,
|
||||
Categories: []string{constants.SetCategory, constants.WriteCategory, constants.FastCategory},
|
||||
Description: "(SMOVE source destination member) Moves a member from source set to destination set.",
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: smoveKeyFunc,
|
||||
HandlerFunc: handleSMOVE,
|
||||
},
|
||||
@@ -680,6 +691,7 @@ Returns the cardinality of the intersection between multiple sets.`,
|
||||
Categories: []string{constants.SetCategory, constants.WriteCategory, constants.SlowCategory},
|
||||
Description: "(SPOP key [count]) Returns and removes one or more random members from the set.",
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: spopKeyFunc,
|
||||
HandlerFunc: handleSPOP,
|
||||
},
|
||||
@@ -689,6 +701,7 @@ Returns the cardinality of the intersection between multiple sets.`,
|
||||
Categories: []string{constants.SetCategory, constants.ReadCategory, constants.SlowCategory},
|
||||
Description: "(SRANDMEMBER key [count]) Returns one or more random members from the set without removing them.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: srandmemberKeyFunc,
|
||||
HandlerFunc: handleSRANDMEMBER,
|
||||
},
|
||||
@@ -698,6 +711,7 @@ Returns the cardinality of the intersection between multiple sets.`,
|
||||
Categories: []string{constants.SetCategory, constants.WriteCategory, constants.FastCategory},
|
||||
Description: "(SREM key member [member...]) Remove one or more members from a set.",
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: sremKeyFunc,
|
||||
HandlerFunc: handleSREM,
|
||||
},
|
||||
@@ -707,6 +721,7 @@ Returns the cardinality of the intersection between multiple sets.`,
|
||||
Categories: []string{constants.SetCategory, constants.ReadCategory, constants.SlowCategory},
|
||||
Description: "(SUNION key [key...]) Returns the members of the set resulting from the union of the provided sets.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: sunionKeyFunc,
|
||||
HandlerFunc: handleSUNION,
|
||||
},
|
||||
@@ -716,6 +731,7 @@ Returns the cardinality of the intersection between multiple sets.`,
|
||||
Categories: []string{constants.SetCategory, constants.WriteCategory, constants.SlowCategory},
|
||||
Description: "(SUNIONSTORE destination key [key...]) Stores the union of the given sets into destination.",
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: sunionstoreKeyFunc,
|
||||
HandlerFunc: handleSUNIONSTORE,
|
||||
},
|
||||
|
@@ -28,17 +28,16 @@ type Set struct {
|
||||
length int
|
||||
}
|
||||
|
||||
func (s *Set) GetMem() int64 {
|
||||
func (set *Set) GetMem() int64 {
|
||||
var size int64
|
||||
size += int64(unsafe.Sizeof(s))
|
||||
// above only gives us the size of the pointer to the map, so we need to add it's headers and contents
|
||||
size += int64(unsafe.Sizeof(s.members))
|
||||
for k, v := range s.members {
|
||||
size += int64(unsafe.Sizeof(set))
|
||||
// above only gives us the size of the pointer to the map, so we need to add its headers and contents
|
||||
size += int64(unsafe.Sizeof(set.members))
|
||||
for k, v := range set.members {
|
||||
size += int64(unsafe.Sizeof(k))
|
||||
size += int64(len(k))
|
||||
size += int64(unsafe.Sizeof(v))
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
@@ -66,7 +65,7 @@ func (set *Set) Add(elems []string) int {
|
||||
return count
|
||||
}
|
||||
|
||||
func (set *Set) Get(e string) interface{} {
|
||||
func (set *Set) get(e string) interface{} {
|
||||
return set.members[e]
|
||||
}
|
||||
|
||||
@@ -123,7 +122,7 @@ func (set *Set) GetRandom(count int) []string {
|
||||
func (set *Set) Remove(elems []string) int {
|
||||
count := 0
|
||||
for _, e := range elems {
|
||||
if set.Get(e) != nil {
|
||||
if set.get(e) != nil {
|
||||
delete(set.members, e)
|
||||
count += 1
|
||||
}
|
||||
@@ -139,7 +138,7 @@ func (set *Set) Pop(count int) []string {
|
||||
}
|
||||
|
||||
func (set *Set) Contains(e string) bool {
|
||||
return set.Get(e) != nil
|
||||
return set.get(e) != nil
|
||||
}
|
||||
|
||||
// Subtract received a list of sets and finds the difference between sets provided
|
||||
|
@@ -1381,6 +1381,7 @@ Adds all the specified members with the specified scores to the sorted set at th
|
||||
"CH" modifies the result to return total number of members changed + added, instead of only new members added.
|
||||
"INCR" modifies the command to act like ZINCRBY, only one score/member pair can be specified in this mode.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zaddKeyFunc,
|
||||
HandlerFunc: handleZADD,
|
||||
},
|
||||
@@ -1392,6 +1393,7 @@ Adds all the specified members with the specified scores to the sorted set at th
|
||||
If the key does not exist, 0 is returned, otherwise the cardinality of the sorted set is returned.
|
||||
If the key holds a value that is not a sorted set, this command will return an error.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zcardKeyFunc,
|
||||
HandlerFunc: handleZCARD,
|
||||
},
|
||||
@@ -1404,6 +1406,7 @@ Returns the number of elements in the sorted set key with scores in the range of
|
||||
If the key does not exist, a count of 0 is returned, otherwise return the count.
|
||||
If the key holds a value that is not a sorted set, an error is returned.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zcountKeyFunc,
|
||||
HandlerFunc: handleZCOUNT,
|
||||
},
|
||||
@@ -1414,6 +1417,7 @@ If the key holds a value that is not a sorted set, an error is returned.`,
|
||||
Description: `(ZDIFF key [key...] [WITHSCORES])
|
||||
Computes the difference between all the sorted sets specified in the list of keys and returns the result.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zdiffKeyFunc,
|
||||
HandlerFunc: handleZDIFF,
|
||||
},
|
||||
@@ -1425,6 +1429,7 @@ Computes the difference between all the sorted sets specified in the list of key
|
||||
Computes the difference between all the sorted sets specifies in the list of keys. Stores the result in destination.
|
||||
If the base set (first key) does not exist, return 0, otherwise, return the cardinality of the diff.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zdiffstoreKeyFunc,
|
||||
HandlerFunc: handleZDIFFSTORE,
|
||||
},
|
||||
@@ -1436,6 +1441,7 @@ If the base set (first key) does not exist, return 0, otherwise, return the card
|
||||
Increments the score of the specified sorted set's member by the increment. If the member does not exist, it is created.
|
||||
If the key does not exist, it is created with new sorted set and the member added with the increment as its score.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zincrbyKeyFunc,
|
||||
HandlerFunc: handleZINCRBY,
|
||||
},
|
||||
@@ -1446,6 +1452,7 @@ If the key does not exist, it is created with new sorted set and the member adde
|
||||
Description: `(ZINTER key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE <SUM | MIN | MAX>] [WITHSCORES]).
|
||||
Computes the intersection of the sets in the keys, with weights, aggregate and scores`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zinterKeyFunc,
|
||||
HandlerFunc: handleZINTER,
|
||||
},
|
||||
@@ -1457,6 +1464,7 @@ Computes the intersection of the sets in the keys, with weights, aggregate and s
|
||||
(ZINTERSTORE destination key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE <SUM | MIN | MAX>] [WITHSCORES]).
|
||||
Computes the intersection of the sets in the keys, with weights, aggregate and scores. The result is stored in destination.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zinterstoreKeyFunc,
|
||||
HandlerFunc: handleZINTERSTORE,
|
||||
},
|
||||
@@ -1468,6 +1476,7 @@ Computes the intersection of the sets in the keys, with weights, aggregate and s
|
||||
Pop a 'count' elements from multiple sorted sets. MIN or MAX determines whether to pop elements with the lowest or highest scores
|
||||
respectively.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zmpopKeyFunc,
|
||||
HandlerFunc: handleZMPOP,
|
||||
},
|
||||
@@ -1479,6 +1488,7 @@ respectively.`,
|
||||
Returns the associated scores of the specified member in the sorted set.
|
||||
Returns nil for members that do not exist in the set`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zmscoreKeyFunc,
|
||||
HandlerFunc: handleZMSCORE,
|
||||
},
|
||||
@@ -1489,6 +1499,7 @@ Returns nil for members that do not exist in the set`,
|
||||
Description: `(ZPOPMAX key [count])
|
||||
Removes and returns 'count' number of members in the sorted set with the highest scores. Default count is 1.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zpopKeyFunc,
|
||||
HandlerFunc: handleZPOP,
|
||||
},
|
||||
@@ -1499,6 +1510,7 @@ Removes and returns 'count' number of members in the sorted set with the highest
|
||||
Description: `(ZPOPMIN key [count])
|
||||
Removes and returns 'count' number of members in the sorted set with the lowest scores. Default count is 1.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zpopKeyFunc,
|
||||
HandlerFunc: handleZPOP,
|
||||
},
|
||||
@@ -1511,6 +1523,7 @@ Return a list of length equivalent to count containing random members of the sor
|
||||
If count is negative, repeated elements are allowed. If count is positive, the returned elements will be distinct.
|
||||
WITHSCORES modifies the result to include scores in the result.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zrandmemberKeyFunc,
|
||||
HandlerFunc: handleZRANDMEMBER,
|
||||
},
|
||||
@@ -1521,6 +1534,7 @@ WITHSCORES modifies the result to include scores in the result.`,
|
||||
Description: `(ZRANK key member [WITHSCORE])
|
||||
Returns the rank of the specified member in the sorted set. WITHSCORE modifies the result to also return the score.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zrankKeyFunc,
|
||||
HandlerFunc: handleZRANK,
|
||||
},
|
||||
@@ -1532,6 +1546,7 @@ Returns the rank of the specified member in the sorted set. WITHSCORE modifies t
|
||||
Returns the rank of the member in the sorted set in reverse order.
|
||||
WITHSCORE modifies the result to include the score.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zrevrankKeyFunc,
|
||||
HandlerFunc: handleZRANK,
|
||||
},
|
||||
@@ -1542,6 +1557,7 @@ WITHSCORE modifies the result to include the score.`,
|
||||
Description: `(ZREM key member [member ...]) Removes the listed members from the sorted set.
|
||||
Returns the number of elements removed.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zremKeyFunc,
|
||||
HandlerFunc: handleZREM,
|
||||
},
|
||||
@@ -1551,6 +1567,7 @@ Returns the number of elements removed.`,
|
||||
Categories: []string{constants.SortedSetCategory, constants.ReadCategory, constants.FastCategory},
|
||||
Description: `(ZSCORE key member) Returns the score of the member in the sorted set.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zscoreKeyFunc,
|
||||
HandlerFunc: handleZSCORE,
|
||||
},
|
||||
@@ -1560,6 +1577,7 @@ Returns the number of elements removed.`,
|
||||
Categories: []string{constants.SortedSetCategory, constants.WriteCategory, constants.SlowCategory},
|
||||
Description: `(ZREMRANGEBYLEX key min max) Removes the elements in the lexicographical range between min and max`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zremrangebylexKeyFunc,
|
||||
HandlerFunc: handleZREMRANGEBYLEX,
|
||||
},
|
||||
@@ -1570,6 +1588,7 @@ Returns the number of elements removed.`,
|
||||
Description: `(ZREMRANGEBYRANK key start stop) Removes the elements in the rank range between start and stop.
|
||||
The elements are ordered from lowest score to highest score`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zremrangebyrankKeyFunc,
|
||||
HandlerFunc: handleZREMRANGEBYRANK,
|
||||
},
|
||||
@@ -1579,6 +1598,7 @@ The elements are ordered from lowest score to highest score`,
|
||||
Categories: []string{constants.SortedSetCategory, constants.WriteCategory, constants.SlowCategory},
|
||||
Description: `(ZREMRANGEBYSCORE key min max) Removes the elements whose scores are in the range between min and max`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zremrangebyscoreKeyFunc,
|
||||
HandlerFunc: handleZREMRANGEBYSCORE,
|
||||
},
|
||||
@@ -1590,6 +1610,7 @@ The elements are ordered from lowest score to highest score`,
|
||||
lexicographical range between min and max. Returns 0, if the keys does not exist or if all the members do not have
|
||||
the same score. If the value held at key is not a sorted set, an error is returned.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zlexcountKeyFunc,
|
||||
HandlerFunc: handleZLEXCOUNT,
|
||||
},
|
||||
@@ -1600,6 +1621,7 @@ the same score. If the value held at key is not a sorted set, an error is return
|
||||
Description: `(ZRANGE key start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count]
|
||||
[WITHSCORES]) Returns the range of elements in the sorted set.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zrangeKeyCount,
|
||||
HandlerFunc: handleZRANGE,
|
||||
},
|
||||
@@ -1610,6 +1632,7 @@ the same score. If the value held at key is not a sorted set, an error is return
|
||||
Description: `ZRANGESTORE destination source start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count]
|
||||
[WITHSCORES] Retrieve the range of elements in the sorted set and store it in destination.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zrangeStoreKeyFunc,
|
||||
HandlerFunc: handleZRANGESTORE,
|
||||
},
|
||||
@@ -1622,6 +1645,7 @@ the same score. If the value held at key is not a sorted set, an error is return
|
||||
a sorted set are multiplied by the corresponding weight in WEIGHTS. Aggregate determines how the scores are combined.
|
||||
WITHSCORES option determines whether to return the result with scores included.`,
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zunionKeyFunc,
|
||||
HandlerFunc: handleZUNION,
|
||||
},
|
||||
@@ -1634,6 +1658,7 @@ WITHSCORES option determines whether to return the result with scores included.`
|
||||
a sorted set are multiplied by the corresponding weight in WEIGHTS. Aggregate determines how the scores are combined.
|
||||
The resulting union is stored at the destination key.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: zunionstoreKeyFunc,
|
||||
HandlerFunc: handleZUNIONSTORE,
|
||||
},
|
||||
|
@@ -206,6 +206,7 @@ func Commands() []internal.Command {
|
||||
Description: `(SETRANGE key offset value)
|
||||
Overwrites part of a string value with another by offset. Creates the key if it doesn't exist.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: setRangeKeyFunc,
|
||||
HandlerFunc: handleSetRange,
|
||||
},
|
||||
@@ -215,6 +216,7 @@ Overwrites part of a string value with another by offset. Creates the key if it
|
||||
Categories: []string{constants.StringCategory, constants.ReadCategory, constants.FastCategory},
|
||||
Description: "(STRLEN key) Returns length of the key's value if it's a string.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: strLenKeyFunc,
|
||||
HandlerFunc: handleStrLen,
|
||||
},
|
||||
@@ -224,6 +226,7 @@ Overwrites part of a string value with another by offset. Creates the key if it
|
||||
Categories: []string{constants.StringCategory, constants.ReadCategory, constants.SlowCategory},
|
||||
Description: "(SUBSTR key start end) Returns a substring from the string value.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: subStrKeyFunc,
|
||||
HandlerFunc: handleSubStr,
|
||||
},
|
||||
@@ -233,6 +236,7 @@ Overwrites part of a string value with another by offset. Creates the key if it
|
||||
Categories: []string{constants.StringCategory, constants.ReadCategory, constants.SlowCategory},
|
||||
Description: "(GETRANGE key start end) Returns a substring from the string value.",
|
||||
Sync: false,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: subStrKeyFunc,
|
||||
HandlerFunc: handleSubStr,
|
||||
},
|
||||
@@ -242,6 +246,7 @@ Overwrites part of a string value with another by offset. Creates the key if it
|
||||
Categories: []string{constants.StringCategory, constants.WriteCategory, constants.SlowCategory},
|
||||
Description: `(APPEND key value) If key already exists and is a string, this command appends the value at the end of the string. If key does not exist it is created and set as an empty string, so APPEND will be similar to [SET] in this special case.`,
|
||||
Sync: true,
|
||||
Type: "BUILT_IN",
|
||||
KeyExtractionFunc: appendKeyFunc,
|
||||
HandlerFunc: handleAppend,
|
||||
},
|
||||
|
@@ -194,15 +194,28 @@ type HandlerFuncParams struct {
|
||||
// FlushDB flushes the specified database keys. It accepts the integer index of the database to be flushed.
|
||||
// If -1 is passed as the index, then all databases will be flushed.
|
||||
Flush func(database int)
|
||||
// Randomkey returns a random key
|
||||
Randomkey func(ctx context.Context) string
|
||||
// (TOUCH key [key ...]) Alters the last access time or access count of the key(s) depending on whether LFU or LRU strategy was used.
|
||||
// RandomKey returns a random key
|
||||
RandomKey func(ctx context.Context) string
|
||||
// (TOUCH key [key ...]) Alters the last access time or access count of the key(s)
|
||||
// depending on whether LFU or LRU strategy was used.
|
||||
// A key is ignored if it does not exist.
|
||||
Touchkey func(ctx context.Context, keys []string) (int64, error)
|
||||
TouchKey func(ctx context.Context, keys []string) (int64, error)
|
||||
// GetObjectFrequency retrieves the access frequency count of a key. Can only be used with LFU type eviction policies.
|
||||
GetObjectFrequency func(ctx context.Context, keys string) (int, error)
|
||||
// GetObjectIdleTime retrieves the time in seconds since the last access of a key. Can only be used with LRU type eviction policies.
|
||||
// GetObjectIdleTime retrieves the time in seconds since the last access of a key.
|
||||
// Can only be used with LRU type eviction policies.
|
||||
GetObjectIdleTime func(ctx context.Context, keys string) (float64, error)
|
||||
// AddScript adds a script to SugarDB that isn't associated with a command.
|
||||
// This script is triggered using the EVAL or EVALSHA commands.
|
||||
// engine defines the interpreter to be used. Possible values: "LUA"
|
||||
// scriptType is either "FILE" or "RAW".
|
||||
// content contains the file path if scriptType is "FILE" and the raw script if scriptType is "RAW"
|
||||
AddScript func(engine string, scriptType string, content string, args []string) error
|
||||
// AddScriptCommand adds a commands to SugarDB that is defined by a scripting language.
|
||||
// engine defines the interpreter to be used. Possible values: "LUA"
|
||||
// scriptType is either "FILE" or "RAW".
|
||||
// content contains the file path if scriptType is "FILE" and the raw script if scriptType is "RAW".
|
||||
AddScriptCommand func(content string, args []string) error
|
||||
}
|
||||
|
||||
// HandlerFunc is a functions described by a command where the bulk of the command handling is done.
|
||||
@@ -217,7 +230,8 @@ type Command struct {
|
||||
Categories []string // The ACL categories this command belongs to. All the available categories are in the `constants` package.
|
||||
Description string // The description of the command. Includes the command syntax.
|
||||
SubCommands []SubCommand // The list of subcommands for this command. Empty if the command has no subcommands.
|
||||
Sync bool // Specifies if command should be synced across replication cluster
|
||||
Sync bool // Specifies if command should be synced across replication cluster.
|
||||
Type string // The type of command ("BUILT_IN", "GO_MODULE", "LUA_SCRIPT", "JS_SCRIPT").
|
||||
KeyExtractionFunc
|
||||
HandlerFunc
|
||||
}
|
||||
@@ -227,7 +241,7 @@ type SubCommand struct {
|
||||
Module string // The module this subcommand belongs to. Should be the same as the parent command.
|
||||
Categories []string // The ACL categories the subcommand belongs to.
|
||||
Description string // The description of the subcommand. Includes syntax.
|
||||
Sync bool // Specifies if sub-command should be synced across replication cluster
|
||||
Sync bool // Specifies if sub-command should be synced across replication cluster.
|
||||
KeyExtractionFunc
|
||||
HandlerFunc
|
||||
}
|
||||
|
109
internal/volumes/modules/lua/example.lua
Normal file
109
internal/volumes/modules/lua/example.lua
Normal file
@@ -0,0 +1,109 @@
|
||||
|
||||
-- The keyword to trigger the command
|
||||
command = "LUA.EXAMPLE"
|
||||
|
||||
--[[
|
||||
The string array of categories this command belongs to.
|
||||
This array can contain both built-in categories and new custom categories.
|
||||
]]
|
||||
categories = {"generic", "write", "fast"}
|
||||
|
||||
-- The description of the command
|
||||
description = "(LUA.EXAMPLE) Example lua command that sets various data types to keys"
|
||||
|
||||
-- Whether the command should be synced across the RAFT cluster
|
||||
sync = true
|
||||
|
||||
--[[
|
||||
keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
|
||||
The returned data from this function is used in the Access Control Layer to determine if the current connection is
|
||||
authorized to execute this command. The function must return a table that specifies which keys in this command
|
||||
are read keys and which ones are write keys.
|
||||
Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4", "key5"}}
|
||||
|
||||
1. "command" is a string array representing the command that triggered this key extraction function.
|
||||
|
||||
2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
|
||||
These args are passed to the key extraction function everytime it's invoked.
|
||||
]]
|
||||
function keyExtractionFunc (command, args)
|
||||
for k,v in pairs(args) do
|
||||
print(k, v)
|
||||
end
|
||||
if (#command ~= 1) then
|
||||
error("wrong number of args, expected 0")
|
||||
end
|
||||
return { ["readKeys"] = {}, ["writeKeys"] = {} }
|
||||
end
|
||||
|
||||
--[[
|
||||
handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
|
||||
SugarDB. The function must return a valid RESP response or throw an error.
|
||||
The handler function accepts the following args:
|
||||
|
||||
1. "context" is a table that contains some information about the environment this command has been executed in.
|
||||
Example: {["protocol"] = 2, ["database"] = 0}
|
||||
This object contains the following properties:
|
||||
i) protocol - the protocol version of the client that executed the command (either 2 or 3).
|
||||
ii) database - the active database index of the client that executed the command.
|
||||
|
||||
2. "command" is the string array representing the command that triggered this handler function.
|
||||
|
||||
3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
|
||||
This function accepts a string array of keys to check and returns a table with each key having a corresponding
|
||||
boolean value indicating whether it exists.
|
||||
Examples:
|
||||
i) Example invocation: keyExists({"key1", "key2", "key3"})
|
||||
ii) Example return: {["key1"] = true, ["key2"] = false, ["key3"] = true}
|
||||
|
||||
4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
|
||||
The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
|
||||
containing the corresponding value from the store.
|
||||
The possible data types for the values are: number, string, nil, hash, set, zset
|
||||
Examples:
|
||||
i) Example invocation: getValues({"key1", "key2", "key3"})
|
||||
ii) Example return: {["key1"] = 3.142, ["key2"] = nil, ["key3"] = "Pi"}
|
||||
|
||||
5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
|
||||
This function accepts a table with keys and the corresponding values to set for each key in the active database
|
||||
in the store.
|
||||
The accepted data types for the values are: number, string, nil, hash, set, zset.
|
||||
The setValues function does not return anything.
|
||||
Examples:
|
||||
i) Example invocation: setValues({["key1"] = 3.142, ["key2"] = nil, ["key3"] = "Pi"})
|
||||
|
||||
6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
|
||||
handler everytime it's invoked.
|
||||
]]
|
||||
function handlerFunc(ctx, command, keysExist, getValues, setValues, args)
|
||||
-- Set various data types to keys
|
||||
local keyValues = {
|
||||
["numberKey"] = 42,
|
||||
["stringKey"] = "Hello, SugarDB!",
|
||||
["nilKey"] = nil,
|
||||
}
|
||||
|
||||
-- Store the values in the database
|
||||
setValues(keyValues)
|
||||
|
||||
-- Verify the values have been set correctly
|
||||
local keysToGet = {"numberKey", "stringKey", "nilKey"}
|
||||
local retrievedValues = getValues(keysToGet)
|
||||
|
||||
-- Create a table to track mismatches
|
||||
local mismatches = {}
|
||||
for key, expectedValue in pairs(keyValues) do
|
||||
local retrievedValue = retrievedValues[key]
|
||||
if retrievedValue ~= expectedValue then
|
||||
table.insert(mismatches, string.format("Key '%s': expected '%s', got '%s'", key, tostring(expectedValue), tostring(retrievedValue)))
|
||||
end
|
||||
end
|
||||
|
||||
-- If mismatches exist, return an error
|
||||
if #mismatches > 0 then
|
||||
error("values mismatch")
|
||||
end
|
||||
|
||||
-- If all values match, return OK
|
||||
return "+OK\r\n"
|
||||
end
|
137
internal/volumes/modules/lua/hash.lua
Normal file
137
internal/volumes/modules/lua/hash.lua
Normal file
@@ -0,0 +1,137 @@
|
||||
|
||||
-- The keyword to trigger the command
|
||||
command = "LUA.HASH"
|
||||
|
||||
--[[
|
||||
The string array of categories this command belongs to.
|
||||
This array can contain both built-in categories and new custom categories.
|
||||
]]
|
||||
categories = {"hash", "write", "fast"}
|
||||
|
||||
-- The description of the command
|
||||
description = "(LUA.HASH key) \
|
||||
This is an example of working with SugarDB hashes/maps in lua scripts."
|
||||
|
||||
-- Whether the command should be synced across the RAFT cluster
|
||||
sync = true
|
||||
|
||||
--[[
|
||||
keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
|
||||
The returned data from this function is used in the Access Control Layer to determine if the current connection is
|
||||
authorized to execute this command. The function must return a table that specifies which keys in this command
|
||||
are read keys and which ones are write keys.
|
||||
Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4", "key5"}}
|
||||
|
||||
1. "command" is a string array representing the command that triggered this key extraction function.
|
||||
|
||||
2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
|
||||
These args are passed to the key extraction function everytime it's invoked.
|
||||
]]
|
||||
function keyExtractionFunc (command, args)
|
||||
for k,v in pairs(args) do
|
||||
print(k, v)
|
||||
end
|
||||
if (#command < 2) then
|
||||
error("wrong number of args, expected 1")
|
||||
end
|
||||
return { ["readKeys"] = {command[2]}, ["writeKeys"] = {} }
|
||||
end
|
||||
|
||||
--[[
|
||||
handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
|
||||
SugarDB. The function must return a valid RESP response or throw an error.
|
||||
The handler function accepts the following args:
|
||||
|
||||
1. "context" is a table that contains some information about the environment this command has been executed in.
|
||||
Example: {["protocol"] = 2, ["database"] = 0}
|
||||
This object contains the following properties:
|
||||
i) protocol - the protocol version of the client that executed the command (either 2 or 3).
|
||||
ii) database - the active database index of the client that executed the command.
|
||||
|
||||
2. "command" is the string array representing the command that triggered this handler function.
|
||||
|
||||
3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
|
||||
This function accepts a string array of keys to check and returns a table with each key having a corresponding
|
||||
boolean value indicating whether it exists.
|
||||
Examples:
|
||||
i) Example invocation: keyExists({"key1", "key2", "key3"})
|
||||
ii) Example return: {["key1"] = true, ["key2"] = false, ["key3"] = true}
|
||||
|
||||
4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
|
||||
The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
|
||||
containing the corresponding value from the store.
|
||||
The possible data types for the values are: number, string, nil, hash, set, zset
|
||||
Examples:
|
||||
i) Example invocation: getValues({"key1", "key2", "key3"})
|
||||
ii) Example return: {["key1"] = 3.142, ["key2"] = nil, ["key3"] = "Pi"}
|
||||
|
||||
5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
|
||||
This function accepts a table with keys and the corresponding values to set for each key in the active database
|
||||
in the store.
|
||||
The accepted data types for the values are: number, string, nil, hash, set, zset.
|
||||
The setValues function does not return anything.
|
||||
Examples:
|
||||
i) Example invocation: setValues({["key1"] = 3.142, ["key2"] = nil, ["key3"] = "Pi"})
|
||||
|
||||
6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
|
||||
handler everytime it's invoked.
|
||||
]]
|
||||
function handlerFunc(context, command, keysExist, getValues, setValues, args)
|
||||
-- Initialize a new hash
|
||||
local h = hash.new()
|
||||
|
||||
-- Set values in the hash
|
||||
h:set({
|
||||
{["field1"] = "value1"},
|
||||
{["field2"] = "value2"},
|
||||
{["field3"] = "value3"},
|
||||
{["field4"] = "value4"},
|
||||
})
|
||||
|
||||
-- Set hash in the store
|
||||
setValues({[command[2]] = h})
|
||||
|
||||
-- Check that the fields were correctly set in the database
|
||||
local hashValue = getValues({command[2]})[command[2]]
|
||||
assert(hashValue:get({"field1"})["field1"] == "value1", "field1 not set correctly")
|
||||
assert(hashValue:get({"field2"})["field2"] == "value2", "field2 not set correctly")
|
||||
assert(hashValue:get({"field3"})["field3"] == "value3", "field3 not set correctly")
|
||||
assert(hashValue:get({"field4"})["field4"] == "value4", "field4 not set correctly")
|
||||
|
||||
-- Test get method
|
||||
local retrieved = h:get({"field1", "field2"})
|
||||
assert(retrieved["field1"] == "value1", "get method failed for field1")
|
||||
assert(retrieved["field2"] == "value2", "get method failed for field2")
|
||||
|
||||
-- Test exists method
|
||||
local exists = h:exists({"field1", "fieldX"})
|
||||
assert(exists["field1"] == true, "exists method failed for field1")
|
||||
assert(exists["fieldX"] == false, "exists method failed for fieldX")
|
||||
|
||||
-- Test setnx method
|
||||
local setnxCount = h:setnx({
|
||||
{["field1"] = "new_value1"}, -- Should not overwrite
|
||||
{["field5"] = "value5"}, -- Should set
|
||||
})
|
||||
assert(setnxCount == 1, "setnx did not set the correct number of fields")
|
||||
assert(h:get({"field1"})["field1"] == "value1", "setnx overwrote field1")
|
||||
assert(h:get({"field5"})["field5"] == "value5", "setnx failed to set field5")
|
||||
|
||||
-- Test del method
|
||||
local delCount = h:del({"field2", "field3"})
|
||||
assert(delCount == 2, "del did not delete the correct number of fields")
|
||||
assert(h:exists({"field2"})["field2"] == false, "del failed to delete field2")
|
||||
assert(h:exists({"field3"})["field3"] == false, "del failed to delete field3")
|
||||
|
||||
-- Test len method
|
||||
assert(h:len() == 3, "len method returned incorrect value")
|
||||
|
||||
-- Retrieve and verify all remaining fields
|
||||
local remainingFields = h:all()
|
||||
assert(remainingFields["field1"] == "value1", "field1 missing after deletion")
|
||||
assert(remainingFields["field4"] == "value4", "field4 missing after deletion")
|
||||
assert(remainingFields["field5"] == "value5", "field5 missing after deletion")
|
||||
|
||||
-- Return RESP response
|
||||
return "+OK\r\n"
|
||||
end
|
120
internal/volumes/modules/lua/list.lua
Normal file
120
internal/volumes/modules/lua/list.lua
Normal file
@@ -0,0 +1,120 @@
|
||||
-- The keyword to trigger the command
|
||||
command = "LUA.LIST"
|
||||
|
||||
--[[
|
||||
The string array of categories this command belongs to.
|
||||
This array can contain both built-in categories and new custom categories.
|
||||
]]
|
||||
categories = {"list", "write", "fast"}
|
||||
|
||||
-- The description of the command
|
||||
description = "(LUA.LIST key) \
|
||||
This is an example of working with SugarDB lists in lua scripts."
|
||||
|
||||
-- Whether the command should be synced across the RAFT cluster
|
||||
sync = true
|
||||
|
||||
--[[
|
||||
keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
|
||||
The returned data from this function is used in the Access Control Layer to determine if the current connection is
|
||||
authorized to execute this command. The function must return a table that specifies which keys in this command
|
||||
are read keys and which ones are write keys.
|
||||
Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4", "key5"}}
|
||||
|
||||
1. "command" is a string array representing the command that triggered this key extraction function.
|
||||
|
||||
2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
|
||||
These args are passed to the key extraction function everytime it's invoked.
|
||||
]]
|
||||
function keyExtractionFunc (command, args)
|
||||
for k,v in pairs(args) do
|
||||
print(k, v)
|
||||
end
|
||||
if (#command < 2) then
|
||||
error("wrong number of args, expected 1")
|
||||
end
|
||||
return { ["readKeys"] = {command[2]}, ["writeKeys"] = {} }
|
||||
end
|
||||
|
||||
--[[
|
||||
handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
|
||||
SugarDB. The function must return a valid RESP response or throw an error.
|
||||
The handler function accepts the following args:
|
||||
|
||||
1. "context" is a table that contains some information about the environment this command has been executed in.
|
||||
Example: {["protocol"] = 2, ["database"] = 0}
|
||||
This object contains the following properties:
|
||||
i) protocol - the protocol version of the client that executed the command (either 2 or 3).
|
||||
ii) database - the active database index of the client that executed the command.
|
||||
|
||||
2. "command" is the string array representing the command that triggered this handler function.
|
||||
|
||||
3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
|
||||
This function accepts a string array of keys to check and returns a table with each key having a corresponding
|
||||
boolean value indicating whether it exists.
|
||||
Examples:
|
||||
i) Example invocation: keyExists({"key1", "key2", "key3"})
|
||||
ii) Example return: {["key1"] = true, ["key2"] = false, ["key3"] = true}
|
||||
|
||||
4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
|
||||
The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
|
||||
containing the corresponding value from the store.
|
||||
The possible data types for the values are: number, string, nil, hash, set, zset
|
||||
Examples:
|
||||
i) Example invocation: getValues({"key1", "key2", "key3"})
|
||||
ii) Example return: {["key1"] = 3.142, ["key2"] = nil, ["key3"] = "Pi"}
|
||||
|
||||
5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
|
||||
This function accepts a table with keys and the corresponding values to set for each key in the active database
|
||||
in the store.
|
||||
The accepted data types for the values are: number, string, nil, hash, set, zset.
|
||||
The setValues function does not return anything.
|
||||
Examples:
|
||||
i) Example invocation: setValues({["key1"] = 3.142, ["key2"] = nil, ["key3"] = "Pi"})
|
||||
|
||||
6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
|
||||
handler everytime it's invoked.
|
||||
]]
|
||||
function handlerFunc(ctx, command, keysExist, getValues, setValues, args)
|
||||
-- Helper function to compare lists
|
||||
local function compareLists(expected, actual)
|
||||
if #expected ~= #actual then
|
||||
return false, string.format("Length mismatch: expected %d, got %d", #expected, #actual)
|
||||
end
|
||||
for i = 1, #expected do
|
||||
if expected[i] ~= actual[i] then
|
||||
return false, string.format("Mismatch at index %d: expected '%s', got '%s'", i, expected[i], actual[i])
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local key = command[2]
|
||||
|
||||
-- First list to set
|
||||
local initialList = {"apple", "banana", "cherry"}
|
||||
setValues({[key] = initialList})
|
||||
|
||||
-- Retrieve and verify the first list
|
||||
local retrievedValues = getValues({key})
|
||||
local retrievedList = retrievedValues[key]
|
||||
local isValid, errorMessage = compareLists(initialList, retrievedList)
|
||||
if not isValid then
|
||||
error(errorMessage)
|
||||
end
|
||||
|
||||
-- Update the list with new values
|
||||
local updatedList = {"orange", "grape", "watermelon"}
|
||||
setValues({[key] = updatedList})
|
||||
|
||||
-- Retrieve and verify the updated list
|
||||
retrievedValues = getValues({key})
|
||||
retrievedList = retrievedValues[key]
|
||||
isValid, errorMessage = compareLists(updatedList, retrievedList)
|
||||
if not isValid then
|
||||
error(errorMessage)
|
||||
end
|
||||
|
||||
-- If all assertions pass
|
||||
return "+OK\r\n"
|
||||
end
|
148
internal/volumes/modules/lua/set.lua
Normal file
148
internal/volumes/modules/lua/set.lua
Normal file
@@ -0,0 +1,148 @@
|
||||
|
||||
-- The keyword to trigger the command
|
||||
command = "LUA.SET"
|
||||
|
||||
--[[
|
||||
The string array of categories this command belongs to.
|
||||
This array can contain both built-in categories and new custom categories.
|
||||
]]
|
||||
categories = {"set", "write", "fast"}
|
||||
|
||||
-- The description of the command
|
||||
description = "([LUA.SET key member [member ...]]) \
|
||||
This is an example of working with SugarDB sets in lua scripts"
|
||||
|
||||
-- Whether the command should be synced across the RAFT cluster
|
||||
sync = true
|
||||
|
||||
--[[
|
||||
keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
|
||||
The returned data from this function is used in the Access Control Layer to determine if the current connection is
|
||||
authorized to execute this command. The function must return a table that specifies which keys in this command
|
||||
are read keys and which ones are write keys.
|
||||
Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4", "key5"}}
|
||||
|
||||
1. "command" is a string array representing the command that triggered this key extraction function.
|
||||
|
||||
2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
|
||||
These args are passed to the key extraction function everytime it's invoked.
|
||||
]]
|
||||
function keyExtractionFunc (command, args)
|
||||
for k,v in pairs(args) do
|
||||
print(k, v)
|
||||
end
|
||||
if (#command < 4) then
|
||||
error("wrong number of args, expected 3")
|
||||
end
|
||||
return { ["readKeys"] = {}, ["writeKeys"] = {command[2], command[3], command[4]} }
|
||||
end
|
||||
|
||||
--[[
|
||||
handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
|
||||
SugarDB. The function must return a valid RESP response or throw an error.
|
||||
The handler function accepts the following args:
|
||||
|
||||
1. "context" is a table that contains some information about the environment this command has been executed in.
|
||||
Example: {["protocol"] = 2, ["database"] = 0}
|
||||
This object contains the following properties:
|
||||
i) protocol - the protocol version of the client that executed the command (either 2 or 3).
|
||||
ii) database - the active database index of the client that executed the command.
|
||||
|
||||
2. "command" is the string array representing the command that triggered this handler function.
|
||||
|
||||
3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
|
||||
This function accepts a string array of keys to check and returns a table with each key having a corresponding
|
||||
boolean value indicating whether it exists.
|
||||
Examples:
|
||||
i) Example invocation: keyExists({"key1", "key2", "key3"})
|
||||
ii) Example return: {["key1"] = true, ["key2"] = false, ["key3"] = true}
|
||||
|
||||
4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
|
||||
The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
|
||||
containing the corresponding value from the store.
|
||||
The possible data types for the values are: number, string, nil, hash, set, zset
|
||||
Examples:
|
||||
i) Example invocation: getValues({"key1", "key2", "key3"})
|
||||
ii) Example return: {["key1"] = 3.142, ["key2"] = nil, ["key3"] = "Pi"}
|
||||
|
||||
5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
|
||||
This function accepts a table with keys and the corresponding values to set for each key in the active database
|
||||
in the store.
|
||||
The accepted data types for the values are: number, string, nil, hash, set, zset.
|
||||
The setValues function does not return anything.
|
||||
Examples:
|
||||
i) Example invocation: setValues({["key1"] = 3.142, ["key2"] = nil, ["key3"] = "Pi"})
|
||||
|
||||
6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
|
||||
handler everytime it's invoked.
|
||||
]]
|
||||
function handlerFunc(ctx, command, keyExists, getValues, setValues, args)
|
||||
-- Ensure there are enough arguments
|
||||
if #command < 4 then
|
||||
error("wrong number of arguments, expected at least 3")
|
||||
end
|
||||
|
||||
-- Extract the key
|
||||
local key1 = command[2]
|
||||
local key2 = command[3]
|
||||
local key3 = command[4]
|
||||
|
||||
-- Create two sets for testing `move` and `subtract`
|
||||
local set1 = set.new({"elem1", "elem2", "elem3"})
|
||||
local set2 = set.new({"elem4", "elem5"})
|
||||
|
||||
-- Call `add` to add elements to set1
|
||||
set1:add({"elem6", "elem7"})
|
||||
|
||||
-- Call `contains` to check if an element exists in set1
|
||||
local containsElem1 = set1:contains("elem1")
|
||||
local containsElemUnknown = set1:contains("unknown")
|
||||
|
||||
-- Call `cardinality` to get the size of set1
|
||||
local set1Cardinality = set1:cardinality()
|
||||
|
||||
-- Call `remove` to remove elements from set1
|
||||
local removedCount = set1:remove({"elem1", "elem2"})
|
||||
|
||||
-- Call `pop` to remove and retrieve elements from set1
|
||||
local poppedElements = set1:pop(2)
|
||||
|
||||
-- Call `random` to get random elements from set1
|
||||
local randomElements = set1:random(1)
|
||||
|
||||
-- Call `all` to retrieve all elements from set1
|
||||
local allElements = set1:all()
|
||||
|
||||
-- Test `move` method: move an element from set1 to set2
|
||||
local moveSuccess = set1:move(set2, "elem3")
|
||||
|
||||
-- Verify that the element was moved
|
||||
local set2ContainsMoved = set2:contains("elem3")
|
||||
local set1NoLongerContainsMoved = not set1:contains("elem3")
|
||||
|
||||
-- Test `subtract` method: subtract set2 from set1
|
||||
local resultSet = set1:subtract({set2})
|
||||
|
||||
-- Store the modified sets in SugarDB using setValues
|
||||
setValues({[key1] = set1, [key2] = set2, [key3] = resultSet})
|
||||
|
||||
-- Retrieve the sets back from SugarDB to verify storage
|
||||
local storedValues = getValues({key1, key2, key3})
|
||||
local storedSet1 = storedValues[key1]
|
||||
local storedSet2 = storedValues[key2]
|
||||
local storedResultSet = storedValues[key3]
|
||||
|
||||
-- Perform additional checks to ensure consistency
|
||||
if not storedSet1 or storedSet1:cardinality() ~= set1:cardinality() then
|
||||
error("Stored set1 does not match the modified set1")
|
||||
end
|
||||
if not storedSet2 or storedSet2:cardinality() ~= set2:cardinality() then
|
||||
error("Stored set2 does not match the modified set2")
|
||||
end
|
||||
if not storedResultSet or storedResultSet:cardinality() ~= resultSet:cardinality() then
|
||||
error("Stored result set does not match the computed result set")
|
||||
end
|
||||
|
||||
-- If all operations succeed, return "+OK\r\n"
|
||||
return "+OK\r\n"
|
||||
end
|
158
internal/volumes/modules/lua/zset.lua
Normal file
158
internal/volumes/modules/lua/zset.lua
Normal file
@@ -0,0 +1,158 @@
|
||||
|
||||
-- The keyword to trigger the command
|
||||
command = "LUA.ZSET"
|
||||
|
||||
--[[
|
||||
The string array of categories this command belongs to.
|
||||
This array can contain both built-in categories and new custom categories.
|
||||
]]
|
||||
categories = {"sortedset", "write", "fast"}
|
||||
|
||||
-- The description of the command
|
||||
description = "(LUA.ZSET key member score [member score ...]) \
|
||||
This is an example of working with sorted sets in lua scripts"
|
||||
|
||||
-- Whether the command should be synced across the RAFT cluster
|
||||
sync = true
|
||||
|
||||
--[[
|
||||
keyExtractionFunc is a function that extracts the keys from the command and returns them to SugarDB.keyExtractionFunc
|
||||
The returned data from this function is used in the Access Control Layer to determine if the current connection is
|
||||
authorized to execute this command. The function must return a table that specifies which keys in this command
|
||||
are read keys and which ones are write keys.
|
||||
Example return: {["readKeys"] = {"key1", "key2"}, ["writeKeys"] = {"key3", "key4", "key5"}}
|
||||
|
||||
1. "command" is a string array representing the command that triggered this key extraction function.
|
||||
|
||||
2. "args" is a string array of the modifier args that were passed when loading the module into SugarDB.
|
||||
These args are passed to the key extraction function everytime it's invoked.
|
||||
]]
|
||||
function keyExtractionFunc (command, args)
|
||||
for k,v in pairs(args) do
|
||||
print(k, v)
|
||||
end
|
||||
if (#command ~= 4) then
|
||||
error("wrong number of args, expected 2")
|
||||
end
|
||||
return { ["readKeys"] = {}, ["writeKeys"] = {command[2], command[3], command[4]} }
|
||||
end
|
||||
|
||||
--[[
|
||||
handlerFunc is the command's handler function. The function is passed some arguments that allow it to interact with
|
||||
SugarDB. The function must return a valid RESP response or throw an error.
|
||||
The handler function accepts the following args:
|
||||
|
||||
1. "context" is a table that contains some information about the environment this command has been executed in.
|
||||
Example: {["protocol"] = 2, ["database"] = 0}
|
||||
This object contains the following properties:
|
||||
i) protocol - the protocol version of the client that executed the command (either 2 or 3).
|
||||
ii) database - the active database index of the client that executed the command.
|
||||
|
||||
2. "command" is the string array representing the command that triggered this handler function.
|
||||
|
||||
3. "keyExists" is a function that can be called to check if a list of keys exists in the SugarDB store database.
|
||||
This function accepts a string array of keys to check and returns a table with each key having a corresponding
|
||||
boolean value indicating whether it exists.
|
||||
Examples:
|
||||
i) Example invocation: keyExists({"key1", "key2", "key3"})
|
||||
ii) Example return: {["key1"] = true, ["key2"] = false, ["key3"] = true}
|
||||
|
||||
4. "getValues" is a function that can be called to retrieve values from the SugarDB store database.
|
||||
The function accepts a string array of keys whose values we would like to fetch, and returns a table with each key
|
||||
containing the corresponding value from the store.
|
||||
The possible data types for the values are: number, string, nil, hash, set, zset
|
||||
Examples:
|
||||
i) Example invocation: getValues({"key1", "key2", "key3"})
|
||||
ii) Example return: {["key1"] = 3.142, ["key2"] = nil, ["key3"] = "Pi"}
|
||||
|
||||
5. "setValues" is a function that can be called to set values in the active database in the SugarDB store.
|
||||
This function accepts a table with keys and the corresponding values to set for each key in the active database
|
||||
in the store.
|
||||
The accepted data types for the values are: number, string, nil, hash, set, zset.
|
||||
The setValues function does not return anything.
|
||||
Examples:
|
||||
i) Example invocation: setValues({["key1"] = 3.142, ["key2"] = nil, ["key3"] = "Pi"})
|
||||
|
||||
6. "args" is a string array of the modifier args passed to the module at load time. These args are passed to the
|
||||
handler everytime it's invoked.
|
||||
]]
|
||||
function handlerFunc(ctx, command, keyExists, getValues, setValues, args)
|
||||
-- Ensure there are enough arguments
|
||||
if #command < 4 then
|
||||
error("wrong number of arguments, expected at least 3")
|
||||
end
|
||||
|
||||
local key1 = command[2]
|
||||
local key2 = command[3]
|
||||
local key3 = command[4]
|
||||
|
||||
-- Create `zmember` instances
|
||||
local member1 = zmember.new({value = "member1", score = 10})
|
||||
local member2 = zmember.new({value = "member2", score = 20})
|
||||
local member3 = zmember.new({value = "member3", score = 30})
|
||||
|
||||
-- Create a `zset` and add initial members
|
||||
local zset1 = zset.new({member1, member2})
|
||||
|
||||
-- Test `add` method with a new member
|
||||
zset1:add({member3})
|
||||
|
||||
-- Test `update` method by modifying an existing member
|
||||
zset1:update({zmember.new({value = "member1", score = 15})})
|
||||
|
||||
-- Test `remove` method
|
||||
zset1:remove("member2")
|
||||
|
||||
-- Test `cardinality` method
|
||||
local zset1Cardinality = zset1:cardinality()
|
||||
|
||||
-- Test `contains` method
|
||||
local containsMember3 = zset1:contains("member3")
|
||||
local containsNonExistent = zset1:contains("nonexistent")
|
||||
|
||||
-- Test `random` method
|
||||
local randomMembers = zset1:random(2)
|
||||
|
||||
-- Test `all` method
|
||||
local allMembers = zset1:all()
|
||||
|
||||
-- Create another `zset` to test `subtract`
|
||||
local zset2 = zset.new({zmember.new({value = "member3", score = 30})})
|
||||
local zsetSubtracted = zset1:subtract({zset2})
|
||||
|
||||
-- Store the `zset` objects in SugarDB
|
||||
setValues({
|
||||
[key1] = zset1,
|
||||
[key2] = zset2,
|
||||
[key3] = zsetSubtracted
|
||||
})
|
||||
|
||||
-- Retrieve the stored `zset` objects to verify storage
|
||||
local storedValues = getValues({key1, key2, key3})
|
||||
local storedZset1 = storedValues[key1]
|
||||
local storedZset2 = storedValues[key2]
|
||||
local storedSubtractedZset = storedValues[key3]
|
||||
|
||||
-- Perform consistency checks
|
||||
if not storedZset1 or storedZset1:cardinality() ~= zset1:cardinality() then
|
||||
error("Stored zset1 does not match the modified zset1")
|
||||
end
|
||||
if not storedZset2 or storedZset2:cardinality() ~= zset2:cardinality() then
|
||||
error("Stored zset2 does not match the modified zset2")
|
||||
end
|
||||
if not storedSubtractedZset or storedSubtractedZset:cardinality() ~= zsetSubtracted:cardinality() then
|
||||
error("Stored subtracted zset does not match the computed result")
|
||||
end
|
||||
|
||||
-- Test `zmember` methods
|
||||
local memberValue = member1:value()
|
||||
member1:value("updated_member1")
|
||||
local updatedValue = member1:value()
|
||||
|
||||
local memberScore = member1:score()
|
||||
member1:score(50)
|
||||
local updatedScore = member1:score()
|
||||
|
||||
-- Return an "OK" response
|
||||
return "+OK\r\n"
|
||||
end
|
@@ -309,71 +309,140 @@ func TestSugarDB_Plugins(t *testing.T) {
|
||||
|
||||
server := createSugarDB()
|
||||
|
||||
moduleSet := path.Join(".", "testdata", "modules", "module_set", "module_set.so")
|
||||
moduleGet := path.Join(".", "testdata", "modules", "module_get", "module_get.so")
|
||||
nonExistent := path.Join(".", "testdata", "modules", "non_existent", "module_non_existent.so")
|
||||
|
||||
// Load module.set module
|
||||
if err := server.LoadModule(moduleSet); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
// Execute module.set command and expect "OK" response
|
||||
res, err := server.ExecuteCommand("module.set", "key1", "15")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
rv, _, err := resp.NewReader(bytes.NewReader(res)).ReadValue()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if rv.String() != "OK" {
|
||||
t.Errorf("expected response \"OK\", got \"%s\"", rv.String())
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
expect bool
|
||||
args []string
|
||||
cmd []string
|
||||
want string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "1. Test shared object plugin MODULE.SET",
|
||||
path: path.Join(".", "testdata", "modules", "module_set", "module_set.so"),
|
||||
expect: true,
|
||||
args: []string{},
|
||||
cmd: []string{"MODULE.SET", "key1", "15"},
|
||||
want: "OK",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "2. Test shared object plugin MODULE.GET",
|
||||
path: path.Join(".", "testdata", "modules", "module_get", "module_get.so"),
|
||||
expect: true,
|
||||
args: []string{"10"},
|
||||
cmd: []string{"MODULE.GET", "key1"},
|
||||
want: "150",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "3. Test Non existent module.",
|
||||
path: path.Join(".", "testdata", "modules", "non_existent", "module_non_existent.so"),
|
||||
expect: false,
|
||||
args: []string{},
|
||||
cmd: []string{"NONEXISTENT", "key", "value"},
|
||||
want: "",
|
||||
wantErr: fmt.Errorf("load module: module %s not found",
|
||||
path.Join(".", "testdata", "modules", "non_existent", "module_non_existent.so")),
|
||||
},
|
||||
{
|
||||
name: "4. Test LUA module that handles hash values",
|
||||
path: path.Join("..", "internal", "volumes", "modules", "lua", "hash.lua"),
|
||||
expect: true,
|
||||
args: []string{},
|
||||
cmd: []string{"LUA.HASH", "LUA.HASH_KEY_1"},
|
||||
want: "OK",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "5. Test LUA module that handles set values",
|
||||
path: path.Join("..", "internal", "volumes", "modules", "lua", "set.lua"),
|
||||
expect: true,
|
||||
args: []string{},
|
||||
cmd: []string{"LUA.SET", "LUA.SET_KEY_1", "LUA.SET_KEY_2", "LUA.SET_KEY_3"},
|
||||
want: "OK",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "6. Test LUA module that handles zset values",
|
||||
path: path.Join("..", "internal", "volumes", "modules", "lua", "zset.lua"),
|
||||
expect: true,
|
||||
args: []string{},
|
||||
cmd: []string{"LUA.ZSET", "LUA.ZSET_KEY_1", "LUA.ZSET_KEY_2", "LUA.ZSET_KEY_3"},
|
||||
want: "OK",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "6. Test LUA module that handles list values",
|
||||
path: path.Join("..", "internal", "volumes", "modules", "lua", "list.lua"),
|
||||
expect: true,
|
||||
args: []string{},
|
||||
cmd: []string{"LUA.LIST", "LUA.LIST_KEY_1"},
|
||||
want: "OK",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "8. Test LUA module that handles primitive types",
|
||||
path: path.Join("..", "internal", "volumes", "modules", "lua", "example.lua"),
|
||||
expect: true,
|
||||
args: []string{},
|
||||
cmd: []string{"LUA.EXAMPLE"},
|
||||
want: "OK",
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
// Load module.get module with args
|
||||
if err := server.LoadModule(moduleGet, "10"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
// Execute module.get command and expect an integer with the value 150
|
||||
res, err = server.ExecuteCommand("module.get", "key1")
|
||||
rv, _, err = resp.NewReader(bytes.NewReader(res)).ReadValue()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if rv.Integer() != 150 {
|
||||
t.Errorf("expected response 150, got %d", rv.Integer())
|
||||
}
|
||||
|
||||
// Return error when trying to load module that does not exist
|
||||
if err := server.LoadModule(nonExistent); err == nil {
|
||||
t.Error("expected error but got nil instead")
|
||||
} else {
|
||||
if err.Error() != fmt.Sprintf("load module: module %s not found", nonExistent) {
|
||||
t.Errorf(
|
||||
"expected error \"%s\", got \"%s\"",
|
||||
fmt.Sprintf("load module: module %s not found", nonExistent),
|
||||
err.Error(),
|
||||
)
|
||||
for _, test := range tests {
|
||||
// Load module
|
||||
err := server.LoadModule(test.path, test.args...)
|
||||
if err != nil {
|
||||
if test.wantErr == nil || err.Error() != test.wantErr.Error() {
|
||||
t.Error(fmt.Errorf("%s: %v", test.name, err))
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Execute command and check expected response
|
||||
res, err := server.ExecuteCommand(test.cmd...)
|
||||
if err != nil {
|
||||
t.Error(fmt.Errorf("%s: %v", test.name, err))
|
||||
}
|
||||
rv, _, err := resp.NewReader(bytes.NewReader(res)).ReadValue()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if test.wantErr != nil {
|
||||
if test.wantErr.Error() != rv.Error().Error() {
|
||||
t.Errorf("expected error \"%s\", got \"%s\"", test.wantErr.Error(), rv.Error().Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
if rv.String() != test.want {
|
||||
t.Errorf("expected response \"%s\", got \"%s\"", test.want, rv.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Module list should contain module_get and module_set modules
|
||||
// Module list should contain all the modules above
|
||||
modules := server.ListModules()
|
||||
for _, mod := range []string{moduleSet, moduleGet} {
|
||||
if !slices.Contains(modules, mod) {
|
||||
t.Errorf("expected modules list to contain module \"%s\" but did not find it", mod)
|
||||
for _, test := range tests {
|
||||
// Skip the module if it's not expected
|
||||
if !test.expect {
|
||||
continue
|
||||
}
|
||||
// Check if module is loaded
|
||||
if !slices.Contains(modules, test.path) {
|
||||
t.Errorf("expected modules list to contain module \"%s\" but did not find it", test.path)
|
||||
}
|
||||
// Unload the module
|
||||
server.UnloadModule(test.path)
|
||||
}
|
||||
|
||||
// Unload modules
|
||||
server.UnloadModule(moduleSet)
|
||||
server.UnloadModule(moduleGet)
|
||||
|
||||
// Make sure the modules are no longer loaded
|
||||
modules = server.ListModules()
|
||||
for _, mod := range []string{moduleSet, moduleGet} {
|
||||
if slices.Contains(modules, mod) {
|
||||
t.Errorf("expected modules list to not contain module \"%s\" but found it", mod)
|
||||
for _, test := range tests {
|
||||
if slices.Contains(modules, test.path) {
|
||||
t.Errorf("expected modules list to not contain module \"%s\" but found it", test.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -163,7 +163,7 @@ type COPYOptions struct {
|
||||
//
|
||||
// "key <key> does not exist"" - when the XX flag is set to true and the key does not exist.
|
||||
//
|
||||
// "key <key> does already exists" - when the NX flag is set to true and the key already exists.
|
||||
// "key <key> already exists" - when the NX flag is set to true and the key already exists.
|
||||
func (server *SugarDB) Set(key, value string, options SETOptions) (string, bool, error) {
|
||||
cmd := []string{"SET", key, value}
|
||||
|
||||
|
@@ -15,6 +15,7 @@
|
||||
package sugardb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/echovault/sugardb/internal"
|
||||
"github.com/echovault/sugardb/internal/config"
|
||||
"github.com/echovault/sugardb/internal/constants"
|
||||
@@ -66,6 +67,24 @@ func WithTLS(b ...bool) func(sugardb *SugarDB) {
|
||||
}
|
||||
}
|
||||
|
||||
// WithContext is an options that for the NewSugarDB function that allows you to
|
||||
// configure a custom context object to be used in SugarDB.
|
||||
// If you don't provide this option, SugarDB will create its own internal context object.
|
||||
func WithContext(ctx context.Context) func(sugardb *SugarDB) {
|
||||
return func(sugardb *SugarDB) {
|
||||
sugardb.context = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// WithConfig is an option for the NewSugarDB function that allows you to pass a
|
||||
// custom configuration to SugarDB.
|
||||
// If not specified, SugarDB will use the default configuration from config.DefaultConfig().
|
||||
func WithConfig(config config.Config) func(sugardb *SugarDB) {
|
||||
return func(sugardb *SugarDB) {
|
||||
sugardb.config = config
|
||||
}
|
||||
}
|
||||
|
||||
// WithMTLS is an option to the NewSugarDB function that allows you to pass a
|
||||
// custom MTLS to SugarDB.
|
||||
// If not specified, SugarDB will use the default configuration from config.DefaultConfig().
|
||||
|
@@ -60,12 +60,14 @@ func (server *SugarDB) getHandlerFuncParams(ctx context.Context, cmd []string, c
|
||||
GetAllCommands: server.getCommands,
|
||||
GetClock: server.getClock,
|
||||
Flush: server.Flush,
|
||||
Randomkey: server.randomKey,
|
||||
Touchkey: server.updateKeysInCache,
|
||||
RandomKey: server.randomKey,
|
||||
TouchKey: server.updateKeysInCache,
|
||||
GetObjectFrequency: server.getObjectFreq,
|
||||
GetObjectIdleTime: server.getObjectIdleTime,
|
||||
SwapDBs: server.SwapDBs,
|
||||
GetServerInfo: server.GetServerInfo,
|
||||
AddScript: server.AddScript,
|
||||
AddScriptCommand: server.AddScriptCommand,
|
||||
DeleteKey: func(ctx context.Context, key string) error {
|
||||
server.storeLock.Lock()
|
||||
defer server.storeLock.Unlock()
|
||||
|
@@ -24,8 +24,101 @@ import (
|
||||
"plugin"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func (server *SugarDB) AddScript(engine string, scriptType string, content string, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (server *SugarDB) AddScriptCommand(
|
||||
path string,
|
||||
args []string,
|
||||
) error {
|
||||
// Extract the engine from the script file extension
|
||||
var engine string
|
||||
if strings.HasSuffix(path, ".lua") {
|
||||
engine = "lua"
|
||||
}
|
||||
|
||||
// Check if the engine is supported
|
||||
supportedEngines := []string{"lua"}
|
||||
if !slices.Contains(supportedEngines, strings.ToLower(engine)) {
|
||||
return fmt.Errorf("engine %s not supported, only %v engines are supported", engine, supportedEngines)
|
||||
}
|
||||
|
||||
// Initialise VM for the command depending on the engine.
|
||||
var vm any
|
||||
var commandName string
|
||||
var categories []string
|
||||
var description string
|
||||
var synchronize bool
|
||||
var commandType string
|
||||
var err error
|
||||
|
||||
switch strings.ToLower(engine) {
|
||||
case "lua":
|
||||
vm, commandName, categories, description, synchronize, commandType, err = generateLuaCommandInfo(path)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the script's VM to the server's list of VMs.
|
||||
server.scriptVMs.Store(commandName, struct {
|
||||
vm any
|
||||
lock *sync.Mutex
|
||||
}{
|
||||
vm: vm,
|
||||
// lock is the script mutex for the commands.
|
||||
// This mutex will be locked everytime the command is executed because
|
||||
// the script's VM is not thread safe.
|
||||
lock: &sync.Mutex{},
|
||||
})
|
||||
|
||||
// Build the command:
|
||||
command := internal.Command{
|
||||
Command: commandName,
|
||||
Module: path,
|
||||
Categories: categories,
|
||||
Description: description,
|
||||
Sync: synchronize,
|
||||
Type: commandType,
|
||||
KeyExtractionFunc: func(engine string, vm any, args []string) internal.KeyExtractionFunc {
|
||||
// Wrapper for the key function
|
||||
return func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
switch strings.ToLower(engine) {
|
||||
default:
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0),
|
||||
ReadKeys: make([]string, 0),
|
||||
WriteKeys: make([]string, 0),
|
||||
}, nil
|
||||
case "lua":
|
||||
return server.buildLuaKeyExtractionFunc(vm, cmd, args)
|
||||
}
|
||||
}
|
||||
}(engine, vm, args),
|
||||
HandlerFunc: func(engine string, vm any, args []string) internal.HandlerFunc {
|
||||
// Wrapper that generates handler function
|
||||
return func(params internal.HandlerFuncParams) ([]byte, error) {
|
||||
switch strings.ToLower(engine) {
|
||||
default:
|
||||
return nil, fmt.Errorf("command %s handler not implemented", commandName)
|
||||
case "lua":
|
||||
return server.buildLuaHandlerFunc(vm, commandName, args, params)
|
||||
}
|
||||
}
|
||||
}(engine, vm, args),
|
||||
}
|
||||
|
||||
// Add the commands to the list of commands.
|
||||
server.commands = append(server.commands, command)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadModule loads an external module into SugarDB ar runtime.
|
||||
//
|
||||
// Parameters:
|
||||
@@ -38,6 +131,12 @@ func (server *SugarDB) LoadModule(path string, args ...string) error {
|
||||
server.commandsRWMut.Lock()
|
||||
defer server.commandsRWMut.Unlock()
|
||||
|
||||
for _, suffix := range []string{".lua"} {
|
||||
if strings.HasSuffix(path, suffix) {
|
||||
return server.AddScriptCommand(path, args)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return fmt.Errorf("load module: module %s not found", path)
|
||||
@@ -81,7 +180,7 @@ func (server *SugarDB) LoadModule(path string, args ...string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sync, ok := syncSymbol.(*bool)
|
||||
synchronize, ok := syncSymbol.(*bool)
|
||||
if !ok {
|
||||
return errors.New("sync symbol is not a bool")
|
||||
}
|
||||
@@ -129,7 +228,7 @@ func (server *SugarDB) LoadModule(path string, args ...string) error {
|
||||
return cats
|
||||
}(),
|
||||
Description: *description,
|
||||
Sync: *sync,
|
||||
Sync: *synchronize,
|
||||
SubCommands: make([]internal.SubCommand, 0),
|
||||
KeyExtractionFunc: func(cmd []string) (internal.KeyExtractionFuncResult, error) {
|
||||
readKeys, writeKeys, err := keyExtractionFunc(cmd, args...)
|
||||
|
940
sugardb/plugin_lua.go
Normal file
940
sugardb/plugin_lua.go
Normal file
@@ -0,0 +1,940 @@
|
||||
// Copyright 2024 Kelvin Clement Mwinuka
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sugardb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/echovault/sugardb/internal"
|
||||
"github.com/echovault/sugardb/internal/modules/hash"
|
||||
"github.com/echovault/sugardb/internal/modules/set"
|
||||
"github.com/echovault/sugardb/internal/modules/sorted_set"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func generateLuaCommandInfo(path string) (*lua.LState, string, []string, string, bool, string, error) {
|
||||
L := lua.NewState()
|
||||
|
||||
// Load lua file
|
||||
if err := L.DoFile(path); err != nil {
|
||||
return nil, "", nil, "", false, "", fmt.Errorf("could not load lua script file %s: %v", path, err)
|
||||
}
|
||||
|
||||
// Register hash data type
|
||||
hashMetaTable := L.NewTypeMetatable("hash")
|
||||
L.SetGlobal("hash", hashMetaTable)
|
||||
// Static methods
|
||||
L.SetField(hashMetaTable, "new", L.NewFunction(func(state *lua.LState) int {
|
||||
ud := state.NewUserData()
|
||||
ud.Value = hash.Hash{}
|
||||
state.SetMetatable(ud, state.GetTypeMetatable("hash"))
|
||||
state.Push(ud)
|
||||
return 1
|
||||
}))
|
||||
// Hash methods
|
||||
L.SetField(hashMetaTable, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
|
||||
"set": func(state *lua.LState) int {
|
||||
// Implement set method, set key/value pair
|
||||
h := checkHash(state, 1)
|
||||
var count int
|
||||
tbl := state.CheckTable(2)
|
||||
tbl.ForEach(func(index lua.LValue, pair lua.LValue) {
|
||||
// Check if the pair is a table
|
||||
p, ok := pair.(*lua.LTable)
|
||||
if !ok {
|
||||
state.ArgError(2, "expected a table containing key/value pairs")
|
||||
return
|
||||
}
|
||||
p.ForEach(func(field lua.LValue, value lua.LValue) {
|
||||
// Check if field is a string
|
||||
if _, ok = field.(lua.LString); !ok {
|
||||
state.ArgError(2, "expected all hash fields to be strings")
|
||||
return
|
||||
}
|
||||
// If the field exists, update it. Otherwise, add it to the hash.
|
||||
v, err := luaTypeToNativeType(value)
|
||||
if err != nil {
|
||||
state.ArgError(2, err.Error())
|
||||
return
|
||||
}
|
||||
if _, exists := h[field.String()]; !exists {
|
||||
h[field.String()] = hash.HashValue{Value: v}
|
||||
} else {
|
||||
hashValue := h[field.String()]
|
||||
hashValue.Value = v
|
||||
h[field.String()] = hashValue
|
||||
}
|
||||
count += 1
|
||||
})
|
||||
})
|
||||
state.Push(lua.LNumber(count))
|
||||
return 1
|
||||
},
|
||||
"setnx": func(state *lua.LState) int {
|
||||
// Implement set methods to set key/value pairs only when the key does not exist in the hash.
|
||||
h := checkHash(state, 1)
|
||||
var count int
|
||||
tbl := state.CheckTable(2)
|
||||
tbl.ForEach(func(index lua.LValue, pair lua.LValue) {
|
||||
// Check if the pair is a table
|
||||
p, ok := pair.(*lua.LTable)
|
||||
if !ok {
|
||||
state.ArgError(2, "expected a table containing key/value pairs")
|
||||
return
|
||||
}
|
||||
p.ForEach(func(field lua.LValue, value lua.LValue) {
|
||||
// Check if field is a string
|
||||
if _, ok = field.(lua.LString); !ok {
|
||||
state.ArgError(2, "expected all table fields to be strings")
|
||||
}
|
||||
v, err := luaTypeToNativeType(value)
|
||||
if err != nil {
|
||||
state.ArgError(2, err.Error())
|
||||
return
|
||||
}
|
||||
// If the field does not exist, add it.
|
||||
if _, exists := h[field.String()]; !exists {
|
||||
h[field.String()] = hash.HashValue{Value: v}
|
||||
count += 1
|
||||
}
|
||||
})
|
||||
})
|
||||
state.Push(lua.LNumber(count))
|
||||
return 1
|
||||
},
|
||||
"get": func(state *lua.LState) int {
|
||||
// Implement get method, return multiple key/value pairs
|
||||
h := checkHash(state, 1)
|
||||
result := state.NewTable()
|
||||
args := state.CheckTable(2)
|
||||
args.ForEach(func(index lua.LValue, field lua.LValue) {
|
||||
if _, ok := index.(lua.LNumber); !ok {
|
||||
state.ArgError(2, "expected key to be a number")
|
||||
return
|
||||
}
|
||||
if _, ok := field.(lua.LString); !ok {
|
||||
state.ArgError(2, "expected field to be a string")
|
||||
return
|
||||
}
|
||||
var value lua.LValue
|
||||
if _, exists := h[field.String()]; exists {
|
||||
value = nativeTypeToLuaType(state, h[field.String()].Value)
|
||||
} else {
|
||||
value = lua.LNil
|
||||
}
|
||||
result.RawSet(field, value)
|
||||
})
|
||||
state.Push(result)
|
||||
return 1
|
||||
},
|
||||
"len": func(state *lua.LState) int {
|
||||
// Implement method len, returns the length of the hash
|
||||
h := checkHash(state, 1)
|
||||
state.Push(lua.LNumber(len(h)))
|
||||
return 1
|
||||
},
|
||||
"all": func(state *lua.LState) int {
|
||||
// Implement method all, returns all key/value pairs in the hash
|
||||
h := checkHash(state, 1)
|
||||
result := state.NewTable()
|
||||
for field, hashValue := range h {
|
||||
result.RawSetString(field, lua.LString(hashValue.Value.(string)))
|
||||
}
|
||||
state.Push(result)
|
||||
return 1
|
||||
},
|
||||
"exists": func(state *lua.LState) int {
|
||||
// Checks if the value exists in the hash
|
||||
h := checkHash(state, 1)
|
||||
result := state.NewTable()
|
||||
args := state.CheckTable(2)
|
||||
args.ForEach(func(index lua.LValue, field lua.LValue) {
|
||||
if _, ok := index.(lua.LNumber); !ok {
|
||||
state.ArgError(2, "expected table key to be number")
|
||||
return
|
||||
}
|
||||
if _, ok := field.(lua.LString); !ok {
|
||||
state.ArgError(2, "expected field to be a string")
|
||||
return
|
||||
}
|
||||
_, exists := h[field.String()]
|
||||
result.RawSet(field, lua.LBool(exists))
|
||||
})
|
||||
state.Push(result)
|
||||
return 1
|
||||
},
|
||||
"del": func(state *lua.LState) int {
|
||||
// Delete multiple fields from a hash, return the number of deleted fields
|
||||
h := checkHash(state, 1)
|
||||
var count int
|
||||
args := state.CheckTable(2)
|
||||
args.ForEach(func(index lua.LValue, field lua.LValue) {
|
||||
if _, ok := index.(lua.LNumber); !ok {
|
||||
state.ArgError(2, "expected table key to be index")
|
||||
return
|
||||
}
|
||||
if _, ok := field.(lua.LString); !ok {
|
||||
state.ArgError(2, "expected field value to be a string")
|
||||
return
|
||||
}
|
||||
if _, exists := h[field.String()]; exists {
|
||||
delete(h, field.String())
|
||||
count += 1
|
||||
}
|
||||
})
|
||||
state.Push(lua.LNumber(count))
|
||||
return 1
|
||||
},
|
||||
}))
|
||||
|
||||
// Register set data type
|
||||
setMetaTable := L.NewTypeMetatable("set")
|
||||
L.SetGlobal("set", setMetaTable)
|
||||
// Static methods
|
||||
L.SetField(setMetaTable, "new", L.NewFunction(func(state *lua.LState) int {
|
||||
// Create set
|
||||
s := set.NewSet([]string{})
|
||||
// If the default values are passed, add them to the set.
|
||||
if state.GetTop() == 1 {
|
||||
elems := state.CheckTable(1)
|
||||
elems.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
s.Add([]string{value.String()})
|
||||
})
|
||||
state.Pop(1)
|
||||
}
|
||||
// Push the set to the stack
|
||||
ud := state.NewUserData()
|
||||
ud.Value = s
|
||||
state.SetMetatable(ud, state.GetTypeMetatable("set"))
|
||||
state.Push(ud)
|
||||
return 1
|
||||
}))
|
||||
// Set methods
|
||||
L.SetField(setMetaTable, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
|
||||
"add": func(state *lua.LState) int {
|
||||
s := checkSet(state, 1)
|
||||
// Extract the elements from the args
|
||||
var elems []string
|
||||
tbl := state.CheckTable(2)
|
||||
tbl.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
elems = append(elems, value.String())
|
||||
})
|
||||
// Add the elements to the set
|
||||
state.Push(lua.LNumber(s.Add(elems)))
|
||||
return 1
|
||||
},
|
||||
"pop": func(state *lua.LState) int {
|
||||
s := checkSet(state, 1)
|
||||
count := state.CheckNumber(2)
|
||||
// Create the table of popped elements
|
||||
popped := state.NewTable()
|
||||
for i, elem := range s.Pop(int(count)) {
|
||||
popped.RawSetInt(i+1, lua.LString(elem))
|
||||
}
|
||||
// Return popped elements
|
||||
state.Push(popped)
|
||||
return 1
|
||||
},
|
||||
"contains": func(state *lua.LState) int {
|
||||
s := checkSet(state, 1)
|
||||
state.Push(lua.LBool(s.Contains(state.CheckString(2))))
|
||||
return 1
|
||||
},
|
||||
"cardinality": func(state *lua.LState) int {
|
||||
s := checkSet(state, 1)
|
||||
state.Push(lua.LNumber(s.Cardinality()))
|
||||
return 1
|
||||
},
|
||||
"remove": func(state *lua.LState) int {
|
||||
s := checkSet(state, 1)
|
||||
// Extract elements to be removed
|
||||
var elems []string
|
||||
tbl := state.CheckTable(2)
|
||||
tbl.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
elems = append(elems, value.String())
|
||||
})
|
||||
// Remove the elements and return the removed count
|
||||
state.Push(lua.LNumber(s.Remove(elems)))
|
||||
return 1
|
||||
},
|
||||
"move": func(state *lua.LState) int {
|
||||
s1 := checkSet(state, 1)
|
||||
s2 := checkSet(state, 2)
|
||||
elem := state.CheckString(3)
|
||||
moved := s1.Move(s2, elem)
|
||||
state.Push(lua.LBool(moved == 1))
|
||||
return 1
|
||||
},
|
||||
"subtract": func(state *lua.LState) int {
|
||||
s1 := checkSet(state, 1)
|
||||
var sets []*set.Set
|
||||
// Extract sets to subtract
|
||||
tbl := state.CheckTable(2)
|
||||
tbl.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
ud, ok := value.(*lua.LUserData)
|
||||
if !ok {
|
||||
state.ArgError(2, "table must only contain sets")
|
||||
return
|
||||
}
|
||||
s, ok := ud.Value.(*set.Set)
|
||||
if !ok {
|
||||
state.ArgError(2, "table must only contain sets")
|
||||
return
|
||||
}
|
||||
sets = append(sets, s)
|
||||
})
|
||||
// Return the resulting set
|
||||
ud := state.NewUserData()
|
||||
ud.Value = s1.Subtract(sets)
|
||||
state.SetMetatable(ud, state.GetTypeMetatable("set"))
|
||||
state.Push(ud)
|
||||
return 1
|
||||
},
|
||||
"all": func(state *lua.LState) int {
|
||||
s := checkSet(state, 1)
|
||||
// Build table of all the elements in the set
|
||||
elems := state.NewTable()
|
||||
for i, e := range s.GetAll() {
|
||||
elems.RawSetInt(i+1, lua.LString(e))
|
||||
}
|
||||
// Return all the set's elements
|
||||
state.Push(elems)
|
||||
return 1
|
||||
},
|
||||
"random": func(state *lua.LState) int {
|
||||
s := checkSet(state, 1)
|
||||
count := state.CheckNumber(2)
|
||||
// Build table of random elements
|
||||
elems := state.NewTable()
|
||||
for i, e := range s.GetRandom(int(count)) {
|
||||
elems.RawSetInt(i+1, lua.LString(e))
|
||||
}
|
||||
// Return random elements
|
||||
state.Push(elems)
|
||||
return 1
|
||||
},
|
||||
}))
|
||||
|
||||
// Register sorted set member data type
|
||||
sortedSetMemberMetaTable := L.NewTypeMetatable("zmember")
|
||||
L.SetGlobal("zmember", sortedSetMemberMetaTable)
|
||||
// Static methods
|
||||
L.SetField(sortedSetMemberMetaTable, "new", L.NewFunction(func(state *lua.LState) int {
|
||||
// Create sorted set member param
|
||||
param := &sorted_set.MemberParam{}
|
||||
// Make sure a value table is passed
|
||||
if state.GetTop() != 1 {
|
||||
state.ArgError(1, "expected table containing value and score to be passed")
|
||||
}
|
||||
// Set the passed values in params
|
||||
arg := state.CheckTable(1)
|
||||
arg.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
switch strings.ToLower(key.String()) {
|
||||
case "score":
|
||||
if score, ok := value.(lua.LNumber); ok {
|
||||
param.Score = sorted_set.Score(score)
|
||||
return
|
||||
}
|
||||
state.ArgError(1, "score is not a number")
|
||||
case "value":
|
||||
param.Value = sorted_set.Value(value.String())
|
||||
default:
|
||||
state.ArgError(1, fmt.Sprintf("unexpected key '%s' in zmember table", key.String()))
|
||||
}
|
||||
})
|
||||
// Check if value is not empty
|
||||
if param.Value == "" {
|
||||
state.ArgError(1, fmt.Sprintf("value is empty string"))
|
||||
}
|
||||
// Push the param to the stack and return
|
||||
ud := state.NewUserData()
|
||||
ud.Value = param
|
||||
state.SetMetatable(ud, state.GetTypeMetatable("zmember"))
|
||||
state.Push(ud)
|
||||
return 1
|
||||
}))
|
||||
// Sorted set member methods
|
||||
L.SetField(sortedSetMemberMetaTable, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
|
||||
"value": func(state *lua.LState) int {
|
||||
m := checkSortedSetMember(state, 1)
|
||||
if state.GetTop() == 2 {
|
||||
m.Value = sorted_set.Value(state.CheckString(2))
|
||||
return 0
|
||||
}
|
||||
L.Push(lua.LString(m.Value))
|
||||
return 1
|
||||
},
|
||||
"score": func(state *lua.LState) int {
|
||||
m := checkSortedSetMember(state, 1)
|
||||
if state.GetTop() == 2 {
|
||||
m.Score = sorted_set.Score(state.CheckNumber(2))
|
||||
return 0
|
||||
}
|
||||
L.Push(lua.LNumber(m.Score))
|
||||
return 1
|
||||
},
|
||||
}))
|
||||
|
||||
// Register sorted set data type
|
||||
sortedSetMetaTable := L.NewTypeMetatable("zset")
|
||||
L.SetGlobal("zset", sortedSetMetaTable)
|
||||
// Static methods
|
||||
L.SetField(sortedSetMetaTable, "new", L.NewFunction(func(state *lua.LState) int {
|
||||
// If default values are passed, add them to the set
|
||||
var members []sorted_set.MemberParam
|
||||
if state.GetTop() == 1 {
|
||||
params := state.CheckTable(1)
|
||||
params.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
d, ok := value.(*lua.LUserData)
|
||||
if !ok {
|
||||
state.ArgError(1, "expected user data")
|
||||
}
|
||||
if m, ok := d.Value.(*sorted_set.MemberParam); ok {
|
||||
members = append(members, sorted_set.MemberParam{Value: m.Value, Score: m.Score})
|
||||
return
|
||||
}
|
||||
state.ArgError(1, fmt.Sprintf("expected member param, got %s", value.Type().String()))
|
||||
})
|
||||
}
|
||||
// Create the sorted set
|
||||
ss := sorted_set.NewSortedSet(members)
|
||||
ud := state.NewUserData()
|
||||
ud.Value = ss
|
||||
state.SetMetatable(ud, state.GetTypeMetatable("zset"))
|
||||
state.Push(ud)
|
||||
return 1
|
||||
}))
|
||||
// Sorted set methods
|
||||
L.SetField(sortedSetMetaTable, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
|
||||
"add": func(state *lua.LState) int {
|
||||
ss := checkSortedSet(state, 1)
|
||||
|
||||
// Extract member params
|
||||
paramArgs := state.CheckTable(2)
|
||||
var params []sorted_set.MemberParam
|
||||
paramArgs.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
ud, ok := value.(*lua.LUserData)
|
||||
if !ok {
|
||||
state.ArgError(2, "expected zmember")
|
||||
}
|
||||
if m, ok := ud.Value.(*sorted_set.MemberParam); ok {
|
||||
params = append(params, sorted_set.MemberParam{Value: m.Value, Score: m.Score})
|
||||
return
|
||||
}
|
||||
state.ArgError(2, "expected zmember to be sorted set member param")
|
||||
})
|
||||
|
||||
// Extract the update options
|
||||
var updatePolicy interface{} = nil
|
||||
var comparison interface{} = nil
|
||||
var changed interface{} = nil
|
||||
var incr interface{} = nil
|
||||
if state.GetTop() == 3 {
|
||||
optsArgs := state.CheckTable(3)
|
||||
optsArgs.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
switch key.String() {
|
||||
default:
|
||||
state.ArgError(3, fmt.Sprintf("unknown option '%s'", key.String()))
|
||||
case "exists":
|
||||
if value == lua.LTrue {
|
||||
updatePolicy = "xx"
|
||||
} else {
|
||||
updatePolicy = "nx"
|
||||
}
|
||||
case "comparison":
|
||||
comparison = value.String()
|
||||
case "changed":
|
||||
if value == lua.LTrue {
|
||||
changed = "ch"
|
||||
}
|
||||
case "incr":
|
||||
if value == lua.LTrue {
|
||||
incr = "incr"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ch, err := ss.AddOrUpdate(params, updatePolicy, comparison, changed, incr)
|
||||
if err != nil {
|
||||
state.ArgError(3, err.Error())
|
||||
}
|
||||
L.Push(lua.LNumber(ch))
|
||||
return 1
|
||||
},
|
||||
"update": func(state *lua.LState) int {
|
||||
ss := checkSortedSet(state, 1)
|
||||
|
||||
// Extract member params
|
||||
paramArgs := state.CheckTable(2)
|
||||
var params []sorted_set.MemberParam
|
||||
paramArgs.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
ud, ok := value.(*lua.LUserData)
|
||||
if !ok {
|
||||
state.ArgError(2, "expected zmember")
|
||||
}
|
||||
if m, ok := ud.Value.(*sorted_set.MemberParam); ok {
|
||||
params = append(params, sorted_set.MemberParam{Value: m.Value, Score: m.Score})
|
||||
return
|
||||
}
|
||||
state.ArgError(2, "expected zmember to be sorted set member param")
|
||||
})
|
||||
|
||||
// Extract the update options
|
||||
var updatePolicy interface{} = nil
|
||||
var comparison interface{} = nil
|
||||
var changed interface{} = nil
|
||||
var incr interface{} = nil
|
||||
if state.GetTop() == 3 {
|
||||
optsArgs := state.CheckTable(3)
|
||||
optsArgs.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
switch key.String() {
|
||||
default:
|
||||
state.ArgError(3, fmt.Sprintf("unknown option '%s'", key.String()))
|
||||
case "exists":
|
||||
if value == lua.LTrue {
|
||||
updatePolicy = "xx"
|
||||
} else {
|
||||
updatePolicy = "nx"
|
||||
}
|
||||
case "comparison":
|
||||
comparison = value.String()
|
||||
case "changed":
|
||||
if value == lua.LTrue {
|
||||
changed = "ch"
|
||||
}
|
||||
case "incr":
|
||||
if value == lua.LTrue {
|
||||
incr = "incr"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ch, err := ss.AddOrUpdate(params, updatePolicy, comparison, changed, incr)
|
||||
if err != nil {
|
||||
state.ArgError(3, err.Error())
|
||||
}
|
||||
L.Push(lua.LNumber(ch))
|
||||
return 1
|
||||
},
|
||||
"remove": func(state *lua.LState) int {
|
||||
ss := checkSortedSet(state, 1)
|
||||
L.Push(lua.LBool(ss.Remove(sorted_set.Value(state.CheckString(2)))))
|
||||
return 1
|
||||
},
|
||||
"cardinality": func(state *lua.LState) int {
|
||||
state.Push(lua.LNumber(checkSortedSet(state, 1).Cardinality()))
|
||||
return 1
|
||||
},
|
||||
"contains": func(state *lua.LState) int {
|
||||
ss := checkSortedSet(state, 1)
|
||||
L.Push(lua.LBool(ss.Contains(sorted_set.Value(state.Get(-2).String()))))
|
||||
return 1
|
||||
},
|
||||
"random": func(state *lua.LState) int {
|
||||
ss := checkSortedSet(state, 1)
|
||||
count := 1
|
||||
// If a count is passed, use that
|
||||
if state.GetTop() == 2 {
|
||||
count = state.CheckInt(2)
|
||||
}
|
||||
// Build members table
|
||||
random := state.NewTable()
|
||||
members := ss.GetRandom(count)
|
||||
for i, member := range members {
|
||||
ud := state.NewUserData()
|
||||
ud.Value = sorted_set.MemberParam{Value: member.Value, Score: member.Score}
|
||||
state.SetMetatable(ud, state.GetTypeMetatable("zmember"))
|
||||
random.RawSetInt(i+1, ud)
|
||||
}
|
||||
// Push the table to the stack
|
||||
state.Push(random)
|
||||
return 1
|
||||
},
|
||||
"all": func(state *lua.LState) int {
|
||||
ss := checkSortedSet(state, 1)
|
||||
// Build members table
|
||||
members := state.NewTable()
|
||||
for i, member := range ss.GetAll() {
|
||||
ud := state.NewUserData()
|
||||
ud.Value = &sorted_set.MemberParam{Value: member.Value, Score: member.Score}
|
||||
state.SetMetatable(ud, state.GetTypeMetatable("zmember"))
|
||||
members.RawSetInt(i+1, ud)
|
||||
}
|
||||
// Push members table to stack and return
|
||||
state.Push(members)
|
||||
return 1
|
||||
},
|
||||
"subtract": func(state *lua.LState) int {
|
||||
ss := checkSortedSet(state, 1)
|
||||
// Get the sorted sets from the args
|
||||
var others []*sorted_set.SortedSet
|
||||
arg := state.CheckTable(2)
|
||||
arg.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
ud, ok := value.(*lua.LUserData)
|
||||
if !ok {
|
||||
state.ArgError(2, "expected user data")
|
||||
}
|
||||
zset, ok := ud.Value.(*sorted_set.SortedSet)
|
||||
if !ok {
|
||||
state.ArgError(2, fmt.Sprintf("expected zset at key '%s'", key.String()))
|
||||
}
|
||||
others = append(others, zset)
|
||||
})
|
||||
// Calculate result
|
||||
result := ss.Subtract(others)
|
||||
// Push result to the stack and return
|
||||
ud := state.NewUserData()
|
||||
ud.Value = result
|
||||
state.SetMetatable(ud, state.GetTypeMetatable("zset"))
|
||||
L.Push(ud)
|
||||
return 1
|
||||
},
|
||||
}))
|
||||
|
||||
// Get the command name
|
||||
cn := L.GetGlobal("command")
|
||||
if _, ok := cn.(lua.LString); !ok {
|
||||
return nil, "", nil, "", false, "", errors.New("command name does not exist or is not a string")
|
||||
}
|
||||
|
||||
// Get the categories
|
||||
c := L.GetGlobal("categories")
|
||||
var categories []string
|
||||
if _, ok := c.(*lua.LTable); !ok {
|
||||
return nil, "", nil, "", false, "", errors.New("categories does not exist or is not an array")
|
||||
}
|
||||
for i := 0; i < c.(*lua.LTable).Len(); i++ {
|
||||
categories = append(categories, c.(*lua.LTable).RawGetInt(i+1).String())
|
||||
}
|
||||
|
||||
// Get the description
|
||||
d := L.GetGlobal("description")
|
||||
if _, ok := d.(lua.LString); !ok {
|
||||
return nil, "", nil, "", false, "", errors.New("description does not exist or is not a string")
|
||||
}
|
||||
|
||||
// Get the sync
|
||||
synchronize := L.GetGlobal("sync") == lua.LTrue
|
||||
|
||||
// Set command type
|
||||
commandType := "LUA_SCRIPT"
|
||||
|
||||
return L, strings.ToLower(cn.String()), categories, d.String(), synchronize, commandType, nil
|
||||
}
|
||||
|
||||
func (server *SugarDB) buildLuaKeyExtractionFunc(vm any, cmd []string, args []string) (internal.KeyExtractionFuncResult, error) {
|
||||
L := vm.(*lua.LState)
|
||||
// Create command table to pass to the Lua function
|
||||
command := L.NewTable()
|
||||
for i, s := range cmd {
|
||||
command.RawSetInt(i+1, lua.LString(s))
|
||||
}
|
||||
// Create args table to pass to the Lua function
|
||||
funcArgs := L.NewTable()
|
||||
for i, s := range args {
|
||||
funcArgs.RawSetInt(i+1, lua.LString(s))
|
||||
}
|
||||
// Lock the script before executing the key extraction function
|
||||
script, ok := server.scriptVMs.Load(strings.ToLower(cmd[0]))
|
||||
if !ok {
|
||||
return internal.KeyExtractionFuncResult{}, fmt.Errorf("no lock found for script command %s", command)
|
||||
}
|
||||
machine := script.(struct {
|
||||
vm any
|
||||
lock *sync.Mutex
|
||||
})
|
||||
machine.lock.Lock()
|
||||
defer machine.lock.Unlock()
|
||||
// Call the Lua key extraction function
|
||||
var err error
|
||||
_ = L.CallByParam(lua.P{
|
||||
Fn: L.GetGlobal("keyExtractionFunc"),
|
||||
NRet: 1,
|
||||
Protect: true,
|
||||
Handler: L.NewFunction(func(state *lua.LState) int {
|
||||
err = errors.New(state.Get(-1).String())
|
||||
state.Pop(1)
|
||||
return 0
|
||||
}),
|
||||
}, command, funcArgs)
|
||||
// Check if error was thrown
|
||||
if err != nil {
|
||||
return internal.KeyExtractionFuncResult{}, err
|
||||
}
|
||||
defer L.Pop(1)
|
||||
if keys, ok := L.Get(-1).(*lua.LTable); ok {
|
||||
// If the returned value is a table, get the keys from the table
|
||||
return internal.KeyExtractionFuncResult{
|
||||
Channels: make([]string, 0),
|
||||
ReadKeys: func() []string {
|
||||
table := keys.RawGetString("readKeys").(*lua.LTable)
|
||||
var k []string
|
||||
for i := 1; i <= table.Len(); i++ {
|
||||
k = append(k, table.RawGetInt(i).String())
|
||||
}
|
||||
return k
|
||||
}(),
|
||||
WriteKeys: func() []string {
|
||||
table := keys.RawGetString("writeKeys").(*lua.LTable)
|
||||
var k []string
|
||||
for i := 1; i <= table.Len(); i++ {
|
||||
k = append(k, table.RawGetInt(i).String())
|
||||
}
|
||||
return k
|
||||
}(),
|
||||
}, nil
|
||||
} else {
|
||||
// If the returned value is not a table, return error
|
||||
return internal.KeyExtractionFuncResult{},
|
||||
fmt.Errorf("key extraction must return a table, got %s", L.Get(-1).Type())
|
||||
}
|
||||
}
|
||||
|
||||
func (server *SugarDB) buildLuaHandlerFunc(vm any, command string, args []string, params internal.HandlerFuncParams) ([]byte, error) {
|
||||
L := vm.(*lua.LState)
|
||||
// Lua table context
|
||||
ctx := L.NewTable()
|
||||
ctx.RawSetString("protocol", lua.LNumber(params.Context.Value("Protocol").(int)))
|
||||
ctx.RawSetString("database", lua.LNumber(params.Context.Value("Database").(int)))
|
||||
// Command that triggered the handler (Array)
|
||||
cmd := L.NewTable()
|
||||
for i, s := range params.Command {
|
||||
cmd.RawSetInt(i+1, lua.LString(s))
|
||||
}
|
||||
// Function that checks if keys exist
|
||||
keysExist := L.NewFunction(func(state *lua.LState) int {
|
||||
// Get the keys array and pop it from the stack.
|
||||
v := state.CheckTable(1)
|
||||
state.Pop(1)
|
||||
// Extract the keys from the keys array passed from the lua script.
|
||||
var keys []string
|
||||
for i := 1; i <= v.Len(); i++ {
|
||||
keys = append(keys, v.RawGetInt(i).String())
|
||||
}
|
||||
// Call the keysExist method to check if the key exists in the store.
|
||||
exist := server.keysExist(params.Context, keys)
|
||||
// Build the response table that specifies if each key exists.
|
||||
res := state.NewTable()
|
||||
for key, exists := range exist {
|
||||
res.RawSetString(key, lua.LBool(exists))
|
||||
}
|
||||
// Push the response to the stack.
|
||||
state.Push(res)
|
||||
return 1
|
||||
})
|
||||
// Function that gets values from keys
|
||||
getValues := L.NewFunction(func(state *lua.LState) int {
|
||||
// Get the keys array and pop it from the stack.
|
||||
v := state.CheckTable(1)
|
||||
state.Pop(1)
|
||||
// Extract the keys from the keys array passed from the lua script.
|
||||
var keys []string
|
||||
for i := 1; i <= v.Len(); i++ {
|
||||
keys = append(keys, v.RawGetInt(i).String())
|
||||
}
|
||||
// Call the getValues method to get the values for each of the keys.
|
||||
values := server.getValues(params.Context, keys)
|
||||
// Build the response table that contains each key/value pair.
|
||||
res := state.NewTable()
|
||||
for key, value := range values {
|
||||
// Actually parse the value and set it in the response as the appropriate LValue.
|
||||
res.RawSetString(key, nativeTypeToLuaType(state, value))
|
||||
}
|
||||
// Push the value to the stack
|
||||
state.Push(res)
|
||||
return 1
|
||||
})
|
||||
// Function that sets values on keys
|
||||
setValues := L.NewFunction(func(state *lua.LState) int {
|
||||
// Get the key/value table.
|
||||
v := state.CheckTable(1)
|
||||
// Get values passed from the Lua script and add.
|
||||
values := make(map[string]interface{})
|
||||
var err error
|
||||
v.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
// Actually parse the value and set it in the response as the appropriate LValue.
|
||||
values[key.String()], err = luaTypeToNativeType(value)
|
||||
if err != nil {
|
||||
state.ArgError(1, err.Error())
|
||||
}
|
||||
})
|
||||
if err = server.setValues(params.Context, values); err != nil {
|
||||
state.ArgError(1, err.Error())
|
||||
}
|
||||
// pop key/value table from the stack
|
||||
state.Pop(1)
|
||||
return 0
|
||||
})
|
||||
// Args (Array)
|
||||
funcArgs := L.NewTable()
|
||||
for i, s := range args {
|
||||
funcArgs.RawSetInt(i+1, lua.LString(s))
|
||||
}
|
||||
// Lock this script's execution key before executing the handler
|
||||
script, ok := server.scriptVMs.Load(command)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no lock found for script command %s", command)
|
||||
}
|
||||
lock := script.(struct {
|
||||
vm any
|
||||
lock *sync.Mutex
|
||||
})
|
||||
lock.lock.Lock()
|
||||
defer lock.lock.Unlock()
|
||||
// Call the lua handler function
|
||||
var err error
|
||||
_ = L.CallByParam(lua.P{
|
||||
Fn: L.GetGlobal("handlerFunc"),
|
||||
NRet: 1,
|
||||
Protect: true,
|
||||
Handler: L.NewFunction(func(state *lua.LState) int {
|
||||
err = errors.New(state.Get(-1).String())
|
||||
state.Pop(1)
|
||||
return 0
|
||||
}),
|
||||
}, ctx, cmd, keysExist, getValues, setValues, funcArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Get and pop the 2 values at the top of the stack, checking whether an error is returned.
|
||||
defer L.Pop(1)
|
||||
return []byte(L.Get(-1).String()), nil
|
||||
}
|
||||
|
||||
func checkHash(L *lua.LState, n int) hash.Hash {
|
||||
ud := L.CheckUserData(n)
|
||||
if v, ok := ud.Value.(hash.Hash); ok {
|
||||
return v
|
||||
}
|
||||
L.ArgError(n, "hash expected")
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkSet(L *lua.LState, n int) *set.Set {
|
||||
ud := L.CheckUserData(n)
|
||||
if v, ok := ud.Value.(*set.Set); ok {
|
||||
return v
|
||||
}
|
||||
L.ArgError(n, "set expected")
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkSortedSetMember(L *lua.LState, n int) *sorted_set.MemberParam {
|
||||
ud := L.CheckUserData(n)
|
||||
if v, ok := ud.Value.(*sorted_set.MemberParam); ok {
|
||||
return v
|
||||
}
|
||||
L.ArgError(n, "zmember expected")
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkSortedSet(L *lua.LState, n int) *sorted_set.SortedSet {
|
||||
ud := L.CheckUserData(n)
|
||||
if v, ok := ud.Value.(*sorted_set.SortedSet); ok {
|
||||
return v
|
||||
}
|
||||
L.ArgError(n, "zset expected")
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkArray(table *lua.LTable) ([]string, error) {
|
||||
list := make([]string, table.Len())
|
||||
var err error = nil
|
||||
table.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
// Check if key is integer
|
||||
idx, ok := key.(lua.LNumber)
|
||||
if !ok {
|
||||
err = fmt.Errorf("expected list keys to be integers, got %s", key.Type())
|
||||
return
|
||||
}
|
||||
// Check if value is string
|
||||
val, ok := value.(lua.LString)
|
||||
if !ok {
|
||||
err = fmt.Errorf("expect all list values to be strings, got %s", key.Type())
|
||||
return
|
||||
}
|
||||
if int(idx)-1 >= len(list) {
|
||||
err = fmt.Errorf("index %d greater than list capacity %d", int(idx), len(list))
|
||||
return
|
||||
}
|
||||
list[int(idx)-1] = val.String()
|
||||
})
|
||||
return list, err
|
||||
}
|
||||
|
||||
func luaTypeToNativeType(value lua.LValue) (interface{}, error) {
|
||||
switch value.Type() {
|
||||
case lua.LTNil:
|
||||
return nil, nil
|
||||
case lua.LTString:
|
||||
return value.String(), nil
|
||||
case lua.LTNumber:
|
||||
return internal.AdaptType(value.String()), nil
|
||||
case lua.LTTable:
|
||||
return checkArray(value.(*lua.LTable))
|
||||
case lua.LTUserData:
|
||||
switch value.(*lua.LUserData).Value.(type) {
|
||||
default:
|
||||
return nil, errors.New("unknown user data")
|
||||
case hash.Hash:
|
||||
return value.(*lua.LUserData).Value.(hash.Hash), nil
|
||||
case *set.Set:
|
||||
return value.(*lua.LUserData).Value.(*set.Set), nil
|
||||
case *sorted_set.SortedSet:
|
||||
return value.(*lua.LUserData).Value.(*sorted_set.SortedSet), nil
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown type %s", value.Type())
|
||||
}
|
||||
}
|
||||
|
||||
func nativeTypeToLuaType(L *lua.LState, value interface{}) lua.LValue {
|
||||
switch value.(type) {
|
||||
case string:
|
||||
return lua.LString(value.(string))
|
||||
case float32:
|
||||
return lua.LNumber(value.(float32))
|
||||
case float64:
|
||||
return lua.LNumber(value.(float64))
|
||||
case int, int64:
|
||||
return lua.LNumber(value.(int))
|
||||
case []string:
|
||||
tbl := L.NewTable()
|
||||
for i, element := range value.([]string) {
|
||||
tbl.RawSetInt(i+1, lua.LString(element))
|
||||
}
|
||||
return tbl
|
||||
case hash.Hash:
|
||||
ud := L.NewUserData()
|
||||
ud.Value = value.(hash.Hash)
|
||||
L.SetMetatable(ud, L.GetTypeMetatable("hash"))
|
||||
return ud
|
||||
case *set.Set:
|
||||
ud := L.NewUserData()
|
||||
ud.Value = value.(*set.Set)
|
||||
L.SetMetatable(ud, L.GetTypeMetatable("set"))
|
||||
return ud
|
||||
case *sorted_set.SortedSet:
|
||||
ud := L.NewUserData()
|
||||
ud.Value = value.(*sorted_set.SortedSet)
|
||||
L.SetMetatable(ud, L.GetTypeMetatable("zset"))
|
||||
return ud
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -39,10 +39,12 @@ import (
|
||||
str "github.com/echovault/sugardb/internal/modules/string"
|
||||
"github.com/echovault/sugardb/internal/raft"
|
||||
"github.com/echovault/sugardb/internal/snapshot"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -52,7 +54,7 @@ type SugarDB struct {
|
||||
// clock is an implementation of a time interface that allows mocking of time functions during testing.
|
||||
clock clock.Clock
|
||||
|
||||
// config holds the echovault configuration variables.
|
||||
// config holds the SugarDB configuration variables.
|
||||
config config.Config
|
||||
|
||||
// The current index for the latest connection id.
|
||||
@@ -101,12 +103,16 @@ type SugarDB struct {
|
||||
cache map[int]*eviction.CacheLRU
|
||||
}
|
||||
|
||||
// Holds the list of all commands supported by the echovault.
|
||||
commandsRWMut sync.RWMutex
|
||||
commands []internal.Command
|
||||
commandsRWMut sync.RWMutex // Mutex used for modifying/reading the list of commands in the instance.
|
||||
commands []internal.Command // Holds the list of all commands supported by SugarDB.
|
||||
// Each commands that's added using a script (e.g. lua), will have a lock associated with the command.
|
||||
// Only one goroutine will be able to trigger a script-associated command at a time. This is because the VM state
|
||||
// for each of the commands is not thread safe.
|
||||
// This map's shape is map[string]struct{vm: any, lock: sync.Mutex} with the string key being the command name.
|
||||
scriptVMs sync.Map
|
||||
|
||||
raft *raft.Raft // The raft replication layer for the echovault.
|
||||
memberList *memberlist.MemberList // The memberlist layer for the echovault.
|
||||
raft *raft.Raft // The raft replication layer for SugarDB.
|
||||
memberList *memberlist.MemberList // The memberlist layer for SugarDB.
|
||||
|
||||
context context.Context
|
||||
|
||||
@@ -126,24 +132,6 @@ type SugarDB struct {
|
||||
stopTTL chan struct{} // Channel that signals the TTL sampling goroutine to stop execution.
|
||||
}
|
||||
|
||||
// WithContext is an options that for the NewSugarDB function that allows you to
|
||||
// configure a custom context object to be used in SugarDB.
|
||||
// If you don't provide this option, SugarDB will create its own internal context object.
|
||||
func WithContext(ctx context.Context) func(echovault *SugarDB) {
|
||||
return func(echovault *SugarDB) {
|
||||
echovault.context = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// WithConfig is an option for the NewSugarDB function that allows you to pass a
|
||||
// custom configuration to SugarDB.
|
||||
// If not specified, SugarDB will use the default configuration from config.DefaultConfig().
|
||||
func WithConfig(config config.Config) func(echovault *SugarDB) {
|
||||
return func(echovault *SugarDB) {
|
||||
echovault.config = config
|
||||
}
|
||||
}
|
||||
|
||||
// NewSugarDB creates a new SugarDB instance.
|
||||
// This functions accepts the WithContext, WithConfig and WithCommands options.
|
||||
func NewSugarDB(options ...func(sugarDB *SugarDB)) (*SugarDB, error) {
|
||||
@@ -569,7 +557,7 @@ func (server *SugarDB) handleConnection(conn net.Conn) {
|
||||
// Start starts the SugarDB instance's TCP listener.
|
||||
// This allows the instance to accept connections handle client commands over TCP.
|
||||
//
|
||||
// You can still use command functions like echovault.Set if you're embedding SugarDB in your application.
|
||||
// You can still use command functions like Set if you're embedding SugarDB in your application.
|
||||
// However, if you'd like to also accept TCP request on the same instance, you must call this function.
|
||||
func (server *SugarDB) Start() {
|
||||
server.startTCP()
|
||||
@@ -640,15 +628,41 @@ func (server *SugarDB) ShutDown() {
|
||||
if server.listener.Load() != nil {
|
||||
go func() { server.quit <- struct{}{} }()
|
||||
go func() { server.stopTTL <- struct{}{} }()
|
||||
|
||||
log.Println("closing tcp listener...")
|
||||
if err := server.listener.Load().(net.Listener).Close(); err != nil {
|
||||
log.Printf("listener close: %v\n", err)
|
||||
}
|
||||
}
|
||||
if !server.isInCluster() {
|
||||
server.aofEngine.Close()
|
||||
|
||||
// Shutdown all script VMs
|
||||
log.Println("shutting down script vms...")
|
||||
server.commandsRWMut.Lock()
|
||||
for _, command := range server.commands {
|
||||
if slices.Contains([]string{"LUA_SCRIPT"}, command.Type) {
|
||||
v, ok := server.scriptVMs.Load(command.Command)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
machine := v.(struct {
|
||||
vm any
|
||||
lock *sync.Mutex
|
||||
})
|
||||
machine.lock.Lock()
|
||||
switch command.Type {
|
||||
case "LUA_SCRIPT":
|
||||
machine.vm.(*lua.LState).Close()
|
||||
}
|
||||
machine.lock.Unlock()
|
||||
}
|
||||
}
|
||||
if server.isInCluster() {
|
||||
server.commandsRWMut.Unlock()
|
||||
|
||||
if !server.isInCluster() {
|
||||
// Server is not in cluster, run standalone-only shutdown processes.
|
||||
server.aofEngine.Close()
|
||||
} else {
|
||||
// Server is in cluster, run cluster-only shutdown processes.
|
||||
server.raft.RaftShutdown()
|
||||
server.memberList.MemberListShutdown()
|
||||
}
|
||||
|
Reference in New Issue
Block a user