├── .gitignore ├── .dockerignore ├── internal ├── testx │ ├── testx.go │ ├── postgres.go │ ├── sqlite3.go │ └── db.go ├── rstring │ ├── postgres.go │ └── sqlite.go ├── sqlx │ ├── drop.sql │ └── sql.go ├── rlist │ └── postgres.go ├── rhash │ ├── postgres.go │ └── scanner.go ├── rset │ ├── postgres.go │ └── scanner.go ├── rzset │ ├── postgres.go │ └── scanner.go └── rkey │ ├── postgres.go │ ├── scanner.go │ └── sqlite.go ├── redsrv ├── internal │ ├── command │ │ ├── key │ │ │ ├── key_test.go │ │ │ ├── flushdb.go │ │ │ ├── del.go │ │ │ ├── keys.go │ │ │ ├── rename.go │ │ │ ├── exists.go │ │ │ ├── randomkey.go │ │ │ ├── type.go │ │ │ ├── persist.go │ │ │ ├── renamenx.go │ │ │ ├── ttl.go │ │ │ ├── expire.go │ │ │ ├── expireat.go │ │ │ ├── flushdb_test.go │ │ │ ├── randomkey_test.go │ │ │ ├── type_test.go │ │ │ ├── exists_test.go │ │ │ ├── ttl_test.go │ │ │ ├── del_test.go │ │ │ └── scan.go │ │ ├── conn │ │ │ ├── conn_test.go │ │ │ ├── echo.go │ │ │ ├── select.go │ │ │ ├── ping.go │ │ │ ├── select_test.go │ │ │ ├── ping_test.go │ │ │ └── echo_test.go │ │ ├── hash │ │ │ ├── hash_test.go │ │ │ ├── hlen.go │ │ │ ├── hvals.go │ │ │ ├── hkeys.go │ │ │ ├── hgetall.go │ │ │ ├── hexists.go │ │ │ ├── hset.go │ │ │ ├── hget.go │ │ │ ├── hmset.go │ │ │ ├── hsetnx.go │ │ │ ├── hdel.go │ │ │ ├── hincrby.go │ │ │ ├── hincrbyfloat.go │ │ │ ├── hscan.go │ │ │ ├── hmget.go │ │ │ ├── hlen_test.go │ │ │ ├── hkeys_test.go │ │ │ ├── hvals_test.go │ │ │ └── hgetall_test.go │ │ ├── list │ │ │ ├── list_test.go │ │ │ ├── llen.go │ │ │ ├── lpush.go │ │ │ ├── rpush.go │ │ │ ├── ltrim.go │ │ │ ├── rpop.go │ │ │ ├── lpop.go │ │ │ ├── lrange.go │ │ │ ├── lindex.go │ │ │ ├── lset.go │ │ │ ├── rpoplpush.go │ │ │ ├── lrem.go │ │ │ └── linsert.go │ │ ├── zset │ │ │ ├── zset_test.go │ │ │ ├── zcard.go │ │ │ ├── zrem.go │ │ │ ├── zcount.go │ │ │ ├── zadd.go │ │ │ ├── zincrby.go │ │ │ ├── zremrangebyrank.go │ │ │ ├── zscore.go │ │ │ ├── zremrangebyscore.go │ │ │ ├── zrank.go │ │ │ ├── zrevrank.go │ │ │ ├── zscan.go │ │ │ ├── zinterstore.go │ │ │ ├── zunionstore.go │ │ │ ├── zrevrange.go │ │ │ ├── zunion.go │ │ │ ├── zinter.go │ │ │ ├── zrangebyscore.go │ │ │ └── zrevrangebyscore.go │ │ ├── server │ │ │ ├── server_test.go │ │ │ ├── ok.go │ │ │ ├── unknown.go │ │ │ ├── dbsize.go │ │ │ ├── configget.go │ │ │ ├── config.go │ │ │ ├── dbsize_test.go │ │ │ ├── lolwut_test.go │ │ │ ├── config_test.go │ │ │ └── lolwut.go │ │ ├── string │ │ │ ├── string_test.go │ │ │ ├── get.go │ │ │ ├── mset.go │ │ │ ├── strlen.go │ │ │ ├── setnx.go │ │ │ ├── incr.go │ │ │ ├── getset.go │ │ │ ├── incrbyfloat.go │ │ │ ├── setex.go │ │ │ ├── incrby.go │ │ │ ├── mget.go │ │ │ ├── strlen_test.go │ │ │ ├── get_test.go │ │ │ ├── incr_test.go │ │ │ ├── decr_test.go │ │ │ ├── setnx_test.go │ │ │ ├── incrby_test.go │ │ │ ├── decrby_test.go │ │ │ ├── mget_test.go │ │ │ └── getset_test.go │ │ └── set │ │ │ ├── set_test.go │ │ │ ├── scard.go │ │ │ ├── smembers.go │ │ │ ├── spop.go │ │ │ ├── srem.go │ │ │ ├── sdiff.go │ │ │ ├── srandmember.go │ │ │ ├── sunion.go │ │ │ ├── sinter.go │ │ │ ├── sadd.go │ │ │ ├── sdiffstore.go │ │ │ ├── sunionstore.go │ │ │ ├── sinterstore.go │ │ │ ├── sismember.go │ │ │ ├── smove.go │ │ │ └── sscan.go │ ├── parser │ │ ├── parsers_test.go │ │ └── pipeline.go │ └── redis │ │ └── conn.go ├── debug.go └── state.go ├── go.mod ├── Dockerfile ├── docs ├── commands │ ├── server.md │ ├── transactions.md │ ├── hashes.md │ ├── lists.md │ ├── sets.md │ ├── strings.md │ └── keys.md ├── install-module.md ├── roadmap.md ├── install-standalone.md ├── persistence.md └── usage-standalone.md ├── example ├── mattn │ └── main.go ├── ncruces │ └── main.go ├── modernc │ └── main.go ├── postgres │ └── main.go ├── tx │ └── main.go ├── go.mod └── server │ └── main.go ├── go.sum ├── LICENSE ├── .github └── workflows │ ├── docker.yml │ └── build.yml └── Makefile /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .git 3 | .gitignore 4 | .vscode/ 5 | build/ 6 | Dockerfile 7 | -------------------------------------------------------------------------------- /internal/testx/testx.go: -------------------------------------------------------------------------------- 1 | // Package testx provides helper functions for testing. 2 | package testx 3 | -------------------------------------------------------------------------------- /internal/testx/postgres.go: -------------------------------------------------------------------------------- 1 | //go:build postgres 2 | 3 | package testx 4 | 5 | import ( 6 | _ "github.com/lib/pq" 7 | ) 8 | 9 | func init() { 10 | driver = "postgres" 11 | } 12 | -------------------------------------------------------------------------------- /internal/testx/sqlite3.go: -------------------------------------------------------------------------------- 1 | //go:build sqlite3 2 | 3 | package testx 4 | 5 | import ( 6 | _ "github.com/mattn/go-sqlite3" 7 | ) 8 | 9 | func init() { 10 | driver = "sqlite3" 11 | } 12 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/key_test.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/redka/internal/testx" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func getRedka(tb testing.TB) redis.Redka { 11 | tb.Helper() 12 | db := testx.OpenDB(tb) 13 | return redis.RedkaDB(db) 14 | } 15 | -------------------------------------------------------------------------------- /redsrv/internal/command/conn/conn_test.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/redka/internal/testx" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func getRedka(tb testing.TB) redis.Redka { 11 | tb.Helper() 12 | db := testx.OpenDB(tb) 13 | return redis.RedkaDB(db) 14 | } 15 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hash_test.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/redka/internal/testx" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func getRedka(tb testing.TB) redis.Redka { 11 | tb.Helper() 12 | db := testx.OpenDB(tb) 13 | return redis.RedkaDB(db) 14 | } 15 | -------------------------------------------------------------------------------- /redsrv/internal/command/list/list_test.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/redka/internal/testx" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func getRedka(tb testing.TB) redis.Redka { 11 | tb.Helper() 12 | db := testx.OpenDB(tb) 13 | return redis.RedkaDB(db) 14 | } 15 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zset_test.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/redka/internal/testx" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func getRedka(tb testing.TB) redis.Redka { 11 | tb.Helper() 12 | db := testx.OpenDB(tb) 13 | return redis.RedkaDB(db) 14 | } 15 | -------------------------------------------------------------------------------- /redsrv/internal/command/server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/redka/internal/testx" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func getRedka(tb testing.TB) redis.Redka { 11 | tb.Helper() 12 | db := testx.OpenDB(tb) 13 | return redis.RedkaDB(db) 14 | } 15 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/string_test.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/redka/internal/testx" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func getRedka(tb testing.TB) redis.Redka { 11 | tb.Helper() 12 | db := testx.OpenDB(tb) 13 | return redis.RedkaDB(db) 14 | } 15 | -------------------------------------------------------------------------------- /internal/rstring/postgres.go: -------------------------------------------------------------------------------- 1 | package rstring 2 | 3 | // Postgres queries for the string repository. 4 | var postgres queries 5 | 6 | func init() { 7 | postgres.get = sqlite.get 8 | postgres.getMany = sqlite.getMany 9 | postgres.set1 = sqlite.set1 10 | postgres.set2 = sqlite.set2 11 | postgres.update1 = sqlite.update1 12 | postgres.update2 = sqlite.update2 13 | } 14 | -------------------------------------------------------------------------------- /internal/sqlx/drop.sql: -------------------------------------------------------------------------------- 1 | drop view if exists vkey; 2 | drop view if exists vstring; 3 | drop view if exists vlist; 4 | drop view if exists vset; 5 | drop view if exists vhash; 6 | drop view if exists vzset; 7 | 8 | drop table if exists rstring; 9 | drop table if exists rlist; 10 | drop table if exists rset; 11 | drop table if exists rhash; 12 | drop table if exists rzset; 13 | drop table if exists rkey; 14 | -------------------------------------------------------------------------------- /redsrv/internal/command/server/ok.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Dummy command that always returns OK. 6 | type OK struct { 7 | redis.BaseCmd 8 | } 9 | 10 | func ParseOK(b redis.BaseCmd) (OK, error) { 11 | return OK{BaseCmd: b}, nil 12 | } 13 | 14 | func (c OK) Run(w redis.Writer, _ redis.Redka) (any, error) { 15 | w.WriteString("OK") 16 | return true, nil 17 | } 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nalgeon/redka 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.0 6 | 7 | // Main dependencies. 8 | require github.com/tidwall/redcon v1.6.2 9 | 10 | // Test dependencies. 11 | require ( 12 | github.com/lib/pq v1.10.9 13 | github.com/mattn/go-sqlite3 v1.14.28 14 | github.com/nalgeon/be v0.2.0 15 | ) 16 | 17 | require ( 18 | github.com/tidwall/btree v1.7.0 // indirect 19 | github.com/tidwall/match v1.1.1 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /redsrv/internal/command/server/unknown.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Unknown is a placeholder for unknown commands. 6 | // Always returns an error. 7 | type Unknown struct { 8 | redis.BaseCmd 9 | } 10 | 11 | func ParseUnknown(b redis.BaseCmd) (Unknown, error) { 12 | return Unknown{BaseCmd: b}, nil 13 | } 14 | 15 | func (cmd Unknown) Run(w redis.Writer, _ redis.Redka) (any, error) { 16 | err := redis.ErrUnknownCmd 17 | w.WriteError(cmd.Error(err)) 18 | return nil, err 19 | } 20 | -------------------------------------------------------------------------------- /redsrv/internal/command/set/set_test.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "slices" 5 | "sort" 6 | "testing" 7 | 8 | "github.com/nalgeon/redka/internal/core" 9 | "github.com/nalgeon/redka/internal/testx" 10 | "github.com/nalgeon/redka/redsrv/internal/redis" 11 | ) 12 | 13 | func getRedka(tb testing.TB) redis.Redka { 14 | tb.Helper() 15 | db := testx.OpenDB(tb) 16 | return redis.RedkaDB(db) 17 | } 18 | 19 | func sortValues(vals []core.Value) { 20 | sort.Slice(vals, func(i, j int) bool { 21 | return slices.Compare(vals[i], vals[j]) < 0 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:edge AS build 2 | RUN apk add --no-cache --update gcc g++ go make 3 | WORKDIR /app 4 | COPY go.mod go.sum ./ 5 | RUN go mod download 6 | COPY . . 7 | RUN make build 8 | 9 | FROM alpine:latest 10 | RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main valkey-cli 11 | RUN ln -s /usr/bin/valkey-cli /usr/local/bin/redis-cli 12 | RUN mkdir /data 13 | VOLUME /data 14 | WORKDIR /data 15 | COPY --from=build /app/build/redka /usr/local/bin/redka 16 | HEALTHCHECK CMD valkey-cli PING || exit 1 17 | EXPOSE 6379 18 | ENTRYPOINT ["redka", "-h", "0.0.0.0", "-p", "6379"] 19 | -------------------------------------------------------------------------------- /docs/commands/server.md: -------------------------------------------------------------------------------- 1 | # Server/connection management 2 | 3 | Redka supports only a couple of server and connection management commands: 4 | 5 | ``` 6 | Command Go API Description 7 | ------- ------ ----------- 8 | ECHO - Returns the given string. 9 | LOLWUT - Provides an answer to a yes/no question. 10 | PING - Returns the server's liveliness response. 11 | SELECT - Changes the selected database (no-op). 12 | ``` 13 | 14 | The rest of the server and connection management commands are not planned for 1.0. 15 | -------------------------------------------------------------------------------- /docs/commands/transactions.md: -------------------------------------------------------------------------------- 1 | # Transactions 2 | 3 | Redka supports the following transaction commands: 4 | 5 | ``` 6 | Command Go API Description 7 | ------- ------ ----------- 8 | DISCARD DB.View / DB.Update Discards a transaction. 9 | EXEC DB.View / DB.Update Executes all commands in a transaction. 10 | MULTI DB.View / DB.Update Starts a transaction. 11 | ``` 12 | 13 | Unlike Redis, Redka's transactions are fully ACID, providing automatic rollback in case of failure. 14 | 15 | The following transaction commands are not planned for 1.0: 16 | 17 | ``` 18 | UNWATCH WATCH 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/install-module.md: -------------------------------------------------------------------------------- 1 | # Installing Redka as a Go module 2 | 3 | Install the module as follows: 4 | 5 | ```shell 6 | go get github.com/nalgeon/redka 7 | ``` 8 | 9 | You'll also need an SQLite or PostgreSQL driver. 10 | 11 | Use one of the following for SQLite: 12 | 13 | - `github.com/mattn/go-sqlite3` (CGO, fastest) 14 | - `github.com/ncruces/go-sqlite3` (pure Go, WASM) 15 | - `modernc.org/sqlite` (pure Go, libc port) 16 | 17 | Or one of the following for PostgreSQL: 18 | 19 | - `github.com/lib/pq` 20 | - `github.com/jackc/pgx/v5` 21 | 22 | Install a driver with `go get` like this: 23 | 24 | ```shell 25 | go get github.com/ncruces/go-sqlite3 26 | ``` 27 | -------------------------------------------------------------------------------- /redsrv/internal/command/server/dbsize.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Returns the number of keys in the database. 6 | // DBSIZE 7 | // https://redis.io/commands/dbsize 8 | type DBSize struct { 9 | redis.BaseCmd 10 | } 11 | 12 | func ParseDBSize(b redis.BaseCmd) (DBSize, error) { 13 | cmd := DBSize{BaseCmd: b} 14 | if len(cmd.Args()) != 0 { 15 | return DBSize{}, redis.ErrInvalidArgNum 16 | } 17 | return cmd, nil 18 | } 19 | 20 | func (cmd DBSize) Run(w redis.Writer, red redis.Redka) (any, error) { 21 | n, err := red.Key().Len() 22 | if err != nil { 23 | w.WriteError(cmd.Error(err)) 24 | return nil, err 25 | } 26 | w.WriteInt(n) 27 | return n, nil 28 | } 29 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/flushdb.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Remove all keys from the current database. 6 | // FLUSHDB 7 | // https://redis.io/commands/flushdb 8 | type FlushDB struct { 9 | redis.BaseCmd 10 | } 11 | 12 | func ParseFlushDB(b redis.BaseCmd) (FlushDB, error) { 13 | cmd := FlushDB{BaseCmd: b} 14 | if len(cmd.Args()) != 0 { 15 | return FlushDB{}, redis.ErrSyntaxError 16 | } 17 | return cmd, nil 18 | } 19 | 20 | func (cmd FlushDB) Run(w redis.Writer, red redis.Redka) (any, error) { 21 | err := red.Key().DeleteAll() 22 | if err != nil { 23 | w.WriteError(cmd.Error(err)) 24 | return nil, err 25 | } 26 | w.WriteString("OK") 27 | return true, nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/rlist/postgres.go: -------------------------------------------------------------------------------- 1 | package rlist 2 | 3 | // Postgres queries for the list repository. 4 | var postgres queries 5 | 6 | func init() { 7 | postgres.delete = sqlite.delete 8 | postgres.deleteBack = sqlite.deleteBack 9 | postgres.deleteFront = sqlite.deleteFront 10 | postgres.get = sqlite.get 11 | postgres.insert = sqlite.insert 12 | postgres.insertAfter = sqlite.insertAfter 13 | postgres.insertBefore = sqlite.insertBefore 14 | postgres.len = sqlite.len 15 | postgres.popBack = sqlite.popBack 16 | postgres.popFront = sqlite.popFront 17 | postgres.push = sqlite.push 18 | postgres.pushBack = sqlite.pushBack 19 | postgres.pushFront = sqlite.pushFront 20 | postgres.lrange = sqlite.lrange 21 | postgres.set = sqlite.set 22 | postgres.trim = sqlite.trim 23 | } 24 | -------------------------------------------------------------------------------- /redsrv/internal/command/set/scard.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Returns the number of members in a set. 6 | // SCARD key 7 | // https://redis.io/commands/scard 8 | type SCard struct { 9 | redis.BaseCmd 10 | key string 11 | } 12 | 13 | func ParseSCard(b redis.BaseCmd) (SCard, error) { 14 | cmd := SCard{BaseCmd: b} 15 | if len(cmd.Args()) != 1 { 16 | return SCard{}, redis.ErrInvalidArgNum 17 | } 18 | cmd.key = string(cmd.Args()[0]) 19 | return cmd, nil 20 | } 21 | 22 | func (cmd SCard) Run(w redis.Writer, red redis.Redka) (any, error) { 23 | n, err := red.Set().Len(cmd.key) 24 | if err != nil { 25 | w.WriteError(cmd.Error(err)) 26 | return nil, err 27 | } 28 | w.WriteInt(n) 29 | return n, nil 30 | } 31 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hlen.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Returns the number of fields in a hash. 6 | // HLEN key 7 | // https://redis.io/commands/hlen 8 | type HLen struct { 9 | redis.BaseCmd 10 | key string 11 | } 12 | 13 | func ParseHLen(b redis.BaseCmd) (HLen, error) { 14 | cmd := HLen{BaseCmd: b} 15 | if len(cmd.Args()) != 1 { 16 | return HLen{}, redis.ErrInvalidArgNum 17 | } 18 | cmd.key = string(cmd.Args()[0]) 19 | return cmd, nil 20 | } 21 | 22 | func (cmd HLen) Run(w redis.Writer, red redis.Redka) (any, error) { 23 | count, err := red.Hash().Len(cmd.key) 24 | if err != nil { 25 | w.WriteError(cmd.Error(err)) 26 | return nil, err 27 | } 28 | w.WriteInt(count) 29 | return count, nil 30 | } 31 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zcard.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Returns the number of members in a sorted set. 6 | // ZCARD key 7 | // https://redis.io/commands/zcard 8 | type ZCard struct { 9 | redis.BaseCmd 10 | key string 11 | } 12 | 13 | func ParseZCard(b redis.BaseCmd) (ZCard, error) { 14 | cmd := ZCard{BaseCmd: b} 15 | if len(cmd.Args()) != 1 { 16 | return ZCard{}, redis.ErrInvalidArgNum 17 | } 18 | cmd.key = string(cmd.Args()[0]) 19 | return cmd, nil 20 | } 21 | 22 | func (cmd ZCard) Run(w redis.Writer, red redis.Redka) (any, error) { 23 | n, err := red.ZSet().Len(cmd.key) 24 | if err != nil { 25 | w.WriteError(cmd.Error(err)) 26 | return nil, err 27 | } 28 | w.WriteInt(n) 29 | return n, nil 30 | } 31 | -------------------------------------------------------------------------------- /redsrv/internal/command/conn/echo.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/nalgeon/redka/redsrv/internal/parser" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | // Echo returns the given string. 11 | // ECHO message 12 | // https://redis.io/commands/echo 13 | type Echo struct { 14 | redis.BaseCmd 15 | parts []string 16 | } 17 | 18 | func ParseEcho(b redis.BaseCmd) (Echo, error) { 19 | cmd := Echo{BaseCmd: b} 20 | err := parser.New( 21 | parser.Strings(&cmd.parts), 22 | ).Required(1).Run(cmd.Args()) 23 | if err != nil { 24 | return Echo{}, err 25 | } 26 | return cmd, nil 27 | } 28 | 29 | func (c Echo) Run(w redis.Writer, _ redis.Redka) (any, error) { 30 | out := strings.Join(c.parts, " ") 31 | w.WriteAny(out) 32 | return out, nil 33 | } 34 | -------------------------------------------------------------------------------- /example/mattn/main.go: -------------------------------------------------------------------------------- 1 | // A basic example of using Redka 2 | // with github.com/mattn/go-sqlite3 driver. 3 | package main 4 | 5 | import ( 6 | "log" 7 | "log/slog" 8 | 9 | _ "github.com/mattn/go-sqlite3" 10 | "github.com/nalgeon/redka" 11 | ) 12 | 13 | func main() { 14 | // Open the database. 15 | db, err := redka.Open("redka.db", nil) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | defer func() { _ = db.Close() }() 20 | 21 | // Set some values. 22 | err = db.Str().Set("name", "alice") 23 | slog.Info("set", "err", err) 24 | err = db.Str().Set("age", 25) 25 | slog.Info("set", "err", err) 26 | 27 | // Read them back. 28 | name, err := db.Str().Get("name") 29 | slog.Info("get", "name", name, "err", err) 30 | age, err := db.Str().Get("age") 31 | slog.Info("get", "age", age, "err", err) 32 | } 33 | -------------------------------------------------------------------------------- /redsrv/internal/command/conn/select.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Changes the selected database. 9 | // Since Redka doesn't support multiple databases, this command is a no-op. 10 | // SELECT index 11 | // https://redis.io/commands/select 12 | type Select struct { 13 | redis.BaseCmd 14 | index int 15 | } 16 | 17 | func ParseSelect(b redis.BaseCmd) (Select, error) { 18 | cmd := Select{BaseCmd: b} 19 | err := parser.New( 20 | parser.Int(&cmd.index), 21 | ).Required(1).Run(cmd.Args()) 22 | if err != nil { 23 | return Select{}, err 24 | } 25 | return cmd, nil 26 | } 27 | 28 | func (c Select) Run(w redis.Writer, _ redis.Redka) (any, error) { 29 | w.WriteString("OK") 30 | return true, nil 31 | } 32 | -------------------------------------------------------------------------------- /redsrv/internal/command/server/configget.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/redis" 5 | ) 6 | 7 | // Returns the effective values of configuration parameters. 8 | // CONFIG GET parameter [parameter ...] 9 | // https://redis.io/commands/config-get 10 | type ConfigGet struct { 11 | params []string 12 | } 13 | 14 | func ParseConfigGet(args [][]byte) (ConfigGet, error) { 15 | if len(args) < 1 { 16 | return ConfigGet{}, redis.ErrInvalidArgNum 17 | } 18 | cmd := ConfigGet{params: make([]string, len(args))} 19 | for i, arg := range args { 20 | cmd.params[i] = string(arg) 21 | } 22 | return cmd, nil 23 | } 24 | 25 | func (c ConfigGet) Run(w redis.Writer, _ redis.Redka) (any, error) { 26 | w.WriteArray(2) 27 | w.WriteString("databases") 28 | w.WriteInt(1) 29 | return true, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/rhash/postgres.go: -------------------------------------------------------------------------------- 1 | package rhash 2 | 3 | // Postgres queries for the hash repository. 4 | var postgres = queries{ 5 | scan: ` 6 | select rhash.rowid, field, value 7 | from rhash join rkey on kid = rkey.id and type = 4 8 | where 9 | key = $1 and (etime is null or etime > $2) 10 | and rhash.rowid > $3 and field like $4 11 | order by rhash.rowid asc 12 | limit $5`, 13 | } 14 | 15 | func init() { 16 | postgres.count = sqlite.count 17 | postgres.delete1 = sqlite.delete1 18 | postgres.delete2 = sqlite.delete2 19 | postgres.fields = sqlite.fields 20 | postgres.get = sqlite.get 21 | postgres.getMany = sqlite.getMany 22 | postgres.items = sqlite.items 23 | postgres.len = sqlite.len 24 | // postgres.scan = sqlite.scan 25 | postgres.set1 = sqlite.set1 26 | postgres.set2 = sqlite.set2 27 | postgres.values = sqlite.values 28 | } 29 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hvals.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Returns all values in a hash. 6 | // HVALS key 7 | // https://redis.io/commands/hvals 8 | type HVals struct { 9 | redis.BaseCmd 10 | key string 11 | } 12 | 13 | func ParseHVals(b redis.BaseCmd) (HVals, error) { 14 | cmd := HVals{BaseCmd: b} 15 | if len(cmd.Args()) != 1 { 16 | return HVals{}, redis.ErrInvalidArgNum 17 | } 18 | cmd.key = string(cmd.Args()[0]) 19 | return cmd, nil 20 | } 21 | 22 | func (cmd HVals) Run(w redis.Writer, red redis.Redka) (any, error) { 23 | vals, err := red.Hash().Values(cmd.key) 24 | if err != nil { 25 | w.WriteError(cmd.Error(err)) 26 | return nil, err 27 | } 28 | w.WriteArray(len(vals)) 29 | for _, val := range vals { 30 | w.WriteBulk(val) 31 | } 32 | return vals, nil 33 | } 34 | -------------------------------------------------------------------------------- /redsrv/internal/command/list/llen.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Returns the length of a list. 9 | // LLEN key 10 | // https://redis.io/commands/llen 11 | type LLen struct { 12 | redis.BaseCmd 13 | key string 14 | } 15 | 16 | func ParseLLen(b redis.BaseCmd) (LLen, error) { 17 | cmd := LLen{BaseCmd: b} 18 | err := parser.New( 19 | parser.String(&cmd.key), 20 | ).Required(1).Run(cmd.Args()) 21 | if err != nil { 22 | return LLen{}, err 23 | } 24 | return cmd, nil 25 | } 26 | 27 | func (cmd LLen) Run(w redis.Writer, red redis.Redka) (any, error) { 28 | n, err := red.List().Len(cmd.key) 29 | if err != nil { 30 | w.WriteError(cmd.Error(err)) 31 | return nil, err 32 | } 33 | w.WriteInt(n) 34 | return n, nil 35 | } 36 | -------------------------------------------------------------------------------- /example/ncruces/main.go: -------------------------------------------------------------------------------- 1 | // An example of using Redka 2 | // with github.com/ncruces/go-sqlite3 driver. 3 | package main 4 | 5 | import ( 6 | "log" 7 | "log/slog" 8 | 9 | "github.com/nalgeon/redka" 10 | _ "github.com/ncruces/go-sqlite3/driver" 11 | _ "github.com/ncruces/go-sqlite3/embed" 12 | ) 13 | 14 | func main() { 15 | // Open the database. 16 | db, err := redka.Open("redka.db", nil) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | defer func() { _ = db.Close() }() 21 | 22 | // Set some values. 23 | err = db.Str().Set("name", "alice") 24 | slog.Info("set", "err", err) 25 | err = db.Str().Set("age", 25) 26 | slog.Info("set", "err", err) 27 | 28 | // Read them back. 29 | name, err := db.Str().Get("name") 30 | slog.Info("get", "name", name, "err", err) 31 | age, err := db.Str().Get("age") 32 | slog.Info("get", "age", age, "err", err) 33 | } 34 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hkeys.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Returns all fields in a hash. 6 | // HKEYS key 7 | // https://redis.io/commands/hkeys 8 | type HKeys struct { 9 | redis.BaseCmd 10 | key string 11 | } 12 | 13 | func ParseHKeys(b redis.BaseCmd) (HKeys, error) { 14 | cmd := HKeys{BaseCmd: b} 15 | if len(cmd.Args()) != 1 { 16 | return HKeys{}, redis.ErrInvalidArgNum 17 | } 18 | cmd.key = string(cmd.Args()[0]) 19 | return cmd, nil 20 | } 21 | 22 | func (cmd HKeys) Run(w redis.Writer, red redis.Redka) (any, error) { 23 | fields, err := red.Hash().Fields(cmd.key) 24 | if err != nil { 25 | w.WriteError(cmd.Error(err)) 26 | return nil, err 27 | } 28 | w.WriteArray(len(fields)) 29 | for _, field := range fields { 30 | w.WriteBulkString(field) 31 | } 32 | return fields, nil 33 | } 34 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/del.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Deletes one or more Keys. 9 | // DEL key [key ...] 10 | // https://redis.io/commands/del 11 | type Del struct { 12 | redis.BaseCmd 13 | keys []string 14 | } 15 | 16 | func ParseDel(b redis.BaseCmd) (Del, error) { 17 | cmd := Del{BaseCmd: b} 18 | err := parser.New( 19 | parser.Strings(&cmd.keys), 20 | ).Required(1).Run(cmd.Args()) 21 | if err != nil { 22 | return Del{}, err 23 | } 24 | return cmd, nil 25 | } 26 | 27 | func (cmd Del) Run(w redis.Writer, red redis.Redka) (any, error) { 28 | count, err := red.Key().Delete(cmd.keys...) 29 | if err != nil { 30 | w.WriteError(cmd.Error(err)) 31 | return nil, err 32 | } 33 | w.WriteInt(count) 34 | return count, nil 35 | } 36 | -------------------------------------------------------------------------------- /redsrv/internal/command/conn/ping.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | const ( 9 | PONG = "PONG" 10 | ) 11 | 12 | // Returns the server's liveliness response. 13 | // https://redis.io/commands/ping 14 | type Ping struct { 15 | redis.BaseCmd 16 | message string 17 | } 18 | 19 | func ParsePing(b redis.BaseCmd) (Ping, error) { 20 | cmd := Ping{BaseCmd: b} 21 | err := parser.New( 22 | parser.String(&cmd.message), 23 | ).Required(0).Run(cmd.Args()) 24 | if err != nil { 25 | return Ping{}, err 26 | } 27 | return cmd, nil 28 | } 29 | 30 | func (c Ping) Run(w redis.Writer, _ redis.Redka) (any, error) { 31 | if c.message == "" { 32 | w.WriteAny(PONG) 33 | return PONG, nil 34 | } 35 | w.WriteBulkString(c.message) 36 | return c.message, nil 37 | } 38 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/keys.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Returns all key names that match a pattern. 6 | // KEYS pattern 7 | // https://redis.io/commands/keys 8 | type Keys struct { 9 | redis.BaseCmd 10 | pattern string 11 | } 12 | 13 | func ParseKeys(b redis.BaseCmd) (Keys, error) { 14 | cmd := Keys{BaseCmd: b} 15 | if len(cmd.Args()) != 1 { 16 | return Keys{}, redis.ErrInvalidArgNum 17 | } 18 | cmd.pattern = string(cmd.Args()[0]) 19 | return cmd, nil 20 | } 21 | 22 | func (cmd Keys) Run(w redis.Writer, red redis.Redka) (any, error) { 23 | keys, err := red.Key().Keys(cmd.pattern) 24 | if err != nil { 25 | w.WriteError(cmd.Error(err)) 26 | return nil, err 27 | } 28 | w.WriteArray(len(keys)) 29 | for _, key := range keys { 30 | w.WriteBulkString(key.Key) 31 | } 32 | return keys, nil 33 | } 34 | -------------------------------------------------------------------------------- /redsrv/internal/command/set/smembers.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Returns all members of a set. 6 | // SMEMBERS key 7 | // https://redis.io/commands/smembers 8 | type SMembers struct { 9 | redis.BaseCmd 10 | key string 11 | } 12 | 13 | func ParseSMembers(b redis.BaseCmd) (SMembers, error) { 14 | cmd := SMembers{BaseCmd: b} 15 | if len(cmd.Args()) != 1 { 16 | return SMembers{}, redis.ErrInvalidArgNum 17 | } 18 | cmd.key = string(cmd.Args()[0]) 19 | return cmd, nil 20 | } 21 | 22 | func (cmd SMembers) Run(w redis.Writer, red redis.Redka) (any, error) { 23 | items, err := red.Set().Items(cmd.key) 24 | if err != nil { 25 | w.WriteError(cmd.Error(err)) 26 | return nil, err 27 | } 28 | w.WriteArray(len(items)) 29 | for _, val := range items { 30 | w.WriteBulk(val) 31 | } 32 | return items, nil 33 | } 34 | -------------------------------------------------------------------------------- /docs/roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | The project is functionally ready for 1.0. Feel free to try it in non-critical production scenarios and provide feedback in the issues. 4 | 5 | The 1.0 release will include the following features: 6 | 7 | - ✅ Strings. 8 | - ✅ Lists. 9 | - ✅ Sets. 10 | - ✅ Hashes. 11 | - ✅ Sorted sets. 12 | - ✅ Key management. 13 | - ✅ Transactions. 14 | 15 | ✅ = done, ⏳ = in progress, ⬜ = next in line 16 | 17 | Future versions may include additional data types (such as streams, HyperLogLog or geo), features like publish/subscribe, and more commands for existing types. 18 | 19 | Features I'd rather not implement even in future versions: 20 | 21 | - Lua scripting. 22 | - Authentication and ACLs. 23 | - Multiple databases. 24 | - Watch/unwatch. 25 | 26 | Features I definitely don't want to implement: 27 | 28 | - Cluster. 29 | - Sentinel. 30 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/rename.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Renames a key and overwrites the destination. 6 | // RENAME key newkey 7 | // https://redis.io/commands/rename 8 | type Rename struct { 9 | redis.BaseCmd 10 | key string 11 | newKey string 12 | } 13 | 14 | func ParseRename(b redis.BaseCmd) (Rename, error) { 15 | cmd := Rename{BaseCmd: b} 16 | if len(cmd.Args()) != 2 { 17 | return Rename{}, redis.ErrInvalidArgNum 18 | } 19 | cmd.key = string(cmd.Args()[0]) 20 | cmd.newKey = string(cmd.Args()[1]) 21 | return cmd, nil 22 | } 23 | 24 | func (cmd Rename) Run(w redis.Writer, red redis.Redka) (any, error) { 25 | err := red.Key().Rename(cmd.key, cmd.newKey) 26 | if err != nil { 27 | w.WriteError(cmd.Error(err)) 28 | return nil, err 29 | } 30 | w.WriteString("OK") 31 | return true, nil 32 | } 33 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/exists.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Determines whether one or more keys exist. 9 | // EXISTS key [key ...] 10 | // https://redis.io/commands/exists 11 | type Exists struct { 12 | redis.BaseCmd 13 | keys []string 14 | } 15 | 16 | func ParseExists(b redis.BaseCmd) (Exists, error) { 17 | cmd := Exists{BaseCmd: b} 18 | err := parser.New( 19 | parser.Strings(&cmd.keys), 20 | ).Required(1).Run(cmd.Args()) 21 | if err != nil { 22 | return Exists{}, err 23 | } 24 | return cmd, nil 25 | } 26 | 27 | func (cmd Exists) Run(w redis.Writer, red redis.Redka) (any, error) { 28 | count, err := red.Key().Count(cmd.keys...) 29 | if err != nil { 30 | w.WriteError(cmd.Error(err)) 31 | return nil, err 32 | } 33 | w.WriteInt(count) 34 | return count, nil 35 | } 36 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/randomkey.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Returns a random key name from the database. 9 | // RANDOMKEY 10 | // https://redis.io/commands/randomkey 11 | type RandomKey struct { 12 | redis.BaseCmd 13 | } 14 | 15 | func ParseRandomKey(b redis.BaseCmd) (RandomKey, error) { 16 | cmd := RandomKey{BaseCmd: b} 17 | if len(cmd.Args()) != 0 { 18 | return RandomKey{}, redis.ErrInvalidArgNum 19 | } 20 | return cmd, nil 21 | } 22 | 23 | func (cmd RandomKey) Run(w redis.Writer, red redis.Redka) (any, error) { 24 | key, err := red.Key().Random() 25 | if err == core.ErrNotFound { 26 | w.WriteNull() 27 | return nil, nil 28 | } 29 | if err != nil { 30 | w.WriteError(cmd.Error(err)) 31 | return nil, err 32 | } 33 | w.WriteBulkString(key.Key) 34 | return key, nil 35 | } 36 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/get.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Get returns the string value of a key. 9 | // GET key 10 | // https://redis.io/commands/get 11 | type Get struct { 12 | redis.BaseCmd 13 | key string 14 | } 15 | 16 | func ParseGet(b redis.BaseCmd) (Get, error) { 17 | cmd := Get{BaseCmd: b} 18 | if len(cmd.Args()) != 1 { 19 | return Get{}, redis.ErrInvalidArgNum 20 | } 21 | cmd.key = string(cmd.Args()[0]) 22 | return cmd, nil 23 | } 24 | 25 | func (cmd Get) Run(w redis.Writer, red redis.Redka) (any, error) { 26 | val, err := red.Str().Get(cmd.key) 27 | if err == core.ErrNotFound { 28 | w.WriteNull() 29 | return val, nil 30 | } 31 | if err != nil { 32 | w.WriteError(cmd.Error(err)) 33 | return nil, err 34 | } 35 | w.WriteBulk(val) 36 | return val, nil 37 | } 38 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hgetall.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Returns all fields and values in a hash. 6 | // HGETALL key 7 | // https://redis.io/commands/hgetall 8 | type HGetAll struct { 9 | redis.BaseCmd 10 | key string 11 | } 12 | 13 | func ParseHGetAll(b redis.BaseCmd) (HGetAll, error) { 14 | cmd := HGetAll{BaseCmd: b} 15 | if len(cmd.Args()) != 1 { 16 | return HGetAll{}, redis.ErrInvalidArgNum 17 | } 18 | cmd.key = string(cmd.Args()[0]) 19 | return cmd, nil 20 | } 21 | 22 | func (cmd HGetAll) Run(w redis.Writer, red redis.Redka) (any, error) { 23 | items, err := red.Hash().Items(cmd.key) 24 | if err != nil { 25 | w.WriteError(cmd.Error(err)) 26 | return nil, err 27 | } 28 | w.WriteArray(len(items) * 2) 29 | for field, val := range items { 30 | w.WriteBulkString(field) 31 | w.WriteBulk(val) 32 | } 33 | return items, nil 34 | } 35 | -------------------------------------------------------------------------------- /redsrv/internal/command/set/spop.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Returns a random member from a set after removing it. 9 | // SPOP key 10 | // https://redis.io/commands/spop 11 | type SPop struct { 12 | redis.BaseCmd 13 | key string 14 | } 15 | 16 | func ParseSPop(b redis.BaseCmd) (SPop, error) { 17 | cmd := SPop{BaseCmd: b} 18 | if len(cmd.Args()) != 1 { 19 | return SPop{}, redis.ErrInvalidArgNum 20 | } 21 | cmd.key = string(cmd.Args()[0]) 22 | return cmd, nil 23 | } 24 | 25 | func (cmd SPop) Run(w redis.Writer, red redis.Redka) (any, error) { 26 | elem, err := red.Set().Pop(cmd.key) 27 | if err == core.ErrNotFound { 28 | w.WriteNull() 29 | return elem, nil 30 | } 31 | if err != nil { 32 | w.WriteError(cmd.Error(err)) 33 | return nil, err 34 | } 35 | w.WriteBulk(elem) 36 | return elem, nil 37 | } 38 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hexists.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Determines whether a field exists in a hash. 6 | // HEXISTS key field 7 | // https://redis.io/commands/hexists 8 | type HExists struct { 9 | redis.BaseCmd 10 | key string 11 | field string 12 | } 13 | 14 | func ParseHExists(b redis.BaseCmd) (HExists, error) { 15 | cmd := HExists{BaseCmd: b} 16 | if len(cmd.Args()) != 2 { 17 | return HExists{}, redis.ErrInvalidArgNum 18 | } 19 | cmd.key = string(cmd.Args()[0]) 20 | cmd.field = string(cmd.Args()[1]) 21 | return cmd, nil 22 | } 23 | 24 | func (cmd HExists) Run(w redis.Writer, red redis.Redka) (any, error) { 25 | ok, err := red.Hash().Exists(cmd.key, cmd.field) 26 | if err != nil { 27 | w.WriteError(cmd.Error(err)) 28 | return nil, err 29 | } 30 | if ok { 31 | w.WriteInt(1) 32 | } else { 33 | w.WriteInt(0) 34 | } 35 | return ok, nil 36 | } 37 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/mset.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Atomically creates or modifies the string values of one or more keys. 9 | // MSET key value [key value ...] 10 | // https://redis.io/commands/mset 11 | type MSet struct { 12 | redis.BaseCmd 13 | items map[string]any 14 | } 15 | 16 | func ParseMSet(b redis.BaseCmd) (MSet, error) { 17 | cmd := MSet{BaseCmd: b} 18 | err := parser.New( 19 | parser.AnyMap(&cmd.items), 20 | ).Required(2).Run(cmd.Args()) 21 | if err != nil { 22 | return MSet{}, err 23 | } 24 | return cmd, nil 25 | } 26 | 27 | func (cmd MSet) Run(w redis.Writer, red redis.Redka) (any, error) { 28 | err := red.Str().SetMany(cmd.items) 29 | if err != nil { 30 | w.WriteError(cmd.Error(err)) 31 | return nil, err 32 | } 33 | w.WriteString("OK") 34 | return true, nil 35 | } 36 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/type.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Determines the type of value stored at a key. 9 | // TYPE key 10 | // https://redis.io/commands/type 11 | type Type struct { 12 | redis.BaseCmd 13 | key string 14 | } 15 | 16 | func ParseType(b redis.BaseCmd) (Type, error) { 17 | cmd := Type{BaseCmd: b} 18 | if len(cmd.Args()) != 1 { 19 | return Type{}, redis.ErrInvalidArgNum 20 | } 21 | cmd.key = string(cmd.Args()[0]) 22 | return cmd, nil 23 | } 24 | 25 | func (cmd Type) Run(w redis.Writer, red redis.Redka) (any, error) { 26 | k, err := red.Key().Get(cmd.key) 27 | if err == core.ErrNotFound { 28 | w.WriteString("none") 29 | return "none", nil 30 | } 31 | if err != nil { 32 | w.WriteError(cmd.Error(err)) 33 | return nil, err 34 | } 35 | w.WriteString(k.TypeName()) 36 | return k.TypeName(), nil 37 | } 38 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/strlen.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Strlen returns the length of a string value. 9 | // STRLEN key 10 | // https://redis.io/commands/strlen 11 | type Strlen struct { 12 | redis.BaseCmd 13 | key string 14 | } 15 | 16 | func ParseStrlen(b redis.BaseCmd) (Strlen, error) { 17 | cmd := Strlen{BaseCmd: b} 18 | if len(cmd.Args()) != 1 { 19 | return Strlen{}, redis.ErrInvalidArgNum 20 | } 21 | cmd.key = string(cmd.Args()[0]) 22 | return cmd, nil 23 | } 24 | 25 | func (cmd Strlen) Run(w redis.Writer, red redis.Redka) (any, error) { 26 | val, err := red.Str().Get(cmd.key) 27 | if err == core.ErrNotFound { 28 | w.WriteInt(0) 29 | return 0, nil 30 | } 31 | if err != nil { 32 | w.WriteError(cmd.Error(err)) 33 | return nil, err 34 | } 35 | w.WriteInt(len(val)) 36 | return len(val), nil 37 | } 38 | -------------------------------------------------------------------------------- /example/modernc/main.go: -------------------------------------------------------------------------------- 1 | // An example of using Redka 2 | // with modernc.org/sqlite driver. 3 | package main 4 | 5 | import ( 6 | "log" 7 | "log/slog" 8 | 9 | "github.com/nalgeon/redka" 10 | _ "modernc.org/sqlite" 11 | ) 12 | 13 | func main() { 14 | // modernc.org/sqlite uses a different driver name 15 | // ("sqlite" instead of "sqlite3"). 16 | opts := redka.Options{ 17 | DriverName: "sqlite", 18 | } 19 | 20 | // Open the database. 21 | db, err := redka.Open("redka.db", &opts) 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | defer func() { _ = db.Close() }() 26 | 27 | // Set some values. 28 | err = db.Str().Set("name", "alice") 29 | slog.Info("set", "err", err) 30 | err = db.Str().Set("age", 25) 31 | slog.Info("set", "err", err) 32 | 33 | // Read them back. 34 | name, err := db.Str().Get("name") 35 | slog.Info("get", "name", name, "err", err) 36 | age, err := db.Str().Get("age") 37 | slog.Info("get", "age", age, "err", err) 38 | } 39 | -------------------------------------------------------------------------------- /example/postgres/main.go: -------------------------------------------------------------------------------- 1 | // An example of using Redka 2 | // with github.com/lib/pq driver. 3 | package main 4 | 5 | import ( 6 | "log" 7 | "log/slog" 8 | 9 | _ "github.com/lib/pq" 10 | "github.com/nalgeon/redka" 11 | ) 12 | 13 | func main() { 14 | // Connections settings. 15 | connString := "postgres://redka:redka@localhost:5432/redka?sslmode=disable" 16 | opts := &redka.Options{DriverName: "postgres"} 17 | 18 | // Open the database. 19 | db, err := redka.Open(connString, opts) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | defer func() { _ = db.Close() }() 24 | 25 | // Set some values. 26 | err = db.Str().Set("name", "alice") 27 | slog.Info("set", "err", err) 28 | err = db.Str().Set("age", 25) 29 | slog.Info("set", "err", err) 30 | 31 | // Read them back. 32 | name, err := db.Str().Get("name") 33 | slog.Info("get", "name", name, "err", err) 34 | age, err := db.Str().Get("age") 35 | slog.Info("get", "age", age, "err", err) 36 | } 37 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zrem.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Removes one or more members from a sorted set. 9 | // ZREM key member [member ...] 10 | // https://redis.io/commands/zrem 11 | type ZRem struct { 12 | redis.BaseCmd 13 | key string 14 | members []any 15 | } 16 | 17 | func ParseZRem(b redis.BaseCmd) (ZRem, error) { 18 | cmd := ZRem{BaseCmd: b} 19 | err := parser.New( 20 | parser.String(&cmd.key), 21 | parser.Anys(&cmd.members), 22 | ).Required(2).Run(cmd.Args()) 23 | if err != nil { 24 | return ZRem{}, err 25 | } 26 | return cmd, nil 27 | } 28 | 29 | func (cmd ZRem) Run(w redis.Writer, red redis.Redka) (any, error) { 30 | n, err := red.ZSet().Delete(cmd.key, cmd.members...) 31 | if err != nil { 32 | w.WriteError(cmd.Error(err)) 33 | return nil, err 34 | } 35 | w.WriteInt(n) 36 | return n, nil 37 | } 38 | -------------------------------------------------------------------------------- /redsrv/internal/command/set/srem.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Removes one or more members from a set. 9 | // SREM key member [member ...] 10 | // https://redis.io/commands/srem 11 | type SRem struct { 12 | redis.BaseCmd 13 | key string 14 | members []any 15 | } 16 | 17 | func ParseSRem(b redis.BaseCmd) (SRem, error) { 18 | cmd := SRem{BaseCmd: b} 19 | err := parser.New( 20 | parser.String(&cmd.key), 21 | parser.Anys(&cmd.members), 22 | ).Required(2).Run(cmd.Args()) 23 | if err != nil { 24 | return SRem{}, err 25 | } 26 | return cmd, nil 27 | } 28 | 29 | func (cmd SRem) Run(w redis.Writer, red redis.Redka) (any, error) { 30 | count, err := red.Set().Delete(cmd.key, cmd.members...) 31 | if err != nil { 32 | w.WriteError(cmd.Error(err)) 33 | return nil, err 34 | } 35 | w.WriteInt(count) 36 | return count, nil 37 | } 38 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/persist.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Removes the expiration time of a key. 9 | // PERSIST key 10 | // https://redis.io/commands/persist 11 | type Persist struct { 12 | redis.BaseCmd 13 | key string 14 | } 15 | 16 | func ParsePersist(b redis.BaseCmd) (Persist, error) { 17 | cmd := Persist{BaseCmd: b} 18 | if len(cmd.Args()) != 1 { 19 | return Persist{}, redis.ErrInvalidArgNum 20 | } 21 | cmd.key = string(cmd.Args()[0]) 22 | return cmd, nil 23 | } 24 | 25 | func (cmd Persist) Run(w redis.Writer, red redis.Redka) (any, error) { 26 | err := red.Key().Persist(cmd.key) 27 | if err != nil && err != core.ErrNotFound { 28 | w.WriteError(cmd.Error(err)) 29 | return nil, err 30 | } 31 | if err == core.ErrNotFound { 32 | w.WriteInt(0) 33 | return false, nil 34 | 35 | } 36 | w.WriteInt(1) 37 | return true, nil 38 | } 39 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/renamenx.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Renames a key only when the target key name doesn't exist. 6 | // RENAMENX key newkey 7 | // https://redis.io/commands/renamenx 8 | type RenameNX struct { 9 | redis.BaseCmd 10 | key string 11 | newKey string 12 | } 13 | 14 | func ParseRenameNX(b redis.BaseCmd) (RenameNX, error) { 15 | cmd := RenameNX{BaseCmd: b} 16 | if len(cmd.Args()) != 2 { 17 | return RenameNX{}, redis.ErrInvalidArgNum 18 | } 19 | cmd.key = string(cmd.Args()[0]) 20 | cmd.newKey = string(cmd.Args()[1]) 21 | return cmd, nil 22 | } 23 | 24 | func (cmd RenameNX) Run(w redis.Writer, red redis.Redka) (any, error) { 25 | ok, err := red.Key().RenameNotExists(cmd.key, cmd.newKey) 26 | if err != nil { 27 | w.WriteError(cmd.Error(err)) 28 | return nil, err 29 | } 30 | if ok { 31 | w.WriteInt(1) 32 | } else { 33 | w.WriteInt(0) 34 | } 35 | return ok, nil 36 | } 37 | -------------------------------------------------------------------------------- /redsrv/internal/command/set/sdiff.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Returns the difference of multiple sets. 9 | // SDIFF key [key ...] 10 | // https://redis.io/commands/sdiff 11 | type SDiff struct { 12 | redis.BaseCmd 13 | keys []string 14 | } 15 | 16 | func ParseSDiff(b redis.BaseCmd) (SDiff, error) { 17 | cmd := SDiff{BaseCmd: b} 18 | err := parser.New( 19 | parser.Strings(&cmd.keys), 20 | ).Required(1).Run(cmd.Args()) 21 | if err != nil { 22 | return SDiff{}, err 23 | } 24 | return cmd, nil 25 | } 26 | 27 | func (cmd SDiff) Run(w redis.Writer, red redis.Redka) (any, error) { 28 | elems, err := red.Set().Diff(cmd.keys...) 29 | if err != nil { 30 | w.WriteError(cmd.Error(err)) 31 | return nil, err 32 | } 33 | w.WriteArray(len(elems)) 34 | for _, elem := range elems { 35 | w.WriteBulk(elem) 36 | } 37 | return elems, nil 38 | } 39 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/setnx.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Set the string value of a key only when the key doesn't exist. 6 | // SETNX key value 7 | // https://redis.io/commands/setnx 8 | type SetNX struct { 9 | redis.BaseCmd 10 | key string 11 | value []byte 12 | } 13 | 14 | func ParseSetNX(b redis.BaseCmd) (SetNX, error) { 15 | cmd := SetNX{BaseCmd: b} 16 | if len(cmd.Args()) != 2 { 17 | return SetNX{}, redis.ErrInvalidArgNum 18 | } 19 | cmd.key = string(cmd.Args()[0]) 20 | cmd.value = cmd.Args()[1] 21 | return cmd, nil 22 | } 23 | 24 | func (cmd SetNX) Run(w redis.Writer, red redis.Redka) (any, error) { 25 | out, err := red.Str().SetWith(cmd.key, cmd.value).IfNotExists().Run() 26 | if err != nil { 27 | w.WriteError(cmd.Error(err)) 28 | return nil, err 29 | } 30 | if out.Created { 31 | w.WriteInt(1) 32 | } else { 33 | w.WriteInt(0) 34 | } 35 | return out.Created, nil 36 | } 37 | -------------------------------------------------------------------------------- /redsrv/internal/command/list/lpush.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Prepends an element to a list. 9 | // Creates the key if it doesn't exist. 10 | // LPUSH key element 11 | // https://redis.io/commands/lpush 12 | type LPush struct { 13 | redis.BaseCmd 14 | key string 15 | elem []byte 16 | } 17 | 18 | func ParseLPush(b redis.BaseCmd) (LPush, error) { 19 | cmd := LPush{BaseCmd: b} 20 | err := parser.New( 21 | parser.String(&cmd.key), 22 | parser.Bytes(&cmd.elem), 23 | ).Required(2).Run(cmd.Args()) 24 | if err != nil { 25 | return LPush{}, err 26 | } 27 | return cmd, nil 28 | } 29 | 30 | func (cmd LPush) Run(w redis.Writer, red redis.Redka) (any, error) { 31 | n, err := red.List().PushFront(cmd.key, cmd.elem) 32 | if err != nil { 33 | w.WriteError(cmd.Error(err)) 34 | return nil, err 35 | } 36 | w.WriteInt(n) 37 | return n, nil 38 | } 39 | -------------------------------------------------------------------------------- /redsrv/internal/command/list/rpush.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Appends an element to a list. 9 | // Creates the key if it doesn't exist. 10 | // RPUSH key element 11 | // https://redis.io/commands/rpush 12 | type RPush struct { 13 | redis.BaseCmd 14 | key string 15 | elem []byte 16 | } 17 | 18 | func ParseRPush(b redis.BaseCmd) (RPush, error) { 19 | cmd := RPush{BaseCmd: b} 20 | err := parser.New( 21 | parser.String(&cmd.key), 22 | parser.Bytes(&cmd.elem), 23 | ).Required(2).Run(cmd.Args()) 24 | if err != nil { 25 | return RPush{}, err 26 | } 27 | return cmd, nil 28 | } 29 | 30 | func (cmd RPush) Run(w redis.Writer, red redis.Redka) (any, error) { 31 | n, err := red.List().PushBack(cmd.key, cmd.elem) 32 | if err != nil { 33 | w.WriteError(cmd.Error(err)) 34 | return nil, err 35 | } 36 | w.WriteInt(n) 37 | return n, nil 38 | } 39 | -------------------------------------------------------------------------------- /redsrv/internal/command/set/srandmember.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Get a random member from a set. 9 | // SRANDMEMBER key 10 | // https://redis.io/commands/srandmember 11 | type SRandMember struct { 12 | redis.BaseCmd 13 | key string 14 | } 15 | 16 | func ParseSRandMember(b redis.BaseCmd) (SRandMember, error) { 17 | cmd := SRandMember{BaseCmd: b} 18 | if len(cmd.Args()) != 1 { 19 | return SRandMember{}, redis.ErrInvalidArgNum 20 | } 21 | cmd.key = string(cmd.Args()[0]) 22 | return cmd, nil 23 | } 24 | 25 | func (cmd SRandMember) Run(w redis.Writer, red redis.Redka) (any, error) { 26 | elem, err := red.Set().Random(cmd.key) 27 | if err == core.ErrNotFound { 28 | w.WriteNull() 29 | return elem, nil 30 | } 31 | if err != nil { 32 | w.WriteError(cmd.Error(err)) 33 | return nil, err 34 | } 35 | w.WriteBulk(elem) 36 | return elem, nil 37 | } 38 | -------------------------------------------------------------------------------- /redsrv/internal/command/set/sunion.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Returns the union of multiple sets. 9 | // SUNION key [key ...] 10 | // https://redis.io/commands/sunion 11 | type SUnion struct { 12 | redis.BaseCmd 13 | keys []string 14 | } 15 | 16 | func ParseSUnion(b redis.BaseCmd) (SUnion, error) { 17 | cmd := SUnion{BaseCmd: b} 18 | err := parser.New( 19 | parser.Strings(&cmd.keys), 20 | ).Required(1).Run(cmd.Args()) 21 | if err != nil { 22 | return SUnion{}, err 23 | } 24 | return cmd, nil 25 | } 26 | 27 | func (cmd SUnion) Run(w redis.Writer, red redis.Redka) (any, error) { 28 | elems, err := red.Set().Union(cmd.keys...) 29 | if err != nil { 30 | w.WriteError(cmd.Error(err)) 31 | return nil, err 32 | } 33 | w.WriteArray(len(elems)) 34 | for _, elem := range elems { 35 | w.WriteBulk(elem) 36 | } 37 | return elems, nil 38 | } 39 | -------------------------------------------------------------------------------- /redsrv/internal/command/set/sinter.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Returns the intersect of multiple sets. 9 | // SINTER key [key ...] 10 | // https://redis.io/commands/sinter 11 | type SInter struct { 12 | redis.BaseCmd 13 | keys []string 14 | } 15 | 16 | func ParseSInter(b redis.BaseCmd) (SInter, error) { 17 | cmd := SInter{BaseCmd: b} 18 | err := parser.New( 19 | parser.Strings(&cmd.keys), 20 | ).Required(1).Run(cmd.Args()) 21 | if err != nil { 22 | return SInter{}, err 23 | } 24 | return cmd, nil 25 | } 26 | 27 | func (cmd SInter) Run(w redis.Writer, red redis.Redka) (any, error) { 28 | elems, err := red.Set().Inter(cmd.keys...) 29 | if err != nil { 30 | w.WriteError(cmd.Error(err)) 31 | return nil, err 32 | } 33 | w.WriteArray(len(elems)) 34 | for _, elem := range elems { 35 | w.WriteBulk(elem) 36 | } 37 | return elems, nil 38 | } 39 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hset.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Sets the values of one ore more fields in a hash. 9 | // HSET key field value [field value ...] 10 | // https://redis.io/commands/hset 11 | type HSet struct { 12 | redis.BaseCmd 13 | key string 14 | items map[string]any 15 | } 16 | 17 | func ParseHSet(b redis.BaseCmd) (HSet, error) { 18 | cmd := HSet{BaseCmd: b} 19 | err := parser.New( 20 | parser.String(&cmd.key), 21 | parser.AnyMap(&cmd.items), 22 | ).Required(3).Run(cmd.Args()) 23 | if err != nil { 24 | return HSet{}, err 25 | } 26 | return cmd, nil 27 | } 28 | 29 | func (cmd HSet) Run(w redis.Writer, red redis.Redka) (any, error) { 30 | count, err := red.Hash().SetMany(cmd.key, cmd.items) 31 | if err != nil { 32 | w.WriteError(cmd.Error(err)) 33 | return nil, err 34 | } 35 | w.WriteInt(count) 36 | return count, nil 37 | } 38 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hget.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Returns the value of a field in a hash. 9 | // HGET key field 10 | // https://redis.io/commands/hget 11 | type HGet struct { 12 | redis.BaseCmd 13 | key string 14 | field string 15 | } 16 | 17 | func ParseHGet(b redis.BaseCmd) (HGet, error) { 18 | cmd := HGet{BaseCmd: b} 19 | if len(cmd.Args()) != 2 { 20 | return HGet{}, redis.ErrInvalidArgNum 21 | } 22 | cmd.key = string(cmd.Args()[0]) 23 | cmd.field = string(cmd.Args()[1]) 24 | return cmd, nil 25 | } 26 | 27 | func (cmd HGet) Run(w redis.Writer, red redis.Redka) (any, error) { 28 | val, err := red.Hash().Get(cmd.key, cmd.field) 29 | if err == core.ErrNotFound { 30 | w.WriteNull() 31 | return val, nil 32 | } 33 | if err != nil { 34 | w.WriteError(cmd.Error(err)) 35 | return nil, err 36 | } 37 | w.WriteBulk(val) 38 | return val, nil 39 | } 40 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hmset.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Sets the values of multiple fields in a hash. 9 | // HMSET key field value [field value ...] 10 | // https://redis.io/commands/hmset 11 | type HMSet struct { 12 | redis.BaseCmd 13 | key string 14 | items map[string]any 15 | } 16 | 17 | func ParseHMSet(b redis.BaseCmd) (HMSet, error) { 18 | cmd := HMSet{BaseCmd: b} 19 | err := parser.New( 20 | parser.String(&cmd.key), 21 | parser.AnyMap(&cmd.items), 22 | ).Required(3).Run(cmd.Args()) 23 | if err != nil { 24 | return HMSet{}, err 25 | } 26 | return cmd, nil 27 | } 28 | 29 | func (cmd HMSet) Run(w redis.Writer, red redis.Redka) (any, error) { 30 | count, err := red.Hash().SetMany(cmd.key, cmd.items) 31 | if err != nil { 32 | w.WriteError(cmd.Error(err)) 33 | return nil, err 34 | } 35 | w.WriteString("OK") 36 | return count, nil 37 | } 38 | -------------------------------------------------------------------------------- /redsrv/internal/command/list/ltrim.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Removes elements from both ends a list. 9 | // LTRIM key start stop 10 | // https://redis.io/commands/ltrim 11 | type LTrim struct { 12 | redis.BaseCmd 13 | key string 14 | start int 15 | stop int 16 | } 17 | 18 | func ParseLTrim(b redis.BaseCmd) (LTrim, error) { 19 | cmd := LTrim{BaseCmd: b} 20 | err := parser.New( 21 | parser.String(&cmd.key), 22 | parser.Int(&cmd.start), 23 | parser.Int(&cmd.stop), 24 | ).Required(3).Run(cmd.Args()) 25 | if err != nil { 26 | return LTrim{}, err 27 | } 28 | return cmd, nil 29 | } 30 | 31 | func (cmd LTrim) Run(w redis.Writer, red redis.Redka) (any, error) { 32 | n, err := red.List().Trim(cmd.key, cmd.start, cmd.stop) 33 | if err != nil { 34 | w.WriteError(cmd.Error(err)) 35 | return nil, err 36 | } 37 | w.WriteString("OK") 38 | return n, nil 39 | } 40 | -------------------------------------------------------------------------------- /redsrv/internal/command/set/sadd.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Adds one or more members to a set. 9 | // Creates the key if it doesn't exist. 10 | // SADD key member [member ...] 11 | // https://redis.io/commands/sadd 12 | type SAdd struct { 13 | redis.BaseCmd 14 | key string 15 | members []any 16 | } 17 | 18 | func ParseSAdd(b redis.BaseCmd) (SAdd, error) { 19 | cmd := SAdd{BaseCmd: b} 20 | err := parser.New( 21 | parser.String(&cmd.key), 22 | parser.Anys(&cmd.members), 23 | ).Required(2).Run(cmd.Args()) 24 | if err != nil { 25 | return SAdd{}, err 26 | } 27 | return cmd, nil 28 | } 29 | 30 | func (cmd SAdd) Run(w redis.Writer, red redis.Redka) (any, error) { 31 | count, err := red.Set().Add(cmd.key, cmd.members...) 32 | if err != nil { 33 | w.WriteError(cmd.Error(err)) 34 | return nil, err 35 | } 36 | w.WriteInt(count) 37 | return count, nil 38 | } 39 | -------------------------------------------------------------------------------- /redsrv/internal/command/list/rpop.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/parser" 6 | "github.com/nalgeon/redka/redsrv/internal/redis" 7 | ) 8 | 9 | // Returns the last element of a list after removing it. 10 | // RPOP key 11 | // https://redis.io/commands/rpop 12 | type RPop struct { 13 | redis.BaseCmd 14 | key string 15 | } 16 | 17 | func ParseRPop(b redis.BaseCmd) (RPop, error) { 18 | cmd := RPop{BaseCmd: b} 19 | err := parser.New( 20 | parser.String(&cmd.key), 21 | ).Required(1).Run(cmd.Args()) 22 | if err != nil { 23 | return RPop{}, err 24 | } 25 | return cmd, nil 26 | } 27 | 28 | func (cmd RPop) Run(w redis.Writer, red redis.Redka) (any, error) { 29 | val, err := red.List().PopBack(cmd.key) 30 | if err == core.ErrNotFound { 31 | w.WriteNull() 32 | return val, nil 33 | } 34 | if err != nil { 35 | w.WriteError(cmd.Error(err)) 36 | return nil, err 37 | } 38 | w.WriteBulk(val) 39 | return val, nil 40 | } 41 | -------------------------------------------------------------------------------- /redsrv/internal/command/set/sdiffstore.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Stores the difference of multiple sets in a key. 9 | // SDIFFSTORE destination key [key ...] 10 | // https://redis.io/commands/sdiffstore 11 | type SDiffStore struct { 12 | redis.BaseCmd 13 | dest string 14 | keys []string 15 | } 16 | 17 | func ParseSDiffStore(b redis.BaseCmd) (SDiffStore, error) { 18 | cmd := SDiffStore{BaseCmd: b} 19 | err := parser.New( 20 | parser.String(&cmd.dest), 21 | parser.Strings(&cmd.keys), 22 | ).Required(2).Run(cmd.Args()) 23 | if err != nil { 24 | return SDiffStore{}, err 25 | } 26 | return cmd, nil 27 | } 28 | 29 | func (cmd SDiffStore) Run(w redis.Writer, red redis.Redka) (any, error) { 30 | n, err := red.Set().DiffStore(cmd.dest, cmd.keys...) 31 | if err != nil { 32 | w.WriteError(cmd.Error(err)) 33 | return nil, err 34 | } 35 | w.WriteInt(n) 36 | return n, nil 37 | } 38 | -------------------------------------------------------------------------------- /redsrv/internal/command/list/lpop.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/parser" 6 | "github.com/nalgeon/redka/redsrv/internal/redis" 7 | ) 8 | 9 | // Returns the first element of a list after removing it. 10 | // LPOP key 11 | // https://redis.io/commands/lpop 12 | type LPop struct { 13 | redis.BaseCmd 14 | key string 15 | } 16 | 17 | func ParseLPop(b redis.BaseCmd) (LPop, error) { 18 | cmd := LPop{BaseCmd: b} 19 | err := parser.New( 20 | parser.String(&cmd.key), 21 | ).Required(1).Run(cmd.Args()) 22 | if err != nil { 23 | return LPop{}, err 24 | } 25 | return cmd, nil 26 | } 27 | 28 | func (cmd LPop) Run(w redis.Writer, red redis.Redka) (any, error) { 29 | val, err := red.List().PopFront(cmd.key) 30 | if err == core.ErrNotFound { 31 | w.WriteNull() 32 | return val, nil 33 | } 34 | if err != nil { 35 | w.WriteError(cmd.Error(err)) 36 | return nil, err 37 | } 38 | w.WriteBulk(val) 39 | return val, nil 40 | } 41 | -------------------------------------------------------------------------------- /redsrv/internal/command/set/sunionstore.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Stores the union of multiple sets in a key. 9 | // SUNIONSTORE destination key [key ...] 10 | // https://redis.io/commands/sunionstore 11 | type SUnionStore struct { 12 | redis.BaseCmd 13 | dest string 14 | keys []string 15 | } 16 | 17 | func ParseSUnionStore(b redis.BaseCmd) (SUnionStore, error) { 18 | cmd := SUnionStore{BaseCmd: b} 19 | err := parser.New( 20 | parser.String(&cmd.dest), 21 | parser.Strings(&cmd.keys), 22 | ).Required(2).Run(cmd.Args()) 23 | if err != nil { 24 | return SUnionStore{}, err 25 | } 26 | return cmd, nil 27 | } 28 | 29 | func (cmd SUnionStore) Run(w redis.Writer, red redis.Redka) (any, error) { 30 | n, err := red.Set().UnionStore(cmd.dest, cmd.keys...) 31 | if err != nil { 32 | w.WriteError(cmd.Error(err)) 33 | return nil, err 34 | } 35 | w.WriteInt(n) 36 | return n, nil 37 | } 38 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hsetnx.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Sets the value of a field in a hash only when the field doesn't exist. 6 | // HSETNX key field value 7 | // https://redis.io/commands/hsetnx 8 | type HSetNX struct { 9 | redis.BaseCmd 10 | key string 11 | field string 12 | value []byte 13 | } 14 | 15 | func ParseHSetNX(b redis.BaseCmd) (HSetNX, error) { 16 | cmd := HSetNX{BaseCmd: b} 17 | if len(cmd.Args()) != 3 { 18 | return HSetNX{}, redis.ErrInvalidArgNum 19 | } 20 | cmd.key = string(cmd.Args()[0]) 21 | cmd.field = string(cmd.Args()[1]) 22 | cmd.value = cmd.Args()[2] 23 | return cmd, nil 24 | } 25 | 26 | func (cmd HSetNX) Run(w redis.Writer, red redis.Redka) (any, error) { 27 | ok, err := red.Hash().SetNotExists(cmd.key, cmd.field, cmd.value) 28 | if err != nil { 29 | w.WriteError(cmd.Error(err)) 30 | return nil, err 31 | } 32 | if ok { 33 | w.WriteInt(1) 34 | } else { 35 | w.WriteInt(0) 36 | } 37 | return ok, nil 38 | } 39 | -------------------------------------------------------------------------------- /redsrv/internal/command/set/sinterstore.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Stores the intersect of multiple sets in a key. 9 | // SINTERSTORE destination key [key ...] 10 | // https://redis.io/commands/sinterstore 11 | type SInterStore struct { 12 | redis.BaseCmd 13 | dest string 14 | keys []string 15 | } 16 | 17 | func ParseSInterStore(b redis.BaseCmd) (SInterStore, error) { 18 | cmd := SInterStore{BaseCmd: b} 19 | err := parser.New( 20 | parser.String(&cmd.dest), 21 | parser.Strings(&cmd.keys), 22 | ).Required(2).Run(cmd.Args()) 23 | if err != nil { 24 | return SInterStore{}, err 25 | } 26 | return cmd, nil 27 | } 28 | 29 | func (cmd SInterStore) Run(w redis.Writer, red redis.Redka) (any, error) { 30 | n, err := red.Set().InterStore(cmd.dest, cmd.keys...) 31 | if err != nil { 32 | w.WriteError(cmd.Error(err)) 33 | return nil, err 34 | } 35 | w.WriteInt(n) 36 | return n, nil 37 | } 38 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hdel.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Deletes one or more fields and their values from a hash. 9 | // Deletes the hash if no fields remain. 10 | // HDEL key field [field ...] 11 | // https://redis.io/commands/hdel 12 | type HDel struct { 13 | redis.BaseCmd 14 | key string 15 | fields []string 16 | } 17 | 18 | func ParseHDel(b redis.BaseCmd) (HDel, error) { 19 | cmd := HDel{BaseCmd: b} 20 | err := parser.New( 21 | parser.String(&cmd.key), 22 | parser.Strings(&cmd.fields), 23 | ).Required(2).Run(cmd.Args()) 24 | if err != nil { 25 | return HDel{}, err 26 | } 27 | return cmd, nil 28 | } 29 | 30 | func (cmd HDel) Run(w redis.Writer, red redis.Redka) (any, error) { 31 | count, err := red.Hash().Delete(cmd.key, cmd.fields...) 32 | if err != nil { 33 | w.WriteError(cmd.Error(err)) 34 | return nil, err 35 | } 36 | w.WriteInt(count) 37 | return count, nil 38 | } 39 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zcount.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Returns the count of members in a sorted set that have scores within a range. 9 | // ZCOUNT key min max 10 | // https://redis.io/commands/zcount 11 | type ZCount struct { 12 | redis.BaseCmd 13 | key string 14 | min float64 15 | max float64 16 | } 17 | 18 | func ParseZCount(b redis.BaseCmd) (ZCount, error) { 19 | cmd := ZCount{BaseCmd: b} 20 | err := parser.New( 21 | parser.String(&cmd.key), 22 | parser.Float(&cmd.min), 23 | parser.Float(&cmd.max), 24 | ).Required(3).Run(cmd.Args()) 25 | if err != nil { 26 | return ZCount{}, err 27 | } 28 | return cmd, nil 29 | } 30 | 31 | func (cmd ZCount) Run(w redis.Writer, red redis.Redka) (any, error) { 32 | n, err := red.ZSet().Count(cmd.key, cmd.min, cmd.max) 33 | if err != nil { 34 | w.WriteError(cmd.Error(err)) 35 | return nil, err 36 | } 37 | w.WriteInt(n) 38 | return n, nil 39 | } 40 | -------------------------------------------------------------------------------- /redsrv/internal/command/set/sismember.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Determines whether a member belongs to a set. 9 | // SISMEMBER key member 10 | // https://redis.io/commands/sismember 11 | type SIsMember struct { 12 | redis.BaseCmd 13 | key string 14 | member []byte 15 | } 16 | 17 | func ParseSIsMember(b redis.BaseCmd) (SIsMember, error) { 18 | cmd := SIsMember{BaseCmd: b} 19 | err := parser.New( 20 | parser.String(&cmd.key), 21 | parser.Bytes(&cmd.member), 22 | ).Required(2).Run(cmd.Args()) 23 | if err != nil { 24 | return SIsMember{}, err 25 | } 26 | return cmd, nil 27 | } 28 | 29 | func (cmd SIsMember) Run(w redis.Writer, red redis.Redka) (any, error) { 30 | ok, err := red.Set().Exists(cmd.key, cmd.member) 31 | if err != nil { 32 | w.WriteError(cmd.Error(err)) 33 | return nil, err 34 | } 35 | if ok { 36 | w.WriteInt(1) 37 | } else { 38 | w.WriteInt(0) 39 | } 40 | return ok, nil 41 | } 42 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zadd.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Adds one or more members to a sorted set, or updates their scores. 9 | // Creates the key if it doesn't exist. 10 | // ZADD key score member [score member ...] 11 | // https://redis.io/commands/zadd 12 | type ZAdd struct { 13 | redis.BaseCmd 14 | key string 15 | items map[any]float64 16 | } 17 | 18 | func ParseZAdd(b redis.BaseCmd) (ZAdd, error) { 19 | cmd := ZAdd{BaseCmd: b} 20 | err := parser.New( 21 | parser.String(&cmd.key), 22 | parser.FloatMap(&cmd.items), 23 | ).Required(3).Run(cmd.Args()) 24 | if err != nil { 25 | return ZAdd{}, err 26 | } 27 | return cmd, nil 28 | } 29 | 30 | func (cmd ZAdd) Run(w redis.Writer, red redis.Redka) (any, error) { 31 | count, err := red.ZSet().AddMany(cmd.key, cmd.items) 32 | if err != nil { 33 | w.WriteError(cmd.Error(err)) 34 | return nil, err 35 | } 36 | w.WriteInt(count) 37 | return count, nil 38 | } 39 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/ttl.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/nalgeon/redka/internal/core" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | // Returns the expiration time in seconds of a key. 11 | // TTL key 12 | // https://redis.io/commands/ttl 13 | type TTL struct { 14 | redis.BaseCmd 15 | key string 16 | } 17 | 18 | func ParseTTL(b redis.BaseCmd) (TTL, error) { 19 | cmd := TTL{BaseCmd: b} 20 | if len(cmd.Args()) != 1 { 21 | return TTL{}, redis.ErrInvalidArgNum 22 | } 23 | cmd.key = string(cmd.Args()[0]) 24 | return cmd, nil 25 | } 26 | 27 | func (cmd TTL) Run(w redis.Writer, red redis.Redka) (any, error) { 28 | k, err := red.Key().Get(cmd.key) 29 | if err == core.ErrNotFound { 30 | w.WriteInt(-2) 31 | return -2, nil 32 | } 33 | if err != nil { 34 | w.WriteError(cmd.Error(err)) 35 | return nil, err 36 | } 37 | if k.ETime == nil { 38 | w.WriteInt(-1) 39 | return -1, nil 40 | } 41 | ttl := int(*k.ETime/1000 - time.Now().Unix()) 42 | w.WriteInt(ttl) 43 | return ttl, nil 44 | } 45 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/incr.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import "github.com/nalgeon/redka/redsrv/internal/redis" 4 | 5 | // Increments the integer value of a key by one. 6 | // Uses 0 as initial value if the key doesn't exist. 7 | // INCR key 8 | // https://redis.io/commands/incr 9 | // 10 | // Decrements the integer value of a key by one. 11 | // Uses 0 as initial value if the key doesn't exist. 12 | // DECR key 13 | // https://redis.io/commands/decr 14 | type Incr struct { 15 | redis.BaseCmd 16 | key string 17 | delta int 18 | } 19 | 20 | func ParseIncr(b redis.BaseCmd, sign int) (Incr, error) { 21 | cmd := Incr{BaseCmd: b} 22 | if len(cmd.Args()) != 1 { 23 | return Incr{}, redis.ErrInvalidArgNum 24 | } 25 | cmd.key = string(cmd.Args()[0]) 26 | cmd.delta = sign 27 | return cmd, nil 28 | } 29 | 30 | func (cmd Incr) Run(w redis.Writer, red redis.Redka) (any, error) { 31 | val, err := red.Str().Incr(cmd.key, cmd.delta) 32 | if err != nil { 33 | w.WriteError(cmd.Error(err)) 34 | return nil, err 35 | } 36 | w.WriteInt(val) 37 | return val, nil 38 | } 39 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zincrby.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Increments the score of a member in a sorted set. 9 | // ZINCRBY key increment member 10 | // https://redis.io/commands/zincrby 11 | type ZIncrBy struct { 12 | redis.BaseCmd 13 | key string 14 | delta float64 15 | member string 16 | } 17 | 18 | func ParseZIncrBy(b redis.BaseCmd) (ZIncrBy, error) { 19 | cmd := ZIncrBy{BaseCmd: b} 20 | err := parser.New( 21 | parser.String(&cmd.key), 22 | parser.Float(&cmd.delta), 23 | parser.String(&cmd.member), 24 | ).Required(3).Run(cmd.Args()) 25 | if err != nil { 26 | return ZIncrBy{}, err 27 | } 28 | return cmd, nil 29 | } 30 | 31 | func (cmd ZIncrBy) Run(w redis.Writer, red redis.Redka) (any, error) { 32 | score, err := red.ZSet().Incr(cmd.key, cmd.member, cmd.delta) 33 | if err != nil { 34 | w.WriteError(cmd.Error(err)) 35 | return nil, err 36 | } 37 | redis.WriteFloat(w, score) 38 | return score, nil 39 | } 40 | -------------------------------------------------------------------------------- /redsrv/internal/command/list/lrange.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Returns a range of elements from a list. 9 | // LRANGE key start stop 10 | // https://redis.io/commands/lrange 11 | type LRange struct { 12 | redis.BaseCmd 13 | key string 14 | start int 15 | stop int 16 | } 17 | 18 | func ParseLRange(b redis.BaseCmd) (LRange, error) { 19 | cmd := LRange{BaseCmd: b} 20 | err := parser.New( 21 | parser.String(&cmd.key), 22 | parser.Int(&cmd.start), 23 | parser.Int(&cmd.stop), 24 | ).Required(3).Run(cmd.Args()) 25 | if err != nil { 26 | return LRange{}, err 27 | } 28 | return cmd, nil 29 | } 30 | 31 | func (cmd LRange) Run(w redis.Writer, red redis.Redka) (any, error) { 32 | vals, err := red.List().Range(cmd.key, cmd.start, cmd.stop) 33 | if err != nil { 34 | w.WriteError(cmd.Error(err)) 35 | return nil, err 36 | } 37 | w.WriteArray(len(vals)) 38 | for _, v := range vals { 39 | w.WriteBulk(v) 40 | } 41 | return vals, nil 42 | } 43 | -------------------------------------------------------------------------------- /redsrv/internal/command/list/lindex.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/parser" 6 | "github.com/nalgeon/redka/redsrv/internal/redis" 7 | ) 8 | 9 | // Returns an element from a list by its index. 10 | // LINDEX key index 11 | // https://redis.io/commands/lindex 12 | type LIndex struct { 13 | redis.BaseCmd 14 | key string 15 | index int 16 | } 17 | 18 | func ParseLIndex(b redis.BaseCmd) (LIndex, error) { 19 | cmd := LIndex{BaseCmd: b} 20 | err := parser.New( 21 | parser.String(&cmd.key), 22 | parser.Int(&cmd.index), 23 | ).Required(2).Run(cmd.Args()) 24 | if err != nil { 25 | return LIndex{}, err 26 | } 27 | return cmd, nil 28 | } 29 | 30 | func (cmd LIndex) Run(w redis.Writer, red redis.Redka) (any, error) { 31 | val, err := red.List().Get(cmd.key, cmd.index) 32 | if err == core.ErrNotFound { 33 | w.WriteNull() 34 | return val, nil 35 | } 36 | if err != nil { 37 | w.WriteError(cmd.Error(err)) 38 | return nil, err 39 | } 40 | w.WriteBulk(val) 41 | return val, nil 42 | } 43 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/getset.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Returns the previous string value of a key after setting it to a new value. 9 | // GETSET key value 10 | // https://redis.io/commands/getset 11 | type GetSet struct { 12 | redis.BaseCmd 13 | key string 14 | value []byte 15 | } 16 | 17 | func ParseGetSet(b redis.BaseCmd) (GetSet, error) { 18 | cmd := GetSet{BaseCmd: b} 19 | if len(cmd.Args()) != 2 { 20 | return GetSet{}, redis.ErrInvalidArgNum 21 | } 22 | cmd.key = string(cmd.Args()[0]) 23 | cmd.value = cmd.Args()[1] 24 | return cmd, nil 25 | } 26 | 27 | func (cmd GetSet) Run(w redis.Writer, red redis.Redka) (any, error) { 28 | out, err := red.Str().SetWith(cmd.key, cmd.value).Run() 29 | if err != nil { 30 | w.WriteError(cmd.Error(err)) 31 | return nil, err 32 | } 33 | if out.Created { 34 | // no previous value 35 | w.WriteNull() 36 | return core.Value(nil), nil 37 | } 38 | w.WriteBulk(out.Prev) 39 | return out.Prev, nil 40 | } 41 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/incrbyfloat.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Increment the floating point value of a key by a number. 9 | // Uses 0 as initial value if the key doesn't exist. 10 | // INCRBYFLOAT key increment 11 | // https://redis.io/commands/incrbyfloat 12 | type IncrByFloat struct { 13 | redis.BaseCmd 14 | key string 15 | delta float64 16 | } 17 | 18 | func ParseIncrByFloat(b redis.BaseCmd) (IncrByFloat, error) { 19 | cmd := IncrByFloat{BaseCmd: b} 20 | err := parser.New( 21 | parser.String(&cmd.key), 22 | parser.Float(&cmd.delta), 23 | ).Required(2).Run(cmd.Args()) 24 | if err != nil { 25 | return IncrByFloat{}, err 26 | } 27 | return cmd, nil 28 | } 29 | 30 | func (cmd IncrByFloat) Run(w redis.Writer, red redis.Redka) (any, error) { 31 | val, err := red.Str().IncrFloat(cmd.key, cmd.delta) 32 | if err != nil { 33 | w.WriteError(cmd.Error(err)) 34 | return nil, err 35 | } 36 | redis.WriteFloat(w, val) 37 | return val, nil 38 | } 39 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hincrby.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Increments the integer value of a field in a hash by a number. 9 | // Uses 0 as initial value if the field doesn't exist. 10 | // HINCRBY key field increment 11 | // https://redis.io/commands/hincrby 12 | type HIncrBy struct { 13 | redis.BaseCmd 14 | key string 15 | field string 16 | delta int 17 | } 18 | 19 | func ParseHIncrBy(b redis.BaseCmd) (HIncrBy, error) { 20 | cmd := HIncrBy{BaseCmd: b} 21 | err := parser.New( 22 | parser.String(&cmd.key), 23 | parser.String(&cmd.field), 24 | parser.Int(&cmd.delta), 25 | ).Required(3).Run(cmd.Args()) 26 | if err != nil { 27 | return HIncrBy{}, err 28 | } 29 | return cmd, nil 30 | } 31 | 32 | func (cmd HIncrBy) Run(w redis.Writer, red redis.Redka) (any, error) { 33 | val, err := red.Hash().Incr(cmd.key, cmd.field, cmd.delta) 34 | if err != nil { 35 | w.WriteError(cmd.Error(err)) 36 | return nil, err 37 | } 38 | w.WriteInt(val) 39 | return val, nil 40 | } 41 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 2 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 3 | github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= 4 | github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 5 | github.com/nalgeon/be v0.2.0 h1:i1Rsh0F+aNnHdbgph5Cy8Xm5uMVeWrUpm1olgzlPsMo= 6 | github.com/nalgeon/be v0.2.0/go.mod h1:PMwMuBLopwKJkSHnr2qHyLcZYUTqNejN7A8RAqNWO3E= 7 | github.com/tidwall/btree v1.1.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4= 8 | github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= 9 | github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= 10 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 11 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 12 | github.com/tidwall/redcon v1.6.2 h1:5qfvrrybgtO85jnhSravmkZyC0D+7WstbfCs3MmPhow= 13 | github.com/tidwall/redcon v1.6.2/go.mod h1:p5Wbsgeyi2VSTBWOcA5vRXrOb9arFTcU2+ZzFjqV75Y= 14 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zremrangebyrank.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Removes members in a sorted set within a range of indexes. 9 | // ZREMRANGEBYRANK key start stop 10 | // https://redis.io/commands/zremrangebyrank 11 | type ZRemRangeByRank struct { 12 | redis.BaseCmd 13 | key string 14 | start int 15 | stop int 16 | } 17 | 18 | func ParseZRemRangeByRank(b redis.BaseCmd) (ZRemRangeByRank, error) { 19 | cmd := ZRemRangeByRank{BaseCmd: b} 20 | err := parser.New( 21 | parser.String(&cmd.key), 22 | parser.Int(&cmd.start), 23 | parser.Int(&cmd.stop), 24 | ).Required(3).Run(cmd.Args()) 25 | if err != nil { 26 | return ZRemRangeByRank{}, err 27 | } 28 | return cmd, nil 29 | } 30 | 31 | func (cmd ZRemRangeByRank) Run(w redis.Writer, red redis.Redka) (any, error) { 32 | n, err := red.ZSet().DeleteWith(cmd.key).ByRank(cmd.start, cmd.stop).Run() 33 | if err != nil { 34 | w.WriteError(cmd.Error(err)) 35 | return nil, err 36 | } 37 | w.WriteInt(n) 38 | return n, nil 39 | } 40 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zscore.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/parser" 6 | "github.com/nalgeon/redka/redsrv/internal/redis" 7 | ) 8 | 9 | // Returns the score of a member in a sorted set. 10 | // ZSCORE key member 11 | // https://redis.io/commands/zscore 12 | type ZScore struct { 13 | redis.BaseCmd 14 | key string 15 | member string 16 | } 17 | 18 | func ParseZScore(b redis.BaseCmd) (ZScore, error) { 19 | cmd := ZScore{BaseCmd: b} 20 | err := parser.New( 21 | parser.String(&cmd.key), 22 | parser.String(&cmd.member), 23 | ).Required(2).Run(cmd.Args()) 24 | if err != nil { 25 | return ZScore{}, err 26 | } 27 | return cmd, nil 28 | } 29 | 30 | func (cmd ZScore) Run(w redis.Writer, red redis.Redka) (any, error) { 31 | score, err := red.ZSet().GetScore(cmd.key, cmd.member) 32 | if err == core.ErrNotFound { 33 | w.WriteNull() 34 | return nil, nil 35 | } 36 | if err != nil { 37 | w.WriteError(cmd.Error(err)) 38 | return nil, err 39 | } 40 | redis.WriteFloat(w, score) 41 | return score, nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/testx/db.go: -------------------------------------------------------------------------------- 1 | package testx 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/redka" 7 | ) 8 | 9 | // driver to connect to the test database. 10 | var driver string 11 | 12 | // connStrings are connection strings for test databases. 13 | var connStrings = map[string]string{ 14 | "postgres": "postgres://redka:redka@localhost:5432/redka?sslmode=disable", 15 | "sqlite3": "file:/redka.db?vfs=memdb", 16 | } 17 | 18 | // OpenDB returns a database handle for testing. 19 | // Uses the driver specified in the build tag. 20 | func OpenDB(tb testing.TB) *redka.DB { 21 | tb.Helper() 22 | 23 | // Get the database connection string. 24 | connStr := connStrings[driver] 25 | if connStr == "" { 26 | tb.Fatalf("unknown driver: %s", driver) 27 | } 28 | 29 | // Open the database. 30 | opts := &redka.Options{DriverName: driver} 31 | db, err := redka.Open(connStr, opts) 32 | if err != nil { 33 | tb.Fatal(err) 34 | } 35 | tb.Cleanup(func() { 36 | _ = db.Close() 37 | }) 38 | 39 | // Clear the database. 40 | err = db.Key().DeleteAll() 41 | if err != nil { 42 | tb.Fatal(err) 43 | } 44 | 45 | return db 46 | } 47 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zremrangebyscore.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Removes members in a sorted set within a range of scores. 9 | // ZREMRANGEBYSCORE key min max 10 | // https://redis.io/commands/zremrangebyscore 11 | type ZRemRangeByScore struct { 12 | redis.BaseCmd 13 | key string 14 | min float64 15 | max float64 16 | } 17 | 18 | func ParseZRemRangeByScore(b redis.BaseCmd) (ZRemRangeByScore, error) { 19 | cmd := ZRemRangeByScore{BaseCmd: b} 20 | err := parser.New( 21 | parser.String(&cmd.key), 22 | parser.Float(&cmd.min), 23 | parser.Float(&cmd.max), 24 | ).Required(3).Run(cmd.Args()) 25 | if err != nil { 26 | return ZRemRangeByScore{}, err 27 | } 28 | return cmd, nil 29 | } 30 | 31 | func (cmd ZRemRangeByScore) Run(w redis.Writer, red redis.Redka) (any, error) { 32 | n, err := red.ZSet().DeleteWith(cmd.key).ByScore(cmd.min, cmd.max).Run() 33 | if err != nil { 34 | w.WriteError(cmd.Error(err)) 35 | return nil, err 36 | } 37 | w.WriteInt(n) 38 | return n, nil 39 | } 40 | -------------------------------------------------------------------------------- /redsrv/internal/command/set/smove.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/parser" 6 | "github.com/nalgeon/redka/redsrv/internal/redis" 7 | ) 8 | 9 | // Moves a member from one set to another. 10 | // SMOVE source destination member 11 | // https://redis.io/commands/smove 12 | type SMove struct { 13 | redis.BaseCmd 14 | src string 15 | dest string 16 | member []byte 17 | } 18 | 19 | func ParseSMove(b redis.BaseCmd) (SMove, error) { 20 | cmd := SMove{BaseCmd: b} 21 | err := parser.New( 22 | parser.String(&cmd.src), 23 | parser.String(&cmd.dest), 24 | parser.Bytes(&cmd.member), 25 | ).Required(3).Run(cmd.Args()) 26 | if err != nil { 27 | return SMove{}, err 28 | } 29 | return cmd, nil 30 | } 31 | 32 | func (cmd SMove) Run(w redis.Writer, red redis.Redka) (any, error) { 33 | err := red.Set().Move(cmd.src, cmd.dest, cmd.member) 34 | if err == core.ErrNotFound { 35 | w.WriteInt(0) 36 | return 0, nil 37 | } 38 | if err != nil { 39 | w.WriteError(cmd.Error(err)) 40 | return nil, err 41 | } 42 | w.WriteInt(1) 43 | return 1, nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/rset/postgres.go: -------------------------------------------------------------------------------- 1 | package rset 2 | 3 | // Postgres queries for the set repository. 4 | var postgres = queries{ 5 | scan: ` 6 | select rset.rowid, elem 7 | from rset join rkey on kid = rkey.id and type = 3 8 | where 9 | key = $1 and (etime is null or etime > $2) 10 | and rset.rowid > $3 and elem like $4 11 | order by rset.rowid asc 12 | limit $5`, 13 | } 14 | 15 | func init() { 16 | postgres.add1 = sqlite.add1 17 | postgres.add2 = sqlite.add2 18 | postgres.clone = sqlite.clone 19 | postgres.delete1 = sqlite.delete1 20 | postgres.delete2 = sqlite.delete2 21 | postgres.deleteKey1 = sqlite.deleteKey1 22 | postgres.deleteKey2 = sqlite.deleteKey2 23 | postgres.diff = sqlite.diff 24 | postgres.diffStore = sqlite.diffStore 25 | postgres.exists = sqlite.exists 26 | postgres.inter = sqlite.inter 27 | postgres.interStore = sqlite.interStore 28 | postgres.items = sqlite.items 29 | postgres.len = sqlite.len 30 | postgres.pop1 = sqlite.pop1 31 | postgres.pop2 = sqlite.pop2 32 | postgres.random = sqlite.random 33 | // postgres.scan = sqlite.scan 34 | postgres.union = sqlite.union 35 | postgres.unionStore = sqlite.unionStore 36 | } 37 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hincrbyfloat.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Increments the floating point value of a field by a number. 9 | // Uses 0 as initial value if the field doesn't exist. 10 | // HINCRBY key field increment 11 | // https://redis.io/commands/hincrbyfloat 12 | type HIncrByFloat struct { 13 | redis.BaseCmd 14 | key string 15 | field string 16 | delta float64 17 | } 18 | 19 | func ParseHIncrByFloat(b redis.BaseCmd) (HIncrByFloat, error) { 20 | cmd := HIncrByFloat{BaseCmd: b} 21 | err := parser.New( 22 | parser.String(&cmd.key), 23 | parser.String(&cmd.field), 24 | parser.Float(&cmd.delta), 25 | ).Required(3).Run(cmd.Args()) 26 | if err != nil { 27 | return HIncrByFloat{}, err 28 | } 29 | return cmd, nil 30 | } 31 | 32 | func (cmd HIncrByFloat) Run(w redis.Writer, red redis.Redka) (any, error) { 33 | val, err := red.Hash().IncrFloat(cmd.key, cmd.field, cmd.delta) 34 | if err != nil { 35 | w.WriteError(cmd.Error(err)) 36 | return nil, err 37 | } 38 | redis.WriteFloat(w, val) 39 | return val, nil 40 | } 41 | -------------------------------------------------------------------------------- /redsrv/internal/command/list/lset.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/parser" 6 | "github.com/nalgeon/redka/redsrv/internal/redis" 7 | ) 8 | 9 | // Sets the value of an element in a list by its index. 10 | // LSET key index element 11 | // https://redis.io/commands/lset 12 | type LSet struct { 13 | redis.BaseCmd 14 | key string 15 | index int 16 | elem []byte 17 | } 18 | 19 | func ParseLSet(b redis.BaseCmd) (LSet, error) { 20 | cmd := LSet{BaseCmd: b} 21 | err := parser.New( 22 | parser.String(&cmd.key), 23 | parser.Int(&cmd.index), 24 | parser.Bytes(&cmd.elem), 25 | ).Required(3).Run(cmd.Args()) 26 | if err != nil { 27 | return LSet{}, err 28 | } 29 | return cmd, nil 30 | } 31 | 32 | func (cmd LSet) Run(w redis.Writer, red redis.Redka) (any, error) { 33 | err := red.List().Set(cmd.key, cmd.index, cmd.elem) 34 | if err == core.ErrNotFound { 35 | w.WriteError(cmd.Error(redis.ErrOutOfRange)) 36 | return nil, err 37 | } 38 | if err != nil { 39 | w.WriteError(cmd.Error(err)) 40 | return nil, err 41 | } 42 | w.WriteString("OK") 43 | return nil, nil 44 | } 45 | -------------------------------------------------------------------------------- /redsrv/internal/command/list/rpoplpush.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/parser" 6 | "github.com/nalgeon/redka/redsrv/internal/redis" 7 | ) 8 | 9 | // Returns the last element of a list after removing 10 | // and pushing it to another list. 11 | // RPOPLPUSH source destination 12 | // https://redis.io/commands/rpoplpush 13 | type RPopLPush struct { 14 | redis.BaseCmd 15 | src string 16 | dst string 17 | } 18 | 19 | func ParseRPopLPush(b redis.BaseCmd) (RPopLPush, error) { 20 | cmd := RPopLPush{BaseCmd: b} 21 | err := parser.New( 22 | parser.String(&cmd.src), 23 | parser.String(&cmd.dst), 24 | ).Required(2).Run(cmd.Args()) 25 | if err != nil { 26 | return RPopLPush{}, err 27 | } 28 | return cmd, nil 29 | } 30 | 31 | func (cmd RPopLPush) Run(w redis.Writer, red redis.Redka) (any, error) { 32 | val, err := red.List().PopBackPushFront(cmd.src, cmd.dst) 33 | if err == core.ErrNotFound { 34 | w.WriteNull() 35 | return val, nil 36 | } 37 | if err != nil { 38 | w.WriteError(cmd.Error(err)) 39 | return nil, err 40 | } 41 | w.WriteBulk(val) 42 | return val, nil 43 | } 44 | -------------------------------------------------------------------------------- /redsrv/internal/command/server/config.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/redis" 5 | ) 6 | 7 | // Container command for runtime configuration commands. 8 | // CONFIG 9 | // https://redis.io/commands/config 10 | type Config struct { 11 | redis.BaseCmd 12 | subcmd string 13 | get ConfigGet 14 | } 15 | 16 | func ParseConfig(b redis.BaseCmd) (Config, error) { 17 | // Extract the subcommand. 18 | cmd := Config{BaseCmd: b} 19 | if len(cmd.Args()) == 0 { 20 | return Config{}, redis.ErrInvalidArgNum 21 | } 22 | cmd.subcmd = string(cmd.Args()[0]) 23 | 24 | // Parse the subcommand. 25 | var err error 26 | args := cmd.Args()[1:] 27 | switch cmd.subcmd { 28 | case "get": 29 | cmd.get, err = ParseConfigGet(args) 30 | default: 31 | err = redis.ErrUnknownSubcmd 32 | } 33 | 34 | // Return the resulting command. 35 | if err != nil { 36 | return Config{}, err 37 | } 38 | return cmd, nil 39 | } 40 | 41 | func (c Config) Run(w redis.Writer, red redis.Redka) (any, error) { 42 | switch c.subcmd { 43 | case "get": 44 | return c.get.Run(w, red) 45 | default: 46 | w.WriteString("OK") 47 | return true, nil 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/setex.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/nalgeon/redka/redsrv/internal/parser" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | // Sets the string value and expiration time of a key. 11 | // Creates the key if it doesn't exist. 12 | // SETEX key seconds value 13 | // https://redis.io/commands/setex 14 | type SetEX struct { 15 | redis.BaseCmd 16 | key string 17 | value []byte 18 | ttl time.Duration 19 | } 20 | 21 | func ParseSetEX(b redis.BaseCmd, multi int) (SetEX, error) { 22 | cmd := SetEX{BaseCmd: b} 23 | var ttl int 24 | err := parser.New( 25 | parser.String(&cmd.key), 26 | parser.Int(&ttl), 27 | parser.Bytes(&cmd.value), 28 | ).Required(3).Run(cmd.Args()) 29 | if err != nil { 30 | return SetEX{}, err 31 | } 32 | cmd.ttl = time.Duration(multi*ttl) * time.Millisecond 33 | return cmd, nil 34 | } 35 | 36 | func (cmd SetEX) Run(w redis.Writer, red redis.Redka) (any, error) { 37 | err := red.Str().SetExpire(cmd.key, cmd.value, cmd.ttl) 38 | if err != nil { 39 | w.WriteError(cmd.Error(err)) 40 | return nil, err 41 | } 42 | w.WriteString("OK") 43 | return true, nil 44 | } 45 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/expire.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/nalgeon/redka/internal/core" 7 | "github.com/nalgeon/redka/redsrv/internal/parser" 8 | "github.com/nalgeon/redka/redsrv/internal/redis" 9 | ) 10 | 11 | // Sets the expiration time of a key in seconds. 12 | // EXPIRE key seconds 13 | // https://redis.io/commands/expire 14 | type Expire struct { 15 | redis.BaseCmd 16 | key string 17 | ttl time.Duration 18 | } 19 | 20 | func ParseExpire(b redis.BaseCmd, multi int) (Expire, error) { 21 | cmd := Expire{BaseCmd: b} 22 | 23 | var ttl int 24 | err := parser.New( 25 | parser.String(&cmd.key), 26 | parser.Int(&ttl), 27 | ).Required(2).Run(cmd.Args()) 28 | if err != nil { 29 | return Expire{}, err 30 | } 31 | 32 | cmd.ttl = time.Duration(multi*ttl) * time.Millisecond 33 | return cmd, nil 34 | } 35 | 36 | func (cmd Expire) Run(w redis.Writer, red redis.Redka) (any, error) { 37 | err := red.Key().Expire(cmd.key, cmd.ttl) 38 | if err != nil && err != core.ErrNotFound { 39 | w.WriteError(cmd.Error(err)) 40 | return nil, err 41 | } 42 | if err == core.ErrNotFound { 43 | w.WriteInt(0) 44 | return false, nil 45 | } 46 | w.WriteInt(1) 47 | return true, nil 48 | } 49 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/expireat.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/nalgeon/redka/internal/core" 7 | "github.com/nalgeon/redka/redsrv/internal/parser" 8 | "github.com/nalgeon/redka/redsrv/internal/redis" 9 | ) 10 | 11 | // Sets the expiration time of a key to a Unix timestamp. 12 | // EXPIREAT key unix-time-seconds 13 | // https://redis.io/commands/expireat 14 | type ExpireAt struct { 15 | redis.BaseCmd 16 | key string 17 | at time.Time 18 | } 19 | 20 | func ParseExpireAt(b redis.BaseCmd, multi int) (ExpireAt, error) { 21 | cmd := ExpireAt{BaseCmd: b} 22 | 23 | var at int 24 | err := parser.New( 25 | parser.String(&cmd.key), 26 | parser.Int(&at), 27 | ).Required(2).Run(cmd.Args()) 28 | if err != nil { 29 | return ExpireAt{}, err 30 | } 31 | 32 | cmd.at = time.UnixMilli(int64(multi * at)) 33 | return cmd, nil 34 | } 35 | 36 | func (cmd ExpireAt) Run(w redis.Writer, red redis.Redka) (any, error) { 37 | err := red.Key().ExpireAt(cmd.key, cmd.at) 38 | if err != nil && err != core.ErrNotFound { 39 | w.WriteError(cmd.Error(err)) 40 | return nil, err 41 | } 42 | if err == core.ErrNotFound { 43 | w.WriteInt(0) 44 | return false, nil 45 | } 46 | w.WriteInt(1) 47 | return true, nil 48 | } 49 | -------------------------------------------------------------------------------- /redsrv/internal/command/list/lrem.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Removes elements from a list. 9 | // LREM key count element 10 | // https://redis.io/commands/lrem 11 | type LRem struct { 12 | redis.BaseCmd 13 | key string 14 | count int 15 | elem []byte 16 | } 17 | 18 | func ParseLRem(b redis.BaseCmd) (LRem, error) { 19 | cmd := LRem{BaseCmd: b} 20 | err := parser.New( 21 | parser.String(&cmd.key), 22 | parser.Int(&cmd.count), 23 | parser.Bytes(&cmd.elem), 24 | ).Required(3).Run(cmd.Args()) 25 | if err != nil { 26 | return LRem{}, err 27 | } 28 | return cmd, nil 29 | } 30 | 31 | func (cmd LRem) Run(w redis.Writer, red redis.Redka) (any, error) { 32 | var n int 33 | var err error 34 | switch { 35 | case cmd.count > 0: 36 | n, err = red.List().DeleteFront(cmd.key, cmd.elem, cmd.count) 37 | case cmd.count < 0: 38 | n, err = red.List().DeleteBack(cmd.key, cmd.elem, -cmd.count) 39 | case cmd.count == 0: 40 | n, err = red.List().Delete(cmd.key, cmd.elem) 41 | } 42 | if err != nil { 43 | w.WriteError(cmd.Error(err)) 44 | return nil, err 45 | } 46 | w.WriteInt(n) 47 | return n, nil 48 | } 49 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/incrby.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Increments the integer value of a key by a number. 9 | // Uses 0 as initial value if the key doesn't exist. 10 | // INCRBY key increment 11 | // https://redis.io/commands/incrby 12 | // 13 | // Decrements the integer value of a key by a number. 14 | // Uses 0 as initial value if the key doesn't exist. 15 | // DECRBY key increment 16 | // https://redis.io/commands/decrby 17 | type IncrBy struct { 18 | redis.BaseCmd 19 | key string 20 | delta int 21 | } 22 | 23 | func ParseIncrBy(b redis.BaseCmd, sign int) (IncrBy, error) { 24 | cmd := IncrBy{BaseCmd: b} 25 | err := parser.New( 26 | parser.String(&cmd.key), 27 | parser.Int(&cmd.delta), 28 | ).Required(2).Run(cmd.Args()) 29 | if err != nil { 30 | return IncrBy{}, err 31 | } 32 | cmd.delta *= sign 33 | return cmd, nil 34 | } 35 | 36 | func (cmd IncrBy) Run(w redis.Writer, red redis.Redka) (any, error) { 37 | val, err := red.Str().Incr(cmd.key, cmd.delta) 38 | if err != nil { 39 | w.WriteError(cmd.Error(err)) 40 | return nil, err 41 | } 42 | w.WriteInt(val) 43 | return val, nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/rzset/postgres.go: -------------------------------------------------------------------------------- 1 | package rzset 2 | 3 | // Postgres queries for the sorted set repository. 4 | var postgres = queries{ 5 | scan: ` 6 | select rzset.rowid, elem, score 7 | from rzset join rkey on kid = rkey.id and type = 5 8 | where 9 | key = $1 and (etime is null or etime > $2) 10 | and rzset.rowid > $3 and elem like $4 11 | order by rzset.rowid asc 12 | limit $5`, 13 | } 14 | 15 | func init() { 16 | postgres.add1 = sqlite.add1 17 | postgres.add2 = sqlite.add2 18 | postgres.count = sqlite.count 19 | postgres.countScore = sqlite.countScore 20 | postgres.delete1 = sqlite.delete1 21 | postgres.delete2 = sqlite.delete2 22 | postgres.deleteAll1 = sqlite.deleteAll1 23 | postgres.deleteAll2 = sqlite.deleteAll2 24 | postgres.deleteRank = sqlite.deleteRank 25 | postgres.deleteScore = sqlite.deleteScore 26 | postgres.getRank = sqlite.getRank 27 | postgres.getScore = sqlite.getScore 28 | postgres.incr = sqlite.incr 29 | postgres.inter = sqlite.inter 30 | postgres.interStore = sqlite.interStore 31 | postgres.len = sqlite.len 32 | postgres.rangeRank = sqlite.rangeRank 33 | postgres.rangeScore = sqlite.rangeScore 34 | // postgres.scan = sqlite.scan 35 | postgres.union = sqlite.union 36 | postgres.unionStore = sqlite.unionStore 37 | } 38 | -------------------------------------------------------------------------------- /internal/rkey/postgres.go: -------------------------------------------------------------------------------- 1 | package rkey 2 | 3 | // Postgres queries for the key repository. 4 | var postgres = queries{ 5 | deleteAll: ` 6 | truncate table rkey cascade`, 7 | 8 | deleteNExpired: ` 9 | delete from rkey 10 | where ctid in ( 11 | select ctid from rkey 12 | where etime <= $1 13 | limit $2 14 | )`, 15 | 16 | keys: ` 17 | select id, key, type, version, etime, mtime from rkey 18 | where key like $1 and (etime is null or etime > $2)`, 19 | 20 | scan: ` 21 | select id, key, type, version, etime, mtime from rkey 22 | where 23 | id > $1 and key like $2 and (type = $3 or $4) 24 | and (etime is null or etime > $5) 25 | order by id asc 26 | limit $6`, 27 | } 28 | 29 | func init() { 30 | postgres.count = sqlite.count 31 | postgres.delete = sqlite.delete 32 | // postgres.deleteAll = sqlite.deleteAll 33 | postgres.deleteAllExpired = sqlite.deleteAllExpired 34 | // postgres.deleteNExpired = sqlite.deleteNExpired 35 | postgres.expire = sqlite.expire 36 | postgres.get = sqlite.get 37 | // postgres.keys = sqlite.keys 38 | postgres.len = sqlite.len 39 | postgres.persist = sqlite.persist 40 | postgres.random = sqlite.random 41 | postgres.rename1 = sqlite.rename1 42 | postgres.rename2 = sqlite.rename2 43 | // postgres.scan = sqlite.scan 44 | } 45 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zrank.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/parser" 6 | "github.com/nalgeon/redka/redsrv/internal/redis" 7 | ) 8 | 9 | // Returns the index of a member in a sorted set ordered by ascending scores. 10 | // ZRANK key member [WITHSCORE] 11 | // https://redis.io/commands/zrank 12 | type ZRank struct { 13 | redis.BaseCmd 14 | key string 15 | member string 16 | withScore bool 17 | } 18 | 19 | func ParseZRank(b redis.BaseCmd) (ZRank, error) { 20 | cmd := ZRank{BaseCmd: b} 21 | err := parser.New( 22 | parser.String(&cmd.key), 23 | parser.String(&cmd.member), 24 | parser.Flag("withscore", &cmd.withScore), 25 | ).Required(2).Run(cmd.Args()) 26 | if err != nil { 27 | return ZRank{}, err 28 | } 29 | return cmd, nil 30 | } 31 | 32 | func (cmd ZRank) Run(w redis.Writer, red redis.Redka) (any, error) { 33 | rank, score, err := red.ZSet().GetRank(cmd.key, cmd.member) 34 | if err == core.ErrNotFound { 35 | w.WriteNull() 36 | return nil, nil 37 | } 38 | if err != nil { 39 | w.WriteError(cmd.Error(err)) 40 | return nil, err 41 | } 42 | if cmd.withScore { 43 | w.WriteArray(2) 44 | w.WriteInt(rank) 45 | redis.WriteFloat(w, score) 46 | return rank, nil 47 | } 48 | w.WriteInt(rank) 49 | return rank, nil 50 | } 51 | -------------------------------------------------------------------------------- /redsrv/internal/command/set/sscan.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Iterates over members of a set. 9 | // SSCAN key cursor [MATCH pattern] [COUNT count] 10 | // https://redis.io/commands/sscan 11 | type SScan struct { 12 | redis.BaseCmd 13 | key string 14 | cursor int 15 | match string 16 | count int 17 | } 18 | 19 | func ParseSScan(b redis.BaseCmd) (SScan, error) { 20 | cmd := SScan{BaseCmd: b} 21 | 22 | err := parser.New( 23 | parser.String(&cmd.key), 24 | parser.Int(&cmd.cursor), 25 | parser.Named("match", parser.String(&cmd.match)), 26 | parser.Named("count", parser.Int(&cmd.count)), 27 | ).Required(2).Run(cmd.Args()) 28 | if err != nil { 29 | return SScan{}, err 30 | } 31 | 32 | // all elements by default 33 | if cmd.match == "" { 34 | cmd.match = "*" 35 | } 36 | 37 | return cmd, nil 38 | } 39 | 40 | func (cmd SScan) Run(w redis.Writer, red redis.Redka) (any, error) { 41 | res, err := red.Set().Scan(cmd.key, cmd.cursor, cmd.match, cmd.count) 42 | if err != nil { 43 | w.WriteError(cmd.Error(err)) 44 | return nil, err 45 | } 46 | 47 | w.WriteArray(2) 48 | w.WriteInt(res.Cursor) 49 | w.WriteArray(len(res.Items)) 50 | for _, val := range res.Items { 51 | w.WriteBulk(val) 52 | } 53 | return res, nil 54 | } 55 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zrevrank.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/parser" 6 | "github.com/nalgeon/redka/redsrv/internal/redis" 7 | ) 8 | 9 | // Returns the index of a member in a sorted set ordered by descending scores. 10 | // ZREVRANK key member [WITHSCORE] 11 | // https://redis.io/commands/zrevrank 12 | type ZRevRank struct { 13 | redis.BaseCmd 14 | key string 15 | member string 16 | withScore bool 17 | } 18 | 19 | func ParseZRevRank(b redis.BaseCmd) (ZRevRank, error) { 20 | cmd := ZRevRank{BaseCmd: b} 21 | err := parser.New( 22 | parser.String(&cmd.key), 23 | parser.String(&cmd.member), 24 | parser.Flag("withscore", &cmd.withScore), 25 | ).Required(2).Run(cmd.Args()) 26 | if err != nil { 27 | return ZRevRank{}, err 28 | } 29 | return cmd, nil 30 | } 31 | 32 | func (cmd ZRevRank) Run(w redis.Writer, red redis.Redka) (any, error) { 33 | rank, score, err := red.ZSet().GetRankRev(cmd.key, cmd.member) 34 | if err == core.ErrNotFound { 35 | w.WriteNull() 36 | return nil, nil 37 | } 38 | if err != nil { 39 | w.WriteError(cmd.Error(err)) 40 | return nil, err 41 | } 42 | if cmd.withScore { 43 | w.WriteArray(2) 44 | w.WriteInt(rank) 45 | redis.WriteFloat(w, score) 46 | return rank, nil 47 | } 48 | w.WriteInt(rank) 49 | return rank, nil 50 | } 51 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/mget.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Atomically returns the string values of one or more keys. 9 | // MGET key [key ...] 10 | // https://redis.io/commands/mget 11 | type MGet struct { 12 | redis.BaseCmd 13 | keys []string 14 | } 15 | 16 | func ParseMGet(b redis.BaseCmd) (MGet, error) { 17 | cmd := MGet{BaseCmd: b} 18 | if len(cmd.Args()) < 1 { 19 | return MGet{}, redis.ErrInvalidArgNum 20 | } 21 | cmd.keys = make([]string, len(cmd.Args())) 22 | for i, arg := range cmd.Args() { 23 | cmd.keys[i] = string(arg) 24 | } 25 | return cmd, nil 26 | } 27 | 28 | func (cmd MGet) Run(w redis.Writer, red redis.Redka) (any, error) { 29 | // Get the key-value map for requested keys. 30 | items, err := red.Str().GetMany(cmd.keys...) 31 | if err != nil { 32 | w.WriteError(cmd.Error(err)) 33 | return nil, err 34 | } 35 | 36 | // Write the result. 37 | // It will contain all values in the order of keys. 38 | // Missing keys will have nil values. 39 | w.WriteArray(len(cmd.keys)) 40 | vals := make([]core.Value, len(cmd.keys)) 41 | for i, key := range cmd.keys { 42 | v, ok := items[key] 43 | vals[i] = v 44 | if ok { 45 | w.WriteBulk(v.Bytes()) 46 | } else { 47 | w.WriteNull() 48 | } 49 | } 50 | 51 | return vals, nil 52 | } 53 | -------------------------------------------------------------------------------- /redsrv/debug.go: -------------------------------------------------------------------------------- 1 | package redsrv 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "net" 7 | "net/http" 8 | "net/http/pprof" 9 | "strconv" 10 | ) 11 | 12 | // DebugServer is a debug server with pprof endpoints. 13 | type DebugServer struct { 14 | srv *http.Server 15 | } 16 | 17 | // NewDebug creates a new debug server. 18 | func NewDebug(host string, port int) *DebugServer { 19 | mux := http.NewServeMux() 20 | mux.HandleFunc("/debug/pprof/", pprof.Index) 21 | mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 22 | mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 23 | mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 24 | mux.HandleFunc("/debug/pprof/trace", pprof.Trace) 25 | return &DebugServer{ 26 | srv: &http.Server{ 27 | Addr: net.JoinHostPort(host, strconv.Itoa(port)), 28 | Handler: mux, 29 | }, 30 | } 31 | } 32 | 33 | // Start starts the debug server. 34 | func (s *DebugServer) Start() error { 35 | slog.Info("starting debug server", "addr", s.srv.Addr) 36 | err := s.srv.ListenAndServe() 37 | if err != nil && err != http.ErrServerClosed { 38 | return fmt.Errorf("serve: %w", err) 39 | } 40 | return nil 41 | } 42 | 43 | // Stop stops the debug server. 44 | func (s *DebugServer) Stop() error { 45 | err := s.srv.Close() 46 | if err != nil { 47 | return fmt.Errorf("close: %w", err) 48 | } 49 | slog.Debug("debug server stopped", "addr", s.srv.Addr) 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /docs/install-standalone.md: -------------------------------------------------------------------------------- 1 | # Installing Redka as a standalone server 2 | 3 | Redka server is a single-file binary. Download it from the [releases](https://github.com/nalgeon/redka/releases/latest). 4 | 5 | Linux (x86 CPU only): 6 | 7 | ```shell 8 | curl -L -O "https://github.com/nalgeon/redka/releases/latest/download/redka_linux_amd64.zip" 9 | unzip redka_linux_amd64.zip 10 | chmod +x redka 11 | ``` 12 | 13 | macOS (x86 CPU): 14 | 15 | ```shell 16 | curl -L -O "https://github.com/nalgeon/redka/releases/latest/download/redka_darwin_amd64.zip" 17 | unzip redka_darwin_amd64.zip 18 | # remove the build from quarantine 19 | # (macOS disables unsigned binaries) 20 | xattr -d com.apple.quarantine redka 21 | chmod +x redka 22 | ``` 23 | 24 | macOS (ARM/Apple Silicon CPU): 25 | 26 | ```shell 27 | curl -L -O "https://github.com/nalgeon/redka/releases/latest/download/redka_darwin_arm64.zip" 28 | unzip redka_darwin_arm64.zip 29 | # remove the build from quarantine 30 | # (macOS disables unsigned binaries) 31 | xattr -d com.apple.quarantine redka 32 | chmod +x redka 33 | ``` 34 | 35 | Or pull with Docker as follows (x86/ARM): 36 | 37 | ```shell 38 | docker pull nalgeon/redka 39 | ``` 40 | 41 | Or build from source (requires Go 1.23+ and GCC): 42 | 43 | ```shell 44 | git clone https://github.com/nalgeon/redka.git 45 | cd redka 46 | make setup build 47 | # the path to the binary after the build 48 | # will be ./build/redka 49 | ``` 50 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hscan.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Iterates over fields and values of a hash. 9 | // HSCAN key cursor [MATCH pattern] [COUNT count] 10 | // https://redis.io/commands/hscan 11 | type HScan struct { 12 | redis.BaseCmd 13 | key string 14 | cursor int 15 | match string 16 | count int 17 | } 18 | 19 | func ParseHScan(b redis.BaseCmd) (HScan, error) { 20 | cmd := HScan{BaseCmd: b} 21 | 22 | err := parser.New( 23 | parser.String(&cmd.key), 24 | parser.Int(&cmd.cursor), 25 | parser.Named("match", parser.String(&cmd.match)), 26 | parser.Named("count", parser.Int(&cmd.count)), 27 | ).Required(2).Run(cmd.Args()) 28 | if err != nil { 29 | return HScan{}, err 30 | } 31 | 32 | // all keys by default 33 | if cmd.match == "" { 34 | cmd.match = "*" 35 | } 36 | 37 | return cmd, nil 38 | } 39 | 40 | func (cmd HScan) Run(w redis.Writer, red redis.Redka) (any, error) { 41 | res, err := red.Hash().Scan(cmd.key, cmd.cursor, cmd.match, cmd.count) 42 | if err != nil { 43 | w.WriteError(cmd.Error(err)) 44 | return nil, err 45 | } 46 | 47 | w.WriteArray(2) 48 | w.WriteInt(res.Cursor) 49 | w.WriteArray(len(res.Items) * 2) 50 | for _, it := range res.Items { 51 | w.WriteBulkString(it.Field) 52 | w.WriteBulk(it.Value) 53 | } 54 | return res, nil 55 | } 56 | -------------------------------------------------------------------------------- /redsrv/internal/command/server/dbsize_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func TestDBSizeParse(t *testing.T) { 11 | tests := []struct { 12 | cmd string 13 | err error 14 | }{ 15 | { 16 | cmd: "dbsize", 17 | err: nil, 18 | }, 19 | { 20 | cmd: "dbsize name", 21 | err: redis.ErrInvalidArgNum, 22 | }, 23 | } 24 | 25 | for _, test := range tests { 26 | t.Run(test.cmd, func(t *testing.T) { 27 | cmd, err := redis.Parse(ParseDBSize, test.cmd) 28 | be.Equal(t, err, test.err) 29 | if err != nil { 30 | be.Equal(t, cmd, DBSize{}) 31 | } 32 | }) 33 | } 34 | } 35 | 36 | func TestDBSizeExec(t *testing.T) { 37 | t.Run("dbsize", func(t *testing.T) { 38 | red := getRedka(t) 39 | _ = red.Str().Set("name", "alice") 40 | _ = red.Str().Set("age", 25) 41 | 42 | cmd := redis.MustParse(ParseDBSize, "dbsize") 43 | conn := redis.NewFakeConn() 44 | res, err := cmd.Run(conn, red) 45 | be.Err(t, err, nil) 46 | be.Equal(t, res, 2) 47 | be.Equal(t, conn.Out(), "2") 48 | }) 49 | 50 | t.Run("empty", func(t *testing.T) { 51 | red := getRedka(t) 52 | 53 | cmd := redis.MustParse(ParseDBSize, "dbsize") 54 | conn := redis.NewFakeConn() 55 | res, err := cmd.Run(conn, red) 56 | be.Err(t, err, nil) 57 | be.Equal(t, res, 0) 58 | be.Equal(t, conn.Out(), "0") 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /redsrv/internal/command/server/lolwut_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/nalgeon/be" 8 | "github.com/nalgeon/redka/redsrv/internal/redis" 9 | ) 10 | 11 | func TestLolwutParse(t *testing.T) { 12 | tests := []struct { 13 | cmd string 14 | err error 15 | }{ 16 | { 17 | cmd: "lolwut", 18 | err: nil, 19 | }, 20 | { 21 | cmd: "lolwut you ok?", 22 | err: nil, 23 | }, 24 | { 25 | cmd: "lolwut is redis cool?", 26 | err: nil, 27 | }, 28 | } 29 | 30 | for _, test := range tests { 31 | t.Run(test.cmd, func(t *testing.T) { 32 | cmd, err := redis.Parse(ParseLolwut, test.cmd) 33 | be.Equal(t, err, test.err) 34 | if err != nil { 35 | be.Equal(t, cmd, Lolwut{}) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestLolwutExec(t *testing.T) { 42 | t.Run("lolwut", func(t *testing.T) { 43 | red := getRedka(t) 44 | 45 | cmd := redis.MustParse(ParseLolwut, "lolwut you ok?") 46 | conn := redis.NewFakeConn() 47 | _, err := cmd.Run(conn, red) 48 | be.Err(t, err, nil) 49 | be.Equal(t, len(conn.Out()) >= 3, true) 50 | }) 51 | 52 | t.Run("empty", func(t *testing.T) { 53 | red := getRedka(t) 54 | 55 | cmd := redis.MustParse(ParseLolwut, "lolwut") 56 | conn := redis.NewFakeConn() 57 | _, err := cmd.Run(conn, red) 58 | be.Err(t, err, nil) 59 | be.Equal(t, strings.HasPrefix(conn.Out(), "Ask me a question"), true) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /docs/commands/hashes.md: -------------------------------------------------------------------------------- 1 | # Hashes 2 | 3 | Hashes are field-value (hash)maps. Redka supports the following hash-related commands: 4 | 5 | ``` 6 | Command Go API Description 7 | ------- ------------------ ----------- 8 | HDEL DB.Hash().Delete Deletes one or more fields and their values. 9 | HEXISTS DB.Hash().Exists Determines whether a field exists. 10 | HGET DB.Hash().Get Returns the value of a field. 11 | HGETALL DB.Hash().Items Returns all fields and values. 12 | HINCRBY DB.Hash().Incr Increments the integer value of a field. 13 | HINCRBYFLOAT DB.Hash().IncrFloat Increments the float value of a field. 14 | HKEYS DB.Hash().Keys Returns all fields. 15 | HLEN DB.Hash().Len Returns the number of fields. 16 | HMGET DB.Hash().GetMany Returns the values of multiple fields. 17 | HMSET DB.Hash().SetMany Sets the values of multiple fields. 18 | HSCAN DB.Hash().Scanner Iterates over fields and values. 19 | HSET DB.Hash().SetMany Sets the values of one or more fields. 20 | HSETNX DB.Hash().SetNotExists Sets the value of a field when it doesn't exist. 21 | HVALS DB.Hash().Exists Returns all values. 22 | ``` 23 | 24 | The following hash-related commands are not planned for 1.0: 25 | 26 | ``` 27 | HRANDFIELD HSTRLEN 28 | ``` 29 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zscan.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Iterates over members and scores of a sorted set. 9 | // ZSCAN key cursor [MATCH pattern] [COUNT count] 10 | // https://redis.io/commands/zscan 11 | type ZScan struct { 12 | redis.BaseCmd 13 | key string 14 | cursor int 15 | match string 16 | count int 17 | } 18 | 19 | func ParseZScan(b redis.BaseCmd) (ZScan, error) { 20 | cmd := ZScan{BaseCmd: b} 21 | 22 | err := parser.New( 23 | parser.String(&cmd.key), 24 | parser.Int(&cmd.cursor), 25 | parser.Named("match", parser.String(&cmd.match)), 26 | parser.Named("count", parser.Int(&cmd.count)), 27 | ).Required(2).Run(cmd.Args()) 28 | if err != nil { 29 | return ZScan{}, err 30 | } 31 | 32 | // all elements by default 33 | if cmd.match == "" { 34 | cmd.match = "*" 35 | } 36 | 37 | return cmd, nil 38 | } 39 | 40 | func (cmd ZScan) Run(w redis.Writer, red redis.Redka) (any, error) { 41 | res, err := red.ZSet().Scan(cmd.key, cmd.cursor, cmd.match, cmd.count) 42 | if err != nil { 43 | w.WriteError(cmd.Error(err)) 44 | return nil, err 45 | } 46 | 47 | w.WriteArray(2) 48 | w.WriteInt(res.Cursor) 49 | w.WriteArray(len(res.Items) * 2) 50 | for _, it := range res.Items { 51 | w.WriteBulk(it.Elem) 52 | redis.WriteFloat(w, it.Score) 53 | } 54 | return res, nil 55 | } 56 | -------------------------------------------------------------------------------- /docs/commands/lists.md: -------------------------------------------------------------------------------- 1 | # Lists 2 | 3 | Lists are sequences of strings sorted by insertion order. Redka supports the following list-related commands: 4 | 5 | ``` 6 | Command Go API Description 7 | ------- ------ ----------- 8 | LINDEX DB.List().Get Returns an element by its index. 9 | LINSERT DB.List().Insert* Inserts an element before or after another element. 10 | LLEN DB.List().Len Returns the length of a list. 11 | LPOP DB.List().PopFront Returns the first element after removing it. 12 | LPUSH DB.List().PushFront Prepends an element to a list. 13 | LRANGE DB.List().Range Returns a range of elements. 14 | LREM DB.List().Delete* Removes elements from a list. 15 | LSET DB.List().Set Sets the value of an element by its index. 16 | LTRIM DB.List().Trim Removes elements from both ends a list. 17 | RPOP DB.List().PopBack Returns the last element after removing it. 18 | RPOPLPUSH DB.List().PopBackPushFront Removes the last element and pushes it to another list. 19 | RPUSH DB.List().PushBack Appends an element to a list. 20 | ``` 21 | 22 | The following list-related commands are not planned for 1.0: 23 | 24 | ``` 25 | BLMOVE BLMPOP BLPOP BRPOP BRPOPLPUSH LMOVE LMPOP 26 | LPOS LPUSHX RPUSHX 27 | ``` 28 | -------------------------------------------------------------------------------- /example/tx/main.go: -------------------------------------------------------------------------------- 1 | // An example of using Redka with transactions. 2 | package main 3 | 4 | import ( 5 | "log" 6 | "log/slog" 7 | 8 | _ "github.com/mattn/go-sqlite3" 9 | "github.com/nalgeon/redka" 10 | ) 11 | 12 | func main() { 13 | // Open the database. 14 | db, err := redka.Open("redka.db", nil) 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | defer func() { _ = db.Close() }() 19 | 20 | { 21 | // Writable transaction. 22 | updCount := 0 23 | err := db.Update(func(tx *redka.Tx) error { 24 | err := tx.Str().Set("name", "alice") 25 | if err != nil { 26 | return err 27 | } 28 | updCount++ 29 | 30 | err = tx.Str().Set("age", 25) 31 | if err != nil { 32 | return err 33 | } 34 | updCount++ 35 | 36 | return nil 37 | }) 38 | slog.Info("updated", "count", updCount, "err", err) 39 | } 40 | 41 | { 42 | // Read-only transaction. 43 | type person struct { 44 | name string 45 | age int 46 | } 47 | 48 | var p person 49 | err := db.View(func(tx *redka.Tx) error { 50 | name, err := tx.Str().Get("name") 51 | if err != nil { 52 | return err 53 | } 54 | p.name = name.String() 55 | 56 | age, err := tx.Str().Get("age") 57 | if err != nil { 58 | return err 59 | } 60 | // Only use MustInt() if you are sure that 61 | // the key exists and is an integer. 62 | p.age = age.MustInt() 63 | return nil 64 | }) 65 | slog.Info("get", "person", p, "err", err) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /redsrv/internal/command/server/config_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func TestConfigParse(t *testing.T) { 11 | tests := []struct { 12 | cmd string 13 | want Config 14 | err error 15 | }{ 16 | { 17 | cmd: "config", 18 | want: Config{}, 19 | err: redis.ErrInvalidArgNum, 20 | }, 21 | { 22 | cmd: "config get", 23 | want: Config{}, 24 | err: redis.ErrInvalidArgNum, 25 | }, 26 | { 27 | cmd: "config get *", 28 | want: Config{subcmd: "get"}, 29 | err: nil, 30 | }, 31 | { 32 | cmd: "config set parameter value", 33 | want: Config{}, 34 | err: redis.ErrUnknownSubcmd, 35 | }, 36 | } 37 | 38 | for _, test := range tests { 39 | t.Run(test.cmd, func(t *testing.T) { 40 | cmd, err := redis.Parse(ParseConfig, test.cmd) 41 | be.Equal(t, err, test.err) 42 | if err == nil { 43 | be.Equal(t, cmd.subcmd, test.want.subcmd) 44 | } else { 45 | be.Equal(t, cmd, test.want) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestConfigExec(t *testing.T) { 52 | t.Run("config get", func(t *testing.T) { 53 | red := getRedka(t) 54 | 55 | cmd := redis.MustParse(ParseConfig, "config get *") 56 | conn := redis.NewFakeConn() 57 | res, err := cmd.Run(conn, red) 58 | be.Err(t, err, nil) 59 | be.Equal(t, res, true) 60 | be.Equal(t, conn.Out(), "2,databases,1") 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hmget.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/parser" 6 | "github.com/nalgeon/redka/redsrv/internal/redis" 7 | ) 8 | 9 | // Returns the values of multiple fields in a hash. 10 | // HMGET key field [field ...] 11 | // https://redis.io/commands/hmget 12 | type HMGet struct { 13 | redis.BaseCmd 14 | key string 15 | fields []string 16 | } 17 | 18 | func ParseHMGet(b redis.BaseCmd) (HMGet, error) { 19 | cmd := HMGet{BaseCmd: b} 20 | err := parser.New( 21 | parser.String(&cmd.key), 22 | parser.Strings(&cmd.fields), 23 | ).Required(2).Run(cmd.Args()) 24 | if err != nil { 25 | return HMGet{}, err 26 | } 27 | return cmd, nil 28 | } 29 | 30 | func (cmd HMGet) Run(w redis.Writer, red redis.Redka) (any, error) { 31 | // Get the field-value map for requested fields. 32 | items, err := red.Hash().GetMany(cmd.key, cmd.fields...) 33 | if err != nil { 34 | w.WriteError(cmd.Error(err)) 35 | return nil, err 36 | } 37 | 38 | // Write the result. 39 | // It will contain all values in the order of fields. 40 | // Missing fields will have nil values. 41 | w.WriteArray(len(cmd.fields)) 42 | vals := make([]core.Value, len(cmd.fields)) 43 | for i, field := range cmd.fields { 44 | v, ok := items[field] 45 | vals[i] = v 46 | if ok { 47 | w.WriteBulk(v.Bytes()) 48 | } else { 49 | w.WriteNull() 50 | } 51 | } 52 | 53 | return vals, nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/rstring/sqlite.go: -------------------------------------------------------------------------------- 1 | package rstring 2 | 3 | // SQLite queries for the string repository. 4 | var sqlite = queries{ 5 | get: ` 6 | select value 7 | from rstring join rkey on kid = rkey.id and type = 1 8 | where key = $1 and (etime is null or etime > $2)`, 9 | 10 | getMany: ` 11 | select key, value 12 | from rstring 13 | join rkey on kid = rkey.id and type = 1 14 | where key in (:keys) and (etime is null or etime > ?)`, 15 | 16 | set1: ` 17 | insert into 18 | rkey (key, type, version, etime, mtime) 19 | values ( $1, 1, 1, $2, $3) 20 | on conflict (key) do update set 21 | type = case when rkey.type = excluded.type then rkey.type else null end, 22 | version = rkey.version + 1, 23 | etime = excluded.etime, 24 | mtime = excluded.mtime`, 25 | 26 | set2: ` 27 | insert into rstring (kid, value) 28 | values ((select id from rkey where key = $1), $2) 29 | on conflict (kid) do update 30 | set value = excluded.value`, 31 | 32 | update1: ` 33 | insert into 34 | rkey (key, type, version, etime, mtime) 35 | values ( $1, 1, 1, null, $2) 36 | on conflict (key) do update set 37 | type = case when rkey.type = excluded.type then rkey.type else null end, 38 | version = rkey.version + 1, 39 | mtime = excluded.mtime`, 40 | 41 | // Same as set2. 42 | update2: ` 43 | insert into rstring (kid, value) 44 | values ((select id from rkey where key = $1), $2) 45 | on conflict (kid) do update 46 | set value = excluded.value`, 47 | } 48 | -------------------------------------------------------------------------------- /redsrv/internal/command/list/linsert.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/parser" 6 | "github.com/nalgeon/redka/redsrv/internal/redis" 7 | ) 8 | 9 | const ( 10 | Before = "before" 11 | After = "after" 12 | ) 13 | 14 | // Inserts an element before or after another element in a list. 15 | // LINSERT key pivot element 16 | // https://redis.io/commands/linsert 17 | type LInsert struct { 18 | redis.BaseCmd 19 | key string 20 | where string 21 | pivot []byte 22 | elem []byte 23 | } 24 | 25 | func ParseLInsert(b redis.BaseCmd) (LInsert, error) { 26 | cmd := LInsert{BaseCmd: b} 27 | err := parser.New( 28 | parser.String(&cmd.key), 29 | parser.Enum(&cmd.where, Before, After), 30 | parser.Bytes(&cmd.pivot), 31 | parser.Bytes(&cmd.elem), 32 | ).Required(4).Run(cmd.Args()) 33 | if err != nil { 34 | return LInsert{}, err 35 | } 36 | return cmd, nil 37 | } 38 | 39 | func (cmd LInsert) Run(w redis.Writer, red redis.Redka) (any, error) { 40 | var n int 41 | var err error 42 | if cmd.where == Before { 43 | n, err = red.List().InsertBefore(cmd.key, cmd.pivot, cmd.elem) 44 | } else { 45 | n, err = red.List().InsertAfter(cmd.key, cmd.pivot, cmd.elem) 46 | } 47 | if err == core.ErrNotFound { 48 | w.WriteInt(n) 49 | return n, nil 50 | } 51 | if err != nil { 52 | w.WriteError(cmd.Error(err)) 53 | return nil, err 54 | } 55 | w.WriteInt(n) 56 | return n, nil 57 | } 58 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zinterstore.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/sqlx" 5 | "github.com/nalgeon/redka/redsrv/internal/parser" 6 | "github.com/nalgeon/redka/redsrv/internal/redis" 7 | ) 8 | 9 | // Stores the intersect of multiple sorted sets in a key. 10 | // ZINTERSTORE dest numkeys key [key ...] [AGGREGATE ] 11 | // https://redis.io/commands/zinterstore 12 | type ZInterStore struct { 13 | redis.BaseCmd 14 | dest string 15 | keys []string 16 | aggregate string 17 | } 18 | 19 | func ParseZInterStore(b redis.BaseCmd) (ZInterStore, error) { 20 | cmd := ZInterStore{BaseCmd: b} 21 | var nKeys int 22 | err := parser.New( 23 | parser.String(&cmd.dest), 24 | parser.Int(&nKeys), 25 | parser.StringsN(&cmd.keys, &nKeys), 26 | parser.Named("aggregate", parser.Enum(&cmd.aggregate, sqlx.Sum, sqlx.Min, sqlx.Max)), 27 | ).Required(3).Run(cmd.Args()) 28 | if err != nil { 29 | return ZInterStore{}, err 30 | } 31 | return cmd, nil 32 | } 33 | 34 | func (cmd ZInterStore) Run(w redis.Writer, red redis.Redka) (any, error) { 35 | inter := red.ZSet().InterWith(cmd.keys...).Dest(cmd.dest) 36 | switch cmd.aggregate { 37 | case sqlx.Min: 38 | inter = inter.Min() 39 | case sqlx.Max: 40 | inter = inter.Max() 41 | case sqlx.Sum: 42 | inter = inter.Sum() 43 | } 44 | 45 | count, err := inter.Store() 46 | if err != nil { 47 | w.WriteError(cmd.Error(err)) 48 | return nil, err 49 | } 50 | 51 | w.WriteInt(count) 52 | return count, nil 53 | } 54 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zunionstore.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/sqlx" 5 | "github.com/nalgeon/redka/redsrv/internal/parser" 6 | "github.com/nalgeon/redka/redsrv/internal/redis" 7 | ) 8 | 9 | // Stores the union of multiple sorted sets in a key. 10 | // ZUNIONSTORE dest numkeys key [key ...] [AGGREGATE ] 11 | // https://redis.io/commands/zunionstore 12 | type ZUnionStore struct { 13 | redis.BaseCmd 14 | dest string 15 | keys []string 16 | aggregate string 17 | } 18 | 19 | func ParseZUnionStore(b redis.BaseCmd) (ZUnionStore, error) { 20 | cmd := ZUnionStore{BaseCmd: b} 21 | var nKeys int 22 | err := parser.New( 23 | parser.String(&cmd.dest), 24 | parser.Int(&nKeys), 25 | parser.StringsN(&cmd.keys, &nKeys), 26 | parser.Named("aggregate", parser.Enum(&cmd.aggregate, sqlx.Sum, sqlx.Min, sqlx.Max)), 27 | ).Required(3).Run(cmd.Args()) 28 | if err != nil { 29 | return ZUnionStore{}, err 30 | } 31 | return cmd, nil 32 | } 33 | 34 | func (cmd ZUnionStore) Run(w redis.Writer, red redis.Redka) (any, error) { 35 | union := red.ZSet().UnionWith(cmd.keys...).Dest(cmd.dest) 36 | switch cmd.aggregate { 37 | case sqlx.Min: 38 | union = union.Min() 39 | case sqlx.Max: 40 | union = union.Max() 41 | case sqlx.Sum: 42 | union = union.Sum() 43 | } 44 | 45 | count, err := union.Store() 46 | if err != nil { 47 | w.WriteError(cmd.Error(err)) 48 | return nil, err 49 | } 50 | 51 | w.WriteInt(count) 52 | return count, nil 53 | } 54 | -------------------------------------------------------------------------------- /redsrv/internal/command/conn/select_test.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func TestSelectParse(t *testing.T) { 11 | tests := []struct { 12 | cmd string 13 | want Select 14 | err error 15 | }{ 16 | { 17 | cmd: "select", 18 | want: Select{}, 19 | err: redis.ErrInvalidArgNum, 20 | }, 21 | { 22 | cmd: "select 5", 23 | want: Select{index: 5}, 24 | err: nil, 25 | }, 26 | { 27 | cmd: "select five", 28 | want: Select{}, 29 | err: redis.ErrInvalidInt, 30 | }, 31 | } 32 | 33 | for _, test := range tests { 34 | t.Run(test.cmd, func(t *testing.T) { 35 | cmd, err := redis.Parse(ParseSelect, test.cmd) 36 | be.Equal(t, err, test.err) 37 | if err == nil { 38 | be.Equal(t, cmd.index, test.want.index) 39 | } else { 40 | be.Equal(t, cmd, Select{}) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func TestSelectExec(t *testing.T) { 47 | red := getRedka(t) 48 | 49 | tests := []struct { 50 | cmd string 51 | res any 52 | out string 53 | }{ 54 | { 55 | cmd: "select 5", 56 | res: true, 57 | out: "OK", 58 | }, 59 | } 60 | 61 | for _, test := range tests { 62 | t.Run(test.cmd, func(t *testing.T) { 63 | conn := redis.NewFakeConn() 64 | cmd := redis.MustParse(ParseSelect, test.cmd) 65 | res, err := cmd.Run(conn, red) 66 | be.Err(t, err, nil) 67 | be.Equal(t, res, test.res) 68 | be.Equal(t, conn.Out(), test.out) 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zrevrange.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Returns members in a sorted set within a range of indexes in reverse order. 9 | // ZREVRANGE key start stop [WITHSCORES] 10 | // https://redis.io/commands/zrevrange 11 | type ZRevRange struct { 12 | redis.BaseCmd 13 | key string 14 | start int 15 | stop int 16 | withScores bool 17 | } 18 | 19 | func ParseZRevRange(b redis.BaseCmd) (ZRevRange, error) { 20 | cmd := ZRevRange{BaseCmd: b} 21 | err := parser.New( 22 | parser.String(&cmd.key), 23 | parser.Int(&cmd.start), 24 | parser.Int(&cmd.stop), 25 | parser.Flag("withscores", &cmd.withScores), 26 | ).Required(3).Run(cmd.Args()) 27 | if err != nil { 28 | return ZRevRange{}, err 29 | } 30 | return cmd, nil 31 | } 32 | 33 | func (cmd ZRevRange) Run(w redis.Writer, red redis.Redka) (any, error) { 34 | items, err := red.ZSet().RangeWith(cmd.key).ByRank(cmd.start, cmd.stop).Desc().Run() 35 | if err != nil { 36 | w.WriteError(cmd.Error(err)) 37 | return nil, err 38 | } 39 | 40 | // write the response with/without scores 41 | if cmd.withScores { 42 | w.WriteArray(len(items) * 2) 43 | for _, item := range items { 44 | w.WriteBulk(item.Elem) 45 | redis.WriteFloat(w, item.Score) 46 | } 47 | } else { 48 | w.WriteArray(len(items)) 49 | for _, item := range items { 50 | w.WriteBulk(item.Elem) 51 | } 52 | } 53 | 54 | return items, nil 55 | } 56 | -------------------------------------------------------------------------------- /docs/commands/sets.md: -------------------------------------------------------------------------------- 1 | # Sets 2 | 3 | Sets are unordered collections of unique strings. Redka supports the following set-related commands: 4 | 5 | ``` 6 | Command Go API Description 7 | ------- ------ ----------- 8 | SADD DB.Set().Add Adds one or more members to a set. 9 | SCARD DB.Set().Len Returns the number of members in a set. 10 | SDIFF DB.Set().Diff Returns the difference of multiple sets. 11 | SDIFFSTORE DB.Set().DiffStore Stores the difference of multiple sets. 12 | SINTER DB.Set().Inter Returns the intersection of multiple sets. 13 | SINTERSTORE DB.Set().InterStore Stores the intersection of multiple sets. 14 | SISMEMBER DB.Set().Exists Determines whether a member belongs to a set. 15 | SMEMBERS DB.Set().Items Returns all members of a set. 16 | SMOVE DB.Set().Move Moves a member from one set to another. 17 | SPOP DB.Set().Pop Returns a random member after removing it. 18 | SRANDMEMBER DB.Set().Random Returns a random member from a set. 19 | SREM DB.Set().Delete Removes one or more members from a set. 20 | SSCAN DB.Set().Scanner Iterates over members of a set. 21 | SUNION DB.Set().Union Returns the union of multiple sets. 22 | SUNIONSTORE DB.Set().UnionStore Stores the union of multiple sets. 23 | ``` 24 | 25 | The following set-related commands are not planned for 1.0: 26 | 27 | ``` 28 | SINTERCARD SMISMEMBER 29 | ``` 30 | -------------------------------------------------------------------------------- /redsrv/state.go: -------------------------------------------------------------------------------- 1 | package redsrv 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | "github.com/tidwall/redcon" 9 | ) 10 | 11 | // normName returns the normalized command name. 12 | func normName(cmd redcon.Command) string { 13 | return strings.ToLower(string(cmd.Args[0])) 14 | } 15 | 16 | // getState returns the connection state. 17 | func getState(conn redcon.Conn) *connState { 18 | state := conn.Context() 19 | if state == nil { 20 | state = new(connState) 21 | conn.SetContext(state) 22 | } 23 | return state.(*connState) 24 | } 25 | 26 | // connState represents the connection state. 27 | type connState struct { 28 | inMulti bool 29 | cmds []redis.Cmd 30 | } 31 | 32 | // push adds a command to the state. 33 | func (s *connState) push(cmd redis.Cmd) { 34 | s.cmds = append(s.cmds, cmd) 35 | } 36 | 37 | // pop removes the last command from the state and returns it. 38 | func (s *connState) pop() redis.Cmd { 39 | if len(s.cmds) == 0 { 40 | return nil 41 | } 42 | var last redis.Cmd 43 | s.cmds, last = s.cmds[:len(s.cmds)-1], s.cmds[len(s.cmds)-1] 44 | return last 45 | } 46 | 47 | // clear removes all commands from the state. 48 | func (s *connState) clear() { 49 | s.cmds = []redis.Cmd{} 50 | } 51 | 52 | // String returns the string representation of the state. 53 | func (s *connState) String() string { 54 | cmds := make([]string, len(s.cmds)) 55 | for i, cmd := range s.cmds { 56 | cmds[i] = cmd.Name() 57 | } 58 | return fmt.Sprintf("[inMulti=%v,commands=%v]", s.inMulti, cmds) 59 | } 60 | -------------------------------------------------------------------------------- /example/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nalgeon/redka/example 2 | 3 | replace github.com/nalgeon/redka => ../ 4 | 5 | go 1.23.0 6 | 7 | toolchain go1.24.0 8 | 9 | require ( 10 | github.com/lib/pq v1.10.9 11 | github.com/mattn/go-sqlite3 v1.14.28 12 | github.com/nalgeon/redka v0.0.0-00010101000000-000000000000 13 | github.com/ncruces/go-sqlite3 v0.16.2 14 | github.com/redis/go-redis/v9 v9.11.0 15 | modernc.org/sqlite v1.29.5 16 | ) 17 | 18 | require ( 19 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 20 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 21 | github.com/dustin/go-humanize v1.0.1 // indirect 22 | github.com/google/uuid v1.3.0 // indirect 23 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 24 | github.com/mattn/go-isatty v0.0.16 // indirect 25 | github.com/ncruces/go-strftime v0.1.9 // indirect 26 | github.com/ncruces/julianday v1.0.0 // indirect 27 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 28 | github.com/tetratelabs/wazero v1.7.3 // indirect 29 | github.com/tidwall/btree v1.7.0 // indirect 30 | github.com/tidwall/match v1.1.1 // indirect 31 | github.com/tidwall/redcon v1.6.2 // indirect 32 | golang.org/x/sys v0.21.0 // indirect 33 | golang.org/x/tools v0.19.0 // indirect 34 | modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect 35 | modernc.org/libc v1.41.0 // indirect 36 | modernc.org/mathutil v1.6.0 // indirect 37 | modernc.org/memory v1.7.2 // indirect 38 | modernc.org/strutil v1.2.0 // indirect 39 | modernc.org/token v1.1.0 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /redsrv/internal/command/conn/ping_test.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func TestPingParse(t *testing.T) { 11 | tests := []struct { 12 | cmd string 13 | want string 14 | err error 15 | }{ 16 | { 17 | cmd: "ping", 18 | want: "", 19 | err: nil, 20 | }, 21 | { 22 | cmd: "ping hello", 23 | want: "hello", 24 | err: nil, 25 | }, 26 | { 27 | cmd: "ping one two", 28 | want: "", 29 | err: redis.ErrSyntaxError, 30 | }, 31 | } 32 | 33 | for _, test := range tests { 34 | t.Run(test.cmd, func(t *testing.T) { 35 | cmd, err := redis.Parse(ParsePing, test.cmd) 36 | be.Equal(t, err, test.err) 37 | if err == nil { 38 | be.Equal(t, cmd.message, test.want) 39 | } else { 40 | be.Equal(t, cmd, Ping{}) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func TestPingExec(t *testing.T) { 47 | red := getRedka(t) 48 | 49 | tests := []struct { 50 | cmd string 51 | res any 52 | out string 53 | }{ 54 | { 55 | cmd: "ping", 56 | res: "PONG", 57 | out: "PONG", 58 | }, 59 | { 60 | cmd: "ping hello", 61 | res: "hello", 62 | out: "hello", 63 | }, 64 | } 65 | 66 | for _, test := range tests { 67 | t.Run(test.cmd, func(t *testing.T) { 68 | conn := redis.NewFakeConn() 69 | cmd := redis.MustParse(ParsePing, test.cmd) 70 | res, err := cmd.Run(conn, red) 71 | be.Err(t, err, nil) 72 | be.Equal(t, res, test.res) 73 | be.Equal(t, conn.Out(), test.out) 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /redsrv/internal/command/server/lolwut.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/nalgeon/redka/redsrv/internal/parser" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | var lolwutAnswers = []string{ 11 | // yes 12 | "As I see it, yes", 13 | "It is certain", 14 | "It is decidedly so", 15 | "Most likely", 16 | "Outlook good", 17 | "Signs point to yes", 18 | "Without a doubt", 19 | "Yes definitely", 20 | "Yes", 21 | "You may rely on it", 22 | // maybe 23 | "Ask again later", 24 | "Better not tell you now", 25 | "Cannot predict now", 26 | "Concentrate and ask again", 27 | "Reply hazy, try again", 28 | // no 29 | "Don't count on it", 30 | "My reply is no", 31 | "My sources say no", 32 | "Outlook not so good", 33 | "Very doubtful", 34 | } 35 | 36 | // Answers any question you throw at it 37 | // with magic ⋆。𖦹°⭒˚。⋆ 38 | // LOLWUT [question...] 39 | type Lolwut struct { 40 | redis.BaseCmd 41 | parts []string 42 | } 43 | 44 | func ParseLolwut(b redis.BaseCmd) (Lolwut, error) { 45 | cmd := Lolwut{BaseCmd: b} 46 | err := parser.New( 47 | parser.Strings(&cmd.parts), 48 | ).Required(0).Run(cmd.Args()) 49 | if err != nil { 50 | return Lolwut{}, err 51 | } 52 | return cmd, nil 53 | } 54 | 55 | func (c Lolwut) Run(w redis.Writer, _ redis.Redka) (any, error) { 56 | var answer string 57 | if len(c.parts) != 0 { 58 | answer = lolwutAnswers[rand.Intn(len(lolwutAnswers))] 59 | } else { 60 | answer = "Ask me a question (⊃。•́‿•̀。)⊃" 61 | } 62 | w.WriteBulkString(answer + "\n") 63 | return answer, nil 64 | } 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024, Anton Zhiyanov 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | * Neither the name of Redka nor the names of its contributors 14 | may be used to endorse or promote products derived from this software 15 | without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 21 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 22 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /docs/commands/strings.md: -------------------------------------------------------------------------------- 1 | # Strings 2 | 3 | Strings are the most basic Redis type, representing a sequence of bytes. Redka supports the following string-related commands: 4 | 5 | ``` 6 | Command Go API Description 7 | ------- ------ ----------- 8 | DECR DB.Str().Incr Decrements the integer value of a key by one. 9 | DECRBY DB.Str().Incr Decrements a number from the integer value of a key. 10 | GET DB.Str().Get Returns the value of a key. 11 | GETSET DB.Str().SetWith Sets the key to a new value and returns the prev value. 12 | INCR DB.Str().Incr Increments the integer value of a key by one. 13 | INCRBY DB.Str().Incr Increments the integer value of a key by a number. 14 | INCRBYFLOAT DB.Str().IncrFloat Increments the float value of a key by a number. 15 | MGET DB.Str().GetMany Returns the values of one or more keys. 16 | MSET DB.Str().SetMany Sets the values of one or more keys. 17 | PSETEX DB.Str().SetExpire Sets the value and expiration time (in ms) of a key. 18 | SET DB.Str().Set Sets the value of a key. 19 | SETEX DB.Str().SetExpire Sets the value and expiration (in sec) time of a key. 20 | SETNX DB.Str().SetWith Sets the value of a key when the key doesn't exist. 21 | STRLEN DB.Str().Get Returns the length of a value in bytes. 22 | ``` 23 | 24 | The following string-related commands are not planned for 1.0: 25 | 26 | ``` 27 | APPEND GETDEL GETEX GETRANGE LCS MSETNX SETRANGE SUBSTR 28 | ``` 29 | -------------------------------------------------------------------------------- /redsrv/internal/command/conn/echo_test.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func TestEchoParse(t *testing.T) { 11 | tests := []struct { 12 | cmd string 13 | args [][]byte 14 | want []string 15 | err error 16 | }{ 17 | { 18 | cmd: "echo", 19 | want: []string{}, 20 | err: redis.ErrInvalidArgNum, 21 | }, 22 | { 23 | cmd: "echo hello", 24 | want: []string{"hello"}, 25 | err: nil, 26 | }, 27 | { 28 | cmd: "echo one two", 29 | want: []string{"one", "two"}, 30 | err: nil, 31 | }, 32 | } 33 | 34 | for _, test := range tests { 35 | t.Run(test.cmd, func(t *testing.T) { 36 | cmd, err := redis.Parse(ParseEcho, test.cmd) 37 | be.Equal(t, err, test.err) 38 | if err == nil { 39 | be.Equal(t, cmd.parts, test.want) 40 | } else { 41 | be.Equal(t, cmd, Echo{}) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestEchoExec(t *testing.T) { 48 | red := getRedka(t) 49 | 50 | tests := []struct { 51 | cmd string 52 | res any 53 | out string 54 | }{ 55 | { 56 | cmd: "echo hello", 57 | res: "hello", 58 | out: "hello", 59 | }, 60 | { 61 | cmd: "echo one two", 62 | res: "one two", 63 | out: "one two", 64 | }, 65 | } 66 | 67 | for _, test := range tests { 68 | t.Run(test.cmd, func(t *testing.T) { 69 | conn := redis.NewFakeConn() 70 | cmd := redis.MustParse(ParseEcho, test.cmd) 71 | res, err := cmd.Run(conn, red) 72 | be.Err(t, err, nil) 73 | be.Equal(t, res, test.res) 74 | be.Equal(t, conn.Out(), test.out) 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/flushdb_test.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func TestFlushDBParse(t *testing.T) { 11 | tests := []struct { 12 | cmd string 13 | err error 14 | }{ 15 | { 16 | cmd: "flushdb", 17 | err: nil, 18 | }, 19 | { 20 | cmd: "flushdb name", 21 | err: redis.ErrSyntaxError, 22 | }, 23 | { 24 | cmd: "flushdb 1", 25 | err: redis.ErrSyntaxError, 26 | }, 27 | } 28 | 29 | for _, test := range tests { 30 | t.Run(test.cmd, func(t *testing.T) { 31 | cmd, err := redis.Parse(ParseFlushDB, test.cmd) 32 | be.Equal(t, err, test.err) 33 | if err != nil { 34 | be.Equal(t, cmd, FlushDB{}) 35 | } 36 | }) 37 | } 38 | } 39 | 40 | func TestFlushDBExec(t *testing.T) { 41 | t.Run("full", func(t *testing.T) { 42 | red := getRedka(t) 43 | _ = red.Str().Set("name", "alice") 44 | _ = red.Str().Set("age", 25) 45 | 46 | cmd := redis.MustParse(ParseFlushDB, "flushdb") 47 | conn := redis.NewFakeConn() 48 | res, err := cmd.Run(conn, red) 49 | be.Err(t, err, nil) 50 | be.Equal(t, res, true) 51 | be.Equal(t, conn.Out(), "OK") 52 | 53 | keys, _ := red.Key().Keys("*") 54 | be.Equal(t, len(keys), 0) 55 | }) 56 | 57 | t.Run("empty", func(t *testing.T) { 58 | red := getRedka(t) 59 | 60 | cmd := redis.MustParse(ParseFlushDB, "flushdb") 61 | conn := redis.NewFakeConn() 62 | res, err := cmd.Run(conn, red) 63 | be.Err(t, err, nil) 64 | be.Equal(t, res, true) 65 | be.Equal(t, conn.Out(), "OK") 66 | 67 | keys, _ := red.Key().Keys("*") 68 | be.Equal(t, len(keys), 0) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/strlen_test.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func TestStrlenParse(t *testing.T) { 11 | tests := []struct { 12 | cmd string 13 | want Strlen 14 | err error 15 | }{ 16 | { 17 | cmd: "strlen", 18 | want: Strlen{}, 19 | err: redis.ErrInvalidArgNum, 20 | }, 21 | { 22 | cmd: "strlen name", 23 | want: Strlen{key: "name"}, 24 | err: nil, 25 | }, 26 | { 27 | cmd: "strlen name age", 28 | want: Strlen{}, 29 | err: redis.ErrInvalidArgNum, 30 | }, 31 | } 32 | 33 | for _, test := range tests { 34 | t.Run(test.cmd, func(t *testing.T) { 35 | cmd, err := redis.Parse(ParseStrlen, test.cmd) 36 | be.Equal(t, err, test.err) 37 | if err == nil { 38 | be.Equal(t, cmd.key, test.want.key) 39 | } else { 40 | be.Equal(t, cmd, test.want) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func TestStrlenExec(t *testing.T) { 47 | t.Run("strlen", func(t *testing.T) { 48 | red := getRedka(t) 49 | _ = red.Str().Set("name", "alice") 50 | 51 | cmd := redis.MustParse(ParseStrlen, "strlen name") 52 | conn := redis.NewFakeConn() 53 | res, err := cmd.Run(conn, red) 54 | be.Err(t, err, nil) 55 | be.Equal(t, res, 5) 56 | be.Equal(t, conn.Out(), "5") 57 | }) 58 | 59 | t.Run("key not found", func(t *testing.T) { 60 | red := getRedka(t) 61 | 62 | cmd := redis.MustParse(ParseStrlen, "strlen name") 63 | conn := redis.NewFakeConn() 64 | res, err := cmd.Run(conn, red) 65 | be.Err(t, err, nil) 66 | be.Equal(t, res, 0) 67 | be.Equal(t, conn.Out(), "0") 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hlen_test.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func TestHLenParse(t *testing.T) { 11 | tests := []struct { 12 | cmd string 13 | key string 14 | err error 15 | }{ 16 | { 17 | cmd: "hlen", 18 | key: "", 19 | err: redis.ErrInvalidArgNum, 20 | }, 21 | { 22 | cmd: "hlen person", 23 | key: "person", 24 | err: nil, 25 | }, 26 | { 27 | cmd: "hlen person name", 28 | key: "", 29 | err: redis.ErrInvalidArgNum, 30 | }, 31 | } 32 | 33 | for _, test := range tests { 34 | t.Run(test.cmd, func(t *testing.T) { 35 | cmd, err := redis.Parse(ParseHLen, test.cmd) 36 | be.Equal(t, err, test.err) 37 | if err == nil { 38 | be.Equal(t, cmd.key, test.key) 39 | } else { 40 | be.Equal(t, cmd, HLen{}) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func TestHLenExec(t *testing.T) { 47 | t.Run("key found", func(t *testing.T) { 48 | red := getRedka(t) 49 | _, _ = red.Hash().Set("person", "name", "alice") 50 | _, _ = red.Hash().Set("person", "age", 25) 51 | 52 | cmd := redis.MustParse(ParseHLen, "hlen person") 53 | conn := redis.NewFakeConn() 54 | res, err := cmd.Run(conn, red) 55 | 56 | be.Err(t, err, nil) 57 | be.Equal(t, res, 2) 58 | be.Equal(t, conn.Out(), "2") 59 | }) 60 | t.Run("key not found", func(t *testing.T) { 61 | red := getRedka(t) 62 | 63 | cmd := redis.MustParse(ParseHLen, "hlen person") 64 | conn := redis.NewFakeConn() 65 | res, err := cmd.Run(conn, red) 66 | 67 | be.Err(t, err, nil) 68 | be.Equal(t, res, 0) 69 | be.Equal(t, conn.Out(), "0") 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: docker 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up QEMU 17 | uses: docker/setup-qemu-action@v3 18 | 19 | - name: Login to Docker Hub 20 | uses: docker/login-action@v3 21 | with: 22 | username: ${{ secrets.DOCKERHUB_USERNAME }} 23 | password: ${{ secrets.DOCKERHUB_TOKEN }} 24 | 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v3 27 | 28 | - name: Docker meta 29 | id: meta 30 | uses: docker/metadata-action@v5 31 | with: 32 | images: | 33 | ${{ secrets.DOCKERHUB_USERNAME }}/redka 34 | tags: | 35 | type=semver,pattern={{version}} 36 | type=semver,pattern={{major}}.{{minor}} 37 | type=semver,pattern={{major}} 38 | type=raw,value=latest 39 | type=sha 40 | 41 | - name: Build and push 42 | uses: docker/build-push-action@v5 43 | with: 44 | context: . 45 | platforms: linux/amd64,linux/arm64 46 | file: ./Dockerfile 47 | push: true 48 | tags: ${{ steps.meta.outputs.tags }} 49 | labels: ${{ steps.meta.outputs.labels }} 50 | -------------------------------------------------------------------------------- /internal/rkey/scanner.go: -------------------------------------------------------------------------------- 1 | package rkey 2 | 3 | import "github.com/nalgeon/redka/internal/core" 4 | 5 | // Scanner is the iterator for keys. 6 | // Stops when there are no more keys or an error occurs. 7 | type Scanner struct { 8 | db *Tx 9 | cursor int 10 | pattern string 11 | ktype core.TypeID 12 | pageSize int 13 | index int 14 | cur core.Key 15 | keys []core.Key 16 | err error 17 | } 18 | 19 | func newScanner(db *Tx, pattern string, ktype core.TypeID, pageSize int) *Scanner { 20 | if pageSize == 0 { 21 | pageSize = scanPageSize 22 | } 23 | return &Scanner{ 24 | db: db, 25 | cursor: 0, 26 | pattern: pattern, 27 | ktype: ktype, 28 | pageSize: pageSize, 29 | index: 0, 30 | keys: []core.Key{}, 31 | } 32 | } 33 | 34 | // Scan advances to the next key, fetching keys from db as necessary. 35 | // Returns false when there are no more keys or an error occurs. 36 | func (sc *Scanner) Scan() bool { 37 | if sc.index >= len(sc.keys) { 38 | // Fetch a new page of keys. 39 | out, err := sc.db.Scan(sc.cursor, sc.pattern, sc.ktype, sc.pageSize) 40 | if err != nil { 41 | sc.err = err 42 | return false 43 | } 44 | sc.cursor = out.Cursor 45 | sc.keys = out.Keys 46 | sc.index = 0 47 | if len(sc.keys) == 0 { 48 | return false 49 | } 50 | } 51 | // Advance to the next key from the current page. 52 | sc.cur = sc.keys[sc.index] 53 | sc.index++ 54 | return true 55 | } 56 | 57 | // Key returns the current key. 58 | func (sc *Scanner) Key() core.Key { 59 | return sc.cur 60 | } 61 | 62 | // Err returns the first error encountered during iteration. 63 | func (sc *Scanner) Err() error { 64 | return sc.err 65 | } 66 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/get_test.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/internal/core" 8 | "github.com/nalgeon/redka/redsrv/internal/redis" 9 | ) 10 | 11 | func TestGetParse(t *testing.T) { 12 | tests := []struct { 13 | cmd string 14 | want string 15 | err error 16 | }{ 17 | { 18 | cmd: "get", 19 | want: "", 20 | err: redis.ErrInvalidArgNum, 21 | }, 22 | { 23 | cmd: "get name", 24 | want: "name", 25 | err: nil, 26 | }, 27 | { 28 | cmd: "get name age", 29 | want: "", 30 | err: redis.ErrInvalidArgNum, 31 | }, 32 | } 33 | 34 | for _, test := range tests { 35 | t.Run(test.cmd, func(t *testing.T) { 36 | cmd, err := redis.Parse(ParseGet, test.cmd) 37 | be.Equal(t, err, test.err) 38 | if err == nil { 39 | be.Equal(t, cmd.key, test.want) 40 | } else { 41 | be.Equal(t, cmd, Get{}) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestGetExec(t *testing.T) { 48 | red := getRedka(t) 49 | _ = red.Str().Set("name", "alice") 50 | 51 | tests := []struct { 52 | cmd string 53 | res any 54 | out string 55 | }{ 56 | { 57 | cmd: "get name", 58 | res: core.Value("alice"), 59 | out: "alice", 60 | }, 61 | { 62 | cmd: "get age", 63 | res: core.Value(nil), 64 | out: "(nil)", 65 | }, 66 | } 67 | 68 | for _, test := range tests { 69 | t.Run(test.cmd, func(t *testing.T) { 70 | conn := redis.NewFakeConn() 71 | cmd := redis.MustParse(ParseGet, test.cmd) 72 | res, err := cmd.Run(conn, red) 73 | be.Err(t, err, nil) 74 | be.Equal(t, res, test.res) 75 | be.Equal(t, conn.Out(), test.out) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /internal/rzset/scanner.go: -------------------------------------------------------------------------------- 1 | package rzset 2 | 3 | // Scanner is the iterator for set items. 4 | // Stops when there are no more items or an error occurs. 5 | type Scanner struct { 6 | tx *Tx 7 | key string 8 | cursor int 9 | pattern string 10 | pageSize int 11 | index int 12 | cur SetItem 13 | items []SetItem 14 | err error 15 | } 16 | 17 | func newScanner(tx *Tx, key string, pattern string, pageSize int) *Scanner { 18 | if pageSize == 0 { 19 | pageSize = scanPageSize 20 | } 21 | return &Scanner{ 22 | tx: tx, 23 | key: key, 24 | cursor: 0, 25 | pattern: pattern, 26 | pageSize: pageSize, 27 | index: 0, 28 | items: []SetItem{}, 29 | } 30 | } 31 | 32 | // Scan advances to the next item, fetching items from db as necessary. 33 | // Returns false when there are no more items or an error occurs. 34 | // Returns false if the key does not exist or is not a set. 35 | func (sc *Scanner) Scan() bool { 36 | if sc.index >= len(sc.items) { 37 | // Fetch a new page of items. 38 | out, err := sc.tx.Scan(sc.key, sc.cursor, sc.pattern, sc.pageSize) 39 | if err != nil { 40 | sc.err = err 41 | return false 42 | } 43 | sc.cursor = out.Cursor 44 | sc.items = out.Items 45 | sc.index = 0 46 | if len(sc.items) == 0 { 47 | return false 48 | } 49 | } 50 | // Advance to the next item from the current page. 51 | sc.cur = sc.items[sc.index] 52 | sc.index++ 53 | return true 54 | } 55 | 56 | // Item returns the current set item. 57 | func (sc *Scanner) Item() SetItem { 58 | return sc.cur 59 | } 60 | 61 | // Err returns the first error encountered during iteration. 62 | func (sc *Scanner) Err() error { 63 | return sc.err 64 | } 65 | -------------------------------------------------------------------------------- /internal/rhash/scanner.go: -------------------------------------------------------------------------------- 1 | package rhash 2 | 3 | // Scanner is the iterator for hash items. 4 | // Stops when there are no more items or an error occurs. 5 | type Scanner struct { 6 | db *Tx 7 | key string 8 | cursor int 9 | pattern string 10 | pageSize int 11 | index int 12 | cur HashItem 13 | items []HashItem 14 | err error 15 | } 16 | 17 | func newScanner(db *Tx, key string, pattern string, pageSize int) *Scanner { 18 | if pageSize == 0 { 19 | pageSize = scanPageSize 20 | } 21 | return &Scanner{ 22 | db: db, 23 | key: key, 24 | cursor: 0, 25 | pattern: pattern, 26 | pageSize: pageSize, 27 | index: 0, 28 | items: []HashItem{}, 29 | } 30 | } 31 | 32 | // Scan advances to the next item, fetching items from db as necessary. 33 | // Returns false when there are no more items or an error occurs. 34 | // Returns false if the key does not exist or is not a hash. 35 | func (sc *Scanner) Scan() bool { 36 | if sc.index >= len(sc.items) { 37 | // Fetch a new page of items. 38 | out, err := sc.db.Scan(sc.key, sc.cursor, sc.pattern, sc.pageSize) 39 | if err != nil { 40 | sc.err = err 41 | return false 42 | } 43 | sc.cursor = out.Cursor 44 | sc.items = out.Items 45 | sc.index = 0 46 | if len(sc.items) == 0 { 47 | return false 48 | } 49 | } 50 | // Advance to the next item from the current page. 51 | sc.cur = sc.items[sc.index] 52 | sc.index++ 53 | return true 54 | } 55 | 56 | // Item returns the current hash item. 57 | func (sc *Scanner) Item() HashItem { 58 | return sc.cur 59 | } 60 | 61 | // Err returns the first error encountered during iteration. 62 | func (sc *Scanner) Err() error { 63 | return sc.err 64 | } 65 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/randomkey_test.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "slices" 5 | "testing" 6 | 7 | "github.com/nalgeon/be" 8 | "github.com/nalgeon/redka/internal/core" 9 | "github.com/nalgeon/redka/redsrv/internal/redis" 10 | ) 11 | 12 | func TestRandomKeyParse(t *testing.T) { 13 | tests := []struct { 14 | cmd string 15 | err error 16 | }{ 17 | { 18 | cmd: "randomkey", 19 | err: nil, 20 | }, 21 | { 22 | cmd: "randomkey name", 23 | err: redis.ErrInvalidArgNum, 24 | }, 25 | { 26 | cmd: "randomkey name age", 27 | err: redis.ErrInvalidArgNum, 28 | }, 29 | } 30 | 31 | for _, test := range tests { 32 | t.Run(test.cmd, func(t *testing.T) { 33 | cmd, err := redis.Parse(ParseRandomKey, test.cmd) 34 | be.Equal(t, err, test.err) 35 | if err != nil { 36 | be.Equal(t, cmd, RandomKey{}) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func TestRandomKeyExec(t *testing.T) { 43 | t.Run("found", func(t *testing.T) { 44 | red := getRedka(t) 45 | _ = red.Str().Set("name", "alice") 46 | _ = red.Str().Set("age", 25) 47 | _ = red.Str().Set("city", "paris") 48 | 49 | conn := redis.NewFakeConn() 50 | cmd := redis.MustParse(ParseRandomKey, "randomkey") 51 | res, err := cmd.Run(conn, red) 52 | be.Err(t, err, nil) 53 | keys := []string{"name", "age", "city"} 54 | be.True(t, slices.Contains(keys, res.(core.Key).Key)) 55 | be.True(t, slices.Contains(keys, conn.Out())) 56 | }) 57 | t.Run("not found", func(t *testing.T) { 58 | red := getRedka(t) 59 | conn := redis.NewFakeConn() 60 | cmd := redis.MustParse(ParseRandomKey, "randomkey") 61 | res, err := cmd.Run(conn, red) 62 | be.Err(t, err, nil) 63 | be.Equal(t, res, nil) 64 | be.Equal(t, conn.Out(), "(nil)") 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zunion.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/sqlx" 5 | "github.com/nalgeon/redka/redsrv/internal/parser" 6 | "github.com/nalgeon/redka/redsrv/internal/redis" 7 | ) 8 | 9 | // Returns the union of multiple sorted sets. 10 | // ZUNION numkeys key [key ...] [AGGREGATE ] [WITHSCORES] 11 | // https://redis.io/commands/zunion 12 | type ZUnion struct { 13 | redis.BaseCmd 14 | keys []string 15 | aggregate string 16 | withScores bool 17 | } 18 | 19 | func ParseZUnion(b redis.BaseCmd) (ZUnion, error) { 20 | cmd := ZUnion{BaseCmd: b} 21 | var nKeys int 22 | err := parser.New( 23 | parser.Int(&nKeys), 24 | parser.StringsN(&cmd.keys, &nKeys), 25 | parser.Named("aggregate", parser.Enum(&cmd.aggregate, sqlx.Sum, sqlx.Min, sqlx.Max)), 26 | parser.Flag("withscores", &cmd.withScores), 27 | ).Required(2).Run(cmd.Args()) 28 | if err != nil { 29 | return ZUnion{}, err 30 | } 31 | return cmd, nil 32 | } 33 | 34 | func (cmd ZUnion) Run(w redis.Writer, red redis.Redka) (any, error) { 35 | union := red.ZSet().UnionWith(cmd.keys...) 36 | switch cmd.aggregate { 37 | case sqlx.Min: 38 | union = union.Min() 39 | case sqlx.Max: 40 | union = union.Max() 41 | case sqlx.Sum: 42 | union = union.Sum() 43 | } 44 | 45 | items, err := union.Run() 46 | if err != nil { 47 | w.WriteError(cmd.Error(err)) 48 | return nil, err 49 | } 50 | 51 | if cmd.withScores { 52 | w.WriteArray(len(items) * 2) 53 | for _, item := range items { 54 | w.WriteBulk(item.Elem) 55 | redis.WriteFloat(w, item.Score) 56 | } 57 | } else { 58 | w.WriteArray(len(items)) 59 | for _, item := range items { 60 | w.WriteBulk(item.Elem) 61 | } 62 | } 63 | 64 | return items, nil 65 | } 66 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zinter.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/sqlx" 5 | "github.com/nalgeon/redka/redsrv/internal/parser" 6 | "github.com/nalgeon/redka/redsrv/internal/redis" 7 | ) 8 | 9 | // Returns the intersect of multiple sorted sets. 10 | // ZINTER numkeys key [key ...] [AGGREGATE ] [WITHSCORES] 11 | // https://redis.io/commands/zinter 12 | type ZInter struct { 13 | redis.BaseCmd 14 | keys []string 15 | aggregate string 16 | withScores bool 17 | } 18 | 19 | func ParseZInter(b redis.BaseCmd) (ZInter, error) { 20 | cmd := ZInter{BaseCmd: b} 21 | var nKeys int 22 | err := parser.New( 23 | parser.Int(&nKeys), 24 | parser.StringsN(&cmd.keys, &nKeys), 25 | parser.Named("aggregate", parser.Enum(&cmd.aggregate, sqlx.Sum, sqlx.Min, sqlx.Max)), 26 | parser.Flag("withscores", &cmd.withScores), 27 | ).Required(2).Run(cmd.Args()) 28 | if err != nil { 29 | return ZInter{}, err 30 | } 31 | return cmd, nil 32 | } 33 | 34 | func (cmd ZInter) Run(w redis.Writer, red redis.Redka) (any, error) { 35 | inter := red.ZSet().InterWith(cmd.keys...) 36 | switch cmd.aggregate { 37 | case sqlx.Min: 38 | inter = inter.Min() 39 | case sqlx.Max: 40 | inter = inter.Max() 41 | case sqlx.Sum: 42 | inter = inter.Sum() 43 | } 44 | 45 | items, err := inter.Run() 46 | if err != nil { 47 | w.WriteError(cmd.Error(err)) 48 | return nil, err 49 | } 50 | 51 | if cmd.withScores { 52 | w.WriteArray(len(items) * 2) 53 | for _, item := range items { 54 | w.WriteBulk(item.Elem) 55 | redis.WriteFloat(w, item.Score) 56 | } 57 | } else { 58 | w.WriteArray(len(items)) 59 | for _, item := range items { 60 | w.WriteBulk(item.Elem) 61 | } 62 | } 63 | 64 | return items, nil 65 | } 66 | -------------------------------------------------------------------------------- /internal/rset/scanner.go: -------------------------------------------------------------------------------- 1 | package rset 2 | 3 | import "github.com/nalgeon/redka/internal/core" 4 | 5 | // Scanner is the iterator for set items. 6 | // Stops when there are no more items or an error occurs. 7 | type Scanner struct { 8 | tx *Tx 9 | key string 10 | cursor int 11 | pattern string 12 | pageSize int 13 | index int 14 | cur core.Value 15 | items []core.Value 16 | err error 17 | } 18 | 19 | func newScanner(tx *Tx, key string, pattern string, pageSize int) *Scanner { 20 | if pageSize == 0 { 21 | pageSize = scanPageSize 22 | } 23 | return &Scanner{ 24 | tx: tx, 25 | key: key, 26 | cursor: 0, 27 | pattern: pattern, 28 | pageSize: pageSize, 29 | index: 0, 30 | items: []core.Value{}, 31 | } 32 | } 33 | 34 | // Scan advances to the next item, fetching items from db as necessary. 35 | // Returns false when there are no more items or an error occurs. 36 | // Returns false if the key does not exist or is not a set. 37 | func (sc *Scanner) Scan() bool { 38 | if sc.index >= len(sc.items) { 39 | // Fetch a new page of items. 40 | out, err := sc.tx.Scan(sc.key, sc.cursor, sc.pattern, sc.pageSize) 41 | if err != nil { 42 | sc.err = err 43 | return false 44 | } 45 | sc.cursor = out.Cursor 46 | sc.items = out.Items 47 | sc.index = 0 48 | if len(sc.items) == 0 { 49 | return false 50 | } 51 | } 52 | // Advance to the next item from the current page. 53 | sc.cur = sc.items[sc.index] 54 | sc.index++ 55 | return true 56 | } 57 | 58 | // Item returns the current set item. 59 | func (sc *Scanner) Item() core.Value { 60 | return sc.cur 61 | } 62 | 63 | // Err returns the first error encountered during iteration. 64 | func (sc *Scanner) Err() error { 65 | return sc.err 66 | } 67 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/type_test.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func TestTypeParse(t *testing.T) { 11 | tests := []struct { 12 | cmd string 13 | key string 14 | err error 15 | }{ 16 | { 17 | cmd: "type", 18 | key: "", 19 | err: redis.ErrInvalidArgNum, 20 | }, 21 | { 22 | cmd: "type name", 23 | key: "name", 24 | err: nil, 25 | }, 26 | { 27 | cmd: "type name age", 28 | key: "", 29 | err: redis.ErrInvalidArgNum, 30 | }, 31 | } 32 | 33 | for _, test := range tests { 34 | t.Run(test.cmd, func(t *testing.T) { 35 | cmd, err := redis.Parse(ParseType, test.cmd) 36 | be.Equal(t, err, test.err) 37 | if err == nil { 38 | be.Equal(t, cmd.key, test.key) 39 | } else { 40 | be.Equal(t, cmd, Type{}) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func TestTypeExec(t *testing.T) { 47 | red := getRedka(t) 48 | 49 | _ = red.Str().Set("kstr", "string") 50 | _, _ = red.List().PushBack("klist", "list") 51 | _, _ = red.Hash().Set("khash", "field", "hash") 52 | _, _ = red.ZSet().Add("kzset", "zset", 1) 53 | 54 | tests := []struct { 55 | key string 56 | want string 57 | }{ 58 | {key: "kstr", want: "string"}, 59 | {key: "klist", want: "list"}, 60 | {key: "khash", want: "hash"}, 61 | {key: "kzset", want: "zset"}, 62 | {key: "knone", want: "none"}, 63 | } 64 | 65 | for _, test := range tests { 66 | t.Run(test.key, func(t *testing.T) { 67 | cmd := redis.MustParse(ParseType, "type "+test.key) 68 | conn := redis.NewFakeConn() 69 | res, err := cmd.Run(conn, red) 70 | be.Err(t, err, nil) 71 | be.Equal(t, res.(string), test.want) 72 | be.Equal(t, conn.Out(), test.want) 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /redsrv/internal/parser/parsers_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | ) 8 | 9 | func TestFlag(t *testing.T) { 10 | tests := []struct { 11 | title string 12 | name string 13 | args [][]byte 14 | match bool 15 | rest [][]byte 16 | }{ 17 | { 18 | title: "flag_flag", 19 | name: "flag", 20 | args: [][]byte{[]byte("flag")}, 21 | match: true, 22 | rest: [][]byte{}, 23 | }, 24 | { 25 | title: "flag_notflag", 26 | name: "flag", 27 | args: [][]byte{[]byte("notflag")}, 28 | match: false, 29 | rest: [][]byte{[]byte("notflag")}, 30 | }, 31 | { 32 | title: "flag_FLAG", 33 | name: "flag", 34 | args: [][]byte{[]byte("FLAG")}, 35 | match: true, 36 | rest: [][]byte{}, 37 | }, 38 | { 39 | title: "FLAG_flag", 40 | name: "FLAG", 41 | args: [][]byte{[]byte("flag")}, 42 | match: true, 43 | rest: [][]byte{}, 44 | }, 45 | { 46 | title: "flag_bytes", 47 | name: "flag", 48 | args: [][]byte{{0, 0, 0}}, 49 | match: false, 50 | rest: [][]byte{{0, 0, 0}}, 51 | }, 52 | { 53 | title: "flag_empty", 54 | name: "flag", 55 | args: [][]byte{}, 56 | match: false, 57 | rest: [][]byte{}, 58 | }, 59 | { 60 | title: "flag_flag_other", 61 | name: "flag", 62 | args: [][]byte{[]byte("flag"), []byte("other")}, 63 | match: true, 64 | rest: [][]byte{[]byte("other")}, 65 | }, 66 | } 67 | 68 | for _, test := range tests { 69 | t.Run(test.title, func(t *testing.T) { 70 | var dest bool 71 | parser := Flag(test.name, &dest) 72 | match, rest, err := parser(test.args) 73 | be.Err(t, err, nil) 74 | be.Equal(t, match, test.match) 75 | be.Equal(t, dest, test.match) 76 | be.Equal(t, rest, test.rest) 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/exists_test.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func TestExistsParse(t *testing.T) { 11 | tests := []struct { 12 | cmd string 13 | want []string 14 | err error 15 | }{ 16 | { 17 | cmd: "exists", 18 | want: nil, 19 | err: redis.ErrInvalidArgNum, 20 | }, 21 | { 22 | cmd: "exists name", 23 | want: []string{"name"}, 24 | err: nil, 25 | }, 26 | { 27 | cmd: "exists name age", 28 | want: []string{"name", "age"}, 29 | err: nil, 30 | }, 31 | } 32 | 33 | for _, test := range tests { 34 | t.Run(test.cmd, func(t *testing.T) { 35 | cmd, err := redis.Parse(ParseExists, test.cmd) 36 | be.Equal(t, err, test.err) 37 | if err == nil { 38 | be.Equal(t, cmd.keys, test.want) 39 | } else { 40 | be.Equal(t, cmd, Exists{}) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func TestExistsExec(t *testing.T) { 47 | red := getRedka(t) 48 | 49 | _ = red.Str().Set("name", "alice") 50 | _ = red.Str().Set("age", 50) 51 | _ = red.Str().Set("city", "paris") 52 | 53 | tests := []struct { 54 | cmd string 55 | res any 56 | out string 57 | }{ 58 | { 59 | cmd: "exists name", 60 | res: 1, 61 | out: "1", 62 | }, 63 | { 64 | cmd: "exists name age", 65 | res: 2, 66 | out: "2", 67 | }, 68 | { 69 | cmd: "exists name age street", 70 | res: 2, 71 | out: "2", 72 | }, 73 | } 74 | 75 | for _, test := range tests { 76 | t.Run(test.cmd, func(t *testing.T) { 77 | conn := redis.NewFakeConn() 78 | cmd := redis.MustParse(ParseExists, test.cmd) 79 | res, err := cmd.Run(conn, red) 80 | be.Err(t, err, nil) 81 | be.Equal(t, res, test.res) 82 | be.Equal(t, conn.Out(), test.out) 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hkeys_test.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "slices" 5 | "testing" 6 | 7 | "github.com/nalgeon/be" 8 | "github.com/nalgeon/redka/redsrv/internal/redis" 9 | ) 10 | 11 | func TestHKeysParse(t *testing.T) { 12 | tests := []struct { 13 | cmd string 14 | key string 15 | err error 16 | }{ 17 | { 18 | cmd: "hkeys", 19 | key: "", 20 | err: redis.ErrInvalidArgNum, 21 | }, 22 | { 23 | cmd: "hkeys person", 24 | key: "person", 25 | err: nil, 26 | }, 27 | { 28 | cmd: "hkeys person name", 29 | key: "", 30 | err: redis.ErrInvalidArgNum, 31 | }, 32 | } 33 | 34 | for _, test := range tests { 35 | t.Run(test.cmd, func(t *testing.T) { 36 | cmd, err := redis.Parse(ParseHKeys, test.cmd) 37 | be.Equal(t, err, test.err) 38 | if err == nil { 39 | be.Equal(t, cmd.key, test.key) 40 | } else { 41 | be.Equal(t, cmd, HKeys{}) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestHKeysExec(t *testing.T) { 48 | t.Run("key found", func(t *testing.T) { 49 | red := getRedka(t) 50 | 51 | _, _ = red.Hash().Set("person", "name", "alice") 52 | _, _ = red.Hash().Set("person", "age", 25) 53 | 54 | cmd := redis.MustParse(ParseHKeys, "hkeys person") 55 | conn := redis.NewFakeConn() 56 | res, err := cmd.Run(conn, red) 57 | 58 | be.Err(t, err, nil) 59 | got := res.([]string) 60 | slices.Sort(got) 61 | be.Equal(t, got, []string{"age", "name"}) 62 | be.True(t, conn.Out() == "2,age,name" || conn.Out() == "2,name,age") 63 | }) 64 | t.Run("key not found", func(t *testing.T) { 65 | red := getRedka(t) 66 | 67 | cmd := redis.MustParse(ParseHKeys, "hkeys person") 68 | conn := redis.NewFakeConn() 69 | res, err := cmd.Run(conn, red) 70 | 71 | be.Err(t, err, nil) 72 | be.Equal(t, res.([]string), []string{}) 73 | be.Equal(t, conn.Out(), "0") 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /example/server/main.go: -------------------------------------------------------------------------------- 1 | // An example of running a Redka server 2 | // within your own application. 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | _ "github.com/mattn/go-sqlite3" 10 | "github.com/nalgeon/redka" 11 | "github.com/nalgeon/redka/redsrv" 12 | "github.com/redis/go-redis/v9" 13 | ) 14 | 15 | func main() { 16 | // Start a Redka server with an in-memory database. 17 | db := mustOpen() 18 | srv := mustStart(db) 19 | defer func() { 20 | _ = srv.Stop() 21 | fmt.Println("redka server stopped") 22 | }() 23 | fmt.Println("redka server started") 24 | 25 | // The server is now running and ready to accept connections. 26 | // You can use the regular go-redis package to access Redka. 27 | rdb := redis.NewClient(&redis.Options{Addr: ":6380"}) 28 | defer func() { _ = rdb.Close() }() 29 | 30 | ctx := context.Background() 31 | rdb.Set(ctx, "name", "alice", 0) 32 | rdb.Set(ctx, "age", 25, 0) 33 | 34 | name, _ := rdb.Get(ctx, "name").Result() 35 | fmt.Println("name =", name) 36 | age, _ := rdb.Get(ctx, "age").Int() 37 | fmt.Println("age =", age) 38 | } 39 | 40 | // mustOpen opens an in-memory Redka database. 41 | func mustOpen() *redka.DB { 42 | db, err := redka.Open("file:/redka.db?vfs=memdb", nil) 43 | if err != nil { 44 | panic(err) 45 | } 46 | return db 47 | } 48 | 49 | // mustStart starts a Redka server on localhost:6380. 50 | func mustStart(db *redka.DB) *redsrv.Server { 51 | srv := redsrv.New("tcp", ":6380", db) 52 | 53 | // The ready channel will receive a nil value when the server is ready, 54 | // or an error if it fails to start. 55 | ready := make(chan error, 1) 56 | go func() { 57 | if err := srv.Start(ready); err != nil { 58 | ready <- err 59 | return 60 | } 61 | }() 62 | 63 | // Wait for the server to be ready. 64 | if err := <-ready; err != nil { 65 | panic(err) 66 | } 67 | return srv 68 | } 69 | -------------------------------------------------------------------------------- /docs/persistence.md: -------------------------------------------------------------------------------- 1 | # Persistence 2 | 3 | Redka stores data in a SQL database using the following tables: 4 | 5 | ``` 6 | rkey 7 | --- 8 | id integer primary key 9 | key text not null 10 | type integer not null -- 1 string, 2 list, 3 set, 4 hash, 5 sorted set 11 | version integer not null -- incremented when the key value is updated 12 | etime integer -- expiration timestamp in unix milliseconds 13 | mtime integer not null -- modification timestamp in unix milliseconds 14 | len integer -- number of child elements 15 | 16 | rstring 17 | --- 18 | kid integer not null -- FK -> rkey.id 19 | value blob not null 20 | 21 | rlist 22 | --- 23 | kid integer not null -- FK -> rkey.id 24 | pos real not null -- is used for ordering, but is not an index 25 | elem blob not null 26 | 27 | rset 28 | --- 29 | kid integer not null -- FK -> rkey.id 30 | elem blob not null 31 | 32 | rhash 33 | --- 34 | kid integer not null -- FK -> rkey.id 35 | field text not null 36 | value blob not null 37 | 38 | rzset 39 | --- 40 | kid integer not null -- FK -> rkey.id 41 | elem blob not null 42 | score real not null 43 | ``` 44 | 45 | To access the data with SQL, use views instead of tables: 46 | 47 | ```sql 48 | select * from vstring; 49 | ``` 50 | 51 | ``` 52 | ┌─────┬──────┬───────┬───────┬─────────────────────┐ 53 | │ kid │ key │ value │ etime │ mtime │ 54 | ├─────┼──────┼───────┼───────┼─────────────────────┤ 55 | │ 1 │ name │ alice │ │ 2024-04-03 16:58:14 │ 56 | │ 2 │ age │ 50 │ │ 2024-04-03 16:34:52 │ 57 | └─────┴──────┴───────┴───────┴─────────────────────┘ 58 | ``` 59 | 60 | `etime` and `mtime` are in UTC. 61 | 62 | There is a separate view for every data type: 63 | 64 | ``` 65 | vkey vstring vlist vset vhash vzset 66 | ``` 67 | -------------------------------------------------------------------------------- /internal/rkey/sqlite.go: -------------------------------------------------------------------------------- 1 | package rkey 2 | 3 | // SQLite queries for the key repository. 4 | var sqlite = queries{ 5 | count: ` 6 | select count(id) from rkey 7 | where key in (:keys) and (etime is null or etime > ?)`, 8 | 9 | delete: ` 10 | delete from rkey 11 | where key in (:keys) and (etime is null or etime > ?)`, 12 | 13 | deleteAll: ` 14 | delete from rkey; 15 | vacuum; 16 | pragma integrity_check;`, 17 | 18 | deleteAllExpired: ` 19 | delete from rkey 20 | where etime <= $1`, 21 | 22 | deleteNExpired: ` 23 | delete from rkey 24 | where rowid in ( 25 | select rowid from rkey 26 | where etime <= $1 27 | limit $2 28 | )`, 29 | 30 | expire: ` 31 | update rkey set 32 | version = version + 1, 33 | etime = $1 34 | where key = $2 and (etime is null or etime > $3)`, 35 | 36 | get: ` 37 | select id, key, type, version, etime, mtime 38 | from rkey 39 | where key = $1 and (etime is null or etime > $2)`, 40 | 41 | keys: ` 42 | select id, key, type, version, etime, mtime from rkey 43 | where key glob $1 and (etime is null or etime > $2)`, 44 | 45 | len: ` 46 | select count(*) from rkey`, 47 | 48 | persist: ` 49 | update rkey set 50 | version = version + 1, 51 | etime = null 52 | where key = $1 and (etime is null or etime > $2)`, 53 | 54 | random: ` 55 | select id, key, type, version, etime, mtime from rkey 56 | where etime is null or etime > $1 57 | order by random() limit 1`, 58 | 59 | rename1: ` 60 | delete from rkey where id = $1`, 61 | 62 | rename2: ` 63 | update rkey 64 | set 65 | key = $1, 66 | version = version + 1, 67 | mtime = $2 68 | where key = $3 and (etime is null or etime > $4)`, 69 | 70 | scan: ` 71 | select id, key, type, version, etime, mtime from rkey 72 | where 73 | id > $1 and key glob $2 and (type = $3 or $4) 74 | and (etime is null or etime > $5) 75 | order by id asc 76 | limit $6`, 77 | } 78 | -------------------------------------------------------------------------------- /docs/commands/keys.md: -------------------------------------------------------------------------------- 1 | # Key management 2 | 3 | Redka supports the following key management (generic) commands: 4 | 5 | ``` 6 | Command Go API Description 7 | ------- ------ ----------- 8 | DBSIZE DB.Key().Len Returns the total number of keys. 9 | DEL DB.Key().Delete Deletes one or more keys. 10 | EXISTS DB.Key().Count Determines whether one or more keys exist. 11 | EXPIRE DB.Key().Expire Sets the expiration time of a key (in seconds). 12 | EXPIREAT DB.Key().ExpireAt Sets the expiration time of a key to a Unix timestamp. 13 | FLUSHALL DB.Key().DeleteAll Deletes all keys from the database. 14 | FLUSHDB DB.Key().DeleteAll Deletes all keys from the database. 15 | KEYS DB.Key().Keys Returns all key names that match a pattern. 16 | PERSIST DB.Key().Persist Removes the expiration time of a key. 17 | PEXPIRE DB.Key().Expire Sets the expiration time of a key in ms. 18 | PEXPIREAT DB.Key().ExpireAt Sets the expiration time of a key to a Unix ms timestamp. 19 | RANDOMKEY DB.Key().Random Returns a random key name from the database. 20 | RENAME DB.Key().Rename Renames a key and overwrites the destination. 21 | RENAMENX DB.Key().RenameNotExists Renames a key only when the target key name doesn't exist. 22 | SCAN DB.Key().Scanner Iterates over the key names in the database. 23 | TTL DB.Key().Get Returns the expiration time in seconds of a key. 24 | TYPE DB.Key().Get Returns the type of value stored at a key. 25 | ``` 26 | 27 | The following generic commands are not planned for 1.0: 28 | 29 | ``` 30 | COPY DUMP EXPIRETIME MIGRATE MOVE OBJECT PEXPIRETIME 31 | PTTL RESTORE SORT SORT_RO TOUCH TTL TYPE UNLINK 32 | WAIT WAITAOF 33 | ``` 34 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zrangebyscore.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Returns members in a sorted set within a range of scores. 9 | // ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] 10 | // https://redis.io/commands/zrangebyscore 11 | type ZRangeByScore struct { 12 | redis.BaseCmd 13 | key string 14 | min float64 15 | max float64 16 | withScores bool 17 | offset int 18 | count int 19 | } 20 | 21 | func ParseZRangeByScore(b redis.BaseCmd) (ZRangeByScore, error) { 22 | cmd := ZRangeByScore{BaseCmd: b} 23 | err := parser.New( 24 | parser.String(&cmd.key), 25 | parser.Float(&cmd.min), 26 | parser.Float(&cmd.max), 27 | parser.Flag("withscores", &cmd.withScores), 28 | parser.Named("limit", parser.Int(&cmd.offset), parser.Int(&cmd.count)), 29 | ).Required(3).Run(cmd.Args()) 30 | if err != nil { 31 | return ZRangeByScore{}, err 32 | } 33 | return cmd, nil 34 | } 35 | 36 | func (cmd ZRangeByScore) Run(w redis.Writer, red redis.Redka) (any, error) { 37 | rang := red.ZSet().RangeWith(cmd.key).ByScore(cmd.min, cmd.max) 38 | 39 | // limit and offset 40 | if cmd.offset > 0 { 41 | rang = rang.Offset(cmd.offset) 42 | } 43 | if cmd.count > 0 { 44 | rang = rang.Count(cmd.count) 45 | } 46 | 47 | // run the command 48 | items, err := rang.Run() 49 | if err != nil { 50 | w.WriteError(cmd.Error(err)) 51 | return nil, err 52 | } 53 | 54 | // write the response with/without scores 55 | if cmd.withScores { 56 | w.WriteArray(len(items) * 2) 57 | for _, item := range items { 58 | w.WriteBulk(item.Elem) 59 | redis.WriteFloat(w, item.Score) 60 | } 61 | } else { 62 | w.WriteArray(len(items)) 63 | for _, item := range items { 64 | w.WriteBulk(item.Elem) 65 | } 66 | } 67 | 68 | return items, nil 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - "docs/**" 8 | - README.md 9 | pull_request: 10 | branches: [main] 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | services: 17 | postgres: 18 | image: postgres:17-alpine 19 | env: 20 | POSTGRES_USER: redka 21 | POSTGRES_PASSWORD: redka 22 | POSTGRES_DB: redka 23 | options: >- 24 | --health-cmd "pg_isready --username=redka --dbname=redka --quiet" 25 | --health-interval 10s 26 | --health-timeout 5s 27 | --health-retries 5 28 | ports: 29 | - 5432:5432 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | 34 | - name: Setup Go 35 | uses: actions/setup-go@v5 36 | with: 37 | go-version-file: "go.mod" 38 | 39 | - name: Install dependencies 40 | run: | 41 | sudo apt-get update 42 | sudo apt-get install -y libsqlite3-dev 43 | go get . 44 | 45 | - name: Install linter 46 | uses: golangci/golangci-lint-action@v8 47 | 48 | - name: Build and test 49 | run: | 50 | make build 51 | make vet lint 52 | make test-sqlite 53 | make test-postgres 54 | 55 | - name: Upload artifact 56 | uses: actions/upload-artifact@v4 57 | with: 58 | name: redka 59 | path: build/redka 60 | retention-days: 7 61 | -------------------------------------------------------------------------------- /docs/usage-standalone.md: -------------------------------------------------------------------------------- 1 | # Using Redka as a standalone server 2 | 3 | Redka server is a single-file binary. After [downloading and unpacking](install-standalone.md) the release asset, run it as follows: 4 | 5 | ``` 6 | redka [-h host] [-p port] [-s unix-socket] [db-path] 7 | ``` 8 | 9 | For example: 10 | 11 | ```shell 12 | # Use in-memory sqlite database. 13 | ./redka 14 | 15 | # Use file sqlite database. 16 | ./redka redka.db 17 | 18 | # Listen on all network interfaces. 19 | ./redka -h 0.0.0.0 -p 6379 redka.db 20 | 21 | # Listen on unix socket. 22 | ./redka -s /tmp/redka.sock redka.db 23 | 24 | # Use postgres database. 25 | ./redka -p 6379 "postgres://redka:redka@localhost:5432/redka?sslmode=disable" 26 | ``` 27 | 28 | Server defaults are host `localhost`, port `6379` and empty DB path. The unix socket path, if given, overrides the host/port arguments. 29 | 30 | Running without a DB path creates an in-memory database. The data is not persisted in this case, and will be gone when the server is stopped. 31 | 32 | You can also run Redka with Docker as follows: 33 | 34 | ```shell 35 | # In-memory sqlite database. 36 | docker run --rm -p 6379:6379 nalgeon/redka 37 | 38 | # Persistent sqlite database 39 | # using the /path/to/data host directory. 40 | docker run --rm -p 6379:6379 -v /path/to/data:/data nalgeon/redka redka.db 41 | 42 | # Postgres database on host machine. 43 | docker run --rm -p 6379:6379 nalgeon/redka "postgres://redka:redka@host.docker.internal:5432/redka?sslmode=disable" 44 | ``` 45 | 46 | Server defaults in Docker are host `0.0.0.0`, port `6379` and empty DB path. 47 | 48 | Once the server is running, connect to it using `redis-cli` or an API client like `redis-py` or `go-redis` — just as you would with Redis. 49 | 50 | ```shell 51 | redis-cli -h localhost -p 6379 52 | ``` 53 | 54 | ```text 55 | 127.0.0.1:6379> echo hello 56 | "hello" 57 | 127.0.0.1:6379> set name alice 58 | OK 59 | 127.0.0.1:6379> get name 60 | "alice" 61 | ``` 62 | -------------------------------------------------------------------------------- /redsrv/internal/command/zset/zrevrangebyscore.go: -------------------------------------------------------------------------------- 1 | package zset 2 | 3 | import ( 4 | "github.com/nalgeon/redka/redsrv/internal/parser" 5 | "github.com/nalgeon/redka/redsrv/internal/redis" 6 | ) 7 | 8 | // Returns members in a sorted set within a range of scores in reverse order. 9 | // ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count] 10 | // https://redis.io/commands/zrangebyscore 11 | type ZRevRangeByScore struct { 12 | redis.BaseCmd 13 | key string 14 | min float64 15 | max float64 16 | withScores bool 17 | offset int 18 | count int 19 | } 20 | 21 | func ParseZRevRangeByScore(b redis.BaseCmd) (ZRevRangeByScore, error) { 22 | cmd := ZRevRangeByScore{BaseCmd: b} 23 | err := parser.New( 24 | parser.String(&cmd.key), 25 | parser.Float(&cmd.min), 26 | parser.Float(&cmd.max), 27 | parser.Flag("withscores", &cmd.withScores), 28 | parser.Named("limit", parser.Int(&cmd.offset), parser.Int(&cmd.count)), 29 | ).Required(3).Run(cmd.Args()) 30 | if err != nil { 31 | return ZRevRangeByScore{}, err 32 | } 33 | return cmd, nil 34 | } 35 | 36 | func (cmd ZRevRangeByScore) Run(w redis.Writer, red redis.Redka) (any, error) { 37 | rang := red.ZSet().RangeWith(cmd.key).ByScore(cmd.min, cmd.max).Desc() 38 | 39 | // limit and offset 40 | if cmd.offset > 0 { 41 | rang = rang.Offset(cmd.offset) 42 | } 43 | if cmd.count > 0 { 44 | rang = rang.Count(cmd.count) 45 | } 46 | 47 | // run the command 48 | items, err := rang.Run() 49 | if err != nil { 50 | w.WriteError(cmd.Error(err)) 51 | return nil, err 52 | } 53 | 54 | // write the response with/without scores 55 | if cmd.withScores { 56 | w.WriteArray(len(items) * 2) 57 | for _, item := range items { 58 | w.WriteBulk(item.Elem) 59 | redis.WriteFloat(w, item.Score) 60 | } 61 | } else { 62 | w.WriteArray(len(items)) 63 | for _, item := range items { 64 | w.WriteBulk(item.Elem) 65 | } 66 | } 67 | 68 | return items, nil 69 | } 70 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/ttl_test.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/nalgeon/be" 8 | "github.com/nalgeon/redka/redsrv/internal/redis" 9 | ) 10 | 11 | func TestTTLParse(t *testing.T) { 12 | tests := []struct { 13 | cmd string 14 | key string 15 | err error 16 | }{ 17 | { 18 | cmd: "ttl", 19 | key: "", 20 | err: redis.ErrInvalidArgNum, 21 | }, 22 | { 23 | cmd: "ttl name", 24 | key: "name", 25 | err: nil, 26 | }, 27 | { 28 | cmd: "ttl name age", 29 | key: "", 30 | err: redis.ErrInvalidArgNum, 31 | }, 32 | } 33 | 34 | for _, test := range tests { 35 | t.Run(test.cmd, func(t *testing.T) { 36 | cmd, err := redis.Parse(ParseTTL, test.cmd) 37 | be.Equal(t, err, test.err) 38 | if err == nil { 39 | be.Equal(t, cmd.key, test.key) 40 | } else { 41 | be.Equal(t, cmd, TTL{}) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestTTLExec(t *testing.T) { 48 | t.Run("has ttl", func(t *testing.T) { 49 | red := getRedka(t) 50 | _ = red.Str().SetExpire("name", "alice", 60*time.Second) 51 | 52 | cmd := redis.MustParse(ParseTTL, "ttl name") 53 | conn := redis.NewFakeConn() 54 | res, err := cmd.Run(conn, red) 55 | be.Err(t, err, nil) 56 | be.Equal(t, res, 60) 57 | be.Equal(t, conn.Out(), "60") 58 | }) 59 | 60 | t.Run("no ttl", func(t *testing.T) { 61 | red := getRedka(t) 62 | _ = red.Str().Set("name", "alice") 63 | 64 | cmd := redis.MustParse(ParseTTL, "ttl name") 65 | conn := redis.NewFakeConn() 66 | res, err := cmd.Run(conn, red) 67 | be.Err(t, err, nil) 68 | be.Equal(t, res, -1) 69 | be.Equal(t, conn.Out(), "-1") 70 | }) 71 | 72 | t.Run("not found", func(t *testing.T) { 73 | red := getRedka(t) 74 | 75 | cmd := redis.MustParse(ParseTTL, "ttl name") 76 | conn := redis.NewFakeConn() 77 | res, err := cmd.Run(conn, red) 78 | be.Err(t, err, nil) 79 | be.Equal(t, res, -2) 80 | be.Equal(t, conn.Out(), "-2") 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hvals_test.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "slices" 5 | "testing" 6 | 7 | "github.com/nalgeon/be" 8 | "github.com/nalgeon/redka/internal/core" 9 | "github.com/nalgeon/redka/redsrv/internal/redis" 10 | ) 11 | 12 | func TestHValsParse(t *testing.T) { 13 | tests := []struct { 14 | cmd string 15 | key string 16 | err error 17 | }{ 18 | { 19 | cmd: "hvals", 20 | key: "", 21 | err: redis.ErrInvalidArgNum, 22 | }, 23 | { 24 | cmd: "hvals person", 25 | key: "person", 26 | err: nil, 27 | }, 28 | { 29 | cmd: "hvals person name", 30 | key: "", 31 | err: redis.ErrInvalidArgNum, 32 | }, 33 | } 34 | 35 | for _, test := range tests { 36 | t.Run(test.cmd, func(t *testing.T) { 37 | cmd, err := redis.Parse(ParseHVals, test.cmd) 38 | be.Equal(t, err, test.err) 39 | if err == nil { 40 | be.Equal(t, cmd.key, test.key) 41 | } else { 42 | be.Equal(t, cmd, HVals{}) 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func TestHValsExec(t *testing.T) { 49 | t.Run("key found", func(t *testing.T) { 50 | red := getRedka(t) 51 | _, _ = red.Hash().Set("person", "name", "alice") 52 | _, _ = red.Hash().Set("person", "age", 25) 53 | 54 | cmd := redis.MustParse(ParseHVals, "hvals person") 55 | conn := redis.NewFakeConn() 56 | res, err := cmd.Run(conn, red) 57 | 58 | be.Err(t, err, nil) 59 | var got []string 60 | for _, val := range res.([]core.Value) { 61 | got = append(got, val.String()) 62 | } 63 | slices.Sort(got) 64 | be.Equal(t, got, []string{"25", "alice"}) 65 | be.True(t, conn.Out() == "2,25,alice" || conn.Out() == "2,alice,25") 66 | }) 67 | t.Run("key not found", func(t *testing.T) { 68 | red := getRedka(t) 69 | 70 | cmd := redis.MustParse(ParseHVals, "hvals person") 71 | conn := redis.NewFakeConn() 72 | res, err := cmd.Run(conn, red) 73 | 74 | be.Err(t, err, nil) 75 | be.Equal(t, res.([]core.Value), []core.Value{}) 76 | be.Equal(t, conn.Out(), "0") 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /redsrv/internal/command/hash/hgetall_test.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/internal/core" 8 | "github.com/nalgeon/redka/redsrv/internal/redis" 9 | ) 10 | 11 | func TestHGetAllParse(t *testing.T) { 12 | tests := []struct { 13 | cmd string 14 | key string 15 | err error 16 | }{ 17 | { 18 | cmd: "hgetall", 19 | key: "", 20 | err: redis.ErrInvalidArgNum, 21 | }, 22 | { 23 | cmd: "hgetall person", 24 | key: "person", 25 | err: nil, 26 | }, 27 | { 28 | cmd: "hgetall person name", 29 | key: "", 30 | err: redis.ErrInvalidArgNum, 31 | }, 32 | } 33 | 34 | for _, test := range tests { 35 | t.Run(test.cmd, func(t *testing.T) { 36 | cmd, err := redis.Parse(ParseHGetAll, test.cmd) 37 | be.Equal(t, err, test.err) 38 | if err == nil { 39 | be.Equal(t, cmd.key, test.key) 40 | } else { 41 | be.Equal(t, cmd, HGetAll{}) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestHGetAllExec(t *testing.T) { 48 | t.Run("key found", func(t *testing.T) { 49 | red := getRedka(t) 50 | _, _ = red.Hash().Set("person", "name", "alice") 51 | _, _ = red.Hash().Set("person", "age", 25) 52 | 53 | cmd := redis.MustParse(ParseHGetAll, "hgetall person") 54 | conn := redis.NewFakeConn() 55 | res, err := cmd.Run(conn, red) 56 | 57 | be.Err(t, err, nil) 58 | be.Equal(t, res.(map[string]core.Value), map[string]core.Value{ 59 | "name": core.Value("alice"), "age": core.Value("25"), 60 | }) 61 | be.Equal(t, 62 | conn.Out() == "4,name,alice,age,25" || conn.Out() == "4,age,25,name,alice", 63 | true) 64 | }) 65 | t.Run("key not found", func(t *testing.T) { 66 | red := getRedka(t) 67 | 68 | cmd := redis.MustParse(ParseHGetAll, "hgetall person") 69 | conn := redis.NewFakeConn() 70 | res, err := cmd.Run(conn, red) 71 | 72 | be.Err(t, err, nil) 73 | be.Equal(t, res.(map[string]core.Value), map[string]core.Value{}) 74 | be.Equal(t, conn.Out(), "0") 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/del_test.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/internal/core" 8 | "github.com/nalgeon/redka/redsrv/internal/redis" 9 | ) 10 | 11 | func TestDelParse(t *testing.T) { 12 | tests := []struct { 13 | cmd string 14 | want []string 15 | err error 16 | }{ 17 | { 18 | cmd: "del", 19 | want: nil, 20 | err: redis.ErrInvalidArgNum, 21 | }, 22 | { 23 | cmd: "del name", 24 | want: []string{"name"}, 25 | err: nil, 26 | }, 27 | { 28 | cmd: "del name age", 29 | want: []string{"name", "age"}, 30 | err: nil, 31 | }, 32 | } 33 | 34 | for _, test := range tests { 35 | t.Run(test.cmd, func(t *testing.T) { 36 | cmd, err := redis.Parse(ParseDel, test.cmd) 37 | be.Equal(t, err, test.err) 38 | if err == nil { 39 | be.Equal(t, cmd.keys, test.want) 40 | } else { 41 | be.Equal(t, cmd, Del{}) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestDelExec(t *testing.T) { 48 | tests := []struct { 49 | cmd string 50 | res any 51 | out string 52 | }{ 53 | { 54 | cmd: "del name", 55 | res: 1, 56 | out: "1", 57 | }, 58 | { 59 | cmd: "del name age", 60 | res: 2, 61 | out: "2", 62 | }, 63 | { 64 | cmd: "del name age street", 65 | res: 2, 66 | out: "2", 67 | }, 68 | } 69 | 70 | for _, test := range tests { 71 | t.Run(test.cmd, func(t *testing.T) { 72 | red := getRedka(t) 73 | 74 | _ = red.Str().Set("name", "alice") 75 | _ = red.Str().Set("age", 50) 76 | _ = red.Str().Set("city", "paris") 77 | 78 | conn := redis.NewFakeConn() 79 | cmd := redis.MustParse(ParseDel, test.cmd) 80 | res, err := cmd.Run(conn, red) 81 | be.Err(t, err, nil) 82 | be.Equal(t, res, test.res) 83 | be.Equal(t, conn.Out(), test.out) 84 | 85 | _, err = red.Str().Get("name") 86 | be.Err(t, err, core.ErrNotFound) 87 | city, _ := red.Str().Get("city") 88 | be.Equal(t, city.String(), "paris") 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/incr_test.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func TestIncrParse(t *testing.T) { 11 | tests := []struct { 12 | cmd string 13 | want Incr 14 | err error 15 | }{ 16 | { 17 | cmd: "incr", 18 | want: Incr{}, 19 | err: redis.ErrInvalidArgNum, 20 | }, 21 | { 22 | cmd: "incr age", 23 | want: Incr{key: "age", delta: 1}, 24 | err: nil, 25 | }, 26 | { 27 | cmd: "incr age 42", 28 | want: Incr{}, 29 | err: redis.ErrInvalidArgNum, 30 | }, 31 | } 32 | 33 | parse := func(b redis.BaseCmd) (Incr, error) { 34 | return ParseIncr(b, 1) 35 | } 36 | 37 | for _, test := range tests { 38 | t.Run(test.cmd, func(t *testing.T) { 39 | cmd, err := redis.Parse(parse, test.cmd) 40 | be.Equal(t, err, test.err) 41 | if err == nil { 42 | be.Equal(t, cmd.key, test.want.key) 43 | be.Equal(t, cmd.delta, test.want.delta) 44 | } else { 45 | be.Equal(t, cmd, test.want) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestIncrExec(t *testing.T) { 52 | parse := func(b redis.BaseCmd) (Incr, error) { 53 | return ParseIncr(b, 1) 54 | } 55 | 56 | t.Run("create", func(t *testing.T) { 57 | red := getRedka(t) 58 | 59 | cmd := redis.MustParse(parse, "incr age") 60 | conn := redis.NewFakeConn() 61 | res, err := cmd.Run(conn, red) 62 | be.Err(t, err, nil) 63 | be.Equal(t, res, 1) 64 | be.Equal(t, conn.Out(), "1") 65 | 66 | age, _ := red.Str().Get("age") 67 | be.Equal(t, age.MustInt(), 1) 68 | }) 69 | 70 | t.Run("incr", func(t *testing.T) { 71 | red := getRedka(t) 72 | _ = red.Str().Set("age", "25") 73 | 74 | cmd := redis.MustParse(parse, "incr age") 75 | conn := redis.NewFakeConn() 76 | res, err := cmd.Run(conn, red) 77 | be.Err(t, err, nil) 78 | be.Equal(t, res, 26) 79 | be.Equal(t, conn.Out(), "26") 80 | 81 | age, _ := red.Str().Get("age") 82 | be.Equal(t, age.MustInt(), 26) 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/decr_test.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func TestDecrParse(t *testing.T) { 11 | tests := []struct { 12 | cmd string 13 | want Incr 14 | err error 15 | }{ 16 | { 17 | cmd: "decr", 18 | want: Incr{}, 19 | err: redis.ErrInvalidArgNum, 20 | }, 21 | { 22 | cmd: "decr age", 23 | want: Incr{key: "age", delta: -1}, 24 | err: nil, 25 | }, 26 | { 27 | cmd: "decr age 42", 28 | want: Incr{}, 29 | err: redis.ErrInvalidArgNum, 30 | }, 31 | } 32 | 33 | parse := func(b redis.BaseCmd) (Incr, error) { 34 | return ParseIncr(b, -1) 35 | } 36 | 37 | for _, test := range tests { 38 | t.Run(test.cmd, func(t *testing.T) { 39 | cmd, err := redis.Parse(parse, test.cmd) 40 | be.Equal(t, err, test.err) 41 | if err == nil { 42 | be.Equal(t, cmd.key, test.want.key) 43 | be.Equal(t, cmd.delta, test.want.delta) 44 | } else { 45 | be.Equal(t, cmd, test.want) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestDecrExec(t *testing.T) { 52 | parse := func(b redis.BaseCmd) (Incr, error) { 53 | return ParseIncr(b, -1) 54 | } 55 | 56 | t.Run("create", func(t *testing.T) { 57 | red := getRedka(t) 58 | 59 | cmd := redis.MustParse(parse, "decr age") 60 | conn := redis.NewFakeConn() 61 | res, err := cmd.Run(conn, red) 62 | be.Err(t, err, nil) 63 | be.Equal(t, res, -1) 64 | be.Equal(t, conn.Out(), "-1") 65 | 66 | age, _ := red.Str().Get("age") 67 | be.Equal(t, age.MustInt(), -1) 68 | }) 69 | 70 | t.Run("decr", func(t *testing.T) { 71 | red := getRedka(t) 72 | _ = red.Str().Set("age", "25") 73 | 74 | cmd := redis.MustParse(parse, "decr age") 75 | conn := redis.NewFakeConn() 76 | res, err := cmd.Run(conn, red) 77 | be.Err(t, err, nil) 78 | be.Equal(t, res, 24) 79 | be.Equal(t, conn.Out(), "24") 80 | 81 | age, _ := red.Str().Get("age") 82 | be.Equal(t, age.MustInt(), 24) 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/setnx_test.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func TestSetNXParse(t *testing.T) { 11 | tests := []struct { 12 | cmd string 13 | want SetNX 14 | err error 15 | }{ 16 | { 17 | cmd: "setnx", 18 | want: SetNX{}, 19 | err: redis.ErrInvalidArgNum, 20 | }, 21 | { 22 | cmd: "setnx name", 23 | want: SetNX{}, 24 | err: redis.ErrInvalidArgNum, 25 | }, 26 | { 27 | cmd: "setnx name alice", 28 | want: SetNX{key: "name", value: []byte("alice")}, 29 | err: nil, 30 | }, 31 | { 32 | cmd: "setnx name alice 60", 33 | want: SetNX{}, 34 | err: redis.ErrInvalidArgNum, 35 | }, 36 | } 37 | 38 | for _, test := range tests { 39 | t.Run(test.cmd, func(t *testing.T) { 40 | cmd, err := redis.Parse(ParseSetNX, test.cmd) 41 | be.Equal(t, err, test.err) 42 | if err == nil { 43 | be.Equal(t, cmd.key, test.want.key) 44 | be.Equal(t, cmd.value, test.want.value) 45 | } else { 46 | be.Equal(t, cmd, test.want) 47 | } 48 | }) 49 | } 50 | } 51 | 52 | func TestSetNXExec(t *testing.T) { 53 | t.Run("create", func(t *testing.T) { 54 | red := getRedka(t) 55 | 56 | cmd := redis.MustParse(ParseSetNX, "setnx name alice") 57 | conn := redis.NewFakeConn() 58 | res, err := cmd.Run(conn, red) 59 | be.Err(t, err, nil) 60 | be.Equal(t, res, true) 61 | be.Equal(t, conn.Out(), "1") 62 | 63 | name, _ := red.Str().Get("name") 64 | be.Equal(t, name.String(), "alice") 65 | }) 66 | 67 | t.Run("update", func(t *testing.T) { 68 | red := getRedka(t) 69 | _ = red.Str().Set("name", "alice") 70 | 71 | cmd := redis.MustParse(ParseSetNX, "setnx name bob") 72 | conn := redis.NewFakeConn() 73 | res, err := cmd.Run(conn, red) 74 | be.Err(t, err, nil) 75 | be.Equal(t, res, false) 76 | be.Equal(t, conn.Out(), "0") 77 | 78 | name, _ := red.Str().Get("name") 79 | be.Equal(t, name.String(), "alice") 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /redsrv/internal/command/key/scan.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "github.com/nalgeon/redka/internal/core" 5 | "github.com/nalgeon/redka/redsrv/internal/parser" 6 | "github.com/nalgeon/redka/redsrv/internal/redis" 7 | ) 8 | 9 | const ( 10 | TypeHash = "hash" 11 | TypeList = "list" 12 | TypeSet = "set" 13 | TypeString = "string" 14 | TypeZSet = "zset" 15 | ) 16 | 17 | // Iterates over the key names in the database. 18 | // SCAN cursor [MATCH pattern] [COUNT count] 19 | // https://redis.io/commands/scan 20 | type Scan struct { 21 | redis.BaseCmd 22 | cursor int 23 | match string 24 | count int 25 | ktype string 26 | } 27 | 28 | func ParseScan(b redis.BaseCmd) (Scan, error) { 29 | cmd := Scan{BaseCmd: b} 30 | 31 | err := parser.New( 32 | parser.Int(&cmd.cursor), 33 | parser.Named("match", parser.String(&cmd.match)), 34 | parser.Named("count", parser.Int(&cmd.count)), 35 | parser.Named("type", parser.Enum(&cmd.ktype, 36 | TypeHash, TypeList, TypeSet, TypeString, TypeZSet)), 37 | ).Required(1).Run(cmd.Args()) 38 | if err != nil { 39 | return Scan{}, err 40 | } 41 | 42 | // all keys by default 43 | if cmd.match == "" { 44 | cmd.match = "*" 45 | } 46 | 47 | return cmd, nil 48 | } 49 | 50 | func (cmd Scan) Run(w redis.Writer, red redis.Redka) (any, error) { 51 | res, err := red.Key().Scan(cmd.cursor, cmd.match, toTypeID(cmd.ktype), cmd.count) 52 | if err != nil { 53 | w.WriteError(cmd.Error(err)) 54 | return nil, err 55 | } 56 | 57 | w.WriteArray(2) 58 | w.WriteInt(res.Cursor) 59 | w.WriteArray(len(res.Keys)) 60 | for _, k := range res.Keys { 61 | w.WriteBulkString(k.Key) 62 | } 63 | return res, nil 64 | } 65 | 66 | func toTypeID(ktype string) core.TypeID { 67 | switch ktype { 68 | case TypeHash: 69 | return core.TypeHash 70 | case TypeList: 71 | return core.TypeList 72 | case TypeSet: 73 | return core.TypeSet 74 | case TypeString: 75 | return core.TypeString 76 | case TypeZSet: 77 | return core.TypeZSet 78 | default: 79 | return core.TypeAny 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /redsrv/internal/parser/pipeline.go: -------------------------------------------------------------------------------- 1 | // Package parser implements command arguments parsing. 2 | package parser 3 | 4 | import ( 5 | "errors" 6 | ) 7 | 8 | var ( 9 | ErrInvalidArgNum = errors.New("ERR wrong number of arguments") 10 | ErrInvalidFloat = errors.New("ERR value is not a float") 11 | ErrInvalidInt = errors.New("ERR value is not an integer") 12 | ErrSyntaxError = errors.New("ERR syntax error") 13 | ) 14 | 15 | // ParserFunc parses some of the arguments and returns the rest. 16 | type ParserFunc func(args [][]byte) (bool, [][]byte, error) 17 | 18 | // Pipeline parses command arguments according to a sequence of parsers. 19 | type Pipeline struct { 20 | parsers []ParserFunc 21 | nRequired int 22 | } 23 | 24 | // New creates a new pipeline with the given parsers. 25 | func New(parsers ...ParserFunc) *Pipeline { 26 | return &Pipeline{parsers: parsers} 27 | } 28 | 29 | // Required sets the number of required positional arguments. 30 | func (p *Pipeline) Required(n int) *Pipeline { 31 | p.nRequired = n 32 | return p 33 | } 34 | 35 | // Run parses the arguments according to the pipeline. 36 | func (p *Pipeline) Run(args [][]byte) error { 37 | if len(args) < p.nRequired { 38 | return ErrInvalidArgNum 39 | } 40 | 41 | // Named arguments order is not guaranteed, 42 | // so we need to try all parsers for each argument. 43 | for len(args) > 0 && len(p.parsers) > 0 { 44 | var err error 45 | var fired bool 46 | firedIdx := -1 47 | 48 | // Try all parsers until one fires. 49 | for i, parser := range p.parsers { 50 | fired, args, err = parser(args) 51 | if err != nil { 52 | return err 53 | } 54 | if fired { 55 | firedIdx = i 56 | break 57 | } 58 | } 59 | 60 | if firedIdx == -1 { 61 | // No parser fired. 62 | break 63 | } 64 | 65 | // Remove the fired parser. 66 | p.parsers = append(p.parsers[:firedIdx], p.parsers[firedIdx+1:]...) 67 | } 68 | 69 | // Check if all arguments were parsed. 70 | if len(args) != 0 { 71 | return ErrSyntaxError 72 | } 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/incrby_test.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func TestIncrByParse(t *testing.T) { 11 | tests := []struct { 12 | cmd string 13 | want IncrBy 14 | err error 15 | }{ 16 | { 17 | cmd: "incrby", 18 | want: IncrBy{}, 19 | err: redis.ErrInvalidArgNum, 20 | }, 21 | { 22 | cmd: "incrby age", 23 | want: IncrBy{}, 24 | err: redis.ErrInvalidArgNum, 25 | }, 26 | { 27 | cmd: "incrby age 42", 28 | want: IncrBy{key: "age", delta: 42}, 29 | err: nil, 30 | }, 31 | } 32 | 33 | parse := func(b redis.BaseCmd) (IncrBy, error) { 34 | return ParseIncrBy(b, 1) 35 | } 36 | 37 | for _, test := range tests { 38 | t.Run(test.cmd, func(t *testing.T) { 39 | cmd, err := redis.Parse(parse, test.cmd) 40 | be.Equal(t, err, test.err) 41 | if err == nil { 42 | be.Equal(t, cmd.key, test.want.key) 43 | be.Equal(t, cmd.delta, test.want.delta) 44 | } else { 45 | be.Equal(t, cmd, test.want) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestIncrByExec(t *testing.T) { 52 | parse := func(b redis.BaseCmd) (IncrBy, error) { 53 | return ParseIncrBy(b, 1) 54 | } 55 | 56 | t.Run("create", func(t *testing.T) { 57 | red := getRedka(t) 58 | 59 | cmd := redis.MustParse(parse, "incrby age 42") 60 | conn := redis.NewFakeConn() 61 | res, err := cmd.Run(conn, red) 62 | be.Err(t, err, nil) 63 | be.Equal(t, res, 42) 64 | be.Equal(t, conn.Out(), "42") 65 | 66 | age, _ := red.Str().Get("age") 67 | be.Equal(t, age.MustInt(), 42) 68 | }) 69 | 70 | t.Run("incrby", func(t *testing.T) { 71 | red := getRedka(t) 72 | _ = red.Str().Set("age", "25") 73 | 74 | cmd := redis.MustParse(parse, "incrby age 42") 75 | conn := redis.NewFakeConn() 76 | res, err := cmd.Run(conn, red) 77 | be.Err(t, err, nil) 78 | be.Equal(t, res, 67) 79 | be.Equal(t, conn.Out(), "67") 80 | 81 | age, _ := red.Str().Get("age") 82 | be.Equal(t, age.MustInt(), 67) 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/decrby_test.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/redsrv/internal/redis" 8 | ) 9 | 10 | func TestDecrByParse(t *testing.T) { 11 | tests := []struct { 12 | cmd string 13 | want IncrBy 14 | err error 15 | }{ 16 | { 17 | cmd: "decrby", 18 | want: IncrBy{}, 19 | err: redis.ErrInvalidArgNum, 20 | }, 21 | { 22 | cmd: "decrby age", 23 | want: IncrBy{}, 24 | err: redis.ErrInvalidArgNum, 25 | }, 26 | { 27 | cmd: "decrby age 42", 28 | want: IncrBy{key: "age", delta: -42}, 29 | err: nil, 30 | }, 31 | } 32 | 33 | parse := func(b redis.BaseCmd) (IncrBy, error) { 34 | return ParseIncrBy(b, -1) 35 | } 36 | 37 | for _, test := range tests { 38 | t.Run(test.cmd, func(t *testing.T) { 39 | cmd, err := redis.Parse(parse, test.cmd) 40 | be.Equal(t, err, test.err) 41 | if err == nil { 42 | be.Equal(t, cmd.key, test.want.key) 43 | be.Equal(t, cmd.delta, test.want.delta) 44 | } else { 45 | be.Equal(t, cmd, test.want) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestDecrByExec(t *testing.T) { 52 | parse := func(b redis.BaseCmd) (IncrBy, error) { 53 | return ParseIncrBy(b, -1) 54 | } 55 | 56 | t.Run("create", func(t *testing.T) { 57 | red := getRedka(t) 58 | 59 | cmd := redis.MustParse(parse, "decrby age 12") 60 | conn := redis.NewFakeConn() 61 | res, err := cmd.Run(conn, red) 62 | be.Err(t, err, nil) 63 | be.Equal(t, res, -12) 64 | be.Equal(t, conn.Out(), "-12") 65 | 66 | age, _ := red.Str().Get("age") 67 | be.Equal(t, age.MustInt(), -12) 68 | }) 69 | 70 | t.Run("decrby", func(t *testing.T) { 71 | red := getRedka(t) 72 | _ = red.Str().Set("age", "25") 73 | 74 | cmd := redis.MustParse(parse, "decrby age 12") 75 | conn := redis.NewFakeConn() 76 | res, err := cmd.Run(conn, red) 77 | be.Err(t, err, nil) 78 | be.Equal(t, res, 13) 79 | be.Equal(t, conn.Out(), "13") 80 | 81 | age, _ := red.Str().Get("age") 82 | be.Equal(t, age.MustInt(), 13) 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | 3 | has_git := $(shell command -v git 2>/dev/null) 4 | 5 | ifdef has_git 6 | build_rev := $(shell git rev-parse --short HEAD) 7 | git_tag := $(shell git describe --tags --exact-match 2>/dev/null) 8 | else 9 | build_rev := unknown 10 | endif 11 | 12 | ifdef git_tag 13 | build_ver := $(git_tag) 14 | else 15 | build_ver := main 16 | endif 17 | 18 | build_date := $(shell date -u '+%Y-%m-%dT%H:%M:%S') 19 | 20 | setup: 21 | @go mod download 22 | 23 | vet: 24 | @echo "> running vet..." 25 | @go vet ./... 26 | @echo "✓ finished vet" 27 | 28 | lint: 29 | @echo "> running lint..." 30 | @golangci-lint run ./... 31 | @echo "✓ finished lint" 32 | 33 | test: 34 | @echo "> running tests with $(driver) driver..." 35 | @go test -tags=$(driver) ./... 36 | @echo "✓ finished tests" 37 | 38 | test-sqlite: 39 | @echo "> running tests with sqlite driver..." 40 | @go test -tags=sqlite3 ./... 41 | @echo "✓ finished tests" 42 | 43 | test-postgres: 44 | @echo "> running tests with postgres driver..." 45 | @go test -tags=postgres -p=1 ./... 46 | @echo "✓ finished tests" 47 | 48 | build: 49 | @echo "> running build..." 50 | @CGO_ENABLED=1 go build -ldflags "-s -w -X main.version=$(build_ver) -X main.commit=$(build_rev) -X main.date=$(build_date)" -trimpath -o build/redka -v cmd/redka/main.go 51 | @echo "✓ finished build" 52 | 53 | run: 54 | @./build/redka 55 | 56 | postgres-start: 57 | @echo "> starting postgres..." 58 | @docker run --rm --detach --name=redka-postgres \ 59 | --env=POSTGRES_DB=redka \ 60 | --env=POSTGRES_USER=redka \ 61 | --env=POSTGRES_PASSWORD=redka \ 62 | --publish=5432:5432 \ 63 | --tmpfs /var/lib/postgresql/data \ 64 | postgres:17-alpine 65 | @until docker exec redka-postgres \ 66 | pg_isready --username=redka --dbname=redka --quiet --quiet; \ 67 | do sleep 1; done 68 | @echo "✓ started postgres" 69 | 70 | postgres-stop: 71 | @echo "> stopping postgres..." 72 | @docker stop redka-postgres 73 | @echo "✓ stopped postgres" 74 | 75 | postgres-shell: 76 | @docker exec -it redka-postgres psql --username=redka --dbname=redka -------------------------------------------------------------------------------- /redsrv/internal/redis/conn.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "net" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/tidwall/redcon" 9 | ) 10 | 11 | type fakeConn struct { 12 | parts []string 13 | ctx any 14 | } 15 | 16 | // NewFakeConn creates a new fake connection for testing. 17 | func NewFakeConn() *fakeConn { 18 | return &fakeConn{} 19 | } 20 | 21 | func (c *fakeConn) RemoteAddr() string { 22 | return "" 23 | } 24 | func (c *fakeConn) Close() error { 25 | return nil 26 | } 27 | func (c *fakeConn) WriteError(msg string) { 28 | c.append(msg) 29 | } 30 | func (c *fakeConn) WriteString(str string) { 31 | c.append(str) 32 | } 33 | func (c *fakeConn) WriteBulk(bulk []byte) { 34 | c.append(string(bulk)) 35 | } 36 | func (c *fakeConn) WriteBulkString(bulk string) { 37 | c.append(bulk) 38 | } 39 | func (c *fakeConn) WriteInt(num int) { 40 | c.append(strconv.Itoa(num)) 41 | } 42 | func (c *fakeConn) WriteInt64(num int64) { 43 | c.append(strconv.FormatInt(num, 10)) 44 | } 45 | func (c *fakeConn) WriteUint64(num uint64) { 46 | c.append(strconv.FormatUint(num, 10)) 47 | } 48 | func (c *fakeConn) WriteArray(count int) { 49 | c.append(strconv.Itoa(count)) 50 | } 51 | func (c *fakeConn) WriteNull() { 52 | c.append("(nil)") 53 | } 54 | func (c *fakeConn) WriteRaw(data []byte) { 55 | c.append(string(data)) 56 | } 57 | func (c *fakeConn) WriteAny(any interface{}) { 58 | c.append(any.(string)) 59 | } 60 | func (c *fakeConn) Context() interface{} { 61 | return c.ctx 62 | } 63 | func (c *fakeConn) SetContext(v interface{}) { 64 | c.ctx = v 65 | } 66 | func (c *fakeConn) SetReadBuffer(bytes int) {} 67 | func (c *fakeConn) Detach() redcon.DetachedConn { 68 | return nil 69 | } 70 | func (c *fakeConn) ReadPipeline() []redcon.Command { 71 | return nil 72 | } 73 | func (c *fakeConn) PeekPipeline() []redcon.Command { 74 | return nil 75 | } 76 | func (c *fakeConn) NetConn() net.Conn { 77 | return nil 78 | } 79 | func (c *fakeConn) append(str string) { 80 | c.parts = append(c.parts, str) 81 | } 82 | func (c *fakeConn) Out() string { 83 | return strings.Join(c.parts, ",") 84 | } 85 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/mget_test.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/internal/core" 8 | "github.com/nalgeon/redka/redsrv/internal/redis" 9 | ) 10 | 11 | func TestMGetParse(t *testing.T) { 12 | tests := []struct { 13 | cmd string 14 | want []string 15 | err error 16 | }{ 17 | { 18 | cmd: "mget", 19 | want: nil, 20 | err: redis.ErrInvalidArgNum, 21 | }, 22 | { 23 | cmd: "mget name", 24 | want: []string{"name"}, 25 | err: nil, 26 | }, 27 | { 28 | cmd: "mget name age", 29 | want: []string{"name", "age"}, 30 | err: nil, 31 | }, 32 | } 33 | 34 | for _, test := range tests { 35 | t.Run(test.cmd, func(t *testing.T) { 36 | cmd, err := redis.Parse(ParseMGet, test.cmd) 37 | be.Equal(t, err, test.err) 38 | if err == nil { 39 | be.Equal(t, cmd.keys, test.want) 40 | } else { 41 | be.Equal(t, cmd, MGet{}) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestMGetExec(t *testing.T) { 48 | red := getRedka(t) 49 | 50 | _ = red.Str().Set("name", "alice") 51 | _ = red.Str().Set("age", 25) 52 | 53 | tests := []struct { 54 | cmd string 55 | res any 56 | out string 57 | }{ 58 | { 59 | cmd: "mget name", 60 | res: []core.Value{core.Value("alice")}, 61 | out: "1,alice", 62 | }, 63 | { 64 | cmd: "mget name age", 65 | res: []core.Value{core.Value("alice"), core.Value("25")}, 66 | out: "2,alice,25", 67 | }, 68 | { 69 | cmd: "mget name city age", 70 | res: []core.Value{core.Value("alice"), core.Value(nil), core.Value("25")}, 71 | out: "3,alice,(nil),25", 72 | }, 73 | { 74 | cmd: "mget one two", 75 | res: []core.Value{core.Value(nil), core.Value(nil)}, 76 | out: "2,(nil),(nil)", 77 | }, 78 | } 79 | 80 | for _, test := range tests { 81 | t.Run(test.cmd, func(t *testing.T) { 82 | conn := redis.NewFakeConn() 83 | cmd := redis.MustParse(ParseMGet, test.cmd) 84 | res, err := cmd.Run(conn, red) 85 | be.Err(t, err, nil) 86 | be.Equal(t, res, test.res) 87 | be.Equal(t, conn.Out(), test.out) 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /internal/sqlx/sql.go: -------------------------------------------------------------------------------- 1 | // Package sqlx provides base types and helper functions 2 | // to work with SQL databases. 3 | package sqlx 4 | 5 | import ( 6 | "database/sql" 7 | "strings" 8 | ) 9 | 10 | // Sorting direction. 11 | const ( 12 | Asc = "asc" 13 | Desc = "desc" 14 | ) 15 | 16 | // Aggregation functions. 17 | const ( 18 | Sum = "sum" 19 | Min = "min" 20 | Max = "max" 21 | ) 22 | 23 | // Tx is a database transaction (or a transaction-like object). 24 | // This is an abstraction over sql.Tx and sql.DB. 25 | type Tx interface { 26 | Query(query string, args ...any) (*sql.Rows, error) 27 | QueryRow(query string, args ...any) *sql.Row 28 | Exec(query string, args ...any) (sql.Result, error) 29 | } 30 | 31 | // rowScanner is an interface to scan rows. 32 | type RowScanner interface { 33 | Scan(dest ...any) error 34 | } 35 | 36 | // InferDialect infers the SQL dialect from the driver name. 37 | func InferDialect(driverName string) Dialect { 38 | if driverName == "postgres" || driverName == "pgx" { 39 | return DialectPostgres 40 | } 41 | if strings.HasPrefix(driverName, "sqlite") { 42 | return DialectSqlite 43 | } 44 | return DialectUnknown 45 | } 46 | 47 | // ExpandIn expands the IN clause in the query for a given parameter. 48 | func ExpandIn[T any](query string, param string, args []T) (string, []any) { 49 | anyArgs := make([]any, len(args)) 50 | pholders := make([]string, len(args)) 51 | for i, arg := range args { 52 | anyArgs[i] = arg 53 | pholders[i] = "?" 54 | } 55 | query = strings.Replace(query, param, strings.Join(pholders, ","), 1) 56 | return query, anyArgs 57 | } 58 | 59 | func Select[T any](db Tx, query string, args []any, 60 | scan func(rows *sql.Rows) (T, error)) ([]T, error) { 61 | 62 | rows, err := db.Query(query, args...) 63 | if err != nil { 64 | return nil, err 65 | } 66 | defer func() { _ = rows.Close() }() 67 | 68 | var vals []T 69 | for rows.Next() { 70 | v, err := scan(rows) 71 | if err != nil { 72 | return nil, err 73 | } 74 | vals = append(vals, v) 75 | } 76 | if err = rows.Err(); err != nil { 77 | return nil, err 78 | } 79 | 80 | return vals, err 81 | } 82 | -------------------------------------------------------------------------------- /redsrv/internal/command/string/getset_test.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nalgeon/be" 7 | "github.com/nalgeon/redka/internal/core" 8 | "github.com/nalgeon/redka/redsrv/internal/redis" 9 | ) 10 | 11 | func TestGetSetParse(t *testing.T) { 12 | tests := []struct { 13 | cmd string 14 | want GetSet 15 | err error 16 | }{ 17 | { 18 | cmd: "getset", 19 | want: GetSet{}, 20 | err: redis.ErrInvalidArgNum, 21 | }, 22 | { 23 | cmd: "getset name", 24 | want: GetSet{}, 25 | err: redis.ErrInvalidArgNum, 26 | }, 27 | { 28 | cmd: "getset name alice", 29 | want: GetSet{key: "name", value: []byte("alice")}, 30 | err: nil, 31 | }, 32 | { 33 | cmd: "getset name alice 60", 34 | want: GetSet{}, 35 | err: redis.ErrInvalidArgNum, 36 | }, 37 | } 38 | 39 | for _, test := range tests { 40 | t.Run(test.cmd, func(t *testing.T) { 41 | cmd, err := redis.Parse(ParseGetSet, test.cmd) 42 | be.Equal(t, err, test.err) 43 | if err == nil { 44 | be.Equal(t, cmd.key, test.want.key) 45 | be.Equal(t, cmd.value, test.want.value) 46 | } else { 47 | be.Equal(t, cmd, test.want) 48 | } 49 | }) 50 | } 51 | } 52 | 53 | func TestGetSetExec(t *testing.T) { 54 | t.Run("create", func(t *testing.T) { 55 | red := getRedka(t) 56 | 57 | cmd := redis.MustParse(ParseGetSet, "getset name alice") 58 | conn := redis.NewFakeConn() 59 | res, err := cmd.Run(conn, red) 60 | be.Err(t, err, nil) 61 | be.Equal(t, res.(core.Value), core.Value(nil)) 62 | be.Equal(t, conn.Out(), "(nil)") 63 | 64 | name, _ := red.Str().Get("name") 65 | be.Equal(t, name.String(), "alice") 66 | }) 67 | 68 | t.Run("update", func(t *testing.T) { 69 | red := getRedka(t) 70 | _ = red.Str().Set("name", "alice") 71 | 72 | cmd := redis.MustParse(ParseGetSet, "getset name bob") 73 | conn := redis.NewFakeConn() 74 | res, err := cmd.Run(conn, red) 75 | be.Err(t, err, nil) 76 | be.Equal(t, res.(core.Value), core.Value("alice")) 77 | be.Equal(t, conn.Out(), "alice") 78 | 79 | name, _ := red.Str().Get("name") 80 | be.Equal(t, name.String(), "bob") 81 | }) 82 | } 83 | --------------------------------------------------------------------------------