Extend SugarDB Commands Using Lua Scripts (#155)

* Extend SugarDB by creating new commands using Lua - @kelvinmwinuka
This commit is contained in:
Kelvin Mwinuka
2024-12-12 09:50:43 +08:00
committed by GitHub
parent 3b15061dbc
commit 108bf97b4d
41 changed files with 9111 additions and 13573 deletions

View File

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

View File

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

View File

@@ -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
View 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 doesnt 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})
```

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View 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;

File diff suppressed because it is too large Load Diff

18
go.mod
View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View 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

View 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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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