+
DICEDB BLOG
+
Explore our insights
+
+ Explore our blogs to stay informed on internal details, announcements, and
+ use cases; and see what we are up to.
+
+
+
{
- site.featureHighlights.map((s) => (
-
-
-
{s.title}
-
{s.description}
+ recentBlogs.map((blog) => {
+ return (
+
-
- ))
+ );
+ })
}
diff --git a/docs/src/pages/redis-compatability.astro b/docs/src/pages/redis-compatability.astro
new file mode 100644
index 000000000..ff62bb602
--- /dev/null
+++ b/docs/src/pages/redis-compatability.astro
@@ -0,0 +1,1424 @@
+---
+import Layout from "../layouts/Layout.astro";
+const title = "DiceDB's Redis Compatability";
+const description = "";
+---
+
+
+
+
+
+
36.4%
+
Redis 7.4.1 compatability
+
+
+
+
+
+
+
+
+ Supported Commands
+ Unsupported Commands
+ New commands in DiceDB
+
+
+ APPEND
+ ACL
+ ABORT
+
+
+ AUTH
+ ACL|CAT
+ BF.ADD
+
+
+ BGREWRITEAOF
+ ACL|DELUSER
+ BF.EXISTS
+
+
+ BITCOUNT
+ ACL|DRYRUN
+ BF.INFO
+
+
+ BITFIELD
+ ACL|GENPASS
+ BF.RESERVE
+
+
+ BITFIELD_RO
+ ACL|GETUSER
+ CMS.INCRBY
+
+
+ BITOP
+ ACL|HELP
+ CMS.INFO
+
+
+ BITPOS
+ ACL|LIST
+ CMS.INITBYDIM
+
+
+ CLIENT
+ ACL|LOAD
+ CMS.INITBYPROB
+
+
+ COMMAND
+ ACL|LOG
+ CMS.MERGE
+
+
+ COMMAND|COUNT
+ ACL|SAVE
+ CMS.QUERY
+
+
+ COMMAND|DOCS
+ ACL|SETUSER
+ JSON.ARRAPPEND
+
+
+ COMMAND|GETKEYS
+ ACL|USERS
+ JSON.ARRINSERT
+
+
+ COMMAND|GETKEYSANDFLAGS
+ ACL|WHOAMI
+ JSON.ARRLEN
+
+
+ COMMAND|HELP
+ ASKING
+ JSON.ARRPOP
+
+
+ COMMAND|INFO
+ BGSAVE
+ JSON.ARRTRIM
+
+
+ COMMAND|LIST
+ BLMOVE
+ JSON.CLEAR
+
+
+ COPY
+ BLMPOP
+ JSON.DEBUG
+
+
+ DBSIZE
+ BLPOP
+ JSON.DEL
+
+
+ DECR
+ BRPOP
+ JSON.FORGET
+
+
+ DECRBY
+ BRPOPLPUSH
+ JSON.GET
+
+
+ DEL
+ BZMPOP
+ JSON.INGEST
+
+
+ DISCARD
+ BZPOPMAX
+ JSON.MGET
+
+
+ DUMP
+ BZPOPMIN
+ JSON.NUMINCRBY
+
+
+ ECHO
+ CLIENT|CACHING
+ JSON.NUMMULTBY
+
+
+ EXEC
+ CLIENT|GETNAME
+ JSON.OBJKEYS
+
+
+ EXISTS
+ CLIENT|GETREDIR
+ JSON.OBJLEN
+
+
+ EXPIRE
+ CLIENT|HELP
+ JSON.RESP
+
+
+ EXPIREAT
+ CLIENT|ID
+ JSON.SET
+
+
+ EXPIRETIME
+ CLIENT|INFO
+ JSON.STRAPPEND
+
+
+ FLUSHDB
+ CLIENT|KILL
+ JSON.STRLEN
+
+
+ GEOADD
+ CLIENT|LIST
+ JSON.TOGGLE
+
+
+ GEODIST
+ CLIENT|NO-EVICT
+ JSON.TYPE
+
+
+ GET
+ CLIENT|NO-TOUCH
+ LRU
+
+
+ GETBIT
+ CLIENT|PAUSE
+ Q.UNWATCH
+
+
+ GETDEL
+ CLIENT|REPLY
+ Q.WATCH
+
+
+ GETEX
+ CLIENT|SETINFO
+ SLEEP
+
+
+ GETRANGE
+ CLIENT|SETNAME
+
+
+
+ GETSET
+ CLIENT|TRACKING
+
+
+
+ HDEL
+ CLIENT|TRACKINGINFO
+
+
+
+ HELLO
+ CLIENT|UNBLOCK
+
+
+
+ HEXISTS
+ CLIENT|UNPAUSE
+
+
+
+ HGET
+ CLUSTER
+
+
+
+ HGETALL
+ CLUSTER|ADDSLOTS
+
+
+
+ HINCRBY
+ CLUSTER|ADDSLOTSRANGE
+
+
+
+ HINCRBYFLOAT
+ CLUSTER|BUMPEPOCH
+
+
+
+ HKEYS
+ CLUSTER|COUNT-FAILURE-REPORTS
+
+
+
+ HLEN
+ CLUSTER|COUNTKEYSINSLOT
+
+
+
+ HMGET
+ CLUSTER|DELSLOTS
+
+
+
+ HMSET
+ CLUSTER|DELSLOTSRANGE
+
+
+
+ HRANDFIELD
+ CLUSTER|FAILOVER
+
+
+
+ HSCAN
+ CLUSTER|FLUSHSLOTS
+
+
+
+ HSET
+ CLUSTER|FORGET
+
+
+
+ HSETNX
+ CLUSTER|GETKEYSINSLOT
+
+
+
+ HSTRLEN
+ CLUSTER|HELP
+
+
+
+ HVALS
+ CLUSTER|INFO
+
+
+
+ INCR
+ CLUSTER|KEYSLOT
+
+
+
+ INCRBY
+ CLUSTER|LINKS
+
+
+
+ INCRBYFLOAT
+ CLUSTER|MEET
+
+
+
+ INFO
+ CLUSTER|MYID
+
+
+
+ KEYS
+ CLUSTER|MYSHARDID
+
+
+
+ LATENCY
+ CLUSTER|NODES
+
+
+
+ LLEN
+ CLUSTER|REPLICAS
+
+
+
+ LPOP
+ CLUSTER|REPLICATE
+
+
+
+ LPUSH
+ CLUSTER|RESET
+
+
+
+ MGET
+ CLUSTER|SAVECONFIG
+
+
+
+ MSET
+ CLUSTER|SET-CONFIG-EPOCH
+
+
+
+ MULTI
+ CLUSTER|SETSLOT
+
+
+
+ OBJECT
+ CLUSTER|SHARDS
+
+
+
+ PERSIST
+ CLUSTER|SLAVES
+
+
+
+ PFADD
+ CLUSTER|SLOTS
+
+
+
+ PFCOUNT
+ CONFIG
+
+
+
+ PFMERGE
+ CONFIG|GET
+
+
+
+ PING
+ CONFIG|HELP
+
+
+
+ PTTL
+ CONFIG|RESETSTAT
+
+
+
+ RENAME
+ CONFIG|REWRITE
+
+
+
+ RESTORE
+ CONFIG|SET
+
+
+
+ RPOP
+ DEBUG
+
+
+
+ RPUSH
+ EVAL
+
+
+
+ SADD
+ EVALSHA
+
+
+
+ SCARD
+ EVALSHA_RO
+
+
+
+ SDIFF
+ EVAL_RO
+
+
+
+ SELECT
+ FAILOVER
+
+
+
+ SET
+ FCALL
+
+
+
+ SETBIT
+ FCALL_RO
+
+
+
+ SETEX
+ FLUSHALL
+
+
+
+ SINTER
+ FUNCTION
+
+
+
+ SMEMBERS
+ FUNCTION|DELETE
+
+
+
+ SREM
+ FUNCTION|DUMP
+
+
+
+ SUBSCRIBE
+ FUNCTION|FLUSH
+
+
+
+ TOUCH
+ FUNCTION|HELP
+
+
+
+ TTL
+ FUNCTION|KILL
+
+
+
+ TYPE
+ FUNCTION|LIST
+
+
+
+ ZADD
+ FUNCTION|LOAD
+
+
+
+ ZCARD
+ FUNCTION|RESTORE
+
+
+
+ ZCOUNT
+ FUNCTION|STATS
+
+
+
+ ZPOPMAX
+ GEOHASH
+
+
+
+ ZPOPMIN
+ GEOPOS
+
+
+
+ ZRANGE
+ GEORADIUS
+
+
+
+ ZRANK
+ GEORADIUSBYMEMBER
+
+
+
+ ZREM
+ GEORADIUSBYMEMBER_RO
+
+
+
+
+ GEORADIUS_RO
+
+
+
+
+ GEOSEARCH
+
+
+
+
+ GEOSEARCHSTORE
+
+
+
+
+ HEXPIRE
+
+
+
+
+ HEXPIREAT
+
+
+
+
+ HEXPIRETIME
+
+
+
+
+ HPERSIST
+
+
+
+
+ HPEXPIRE
+
+
+
+
+ HPEXPIREAT
+
+
+
+
+ HPEXPIRETIME
+
+
+
+
+ HPTTL
+
+
+
+
+ HTTL
+
+
+
+
+ LASTSAVE
+
+
+
+
+ LATENCY|DOCTOR
+
+
+
+
+ LATENCY|GRAPH
+
+
+
+
+ LATENCY|HELP
+
+
+
+
+ LATENCY|HISTOGRAM
+
+
+
+
+ LATENCY|HISTORY
+
+
+
+
+ LATENCY|LATEST
+
+
+
+
+ LATENCY|RESET
+
+
+
+
+ LCS
+
+
+
+
+ LINDEX
+
+
+
+
+ LINSERT
+
+
+
+
+ LMOVE
+
+
+
+
+ LMPOP
+
+
+
+
+ LOLWUT
+
+
+
+
+ LPOS
+
+
+
+
+ LPUSHX
+
+
+
+
+ LRANGE
+
+
+
+
+ LREM
+
+
+
+
+ LSET
+
+
+
+
+ LTRIM
+
+
+
+
+ MEMORY
+
+
+
+
+ MEMORY|DOCTOR
+
+
+
+
+ MEMORY|HELP
+
+
+
+
+ MEMORY|MALLOC-STATS
+
+
+
+
+ MEMORY|PURGE
+
+
+
+
+ MEMORY|STATS
+
+
+
+
+ MEMORY|USAGE
+
+
+
+
+ MIGRATE
+
+
+
+
+ MODULE
+
+
+
+
+ MODULE|HELP
+
+
+
+
+ MODULE|LIST
+
+
+
+
+ MODULE|LOAD
+
+
+
+
+ MODULE|LOADEX
+
+
+
+
+ MODULE|UNLOAD
+
+
+
+
+ MONITOR
+
+
+
+
+ MOVE
+
+
+
+
+ MSETNX
+
+
+
+
+ OBJECT|ENCODING
+
+
+
+
+ OBJECT|FREQ
+
+
+
+
+ OBJECT|HELP
+
+
+
+
+ OBJECT|IDLETIME
+
+
+
+
+ OBJECT|REFCOUNT
+
+
+
+
+ PEXPIRE
+
+
+
+
+ PEXPIREAT
+
+
+
+
+ PEXPIRETIME
+
+
+
+
+ PFDEBUG
+
+
+
+
+ PFSELFTEST
+
+
+
+
+ PSETEX
+
+
+
+
+ PSUBSCRIBE
+
+
+
+
+ PSYNC
+
+
+
+
+ PUBLISH
+
+
+
+
+ PUBSUB
+
+
+
+
+ PUBSUB|CHANNELS
+
+
+
+
+ PUBSUB|HELP
+
+
+
+
+ PUBSUB|NUMPAT
+
+
+
+
+ PUBSUB|NUMSUB
+
+
+
+
+ PUBSUB|SHARDCHANNELS
+
+
+
+
+ PUBSUB|SHARDNUMSUB
+
+
+
+
+ PUNSUBSCRIBE
+
+
+
+
+ QUIT
+
+
+
+
+ RANDOMKEY
+
+
+
+
+ READONLY
+
+
+
+
+ READWRITE
+
+
+
+
+ RENAMENX
+
+
+
+
+ REPLCONF
+
+
+
+
+ REPLICAOF
+
+
+
+
+ RESET
+
+
+
+
+ RESTORE-ASKING
+
+
+
+
+ ROLE
+
+
+
+
+ RPOPLPUSH
+
+
+
+
+ RPUSHX
+
+
+
+
+ SAVE
+
+
+
+
+ SCAN
+
+
+
+
+ SCRIPT
+
+
+
+
+ SCRIPT|DEBUG
+
+
+
+
+ SCRIPT|EXISTS
+
+
+
+
+ SCRIPT|FLUSH
+
+
+
+
+ SCRIPT|HELP
+
+
+
+
+ SCRIPT|KILL
+
+
+
+
+ SCRIPT|LOAD
+
+
+
+
+ SDIFFSTORE
+
+
+
+
+ SETNX
+
+
+
+
+ SETRANGE
+
+
+
+
+ SHUTDOWN
+
+
+
+
+ SINTERCARD
+
+
+
+
+ SINTERSTORE
+
+
+
+
+ SISMEMBER
+
+
+
+
+ SLAVEOF
+
+
+
+
+ SLOWLOG
+
+
+
+
+ SLOWLOG|GET
+
+
+
+
+ SLOWLOG|HELP
+
+
+
+
+ SLOWLOG|LEN
+
+
+
+
+ SLOWLOG|RESET
+
+
+
+
+ SMISMEMBER
+
+
+
+
+ SMOVE
+
+
+
+
+ SORT
+
+
+
+
+ SORT_RO
+
+
+
+
+ SPOP
+
+
+
+
+ SPUBLISH
+
+
+
+
+ SRANDMEMBER
+
+
+
+
+ SSCAN
+
+
+
+
+ SSUBSCRIBE
+
+
+
+
+ STRLEN
+
+
+
+
+ SUBSTR
+
+
+
+
+ SUNION
+
+
+
+
+ SUNIONSTORE
+
+
+
+
+ SUNSUBSCRIBE
+
+
+
+
+ SWAPDB
+
+
+
+
+ SYNC
+
+
+
+
+ TIME
+
+
+
+
+ UNLINK
+
+
+
+
+ UNSUBSCRIBE
+
+
+
+
+ UNWATCH
+
+
+
+
+ WAIT
+
+
+
+
+ WAITAOF
+
+
+
+
+ WATCH
+
+
+
+
+ XACK
+
+
+
+
+ XADD
+
+
+
+
+ XAUTOCLAIM
+
+
+
+
+ XCLAIM
+
+
+
+
+ XDEL
+
+
+
+
+ XGROUP
+
+
+
+
+ XGROUP|CREATE
+
+
+
+
+ XGROUP|CREATECONSUMER
+
+
+
+
+ XGROUP|DELCONSUMER
+
+
+
+
+ XGROUP|DESTROY
+
+
+
+
+ XGROUP|HELP
+
+
+
+
+ XGROUP|SETID
+
+
+
+
+ XINFO
+
+
+
+
+ XINFO|CONSUMERS
+
+
+
+
+ XINFO|GROUPS
+
+
+
+
+ XINFO|HELP
+
+
+
+
+ XINFO|STREAM
+
+
+
+
+ XLEN
+
+
+
+
+ XPENDING
+
+
+
+
+ XRANGE
+
+
+
+
+ XREAD
+
+
+
+
+ XREADGROUP
+
+
+
+
+ XREVRANGE
+
+
+
+
+ XSETID
+
+
+
+
+ XTRIM
+
+
+
+
+ ZDIFF
+
+
+
+
+ ZDIFFSTORE
+
+
+
+
+ ZINCRBY
+
+
+
+
+ ZINTER
+
+
+
+
+ ZINTERCARD
+
+
+
+
+ ZINTERSTORE
+
+
+
+
+ ZLEXCOUNT
+
+
+
+
+ ZMPOP
+
+
+
+
+ ZMSCORE
+
+
+
+
+ ZRANDMEMBER
+
+
+
+
+ ZRANGEBYLEX
+
+
+
+
+ ZRANGEBYSCORE
+
+
+
+
+ ZRANGESTORE
+
+
+
+
+ ZREMRANGEBYLEX
+
+
+
+
+ ZREMRANGEBYRANK
+
+
+
+
+ ZREMRANGEBYSCORE
+
+
+
+
+ ZREVRANGE
+
+
+
+
+ ZREVRANGEBYLEX
+
+
+
+
+ ZREVRANGEBYSCORE
+
+
+
+
+ ZREVRANK
+
+
+
+
+ ZSCAN
+
+
+
+
+ ZSCORE
+
+
+
+
+ ZUNION
+
+
+
+
+ ZUNIONSTORE
+
+
+
+
+
+
+
+
diff --git a/docs/src/pages/releases/[slug].astro b/docs/src/pages/releases/[slug].astro
new file mode 100644
index 000000000..c2eafabdc
--- /dev/null
+++ b/docs/src/pages/releases/[slug].astro
@@ -0,0 +1,20 @@
+---
+import { getCollection } from "astro:content";
+import BlogLayout from "../../layouts/BlogLayout.astro";
+export async function getStaticPaths() {
+ const releases = (await getCollection("releases")).sort(
+ (a, b) => b.data.published_at.getTime() - a.data.published_at.getTime(),
+ );
+ return releases.map((release) => ({
+ params: { slug: release.slug },
+ props: { release },
+ }));
+}
+
+const { release } = Astro.props;
+const { Content } = await release.render();
+---
+
+
+
+
diff --git a/docs/src/pages/releases/index.astro b/docs/src/pages/releases/index.astro
new file mode 100644
index 000000000..98a16eeae
--- /dev/null
+++ b/docs/src/pages/releases/index.astro
@@ -0,0 +1,34 @@
+---
+import { getCollection } from "astro:content";
+
+import Layout from "../../layouts/Layout.astro";
+import RecentReleases from "../../components/RecentReleases.astro";
+const releases = (await getCollection("releases")).sort(
+ (a, b) => b.data.published_at.getTime() - a.data.published_at.getTime(),
+);
+const title = "DiceDB Releases";
+const description = "";
+---
+
+
+
+
diff --git a/docs/src/pages/roadmap.astro b/docs/src/pages/roadmap.astro
new file mode 100644
index 000000000..c4679964a
--- /dev/null
+++ b/docs/src/pages/roadmap.astro
@@ -0,0 +1,17 @@
+---
+import Layout from "../layouts/Layout.astro";
+import Content from "../data/roadmap.md";
+
+const title = "DiceDB Roadmap";
+const description = "";
+---
+
+
+
+
diff --git a/docs/src/styles/main.scss b/docs/src/styles/main.scss
index f2b53340d..61376bc86 100644
--- a/docs/src/styles/main.scss
+++ b/docs/src/styles/main.scss
@@ -1,7 +1,7 @@
-@import url("https://fonts.googleapis.com/css2?family=Assistant:wght@500;700&display=swap");
+@import url("https://fonts.googleapis.com/css2?family=Spline+Sans+Mono:ital,wght@0,300..700;1,300..700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400;1,700;1,900&display=swap");
-$family-serif: "Assistant";
+$family-serif: "Spline Sans Mono", monospace;
$family-primary: $family-serif;
$family-secondary: "Merriweather", serif;
@@ -33,11 +33,11 @@ $button-border-color: #111;
$button-hover-border-color: $link;
pre {
- background-color: #111 !important;
+ background-color: #111 !important;
}
figcaption.header {
- display: none !important;
+ display: none !important;
}
.hnavbar {
@@ -107,6 +107,10 @@ ul.horizontal a {
cursor: pointer;
}
+.content img {
+ border-radius: 0.5em;
+}
+
img.logo {
filter: url("data:image/svg+xml;utf8,
#grayscale"); /* Firefox 10+, Firefox on Android */
-webkit-filter: grayscale(100%);
@@ -193,150 +197,8 @@ pre {
padding: 1.75em !important;
}
-@import "../../node_modules/bulma/bulma.sass";
-
-@-webkit-keyframes bg-scrolling-reverse {
- 100% {
- background-position: 50px 50px;
- }
-}
-@-moz-keyframes bg-scrolling-reverse {
- 100% {
- background-position: 50px 50px;
- }
-}
-@-o-keyframes bg-scrolling-reverse {
- 100% {
- background-position: 50px 50px;
- }
-}
-@keyframes bg-scrolling-reverse {
- 100% {
- background-position: 50px 50px;
- }
-}
-@-webkit-keyframes bg-scrolling {
- 0% {
- background-position: 50px 50px;
- }
-}
-@-moz-keyframes bg-scrolling {
- 0% {
- background-position: 50px 50px;
- }
-}
-@-o-keyframes bg-scrolling {
- 0% {
- background-position: 50px 50px;
- }
-}
-@keyframes bg-scrolling {
- 0% {
- background-position: 50px 50px;
- }
-}
-
-#hero {
- background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAABnSURBVHja7M5RDYAwDEXRDgmvEocnlrQS2SwUFST9uEfBGWs9c97nbGtDcquqiKhOImLs/UpuzVzWEi1atGjRokWLFi1atGjRokWLFi1atGjRokWLFi1af7Ukz8xWp8z8AAAA//8DAJ4LoEAAlL1nAAAAAElFTkSuQmCC")
- repeat 0 0;
- -webkit-animation: bg-scrolling-reverse 0.92s infinite;
- -moz-animation: bg-scrolling-reverse 0.92s infinite;
- -o-animation: bg-scrolling-reverse 0.92s infinite;
- animation: bg-scrolling-reverse 0.92s infinite;
- -webkit-animation-timing-function: linear;
- -moz-animation-timing-function: linear;
- -o-animation-timing-function: linear;
- animation-timing-function: linear;
-}
-
-$color-light-gray: #ecf0f1;
-$color-medium-gray: #bdc3c7;
-$color-dark-gray: #3f0a0a;
-$color-light-green: #ff6060;
-
-$cube-size: 250px;
-$animation-duration: 15s;
-
-.cube {
- width: $cube-size;
- height: $cube-size;
- position: relative;
- transform-style: preserve-3d;
- animation: rotate $animation-duration linear infinite;
-
- &:after {
- content: "";
- width: 100%;
- height: 100%;
- box-shadow: 0 0 50px rgba(0, 0, 0, 0.2);
- position: absolute;
- transform-origin: bottom;
- transform-style: preserve-3d;
- transform: rotateX(90deg) translateY($cube-size/2) translateZ(-$cube-size/2);
- background-color: rgba(0, 0, 0, 0.1);
- }
-
- div {
- background-color: rgba($color-light-green, 0.7);
- position: absolute;
- width: 100%;
- height: 100%;
- border: 1px solid $color-dark-gray;
- box-shadow: 0 0 2px rgba($color-light-green, 0.7);
-
- // Back face
- &:nth-child(1) {
- transform: translateZ(-$cube-size/2);
- animation: shade #{$animation-duration} #{-$animation-duration/2} linear infinite;
- }
-
- // Front face
- &:nth-child(2) {
- transform: translateZ($cube-size/2) rotateY(180deg);
- animation: shade $animation-duration linear infinite;
- }
-
- // Right face
- &:nth-child(3) {
- transform-origin: right;
- transform: translateZ($cube-size/2) rotateY(270deg);
- animation: shade #{$animation-duration} #{-$animation-duration/4} linear infinite;
- }
-
- // Left face
- &:nth-child(4) {
- transform-origin: left;
- transform: translateZ($cube-size/2) rotateY(90deg);
- animation: shade #{$animation-duration} #{-($animation-duration * 3)/4} linear
- infinite;
- }
-
- // Bottom face
- &:nth-child(5) {
- transform-origin: bottom;
- transform: translateZ($cube-size/2) rotateX(90deg);
- background-color: rgba(black, 0.7);
- }
-
- // Top face
- &:nth-child(6) {
- transform-origin: top;
- transform: translateZ($cube-size/2) rotateX(270deg);
- }
- }
+.title {
+ line-height: 1.5em !important;
}
-@keyframes rotate {
- 0% {
- transform: rotateX(-15deg) rotateY(0deg);
- }
- 100% {
- transform: rotateX(-15deg) rotateY(360deg);
- }
-}
-
-@keyframes shade {
- 50% {
- background-color: rgba(black, 0.7);
- }
-}
+@import "../../node_modules/bulma/bulma.sass";
diff --git a/go.mod b/go.mod
index 8d93328fb..9791cfecd 100644
--- a/go.mod
+++ b/go.mod
@@ -47,6 +47,7 @@ require (
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
+ github.com/mattn/go-sqlite3 v1.14.24
github.com/mmcloughlin/geohash v0.10.0
github.com/ohler55/ojg v1.25.0
github.com/pelletier/go-toml/v2 v2.2.3
@@ -58,4 +59,5 @@ require (
github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2
golang.org/x/crypto v0.28.0
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
+ google.golang.org/protobuf v1.35.1
)
diff --git a/go.sum b/go.sum
index aefcf136c..2b7c9f001 100644
--- a/go.sum
+++ b/go.sum
@@ -65,6 +65,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
+github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mmcloughlin/geohash v0.10.0 h1:9w1HchfDfdeLc+jFEf/04D27KP7E2QmpDu52wPbJWRE=
@@ -131,6 +133,8 @@ golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
+google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
diff --git a/integration_tests/commands/async/bit_operation_test.go b/integration_tests/commands/async/bit_operation_test.go
deleted file mode 100644
index 959745f58..000000000
--- a/integration_tests/commands/async/bit_operation_test.go
+++ /dev/null
@@ -1,564 +0,0 @@
-package async
-
-import (
- "fmt"
- "math"
- "net"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestBitOp(t *testing.T) {
- conn := getLocalConnection()
- testcases := []struct {
- InCmds []string
- Out []interface{}
- }{
- {
- InCmds: []string{"SETBIT unitTestKeyA 1 1", "SETBIT unitTestKeyA 3 1", "SETBIT unitTestKeyA 5 1", "SETBIT unitTestKeyA 7 1", "SETBIT unitTestKeyA 8 1"},
- Out: []interface{}{int64(0), int64(0), int64(0), int64(0), int64(0)},
- },
- {
- InCmds: []string{"SETBIT unitTestKeyB 2 1", "SETBIT unitTestKeyB 4 1", "SETBIT unitTestKeyB 7 1"},
- Out: []interface{}{int64(0), int64(0), int64(0)},
- },
- {
- InCmds: []string{"SET foo bar", "SETBIT foo 2 1", "SETBIT foo 4 1", "SETBIT foo 7 1", "GET foo"},
- Out: []interface{}{"OK", int64(1), int64(0), int64(0), "kar"},
- },
- {
- InCmds: []string{"SET mykey12 1343", "SETBIT mykey12 2 1", "SETBIT mykey12 4 1", "SETBIT mykey12 7 1", "GET mykey12"},
- Out: []interface{}{"OK", int64(1), int64(0), int64(1), int64(9343)},
- },
- {
- InCmds: []string{"SET foo12 bar", "SETBIT foo12 2 1", "SETBIT foo12 4 1", "SETBIT foo12 7 1", "GET foo12"},
- Out: []interface{}{"OK", int64(1), int64(0), int64(0), "kar"},
- },
- {
- InCmds: []string{"BITOP NOT unitTestKeyNOT unitTestKeyA "},
- Out: []interface{}{int64(2)},
- },
- {
- InCmds: []string{"GETBIT unitTestKeyNOT 1", "GETBIT unitTestKeyNOT 2", "GETBIT unitTestKeyNOT 7", "GETBIT unitTestKeyNOT 8", "GETBIT unitTestKeyNOT 9"},
- Out: []interface{}{int64(0), int64(1), int64(0), int64(0), int64(1)},
- },
- {
- InCmds: []string{"BITOP OR unitTestKeyOR unitTestKeyB unitTestKeyA"},
- Out: []interface{}{int64(2)},
- },
- {
- InCmds: []string{"GETBIT unitTestKeyOR 1", "GETBIT unitTestKeyOR 2", "GETBIT unitTestKeyOR 3", "GETBIT unitTestKeyOR 7", "GETBIT unitTestKeyOR 8", "GETBIT unitTestKeyOR 9", "GETBIT unitTestKeyOR 12"},
- Out: []interface{}{int64(1), int64(1), int64(1), int64(1), int64(1), int64(0), int64(0)},
- },
- {
- InCmds: []string{"BITOP AND unitTestKeyAND unitTestKeyB unitTestKeyA"},
- Out: []interface{}{int64(2)},
- },
- {
- InCmds: []string{"GETBIT unitTestKeyAND 1", "GETBIT unitTestKeyAND 2", "GETBIT unitTestKeyAND 7", "GETBIT unitTestKeyAND 8", "GETBIT unitTestKeyAND 9"},
- Out: []interface{}{int64(0), int64(0), int64(1), int64(0), int64(0)},
- },
- {
- InCmds: []string{"BITOP XOR unitTestKeyXOR unitTestKeyB unitTestKeyA"},
- Out: []interface{}{int64(2)},
- },
- {
- InCmds: []string{"GETBIT unitTestKeyXOR 1", "GETBIT unitTestKeyXOR 2", "GETBIT unitTestKeyXOR 3", "GETBIT unitTestKeyXOR 7", "GETBIT unitTestKeyXOR 8"},
- Out: []interface{}{int64(1), int64(1), int64(1), int64(0), int64(1)},
- },
- }
-
- for _, tcase := range testcases {
- for i := 0; i < len(tcase.InCmds); i++ {
- cmd := tcase.InCmds[i]
- out := tcase.Out[i]
- assert.Equal(t, out, FireCommand(conn, cmd), "Value mismatch for cmd %s\n.", cmd)
- }
- }
-}
-
-func TestBitCount(t *testing.T) {
- conn := getLocalConnection()
- testcases := []struct {
- InCmds []string
- Out []interface{}
- }{
- {
- InCmds: []string{"SETBIT mykey 7 1"},
- Out: []interface{}{int64(0)},
- },
- {
- InCmds: []string{"SETBIT mykey 7 1"},
- Out: []interface{}{int64(1)},
- },
- {
- InCmds: []string{"SETBIT mykey 122 1"},
- Out: []interface{}{int64(0)},
- },
- {
- InCmds: []string{"GETBIT mykey 122"},
- Out: []interface{}{int64(1)},
- },
- {
- InCmds: []string{"SETBIT mykey 122 0"},
- Out: []interface{}{int64(1)},
- },
- {
- InCmds: []string{"GETBIT mykey 122"},
- Out: []interface{}{int64(0)},
- },
- {
- InCmds: []string{"GETBIT mykey 1223232"},
- Out: []interface{}{int64(0)},
- },
- {
- InCmds: []string{"GETBIT mykey 7"},
- Out: []interface{}{int64(1)},
- },
- {
- InCmds: []string{"GETBIT mykey 8"},
- Out: []interface{}{int64(0)},
- },
- {
- InCmds: []string{"BITCOUNT mykey 3 7 BIT"},
- Out: []interface{}{int64(1)},
- },
- {
- InCmds: []string{"BITCOUNT mykey 3 7"},
- Out: []interface{}{int64(0)},
- },
- {
- InCmds: []string{"BITCOUNT mykey 0 0"},
- Out: []interface{}{int64(1)},
- },
- {
- InCmds: []string{"BITCOUNT"},
- Out: []interface{}{"ERR wrong number of arguments for 'bitcount' command"},
- },
- {
- InCmds: []string{"BITCOUNT mykey"},
- Out: []interface{}{int64(1)},
- },
- {
- InCmds: []string{"BITCOUNT mykey 0"},
- Out: []interface{}{"ERR syntax error"},
- },
- }
-
- for _, tcase := range testcases {
- for i := 0; i < len(tcase.InCmds); i++ {
- cmd := tcase.InCmds[i]
- out := tcase.Out[i]
- assert.Equal(t, out, FireCommand(conn, cmd), "Value mismatch for cmd %s\n.", cmd)
- }
- }
-}
-
-func TestBitPos(t *testing.T) {
- conn := getLocalConnection()
- testcases := []struct {
- name string
- val interface{}
- inCmd string
- out interface{}
- setCmdSETBIT bool
- }{
- {
- name: "String interval BIT 0,-1 ",
- val: "\\x00\\xff\\x00",
- inCmd: "BITPOS testkey 0 0 -1 bit",
- out: int64(0),
- },
- {
- name: "String interval BIT 8,-1",
- val: "\\x00\\xff\\x00",
- inCmd: "BITPOS testkey 0 8 -1 bit",
- out: int64(8),
- },
- {
- name: "String interval BIT 16,-1",
- val: "\\x00\\xff\\x00",
- inCmd: "BITPOS testkey 0 16 -1 bit",
- out: int64(16),
- },
- {
- name: "String interval BIT 16,200",
- val: "\\x00\\xff\\x00",
- inCmd: "BITPOS testkey 0 16 200 bit",
- out: int64(16),
- },
- {
- name: "String interval BIT 8,8",
- val: "\\x00\\xff\\x00",
- inCmd: "BITPOS testkey 0 8 8 bit",
- out: int64(8),
- },
- {
- name: "FindsFirstZeroBit",
- val: "\xff\xf0\x00",
- inCmd: "BITPOS testkey 0",
- out: int64(12),
- },
- {
- name: "FindsFirstOneBit",
- val: "\x00\x0f\xff",
- inCmd: "BITPOS testkey 1",
- out: int64(12),
- },
- {
- name: "NoOneBitFound",
- val: "\x00\x00\x00",
- inCmd: "BITPOS testkey 1",
- out: int64(-1),
- },
- {
- name: "NoZeroBitFound",
- val: "\xff\xff\xff",
- inCmd: "BITPOS testkey 0",
- out: int64(24),
- },
- {
- name: "NoZeroBitFoundWithRangeStartPos",
- val: "\xff\xff\xff",
- inCmd: "BITPOS testkey 0 2",
- out: int64(24),
- },
- {
- name: "NoZeroBitFoundWithOOBRangeStartPos",
- val: "\xff\xff\xff",
- inCmd: "BITPOS testkey 0 4",
- out: int64(-1),
- },
- {
- name: "NoZeroBitFoundWithRange",
- val: "\xff\xff\xff",
- inCmd: "BITPOS testkey 0 2 2",
- out: int64(-1),
- },
- {
- name: "NoZeroBitFoundWithRangeAndRangeType",
- val: "\xff\xff\xff",
- inCmd: "BITPOS testkey 0 2 2 BIT",
- out: int64(-1),
- },
- {
- name: "FindsFirstZeroBitInRange",
- val: "\xff\xf0\xff",
- inCmd: "BITPOS testkey 0 1 2",
- out: int64(12),
- },
- {
- name: "FindsFirstOneBitInRange",
- val: "\x00\x00\xf0",
- inCmd: "BITPOS testkey 1 2 3",
- out: int64(16),
- },
- {
- name: "StartGreaterThanEnd",
- val: "\xff\xf0\x00",
- inCmd: "BITPOS testkey 0 3 2",
- out: int64(-1),
- },
- {
- name: "FindsFirstOneBitWithNegativeStart",
- val: "\x00\x00\xf0",
- inCmd: "BITPOS testkey 1 -2 -1",
- out: int64(16),
- },
- {
- name: "FindsFirstZeroBitWithNegativeEnd",
- val: "\xff\xf0\xff",
- inCmd: "BITPOS testkey 0 1 -1",
- out: int64(12),
- },
- {
- name: "FindsFirstZeroBitInByteRange",
- val: "\xff\x00\xff",
- inCmd: "BITPOS testkey 0 1 2 BYTE",
- out: int64(8),
- },
- {
- name: "FindsFirstOneBitInBitRange",
- val: "\x00\x01\x00",
- inCmd: "BITPOS testkey 1 0 16 BIT",
- out: int64(15),
- },
- {
- name: "NoBitFoundInByteRange",
- val: "\xff\xff\xff",
- inCmd: "BITPOS testkey 0 0 2 BYTE",
- out: int64(-1),
- },
- {
- name: "NoBitFoundInBitRange",
- val: "\x00\x00\x00",
- inCmd: "BITPOS testkey 1 0 23 BIT",
- out: int64(-1),
- },
- {
- name: "EmptyStringReturnsMinusOneForZeroBit",
- val: "\"\"",
- inCmd: "BITPOS testkey 0",
- out: int64(-1),
- },
- {
- name: "EmptyStringReturnsMinusOneForOneBit",
- val: "\"\"",
- inCmd: "BITPOS testkey 1",
- out: int64(-1),
- },
- {
- name: "SingleByteString",
- val: "\x80",
- inCmd: "BITPOS testkey 1",
- out: int64(0),
- },
- {
- name: "RangeExceedsStringLength",
- val: "\x00\xff",
- inCmd: "BITPOS testkey 1 0 20 BIT",
- out: int64(8),
- },
- {
- name: "InvalidBitArgument",
- inCmd: "BITPOS testkey 2",
- out: "ERR the bit argument must be 1 or 0",
- },
- {
- name: "NonIntegerStartParameter",
- inCmd: "BITPOS testkey 0 start",
- out: "ERR value is not an integer or out of range",
- },
- {
- name: "NonIntegerEndParameter",
- inCmd: "BITPOS testkey 0 1 end",
- out: "ERR value is not an integer or out of range",
- },
- {
- name: "InvalidRangeType",
- inCmd: "BITPOS testkey 0 1 2 BYTEs",
- out: "ERR syntax error",
- },
- {
- name: "InsufficientArguments",
- inCmd: "BITPOS testkey",
- out: "ERR wrong number of arguments for 'bitpos' command",
- },
- {
- name: "NonExistentKeyForZeroBit",
- inCmd: "BITPOS nonexistentkey 0",
- out: int64(0),
- },
- {
- name: "NonExistentKeyForOneBit",
- inCmd: "BITPOS nonexistentkey 1",
- out: int64(-1),
- },
- {
- name: "IntegerValue",
- val: 65280, // 0xFF00 in decimal
- inCmd: "BITPOS testkey 0",
- out: int64(0),
- },
- {
- name: "LargeIntegerValue",
- val: 16777215, // 0xFFFFFF in decimal
- inCmd: "BITPOS testkey 1",
- out: int64(2),
- },
- {
- name: "SmallIntegerValue",
- val: 1, // 0x01 in decimal
- inCmd: "BITPOS testkey 0",
- out: int64(0),
- },
- {
- name: "ZeroIntegerValue",
- val: 0,
- inCmd: "BITPOS testkey 1",
- out: int64(2),
- },
- {
- name: "BitRangeStartGreaterThanBitLength",
- val: "\xff\xff\xff",
- inCmd: "BITPOS testkey 0 25 30 BIT",
- out: int64(-1),
- },
- {
- name: "BitRangeEndExceedsBitLength",
- val: "\xff\xff\xff",
- inCmd: "BITPOS testkey 0 0 30 BIT",
- out: int64(-1),
- },
- {
- name: "NegativeStartInBitRange",
- val: "\x00\xff\xff",
- inCmd: "BITPOS testkey 1 -16 -1 BIT",
- out: int64(8),
- },
- {
- name: "LargeNegativeStart",
- val: "\x00\xff\xff",
- inCmd: "BITPOS testkey 1 -100 -1",
- out: int64(8),
- },
- {
- name: "LargePositiveEnd",
- val: "\x00\xff\xff",
- inCmd: "BITPOS testkey 1 0 100",
- out: int64(8),
- },
- {
- name: "StartAndEndEqualInByteRange",
- val: "\x0f\xff\xff",
- inCmd: "BITPOS testkey 0 1 1 BYTE",
- out: int64(-1),
- },
- {
- name: "StartAndEndEqualInBitRange",
- val: "\x0f\xff\xff",
- inCmd: "BITPOS testkey 1 1 1 BIT",
- out: int64(-1),
- },
- {
- name: "FindFirstZeroBitInNegativeRange",
- val: "\xff\x00\xff",
- inCmd: "BITPOS testkey 0 -2 -1",
- out: int64(8),
- },
- {
- name: "FindFirstOneBitInNegativeRangeBIT",
- val: "\x00\x00\x80",
- inCmd: "BITPOS testkey 1 -8 -1 BIT",
- out: int64(16),
- },
- {
- name: "MaxIntegerValue",
- val: math.MaxInt64,
- inCmd: "BITPOS testkey 0",
- out: int64(0),
- },
- {
- name: "MinIntegerValue",
- val: math.MinInt64,
- inCmd: "BITPOS testkey 1",
- out: int64(2),
- },
- {
- name: "SingleBitStringZero",
- val: "\x00",
- inCmd: "BITPOS testkey 1",
- out: int64(-1),
- },
- {
- name: "SingleBitStringOne",
- val: "\x01",
- inCmd: "BITPOS testkey 0",
- out: int64(0),
- },
- {
- name: "AllBitsSetExceptLast",
- val: "\xff\xff\xfe",
- inCmd: "BITPOS testkey 0",
- out: int64(23),
- },
- {
- name: "OnlyLastBitSet",
- val: "\x00\x00\x01",
- inCmd: "BITPOS testkey 1",
- out: int64(23),
- },
- {
- name: "AlternatingBitsLongString",
- val: "\xaa\xaa\xaa\xaa\xaa",
- inCmd: "BITPOS testkey 0",
- out: int64(1),
- },
- {
- name: "VeryLargeByteString",
- val: strings.Repeat("\xff", 1000) + "\x00",
- inCmd: "BITPOS testkey 0",
- out: int64(8000),
- },
- {
- name: "FindZeroBitOnSetBitKey",
- val: "8 1",
- inCmd: "BITPOS testkeysb 1",
- out: int64(8),
- setCmdSETBIT: true,
- },
- {
- name: "FindOneBitOnSetBitKey",
- val: "1 1",
- inCmd: "BITPOS testkeysb 1",
- out: int64(1),
- setCmdSETBIT: true,
- },
- }
-
- for _, tc := range testcases {
- t.Run(tc.name, func(t *testing.T) {
- var setCmd string
- if tc.setCmdSETBIT {
- setCmd = fmt.Sprintf("SETBIT testkeysb %s", tc.val.(string))
- } else {
- switch v := tc.val.(type) {
- case string:
- setCmd = fmt.Sprintf("SET testkey %s", v)
- case int:
- setCmd = fmt.Sprintf("SET testkey %d", v)
- default:
- // For test cases where we don't set a value (e.g., error cases)
- setCmd = ""
- }
- }
-
- if setCmd != "" {
- FireCommand(conn, setCmd)
- }
-
- result := FireCommand(conn, tc.inCmd)
- assert.Equal(t, tc.out, result, "Mismatch for cmd %s\n", tc.inCmd)
- })
- }
-}
-
-func generateSetBitCommand(connection net.Conn, bitPosition int) int64 {
- command := fmt.Sprintf("SETBIT unitTestKeyA %d 1", bitPosition)
- responseValue := FireCommand(connection, command)
- if responseValue == nil {
- return -1
- }
- return responseValue.(int64)
-}
-
-func BenchmarkSetBitCommand(b *testing.B) {
- connection := getLocalConnection()
- for n := 0; n < 1000; n++ {
- setBitCommand := generateSetBitCommand(connection, n)
- if setBitCommand < 0 {
- b.Fail()
- }
- }
-}
-
-func generateGetBitCommand(connection net.Conn, bitPosition int) int64 {
- command := fmt.Sprintf("GETBIT unitTestKeyA %d", bitPosition)
- responseValue := FireCommand(connection, command)
- if responseValue == nil {
- return -1
- }
- return responseValue.(int64)
-}
-
-func BenchmarkGetBitCommand(b *testing.B) {
- connection := getLocalConnection()
- for n := 0; n < 1000; n++ {
- getBitCommand := generateGetBitCommand(connection, n)
- if getBitCommand < 0 {
- b.Fail()
- }
- }
-}
diff --git a/integration_tests/commands/async/bit_ops_string_int_test.go b/integration_tests/commands/async/bit_test.go
similarity index 76%
rename from integration_tests/commands/async/bit_ops_string_int_test.go
rename to integration_tests/commands/async/bit_test.go
index 13379fdfc..68f565c88 100644
--- a/integration_tests/commands/async/bit_ops_string_int_test.go
+++ b/integration_tests/commands/async/bit_test.go
@@ -8,6 +8,75 @@ import (
"github.com/stretchr/testify/assert"
)
+func TestBitOp(t *testing.T) {
+ conn := getLocalConnection()
+ testcases := []struct {
+ InCmds []string
+ Out []interface{}
+ }{
+ {
+ InCmds: []string{"SETBIT unitTestKeyA 1 1", "SETBIT unitTestKeyA 3 1", "SETBIT unitTestKeyA 5 1", "SETBIT unitTestKeyA 7 1", "SETBIT unitTestKeyA 8 1"},
+ Out: []interface{}{int64(0), int64(0), int64(0), int64(0), int64(0)},
+ },
+ {
+ InCmds: []string{"SETBIT unitTestKeyB 2 1", "SETBIT unitTestKeyB 4 1", "SETBIT unitTestKeyB 7 1"},
+ Out: []interface{}{int64(0), int64(0), int64(0)},
+ },
+ {
+ InCmds: []string{"SET foo bar", "SETBIT foo 2 1", "SETBIT foo 4 1", "SETBIT foo 7 1", "GET foo"},
+ Out: []interface{}{"OK", int64(1), int64(0), int64(0), "kar"},
+ },
+ {
+ InCmds: []string{"SET mykey12 1343", "SETBIT mykey12 2 1", "SETBIT mykey12 4 1", "SETBIT mykey12 7 1", "GET mykey12"},
+ Out: []interface{}{"OK", int64(1), int64(0), int64(1), int64(9343)},
+ },
+ {
+ InCmds: []string{"SET foo12 bar", "SETBIT foo12 2 1", "SETBIT foo12 4 1", "SETBIT foo12 7 1", "GET foo12"},
+ Out: []interface{}{"OK", int64(1), int64(0), int64(0), "kar"},
+ },
+ {
+ InCmds: []string{"BITOP NOT unitTestKeyNOT unitTestKeyA "},
+ Out: []interface{}{int64(2)},
+ },
+ {
+ InCmds: []string{"GETBIT unitTestKeyNOT 1", "GETBIT unitTestKeyNOT 2", "GETBIT unitTestKeyNOT 7", "GETBIT unitTestKeyNOT 8", "GETBIT unitTestKeyNOT 9"},
+ Out: []interface{}{int64(0), int64(1), int64(0), int64(0), int64(1)},
+ },
+ {
+ InCmds: []string{"BITOP OR unitTestKeyOR unitTestKeyB unitTestKeyA"},
+ Out: []interface{}{int64(2)},
+ },
+ {
+ InCmds: []string{"GETBIT unitTestKeyOR 1", "GETBIT unitTestKeyOR 2", "GETBIT unitTestKeyOR 3", "GETBIT unitTestKeyOR 7", "GETBIT unitTestKeyOR 8", "GETBIT unitTestKeyOR 9", "GETBIT unitTestKeyOR 12"},
+ Out: []interface{}{int64(1), int64(1), int64(1), int64(1), int64(1), int64(0), int64(0)},
+ },
+ {
+ InCmds: []string{"BITOP AND unitTestKeyAND unitTestKeyB unitTestKeyA"},
+ Out: []interface{}{int64(2)},
+ },
+ {
+ InCmds: []string{"GETBIT unitTestKeyAND 1", "GETBIT unitTestKeyAND 2", "GETBIT unitTestKeyAND 7", "GETBIT unitTestKeyAND 8", "GETBIT unitTestKeyAND 9"},
+ Out: []interface{}{int64(0), int64(0), int64(1), int64(0), int64(0)},
+ },
+ {
+ InCmds: []string{"BITOP XOR unitTestKeyXOR unitTestKeyB unitTestKeyA"},
+ Out: []interface{}{int64(2)},
+ },
+ {
+ InCmds: []string{"GETBIT unitTestKeyXOR 1", "GETBIT unitTestKeyXOR 2", "GETBIT unitTestKeyXOR 3", "GETBIT unitTestKeyXOR 7", "GETBIT unitTestKeyXOR 8"},
+ Out: []interface{}{int64(1), int64(1), int64(1), int64(0), int64(1)},
+ },
+ }
+
+ for _, tcase := range testcases {
+ for i := 0; i < len(tcase.InCmds); i++ {
+ cmd := tcase.InCmds[i]
+ out := tcase.Out[i]
+ assert.Equal(t, out, FireCommand(conn, cmd), "Value mismatch for cmd %s\n.", cmd)
+ }
+ }
+}
+
func TestBitOpsString(t *testing.T) {
// test code
diff --git a/integration_tests/commands/async/bitfield_test.go b/integration_tests/commands/async/bitfield_test.go
deleted file mode 100644
index 4664e8ebe..000000000
--- a/integration_tests/commands/async/bitfield_test.go
+++ /dev/null
@@ -1,377 +0,0 @@
-package async
-
-import (
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestBitfield(t *testing.T) {
- conn := getLocalConnection()
- defer conn.Close()
-
- FireCommand(conn, "FLUSHDB")
- defer FireCommand(conn, "FLUSHDB") // clean up after all test cases
- syntaxErrMsg := "ERR syntax error"
- bitFieldTypeErrMsg := "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is."
- integerErrMsg := "ERR value is not an integer or out of range"
- overflowErrMsg := "ERR Invalid OVERFLOW type specified"
-
- testCases := []struct {
- Name string
- Commands []string
- Expected []interface{}
- Delay []time.Duration
- CleanUp []string
- }{
- {
- Name: "BITFIELD Arity Check",
- Commands: []string{"bitfield"},
- Expected: []interface{}{"ERR wrong number of arguments for 'bitfield' command"},
- Delay: []time.Duration{0},
- CleanUp: []string{},
- },
- {
- Name: "BITFIELD on unsupported type of SET",
- Commands: []string{"SADD bits a b c", "bitfield bits"},
- Expected: []interface{}{int64(3), "WRONGTYPE Operation against a key holding the wrong kind of value"},
- Delay: []time.Duration{0, 0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD on unsupported type of JSON",
- Commands: []string{"json.set bits $ 1", "bitfield bits"},
- Expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
- Delay: []time.Duration{0, 0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD on unsupported type of HSET",
- Commands: []string{"HSET bits a 1", "bitfield bits"},
- Expected: []interface{}{int64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"},
- Delay: []time.Duration{0, 0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD with syntax errors",
- Commands: []string{
- "bitfield bits set u8 0 255 incrby u8 0 100 get u8",
- "bitfield bits set a8 0 255 incrby u8 0 100 get u8",
- "bitfield bits set u8 a 255 incrby u8 0 100 get u8",
- "bitfield bits set u8 0 255 incrby u8 0 100 overflow wraap",
- "bitfield bits set u8 0 incrby u8 0 100 get u8 288",
- },
- Expected: []interface{}{
- syntaxErrMsg,
- bitFieldTypeErrMsg,
- "ERR bit offset is not an integer or out of range",
- overflowErrMsg,
- integerErrMsg,
- },
- Delay: []time.Duration{0, 0, 0, 0, 0},
- CleanUp: []string{"Del bits"},
- },
- {
- Name: "BITFIELD signed SET and GET basics",
- Commands: []string{"bitfield bits set i8 0 -100", "bitfield bits set i8 0 101", "bitfield bits get i8 0"},
- Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(-100)}, []interface{}{int64(101)}},
- Delay: []time.Duration{0, 0, 0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD unsigned SET and GET basics",
- Commands: []string{"bitfield bits set u8 0 255", "bitfield bits set u8 0 100", "bitfield bits get u8 0"},
- Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(255)}, []interface{}{int64(100)}},
- Delay: []time.Duration{0, 0, 0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD signed SET and GET together",
- Commands: []string{"bitfield bits set i8 0 255 set i8 0 100 get i8 0"},
- Expected: []interface{}{[]interface{}{int64(0), int64(-1), int64(100)}},
- Delay: []time.Duration{0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD unsigned with SET, GET and INCRBY arguments",
- Commands: []string{"bitfield bits set u8 0 255 incrby u8 0 100 get u8 0"},
- Expected: []interface{}{[]interface{}{int64(0), int64(99), int64(99)}},
- Delay: []time.Duration{0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD with only key as argument",
- Commands: []string{"bitfield bits"},
- Expected: []interface{}{[]interface{}{}},
- Delay: []time.Duration{0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD #
form",
- Commands: []string{
- "bitfield bits set u8 #0 65",
- "bitfield bits set u8 #1 66",
- "bitfield bits set u8 #2 67",
- "get bits",
- },
- Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(0)}, []interface{}{int64(0)}, "ABC"},
- Delay: []time.Duration{0, 0, 0, 0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD basic INCRBY form",
- Commands: []string{
- "bitfield bits set u8 #0 10",
- "bitfield bits incrby u8 #0 100",
- "bitfield bits incrby u8 #0 100",
- },
- Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(110)}, []interface{}{int64(210)}},
- Delay: []time.Duration{0, 0, 0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD chaining of multiple commands",
- Commands: []string{
- "bitfield bits set u8 #0 10",
- "bitfield bits incrby u8 #0 100 incrby u8 #0 100",
- },
- Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(110), int64(210)}},
- Delay: []time.Duration{0, 0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD unsigned overflow wrap",
- Commands: []string{
- "bitfield bits set u8 #0 100",
- "bitfield bits overflow wrap incrby u8 #0 257",
- "bitfield bits get u8 #0",
- "bitfield bits overflow wrap incrby u8 #0 255",
- "bitfield bits get u8 #0",
- },
- Expected: []interface{}{
- []interface{}{int64(0)},
- []interface{}{int64(101)},
- []interface{}{int64(101)},
- []interface{}{int64(100)},
- []interface{}{int64(100)},
- },
- Delay: []time.Duration{0, 0, 0, 0, 0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD unsigned overflow sat",
- Commands: []string{
- "bitfield bits set u8 #0 100",
- "bitfield bits overflow sat incrby u8 #0 257",
- "bitfield bits get u8 #0",
- "bitfield bits overflow sat incrby u8 #0 -255",
- "bitfield bits get u8 #0",
- },
- Expected: []interface{}{
- []interface{}{int64(0)},
- []interface{}{int64(255)},
- []interface{}{int64(255)},
- []interface{}{int64(0)},
- []interface{}{int64(0)},
- },
- Delay: []time.Duration{0, 0, 0, 0, 0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD signed overflow wrap",
- Commands: []string{
- "bitfield bits set i8 #0 100",
- "bitfield bits overflow wrap incrby i8 #0 257",
- "bitfield bits get i8 #0",
- "bitfield bits overflow wrap incrby i8 #0 255",
- "bitfield bits get i8 #0",
- },
- Expected: []interface{}{
- []interface{}{int64(0)},
- []interface{}{int64(101)},
- []interface{}{int64(101)},
- []interface{}{int64(100)},
- []interface{}{int64(100)},
- },
- Delay: []time.Duration{0, 0, 0, 0, 0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD signed overflow sat",
- Commands: []string{
- "bitfield bits set u8 #0 100",
- "bitfield bits overflow sat incrby i8 #0 257",
- "bitfield bits get i8 #0",
- "bitfield bits overflow sat incrby i8 #0 -255",
- "bitfield bits get i8 #0",
- },
- Expected: []interface{}{
- []interface{}{int64(0)},
- []interface{}{int64(127)},
- []interface{}{int64(127)},
- []interface{}{int64(-128)},
- []interface{}{int64(-128)},
- },
- Delay: []time.Duration{0, 0, 0, 0, 0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD regression 1",
- Commands: []string{"set bits 1", "bitfield bits get u1 0"},
- Expected: []interface{}{"OK", []interface{}{int64(0)}},
- Delay: []time.Duration{0, 0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD regression 2",
- Commands: []string{
- "bitfield mystring set i8 0 10",
- "bitfield mystring set i8 64 10",
- "bitfield mystring incrby i8 10 99900",
- },
- Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(0)}, []interface{}{int64(60)}},
- Delay: []time.Duration{0, 0, 0},
- CleanUp: []string{"DEL mystring"},
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.Name, func(t *testing.T) {
-
- for i := 0; i < len(tc.Commands); i++ {
- if tc.Delay[i] > 0 {
- time.Sleep(tc.Delay[i])
- }
- result := FireCommand(conn, tc.Commands[i])
- expected := tc.Expected[i]
- assert.Equal(t, expected, result)
- }
-
- for _, cmd := range tc.CleanUp {
- FireCommand(conn, cmd)
- }
- })
- }
-}
-
-func TestBitfieldRO(t *testing.T) {
- conn := getLocalConnection()
- defer conn.Close()
-
- FireCommand(conn, "FLUSHDB")
- defer FireCommand(conn, "FLUSHDB")
-
- syntaxErrMsg := "ERR syntax error"
- bitFieldTypeErrMsg := "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is."
- unsupportedCmdErrMsg := "ERR BITFIELD_RO only supports the GET subcommand"
-
- testCases := []struct {
- Name string
- Commands []string
- Expected []interface{}
- Delay []time.Duration
- CleanUp []string
- }{
- {
- Name: "BITFIELD_RO Arity Check",
- Commands: []string{"bitfield_ro"},
- Expected: []interface{}{"ERR wrong number of arguments for 'bitfield_ro' command"},
- Delay: []time.Duration{0},
- CleanUp: []string{},
- },
- {
- Name: "BITFIELD_RO on unsupported type of SET",
- Commands: []string{"SADD bits a b c", "bitfield_ro bits"},
- Expected: []interface{}{int64(3), "WRONGTYPE Operation against a key holding the wrong kind of value"},
- Delay: []time.Duration{0, 0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD_RO on unsupported type of JSON",
- Commands: []string{"json.set bits $ 1", "bitfield_ro bits"},
- Expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
- Delay: []time.Duration{0, 0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD_RO on unsupported type of HSET",
- Commands: []string{"HSET bits a 1", "bitfield_ro bits"},
- Expected: []interface{}{int64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"},
- Delay: []time.Duration{0, 0},
- CleanUp: []string{"DEL bits"},
- },
- {
- Name: "BITFIELD_RO with unsupported commands",
- Commands: []string{
- "bitfield_ro bits set u8 0 255",
- "bitfield_ro bits incrby u8 0 100",
- },
- Expected: []interface{}{
- unsupportedCmdErrMsg,
- unsupportedCmdErrMsg,
- },
- Delay: []time.Duration{0, 0},
- CleanUp: []string{"Del bits"},
- },
- {
- Name: "BITFIELD_RO with syntax error",
- Commands: []string{
- "set bits 1",
- "bitfield_ro bits get u8",
- "bitfield_ro bits get",
- "bitfield_ro bits get somethingrandom",
- },
- Expected: []interface{}{
- "OK",
- syntaxErrMsg,
- syntaxErrMsg,
- syntaxErrMsg,
- },
- Delay: []time.Duration{0, 0, 0, 0},
- CleanUp: []string{"Del bits"},
- },
- {
- Name: "BITFIELD_RO with invalid bitfield type",
- Commands: []string{
- "set bits 1",
- "bitfield_ro bits get a8 0",
- "bitfield_ro bits get s8 0",
- "bitfield_ro bits get somethingrandom 0",
- },
- Expected: []interface{}{
- "OK",
- bitFieldTypeErrMsg,
- bitFieldTypeErrMsg,
- bitFieldTypeErrMsg,
- },
- Delay: []time.Duration{0, 0, 0, 0},
- CleanUp: []string{"Del bits"},
- },
- {
- Name: "BITFIELD_RO with only key as argument",
- Commands: []string{"bitfield_ro bits"},
- Expected: []interface{}{[]interface{}{}},
- Delay: []time.Duration{0},
- CleanUp: []string{"DEL bits"},
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.Name, func(t *testing.T) {
-
- for i := 0; i < len(tc.Commands); i++ {
- if tc.Delay[i] > 0 {
- time.Sleep(tc.Delay[i])
- }
- result := FireCommand(conn, tc.Commands[i])
- expected := tc.Expected[i]
- assert.Equal(t, expected, result)
- }
-
- for _, cmd := range tc.CleanUp {
- FireCommand(conn, cmd)
- }
- })
- }
-}
diff --git a/integration_tests/commands/async/deque_test.go b/integration_tests/commands/async/deque_test.go
index 2e48bc04e..1ae7875f1 100644
--- a/integration_tests/commands/async/deque_test.go
+++ b/integration_tests/commands/async/deque_test.go
@@ -121,17 +121,17 @@ func TestRPush(t *testing.T) {
}{
{
name: "RPUSH",
- cmds: []string{"LPUSH k v", "LPUSH k v1 1 v2 2", "LPUSH k 3 3 3 v3 v3 v3"},
+ cmds: []string{"RPUSH k v", "RPUSH k v1 1 v2 2", "RPUSH k 3 3 3 v3 v3 v3"},
expect: []any{int64(1), int64(5), int64(11)},
},
{
name: "RPUSH normal values",
- cmds: []string{"LPUSH k " + strings.Join(deqNormalValues, " ")},
+ cmds: []string{"RPUSH k " + strings.Join(deqNormalValues, " ")},
expect: []any{int64(25)},
},
{
name: "RPUSH edge values",
- cmds: []string{"LPUSH k " + strings.Join(deqEdgeValues, " ")},
+ cmds: []string{"RPUSH k " + strings.Join(deqEdgeValues, " ")},
expect: []any{int64(42)},
},
}
@@ -442,6 +442,58 @@ func TestLLEN(t *testing.T) {
deqCleanUp(conn, "k")
}
+func TestLPOPCount(t *testing.T) {
+ deqTestInit()
+ conn := getLocalConnection()
+ defer conn.Close()
+
+ testCases := []struct {
+ name string
+ cmds []string
+ expect []interface{}
+ }{
+ {
+ name: "LPOP with count argument - valid, invalid, and edge cases",
+ cmds: []string{
+ "RPUSH k v1 v2 v3 v4",
+ "LPOP k 2",
+ "LLEN k",
+ "LPOP k 0",
+ "LLEN k",
+ "LPOP k 5",
+ "LLEN k",
+ "LPOP k -1",
+ "LPOP k abc",
+ "LLEN k",
+ },
+ expect: []any{
+ int64(4),
+ []interface{}{"v1", "v2"},
+ int64(2),
+ []interface{}{},
+ int64(2),
+ []interface{}{"v3", "v4"},
+ int64(0),
+ "ERR value is out of range",
+ "ERR value is not an integer or out of range",
+ int64(0),
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ for i, cmd := range tc.cmds {
+ result := FireCommand(conn, cmd)
+ assert.Equal(t, tc.expect[i], result)
+ }
+ })
+ }
+
+ deqCleanUp(conn, "k")
+
+}
+
func deqCleanUp(conn net.Conn, key string) {
for {
result := FireCommand(conn, "LPOP "+key)
diff --git a/integration_tests/commands/async/object_test.go b/integration_tests/commands/async/object_test.go
index 93329411d..f434264c4 100644
--- a/integration_tests/commands/async/object_test.go
+++ b/integration_tests/commands/async/object_test.go
@@ -1,7 +1,6 @@
package async
import (
- "fmt"
"testing"
"time"
@@ -124,7 +123,6 @@ func TestObjectCommand(t *testing.T) {
result := FireCommand(conn, cmd)
- fmt.Println(cmd, result, tc.expected[i])
if tc.assertType[i] == "equal" {
assert.Equal(t, tc.expected[i], result)
} else {
diff --git a/integration_tests/commands/async/set_data_cmd_test.go b/integration_tests/commands/async/set_data_cmd_test.go
index ffac6fe07..5b16dde3b 100644
--- a/integration_tests/commands/async/set_data_cmd_test.go
+++ b/integration_tests/commands/async/set_data_cmd_test.go
@@ -36,115 +36,6 @@ func TestSetDataCommand(t *testing.T) {
assertType []string
delay []time.Duration
}{
- // SADD
- {
- name: "SADD Simple Value",
- cmd: []string{"SADD foo bar", "SMEMBERS foo"},
- expected: []interface{}{int64(1), []any{"bar"}},
- assertType: []string{"equal", "equal"},
- delay: []time.Duration{0, 0},
- },
- {
- name: "SADD Multiple Values",
- cmd: []string{"SADD foo bar", "SADD foo baz", "SMEMBERS foo"},
- expected: []interface{}{int64(1), int64(1), []any{"bar", "baz"}},
- assertType: []string{"equal", "equal", "equal"},
- delay: []time.Duration{0, 0, 0},
- },
- {
- name: "SADD Duplicate Values",
- cmd: []string{"SADD foo bar", "SADD foo bar", "SMEMBERS foo"},
- expected: []interface{}{int64(1), int64(0), []any{"bar"}},
- assertType: []string{"equal", "equal", "equal"},
- delay: []time.Duration{0, 0, 0},
- },
- {
- name: "SADD Wrong Key Value Type",
- cmd: []string{"SET foo bar", "SADD foo baz"},
- expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
- assertType: []string{"equal", "equal"},
- delay: []time.Duration{0, 0},
- },
- {
- name: "SADD Multiple add and multiple kind of values",
- cmd: []string{"SADD foo bar", "SADD foo baz", "SADD foo 1", "SMEMBERS foo"},
- expected: []interface{}{int64(1), int64(1), int64(1), []any{"bar", "baz", "1"}},
- assertType: []string{"equal", "equal", "equal", "equal"},
- delay: []time.Duration{0, 0, 0, 0},
- },
- // SCARD
- {
- name: "SADD & SCARD",
- cmd: []string{"SADD foo bar", "SADD foo baz", "SCARD foo"},
- expected: []interface{}{int64(1), int64(1), int64(2)},
- assertType: []string{"equal", "equal", "equal"},
- delay: []time.Duration{0, 0, 0},
- },
- {
- name: "SADD & CARD with non existing key",
- cmd: []string{"SADD foo bar", "SADD foo baz", "SCARD bar"},
- expected: []interface{}{int64(1), int64(1), int64(0)},
- assertType: []string{"equal", "equal", "equal"},
- delay: []time.Duration{0, 0, 0},
- },
- {
- name: "SADD & SCARD with wrong key type",
- cmd: []string{"SET foo bar", "SCARD foo"},
- expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
- assertType: []string{"equal", "equal"},
- delay: []time.Duration{0, 0},
- },
- // SMEMBERS
- {
- name: "SADD & SMEMBERS",
- cmd: []string{"SADD foo bar", "SADD foo baz", "SMEMBERS foo"},
- expected: []interface{}{int64(1), int64(1), []any{"bar", "baz"}},
- assertType: []string{"equal", "equal", "equal"},
- delay: []time.Duration{0, 0, 0},
- },
- {
- name: "SADD & SMEMBERS with non existing key",
- cmd: []string{"SMEMBERS foo"},
- expected: []interface{}{[]any{}},
- assertType: []string{"equal"},
- delay: []time.Duration{0},
- },
- {
- name: "SADD & SMEMBERS with wrong key type",
- cmd: []string{"SET foo bar", "SMEMBERS foo"},
- expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
- assertType: []string{"equal", "equal"},
- delay: []time.Duration{0, 0},
- },
- // SREM
- {
- name: "SADD & SREM",
- cmd: []string{"SADD foo bar", "SADD foo baz", "SREM foo bar", "SMEMBERS foo"},
- expected: []interface{}{int64(1), int64(1), int64(1), []any{"baz"}},
- assertType: []string{"equal", "equal", "equal", "equal"},
- delay: []time.Duration{0, 0, 0, 0},
- },
- {
- name: "SADD & SREM with non existing key",
- cmd: []string{"SREM foo bar"},
- expected: []interface{}{int64(0)},
- assertType: []string{"equal"},
- delay: []time.Duration{0},
- },
- {
- name: "SADD & SREM with wrong key type",
- cmd: []string{"SET foo bar", "SREM foo bar"},
- expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
- assertType: []string{"equal", "equal"},
- delay: []time.Duration{0, 0},
- },
- {
- name: "SADD & SREM with non existing value",
- cmd: []string{"SADD foo bar baz bax", "SMEMBERS foo", "SREM foo bat", "SMEMBERS foo"},
- expected: []interface{}{int64(3), []any{"bar", "baz", "bax"}, int64(0), []any{"bar", "baz", "bax"}},
- assertType: []string{"equal", "equal", "equal", "equal"},
- delay: []time.Duration{0, 0, 0, 0},
- },
// SADD & SDIFF
{
name: "SADD & SDIFF",
diff --git a/integration_tests/commands/async/setup.go b/integration_tests/commands/async/setup.go
index cfdcaebdc..b8a3ea684 100644
--- a/integration_tests/commands/async/setup.go
+++ b/integration_tests/commands/async/setup.go
@@ -123,7 +123,7 @@ func RunTestServer(ctx context.Context, wg *sync.WaitGroup, opt TestServerOption
gec := make(chan error)
shardManager := shard.NewShardManager(1, watchChan, nil, gec)
// Initialize the AsyncServer
- testServer := server.NewAsyncServer(shardManager, watchChan)
+ testServer := server.NewAsyncServer(shardManager, watchChan, nil)
// Try to bind to a port with a maximum of `totalRetries` retries.
for i := 0; i < totalRetries; i++ {
diff --git a/integration_tests/commands/http/bit_test.go b/integration_tests/commands/http/bit_test.go
index 783ec4249..a26456504 100644
--- a/integration_tests/commands/http/bit_test.go
+++ b/integration_tests/commands/http/bit_test.go
@@ -1,11 +1,570 @@
package http
+// The following commands are a part of this test class:
+// SETBIT, GETBIT, BITCOUNT, BITOP, BITPOS, BITFIELD, BITFIELD_RO
+
import (
"fmt"
- "github.com/stretchr/testify/assert"
+ "math"
+ "strings"
"testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
)
+// TODO: BITOP has not been migrated yet. Once done, we can uncomment the tests - please check accuracy and validate for expected values.
+
+// func TestBitOp(t *testing.T) {
+// exec := NewHTTPCommandExecutor()
+
+// testcases := []struct {
+// InCmds []HTTPCommand
+// Out []interface{}
+// }{
+// {
+// InCmds: []HTTPCommand{
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "unitTestKeyA", "values": []interface{}{1, 1}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "unitTestKeyA", "values": []interface{}{3, 1}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "unitTestKeyA", "values": []interface{}{5, 1}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "unitTestKeyA", "values": []interface{}{7, 1}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "unitTestKeyA", "values": []interface{}{8, 1}}},
+// },
+// Out: []interface{}{float64(0), float64(0), float64(0), float64(0), float64(0)},
+// },
+// {
+// InCmds: []HTTPCommand{
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "unitTestKeyB", "values": []interface{}{2, 1}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "unitTestKeyB", "values": []interface{}{4, 1}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "unitTestKeyB", "values": []interface{}{7, 1}}},
+// },
+// Out: []interface{}{float64(0), float64(0), float64(0)},
+// },
+// {
+// InCmds: []HTTPCommand{
+// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "bar"}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{2, 1}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{4, 1}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{7, 1}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "foo"}},
+// },
+// Out: []interface{}{"OK", float64(1), float64(0), float64(0), "kar"},
+// },
+// {
+// InCmds: []HTTPCommand{
+// {Command: "SET", Body: map[string]interface{}{"key": "mykey12", "value": "1343"}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "mykey12", "values": []interface{}{2, 1}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "mykey12", "values": []interface{}{4, 1}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "mykey12", "values": []interface{}{7, 1}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "mykey12"}},
+// },
+// Out: []interface{}{"OK", float64(1), float64(0), float64(1), float64(9343)},
+// },
+// {
+// InCmds: []HTTPCommand{{Command: "SET", Body: map[string]interface{}{"key": "foo12", "value": "bar"}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo12", "values": []interface{}{2, 1}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo12", "values": []interface{}{4, 1}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo12", "values": []interface{}{7, 1}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "foo12"}},
+// },
+// Out: []interface{}{"OK", float64(1), float64(0), float64(0), "kar"},
+// },
+// {
+// InCmds: []HTTPCommand{
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"NOT", "unitTestKeyNOT", "unitTestKeyA"}}},
+// },
+// Out: []interface{}{float64(2)},
+// },
+// {
+// InCmds: []HTTPCommand{
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyNOT", "values": []interface{}{1}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyNOT", "values": []interface{}{2}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyNOT", "values": []interface{}{7}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyNOT", "values": []interface{}{8}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyNOT", "values": []interface{}{9}}},
+// },
+// Out: []interface{}{float64(0), float64(1), float64(0), float64(0), float64(1)},
+// },
+// {
+// InCmds: []HTTPCommand{
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"OR", "unitTestKeyOR", "unitTestKeyB", "unitTestKeyA"}}},
+// },
+// Out: []interface{}{float64(2)},
+// },
+// {
+// InCmds: []HTTPCommand{
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyOR", "values": []interface{}{1}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyOR", "values": []interface{}{2}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyOR", "values": []interface{}{3}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyOR", "values": []interface{}{7}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyOR", "values": []interface{}{8}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyOR", "values": []interface{}{9}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyOR", "values": []interface{}{12}}},
+// },
+// Out: []interface{}{float64(1), float64(1), float64(1), float64(1), float64(1), float64(0), float64(0)},
+// },
+// {
+// InCmds: []HTTPCommand{
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"AND", "unitTestKeyAND", "unitTestKeyB", "unitTestKeyA"}}},
+// },
+// Out: []interface{}{float64(2)},
+// },
+// {
+// InCmds: []HTTPCommand{
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyAND", "values": []interface{}{1}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyAND", "values": []interface{}{2}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyAND", "values": []interface{}{7}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyAND", "values": []interface{}{8}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyAND", "values": []interface{}{9}}},
+// },
+// Out: []interface{}{float64(0), float64(0), float64(1), float64(0), float64(0)},
+// },
+// {
+// InCmds: []HTTPCommand{
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"XOR", "unitTestKeyXOR", "unitTestKeyB", "unitTestKeyA"}}},
+// },
+// Out: []interface{}{float64(2)},
+// },
+// {
+// InCmds: []HTTPCommand{
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyXOR", "values": []interface{}{1}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyXOR", "values": []interface{}{2}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyXOR", "values": []interface{}{3}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyXOR", "values": []interface{}{7}}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "unitTestKeyXOR", "values": []interface{}{8}}},
+// },
+// Out: []interface{}{float64(1), float64(1), float64(1), float64(0), float64(1)},
+// },
+// }
+
+// for _, tcase := range testcases {
+// for i := 0; i < len(tcase.InCmds); i++ {
+// cmd := tcase.InCmds[i]
+// out := tcase.Out[i]
+// res, _ := exec.FireCommand(cmd)
+// assert.Equal(t, out, res, "Value mismatch for cmd %s\n.", cmd)
+// }
+// }
+// }
+
+// func TestBitOpsString(t *testing.T) {
+
+// exec := NewHTTPCommandExecutor()
+
+// // foobar in bits is 01100110 01101111 01101111 01100010 01100001 01110010
+// fooBarBits := "011001100110111101101111011000100110000101110010"
+// // randomly get 8 bits for testing
+// testOffsets := make([]int, 8)
+
+// for i := 0; i < 8; i++ {
+// testOffsets[i] = rand.Intn(len(fooBarBits))
+// }
+
+// getBitTestCommands := make([]HTTPCommand, 8+1)
+// getBitTestExpected := make([]interface{}, 8+1)
+
+// getBitTestCommands[0] = HTTPCommand{Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "foobar"}}
+// getBitTestExpected[0] = "OK"
+
+// for i := 1; i < 8+1; i++ {
+// getBitTestCommands[i] = HTTPCommand{Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "value": fmt.Sprintf("%d", testOffsets[i-1])}}
+// getBitTestExpected[i] = float64(fooBarBits[testOffsets[i-1]] - '0')
+// }
+
+// testCases := []struct {
+// name string
+// cmds []HTTPCommand
+// expected []interface{}
+// assertType []string
+// }{
+// {
+// name: "Getbit of a key containing a string",
+// cmds: getBitTestCommands,
+// expected: getBitTestExpected,
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Getbit of a key containing an integer",
+// cmds: []HTTPCommand{
+// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "10"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "0"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "1"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "2"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "3"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "4"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "5"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "6"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "7"}},
+// },
+// expected: []interface{}{"OK", float64(0), float64(0), float64(1), float64(1), float64(0), float64(0), float64(0), float64(1)},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Getbit of a key containing an integer 2nd byte",
+// cmds: []HTTPCommand{
+// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "10"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "8"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "9"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "10"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "11"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "12"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "13"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "14"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "15"}},
+// },
+// expected: []interface{}{"OK", float64(0), float64(0), float64(1), float64(1), float64(0), float64(0), float64(0), float64(0)},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Getbit of a key with an offset greater than the length of the string in bits",
+// cmds: []HTTPCommand{
+// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "foobar"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "100"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "48"}},
+// {Command: "GETBIT", Body: map[string]interface{}{"key": "foo", "offset": "47"}},
+// },
+// expected: []interface{}{"OK", float64(0), float64(0), float64(0)},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Bitcount of a key containing a string",
+// cmds: []HTTPCommand{
+// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "foobar"}},
+// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{0, -1}}},
+// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo"}},
+// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{0, 0}}},
+// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{1, 1}}},
+// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{1, 1, "BYTE"}}},
+// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{5, 30, "BIT"}}},
+// },
+// expected: []interface{}{"OK", float64(26), float64(26), float64(4), float64(6), float64(6), float64(17)},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Bitcount of a key containing an integer",
+// cmds: []HTTPCommand{
+// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "10"}},
+// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{0, -1}}},
+// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo"}},
+// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{0, 0}}},
+// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{1, 1}}},
+// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{1, 1, "BYTE"}}},
+// {Command: "BITCOUNT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{5, 30, "BIT"}}},
+// },
+// expected: []interface{}{"OK", float64(5), float64(5), float64(3), float64(2), float64(2), float64(3)},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Setbit of a key containing a string",
+// cmds: []HTTPCommand{
+// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "foobar"}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{7, 1}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "foo"}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{49, 1}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{50, 1}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "foo"}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{49, 0}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "foo"}},
+// },
+// expected: []interface{}{"OK", float64(0), "goobar", float64(0), float64(0), "goobar`", float64(1), "goobar "},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Setbit of a key must not change the expiry of the key if expiry is set",
+// cmds: []HTTPCommand{
+// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "foobar"}},
+// {Command: "EXPIRE", Body: map[string]interface{}{"key": "foo", "values": []interface{}{100}}},
+// {Command: "TTL", Body: map[string]interface{}{"key": "foo"}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{7, 1}}},
+// {Command: "TTL", Body: map[string]interface{}{"key": "foo"}},
+// },
+// expected: []interface{}{"OK", float64(1), float64(100), float64(0), float64(100)},
+// assertType: []string{"equal", "equal", "less", "equal", "less"},
+// },
+// {
+// name: "Setbit of a key must not add expiry to the key if expiry is not set",
+// cmds: []HTTPCommand{
+// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "foobar"}},
+// {Command: "TTL", Body: map[string]interface{}{"key": "foo"}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{7, 1}}},
+// {Command: "TTL", Body: map[string]interface{}{"key": "foo"}},
+// },
+// expected: []interface{}{"OK", float64(-1), float64(0), float64(-1)},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Bitop not of a key containing a string",
+// cmds: []HTTPCommand{
+// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "foobar"}},
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"NOT", "baz", "foo"}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "baz"}},
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"NOT", "bazz", "baz"}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "bazz"}},
+// },
+// expected: []interface{}{"OK", float64(6), "\\x99\\x90\\x90\\x9d\\x9e\\x8d", float64(6), "foobar"},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Bitop not of a key containing an integer",
+// cmds: []HTTPCommand{
+// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": 10}},
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"NOT", "baz", "foo"}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "baz"}},
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"NOT", "bazz", "baz"}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "bazz"}},
+// },
+// expected: []interface{}{"OK", float64(2), "\\xce\\xcf", float64(2), float64(10)},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Get a string created with setbit",
+// cmds: []HTTPCommand{
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{1, 1}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "foo", "values": []interface{}{3, 1}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "foo"}},
+// },
+// expected: []interface{}{float64(0), float64(0), "P"},
+// assertType: []string{"equal", "equal", "equal"},
+// },
+// {
+// name: "Bitop and of keys containing a string and get the destkey",
+// cmds: []HTTPCommand{
+// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "foobar"}},
+// {Command: "SET", Body: map[string]interface{}{"key": "baz", "value": "abcdef"}},
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"AND", "bazz", "foo", "baz"}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "bazz"}},
+// },
+// expected: []interface{}{"OK", "OK", float64(6), "`bc`ab"},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP AND of keys containing integers and get the destkey",
+// cmds: []HTTPCommand{
+// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": 10}},
+// {Command: "SET", Body: map[string]interface{}{"key": "baz", "value": 5}},
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"AND", "bazz", "foo", "baz"}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "bazz"}},
+// },
+// expected: []interface{}{"OK", "OK", float64(2), "1\x00"},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Bitop or of keys containing a string, a bytearray and get the destkey",
+// cmds: []HTTPCommand{
+// {Command: "MSET", Body: map[string]interface{}{"keys": []interface{}{"foo", "foobar", "baz", "abcdef"}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "bazz", "values": []interface{}{8, 1}}},
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"AND", "bazzz", "foo", "baz", "bazz"}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "bazzz"}},
+// },
+// expected: []interface{}{"OK", float64(0), float64(6), "\x00\x00\x00\x00\x00\x00"},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP OR of keys containing strings and get the destkey",
+// cmds: []HTTPCommand{
+// {Command: "MSET", Body: map[string]interface{}{"keys": []interface{}{"foo", "foobar", "baz", "abcdef"}}},
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"OR", "bazz", "foo", "baz"}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "bazz"}},
+// },
+// expected: []interface{}{"OK", float64(6), "goofev"},
+// assertType: []string{"equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP OR of keys containing integers and get the destkey",
+// cmds: []HTTPCommand{
+// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": 10}},
+// {Command: "SET", Body: map[string]interface{}{"key": "baz", "value": 5}},
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"OR", "bazz", "foo", "baz"}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "bazz"}},
+// },
+// expected: []interface{}{"OK", "OK", float64(2), "50"},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP OR of keys containing strings and a bytearray and get the destkey",
+// cmds: []HTTPCommand{
+// {Command: "MSET", Body: map[string]interface{}{"keys": []interface{}{"foo", "foobar", "baz", "abcdef"}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "bazz", "values": []interface{}{8, 1}}},
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"OR", "bazzz", "foo", "baz", "bazz"}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "bazzz"}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "bazz", "values": []interface{}{8, 0}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "bazz", "values": []interface{}{49, 1}}},
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"OR", "bazzz", "foo", "baz", "bazz"}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "bazzz"}},
+// },
+// expected: []interface{}{"OK", float64(0), float64(6), "g\xefofev", float64(1), float64(0), float64(7), "goofev@"},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP XOR of keys containing strings and get the destkey",
+// cmds: []HTTPCommand{
+// {Command: "MSET", Body: map[string]interface{}{"keys": []interface{}{"foo", "foobar", "baz", "abcdef"}}},
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"XOR", "bazz", "foo", "baz"}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "bazz"}},
+// },
+// expected: []interface{}{"OK", float64(6), "\x07\x0d\x0c\x06\x04\x14"},
+// assertType: []string{"equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP XOR of keys containing strings and a bytearray and get the destkey",
+// cmds: []HTTPCommand{
+// {Command: "MSET", Body: map[string]interface{}{"keys": []interface{}{"foo", "foobar", "baz", "abcdef"}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "bazz", "values": []interface{}{8, 1}}},
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"XOR", "bazzz", "foo", "baz", "bazz"}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "bazzz"}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "bazz", "values": []interface{}{8, 0}}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "bazz", "values": []interface{}{49, 1}}},
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"XOR", "bazzz", "foo", "baz", "bazz"}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "bazzz"}},
+// {Command: "SETBIT", Body: map[string]interface{}{"key": "bazz", "values": []interface{}{49, 0}}},
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"XOR", "bazzz", "foo", "baz", "bazz"}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "bazzz"}},
+// },
+// expected: []interface{}{"OK", float64(0), float64(6), "\x07\x8d\x0c\x06\x04\x14", float64(1), float64(0), float64(7), "\x07\r\x0c\x06\x04\x14@", float64(1), float64(7), "\x07\r\x0c\x06\x04\x14\x00"},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP XOR of keys containing integers and get the destkey",
+// cmds: []HTTPCommand{
+// {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": 10}},
+// {Command: "SET", Body: map[string]interface{}{"key": "baz", "value": 5}},
+// {Command: "BITOP", Body: map[string]interface{}{"values": []interface{}{"XOR", "bazz", "foo", "baz"}}},
+// {Command: "GET", Body: map[string]interface{}{"key": "bazz"}},
+// },
+// expected: []interface{}{"OK", "OK", float64(2), "\x040"},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// }
+
+// for _, tc := range testCases {
+// t.Run(tc.name, func(t *testing.T) {
+// // Delete the key before running the test
+// exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"key": "foo"}})
+// exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"key": "baz"}})
+// exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"key": "bazz"}})
+// exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"key": "bazzz"}})
+// for i := 0; i < len(tc.cmds); i++ {
+// res, _ := exec.FireCommand(tc.cmds[i])
+
+// switch tc.assertType[i] {
+// case "equal":
+// assert.Equal(t, tc.expected[i], res)
+// case "less":
+// assert.True(t, res.(float64) <= tc.expected[i].(float64), "CMD: %s Expected %d to be less than or equal to %d", tc.cmds[i], res, tc.expected[i])
+// }
+// }
+// })
+// }
+// }
+
+func TestBitCount(t *testing.T) {
+ exec := NewHTTPCommandExecutor()
+ testcases := []struct {
+ InCmds []HTTPCommand
+ Out []interface{}
+ }{
+ {
+ InCmds: []HTTPCommand{
+ {Command: "SETBIT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{7, 1}}},
+ },
+ Out: []interface{}{float64(0)},
+ },
+ {
+ InCmds: []HTTPCommand{
+ {Command: "SETBIT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{7, 1}}},
+ },
+ Out: []interface{}{float64(1)},
+ },
+ {
+ InCmds: []HTTPCommand{
+ {Command: "SETBIT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{122, 1}}},
+ },
+ Out: []interface{}{float64(0)},
+ },
+ {
+ InCmds: []HTTPCommand{
+ {Command: "GETBIT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{122}}},
+ },
+ Out: []interface{}{float64(1)},
+ },
+ {
+ InCmds: []HTTPCommand{
+ {Command: "SETBIT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{122, 0}}},
+ },
+ Out: []interface{}{float64(1)},
+ },
+ {
+ InCmds: []HTTPCommand{
+ {Command: "GETBIT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{122}}},
+ },
+ Out: []interface{}{float64(0)},
+ },
+ {
+ InCmds: []HTTPCommand{
+ {Command: "GETBIT", Body: map[string]interface{}{"key": "mykey", "value": 1223232}},
+ },
+ Out: []interface{}{float64(0)},
+ },
+ {
+ InCmds: []HTTPCommand{
+ {Command: "GETBIT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{7}}},
+ },
+ Out: []interface{}{float64(1)},
+ },
+ {
+ InCmds: []HTTPCommand{
+ {Command: "GETBIT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{8}}},
+ },
+ Out: []interface{}{float64(0)},
+ },
+ {
+ InCmds: []HTTPCommand{
+ {Command: "BITCOUNT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{3, 7, "BIT"}}},
+ },
+ Out: []interface{}{float64(1)},
+ },
+ {
+ InCmds: []HTTPCommand{
+ {Command: "BITCOUNT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{3, 7}}},
+ },
+ Out: []interface{}{float64(0)},
+ },
+ {
+ InCmds: []HTTPCommand{
+ {Command: "BITCOUNT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{0, 0}}},
+ },
+ Out: []interface{}{float64(1)},
+ },
+ {
+ InCmds: []HTTPCommand{
+ {Command: "BITCOUNT"},
+ },
+ Out: []interface{}{"ERR wrong number of arguments for 'bitcount' command"},
+ },
+ {
+ InCmds: []HTTPCommand{
+ {Command: "BITCOUNT", Body: map[string]interface{}{"key": "mykey"}},
+ },
+ Out: []interface{}{float64(1)},
+ },
+ {
+ InCmds: []HTTPCommand{
+ {Command: "BITCOUNT", Body: map[string]interface{}{"key": "mykey", "values": []interface{}{0}}},
+ },
+ Out: []interface{}{"ERR syntax error"},
+ },
+ }
+
+ for _, tcase := range testcases {
+ for i := 0; i < len(tcase.InCmds); i++ {
+ cmd := tcase.InCmds[i]
+ out := tcase.Out[i]
+ res, _ := exec.FireCommand(cmd)
+ assert.Equal(t, out, res, "Value mismatch for cmd %s\n.", cmd)
+ }
+ }
+}
+
func TestBitPos(t *testing.T) {
exec := NewHTTPCommandExecutor()
@@ -18,6 +577,36 @@ func TestBitPos(t *testing.T) {
out interface{}
setCmdSETBIT bool
}{
+ {
+ name: "String interval BIT 0,-1 ",
+ val: "\\x00\\xff\\x00",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 0, -1, "bit"}}},
+ out: float64(0),
+ },
+ {
+ name: "String interval BIT 8,-1",
+ val: "\\x00\\xff\\x00",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 8, -1, "bit"}}},
+ out: float64(8),
+ },
+ {
+ name: "String interval BIT 16,-1",
+ val: "\\x00\\xff\\x00",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 16, -1, "bit"}}},
+ out: float64(16),
+ },
+ {
+ name: "String interval BIT 16,200",
+ val: "\\x00\\xff\\x00",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 16, 200, "bit"}}},
+ out: float64(16),
+ },
+ {
+ name: "String interval BIT 8,8",
+ val: "\\x00\\xff\\x00",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 8, 8, "bit"}}},
+ out: float64(8),
+ },
{
name: "FindsFirstZeroBit",
val: []byte("\xff\xf0\x00"),
@@ -30,6 +619,12 @@ func TestBitPos(t *testing.T) {
inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}},
out: float64(12),
},
+ {
+ name: "NoOneBitFound",
+ val: "\x00\x00\x00",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}},
+ out: float64(-1),
+ },
{
name: "NoZeroBitFound",
val: []byte("\xff\xff\xff"),
@@ -60,6 +655,259 @@ func TestBitPos(t *testing.T) {
inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 2, 2, "BIT"}}},
out: float64(-1),
},
+ {
+ name: "FindsFirstZeroBitInRange",
+ val: []byte("\xff\xf0\xff"),
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 1, 2}}},
+ out: float64(12),
+ },
+ {
+ name: "FindsFirstOneBitInRange",
+ val: "\x00\x00\xf0",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, 2, 3}}},
+ out: float64(16),
+ },
+ {
+ name: "StartGreaterThanEnd",
+ val: "\xff\xf0\x00",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 3, 2}}},
+ out: float64(-1),
+ },
+ {
+ name: "FindsFirstOneBitWithNegativeStart",
+ val: []byte("\x00\x00\xf0"),
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, -2, -1}}},
+ out: float64(16),
+ },
+ {
+ name: "FindsFirstZeroBitWithNegativeEnd",
+ val: []byte("\xff\xf0\xff"),
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 1, -1}}},
+ out: float64(12),
+ },
+ {
+ name: "FindsFirstZeroBitInByteRange",
+ val: []byte("\xff\x00\xff"),
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 1, 2, "BYTE"}}},
+ out: float64(8),
+ },
+ {
+ name: "FindsFirstOneBitInBitRange",
+ val: "\x00\x01\x00",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, 0, 16, "BIT"}}},
+ out: float64(15),
+ },
+ {
+ name: "NoBitFoundInByteRange",
+ val: []byte("\xff\xff\xff"),
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 0, 2, "BYTE"}}},
+ out: float64(-1),
+ },
+ {
+ name: "NoBitFoundInBitRange",
+ val: "\x00\x00\x00",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, 0, 23, "BIT"}}},
+ out: float64(-1),
+ },
+ {
+ name: "EmptyStringReturnsMinusOneForZeroBit",
+ val: []byte(""),
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 0}},
+ out: float64(-1),
+ },
+ {
+ name: "EmptyStringReturnsMinusOneForOneBit",
+ val: []byte(""),
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}},
+ out: float64(-1),
+ },
+ {
+ name: "SingleByteString",
+ val: "\x80",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}},
+ out: float64(0),
+ },
+ {
+ name: "RangeExceedsStringLength",
+ val: "\x00\xff",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, 0, 20, "BIT"}}},
+ out: float64(8),
+ },
+ {
+ name: "InvalidBitArgument",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 2}},
+ out: "ERR the bit argument must be 1 or 0",
+ },
+ {
+ name: "NonIntegerStartParameter",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, "start"}}},
+ out: "ERR value is not an integer or out of range",
+ },
+ {
+ name: "NonIntegerEndParameter",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 1, "end"}}},
+ out: "ERR value is not an integer or out of range",
+ },
+ {
+ name: "InvalidRangeType",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 1, 2, "BYTEs"}}},
+ out: "ERR syntax error",
+ },
+ {
+ name: "InsufficientArguments",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey"}},
+ out: "ERR wrong number of arguments for 'bitpos' command",
+ },
+ {
+ name: "NonExistentKeyForZeroBit",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "nonexistentkey", "value": 0}},
+ out: float64(0),
+ },
+ {
+ name: "NonExistentKeyForOneBit",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "nonexistentkey", "value": 1}},
+ out: float64(-1),
+ },
+ {
+ name: "IntegerValue",
+ val: 65280, // 0xFF00 in decimal
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 0}},
+ out: float64(0),
+ },
+ {
+ name: "LargeIntegerValue",
+ val: 16777215, // 0xFFFFFF in decimal
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}},
+ out: float64(2),
+ },
+ {
+ name: "SmallIntegerValue",
+ val: 1, // 0x01 in decimal
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 0}},
+ out: float64(0),
+ },
+ {
+ name: "ZeroIntegerValue",
+ val: 0,
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}},
+ out: float64(2),
+ },
+ {
+ name: "BitRangeStartGreaterThanBitLength",
+ val: []byte("\xff\xff\xff"),
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 25, 30, "BIT"}}},
+ out: float64(-1),
+ },
+ {
+ name: "BitRangeEndExceedsBitLength",
+ val: []byte("\xff\xff\xff"),
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 0, 30, "BIT"}}},
+ out: float64(-1),
+ },
+ {
+ name: "NegativeStartInBitRange",
+ val: []byte("\x00\xff\xff"),
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, -16, -1, "BIT"}}},
+ out: float64(8),
+ },
+ {
+ name: "LargeNegativeStart",
+ val: []byte("\x00\xff\xff"),
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, -100, -1}}},
+ out: float64(8),
+ },
+ {
+ name: "LargePositiveEnd",
+ val: "\x00\xff\xff",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, 0, 100}}},
+ out: float64(8),
+ },
+ {
+ name: "StartAndEndEqualInByteRange",
+ val: []byte("\x0f\xff\xff"),
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, 1, 1, "BYTE"}}},
+ out: float64(-1),
+ },
+ {
+ name: "StartAndEndEqualInBitRange",
+ val: "\x0f\xff\xff",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, 1, 1, "BIT"}}},
+ out: float64(-1),
+ },
+ {
+ name: "FindFirstZeroBitInNegativeRange",
+ val: []byte("\xff\x00\xff"),
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{0, -2, -1}}},
+ out: float64(8),
+ },
+ {
+ name: "FindFirstOneBitInNegativeRangeBIT",
+ val: []byte("\x00\x00\x80"),
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "values": []interface{}{1, -8, -1, "BIT"}}},
+ out: float64(16),
+ },
+ {
+ name: "MaxIntegerValue",
+ val: math.MaxInt64,
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 0}},
+ out: float64(0),
+ },
+ {
+ name: "MinIntegerValue",
+ val: math.MinInt64,
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}},
+ out: float64(2),
+ },
+ {
+ name: "SingleBitStringZero",
+ val: "\x00",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}},
+ out: float64(-1),
+ },
+ {
+ name: "SingleBitStringOne",
+ val: "\x01",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 0}},
+ out: float64(0),
+ },
+ {
+ name: "AllBitsSetExceptLast",
+ val: []byte("\xff\xff\xfe"),
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 0}},
+ out: float64(23),
+ },
+ {
+ name: "OnlyLastBitSet",
+ val: "\x00\x00\x01",
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 1}},
+ out: float64(23),
+ },
+ {
+ name: "AlternatingBitsLongString",
+ val: []byte("\xaa\xaa\xaa\xaa\xaa"),
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 0}},
+ out: float64(1),
+ },
+ {
+ name: "VeryLargeByteString",
+ val: []byte(strings.Repeat("\xff", 1000) + "\x00"),
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkey", "value": 0}},
+ out: float64(8000),
+ },
+ {
+ name: "FindZeroBitOnSetBitKey",
+ val: []interface{}{8, 1},
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkeysb", "value": 1}},
+ out: float64(8),
+ setCmdSETBIT: true,
+ },
+ {
+ name: "FindOneBitOnSetBitKey",
+ val: []interface{}{1, 1},
+ inCmd: HTTPCommand{Command: "BITPOS", Body: map[string]interface{}{"key": "testkeysb", "value": 1}},
+ out: float64(1),
+ setCmdSETBIT: true,
+ },
}
for _, tc := range testcases {
@@ -68,7 +916,7 @@ func TestBitPos(t *testing.T) {
if tc.setCmdSETBIT {
setCmd = HTTPCommand{
Command: "SETBIT",
- Body: map[string]interface{}{"key": "testkeysb", "value": fmt.Sprintf("%s", tc.val.(string))},
+ Body: map[string]interface{}{"key": "testkeysb", "values": tc.val},
}
} else {
switch v := tc.val.(type) {
@@ -80,7 +928,7 @@ func TestBitPos(t *testing.T) {
case string:
setCmd = HTTPCommand{
Command: "SET",
- Body: map[string]interface{}{"key": "testkey", "value": fmt.Sprintf("%s", v)},
+ Body: map[string]interface{}{"key": "testkey", "value": v},
}
case int:
setCmd = HTTPCommand{
@@ -102,3 +950,378 @@ func TestBitPos(t *testing.T) {
})
}
}
+
+func TestBitfield(t *testing.T) {
+ exec := NewHTTPCommandExecutor()
+
+ exec.FireCommand(HTTPCommand{Command: "FLUSHDB"})
+ defer exec.FireCommand(HTTPCommand{Command: "FLUSHDB"}) // clean up after all test cases
+ syntaxErrMsg := "ERR syntax error"
+ bitFieldTypeErrMsg := "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is"
+ integerErrMsg := "ERR value is not an integer or out of range"
+ overflowErrMsg := "ERR Invalid OVERFLOW type specified"
+
+ testCases := []struct {
+ Name string
+ Commands []HTTPCommand
+ Expected []interface{}
+ Delay []time.Duration
+ CleanUp []HTTPCommand
+ }{
+ {
+ Name: "BITFIELD Arity Check",
+ Commands: []HTTPCommand{{Command: "BITFIELD"}},
+ Expected: []interface{}{"ERR wrong number of arguments for 'bitfield' command"},
+ Delay: []time.Duration{0},
+ CleanUp: []HTTPCommand{},
+ },
+ {
+ Name: "BITFIELD on unsupported type of SET",
+ Commands: []HTTPCommand{{Command: "SADD", Body: map[string]interface{}{"key": "bits", "values": []string{"a", "b", "c"}}}, {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits"}}},
+ Expected: []interface{}{float64(3), "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD on unsupported type of JSON",
+ Commands: []HTTPCommand{{Command: "json.set", Body: map[string]interface{}{"key": "bits", "path": "$", "value": "1"}}, {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits"}}},
+ Expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD on unsupported type of HSET",
+ Commands: []HTTPCommand{{Command: "HSET", Body: map[string]interface{}{"key": "bits", "field": "a", "value": "1"}}, {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits"}}},
+ Expected: []interface{}{float64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD with syntax errors",
+ Commands: []HTTPCommand{
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", 0, 255, "incrby", "u8", 0, 100, "get", "u8"}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "a8", 0, 255, "incrby", "u8", 0, 100, "get", "u8"}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "a", 255, "incrby", "u8", 0, 100}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", 0, 255, "incrby", "u8", 0, 100, "overflow", "wraap"}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", 0, "incrby", "u8", 0, 100, "get", "u8", 288}}},
+ },
+ Expected: []interface{}{
+ syntaxErrMsg,
+ bitFieldTypeErrMsg,
+ "ERR bit offset is not an integer or out of range",
+ overflowErrMsg,
+ integerErrMsg,
+ },
+ Delay: []time.Duration{0, 0, 0, 0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD signed SET and GET basics",
+ Commands: []HTTPCommand{
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "i8", 0, -100}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "i8", 0, 101}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "i8", 0}}},
+ },
+ Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(-100)}, []interface{}{float64(101)}},
+ Delay: []time.Duration{0, 0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD unsigned SET and GET basics",
+ Commands: []HTTPCommand{
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", 0, 255}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", 0, 100}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "u8", 0}}},
+ },
+ Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(255)}, []interface{}{float64(100)}},
+ Delay: []time.Duration{0, 0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD signed SET and GET together",
+ Commands: []HTTPCommand{{Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "i8", 0, 255, "set", "i8", 0, 100, "get", "i8", 0}}}},
+ Expected: []interface{}{[]interface{}{float64(0), float64(-1), float64(100)}},
+ Delay: []time.Duration{0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD unsigned with SET, GET and INCRBY arguments",
+ Commands: []HTTPCommand{{Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", 0, 255, "incrby", "u8", 0, 100, "get", "u8", 0}}}},
+ Expected: []interface{}{[]interface{}{float64(0), float64(99), float64(99)}},
+ Delay: []time.Duration{0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD with only key as argument",
+ Commands: []HTTPCommand{{Command: "BITFIELD", Body: map[string]interface{}{"key": "bits"}}},
+ Expected: []interface{}{[]interface{}{}},
+ Delay: []time.Duration{0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD # form",
+ Commands: []HTTPCommand{
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "#0", 65}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "#1", 66}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "#2", 67}}},
+ {Command: "GET", Body: map[string]interface{}{"key": "bits"}},
+ },
+ Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(0)}, []interface{}{float64(0)}, "ABC"},
+ Delay: []time.Duration{0, 0, 0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD basic INCRBY form",
+ Commands: []HTTPCommand{
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "#0", 10}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"incrby", "u8", "#0", 100}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"incrby", "u8", "#0", 100}}},
+ },
+ Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(110)}, []interface{}{float64(210)}},
+ Delay: []time.Duration{0, 0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD chaining of multiple commands",
+ Commands: []HTTPCommand{
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "#0", 10}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"incrby", "u8", "#0", 100, "incrby", "u8", "#0", 100}}},
+ },
+ Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(110), float64(210)}},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD unsigned overflow wrap",
+ Commands: []HTTPCommand{
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "#0", 100}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"overflow", "wrap", "incrby", "u8", "#0", 257}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "u8", "#0"}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"overflow", "wrap", "incrby", "u8", "#0", 255}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "u8", "#0"}}},
+ },
+ Expected: []interface{}{
+ []interface{}{float64(0)},
+ []interface{}{float64(101)},
+ []interface{}{float64(101)},
+ []interface{}{float64(100)},
+ []interface{}{float64(100)},
+ },
+ Delay: []time.Duration{0, 0, 0, 0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD unsigned overflow sat",
+ Commands: []HTTPCommand{
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "#0", 100}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"overflow", "sat", "incrby", "u8", "#0", 257}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "u8", "#0"}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"overflow", "sat", "incrby", "u8", "#0", -255}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "u8", "#0"}}},
+ },
+ Expected: []interface{}{
+ []interface{}{float64(0)},
+ []interface{}{float64(255)},
+ []interface{}{float64(255)},
+ []interface{}{float64(0)},
+ []interface{}{float64(0)},
+ },
+ Delay: []time.Duration{0, 0, 0, 0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD signed overflow wrap",
+ Commands: []HTTPCommand{
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "i8", "#0", 100}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"overflow", "wrap", "incrby", "i8", "#0", 257}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "i8", "#0"}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"overflow", "wrap", "incrby", "i8", "#0", 255}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "i8", "#0"}}},
+ },
+ Expected: []interface{}{
+ []interface{}{float64(0)},
+ []interface{}{float64(101)},
+ []interface{}{float64(101)},
+ []interface{}{float64(100)},
+ []interface{}{float64(100)},
+ },
+ Delay: []time.Duration{0, 0, 0, 0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD signed overflow sat",
+ Commands: []HTTPCommand{
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", "#0", 100}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"overflow", "sat", "incrby", "i8", "#0", 257}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "i8", "#0"}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"overflow", "sat", "incrby", "i8", "#0", -255}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "i8", "#0"}}},
+ },
+ Expected: []interface{}{
+ []interface{}{float64(0)},
+ []interface{}{float64(127)},
+ []interface{}{float64(127)},
+ []interface{}{float64(-128)},
+ []interface{}{float64(-128)},
+ },
+ Delay: []time.Duration{0, 0, 0, 0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD regression 1",
+ Commands: []HTTPCommand{{Command: "SET", Body: map[string]interface{}{"key": "bits", "value": "1"}}, {Command: "BITFIELD", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "u1", 0}}}},
+ Expected: []interface{}{"OK", []interface{}{float64(0)}},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD regression 2",
+ Commands: []HTTPCommand{
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "mystring", "values": []interface{}{"set", "i8", 0, 10}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "mystring", "values": []interface{}{"set", "i8", 64, 10}}},
+ {Command: "BITFIELD", Body: map[string]interface{}{"key": "mystring", "values": []interface{}{"incrby", "i8", 10, 99900}}},
+ },
+ Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(0)}, []interface{}{float64(60)}},
+ Delay: []time.Duration{0, 0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "mystring"}}},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.Name, func(t *testing.T) {
+
+ for i := 0; i < len(tc.Commands); i++ {
+ if tc.Delay[i] > 0 {
+ time.Sleep(tc.Delay[i])
+ }
+ result, _ := exec.FireCommand(tc.Commands[i])
+ expected := tc.Expected[i]
+ assert.Equal(t, expected, result)
+ }
+
+ for _, cmd := range tc.CleanUp {
+ exec.FireCommand(cmd)
+ }
+ })
+ }
+}
+
+func TestBitfieldRO(t *testing.T) {
+ exec := NewHTTPCommandExecutor()
+
+ exec.FireCommand(HTTPCommand{Command: "FLUSHDB"})
+ defer exec.FireCommand(HTTPCommand{Command: "FLUSHDB"})
+
+ syntaxErrMsg := "ERR syntax error"
+ bitFieldTypeErrMsg := "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is"
+ unsupportedCmdErrMsg := "ERR BITFIELD_RO only supports the GET subcommand"
+
+ testCases := []struct {
+ Name string
+ Commands []HTTPCommand
+ Expected []interface{}
+ Delay []time.Duration
+ CleanUp []HTTPCommand
+ }{
+ {
+ Name: "BITFIELD_RO Arity Check",
+ Commands: []HTTPCommand{{Command: "BITFIELD_RO"}},
+ Expected: []interface{}{"ERR wrong number of arguments for 'bitfield_ro' command"},
+ Delay: []time.Duration{0},
+ CleanUp: []HTTPCommand{},
+ },
+ {
+ Name: "BITFIELD_RO on unsupported type of SET",
+ Commands: []HTTPCommand{{Command: "SADD", Body: map[string]interface{}{"key": "bits", "values": []string{"a", "b", "c"}}}, {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits"}}},
+ Expected: []interface{}{float64(3), "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD_RO on unsupported type of JSON",
+ Commands: []HTTPCommand{{Command: "JSON.SET", Body: map[string]interface{}{"key": "bits", "path": "$", "value": "1"}}, {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits"}}},
+ Expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD_RO on unsupported type of HSET",
+ Commands: []HTTPCommand{{Command: "HSET", Body: map[string]interface{}{"key": "bits", "field": "a", "value": "1"}}, {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits"}}},
+ Expected: []interface{}{float64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD_RO with unsupported commands",
+ Commands: []HTTPCommand{
+ {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"set", "u8", 0, 255}}},
+ {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"incrby", "u8", 0, 100}}},
+ },
+ Expected: []interface{}{
+ unsupportedCmdErrMsg,
+ unsupportedCmdErrMsg,
+ },
+ Delay: []time.Duration{0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD_RO with syntax error",
+ Commands: []HTTPCommand{
+ {Command: "SET", Body: map[string]interface{}{"key": "bits", "value": "1"}},
+ {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "u8"}}},
+ {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get"}}},
+ {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "somethingrandom"}}},
+ },
+ Expected: []interface{}{
+ "OK",
+ syntaxErrMsg,
+ syntaxErrMsg,
+ syntaxErrMsg,
+ },
+ Delay: []time.Duration{0, 0, 0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD_RO with invalid bitfield type",
+ Commands: []HTTPCommand{
+ {Command: "SET", Body: map[string]interface{}{"key": "bits", "value": "1"}},
+ {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "a8", 0}}},
+ {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "s8", 0}}},
+ {Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits", "values": []interface{}{"get", "somethingrandom", 0}}},
+ },
+ Expected: []interface{}{
+ "OK",
+ bitFieldTypeErrMsg,
+ bitFieldTypeErrMsg,
+ bitFieldTypeErrMsg,
+ },
+ Delay: []time.Duration{0, 0, 0, 0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ {
+ Name: "BITFIELD_RO with only key as argument",
+ Commands: []HTTPCommand{{Command: "BITFIELD_RO", Body: map[string]interface{}{"key": "bits"}}},
+ Expected: []interface{}{[]interface{}{}},
+ Delay: []time.Duration{0},
+ CleanUp: []HTTPCommand{{Command: "DEL", Body: map[string]interface{}{"key": "bits"}}},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.Name, func(t *testing.T) {
+
+ for i := 0; i < len(tc.Commands); i++ {
+ if tc.Delay[i] > 0 {
+ time.Sleep(tc.Delay[i])
+ }
+ result, _ := exec.FireCommand(tc.Commands[i])
+ expected := tc.Expected[i]
+ assert.Equal(t, expected, result)
+ }
+
+ for _, cmd := range tc.CleanUp {
+ _, _ = exec.FireCommand(cmd)
+ }
+ })
+ }
+}
diff --git a/integration_tests/commands/http/bloom_test.go b/integration_tests/commands/http/bloom_test.go
index f0cae9a12..d6c47e41b 100644
--- a/integration_tests/commands/http/bloom_test.go
+++ b/integration_tests/commands/http/bloom_test.go
@@ -376,5 +376,10 @@ func TestBFEdgeCasesAndErrors(t *testing.T) {
Body: map[string]interface{}{"key": "foo"},
})
})
+ exec.FireCommand(HTTPCommand{
+ Command: "FLUSHDB",
+ Body: map[string]interface{}{"values": []interface{}{}},
+ },
+ )
}
}
diff --git a/integration_tests/commands/http/deque_test.go b/integration_tests/commands/http/deque_test.go
index 234d551f0..0502f460d 100644
--- a/integration_tests/commands/http/deque_test.go
+++ b/integration_tests/commands/http/deque_test.go
@@ -556,3 +556,51 @@ func TestLLEN(t *testing.T) {
exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"keys": [...]string{"k"}}})
}
+
+func TestLPOPCount(t *testing.T) {
+ deqTestInit()
+ exec := NewHTTPCommandExecutor()
+
+ testCases := []struct {
+ name string
+ cmds []HTTPCommand
+ expect []any
+ }{
+ {
+ name: "LPOP with count argument - valid, invalid, and edge cases",
+ cmds: []HTTPCommand{
+ {Command: "RPUSH", Body: map[string]interface{}{"key": "k", "value": "v1"}},
+ {Command: "RPUSH", Body: map[string]interface{}{"key": "k", "value": "v2"}},
+ {Command: "RPUSH", Body: map[string]interface{}{"key": "k", "value": "v3"}},
+ {Command: "RPUSH", Body: map[string]interface{}{"key": "k", "value": "v4"}},
+ {Command: "LPOP", Body: map[string]interface{}{"key": "k", "value": 2}},
+ {Command: "LPOP", Body: map[string]interface{}{"key": "k", "value": 2}},
+ {Command: "LPOP", Body: map[string]interface{}{"key": "k", "value": -1}},
+ {Command: "LPOP", Body: map[string]interface{}{"key": "k", "value": "abc"}},
+ {Command: "LLEN", Body: map[string]interface{}{"key": "k"}},
+ },
+ expect: []any{
+ float64(1),
+ float64(2),
+ float64(3),
+ float64(4),
+ []interface{}{"v1", "v2"},
+ []interface{}{"v3", "v4"},
+ "ERR value is out of range",
+ "ERR value is not an integer or out of range",
+ float64(0),
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"keys": []interface{}{"k"}}})
+ for i, cmd := range tc.cmds {
+ result, _ := exec.FireCommand(cmd)
+ assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %v", cmd)
+ }
+ })
+ }
+}
+
diff --git a/integration_tests/commands/http/hdel_test.go b/integration_tests/commands/http/hdel_test.go
new file mode 100644
index 000000000..a955fc274
--- /dev/null
+++ b/integration_tests/commands/http/hdel_test.go
@@ -0,0 +1,100 @@
+package http
+
+import (
+ "log"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestHDel(t *testing.T) {
+ exec := NewHTTPCommandExecutor()
+
+ testCases := []struct {
+ name string
+ commands []HTTPCommand
+ expected []interface{}
+ delays []time.Duration
+ }{
+ {
+ name: "HDEL with wrong number of arguments",
+ commands: []HTTPCommand{
+ {Command: "HDEL", Body: map[string]interface{}{"key": nil}},
+ {Command: "HDEL", Body: map[string]interface{}{"key": "key_hDel1"}},
+ },
+ expected: []interface{}{
+ "ERR wrong number of arguments for 'hdel' command",
+ "ERR wrong number of arguments for 'hdel' command"},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HDEL with single field",
+ commands: []HTTPCommand{
+ {Command: "HSET", Body: map[string]interface{}{"key": "key_hDel2", "field": "field1", "value": "value1"}},
+ {Command: "HLEN", Body: map[string]interface{}{"key": "key_hDel2"}},
+ {Command: "HDEL", Body: map[string]interface{}{"key": "key_hDel2", "field": "field1"}},
+ {Command: "HLEN", Body: map[string]interface{}{"key": "key_hDel2"}},
+ },
+ expected: []interface{}{float64(1), float64(1), float64(1), float64(0)},
+ delays: []time.Duration{0, 0, 0, 0},
+ },
+ {
+ name: "HDEL with multiple fields",
+ commands: []HTTPCommand{
+ {Command: "HSET", Body: map[string]interface{}{"key": "key_hDel3", "key_values": map[string]interface{}{"field1": "value1", "field2": "value2", "field3": "value3", "field4": "value4"}}},
+ {Command: "HLEN", Body: map[string]interface{}{"key": "key_hDel3"}},
+ {Command: "HDEL", Body: map[string]interface{}{"key": "key_hDel3", "values": []string{"field1", "field2"}}},
+ {Command: "HLEN", Body: map[string]interface{}{"key": "key_hDel3"}},
+ },
+ expected: []interface{}{float64(4), float64(4), float64(2), float64(2)},
+ delays: []time.Duration{0, 0, 0, 0},
+ },
+ {
+ name: "HDEL on non-existent field",
+ commands: []HTTPCommand{
+ {Command: "HSET", Body: map[string]interface{}{"key": "key_hDel4", "key_values": map[string]interface{}{"field1": "value1", "field2": "value2"}}},
+ {Command: "HDEL", Body: map[string]interface{}{"key": "key_hDel4", "field": "field3"}},
+ },
+ expected: []interface{}{float64(2), float64(0)},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HDEL on non-existent hash",
+ commands: []HTTPCommand{
+ {Command: "HSET", Body: map[string]interface{}{"key": "key_hDel5", "key_values": map[string]interface{}{"field1": "value1", "field2": "value2"}}},
+ {Command: "HDEL", Body: map[string]interface{}{"key": "wrong_key_hDel5", "field": "field1"}},
+ },
+ expected: []interface{}{float64(2), float64(0)},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HDEL with wrong type",
+ commands: []HTTPCommand{
+ {Command: "SET", Body: map[string]interface{}{"key": "string_key", "value": "value"}},
+ {Command: "HDEL", Body: map[string]interface{}{"key": "string_key", "field": "field"}},
+ },
+ expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ delays: []time.Duration{0, 0},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ defer exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"keys": [...]string{"key_hDel1", "key_hDel2", "key_hDel3", "key_hDel4", "key_hDel5", "string_key"}}})
+
+ for i, cmd := range tc.commands {
+ if tc.delays[i] > 0 {
+ time.Sleep(tc.delays[i])
+ }
+ result, err := exec.FireCommand(cmd)
+ if err != nil {
+ log.Println(tc.expected[i])
+ assert.Equal(t, tc.expected[i], err.Error(), "Error message mismatch for cmd %s", cmd)
+ } else {
+ assert.Equal(t, tc.expected[i], result, "Value mismatch for cmd %s, expected %v, got %v", cmd, tc.expected[i], result)
+ }
+ }
+ })
+ }
+}
diff --git a/integration_tests/commands/http/hget_test.go b/integration_tests/commands/http/hget_test.go
new file mode 100644
index 000000000..364fdc0d2
--- /dev/null
+++ b/integration_tests/commands/http/hget_test.go
@@ -0,0 +1,90 @@
+package http
+
+import (
+ "log"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestHGet(t *testing.T) {
+ exec := NewHTTPCommandExecutor()
+
+ testCases := []struct {
+ name string
+ commands []HTTPCommand
+ expected []interface{}
+ delays []time.Duration
+ }{
+ {
+ name: "HGET with wrong number of arguments",
+ commands: []HTTPCommand{
+ {Command: "HGET", Body: map[string]interface{}{"key": nil}},
+ {Command: "HGET", Body: map[string]interface{}{"key": "key_hGet1"}},
+ },
+ expected: []interface{}{
+ "ERR wrong number of arguments for 'hget' command",
+ "ERR wrong number of arguments for 'hget' command"},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HGET on existent hash",
+ commands: []HTTPCommand{
+ {Command: "HSET", Body: map[string]interface{}{"key": "key_hGet2", "key_values": map[string]interface{}{"field1": "value1", "field2": "value2", "field3": "value3"}}},
+ {Command: "HGET", Body: map[string]interface{}{"key": "key_hGet2", "field": "field2"}},
+ },
+ expected: []interface{}{float64(3), "value2"},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HGET on non-existent field",
+ commands: []HTTPCommand{
+ {Command: "HSET", Body: map[string]interface{}{"key": "key_hGet3", "key_values": map[string]interface{}{"field1": "value1", "field2": "value2"}}},
+ {Command: "HGET", Body: map[string]interface{}{"key": "key_hGet3", "field": "field2"}},
+ {Command: "HDEL", Body: map[string]interface{}{"key": "key_hGet3", "field": "field2"}},
+ {Command: "HGET", Body: map[string]interface{}{"key": "key_hGet3", "field": "field2"}},
+ {Command: "HGET", Body: map[string]interface{}{"key": "key_hGet3", "field": "field3"}},
+ },
+ expected: []interface{}{float64(2), "value2", float64(1), nil, nil},
+ delays: []time.Duration{0, 0, 0, 0, 0},
+ },
+ {
+ name: "HGET on non-existent hash",
+ commands: []HTTPCommand{
+ {Command: "HSET", Body: map[string]interface{}{"key": "key_hGet4", "key_values": map[string]interface{}{"field1": "value1", "field2": "value2"}}},
+ {Command: "HGET", Body: map[string]interface{}{"key": "wrong_key_hGet4", "field": "field2"}},
+ },
+ expected: []interface{}{float64(2), nil},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HGET with wrong type",
+ commands: []HTTPCommand{
+ {Command: "SET", Body: map[string]interface{}{"key": "string_key", "value": "value"}},
+ {Command: "HGET", Body: map[string]interface{}{"key": "string_key", "field": "field"}},
+ },
+ expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ delays: []time.Duration{0, 0},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ defer exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"keys": [...]string{"key_hGet1", "key_hGet2", "key_hGet3", "key_hGet4", "key_hGet5", "string_key"}}})
+
+ for i, cmd := range tc.commands {
+ if tc.delays[i] > 0 {
+ time.Sleep(tc.delays[i])
+ }
+ result, err := exec.FireCommand(cmd)
+ if err != nil {
+ log.Println(tc.expected[i])
+ assert.Equal(t, tc.expected[i], err.Error(), "Error message mismatch for cmd %s", cmd)
+ } else {
+ assert.Equal(t, tc.expected[i], result, "Value mismatch for cmd %s, expected %v, got %v", cmd, tc.expected[i], result)
+ }
+ }
+ })
+ }
+}
diff --git a/integration_tests/commands/http/hset_test.go b/integration_tests/commands/http/hset_test.go
new file mode 100644
index 000000000..e17bf6574
--- /dev/null
+++ b/integration_tests/commands/http/hset_test.go
@@ -0,0 +1,90 @@
+package http
+
+import (
+ "log"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestHSet(t *testing.T) {
+ exec := NewHTTPCommandExecutor()
+
+ testCases := []struct {
+ name string
+ commands []HTTPCommand
+ expected []interface{}
+ delays []time.Duration
+ }{
+ {
+ name: "HSET with wrong number of arguments",
+ commands: []HTTPCommand{
+ {Command: "HSET", Body: map[string]interface{}{"key": nil}},
+ {Command: "HSET", Body: map[string]interface{}{"key": "key_hSet1", "field": nil}},
+ },
+ expected: []interface{}{
+ "ERR wrong number of arguments for 'hset' command",
+ "ERR wrong number of arguments for 'hset' command"},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HSET with single field",
+ commands: []HTTPCommand{
+ {Command: "HSET", Body: map[string]interface{}{"key": "key_hSet2", "field": "field1", "value": "value1"}},
+ {Command: "HLEN", Body: map[string]interface{}{"key": "key_hSet2"}},
+ },
+ expected: []interface{}{float64(1), float64(1)},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HSET with multiple fields",
+ commands: []HTTPCommand{
+ {Command: "HSET", Body: map[string]interface{}{"key": "key_hSet3", "key_values": map[string]interface{}{"field1": "value1", "field2": "value2", "field3": "value3"}}},
+ {Command: "HLEN", Body: map[string]interface{}{"key": "key_hSet3"}},
+ },
+ expected: []interface{}{float64(3), float64(3)},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HSET on existing hash",
+ commands: []HTTPCommand{
+ {Command: "HSET", Body: map[string]interface{}{"key": "key_hSet4", "key_values": map[string]interface{}{"field1": "value1", "field2": "value2"}}},
+ {Command: "HGET", Body: map[string]interface{}{"key": "key_hSet4", "field": "field2"}},
+ {Command: "HSET", Body: map[string]interface{}{"key": "key_hSet4", "key_values": map[string]interface{}{"field2": "newvalue2"}}},
+ {Command: "HGET", Body: map[string]interface{}{"key": "key_hSet4", "field": "field2"}},
+ },
+ expected: []interface{}{float64(2), "value2", float64(0), "newvalue2"},
+ delays: []time.Duration{0, 0, 0, 0},
+ },
+ {
+ name: "HSET with wrong type",
+ commands: []HTTPCommand{
+ {Command: "SET", Body: map[string]interface{}{"key": "string_key", "value": "value"}},
+ {Command: "HSET", Body: map[string]interface{}{"key": "string_key", "field": "field", "value": "value"}},
+ },
+ expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ delays: []time.Duration{0, 0},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ defer exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"keys": [...]string{"key_hSet1", "key_hSet2", "key_hSet3", "key_hSet4", "string_key"}}})
+
+ for i, cmd := range tc.commands {
+ if tc.delays[i] > 0 {
+ time.Sleep(tc.delays[i])
+ }
+
+ result, err := exec.FireCommand(cmd)
+ if err != nil {
+ log.Println(tc.expected[i])
+ assert.Equal(t, tc.expected[i], err.Error(), "Error message mismatch for cmd %s", cmd)
+ } else {
+ assert.Equal(t, tc.expected[i], result, "Value mismatch for cmd %s, expected %v, got %v", cmd, tc.expected[i], result)
+ }
+ }
+ })
+ }
+}
diff --git a/integration_tests/commands/http/hvals_test.go b/integration_tests/commands/http/hvals_test.go
index 01195fcd1..7b0c3b2e1 100644
--- a/integration_tests/commands/http/hvals_test.go
+++ b/integration_tests/commands/http/hvals_test.go
@@ -1,7 +1,6 @@
package http
import (
- "fmt"
"testing"
"github.com/stretchr/testify/assert"
@@ -42,7 +41,6 @@ func TestHVals(t *testing.T) {
for i, cmd := range tc.commands {
result, _ := cmdExec.FireCommand(cmd)
- fmt.Printf("%v | %v\n", result, tc.expected[i])
switch e := tc.expected[i].(type) {
case []interface{}:
assert.ElementsMatch(t, e, tc.expected[i])
diff --git a/integration_tests/commands/http/json_test.go b/integration_tests/commands/http/json_test.go
index 9a8e8f4ae..e4a8ae5a8 100644
--- a/integration_tests/commands/http/json_test.go
+++ b/integration_tests/commands/http/json_test.go
@@ -727,7 +727,6 @@ func TestJSONMGET(t *testing.T) {
},
})
- fmt.Printf("expacting: %s with got: %s\n", "OK", resp)
assert.Equal(t, "OK", resp)
}
@@ -1433,8 +1432,8 @@ func TestJsonObjKeys(t *testing.T) {
if _, ok := result.([]interface{}); ok {
assert.ElementsMatch(t, tc.expected[i].([]interface{}), result.([]interface{}))
} else {
- // handle the case where result is not a []interface{}
- assert.Equal(t, tc.expected[i], result)
+ // handle the case where result is not a []interface{}
+ assert.Equal(t, tc.expected[i], result)
}
}
diff --git a/integration_tests/commands/http/set_test.go b/integration_tests/commands/http/set_test.go
index f4b3f3781..84c10f8d8 100644
--- a/integration_tests/commands/http/set_test.go
+++ b/integration_tests/commands/http/set_test.go
@@ -188,6 +188,29 @@ func TestSetWithOptions(t *testing.T) {
},
expected: []interface{}{nil, nil, "OK", nil, nil, nil},
},
+ {
+ name: "GET with Existing Value",
+ commands: []HTTPCommand{
+ {Command: "SET", Body: map[string]interface{}{"key": "k", "value": "v"}},
+ {Command: "SET", Body: map[string]interface{}{"key": "k", "value": "vv", "get": true}},
+ },
+ expected: []interface{}{"OK", "v"},
+ },
+ {
+ name: "GET with Non-Existing Value",
+ commands: []HTTPCommand{
+ {Command: "SET", Body: map[string]interface{}{"key": "k", "value": "vv", "get": true}},
+ },
+ expected: []interface{}{nil},
+ },
+ {
+ name: "GET with wrong type of value",
+ commands: []HTTPCommand{
+ {Command: "SADD", Body: map[string]interface{}{"key": "k", "value": "b"}},
+ {Command: "SET", Body: map[string]interface{}{"key": "k", "value": "v", "get": true}},
+ },
+ expected: []interface{}{float64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ },
}
for _, tc := range testCases {
@@ -207,7 +230,7 @@ func TestWithKeepTTLFlag(t *testing.T) {
exec := NewHTTPCommandExecutor()
expiryTime := strconv.FormatInt(time.Now().Add(1*time.Minute).UnixMilli(), 10)
- testCases := []TestCase {
+ testCases := []TestCase{
{
name: "SET WITH KEEP TTL",
commands: []HTTPCommand{
@@ -228,7 +251,7 @@ func TestWithKeepTTLFlag(t *testing.T) {
},
{
name: "SET WITH KEEPTTL with PX",
- commands: []HTTPCommand {
+ commands: []HTTPCommand{
{Command: "SET", Body: map[string]interface{}{"key": "k", "value": "v", "px": 2000, "keepttl": true}},
{Command: "GET", Body: map[string]interface{}{"key": "k"}},
},
@@ -236,7 +259,7 @@ func TestWithKeepTTLFlag(t *testing.T) {
},
{
name: "SET WITH KEEPTTL with EX",
- commands: []HTTPCommand {
+ commands: []HTTPCommand{
{Command: "SET", Body: map[string]interface{}{"key": "k", "value": "v", "ex": 3, "keepttl": true}},
{Command: "GET", Body: map[string]interface{}{"key": "k"}},
},
@@ -244,7 +267,7 @@ func TestWithKeepTTLFlag(t *testing.T) {
},
{
name: "SET WITH KEEPTTL with NX",
- commands: []HTTPCommand {
+ commands: []HTTPCommand{
{Command: "SET", Body: map[string]interface{}{"key": "k", "value": "v", "nx": true, "keepttl": true}},
{Command: "GET", Body: map[string]interface{}{"key": "k"}},
},
@@ -252,7 +275,7 @@ func TestWithKeepTTLFlag(t *testing.T) {
},
{
name: "SET WITH KEEPTTL with XX",
- commands: []HTTPCommand {
+ commands: []HTTPCommand{
{Command: "SET", Body: map[string]interface{}{"key": "k", "value": "v", "xx": true, "keepttl": true}},
{Command: "GET", Body: map[string]interface{}{"key": "k"}},
},
@@ -260,7 +283,7 @@ func TestWithKeepTTLFlag(t *testing.T) {
},
{
name: "SET WITH KEEPTTL with PXAT",
- commands: []HTTPCommand {
+ commands: []HTTPCommand{
{Command: "SET", Body: map[string]interface{}{"key": "k", "value": "v", "pxat": expiryTime, "keepttl": true}},
{Command: "GET", Body: map[string]interface{}{"key": "k"}},
},
@@ -269,7 +292,7 @@ func TestWithKeepTTLFlag(t *testing.T) {
{
name: "SET WITH KEEPTTL with EXAT",
- commands: []HTTPCommand {
+ commands: []HTTPCommand{
{Command: "SET", Body: map[string]interface{}{"key": "k", "value": "v", "exat": expiryTime, "keepttl": true}},
{Command: "GET", Body: map[string]interface{}{"key": "k"}},
},
diff --git a/integration_tests/commands/http/setup.go b/integration_tests/commands/http/setup.go
index a38d2b075..62bd81b5d 100644
--- a/integration_tests/commands/http/setup.go
+++ b/integration_tests/commands/http/setup.go
@@ -108,7 +108,7 @@ func RunHTTPServer(ctx context.Context, wg *sync.WaitGroup, opt TestServerOption
queryWatcherLocal := querymanager.NewQueryManager()
config.HTTPPort = opt.Port
// Initialize the HTTPServer
- testServer := server.NewHTTPServer(shardManager)
+ testServer := server.NewHTTPServer(shardManager, nil)
// Inform the user that the server is starting
fmt.Println("Starting the test server on port", config.HTTPPort)
shardManagerCtx, cancelShardManager := context.WithCancel(ctx)
diff --git a/integration_tests/commands/http/zset_test.go b/integration_tests/commands/http/zset_test.go
index 0c442aae3..ed5a1f293 100644
--- a/integration_tests/commands/http/zset_test.go
+++ b/integration_tests/commands/http/zset_test.go
@@ -29,6 +29,7 @@ func TestZPOPMIN(t *testing.T) {
commands: []HTTPCommand{
{Command: "ZADD", Body: map[string]interface{}{"key": "myzset", "values": [...]string{"1", "member1", "2", "member2", "3", "member3"}}},
{Command: "ZPOPMIN", Body: map[string]interface{}{"key": "myzset"}},
+ {Command: "ZCOUNT", Body: map[string]interface{}{"key": "myzset", "values": [...]string{"1", "2.98"}}},
},
expected: []interface{}{float64(3), []interface{}{"member1", "1"}, float64(1)},
},
@@ -37,14 +38,16 @@ func TestZPOPMIN(t *testing.T) {
commands: []HTTPCommand{
{Command: "ZADD", Body: map[string]interface{}{"key": "myzset", "values": [...]string{"1", "member1", "2", "member2", "3", "member3"}}},
{Command: "ZPOPMIN", Body: map[string]interface{}{"key": "myzset", "value": int64(2)}},
+ {Command: "ZCOUNT", Body: map[string]interface{}{"key": "myzset", "values": [...]string{"0.44", "2"}}},
},
- expected: []interface{}{float64(3), []interface{}{"member1", "1", "member2", "2"}, float64(1)},
+ expected: []interface{}{float64(3), []interface{}{"member1", "1", "member2", "2"}, float64(0)},
},
{
name: "ZPOPMIN with count argument but multiple members have the same score",
commands: []HTTPCommand{
{Command: "ZADD", Body: map[string]interface{}{"key": "myzset", "values": [...]string{"1", "member1", "1", "member2", "1", "member3"}}},
{Command: "ZPOPMIN", Body: map[string]interface{}{"key": "myzset", "value": int64(2)}},
+ {Command: "ZCOUNT", Body: map[string]interface{}{"key": "myzset", "values": [...]string{"1", "2"}}},
},
expected: []interface{}{float64(3), []interface{}{"member1", "1", "member2", "1"}, float64(1)},
},
@@ -53,14 +56,16 @@ func TestZPOPMIN(t *testing.T) {
commands: []HTTPCommand{
{Command: "ZADD", Body: map[string]interface{}{"key": "myzset", "values": [...]string{"1", "member1", "2", "member2", "3", "member3"}}},
{Command: "ZPOPMIN", Body: map[string]interface{}{"key": "myzset", "value": int64(-1)}},
+ {Command: "ZCOUNT", Body: map[string]interface{}{"key": "myzset", "values": [...]string{"1", "1000"}}},
},
- expected: []interface{}{float64(3), []interface{}{}, float64(1)},
+ expected: []interface{}{float64(3), []interface{}{}, float64(3)},
},
{
name: "ZPOPMIN with invalid count argument",
commands: []HTTPCommand{
{Command: "ZADD", Body: map[string]interface{}{"key": "myzset", "values": [...]string{"1", "member1"}}},
{Command: "ZPOPMIN", Body: map[string]interface{}{"key": "myzset", "value": "INCORRECT_COUNT_ARGUMENT"}},
+ {Command: "ZCOUNT", Body: map[string]interface{}{"key": "myzset", "values": [...]string{"1", "2"}}},
},
expected: []interface{}{float64(1), "ERR value is not an integer or out of range", float64(1)},
},
@@ -69,8 +74,9 @@ func TestZPOPMIN(t *testing.T) {
commands: []HTTPCommand{
{Command: "ZADD", Body: map[string]interface{}{"key": "myzset", "values": [...]string{"1", "member1", "2", "member2", "3", "member3"}}},
{Command: "ZPOPMIN", Body: map[string]interface{}{"key": "myzset", "value": int64(10)}},
+ {Command: "ZCOUNT", Body: map[string]interface{}{"key": "myzset", "values": [...]string{"1", "2"}}},
},
- expected: []interface{}{float64(3), []interface{}{"member1", "1", "member2", "2", "member3", "3"}, float64(1)},
+ expected: []interface{}{float64(3), []interface{}{"member1", "1", "member2", "2", "member3", "3"}, float64(0)},
},
{
name: "ZPOPMIN on empty sorted set",
@@ -86,6 +92,7 @@ func TestZPOPMIN(t *testing.T) {
commands: []HTTPCommand{
{Command: "ZADD", Body: map[string]interface{}{"key": "myzset", "values": [...]string{"1.5", "member1", "2.7", "member2", "3.8", "member3"}}},
{Command: "ZPOPMIN", Body: map[string]interface{}{"key": "myzset"}},
+ {Command: "ZCOUNT", Body: map[string]interface{}{"key": "myzset", "values": [...]string{"1.3", "3.6"}}},
},
expected: []interface{}{float64(3), []interface{}{"member1", "1.5"}, float64(1)},
},
@@ -93,15 +100,14 @@ func TestZPOPMIN(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- exec.FireCommand(HTTPCommand{
- Command: "DEL",
- Body: map[string]interface{}{"key": "myzset"},
- })
for i, cmd := range tc.commands {
result, _ := exec.FireCommand(cmd)
-
assert.Equal(t, tc.expected[i], result)
}
+ exec.FireCommand(HTTPCommand{
+ Command: "DEL",
+ Body: map[string]interface{}{"key": "myzset"},
+ })
})
}
}
diff --git a/integration_tests/commands/resp/append_test.go b/integration_tests/commands/resp/append_test.go
index fc70851cf..b1b024b96 100644
--- a/integration_tests/commands/resp/append_test.go
+++ b/integration_tests/commands/resp/append_test.go
@@ -8,6 +8,7 @@ import (
func TestAPPEND(t *testing.T) {
conn := getLocalConnection()
+ FireCommand(conn, "FLUSHDB")
defer conn.Close()
testCases := []struct {
diff --git a/integration_tests/commands/resp/bit_test.go b/integration_tests/commands/resp/bit_test.go
new file mode 100644
index 000000000..f6033914a
--- /dev/null
+++ b/integration_tests/commands/resp/bit_test.go
@@ -0,0 +1,1079 @@
+package resp
+
+// The following commands are a part of this test class:
+// SETBIT, GETBIT, BITCOUNT, BITOP, BITPOS, BITFIELD, BITFIELD_RO
+
+import (
+ "fmt"
+ "math"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+// TODO: BITOP has not been migrated yet. Once done, we can uncomment the tests - please check accuracy and validate for expected values.
+
+// func TestBitOp(t *testing.T) {
+// conn := getLocalConnection()
+// defer conn.Close()
+// testcases := []struct {
+// InCmds []string
+// Out []interface{}
+// }{
+// {
+// InCmds: []string{"SETBIT unitTestKeyA 1 1", "SETBIT unitTestKeyA 3 1", "SETBIT unitTestKeyA 5 1", "SETBIT unitTestKeyA 7 1", "SETBIT unitTestKeyA 8 1"},
+// Out: []interface{}{int64(0), int64(0), int64(0), int64(0), int64(0)},
+// },
+// {
+// InCmds: []string{"SETBIT unitTestKeyB 2 1", "SETBIT unitTestKeyB 4 1", "SETBIT unitTestKeyB 7 1"},
+// Out: []interface{}{int64(0), int64(0), int64(0)},
+// },
+// {
+// InCmds: []string{"SET foo bar", "SETBIT foo 2 1", "SETBIT foo 4 1", "SETBIT foo 7 1", "GET foo"},
+// Out: []interface{}{"OK", int64(1), int64(0), int64(0), "kar"},
+// },
+// {
+// InCmds: []string{"SET mykey12 1343", "SETBIT mykey12 2 1", "SETBIT mykey12 4 1", "SETBIT mykey12 7 1", "GET mykey12"},
+// Out: []interface{}{"OK", int64(1), int64(0), int64(1), int64(9343)},
+// },
+// {
+// InCmds: []string{"SET foo12 bar", "SETBIT foo12 2 1", "SETBIT foo12 4 1", "SETBIT foo12 7 1", "GET foo12"},
+// Out: []interface{}{"OK", int64(1), int64(0), int64(0), "kar"},
+// },
+// {
+// InCmds: []string{"BITOP NOT unitTestKeyNOT unitTestKeyA "},
+// Out: []interface{}{int64(2)},
+// },
+// {
+// InCmds: []string{"GETBIT unitTestKeyNOT 1", "GETBIT unitTestKeyNOT 2", "GETBIT unitTestKeyNOT 7", "GETBIT unitTestKeyNOT 8", "GETBIT unitTestKeyNOT 9"},
+// Out: []interface{}{int64(0), int64(1), int64(0), int64(0), int64(1)},
+// },
+// {
+// InCmds: []string{"BITOP OR unitTestKeyOR unitTestKeyB unitTestKeyA"},
+// Out: []interface{}{int64(2)},
+// },
+// {
+// InCmds: []string{"GETBIT unitTestKeyOR 1", "GETBIT unitTestKeyOR 2", "GETBIT unitTestKeyOR 3", "GETBIT unitTestKeyOR 7", "GETBIT unitTestKeyOR 8", "GETBIT unitTestKeyOR 9", "GETBIT unitTestKeyOR 12"},
+// Out: []interface{}{int64(1), int64(1), int64(1), int64(1), int64(1), int64(0), int64(0)},
+// },
+// {
+// InCmds: []string{"BITOP AND unitTestKeyAND unitTestKeyB unitTestKeyA"},
+// Out: []interface{}{int64(2)},
+// },
+// {
+// InCmds: []string{"GETBIT unitTestKeyAND 1", "GETBIT unitTestKeyAND 2", "GETBIT unitTestKeyAND 7", "GETBIT unitTestKeyAND 8", "GETBIT unitTestKeyAND 9"},
+// Out: []interface{}{int64(0), int64(0), int64(1), int64(0), int64(0)},
+// },
+// {
+// InCmds: []string{"BITOP XOR unitTestKeyXOR unitTestKeyB unitTestKeyA"},
+// Out: []interface{}{int64(2)},
+// },
+// {
+// InCmds: []string{"GETBIT unitTestKeyXOR 1", "GETBIT unitTestKeyXOR 2", "GETBIT unitTestKeyXOR 3", "GETBIT unitTestKeyXOR 7", "GETBIT unitTestKeyXOR 8"},
+// Out: []interface{}{int64(1), int64(1), int64(1), int64(0), int64(1)},
+// },
+// }
+
+// for _, tcase := range testcases {
+// for i := 0; i < len(tcase.InCmds); i++ {
+// cmd := tcase.InCmds[i]
+// out := tcase.Out[i]
+// assert.Equal(t, out, FireCommand(conn, cmd), "Value mismatch for cmd %s\n.", cmd)
+// }
+// }
+// }
+
+// func TestBitOpsString(t *testing.T) {
+
+// conn := getLocalConnection()
+// defer conn.Close()
+// // foobar in bits is 01100110 01101111 01101111 01100010 01100001 01110010
+// fooBarBits := "011001100110111101101111011000100110000101110010"
+// // randomly get 8 bits for testing
+// testOffsets := make([]int, 8)
+
+// for i := 0; i < 8; i++ {
+// testOffsets[i] = rand.Intn(len(fooBarBits))
+// }
+
+// getBitTestCommands := make([]string, 8+1)
+// getBitTestExpected := make([]interface{}, 8+1)
+
+// getBitTestCommands[0] = "SET foo foobar"
+// getBitTestExpected[0] = "OK"
+
+// for i := 1; i < 8+1; i++ {
+// getBitTestCommands[i] = fmt.Sprintf("GETBIT foo %d", testOffsets[i-1])
+// getBitTestExpected[i] = int64(fooBarBits[testOffsets[i-1]] - '0')
+// }
+
+// testCases := []struct {
+// name string
+// cmds []string
+// expected []interface{}
+// assertType []string
+// }{
+// {
+// name: "Getbit of a key containing a string",
+// cmds: getBitTestCommands,
+// expected: getBitTestExpected,
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Getbit of a key containing an integer",
+// cmds: []string{"SET foo 10", "GETBIT foo 0", "GETBIT foo 1", "GETBIT foo 2", "GETBIT foo 3", "GETBIT foo 4", "GETBIT foo 5", "GETBIT foo 6", "GETBIT foo 7"},
+// expected: []interface{}{"OK", int64(0), int64(0), int64(1), int64(1), int64(0), int64(0), int64(0), int64(1)},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// }, {
+// name: "Getbit of a key containing an integer 2nd byte",
+// cmds: []string{"SET foo 10", "GETBIT foo 8", "GETBIT foo 9", "GETBIT foo 10", "GETBIT foo 11", "GETBIT foo 12", "GETBIT foo 13", "GETBIT foo 14", "GETBIT foo 15"},
+// expected: []interface{}{"OK", int64(0), int64(0), int64(1), int64(1), int64(0), int64(0), int64(0), int64(0)},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Getbit of a key with an offset greater than the length of the string in bits",
+// cmds: []string{"SET foo foobar", "GETBIT foo 100", "GETBIT foo 48", "GETBIT foo 47"},
+// expected: []interface{}{"OK", int64(0), int64(0), int64(0)},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Bitcount of a key containing a string",
+// cmds: []string{"SET foo foobar", "BITCOUNT foo 0 -1", "BITCOUNT foo", "BITCOUNT foo 0 0", "BITCOUNT foo 1 1", "BITCOUNT foo 1 1 Byte", "BITCOUNT foo 5 30 BIT"},
+// expected: []interface{}{"OK", int64(26), int64(26), int64(4), int64(6), int64(6), int64(17)},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Bitcount of a key containing an integer",
+// cmds: []string{"SET foo 10", "BITCOUNT foo 0 -1", "BITCOUNT foo", "BITCOUNT foo 0 0", "BITCOUNT foo 1 1", "BITCOUNT foo 1 1 Byte", "BITCOUNT foo 5 30 BIT"},
+// expected: []interface{}{"OK", int64(5), int64(5), int64(3), int64(2), int64(2), int64(3)},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Setbit of a key containing a string",
+// cmds: []string{"SET foo foobar", "setbit foo 7 1", "get foo", "setbit foo 49 1", "setbit foo 50 1", "get foo", "setbit foo 49 0", "get foo"},
+// expected: []interface{}{"OK", int64(0), "goobar", int64(0), int64(0), "goobar`", int64(1), "goobar "},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Setbit of a key must not change the expiry of the key if expiry is set",
+// cmds: []string{"SET foo foobar", "EXPIRE foo 100", "TTL foo", "SETBIT foo 7 1", "TTL foo"},
+// expected: []interface{}{"OK", int64(1), int64(100), int64(0), int64(100)},
+// assertType: []string{"equal", "equal", "less", "equal", "less"},
+// },
+// {
+// name: "Setbit of a key must not add expiry to the key if expiry is not set",
+// cmds: []string{"SET foo foobar", "TTL foo", "SETBIT foo 7 1", "TTL foo"},
+// expected: []interface{}{"OK", int64(-1), int64(0), int64(-1)},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Bitop not of a key containing a string",
+// cmds: []string{"SET foo foobar", "BITOP NOT baz foo", "GET baz", "BITOP NOT bazz baz", "GET bazz"},
+// expected: []interface{}{"OK", int64(6), "\x99\x90\x90\x9d\x9e\x8d", int64(6), "foobar"},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Bitop not of a key containing an integer",
+// cmds: []string{"SET foo 10", "BITOP NOT baz foo", "GET baz", "BITOP NOT bazz baz", "GET bazz"},
+// expected: []interface{}{"OK", int64(2), "\xce\xcf", int64(2), int64(10)},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Get a string created with setbit",
+// cmds: []string{"SETBIT foo 1 1", "SETBIT foo 3 1", "GET foo"},
+// expected: []interface{}{int64(0), int64(0), "P"},
+// assertType: []string{"equal", "equal", "equal"},
+// },
+// {
+// name: "Bitop and of keys containing a string and get the destkey",
+// cmds: []string{"SET foo foobar", "SET baz abcdef", "BITOP AND bazz foo baz", "GET bazz"},
+// expected: []interface{}{"OK", "OK", int64(6), "`bc`ab"},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP AND of keys containing integers and get the destkey",
+// cmds: []string{"SET foo 10", "SET baz 5", "BITOP AND bazz foo baz", "GET bazz"},
+// expected: []interface{}{"OK", "OK", int64(2), "1\x00"},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Bitop or of keys containing a string, a bytearray and get the destkey",
+// cmds: []string{"MSET foo foobar baz abcdef", "SETBIT bazz 8 1", "BITOP and bazzz foo baz bazz", "GET bazzz"},
+// expected: []interface{}{"OK", int64(0), int64(6), "\x00\x00\x00\x00\x00\x00"},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP OR of keys containing strings and get the destkey",
+// cmds: []string{"MSET foo foobar baz abcdef", "BITOP OR bazz foo baz", "GET bazz"},
+// expected: []interface{}{"OK", int64(6), "goofev"},
+// assertType: []string{"equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP OR of keys containing integers and get the destkey",
+// cmds: []string{"SET foo 10", "SET baz 5", "BITOP OR bazz foo baz", "GET bazz"},
+// expected: []interface{}{"OK", "OK", int64(2), "50"},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP OR of keys containing strings and a bytearray and get the destkey",
+// cmds: []string{"MSET foo foobar baz abcdef", "SETBIT bazz 8 1", "BITOP OR bazzz foo baz bazz", "GET bazzz", "SETBIT bazz 8 0", "SETBIT bazz 49 1", "BITOP OR bazzz foo baz bazz", "GET bazzz"},
+// expected: []interface{}{"OK", int64(0), int64(6), "g\xefofev", int64(1), int64(0), int64(7), "goofev@"},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP XOR of keys containing strings and get the destkey",
+// cmds: []string{"MSET foo foobar baz abcdef", "BITOP XOR bazz foo baz", "GET bazz"},
+// expected: []interface{}{"OK", int64(6), "\x07\x0d\x0c\x06\x04\x14"},
+// assertType: []string{"equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP XOR of keys containing strings and a bytearray and get the destkey",
+// cmds: []string{"MSET foo foobar baz abcdef", "SETBIT bazz 8 1", "BITOP XOR bazzz foo baz bazz", "GET bazzz", "SETBIT bazz 8 0", "SETBIT bazz 49 1", "BITOP XOR bazzz foo baz bazz", "GET bazzz", "Setbit bazz 49 0", "BITOP XOR bazzz foo baz bazz", "GET bazzz"},
+// expected: []interface{}{"OK", int64(0), int64(6), "\x07\x8d\x0c\x06\x04\x14", int64(1), int64(0), int64(7), "\x07\r\x0c\x06\x04\x14@", int64(1), int64(7), "\x07\r\x0c\x06\x04\x14\x00"},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP XOR of keys containing integers and get the destkey",
+// cmds: []string{"SET foo 10", "SET baz 5", "BITOP XOR bazz foo baz", "GET bazz"},
+// expected: []interface{}{"OK", "OK", int64(2), "\x040"},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// }
+
+// for _, tc := range testCases {
+// t.Run(tc.name, func(t *testing.T) {
+// // Delete the key before running the test
+// FireCommand(conn, "DEL foo")
+// FireCommand(conn, "DEL baz")
+// FireCommand(conn, "DEL bazz")
+// FireCommand(conn, "DEL bazzz")
+// for i := 0; i < len(tc.cmds); i++ {
+// res := FireCommand(conn, tc.cmds[i])
+
+// switch tc.assertType[i] {
+// case "equal":
+// assert.Equal(t, tc.expected[i], res)
+// case "less":
+// assert.True(t, res.(int64) <= tc.expected[i].(int64), "CMD: %s Expected %d to be less than or equal to %d", tc.cmds[i], res, tc.expected[i])
+// }
+// }
+// })
+// }
+// }
+
+func TestBitCount(t *testing.T) {
+ conn := getLocalConnection()
+ testcases := []struct {
+ InCmds []string
+ Out []interface{}
+ }{
+ {
+ InCmds: []string{"SETBIT mykey 7 1"},
+ Out: []interface{}{int64(0)},
+ },
+ {
+ InCmds: []string{"SETBIT mykey 7 1"},
+ Out: []interface{}{int64(1)},
+ },
+ {
+ InCmds: []string{"SETBIT mykey 122 1"},
+ Out: []interface{}{int64(0)},
+ },
+ {
+ InCmds: []string{"GETBIT mykey 122"},
+ Out: []interface{}{int64(1)},
+ },
+ {
+ InCmds: []string{"SETBIT mykey 122 0"},
+ Out: []interface{}{int64(1)},
+ },
+ {
+ InCmds: []string{"GETBIT mykey 122"},
+ Out: []interface{}{int64(0)},
+ },
+ {
+ InCmds: []string{"GETBIT mykey 1223232"},
+ Out: []interface{}{int64(0)},
+ },
+ {
+ InCmds: []string{"GETBIT mykey 7"},
+ Out: []interface{}{int64(1)},
+ },
+ {
+ InCmds: []string{"GETBIT mykey 8"},
+ Out: []interface{}{int64(0)},
+ },
+ {
+ InCmds: []string{"BITCOUNT mykey 3 7 BIT"},
+ Out: []interface{}{int64(1)},
+ },
+ {
+ InCmds: []string{"BITCOUNT mykey 3 7"},
+ Out: []interface{}{int64(0)},
+ },
+ {
+ InCmds: []string{"BITCOUNT mykey 0 0"},
+ Out: []interface{}{int64(1)},
+ },
+ {
+ InCmds: []string{"BITCOUNT"},
+ Out: []interface{}{"ERR wrong number of arguments for 'bitcount' command"},
+ },
+ {
+ InCmds: []string{"BITCOUNT mykey"},
+ Out: []interface{}{int64(1)},
+ },
+ {
+ InCmds: []string{"BITCOUNT mykey 0"},
+ Out: []interface{}{"ERR syntax error"},
+ },
+ }
+
+ for _, tcase := range testcases {
+ for i := 0; i < len(tcase.InCmds); i++ {
+ cmd := tcase.InCmds[i]
+ out := tcase.Out[i]
+ assert.Equal(t, out, FireCommand(conn, cmd), "Value mismatch for cmd %s\n.", cmd)
+ }
+ }
+}
+
+func TestBitPos(t *testing.T) {
+ conn := getLocalConnection()
+ testcases := []struct {
+ name string
+ val interface{}
+ inCmd string
+ out interface{}
+ setCmdSETBIT bool
+ }{
+ {
+ name: "String interval BIT 0,-1 ",
+ val: "\\x00\\xff\\x00",
+ inCmd: "BITPOS testkey 0 0 -1 bit",
+ out: int64(0),
+ },
+ {
+ name: "String interval BIT 8,-1",
+ val: "\\x00\\xff\\x00",
+ inCmd: "BITPOS testkey 0 8 -1 bit",
+ out: int64(8),
+ },
+ {
+ name: "String interval BIT 16,-1",
+ val: "\\x00\\xff\\x00",
+ inCmd: "BITPOS testkey 0 16 -1 bit",
+ out: int64(16),
+ },
+ {
+ name: "String interval BIT 16,200",
+ val: "\\x00\\xff\\x00",
+ inCmd: "BITPOS testkey 0 16 200 bit",
+ out: int64(16),
+ },
+ {
+ name: "String interval BIT 8,8",
+ val: "\\x00\\xff\\x00",
+ inCmd: "BITPOS testkey 0 8 8 bit",
+ out: int64(8),
+ },
+ {
+ name: "FindsFirstZeroBit",
+ val: "\xff\xf0\x00",
+ inCmd: "BITPOS testkey 0",
+ out: int64(12),
+ },
+ {
+ name: "FindsFirstOneBit",
+ val: "\x00\x0f\xff",
+ inCmd: "BITPOS testkey 1",
+ out: int64(12),
+ },
+ {
+ name: "NoOneBitFound",
+ val: "\x00\x00\x00",
+ inCmd: "BITPOS testkey 1",
+ out: int64(-1),
+ },
+ {
+ name: "NoZeroBitFound",
+ val: "\xff\xff\xff",
+ inCmd: "BITPOS testkey 0",
+ out: int64(24),
+ },
+ {
+ name: "NoZeroBitFoundWithRangeStartPos",
+ val: "\xff\xff\xff",
+ inCmd: "BITPOS testkey 0 2",
+ out: int64(24),
+ },
+ {
+ name: "NoZeroBitFoundWithOOBRangeStartPos",
+ val: "\xff\xff\xff",
+ inCmd: "BITPOS testkey 0 4",
+ out: int64(-1),
+ },
+ {
+ name: "NoZeroBitFoundWithRange",
+ val: "\xff\xff\xff",
+ inCmd: "BITPOS testkey 0 2 2",
+ out: int64(-1),
+ },
+ {
+ name: "NoZeroBitFoundWithRangeAndRangeType",
+ val: "\xff\xff\xff",
+ inCmd: "BITPOS testkey 0 2 2 BIT",
+ out: int64(-1),
+ },
+ {
+ name: "FindsFirstZeroBitInRange",
+ val: "\xff\xf0\xff",
+ inCmd: "BITPOS testkey 0 1 2",
+ out: int64(12),
+ },
+ {
+ name: "FindsFirstOneBitInRange",
+ val: "\x00\x00\xf0",
+ inCmd: "BITPOS testkey 1 2 3",
+ out: int64(16),
+ },
+ {
+ name: "StartGreaterThanEnd",
+ val: "\xff\xf0\x00",
+ inCmd: "BITPOS testkey 0 3 2",
+ out: int64(-1),
+ },
+ {
+ name: "FindsFirstOneBitWithNegativeStart",
+ val: "\x00\x00\xf0",
+ inCmd: "BITPOS testkey 1 -2 -1",
+ out: int64(16),
+ },
+ {
+ name: "FindsFirstZeroBitWithNegativeEnd",
+ val: "\xff\xf0\xff",
+ inCmd: "BITPOS testkey 0 1 -1",
+ out: int64(12),
+ },
+ {
+ name: "FindsFirstZeroBitInByteRange",
+ val: "\xff\x00\xff",
+ inCmd: "BITPOS testkey 0 1 2 BYTE",
+ out: int64(8),
+ },
+ {
+ name: "FindsFirstOneBitInBitRange",
+ val: "\x00\x01\x00",
+ inCmd: "BITPOS testkey 1 0 16 BIT",
+ out: int64(15),
+ },
+ {
+ name: "NoBitFoundInByteRange",
+ val: "\xff\xff\xff",
+ inCmd: "BITPOS testkey 0 0 2 BYTE",
+ out: int64(-1),
+ },
+ {
+ name: "NoBitFoundInBitRange",
+ val: "\x00\x00\x00",
+ inCmd: "BITPOS testkey 1 0 23 BIT",
+ out: int64(-1),
+ },
+ {
+ name: "EmptyStringReturnsMinusOneForZeroBit",
+ val: "\"\"",
+ inCmd: "BITPOS testkey 0",
+ out: int64(-1),
+ },
+ {
+ name: "EmptyStringReturnsMinusOneForOneBit",
+ val: "\"\"",
+ inCmd: "BITPOS testkey 1",
+ out: int64(-1),
+ },
+ {
+ name: "SingleByteString",
+ val: "\x80",
+ inCmd: "BITPOS testkey 1",
+ out: int64(0),
+ },
+ {
+ name: "RangeExceedsStringLength",
+ val: "\x00\xff",
+ inCmd: "BITPOS testkey 1 0 20 BIT",
+ out: int64(8),
+ },
+ {
+ name: "InvalidBitArgument",
+ inCmd: "BITPOS testkey 2",
+ out: "ERR the bit argument must be 1 or 0",
+ },
+ {
+ name: "NonIntegerStartParameter",
+ inCmd: "BITPOS testkey 0 start",
+ out: "ERR value is not an integer or out of range",
+ },
+ {
+ name: "NonIntegerEndParameter",
+ inCmd: "BITPOS testkey 0 1 end",
+ out: "ERR value is not an integer or out of range",
+ },
+ {
+ name: "InvalidRangeType",
+ inCmd: "BITPOS testkey 0 1 2 BYTEs",
+ out: "ERR syntax error",
+ },
+ {
+ name: "InsufficientArguments",
+ inCmd: "BITPOS testkey",
+ out: "ERR wrong number of arguments for 'bitpos' command",
+ },
+ {
+ name: "NonExistentKeyForZeroBit",
+ inCmd: "BITPOS nonexistentkey 0",
+ out: int64(0),
+ },
+ {
+ name: "NonExistentKeyForOneBit",
+ inCmd: "BITPOS nonexistentkey 1",
+ out: int64(-1),
+ },
+ {
+ name: "IntegerValue",
+ val: 65280, // 0xFF00 in decimal
+ inCmd: "BITPOS testkey 0",
+ out: int64(0),
+ },
+ {
+ name: "LargeIntegerValue",
+ val: 16777215, // 0xFFFFFF in decimal
+ inCmd: "BITPOS testkey 1",
+ out: int64(2),
+ },
+ {
+ name: "SmallIntegerValue",
+ val: 1, // 0x01 in decimal
+ inCmd: "BITPOS testkey 0",
+ out: int64(0),
+ },
+ {
+ name: "ZeroIntegerValue",
+ val: 0,
+ inCmd: "BITPOS testkey 1",
+ out: int64(2),
+ },
+ {
+ name: "BitRangeStartGreaterThanBitLength",
+ val: "\xff\xff\xff",
+ inCmd: "BITPOS testkey 0 25 30 BIT",
+ out: int64(-1),
+ },
+ {
+ name: "BitRangeEndExceedsBitLength",
+ val: "\xff\xff\xff",
+ inCmd: "BITPOS testkey 0 0 30 BIT",
+ out: int64(-1),
+ },
+ {
+ name: "NegativeStartInBitRange",
+ val: "\x00\xff\xff",
+ inCmd: "BITPOS testkey 1 -16 -1 BIT",
+ out: int64(8),
+ },
+ {
+ name: "LargeNegativeStart",
+ val: "\x00\xff\xff",
+ inCmd: "BITPOS testkey 1 -100 -1",
+ out: int64(8),
+ },
+ {
+ name: "LargePositiveEnd",
+ val: "\x00\xff\xff",
+ inCmd: "BITPOS testkey 1 0 100",
+ out: int64(8),
+ },
+ {
+ name: "StartAndEndEqualInByteRange",
+ val: "\x0f\xff\xff",
+ inCmd: "BITPOS testkey 0 1 1 BYTE",
+ out: int64(-1),
+ },
+ {
+ name: "StartAndEndEqualInBitRange",
+ val: "\x0f\xff\xff",
+ inCmd: "BITPOS testkey 1 1 1 BIT",
+ out: int64(-1),
+ },
+ {
+ name: "FindFirstZeroBitInNegativeRange",
+ val: "\xff\x00\xff",
+ inCmd: "BITPOS testkey 0 -2 -1",
+ out: int64(8),
+ },
+ {
+ name: "FindFirstOneBitInNegativeRangeBIT",
+ val: "\x00\x00\x80",
+ inCmd: "BITPOS testkey 1 -8 -1 BIT",
+ out: int64(16),
+ },
+ {
+ name: "MaxIntegerValue",
+ val: math.MaxInt64,
+ inCmd: "BITPOS testkey 0",
+ out: int64(0),
+ },
+ {
+ name: "MinIntegerValue",
+ val: math.MinInt64,
+ inCmd: "BITPOS testkey 1",
+ out: int64(2),
+ },
+ {
+ name: "SingleBitStringZero",
+ val: "\x00",
+ inCmd: "BITPOS testkey 1",
+ out: int64(-1),
+ },
+ {
+ name: "SingleBitStringOne",
+ val: "\x01",
+ inCmd: "BITPOS testkey 0",
+ out: int64(0),
+ },
+ {
+ name: "AllBitsSetExceptLast",
+ val: "\xff\xff\xfe",
+ inCmd: "BITPOS testkey 0",
+ out: int64(23),
+ },
+ {
+ name: "OnlyLastBitSet",
+ val: "\x00\x00\x01",
+ inCmd: "BITPOS testkey 1",
+ out: int64(23),
+ },
+ {
+ name: "AlternatingBitsLongString",
+ val: "\xaa\xaa\xaa\xaa\xaa",
+ inCmd: "BITPOS testkey 0",
+ out: int64(1),
+ },
+ {
+ name: "VeryLargeByteString",
+ val: strings.Repeat("\xff", 1000) + "\x00",
+ inCmd: "BITPOS testkey 0",
+ out: int64(8000),
+ },
+ {
+ name: "FindZeroBitOnSetBitKey",
+ val: "8 1",
+ inCmd: "BITPOS testkeysb 1",
+ out: int64(8),
+ setCmdSETBIT: true,
+ },
+ {
+ name: "FindOneBitOnSetBitKey",
+ val: "1 1",
+ inCmd: "BITPOS testkeysb 1",
+ out: int64(1),
+ setCmdSETBIT: true,
+ },
+ }
+
+ for _, tc := range testcases {
+ t.Run(tc.name, func(t *testing.T) {
+ var setCmd string
+ if tc.setCmdSETBIT {
+ setCmd = fmt.Sprintf("SETBIT testkeysb %s", tc.val.(string))
+ } else {
+ switch v := tc.val.(type) {
+ case string:
+ setCmd = fmt.Sprintf("SET testkey %s", v)
+ case int:
+ setCmd = fmt.Sprintf("SET testkey %d", v)
+ default:
+ // For test cases where we don't set a value (e.g., error cases)
+ setCmd = ""
+ }
+ }
+
+ if setCmd != "" {
+ FireCommand(conn, setCmd)
+ }
+
+ result := FireCommand(conn, tc.inCmd)
+ assert.Equal(t, tc.out, result, "Mismatch for cmd %s\n", tc.inCmd)
+ })
+ }
+}
+
+func TestBitfield(t *testing.T) {
+ conn := getLocalConnection()
+ defer conn.Close()
+
+ FireCommand(conn, "FLUSHDB")
+ defer FireCommand(conn, "FLUSHDB") // clean up after all test cases
+ syntaxErrMsg := "ERR syntax error"
+ bitFieldTypeErrMsg := "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is"
+ integerErrMsg := "ERR value is not an integer or out of range"
+ overflowErrMsg := "ERR Invalid OVERFLOW type specified"
+
+ testCases := []struct {
+ Name string
+ Commands []string
+ Expected []interface{}
+ Delay []time.Duration
+ CleanUp []string
+ }{
+ {
+ Name: "BITFIELD Arity Check",
+ Commands: []string{"bitfield"},
+ Expected: []interface{}{"ERR wrong number of arguments for 'bitfield' command"},
+ Delay: []time.Duration{0},
+ CleanUp: []string{},
+ },
+ {
+ Name: "BITFIELD on unsupported type of SET",
+ Commands: []string{"SADD bits a b c", "bitfield bits"},
+ Expected: []interface{}{int64(3), "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD on unsupported type of JSON",
+ Commands: []string{"json.set bits $ 1", "bitfield bits"},
+ Expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD on unsupported type of HSET",
+ Commands: []string{"HSET bits a 1", "bitfield bits"},
+ Expected: []interface{}{int64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD with syntax errors",
+ Commands: []string{
+ "bitfield bits set u8 0 255 incrby u8 0 100 get u8",
+ "bitfield bits set a8 0 255 incrby u8 0 100 get u8",
+ "bitfield bits set u8 a 255 incrby u8 0 100 get u8",
+ "bitfield bits set u8 0 255 incrby u8 0 100 overflow wraap",
+ "bitfield bits set u8 0 incrby u8 0 100 get u8 288",
+ },
+ Expected: []interface{}{
+ syntaxErrMsg,
+ bitFieldTypeErrMsg,
+ "ERR bit offset is not an integer or out of range",
+ overflowErrMsg,
+ integerErrMsg,
+ },
+ Delay: []time.Duration{0, 0, 0, 0, 0},
+ CleanUp: []string{"Del bits"},
+ },
+ {
+ Name: "BITFIELD signed SET and GET basics",
+ Commands: []string{"bitfield bits set i8 0 -100", "bitfield bits set i8 0 101", "bitfield bits get i8 0"},
+ Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(-100)}, []interface{}{int64(101)}},
+ Delay: []time.Duration{0, 0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD unsigned SET and GET basics",
+ Commands: []string{"bitfield bits set u8 0 255", "bitfield bits set u8 0 100", "bitfield bits get u8 0"},
+ Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(255)}, []interface{}{int64(100)}},
+ Delay: []time.Duration{0, 0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD signed SET and GET together",
+ Commands: []string{"bitfield bits set i8 0 255 set i8 0 100 get i8 0"},
+ Expected: []interface{}{[]interface{}{int64(0), int64(-1), int64(100)}},
+ Delay: []time.Duration{0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD unsigned with SET, GET and INCRBY arguments",
+ Commands: []string{"bitfield bits set u8 0 255 incrby u8 0 100 get u8 0"},
+ Expected: []interface{}{[]interface{}{int64(0), int64(99), int64(99)}},
+ Delay: []time.Duration{0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD with only key as argument",
+ Commands: []string{"bitfield bits"},
+ Expected: []interface{}{[]interface{}{}},
+ Delay: []time.Duration{0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD # form",
+ Commands: []string{
+ "bitfield bits set u8 #0 65",
+ "bitfield bits set u8 #1 66",
+ "bitfield bits set u8 #2 67",
+ "get bits",
+ },
+ Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(0)}, []interface{}{int64(0)}, "ABC"},
+ Delay: []time.Duration{0, 0, 0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD basic INCRBY form",
+ Commands: []string{
+ "bitfield bits set u8 #0 10",
+ "bitfield bits incrby u8 #0 100",
+ "bitfield bits incrby u8 #0 100",
+ },
+ Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(110)}, []interface{}{int64(210)}},
+ Delay: []time.Duration{0, 0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD chaining of multiple commands",
+ Commands: []string{
+ "bitfield bits set u8 #0 10",
+ "bitfield bits incrby u8 #0 100 incrby u8 #0 100",
+ },
+ Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(110), int64(210)}},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD unsigned overflow wrap",
+ Commands: []string{
+ "bitfield bits set u8 #0 100",
+ "bitfield bits overflow wrap incrby u8 #0 257",
+ "bitfield bits get u8 #0",
+ "bitfield bits overflow wrap incrby u8 #0 255",
+ "bitfield bits get u8 #0",
+ },
+ Expected: []interface{}{
+ []interface{}{int64(0)},
+ []interface{}{int64(101)},
+ []interface{}{int64(101)},
+ []interface{}{int64(100)},
+ []interface{}{int64(100)},
+ },
+ Delay: []time.Duration{0, 0, 0, 0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD unsigned overflow sat",
+ Commands: []string{
+ "bitfield bits set u8 #0 100",
+ "bitfield bits overflow sat incrby u8 #0 257",
+ "bitfield bits get u8 #0",
+ "bitfield bits overflow sat incrby u8 #0 -255",
+ "bitfield bits get u8 #0",
+ },
+ Expected: []interface{}{
+ []interface{}{int64(0)},
+ []interface{}{int64(255)},
+ []interface{}{int64(255)},
+ []interface{}{int64(0)},
+ []interface{}{int64(0)},
+ },
+ Delay: []time.Duration{0, 0, 0, 0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD signed overflow wrap",
+ Commands: []string{
+ "bitfield bits set i8 #0 100",
+ "bitfield bits overflow wrap incrby i8 #0 257",
+ "bitfield bits get i8 #0",
+ "bitfield bits overflow wrap incrby i8 #0 255",
+ "bitfield bits get i8 #0",
+ },
+ Expected: []interface{}{
+ []interface{}{int64(0)},
+ []interface{}{int64(101)},
+ []interface{}{int64(101)},
+ []interface{}{int64(100)},
+ []interface{}{int64(100)},
+ },
+ Delay: []time.Duration{0, 0, 0, 0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD signed overflow sat",
+ Commands: []string{
+ "bitfield bits set u8 #0 100",
+ "bitfield bits overflow sat incrby i8 #0 257",
+ "bitfield bits get i8 #0",
+ "bitfield bits overflow sat incrby i8 #0 -255",
+ "bitfield bits get i8 #0",
+ },
+ Expected: []interface{}{
+ []interface{}{int64(0)},
+ []interface{}{int64(127)},
+ []interface{}{int64(127)},
+ []interface{}{int64(-128)},
+ []interface{}{int64(-128)},
+ },
+ Delay: []time.Duration{0, 0, 0, 0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD regression 1",
+ Commands: []string{"set bits 1", "bitfield bits get u1 0"},
+ Expected: []interface{}{"OK", []interface{}{int64(0)}},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD regression 2",
+ Commands: []string{
+ "bitfield mystring set i8 0 10",
+ "bitfield mystring set i8 64 10",
+ "bitfield mystring incrby i8 10 99900",
+ },
+ Expected: []interface{}{[]interface{}{int64(0)}, []interface{}{int64(0)}, []interface{}{int64(60)}},
+ Delay: []time.Duration{0, 0, 0},
+ CleanUp: []string{"DEL mystring"},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.Name, func(t *testing.T) {
+
+ for i := 0; i < len(tc.Commands); i++ {
+ if tc.Delay[i] > 0 {
+ time.Sleep(tc.Delay[i])
+ }
+ result := FireCommand(conn, tc.Commands[i])
+ expected := tc.Expected[i]
+ assert.Equal(t, expected, result)
+ }
+
+ for _, cmd := range tc.CleanUp {
+ FireCommand(conn, cmd)
+ }
+ })
+ }
+}
+
+func TestBitfieldRO(t *testing.T) {
+ conn := getLocalConnection()
+ defer conn.Close()
+
+ FireCommand(conn, "FLUSHDB")
+ defer FireCommand(conn, "FLUSHDB")
+
+ syntaxErrMsg := "ERR syntax error"
+ bitFieldTypeErrMsg := "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is"
+ unsupportedCmdErrMsg := "ERR BITFIELD_RO only supports the GET subcommand"
+
+ testCases := []struct {
+ Name string
+ Commands []string
+ Expected []interface{}
+ Delay []time.Duration
+ CleanUp []string
+ }{
+ {
+ Name: "BITFIELD_RO Arity Check",
+ Commands: []string{"bitfield_ro"},
+ Expected: []interface{}{"ERR wrong number of arguments for 'bitfield_ro' command"},
+ Delay: []time.Duration{0},
+ CleanUp: []string{},
+ },
+ {
+ Name: "BITFIELD_RO on unsupported type of SET",
+ Commands: []string{"SADD bits a b c", "bitfield_ro bits"},
+ Expected: []interface{}{int64(3), "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD_RO on unsupported type of JSON",
+ Commands: []string{"json.set bits $ 1", "bitfield_ro bits"},
+ Expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD_RO on unsupported type of HSET",
+ Commands: []string{"HSET bits a 1", "bitfield_ro bits"},
+ Expected: []interface{}{int64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD_RO with unsupported commands",
+ Commands: []string{
+ "bitfield_ro bits set u8 0 255",
+ "bitfield_ro bits incrby u8 0 100",
+ },
+ Expected: []interface{}{
+ unsupportedCmdErrMsg,
+ unsupportedCmdErrMsg,
+ },
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"Del bits"},
+ },
+ {
+ Name: "BITFIELD_RO with syntax error",
+ Commands: []string{
+ "set bits 1",
+ "bitfield_ro bits get u8",
+ "bitfield_ro bits get",
+ "bitfield_ro bits get somethingrandom",
+ },
+ Expected: []interface{}{
+ "OK",
+ syntaxErrMsg,
+ syntaxErrMsg,
+ syntaxErrMsg,
+ },
+ Delay: []time.Duration{0, 0, 0, 0},
+ CleanUp: []string{"Del bits"},
+ },
+ {
+ Name: "BITFIELD_RO with invalid bitfield type",
+ Commands: []string{
+ "set bits 1",
+ "bitfield_ro bits get a8 0",
+ "bitfield_ro bits get s8 0",
+ "bitfield_ro bits get somethingrandom 0",
+ },
+ Expected: []interface{}{
+ "OK",
+ bitFieldTypeErrMsg,
+ bitFieldTypeErrMsg,
+ bitFieldTypeErrMsg,
+ },
+ Delay: []time.Duration{0, 0, 0, 0},
+ CleanUp: []string{"Del bits"},
+ },
+ {
+ Name: "BITFIELD_RO with only key as argument",
+ Commands: []string{"bitfield_ro bits"},
+ Expected: []interface{}{[]interface{}{}},
+ Delay: []time.Duration{0},
+ CleanUp: []string{"DEL bits"},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.Name, func(t *testing.T) {
+
+ for i := 0; i < len(tc.Commands); i++ {
+ if tc.Delay[i] > 0 {
+ time.Sleep(tc.Delay[i])
+ }
+ result := FireCommand(conn, tc.Commands[i])
+ expected := tc.Expected[i]
+ assert.Equal(t, expected, result)
+ }
+
+ for _, cmd := range tc.CleanUp {
+ FireCommand(conn, cmd)
+ }
+ })
+ }
+}
diff --git a/integration_tests/commands/resp/bloom_test.go b/integration_tests/commands/resp/bloom_test.go
index bda85c284..53ba8b506 100644
--- a/integration_tests/commands/resp/bloom_test.go
+++ b/integration_tests/commands/resp/bloom_test.go
@@ -218,5 +218,6 @@ func TestBFEdgeCasesAndErrors(t *testing.T) {
FireCommand(conn, cmd)
}
})
+ FireCommand(conn, "FLUSHDB")
}
}
diff --git a/integration_tests/commands/resp/deque_test.go b/integration_tests/commands/resp/deque_test.go
new file mode 100644
index 000000000..e022e8b5d
--- /dev/null
+++ b/integration_tests/commands/resp/deque_test.go
@@ -0,0 +1,105 @@
+package resp
+
+import (
+ "net"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestLInsert(t *testing.T) {
+ conn := getLocalConnection()
+ defer conn.Close()
+
+ testCases := []struct {
+ name string
+ cmds []string
+ expect []any
+ }{
+ {
+ name: "LINSERT before",
+ cmds: []string{"LPUSH k v1 v2 v3 v4", "LINSERT k before v2 e1", "LINSERT k before v1 e2", "LINSERT k before v4 e3", "LRANGE k 0 6"},
+ expect: []any{int64(4), int64(5), int64(6), int64(7), []any{"e3", "v4", "v3", "e1", "v2", "e2", "v1"}},
+ },
+ {
+ name: "LINSERT after",
+ cmds: []string{"LINSERT k after v2 e4", "LINSERT k after v1 e5", "LINSERT k after v4 e6", "LRANGE k 0 10"},
+ expect: []any{int64(8), int64(9), int64(10), []any{"e3", "v4", "e6", "v3", "e1", "v2", "e4", "e2", "v1", "e5"}},
+ },
+ {
+ name: "LINSERT wrong number of args",
+ cmds: []string{"LINSERT k before e1"},
+ expect: []any{"-wrong number of arguments for LINSERT"},
+ },
+ {
+ name: "LINSERT wrong type",
+ cmds: []string{"SET k1 val1", "LINSERT k1 before val1 val2"},
+ expect: []any{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ for i, cmd := range tc.cmds {
+ result := FireCommand(conn, cmd)
+ // assert.DeepEqual(t, tc.expect[i], result)
+ assert.EqualValues(t, tc.expect[i], result)
+ }
+ })
+ }
+
+ deqCleanUp(conn, "k")
+}
+
+func TestLRange(t *testing.T) {
+ conn := getLocalConnection()
+ defer conn.Close()
+
+ testCases := []struct {
+ name string
+ cmds []string
+ expect []any
+ }{
+ {
+ name: "LRANGE with +ve start stop",
+ cmds: []string{"LPUSH k v1 v2 v3 v4", "LINSERT k before v2 e1", "LINSERT k before v1 e2", "LINSERT k before v4 e3", "LRANGE k 0 6"},
+ expect: []any{int64(4), int64(5), int64(6), int64(7), []any{"e3", "v4", "v3", "e1", "v2", "e2", "v1"}},
+ },
+ {
+ name: "LRANGE with -ve start stop",
+ cmds: []string{"LRANGE k -100 -2"},
+ expect: []any{[]any{"e3", "v4", "v3", "e1", "v2", "e2"}},
+ },
+ {
+ name: "LRANGE wrong number of args",
+ cmds: []string{"LRANGE k -100"},
+ expect: []any{"-wrong number of arguments for LRANGE"},
+ },
+ {
+ name: "LRANGE wrong type",
+ cmds: []string{"SET k1 val1", "LRANGE k1 0 100"},
+ expect: []any{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ for i, cmd := range tc.cmds {
+ result := FireCommand(conn, cmd)
+ // assert.DeepEqual(t, tc.expect[i], result)
+ assert.EqualValues(t, tc.expect[i], result)
+ }
+ })
+ }
+
+ deqCleanUp(conn, "k")
+}
+
+func deqCleanUp(conn net.Conn, key string) {
+ for {
+ result := FireCommand(conn, "LPOP "+key)
+ if result == "(nil)" {
+ break
+ }
+ }
+}
diff --git a/integration_tests/commands/resp/getunwatch_test.go b/integration_tests/commands/resp/getunwatch_test.go
index 542260974..addcfadb7 100644
--- a/integration_tests/commands/resp/getunwatch_test.go
+++ b/integration_tests/commands/resp/getunwatch_test.go
@@ -14,7 +14,6 @@ import (
const (
getUnwatchKey = "getunwatchkey"
- fingerprint = "3557732805"
)
type getUnwatchTestCase struct {
@@ -78,16 +77,17 @@ func TestGETUNWATCH(t *testing.T) {
if !ok {
t.Errorf("Type assertion to []interface{} failed for value: %v", v)
}
+ fmt.Println(castedValue)
assert.Equal(t, 3, len(castedValue))
assert.Equal(t, "GET", castedValue[0])
- assert.Equal(t, fingerprint, castedValue[1])
+ assert.Equal(t, "426696421", castedValue[1])
assert.Equal(t, tc.val, castedValue[2])
}
}
// unsubscribe from updates
for _, subscriber := range subscribers {
- rp := fireCommandAndGetRESPParser(subscriber, fmt.Sprintf("GET.UNWATCH %s", fingerprint))
+ rp := fireCommandAndGetRESPParser(subscriber, fmt.Sprintf("GET.UNWATCH %s", "426696421"))
assert.NotNil(t, rp)
v, err := rp.DecodeOne()
@@ -98,7 +98,6 @@ func TestGETUNWATCH(t *testing.T) {
}
assert.Equal(t, castedValue, "OK")
}
-
// Test updates are not sent after unsubscribing
for _, tc := range getUnwatchTestCases[2:] {
res := FireCommand(publisher, fmt.Sprintf("SET %s %s", tc.key, tc.val))
@@ -144,7 +143,7 @@ func TestGETUNWATCHWithSDK(t *testing.T) {
firstMsg, err := watch.Watch(context.Background(), "GET", getUnwatchKey)
assert.Nil(t, err)
assert.Equal(t, firstMsg.Command, "GET")
- assert.Equal(t, firstMsg.Fingerprint, fingerprint)
+ assert.Equal(t, "426696421", firstMsg.Fingerprint)
channels[i] = watch.Channel()
}
@@ -155,13 +154,13 @@ func TestGETUNWATCHWithSDK(t *testing.T) {
for _, channel := range channels {
v := <-channel
assert.Equal(t, "GET", v.Command) // command
- assert.Equal(t, fingerprint, v.Fingerprint) // Fingerprint
+ assert.Equal(t, "426696421", v.Fingerprint) // Fingerprint
assert.Equal(t, "check", v.Data.(string)) // data
}
// unsubscribe from updates
for _, subscriber := range subscribers {
- err := subscriber.watch.Unwatch(context.Background(), "GET", fingerprint)
+ err := subscriber.watch.Unwatch(context.Background(), "GET", "426696421")
assert.Nil(t, err)
}
diff --git a/integration_tests/commands/resp/getwatch_test.go b/integration_tests/commands/resp/getwatch_test.go
index 537644b8f..da5fa9042 100644
--- a/integration_tests/commands/resp/getwatch_test.go
+++ b/integration_tests/commands/resp/getwatch_test.go
@@ -17,7 +17,9 @@ type WatchSubscriber struct {
watch *dicedb.WatchConn
}
-const getWatchKey = "getwatchkey"
+const (
+ getWatchKey = "getwatchkey"
+)
type getWatchTestCase struct {
key string
@@ -34,7 +36,6 @@ var getWatchTestCases = []getWatchTestCase{
func TestGETWATCH(t *testing.T) {
publisher := getLocalConnection()
subscribers := []net.Conn{getLocalConnection(), getLocalConnection(), getLocalConnection()}
-
FireCommand(publisher, fmt.Sprintf("DEL %s", getWatchKey))
defer func() {
@@ -83,7 +84,7 @@ func TestGETWATCH(t *testing.T) {
}
assert.Equal(t, 3, len(castedValue))
assert.Equal(t, "GET", castedValue[0])
- assert.Equal(t, "1768826704", castedValue[1])
+ assert.Equal(t, "2714318480", castedValue[1])
assert.Equal(t, tc.val, castedValue[2])
}
}
@@ -103,7 +104,7 @@ func TestGETWATCHWithSDK(t *testing.T) {
firstMsg, err := watch.Watch(context.Background(), "GET", getWatchKey)
assert.Nil(t, err)
assert.Equal(t, firstMsg.Command, "GET")
- assert.Equal(t, firstMsg.Fingerprint, "1768826704")
+ assert.Equal(t, "2714318480", firstMsg.Fingerprint)
channels[i] = watch.Channel()
}
@@ -113,9 +114,9 @@ func TestGETWATCHWithSDK(t *testing.T) {
for _, channel := range channels {
v := <-channel
- assert.Equal(t, "GET", v.Command) // command
- assert.Equal(t, "1768826704", v.Fingerprint) // Fingerprint
- assert.Equal(t, tc.val, v.Data.(string)) // data
+ assert.Equal(t, "GET", v.Command) // command
+ assert.Equal(t, "2714318480", v.Fingerprint) // Fingerprint
+ assert.Equal(t, tc.val, v.Data.(string)) // data
}
}
}
@@ -134,7 +135,7 @@ func TestGETWATCHWithSDK2(t *testing.T) {
firstMsg, err := watch.GetWatch(context.Background(), getWatchKey)
assert.Nil(t, err)
assert.Equal(t, firstMsg.Command, "GET")
- assert.Equal(t, firstMsg.Fingerprint, "1768826704")
+ assert.Equal(t, "2714318480", firstMsg.Fingerprint)
channels[i] = watch.Channel()
}
@@ -144,9 +145,9 @@ func TestGETWATCHWithSDK2(t *testing.T) {
for _, channel := range channels {
v := <-channel
- assert.Equal(t, "GET", v.Command) // command
- assert.Equal(t, "1768826704", v.Fingerprint) // Fingerprint
- assert.Equal(t, tc.val, v.Data.(string)) // data
+ assert.Equal(t, "GET", v.Command) // command
+ assert.Equal(t, "2714318480", v.Fingerprint) // Fingerprint
+ assert.Equal(t, tc.val, v.Data.(string)) // data
}
}
}
diff --git a/integration_tests/commands/resp/set_data_cmd_test.go b/integration_tests/commands/resp/set_data_cmd_test.go
new file mode 100644
index 000000000..959c0b1d7
--- /dev/null
+++ b/integration_tests/commands/resp/set_data_cmd_test.go
@@ -0,0 +1,164 @@
+package resp
+
+import (
+ "gotest.tools/v3/assert"
+ "sort"
+ "testing"
+ "time"
+)
+
+func CustomDeepEqual(t *testing.T, a, b interface{}) {
+ if a == nil || b == nil {
+ assert.DeepEqual(t, a, b)
+ }
+
+ switch a.(type) {
+ case []any:
+ sort.Slice(a.([]any), func(i, j int) bool {
+ return a.([]any)[i].(string) < a.([]any)[j].(string)
+ })
+ sort.Slice(b.([]any), func(i, j int) bool {
+ return b.([]any)[i].(string) < b.([]any)[j].(string)
+ })
+ }
+
+ assert.DeepEqual(t, a, b)
+}
+
+func TestSetDataCommand(t *testing.T) {
+ conn := getLocalConnection()
+ defer conn.Close()
+
+ testCases := []struct {
+ name string
+ cmd []string
+ expected []interface{}
+ assertType []string
+ delay []time.Duration
+ }{
+ // SADD
+ {
+ name: "SADD Simple Value",
+ cmd: []string{"SADD foo bar", "SMEMBERS foo"},
+ expected: []interface{}{int64(1), []any{"bar"}},
+ assertType: []string{"equal", "equal"},
+ delay: []time.Duration{0, 0},
+ },
+ {
+ name: "SADD Multiple Values",
+ cmd: []string{"SADD foo bar", "SADD foo baz", "SMEMBERS foo"},
+ expected: []interface{}{int64(1), int64(1), []any{"bar", "baz"}},
+ assertType: []string{"equal", "equal", "equal"},
+ delay: []time.Duration{0, 0, 0},
+ },
+ {
+ name: "SADD Duplicate Values",
+ cmd: []string{"SADD foo bar", "SADD foo bar", "SMEMBERS foo"},
+ expected: []interface{}{int64(1), int64(0), []any{"bar"}},
+ assertType: []string{"equal", "equal", "equal"},
+ delay: []time.Duration{0, 0, 0},
+ },
+ {
+ name: "SADD Wrong Key Value Type",
+ cmd: []string{"SET foo bar", "SADD foo baz"},
+ expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ assertType: []string{"equal", "equal"},
+ delay: []time.Duration{0, 0},
+ },
+ {
+ name: "SADD Multiple add and multiple kind of values",
+ cmd: []string{"SADD foo bar", "SADD foo baz", "SADD foo 1", "SMEMBERS foo"},
+ expected: []interface{}{int64(1), int64(1), int64(1), []any{"bar", "baz", "1"}},
+ assertType: []string{"equal", "equal", "equal", "equal"},
+ delay: []time.Duration{0, 0, 0, 0},
+ },
+ // SCARD
+ {
+ name: "SADD & SCARD",
+ cmd: []string{"SADD foo bar", "SADD foo baz", "SCARD foo"},
+ expected: []interface{}{int64(1), int64(1), int64(2)},
+ assertType: []string{"equal", "equal", "equal"},
+ delay: []time.Duration{0, 0, 0},
+ },
+ {
+ name: "SADD & CARD with non existing key",
+ cmd: []string{"SADD foo bar", "SADD foo baz", "SCARD bar"},
+ expected: []interface{}{int64(1), int64(1), int64(0)},
+ assertType: []string{"equal", "equal", "equal"},
+ delay: []time.Duration{0, 0, 0},
+ },
+ {
+ name: "SADD & SCARD with wrong key type",
+ cmd: []string{"SET foo bar", "SCARD foo"},
+ expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ assertType: []string{"equal", "equal"},
+ delay: []time.Duration{0, 0},
+ },
+ // SMEMBERS
+ {
+ name: "SADD & SMEMBERS",
+ cmd: []string{"SADD foo bar", "SADD foo baz", "SMEMBERS foo"},
+ expected: []interface{}{int64(1), int64(1), []any{"bar", "baz"}},
+ assertType: []string{"equal", "equal", "equal"},
+ delay: []time.Duration{0, 0, 0},
+ },
+ {
+ name: "SADD & SMEMBERS with non existing key",
+ cmd: []string{"SMEMBERS foo"},
+ expected: []interface{}{[]any{}},
+ assertType: []string{"equal"},
+ delay: []time.Duration{0},
+ },
+ {
+ name: "SADD & SMEMBERS with wrong key type",
+ cmd: []string{"SET foo bar", "SMEMBERS foo"},
+ expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ assertType: []string{"equal", "equal"},
+ delay: []time.Duration{0, 0},
+ },
+ // SREM
+ {
+ name: "SADD & SREM",
+ cmd: []string{"SADD foo bar", "SADD foo baz", "SREM foo bar", "SMEMBERS foo"},
+ expected: []interface{}{int64(1), int64(1), int64(1), []any{"baz"}},
+ assertType: []string{"equal", "equal", "equal", "equal"},
+ delay: []time.Duration{0, 0, 0, 0},
+ },
+ {
+ name: "SADD & SREM with non existing key",
+ cmd: []string{"SREM foo bar"},
+ expected: []interface{}{int64(0)},
+ assertType: []string{"equal"},
+ delay: []time.Duration{0},
+ },
+ {
+ name: "SADD & SREM with wrong key type",
+ cmd: []string{"SET foo bar", "SREM foo bar"},
+ expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ assertType: []string{"equal", "equal"},
+ delay: []time.Duration{0, 0},
+ },
+ {
+ name: "SADD & SREM with non existing value",
+ cmd: []string{"SADD foo bar baz bax", "SMEMBERS foo", "SREM foo bat", "SMEMBERS foo"},
+ expected: []interface{}{int64(3), []any{"bar", "baz", "bax"}, int64(0), []any{"bar", "baz", "bax"}},
+ assertType: []string{"equal", "equal", "equal", "equal"},
+ delay: []time.Duration{0, 0, 0, 0},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ FireCommand(conn, "DEL foo")
+ FireCommand(conn, "DEL foo2")
+ for i, cmd := range tc.cmd {
+ if tc.delay[i] > 0 {
+ time.Sleep(tc.delay[i])
+ }
+ result := FireCommand(conn, cmd)
+ CustomDeepEqual(t, result, tc.expected[i])
+ }
+ })
+ }
+
+}
diff --git a/integration_tests/commands/resp/set_test.go b/integration_tests/commands/resp/set_test.go
index e5ddb036c..781e4d347 100644
--- a/integration_tests/commands/resp/set_test.go
+++ b/integration_tests/commands/resp/set_test.go
@@ -120,6 +120,21 @@ func TestSetWithOptions(t *testing.T) {
commands: []string{"SET k v XX EX 1", "GET k", "SLEEP 2", "GET k", "SET k v XX EX 1", "GET k"},
expected: []interface{}{"(nil)", "(nil)", "OK", "(nil)", "(nil)", "(nil)"},
},
+ {
+ name: "GET with Existing Value",
+ commands: []string{"SET k v", "SET k vv GET"},
+ expected: []interface{}{"OK", "v"},
+ },
+ {
+ name: "GET with Non-Existing Value",
+ commands: []string{"SET k vv GET"},
+ expected: []interface{}{"(nil)"},
+ },
+ {
+ name: "GET with wrong type of value",
+ commands: []string{"sadd k v", "SET k vv GET"},
+ expected: []interface{}{int64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ },
}
for _, tc := range testCases {
@@ -134,6 +149,8 @@ func TestSetWithOptions(t *testing.T) {
}
})
}
+
+ FireCommand(conn, "FLUSHDB")
}
func TestSetWithExat(t *testing.T) {
diff --git a/integration_tests/commands/resp/setup.go b/integration_tests/commands/resp/setup.go
index 7026134eb..a71d5a51d 100644
--- a/integration_tests/commands/resp/setup.go
+++ b/integration_tests/commands/resp/setup.go
@@ -12,6 +12,7 @@ import (
"time"
"github.com/dicedb/dice/internal/server/resp"
+ "github.com/dicedb/dice/internal/wal"
"github.com/dicedb/dice/internal/watchmanager"
"github.com/dicedb/dice/internal/worker"
@@ -121,14 +122,15 @@ func RunTestServer(wg *sync.WaitGroup, opt TestServerOptions) {
config.DiceConfig.AsyncServer.Port = 9739
}
- queryWatchChan := make(chan dstore.QueryWatchEvent, config.DiceConfig.Memory.KeysLimit)
- cmdWatchChan := make(chan dstore.CmdWatchEvent, config.DiceConfig.Memory.KeysLimit)
+ queryWatchChan := make(chan dstore.QueryWatchEvent, config.DiceConfig.Performance.WatchChanBufSize)
+ cmdWatchChan := make(chan dstore.CmdWatchEvent, config.DiceConfig.Performance.WatchChanBufSize)
cmdWatchSubscriptionChan := make(chan watchmanager.WatchSubscription)
gec := make(chan error)
shardManager := shard.NewShardManager(1, queryWatchChan, cmdWatchChan, gec)
workerManager := worker.NewWorkerManager(20000, shardManager)
// Initialize the RESP Server
- testServer := resp.NewServer(shardManager, workerManager, cmdWatchSubscriptionChan, cmdWatchChan, gec)
+ wl, _ := wal.NewNullWAL()
+ testServer := resp.NewServer(shardManager, workerManager, cmdWatchSubscriptionChan, cmdWatchChan, gec, wl)
ctx, cancel := context.WithCancel(context.Background())
fmt.Println("Starting the test server on port", config.DiceConfig.AsyncServer.Port)
diff --git a/integration_tests/commands/resp/zrangewatch_test.go b/integration_tests/commands/resp/zrangewatch_test.go
index 8f56eb3eb..d7ba06cfc 100644
--- a/integration_tests/commands/resp/zrangewatch_test.go
+++ b/integration_tests/commands/resp/zrangewatch_test.go
@@ -76,7 +76,7 @@ func TestZRANGEWATCH(t *testing.T) {
}
assert.Equal(t, 3, len(castedValue))
assert.Equal(t, "ZRANGE", castedValue[0])
- assert.Equal(t, "2491069200", castedValue[1])
+ assert.Equal(t, "1178068413", castedValue[1])
assert.DeepEqual(t, tc.result, castedValue[2])
}
}
@@ -124,7 +124,7 @@ func TestZRANGEWATCHWithSDK(t *testing.T) {
firstMsg, err := watch.Watch(context.Background(), "ZRANGE", zrangeWatchKey, "0", "-1", "REV", "WITHSCORES")
assert.NilError(t, err)
assert.Equal(t, firstMsg.Command, "ZRANGE")
- assert.Equal(t, firstMsg.Fingerprint, "2491069200")
+ assert.Equal(t, firstMsg.Fingerprint, "1178068413")
channels[i] = watch.Channel()
}
@@ -139,7 +139,7 @@ func TestZRANGEWATCHWithSDK(t *testing.T) {
v := <-channel
assert.Equal(t, "ZRANGE", v.Command) // command
- assert.Equal(t, "2491069200", v.Fingerprint) // Fingerprint
+ assert.Equal(t, "1178068413", v.Fingerprint) // Fingerprint
assert.DeepEqual(t, tc.result, v.Data) // data
}
}
@@ -158,7 +158,7 @@ func TestZRANGEWATCHWithSDK2(t *testing.T) {
firstMsg, err := conn.ZRangeWatch(context.Background(), zrangeWatchKey, "0", "-1", "REV", "WITHSCORES")
assert.NilError(t, err)
assert.Equal(t, firstMsg.Command, "ZRANGE")
- assert.Equal(t, firstMsg.Fingerprint, "2491069200")
+ assert.Equal(t, firstMsg.Fingerprint, "1178068413")
channels[i] = conn.Channel()
}
@@ -173,7 +173,7 @@ func TestZRANGEWATCHWithSDK2(t *testing.T) {
v := <-channel
assert.Equal(t, "ZRANGE", v.Command)
- assert.Equal(t, "2491069200", v.Fingerprint)
+ assert.Equal(t, "1178068413", v.Fingerprint)
assert.DeepEqual(t, tc.result, v.Data)
}
}
diff --git a/integration_tests/commands/resp/zset_test.go b/integration_tests/commands/resp/zset_test.go
index 1139bb5f4..be6e3a9a3 100644
--- a/integration_tests/commands/resp/zset_test.go
+++ b/integration_tests/commands/resp/zset_test.go
@@ -23,53 +23,53 @@ func TestZPOPMIN(t *testing.T) {
},
{
name: "ZPOPMIN on existing key (without count argument)",
- commands: []string{"ZADD myzset 1 member1 2 member2 3 member3", "ZPOPMIN myzset"},
- expected: []interface{}{int64(3), []interface{}{"member1", "1"}},
+ commands: []string{"ZADD myzset 1 member1 2 member2 3 member3", "ZPOPMIN myzset", "ZCOUNT myzset 1 10"},
+ expected: []interface{}{int64(3), []interface{}{"member1", "1"}, int64(2)},
},
{
name: "ZPOPMIN with normal count argument",
- commands: []string{"ZADD myzset 1 member1 2 member2 3 member3", "ZPOPMIN myzset 2"},
- expected: []interface{}{int64(3), []interface{}{"member1", "1", "member2", "2"}},
+ commands: []string{"ZADD myzset 1 member1 2 member2 3 member3", "ZPOPMIN myzset 2", "ZCOUNT myzset 1 2"},
+ expected: []interface{}{int64(3), []interface{}{"member1", "1", "member2", "2"}, int64(0)},
},
{
name: "ZPOPMIN with count argument but multiple members have the same score",
- commands: []string{"ZADD myzset 1 member1 1 member2 1 member3", "ZPOPMIN myzset 2"},
- expected: []interface{}{int64(3), []interface{}{"member1", "1", "member2", "1"}},
+ commands: []string{"ZADD myzset 1 member1 1 member2 1 member3", "ZPOPMIN myzset 2", "ZCOUNT myzset 1 1"},
+ expected: []interface{}{int64(3), []interface{}{"member1", "1", "member2", "1"}, int64(1)},
},
{
name: "ZPOPMIN with negative count argument",
- commands: []string{"ZADD myzset 1 member1 2 member2 3 member3", "ZPOPMIN myzset -1"},
- expected: []interface{}{int64(3), []interface{}{}},
+ commands: []string{"ZADD myzset 1 member1 2 member2 3 member3", "ZPOPMIN myzset -1", "ZCOUNT myzset 0.6 3.231"},
+ expected: []interface{}{int64(3), []interface{}{}, int64(3)},
},
{
name: "ZPOPMIN with invalid count argument",
- commands: []string{"ZADD myzset 1 member1", "ZPOPMIN myzset INCORRECT_COUNT_ARGUMENT"},
- expected: []interface{}{int64(1), "ERR value is not an integer or out of range"},
+ commands: []string{"ZADD myzset 1 member1", "ZPOPMIN myzset INCORRECT_COUNT_ARGUMENT", "ZCOUNT myzset 1 10"},
+ expected: []interface{}{int64(1), "ERR value is not an integer or out of range", int64(1)},
},
{
name: "ZPOPMIN with count argument greater than length of sorted set",
- commands: []string{"ZADD myzset 1 member1 2 member2 3 member3", "ZPOPMIN myzset 10"},
- expected: []interface{}{int64(3), []interface{}{"member1", "1", "member2", "2", "member3", "3"}},
+ commands: []string{"ZADD myzset 1 member1 2 member2 3 member3", "ZPOPMIN myzset 10", "ZCOUNT myzset 1 10"},
+ expected: []interface{}{int64(3), []interface{}{"member1", "1", "member2", "2", "member3", "3"}, int64(0)},
},
{
name: "ZPOPMIN on empty sorted set",
- commands: []string{"ZADD myzset 1 member1", "ZPOPMIN myzset 1", "ZPOPMIN myzset"},
- expected: []interface{}{int64(1), []interface{}{"member1", "1"}, []interface{}{}},
+ commands: []string{"ZADD myzset 1 member1", "ZPOPMIN myzset 1", "ZPOPMIN myzset", "ZCOUNT myzset 0 10000"},
+ expected: []interface{}{int64(1), []interface{}{"member1", "1"}, []interface{}{}, int64(0)},
},
{
name: "ZPOPMIN with floating-point scores",
- commands: []string{"ZADD myzset 1.5 member1 2.7 member2 3.8 member3", "ZPOPMIN myzset"},
- expected: []interface{}{int64(3), []interface{}{"member1", "1.5"}},
+ commands: []string{"ZADD myzset 1.5 member1 2.7 member2 3.8 member3", "ZPOPMIN myzset", "ZCOUNT myzset 1.499 2.711"},
+ expected: []interface{}{int64(3), []interface{}{"member1", "1.5"}, int64(1)},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- FireCommand(conn, "DEL myzset")
for i, cmd := range tc.commands {
result := FireCommand(conn, cmd)
assert.Equal(t, tc.expected[i], result)
}
+ FireCommand(conn, "DEL myzset")
})
}
}
diff --git a/integration_tests/commands/websocket/bit_test.go b/integration_tests/commands/websocket/bit_test.go
new file mode 100644
index 000000000..687b339d5
--- /dev/null
+++ b/integration_tests/commands/websocket/bit_test.go
@@ -0,0 +1,1090 @@
+package websocket
+
+import (
+ "fmt"
+ "math"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+// TODO: BITOP has not been migrated yet. Once done, we can uncomment the tests - please check accuracy and validate for expected values.
+
+// func TestBitOp(t *testing.T) {
+// exec := NewWebsocketCommandExecutor()
+// conn := exec.ConnectToServer()
+// defer conn.Close()
+// testcases := []struct {
+// InCmds []string
+// Out []interface{}
+// }{
+// {
+// InCmds: []string{"SETBIT unitTestKeyA 1 1", "SETBIT unitTestKeyA 3 1", "SETBIT unitTestKeyA 5 1", "SETBIT unitTestKeyA 7 1", "SETBIT unitTestKeyA 8 1"},
+// Out: []interface{}{float64(0), float64(0), float64(0), float64(0), float64(0)},
+// },
+// {
+// InCmds: []string{"SETBIT unitTestKeyB 2 1", "SETBIT unitTestKeyB 4 1", "SETBIT unitTestKeyB 7 1"},
+// Out: []interface{}{float64(0), float64(0), float64(0)},
+// },
+// {
+// InCmds: []string{"SET foo bar", "SETBIT foo 2 1", "SETBIT foo 4 1", "SETBIT foo 7 1", "GET foo"},
+// Out: []interface{}{"OK", float64(1), float64(0), float64(0), "kar"},
+// },
+// {
+// InCmds: []string{"SET mykey12 1343", "SETBIT mykey12 2 1", "SETBIT mykey12 4 1", "SETBIT mykey12 7 1", "GET mykey12"},
+// Out: []interface{}{"OK", float64(1), float64(0), float64(1), float64(9343)},
+// },
+// {
+// InCmds: []string{"SET foo12 bar", "SETBIT foo12 2 1", "SETBIT foo12 4 1", "SETBIT foo12 7 1", "GET foo12"},
+// Out: []interface{}{"OK", float64(1), float64(0), float64(0), "kar"},
+// },
+// {
+// InCmds: []string{"BITOP NOT unitTestKeyNOT unitTestKeyA "},
+// Out: []interface{}{float64(2)},
+// },
+// {
+// InCmds: []string{"GETBIT unitTestKeyNOT 1", "GETBIT unitTestKeyNOT 2", "GETBIT unitTestKeyNOT 7", "GETBIT unitTestKeyNOT 8", "GETBIT unitTestKeyNOT 9"},
+// Out: []interface{}{float64(0), float64(1), float64(0), float64(0), float64(1)},
+// },
+// {
+// InCmds: []string{"BITOP OR unitTestKeyOR unitTestKeyB unitTestKeyA"},
+// Out: []interface{}{float64(2)},
+// },
+// {
+// InCmds: []string{"GETBIT unitTestKeyOR 1", "GETBIT unitTestKeyOR 2", "GETBIT unitTestKeyOR 3", "GETBIT unitTestKeyOR 7", "GETBIT unitTestKeyOR 8", "GETBIT unitTestKeyOR 9", "GETBIT unitTestKeyOR 12"},
+// Out: []interface{}{float64(1), float64(1), float64(1), float64(1), float64(1), float64(0), float64(0)},
+// },
+// {
+// InCmds: []string{"BITOP AND unitTestKeyAND unitTestKeyB unitTestKeyA"},
+// Out: []interface{}{float64(2)},
+// },
+// {
+// InCmds: []string{"GETBIT unitTestKeyAND 1", "GETBIT unitTestKeyAND 2", "GETBIT unitTestKeyAND 7", "GETBIT unitTestKeyAND 8", "GETBIT unitTestKeyAND 9"},
+// Out: []interface{}{float64(0), float64(0), float64(1), float64(0), float64(0)},
+// },
+// {
+// InCmds: []string{"BITOP XOR unitTestKeyXOR unitTestKeyB unitTestKeyA"},
+// Out: []interface{}{float64(2)},
+// },
+// {
+// InCmds: []string{"GETBIT unitTestKeyXOR 1", "GETBIT unitTestKeyXOR 2", "GETBIT unitTestKeyXOR 3", "GETBIT unitTestKeyXOR 7", "GETBIT unitTestKeyXOR 8"},
+// Out: []interface{}{float64(1), float64(1), float64(1), float64(0), float64(1)},
+// },
+// }
+
+// for _, tcase := range testcases {
+// for i := 0; i < len(tcase.InCmds); i++ {
+// cmd := tcase.InCmds[i]
+// out := tcase.Out[i]
+// result, err := exec.FireCommandAndReadResponse(conn, cmd)
+// assert.Nil(t, err)
+// assert.Equal(t, out, result, "Value mismatch for cmd %s\n.", cmd)
+// }
+// }
+// }
+
+// func TestBitOpsString(t *testing.T) {
+
+// exec := NewWebsocketCommandExecutor()
+// conn := exec.ConnectToServer()
+// defer conn.Close()
+// // foobar in bits is 01100110 01101111 01101111 01100010 01100001 01110010
+// fooBarBits := "011001100110111101101111011000100110000101110010"
+// // randomly get 8 bits for testing
+// testOffsets := make([]int, 8)
+
+// for i := 0; i < 8; i++ {
+// testOffsets[i] = rand.Intn(len(fooBarBits))
+// }
+
+// getBitTestCommands := make([]string, 8+1)
+// getBitTestExpected := make([]interface{}, 8+1)
+
+// getBitTestCommands[0] = "SET foo foobar"
+// getBitTestExpected[0] = "OK"
+
+// for i := 1; i < 8+1; i++ {
+// getBitTestCommands[i] = fmt.Sprintf("GETBIT foo %d", testOffsets[i-1])
+// getBitTestExpected[i] = float64(fooBarBits[testOffsets[i-1]] - '0')
+// }
+
+// testCases := []struct {
+// name string
+// cmds []string
+// expected []interface{}
+// assertType []string
+// }{
+// {
+// name: "Getbit of a key containing a string",
+// cmds: getBitTestCommands,
+// expected: getBitTestExpected,
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Getbit of a key containing an integer",
+// cmds: []string{"SET foo 10", "GETBIT foo 0", "GETBIT foo 1", "GETBIT foo 2", "GETBIT foo 3", "GETBIT foo 4", "GETBIT foo 5", "GETBIT foo 6", "GETBIT foo 7"},
+// expected: []interface{}{"OK", float64(0), float64(0), float64(1), float64(1), float64(0), float64(0), float64(0), float64(1)},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// }, {
+// name: "Getbit of a key containing an integer 2nd byte",
+// cmds: []string{"SET foo 10", "GETBIT foo 8", "GETBIT foo 9", "GETBIT foo 10", "GETBIT foo 11", "GETBIT foo 12", "GETBIT foo 13", "GETBIT foo 14", "GETBIT foo 15"},
+// expected: []interface{}{"OK", float64(0), float64(0), float64(1), float64(1), float64(0), float64(0), float64(0), float64(0)},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Getbit of a key with an offset greater than the length of the string in bits",
+// cmds: []string{"SET foo foobar", "GETBIT foo 100", "GETBIT foo 48", "GETBIT foo 47"},
+// expected: []interface{}{"OK", float64(0), float64(0), float64(0)},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Bitcount of a key containing a string",
+// cmds: []string{"SET foo foobar", "BITCOUNT foo 0 -1", "BITCOUNT foo", "BITCOUNT foo 0 0", "BITCOUNT foo 1 1", "BITCOUNT foo 1 1 Byte", "BITCOUNT foo 5 30 BIT"},
+// expected: []interface{}{"OK", float64(26), float64(26), float64(4), float64(6), float64(6), float64(17)},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Bitcount of a key containing an integer",
+// cmds: []string{"SET foo 10", "BITCOUNT foo 0 -1", "BITCOUNT foo", "BITCOUNT foo 0 0", "BITCOUNT foo 1 1", "BITCOUNT foo 1 1 Byte", "BITCOUNT foo 5 30 BIT"},
+// expected: []interface{}{"OK", float64(5), float64(5), float64(3), float64(2), float64(2), float64(3)},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Setbit of a key containing a string",
+// cmds: []string{"SET foo foobar", "setbit foo 7 1", "get foo", "setbit foo 49 1", "setbit foo 50 1", "get foo", "setbit foo 49 0", "get foo"},
+// expected: []interface{}{"OK", float64(0), "goobar", float64(0), float64(0), "goobar`", float64(1), "goobar "},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Setbit of a key must not change the expiry of the key if expiry is set",
+// cmds: []string{"SET foo foobar", "EXPIRE foo 100", "TTL foo", "SETBIT foo 7 1", "TTL foo"},
+// expected: []interface{}{"OK", float64(1), float64(100), float64(0), float64(100)},
+// assertType: []string{"equal", "equal", "less", "equal", "less"},
+// },
+// {
+// name: "Setbit of a key must not add expiry to the key if expiry is not set",
+// cmds: []string{"SET foo foobar", "TTL foo", "SETBIT foo 7 1", "TTL foo"},
+// expected: []interface{}{"OK", float64(-1), float64(0), float64(-1)},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Bitop not of a key containing a string",
+// cmds: []string{"SET foo foobar", "BITOP NOT baz foo", "GET baz", "BITOP NOT bazz baz", "GET bazz"},
+// expected: []interface{}{"OK", float64(6), "\\x99\\x90\\x90\\x9d\\x9e\\x8d", float64(6), "foobar"},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Bitop not of a key containing an integer",
+// cmds: []string{"SET foo 10", "BITOP NOT baz foo", "GET baz", "BITOP NOT bazz baz", "GET bazz"},
+// expected: []interface{}{"OK", float64(2), "\\xce\\xcf", float64(2), float64(10)},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Get a string created with setbit",
+// cmds: []string{"SETBIT foo 1 1", "SETBIT foo 3 1", "GET foo"},
+// expected: []interface{}{float64(0), float64(0), "P"},
+// assertType: []string{"equal", "equal", "equal"},
+// },
+// {
+// name: "Bitop and of keys containing a string and get the destkey",
+// cmds: []string{"SET foo foobar", "SET baz abcdef", "BITOP AND bazz foo baz", "GET bazz"},
+// expected: []interface{}{"OK", "OK", float64(6), "`bc`ab"},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP AND of keys containing integers and get the destkey",
+// cmds: []string{"SET foo 10", "SET baz 5", "BITOP AND bazz foo baz", "GET bazz"},
+// expected: []interface{}{"OK", "OK", float64(2), "1\x00"},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "Bitop or of keys containing a string, a bytearray and get the destkey",
+// cmds: []string{"MSET foo foobar baz abcdef", "SETBIT bazz 8 1", "BITOP and bazzz foo baz bazz", "GET bazzz"},
+// expected: []interface{}{"OK", float64(0), float64(6), "\x00\x00\x00\x00\x00\x00"},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP OR of keys containing strings and get the destkey",
+// cmds: []string{"MSET foo foobar baz abcdef", "BITOP OR bazz foo baz", "GET bazz"},
+// expected: []interface{}{"OK", float64(6), "goofev"},
+// assertType: []string{"equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP OR of keys containing integers and get the destkey",
+// cmds: []string{"SET foo 10", "SET baz 5", "BITOP OR bazz foo baz", "GET bazz"},
+// expected: []interface{}{"OK", "OK", float64(2), "50"},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP OR of keys containing strings and a bytearray and get the destkey",
+// cmds: []string{"MSET foo foobar baz abcdef", "SETBIT bazz 8 1", "BITOP OR bazzz foo baz bazz", "GET bazzz", "SETBIT bazz 8 0", "SETBIT bazz 49 1", "BITOP OR bazzz foo baz bazz", "GET bazzz"},
+// expected: []interface{}{"OK", float64(0), float64(6), "g\xefofev", float64(1), float64(0), float64(7), "goofev@"},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP XOR of keys containing strings and get the destkey",
+// cmds: []string{"MSET foo foobar baz abcdef", "BITOP XOR bazz foo baz", "GET bazz"},
+// expected: []interface{}{"OK", float64(6), "\x07\x0d\x0c\x06\x04\x14"},
+// assertType: []string{"equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP XOR of keys containing strings and a bytearray and get the destkey",
+// cmds: []string{"MSET foo foobar baz abcdef", "SETBIT bazz 8 1", "BITOP XOR bazzz foo baz bazz", "GET bazzz", "SETBIT bazz 8 0", "SETBIT bazz 49 1", "BITOP XOR bazzz foo baz bazz", "GET bazzz", "Setbit bazz 49 0", "bitop xor bazzz foo baz bazz", "get bazzz"},
+// expected: []interface{}{"OK", float64(0), float64(6), "\x07\x8d\x0c\x06\x04\x14", float64(1), float64(0), float64(7), "\x07\r\x0c\x06\x04\x14@", float64(1), float64(7), "\x07\r\x0c\x06\x04\x14\x00"},
+// assertType: []string{"equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal", "equal"},
+// },
+// {
+// name: "BITOP XOR of keys containing integers and get the destkey",
+// cmds: []string{"SET foo 10", "SET baz 5", "BITOP XOR bazz foo baz", "GET bazz"},
+// expected: []interface{}{"OK", "OK", float64(2), "\x040"},
+// assertType: []string{"equal", "equal", "equal", "equal"},
+// },
+// }
+
+// for _, tc := range testCases {
+// t.Run(tc.name, func(t *testing.T) {
+// // Delete the key before running the test
+// _, _ = exec.FireCommandAndReadResponse(conn, "DEL foo")
+// _, _ = exec.FireCommandAndReadResponse(conn, "DEL baz")
+// _, _ = exec.FireCommandAndReadResponse(conn, "DEL bazz")
+// _, _ = exec.FireCommandAndReadResponse(conn, "DEL bazzz")
+// for i := 0; i < len(tc.cmds); i++ {
+// res, err := exec.FireCommandAndReadResponse(conn, tc.cmds[i])
+// assert.Nil(t, err)
+
+// switch tc.assertType[i] {
+// case "equal":
+// assert.Equal(t, tc.expected[i], res)
+// case "less":
+// assert.True(t, res.(float64) <= tc.expected[i].(float64), "CMD: %s Expected %d to be less than or equal to %d", tc.cmds[i], res, tc.expected[i])
+// }
+// }
+// })
+// }
+// }
+
+func TestBitCount(t *testing.T) {
+ exec := NewWebsocketCommandExecutor()
+ conn := exec.ConnectToServer()
+ testcases := []struct {
+ InCmds []string
+ Out []interface{}
+ }{
+ {
+ InCmds: []string{"SETBIT mykey 7 1"},
+ Out: []interface{}{float64(0)},
+ },
+ {
+ InCmds: []string{"SETBIT mykey 7 1"},
+ Out: []interface{}{float64(1)},
+ },
+ {
+ InCmds: []string{"SETBIT mykey 122 1"},
+ Out: []interface{}{float64(0)},
+ },
+ {
+ InCmds: []string{"GETBIT mykey 122"},
+ Out: []interface{}{float64(1)},
+ },
+ {
+ InCmds: []string{"SETBIT mykey 122 0"},
+ Out: []interface{}{float64(1)},
+ },
+ {
+ InCmds: []string{"GETBIT mykey 122"},
+ Out: []interface{}{float64(0)},
+ },
+ {
+ InCmds: []string{"GETBIT mykey 1223232"},
+ Out: []interface{}{float64(0)},
+ },
+ {
+ InCmds: []string{"GETBIT mykey 7"},
+ Out: []interface{}{float64(1)},
+ },
+ {
+ InCmds: []string{"GETBIT mykey 8"},
+ Out: []interface{}{float64(0)},
+ },
+ {
+ InCmds: []string{"BITCOUNT mykey 3 7 BIT"},
+ Out: []interface{}{float64(1)},
+ },
+ {
+ InCmds: []string{"BITCOUNT mykey 3 7"},
+ Out: []interface{}{float64(0)},
+ },
+ {
+ InCmds: []string{"BITCOUNT mykey 0 0"},
+ Out: []interface{}{float64(1)},
+ },
+ {
+ InCmds: []string{"BITCOUNT"},
+ Out: []interface{}{"ERR wrong number of arguments for 'bitcount' command"},
+ },
+ {
+ InCmds: []string{"BITCOUNT mykey"},
+ Out: []interface{}{float64(1)},
+ },
+ {
+ InCmds: []string{"BITCOUNT mykey 0"},
+ Out: []interface{}{"ERR syntax error"},
+ },
+ }
+
+ for _, tcase := range testcases {
+ for i := 0; i < len(tcase.InCmds); i++ {
+ cmd := tcase.InCmds[i]
+ out := tcase.Out[i]
+ res, err := exec.FireCommandAndReadResponse(conn, cmd)
+ assert.Nil(t, err)
+ assert.Equal(t, out, res, "Value mismatch for cmd %s\n.", cmd)
+ }
+ }
+}
+
+func TestBitPos(t *testing.T) {
+ exec := NewWebsocketCommandExecutor()
+ conn := exec.ConnectToServer()
+ testcases := []struct {
+ name string
+ val interface{}
+ inCmd string
+ out interface{}
+ setCmdSETBIT bool
+ }{
+ {
+ name: "String interval BIT 0,-1 ",
+ val: "\\x00\\xff\\x00",
+ inCmd: "BITPOS testkey 0 0 -1 bit",
+ out: float64(0),
+ },
+ {
+ name: "String interval BIT 8,-1",
+ val: "\\x00\\xff\\x00",
+ inCmd: "BITPOS testkey 0 8 -1 bit",
+ out: float64(8),
+ },
+ {
+ name: "String interval BIT 16,-1",
+ val: "\\x00\\xff\\x00",
+ inCmd: "BITPOS testkey 0 16 -1 bit",
+ out: float64(16),
+ },
+ {
+ name: "String interval BIT 16,200",
+ val: "\\x00\\xff\\x00",
+ inCmd: "BITPOS testkey 0 16 200 bit",
+ out: float64(16),
+ },
+ {
+ name: "String interval BIT 8,8",
+ val: "\\x00\\xff\\x00",
+ inCmd: "BITPOS testkey 0 8 8 bit",
+ out: float64(8),
+ },
+ {
+ name: "FindsFirstZeroBit",
+ val: "\xff\xf0\x00",
+ inCmd: "BITPOS testkey 0",
+ out: float64(12),
+ },
+ {
+ name: "FindsFirstOneBit",
+ val: "\x00\x0f\xff",
+ inCmd: "BITPOS testkey 1",
+ out: float64(12),
+ },
+ {
+ name: "NoOneBitFound",
+ val: "\x00\x00\x00",
+ inCmd: "BITPOS testkey 1",
+ out: float64(-1),
+ },
+ {
+ name: "NoZeroBitFound",
+ val: "\xff\xff\xff",
+ inCmd: "BITPOS testkey 0",
+ out: float64(24),
+ },
+ {
+ name: "NoZeroBitFoundWithRangeStartPos",
+ val: "\xff\xff\xff",
+ inCmd: "BITPOS testkey 0 2",
+ out: float64(24),
+ },
+ {
+ name: "NoZeroBitFoundWithOOBRangeStartPos",
+ val: "\xff\xff\xff",
+ inCmd: "BITPOS testkey 0 4",
+ out: float64(-1),
+ },
+ {
+ name: "NoZeroBitFoundWithRange",
+ val: "\xff\xff\xff",
+ inCmd: "BITPOS testkey 0 2 2",
+ out: float64(-1),
+ },
+ {
+ name: "NoZeroBitFoundWithRangeAndRangeType",
+ val: "\xff\xff\xff",
+ inCmd: "BITPOS testkey 0 2 2 BIT",
+ out: float64(-1),
+ },
+ {
+ name: "FindsFirstZeroBitInRange",
+ val: "\xff\xf0\xff",
+ inCmd: "BITPOS testkey 0 1 2",
+ out: float64(12),
+ },
+ {
+ name: "FindsFirstOneBitInRange",
+ val: "\x00\x00\xf0",
+ inCmd: "BITPOS testkey 1 2 3",
+ out: float64(16),
+ },
+ {
+ name: "StartGreaterThanEnd",
+ val: "\xff\xf0\x00",
+ inCmd: "BITPOS testkey 0 3 2",
+ out: float64(-1),
+ },
+ {
+ name: "FindsFirstOneBitWithNegativeStart",
+ val: "\x00\x00\xf0",
+ inCmd: "BITPOS testkey 1 -2 -1",
+ out: float64(16),
+ },
+ {
+ name: "FindsFirstZeroBitWithNegativeEnd",
+ val: "\xff\xf0\xff",
+ inCmd: "BITPOS testkey 0 1 -1",
+ out: float64(12),
+ },
+ {
+ name: "FindsFirstZeroBitInByteRange",
+ val: "\xff\x00\xff",
+ inCmd: "BITPOS testkey 0 1 2 BYTE",
+ out: float64(8),
+ },
+ {
+ name: "FindsFirstOneBitInBitRange",
+ val: "\x00\x01\x00",
+ inCmd: "BITPOS testkey 1 0 16 BIT",
+ out: float64(15),
+ },
+ {
+ name: "NoBitFoundInByteRange",
+ val: "\xff\xff\xff",
+ inCmd: "BITPOS testkey 0 0 2 BYTE",
+ out: float64(-1),
+ },
+ {
+ name: "NoBitFoundInBitRange",
+ val: "\x00\x00\x00",
+ inCmd: "BITPOS testkey 1 0 23 BIT",
+ out: float64(-1),
+ },
+ {
+ name: "EmptyStringReturnsMinusOneForZeroBit",
+ val: "\"\"",
+ inCmd: "BITPOS testkey 0",
+ out: float64(-1),
+ },
+ {
+ name: "EmptyStringReturnsMinusOneForOneBit",
+ val: "\"\"",
+ inCmd: "BITPOS testkey 1",
+ out: float64(-1),
+ },
+ {
+ name: "SingleByteString",
+ val: "\x80",
+ inCmd: "BITPOS testkey 1",
+ out: float64(0),
+ },
+ {
+ name: "RangeExceedsStringLength",
+ val: "\x00\xff",
+ inCmd: "BITPOS testkey 1 0 20 BIT",
+ out: float64(8),
+ },
+ {
+ name: "InvalidBitArgument",
+ inCmd: "BITPOS testkey 2",
+ out: "ERR the bit argument must be 1 or 0",
+ },
+ {
+ name: "NonIntegerStartParameter",
+ inCmd: "BITPOS testkey 0 start",
+ out: "ERR value is not an integer or out of range",
+ },
+ {
+ name: "NonIntegerEndParameter",
+ inCmd: "BITPOS testkey 0 1 end",
+ out: "ERR value is not an integer or out of range",
+ },
+ {
+ name: "InvalidRangeType",
+ inCmd: "BITPOS testkey 0 1 2 BYTEs",
+ out: "ERR syntax error",
+ },
+ {
+ name: "InsufficientArguments",
+ inCmd: "BITPOS testkey",
+ out: "ERR wrong number of arguments for 'bitpos' command",
+ },
+ {
+ name: "NonExistentKeyForZeroBit",
+ inCmd: "BITPOS nonexistentkey 0",
+ out: float64(0),
+ },
+ {
+ name: "NonExistentKeyForOneBit",
+ inCmd: "BITPOS nonexistentkey 1",
+ out: float64(-1),
+ },
+ {
+ name: "IntegerValue",
+ val: 65280, // 0xFF00 in decimal
+ inCmd: "BITPOS testkey 0",
+ out: float64(0),
+ },
+ {
+ name: "LargeIntegerValue",
+ val: 16777215, // 0xFFFFFF in decimal
+ inCmd: "BITPOS testkey 1",
+ out: float64(2),
+ },
+ {
+ name: "SmallIntegerValue",
+ val: 1, // 0x01 in decimal
+ inCmd: "BITPOS testkey 0",
+ out: float64(0),
+ },
+ {
+ name: "ZeroIntegerValue",
+ val: 0,
+ inCmd: "BITPOS testkey 1",
+ out: float64(2),
+ },
+ {
+ name: "BitRangeStartGreaterThanBitLength",
+ val: "\xff\xff\xff",
+ inCmd: "BITPOS testkey 0 25 30 BIT",
+ out: float64(-1),
+ },
+ {
+ name: "BitRangeEndExceedsBitLength",
+ val: "\xff\xff\xff",
+ inCmd: "BITPOS testkey 0 0 30 BIT",
+ out: float64(-1),
+ },
+ {
+ name: "NegativeStartInBitRange",
+ val: "\x00\xff\xff",
+ inCmd: "BITPOS testkey 1 -16 -1 BIT",
+ out: float64(8),
+ },
+ {
+ name: "LargeNegativeStart",
+ val: "\x00\xff\xff",
+ inCmd: "BITPOS testkey 1 -100 -1",
+ out: float64(8),
+ },
+ {
+ name: "LargePositiveEnd",
+ val: "\x00\xff\xff",
+ inCmd: "BITPOS testkey 1 0 100",
+ out: float64(8),
+ },
+ {
+ name: "StartAndEndEqualInByteRange",
+ val: "\x0f\xff\xff",
+ inCmd: "BITPOS testkey 0 1 1 BYTE",
+ out: float64(-1),
+ },
+ {
+ name: "StartAndEndEqualInBitRange",
+ val: "\x0f\xff\xff",
+ inCmd: "BITPOS testkey 1 1 1 BIT",
+ out: float64(-1),
+ },
+ {
+ name: "FindFirstZeroBitInNegativeRange",
+ val: "\xff\x00\xff",
+ inCmd: "BITPOS testkey 0 -2 -1",
+ out: float64(8),
+ },
+ {
+ name: "FindFirstOneBitInNegativeRangeBIT",
+ val: "\x00\x00\x80",
+ inCmd: "BITPOS testkey 1 -8 -1 BIT",
+ out: float64(16),
+ },
+ {
+ name: "MaxIntegerValue",
+ val: math.MaxInt64,
+ inCmd: "BITPOS testkey 0",
+ out: float64(0),
+ },
+ {
+ name: "MinIntegerValue",
+ val: math.MinInt64,
+ inCmd: "BITPOS testkey 1",
+ out: float64(2),
+ },
+ {
+ name: "SingleBitStringZero",
+ val: "\x00",
+ inCmd: "BITPOS testkey 1",
+ out: float64(-1),
+ },
+ {
+ name: "SingleBitStringOne",
+ val: "\x01",
+ inCmd: "BITPOS testkey 0",
+ out: float64(0),
+ },
+ {
+ name: "AllBitsSetExceptLast",
+ val: "\xff\xff\xfe",
+ inCmd: "BITPOS testkey 0",
+ out: float64(23),
+ },
+ {
+ name: "OnlyLastBitSet",
+ val: "\x00\x00\x01",
+ inCmd: "BITPOS testkey 1",
+ out: float64(23),
+ },
+ {
+ name: "AlternatingBitsLongString",
+ val: "\xaa\xaa\xaa\xaa\xaa",
+ inCmd: "BITPOS testkey 0",
+ out: float64(1),
+ },
+ {
+ name: "VeryLargeByteString",
+ val: strings.Repeat("\xff", 1000) + "\x00",
+ inCmd: "BITPOS testkey 0",
+ out: float64(8000),
+ },
+ {
+ name: "FindZeroBitOnSetBitKey",
+ val: "8 1",
+ inCmd: "BITPOS testkeysb 1",
+ out: float64(8),
+ setCmdSETBIT: true,
+ },
+ {
+ name: "FindOneBitOnSetBitKey",
+ val: "1 1",
+ inCmd: "BITPOS testkeysb 1",
+ out: float64(1),
+ setCmdSETBIT: true,
+ },
+ }
+
+ for _, tc := range testcases {
+ t.Run(tc.name, func(t *testing.T) {
+ var setCmd string
+ if tc.setCmdSETBIT {
+ setCmd = fmt.Sprintf("SETBIT testkeysb %s", tc.val.(string))
+ } else {
+ switch v := tc.val.(type) {
+ case string:
+ setCmd = fmt.Sprintf("SET testkey %s", v)
+ case int:
+ setCmd = fmt.Sprintf("SET testkey %d", v)
+ default:
+ // For test cases where we don't set a value (e.g., error cases)
+ setCmd = ""
+ }
+ }
+
+ if setCmd != "" {
+ _, _ = exec.FireCommandAndReadResponse(conn, setCmd)
+ }
+
+ result, err := exec.FireCommandAndReadResponse(conn, tc.inCmd)
+ assert.Nil(t, err)
+ assert.Equal(t, tc.out, result, "Mismatch for cmd %s\n", tc.inCmd)
+ })
+ }
+}
+
+func TestBitfield(t *testing.T) {
+ exec := NewWebsocketCommandExecutor()
+ conn := exec.ConnectToServer()
+ defer conn.Close()
+
+ _, _ = exec.FireCommandAndReadResponse(conn, "FLUSHDB")
+ defer exec.FireCommand(conn, "FLUSHDB") // clean up after all test cases
+ syntaxErrMsg := "ERR syntax error"
+ bitFieldTypeErrMsg := "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is"
+ integerErrMsg := "ERR value is not an integer or out of range"
+ overflowErrMsg := "ERR Invalid OVERFLOW type specified"
+
+ testCases := []struct {
+ Name string
+ Commands []string
+ Expected []interface{}
+ Delay []time.Duration
+ CleanUp []string
+ }{
+ {
+ Name: "BITFIELD Arity Check",
+ Commands: []string{"bitfield"},
+ Expected: []interface{}{"ERR wrong number of arguments for 'bitfield' command"},
+ Delay: []time.Duration{0},
+ CleanUp: []string{},
+ },
+ {
+ Name: "BITFIELD on unsupported type of SET",
+ Commands: []string{"SADD bits a b c", "bitfield bits"},
+ Expected: []interface{}{float64(3), "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD on unsupported type of JSON",
+ Commands: []string{"json.set bits $ 1", "bitfield bits"},
+ Expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD on unsupported type of HSET",
+ Commands: []string{"HSET bits a 1", "bitfield bits"},
+ Expected: []interface{}{float64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD with syntax errors",
+ Commands: []string{
+ "bitfield bits set u8 0 255 incrby u8 0 100 get u8",
+ "bitfield bits set a8 0 255 incrby u8 0 100 get u8",
+ "bitfield bits set u8 a 255 incrby u8 0 100 get u8",
+ "bitfield bits set u8 0 255 incrby u8 0 100 overflow wraap",
+ "bitfield bits set u8 0 incrby u8 0 100 get u8 288",
+ },
+ Expected: []interface{}{
+ syntaxErrMsg,
+ bitFieldTypeErrMsg,
+ "ERR bit offset is not an integer or out of range",
+ overflowErrMsg,
+ integerErrMsg,
+ },
+ Delay: []time.Duration{0, 0, 0, 0, 0},
+ CleanUp: []string{"Del bits"},
+ },
+ {
+ Name: "BITFIELD signed SET and GET basics",
+ Commands: []string{"bitfield bits set i8 0 -100", "bitfield bits set i8 0 101", "bitfield bits get i8 0"},
+ Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(-100)}, []interface{}{float64(101)}},
+ Delay: []time.Duration{0, 0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD unsigned SET and GET basics",
+ Commands: []string{"bitfield bits set u8 0 255", "bitfield bits set u8 0 100", "bitfield bits get u8 0"},
+ Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(255)}, []interface{}{float64(100)}},
+ Delay: []time.Duration{0, 0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD signed SET and GET together",
+ Commands: []string{"bitfield bits set i8 0 255 set i8 0 100 get i8 0"},
+ Expected: []interface{}{[]interface{}{float64(0), float64(-1), float64(100)}},
+ Delay: []time.Duration{0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD unsigned with SET, GET and INCRBY arguments",
+ Commands: []string{"bitfield bits set u8 0 255 incrby u8 0 100 get u8 0"},
+ Expected: []interface{}{[]interface{}{float64(0), float64(99), float64(99)}},
+ Delay: []time.Duration{0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD with only key as argument",
+ Commands: []string{"bitfield bits"},
+ Expected: []interface{}{[]interface{}{}},
+ Delay: []time.Duration{0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD # form",
+ Commands: []string{
+ "bitfield bits set u8 #0 65",
+ "bitfield bits set u8 #1 66",
+ "bitfield bits set u8 #2 67",
+ "get bits",
+ },
+ Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(0)}, []interface{}{float64(0)}, "ABC"},
+ Delay: []time.Duration{0, 0, 0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD basic INCRBY form",
+ Commands: []string{
+ "bitfield bits set u8 #0 10",
+ "bitfield bits incrby u8 #0 100",
+ "bitfield bits incrby u8 #0 100",
+ },
+ Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(110)}, []interface{}{float64(210)}},
+ Delay: []time.Duration{0, 0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD chaining of multiple commands",
+ Commands: []string{
+ "bitfield bits set u8 #0 10",
+ "bitfield bits incrby u8 #0 100 incrby u8 #0 100",
+ },
+ Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(110), float64(210)}},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD unsigned overflow wrap",
+ Commands: []string{
+ "bitfield bits set u8 #0 100",
+ "bitfield bits overflow wrap incrby u8 #0 257",
+ "bitfield bits get u8 #0",
+ "bitfield bits overflow wrap incrby u8 #0 255",
+ "bitfield bits get u8 #0",
+ },
+ Expected: []interface{}{
+ []interface{}{float64(0)},
+ []interface{}{float64(101)},
+ []interface{}{float64(101)},
+ []interface{}{float64(100)},
+ []interface{}{float64(100)},
+ },
+ Delay: []time.Duration{0, 0, 0, 0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD unsigned overflow sat",
+ Commands: []string{
+ "bitfield bits set u8 #0 100",
+ "bitfield bits overflow sat incrby u8 #0 257",
+ "bitfield bits get u8 #0",
+ "bitfield bits overflow sat incrby u8 #0 -255",
+ "bitfield bits get u8 #0",
+ },
+ Expected: []interface{}{
+ []interface{}{float64(0)},
+ []interface{}{float64(255)},
+ []interface{}{float64(255)},
+ []interface{}{float64(0)},
+ []interface{}{float64(0)},
+ },
+ Delay: []time.Duration{0, 0, 0, 0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD signed overflow wrap",
+ Commands: []string{
+ "bitfield bits set i8 #0 100",
+ "bitfield bits overflow wrap incrby i8 #0 257",
+ "bitfield bits get i8 #0",
+ "bitfield bits overflow wrap incrby i8 #0 255",
+ "bitfield bits get i8 #0",
+ },
+ Expected: []interface{}{
+ []interface{}{float64(0)},
+ []interface{}{float64(101)},
+ []interface{}{float64(101)},
+ []interface{}{float64(100)},
+ []interface{}{float64(100)},
+ },
+ Delay: []time.Duration{0, 0, 0, 0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD signed overflow sat",
+ Commands: []string{
+ "bitfield bits set u8 #0 100",
+ "bitfield bits overflow sat incrby i8 #0 257",
+ "bitfield bits get i8 #0",
+ "bitfield bits overflow sat incrby i8 #0 -255",
+ "bitfield bits get i8 #0",
+ },
+ Expected: []interface{}{
+ []interface{}{float64(0)},
+ []interface{}{float64(127)},
+ []interface{}{float64(127)},
+ []interface{}{float64(-128)},
+ []interface{}{float64(-128)},
+ },
+ Delay: []time.Duration{0, 0, 0, 0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD regression 1",
+ Commands: []string{"set bits 1", "bitfield bits get u1 0"},
+ Expected: []interface{}{"OK", []interface{}{float64(0)}},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD regression 2",
+ Commands: []string{
+ "bitfield mystring set i8 0 10",
+ "bitfield mystring set i8 64 10",
+ "bitfield mystring incrby i8 10 99900",
+ },
+ Expected: []interface{}{[]interface{}{float64(0)}, []interface{}{float64(0)}, []interface{}{float64(60)}},
+ Delay: []time.Duration{0, 0, 0},
+ CleanUp: []string{"DEL mystring"},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.Name, func(t *testing.T) {
+
+ for i := 0; i < len(tc.Commands); i++ {
+ if tc.Delay[i] > 0 {
+ time.Sleep(tc.Delay[i])
+ }
+ result, err := exec.FireCommandAndReadResponse(conn, tc.Commands[i])
+ assert.Nil(t, err)
+ expected := tc.Expected[i]
+ assert.Equal(t, expected, result)
+ }
+
+ for _, cmd := range tc.CleanUp {
+ _, _ = exec.FireCommandAndReadResponse(conn, cmd)
+ }
+ })
+ }
+}
+
+func TestBitfieldRO(t *testing.T) {
+ exec := NewWebsocketCommandExecutor()
+ conn := exec.ConnectToServer()
+ defer conn.Close()
+
+ _, _ = exec.FireCommandAndReadResponse(conn, "FLUSHDB")
+ defer exec.FireCommand(conn, "FLUSHDB")
+
+ syntaxErrMsg := "ERR syntax error"
+ bitFieldTypeErrMsg := "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is"
+ unsupportedCmdErrMsg := "ERR BITFIELD_RO only supports the GET subcommand"
+
+ testCases := []struct {
+ Name string
+ Commands []string
+ Expected []interface{}
+ Delay []time.Duration
+ CleanUp []string
+ }{
+ {
+ Name: "BITFIELD_RO Arity Check",
+ Commands: []string{"bitfield_ro"},
+ Expected: []interface{}{"ERR wrong number of arguments for 'bitfield_ro' command"},
+ Delay: []time.Duration{0},
+ CleanUp: []string{},
+ },
+ {
+ Name: "BITFIELD_RO on unsupported type of SET",
+ Commands: []string{"SADD bits a b c", "bitfield_ro bits"},
+ Expected: []interface{}{float64(3), "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD_RO on unsupported type of JSON",
+ Commands: []string{"json.set bits $ 1", "bitfield_ro bits"},
+ Expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD_RO on unsupported type of HSET",
+ Commands: []string{"HSET bits a 1", "bitfield_ro bits"},
+ Expected: []interface{}{float64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"DEL bits"},
+ },
+ {
+ Name: "BITFIELD_RO with unsupported commands",
+ Commands: []string{
+ "bitfield_ro bits set u8 0 255",
+ "bitfield_ro bits incrby u8 0 100",
+ },
+ Expected: []interface{}{
+ unsupportedCmdErrMsg,
+ unsupportedCmdErrMsg,
+ },
+ Delay: []time.Duration{0, 0},
+ CleanUp: []string{"Del bits"},
+ },
+ {
+ Name: "BITFIELD_RO with syntax error",
+ Commands: []string{
+ "set bits 1",
+ "bitfield_ro bits get u8",
+ "bitfield_ro bits get",
+ "bitfield_ro bits get somethingrandom",
+ },
+ Expected: []interface{}{
+ "OK",
+ syntaxErrMsg,
+ syntaxErrMsg,
+ syntaxErrMsg,
+ },
+ Delay: []time.Duration{0, 0, 0, 0},
+ CleanUp: []string{"Del bits"},
+ },
+ {
+ Name: "BITFIELD_RO with invalid bitfield type",
+ Commands: []string{
+ "set bits 1",
+ "bitfield_ro bits get a8 0",
+ "bitfield_ro bits get s8 0",
+ "bitfield_ro bits get somethingrandom 0",
+ },
+ Expected: []interface{}{
+ "OK",
+ bitFieldTypeErrMsg,
+ bitFieldTypeErrMsg,
+ bitFieldTypeErrMsg,
+ },
+ Delay: []time.Duration{0, 0, 0, 0},
+ CleanUp: []string{"Del bits"},
+ },
+ {
+ Name: "BITFIELD_RO with only key as argument",
+ Commands: []string{"bitfield_ro bits"},
+ Expected: []interface{}{[]interface{}{}},
+ Delay: []time.Duration{0},
+ CleanUp: []string{"DEL bits"},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.Name, func(t *testing.T) {
+
+ for i := 0; i < len(tc.Commands); i++ {
+ if tc.Delay[i] > 0 {
+ time.Sleep(tc.Delay[i])
+ }
+ result, err := exec.FireCommandAndReadResponse(conn, tc.Commands[i])
+ assert.Nil(t, err)
+ expected := tc.Expected[i]
+ assert.Equal(t, expected, result)
+ }
+
+ for _, cmd := range tc.CleanUp {
+ _, _ = exec.FireCommandAndReadResponse(conn, cmd)
+ }
+ })
+ }
+}
diff --git a/integration_tests/commands/websocket/bloom_test.go b/integration_tests/commands/websocket/bloom_test.go
index 44c28cb08..7198505df 100644
--- a/integration_tests/commands/websocket/bloom_test.go
+++ b/integration_tests/commands/websocket/bloom_test.go
@@ -211,5 +211,7 @@ func TestBFEdgeCasesAndErrors(t *testing.T) {
exec.FireCommand(conn, cmd)
}
})
+ conn := exec.ConnectToServer()
+ exec.FireCommandAndReadResponse(conn, "FLUSHDB")
}
}
diff --git a/integration_tests/commands/websocket/deque_test.go b/integration_tests/commands/websocket/deque_test.go
new file mode 100644
index 000000000..9f28391db
--- /dev/null
+++ b/integration_tests/commands/websocket/deque_test.go
@@ -0,0 +1,58 @@
+package websocket
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestLPOPCount(t *testing.T) {
+ exec := NewWebsocketCommandExecutor()
+
+ testCases := []struct {
+ name string
+ commands []string
+ expected []interface{}
+ cleanupKey string
+ }{
+ {
+ name: "LPOP with count argument - valid, invalid, and edge cases",
+ commands: []string{
+ "RPUSH k v1",
+ "RPUSH k v2",
+ "RPUSH k v3",
+ "RPUSH k v4",
+ "LPOP k 2",
+ "LPOP k 2",
+ "LPOP k -1",
+ "LPOP k abc",
+ "LLEN k",
+ },
+ expected: []any{
+ float64(1),
+ float64(2),
+ float64(3),
+ float64(4),
+ []interface{}{"v1", "v2"},
+ []interface{}{"v3", "v4"},
+ "ERR value is out of range",
+ "ERR value is not an integer or out of range",
+ float64(0),
+ },
+ cleanupKey: "k",
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ conn := exec.ConnectToServer()
+
+ for i, cmd := range tc.commands {
+ result, err := exec.FireCommandAndReadResponse(conn, cmd)
+ assert.Nil(t, err)
+ assert.Equal(t, tc.expected[i], result)
+ }
+ DeleteKey(t, conn, exec, tc.cleanupKey)
+ })
+ }
+}
+
diff --git a/integration_tests/commands/websocket/hdel_test.go b/integration_tests/commands/websocket/hdel_test.go
new file mode 100644
index 000000000..92ca4e481
--- /dev/null
+++ b/integration_tests/commands/websocket/hdel_test.go
@@ -0,0 +1,101 @@
+package websocket
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestHDEL(t *testing.T) {
+ exec := NewWebsocketCommandExecutor()
+ conn := exec.ConnectToServer()
+ DeleteKey(t, conn, exec, "key_hDel1")
+ DeleteKey(t, conn, exec, "key_hDel2")
+ DeleteKey(t, conn, exec, "key_hDel3")
+ DeleteKey(t, conn, exec, "key_hDel4")
+ DeleteKey(t, conn, exec, "key_hDel5")
+ DeleteKey(t, conn, exec, "string_key")
+
+ testCases := []struct {
+ name string
+ cmds []string
+ expect []interface{}
+ delays []time.Duration
+ }{
+ {
+ name: "HDEL with wrong number of arguments",
+ cmds: []string{
+ "HDEL",
+ "HDEL key_hDel1",
+ },
+ expect: []interface{}{
+ "ERR wrong number of arguments for 'hdel' command",
+ "ERR wrong number of arguments for 'hdel' command"},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HDEL with single field",
+ cmds: []string{
+ "HSET key_hDel2 field1 value1",
+ "HLEN key_hDel2",
+ "HDEL key_hDel2 field1",
+ "HLEN key_hDel2",
+ },
+ expect: []interface{}{float64(1), float64(1), float64(1), float64(0)},
+ delays: []time.Duration{0, 0, 0, 0},
+ },
+ {
+ name: "HDEL with multiple fields",
+ cmds: []string{
+ "HSET key_hDel3 field1 value1 field2 value2 field3 value3 field4 value4",
+ "HLEN key_hDel3",
+ "HDEL key_hDel3 field1 field2",
+ "HLEN key_hDel3",
+ },
+ expect: []interface{}{float64(4), float64(4), float64(2), float64(2)},
+ delays: []time.Duration{0, 0, 0, 0},
+ },
+ {
+ name: "HDEL on non-existent field",
+ cmds: []string{
+ "HSET key_hDel4 field1 value1 field2 value2",
+ "HDEL key_hDel4 field3",
+ },
+ expect: []interface{}{float64(2), float64(0)},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HDEL on non-existent hash",
+ cmds: []string{
+ "HSET key_hDel5 field1 value1 field2 value2",
+ "HDEL wrong_key_hDel5 field1",
+ },
+ expect: []interface{}{float64(2), float64(0)},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HDEL with wrong type",
+ cmds: []string{
+ "SET string_key value",
+ "HDEL string_key field",
+ },
+ expect: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ delays: []time.Duration{0, 0},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+
+ for i, cmd := range tc.cmds {
+ if tc.delays[i] > 0 {
+ time.Sleep(tc.delays[i])
+ }
+ result, err := exec.FireCommandAndReadResponse(conn, cmd)
+ assert.Nil(t, err)
+ assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd)
+ }
+ })
+ }
+}
diff --git a/integration_tests/commands/websocket/hget_test.go b/integration_tests/commands/websocket/hget_test.go
new file mode 100644
index 000000000..36aeb2feb
--- /dev/null
+++ b/integration_tests/commands/websocket/hget_test.go
@@ -0,0 +1,90 @@
+package websocket
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestHGET(t *testing.T) {
+ exec := NewWebsocketCommandExecutor()
+ conn := exec.ConnectToServer()
+ DeleteKey(t, conn, exec, "key_hGet1")
+ DeleteKey(t, conn, exec, "key_hGet2")
+ DeleteKey(t, conn, exec, "key_hGet3")
+ DeleteKey(t, conn, exec, "key_hGet4")
+ DeleteKey(t, conn, exec, "string_key")
+
+ testCases := []struct {
+ name string
+ cmds []string
+ expect []interface{}
+ delays []time.Duration
+ }{
+ {
+ name: "HGET with wrong number of arguments",
+ cmds: []string{
+ "HGET",
+ "HGET key_hGet1",
+ },
+ expect: []interface{}{
+ "ERR wrong number of arguments for 'hget' command",
+ "ERR wrong number of arguments for 'hget' command"},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HGET on existent hash",
+ cmds: []string{
+ "HSET key_hGet2 field1 value1 field2 value2 field3 value3",
+ "HGET key_hGet2 field2",
+ },
+ expect: []interface{}{float64(3), "value2"},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HGET on non-existent field",
+ cmds: []string{
+ "HSET key_hGet3 field1 value1 field2 value2",
+ "HGET key_hGet3 field2",
+ "HDEL key_hGet3 field2",
+ "HGET key_hGet3 field2",
+ "HGET key_hGet3 field3",
+ },
+ expect: []interface{}{float64(2), "value2", float64(1), nil, nil},
+ delays: []time.Duration{0, 0, 0, 0, 0},
+ },
+ {
+ name: "HGET on non-existent hash",
+ cmds: []string{
+ "HSET key_hGet4 field1 value1 field2 value2",
+ "HGET wrong_key_hGet4 field2",
+ },
+ expect: []interface{}{float64(2), nil},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HGET with wrong type",
+ cmds: []string{
+ "SET string_key value",
+ "HGET string_key field",
+ },
+ expect: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ delays: []time.Duration{0, 0},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+
+ for i, cmd := range tc.cmds {
+ if tc.delays[i] > 0 {
+ time.Sleep(tc.delays[i])
+ }
+ result, err := exec.FireCommandAndReadResponse(conn, cmd)
+ assert.Nil(t, err)
+ assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd)
+ }
+ })
+ }
+}
diff --git a/integration_tests/commands/websocket/hset_test.go b/integration_tests/commands/websocket/hset_test.go
new file mode 100644
index 000000000..bc2764de2
--- /dev/null
+++ b/integration_tests/commands/websocket/hset_test.go
@@ -0,0 +1,89 @@
+package websocket
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestHSET(t *testing.T) {
+ exec := NewWebsocketCommandExecutor()
+ conn := exec.ConnectToServer()
+ DeleteKey(t, conn, exec, "key_hSet1")
+ DeleteKey(t, conn, exec, "key_hSet2")
+ DeleteKey(t, conn, exec, "key_hSet3")
+ DeleteKey(t, conn, exec, "key_hSet4")
+ DeleteKey(t, conn, exec, "string_key")
+
+ testCases := []struct {
+ name string
+ cmds []string
+ expect []interface{}
+ delays []time.Duration
+ }{
+ {
+ name: "HSET with wrong number of arguments",
+ cmds: []string{
+ "HSET",
+ "HSET key_hSet1",
+ },
+ expect: []interface{}{
+ "ERR wrong number of arguments for 'hset' command",
+ "ERR wrong number of arguments for 'hset' command"},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HSET with single field",
+ cmds: []string{
+ "HSET key_hSet2 field1 value1",
+ "HLEN key_hSet2",
+ },
+ expect: []interface{}{float64(1), float64(1)},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HSET with multiple fields",
+ cmds: []string{
+ "HSET key_hSet3 field1 value1 field2 value2 field3 value3",
+ "HLEN key_hSet3",
+ },
+ expect: []interface{}{float64(3), float64(3)},
+ delays: []time.Duration{0, 0},
+ },
+ {
+ name: "HSET on existing hash",
+ cmds: []string{
+ "HSET key_hSet4 field1 value1 field2 value2",
+ "HGET key_hSet4 field2",
+ "HSET key_hSet4 field2 newvalue2",
+ "HGET key_hSet4 field2",
+ },
+ expect: []interface{}{float64(2), "value2", float64(0), "newvalue2"},
+ delays: []time.Duration{0, 0, 0, 0},
+ },
+ {
+ name: "HSET with wrong type",
+ cmds: []string{
+ "SET string_key value",
+ "HSET string_key field value",
+ },
+ expect: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ delays: []time.Duration{0, 0},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+
+ for i, cmd := range tc.cmds {
+ if tc.delays[i] > 0 {
+ time.Sleep(tc.delays[i])
+ }
+ result, err := exec.FireCommandAndReadResponse(conn, cmd)
+ assert.Nil(t, err)
+ assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd)
+ }
+ })
+ }
+}
diff --git a/integration_tests/commands/websocket/set_test.go b/integration_tests/commands/websocket/set_test.go
index 94480ebde..8dac72f15 100644
--- a/integration_tests/commands/websocket/set_test.go
+++ b/integration_tests/commands/websocket/set_test.go
@@ -121,6 +121,21 @@ func TestSetWithOptions(t *testing.T) {
commands: []string{"SET k v XX EX 1", "GET k", "SLEEP 2", "GET k", "SET k v XX EX 1", "GET k"},
expected: []interface{}{nil, nil, "OK", nil, nil, nil},
},
+ {
+ name: "GET with Existing Value",
+ commands: []string{"SET k v", "SET k vv GET"},
+ expected: []interface{}{"OK", "v"},
+ },
+ {
+ name: "GET with Non-Existing Value",
+ commands: []string{"SET k vv GET"},
+ expected: []interface{}{nil},
+ },
+ {
+ name: "GET with wrong type of value",
+ commands: []string{"sadd k v", "SET k vv GET"},
+ expected: []interface{}{float64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"},
+ },
}
for _, tc := range testCases {
diff --git a/integration_tests/commands/websocket/setup.go b/integration_tests/commands/websocket/setup.go
index 339820896..81d56be0a 100644
--- a/integration_tests/commands/websocket/setup.go
+++ b/integration_tests/commands/websocket/setup.go
@@ -116,7 +116,7 @@ func RunWebsocketServer(ctx context.Context, wg *sync.WaitGroup, opt TestServerO
shardManager := shard.NewShardManager(1, watchChan, nil, globalErrChannel)
queryWatcherLocal := querymanager.NewQueryManager()
config.WebsocketPort = opt.Port
- testServer := server.NewWebSocketServer(shardManager, testPort1)
+ testServer := server.NewWebSocketServer(shardManager, testPort1, nil)
setupCtx, cancelSetupCtx := context.WithCancel(ctx)
// run shard manager
diff --git a/integration_tests/commands/websocket/zset_test.go b/integration_tests/commands/websocket/zset_test.go
index 37d15d1a1..e57cccdf9 100644
--- a/integration_tests/commands/websocket/zset_test.go
+++ b/integration_tests/commands/websocket/zset_test.go
@@ -22,43 +22,43 @@ func TestZPOPMIN(t *testing.T) {
},
{
name: "ZPOPMIN on existing key (without count argument)",
- commands: []string{"ZADD myzset 1 member1 2 member2 3 member3", "ZPOPMIN myzset"},
- expected: []interface{}{float64(3), []interface{}{"member1", "1"}},
+ commands: []string{"ZADD myzset 1 member1 2 member2 3 member3", "ZPOPMIN myzset", "ZCOUNT myzset 1 10"},
+ expected: []interface{}{float64(3), []interface{}{"member1", "1"}, float64(2)},
},
{
name: "ZPOPMIN with normal count argument",
- commands: []string{"ZADD myzset 1 member1 2 member2 3 member3", "ZPOPMIN myzset 2"},
- expected: []interface{}{float64(3), []interface{}{"member1", "1", "member2", "2"}},
+ commands: []string{"ZADD myzset 1 member1 2 member2 3 member3", "ZPOPMIN myzset 2", "ZCOUNT myzset 1 2"},
+ expected: []interface{}{float64(3), []interface{}{"member1", "1", "member2", "2"}, float64(0)},
},
{
name: "ZPOPMIN with count argument but multiple members have the same score",
- commands: []string{"ZADD myzset 1 member1 1 member2 1 member3", "ZPOPMIN myzset 2"},
- expected: []interface{}{float64(3), []interface{}{"member1", "1", "member2", "1"}},
+ commands: []string{"ZADD myzset 1 member1 1 member2 1 member3", "ZPOPMIN myzset 2", "ZCOUNT myzset 1 1"},
+ expected: []interface{}{float64(3), []interface{}{"member1", "1", "member2", "1"}, float64(1)},
},
{
name: "ZPOPMIN with negative count argument",
- commands: []string{"ZADD myzset 1 member1 2 member2 3 member3", "ZPOPMIN myzset -1"},
- expected: []interface{}{float64(3), []interface{}{}},
+ commands: []string{"ZADD myzset 1 member1 2 member2 3 member3", "ZPOPMIN myzset -1", "ZCOUNT myzset 0.6 3.231"},
+ expected: []interface{}{float64(3), []interface{}{}, float64(3)},
},
{
name: "ZPOPMIN with invalid count argument",
- commands: []string{"ZADD myzset 1 member1", "ZPOPMIN myzset INCORRECT_COUNT_ARGUMENT"},
- expected: []interface{}{float64(1), "ERR value is not an integer or out of range"},
+ commands: []string{"ZADD myzset 1 member1", "ZPOPMIN myzset INCORRECT_COUNT_ARGUMENT", "ZCOUNT myzset 1 10"},
+ expected: []interface{}{float64(1), "ERR value is not an integer or out of range", float64(1)},
},
{
name: "ZPOPMIN with count argument greater than length of sorted set",
- commands: []string{"ZADD myzset 1 member1 2 member2 3 member3", "ZPOPMIN myzset 10"},
- expected: []interface{}{float64(3), []interface{}{"member1", "1", "member2", "2", "member3", "3"}},
+ commands: []string{"ZADD myzset 1 member1 2 member2 3 member3", "ZPOPMIN myzset 10", "ZCOUNT myzset 1 10"},
+ expected: []interface{}{float64(3), []interface{}{"member1", "1", "member2", "2", "member3", "3"}, float64(0)},
},
{
name: "ZPOPMIN on empty sorted set",
- commands: []string{"ZADD myzset 1 member1", "ZPOPMIN myzset 1", "ZPOPMIN myzset"},
- expected: []interface{}{float64(1), []interface{}{"member1", "1"}, []interface{}{}},
+ commands: []string{"ZADD myzset 1 member1", "ZPOPMIN myzset 1", "ZPOPMIN myzset", "ZCOUNT myzset 0 10000"},
+ expected: []interface{}{float64(1), []interface{}{"member1", "1"}, []interface{}{}, float64(0)},
},
{
name: "ZPOPMIN with floating-point scores",
- commands: []string{"ZADD myzset 1.5 member1 2.7 member2 3.8 member3", "ZPOPMIN myzset"},
- expected: []interface{}{float64(3), []interface{}{"member1", "1.5"}},
+ commands: []string{"ZADD myzset 1.5 member1 2.7 member2 3.8 member3", "ZPOPMIN myzset", "ZCOUNT myzset 1.499 2.711"},
+ expected: []interface{}{float64(3), []interface{}{"member1", "1.5"}, float64(1)},
},
}
@@ -66,13 +66,12 @@ func TestZPOPMIN(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
conn := exec.ConnectToServer()
- DeleteKey(t, conn, exec, "myzset")
-
for i, cmd := range tc.commands {
result, err := exec.FireCommandAndReadResponse(conn, cmd)
assert.Nil(t, err)
assert.Equal(t, tc.expected[i], result)
}
+ DeleteKey(t, conn, exec, "myzset")
})
}
}
diff --git a/internal/clientio/resp.go b/internal/clientio/resp.go
index d908ba6c2..fdfc1dccb 100644
--- a/internal/clientio/resp.go
+++ b/internal/clientio/resp.go
@@ -54,15 +54,34 @@ func readLength(buf *bytes.Buffer) (int64, error) {
}
func readStringUntilSr(buf *bytes.Buffer) (string, error) {
- s, err := buf.ReadString('\r')
- if err != nil {
- return utils.EmptyStr, err
- }
- // incrementing to skip `\n`
- if _, err := buf.ReadByte(); err != nil {
- return utils.EmptyStr, err
+ var result []byte
+
+ for {
+ byteRead, err := buf.ReadByte()
+ if err != nil {
+ return utils.EmptyStr, err
+ }
+
+ result = append(result, byteRead)
+
+ // If we find '\r', we check the next byte for '\n'
+ if byteRead == '\r' {
+ nextByte, err := buf.ReadByte() // Peek the next byte
+ if err != nil {
+ return utils.EmptyStr, err
+ }
+
+ // If the next byte is '\n', we've found a valid end of string
+ if nextByte == '\n' {
+ break
+ }
+
+ // Otherwise, add the next byte to the result and continue
+ result = append(result, nextByte)
+ }
}
- return s[:len(s)-1], nil
+ // Return without the last '\r'
+ return string(result[:len(result)-1]), nil
}
// reads a RESP encoded simple string from data and returns
diff --git a/internal/clientio/resp_test.go b/internal/clientio/resp_test.go
index 97cbacd3e..96495e015 100644
--- a/internal/clientio/resp_test.go
+++ b/internal/clientio/resp_test.go
@@ -13,7 +13,9 @@ import (
func TestSimpleStringDecode(t *testing.T) {
cases := map[string]string{
- "+OK\r\n": "OK",
+ "+OK\r\n": "OK",
+ "+Hello\rWorld\r\n": "Hello\rWorld",
+ "+Hello\rWorld\rAgain\r\n": "Hello\rWorld\rAgain",
}
for k, v := range cases {
p := clientio.NewRESPParser(bytes.NewBuffer([]byte(k)))
@@ -25,6 +27,7 @@ func TestSimpleStringDecode(t *testing.T) {
if v != value {
t.Fail()
}
+ fmt.Println(v, value)
}
}
diff --git a/internal/cmd/cmds.go b/internal/cmd/cmds.go
index 68143983a..a561764be 100644
--- a/internal/cmd/cmds.go
+++ b/internal/cmd/cmds.go
@@ -17,9 +17,14 @@ type RedisCmds struct {
RequestID uint32
}
+// Repr returns a string representation of the command.
+func (cmd *DiceDBCmd) Repr() string {
+ return fmt.Sprintf("%s %s", cmd.Cmd, strings.Join(cmd.Args, " "))
+}
+
// GetFingerprint returns a 32-bit fingerprint of the command and its arguments.
func (cmd *DiceDBCmd) GetFingerprint() uint32 {
- return farm.Fingerprint32([]byte(fmt.Sprintf("%s-%s", cmd.Cmd, strings.Join(cmd.Args, " "))))
+ return farm.Fingerprint32([]byte(cmd.Repr()))
}
// GetKey Returns the key which the command operates on.
diff --git a/internal/eval/bitpos.go b/internal/eval/bitpos.go
index 43913d559..701a5ce74 100644
--- a/internal/eval/bitpos.go
+++ b/internal/eval/bitpos.go
@@ -10,9 +10,12 @@ import (
dstore "github.com/dicedb/dice/internal/store"
)
-func evalBITPOS(args []string, store *dstore.Store) []byte {
+func evalBITPOS(args []string, store *dstore.Store) *EvalResponse {
if len(args) < 2 || len(args) > 5 {
- return diceerrors.NewErrArity("BITPOS")
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongArgumentCount("BITPOS"),
+ }
}
key := args[0]
@@ -20,36 +23,54 @@ func evalBITPOS(args []string, store *dstore.Store) []byte {
bitToFind, err := parseBitToFind(args[1])
if err != nil {
- return diceerrors.NewErrWithMessage(err.Error())
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrGeneral(err.Error()),
+ }
}
if obj == nil {
if bitToFind == 0 {
- return clientio.Encode(0, true)
+ return &EvalResponse{
+ Result: clientio.IntegerZero,
+ Error: nil,
+ }
}
- return clientio.Encode(-1, true)
+ return &EvalResponse{
+ Result: clientio.IntegerNegativeOne,
+ Error: nil,
+ }
}
byteSlice, err := getValueAsByteSlice(obj)
if err != nil {
- return diceerrors.NewErrWithMessage(err.Error())
+ return &EvalResponse{
+ Result: nil,
+ Error: err,
+ }
}
start, end, rangeType, endRangeProvided, err := parseOptionalParams(args[2:], len(byteSlice))
if err != nil {
- return diceerrors.NewErrWithMessage(err.Error())
+ return &EvalResponse{
+ Result: nil,
+ Error: err,
+ }
}
result := getBitPos(byteSlice, bitToFind, start, end, rangeType, endRangeProvided)
- return clientio.Encode(result, true)
+ return &EvalResponse{
+ Result: result,
+ Error: nil,
+ }
}
func parseBitToFind(arg string) (byte, error) {
bitToFindInt, err := strconv.Atoi(arg)
if err != nil {
- return 0, errors.New("value is not an integer or out of range")
+ return 0, diceerrors.ErrIntegerOutOfRange
}
if bitToFindInt != 0 && bitToFindInt != 1 {
@@ -66,14 +87,14 @@ func parseOptionalParams(args []string, byteLen int) (start, end int, rangeType
if len(args) > 0 {
start, err = strconv.Atoi(args[0])
if err != nil {
- return 0, 0, "", false, errors.New("value is not an integer or out of range")
+ return 0, 0, "", false, diceerrors.ErrIntegerOutOfRange
}
}
if len(args) > 1 {
end, err = strconv.Atoi(args[1])
if err != nil {
- return 0, 0, "", false, errors.New("value is not an integer or out of range")
+ return 0, 0, "", false, diceerrors.ErrIntegerOutOfRange
}
endRangeProvided = true
}
@@ -81,7 +102,7 @@ func parseOptionalParams(args []string, byteLen int) (start, end int, rangeType
if len(args) > 2 {
rangeType = strings.ToUpper(args[2])
if rangeType != BYTE && rangeType != BIT {
- return 0, 0, "", false, errors.New("syntax error")
+ return 0, 0, "", false, diceerrors.ErrSyntax
}
}
return start, end, rangeType, endRangeProvided, err
@@ -152,7 +173,6 @@ func getBitPosWithBitRange(byteSlice []byte, bitToFind byte, start, end int) int
return i
}
}
-
// Bit not found in the range
return -1
}
diff --git a/internal/eval/commands.go b/internal/eval/commands.go
index ba877edc2..71fd7eaf7 100644
--- a/internal/eval/commands.go
+++ b/internal/eval/commands.go
@@ -537,20 +537,23 @@ var (
Arity: 1,
}
setBitCmdMeta = DiceCmdMeta{
- Name: "SETBIT",
- Info: "SETBIT sets or clears the bit at offset in the string value stored at key",
- Eval: evalSETBIT,
+ Name: "SETBIT",
+ Info: "SETBIT sets or clears the bit at offset in the string value stored at key",
+ IsMigrated: true,
+ NewEval: evalSETBIT,
}
getBitCmdMeta = DiceCmdMeta{
- Name: "GETBIT",
- Info: "GETBIT returns the bit value at offset in the string value stored at key",
- Eval: evalGETBIT,
+ Name: "GETBIT",
+ Info: "GETBIT returns the bit value at offset in the string value stored at key",
+ IsMigrated: true,
+ NewEval: evalGETBIT,
}
bitCountCmdMeta = DiceCmdMeta{
- Name: "BITCOUNT",
- Info: "BITCOUNT counts the number of set bits in the string value stored at key",
- Eval: evalBITCOUNT,
- Arity: -1,
+ Name: "BITCOUNT",
+ Info: "BITCOUNT counts the number of set bits in the string value stored at key",
+ Arity: -1,
+ IsMigrated: true,
+ NewEval: evalBITCOUNT,
}
bitOpCmdMeta = DiceCmdMeta{
Name: "BITOP",
@@ -912,8 +915,9 @@ var (
RESP encoded -1 in case the bit argument is 1 and the string is empty or composed of just zero bytes.
RESP encoded -1 if we look for set bits and the string is empty or composed of just zero bytes, -1 is returned.
RESP encoded -1 if a clear bit isn't found in the specified range.`,
- Eval: evalBITPOS,
- Arity: -2,
+ IsMigrated: true,
+ NewEval: evalBITPOS,
+ Arity: -2,
}
saddCmdMeta = DiceCmdMeta{
Name: "SADD",
@@ -922,17 +926,19 @@ var (
Specified members that are already a member of this set are ignored
Non existing keys are treated as empty sets.
An error is returned when the value stored at key is not a set.`,
- Eval: evalSADD,
- Arity: -3,
- KeySpecs: KeySpecs{BeginIndex: 1},
+ NewEval: evalSADD,
+ Arity: -3,
+ KeySpecs: KeySpecs{BeginIndex: 1},
+ IsMigrated: true,
}
smembersCmdMeta = DiceCmdMeta{
Name: "SMEMBERS",
Info: `SMEMBERS key
Returns all the members of the set value stored at key.`,
- Eval: evalSMEMBERS,
- Arity: 2,
- KeySpecs: KeySpecs{BeginIndex: 1},
+ Arity: 2,
+ KeySpecs: KeySpecs{BeginIndex: 1},
+ IsMigrated: true,
+ NewEval: evalSMEMBERS,
}
sremCmdMeta = DiceCmdMeta{
Name: "SREM",
@@ -940,18 +946,20 @@ var (
Removes the specified members from the set stored at key.
Non existing keys are treated as empty sets.
An error is returned when the value stored at key is not a set.`,
- Eval: evalSREM,
- Arity: -3,
- KeySpecs: KeySpecs{BeginIndex: 1},
+ Arity: -3,
+ KeySpecs: KeySpecs{BeginIndex: 1},
+ IsMigrated: true,
+ NewEval: evalSREM,
}
scardCmdMeta = DiceCmdMeta{
Name: "SCARD",
Info: `SCARD key
Returns the number of elements of the set stored at key.
An error is returned when the value stored at key is not a set.`,
- Eval: evalSCARD,
- Arity: 2,
- KeySpecs: KeySpecs{BeginIndex: 1},
+ Arity: 2,
+ KeySpecs: KeySpecs{BeginIndex: 1},
+ IsMigrated: true,
+ NewEval: evalSCARD,
}
sdiffCmdMeta = DiceCmdMeta{
Name: "SDIFF",
@@ -1210,17 +1218,19 @@ var (
There is another subcommand that only changes the behavior of successive
INCRBY and SET subcommands calls by setting the overflow behavior:
OVERFLOW [WRAP|SAT|FAIL]`,
- Arity: -1,
- KeySpecs: KeySpecs{BeginIndex: 1},
- Eval: evalBITFIELD,
+ Arity: -1,
+ KeySpecs: KeySpecs{BeginIndex: 1},
+ IsMigrated: true,
+ NewEval: evalBITFIELD,
}
bitfieldroCmdMeta = DiceCmdMeta{
Name: "BITFIELD_RO",
Info: `It is read-only variant of the BITFIELD command.
It is like the original BITFIELD but only accepts GET subcommand.`,
- Arity: -1,
- KeySpecs: KeySpecs{BeginIndex: 1},
- Eval: evalBITFIELDRO,
+ Arity: -1,
+ KeySpecs: KeySpecs{BeginIndex: 1},
+ IsMigrated: true,
+ NewEval: evalBITFIELDRO,
}
hincrbyFloatCmdMeta = DiceCmdMeta{
Name: "HINCRBYFLOAT",
@@ -1309,6 +1319,47 @@ var (
NewEval: evalCMSMerge,
KeySpecs: KeySpecs{BeginIndex: 1},
}
+ linsertCmdMeta = DiceCmdMeta{
+ Name: "LINSERT",
+ Info: `
+ Usage:
+ LINSERT key pivot element
+ Info:
+ Inserts element in the list stored at key either before or after the reference value pivot.
+ When key does not exist, it is considered an empty list and no operation is performed.
+ An error is returned when key exists but does not hold a list value.
+ Returns:
+ Integer - the list length after a successful insert operation.
+ 0 when the key doesn't exist.
+ -1 when the pivot wasn't found.
+ `,
+ NewEval: evalLINSERT,
+ IsMigrated: true,
+ Arity: 5,
+ KeySpecs: KeySpecs{BeginIndex: 1},
+ }
+ lrangeCmdMeta = DiceCmdMeta{
+ Name: "LRANGE",
+ Info: `
+ Usage:
+ LRANGE key start stop
+ Info:
+ Returns the specified elements of the list stored at key.
+ The offsets start and stop are zero-based indexes, with 0 being the first element of the list (the head of the list), 1 being the next element and so on.
+
+ These offsets can also be negative numbers indicating offsets starting at the end of the list.
+ For example, -1 is the last element of the list, -2 the penultimate, and so on.
+
+ Out of range indexes will not produce an error. If start is larger than the end of the list, an empty list is returned.
+ If stop is larger than the actual end of the list it will be treated like the last element of the list.
+ Returns:
+ Array reply: a list of elements in the specified range, or an empty array if the key doesn't exist.
+ `,
+ NewEval: evalLRANGE,
+ IsMigrated: true,
+ Arity: 4,
+ KeySpecs: KeySpecs{BeginIndex: 1},
+ }
)
func init() {
@@ -1450,6 +1501,8 @@ func init() {
DiceCmds["CMS.QUERY"] = cmsQueryCmdMeta
DiceCmds["CMS.INCRBY"] = cmsIncrByCmdMeta
DiceCmds["CMS.MERGE"] = cmsMergeCmdMeta
+ DiceCmds["LINSERT"] = linsertCmdMeta
+ DiceCmds["LRANGE"] = lrangeCmdMeta
}
// Function to convert DiceCmdMeta to []interface{}
diff --git a/internal/eval/deque.go b/internal/eval/deque.go
index a21e10eac..59d755c5f 100644
--- a/internal/eval/deque.go
+++ b/internal/eval/deque.go
@@ -2,6 +2,7 @@ package eval
import (
"errors"
+ "fmt"
"strconv"
"github.com/dicedb/dice/internal/dencoding"
@@ -10,10 +11,13 @@ import (
var ErrDequeEmpty = errors.New("deque is empty")
type DequeI interface {
+ GetLength() int64
LPush(string)
RPush(string)
LPop() (string, error)
RPop() (string, error)
+ LInsert(string, string, string) (int64, error)
+ LRange(start, stop int64) ([]string, error)
}
var _ DequeI = (*DequeBasic)(nil)
@@ -30,6 +34,10 @@ func NewBasicDeque() *DequeBasic {
return l
}
+func (q *DequeBasic) GetLength() int64 {
+ return q.Length
+}
+
// LPush pushes `x` into the left side of the Deque.
func (q *DequeBasic) LPush(x string) {
// enc + data + backlen
@@ -93,8 +101,127 @@ func (q *DequeBasic) LPop() (string, error) {
return x, nil
}
+// Inserts element after the given index in the buffer.
+func (q *DequeBasic) insertElementAfterIndex(element string, idx int) {
+ // enc + data + backlen
+ xb := EncodeDeqEntry(element)
+ xbLen := len(xb)
+
+ if cap(q.buf)-len(q.buf) < xbLen {
+ newArr := make([]byte, len(q.buf)+xbLen, (len(q.buf)+xbLen)*2)
+ copy(newArr[xbLen+idx:], q.buf[idx:])
+ copy(newArr[:idx], q.buf[:idx])
+ copy(newArr[idx:idx+xbLen], xb)
+ q.buf = newArr
+ } else {
+ q.buf = q.buf[:xbLen+len(q.buf)]
+ copy(q.buf[xbLen+idx:], q.buf[idx:])
+ copy(q.buf[:idx], q.buf[:idx])
+ copy(q.buf[idx:idx+xbLen], xb)
+ }
+ q.Length++
+}
+
+// Inserts the element before/after based on pivot's position.
+func (q *DequeBasic) insertBeforeAfterPivot(element, beforeAfter string, pivotIndexStart int, qIterator *DequeBasicIterator) {
+ if pivotIndexStart == 0 && beforeAfter == Before {
+ q.LPush(element)
+ return
+ }
+ if !qIterator.HasNext() && beforeAfter == After {
+ q.RPush(element)
+ return
+ }
+ idx := pivotIndexStart
+ if beforeAfter == After {
+ idx = qIterator.bufIndex
+ }
+ q.insertElementAfterIndex(element, idx)
+}
+
+// Inserts element before/after pivot element.
+func (q *DequeBasic) LInsert(pivot, element, beforeAfter string) (int64, error) {
+ // Check if the deque is empty.
+ if q.Length == 0 {
+ return -1, nil
+ }
+ if beforeAfter != Before && beforeAfter != After {
+ return -1, errors.New("syntax error")
+ }
+
+ qIterator := q.NewIterator()
+ for qIterator.HasNext() {
+ pivotIndexStart := qIterator.bufIndex
+ if x, _ := qIterator.Next(); x == pivot {
+ q.insertBeforeAfterPivot(element, beforeAfter, pivotIndexStart, qIterator)
+ return q.Length, nil
+ }
+ }
+ return -1, nil
+}
+
+// Iterates over the Deque and returns the element in order.
+func (q *DequeBasic) LRange(start, stop int64) ([]string, error) {
+ start = sanitizeStartIndex(q, start)
+ stop = sanitizeStopIndex(q, stop)
+ if start > stop {
+ return []string{}, nil
+ }
+ qIterator := q.NewIterator()
+ currIndex := int64(0)
+ res := make([]string, 0, stop-start+1)
+
+ for qIterator.HasNext() {
+ if currIndex > stop {
+ break
+ }
+
+ currElem, err := qIterator.Next()
+ if err != nil {
+ return []string{}, err
+ }
+
+ if currIndex >= start && currIndex <= stop {
+ res = append(res, currElem)
+ }
+ currIndex++
+ }
+
+ return res, nil
+}
+
+type DequeBasicIterator struct {
+ deque *DequeBasic
+ elementsTraversed int64
+ bufIndex int
+}
+
+func (q *DequeBasic) NewIterator() *DequeBasicIterator {
+ return &DequeBasicIterator{
+ deque: q,
+ elementsTraversed: 0,
+ bufIndex: 0,
+ }
+}
+
+func (i *DequeBasicIterator) HasNext() bool {
+ return i.elementsTraversed < i.deque.Length
+}
+
+func (i *DequeBasicIterator) Next() (string, error) {
+ if !i.HasNext() {
+ return "", fmt.Errorf("iterator exhausted")
+ }
+ x, entryLen := DecodeDeqEntry(i.deque.buf[i.bufIndex:])
+ i.bufIndex += entryLen
+ i.elementsTraversed++
+ return x, nil
+}
+
const (
minDequeNodeSize = 256
+ Before = "before"
+ After = "after"
)
var _ DequeI = (*Deque)(nil)
@@ -113,6 +240,10 @@ func NewDeque() *Deque {
}
}
+func (q *Deque) GetLength() int64 {
+ return q.Length
+}
+
func (q *Deque) LPush(x string) {
// enc + data + backlen
entrySize := int(GetEncodeDeqEntrySize(x))
@@ -146,7 +277,6 @@ func (q *Deque) RPush(x string) {
// enc + data + backlen
entrySize := int(GetEncodeDeqEntrySize(x))
tail := q.list.tail
-
if tail == nil || len(tail.buf) == cap(tail.buf) {
if entrySize > minDequeNodeSize {
tail = q.list.newNodeWithCapacity(entrySize)
@@ -215,6 +345,253 @@ func (q *Deque) RPop() (string, error) {
return x, nil
}
+// Breaks the pivot node's buffer and inserts the element after the pivot in a Deque.
+func (q *Deque) breakPivotNodeAndInsertAfter(qIterator *DequeIterator, pivotNode *byteListNode, element string) *byteListNode {
+ newNode := q.list.newNode()
+ if pivotNode.next != nil {
+ pivotNode.next.prev = newNode
+ }
+ newNode.next = pivotNode.next
+ pivotNode.next = nil
+ newNode.buf = append([]byte{}, pivotNode.buf[qIterator.BufIndex:]...)
+ pivotNode.buf = pivotNode.buf[:qIterator.BufIndex]
+ q.list.tail = pivotNode
+ q.RPush(element)
+ newNode.prev = q.list.tail
+ q.list.tail.next = newNode
+ return newNode
+}
+
+// Helper function to insert the element after pivot.
+// Uses RPush to insert the element after the pivot node and updates the tail of the node accordingly.
+func (q *Deque) insertAfterPivotNodeHelper(element string, qIterator *DequeIterator, pivotNode *byteListNode) {
+ prevTail := q.list.tail
+ if qIterator.BufIndex == 0 {
+ pivotNode.next = nil
+ q.list.tail = pivotNode
+ q.RPush(element)
+ q.list.tail.next = qIterator.CurrentNode
+ qIterator.CurrentNode.prev = q.list.tail
+ q.list.tail = prevTail
+ } else {
+ newNode := q.breakPivotNodeAndInsertAfter(qIterator, pivotNode, element)
+ if newNode.next == nil {
+ q.list.tail = newNode
+ } else {
+ q.list.tail = prevTail
+ }
+ }
+}
+
+// Inserts the element after the pivot.
+func (q *Deque) insertAfterPivotNode(element string, qIterator *DequeIterator, pivotNode *byteListNode) {
+ if qIterator.ElementsTraversed == q.Length {
+ // Element needs to be inserted at the end of the Deque.
+ q.RPush(element)
+ } else {
+ // Element needs to be inserted b/w 2 nodes in the Deque.
+ q.insertAfterPivotNodeHelper(element, qIterator, pivotNode)
+ }
+}
+
+// Creates a new byteListNode and encodes the given element into its buffer.
+func (q *Deque) getNewNodeWithElement(element string) *byteListNode {
+ elementEntryLen := int(GetEncodeDeqEntrySize(element))
+ elementNode := q.list.newNodeWithCapacity(elementEntryLen)
+ elementNode.buf = elementNode.buf[:elementEntryLen]
+ EncodeDeqEntryInPlace(element, elementNode.buf[:elementEntryLen])
+ return elementNode
+}
+
+// Breaks the pivot node's buffer and inserts the element before the pivot in a Deque.
+func (q *Deque) breakPivotNodeAndInsertBefore(qIterator *DequeIterator, pivotNode, elementNode *byteListNode, pivotEntryLen, leftIdx int) *byteListNode {
+ bufIndex := qIterator.BufIndex
+ if bufIndex == 0 {
+ bufIndex = len(pivotNode.buf)
+ }
+ newNode := q.list.newNodeWithCapacity(bufIndex - pivotEntryLen - leftIdx)
+ newNode.buf = append([]byte{}, pivotNode.buf[leftIdx:bufIndex-pivotEntryLen]...)
+ pivotNode.buf = pivotNode.buf[bufIndex-pivotEntryLen:]
+ if pivotNode.prev != nil {
+ pivotNode.prev.next = newNode
+ }
+ newNode.prev = pivotNode.prev
+ newNode.next = elementNode
+ elementNode.prev = newNode
+ elementNode.next = pivotNode
+ pivotNode.prev = elementNode
+ return newNode
+}
+
+// Updates the head node and left index of a Deque based on the newly inserted node.
+// It determines whether the newly inserted node becomes the new head of the Deque and updates the q.list.head and q.leftIdx accordingly.
+func (q *Deque) updateHeadLInsert(newNode, prevHead *byteListNode, prevLeftIdx int) {
+ if newNode.prev == nil {
+ q.list.head = newNode
+ q.leftIdx = 0
+ } else {
+ q.list.head = prevHead
+ q.leftIdx = prevLeftIdx
+ }
+}
+
+// Helper function to insert the element before pivot.
+// If it's a simple insertion at the beginning: Connects the new element node with the previous node before the pivot.
+// If it's a complex insertion within the pivot:
+// Splits the pivot node's buffer using breakPivotNodeAndInsertBefore. Updates the Deque's head and left index using updateHeadLInsert.
+func (q *Deque) insertBeforePivotHelper(pivot, element string, qIterator *DequeIterator, pivotNode *byteListNode) {
+ pivotEntryLen := int(GetEncodeDeqEntrySize(pivot))
+ prevHead := q.list.head
+ prevLeftIdx := q.leftIdx
+ leftIdx := q.leftIdx
+ if pivotNode.prev != nil {
+ leftIdx = 0
+ }
+ elementNode := q.getNewNodeWithElement(element)
+ if qIterator.BufIndex == pivotEntryLen || (qIterator.BufIndex == 0 && (len(pivotNode.buf) == pivotEntryLen)) {
+ // No need to break the pivotNode into two nodes when the pivot element is the first element in the buffer.
+ prevNode := pivotNode.prev
+ prevNode.next = elementNode
+ pivotNode.prev = elementNode
+ elementNode.next = pivotNode
+ elementNode.prev = prevNode
+ } else {
+ newNode := q.breakPivotNodeAndInsertBefore(qIterator, pivotNode, elementNode, pivotEntryLen, leftIdx)
+ q.updateHeadLInsert(newNode, prevHead, prevLeftIdx)
+ }
+ q.Length++
+}
+
+// Inserts the element before the pivot.
+func (q *Deque) insertBeforePivotNode(pivot, element string, qIterator *DequeIterator, pivotNode *byteListNode) {
+ if qIterator.ElementsTraversed == 1 {
+ // Element needs to be inserted at the front of the Deque.
+ q.LPush(element)
+ } else {
+ q.insertBeforePivotHelper(pivot, element, qIterator, pivotNode)
+ }
+}
+
+// Inserts element before/after pivot element.
+func (q *Deque) LInsert(pivot, element, beforeAfter string) (int64, error) {
+ // Check if the deque is empty.
+ if q.Length == 0 {
+ return -1, nil
+ }
+ if beforeAfter != Before && beforeAfter != After {
+ return -1, errors.New("syntax error")
+ }
+
+ qIterator := q.NewIterator()
+ for qIterator.HasNext() {
+ pivotNode := qIterator.CurrentNode
+ if x, _ := qIterator.Next(); x == pivot {
+ switch beforeAfter {
+ case Before:
+ q.insertBeforePivotNode(pivot, element, qIterator, pivotNode)
+ case After:
+ q.insertAfterPivotNode(element, qIterator, pivotNode)
+ }
+ return q.Length, nil
+ }
+ }
+ return -1, nil
+}
+
+// Validates and adjusts the start index to ensure it's within the valid range of the Deque.
+// Ensure that the start index for operations on the Deque is valid, preventing potential errors or unexpected behavior.
+func sanitizeStartIndex(q DequeI, start int64) int64 {
+ // if start offset is -ve then find the offset from the end of the list.
+ if start < 0 {
+ start = q.GetLength() + start
+ }
+ // if start offset is still -ve then set it to 0 i.e. the first index of the list.
+ if start < 0 {
+ start = 0
+ }
+ return start
+}
+
+// Validates and adjusts the stop index to ensure it's within the valid range of the Deque.
+// Handles both negative and out-of-range stop indices.
+func sanitizeStopIndex(q DequeI, stop int64) int64 {
+ // if stop offset is -ve then find the offset from the end of the list.
+ qLen := q.GetLength()
+ if stop < 0 {
+ stop = qLen + stop
+ }
+ // if stop offset is greater than the last index then set it to last index.
+ if stop >= qLen {
+ stop = qLen - 1
+ }
+ return stop
+}
+
+// Iterates over the Deque and returns the element in order.
+func (q *Deque) LRange(start, stop int64) ([]string, error) {
+ start = sanitizeStartIndex(q, start)
+ stop = sanitizeStopIndex(q, stop)
+ if start > stop {
+ return []string{}, nil
+ }
+ qIterator := q.NewIterator()
+ currIndex := int64(0)
+ res := make([]string, 0, stop-start+1)
+
+ for qIterator.HasNext() {
+ if currIndex > stop {
+ break
+ }
+
+ currElem, err := qIterator.Next()
+ if err != nil {
+ return []string{}, err
+ }
+
+ if currIndex >= start && currIndex <= stop {
+ res = append(res, currElem)
+ }
+ currIndex++
+ }
+
+ return res, nil
+}
+
+type DequeIterator struct {
+ deque *Deque
+ CurrentNode *byteListNode
+ ElementsTraversed int64
+ BufIndex int
+}
+
+// Creates a new iterator for Deque.
+func (q *Deque) NewIterator() *DequeIterator {
+ return &DequeIterator{
+ deque: q,
+ CurrentNode: q.list.head,
+ ElementsTraversed: 0,
+ BufIndex: q.leftIdx,
+ }
+}
+
+func (i *DequeIterator) HasNext() bool {
+ return i.ElementsTraversed < i.deque.Length
+}
+
+func (i *DequeIterator) Next() (string, error) {
+ if !i.HasNext() {
+ return "", fmt.Errorf("iterator exhausted")
+ }
+ x, entryLen := DecodeDeqEntry(i.CurrentNode.buf[i.BufIndex:])
+ i.BufIndex += entryLen
+ if i.BufIndex == len(i.CurrentNode.buf) {
+ i.CurrentNode = i.CurrentNode.next
+ i.BufIndex = 0
+ }
+ i.ElementsTraversed++
+ return x, nil
+}
+
// *************************** deque entry encode/decode ***************************
// EncodeDeqEntry encodes `x` into an entry of Deque. An entry will be encoded as [enc + data + backlen].
diff --git a/internal/eval/deque_test.go b/internal/eval/deque_test.go
index b21ee3243..8788aacfe 100644
--- a/internal/eval/deque_test.go
+++ b/internal/eval/deque_test.go
@@ -2,12 +2,13 @@ package eval_test
import (
"fmt"
- "github.com/dicedb/dice/internal/eval"
"math/rand"
"strconv"
+ "strings"
"testing"
"time"
+ "github.com/dicedb/dice/internal/eval"
"github.com/stretchr/testify/assert"
)
@@ -85,6 +86,39 @@ func dequeLPushIntStrMany(howmany int, deq eval.DequeI) {
}
}
+func dequeLInsertIntStrMany(howMany int, beforeAfter string, deq eval.DequeI) {
+ const pivot string = "10"
+ const element string = "50"
+ deq.LPush(pivot)
+ for i := 0; i < howMany; i++ {
+ deq.LInsert(pivot, element, beforeAfter)
+ }
+}
+
+func BenchmarkBasicDequeLInsertBefore2000(b *testing.B) {
+ for n := 0; n < b.N; n++ {
+ dequeLInsertIntStrMany(2000, "before", eval.NewBasicDeque())
+ }
+}
+
+func BenchmarkBasicDequeLInsertAfter2000(b *testing.B) {
+ for n := 0; n < b.N; n++ {
+ dequeLInsertIntStrMany(2000, "after", eval.NewBasicDeque())
+ }
+}
+
+func BenchmarkDequeLInsertBefore2000(b *testing.B) {
+ for n := 0; n < b.N; n++ {
+ dequeLInsertIntStrMany(2000, "before", eval.NewDeque())
+ }
+}
+
+func BenchmarkDequeLInsertAfter2000(b *testing.B) {
+ for n := 0; n < b.N; n++ {
+ dequeLInsertIntStrMany(2000, "after", eval.NewDeque())
+ }
+}
+
func BenchmarkBasicDequeRPush20(b *testing.B) {
for n := 0; n < b.N; n++ {
dequeRPushIntStrMany(20, eval.NewBasicDeque())
@@ -138,3 +172,282 @@ func BenchmarkDequeLPush2000(b *testing.B) {
dequeLPushIntStrMany(2000, eval.NewDeque())
}
}
+
+func TestLRange(t *testing.T) {
+ testCases := []struct {
+ name string
+ dq eval.DequeI
+ input []string
+ expectedOutput []string
+ start int64
+ stop int64
+ }{
+ {"DequeWithStartStopPositiveAndInRange", eval.NewDeque(), []string{"a", "b", "c"}, []string{"c", "b", "a"}, 0, 2},
+ {"DequeWhereStopIsOutOfRange", eval.NewDeque(), []string{"a", "b", "c"}, []string{"c", "b", "a"}, 0, 20},
+ {"DequeWhereStartIsOutOfRange", eval.NewDeque(), []string{"a", "b", "c"}, []string{}, 10, 2},
+ {"DequeWhereStartIsNegative", eval.NewDeque(), []string{"a", "b", "c"}, []string{"b", "a"}, -2, 2},
+ {"DequeWhereStartIsNegativeOutOfRange", eval.NewDeque(), []string{"a", "b", "c"}, []string{"c", "b", "a"}, -20, 2},
+ {"DequeWhereStopIsNegative", eval.NewDeque(), []string{"a", "b", "c"}, []string{"c", "b"}, 0, -2},
+ {"DequeWhereStopIsNegativeOutOfRange", eval.NewDeque(), []string{"a", "b", "c"}, []string{}, 0, -4},
+ {"DequeWhereStartGreaterThanStop", eval.NewDeque(), []string{"a", "b", "c"}, []string{}, 2, 0},
+ {"BasicDequeWithStartStopPositiveAndInRange", eval.NewBasicDeque(), []string{"a", "b", "c"}, []string{"c", "b", "a"}, 0, 2},
+ {"BasicDequeWhereStopIsOutOfRange", eval.NewBasicDeque(), []string{"a", "b", "c"}, []string{"c", "b", "a"}, 0, 20},
+ {"BasicDequeWhereStartIsOutOfRange", eval.NewBasicDeque(), []string{"a", "b", "c"}, []string{}, 10, 2},
+ {"BasicDequeWhereStartIsNegative", eval.NewBasicDeque(), []string{"a", "b", "c"}, []string{"b", "a"}, -2, 2},
+ {"BasicDequeWhereStartIsNegativeOutOfRange", eval.NewBasicDeque(), []string{"a", "b", "c"}, []string{"c", "b", "a"}, -20, 2},
+ {"BasicDequeWhereStopIsNegative", eval.NewBasicDeque(), []string{"a", "b", "c"}, []string{"c", "b"}, 0, -2},
+ {"BasicDequeWhereStopIsNegativeOutOfRange", eval.NewBasicDeque(), []string{"a", "b", "c"}, []string{}, 0, -4},
+ {"BasicDequeWhereStartGreaterThanStop", eval.NewBasicDeque(), []string{"a", "b", "c"}, []string{}, 2, 0},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ for _, i := range tc.input {
+ tc.dq.LPush(i)
+ }
+ output, _ := tc.dq.LRange(tc.start, tc.stop)
+ assert.ElementsMatch(t, output, tc.expectedOutput)
+
+ })
+ }
+}
+
+func TestLInsertOnInvalidOperationTypeReturnsError(t *testing.T) {
+ testCases := []struct {
+ name string
+ dq eval.DequeI
+ }{
+ {"WithDeque", eval.NewDeque()},
+ {"WithBasicDeque", eval.NewBasicDeque()},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ tc.dq.LPush("a")
+ tc.dq.LPush("b")
+ tc.dq.LPush("c")
+ newLen, err := tc.dq.LInsert("a", "x", "randomOperation")
+ if err == nil || err.Error() != "syntax error" {
+ t.Errorf("Expected error 'syntax error', got %v", err)
+ }
+ if newLen != -1 {
+ t.Errorf("Expected int -1, got %v", newLen)
+ }
+ })
+ }
+}
+
+func TestLInsertBasicDeque(t *testing.T) {
+ dq := eval.NewBasicDeque()
+ dq.RPush("a")
+ dq.RPush("b")
+ dq.RPush("c")
+ testCases := []struct {
+ name string
+ pivotElement string
+ elementToBeInserted string
+ beforeAfter string
+ expectedOutput int64
+ expectedErr error
+ expectedElementsOrder []string
+ }{
+ {"InMiddleBefore", "b", "d", "before", 4, nil, []string{"a", "d", "b", "c"}},
+ {"AtFrontBefore", "a", "e", "before", 5, nil, []string{"e", "a", "d", "b", "c"}},
+ {"AtEndBefore", "c", "f", "before", 6, nil, []string{"e", "a", "d", "b", "f", "c"}},
+ {"InMiddleAfter", "b", "g", "after", 7, nil, []string{"e", "a", "d", "b", "g", "f", "c"}},
+ {"AtFrontAfter", "e", "h", "after", 8, nil, []string{"e", "h", "a", "d", "b", "g", "f", "c"}},
+ {"AtEndAfter", "c", "i", "after", 9, nil, []string{"e", "h", "a", "d", "b", "g", "f", "c", "i"}},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ result, err := dq.LInsert(tc.pivotElement, tc.elementToBeInserted, tc.beforeAfter)
+ if result != tc.expectedOutput {
+ t.Errorf("Expected %v, got %v.", tc.expectedOutput, result)
+ }
+ if err != tc.expectedErr {
+ t.Errorf("Expected error %v, got %v", tc.expectedErr, err)
+ }
+ iter := dq.NewIterator()
+ for i, expected := range tc.expectedElementsOrder {
+ val, err := iter.Next()
+ if err != nil {
+ t.Errorf("Error iterating deque: %v", err)
+ }
+ if strings.Compare(val, expected) != 0 {
+ t.Errorf("Expected value %d to be '%s', got '%s'", i, expected, val)
+ }
+ }
+ })
+ }
+}
+
+type DequeLInsertFixture struct {
+ dq *eval.Deque
+ initialElements []string
+ elementsToBeInserted []string
+}
+
+func newDequeLInsertFixture() *DequeLInsertFixture {
+ dq := eval.NewDeque()
+ initElements := []string{deqRandStr(10), deqRandStr(100), deqRandStr(250), deqRandStr(150), deqRandStr(200)}
+ for _, elem := range initElements {
+ dq.LPush(elem)
+ }
+ return &DequeLInsertFixture{
+ dq,
+ initElements,
+ []string{deqRandStr(30), deqRandStr(50), deqRandStr(80), deqRandStr(130)},
+ }
+}
+
+func TestDequeLInsertBefore(t *testing.T) {
+ deqTestInit()
+ fixture := newDequeLInsertFixture()
+ testCases := []struct {
+ name string
+ pivotElement string
+ elementToBeInserted string
+ beforeAfter string
+ expectedOutput int64
+ expectedErr error
+ expectedElementsOrder []string
+ }{
+ {"WhenPivotInMiddleOfHeadNode",
+ fixture.initialElements[3],
+ fixture.elementsToBeInserted[0],
+ "before",
+ 6,
+ nil,
+ []string{fixture.initialElements[4], fixture.elementsToBeInserted[0], fixture.initialElements[3], fixture.initialElements[2], fixture.initialElements[1], fixture.initialElements[0]},
+ },
+ {"WhenPivotAtStartOfHeadNode",
+ fixture.initialElements[4],
+ fixture.elementsToBeInserted[1],
+ "before",
+ 7,
+ nil,
+ []string{fixture.elementsToBeInserted[1], fixture.initialElements[4], fixture.elementsToBeInserted[0], fixture.initialElements[3], fixture.initialElements[2], fixture.initialElements[1], fixture.initialElements[0]},
+ },
+ {"WhenPivotAtStartOfNonHeadNode",
+ fixture.initialElements[2],
+ fixture.elementsToBeInserted[2],
+ "before",
+ 8,
+ nil,
+ []string{fixture.elementsToBeInserted[1], fixture.initialElements[4], fixture.elementsToBeInserted[0], fixture.initialElements[3], fixture.elementsToBeInserted[2], fixture.initialElements[2], fixture.initialElements[1], fixture.initialElements[0]},
+ },
+ {"WhenPivotInMiddleOfNonHeadNode",
+ fixture.initialElements[1],
+ fixture.elementsToBeInserted[3],
+ "before",
+ 9,
+ nil,
+ []string{fixture.elementsToBeInserted[1], fixture.initialElements[4], fixture.elementsToBeInserted[0], fixture.initialElements[3], fixture.elementsToBeInserted[2], fixture.initialElements[2], fixture.elementsToBeInserted[3], fixture.initialElements[1], fixture.initialElements[0]},
+ },
+ {"WhenPivotDoesNotExist",
+ "pivot",
+ "element",
+ "before",
+ -1,
+ nil,
+ []string{fixture.elementsToBeInserted[1], fixture.initialElements[4], fixture.elementsToBeInserted[0], fixture.initialElements[3], fixture.elementsToBeInserted[2], fixture.initialElements[2], fixture.elementsToBeInserted[3], fixture.initialElements[1], fixture.initialElements[0]},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ result, err := fixture.dq.LInsert(tc.pivotElement, tc.elementToBeInserted, tc.beforeAfter)
+ if result != tc.expectedOutput {
+ t.Errorf("Expected %v, got %v.", tc.expectedOutput, result)
+ }
+ if err != tc.expectedErr {
+ t.Errorf("Expected error %v, got %v", tc.expectedErr, err)
+ }
+ iter := fixture.dq.NewIterator()
+ for i, expected := range tc.expectedElementsOrder {
+ val, err := iter.Next()
+ if err != nil {
+ t.Errorf("Error iterating deque: %v", err)
+ }
+ if strings.Compare(val, expected) != 0 {
+ t.Errorf("Expected value %d to be '%s', got '%s'", i, expected, val)
+ }
+ }
+ })
+ }
+}
+
+func TestLInsertAfter(t *testing.T) {
+ deqTestInit()
+ fixture := newDequeLInsertFixture()
+ testCases := []struct {
+ name string
+ pivotElement string
+ elementToBeInserted string
+ beforeAfter string
+ expectedOutput int64
+ expectedErr error
+ expectedElementsOrder []string
+ }{
+ {"WhenPivotInMiddleOfTailNode",
+ fixture.initialElements[1],
+ fixture.elementsToBeInserted[0],
+ "after",
+ 6,
+ nil,
+ []string{fixture.initialElements[4], fixture.initialElements[3], fixture.initialElements[2], fixture.initialElements[1], fixture.elementsToBeInserted[0], fixture.initialElements[0]},
+ },
+ {"WhenPivotAtEndOfTailNode",
+ fixture.initialElements[0],
+ fixture.elementsToBeInserted[1],
+ "after",
+ 7,
+ nil,
+ []string{fixture.initialElements[4], fixture.initialElements[3], fixture.initialElements[2], fixture.initialElements[1], fixture.elementsToBeInserted[0], fixture.initialElements[0], fixture.elementsToBeInserted[1]},
+ },
+ {"WhenPivotAtEndOfNonTailNode",
+ fixture.initialElements[3],
+ fixture.elementsToBeInserted[2],
+ "after",
+ 8,
+ nil,
+ []string{fixture.initialElements[4], fixture.initialElements[3], fixture.elementsToBeInserted[2], fixture.initialElements[2], fixture.initialElements[1], fixture.elementsToBeInserted[0], fixture.initialElements[0], fixture.elementsToBeInserted[1]},
+ },
+ {"WhenPivotInMiddleOfNonLastNode",
+ fixture.initialElements[4],
+ fixture.elementsToBeInserted[3],
+ "after",
+ 9,
+ nil,
+ []string{fixture.initialElements[4], fixture.elementsToBeInserted[3], fixture.initialElements[3], fixture.elementsToBeInserted[2], fixture.initialElements[2], fixture.initialElements[1], fixture.elementsToBeInserted[0], fixture.initialElements[0], fixture.elementsToBeInserted[1]},
+ },
+ {"WhenPivotDoesNotExist",
+ "pivot",
+ "element",
+ "after",
+ -1,
+ nil,
+ []string{fixture.initialElements[4], fixture.elementsToBeInserted[3], fixture.initialElements[3], fixture.elementsToBeInserted[2], fixture.initialElements[2], fixture.initialElements[1], fixture.elementsToBeInserted[0], fixture.initialElements[0], fixture.elementsToBeInserted[1]},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ result, err := fixture.dq.LInsert(tc.pivotElement, tc.elementToBeInserted, tc.beforeAfter)
+ if result != tc.expectedOutput {
+ t.Errorf("Expected %v, got %v.", tc.expectedOutput, result)
+ }
+ if err != tc.expectedErr {
+ t.Errorf("Expected error %v, got %v", tc.expectedErr, err)
+ }
+ iter := fixture.dq.NewIterator()
+ for i, expected := range tc.expectedElementsOrder {
+ val, err := iter.Next()
+ if err != nil {
+ t.Errorf("Error iterating deque: %v", err)
+ }
+ if strings.Compare(val, expected) != 0 {
+ t.Errorf("Expected value %d to be '%s', got '%s'", i, expected, val)
+ }
+ }
+ })
+ }
+}
diff --git a/internal/eval/eval.go b/internal/eval/eval.go
index 2bf259f6c..8bf1fb8c5 100644
--- a/internal/eval/eval.go
+++ b/internal/eval/eval.go
@@ -6,7 +6,6 @@ import (
"fmt"
"log/slog"
"math"
- "math/bits"
"regexp"
"sort"
"strconv"
@@ -1185,268 +1184,6 @@ func EvalQUNWATCH(args []string, httpOp bool, client *comm.Client) []byte {
return clientio.RespOK
}
-// SETBIT key offset value
-func evalSETBIT(args []string, store *dstore.Store) []byte {
- var err error
-
- if len(args) != 3 {
- return diceerrors.NewErrArity("SETBIT")
- }
-
- key := args[0]
- offset, err := strconv.ParseInt(args[1], 10, 64)
- if err != nil {
- return diceerrors.NewErrWithMessage("bit offset is not an integer or out of range")
- }
-
- value, err := strconv.ParseBool(args[2])
- if err != nil {
- return diceerrors.NewErrWithMessage("bit is not an integer or out of range")
- }
-
- obj := store.Get(key)
- requiredByteArraySize := offset>>3 + 1
-
- if obj == nil {
- obj = store.NewObj(NewByteArray(int(requiredByteArraySize)), -1, object.ObjTypeByteArray, object.ObjEncodingByteArray)
- store.Put(args[0], obj)
- }
-
- if object.AssertType(obj.TypeEncoding, object.ObjTypeByteArray) == nil ||
- object.AssertType(obj.TypeEncoding, object.ObjTypeString) == nil ||
- object.AssertType(obj.TypeEncoding, object.ObjTypeInt) == nil {
- var byteArray *ByteArray
- oType, oEnc := object.ExtractTypeEncoding(obj)
-
- switch oType {
- case object.ObjTypeByteArray:
- byteArray = obj.Value.(*ByteArray)
- case object.ObjTypeString, object.ObjTypeInt:
- byteArray, err = NewByteArrayFromObj(obj)
- if err != nil {
- return diceerrors.NewErrWithMessage(diceerrors.WrongTypeErr)
- }
- default:
- return diceerrors.NewErrWithMessage(diceerrors.WrongTypeErr)
- }
-
- // Perform the resizing check
- byteArrayLength := byteArray.Length
-
- // check whether resize required or not
- if requiredByteArraySize > byteArrayLength {
- // resize as per the offset
- byteArray = byteArray.IncreaseSize(int(requiredByteArraySize))
- }
-
- resp := byteArray.GetBit(int(offset))
- byteArray.SetBit(int(offset), value)
-
- // We are returning newObject here so it is thread-safe
- // Old will be removed by GC
- newObj, err := ByteSliceToObj(store, obj, byteArray.data, oType, oEnc)
- if err != nil {
- return diceerrors.NewErrWithMessage(diceerrors.WrongTypeErr)
- }
-
- exp, ok := dstore.GetExpiry(obj, store)
- var exDurationMs int64 = -1
- if ok {
- exDurationMs = int64(exp - uint64(utils.GetCurrentTime().UnixMilli()))
- }
- // newObj has bydefault expiry time -1 , we need to set it
- if exDurationMs > 0 {
- store.SetExpiry(newObj, exDurationMs)
- }
-
- store.Put(key, newObj)
- if resp {
- return clientio.Encode(1, true)
- }
- return clientio.Encode(0, true)
- }
- return diceerrors.NewErrWithMessage(diceerrors.WrongTypeErr)
-}
-
-// GETBIT key offset
-func evalGETBIT(args []string, store *dstore.Store) []byte {
- var err error
-
- if len(args) != 2 {
- return diceerrors.NewErrArity("GETBIT")
- }
-
- key := args[0]
- offset, err := strconv.ParseInt(args[1], 10, 64)
- if err != nil {
- return diceerrors.NewErrWithMessage("bit offset is not an integer or out of range")
- }
-
- obj := store.Get(key)
- if obj == nil {
- return clientio.Encode(0, true)
- }
-
- requiredByteArraySize := offset>>3 + 1
- switch oType, _ := object.ExtractTypeEncoding(obj); oType {
- case object.ObjTypeSet:
- return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)
- case object.ObjTypeByteArray:
- byteArray := obj.Value.(*ByteArray)
- byteArrayLength := byteArray.Length
-
- // check whether offset, length exists or not
- if requiredByteArraySize > byteArrayLength {
- return clientio.Encode(0, true)
- }
- value := byteArray.GetBit(int(offset))
- if value {
- return clientio.Encode(1, true)
- }
- return clientio.Encode(0, true)
- case object.ObjTypeString, object.ObjTypeInt:
- byteArray, err := NewByteArrayFromObj(obj)
- if err != nil {
- return diceerrors.NewErrWithMessage(diceerrors.WrongTypeErr)
- }
- if requiredByteArraySize > byteArray.Length {
- return clientio.Encode(0, true)
- }
- value := byteArray.GetBit(int(offset))
- if value {
- return clientio.Encode(1, true)
- }
- return clientio.Encode(0, true)
- default:
- return clientio.Encode(0, true)
- }
-}
-
-func evalBITCOUNT(args []string, store *dstore.Store) []byte {
- var err error
-
- // if no key is provided, return error
- if len(args) == 0 {
- return diceerrors.NewErrArity("BITCOUNT")
- }
-
- // if more than 4 arguments are provided, return error
- if len(args) > 4 {
- return diceerrors.NewErrWithMessage(diceerrors.SyntaxErr)
- }
-
- // fetching value of the key
- key := args[0]
- obj := store.Get(key)
- if obj == nil {
- return clientio.Encode(0, false)
- }
-
- var value []byte
- var valueLength int64
-
- switch {
- case object.AssertType(obj.TypeEncoding, object.ObjTypeByteArray) == nil:
- byteArray := obj.Value.(*ByteArray)
- value = byteArray.data
- valueLength = byteArray.Length
- case object.AssertType(obj.TypeEncoding, object.ObjTypeString) == nil:
- value = []byte(obj.Value.(string))
- valueLength = int64(len(value))
- case object.AssertType(obj.TypeEncoding, object.ObjTypeInt) == nil:
- value = []byte(strconv.FormatInt(obj.Value.(int64), 10))
- valueLength = int64(len(value))
- default:
- return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)
- }
-
- // defining constants of the function
- start, end := int64(0), valueLength-1
- unit := BYTE
-
- // checking which arguments are present and validating arguments
- if len(args) > 1 {
- start, err = strconv.ParseInt(args[1], 10, 64)
- if err != nil {
- return diceerrors.NewErrWithMessage(diceerrors.IntOrOutOfRangeErr)
- }
- if len(args) <= 2 {
- return diceerrors.NewErrWithMessage(diceerrors.SyntaxErr)
- }
- end, err = strconv.ParseInt(args[2], 10, 64)
- if err != nil {
- return diceerrors.NewErrWithMessage(diceerrors.IntOrOutOfRangeErr)
- }
- }
- if len(args) > 3 {
- unit = strings.ToUpper(args[3])
- }
-
- switch unit {
- case BYTE:
- if start < 0 {
- start += valueLength
- }
- if end < 0 {
- end += valueLength
- }
- if start > end || start >= valueLength {
- return clientio.Encode(0, true)
- }
- end = min(end, valueLength-1)
- bitCount := 0
- for i := start; i <= end; i++ {
- bitCount += bits.OnesCount8(value[i])
- }
- return clientio.Encode(bitCount, true)
- case BIT:
- if start < 0 {
- start += valueLength * 8
- }
- if end < 0 {
- end += valueLength * 8
- }
- if start > end {
- return clientio.Encode(0, true)
- }
- startByte, endByte := start/8, min(end/8, valueLength-1)
- startBitOffset, endBitOffset := start%8, end%8
-
- if endByte == valueLength-1 {
- endBitOffset = 7
- }
-
- if startByte >= valueLength {
- return clientio.Encode(0, true)
- }
-
- bitCount := 0
-
- // Use bit masks to count the bits instead of a loop
- if startByte == endByte {
- mask := byte(0xFF >> startBitOffset)
- mask &= byte(0xFF << (7 - endBitOffset))
- bitCount = bits.OnesCount8(value[startByte] & mask)
- } else {
- // Handle first byte
- firstByteMask := byte(0xFF >> startBitOffset)
- bitCount += bits.OnesCount8(value[startByte] & firstByteMask)
-
- // Handle all the middle ones
- for i := startByte + 1; i < endByte; i++ {
- bitCount += bits.OnesCount8(value[i])
- }
-
- // Handle last byte
- lastByteMask := byte(0xFF << (7 - endBitOffset))
- bitCount += bits.OnesCount8(value[endByte] & lastByteMask)
- }
- return clientio.Encode(bitCount, true)
- default:
- return diceerrors.NewErrWithMessage(diceerrors.SyntaxErr)
- }
-}
-
// BITOP destkey key [key ...]
func evalBITOP(args []string, store *dstore.Store) []byte {
operation, destKey := args[0], args[1]
@@ -2157,10 +1894,31 @@ func evalRPOP(args []string, store *dstore.Store) []byte {
}
func evalLPOP(args []string, store *dstore.Store) []byte {
- if len(args) != 1 {
+ // By default we pop only 1
+ popNumber := 1
+
+ // LPOP accepts 1 or 2 arguments only - LPOP key [count]
+ if len(args) < 1 || len(args) > 2 {
return diceerrors.NewErrArity("LPOP")
}
+ // to updated the number of pops
+ if len(args) == 2 {
+ nos, err := strconv.Atoi(args[1])
+ if err != nil {
+ return diceerrors.NewErrWithFormattedMessage(diceerrors.IntOrOutOfRangeErr)
+ }
+ if nos == 0 {
+ // returns empty string if count given is 0
+ return clientio.Encode([]string{}, false)
+ }
+ if nos < 0 {
+ // returns an out of range err if count is negetive
+ return diceerrors.NewErrWithFormattedMessage(diceerrors.ValOutOfRangeErr)
+ }
+ popNumber = nos
+ }
+
obj := store.Get(args[0])
if obj == nil {
return clientio.RespNIL
@@ -2180,15 +1938,29 @@ func evalLPOP(args []string, store *dstore.Store) []byte {
}
deq := obj.Value.(*Deque)
- x, err := deq.LPop()
- if err != nil {
- if errors.Is(err, ErrDequeEmpty) {
- return clientio.RespNIL
+
+ // holds the elements popped
+ var elements []string
+ for iter := 0; iter < popNumber; iter++ {
+ x, err := deq.LPop()
+ if err != nil {
+ if errors.Is(err, ErrDequeEmpty) {
+ break
+ }
+ panic(fmt.Sprintf("unknown error: %v", err))
}
- panic(fmt.Sprintf("unknown error: %v", err))
+ elements = append(elements, x)
}
- return clientio.Encode(x, false)
+ if len(elements) == 0 {
+ return clientio.RespNIL
+ }
+
+ if len(elements) == 1 {
+ return clientio.Encode(elements[0], false)
+ }
+
+ return clientio.Encode(elements, false)
}
func evalLLEN(args []string, store *dstore.Store) []byte {
@@ -2231,145 +2003,6 @@ func evalFLUSHDB(args []string, store *dstore.Store) []byte {
return clientio.RespOK
}
-func evalSADD(args []string, store *dstore.Store) []byte {
- if len(args) < 2 {
- return diceerrors.NewErrArity("SADD")
- }
- key := args[0]
-
- // Get the set object from the store.
- obj := store.Get(key)
- lengthOfItems := len(args[1:])
-
- count := 0
- if obj == nil {
- var exDurationMs int64 = -1
- keepttl := false
- // If the object does not exist, create a new set object.
- value := make(map[string]struct{}, lengthOfItems)
- // Create a new object.
- obj = store.NewObj(value, exDurationMs, object.ObjTypeSet, object.ObjEncodingSetStr)
- store.Put(key, obj, dstore.WithKeepTTL(keepttl))
- }
-
- if err := object.AssertType(obj.TypeEncoding, object.ObjTypeSet); err != nil {
- return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)
- }
-
- if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingSetStr); err != nil {
- return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)
- }
-
- // Get the set object.
- set := obj.Value.(map[string]struct{})
-
- for _, arg := range args[1:] {
- if _, ok := set[arg]; !ok {
- set[arg] = struct{}{}
- count++
- }
- }
-
- return clientio.Encode(count, false)
-}
-
-func evalSMEMBERS(args []string, store *dstore.Store) []byte {
- if len(args) != 1 {
- return diceerrors.NewErrArity("SMEMBERS")
- }
- key := args[0]
-
- // Get the set object from the store.
- obj := store.Get(key)
-
- if obj == nil {
- return clientio.Encode([]string{}, false)
- }
-
- // If the object exists, check if it is a set object.
- if err := object.AssertType(obj.TypeEncoding, object.ObjTypeSet); err != nil {
- return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)
- }
-
- if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingSetStr); err != nil {
- return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)
- }
-
- // Get the set object.
- set := obj.Value.(map[string]struct{})
- // Get the members of the set.
- members := make([]string, 0, len(set))
- for k := range set {
- members = append(members, k)
- }
-
- return clientio.Encode(members, false)
-}
-
-func evalSREM(args []string, store *dstore.Store) []byte {
- if len(args) < 2 {
- return diceerrors.NewErrArity("SREM")
- }
- key := args[0]
-
- // Get the set object from the store.
- obj := store.Get(key)
-
- count := 0
- if obj == nil {
- return clientio.Encode(count, false)
- }
-
- // If the object exists, check if it is a set object.
- if err := object.AssertType(obj.TypeEncoding, object.ObjTypeSet); err != nil {
- return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)
- }
-
- if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingSetStr); err != nil {
- return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)
- }
-
- // Get the set object.
- set := obj.Value.(map[string]struct{})
-
- for _, arg := range args[1:] {
- if _, ok := set[arg]; ok {
- delete(set, arg)
- count++
- }
- }
-
- return clientio.Encode(count, false)
-}
-
-func evalSCARD(args []string, store *dstore.Store) []byte {
- if len(args) != 1 {
- return diceerrors.NewErrArity("SCARD")
- }
-
- key := args[0]
-
- // Get the set object from the store.
- obj := store.Get(key)
-
- if obj == nil {
- return clientio.Encode(0, false)
- }
-
- // If the object exists, check if it is a set object.
- if err := object.AssertType(obj.TypeEncoding, object.ObjTypeSet); err != nil {
- return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)
- }
-
- if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingSetStr); err != nil {
- return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)
- }
-
- // Get the set object.
- count := len(obj.Value.(map[string]struct{}))
- return clientio.Encode(count, false)
-}
-
func evalSDIFF(args []string, store *dstore.Store) []byte {
if len(args) < 1 {
return diceerrors.NewErrArity("SDIFF")
@@ -2793,70 +2426,6 @@ func executeBitfieldOps(value *ByteArray, ops []utils.BitFieldOp) []interface{}
}
return result
}
-
-// Generic method for both BITFIELD and BITFIELD_RO.
-// isReadOnly method is true for BITFIELD_RO command.
-func bitfieldEvalGeneric(args []string, store *dstore.Store, isReadOnly bool) []byte {
- var ops []utils.BitFieldOp
- ops, err2 := utils.ParseBitfieldOps(args, isReadOnly)
-
- if err2 != nil {
- return err2
- }
-
- key := args[0]
- obj := store.Get(key)
- if obj == nil {
- obj = store.NewObj(NewByteArray(1), -1, object.ObjTypeByteArray, object.ObjEncodingByteArray)
- store.Put(args[0], obj)
- }
- var value *ByteArray
- var err error
-
- switch oType, _ := object.ExtractTypeEncoding(obj); oType {
- case object.ObjTypeByteArray:
- value = obj.Value.(*ByteArray)
- case object.ObjTypeString, object.ObjTypeInt:
- value, err = NewByteArrayFromObj(obj)
- if err != nil {
- return diceerrors.NewErrWithMessage("value is not a valid byte array")
- }
- default:
- return diceerrors.NewErrWithFormattedMessage(diceerrors.WrongTypeErr)
- }
-
- result := executeBitfieldOps(value, ops)
- return clientio.Encode(result, false)
-}
-
-// evalBITFIELD evaluates BITFIELD operations on a key store string, int or bytearray types
-// it returns an array of results depending on the subcommands
-// it allows mutation using SET and INCRBY commands
-// returns arity error, offset type error, overflow type error, encoding type error, integer error, syntax error
-// GET -- Returns the specified bit field.
-// SET -- Set the specified bit field
-// and returns its old value.
-// INCRBY -- Increments or decrements
-// (if a negative increment is given) the specified bit field and returns the new value.
-// There is another subcommand that only changes the behavior of successive
-// INCRBY and SET subcommands calls by setting the overflow behavior:
-// OVERFLOW [WRAP|SAT|FAIL]`
-func evalBITFIELD(args []string, store *dstore.Store) []byte {
- if len(args) < 1 {
- return diceerrors.NewErrArity("BITFIELD")
- }
-
- return bitfieldEvalGeneric(args, store, false)
-}
-
-// Read-only variant of the BITFIELD command. It is like the original BITFIELD but only accepts GET subcommand and can safely be used in read-only replicas.
-func evalBITFIELDRO(args []string, store *dstore.Store) []byte {
- if len(args) < 1 {
- return diceerrors.NewErrArity("BITFIELD_RO")
- }
-
- return bitfieldEvalGeneric(args, store, true)
-}
func evalGEOADD(args []string, store *dstore.Store) []byte {
if len(args) < 4 {
return diceerrors.NewErrArity("GEOADD")
diff --git a/internal/eval/eval_test.go b/internal/eval/eval_test.go
index 29d7f4974..7ff45c31c 100644
--- a/internal/eval/eval_test.go
+++ b/internal/eval/eval_test.go
@@ -86,12 +86,14 @@ func TestEval(t *testing.T) {
testEvalHEXISTS(t, store)
testEvalHDEL(t, store)
testEvalHSCAN(t, store)
+ testEvalPFMERGE(t, store)
testEvalJSONSTRLEN(t, store)
testEvalJSONOBJLEN(t, store)
testEvalHLEN(t, store)
testEvalSELECT(t, store)
testEvalLLEN(t, store)
testEvalGETDEL(t, store)
+ testEvalGETEX(t, store)
testEvalJSONNUMINCRBY(t, store)
testEvalDUMP(t, store)
testEvalTYPE(t, store)
@@ -107,6 +109,10 @@ func TestEval(t *testing.T) {
testEvalBITOP(t, store)
testEvalAPPEND(t, store)
testEvalHRANDFIELD(t, store)
+ testEvalSADD(t, store)
+ testEvalSREM(t, store)
+ testEvalSCARD(t, store)
+ testEvalSMEMBERS(t, store)
testEvalZADD(t, store)
testEvalZRANGE(t, store)
testEvalZPOPMAX(t, store)
@@ -114,6 +120,8 @@ func TestEval(t *testing.T) {
testEvalZRANK(t, store)
testEvalZCARD(t, store)
testEvalZREM(t, store)
+ testEvalZADD(t, store)
+ testEvalZRANGE(t, store)
testEvalHVALS(t, store)
testEvalBitField(t, store)
testEvalHINCRBYFLOAT(t, store)
@@ -131,6 +139,9 @@ func TestEval(t *testing.T) {
testEvalBFINFO(t, store)
testEvalBFEXISTS(t, store)
testEvalBFADD(t, store)
+ testEvalLINSERT(t, store)
+ testEvalLRANGE(t, store)
+ testEvalLPOP(t, store)
}
func testEvalPING(t *testing.T, store *dstore.Store) {
@@ -176,204 +187,129 @@ func testEvalHELLO(t *testing.T, store *dstore.Store) {
}
func testEvalSET(t *testing.T, store *dstore.Store) {
- tests := []evalTestCase{
- {
- name: "nil value",
+ tests := map[string]evalTestCase{
+ "nil value": {
input: nil,
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'set' command")},
},
- {
- name: "empty array",
+ "empty array": {
input: []string{},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'set' command")},
},
- {
- name: "one value",
+ "one value": {
input: []string{"KEY"},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR wrong number of arguments for 'set' command")},
},
- {
- name: "key val pair",
+ "key val pair": {
input: []string{"KEY", "VAL"},
migratedOutput: EvalResponse{Result: clientio.OK, Error: nil},
},
- {
- name: "key val pair with int val",
+ "key val pair with int val": {
input: []string{"KEY", "123456"},
migratedOutput: EvalResponse{Result: clientio.OK, Error: nil},
},
- {
- name: "key val pair and expiry key",
+ "key val pair and expiry key": {
input: []string{"KEY", "VAL", Px},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR syntax error")},
},
- {
- name: "key val pair and EX no val",
+ "key val pair and EX no val": {
input: []string{"KEY", "VAL", Ex},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR syntax error")},
},
- {
- name: "key val pair and valid EX",
+ "key val pair and valid EX": {
input: []string{"KEY", "VAL", Ex, "2"},
migratedOutput: EvalResponse{Result: clientio.OK, Error: nil},
},
- {
- name: "key val pair and invalid negative EX",
+ "key val pair and invalid negative EX": {
input: []string{"KEY", "VAL", Ex, "-2"},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR invalid expire time in 'set' command")},
},
- {
- name: "key val pair and invalid float EX",
+ "key val pair and invalid float EX": {
input: []string{"KEY", "VAL", Ex, "2.0"},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR value is not an integer or out of range")},
},
- {
- name: "key val pair and invalid out of range int EX",
+ "key val pair and invalid out of range int EX": {
input: []string{"KEY", "VAL", Ex, "9223372036854775807"},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR invalid expire time in 'set' command")},
},
- {
- name: "key val pair and invalid greater than max duration EX",
+ "key val pair and invalid greater than max duration EX": {
input: []string{"KEY", "VAL", Ex, "9223372036854775"},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR invalid expire time in 'set' command")},
},
- {
- name: "key val pair and invalid EX",
+ "key val pair and invalid EX": {
input: []string{"KEY", "VAL", Ex, "invalid_expiry_val"},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR value is not an integer or out of range")},
},
- {
- name: "key val pair and PX no val",
+ "key val pair and PX no val": {
input: []string{"KEY", "VAL", Px},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR syntax error")},
},
- {
- name: "key val pair and valid PX",
+ "key val pair and valid PX": {
input: []string{"KEY", "VAL", Px, "2000"},
migratedOutput: EvalResponse{Result: clientio.OK, Error: nil},
},
- {
- name: "key val pair and invalid PX",
+ "key val pair and invalid PX": {
input: []string{"KEY", "VAL", Px, "invalid_expiry_val"},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR value is not an integer or out of range")},
},
- {
- name: "key val pair and invalid negative PX",
+ "key val pair and invalid negative PX": {
input: []string{"KEY", "VAL", Px, "-2"},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR invalid expire time in 'set' command")},
},
- {
- name: "key val pair and invalid float PX",
+ "key val pair and invalid float PX": {
input: []string{"KEY", "VAL", Px, "2.0"},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR value is not an integer or out of range")},
},
- {
- name: "key val pair and invalid out of range int PX",
+
+ "key val pair and invalid out of range int PX": {
input: []string{"KEY", "VAL", Px, "9223372036854775807"},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR invalid expire time in 'set' command")},
},
- {
- name: "key val pair and invalid greater than max duration PX",
+ "key val pair and invalid greater than max duration PX": {
input: []string{"KEY", "VAL", Px, "9223372036854775"},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR invalid expire time in 'set' command")},
},
- {
- name: "key val pair and both EX and PX",
+ "key val pair and both EX and PX": {
input: []string{"KEY", "VAL", Ex, "2", Px, "2000"},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR syntax error")},
},
- {
- name: "key val pair and PXAT no val",
+ "key val pair and PXAT no val": {
input: []string{"KEY", "VAL", Pxat},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR syntax error")},
},
- {
- name: "key val pair and invalid PXAT",
+ "key val pair and invalid PXAT": {
input: []string{"KEY", "VAL", Pxat, "invalid_expiry_val"},
migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR value is not an integer or out of range")},
},
- {
- name: "key val pair and expired PXAT",
- input: []string{"KEY", "VAL", Pxat, "2"},
- migratedOutput: EvalResponse{Result: clientio.OK, Error: nil},
- },
- {
- name: "key val pair and negative PXAT",
- input: []string{"KEY", "VAL", Pxat, "-123456"},
- migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR invalid expire time in 'set' command")},
- },
- {
- name: "key val pair and valid PXAT",
- input: []string{"KEY", "VAL", Pxat, strconv.FormatInt(time.Now().Add(2*time.Minute).UnixMilli(), 10)},
- migratedOutput: EvalResponse{Result: clientio.OK, Error: nil},
- },
- {
- name: "key val pair and invalid EX and PX",
- input: []string{"KEY", "VAL", Ex, "2", Px, "2000"},
- migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR syntax error")},
- },
- {
- name: "key val pair and invalid EX and PXAT",
- input: []string{"KEY", "VAL", Ex, "2", Pxat, "2"},
- migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR syntax error")},
- },
- {
- name: "key val pair and invalid PX and PXAT",
- input: []string{"KEY", "VAL", Px, "2000", Pxat, "2"},
- migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR syntax error")},
- },
- {
- name: "key val pair and KeepTTL",
- input: []string{"KEY", "VAL", KeepTTL},
- migratedOutput: EvalResponse{Result: clientio.OK, Error: nil},
- },
- {
- name: "key val pair and invalid KeepTTL",
- input: []string{"KEY", "VAL", KeepTTL, "2"},
- migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR syntax error")},
- },
- {
- name: "key val pair and KeepTTL, EX",
- input: []string{"KEY", "VAL", Ex, "2", KeepTTL},
- migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR syntax error")},
- },
- {
- name: "key val pair and KeepTTL, PX",
- input: []string{"KEY", "VAL", Px, "2000", KeepTTL},
- migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR syntax error")},
+ "key val with get": {
+ input: []string{"key", "bazz", "GET"},
+ setup: func() {
+ key := "key"
+ value := "bar"
+ obj := store.NewObj(value, -1, object.ObjTypeString, object.ObjEncodingEmbStr)
+ store.Put(key, obj)
+ },
+ migratedOutput: EvalResponse{Result: "bar", Error: nil},
},
- {
- name: "key val pair and KeepTTL, PXAT",
- input: []string{"KEY", "VAL", Pxat, strconv.FormatInt(time.Now().Add(2*time.Minute).UnixMilli(), 10), KeepTTL},
- migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR syntax error")},
+ "key val with get and nil get": {
+ input: []string{"key", "bar", "GET"},
+ migratedOutput: EvalResponse{Result: clientio.NIL, Error: nil},
},
- {
- name: "key val pair and KeepTTL, invalid PXAT",
- input: []string{"KEY", "VAL", Pxat, "invalid_expiry_val", KeepTTL},
- migratedOutput: EvalResponse{Result: nil, Error: errors.New("ERR value is not an integer or out of range")},
+ "key val with get and but value is json": {
+ input: []string{"key", "bar", "GET"},
+ setup: func() {
+ key := "key"
+ value := "{\"a\":2}"
+ var rootData interface{}
+ _ = sonic.Unmarshal([]byte(value), &rootData)
+ obj := store.NewObj(rootData, -1, object.ObjTypeJSON, object.ObjEncodingJSON)
+ store.Put(key, obj)
+ },
+ migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrWrongTypeOperation},
},
}
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- response := evalSET(tt.input, store)
-
- // Handle comparison for byte slices
- if b, ok := response.Result.([]byte); ok && tt.migratedOutput.Result != nil {
- if expectedBytes, ok := tt.migratedOutput.Result.([]byte); ok {
- assert.True(t, bytes.Equal(b, expectedBytes), "expected and actual byte slices should be equal")
- }
- } else {
- assert.Equal(t, tt.migratedOutput.Result, response.Result)
- }
-
- if tt.migratedOutput.Error != nil {
- assert.EqualError(t, response.Error, tt.migratedOutput.Error.Error())
- } else {
- assert.NoError(t, response.Error)
- }
- })
- }
+ runMigratedEvalTests(t, tests, evalSET, store)
}
func testEvalGETEX(t *testing.T, store *dstore.Store) {
@@ -3254,15 +3190,12 @@ func testEvalHVALS(t *testing.T, store *dstore.Store) {
response := evalHVALS(tt.input, store)
- fmt.Printf("Eval Response: %v\n", response)
-
// Handle comparison for byte slices
if responseBytes, ok := response.Result.([]byte); ok && tt.migratedOutput.Result != nil {
if expectedBytes, ok := tt.migratedOutput.Result.([]byte); ok {
assert.True(t, bytes.Equal(responseBytes, expectedBytes), "expected and actual byte slices should be equal")
}
} else {
- fmt.Printf("G1: %v | %v\n", response.Result, tt.migratedOutput.Result)
switch e := tt.migratedOutput.Result.(type) {
case []interface{}, []string:
assert.ElementsMatch(t, e, response.Result)
@@ -3776,6 +3709,57 @@ func testEvalLLEN(t *testing.T, store *dstore.Store) {
runEvalTests(t, tests, evalLLEN, store)
}
+func testEvalLPOP(t *testing.T, store *dstore.Store) {
+ tests := map[string]evalTestCase{
+ "empty args": {
+ input: nil,
+ output: []byte("-ERR wrong number of arguments for 'lpop' command\r\n"),
+ },
+ "more than 2 args": {
+ input: []string{"k", "2", "3"},
+ output: []byte("-ERR wrong number of arguments for 'lpop' command\r\n"),
+ },
+ "pop one element": {
+ setup: func() {
+ evalRPUSH([]string{"k", "v1", "v2", "v3", "v4"}, store)
+ },
+ input: []string{"k"},
+ output: []byte("$2\r\nv1\r\n"),
+ },
+ "pop two elements": {
+ setup: func() {
+ evalRPUSH([]string{"k", "v1", "v2", "v3", "v4"}, store)
+ },
+ input: []string{"k", "2"},
+ output: []byte("*2\r\n$2\r\nv1\r\n$2\r\nv2\r\n")},
+ "pop more elements than available": {
+ setup: func() {
+ evalRPUSH([]string{"k", "v1", "v2"}, store)
+ },
+ input: []string{"k", "5"},
+ output: []byte("*2\r\n$2\r\nv1\r\n$2\r\nv2\r\n")},
+ "pop 0 elements": {
+ setup: func() {
+ evalRPUSH([]string{"k", "v1", "v2"}, store)
+ },
+ input: []string{"k", "0"},
+ output: []byte("*0\r\n")},
+ "negative count": {
+ input: []string{"k", "-1"},
+ output: []byte("-ERR value is out of range\r\n"),
+ },
+ "non-integer count": {
+ input: []string{"k", "abc"},
+ output: []byte("-ERR value is not an integer or out of range\r\n"),
+ },
+ "key does not exist": {
+ input: []string{"nonexistent_key"},
+ output: []byte("$-1\r\n"),
+ },
+ }
+ runEvalTests(t, tests, evalLPOP, store)
+}
+
func testEvalJSONNUMINCRBY(t *testing.T, store *dstore.Store) {
tests := map[string]evalTestCase{
"incr on numeric field": {
@@ -6093,6 +6077,130 @@ func BenchmarkEvalINCRBYFLOAT(b *testing.B) {
}
}
+// TODO: BITOP has not been migrated yet. Once done, we can uncomment the tests - please check accuracy and validate for expected values.
+
+// func testEvalBITOP(t *testing.T, store *dstore.Store) {
+// tests := map[string]evalTestCase{
+// "BITOP NOT (empty string)": {
+// setup: func() {
+// store.Put("s{t}", store.NewObj(&ByteArray{data: []byte("")}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+// },
+// input: []string{"NOT", "dest{t}", "s{t}"},
+// migratedOutput: EvalResponse{Result: clientio.IntegerZero, Error: nil},
+// newValidator: func(output interface{}) {
+// expectedResult := []byte{}
+// assert.Equal(t, expectedResult, store.Get("dest{t}").Value.(*ByteArray).data)
+// },
+// },
+// "BITOP NOT (known string)": {
+// setup: func() {
+// store.Put("s{t}", store.NewObj(&ByteArray{data: []byte{0xaa, 0x00, 0xff, 0x55}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+// },
+// input: []string{"NOT", "dest{t}", "s{t}"},
+// migratedOutput: EvalResponse{Result: 4, Error: nil},
+// newValidator: func(output interface{}) {
+// expectedResult := []byte{0x55, 0xff, 0x00, 0xaa}
+// assert.Equal(t, expectedResult, store.Get("dest{t}").Value.(*ByteArray).data)
+// },
+// },
+// "BITOP where dest and target are the same key": {
+// setup: func() {
+// store.Put("s", store.NewObj(&ByteArray{data: []byte{0xaa, 0x00, 0xff, 0x55}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+// },
+// input: []string{"NOT", "s", "s"},
+// migratedOutput: EvalResponse{Result: 4, Error: nil},
+// newValidator: func(output interface{}) {
+// expectedResult := []byte{0x55, 0xff, 0x00, 0xaa}
+// assert.Equal(t, expectedResult, store.Get("s").Value.(*ByteArray).data)
+// },
+// },
+// "BITOP AND|OR|XOR don't change the string with single input key": {
+// setup: func() {
+// store.Put("a{t}", store.NewObj(&ByteArray{data: []byte{0x01, 0x02, 0xff}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+// },
+// input: []string{"AND", "res1{t}", "a{t}"},
+// migratedOutput: EvalResponse{Result: 3, Error: nil},
+// newValidator: func(output interface{}) {
+// expectedResult := []byte{0x01, 0x02, 0xff}
+// assert.Equal(t, expectedResult, store.Get("res1{t}").Value.(*ByteArray).data)
+// },
+// },
+// "BITOP missing key is considered a stream of zero": {
+// setup: func() {
+// store.Put("a{t}", store.NewObj(&ByteArray{data: []byte{0x01, 0x02, 0xff}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+// },
+// input: []string{"AND", "res1{t}", "no-such-key{t}", "a{t}"},
+// migratedOutput: EvalResponse{Result: 3, Error: nil},
+// newValidator: func(output interface{}) {
+// expectedResult := []byte{0x00, 0x00, 0x00}
+// assert.Equal(t, expectedResult, store.Get("res1{t}").Value.(*ByteArray).data)
+// },
+// },
+// "BITOP shorter keys are zero-padded to the key with max length": {
+// setup: func() {
+// store.Put("a{t}", store.NewObj(&ByteArray{data: []byte{0x01, 0x02, 0xff, 0xff}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+// store.Put("b{t}", store.NewObj(&ByteArray{data: []byte{0x01, 0x02, 0xff}}, maxExDuration, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+// },
+// input: []string{"AND", "res1{t}", "a{t}", "b{t}"},
+// migratedOutput: EvalResponse{Result: 4, Error: nil},
+// newValidator: func(output interface{}) {
+// expectedResult := []byte{0x01, 0x02, 0xff, 0x00}
+// assert.Equal(t, expectedResult, store.Get("res1{t}").Value.(*ByteArray).data)
+// },
+// },
+// "BITOP with non string source key": {
+// setup: func() {
+// store.Put("a{t}", store.NewObj("1", maxExDuration, object.ObjTypeString, object.ObjEncodingRaw))
+// store.Put("b{t}", store.NewObj("2", maxExDuration, object.ObjTypeString, object.ObjEncodingRaw))
+// store.Put("c{t}", store.NewObj([]byte("foo"), maxExDuration, object.ObjTypeByteList, object.ObjEncodingRaw))
+// },
+// input: []string{"XOR", "dest{t}", "a{t}", "b{t}", "c{t}", "d{t}"},
+// migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrWrongTypeOperation},
+// },
+// "BITOP with empty string after non empty string": {
+// setup: func() {
+// store.Put("a{t}", store.NewObj(&ByteArray{data: []byte("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")}, -1, object.ObjTypeByteArray, object.ObjEncodingByteArray))
+// },
+// input: []string{"OR", "x{t}", "a{t}", "b{t}"},
+// migratedOutput: EvalResponse{Result: 32, Error: nil},
+// },
+// }
+
+// //runEvalTests(t, tests, evalBITOP, store)
+// for _, tt := range tests {
+// t.Run(tt.name, func(t *testing.T) {
+
+// if tt.setup != nil {
+// tt.setup()
+// }
+// response := evalBITOP(tt.input, store)
+
+// if tt.newValidator != nil {
+// if tt.migratedOutput.Error != nil {
+// tt.newValidator(tt.migratedOutput.Error)
+// } else {
+// tt.newValidator(response.Result)
+// }
+// } else {
+// // Handle comparison for byte slices
+// if b, ok := response.Result.([]byte); ok && tt.migratedOutput.Result != nil {
+// if expectedBytes, ok := tt.migratedOutput.Result.([]byte); ok {
+// assert.True(t, bytes.Equal(b, expectedBytes), "expected and actual byte slices should be equal")
+// }
+// } else {
+// assert.Equal(t, tt.migratedOutput.Result, response.Result)
+// }
+
+// if tt.migratedOutput.Error != nil {
+// assert.EqualError(t, response.Error, tt.migratedOutput.Error.Error())
+// } else {
+// assert.NoError(t, response.Error)
+// }
+// }
+// })
+// }
+// }
+
func testEvalBITOP(t *testing.T, store *dstore.Store) {
tests := map[string]evalTestCase{
"BITOP NOT (empty string)": {
@@ -6628,6 +6736,246 @@ func testEvalJSONRESP(t *testing.T, store *dstore.Store) {
runEvalTests(t, tests, evalJSONRESP, store)
}
+func testEvalSADD(t *testing.T, store *dstore.Store) {
+ tests := map[string]evalTestCase{
+ "SADD with wrong number of arguments": {
+ input: []string{},
+ migratedOutput: EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongArgumentCount("SADD"),
+ },
+ },
+ "SADD new member to non existing key": {
+ input: []string{"myset", "member"},
+ migratedOutput: EvalResponse{
+ Result: 1,
+ Error: nil,
+ },
+ },
+ "SADD new members to non existing key": {
+ input: []string{"myset", "member1", "member2"},
+ migratedOutput: EvalResponse{
+ Result: 2,
+ Error: nil,
+ },
+ },
+ "SADD new member to existing key": {
+ setup: func() {
+ evalSADD([]string{"myset", "member1"}, store)
+ },
+ input: []string{"myset", "member2"},
+ migratedOutput: EvalResponse{
+ Result: 1,
+ Error: nil,
+ },
+ },
+ "SADD existing member to existing key": {
+ setup: func() {
+ evalSADD([]string{"myset", "member1"}, store)
+ },
+ input: []string{"myset", "member1"},
+ migratedOutput: EvalResponse{
+ Result: 0,
+ Error: nil,
+ },
+ },
+ "SADD new and existing member to existing key": {
+ setup: func() {
+ evalSADD([]string{"myset", "member1"}, store)
+ },
+ input: []string{"myset", "member1", "member2", "member3"},
+ migratedOutput: EvalResponse{
+ Result: 2,
+ Error: nil,
+ },
+ },
+ "SADD member to key with invalid type": {
+ setup: func() {
+ evalSET([]string{"key", "value"}, store)
+ },
+ input: []string{"key", "member"},
+ migratedOutput: EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ },
+ },
+ }
+
+ runMigratedEvalTests(t, tests, evalSADD, store)
+}
+
+func testEvalSREM(t *testing.T, store *dstore.Store) {
+ tests := map[string]evalTestCase{
+ "SREM with wrong number of arguments": {
+ input: []string{},
+ migratedOutput: EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongArgumentCount("SREM"),
+ },
+ },
+ "SREM on key with invalid type": {
+ setup: func() {
+ evalSET([]string{"key", "value"}, store)
+ },
+ input: []string{"key", "member"},
+ migratedOutput: EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ },
+ },
+ "SREM with non existing key": {
+ input: []string{"myset", "member"},
+ migratedOutput: EvalResponse{
+ Result: 0,
+ Error: nil,
+ },
+ },
+ "SREM on existing key with existing member": {
+ setup: func() {
+ evalSADD([]string{"myset", "a"}, store)
+ },
+ input: []string{"myset", "a"},
+ migratedOutput: EvalResponse{
+ Result: 1,
+ Error: nil,
+ },
+ },
+ "SREM on existing key with not existing member": {
+ setup: func() {
+ evalSADD([]string{"myset", "a"}, store)
+ },
+ input: []string{"myset", "b"},
+ migratedOutput: EvalResponse{
+ Result: 0,
+ Error: nil,
+ },
+ },
+ "SREM on existing key with existing and not existing members": {
+ setup: func() {
+ evalSADD([]string{"myset", "a"}, store)
+ },
+ input: []string{"myset", "a", "b"},
+ migratedOutput: EvalResponse{
+ Result: 1,
+ Error: nil,
+ },
+ },
+ "SREM on existing key with repeated existing members": {
+ setup: func() {
+ evalSADD([]string{"myset", "a"}, store)
+ },
+ input: []string{"myset", "a", "b", "a"},
+ migratedOutput: EvalResponse{
+ Result: 1,
+ Error: nil,
+ },
+ },
+ }
+
+ runMigratedEvalTests(t, tests, evalSREM, store)
+}
+
+func testEvalSCARD(t *testing.T, store *dstore.Store) {
+ tests := map[string]evalTestCase{
+ "SCARD with wrong number of arguments": {
+ input: []string{"mykey", "value"},
+ migratedOutput: EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongArgumentCount("SCARD"),
+ },
+ },
+ "SCARD on key with invalid type": {
+ setup: func() {
+ evalSET([]string{"mykey", "value"}, store)
+ },
+ input: []string{"mykey"},
+ migratedOutput: EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ },
+ },
+ "SCARD with non existing key": {
+ input: []string{"mykey"},
+ migratedOutput: EvalResponse{
+ Result: 0,
+ Error: nil,
+ },
+ },
+ "SCARD with existing key and no member": {
+ setup: func() {
+ evalSADD([]string{"mykey"}, store)
+ },
+ input: []string{"mykey"},
+ migratedOutput: EvalResponse{
+ Result: 0,
+ Error: nil,
+ },
+ },
+ "SCARD with existing key": {
+ setup: func() {
+ evalSADD([]string{"mykey", "a", "b"}, store)
+ },
+ input: []string{"mykey"},
+ migratedOutput: EvalResponse{
+ Result: 2,
+ Error: nil,
+ },
+ },
+ }
+
+ runMigratedEvalTests(t, tests, evalSCARD, store)
+}
+
+func testEvalSMEMBERS(t *testing.T, store *dstore.Store) {
+ tests := map[string]evalTestCase{
+ "SMEMBERS with wrong number of arguments": {
+ input: []string{"mykey", "mykey"},
+ migratedOutput: EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongArgumentCount("SMEMBERS"),
+ },
+ },
+ "SMEMBERS on key with invalid type": {
+ setup: func() {
+ evalSET([]string{"mykey", "value"}, store)
+ },
+ input: []string{"mykey"},
+ migratedOutput: EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ },
+ },
+ "SMEMBERS with non existing key": {
+ input: []string{"mykey"},
+ migratedOutput: EvalResponse{
+ Result: []string{},
+ Error: nil,
+ },
+ },
+ "SMEMBERS with existing key": {
+ setup: func() {
+ evalSADD([]string{"mykey", "a", "b"}, store)
+ },
+ input: []string{"mykey"},
+ migratedOutput: EvalResponse{
+ Result: []string{"a", "b"},
+ Error: nil,
+ },
+ },
+ "SMEMBERS with existing key and no members": {
+ setup: func() {
+ evalSADD([]string{"mykey"}, store)
+ },
+ input: []string{"mykey"},
+ migratedOutput: EvalResponse{
+ Result: []string{},
+ Error: nil,
+ },
+ },
+ }
+ runMigratedEvalTests(t, tests, evalSMEMBERS, store)
+}
+
func testEvalZADD(t *testing.T, store *dstore.Store) {
tests := map[string]evalTestCase{
"ZADD with wrong number of arguments": {
@@ -7237,51 +7585,61 @@ func testEvalZCARD(t *testing.T, store *dstore.Store) {
func testEvalBitField(t *testing.T, store *dstore.Store) {
testCases := map[string]evalTestCase{
"BITFIELD signed SET": {
- input: []string{"bits", "set", "i8", "0", "-100"},
- output: clientio.Encode([]int64{0}, false),
+ input: []string{"bits", "set", "i8", "0", "-100"},
+ migratedOutput: EvalResponse{
+ Result: []interface{}{int64(0)},
+ Error: nil,
+ },
},
"BITFIELD GET": {
setup: func() {
args := []string{"bits", "set", "u8", "0", "255"}
evalBITFIELD(args, store)
},
- input: []string{"bits", "get", "u8", "0"},
- output: clientio.Encode([]int64{255}, false),
+ input: []string{"bits", "get", "u8", "0"},
+ migratedOutput: EvalResponse{
+ Result: []interface{}{int64(255)},
+ Error: nil,
+ },
},
"BITFIELD INCRBY": {
setup: func() {
args := []string{"bits", "set", "u8", "0", "255"}
evalBITFIELD(args, store)
},
- input: []string{"bits", "incrby", "u8", "0", "100"},
- output: clientio.Encode([]int64{99}, false),
+ input: []string{"bits", "incrby", "u8", "0", "100"},
+ migratedOutput: EvalResponse{
+ Result: []interface{}{int64(99)},
+ Error: nil,
+ },
},
"BITFIELD Arity": {
- input: []string{},
- output: diceerrors.NewErrArity("BITFIELD"),
+ input: []string{},
+ migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrWrongArgumentCount("BITFIELD")},
},
"BITFIELD invalid combination of commands in a single operation": {
- input: []string{"bits", "SET", "u8", "0", "255", "INCRBY", "u8", "0", "100", "GET", "u8"},
- output: []byte("-ERR syntax error\r\n"),
+ input: []string{"bits", "SET", "u8", "0", "255", "INCRBY", "u8", "0", "100", "GET", "u8"},
+ migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrSyntax},
},
"BITFIELD invalid bitfield type": {
- input: []string{"bits", "SET", "a8", "0", "255", "INCRBY", "u8", "0", "100", "GET", "u8"},
- output: []byte("-ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is.\r\n"),
+ input: []string{"bits", "SET", "a8", "0", "255", "INCRBY", "u8", "0", "100", "GET", "u8"},
+ migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrGeneral("Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is")},
},
"BITFIELD invalid bit offset": {
- input: []string{"bits", "SET", "u8", "a", "255", "INCRBY", "u8", "0", "100", "GET", "u8"},
- output: []byte("-ERR bit offset is not an integer or out of range\r\n"),
+ input: []string{"bits", "SET", "u8", "a", "255", "INCRBY", "u8", "0", "100", "GET", "u8"},
+ migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrGeneral("bit offset is not an integer or out of range")},
},
"BITFIELD invalid overflow type": {
- input: []string{"bits", "SET", "u8", "0", "255", "INCRBY", "u8", "0", "100", "OVERFLOW", "wraap"},
- output: []byte("-ERR Invalid OVERFLOW type specified\r\n"),
+ input: []string{"bits", "SET", "u8", "0", "255", "INCRBY", "u8", "0", "100", "OVERFLOW", "wraap"},
+ migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrGeneral("Invalid OVERFLOW type specified")},
},
"BITFIELD missing arguments in SET": {
- input: []string{"bits", "SET", "u8", "0", "INCRBY", "u8", "0", "100", "GET", "u8", "288"},
- output: []byte("-ERR value is not an integer or out of range\r\n"),
+ input: []string{"bits", "SET", "u8", "0", "INCRBY", "u8", "0", "100", "GET", "u8", "288"},
+ migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrIntegerOutOfRange},
},
}
- runEvalTests(t, testCases, evalBITFIELD, store)
+
+ runMigratedEvalTests(t, testCases, evalBITFIELD, store)
}
func testEvalHINCRBYFLOAT(t *testing.T, store *dstore.Store) {
@@ -7521,23 +7879,27 @@ func testEvalDUMP(t *testing.T, store *dstore.Store) {
func testEvalBitFieldRO(t *testing.T, store *dstore.Store) {
testCases := map[string]evalTestCase{
"BITFIELD_RO Arity": {
- input: []string{},
- output: diceerrors.NewErrArity("BITFIELD_RO"),
+ input: []string{},
+ migratedOutput: EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongArgumentCount("BITFIELD_RO"),
+ },
},
"BITFIELD_RO syntax error": {
- input: []string{"bits", "GET", "u8"},
- output: []byte("-ERR syntax error\r\n"),
+ input: []string{"bits", "GET", "u8"},
+ migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrSyntax},
},
"BITFIELD_RO invalid bitfield type": {
- input: []string{"bits", "GET", "a8", "0", "255"},
- output: []byte("-ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is.\r\n"),
+ input: []string{"bits", "GET", "a8", "0", "255"},
+ migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrGeneral("Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is")},
},
"BITFIELD_RO unsupported commands": {
- input: []string{"bits", "set", "u8", "0", "255"},
- output: []byte("-ERR BITFIELD_RO only supports the GET subcommand\r\n"),
+ input: []string{"bits", "set", "u8", "0", "255"},
+ migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrGeneral("BITFIELD_RO only supports the GET subcommand")},
},
}
- runEvalTests(t, testCases, evalBITFIELDRO, store)
+
+ runMigratedEvalTests(t, testCases, evalBITFIELDRO, store)
}
func testEvalGEOADD(t *testing.T, store *dstore.Store) {
@@ -8493,3 +8855,85 @@ func testEvalBFEXISTS(t *testing.T, store *dstore.Store) {
})
}
}
+
+func testEvalLINSERT(t *testing.T, store *dstore.Store) {
+ tests := map[string]evalTestCase{
+ "nil value": {
+ input: nil,
+ migratedOutput: EvalResponse{Result: nil, Error: errors.New("-wrong number of arguments for LINSERT")},
+ },
+ "empty args": {
+ input: []string{},
+ migratedOutput: EvalResponse{Result: nil, Error: errors.New("-wrong number of arguments for LINSERT")},
+ },
+ "wrong number of args": {
+ input: []string{"KEY1", "KEY2"},
+ migratedOutput: EvalResponse{Result: nil, Error: errors.New("-wrong number of arguments for LINSERT")},
+ },
+ "key does not exist": {
+ input: []string{"NONEXISTENT_KEY", "before", "pivot", "element"},
+ migratedOutput: EvalResponse{Result: 0, Error: nil},
+ },
+ "key exists": {
+ setup: func() {
+ evalLPUSH([]string{"EXISTING_KEY", "mock_value"}, store)
+ },
+ input: []string{"EXISTING_KEY", "before", "mock_value", "element"},
+ migratedOutput: EvalResponse{Result: int64(2), Error: nil},
+ },
+ "key with different type": {
+ setup: func() {
+ evalSET([]string{"EXISTING_KEY", "mock_value"}, store)
+ },
+ input: []string{"EXISTING_KEY", "before", "mock_value", "element"},
+ migratedOutput: EvalResponse{Result: nil, Error: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")},
+ },
+ }
+ runMigratedEvalTests(t, tests, evalLINSERT, store)
+}
+
+func testEvalLRANGE(t *testing.T, store *dstore.Store) {
+ tests := map[string]evalTestCase{
+ "nil value": {
+ input: nil,
+ migratedOutput: EvalResponse{Result: nil, Error: errors.New("-wrong number of arguments for LRANGE")},
+ },
+ "empty args": {
+ input: []string{},
+ migratedOutput: EvalResponse{Result: nil, Error: errors.New("-wrong number of arguments for LRANGE")},
+ },
+ "wrong number of args": {
+ input: []string{"KEY1"},
+ migratedOutput: EvalResponse{Result: nil, Error: errors.New("-wrong number of arguments for LRANGE")},
+ },
+ "invalid start": {
+ input: []string{"KEY1", "014f", "2"},
+ migratedOutput: EvalResponse{Result: nil, Error: errors.New("-ERR value is not an integer or out of range")},
+ },
+ "invalid stop": {
+ input: []string{"KEY1", "2", "f2"},
+ migratedOutput: EvalResponse{Result: nil, Error: errors.New("-ERR value is not an integer or out of range")},
+ },
+ "key does not exist": {
+ input: []string{"NONEXISTENT_KEY", "2", "4"},
+ migratedOutput: EvalResponse{Result: []string{}, Error: nil},
+ },
+ "key exists": {
+ setup: func() {
+ evalLPUSH([]string{"EXISTING_KEY", "pivot_value"}, store)
+ evalLINSERT([]string{"EXISTING_KEY", "before", "pivot_value", "before_value"}, store)
+ evalLINSERT([]string{"EXISTING_KEY", "after", "pivot_value", "after_value"}, store)
+ },
+ input: []string{"EXISTING_KEY", "0", "5"},
+ migratedOutput: EvalResponse{Result: []string{"before_value", "pivot_value", "after_value"}, Error: nil},
+ },
+ "key with different type": {
+ setup: func() {
+ evalSET([]string{"EXISTING_KEY", "mock_value"}, store)
+ },
+ input: []string{"EXISTING_KEY", "0", "4"},
+ migratedOutput: EvalResponse{Result: nil, Error: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")},
+ },
+ }
+ runMigratedEvalTests(t, tests, evalLRANGE, store)
+}
diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go
index 06c036cb8..440d974b2 100644
--- a/internal/eval/store_eval.go
+++ b/internal/eval/store_eval.go
@@ -1,8 +1,10 @@
package eval
import (
+ "errors"
"fmt"
"math"
+ "math/bits"
"sort"
"strconv"
"strings"
@@ -182,16 +184,14 @@ func evalEXPIRETIME(args []string, store *dstore.Store) *EvalResponse {
// If the key already exists then the value will be overwritten and expiry will be discarded
func evalSET(args []string, store *dstore.Store) *EvalResponse {
if len(args) <= 1 {
- return &EvalResponse{
- Result: nil,
- Error: diceerrors.ErrWrongArgumentCount("SET"),
- }
+ return makeEvalError(diceerrors.ErrWrongArgumentCount("SET"))
}
var key, value string
var exDurationMs int64 = -1
var state exDurationState = Uninitialized
var keepttl bool = false
+ var oldVal *interface{}
key, value = args[0], args[1]
oType, oEnc := deduceTypeEncoding(value)
@@ -201,38 +201,23 @@ func evalSET(args []string, store *dstore.Store) *EvalResponse {
switch arg {
case Ex, Px:
if state != Uninitialized {
- return &EvalResponse{
- Result: nil,
- Error: diceerrors.ErrSyntax,
- }
+ return makeEvalError(diceerrors.ErrSyntax)
}
if keepttl {
- return &EvalResponse{
- Result: nil,
- Error: diceerrors.ErrSyntax,
- }
+ return makeEvalError(diceerrors.ErrSyntax)
}
i++
if i == len(args) {
- return &EvalResponse{
- Result: nil,
- Error: diceerrors.ErrSyntax,
- }
+ return makeEvalError(diceerrors.ErrSyntax)
}
exDuration, err := strconv.ParseInt(args[i], 10, 64)
if err != nil {
- return &EvalResponse{
- Result: nil,
- Error: diceerrors.ErrIntegerOutOfRange,
- }
+ return makeEvalError(diceerrors.ErrIntegerOutOfRange)
}
if exDuration <= 0 || exDuration >= maxExDuration {
- return &EvalResponse{
- Result: nil,
- Error: diceerrors.ErrInvalidExpireTime("SET"),
- }
+ return makeEvalError(diceerrors.ErrInvalidExpireTime("SET"))
}
// converting seconds to milliseconds
@@ -244,37 +229,22 @@ func evalSET(args []string, store *dstore.Store) *EvalResponse {
case Pxat, Exat:
if state != Uninitialized {
- return &EvalResponse{
- Result: nil,
- Error: diceerrors.ErrSyntax,
- }
+ return makeEvalError(diceerrors.ErrSyntax)
}
if keepttl {
- return &EvalResponse{
- Result: nil,
- Error: diceerrors.ErrSyntax,
- }
+ return makeEvalError(diceerrors.ErrSyntax)
}
i++
if i == len(args) {
- return &EvalResponse{
- Result: nil,
- Error: diceerrors.ErrSyntax,
- }
+ return makeEvalError(diceerrors.ErrSyntax)
}
exDuration, err := strconv.ParseInt(args[i], 10, 64)
if err != nil {
- return &EvalResponse{
- Result: nil,
- Error: diceerrors.ErrIntegerOutOfRange,
- }
+ return makeEvalError(diceerrors.ErrIntegerOutOfRange)
}
if exDuration < 0 {
- return &EvalResponse{
- Result: nil,
- Error: diceerrors.ErrInvalidExpireTime("SET"),
- }
+ return makeEvalError(diceerrors.ErrInvalidExpireTime("SET"))
}
if arg == Exat {
@@ -294,32 +264,26 @@ func evalSET(args []string, store *dstore.Store) *EvalResponse {
// if key does not exist, return RESP encoded nil
if obj == nil {
- return &EvalResponse{
- Result: clientio.NIL,
- Error: nil,
- }
+ return makeEvalResult(clientio.NIL)
}
case NX:
obj := store.Get(key)
if obj != nil {
- return &EvalResponse{
- Result: clientio.NIL,
- Error: nil,
- }
+ return makeEvalResult(clientio.NIL)
}
case KeepTTL:
if state != Uninitialized {
- return &EvalResponse{
- Result: nil,
- Error: diceerrors.ErrSyntax,
- }
+ return makeEvalError(diceerrors.ErrSyntax)
}
keepttl = true
- default:
- return &EvalResponse{
- Result: nil,
- Error: diceerrors.ErrSyntax,
+ case GET:
+ getResult := evalGET([]string{key}, store)
+ if getResult.Error != nil {
+ return makeEvalError(diceerrors.ErrWrongTypeOperation)
}
+ oldVal = &getResult.Result
+ default:
+ return makeEvalError(diceerrors.ErrSyntax)
}
}
@@ -331,19 +295,15 @@ func evalSET(args []string, store *dstore.Store) *EvalResponse {
case object.ObjEncodingEmbStr, object.ObjEncodingRaw:
storedValue = value
default:
- return &EvalResponse{
- Result: nil,
- Error: diceerrors.ErrUnsupportedEncoding(int(oEnc)),
- }
+ return makeEvalError(diceerrors.ErrUnsupportedEncoding(int(oEnc)))
}
// putting the k and value in a Hash Table
store.Put(key, store.NewObj(storedValue, exDurationMs, oType, oEnc), dstore.WithKeepTTL(keepttl))
-
- return &EvalResponse{
- Result: clientio.OK,
- Error: nil,
+ if oldVal != nil {
+ return makeEvalResult(*oldVal)
}
+ return makeEvalResult(clientio.OK)
}
// evalGET returns the value for the queried key in args
@@ -3734,3 +3694,750 @@ func evalHDEL(args []string, store *dstore.Store) *EvalResponse {
Error: nil,
}
}
+
+// evalSADD adds one or more members to a set
+// args must contain a key and one or more members to add the set
+// If the set does not exist, a new set is created and members are added to it
+// An error response is returned if the command is used on a key that contains a non-set value(eg: string)
+// Returns an integer which represents the number of members that were added to the set, not including
+// the members that were already present
+func evalSADD(args []string, store *dstore.Store) *EvalResponse {
+ if len(args) < 2 {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongArgumentCount("SADD"),
+ }
+ }
+ key := args[0]
+
+ // Get the set object from the store.
+ obj := store.Get(key)
+ lengthOfItems := len(args[1:])
+
+ var count = 0
+ if obj == nil {
+ var exDurationMs int64 = -1
+ var keepttl = false
+ // If the object does not exist, create a new set object.
+ value := make(map[string]struct{}, lengthOfItems)
+ // Create a new object.
+ obj = store.NewObj(value, exDurationMs, object.ObjTypeSet, object.ObjEncodingSetStr)
+ store.Put(key, obj, dstore.WithKeepTTL(keepttl))
+ }
+
+ if err := object.AssertType(obj.TypeEncoding, object.ObjTypeSet); err != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ }
+ }
+
+ if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingSetStr); err != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ }
+ }
+
+ // Get the set object.
+ set := obj.Value.(map[string]struct{})
+
+ for _, arg := range args[1:] {
+ if _, ok := set[arg]; !ok {
+ set[arg] = struct{}{}
+ count++
+ }
+ }
+
+ return &EvalResponse{
+ Result: count,
+ Error: nil,
+ }
+}
+
+// evalSREM removes one or more members from a set
+// Members that are not member of this set are ignored
+// Returns the number of members that are removed from set
+// If set does not exist, 0 is returned
+// An error response is returned if the command is used on a key that contains a non-set value(eg: string)
+func evalSREM(args []string, store *dstore.Store) *EvalResponse {
+ if len(args) < 2 {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongArgumentCount("SREM"),
+ }
+ }
+ key := args[0]
+
+ // Get the set object from the store.
+ obj := store.Get(key)
+
+ var count = 0
+ if obj == nil {
+ return &EvalResponse{
+ Result: 0,
+ Error: nil,
+ }
+ }
+
+ // If the object exists, check if it is a set object.
+ if err := object.AssertType(obj.TypeEncoding, object.ObjTypeSet); err != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ }
+ }
+
+ if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingSetStr); err != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ }
+ }
+
+ // Get the set object.
+ set := obj.Value.(map[string]struct{})
+
+ for _, arg := range args[1:] {
+ if _, ok := set[arg]; ok {
+ delete(set, arg)
+ count++
+ }
+ }
+
+ return &EvalResponse{
+ Result: count,
+ Error: nil,
+ }
+}
+
+// evalSCARD returns the number of elements of the set stored at key
+// Returns 0 if the key does not exist
+// An error response is returned if the command is used on a key that contains a non-set value(eg: string)
+func evalSCARD(args []string, store *dstore.Store) *EvalResponse {
+ if len(args) != 1 {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongArgumentCount("SCARD"),
+ }
+ }
+
+ key := args[0]
+
+ // Get the set object from the store.
+ obj := store.Get(key)
+
+ if obj == nil {
+ return &EvalResponse{
+ Result: 0,
+ Error: nil,
+ }
+ }
+
+ // If the object exists, check if it is a set object.
+ if err := object.AssertType(obj.TypeEncoding, object.ObjTypeSet); err != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ }
+ }
+
+ if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingSetStr); err != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ }
+ }
+
+ // Get the set object.
+ count := len(obj.Value.(map[string]struct{}))
+ return &EvalResponse{
+ Result: count,
+ Error: nil,
+ }
+}
+
+// evalSMEMBERS returns all the members of a set
+// An error response is returned if the command is used on a key that contains a non-set value(eg: string)
+// An empty set is returned if no set exists for given key
+func evalSMEMBERS(args []string, store *dstore.Store) *EvalResponse {
+ if len(args) != 1 {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongArgumentCount("SMEMBERS"),
+ }
+ }
+ key := args[0]
+
+ // Get the set object from the store.
+ obj := store.Get(key)
+
+ if obj == nil {
+ return &EvalResponse{
+ Result: []string{},
+ Error: nil,
+ }
+ }
+
+ // If the object exists, check if it is a set object.
+ if err := object.AssertType(obj.TypeEncoding, object.ObjTypeSet); err != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ }
+ }
+
+ if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingSetStr); err != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ }
+ }
+
+ // Get the set object.
+ set := obj.Value.(map[string]struct{})
+ // Get the members of the set.
+ members := make([]string, 0, len(set))
+ for k := range set {
+ members = append(members, k)
+ }
+
+ return &EvalResponse{
+ Result: members,
+ Error: nil,
+ }
+}
+
+// evalLRANGE returns the specified elements of the list stored at key.
+//
+// Returns Array reply: a list of elements in the specified range, or an empty array if the key doesn't exist.
+//
+// Usage: LRANGE key start stop
+func evalLRANGE(args []string, store *dstore.Store) *EvalResponse {
+ if len(args) != 3 {
+ return makeEvalError(errors.New("-wrong number of arguments for LRANGE"))
+ }
+ key := args[0]
+ start, err := strconv.ParseInt(args[1], 10, 64)
+ if err != nil {
+ return makeEvalError(errors.New("-ERR value is not an integer or out of range"))
+ }
+ stop, err := strconv.ParseInt(args[2], 10, 64)
+ if err != nil {
+ return makeEvalError(errors.New("-ERR value is not an integer or out of range"))
+ }
+
+ obj := store.Get(key)
+ if obj == nil {
+ return makeEvalResult([]string{})
+ }
+
+ // if object is a set type, return error
+ if object.AssertType(obj.TypeEncoding, object.ObjTypeSet) == nil {
+ return makeEvalError(errors.New(diceerrors.WrongTypeErr))
+ }
+
+ if err := object.AssertType(obj.TypeEncoding, object.ObjTypeByteList); err != nil {
+ return makeEvalError(err)
+ }
+
+ if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingDeque); err != nil {
+ return makeEvalError(err)
+ }
+
+ q := obj.Value.(*Deque)
+ res, err := q.LRange(start, stop)
+ if err != nil {
+ return makeEvalError(err)
+ }
+ return makeEvalResult(res)
+}
+
+// evalLINSERT command inserts the element at a key before / after the pivot element.
+//
+// Returns the list length (integer) after a successful insert operation, 0 when the key doesn't exist, -1 when the pivot wasn't found.
+//
+// Usage: LINSERT key pivot element
+func evalLINSERT(args []string, store *dstore.Store) *EvalResponse {
+ if len(args) != 4 {
+ return makeEvalError(errors.New("-wrong number of arguments for LINSERT"))
+ }
+
+ key := args[0]
+ beforeAfter := strings.ToLower(args[1])
+ pivot := args[2]
+ element := args[3]
+
+ obj := store.Get(key)
+ if obj == nil {
+ return makeEvalResult(0)
+ }
+
+ // if object is a set type, return error
+ if object.AssertType(obj.TypeEncoding, object.ObjTypeSet) == nil {
+ return makeEvalError(errors.New(diceerrors.WrongTypeErr))
+ }
+
+ if err := object.AssertType(obj.TypeEncoding, object.ObjTypeByteList); err != nil {
+ return makeEvalError(err)
+ }
+
+ if err := object.AssertEncoding(obj.TypeEncoding, object.ObjEncodingDeque); err != nil {
+ return makeEvalError(err)
+ }
+
+ q := obj.Value.(*Deque)
+ res, err := q.LInsert(pivot, element, beforeAfter)
+ if err != nil {
+ return makeEvalError(err)
+ }
+ return makeEvalResult(res)
+}
+
+// SETBIT key offset value
+func evalSETBIT(args []string, store *dstore.Store) *EvalResponse {
+ var err error
+
+ if len(args) != 3 {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongArgumentCount("SETBIT"),
+ }
+ }
+
+ key := args[0]
+ offset, err := strconv.ParseInt(args[1], 10, 64)
+ if err != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrGeneral("bit offset is not an integer or out of range"),
+ }
+ }
+
+ value, err := strconv.ParseBool(args[2])
+ if err != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrGeneral("bit is not an integer or out of range"),
+ }
+ }
+
+ obj := store.Get(key)
+ requiredByteArraySize := offset>>3 + 1
+
+ if obj == nil {
+ obj = store.NewObj(NewByteArray(int(requiredByteArraySize)), -1, object.ObjTypeByteArray, object.ObjEncodingByteArray)
+ store.Put(args[0], obj)
+ }
+
+ if object.AssertType(obj.TypeEncoding, object.ObjTypeByteArray) == nil ||
+ object.AssertType(obj.TypeEncoding, object.ObjTypeString) == nil ||
+ object.AssertType(obj.TypeEncoding, object.ObjTypeInt) == nil {
+ var byteArray *ByteArray
+ oType, oEnc := object.ExtractTypeEncoding(obj)
+
+ switch oType {
+ case object.ObjTypeByteArray:
+ byteArray = obj.Value.(*ByteArray)
+ case object.ObjTypeString, object.ObjTypeInt:
+ byteArray, err = NewByteArrayFromObj(obj)
+ if err != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ }
+ }
+ default:
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ }
+ }
+
+ // Perform the resizing check
+ byteArrayLength := byteArray.Length
+
+ // check whether resize required or not
+ if requiredByteArraySize > byteArrayLength {
+ // resize as per the offset
+ byteArray = byteArray.IncreaseSize(int(requiredByteArraySize))
+ }
+
+ resp := byteArray.GetBit(int(offset))
+ byteArray.SetBit(int(offset), value)
+
+ // We are returning newObject here so it is thread-safe
+ // Old will be removed by GC
+ newObj, err := ByteSliceToObj(store, obj, byteArray.data, oType, oEnc)
+ if err != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ }
+ }
+
+ exp, ok := dstore.GetExpiry(obj, store)
+ var exDurationMs int64 = -1
+ if ok {
+ exDurationMs = int64(exp - uint64(utils.GetCurrentTime().UnixMilli()))
+ }
+ // newObj has bydefault expiry time -1 , we need to set it
+ if exDurationMs > 0 {
+ store.SetExpiry(newObj, exDurationMs)
+ }
+
+ store.Put(key, newObj)
+ if resp {
+ return &EvalResponse{
+ Result: clientio.IntegerOne,
+ Error: nil,
+ }
+ }
+ return &EvalResponse{
+ Result: clientio.IntegerZero,
+ Error: nil,
+ }
+ }
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ }
+}
+
+// GETBIT key offset
+func evalGETBIT(args []string, store *dstore.Store) *EvalResponse {
+ var err error
+
+ if len(args) != 2 {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongArgumentCount("GETBIT"),
+ }
+ }
+
+ key := args[0]
+ offset, err := strconv.ParseInt(args[1], 10, 64)
+ if err != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrIntegerOutOfRange,
+ }
+ }
+
+ obj := store.Get(key)
+ if obj == nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ }
+ }
+
+ requiredByteArraySize := offset>>3 + 1
+ switch oType, _ := object.ExtractTypeEncoding(obj); oType {
+ case object.ObjTypeSet:
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ }
+ case object.ObjTypeByteArray:
+ byteArray := obj.Value.(*ByteArray)
+ byteArrayLength := byteArray.Length
+
+ // check whether offset, length exists or not
+ if requiredByteArraySize > byteArrayLength {
+ return &EvalResponse{
+ Result: clientio.IntegerZero,
+ Error: nil,
+ }
+ }
+ value := byteArray.GetBit(int(offset))
+ if value {
+ return &EvalResponse{
+ Result: clientio.IntegerOne,
+ Error: nil,
+ }
+ }
+ return &EvalResponse{
+ Result: clientio.IntegerZero,
+ Error: nil,
+ }
+
+ case object.ObjTypeString, object.ObjTypeInt:
+ byteArray, err := NewByteArrayFromObj(obj)
+ if err != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ }
+ }
+ if requiredByteArraySize > byteArray.Length {
+ return &EvalResponse{
+ Result: clientio.IntegerZero,
+ Error: nil,
+ }
+ }
+ value := byteArray.GetBit(int(offset))
+ if value {
+ return &EvalResponse{
+ Result: clientio.IntegerOne,
+ Error: nil,
+ }
+ }
+ return &EvalResponse{
+ Result: clientio.IntegerZero,
+ Error: nil,
+ }
+
+ default:
+ return &EvalResponse{
+ Result: clientio.IntegerZero,
+ Error: nil,
+ }
+ }
+}
+
+func evalBITCOUNT(args []string, store *dstore.Store) *EvalResponse {
+ var err error
+
+ // if no key is provided, return error
+ if len(args) == 0 {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongArgumentCount("BITCOUNT"),
+ }
+ }
+
+ // if more than 4 arguments are provided, return error
+ if len(args) > 4 {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrSyntax,
+ }
+ }
+
+ // fetching value of the key
+ key := args[0]
+ obj := store.Get(key)
+ if obj == nil {
+ return &EvalResponse{
+ Result: clientio.IntegerZero,
+ Error: nil,
+ }
+ }
+
+ var value []byte
+ var valueLength int64
+
+ switch {
+ case object.AssertType(obj.TypeEncoding, object.ObjTypeByteArray) == nil:
+ byteArray := obj.Value.(*ByteArray)
+ value = byteArray.data
+ valueLength = byteArray.Length
+ case object.AssertType(obj.TypeEncoding, object.ObjTypeString) == nil:
+ value = []byte(obj.Value.(string))
+ valueLength = int64(len(value))
+ case object.AssertType(obj.TypeEncoding, object.ObjTypeInt) == nil:
+ value = []byte(strconv.FormatInt(obj.Value.(int64), 10))
+ valueLength = int64(len(value))
+ default:
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ }
+ }
+
+ // defining constants of the function
+ start, end := int64(0), valueLength-1
+ unit := BYTE
+
+ // checking which arguments are present and validating arguments
+ if len(args) > 1 {
+ start, err = strconv.ParseInt(args[1], 10, 64)
+ if err != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrIntegerOutOfRange,
+ }
+ }
+ if len(args) <= 2 {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrSyntax,
+ }
+ }
+ end, err = strconv.ParseInt(args[2], 10, 64)
+ if err != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrIntegerOutOfRange,
+ }
+ }
+ }
+ if len(args) > 3 {
+ unit = strings.ToUpper(args[3])
+ }
+
+ switch unit {
+ case BYTE:
+ if start < 0 {
+ start += valueLength
+ }
+ if end < 0 {
+ end += valueLength
+ }
+ if start > end || start >= valueLength {
+ return &EvalResponse{
+ Result: clientio.IntegerZero,
+ Error: nil,
+ }
+ }
+ end = min(end, valueLength-1)
+ bitCount := 0
+ for i := start; i <= end; i++ {
+ bitCount += bits.OnesCount8(value[i])
+ }
+ return &EvalResponse{
+ Result: bitCount,
+ Error: nil,
+ }
+ case BIT:
+ if start < 0 {
+ start += valueLength * 8
+ }
+ if end < 0 {
+ end += valueLength * 8
+ }
+ if start > end {
+ return &EvalResponse{
+ Result: clientio.IntegerZero,
+ Error: nil,
+ }
+ }
+ startByte, endByte := start/8, min(end/8, valueLength-1)
+ startBitOffset, endBitOffset := start%8, end%8
+
+ if endByte == valueLength-1 {
+ endBitOffset = 7
+ }
+
+ if startByte >= valueLength {
+ return &EvalResponse{
+ Result: clientio.IntegerZero,
+ Error: nil,
+ }
+ }
+
+ bitCount := 0
+
+ // Use bit masks to count the bits instead of a loop
+ if startByte == endByte {
+ mask := byte(0xFF >> startBitOffset)
+ mask &= byte(0xFF << (7 - endBitOffset))
+ bitCount = bits.OnesCount8(value[startByte] & mask)
+ } else {
+ // Handle first byte
+ firstByteMask := byte(0xFF >> startBitOffset)
+ bitCount += bits.OnesCount8(value[startByte] & firstByteMask)
+
+ // Handle all the middle ones
+ for i := startByte + 1; i < endByte; i++ {
+ bitCount += bits.OnesCount8(value[i])
+ }
+
+ // Handle last byte
+ lastByteMask := byte(0xFF << (7 - endBitOffset))
+ bitCount += bits.OnesCount8(value[endByte] & lastByteMask)
+ }
+ return &EvalResponse{
+ Result: bitCount,
+ Error: nil,
+ }
+ default:
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrSyntax,
+ }
+ }
+}
+
+// Generic method for both BITFIELD and BITFIELD_RO.
+// isReadOnly method is true for BITFIELD_RO command.
+func bitfieldEvalGeneric(args []string, store *dstore.Store, isReadOnly bool) *EvalResponse {
+ var ops []utils.BitFieldOp
+ ops, err2 := utils.ParseBitfieldOps(args, isReadOnly)
+
+ if err2 != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: err2,
+ }
+ }
+
+ key := args[0]
+ obj := store.Get(key)
+ if obj == nil {
+ obj = store.NewObj(NewByteArray(1), -1, object.ObjTypeByteArray, object.ObjEncodingByteArray)
+ store.Put(args[0], obj)
+ }
+ var value *ByteArray
+ var err error
+
+ switch oType, _ := object.ExtractTypeEncoding(obj); oType {
+ case object.ObjTypeByteArray:
+ value = obj.Value.(*ByteArray)
+ case object.ObjTypeString, object.ObjTypeInt:
+ value, err = NewByteArrayFromObj(obj)
+ if err != nil {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrGeneral("value is not a valid byte array"),
+ }
+ }
+ default:
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongTypeOperation,
+ }
+ }
+
+ result := executeBitfieldOps(value, ops)
+ return &EvalResponse{
+ Result: result,
+ Error: nil,
+ }
+}
+
+// evalBITFIELD evaluates BITFIELD operations on a key store string, int or bytearray types
+// it returns an array of results depending on the subcommands
+// it allows mutation using SET and INCRBY commands
+// returns arity error, offset type error, overflow type error, encoding type error, integer error, syntax error
+// GET -- Returns the specified bit field.
+// SET -- Set the specified bit field
+// and returns its old value.
+// INCRBY -- Increments or decrements
+// (if a negative increment is given) the specified bit field and returns the new value.
+// There is another subcommand that only changes the behavior of successive
+// INCRBY and SET subcommands calls by setting the overflow behavior:
+// OVERFLOW [WRAP|SAT|FAIL]`
+func evalBITFIELD(args []string, store *dstore.Store) *EvalResponse {
+ if len(args) < 1 {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongArgumentCount("BITFIELD"),
+ }
+ }
+
+ return bitfieldEvalGeneric(args, store, false)
+}
+
+// Read-only variant of the BITFIELD command. It is like the original BITFIELD but only accepts GET subcommand and can safely be used in read-only replicas.
+func evalBITFIELDRO(args []string, store *dstore.Store) *EvalResponse {
+ if len(args) < 1 {
+ return &EvalResponse{
+ Result: nil,
+ Error: diceerrors.ErrWrongArgumentCount("BITFIELD_RO"),
+ }
+ }
+
+ return bitfieldEvalGeneric(args, store, true)
+}
diff --git a/internal/server/cmd_meta.go b/internal/server/cmd_meta.go
index 8be8af87d..41f5d7bae 100644
--- a/internal/server/cmd_meta.go
+++ b/internal/server/cmd_meta.go
@@ -74,6 +74,22 @@ var (
Cmd: "SETEX",
CmdType: SingleShard,
}
+ saddCmdMeta = CmdsMeta{
+ Cmd: "SADD",
+ CmdType: SingleShard,
+ }
+ sremCmdMeta = CmdsMeta{
+ Cmd: "SREM",
+ CmdType: SingleShard,
+ }
+ scardCmdMeta = CmdsMeta{
+ Cmd: "SCARD",
+ CmdType: SingleShard,
+ }
+ smembersCmdMeta = CmdsMeta{
+ Cmd: "SMEMBERS",
+ }
+
jsonArrAppendCmdMeta = CmdsMeta{
Cmd: "JSON.ARRAPPEND",
CmdType: SingleShard,
@@ -86,6 +102,7 @@ var (
Cmd: "JSON.ARRPOP",
CmdType: SingleShard,
}
+
getrangeCmdMeta = CmdsMeta{
Cmd: "GETRANGE",
CmdType: SingleShard,
@@ -154,6 +171,30 @@ var (
Cmd: "PTTL",
CmdType: SingleShard,
}
+ setbitCmdMeta = CmdsMeta{
+ Cmd: "SETBIT",
+ CmdType: SingleShard,
+ }
+ getbitCmdMeta = CmdsMeta{
+ Cmd: "GETBIT",
+ CmdType: SingleShard,
+ }
+ bitcountCmdMeta = CmdsMeta{
+ Cmd: "BITCOUNT",
+ CmdType: SingleShard,
+ }
+ bitfieldCmdMeta = CmdsMeta{
+ Cmd: "BITFIELD",
+ CmdType: SingleShard,
+ }
+ bitposCmdMeta = CmdsMeta{
+ Cmd: "BITPOS",
+ CmdType: SingleShard,
+ }
+ bitfieldroCmdMeta = CmdsMeta{
+ Cmd: "BITFIELD_RO",
+ CmdType: SingleShard,
+ }
jsonclearCmdMeta = CmdsMeta{
Cmd: "JSON.CLEAR",
@@ -331,9 +372,16 @@ func init() {
WorkerCmdsMeta["GET"] = getCmdMeta
WorkerCmdsMeta["GETSET"] = getsetCmdMeta
WorkerCmdsMeta["SETEX"] = setexCmdMeta
+
+ WorkerCmdsMeta["SADD"] = saddCmdMeta
+ WorkerCmdsMeta["SREM"] = sremCmdMeta
+ WorkerCmdsMeta["SCARD"] = scardCmdMeta
+ WorkerCmdsMeta["SMEMBERS"] = smembersCmdMeta
+
WorkerCmdsMeta["JSON.ARRAPPEND"] = jsonArrAppendCmdMeta
WorkerCmdsMeta["JSON.ARRLEN"] = jsonArrLenCmdMeta
WorkerCmdsMeta["JSON.ARRPOP"] = jsonArrPopCmdMeta
+
WorkerCmdsMeta["GETRANGE"] = getrangeCmdMeta
WorkerCmdsMeta["APPEND"] = appendCmdMeta
WorkerCmdsMeta["JSON.CLEAR"] = jsonclearCmdMeta
@@ -401,5 +449,12 @@ func init() {
WorkerCmdsMeta["HDEL"] = hdelCmdMeta
WorkerCmdsMeta["HMSET"] = hmsetCmdMeta
WorkerCmdsMeta["HMGET"] = hmgetCmdMeta
+ WorkerCmdsMeta["SETBIT"] = setbitCmdMeta
+ WorkerCmdsMeta["GETBIT"] = getbitCmdMeta
+ WorkerCmdsMeta["BITCOUNT"] = bitcountCmdMeta
+ WorkerCmdsMeta["BITFIELD"] = bitfieldCmdMeta
+ WorkerCmdsMeta["BITPOS"] = bitposCmdMeta
+ WorkerCmdsMeta["BITFIELD_RO"] = bitfieldroCmdMeta
+
// Additional commands (multishard, custom) can be added here as needed.
}
diff --git a/internal/server/httpServer.go b/internal/server/httpServer.go
index c748be162..2a993c9a0 100644
--- a/internal/server/httpServer.go
+++ b/internal/server/httpServer.go
@@ -13,8 +13,8 @@ import (
"time"
"github.com/dicedb/dice/internal/eval"
-
"github.com/dicedb/dice/internal/server/abstractserver"
+ "github.com/dicedb/dice/internal/wal"
"github.com/dicedb/dice/config"
"github.com/dicedb/dice/internal/clientio"
@@ -61,7 +61,7 @@ func (cim *CaseInsensitiveMux) ServeHTTP(w http.ResponseWriter, r *http.Request)
cim.mux.ServeHTTP(w, r)
}
-func NewHTTPServer(shardManager *shard.ShardManager) *HTTPServer {
+func NewHTTPServer(shardManager *shard.ShardManager, wl wal.AbstractWAL) *HTTPServer {
mux := http.NewServeMux()
caseInsensitiveMux := &CaseInsensitiveMux{mux: mux}
srv := &http.Server{
diff --git a/internal/server/resp/server.go b/internal/server/resp/server.go
index 1fb8b86c0..17244d076 100644
--- a/internal/server/resp/server.go
+++ b/internal/server/resp/server.go
@@ -12,6 +12,7 @@ import (
"time"
"github.com/dicedb/dice/internal/server/abstractserver"
+ "github.com/dicedb/dice/internal/wal"
dstore "github.com/dicedb/dice/internal/store"
"github.com/dicedb/dice/internal/watchmanager"
@@ -47,22 +48,22 @@ type Server struct {
shardManager *shard.ShardManager
watchManager *watchmanager.Manager
cmdWatchSubscriptionChan chan watchmanager.WatchSubscription
- cmdWatchChan chan dstore.CmdWatchEvent
globalErrorChan chan error
+ wl wal.AbstractWAL
}
-func NewServer(shardManager *shard.ShardManager, workerManager *worker.WorkerManager, cmdWatchSubscriptionChan chan watchmanager.WatchSubscription,
- cmdWatchChan chan dstore.CmdWatchEvent, globalErrChan chan error) *Server {
+func NewServer(shardManager *shard.ShardManager, workerManager *worker.WorkerManager,
+ cmdWatchSubscriptionChan chan watchmanager.WatchSubscription, cmdWatchChan chan dstore.CmdWatchEvent, globalErrChan chan error, wl wal.AbstractWAL) *Server {
return &Server{
Host: config.DiceConfig.AsyncServer.Addr,
Port: config.DiceConfig.AsyncServer.Port,
connBacklogSize: DefaultConnBacklogSize,
workerManager: workerManager,
shardManager: shardManager,
- watchManager: watchmanager.NewManager(cmdWatchSubscriptionChan),
- cmdWatchChan: cmdWatchChan,
+ watchManager: watchmanager.NewManager(cmdWatchSubscriptionChan, cmdWatchChan),
cmdWatchSubscriptionChan: cmdWatchSubscriptionChan,
globalErrorChan: globalErrChan,
+ wl: wl,
}
}
@@ -79,11 +80,11 @@ func (s *Server) Run(ctx context.Context) (err error) {
errChan := make(chan error, 1)
wg := &sync.WaitGroup{}
- if s.cmdWatchChan != nil {
+ if s.cmdWatchSubscriptionChan != nil {
wg.Add(1)
go func() {
defer wg.Done()
- s.watchManager.Run(ctx, s.cmdWatchChan)
+ s.watchManager.Run(ctx)
}()
}
@@ -198,7 +199,7 @@ func (s *Server) AcceptConnectionRequests(ctx context.Context, wg *sync.WaitGrou
preprocessingChan := make(chan *ops.StoreResponse) // preprocessingChan is specifically for handling responses from shards for commands that require preprocessing
wID := GenerateUniqueWorkerID()
- w := worker.NewWorker(wID, responseChan, preprocessingChan, s.cmdWatchSubscriptionChan, ioHandler, parser, s.shardManager, s.globalErrorChan)
+ w := worker.NewWorker(wID, responseChan, preprocessingChan, s.cmdWatchSubscriptionChan, ioHandler, parser, s.shardManager, s.globalErrorChan, s.wl)
// Register the worker with the worker manager
err = s.workerManager.RegisterWorker(w)
diff --git a/internal/server/server.go b/internal/server/server.go
index cf9b98ca1..3b27efa79 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -14,6 +14,7 @@ import (
"time"
"github.com/dicedb/dice/internal/server/abstractserver"
+ "github.com/dicedb/dice/internal/wal"
"github.com/dicedb/dice/config"
"github.com/dicedb/dice/internal/auth"
@@ -44,7 +45,7 @@ type AsyncServer struct {
}
// NewAsyncServer initializes a new AsyncServer
-func NewAsyncServer(shardManager *shard.ShardManager, queryWatchChan chan dstore.QueryWatchEvent) *AsyncServer {
+func NewAsyncServer(shardManager *shard.ShardManager, queryWatchChan chan dstore.QueryWatchEvent, wl wal.AbstractWAL) *AsyncServer {
return &AsyncServer{
maxClients: config.DiceConfig.Performance.MaxClients,
connectedClients: make(map[int]*comm.Client),
diff --git a/internal/server/utils/bitfield.go b/internal/server/utils/bitfield.go
index 29544a4c6..bf5f389fc 100644
--- a/internal/server/utils/bitfield.go
+++ b/internal/server/utils/bitfield.go
@@ -25,26 +25,26 @@ func parseBitfieldEncodingAndOffset(args []string) (eType, eVal, offset interfac
eType = SIGNED
eVal, err = strconv.ParseInt(encodingRaw[1:], 10, 64)
if err != nil {
- err = diceerrors.NewErr(diceerrors.InvalidBitfieldType)
+ err = diceerrors.ErrGeneral("Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is")
return eType, eVal, offset, err
}
if eVal.(int64) <= 0 || eVal.(int64) > 64 {
- err = diceerrors.NewErr(diceerrors.InvalidBitfieldType)
+ err = diceerrors.ErrGeneral("Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is")
return eType, eVal, offset, err
}
case 'u':
eType = UNSIGNED
eVal, err = strconv.ParseInt(encodingRaw[1:], 10, 64)
if err != nil {
- err = diceerrors.NewErr(diceerrors.InvalidBitfieldType)
+ err = diceerrors.ErrGeneral("Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is")
return eType, eVal, offset, err
}
if eVal.(int64) <= 0 || eVal.(int64) >= 64 {
- err = diceerrors.NewErr(diceerrors.InvalidBitfieldType)
+ err = diceerrors.ErrGeneral("Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is")
return eType, eVal, offset, err
}
default:
- err = diceerrors.NewErr(diceerrors.InvalidBitfieldType)
+ err = diceerrors.ErrGeneral("Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is")
return eType, eVal, offset, err
}
@@ -52,21 +52,21 @@ func parseBitfieldEncodingAndOffset(args []string) (eType, eVal, offset interfac
case '#':
offset, err = strconv.ParseInt(offsetRaw[1:], 10, 64)
if err != nil {
- err = diceerrors.NewErr(diceerrors.BitfieldOffsetErr)
+ err = diceerrors.ErrGeneral("bit offset is not an integer or out of range")
return eType, eVal, offset, err
}
offset = offset.(int64) * eVal.(int64)
default:
offset, err = strconv.ParseInt(offsetRaw, 10, 64)
if err != nil {
- err = diceerrors.NewErr(diceerrors.BitfieldOffsetErr)
+ err = diceerrors.ErrGeneral("bit offset is not an integer or out of range")
return eType, eVal, offset, err
}
}
return eType, eVal, offset, err
}
-func ParseBitfieldOps(args []string, readOnly bool) (ops []BitFieldOp, err []byte) {
+func ParseBitfieldOps(args []string, readOnly bool) (ops []BitFieldOp, err error) {
var overflowType string
for i := 1; i < len(args); {
@@ -74,11 +74,11 @@ func ParseBitfieldOps(args []string, readOnly bool) (ops []BitFieldOp, err []byt
switch strings.ToUpper(args[i]) {
case GET:
if len(args) <= i+2 {
- return nil, diceerrors.NewErrWithMessage(diceerrors.SyntaxErr)
+ return nil, diceerrors.ErrSyntax
}
eType, eVal, offset, err := parseBitfieldEncodingAndOffset(args[i+1 : i+3])
if err != nil {
- return nil, diceerrors.NewErrWithFormattedMessage(err.Error())
+ return nil, err
}
ops = append(ops, BitFieldOp{
Kind: GET,
@@ -91,15 +91,15 @@ func ParseBitfieldOps(args []string, readOnly bool) (ops []BitFieldOp, err []byt
isReadOnlyCommand = true
case SET:
if len(args) <= i+3 {
- return nil, diceerrors.NewErrWithMessage(diceerrors.SyntaxErr)
+ return nil, diceerrors.ErrSyntax
}
eType, eVal, offset, err := parseBitfieldEncodingAndOffset(args[i+1 : i+3])
if err != nil {
- return nil, diceerrors.NewErrWithFormattedMessage(err.Error())
+ return nil, err
}
value, err1 := strconv.ParseInt(args[i+3], 10, 64)
if err1 != nil {
- return nil, diceerrors.NewErrWithMessage(diceerrors.IntOrOutOfRangeErr)
+ return nil, diceerrors.ErrIntegerOutOfRange
}
ops = append(ops, BitFieldOp{
Kind: SET,
@@ -111,15 +111,15 @@ func ParseBitfieldOps(args []string, readOnly bool) (ops []BitFieldOp, err []byt
i += 4
case INCRBY:
if len(args) <= i+3 {
- return nil, diceerrors.NewErrWithMessage(diceerrors.SyntaxErr)
+ return nil, diceerrors.ErrSyntax
}
eType, eVal, offset, err := parseBitfieldEncodingAndOffset(args[i+1 : i+3])
if err != nil {
- return nil, diceerrors.NewErrWithFormattedMessage(err.Error())
+ return nil, err
}
value, err1 := strconv.ParseInt(args[i+3], 10, 64)
if err1 != nil {
- return nil, diceerrors.NewErrWithMessage(diceerrors.IntOrOutOfRangeErr)
+ return nil, diceerrors.ErrIntegerOutOfRange
}
ops = append(ops, BitFieldOp{
Kind: INCRBY,
@@ -131,13 +131,13 @@ func ParseBitfieldOps(args []string, readOnly bool) (ops []BitFieldOp, err []byt
i += 4
case OVERFLOW:
if len(args) <= i+1 {
- return nil, diceerrors.NewErrWithMessage(diceerrors.SyntaxErr)
+ return nil, diceerrors.ErrSyntax
}
switch strings.ToUpper(args[i+1]) {
case WRAP, FAIL, SAT:
overflowType = strings.ToUpper(args[i+1])
default:
- return nil, diceerrors.NewErrWithFormattedMessage(diceerrors.OverflowTypeErr)
+ return nil, diceerrors.ErrGeneral("Invalid OVERFLOW type specified")
}
ops = append(ops, BitFieldOp{
Kind: OVERFLOW,
@@ -148,11 +148,11 @@ func ParseBitfieldOps(args []string, readOnly bool) (ops []BitFieldOp, err []byt
})
i += 2
default:
- return nil, diceerrors.NewErrWithMessage(diceerrors.SyntaxErr)
+ return nil, diceerrors.ErrSyntax
}
if readOnly && !isReadOnlyCommand {
- return nil, diceerrors.NewErrWithMessage("BITFIELD_RO only supports the GET subcommand")
+ return nil, diceerrors.ErrGeneral("BITFIELD_RO only supports the GET subcommand")
}
}
diff --git a/internal/server/utils/redisCmdAdapter.go b/internal/server/utils/redisCmdAdapter.go
index 1ffd4d396..06e093b27 100644
--- a/internal/server/utils/redisCmdAdapter.go
+++ b/internal/server/utils/redisCmdAdapter.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
+
"io"
"net/http"
"regexp"
diff --git a/internal/server/websocketServer.go b/internal/server/websocketServer.go
index 7fe4f723f..d86eb977a 100644
--- a/internal/server/websocketServer.go
+++ b/internal/server/websocketServer.go
@@ -15,6 +15,7 @@ import (
"time"
"github.com/dicedb/dice/internal/server/abstractserver"
+ "github.com/dicedb/dice/internal/wal"
"github.com/dicedb/dice/config"
"github.com/dicedb/dice/internal/clientio"
@@ -56,7 +57,7 @@ type WebsocketServer struct {
mu sync.Mutex
}
-func NewWebSocketServer(shardManager *shard.ShardManager, port int) *WebsocketServer {
+func NewWebSocketServer(shardManager *shard.ShardManager, port int, wl wal.AbstractWAL) *WebsocketServer {
mux := http.NewServeMux()
srv := &http.Server{
Addr: fmt.Sprintf(":%d", port),
diff --git a/internal/wal/wal.go b/internal/wal/wal.go
new file mode 100644
index 000000000..ca251560e
--- /dev/null
+++ b/internal/wal/wal.go
@@ -0,0 +1,72 @@
+package wal
+
+import (
+ "fmt"
+ "log/slog"
+ sync "sync"
+ "time"
+
+ "github.com/dicedb/dice/internal/cmd"
+)
+
+type AbstractWAL interface {
+ LogCommand(c *cmd.DiceDBCmd)
+ Close() error
+ Init(t time.Time) error
+ ForEachCommand(f func(c cmd.DiceDBCmd) error) error
+}
+
+var (
+ ticker *time.Ticker
+ stopCh chan struct{}
+ mu sync.Mutex
+)
+
+func init() {
+ ticker = time.NewTicker(1 * time.Minute)
+ stopCh = make(chan struct{})
+}
+
+func rotateWAL(wl AbstractWAL) {
+ mu.Lock()
+ defer mu.Unlock()
+
+ if err := wl.Close(); err != nil {
+ slog.Warn("error closing the WAL", slog.Any("error", err))
+ }
+
+ if err := wl.Init(time.Now()); err != nil {
+ slog.Warn("error creating a new WAL", slog.Any("error", err))
+ }
+}
+
+func periodicRotate(wl AbstractWAL) {
+ for {
+ select {
+ case <-ticker.C:
+ rotateWAL(wl)
+ case <-stopCh:
+ return
+ }
+ }
+}
+
+func InitBG(wl AbstractWAL) {
+ go periodicRotate(wl)
+}
+
+func ShutdownBG() {
+ close(stopCh)
+ ticker.Stop()
+}
+
+func ReplayWAL(wl AbstractWAL) {
+ err := wl.ForEachCommand(func(c cmd.DiceDBCmd) error {
+ fmt.Println("replaying", c.Cmd, c.Args)
+ return nil
+ })
+
+ if err != nil {
+ slog.Warn("error replaying WAL", slog.Any("error", err))
+ }
+}
diff --git a/internal/wal/wal.pb.go b/internal/wal/wal.pb.go
new file mode 100644
index 000000000..3adf5277b
--- /dev/null
+++ b/internal/wal/wal.pb.go
@@ -0,0 +1,137 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.35.1
+// protoc v3.12.4
+// source: internal/wal/wal.proto
+
+package wal
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// WALLogEntry represents a single log entry in the WAL.
+type WALLogEntry struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Checksum []byte `protobuf:"bytes,1,opt,name=checksum,proto3" json:"checksum,omitempty"` // SHA-256 checksum of the command for integrity
+ Command string `protobuf:"bytes,2,opt,name=command,proto3" json:"command,omitempty"` // Command
+}
+
+func (x *WALLogEntry) Reset() {
+ *x = WALLogEntry{}
+ mi := &file_internal_wal_wal_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *WALLogEntry) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*WALLogEntry) ProtoMessage() {}
+
+func (x *WALLogEntry) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_wal_wal_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use WALLogEntry.ProtoReflect.Descriptor instead.
+func (*WALLogEntry) Descriptor() ([]byte, []int) {
+ return file_internal_wal_wal_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *WALLogEntry) GetChecksum() []byte {
+ if x != nil {
+ return x.Checksum
+ }
+ return nil
+}
+
+func (x *WALLogEntry) GetCommand() string {
+ if x != nil {
+ return x.Command
+ }
+ return ""
+}
+
+var File_internal_wal_wal_proto protoreflect.FileDescriptor
+
+var file_internal_wal_wal_proto_rawDesc = []byte{
+ 0x0a, 0x16, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x77, 0x61, 0x6c, 0x2f, 0x77,
+ 0x61, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x77, 0x61, 0x6c, 0x22, 0x43, 0x0a,
+ 0x0b, 0x57, 0x41, 0x4c, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x1a, 0x0a, 0x08,
+ 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08,
+ 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d,
+ 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61,
+ 0x6e, 0x64, 0x42, 0x0e, 0x5a, 0x0c, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x77,
+ 0x61, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_internal_wal_wal_proto_rawDescOnce sync.Once
+ file_internal_wal_wal_proto_rawDescData = file_internal_wal_wal_proto_rawDesc
+)
+
+func file_internal_wal_wal_proto_rawDescGZIP() []byte {
+ file_internal_wal_wal_proto_rawDescOnce.Do(func() {
+ file_internal_wal_wal_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_wal_wal_proto_rawDescData)
+ })
+ return file_internal_wal_wal_proto_rawDescData
+}
+
+var file_internal_wal_wal_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_internal_wal_wal_proto_goTypes = []any{
+ (*WALLogEntry)(nil), // 0: wal.WALLogEntry
+}
+var file_internal_wal_wal_proto_depIdxs = []int32{
+ 0, // [0:0] is the sub-list for method output_type
+ 0, // [0:0] is the sub-list for method input_type
+ 0, // [0:0] is the sub-list for extension type_name
+ 0, // [0:0] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_internal_wal_wal_proto_init() }
+func file_internal_wal_wal_proto_init() {
+ if File_internal_wal_wal_proto != nil {
+ return
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_internal_wal_wal_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 1,
+ NumExtensions: 0,
+ NumServices: 0,
+ },
+ GoTypes: file_internal_wal_wal_proto_goTypes,
+ DependencyIndexes: file_internal_wal_wal_proto_depIdxs,
+ MessageInfos: file_internal_wal_wal_proto_msgTypes,
+ }.Build()
+ File_internal_wal_wal_proto = out.File
+ file_internal_wal_wal_proto_rawDesc = nil
+ file_internal_wal_wal_proto_goTypes = nil
+ file_internal_wal_wal_proto_depIdxs = nil
+}
diff --git a/internal/wal/wal.proto b/internal/wal/wal.proto
new file mode 100644
index 000000000..e935d665d
--- /dev/null
+++ b/internal/wal/wal.proto
@@ -0,0 +1,10 @@
+syntax = "proto3";
+
+package wal;
+option go_package = "internal/wal";
+
+// WALLogEntry represents a single log entry in the WAL.
+message WALLogEntry {
+ bytes checksum = 1; // SHA-256 checksum of the command for integrity
+ string command = 2; // Command
+}
diff --git a/internal/wal/wal_aof.go b/internal/wal/wal_aof.go
new file mode 100644
index 000000000..ab690a097
--- /dev/null
+++ b/internal/wal/wal_aof.go
@@ -0,0 +1,182 @@
+package wal
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "encoding/binary"
+ "fmt"
+ "io"
+ "log/slog"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/dicedb/dice/internal/cmd"
+ "google.golang.org/protobuf/proto"
+)
+
+var writeBuf bytes.Buffer
+
+type WALAOF struct {
+ file *os.File
+ mutex sync.Mutex
+ logDir string
+}
+
+func NewAOFWAL(logDir string) (*WALAOF, error) {
+ return &WALAOF{
+ logDir: logDir,
+ }, nil
+}
+
+func (w *WALAOF) Init(t time.Time) error {
+ slog.Debug("initializing WAL at", slog.Any("log-dir", w.logDir))
+ if err := os.MkdirAll(w.logDir, os.ModePerm); err != nil {
+ return fmt.Errorf("failed to create log directory: %w", err)
+ }
+
+ timestamp := t.Format("20060102_1504")
+ path := filepath.Join(w.logDir, fmt.Sprintf("wal_%s.aof", timestamp))
+
+ newFile, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+ if err != nil {
+ return fmt.Errorf("failed to open new WAL file: %v", err)
+ }
+
+ w.file = newFile
+ return nil
+}
+
+// LogCommand serializes a WALLogEntry and writes it to the current WAL file.
+func (w *WALAOF) LogCommand(c *cmd.DiceDBCmd) {
+ w.mutex.Lock()
+ defer w.mutex.Unlock()
+
+ repr := fmt.Sprintf("%s %s", c.Cmd, strings.Join(c.Args, " "))
+
+ entry := &WALLogEntry{
+ Command: repr,
+ Checksum: checksum(repr),
+ }
+
+ data, err := proto.Marshal(entry)
+ if err != nil {
+ slog.Warn("failed to serialize command", slog.Any("error", err.Error()))
+ }
+
+ writeBuf.Reset()
+ writeBuf.Grow(4 + len(data))
+ if binary.Write(&writeBuf, binary.BigEndian, uint32(len(data))) != nil {
+ slog.Warn("failed to write entry length to WAL", slog.Any("error", err.Error()))
+ }
+ writeBuf.Write(data)
+
+ if _, err := w.file.Write(writeBuf.Bytes()); err != nil {
+ slog.Warn("failed to write serialized command to WAL", slog.Any("error", err.Error()))
+ }
+
+ if err := w.file.Sync(); err != nil {
+ slog.Warn("failed to sync WAL", slog.Any("error", err.Error()))
+ }
+
+ slog.Debug("logged command in WAL", slog.Any("command", c.Repr()))
+}
+
+func (w *WALAOF) Close() error {
+ if w.file == nil {
+ return nil
+ }
+ return w.file.Close()
+}
+
+// checksum generates a SHA-256 hash for the given command.
+func checksum(command string) []byte {
+ hash := sha256.Sum256([]byte(command))
+ return hash[:]
+}
+
+func (w *WALAOF) ForEachCommand(f func(c cmd.DiceDBCmd) error) error {
+ var length uint32
+
+ files, err := os.ReadDir(w.logDir)
+ if err != nil {
+ return fmt.Errorf("failed to read log directory: %v", err)
+ }
+
+ var walFiles []os.DirEntry
+
+ for _, file := range files {
+ if !file.IsDir() && filepath.Ext(file.Name()) == ".aof" {
+ walFiles = append(walFiles, file)
+ }
+ }
+
+ if len(walFiles) == 0 {
+ return fmt.Errorf("no valid WAL files found in log directory")
+ }
+
+ // Sort files by timestamp in ascending order
+ sort.Slice(walFiles, func(i, j int) bool {
+ timestampStrI := walFiles[i].Name()[4:17]
+ timestampStrJ := walFiles[j].Name()[4:17]
+ timestampI, errI := time.Parse("20060102_1504", timestampStrI)
+ timestampJ, errJ := time.Parse("20060102_1504", timestampStrJ)
+ if errI != nil || errJ != nil {
+ return false
+ }
+ return timestampI.Before(timestampJ)
+ })
+
+ for _, file := range walFiles {
+ filePath := filepath.Join(w.logDir, file.Name())
+
+ slog.Debug("loading WAL", slog.Any("file", filePath))
+
+ file, err := os.OpenFile(filePath, os.O_RDONLY, 0644)
+ if err != nil {
+ return fmt.Errorf("failed to open WAL file %s: %v", file.Name(), err)
+ }
+
+ for {
+ if err := binary.Read(file, binary.BigEndian, &length); err != nil {
+ if err == io.EOF {
+ break
+ }
+ return fmt.Errorf("failed to read entry length: %v", err)
+ }
+
+ // TODO: Optimize this allocation.
+ // Pre-allocate and reuse rather than allocating for each entry.
+ readBufBytes := make([]byte, length)
+ if _, err := io.ReadFull(file, readBufBytes); err != nil {
+ return fmt.Errorf("failed to read entry data: %v", err)
+ }
+
+ entry := &WALLogEntry{}
+ if err := proto.Unmarshal(readBufBytes, entry); err != nil {
+ return fmt.Errorf("failed to unmarshal WAL entry: %v", err)
+ }
+
+ commandParts := strings.SplitN(entry.Command, " ", 2)
+ if len(commandParts) < 2 {
+ return fmt.Errorf("invalid command format in WAL entry: %s", entry.Command)
+ }
+
+ c := cmd.DiceDBCmd{
+ Cmd: commandParts[0],
+ Args: strings.Split(commandParts[1], " "),
+ }
+
+ if err := f(c); err != nil {
+ return fmt.Errorf("error processing command: %v", err)
+ }
+ }
+
+ file.Close()
+ }
+
+ return nil
+}
diff --git a/internal/wal/wal_null.go b/internal/wal/wal_null.go
new file mode 100644
index 000000000..c4a58ccc9
--- /dev/null
+++ b/internal/wal/wal_null.go
@@ -0,0 +1,30 @@
+package wal
+
+import (
+ "time"
+
+ "github.com/dicedb/dice/internal/cmd"
+)
+
+type WALNull struct {
+}
+
+func NewNullWAL() (*WALNull, error) {
+ return &WALNull{}, nil
+}
+
+func (w *WALNull) Init(t time.Time) error {
+ return nil
+}
+
+// LogCommand serializes a WALLogEntry and writes it to the current WAL file.
+func (w *WALNull) LogCommand(c *cmd.DiceDBCmd) {
+}
+
+func (w *WALNull) Close() error {
+ return nil
+}
+
+func (w *WALNull) ForEachCommand(f func(c cmd.DiceDBCmd) error) error {
+ return nil
+}
diff --git a/internal/wal/wal_sqlite.go b/internal/wal/wal_sqlite.go
new file mode 100644
index 000000000..5805667d9
--- /dev/null
+++ b/internal/wal/wal_sqlite.go
@@ -0,0 +1,147 @@
+package wal
+
+import (
+ "database/sql"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ sync "sync"
+ "time"
+
+ "log/slog"
+
+ "github.com/dicedb/dice/internal/cmd"
+ _ "github.com/mattn/go-sqlite3"
+)
+
+type WALSQLite struct {
+ logDir string
+ curDB *sql.DB
+ mu sync.Mutex
+}
+
+func NewSQLiteWAL(logDir string) (*WALSQLite, error) {
+ return &WALSQLite{
+ logDir: logDir,
+ }, nil
+}
+
+func (w *WALSQLite) Init(t time.Time) error {
+ slog.Debug("initializing WAL at", slog.Any("log-dir", w.logDir))
+ if err := os.MkdirAll(w.logDir, os.ModePerm); err != nil {
+ return fmt.Errorf("failed to create log directory: %w", err)
+ }
+
+ timestamp := t.Format("20060102_1504")
+ path := filepath.Join(w.logDir, fmt.Sprintf("wal_%s.sqlite3", timestamp))
+
+ db, err := sql.Open("sqlite3", path)
+ if err != nil {
+ return err
+ }
+
+ _, err = db.Exec("PRAGMA journal_mode=WAL;")
+ if err != nil {
+ return err
+ }
+
+ _, err = db.Exec(`CREATE TABLE IF NOT EXISTS wal (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ command TEXT NOT NULL
+ );`)
+ if err != nil {
+ return err
+ }
+
+ w.curDB = db
+ return nil
+}
+
+func (w *WALSQLite) LogCommand(c *cmd.DiceDBCmd) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+
+ if _, err := w.curDB.Exec("INSERT INTO wal (command) VALUES (?)", c.Repr()); err != nil {
+ slog.Error("failed to log command in WAL", slog.Any("error", err))
+ } else {
+ slog.Debug("logged command in WAL", slog.Any("command", c.Repr()))
+ }
+}
+
+func (w *WALSQLite) Close() error {
+ return w.curDB.Close()
+}
+
+func (w *WALSQLite) ForEachCommand(f func(c cmd.DiceDBCmd) error) error {
+ files, err := os.ReadDir(w.logDir)
+ if err != nil {
+ return fmt.Errorf("failed to read log directory: %v", err)
+ }
+
+ var walFiles []os.DirEntry
+
+ for _, file := range files {
+ if !file.IsDir() && filepath.Ext(file.Name()) == ".sqlite3" {
+ walFiles = append(walFiles, file)
+ }
+ }
+
+ if len(walFiles) == 0 {
+ return fmt.Errorf("no valid WAL files found in log directory")
+ }
+
+ // Sort files by timestamp in ascending order
+ sort.Slice(walFiles, func(i, j int) bool {
+ timestampStrI := walFiles[i].Name()[4:17]
+ timestampStrJ := walFiles[j].Name()[4:17]
+ timestampI, errI := time.Parse("20060102_1504", timestampStrI)
+ timestampJ, errJ := time.Parse("20060102_1504", timestampStrJ)
+ if errI != nil || errJ != nil {
+ return false
+ }
+ return timestampI.Before(timestampJ)
+ })
+
+ for _, file := range walFiles {
+ filePath := filepath.Join(w.logDir, file.Name())
+
+ slog.Debug("loading WAL", slog.Any("file", filePath))
+
+ db, err := sql.Open("sqlite3", filePath)
+ if err != nil {
+ return fmt.Errorf("failed to open WAL file %s: %v", file.Name(), err)
+ }
+
+ rows, err := db.Query("SELECT command FROM wal")
+ if err != nil {
+ return fmt.Errorf("failed to query WAL file %s: %v", file.Name(), err)
+ }
+
+ for rows.Next() {
+ var command string
+ if err := rows.Scan(&command); err != nil {
+ return fmt.Errorf("failed to scan WAL file %s: %v", file.Name(), err)
+ }
+
+ tokens := strings.Split(command, " ")
+ if err := f(cmd.DiceDBCmd{
+ Cmd: tokens[0],
+ Args: tokens[1:],
+ }); err != nil {
+ return err
+ }
+ }
+
+ if err := rows.Err(); err != nil {
+ return fmt.Errorf("failed to iterate WAL file %s: %v", file.Name(), err)
+ }
+
+ if err := db.Close(); err != nil {
+ return fmt.Errorf("failed to close WAL file %s: %v", file.Name(), err)
+ }
+ }
+
+ return nil
+}
diff --git a/internal/wal/wal_test.go b/internal/wal/wal_test.go
new file mode 100644
index 000000000..b3ee707e3
--- /dev/null
+++ b/internal/wal/wal_test.go
@@ -0,0 +1,50 @@
+package wal_test
+
+import (
+ "log/slog"
+ "testing"
+ "time"
+
+ "github.com/dicedb/dice/internal/cmd"
+ "github.com/dicedb/dice/internal/wal"
+)
+
+func BenchmarkLogCommandSQLite(b *testing.B) {
+ wl, err := wal.NewSQLiteWAL("/tmp/dicedb-lt")
+ if err != nil {
+ panic(err)
+ }
+
+ if err := wl.Init(time.Now()); err != nil {
+ slog.Error("could not initialize WAL", slog.Any("error", err))
+ } else {
+ go wal.InitBG(wl)
+ }
+
+ for i := 0; i < b.N; i++ {
+ wl.LogCommand(&cmd.DiceDBCmd{
+ Cmd: "SET",
+ Args: []string{"key", "value"},
+ })
+ }
+}
+
+func BenchmarkLogCommandAOF(b *testing.B) {
+ wl, err := wal.NewAOFWAL("/tmp/dicedb-lt")
+ if err != nil {
+ panic(err)
+ }
+
+ if err := wl.Init(time.Now()); err != nil {
+ slog.Error("could not initialize WAL", slog.Any("error", err))
+ } else {
+ go wal.InitBG(wl)
+ }
+
+ for i := 0; i < b.N; i++ {
+ wl.LogCommand(&cmd.DiceDBCmd{
+ Cmd: "SET",
+ Args: []string{"key", "value"},
+ })
+ }
+}
diff --git a/internal/watchmanager/watch_manager.go b/internal/watchmanager/watch_manager.go
index 12451c654..18fe5369f 100644
--- a/internal/watchmanager/watch_manager.go
+++ b/internal/watchmanager/watch_manager.go
@@ -22,6 +22,7 @@ type (
tcpSubscriptionMap map[uint32]map[chan *cmd.DiceDBCmd]struct{} // tcpSubscriptionMap is a map of fingerprint -> [client1Chan, client2Chan, ...]
fingerprintCmdMap map[uint32]*cmd.DiceDBCmd // fingerprintCmdMap is a map of fingerprint -> DiceDBCmd
cmdWatchSubscriptionChan chan WatchSubscription // cmdWatchSubscriptionChan is the channel to send/receive watch subscription requests.
+ cmdWatchChan chan dstore.CmdWatchEvent // cmdWatchChan is the channel to send/receive watch events.
}
)
@@ -34,31 +35,32 @@ var (
}
)
-func NewManager(cmdWatchSubscriptionChan chan WatchSubscription) *Manager {
+func NewManager(cmdWatchSubscriptionChan chan WatchSubscription, cmdWatchChan chan dstore.CmdWatchEvent) *Manager {
return &Manager{
querySubscriptionMap: make(map[string]map[uint32]struct{}),
tcpSubscriptionMap: make(map[uint32]map[chan *cmd.DiceDBCmd]struct{}),
fingerprintCmdMap: make(map[uint32]*cmd.DiceDBCmd),
cmdWatchSubscriptionChan: cmdWatchSubscriptionChan,
+ cmdWatchChan: cmdWatchChan,
}
}
// Run starts the watch manager, listening for subscription requests and events
-func (m *Manager) Run(ctx context.Context, cmdWatchChan chan dstore.CmdWatchEvent) {
+func (m *Manager) Run(ctx context.Context) {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
- m.listenForEvents(ctx, cmdWatchChan)
+ m.listenForEvents(ctx)
}()
<-ctx.Done()
wg.Wait()
}
-func (m *Manager) listenForEvents(ctx context.Context, cmdWatchChan chan dstore.CmdWatchEvent) {
+func (m *Manager) listenForEvents(ctx context.Context) {
for {
select {
case <-ctx.Done():
@@ -69,7 +71,7 @@ func (m *Manager) listenForEvents(ctx context.Context, cmdWatchChan chan dstore.
} else {
m.handleUnsubscription(sub)
}
- case watchEvent := <-cmdWatchChan:
+ case watchEvent := <-m.cmdWatchChan:
m.handleWatchEvent(watchEvent)
}
}
diff --git a/internal/worker/cmd_meta.go b/internal/worker/cmd_meta.go
index 5c9ed2fb0..9720c4d61 100644
--- a/internal/worker/cmd_meta.go
+++ b/internal/worker/cmd_meta.go
@@ -74,6 +74,11 @@ const (
// Watch commands
const (
+ CmdSadd = "SADD"
+ CmdSrem = "SREM"
+ CmdScard = "SCARD"
+ CmdSmembers = "SMEMBERS"
+
CmdGetWatch = "GET.WATCH"
CmdGetUnWatch = "GET.UNWATCH"
CmdZRangeWatch = "ZRANGE.WATCH"
@@ -125,6 +130,12 @@ const (
CmdHDel = "HDEL"
CmdHMSet = "HMSET"
CmdHMGet = "HMGET"
+ CmdSetBit = "SETBIT"
+ CmdGetBit = "GETBIT"
+ CmdBitCount = "BITCOUNT"
+ CmdBitField = "BITFIELD"
+ CmdBitPos = "BITPOS"
+ CmdBitFieldRO = "BITFIELD_RO"
)
type CmdMeta struct {
@@ -187,6 +198,18 @@ var CommandsMeta = map[string]CmdMeta{
CmdGetDel: {
CmdType: SingleShard,
},
+ CmdSadd: {
+ CmdType: SingleShard,
+ },
+ CmdSrem: {
+ CmdType: SingleShard,
+ },
+ CmdScard: {
+ CmdType: SingleShard,
+ },
+ CmdSmembers: {
+ CmdType: SingleShard,
+ },
CmdHExists: {
CmdType: SingleShard,
},
@@ -250,6 +273,24 @@ var CommandsMeta = map[string]CmdMeta{
CmdHRandField: {
CmdType: SingleShard,
},
+ CmdSetBit: {
+ CmdType: SingleShard,
+ },
+ CmdGetBit: {
+ CmdType: SingleShard,
+ },
+ CmdBitCount: {
+ CmdType: SingleShard,
+ },
+ CmdBitField: {
+ CmdType: SingleShard,
+ },
+ CmdBitPos: {
+ CmdType: SingleShard,
+ },
+ CmdBitFieldRO: {
+ CmdType: SingleShard,
+ },
// Multi-shard commands.
CmdRename: {
diff --git a/internal/worker/worker.go b/internal/worker/worker.go
index 304d3aced..8a33c28e5 100644
--- a/internal/worker/worker.go
+++ b/internal/worker/worker.go
@@ -14,6 +14,7 @@ import (
"time"
"github.com/dicedb/dice/internal/querymanager"
+ "github.com/dicedb/dice/internal/wal"
"github.com/dicedb/dice/internal/watchmanager"
"github.com/dicedb/dice/config"
@@ -49,12 +50,13 @@ type BaseWorker struct {
responseChan chan *ops.StoreResponse
preprocessingChan chan *ops.StoreResponse
cmdWatchSubscriptionChan chan watchmanager.WatchSubscription
+ wl wal.AbstractWAL
}
func NewWorker(wid string, responseChan, preprocessingChan chan *ops.StoreResponse,
cmdWatchSubscriptionChan chan watchmanager.WatchSubscription,
ioHandler iohandler.IOHandler, parser requestparser.Parser,
- shardManager *shard.ShardManager, gec chan error) *BaseWorker {
+ shardManager *shard.ShardManager, gec chan error, wl wal.AbstractWAL) *BaseWorker {
return &BaseWorker{
id: wid,
ioHandler: ioHandler,
@@ -63,9 +65,10 @@ func NewWorker(wid string, responseChan, preprocessingChan chan *ops.StoreRespon
globalErrorChan: gec,
responseChan: responseChan,
preprocessingChan: preprocessingChan,
- cmdWatchSubscriptionChan: cmdWatchSubscriptionChan,
Session: auth.NewSession(),
adhocReqChan: make(chan *cmd.DiceDBCmd, config.DiceConfig.Performance.AdhocReqChanBufSize),
+ cmdWatchSubscriptionChan: cmdWatchSubscriptionChan,
+ wl: wl,
}
}
@@ -267,29 +270,7 @@ func (w *BaseWorker) executeCommand(ctx context.Context, diceDBCmd *cmd.DiceDBCm
// Unsubscribe Unwatch command type
if meta.CmdType == Unwatch {
- // extract the fingerprint
- command := cmdList[len(cmdList)-1]
- fp, fperr := strconv.ParseUint(command.Args[0], 10, 32)
- if fperr != nil {
- err := w.ioHandler.Write(ctx, diceerrors.ErrInvalidFingerprint)
- if err != nil {
- return fmt.Errorf("error sending push response to client: %v", err)
- }
- return fperr
- }
-
- // send the unsubscribe request
- w.cmdWatchSubscriptionChan <- watchmanager.WatchSubscription{
- Subscribe: false,
- AdhocReqChan: w.adhocReqChan,
- Fingerprint: uint32(fp),
- }
-
- err := w.ioHandler.Write(ctx, "OK")
- if err != nil {
- return fmt.Errorf("error sending push response to client: %v", err)
- }
- return nil
+ return w.handleCommandUnwatch(ctx, cmdList)
}
// Scatter the broken-down commands to the appropriate shards.
@@ -304,13 +285,46 @@ func (w *BaseWorker) executeCommand(ctx context.Context, diceDBCmd *cmd.DiceDBCm
if meta.CmdType == Watch {
// Proceed to subscribe after successful execution
- w.cmdWatchSubscriptionChan <- watchmanager.WatchSubscription{
- Subscribe: true,
- WatchCmd: cmdList[len(cmdList)-1],
- AdhocReqChan: w.adhocReqChan,
+ w.handleCommandWatch(cmdList)
+ }
+
+ return nil
+}
+
+// handleCommandWatch sends a watch subscription request to the watch manager.
+func (w *BaseWorker) handleCommandWatch(cmdList []*cmd.DiceDBCmd) {
+ w.cmdWatchSubscriptionChan <- watchmanager.WatchSubscription{
+ Subscribe: true,
+ WatchCmd: cmdList[len(cmdList)-1],
+ AdhocReqChan: w.adhocReqChan,
+ }
+}
+
+// handleCommandUnwatch sends an unwatch subscription request to the watch manager. It also sends a response to the client.
+// The response is sent before the unwatch request is processed by the watch manager.
+func (w *BaseWorker) handleCommandUnwatch(ctx context.Context, cmdList []*cmd.DiceDBCmd) error {
+ // extract the fingerprint
+ command := cmdList[len(cmdList)-1]
+ fp, parseErr := strconv.ParseUint(command.Args[0], 10, 32)
+ if parseErr != nil {
+ err := w.ioHandler.Write(ctx, diceerrors.ErrInvalidFingerprint)
+ if err != nil {
+ return fmt.Errorf("error sending push response to client: %v", err)
}
+ return parseErr
+ }
+
+ // send the unsubscribe request
+ w.cmdWatchSubscriptionChan <- watchmanager.WatchSubscription{
+ Subscribe: false,
+ AdhocReqChan: w.adhocReqChan,
+ Fingerprint: uint32(fp),
}
+ err := w.ioHandler.Write(ctx, clientio.RespOK)
+ if err != nil {
+ return fmt.Errorf("error sending push response to client: %v", err)
+ }
return nil
}
@@ -324,123 +338,148 @@ func (w *BaseWorker) scatter(ctx context.Context, cmds []*cmd.DiceDBCmd) error {
return ctx.Err()
default:
for i := uint8(0); i < uint8(len(cmds)); i++ {
- var rc chan *ops.StoreOp
- var sid shard.ShardID
- var key string
- if len(cmds[i].Args) > 0 {
- key = cmds[i].Args[0]
- } else {
- key = cmds[i].Cmd
- }
+ shardID, responseChan := w.shardManager.GetShardInfo(getRoutingKeyFromCommand(cmds[i]))
- sid, rc = w.shardManager.GetShardInfo(key)
-
- rc <- &ops.StoreOp{
+ responseChan <- &ops.StoreOp{
SeqID: i,
RequestID: GenerateUniqueRequestID(),
Cmd: cmds[i],
WorkerID: w.id,
- ShardID: sid,
+ ShardID: shardID,
Client: nil,
}
}
}
-
return nil
}
+// getRoutingKeyFromCommand determines the key used for shard routing
+func getRoutingKeyFromCommand(diceDBCmd *cmd.DiceDBCmd) string {
+ if len(diceDBCmd.Args) > 0 {
+ return diceDBCmd.Args[0]
+ }
+ return diceDBCmd.Cmd
+}
+
// gather collects the responses from multiple shards and writes the results into the provided buffer.
// It first waits for responses from all the shards and then processes the result based on the command type (SingleShard, Custom, or Multishard).
func (w *BaseWorker) gather(ctx context.Context, diceDBCmd *cmd.DiceDBCmd, numCmds int, isWatchNotification bool) error {
- // Loop to wait for messages from number of shards
+ // Collect responses from all shards
+ storeOp, err := w.gatherResponses(ctx, numCmds)
+ if err != nil {
+ return err
+ }
+
+ if len(storeOp) == 0 {
+ slog.Error("No response from shards",
+ slog.String("workerID", w.id),
+ slog.String("command", diceDBCmd.Cmd))
+ return fmt.Errorf("no response from shards for command: %s", diceDBCmd.Cmd)
+ }
+
+ if isWatchNotification {
+ return w.handleWatchNotification(ctx, diceDBCmd, storeOp[0])
+ }
+
+ // Process command based on its type
+ cmdMeta, ok := CommandsMeta[diceDBCmd.Cmd]
+ if !ok {
+ return w.handleLegacyCommand(ctx, storeOp[0])
+ }
+
+ return w.handleCommand(ctx, cmdMeta, diceDBCmd, storeOp)
+}
+
+// gatherResponses collects responses from all shards
+func (w *BaseWorker) gatherResponses(ctx context.Context, numCmds int) ([]ops.StoreResponse, error) {
var storeOp []ops.StoreResponse
- for numCmds != 0 {
+
+ for numCmds > 0 {
select {
case <-ctx.Done():
- slog.Error("Timed out waiting for response from shards", slog.String("workerID", w.id), slog.Any("error", ctx.Err()))
+ slog.Error("Timed out waiting for response from shards",
+ slog.String("workerID", w.id),
+ slog.Any("error", ctx.Err()))
+ return nil, ctx.Err()
+
case resp, ok := <-w.responseChan:
if ok {
storeOp = append(storeOp, *resp)
}
numCmds--
- continue
+
case sError, ok := <-w.shardManager.ShardErrorChan:
if ok {
- slog.Error("Error from shard", slog.String("workerID", w.id), slog.Any("error", sError))
+ slog.Error("Error from shard",
+ slog.String("workerID", w.id),
+ slog.Any("error", sError))
+ return nil, sError.Error
}
}
}
- val, ok := CommandsMeta[diceDBCmd.Cmd]
+ return storeOp, nil
+}
- if isWatchNotification {
- if storeOp[0].EvalResponse.Error != nil {
- err := w.ioHandler.Write(ctx, querymanager.GenericWatchResponse(diceDBCmd.Cmd, fmt.Sprintf("%d", diceDBCmd.GetFingerprint()), storeOp[0].EvalResponse.Error))
- if err != nil {
- slog.Debug("Error sending push response to client", slog.String("workerID", w.id), slog.Any("error", err))
- }
- return err
- }
+// handleWatchNotification processes watch notification responses
+func (w *BaseWorker) handleWatchNotification(ctx context.Context, diceDBCmd *cmd.DiceDBCmd, resp ops.StoreResponse) error {
+ fingerprint := fmt.Sprintf("%d", diceDBCmd.GetFingerprint())
- err := w.ioHandler.Write(ctx, querymanager.GenericWatchResponse(diceDBCmd.Cmd, fmt.Sprintf("%d", diceDBCmd.GetFingerprint()), storeOp[0].EvalResponse.Result))
- if err != nil {
- slog.Debug("Error sending push response to client", slog.String("workerID", w.id), slog.Any("error", err))
- return err
- }
- return nil // Exit after handling watch case
+ if resp.EvalResponse.Error != nil {
+ return w.writeResponse(ctx, querymanager.GenericWatchResponse(diceDBCmd.Cmd, fingerprint, resp.EvalResponse.Error))
}
- // TODO: Remove it once we have migrated all the commands
- if !ok {
+ return w.writeResponse(ctx, querymanager.GenericWatchResponse(diceDBCmd.Cmd, fingerprint, resp.EvalResponse.Result))
+}
+
+// handleLegacyCommand processes commands not in CommandsMeta
+func (w *BaseWorker) handleLegacyCommand(ctx context.Context, resp ops.StoreResponse) error {
+ if resp.EvalResponse.Error != nil {
+ return w.writeResponse(ctx, resp.EvalResponse.Error)
+ }
+ return w.writeResponse(ctx, resp.EvalResponse.Result)
+}
+
+// handleCommand processes commands based on their type
+func (w *BaseWorker) handleCommand(ctx context.Context, cmdMeta CmdMeta, diceDBCmd *cmd.DiceDBCmd, storeOp []ops.StoreResponse) error {
+ var err error
+
+ switch cmdMeta.CmdType {
+ case SingleShard, Custom:
if storeOp[0].EvalResponse.Error != nil {
- err := w.ioHandler.Write(ctx, storeOp[0].EvalResponse.Error)
- if err != nil {
- slog.Debug("Error sending response to client", slog.String("workerID", w.id), slog.Any("error", err))
- }
- return err
+ err = w.writeResponse(ctx, storeOp[0].EvalResponse.Error)
+ } else {
+ err = w.writeResponse(ctx, storeOp[0].EvalResponse.Result)
}
- err := w.ioHandler.Write(ctx, storeOp[0].EvalResponse.Result)
- if err != nil {
- slog.Debug("Error sending response to client", slog.String("workerID", w.id), slog.Any("error", err))
- return err
+ if err == nil && w.wl != nil {
+ w.wl.LogCommand(diceDBCmd)
}
- } else {
- switch val.CmdType {
- case SingleShard, Custom:
- // Handle single-shard or custom commands
- if storeOp[0].EvalResponse.Error != nil {
- err := w.ioHandler.Write(ctx, storeOp[0].EvalResponse.Error)
- if err != nil {
- slog.Debug("Error sending response to client", slog.String("workerID", w.id), slog.Any("error", err))
- }
- return err
- }
+ case MultiShard:
+ err = w.writeResponse(ctx, cmdMeta.composeResponse(storeOp...))
- err := w.ioHandler.Write(ctx, storeOp[0].EvalResponse.Result)
- if err != nil {
- slog.Debug("Error sending response to client", slog.String("workerID", w.id), slog.Any("error", err))
- return err
- }
-
- case MultiShard:
- err := w.ioHandler.Write(ctx, val.composeResponse(storeOp...))
- if err != nil {
- slog.Debug("Error sending response to client", slog.String("workerID", w.id), slog.Any("error", err))
- return err
- }
-
- default:
- slog.Error("Unknown command type", slog.String("workerID", w.id), slog.String("command", diceDBCmd.Cmd), slog.Any("evalResp", storeOp))
- err := w.ioHandler.Write(ctx, diceerrors.ErrInternalServer)
- if err != nil {
- slog.Debug("Error sending response to client", slog.String("workerID", w.id), slog.Any("error", err))
- return err
- }
+ if err == nil && w.wl != nil {
+ w.wl.LogCommand(diceDBCmd)
}
+ default:
+ slog.Error("Unknown command type",
+ slog.String("workerID", w.id),
+ slog.String("command", diceDBCmd.Cmd),
+ slog.Any("evalResp", storeOp))
+ err = w.writeResponse(ctx, diceerrors.ErrInternalServer)
}
+ return err
+}
- return nil
+// writeResponse handles writing responses and logging errors
+func (w *BaseWorker) writeResponse(ctx context.Context, response interface{}) error {
+ err := w.ioHandler.Write(ctx, response)
+ if err != nil {
+ slog.Debug("Error sending response to client",
+ slog.String("workerID", w.id),
+ slog.Any("error", err))
+ }
+ return err
}
func (w *BaseWorker) isAuthenticated(diceDBCmd *cmd.DiceDBCmd) error {
diff --git a/main.go b/main.go
index 20fbc109b..6913fa75d 100644
--- a/main.go
+++ b/main.go
@@ -15,9 +15,11 @@ import (
"strings"
"sync"
"syscall"
+ "time"
"github.com/dicedb/dice/internal/logger"
"github.com/dicedb/dice/internal/server/abstractserver"
+ "github.com/dicedb/dice/internal/wal"
"github.com/dicedb/dice/internal/watchmanager"
"github.com/dicedb/dice/config"
@@ -55,6 +57,11 @@ func init() {
flag.BoolVar(&config.EnableProfiling, "enable-profiling", false, "enable profiling and capture critical metrics and traces in .prof files")
flag.StringVar(&config.DiceConfig.Logging.LogLevel, "log-level", "info", "log level, values: info, debug")
+ flag.StringVar(&config.LogDir, "log-dir", "/tmp/dicedb", "log directory path")
+
+ flag.BoolVar(&config.EnableWAL, "enable-wal", false, "enable write-ahead logging")
+ flag.BoolVar(&config.RestoreFromWAL, "restore-wal", false, "restore the database from the WAL files")
+ flag.StringVar(&config.WALEngine, "wal-engine", "null", "wal engine to use, values: sqlite, aof")
flag.StringVar(&config.RequirePass, "requirepass", config.RequirePass, "enable authentication for the default user")
flag.StringVar(&config.CustomConfigFilePath, "o", config.CustomConfigFilePath, "dir path to create the config file")
@@ -174,10 +181,51 @@ func main() {
var (
queryWatchChan chan dstore.QueryWatchEvent
cmdWatchChan chan dstore.CmdWatchEvent
- cmdWatchSubscriptionChan = make(chan watchmanager.WatchSubscription)
serverErrCh = make(chan error, 2)
+ cmdWatchSubscriptionChan = make(chan watchmanager.WatchSubscription)
+ wl wal.AbstractWAL
)
+ wl, _ = wal.NewNullWAL()
+ slog.Info("running with", slog.Bool("enable-wal", config.EnableWAL))
+ if config.EnableWAL {
+ if config.WALEngine == "sqlite" {
+ _wl, err := wal.NewSQLiteWAL(config.LogDir)
+ if err != nil {
+ slog.Warn("could not create WAL with", slog.String("wal-engine", config.WALEngine), slog.Any("error", err))
+ sigs <- syscall.SIGKILL
+ return
+ }
+ wl = _wl
+ } else if config.WALEngine == "aof" {
+ _wl, err := wal.NewAOFWAL(config.LogDir)
+ if err != nil {
+ slog.Warn("could not create WAL with", slog.String("wal-engine", config.WALEngine), slog.Any("error", err))
+ sigs <- syscall.SIGKILL
+ return
+ }
+ wl = _wl
+ } else {
+ slog.Error("unsupported WAL engine", slog.String("engine", config.WALEngine))
+ sigs <- syscall.SIGKILL
+ return
+ }
+
+ if err := wl.Init(time.Now()); err != nil {
+ slog.Error("could not initialize WAL", slog.Any("error", err))
+ } else {
+ go wal.InitBG(wl)
+ }
+
+ slog.Debug("WAL initialization complete")
+
+ if config.RestoreFromWAL {
+ slog.Info("restoring database from WAL")
+ wal.ReplayWAL(wl)
+ slog.Info("database restored from WAL")
+ }
+ }
+
if config.EnableWatch {
bufSize := config.DiceConfig.Performance.WatchChanBufSize
queryWatchChan = make(chan dstore.QueryWatchEvent, bufSize)
@@ -229,11 +277,11 @@ func main() {
}
workerManager := worker.NewWorkerManager(config.DiceConfig.Performance.MaxClients, shardManager)
- respServer := resp.NewServer(shardManager, workerManager, cmdWatchSubscriptionChan, cmdWatchChan, serverErrCh)
+ respServer := resp.NewServer(shardManager, workerManager, cmdWatchSubscriptionChan, cmdWatchChan, serverErrCh, wl)
serverWg.Add(1)
go runServer(ctx, &serverWg, respServer, serverErrCh)
} else {
- asyncServer := server.NewAsyncServer(shardManager, queryWatchChan)
+ asyncServer := server.NewAsyncServer(shardManager, queryWatchChan, wl)
if err := asyncServer.FindPortAndBind(); err != nil {
slog.Error("Error finding and binding port", slog.Any("error", err))
sigs <- syscall.SIGKILL
@@ -243,14 +291,14 @@ func main() {
go runServer(ctx, &serverWg, asyncServer, serverErrCh)
if config.EnableHTTP {
- httpServer := server.NewHTTPServer(shardManager)
+ httpServer := server.NewHTTPServer(shardManager, wl)
serverWg.Add(1)
go runServer(ctx, &serverWg, httpServer, serverErrCh)
}
}
if config.EnableWebsocket {
- websocketServer := server.NewWebSocketServer(shardManager, config.WebsocketPort)
+ websocketServer := server.NewWebSocketServer(shardManager, config.WebsocketPort, wl)
serverWg.Add(1)
go runServer(ctx, &serverWg, websocketServer, serverErrCh)
}
@@ -276,6 +324,11 @@ func main() {
}
close(sigs)
+
+ if config.EnableWAL {
+ wal.ShutdownBG()
+ }
+
cancel()
wg.Wait()