diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6a61b0e0..d2edf2f4b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,3 +75,4 @@ jobs: run: | yum -y install gcc make make REDIS_CFLAGS='-Werror' + diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 5ed0e7a94..d792ae3ab 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -11,7 +11,7 @@ on: inputs: skipjobs: description: 'jobs to skip (delete the ones you wanna keep, do not leave empty)' - default: 'valgrind,sanitizer,tls,freebsd,macos,alpine,32bit,iothreads,ubuntu,centos,malloc,specific' + default: 'valgrind,sanitizer,tls,freebsd,macos,alpine,32bit,iothreads,ubuntu,centos,malloc,specific,reply-schema' skiptests: description: 'tests to skip (delete the ones you wanna keep, do not leave empty)' default: 'redis,modules,sentinel,cluster,unittest' @@ -898,3 +898,44 @@ jobs: - name: cluster tests if: true && !contains(github.event.inputs.skiptests, 'cluster') run: ./runtest-cluster ${{github.event.inputs.cluster_test_args}} + + reply-schemas-validator: + runs-on: ubuntu-latest + timeout-minutes: 14400 + if: | + (github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) && + !contains(github.event.inputs.skipjobs, 'reply-schema') + steps: + - name: prep + if: github.event_name == 'workflow_dispatch' + run: | + echo "GITHUB_REPOSITORY=${{github.event.inputs.use_repo}}" >> $GITHUB_ENV + echo "GITHUB_HEAD_REF=${{github.event.inputs.use_git_ref}}" >> $GITHUB_ENV + echo "skipping: ${{github.event.inputs.skipjobs}} and ${{github.event.inputs.skiptests}}" + - uses: actions/checkout@v3 + with: + repository: ${{ env.GITHUB_REPOSITORY }} + ref: ${{ env.GITHUB_HEAD_REF }} + - name: make + run: make REDIS_CFLAGS='-Werror -DLOG_REQ_RES' + - name: testprep + run: sudo apt-get install tcl8.6 tclx + - name: test + if: true && !contains(github.event.inputs.skiptests, 'redis') + run: ./runtest --log-req-res --dont-clean --force-resp3 --tags -slow --verbose --dump-logs ${{github.event.inputs.test_args}} + - name: module api test + if: true && !contains(github.event.inputs.skiptests, 'modules') + run: ./runtest-moduleapi --log-req-res --dont-clean --force-resp3 --dont-pre-clean --verbose --dump-logs ${{github.event.inputs.test_args}} + - name: sentinel tests + if: true && !contains(github.event.inputs.skiptests, 'sentinel') + run: ./runtest-sentinel --log-req-res --dont-clean --force-resp3 ${{github.event.inputs.cluster_test_args}} + - name: cluster tests + if: true && !contains(github.event.inputs.skiptests, 'cluster') + run: ./runtest-cluster --log-req-res --dont-clean --force-resp3 ${{github.event.inputs.cluster_test_args}} + - name: Install Python dependencies + uses: py-actions/py-dependency-install@v4 + with: + path: "./utils/req-res-validator/requirements.txt" + - name: validator + run: ./utils/req-res-log-validator.py --verbose --fail-missing-reply-schemas --fail-commands-not-all-hit + diff --git a/.github/workflows/reply-schemas-linter.yml b/.github/workflows/reply-schemas-linter.yml new file mode 100644 index 000000000..13fc8ab88 --- /dev/null +++ b/.github/workflows/reply-schemas-linter.yml @@ -0,0 +1,22 @@ +name: Reply-schemas linter + +on: + push: + paths: + - 'src/commands/*.json' + pull_request: + paths: + - 'src/commands/*.json' + +jobs: + reply-schemas-linter: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup nodejs + uses: actions/setup-node@v3 + - name: Install packages + run: npm install ajv + - name: linter + run: node ./utils/reply_schema_linter.js + diff --git a/src/Makefile b/src/Makefile index 3600d33d6..13e142442 100644 --- a/src/Makefile +++ b/src/Makefile @@ -330,9 +330,17 @@ QUIET_LINK = @printf ' %b %b\n' $(LINKCOLOR)LINK$(ENDCOLOR) $(BINCOLOR)$@$(EN QUIET_INSTALL = @printf ' %b %b\n' $(LINKCOLOR)INSTALL$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) 1>&2; endif +ifneq (, $(findstring LOG_REQ_RES, $(REDIS_CFLAGS))) + COMMANDS_FILENAME=commands_with_reply_schema + GEN_COMMANDS_FLAGS=--with-reply-schema +else + COMMANDS_FILENAME=commands + GEN_COMMANDS_FLAGS= +endif + REDIS_SERVER_NAME=redis-server$(PROG_SUFFIX) REDIS_SENTINEL_NAME=redis-sentinel$(PROG_SUFFIX) -REDIS_SERVER_OBJ=adlist.o quicklist.o ae.o anet.o dict.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o commands.o strl.o connection.o unix.o +REDIS_SERVER_OBJ=adlist.o quicklist.o ae.o anet.o dict.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o $(COMMANDS_FILENAME).o strl.o connection.o unix.o logreqres.o REDIS_CLI_NAME=redis-cli$(PROG_SUFFIX) REDIS_CLI_OBJ=anet.o adlist.o dict.o redis-cli.o zmalloc.o release.o ae.o redisassert.o crcspeed.o crc64.o siphash.o crc16.o monotonic.o cli_common.o mt19937-64.o strl.o REDIS_BENCHMARK_NAME=redis-benchmark$(PROG_SUFFIX) @@ -425,8 +433,8 @@ DEP = $(REDIS_SERVER_OBJ:%.o=%.d) $(REDIS_CLI_OBJ:%.o=%.d) $(REDIS_BENCHMARK_OBJ # The file commands.c is checked in and doesn't normally need to be rebuilt. It # is built only if python is available and its prereqs are modified. ifneq (,$(PYTHON)) -commands.c: commands/*.json ../utils/generate-command-code.py - $(QUIET_GEN)$(PYTHON) ../utils/generate-command-code.py +$(COMMANDS_FILENAME).c: commands/*.json ../utils/generate-command-code.py + $(QUIET_GEN)$(PYTHON) ../utils/generate-command-code.py $(GEN_COMMANDS_FLAGS) endif clean: diff --git a/src/blocked.c b/src/blocked.c index 5909d2522..753442c2b 100644 --- a/src/blocked.c +++ b/src/blocked.c @@ -195,6 +195,11 @@ void unblockClient(client *c) { * or in case a shutdown operation was canceled and we are still in the processCommand sequence */ if (!(c->flags & CLIENT_PENDING_COMMAND) && c->bstate.btype != BLOCKED_SHUTDOWN) { freeClientOriginalArgv(c); + /* Clients that are not blocked on keys are not reprocessed so we must + * call reqresAppendResponse here (for clients blocked on key, + * unblockClientOnKey is called, which eventually calls processCommand, + * which calls reqresAppendResponse) */ + reqresAppendResponse(c); resetClient(c); } @@ -612,6 +617,8 @@ static void unblockClientOnKey(client *c, robj *key) { c->bstate.btype == BLOCKED_LIST || c->bstate.btype == BLOCKED_ZSET); + /* We need to unblock the client before calling processCommandAndResetClient + * because it checks the CLIENT_BLOCKED flag */ unblockClient(c); /* In case this client was blocked on keys during command * we need to re process the command again */ diff --git a/src/commands/acl-cat.json b/src/commands/acl-cat.json index a132cbcf4..635e2b88d 100644 --- a/src/commands/acl-cat.json +++ b/src/commands/acl-cat.json @@ -13,6 +13,24 @@ "STALE", "SENTINEL" ], + "reply_schema": { + "anyOf": [ + { + "type": "array", + "description": "In case `categoryname` was not given, a list of existing ACL categories", + "items": { + "type": "string" + } + }, + { + "type": "array", + "description": "In case `categoryname` was given, list of commands that fall under the provided ACL category", + "items": { + "type": "string" + } + } + ] + }, "arguments": [ { "name": "categoryname", diff --git a/src/commands/acl-deluser.json b/src/commands/acl-deluser.json index 3c61557d4..9568d6d4b 100644 --- a/src/commands/acl-deluser.json +++ b/src/commands/acl-deluser.json @@ -14,6 +14,10 @@ "STALE", "SENTINEL" ], + "reply_schema": { + "type": "integer", + "description": "The number of users that were deleted" + }, "arguments": [ { "name": "username", diff --git a/src/commands/acl-dryrun.json b/src/commands/acl-dryrun.json index 544858c3a..f8d009d4d 100644 --- a/src/commands/acl-dryrun.json +++ b/src/commands/acl-dryrun.json @@ -15,6 +15,18 @@ "STALE", "SENTINEL" ], + "reply_schema": { + "anyOf": [ + { + "const": "OK", + "description": "The given user may successfully execute the given command." + }, + { + "type": "string", + "description": "The description of the problem, in case the user is not allowed to run the given command." + } + ] + }, "arguments": [ { "name": "username", diff --git a/src/commands/acl-genpass.json b/src/commands/acl-genpass.json index 9de0313ec..8af04875a 100644 --- a/src/commands/acl-genpass.json +++ b/src/commands/acl-genpass.json @@ -13,6 +13,10 @@ "STALE", "SENTINEL" ], + "reply_schema": { + "type": "string", + "description": "Pseudorandom data. By default it contains 64 bytes, representing 256 bits of data. If `bits` was given, the output string length is the number of specified bits (rounded to the next multiple of 4) divided by 4." + }, "arguments": [ { "name": "bits", diff --git a/src/commands/acl-getuser.json b/src/commands/acl-getuser.json index b87c7f6d4..b09b6abeb 100644 --- a/src/commands/acl-getuser.json +++ b/src/commands/acl-getuser.json @@ -29,6 +29,63 @@ "name": "username", "type": "string" } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "a set of ACL rule definitions for the user", + "type": "object", + "additionalProperties": false, + "properties": { + "flags": { + "type": "array", + "items": { + "type": "string" + } + }, + "passwords": { + "type": "array", + "items": { + "type": "string" + } + }, + "commands": { + "description": "root selector's commands", + "type": "string" + }, + "keys": { + "description": "root selector's keys", + "type": "string" + }, + "channels": { + "description": "root selector's channels", + "type": "string" + }, + "selectors": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "commands": { + "type": "string" + }, + "keys": { + "type": "string" + }, + "channels": { + "type": "string" + } + } + } + } + } + }, + { + "description": "If user does not exist", + "type": "null" + } + ] + } } } diff --git a/src/commands/acl-help.json b/src/commands/acl-help.json index 1cec00a53..f6afea308 100644 --- a/src/commands/acl-help.json +++ b/src/commands/acl-help.json @@ -11,6 +11,13 @@ "LOADING", "STALE", "SENTINEL" - ] + ], + "reply_schema": { + "type": "array", + "description": "A list of subcommands and their description", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/acl-list.json b/src/commands/acl-list.json index f7a740d9d..cb4de7e9f 100644 --- a/src/commands/acl-list.json +++ b/src/commands/acl-list.json @@ -13,6 +13,13 @@ "LOADING", "STALE", "SENTINEL" - ] + ], + "reply_schema": { + "type": "array", + "description": "A list of currently active ACL rules", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/acl-load.json b/src/commands/acl-load.json index a4f138e48..b9d377c2d 100644 --- a/src/commands/acl-load.json +++ b/src/commands/acl-load.json @@ -13,6 +13,9 @@ "LOADING", "STALE", "SENTINEL" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/acl-log.json b/src/commands/acl-log.json index 0e88ed68b..eb3a59b0e 100644 --- a/src/commands/acl-log.json +++ b/src/commands/acl-log.json @@ -20,6 +20,54 @@ "STALE", "SENTINEL" ], + "reply_schema": { + "oneOf": [ + { + "description": "In case `RESET` was not given, a list of recent ACL security events.", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "count": { + "type": "integer" + }, + "reason": { + "type": "string" + }, + "context": { + "type": "string" + }, + "object": { + "type": "string" + }, + "username": { + "type": "string" + }, + "age-seconds": { + "type": "number" + }, + "client-info": { + "type": "string" + }, + "entry-id": { + "type": "integer" + }, + "timestamp-created": { + "type": "integer" + }, + "timestamp-last-updated": { + "type": "integer" + } + } + } + }, + { + "const": "OK", + "description": "In case `RESET` was given, OK indicates ACL log was cleared." + } + ] + }, "arguments": [ { "name": "operation", diff --git a/src/commands/acl-save.json b/src/commands/acl-save.json index 0c6ee8a1d..4e2942f5d 100644 --- a/src/commands/acl-save.json +++ b/src/commands/acl-save.json @@ -13,6 +13,9 @@ "LOADING", "STALE", "SENTINEL" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/acl-setuser.json b/src/commands/acl-setuser.json index 7f1f308df..875840d42 100644 --- a/src/commands/acl-setuser.json +++ b/src/commands/acl-setuser.json @@ -24,6 +24,9 @@ "STALE", "SENTINEL" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "username", diff --git a/src/commands/acl-users.json b/src/commands/acl-users.json index 5d00edbbf..13e8eba95 100644 --- a/src/commands/acl-users.json +++ b/src/commands/acl-users.json @@ -13,6 +13,13 @@ "LOADING", "STALE", "SENTINEL" - ] + ], + "reply_schema": { + "type": "array", + "description": "List of existing ACL users", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/acl-whoami.json b/src/commands/acl-whoami.json index 7c3cc9ace..b0477b363 100644 --- a/src/commands/acl-whoami.json +++ b/src/commands/acl-whoami.json @@ -12,6 +12,10 @@ "LOADING", "STALE", "SENTINEL" - ] + ], + "reply_schema": { + "type": "string", + "description": "The username of the current connection." + } } } diff --git a/src/commands/append.json b/src/commands/append.json index 77d170541..0af5e7bb1 100644 --- a/src/commands/append.json +++ b/src/commands/append.json @@ -34,6 +34,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "The the length of the string after the append operation." + }, "arguments": [ { "name": "key", diff --git a/src/commands/asking.json b/src/commands/asking.json index a825804d1..432c84709 100644 --- a/src/commands/asking.json +++ b/src/commands/asking.json @@ -11,6 +11,9 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/auth.json b/src/commands/auth.json index ff5e4b285..aaad29ad2 100644 --- a/src/commands/auth.json +++ b/src/commands/auth.json @@ -24,6 +24,9 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "username", diff --git a/src/commands/bgrewriteaof.json b/src/commands/bgrewriteaof.json index 27d64dd93..e73351294 100644 --- a/src/commands/bgrewriteaof.json +++ b/src/commands/bgrewriteaof.json @@ -10,6 +10,10 @@ "NO_ASYNC_LOADING", "ADMIN", "NOSCRIPT" - ] + ], + "reply_schema": { + "description": "A simple string reply indicating that the rewriting started or is about to start ASAP", + "type": "string" + } } } diff --git a/src/commands/bgsave.json b/src/commands/bgsave.json index 4b645db4b..28aa14a02 100644 --- a/src/commands/bgsave.json +++ b/src/commands/bgsave.json @@ -25,6 +25,16 @@ "optional": true, "since": "3.2.2" } - ] + ], + "reply_schema": { + "oneOf": [ + { + "const": "Background saving started" + }, + { + "const": "Background saving scheduled" + } + ] + } } } diff --git a/src/commands/bitcount.json b/src/commands/bitcount.json index da34eec31..7906415b6 100644 --- a/src/commands/bitcount.json +++ b/src/commands/bitcount.json @@ -77,6 +77,11 @@ } ] } - ] + ], + "reply_schema": { + "description": "The number of bits set to 1.", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/bitfield.json b/src/commands/bitfield.json index 1f8538294..9f867ab55 100644 --- a/src/commands/bitfield.json +++ b/src/commands/bitfield.json @@ -139,6 +139,21 @@ } ] } - ] + ], + "reply_schema": { + "type": "array", + "items": { + "oneOf": [ + { + "description": "The result of the subcommand at the same position", + "type": "integer" + }, + { + "description": "In case OVERFLOW FAIL was given and overflows or underflows detected", + "type": "null" + } + ] + } + } } } diff --git a/src/commands/bitfield_ro.json b/src/commands/bitfield_ro.json index 92031564b..ccf1aae23 100644 --- a/src/commands/bitfield_ro.json +++ b/src/commands/bitfield_ro.json @@ -57,6 +57,13 @@ } ] } - ] + ], + "reply_schema": { + "type": "array", + "items": { + "description": "The result of the subcommand at the same position", + "type": "integer" + } + } } } diff --git a/src/commands/bitop.json b/src/commands/bitop.json index 0ddcaa974..7c959683e 100644 --- a/src/commands/bitop.json +++ b/src/commands/bitop.json @@ -89,6 +89,11 @@ "key_spec_index": 1, "multiple": true } - ] + ], + "reply_schema": { + "description": "the size of the string stored in the destination key, that is equal to the size of the longest input string", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/bitpos.json b/src/commands/bitpos.json index 24e357dcc..439b61246 100644 --- a/src/commands/bitpos.json +++ b/src/commands/bitpos.json @@ -88,6 +88,19 @@ } ] } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "the position of the first bit set to 1 or 0 according to the request", + "type": "integer", + "minimum": 0 + }, + { + "description": "In case the `bit` argument is 1 and the string is empty or composed of just zero bytes", + "const": -1 + } + ] + } } } diff --git a/src/commands/blmove.json b/src/commands/blmove.json index 62036147b..cabcbbc7f 100644 --- a/src/commands/blmove.json +++ b/src/commands/blmove.json @@ -54,6 +54,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The popped element.", + "type": "string" + }, + { + "description": "Operation timed-out", + "type": "null" + } + ] + }, "arguments": [ { "name": "source", diff --git a/src/commands/blmpop.json b/src/commands/blmpop.json index 29d381ad8..419eb3e6f 100644 --- a/src/commands/blmpop.json +++ b/src/commands/blmpop.json @@ -35,6 +35,34 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Operation timed-out", + "type": "null" + }, + { + "description": "The key from which elements were popped and the popped elements", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "List key from which elements were popped.", + "type": "string" + }, + { + "description": "Array of popped elements.", + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ] + } + ] + }, "arguments": [ { "name": "timeout", diff --git a/src/commands/blpop.json b/src/commands/blpop.json index 687165481..63625309b 100644 --- a/src/commands/blpop.json +++ b/src/commands/blpop.json @@ -41,6 +41,30 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "null", + "description": "No element could be popped and timeout expired" + }, + { + "description": "The key from which the element was popped and the value of the popped element", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "List key from which the element was popped.", + "type": "string" + }, + { + "description": "Value of the popped element.", + "type": "string" + } + ] + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/brpop.json b/src/commands/brpop.json index 8f65202dd..faea8b8c0 100644 --- a/src/commands/brpop.json +++ b/src/commands/brpop.json @@ -52,6 +52,29 @@ "name": "timeout", "type": "double" } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "No element could be popped and the timeout expired.", + "type": "null" + }, + { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "The name of the key where an element was popped ", + "type": "string" + }, + { + "description": "The value of the popped element", + "type": "string" + } + ] + } + ] + } } } diff --git a/src/commands/brpoplpush.json b/src/commands/brpoplpush.json index 7f8d11aba..e959d0fe1 100644 --- a/src/commands/brpoplpush.json +++ b/src/commands/brpoplpush.json @@ -65,6 +65,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "string", + "description": "The element being popped from source and pushed to destination." + }, + { + "type": "null", + "description": "Timeout is reached." + } + ] + }, "arguments": [ { "name": "source", diff --git a/src/commands/bzmpop.json b/src/commands/bzmpop.json index 87a1cd8b3..30d502dc3 100644 --- a/src/commands/bzmpop.json +++ b/src/commands/bzmpop.json @@ -35,6 +35,46 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Timeout reached and no elements were popped.", + "type": "null" + }, + { + "description": "The keyname and the popped members.", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Keyname", + "type": "string" + }, + { + "description": "Popped members and their scores.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Member", + "type": "string" + }, + { + "description": "Score", + "type": "number" + } + ] + } + } + ] + } + ] + }, "arguments": [ { "name": "timeout", diff --git a/src/commands/bzpopmax.json b/src/commands/bzpopmax.json index 5ca53aa2a..f1f686179 100644 --- a/src/commands/bzpopmax.json +++ b/src/commands/bzpopmax.json @@ -42,6 +42,34 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Timeout reached and no elements were popped.", + "type": "null" + }, + { + "description": "The keyname, popped member, and its score.", + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "description": "Keyname", + "type": "string" + }, + { + "description": "Member", + "type": "string" + }, + { + "description": "Score", + "type": "number" + } + ] + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/bzpopmin.json b/src/commands/bzpopmin.json index 742a2310c..312f409e5 100644 --- a/src/commands/bzpopmin.json +++ b/src/commands/bzpopmin.json @@ -42,6 +42,34 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Timeout reached and no elements were popped.", + "type": "null" + }, + { + "description": "The keyname, popped member, and its score.", + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "description": "Keyname", + "type": "string" + }, + { + "description": "Member", + "type": "string" + }, + { + "description": "Score", + "type": "number" + } + ] + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/client-caching.json b/src/commands/client-caching.json index b8beaa8b6..22172e094 100644 --- a/src/commands/client-caching.json +++ b/src/commands/client-caching.json @@ -16,6 +16,9 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "mode", diff --git a/src/commands/client-getname.json b/src/commands/client-getname.json index 515e0ed67..c8cb21946 100644 --- a/src/commands/client-getname.json +++ b/src/commands/client-getname.json @@ -15,6 +15,18 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "oneOf": [ + { + "type": "string", + "description": "The connection name of the current connection" + }, + { + "type": "null", + "description": "Connection name was not set" + } + ] + } } } diff --git a/src/commands/client-getredir.json b/src/commands/client-getredir.json index 8d5b23997..5cbc27b87 100644 --- a/src/commands/client-getredir.json +++ b/src/commands/client-getredir.json @@ -15,6 +15,23 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "oneOf": [ + { + "const": 0, + "description": "Not redirecting notifications to any client." + }, + { + "const": -1, + "description": "Client tracking is not enabled." + }, + { + "type": "integer", + "description": "ID of the client we are redirecting the notifications to.", + "minimum": 1 + } + ] + } } } diff --git a/src/commands/client-help.json b/src/commands/client-help.json index fee49f9b8..ab18a6e6c 100644 --- a/src/commands/client-help.json +++ b/src/commands/client-help.json @@ -14,6 +14,13 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/client-id.json b/src/commands/client-id.json index 792da7dbb..771a3d057 100644 --- a/src/commands/client-id.json +++ b/src/commands/client-id.json @@ -15,6 +15,10 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "type": "integer", + "description": "The id of the client" + } } } diff --git a/src/commands/client-info.json b/src/commands/client-info.json index 06fa094bb..19c0a3cfd 100644 --- a/src/commands/client-info.json +++ b/src/commands/client-info.json @@ -18,6 +18,10 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "description": "a unique string, as described at the CLIENT LIST page, for the current client", + "type": "string" + } } } diff --git a/src/commands/client-kill.json b/src/commands/client-kill.json index 0d48f858c..5791eea40 100644 --- a/src/commands/client-kill.json +++ b/src/commands/client-kill.json @@ -141,6 +141,19 @@ } ] } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "when called in 3 argument format", + "const": "OK" + }, + { + "description": "when called in filter/value format, the number of clients killed", + "type": "integer", + "minimum": 0 + } + ] + } } } diff --git a/src/commands/client-list.json b/src/commands/client-list.json index e34a3cd63..5c822e6c3 100644 --- a/src/commands/client-list.json +++ b/src/commands/client-list.json @@ -46,6 +46,10 @@ "command_tips": [ "NONDETERMINISTIC_OUTPUT" ], + "reply_schema": { + "type": "string", + "description": "Information and statistics about client connections" + }, "arguments": [ { "token": "TYPE", diff --git a/src/commands/client-no-evict.json b/src/commands/client-no-evict.json index fc0ad71c9..9cfb06628 100644 --- a/src/commands/client-no-evict.json +++ b/src/commands/client-no-evict.json @@ -34,6 +34,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/client-no-touch.json b/src/commands/client-no-touch.json index f59320f21..03c2ad0dc 100644 --- a/src/commands/client-no-touch.json +++ b/src/commands/client-no-touch.json @@ -15,6 +15,9 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "enabled", diff --git a/src/commands/client-pause.json b/src/commands/client-pause.json index 57c88951c..2be85424c 100644 --- a/src/commands/client-pause.json +++ b/src/commands/client-pause.json @@ -46,6 +46,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/client-reply.json b/src/commands/client-reply.json index 5aa919cca..51e93c81d 100644 --- a/src/commands/client-reply.json +++ b/src/commands/client-reply.json @@ -16,6 +16,10 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "const": "OK", + "description": "When called with either OFF or SKIP subcommands, no reply is made. When called with ON, reply is OK." + }, "arguments": [ { "name": "action", diff --git a/src/commands/client-setname.json b/src/commands/client-setname.json index cc9199fea..c53bf1651 100644 --- a/src/commands/client-setname.json +++ b/src/commands/client-setname.json @@ -21,6 +21,9 @@ "name": "connection-name", "type": "string" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/client-tracking.json b/src/commands/client-tracking.json index 40811f155..48ed0f7ea 100644 --- a/src/commands/client-tracking.json +++ b/src/commands/client-tracking.json @@ -71,6 +71,10 @@ "type": "pure-token", "optional": true } - ] + ], + "reply_schema": { + "description": "if the client was successfully put into or taken out of tracking mode", + "const": "OK" + } } } diff --git a/src/commands/client-trackinginfo.json b/src/commands/client-trackinginfo.json index 124c44281..6873c7512 100644 --- a/src/commands/client-trackinginfo.json +++ b/src/commands/client-trackinginfo.json @@ -15,6 +15,66 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "flags": { + "type": "array", + "items": { + "oneOf": [ + { + "const": "off", + "description": "The connection isn't using server assisted client side caching." + }, + { + "const": "on", + "description": "Server assisted client side caching is enabled for the connection." + }, + { + "const": "bcast", + "description": "The client uses broadcasting mode." + }, + { + "const": "optin", + "description": "The client does not cache keys by default." + }, + { + "const": "optout", + "description": "The client caches keys by default." + }, + { + "const": "caching-yes", + "description": "The next command will cache keys (exists only together with optin)." + }, + { + "const": "caching-no", + "description": "The next command won't cache keys (exists only together with optout)." + }, + { + "const": "noloop", + "description": "The client isn't notified about keys modified by itself." + }, + { + "const": "broken_redirect", + "description": "The client ID used for redirection isn't valid anymore." + } + ] + } + }, + "redirect": { + "type": "integer", + "description": "The client ID used for notifications redirection, or -1 when none." + }, + "prefixes": { + "type": "array", + "description": "List of key prefixes for which notifications are sent to the client.", + "items": { + "type": "string" + } + } + } + } } } diff --git a/src/commands/client-unblock.json b/src/commands/client-unblock.json index 4b37d2fbd..c96b78d24 100644 --- a/src/commands/client-unblock.json +++ b/src/commands/client-unblock.json @@ -17,6 +17,18 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "oneOf": [ + { + "const": 0, + "description": "if the client was unblocked successfully" + }, + { + "const": 1, + "description": "if the client wasn't unblocked" + } + ] + }, "arguments": [ { "name": "client-id", diff --git a/src/commands/client-unpause.json b/src/commands/client-unpause.json index 661baa0fd..186b9cc4a 100644 --- a/src/commands/client-unpause.json +++ b/src/commands/client-unpause.json @@ -16,6 +16,9 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-addslots.json b/src/commands/cluster-addslots.json index 0a2d0a82f..db06e1ddb 100644 --- a/src/commands/cluster-addslots.json +++ b/src/commands/cluster-addslots.json @@ -18,6 +18,9 @@ "type": "integer", "multiple": true } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-addslotsrange.json b/src/commands/cluster-addslotsrange.json index e0bc8f228..769392bca 100644 --- a/src/commands/cluster-addslotsrange.json +++ b/src/commands/cluster-addslotsrange.json @@ -28,6 +28,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-bumpepoch.json b/src/commands/cluster-bumpepoch.json index 66dc28f0a..ce4af47f3 100644 --- a/src/commands/cluster-bumpepoch.json +++ b/src/commands/cluster-bumpepoch.json @@ -14,6 +14,20 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "if the epoch was incremented", + "type": "string", + "pattern": "^BUMPED [0-9]*$" + }, + { + "description": "if the node already has the greatest config epoch in the cluster", + "type": "string", + "pattern": "^STILL [0-9]*$" + } + ] + } } } diff --git a/src/commands/cluster-count-failure-reports.json b/src/commands/cluster-count-failure-reports.json index bf25bc264..7964d2be6 100644 --- a/src/commands/cluster-count-failure-reports.json +++ b/src/commands/cluster-count-failure-reports.json @@ -19,6 +19,11 @@ "name": "node-id", "type": "string" } - ] + ], + "reply_schema": { + "description": "the number of active failure reports for the node", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/cluster-countkeysinslot.json b/src/commands/cluster-countkeysinslot.json index eefae6e16..c4e7d7d4b 100644 --- a/src/commands/cluster-countkeysinslot.json +++ b/src/commands/cluster-countkeysinslot.json @@ -15,6 +15,11 @@ "name": "slot", "type": "integer" } - ] + ], + "reply_schema": { + "description": "The number of keys in the specified hash slot", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/cluster-delslots.json b/src/commands/cluster-delslots.json index 89c147b35..cc96b214f 100644 --- a/src/commands/cluster-delslots.json +++ b/src/commands/cluster-delslots.json @@ -18,6 +18,9 @@ "type": "integer", "multiple": true } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-delslotsrange.json b/src/commands/cluster-delslotsrange.json index 68a620d69..2ecc81701 100644 --- a/src/commands/cluster-delslotsrange.json +++ b/src/commands/cluster-delslotsrange.json @@ -28,6 +28,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-failover.json b/src/commands/cluster-failover.json index e7daf241c..f58fd562a 100644 --- a/src/commands/cluster-failover.json +++ b/src/commands/cluster-failover.json @@ -30,6 +30,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-flushslots.json b/src/commands/cluster-flushslots.json index 214aa396c..7834a4f35 100644 --- a/src/commands/cluster-flushslots.json +++ b/src/commands/cluster-flushslots.json @@ -11,6 +11,9 @@ "NO_ASYNC_LOADING", "ADMIN", "STALE" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-forget.json b/src/commands/cluster-forget.json index 6668eab21..1c4e74aa3 100644 --- a/src/commands/cluster-forget.json +++ b/src/commands/cluster-forget.json @@ -17,6 +17,9 @@ "name": "node-id", "type": "string" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-getkeysinslot.json b/src/commands/cluster-getkeysinslot.json index 06c1b03c8..6aa90234e 100644 --- a/src/commands/cluster-getkeysinslot.json +++ b/src/commands/cluster-getkeysinslot.json @@ -22,6 +22,14 @@ "name": "count", "type": "integer" } - ] + ], + "reply_schema": { + "description": "an array with up to count elements", + "type": "array", + "items": { + "description": "key name", + "type": "string" + } + } } } diff --git a/src/commands/cluster-help.json b/src/commands/cluster-help.json index d0ddf11f2..59a8362d7 100644 --- a/src/commands/cluster-help.json +++ b/src/commands/cluster-help.json @@ -10,6 +10,13 @@ "command_flags": [ "LOADING", "STALE" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/cluster-info.json b/src/commands/cluster-info.json index 08250f15b..cf8b611b4 100644 --- a/src/commands/cluster-info.json +++ b/src/commands/cluster-info.json @@ -12,6 +12,10 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "description": "A map between named fields and values in the form of : lines separated by newlines composed by the two bytes CRLF", + "type": "string" + } } } diff --git a/src/commands/cluster-keyslot.json b/src/commands/cluster-keyslot.json index 10645477f..19f1ad65e 100644 --- a/src/commands/cluster-keyslot.json +++ b/src/commands/cluster-keyslot.json @@ -15,6 +15,11 @@ "name": "key", "type": "string" } - ] + ], + "reply_schema": { + "description": "The hash slot number for the specified key", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/cluster-links.json b/src/commands/cluster-links.json index 4d5024764..3681c7c55 100644 --- a/src/commands/cluster-links.json +++ b/src/commands/cluster-links.json @@ -12,6 +12,49 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "description": "an array of cluster links and their attributes", + "type": "array", + "items": { + "type": "object", + "properties": { + "direction": { + "description": "This link is established by the local node _to_ the peer, or accepted by the local node _from_ the peer.", + "oneOf": [ + { + "description": "connection initiated from peer", + "const": "from" + }, + { + "description": "connection initiated to peer", + "const": "to" + } + ] + }, + "node": { + "description": "the node id of the peer", + "type": "string" + }, + "create-time": { + "description": "unix time creation time of the link. (In the case of a _to_ link, this is the time when the TCP link is created by the local node, not the time when it is actually established.)", + "type": "integer" + }, + "events": { + "description": "events currently registered for the link. r means readable event, w means writable event", + "type": "string" + }, + "send-buffer-allocated": { + "description": "allocated size of the link's send buffer, which is used to buffer outgoing messages toward the peer", + "type": "integer" + }, + "send-buffer-used": { + "description": "size of the portion of the link's send buffer that is currently holding data(messages)", + "type": "integer" + } + }, + "additionalProperties": false + } + } } } diff --git a/src/commands/cluster-meet.json b/src/commands/cluster-meet.json index 63d2cc201..04d374ae2 100644 --- a/src/commands/cluster-meet.json +++ b/src/commands/cluster-meet.json @@ -33,6 +33,9 @@ "optional": true, "since": "4.0.0" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-myid.json b/src/commands/cluster-myid.json index dc4f8a773..f2c13b581 100644 --- a/src/commands/cluster-myid.json +++ b/src/commands/cluster-myid.json @@ -9,6 +9,10 @@ "function": "clusterCommand", "command_flags": [ "STALE" - ] + ], + "reply_schema": { + "description": "the node id", + "type": "string" + } } } diff --git a/src/commands/cluster-myshardid.json b/src/commands/cluster-myshardid.json index ffd26eec6..70ac1ddc3 100644 --- a/src/commands/cluster-myshardid.json +++ b/src/commands/cluster-myshardid.json @@ -13,6 +13,10 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "description": "the node's shard id", + "type": "string" + } } } diff --git a/src/commands/cluster-nodes.json b/src/commands/cluster-nodes.json index 945213985..6eb7f5385 100644 --- a/src/commands/cluster-nodes.json +++ b/src/commands/cluster-nodes.json @@ -12,6 +12,10 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "description": "the serialized cluster configuration", + "type": "string" + } } } diff --git a/src/commands/cluster-replicas.json b/src/commands/cluster-replicas.json index e86322bc1..a7c5a4604 100644 --- a/src/commands/cluster-replicas.json +++ b/src/commands/cluster-replicas.json @@ -19,6 +19,14 @@ "name": "node-id", "type": "string" } - ] + ], + "reply_schema": { + "description": "a list of replica nodes replicating from the specified master node provided in the same format used by CLUSTER NODES", + "type": "array", + "items": { + "type": "string", + "description": "the serialized cluster configuration" + } + } } } diff --git a/src/commands/cluster-replicate.json b/src/commands/cluster-replicate.json index beda5e788..d49be4fb2 100644 --- a/src/commands/cluster-replicate.json +++ b/src/commands/cluster-replicate.json @@ -17,6 +17,9 @@ "name": "node-id", "type": "string" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-reset.json b/src/commands/cluster-reset.json index 90c810838..cd49900c5 100644 --- a/src/commands/cluster-reset.json +++ b/src/commands/cluster-reset.json @@ -30,6 +30,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-saveconfig.json b/src/commands/cluster-saveconfig.json index 991286d09..09b73db5f 100644 --- a/src/commands/cluster-saveconfig.json +++ b/src/commands/cluster-saveconfig.json @@ -11,6 +11,9 @@ "NO_ASYNC_LOADING", "ADMIN", "STALE" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-set-config-epoch.json b/src/commands/cluster-set-config-epoch.json index 5f07f63c2..c92cc62a7 100644 --- a/src/commands/cluster-set-config-epoch.json +++ b/src/commands/cluster-set-config-epoch.json @@ -17,6 +17,9 @@ "name": "config-epoch", "type": "integer" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-setslot.json b/src/commands/cluster-setslot.json index df3b0448c..9ddff8136 100644 --- a/src/commands/cluster-setslot.json +++ b/src/commands/cluster-setslot.json @@ -46,6 +46,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-shards.json b/src/commands/cluster-shards.json index 925ce8bf3..ac45d70eb 100644 --- a/src/commands/cluster-shards.json +++ b/src/commands/cluster-shards.json @@ -13,6 +13,77 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "description": "a nested list of a map of hash ranges and shard nodes describing individual shards", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "slots": { + "description": "an even number element array specifying the start and end slot numbers for slot ranges owned by this shard", + "type": "array", + "items": { + "type": "string" + } + }, + "nodes": { + "description": "nodes that handle these slot ranges", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "tls-port": { + "type": "integer" + }, + "ip": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "role": { + "oneOf": [ + { + "const": "master" + }, + { + "const": "replica" + } + ] + }, + "replication-offset": { + "type": "integer" + }, + "health": { + "oneOf": [ + { + "const": "fail" + }, + { + "const": "loading" + }, + { + "const": "online" + } + ] + } + } + } + } + } + } + } } } diff --git a/src/commands/cluster-slaves.json b/src/commands/cluster-slaves.json index 0ea77a876..e7a0eb3be 100644 --- a/src/commands/cluster-slaves.json +++ b/src/commands/cluster-slaves.json @@ -24,6 +24,14 @@ "name": "node-id", "type": "string" } - ] + ], + "reply_schema": { + "description": "a list of replica nodes replicating from the specified master node provided in the same format used by CLUSTER NODES", + "type": "array", + "items": { + "type": "string", + "description": "the serialized cluster configuration" + } + } } } diff --git a/src/commands/cluster-slots.json b/src/commands/cluster-slots.json index e8782420e..3f76af518 100644 --- a/src/commands/cluster-slots.json +++ b/src/commands/cluster-slots.json @@ -27,6 +27,109 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "description": "nested list of slot ranges with networking information", + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 4294967295, + "items": [ + { + "description": "start slot number", + "type": "integer" + }, + { + "description": "end slot number", + "type": "integer" + }, + { + "type": "array", + "description": "Master node for the slot range", + "minItems": 4, + "maxItems": 4, + "items": [ + { + "description": "endpoint description", + "oneOf": [ + { + "description": "hostname or ip", + "type": "string" + }, + { + "description": "unknown type", + "type": "null" + } + ] + }, + { + "description": "port", + "type": "integer" + }, + { + "description": "node name", + "type": "string" + }, + { + "description": "array of node descriptions", + "type": "object", + "additionalProperties": false, + "properties": { + "hostname": { + "type": "string" + }, + "ip": { + "type": "string" + } + } + } + ] + } + ], + "additionalItems": { + "type": "array", + "description": "Replica node for the slot range", + "minItems": 4, + "maxItems": 4, + "items": [ + { + "description": "endpoint description", + "oneOf": [ + { + "description": "hostname or ip", + "type": "string" + }, + { + "description": "unknown type", + "type": "null" + } + ] + }, + { + "description": "port", + "type": "integer" + }, + { + "description": "node name", + "type": "string" + }, + { + "description": "array of node descriptions", + "type": "object", + "additionalProperties": false, + "properties": { + "hostname": { + "type": "string" + }, + "ip": { + "type": "string" + } + } + } + ] + } + } + } } } diff --git a/src/commands/command-count.json b/src/commands/command-count.json index f2081ca46..ecc29e4af 100644 --- a/src/commands/command-count.json +++ b/src/commands/command-count.json @@ -14,6 +14,10 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "description": "Number of total commands in this Redis server.", + "type": "integer" + } } } diff --git a/src/commands/command-docs.json b/src/commands/command-docs.json index 7dc81d61c..73ac9fd3e 100644 --- a/src/commands/command-docs.json +++ b/src/commands/command-docs.json @@ -18,6 +18,187 @@ "command_tips": [ "NONDETERMINISTIC_OUTPUT_ORDER" ], + "reply_schema": { + "description": "A map where each key is a command name, and each value is the documentary information", + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^.*$": { + "type": "object", + "additionalProperties": false, + "properties": { + "summary": { + "description": "short command description", + "type": "string" + }, + "since": { + "description": "the Redis version that added the command (or for module commands, the module version).", + "type": "string" + }, + "group": { + "description": "the functional group to which the command belongs", + "oneOf": [ + { + "const": "bitmap" + }, + { + "const": "cluster" + }, + { + "const": "connection" + }, + { + "const": "generic" + }, + { + "const": "geo" + }, + { + "const": "hash" + }, + { + "const": "hyperloglog" + }, + { + "const": "list" + }, + { + "const": "module" + }, + { + "const": "pubsub" + }, + { + "const": "scripting" + }, + { + "const": "sentinel" + }, + { + "const": "server" + }, + { + "const": "set" + }, + { + "const": "sorted-set" + }, + { + "const": "stream" + }, + { + "const": "string" + }, + { + "const": "transactions" + } + ] + }, + "complexity": { + "description": "a short explanation about the command's time complexity.", + "type": "string" + }, + "module": { + "type": "string" + }, + "doc_flags": { + "description": "an array of documentation flags", + "type": "array", + "items": { + "oneOf": [ + { + "description": "the command is deprecated.", + "const": "deprecated" + }, + { + "description": "a system command that isn't meant to be called by users.", + "const": "syscmd" + } + ] + } + }, + "deprecated_since": { + "description": "the Redis version that deprecated the command (or for module commands, the module version)", + "type": "string" + }, + "replaced_by": { + "description": "the alternative for a deprecated command.", + "type": "string" + }, + "history": { + "description": "an array of historical notes describing changes to the command's behavior or arguments.", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "The Redis version that the entry applies to." + }, + { + "type": "string", + "description": "The description of the change." + } + ] + } + }, + "arguments": { + "description": "an array of maps that describe the command's arguments.", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "display_text": { + "type": "string" + }, + "key_spec_index": { + "type": "integer" + }, + "token": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "since": { + "type": "string" + }, + "deprecated_since": { + "type": "string" + }, + "flags": { + "type": "array", + "items": { + "type": "string" + } + }, + "arguments": { + "type": "array" + } + } + } + }, + "reply_schema": { + "description": "command reply schema", + "type": "object" + }, + "subcommands": { + "description": "A map where each key is a subcommand, and each value is the documentary information", + "$ref": "#" + } + } + } + } + }, "arguments": [ { "name": "command-name", diff --git a/src/commands/command-getkeys.json b/src/commands/command-getkeys.json index 20bf7519b..92c95252d 100644 --- a/src/commands/command-getkeys.json +++ b/src/commands/command-getkeys.json @@ -15,6 +15,14 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "description": "List of keys from the given Redis command.", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, "arguments": [ { "name": "command", diff --git a/src/commands/command-getkeysandflags.json b/src/commands/command-getkeysandflags.json index ce96dd4fb..05668a241 100644 --- a/src/commands/command-getkeysandflags.json +++ b/src/commands/command-getkeysandflags.json @@ -15,6 +15,30 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "description": "List of keys from the given Redis command and their usage flags.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Key name", + "type": "string" + }, + { + "description": "Set of key flags", + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ] + } + }, "arguments": [ { "name": "command", diff --git a/src/commands/command-help.json b/src/commands/command-help.json index d5ad719f0..22d31563a 100644 --- a/src/commands/command-help.json +++ b/src/commands/command-help.json @@ -14,6 +14,13 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/command-info.json b/src/commands/command-info.json index 52ab40084..84e36dfaf 100644 --- a/src/commands/command-info.json +++ b/src/commands/command-info.json @@ -31,6 +31,183 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "type": "array", + "items": { + "oneOf": [ + { + "description": "command does not exist", + "type": "null" + }, + { + "description": "command info array output", + "type": "array", + "minItems": 10, + "maxItems": 10, + "items": [ + { + "description": "command name", + "type": "string" + }, + { + "description": "command arity", + "type": "integer" + }, + { + "description": "command flags", + "type": "array", + "items": { + "description": "command flag", + "type": "string" + } + }, + { + "description": "command first key index", + "type": "integer" + }, + { + "description": "command last key index", + "type": "integer" + }, + { + "description": "command key step index", + "type": "integer" + }, + { + "description": "command categories", + "type": "array", + "items": { + "description": "command category", + "type": "string" + } + }, + { + "description": "command tips", + "type": "array", + "items": { + "description": "command tip", + "type": "string" + } + }, + { + "description": "command key specs", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "notes": { + "type": "string" + }, + "flags": { + "type": "array", + "items": { + "type": "string" + } + }, + "begin_search": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string" + }, + "spec": { + "anyOf": [ + { + "description": "unknown type, empty map", + "type": "object", + "additionalProperties": false + }, + { + "description": "index type", + "type": "object", + "additionalProperties": false, + "properties": { + "index": { + "type": "integer" + } + } + }, + { + "description": "keyword type", + "type": "object", + "additionalProperties": false, + "properties": { + "keyword": { + "type": "string" + }, + "startfrom": { + "type": "integer" + } + } + } + ] + } + } + }, + "find_keys": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string" + }, + "spec": { + "anyOf": [ + { + "description": "unknown type", + "type": "object", + "additionalProperties": false + }, + { + "description": "range type", + "type": "object", + "additionalProperties": false, + "properties": { + "lastkey": { + "type": "integer" + }, + "keystep": { + "type": "integer" + }, + "limit": { + "type": "integer" + } + } + }, + { + "description": "keynum type", + "type": "object", + "additionalProperties": false, + "properties": { + "keynumidx": { + "type": "integer" + }, + "firstkey": { + "type": "integer" + }, + "keystep": { + "type": "integer" + } + } + } + ] + } + } + } + } + } + }, + { + "type": "array", + "description": "subcommands" + } + ] + } + ] + } + } } } diff --git a/src/commands/command-list.json b/src/commands/command-list.json index 9ef624f07..5d6be8071 100644 --- a/src/commands/command-list.json +++ b/src/commands/command-list.json @@ -42,6 +42,14 @@ } ] } - ] + ], + "reply_schema": { + "type": "array", + "items": { + "description": "command name", + "type": "string" + }, + "uniqueItems": true + } } } diff --git a/src/commands/config-get.json b/src/commands/config-get.json index 1ea387686..e21c64b78 100644 --- a/src/commands/config-get.json +++ b/src/commands/config-get.json @@ -19,6 +19,12 @@ "LOADING", "STALE" ], + "reply_schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "arguments": [ { "name": "parameter", diff --git a/src/commands/config-help.json b/src/commands/config-help.json index 537dd6bba..cd90bbb59 100644 --- a/src/commands/config-help.json +++ b/src/commands/config-help.json @@ -10,6 +10,13 @@ "command_flags": [ "LOADING", "STALE" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/config-resetstat.json b/src/commands/config-resetstat.json index 0180402ab..353d46bd2 100644 --- a/src/commands/config-resetstat.json +++ b/src/commands/config-resetstat.json @@ -12,6 +12,9 @@ "NOSCRIPT", "LOADING", "STALE" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/config-rewrite.json b/src/commands/config-rewrite.json index 4e31dd82d..3e37bf49a 100644 --- a/src/commands/config-rewrite.json +++ b/src/commands/config-rewrite.json @@ -12,6 +12,9 @@ "NOSCRIPT", "LOADING", "STALE" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/config-set.json b/src/commands/config-set.json index 3a337f5c6..6e95fe0ef 100644 --- a/src/commands/config-set.json +++ b/src/commands/config-set.json @@ -23,6 +23,9 @@ "REQUEST_POLICY:ALL_NODES", "RESPONSE_POLICY:ALL_SUCCEEDED" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "data", diff --git a/src/commands/copy.json b/src/commands/copy.json index 0ffb94997..450b7397a 100644 --- a/src/commands/copy.json +++ b/src/commands/copy.json @@ -74,6 +74,18 @@ "type": "pure-token", "optional": true } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "source was copied", + "const": 1 + }, + { + "description": "source was not copied", + "const": 0 + } + ] + } } } diff --git a/src/commands/dbsize.json b/src/commands/dbsize.json index 4d65574db..989e37d9f 100644 --- a/src/commands/dbsize.json +++ b/src/commands/dbsize.json @@ -16,6 +16,10 @@ "command_tips": [ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:AGG_SUM" - ] + ], + "reply_schema": { + "type": "integer", + "description": "The number of keys in the currently-selected database." + } } } diff --git a/src/commands/decr.json b/src/commands/decr.json index 4a5128d27..1bae4e098 100644 --- a/src/commands/decr.json +++ b/src/commands/decr.json @@ -35,6 +35,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "The value of the key after decrementing it." + }, "arguments": [ { "name": "key", diff --git a/src/commands/decrby.json b/src/commands/decrby.json index 19f376b8e..de5724c3a 100644 --- a/src/commands/decrby.json +++ b/src/commands/decrby.json @@ -35,6 +35,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "The value of the key after decrementing it." + }, "arguments": [ { "name": "key", diff --git a/src/commands/del.json b/src/commands/del.json index bc500a9af..6d2ff7209 100644 --- a/src/commands/del.json +++ b/src/commands/del.json @@ -36,6 +36,11 @@ } } ], + "reply_schema": { + "description": "the number of keys that were removed", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/discard.json b/src/commands/discard.json index 56589a84f..6ef0dd809 100644 --- a/src/commands/discard.json +++ b/src/commands/discard.json @@ -15,6 +15,9 @@ ], "acl_categories": [ "TRANSACTION" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/dump.json b/src/commands/dump.json index 2e9453cdd..c0f0aa0ea 100644 --- a/src/commands/dump.json +++ b/src/commands/dump.json @@ -35,6 +35,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The serialized value.", + "type": "string" + }, + { + "description": "Key does not exist.", + "type": "null" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/echo.json b/src/commands/echo.json index f38d10bc5..abb3d26f3 100644 --- a/src/commands/echo.json +++ b/src/commands/echo.json @@ -14,6 +14,10 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "description": "The given string", + "type": "string" + }, "arguments": [ { "name": "message", diff --git a/src/commands/eval.json b/src/commands/eval.json index 50fc022a2..4f25b2fe3 100644 --- a/src/commands/eval.json +++ b/src/commands/eval.json @@ -61,6 +61,9 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "description": "Return value depends on the script that is executed" + } } } diff --git a/src/commands/eval_ro.json b/src/commands/eval_ro.json index 5c74b0437..f9aa54942 100644 --- a/src/commands/eval_ro.json +++ b/src/commands/eval_ro.json @@ -60,6 +60,9 @@ "optional":true, "multiple": true } - ] + ], + "reply_schema": { + "description": "Return value depends on the script that is executed" + } } } diff --git a/src/commands/evalsha.json b/src/commands/evalsha.json index 9b68b87f1..66e4379c6 100644 --- a/src/commands/evalsha.json +++ b/src/commands/evalsha.json @@ -60,6 +60,9 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "description": "Return value depends on the script that is executed" + } } } diff --git a/src/commands/evalsha_ro.json b/src/commands/evalsha_ro.json index 2628613fb..8dca0964b 100644 --- a/src/commands/evalsha_ro.json +++ b/src/commands/evalsha_ro.json @@ -59,6 +59,9 @@ "optional":true, "multiple": true } - ] + ], + "reply_schema": { + "description": "Return value depends on the script that is executed" + } } } diff --git a/src/commands/exec.json b/src/commands/exec.json index 80856ef99..b5ec6f0ab 100644 --- a/src/commands/exec.json +++ b/src/commands/exec.json @@ -14,6 +14,18 @@ ], "acl_categories": [ "TRANSACTION" - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "Each element being the reply to each of the commands in the atomic transaction.", + "type": "array" + }, + { + "description": "The transaction was aborted because a `WATCH`ed key was touched", + "type": "null" + } + ] + } } } diff --git a/src/commands/exists.json b/src/commands/exists.json index b31363352..e8793cf2b 100644 --- a/src/commands/exists.json +++ b/src/commands/exists.json @@ -42,6 +42,10 @@ } } ], + "reply_schema": { + "description": "Number of keys that exist from those specified as arguments.", + "type": "integer" + }, "arguments": [ { "name": "key", diff --git a/src/commands/expire.json b/src/commands/expire.json index 712830d3e..f0236f1d5 100644 --- a/src/commands/expire.json +++ b/src/commands/expire.json @@ -39,6 +39,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The timeout was not set. e.g. key doesn't exist, or operation skipped due to the provided arguments.", + "const": 0 + }, + { + "description": "The timeout was set.", + "const": 1 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/expireat.json b/src/commands/expireat.json index 43d9b748e..a6e22754b 100644 --- a/src/commands/expireat.json +++ b/src/commands/expireat.json @@ -39,6 +39,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "const": 1, + "description": "The timeout was set." + }, + { + "const": 0, + "description": "The timeout was not set. e.g. key doesn't exist, or operation skipped due to the provided arguments." + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/expiretime.json b/src/commands/expiretime.json index 9393c1226..90d8525ad 100644 --- a/src/commands/expiretime.json +++ b/src/commands/expiretime.json @@ -33,6 +33,23 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "integer", + "description": "Expiration Unix timestamp in seconds.", + "minimum": 0 + }, + { + "const": -1, + "description": "The key exists but has no associated expiration time." + }, + { + "const": -2, + "description": "The key does not exist." + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/failover.json b/src/commands/failover.json index dd2e2951f..d208bfcf1 100644 --- a/src/commands/failover.json +++ b/src/commands/failover.json @@ -11,6 +11,9 @@ "NOSCRIPT", "STALE" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "target", diff --git a/src/commands/fcall.json b/src/commands/fcall.json index 9e7a905ec..5817ed7c9 100644 --- a/src/commands/fcall.json +++ b/src/commands/fcall.json @@ -61,6 +61,9 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "description": "Return value depends on the function that is executed" + } } } diff --git a/src/commands/fcall_ro.json b/src/commands/fcall_ro.json index 6ba2736c0..def485ffa 100644 --- a/src/commands/fcall_ro.json +++ b/src/commands/fcall_ro.json @@ -60,6 +60,9 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "description": "Return value depends on the function that is executed" + } } } diff --git a/src/commands/flushall.json b/src/commands/flushall.json index 4add012ba..dc5c7ca39 100644 --- a/src/commands/flushall.json +++ b/src/commands/flushall.json @@ -27,6 +27,9 @@ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:ALL_SUCCEEDED" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "flush-type", diff --git a/src/commands/flushdb.json b/src/commands/flushdb.json index 858939f07..1590221eb 100644 --- a/src/commands/flushdb.json +++ b/src/commands/flushdb.json @@ -27,6 +27,9 @@ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:ALL_SUCCEEDED" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "flush-type", diff --git a/src/commands/function-delete.json b/src/commands/function-delete.json index 01dc78ba4..c56a0fbe6 100644 --- a/src/commands/function-delete.json +++ b/src/commands/function-delete.json @@ -23,6 +23,9 @@ "name": "library-name", "type": "string" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/function-dump.json b/src/commands/function-dump.json index de402f589..b535b28ad 100644 --- a/src/commands/function-dump.json +++ b/src/commands/function-dump.json @@ -12,6 +12,10 @@ ], "acl_categories": [ "SCRIPTING" - ] + ], + "reply_schema": { + "description": "the serialized payload", + "type": "string" + } } } diff --git a/src/commands/function-flush.json b/src/commands/function-flush.json index a2ab58e4f..538074878 100644 --- a/src/commands/function-flush.json +++ b/src/commands/function-flush.json @@ -36,6 +36,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/function-help.json b/src/commands/function-help.json index b8213cb61..37a3826df 100644 --- a/src/commands/function-help.json +++ b/src/commands/function-help.json @@ -13,6 +13,13 @@ ], "acl_categories": [ "SCRIPTING" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/function-kill.json b/src/commands/function-kill.json index 87432f996..2f9ae833e 100644 --- a/src/commands/function-kill.json +++ b/src/commands/function-kill.json @@ -17,6 +17,9 @@ "command_tips": [ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:ONE_SUCCEEDED" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/function-list.json b/src/commands/function-list.json index 6513b80cb..89c5162e7 100644 --- a/src/commands/function-list.json +++ b/src/commands/function-list.json @@ -16,6 +16,59 @@ "acl_categories": [ "SCRIPTING" ], + "reply_schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "library_name": { + "description": " the name of the library", + "type": "string" + }, + "engine": { + "description": "the engine of the library", + "type": "string" + }, + "functions": { + "description": "the list of functions in the library", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "description": "the name of the function", + "type": "string" + }, + "description": { + "description": "the function's description", + "oneOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] + }, + "flags": { + "description": "an array of function flags", + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "library_code": { + "description": "the library's source code (when given the WITHCODE modifier)", + "type": "string" + } + } + } + }, "arguments": [ { "name": "library-name-pattern", diff --git a/src/commands/function-load.json b/src/commands/function-load.json index d04721279..28f5b2084 100644 --- a/src/commands/function-load.json +++ b/src/commands/function-load.json @@ -30,6 +30,10 @@ "name": "function-code", "type": "string" } - ] + ], + "reply_schema": { + "description": "The library name that was loaded", + "type": "string" + } } } diff --git a/src/commands/function-restore.json b/src/commands/function-restore.json index ede016895..900602577 100644 --- a/src/commands/function-restore.json +++ b/src/commands/function-restore.json @@ -46,6 +46,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/function-stats.json b/src/commands/function-stats.json index 0d055b65b..8a64b9246 100644 --- a/src/commands/function-stats.json +++ b/src/commands/function-stats.json @@ -18,6 +18,64 @@ "NONDETERMINISTIC_OUTPUT", "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:SPECIAL" - ] + ], + "reply_schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "running_script": { + "description": "information about the running script.", + "oneOf": [ + { + "description": "If there's no in-flight function", + "type": "null" + }, + { + "description": "a map with the information about the running script", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "description": "the name of the function.", + "type": "string" + }, + "command": { + "description": "the command and arguments used for invoking the function.", + "type": "array", + "items": { + "type": "string" + } + }, + "duration_ms": { + "description": "the function's runtime duration in milliseconds.", + "type": "integer" + } + } + } + ] + }, + "engines": { + "description": "A map when each entry in the map represent a single engine.", + "type": "object", + "patternProperties": { + "^.*$": { + "description": "Engine map contains statistics about the engine", + "type": "object", + "additionalProperties": false, + "properties": { + "libraries_count": { + "description": "number of libraries", + "type": "integer" + }, + "functions_count": { + "description": "number of functions", + "type": "integer" + } + } + } + } + } + } + } } } diff --git a/src/commands/geoadd.json b/src/commands/geoadd.json index d33836cf4..bd9c40ebc 100644 --- a/src/commands/geoadd.json +++ b/src/commands/geoadd.json @@ -89,6 +89,10 @@ } ] } - ] + ], + "reply_schema": { + "description": "When used without optional arguments, the number of elements added to the sorted set (excluding score updates). If the CH option is specified, the number of elements that were changed (added or updated).", + "type": "integer" + } } } diff --git a/src/commands/geodist.json b/src/commands/geodist.json index 87782b243..61f281a1b 100644 --- a/src/commands/geodist.json +++ b/src/commands/geodist.json @@ -73,6 +73,19 @@ } ] } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "one or both of elements are missing", + "type": "null" + }, + { + "description": "distance as a double (represented as a string) in the specified units", + "type": "string", + "pattern": "^[0-9]*(.[0-9]*)?$" + } + ] + } } } diff --git a/src/commands/geohash.json b/src/commands/geohash.json index 040c631d1..0db62f315 100644 --- a/src/commands/geohash.json +++ b/src/commands/geohash.json @@ -44,6 +44,13 @@ "multiple": true, "optional": true } - ] + ], + "reply_schema": { + "description": "An array where each element is the Geohash corresponding to each member name passed as argument to the command.", + "type": "array", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/geopos.json b/src/commands/geopos.json index 242f6a887..64635fa7f 100644 --- a/src/commands/geopos.json +++ b/src/commands/geopos.json @@ -44,6 +44,33 @@ "multiple": true, "optional": true } - ] + ], + "reply_schema": { + "description": "An array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command", + "type": "array", + "items": { + "oneOf": [ + { + "description": "Element does not exist", + "type": "null" + }, + { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Latitude (x)", + "type": "number" + }, + { + "description": "Longitude (y)", + "type": "number" + } + ] + } + ] + } + } } } diff --git a/src/commands/georadius.json b/src/commands/georadius.json index cb14d51b4..3c475784b 100644 --- a/src/commands/georadius.json +++ b/src/commands/georadius.json @@ -201,6 +201,65 @@ "key_spec_index": 2, "optional": true } - ] + ], + "reply_schema": { + "description": "Array of matched members information", + "anyOf": [ + { + "description": "If no WITH* option is specified, array of matched members names", + "type": "array", + "items": { + "description": "name", + "type": "string" + } + }, + { + "type": "array", + "items": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "items": [ + { + "description": "Matched member name", + "type": "string" + } + ], + "additionalItems": { + "oneOf": [ + { + "description": "If WITHDIST option is specified, the distance from the center as a floating point number, in the same unit specified in the radius", + "type": "string" + }, + { + "description": "If WITHHASH option is specified, the geohash integer", + "type": "integer" + }, + { + "description": "If WITHCOORD option is specified, the coordinates as a two items x,y array (longitude,latitude)", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "latitude (x)", + "type": "number" + }, + { + "description": "longitude (y)", + "type": "number" + } + ] + } + ] + } + } + }, + { + "description": "number of items stored in key", + "type": "integer" + } + ] + } } } diff --git a/src/commands/georadius_ro.json b/src/commands/georadius_ro.json index 4696f78b8..5b0b2a5e0 100644 --- a/src/commands/georadius_ro.json +++ b/src/commands/georadius_ro.json @@ -141,6 +141,61 @@ } ] } - ] + ], + "reply_schema": { + "description": "Array of matched members information", + "anyOf": [ + { + "description": "If no WITH* option is specified, array of matched members names", + "type": "array", + "items": { + "description": "name", + "type": "string" + } + }, + { + "type": "array", + "items": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "items": [ + { + "description": "Matched member name", + "type": "string" + } + ], + "additionalItems": { + "oneOf": [ + { + "description": "If WITHDIST option is specified, the distance from the center as a floating point number, in the same unit specified in the radius", + "type": "string" + }, + { + "description": "If WITHHASH option is specified, the geohash integer", + "type": "integer" + }, + { + "description": "If WITHCOORD option is specified, the coordinates as a two items x,y array (longitude,latitude)", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "latitude (x)", + "type": "number" + }, + { + "description": "longitude (y)", + "type": "number" + } + ] + } + ] + } + } + } + ] + } } } diff --git a/src/commands/georadiusbymember.json b/src/commands/georadiusbymember.json index de8aa5b16..f2db041d1 100644 --- a/src/commands/georadiusbymember.json +++ b/src/commands/georadiusbymember.json @@ -192,6 +192,65 @@ "key_spec_index": 2, "optional": true } - ] + ], + "reply_schema": { + "description": "Array of matched members information", + "anyOf": [ + { + "description": "If no WITH* option is specified, array of matched members names", + "type": "array", + "items": { + "description": "name", + "type": "string" + } + }, + { + "type": "array", + "items": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "items": [ + { + "description": "Matched member name", + "type": "string" + } + ], + "additionalItems": { + "oneOf": [ + { + "description": "If WITHDIST option is specified, the distance from the center as a floating point number, in the same unit specified in the radius", + "type": "string" + }, + { + "description": "If WITHHASH option is specified, the geohash integer", + "type": "integer" + }, + { + "description": "If WITHCOORD option is specified, the coordinates as a two items x,y array (longitude,latitude)", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "latitude (x)", + "type": "number" + }, + { + "description": "longitude (y)", + "type": "number" + } + ] + } + ] + } + } + }, + { + "description": "number of items stored in key", + "type": "integer" + } + ] + } } } diff --git a/src/commands/georadiusbymember_ro.json b/src/commands/georadiusbymember_ro.json index 062a72ab2..fa35d529b 100644 --- a/src/commands/georadiusbymember_ro.json +++ b/src/commands/georadiusbymember_ro.json @@ -130,6 +130,61 @@ } ] } - ] + ], + "reply_schema": { + "description": "Array of matched members information", + "anyOf": [ + { + "description": "If no WITH* option is specified, array of matched members names", + "type": "array", + "items": { + "description": "name", + "type": "string" + } + }, + { + "type": "array", + "items": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "items": [ + { + "description": "Matched member name", + "type": "string" + } + ], + "additionalItems": { + "oneOf": [ + { + "description": "If WITHDIST option is specified, the distance from the center as a floating point number, in the same unit specified in the radius", + "type": "string" + }, + { + "description": "If WITHHASH option is specified, the geohash integer", + "type": "integer" + }, + { + "description": "If WITHCOORD option is specified, the coordinates as a two items x,y array (longitude,latitude)", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "latitude (x)", + "type": "number" + }, + { + "description": "longitude (y)", + "type": "number" + } + ] + } + ] + } + } + } + ] + } } } diff --git a/src/commands/geosearch.json b/src/commands/geosearch.json index 64f5df355..88c6bf287 100644 --- a/src/commands/geosearch.json +++ b/src/commands/geosearch.json @@ -207,6 +207,61 @@ "type": "pure-token", "optional": true } - ] + ], + "reply_schema": { + "description": "Array of matched members information", + "anyOf": [ + { + "description": "If no WITH* option is specified, array of matched members names", + "type": "array", + "items": { + "description": "name", + "type": "string" + } + }, + { + "type": "array", + "items": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "items": [ + { + "description": "Matched member name", + "type": "string" + } + ], + "additionalItems": { + "oneOf": [ + { + "description": "If WITHDIST option is specified, the distance from the center as a floating point number, in the same unit specified in the radius", + "type": "string" + }, + { + "description": "If WITHHASH option is specified, the geohash integer", + "type": "integer" + }, + { + "description": "If WITHCOORD option is specified, the coordinates as a two items x,y array (longitude,latitude)", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "latitude (x)", + "type": "number" + }, + { + "description": "longitude (y)", + "type": "number" + } + ] + } + ] + } + } + } + ] + } } } diff --git a/src/commands/geosearchstore.json b/src/commands/geosearchstore.json index cd3815166..abb7ba152 100644 --- a/src/commands/geosearchstore.json +++ b/src/commands/geosearchstore.json @@ -219,6 +219,10 @@ "type": "pure-token", "optional": true } - ] + ], + "reply_schema": { + "description": "the number of elements in the resulting set", + "type": "integer" + } } } diff --git a/src/commands/get.json b/src/commands/get.json index 342e100e9..0de9b164c 100644 --- a/src/commands/get.json +++ b/src/commands/get.json @@ -33,6 +33,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The value of the key.", + "type": "string" + }, + { + "description": "Key does not exist.", + "type": "null" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/getbit.json b/src/commands/getbit.json index 759784e79..773445f63 100644 --- a/src/commands/getbit.json +++ b/src/commands/getbit.json @@ -33,6 +33,17 @@ } } ], + "reply_schema": { + "description": "The bit value stored at offset.", + "oneOf": [ + { + "const": 0 + }, + { + "const": 1 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/getdel.json b/src/commands/getdel.json index 1d2f56e30..17e1664bb 100644 --- a/src/commands/getdel.json +++ b/src/commands/getdel.json @@ -34,6 +34,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The value of the key.", + "type": "string" + }, + { + "description": "The key does not exist.", + "type": "null" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/getex.json b/src/commands/getex.json index 8557bcdd9..cb70eac8f 100644 --- a/src/commands/getex.json +++ b/src/commands/getex.json @@ -35,6 +35,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The value of the key.", + "type": "string" + }, + { + "description": "Key does not exist.", + "type": "null" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/getrange.json b/src/commands/getrange.json index 03eb58e38..f54836535 100644 --- a/src/commands/getrange.json +++ b/src/commands/getrange.json @@ -32,6 +32,10 @@ } } ], + "reply_schema": { + "type": "string", + "description": "The substring of the string value stored at key, determined by the offsets start and end (both are inclusive)." + }, "arguments": [ { "name": "key", diff --git a/src/commands/getset.json b/src/commands/getset.json index 2f6b8911c..78629ff91 100644 --- a/src/commands/getset.json +++ b/src/commands/getset.json @@ -40,6 +40,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The old value stored at the key.", + "type": "string" + }, + { + "description": "The key does not exist.", + "type": "null" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/hdel.json b/src/commands/hdel.json index df70430d8..b5d63476b 100644 --- a/src/commands/hdel.json +++ b/src/commands/hdel.json @@ -39,6 +39,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "The number of fields that were removed from the hash." + }, "arguments": [ { "name": "key", diff --git a/src/commands/hello.json b/src/commands/hello.json index 675edff5b..7f5d6f202 100644 --- a/src/commands/hello.json +++ b/src/commands/hello.json @@ -24,6 +24,54 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "server": { + "type": "string" + }, + "version": { + "type": "string" + }, + "proto": { + "const": 3 + }, + "id": { + "type": "integer" + }, + "mode": { + "type": "string" + }, + "role": { + "type": "string" + }, + "modules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "ver": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, "arguments": [ { "name": "arguments", diff --git a/src/commands/hexists.json b/src/commands/hexists.json index 0518e6209..1fc1b6565 100644 --- a/src/commands/hexists.json +++ b/src/commands/hexists.json @@ -32,6 +32,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The hash does not contain the field, or key does not exist.", + "const": 0 + }, + { + "description": "The hash contains the field.", + "const": 1 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/hget.json b/src/commands/hget.json index 12e1fbfbc..101a38ab4 100644 --- a/src/commands/hget.json +++ b/src/commands/hget.json @@ -33,6 +33,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The value associated with the field.", + "type": "string" + }, + { + "description": "If the field is not present in the hash or key does not exist.", + "type": "null" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/hgetall.json b/src/commands/hgetall.json index 9fef00646..03373ae77 100644 --- a/src/commands/hgetall.json +++ b/src/commands/hgetall.json @@ -35,6 +35,13 @@ } } ], + "reply_schema": { + "type": "object", + "description": "Map of fields and their values stored in the hash, or an empty list when key does not exist. In RESP2 this is returned as a flat array.", + "additionalProperties": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/hincrby.json b/src/commands/hincrby.json index 124e365f1..bd9b0ea4c 100644 --- a/src/commands/hincrby.json +++ b/src/commands/hincrby.json @@ -35,6 +35,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "The value of the field after the increment operation." + }, "arguments": [ { "name": "key", diff --git a/src/commands/hincrbyfloat.json b/src/commands/hincrbyfloat.json index b4c81d181..bd34db5e1 100644 --- a/src/commands/hincrbyfloat.json +++ b/src/commands/hincrbyfloat.json @@ -35,6 +35,10 @@ } } ], + "reply_schema": { + "type": "string", + "description": "The value of the field after the increment operation." + }, "arguments": [ { "name": "key", diff --git a/src/commands/hkeys.json b/src/commands/hkeys.json index 243566aa9..c6a4d1fcf 100644 --- a/src/commands/hkeys.json +++ b/src/commands/hkeys.json @@ -35,6 +35,14 @@ } } ], + "reply_schema": { + "type": "array", + "description": "List of fields in the hash, or an empty list when the key does not exist.", + "uniqueItems": true, + "items": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/hlen.json b/src/commands/hlen.json index 8320651ae..1b476ab74 100644 --- a/src/commands/hlen.json +++ b/src/commands/hlen.json @@ -32,6 +32,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "Number of the fields in the hash, or 0 when the key does not exist." + }, "arguments": [ { "name": "key", diff --git a/src/commands/hmget.json b/src/commands/hmget.json index d7e7c8e45..7a31598a7 100644 --- a/src/commands/hmget.json +++ b/src/commands/hmget.json @@ -33,6 +33,21 @@ } } ], + "reply_schema": { + "description": "List of values associated with the given fields, in the same order as they are requested.", + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/hmset.json b/src/commands/hmset.json index 1fda1b2b4..c498b9086 100644 --- a/src/commands/hmset.json +++ b/src/commands/hmset.json @@ -39,6 +39,9 @@ } } ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "key", diff --git a/src/commands/hrandfield.json b/src/commands/hrandfield.json index ef2ff4199..c821d4508 100644 --- a/src/commands/hrandfield.json +++ b/src/commands/hrandfield.json @@ -35,6 +35,44 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "Key doesn't exist", + "type": "null" + }, + { + "description": "A single random field. Returned in case `COUNT` was not used.", + "type": "string" + }, + { + "description": "A list of fields. Returned in case `COUNT` was used.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "description": "Fields and their values. Returned in case `COUNT` and `WITHVALUES` were used. In RESP2 this is returned as a flat array.", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Field", + "type": "string" + }, + { + "description": "Value", + "type": "string" + } + ] + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/hscan.json b/src/commands/hscan.json index 67b3bf552..478bda447 100644 --- a/src/commands/hscan.json +++ b/src/commands/hscan.json @@ -57,6 +57,25 @@ "type": "integer", "optional": true } - ] + ], + "reply_schema": { + "description": "cursor and scan response in array form", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "cursor", + "type": "string" + }, + { + "description": "list of key/value pairs from the hash where each even element is the key, and each odd element is the value", + "type": "array", + "items": { + "type": "string" + } + } + ] + } } } diff --git a/src/commands/hset.json b/src/commands/hset.json index 1b665c369..27762afea 100644 --- a/src/commands/hset.json +++ b/src/commands/hset.json @@ -40,6 +40,9 @@ } } ], + "reply_schema": { + "type": "integer" + }, "arguments": [ { "name": "key", diff --git a/src/commands/hsetnx.json b/src/commands/hsetnx.json index abd0ccafb..53cd9c9d0 100644 --- a/src/commands/hsetnx.json +++ b/src/commands/hsetnx.json @@ -34,6 +34,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The field is a new field in the hash and value was set.", + "const": 0 + }, + { + "description": "The field already exists in the hash and no operation was performed.", + "const": 1 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/hstrlen.json b/src/commands/hstrlen.json index 4ba4df7ad..5d2c6b1a9 100644 --- a/src/commands/hstrlen.json +++ b/src/commands/hstrlen.json @@ -32,6 +32,11 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "String length of the value associated with the field, or zero when the field is not present in the hash or key does not exist at all.", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/hvals.json b/src/commands/hvals.json index 829f63dea..6118bcff4 100644 --- a/src/commands/hvals.json +++ b/src/commands/hvals.json @@ -35,6 +35,13 @@ } } ], + "reply_schema": { + "type": "array", + "description": "List of values in the hash, or an empty list when the key does not exist.", + "items": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/incr.json b/src/commands/incr.json index 09a496056..d6c1bd2cd 100644 --- a/src/commands/incr.json +++ b/src/commands/incr.json @@ -41,6 +41,10 @@ "type": "key", "key_spec_index": 0 } - ] + ], + "reply_schema": { + "description": "The value of key after the increment", + "type": "integer" + } } } diff --git a/src/commands/incrby.json b/src/commands/incrby.json index 27418114a..0febdd56e 100644 --- a/src/commands/incrby.json +++ b/src/commands/incrby.json @@ -35,6 +35,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "The value of the key after incrementing it." + }, "arguments": [ { "name": "key", diff --git a/src/commands/incrbyfloat.json b/src/commands/incrbyfloat.json index f28b91be6..0f2440024 100644 --- a/src/commands/incrbyfloat.json +++ b/src/commands/incrbyfloat.json @@ -35,6 +35,10 @@ } } ], + "reply_schema": { + "type": "string", + "description": "The value of the key after incrementing it." + }, "arguments": [ { "name": "key", diff --git a/src/commands/info.json b/src/commands/info.json index 612294d34..b44e0c01f 100644 --- a/src/commands/info.json +++ b/src/commands/info.json @@ -25,6 +25,9 @@ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:SPECIAL" ], + "reply_schema": { + "type": "string" + }, "arguments": [ { "name": "section", diff --git a/src/commands/keys.json b/src/commands/keys.json index 546241f95..cedf3ce7f 100644 --- a/src/commands/keys.json +++ b/src/commands/keys.json @@ -22,6 +22,13 @@ "name": "pattern", "type": "pattern" } - ] + ], + "reply_schema": { + "description": "list of keys matching pattern", + "type": "array", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/lastsave.json b/src/commands/lastsave.json index 929058458..8988e75d7 100644 --- a/src/commands/lastsave.json +++ b/src/commands/lastsave.json @@ -17,6 +17,10 @@ "acl_categories": [ "ADMIN", "DANGEROUS" - ] + ], + "reply_schema": { + "type": "integer", + "description": "UNIX TIME of the last DB save executed with success." + } } } diff --git a/src/commands/latency-doctor.json b/src/commands/latency-doctor.json index 129b32358..8d3a98b3c 100644 --- a/src/commands/latency-doctor.json +++ b/src/commands/latency-doctor.json @@ -17,6 +17,10 @@ "NONDETERMINISTIC_OUTPUT", "REQUEST_POLICY:ALL_NODES", "RESPONSE_POLICY:SPECIAL" - ] + ], + "reply_schema": { + "type": "string", + "description": "A human readable latency analysis report." + } } } diff --git a/src/commands/latency-graph.json b/src/commands/latency-graph.json index 0644c1cb0..d634da746 100644 --- a/src/commands/latency-graph.json +++ b/src/commands/latency-graph.json @@ -23,6 +23,10 @@ "name": "event", "type": "string" } - ] + ], + "reply_schema": { + "type": "string", + "description": "Latency graph" + } } } diff --git a/src/commands/latency-help.json b/src/commands/latency-help.json index 682beee2a..e91679eb7 100644 --- a/src/commands/latency-help.json +++ b/src/commands/latency-help.json @@ -10,6 +10,13 @@ "command_flags": [ "LOADING", "STALE" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/latency-histogram.json b/src/commands/latency-histogram.json index dc14d47f8..d9821ea85 100644 --- a/src/commands/latency-histogram.json +++ b/src/commands/latency-histogram.json @@ -18,6 +18,30 @@ "REQUEST_POLICY:ALL_NODES", "RESPONSE_POLICY:SPECIAL" ], + "reply_schema": { + "type": "object", + "description": "A map where each key is a command name, and each value is a map with the total calls, and an inner map of the histogram time buckets.", + "patternProperties": { + "^.*$": { + "type": "object", + "additionalProperties": false, + "properties": { + "calls": { + "description": "The total calls for the command.", + "type": "integer", + "minimum": 0 + }, + "histogram_usec": { + "description": "Histogram map, bucket id to latency", + "type": "object", + "additionalProperties": { + "type": "integer" + } + } + } + } + } + }, "arguments": [ { "name": "COMMAND", diff --git a/src/commands/latency-history.json b/src/commands/latency-history.json index 6d9267064..11fa8857b 100644 --- a/src/commands/latency-history.json +++ b/src/commands/latency-history.json @@ -18,6 +18,27 @@ "REQUEST_POLICY:ALL_NODES", "RESPONSE_POLICY:SPECIAL" ], + "reply_schema": { + "type": "array", + "description": "An array where each element is a two elements array representing the timestamp and the latency of the event.", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "timestamp of the event", + "type": "integer", + "minimum": 0 + }, + { + "description": "latency of the event", + "type": "integer", + "minimum": 0 + } + ] + } + }, "arguments": [ { "name": "event", diff --git a/src/commands/latency-latest.json b/src/commands/latency-latest.json index f513689c5..7e4d42317 100644 --- a/src/commands/latency-latest.json +++ b/src/commands/latency-latest.json @@ -17,6 +17,33 @@ "NONDETERMINISTIC_OUTPUT", "REQUEST_POLICY:ALL_NODES", "RESPONSE_POLICY:SPECIAL" - ] + ], + "reply_schema": { + "type": "array", + "description": "An array where each element is a four elements array representing the event's name, timestamp, latest and all-time latency measurements.", + "items": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "items": [ + { + "type": "string", + "description": "Event name." + }, + { + "type": "integer", + "description": "Timestamp." + }, + { + "type": "integer", + "description": "Latest latency in milliseconds." + }, + { + "type": "integer", + "description": "Max latency in milliseconds." + } + ] + } + } } } diff --git a/src/commands/latency-reset.json b/src/commands/latency-reset.json index 30295cc05..354e3fe74 100644 --- a/src/commands/latency-reset.json +++ b/src/commands/latency-reset.json @@ -17,6 +17,10 @@ "REQUEST_POLICY:ALL_NODES", "RESPONSE_POLICY:ALL_SUCCEEDED" ], + "reply_schema": { + "type": "integer", + "description": "Number of event time series that were reset." + }, "arguments": [ { "name": "event", diff --git a/src/commands/lcs.json b/src/commands/lcs.json index 8d53e425e..fe7840b34 100644 --- a/src/commands/lcs.json +++ b/src/commands/lcs.json @@ -32,6 +32,61 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "string", + "description": "The longest common subsequence." + }, + { + "type": "integer", + "description": "The length of the longest common subsequence when 'LEN' is given." + }, + { + "type": "object", + "description": "Array with the LCS length and all the ranges in both the strings when 'IDX' is given. In RESP2 this is returned as a flat array", + "additionalProperties": false, + "properties": { + "matches": { + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 3, + "items": [ + { + "type": "array", + "description": "Matched range in the first string.", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "integer" + } + }, + { + "type": "array", + "description": "Matched range in the second string.", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "integer" + } + } + ], + "additionalItems": { + "type": "integer", + "description": "The length of the match when 'WITHMATCHLEN' is given." + } + } + }, + "len": { + "type": "integer", + "description": "Length of the longest common subsequence." + } + } + } + ] + }, "arguments": [ { "name": "key1", diff --git a/src/commands/lindex.json b/src/commands/lindex.json index b2397241f..89b14ea9f 100644 --- a/src/commands/lindex.json +++ b/src/commands/lindex.json @@ -32,6 +32,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "null", + "description": "Index is out of range" + }, + { + "description": "The requested element", + "type": "string" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/linsert.json b/src/commands/linsert.json index 71046fa58..d31a4de8d 100644 --- a/src/commands/linsert.json +++ b/src/commands/linsert.json @@ -33,6 +33,23 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "List length after a successful insert operation.", + "type": "integer", + "minimum": 1 + }, + { + "description": "in case key doesn't exist.", + "const": 0 + }, + { + "description": "when the pivot wasn't found.", + "const": -1 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/llen.json b/src/commands/llen.json index 720b23778..1452e22d7 100644 --- a/src/commands/llen.json +++ b/src/commands/llen.json @@ -32,6 +32,11 @@ } } ], + "reply_schema": { + "description": "List length.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/lmove.json b/src/commands/lmove.json index 060cc7a32..82c305c12 100644 --- a/src/commands/lmove.json +++ b/src/commands/lmove.json @@ -52,6 +52,10 @@ } } ], + "reply_schema": { + "description": "The element being popped and pushed.", + "type": "string" + }, "arguments": [ { "name": "source", diff --git a/src/commands/lmpop.json b/src/commands/lmpop.json index 0821e4c68..e788b5136 100644 --- a/src/commands/lmpop.json +++ b/src/commands/lmpop.json @@ -34,6 +34,34 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "If no element could be popped.", + "type": "null" + }, + { + "description": "List key from which elements were popped.", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Name of the key from which elements were popped.", + "type": "string" + }, + { + "description": "Array of popped elements.", + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ] + } + ] + }, "arguments": [ { "name": "numkeys", diff --git a/src/commands/lolwut.json b/src/commands/lolwut.json index cf0a98504..b093404b5 100644 --- a/src/commands/lolwut.json +++ b/src/commands/lolwut.json @@ -9,6 +9,10 @@ "READONLY", "FAST" ], + "reply_schema": { + "type": "string", + "description": "String containing the generative computer art, and a text with the Redis version." + }, "arguments": [ { "token": "VERSION", diff --git a/src/commands/lpop.json b/src/commands/lpop.json index cd3de0c7a..a7b93c276 100644 --- a/src/commands/lpop.json +++ b/src/commands/lpop.json @@ -40,6 +40,26 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Key does not exist.", + "type": "null" + }, + { + "description": "In case `count` argument was not given, the value of the first element.", + "type": "string" + }, + { + "description": "In case `count` argument was given, a list of popped elements", + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "arguments": [ { "name": "key", diff --git a/src/commands/lpos.json b/src/commands/lpos.json index 3aea3191f..8e3a9fa02 100644 --- a/src/commands/lpos.json +++ b/src/commands/lpos.json @@ -32,6 +32,26 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "In case there is no matching element", + "type": "null" + }, + { + "description": "An integer representing the matching element", + "type": "integer" + }, + { + "description": "If the COUNT option is given, an array of integers representing the matching elements (empty if there are no matches)", + "type": "array", + "uniqueItems": true, + "items": { + "type": "integer" + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/lpush.json b/src/commands/lpush.json index 23a9c36b4..6fc815298 100644 --- a/src/commands/lpush.json +++ b/src/commands/lpush.json @@ -40,6 +40,10 @@ } } ], + "reply_schema": { + "description": "Length of the list after the push operations.", + "type": "integer" + }, "arguments": [ { "name": "key", diff --git a/src/commands/lpushx.json b/src/commands/lpushx.json index b1629b629..d41f50b77 100644 --- a/src/commands/lpushx.json +++ b/src/commands/lpushx.json @@ -40,6 +40,11 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "the length of the list after the push operation", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/lrange.json b/src/commands/lrange.json index 8fa9352bb..1c0868caa 100644 --- a/src/commands/lrange.json +++ b/src/commands/lrange.json @@ -46,6 +46,13 @@ "name": "stop", "type": "integer" } - ] + ], + "reply_schema": { + "description": "List of elements in the specified range", + "type": "array", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/lrem.json b/src/commands/lrem.json index c845cc04a..84baeea0b 100644 --- a/src/commands/lrem.json +++ b/src/commands/lrem.json @@ -32,6 +32,11 @@ } } ], + "reply_schema": { + "description": "The number of removed elements.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/lset.json b/src/commands/lset.json index 9a9e4fd5e..441db073c 100644 --- a/src/commands/lset.json +++ b/src/commands/lset.json @@ -33,6 +33,9 @@ } } ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "key", diff --git a/src/commands/ltrim.json b/src/commands/ltrim.json index f177d8f66..c041f49ca 100644 --- a/src/commands/ltrim.json +++ b/src/commands/ltrim.json @@ -32,6 +32,9 @@ } } ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "key", diff --git a/src/commands/memory-doctor.json b/src/commands/memory-doctor.json index b6691dfa0..5df7456f5 100644 --- a/src/commands/memory-doctor.json +++ b/src/commands/memory-doctor.json @@ -11,6 +11,10 @@ "NONDETERMINISTIC_OUTPUT", "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:SPECIAL" - ] + ], + "reply_schema": { + "description": "memory problems report", + "type": "string" + } } } diff --git a/src/commands/memory-help.json b/src/commands/memory-help.json index a1cda71f8..34ee382c5 100644 --- a/src/commands/memory-help.json +++ b/src/commands/memory-help.json @@ -10,6 +10,13 @@ "command_flags": [ "LOADING", "STALE" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/memory-malloc-stats.json b/src/commands/memory-malloc-stats.json index 5106781fe..a44959c88 100644 --- a/src/commands/memory-malloc-stats.json +++ b/src/commands/memory-malloc-stats.json @@ -11,6 +11,10 @@ "NONDETERMINISTIC_OUTPUT", "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:SPECIAL" - ] + ], + "reply_schema": { + "type": "string", + "description": "The memory allocator's internal statistics report." + } } } diff --git a/src/commands/memory-purge.json b/src/commands/memory-purge.json index b862534d1..09c7d124c 100644 --- a/src/commands/memory-purge.json +++ b/src/commands/memory-purge.json @@ -10,6 +10,9 @@ "command_tips": [ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:ALL_SUCCEEDED" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/memory-stats.json b/src/commands/memory-stats.json index 76e6baa3e..b50a9eb31 100644 --- a/src/commands/memory-stats.json +++ b/src/commands/memory-stats.json @@ -11,6 +11,111 @@ "NONDETERMINISTIC_OUTPUT", "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:SPECIAL" - ] + ], + "reply_schema": { + "description": "memory usage details", + "type": "object", + "additionalProperties": false, + "properties": { + "peak.allocated": { + "type": "integer" + }, + "total.allocated": { + "type": "integer" + }, + "startup.allocated": { + "type": "integer" + }, + "replication.backlog": { + "type": "integer" + }, + "clients.slaves": { + "type": "integer" + }, + "clients.normal": { + "type": "integer" + }, + "cluster.links": { + "type": "integer" + }, + "aof.buffer": { + "type": "integer" + }, + "lua.caches": { + "type": "integer" + }, + "functions.caches": { + "type": "integer" + }, + "overhead.total": { + "type": "integer" + }, + "keys.count": { + "type": "integer" + }, + "keys.bytes-per-key": { + "type": "integer" + }, + "dataset.bytes": { + "type": "integer" + }, + "dataset.percentage": { + "type": "number" + }, + "peak.percentage": { + "type": "number" + }, + "allocator.allocated": { + "type": "integer" + }, + "allocator.active": { + "type": "integer" + }, + "allocator.resident": { + "type": "integer" + }, + "allocator-fragmentation.ratio": { + "type": "number" + }, + "allocator-fragmentation.bytes": { + "type": "integer" + }, + "allocator-rss.ratio": { + "type": "number" + }, + "allocator-rss.bytes": { + "type": "integer" + }, + "rss-overhead.ratio": { + "type": "number" + }, + "rss-overhead.bytes": { + "type": "integer" + }, + "fragmentation": { + "type": "number" + }, + "fragmentation.bytes": { + "type": "integer" + } + }, + "patternProperties": { + "^db.": { + "type": "object", + "properties": { + "overhead.hashtable.main": { + "type": "integer" + }, + "overhead.hashtable.expires": { + "type": "integer" + }, + "overhead.hashtable.slot-to-keys": { + "type": "integer" + } + }, + "additionalProperties": false + } + } + } } } diff --git a/src/commands/memory-usage.json b/src/commands/memory-usage.json index fa6b7c7e8..ff977d9be 100644 --- a/src/commands/memory-usage.json +++ b/src/commands/memory-usage.json @@ -29,6 +29,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Number of bytes that a key and its value require to be stored in RAM.", + "type": "integer" + }, + { + "description": "Key does not exist.", + "type": "null" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/mget.json b/src/commands/mget.json index fdff80998..a5bd8c30e 100644 --- a/src/commands/mget.json +++ b/src/commands/mget.json @@ -36,6 +36,21 @@ } } ], + "reply_schema": { + "description": "List of values at the specified keys.", + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/migrate.json b/src/commands/migrate.json index 64bf78bba..83a1dd149 100644 --- a/src/commands/migrate.json +++ b/src/commands/migrate.json @@ -77,6 +77,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "const": "OK", + "description": "Success." + }, + { + "const": "NOKEY", + "description": "No keys were found in the source instance." + } + ] + }, "arguments": [ { "name": "host", diff --git a/src/commands/module-help.json b/src/commands/module-help.json index b8db8aee3..10b59dc2d 100644 --- a/src/commands/module-help.json +++ b/src/commands/module-help.json @@ -10,6 +10,13 @@ "command_flags": [ "LOADING", "STALE" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/module-list.json b/src/commands/module-list.json index ed6e7d19b..65a8c9f3b 100644 --- a/src/commands/module-list.json +++ b/src/commands/module-list.json @@ -13,6 +13,35 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT_ORDER" - ] + ], + "reply_schema": { + "type": "array", + "description": "Returns information about the modules loaded to the server.", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Name of the module." + }, + "ver": { + "type": "integer", + "description": "Version of the module." + }, + "path": { + "type": "string", + "description": "Module path." + }, + "args": { + "type": "array", + "description": "Module arguments.", + "items": { + "type": "string" + } + } + } + } + } } } diff --git a/src/commands/module-load.json b/src/commands/module-load.json index 84e6d3596..1ce5faf10 100644 --- a/src/commands/module-load.json +++ b/src/commands/module-load.json @@ -13,6 +13,9 @@ "NOSCRIPT", "PROTECTED" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "path", diff --git a/src/commands/module-loadex.json b/src/commands/module-loadex.json index 9419aa010..7aa278831 100644 --- a/src/commands/module-loadex.json +++ b/src/commands/module-loadex.json @@ -13,6 +13,9 @@ "NOSCRIPT", "PROTECTED" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "path", diff --git a/src/commands/module-unload.json b/src/commands/module-unload.json index 8820ba3aa..3cd85b378 100644 --- a/src/commands/module-unload.json +++ b/src/commands/module-unload.json @@ -13,6 +13,9 @@ "NOSCRIPT", "PROTECTED" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "name", diff --git a/src/commands/move.json b/src/commands/move.json index 0c7c71e05..cd588ff59 100644 --- a/src/commands/move.json +++ b/src/commands/move.json @@ -44,6 +44,18 @@ "name": "db", "type": "integer" } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "key was moved", + "const": 1 + }, + { + "description": "key wasn't moved", + "const": 0 + } + ] + } } } diff --git a/src/commands/mset.json b/src/commands/mset.json index ebf3c5216..deff39ec8 100644 --- a/src/commands/mset.json +++ b/src/commands/mset.json @@ -37,6 +37,9 @@ } } ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "data", diff --git a/src/commands/msetnx.json b/src/commands/msetnx.json index 84ec00063..90a6449d1 100644 --- a/src/commands/msetnx.json +++ b/src/commands/msetnx.json @@ -37,6 +37,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "No key was set (at least one key already existed).", + "const": 0 + }, + { + "description": "All the keys were set.", + "const": 1 + } + ] + }, "arguments": [ { "name": "data", diff --git a/src/commands/multi.json b/src/commands/multi.json index f1299a6f4..dd94bce26 100644 --- a/src/commands/multi.json +++ b/src/commands/multi.json @@ -15,6 +15,9 @@ ], "acl_categories": [ "TRANSACTION" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/object-encoding.json b/src/commands/object-encoding.json index 2d39a07ef..902a2f39e 100644 --- a/src/commands/object-encoding.json +++ b/src/commands/object-encoding.json @@ -41,6 +41,18 @@ "type": "key", "key_spec_index": 0 } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "key doesn't exist", + "type": "null" + }, + { + "description": "encoding of the object", + "type": "string" + } + ] + } } } diff --git a/src/commands/object-freq.json b/src/commands/object-freq.json index d184f2e7e..79c58fb63 100644 --- a/src/commands/object-freq.json +++ b/src/commands/object-freq.json @@ -41,6 +41,10 @@ "type": "key", "key_spec_index": 0 } - ] + ], + "reply_schema": { + "description": "the counter's value", + "type": "integer" + } } } diff --git a/src/commands/object-help.json b/src/commands/object-help.json index 22864bafa..bf1fac643 100644 --- a/src/commands/object-help.json +++ b/src/commands/object-help.json @@ -13,6 +13,13 @@ ], "acl_categories": [ "KEYSPACE" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/object-idletime.json b/src/commands/object-idletime.json index 162d6f514..8e124df6f 100644 --- a/src/commands/object-idletime.json +++ b/src/commands/object-idletime.json @@ -41,6 +41,10 @@ "type": "key", "key_spec_index": 0 } - ] + ], + "reply_schema": { + "description": "the idle time in seconds", + "type": "integer" + } } } diff --git a/src/commands/object-refcount.json b/src/commands/object-refcount.json index 0f36f5092..82e2a54ea 100644 --- a/src/commands/object-refcount.json +++ b/src/commands/object-refcount.json @@ -41,6 +41,10 @@ "type": "key", "key_spec_index": 0 } - ] + ], + "reply_schema": { + "description": "the number of references", + "type": "integer" + } } } diff --git a/src/commands/persist.json b/src/commands/persist.json index f08df4c1a..2ca99642b 100644 --- a/src/commands/persist.json +++ b/src/commands/persist.json @@ -33,6 +33,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "const": 0, + "description": "Key does not exist or does not have an associated timeout." + }, + { + "const": 1, + "description": "The timeout has been removed." + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/pexpire.json b/src/commands/pexpire.json index 9638b8eaa..03416a1eb 100644 --- a/src/commands/pexpire.json +++ b/src/commands/pexpire.json @@ -39,6 +39,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "const": 0, + "description": "The timeout was not set. e.g. key doesn't exist, or operation skipped due to the provided arguments." + }, + { + "const": 1, + "description": "The timeout was set." + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/pexpireat.json b/src/commands/pexpireat.json index c08782c26..ac09ba2e0 100644 --- a/src/commands/pexpireat.json +++ b/src/commands/pexpireat.json @@ -39,6 +39,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "const": 1, + "description": "The timeout was set." + }, + { + "const": 0, + "description": "The timeout was not set. e.g. key doesn't exist, or operation skipped due to the provided arguments." + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/pexpiretime.json b/src/commands/pexpiretime.json index 6ba921278..9295a6a17 100644 --- a/src/commands/pexpiretime.json +++ b/src/commands/pexpiretime.json @@ -33,6 +33,23 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "integer", + "description": "Expiration Unix timestamp in milliseconds.", + "minimum": 0 + }, + { + "const": -1, + "description": "The key exists but has no associated expiration time." + }, + { + "const": -2, + "description": "The key does not exist." + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/pfadd.json b/src/commands/pfadd.json index 8076a7631..f457d0f55 100644 --- a/src/commands/pfadd.json +++ b/src/commands/pfadd.json @@ -46,6 +46,18 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "if at least 1 HyperLogLog internal register was altered", + "const": 1 + }, + { + "description": "if no HyperLogLog internal register were altered", + "const": 0 + } + ] + } } } diff --git a/src/commands/pfcount.json b/src/commands/pfcount.json index 4d89e1d38..92f84d895 100644 --- a/src/commands/pfcount.json +++ b/src/commands/pfcount.json @@ -41,6 +41,10 @@ "key_spec_index": 0, "multiple": true } - ] + ], + "reply_schema": { + "description": "The approximated number of unique elements observed via PFADD", + "type": "integer" + } } } diff --git a/src/commands/pfmerge.json b/src/commands/pfmerge.json index 648a8b43a..9f1f417ff 100644 --- a/src/commands/pfmerge.json +++ b/src/commands/pfmerge.json @@ -65,6 +65,9 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/pfselftest.json b/src/commands/pfselftest.json index b75ce0369..f9c6a1002 100644 --- a/src/commands/pfselftest.json +++ b/src/commands/pfselftest.json @@ -14,6 +14,9 @@ ], "acl_categories": [ "HYPERLOGLOG" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/ping.json b/src/commands/ping.json index e7db2c332..13db35d43 100644 --- a/src/commands/ping.json +++ b/src/commands/ping.json @@ -17,6 +17,18 @@ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:ALL_SUCCEEDED" ], + "reply_schema": { + "anyOf": [ + { + "const": "PONG", + "description": "Default reply." + }, + { + "type": "string", + "description": "Relay of given `message`." + } + ] + }, "arguments": [ { "name": "message", diff --git a/src/commands/psetex.json b/src/commands/psetex.json index ce957f82e..427f23279 100644 --- a/src/commands/psetex.json +++ b/src/commands/psetex.json @@ -38,6 +38,9 @@ } } ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "key", diff --git a/src/commands/pttl.json b/src/commands/pttl.json index 1d37b9a49..863248901 100644 --- a/src/commands/pttl.json +++ b/src/commands/pttl.json @@ -42,6 +42,23 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "TTL in milliseconds.", + "type": "integer", + "minimum": 0 + }, + { + "description": "The key exists but has no associated expire.", + "const": -1 + }, + { + "description": "The key does not exist.", + "const": -2 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/publish.json b/src/commands/publish.json index 3c9b12f4d..05313766b 100644 --- a/src/commands/publish.json +++ b/src/commands/publish.json @@ -23,6 +23,11 @@ "name": "message", "type": "string" } - ] + ], + "reply_schema": { + "description": "the number of clients that received the message. Note that in a Redis Cluster, only clients that are connected to the same node as the publishing client are included in the count", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/pubsub-channels.json b/src/commands/pubsub-channels.json index 0522504b1..173271271 100644 --- a/src/commands/pubsub-channels.json +++ b/src/commands/pubsub-channels.json @@ -18,6 +18,14 @@ "type": "pattern", "optional": true } - ] + ], + "reply_schema": { + "description": "a list of active channels, optionally matching the specified pattern", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + } } } diff --git a/src/commands/pubsub-help.json b/src/commands/pubsub-help.json index e0c2a6123..09c04f3a4 100644 --- a/src/commands/pubsub-help.json +++ b/src/commands/pubsub-help.json @@ -10,6 +10,13 @@ "command_flags": [ "LOADING", "STALE" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/pubsub-numpat.json b/src/commands/pubsub-numpat.json index 382ec1b70..b593a4e6d 100644 --- a/src/commands/pubsub-numpat.json +++ b/src/commands/pubsub-numpat.json @@ -11,6 +11,11 @@ "PUBSUB", "LOADING", "STALE" - ] + ], + "reply_schema": { + "description": "the number of patterns all the clients are subscribed to", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/pubsub-numsub.json b/src/commands/pubsub-numsub.json index fae05c8fd..1df663be2 100644 --- a/src/commands/pubsub-numsub.json +++ b/src/commands/pubsub-numsub.json @@ -19,6 +19,10 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "description": "the number of subscribers per channel, each even element (including 0th) is channel name, each odd element is the number of subscribers", + "type": "array" + } } } diff --git a/src/commands/pubsub-shardchannels.json b/src/commands/pubsub-shardchannels.json index 90b907d30..cd196105b 100644 --- a/src/commands/pubsub-shardchannels.json +++ b/src/commands/pubsub-shardchannels.json @@ -18,6 +18,14 @@ "type": "pattern", "optional": true } - ] + ], + "reply_schema": { + "description": "a list of active channels, optionally matching the specified pattern", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } } } diff --git a/src/commands/pubsub-shardnumsub.json b/src/commands/pubsub-shardnumsub.json index 89187696a..536568a2a 100644 --- a/src/commands/pubsub-shardnumsub.json +++ b/src/commands/pubsub-shardnumsub.json @@ -19,6 +19,10 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "description": "the number of subscribers per shard channel, each even element (including 0th) is channel name, each odd element is the number of subscribers", + "type": "array" + } } } diff --git a/src/commands/quit.json b/src/commands/quit.json index cdb80336d..feb371955 100644 --- a/src/commands/quit.json +++ b/src/commands/quit.json @@ -21,6 +21,9 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/randomkey.json b/src/commands/randomkey.json index ceaa3c886..18f838b05 100644 --- a/src/commands/randomkey.json +++ b/src/commands/randomkey.json @@ -16,6 +16,18 @@ "command_tips": [ "REQUEST_POLICY:ALL_SHARDS", "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "when the database is empty", + "type": "null" + }, + { + "description": "random key in db", + "type": "string" + } + ] + } } } diff --git a/src/commands/readonly.json b/src/commands/readonly.json index 1bbc220ed..4dcd50af8 100644 --- a/src/commands/readonly.json +++ b/src/commands/readonly.json @@ -13,6 +13,9 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/readwrite.json b/src/commands/readwrite.json index 81e505ffe..e72d5cac5 100644 --- a/src/commands/readwrite.json +++ b/src/commands/readwrite.json @@ -13,6 +13,9 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/rename.json b/src/commands/rename.json index 561bf22b6..046d40ae3 100644 --- a/src/commands/rename.json +++ b/src/commands/rename.json @@ -64,6 +64,9 @@ "type": "key", "key_spec_index": 1 } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/renamenx.json b/src/commands/renamenx.json index afa4f658b..a6c039182 100644 --- a/src/commands/renamenx.json +++ b/src/commands/renamenx.json @@ -69,6 +69,18 @@ "type": "key", "key_spec_index": 1 } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "key was renamed to newkey", + "const": 1 + }, + { + "description": "new key already exists", + "const": 0 + } + ] + } } } diff --git a/src/commands/replconf.json b/src/commands/replconf.json index 630b62136..d5c43d525 100644 --- a/src/commands/replconf.json +++ b/src/commands/replconf.json @@ -15,6 +15,9 @@ "LOADING", "STALE", "ALLOW_BUSY" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/replicaof.json b/src/commands/replicaof.json index 6299ea3ff..7f1d08754 100644 --- a/src/commands/replicaof.json +++ b/src/commands/replicaof.json @@ -21,6 +21,11 @@ "name": "port", "type": "integer" } - ] + ], + "reply_schema": { + "description": "replicaOf status", + "type": "string", + "pattern": "OK*" + } } } diff --git a/src/commands/reset.json b/src/commands/reset.json index 40041cd8c..81a20a3a8 100644 --- a/src/commands/reset.json +++ b/src/commands/reset.json @@ -16,6 +16,9 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "const": "RESET" + } } } diff --git a/src/commands/restore-asking.json b/src/commands/restore-asking.json index f4602f971..b260478f2 100644 --- a/src/commands/restore-asking.json +++ b/src/commands/restore-asking.json @@ -94,6 +94,9 @@ "optional": true, "since": "5.0.0" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/restore.json b/src/commands/restore.json index d6cebf578..7e40d1c4f 100644 --- a/src/commands/restore.json +++ b/src/commands/restore.json @@ -48,6 +48,9 @@ } } ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "key", diff --git a/src/commands/role.json b/src/commands/role.json index 4d470e350..498a5de52 100644 --- a/src/commands/role.json +++ b/src/commands/role.json @@ -16,6 +16,119 @@ "acl_categories": [ "ADMIN", "DANGEROUS" - ] + ], + "reply_schema": { + "oneOf": [ + { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "const": "master" + }, + { + "description": "current replication master offset", + "type": "integer" + }, + { + "description": "connected replicas", + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "description": "replica ip", + "type": "string" + }, + { + "description": "replica port", + "type": "string" + }, + { + "description": "last acknowledged replication offset", + "type": "string" + } + ] + } + } + ] + }, + { + "type": "array", + "minItems": 5, + "maxItems": 5, + "items": [ + { + "const": "slave" + }, + { + "description": "ip of master", + "type": "string" + }, + { + "description": "port number of master", + "type": "integer" + }, + { + "description": "state of the replication from the point of view of the master", + "oneOf": [ + { + "description": "the instance is in handshake with its master", + "const": "handshake" + }, + { + "description": "the instance in not active", + "const": "none" + }, + { + "description": "the instance needs to connect to its master", + "const": "connect" + }, + { + "description": "the master-replica connection is in progress", + "const": "connecting" + }, + { + "description": "the master and replica are trying to perform the synchronization", + "const": "sync" + }, + { + "description": "the replica is online", + "const": "connected" + }, + { + "description": "instance state is unknown", + "const": "unknown" + } + ] + }, + { + "description": "the amount of data received from the replica so far in terms of master replication offset", + "type": "integer" + } + ] + }, + { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "const": "sentinel" + }, + { + "description": "list of master names monitored by this sentinel instance", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + ] + } } } diff --git a/src/commands/rpop.json b/src/commands/rpop.json index 10518a827..c9b55b125 100644 --- a/src/commands/rpop.json +++ b/src/commands/rpop.json @@ -40,6 +40,25 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "null", + "description": "Key does not exist." + }, + { + "type": "string", + "description": "When 'COUNT' was not given, the value of the last element." + }, + { + "type": "array", + "description": "When 'COUNT' was given, list of popped elements.", + "items": { + "type": "string" + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/rpoplpush.json b/src/commands/rpoplpush.json index ea3c7749d..ddb0537e7 100644 --- a/src/commands/rpoplpush.json +++ b/src/commands/rpoplpush.json @@ -57,6 +57,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "string", + "description": "The element being popped and pushed." + }, + { + "type": "null", + "description": "Source list is empty." + } + ] + }, "arguments": [ { "name": "source", diff --git a/src/commands/rpush.json b/src/commands/rpush.json index 03c1f862a..7b6a4a634 100644 --- a/src/commands/rpush.json +++ b/src/commands/rpush.json @@ -40,6 +40,11 @@ } } ], + "reply_schema": { + "description": "Length of the list after the push operations.", + "type": "integer", + "minimum": 1 + }, "arguments": [ { "name": "key", diff --git a/src/commands/rpushx.json b/src/commands/rpushx.json index 9d8c14eed..19294dd02 100644 --- a/src/commands/rpushx.json +++ b/src/commands/rpushx.json @@ -40,6 +40,11 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "Length of the list after the push operation.", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/sadd.json b/src/commands/sadd.json index 841eb1ffa..89c824625 100644 --- a/src/commands/sadd.json +++ b/src/commands/sadd.json @@ -40,6 +40,10 @@ } } ], + "reply_schema": { + "description": "Number of elements that were added to the set, not including all the elements already present in the set.", + "type": "integer" + }, "arguments": [ { "name": "key", diff --git a/src/commands/save.json b/src/commands/save.json index 7cf5cd246..1885128bf 100644 --- a/src/commands/save.json +++ b/src/commands/save.json @@ -11,6 +11,9 @@ "ADMIN", "NOSCRIPT", "NO_MULTI" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/scan.json b/src/commands/scan.json index e0856d0f0..bdf27a575 100644 --- a/src/commands/scan.json +++ b/src/commands/scan.json @@ -47,6 +47,25 @@ "optional": true, "since": "6.0.0" } - ] + ], + "reply_schema": { + "description": "cursor and scan response in array form", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "cursor", + "type": "string" + }, + { + "description": "list of keys", + "type": "array", + "items": { + "type": "string" + } + } + ] + } } } diff --git a/src/commands/scard.json b/src/commands/scard.json index a1f1f8ef2..0b7a832de 100644 --- a/src/commands/scard.json +++ b/src/commands/scard.json @@ -32,6 +32,11 @@ } } ], + "reply_schema": { + "description": "The cardinality (number of elements) of the set, or 0 if key does not exist.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/script-debug.json b/src/commands/script-debug.json index a69ddcac1..25899b94a 100644 --- a/src/commands/script-debug.json +++ b/src/commands/script-debug.json @@ -35,6 +35,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/script-exists.json b/src/commands/script-exists.json index e4070f239..629615907 100644 --- a/src/commands/script-exists.json +++ b/src/commands/script-exists.json @@ -23,6 +23,22 @@ "type": "string", "multiple": true } - ] + ], + "reply_schema": { + "description": "An array of integers that correspond to the specified SHA1 digest arguments.", + "type": "array", + "items": { + "oneOf": [ + { + "description": "sha1 hash exists in script cache", + "const": 1 + }, + { + "description": "sha1 hash does not exist in script cache", + "const": 0 + } + ] + } + } } } diff --git a/src/commands/script-flush.json b/src/commands/script-flush.json index f4d340833..63cfb1e28 100644 --- a/src/commands/script-flush.json +++ b/src/commands/script-flush.json @@ -42,6 +42,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/script-help.json b/src/commands/script-help.json index 7b3bc89ec..d6c6853fd 100644 --- a/src/commands/script-help.json +++ b/src/commands/script-help.json @@ -13,6 +13,13 @@ ], "acl_categories": [ "SCRIPTING" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/script-kill.json b/src/commands/script-kill.json index 970ccd407..c10ff0a0d 100644 --- a/src/commands/script-kill.json +++ b/src/commands/script-kill.json @@ -17,6 +17,9 @@ "command_tips": [ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:ONE_SUCCEEDED" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/script-load.json b/src/commands/script-load.json index b0b4e67e5..a369ee459 100644 --- a/src/commands/script-load.json +++ b/src/commands/script-load.json @@ -23,6 +23,10 @@ "name": "script", "type": "string" } - ] + ], + "reply_schema": { + "description": "The SHA1 digest of the script added into the script cache", + "type": "string" + } } } diff --git a/src/commands/sdiff.json b/src/commands/sdiff.json index 6f5fd0a81..ce7846c46 100644 --- a/src/commands/sdiff.json +++ b/src/commands/sdiff.json @@ -35,6 +35,14 @@ } } ], + "reply_schema": { + "type": "array", + "description": "List with the members of the resulting set.", + "uniqueItems": true, + "items": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/sdiffstore.json b/src/commands/sdiffstore.json index c78cd8999..8ba88e627 100644 --- a/src/commands/sdiffstore.json +++ b/src/commands/sdiffstore.json @@ -51,6 +51,11 @@ } } ], + "reply_schema": { + "description": "Number of the elements in the resulting set.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "destination", diff --git a/src/commands/select.json b/src/commands/select.json index 4375cac64..0f68cde80 100644 --- a/src/commands/select.json +++ b/src/commands/select.json @@ -14,6 +14,9 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "index", diff --git a/src/commands/sentinel-ckquorum.json b/src/commands/sentinel-ckquorum.json index 6180614cc..df0a0032e 100644 --- a/src/commands/sentinel-ckquorum.json +++ b/src/commands/sentinel-ckquorum.json @@ -11,6 +11,11 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "type": "string", + "description": "Returns OK if the current Sentinel configuration is able to reach the quorum needed to failover a master, and the majority needed to authorize the failover.", + "pattern": "OK" + }, "arguments": [ { "name": "master-name", diff --git a/src/commands/sentinel-config.json b/src/commands/sentinel-config.json index f68d17ce8..2369ec1fe 100644 --- a/src/commands/sentinel-config.json +++ b/src/commands/sentinel-config.json @@ -12,6 +12,72 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "oneOf": [ + { + "type": "object", + "description": "When 'SENTINEL-CONFIG GET' is called, returns a map.", + "properties": { + "resolve-hostnames": { + "oneOf": [ + { + "const": "yes" + }, + { + "const": "no" + } + ] + }, + "announce-hostnames": { + "oneOf": [ + { + "const": "yes" + }, + { + "const": "no" + } + ] + }, + "announce-ip": { + "type": "string" + }, + "announce-port": { + "type": "integer" + }, + "sentinel-user": { + "type": "string" + }, + "sentinel-pass": { + "type": "string" + }, + "loglevel": { + "oneOf": [ + { + "const": "debug" + }, + { + "const": "verbose" + }, + { + "const": "notice" + }, + { + "const": "warning" + }, + { + "const": "unknown" + } + ] + } + }, + "additionalProperties": false + }, + { + "const": "OK", + "description": "When 'SENTINEL-CONFIG SET' is called, returns OK on success." + } + ] + }, "arguments": [ { "name":"action", diff --git a/src/commands/sentinel-failover.json b/src/commands/sentinel-failover.json index f6640168a..87f9c4aca 100644 --- a/src/commands/sentinel-failover.json +++ b/src/commands/sentinel-failover.json @@ -11,6 +11,10 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "const": "OK", + "description": "Force a fail over as if the master was not reachable, and without asking for agreement to other Sentinels." + }, "arguments": [ { "name": "master-name", diff --git a/src/commands/sentinel-flushconfig.json b/src/commands/sentinel-flushconfig.json index 7d48cd482..117109f06 100644 --- a/src/commands/sentinel-flushconfig.json +++ b/src/commands/sentinel-flushconfig.json @@ -11,6 +11,10 @@ "ADMIN", "SENTINEL", "ONLY_SENTINEL" - ] + ], + "reply_schema": { + "const": "OK", + "description": "Force Sentinel to rewrite its configuration on disk, including the current Sentinel state." + } } } diff --git a/src/commands/sentinel-get-master-addr-by-name.json b/src/commands/sentinel-get-master-addr-by-name.json index e0fde851c..3dc307867 100644 --- a/src/commands/sentinel-get-master-addr-by-name.json +++ b/src/commands/sentinel-get-master-addr-by-name.json @@ -12,6 +12,22 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "IP addr or hostname." + }, + { + "type": "string", + "description": "Port.", + "pattern": "[0-9]+" + } + ] + }, "arguments": [ { "name": "master-name", diff --git a/src/commands/sentinel-help.json b/src/commands/sentinel-help.json index 4c20313eb..5e3e9a712 100644 --- a/src/commands/sentinel-help.json +++ b/src/commands/sentinel-help.json @@ -12,6 +12,13 @@ "STALE", "SENTINEL", "ONLY_SENTINEL" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/sentinel-is-master-down-by-addr.json b/src/commands/sentinel-is-master-down-by-addr.json index 456ad183a..b0ca319f2 100644 --- a/src/commands/sentinel-is-master-down-by-addr.json +++ b/src/commands/sentinel-is-master-down-by-addr.json @@ -12,6 +12,33 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "oneOf": [ + { + "const": 0, + "description": "Master is up." + }, + { + "const": 1, + "description": "Master is down." + } + ] + }, + { + "type": "string", + "description": "Sentinel address." + }, + { + "type": "integer", + "description": "Port." + } + ] + }, "arguments": [ { "name": "ip", diff --git a/src/commands/sentinel-master.json b/src/commands/sentinel-master.json index ec10f43fd..46d6d950a 100644 --- a/src/commands/sentinel-master.json +++ b/src/commands/sentinel-master.json @@ -12,6 +12,13 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "type": "object", + "description": "The state and info of the specified master.", + "additionalProperties": { + "type": "string" + } + }, "arguments": [ { "name": "master-name", diff --git a/src/commands/sentinel-monitor.json b/src/commands/sentinel-monitor.json index 2c01df900..2ea9aff58 100644 --- a/src/commands/sentinel-monitor.json +++ b/src/commands/sentinel-monitor.json @@ -12,6 +12,9 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "name", diff --git a/src/commands/sentinel-remove.json b/src/commands/sentinel-remove.json index 2e655e7f4..d79f60e6c 100644 --- a/src/commands/sentinel-remove.json +++ b/src/commands/sentinel-remove.json @@ -12,6 +12,9 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "master-name", diff --git a/src/commands/sentinel-replicas.json b/src/commands/sentinel-replicas.json index dc175a7ec..454fcfb91 100644 --- a/src/commands/sentinel-replicas.json +++ b/src/commands/sentinel-replicas.json @@ -12,6 +12,16 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "type": "array", + "description": "List of replicas for this master, and their state.", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, "arguments": [ { "name": "master-name", diff --git a/src/commands/sentinel-set.json b/src/commands/sentinel-set.json index 49feefced..10dcc5735 100644 --- a/src/commands/sentinel-set.json +++ b/src/commands/sentinel-set.json @@ -12,6 +12,9 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "master-name", diff --git a/src/commands/set.json b/src/commands/set.json index 688d534d7..3c6c06ff4 100644 --- a/src/commands/set.json +++ b/src/commands/set.json @@ -55,6 +55,26 @@ } } ], + "reply_schema": { + "anyOf":[ + { + "description": "`GET` not given: Operation was aborted (conflict with one of the `XX`/`NX` options).", + "type": "null" + }, + { + "description": "`GET` not given: The key was set.", + "const": "OK" + }, + { + "description": "`GET` given: The key didn't exist before the `SET`", + "type": "null" + }, + { + "description": "`GET` given: The previous value of the key", + "type": "string" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/setbit.json b/src/commands/setbit.json index 723dfd8f4..bf1843078 100644 --- a/src/commands/setbit.json +++ b/src/commands/setbit.json @@ -34,6 +34,17 @@ } } ], + "reply_schema": { + "description": "The original bit value stored at offset.", + "oneOf": [ + { + "const": 0 + }, + { + "const": 1 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/setex.json b/src/commands/setex.json index b99f59f7e..a45561a21 100644 --- a/src/commands/setex.json +++ b/src/commands/setex.json @@ -38,6 +38,9 @@ } } ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "key", diff --git a/src/commands/setnx.json b/src/commands/setnx.json index 5150d8fd2..d026272bb 100644 --- a/src/commands/setnx.json +++ b/src/commands/setnx.json @@ -39,6 +39,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The key was set.", + "const": 0 + }, + { + "description": "The key was not set.", + "const": 1 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/setrange.json b/src/commands/setrange.json index d1336719a..f9d61dfa3 100644 --- a/src/commands/setrange.json +++ b/src/commands/setrange.json @@ -33,6 +33,11 @@ } } ], + "reply_schema": { + "description": "Length of the string after it was modified by the command.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/shutdown.json b/src/commands/shutdown.json index 001cc2552..033a7012e 100644 --- a/src/commands/shutdown.json +++ b/src/commands/shutdown.json @@ -60,6 +60,10 @@ "optional": true, "since": "7.0.0" } - ] + ], + "reply_schema": { + "description": "OK if ABORT was specified and shutdown was aborted. On successful shutdown, nothing is returned since the server quits and the connection is closed. On failure, an error is returned.", + "const": "OK" + } } } diff --git a/src/commands/sinter.json b/src/commands/sinter.json index 63f9e8676..f0cd95de0 100644 --- a/src/commands/sinter.json +++ b/src/commands/sinter.json @@ -35,6 +35,14 @@ } } ], + "reply_schema": { + "type": "array", + "description": "List with the members of the resulting set.", + "uniqueItems": true, + "items": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/sintercard.json b/src/commands/sintercard.json index 8047f7a69..16769850d 100644 --- a/src/commands/sintercard.json +++ b/src/commands/sintercard.json @@ -33,6 +33,11 @@ } } ], + "reply_schema": { + "description": "Number of the elements in the resulting intersection.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "numkeys", diff --git a/src/commands/sinterstore.json b/src/commands/sinterstore.json index 85e462e3e..60a8db52a 100644 --- a/src/commands/sinterstore.json +++ b/src/commands/sinterstore.json @@ -51,6 +51,11 @@ } } ], + "reply_schema": { + "description": "Number of the elements in the result set.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "destination", diff --git a/src/commands/sismember.json b/src/commands/sismember.json index 7a814b82b..cb81682cc 100644 --- a/src/commands/sismember.json +++ b/src/commands/sismember.json @@ -32,6 +32,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "const": 0, + "description": "The element is not a member of the set, or the key does not exist." + }, + { + "const": 1, + "description": "The element is a member of the set." + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/slaveof.json b/src/commands/slaveof.json index 70560f1b6..f9266fabe 100644 --- a/src/commands/slaveof.json +++ b/src/commands/slaveof.json @@ -26,6 +26,11 @@ "name": "port", "type": "integer" } - ] + ], + "reply_schema": { + "description": "slaveOf status", + "type": "string", + "pattern": "OK*" + } } } diff --git a/src/commands/slowlog-get.json b/src/commands/slowlog-get.json index 11212643e..e4652e895 100644 --- a/src/commands/slowlog-get.json +++ b/src/commands/slowlog-get.json @@ -22,6 +22,47 @@ "REQUEST_POLICY:ALL_NODES", "NONDETERMINISTIC_OUTPUT" ], + "reply_schema": { + "type": "array", + "description": "Entries from the slow log in chronological order.", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 6, + "maxItems": 6, + "items": [ + { + "type": "integer", + "description": "Slow log entry ID." + }, + { + "type": "integer", + "description": "The unix timestamp at which the logged command was processed.", + "minimum": 0 + }, + { + "type": "integer", + "description": "The amount of time needed for its execution, in microseconds.", + "minimum": 0 + }, + { + "type": "array", + "description": "The arguments of the command.", + "items": { + "type": "string" + } + }, + { + "type": "string", + "description": "Client IP address and port." + }, + { + "type": "string", + "description": "Client name if set via the CLIENT SETNAME command." + } + ] + } + }, "arguments": [ { "name": "count", diff --git a/src/commands/slowlog-help.json b/src/commands/slowlog-help.json index cf2707d38..dde8fd459 100644 --- a/src/commands/slowlog-help.json +++ b/src/commands/slowlog-help.json @@ -10,6 +10,13 @@ "command_flags": [ "LOADING", "STALE" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/slowlog-len.json b/src/commands/slowlog-len.json index 9a8969b0d..f8c7798cf 100644 --- a/src/commands/slowlog-len.json +++ b/src/commands/slowlog-len.json @@ -16,6 +16,11 @@ "REQUEST_POLICY:ALL_NODES", "RESPONSE_POLICY:AGG_SUM", "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "type": "integer", + "description": "Number of entries in the slow log.", + "minimum": 0 + } } } diff --git a/src/commands/slowlog-reset.json b/src/commands/slowlog-reset.json index 36c024156..c4006e371 100644 --- a/src/commands/slowlog-reset.json +++ b/src/commands/slowlog-reset.json @@ -15,6 +15,9 @@ "command_tips": [ "REQUEST_POLICY:ALL_NODES", "RESPONSE_POLICY:ALL_SUCCEEDED" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/smembers.json b/src/commands/smembers.json index b5d4ff29d..ff5969f51 100644 --- a/src/commands/smembers.json +++ b/src/commands/smembers.json @@ -35,6 +35,14 @@ } } ], + "reply_schema": { + "type": "array", + "description": "All elements of the set.", + "uniqueItems": true, + "items": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/smismember.json b/src/commands/smismember.json index cb4dd2e67..d78787900 100644 --- a/src/commands/smismember.json +++ b/src/commands/smismember.json @@ -33,6 +33,23 @@ } } ], + "reply_schema": { + "type": "array", + "description": "List representing the membership of the given elements, in the same order as they are requested.", + "minItems": 1, + "items": { + "oneOf": [ + { + "const": 0, + "description": "Not a member of the set or the key does not exist." + }, + { + "const": 1, + "description": "A member of the set." + } + ] + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/smove.json b/src/commands/smove.json index de5756de9..df282b2bb 100644 --- a/src/commands/smove.json +++ b/src/commands/smove.json @@ -52,6 +52,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "const": 1, + "description": "Element is moved." + }, + { + "const": 0, + "description": "The element is not a member of source and no operation was performed." + } + ] + }, "arguments": [ { "name": "source", diff --git a/src/commands/sort.json b/src/commands/sort.json index 1c332461b..6e1b2626e 100644 --- a/src/commands/sort.json +++ b/src/commands/sort.json @@ -133,6 +133,30 @@ "key_spec_index": 2, "optional": true } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "when the store option is specified the command returns the number of sorted elements in the destination list", + "type": "integer", + "minimum": 0 + }, + { + "description": "when not passing the store option the command returns a list of sorted elements", + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "description": "GET option is specified, but no object was found ", + "type": "null" + } + ] + } + } + ] + } } } diff --git a/src/commands/sort_ro.json b/src/commands/sort_ro.json index 0b00ba8ad..27af54c99 100644 --- a/src/commands/sort_ro.json +++ b/src/commands/sort_ro.json @@ -112,6 +112,13 @@ "type": "pure-token", "optional": true } - ] + ], + "reply_schema": { + "description": "a list of sorted elements", + "type": "array", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/spop.json b/src/commands/spop.json index c93e426e1..a116c8473 100644 --- a/src/commands/spop.json +++ b/src/commands/spop.json @@ -43,6 +43,26 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "null", + "description": "The key does not exist." + }, + { + "type": "string", + "description": "The removed member when 'COUNT' is not given." + }, + { + "type": "array", + "description": "List to the removed members when 'COUNT' is given.", + "uniqueItems": true, + "items": { + "type": "string" + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/spublish.json b/src/commands/spublish.json index 6ed748f95..16c948cc6 100644 --- a/src/commands/spublish.json +++ b/src/commands/spublish.json @@ -41,6 +41,11 @@ } } } - ] + ], + "reply_schema": { + "description": "the number of clients that received the message. Note that in a Redis Cluster, only clients that are connected to the same node as the publishing client are included in the count", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/srandmember.json b/src/commands/srandmember.json index 67efc87ca..4ba2b7501 100644 --- a/src/commands/srandmember.json +++ b/src/commands/srandmember.json @@ -53,6 +53,31 @@ "optional": true, "since": "2.6.0" } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "In case `count` is not given and key doesn't exist", + "type": "null" + }, + { + "description": "In case `count` is not given, randomly selected element", + "type": "string" + }, + { + "description": "In case `count` is given, an array of elements", + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + { + "description": "In case `count` is given and key doesn't exist", + "type": "array", + "maxItems": 0 + } + ] + } } } diff --git a/src/commands/srem.json b/src/commands/srem.json index 82433a4a8..ec9ab41db 100644 --- a/src/commands/srem.json +++ b/src/commands/srem.json @@ -39,6 +39,11 @@ } } ], + "reply_schema": { + "description": "Number of members that were removed from the set, not including non existing members.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/sscan.json b/src/commands/sscan.json index 875ad0cd1..f0d89e224 100644 --- a/src/commands/sscan.json +++ b/src/commands/sscan.json @@ -57,6 +57,25 @@ "type": "integer", "optional": true } - ] + ], + "reply_schema": { + "description": "cursor and scan response in array form", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "cursor", + "type": "string" + }, + { + "description": "list of set members", + "type": "array", + "items": { + "type": "string" + } + } + ] + } } } diff --git a/src/commands/strlen.json b/src/commands/strlen.json index a5e2d6ffb..3cdce4807 100644 --- a/src/commands/strlen.json +++ b/src/commands/strlen.json @@ -32,6 +32,11 @@ } } ], + "reply_schema": { + "description": "The length of the string value stored at key, or 0 when key does not exist.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/substr.json b/src/commands/substr.json index 9f3f2bf03..c1134ce66 100644 --- a/src/commands/substr.json +++ b/src/commands/substr.json @@ -37,6 +37,10 @@ } } ], + "reply_schema": { + "type": "string", + "description": "The substring of the string value stored at key, determined by the offsets start and end (both are inclusive)." + }, "arguments": [ { "name": "key", diff --git a/src/commands/sunion.json b/src/commands/sunion.json index 9bdccacd4..3873a6a39 100644 --- a/src/commands/sunion.json +++ b/src/commands/sunion.json @@ -35,6 +35,14 @@ } } ], + "reply_schema": { + "type": "array", + "description": "List with the members of the resulting set.", + "uniqueItems": true, + "items": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/sunionstore.json b/src/commands/sunionstore.json index f4ef0b3b2..b703904ac 100644 --- a/src/commands/sunionstore.json +++ b/src/commands/sunionstore.json @@ -51,6 +51,11 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "Number of the elements in the resulting set.", + "minimum": 0 + }, "arguments": [ { "name": "destination", diff --git a/src/commands/swapdb.json b/src/commands/swapdb.json index 6ea2baeaa..7ed001871 100644 --- a/src/commands/swapdb.json +++ b/src/commands/swapdb.json @@ -23,6 +23,9 @@ "name": "index2", "type": "integer" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/time.json b/src/commands/time.json index b5711a3ba..540190f55 100644 --- a/src/commands/time.json +++ b/src/commands/time.json @@ -13,6 +13,16 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "type": "array", + "description": "Array containing two elements: Unix time in seconds and microseconds.", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "string", + "pattern": "[0-9]+" + } + } } } diff --git a/src/commands/touch.json b/src/commands/touch.json index ef4c1c926..b2a2894c9 100644 --- a/src/commands/touch.json +++ b/src/commands/touch.json @@ -43,6 +43,11 @@ "key_spec_index": 0, "multiple": true } - ] + ], + "reply_schema": { + "description": "the number of touched keys", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/ttl.json b/src/commands/ttl.json index 36297eeb9..f4d957825 100644 --- a/src/commands/ttl.json +++ b/src/commands/ttl.json @@ -42,6 +42,23 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "TTL in seconds.", + "type": "integer", + "minimum": 0 + }, + { + "description": "The key exists but has no associated expire.", + "const": -1 + }, + { + "description": "The key does not exist.", + "const": -2 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/type.json b/src/commands/type.json index df8e45352..b4a4e766f 100644 --- a/src/commands/type.json +++ b/src/commands/type.json @@ -32,6 +32,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Key doesn't exist", + "type": "null" + }, + { + "description": "Type of the key", + "type": "string" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/unlink.json b/src/commands/unlink.json index 511e728a2..559f1e96b 100644 --- a/src/commands/unlink.json +++ b/src/commands/unlink.json @@ -44,6 +44,11 @@ "key_spec_index": 0, "multiple": true } - ] + ], + "reply_schema": { + "description": "the number of keys that were unlinked", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/unwatch.json b/src/commands/unwatch.json index 820ea5b93..256411f1d 100644 --- a/src/commands/unwatch.json +++ b/src/commands/unwatch.json @@ -15,6 +15,9 @@ ], "acl_categories": [ "TRANSACTION" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/wait.json b/src/commands/wait.json index 4930932d9..110c00af6 100644 --- a/src/commands/wait.json +++ b/src/commands/wait.json @@ -16,6 +16,11 @@ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:AGG_MIN" ], + "reply_schema": { + "type": "integer", + "description": "The number of replicas reached by all the writes performed in the context of the current connection.", + "minimum": 0 + }, "arguments": [ { "name": "numreplicas", diff --git a/src/commands/watch.json b/src/commands/watch.json index 0a9e3703e..3f16f7360 100644 --- a/src/commands/watch.json +++ b/src/commands/watch.json @@ -35,6 +35,9 @@ } } ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "key", diff --git a/src/commands/xack.json b/src/commands/xack.json index b9d0aa4dd..f7791f270 100644 --- a/src/commands/xack.json +++ b/src/commands/xack.json @@ -48,6 +48,11 @@ "type": "string", "multiple": true } - ] + ], + "reply_schema": { + "description": "The command returns the number of messages successfully acknowledged. Certain message IDs may no longer be part of the PEL (for example because they have already been acknowledged), and XACK will not count them as successfully acknowledged.", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/xadd.json b/src/commands/xadd.json index f3d99f841..72f864d92 100644 --- a/src/commands/xadd.json +++ b/src/commands/xadd.json @@ -143,6 +143,19 @@ } ] } - ] + ], + "reply_schema": { + "oneOf":[ + { + "description": "The ID of the added entry. The ID is the one auto-generated if * is passed as ID argument, otherwise the command just returns the same ID specified by the user during insertion.", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "The NOMKSTREAM option is given and the key doesn't exist.", + "type": "null" + } + ] + } } } diff --git a/src/commands/xautoclaim.json b/src/commands/xautoclaim.json index 726bf38fe..f23386e4c 100644 --- a/src/commands/xautoclaim.json +++ b/src/commands/xautoclaim.json @@ -42,6 +42,83 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "Claimed stream entries (with data, if `JUSTID` was not given).", + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "description": "Cursor for next call.", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Data", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + { + "description": "Entry IDs which no longer exist in the stream, and were deleted from the PEL in which they were found.", + "type": "array", + "items": { + "type": "string", + "pattern": "[0-9]+-[0-9]+" + } + } + ] + }, + { + "description": "Claimed stream entries (without data, if `JUSTID` was given).", + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "description": "Cursor for next call.", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "[0-9]+-[0-9]+" + } + }, + { + "description": "Entry IDs which no longer exist in the stream, and were deleted from the PEL in which they were found.", + "type": "array", + "items": { + "type": "string", + "pattern": "[0-9]+-[0-9]+" + } + } + ] + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/xclaim.json b/src/commands/xclaim.json index 132a60c2e..950c215b9 100644 --- a/src/commands/xclaim.json +++ b/src/commands/xclaim.json @@ -95,6 +95,44 @@ "type": "string", "optional": true } - ] + ], + "reply_schema": { + "description": "Stream entries with IDs matching the specified range.", + "anyOf": [ + { + "description": "If JUSTID option is specified, return just an array of IDs of messages successfully claimed", + "type": "array", + "items": { + "description": "Entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + } + }, + { + "description": "array of stream entries that contains each entry as an array of 2 elements, the Entry ID and the entry data itself", + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Data", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + } + ] + } } } diff --git a/src/commands/xdel.json b/src/commands/xdel.json index 061ea80c0..5854f5084 100644 --- a/src/commands/xdel.json +++ b/src/commands/xdel.json @@ -44,6 +44,11 @@ "type": "string", "multiple": true } - ] + ], + "reply_schema": { + "description": "The number of entries actually deleted", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/xgroup-create.json b/src/commands/xgroup-create.json index f099d90ce..16a8b99eb 100644 --- a/src/commands/xgroup-create.json +++ b/src/commands/xgroup-create.json @@ -77,6 +77,9 @@ "type": "integer", "optional": true } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/xgroup-createconsumer.json b/src/commands/xgroup-createconsumer.json index c338d4af6..764b26447 100644 --- a/src/commands/xgroup-createconsumer.json +++ b/src/commands/xgroup-createconsumer.json @@ -48,6 +48,17 @@ "name": "consumer", "type": "string" } - ] + ], + "reply_schema": { + "description": "The number of created consumers (0 or 1)", + "oneOf": [ + { + "const": 1 + }, + { + "const": 0 + } + ] + } } } diff --git a/src/commands/xgroup-delconsumer.json b/src/commands/xgroup-delconsumer.json index 29d62912d..c68f0b567 100644 --- a/src/commands/xgroup-delconsumer.json +++ b/src/commands/xgroup-delconsumer.json @@ -47,6 +47,11 @@ "name": "consumer", "type": "string" } - ] + ], + "reply_schema": { + "description": "The number of pending messages that were yet associated with such a consumer", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/xgroup-destroy.json b/src/commands/xgroup-destroy.json index 4a78f4c14..60a481a80 100644 --- a/src/commands/xgroup-destroy.json +++ b/src/commands/xgroup-destroy.json @@ -43,6 +43,17 @@ "name": "group", "type": "string" } - ] + ], + "reply_schema": { + "description": "The number of destroyed consumer groups (0 or 1)", + "oneOf": [ + { + "const": 1 + }, + { + "const": 0 + } + ] + } } } diff --git a/src/commands/xgroup-help.json b/src/commands/xgroup-help.json index 4c5a2b957..d4e9d4ad3 100644 --- a/src/commands/xgroup-help.json +++ b/src/commands/xgroup-help.json @@ -13,6 +13,13 @@ ], "acl_categories": [ "STREAM" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/xgroup-setid.json b/src/commands/xgroup-setid.json index e7c41b3ab..89bbc2a6b 100644 --- a/src/commands/xgroup-setid.json +++ b/src/commands/xgroup-setid.json @@ -71,6 +71,9 @@ "type": "integer", "optional": true } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/xinfo-consumers.json b/src/commands/xinfo-consumers.json index 15fe5e1a2..2acb0bf05 100644 --- a/src/commands/xinfo-consumers.json +++ b/src/commands/xinfo-consumers.json @@ -46,6 +46,29 @@ "name": "group", "type": "string" } - ] + ], + "reply_schema": { + "description": "Array list of consumers", + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "pending": { + "type": "integer" + }, + "idle": { + "type": "integer" + }, + "inactive": { + "type": "integer" + } + } + } + } } } diff --git a/src/commands/xinfo-groups.json b/src/commands/xinfo-groups.json index e9b61ba06..4196773f3 100644 --- a/src/commands/xinfo-groups.json +++ b/src/commands/xinfo-groups.json @@ -39,6 +39,48 @@ } } ], + "reply_schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "consumers": { + "type": "integer" + }, + "pending": { + "type": "integer" + }, + "last-delivered-id": { + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + "entries-read": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer" + } + ] + }, + "lag": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer" + } + ] + } + } + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/xinfo-help.json b/src/commands/xinfo-help.json index e11468353..3441ace14 100644 --- a/src/commands/xinfo-help.json +++ b/src/commands/xinfo-help.json @@ -13,6 +13,13 @@ ], "acl_categories": [ "STREAM" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/xinfo-stream.json b/src/commands/xinfo-stream.json index 233afdde3..25941f33b 100644 --- a/src/commands/xinfo-stream.json +++ b/src/commands/xinfo-stream.json @@ -43,6 +43,291 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Summary form, in case `FULL` was not given.", + "type": "object", + "additionalProperties": false, + "properties": { + "length": { + "description": "the number of entries in the stream (see `XLEN`)", + "type": "integer" + }, + "radix-tree-keys": { + "description": "the number of keys in the underlying radix data structure", + "type": "integer" + }, + "radix-tree-nodes": { + "description": "the number of nodes in the underlying radix data structure", + "type": "integer" + }, + "last-generated-id": { + "description": "the ID of the least-recently entry that was added to the stream", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + "max-deleted-entry-id": { + "description": "the maximal entry ID that was deleted from the stream", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + "recorded-first-entry-id": { + "description": "cached copy of the first entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + "entries-added": { + "description": "the count of all entries added to the stream during its lifetime", + "type": "integer" + }, + "groups": { + "description": "the number of consumer groups defined for the stream", + "type": "integer" + }, + "first-entry": { + "description": "the first entry of the stream", + "oneOf": [ + { + "type": "null" + }, + { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "data", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + ] + }, + "last-entry": { + "description": "the last entry of the stream", + "oneOf": [ + { + "type": "null" + }, + { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "data", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + ] + } + } + }, + { + "description": "Extended form, in case `FULL` was given.", + "type": "object", + "additionalProperties": false, + "properties": { + "length": { + "description": "the number of entries in the stream (see `XLEN`)", + "type": "integer" + }, + "radix-tree-keys": { + "description": "the number of keys in the underlying radix data structure", + "type": "integer" + }, + "radix-tree-nodes": { + "description": "the number of nodes in the underlying radix data structure", + "type": "integer" + }, + "last-generated-id": { + "description": "the ID of the least-recently entry that was added to the stream", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + "max-deleted-entry-id": { + "description": "the maximal entry ID that was deleted from the stream", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + "recorded-first-entry-id": { + "description": "cached copy of the first entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + "entries-added": { + "description": "the count of all entries added to the stream during its lifetime", + "type": "integer" + }, + "entries": { + "description": "all the entries of the stream", + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "data", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "groups": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "description": "group name", + "type": "string" + }, + "last-delivered-id": { + "description": "last entry ID that was delivered to a consumer", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + "entries-read": { + "description": "total number of entries ever read by consumers in the group", + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer" + } + ] + }, + "lag": { + "description": "number of entries left to be consumed from the stream", + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer" + } + ] + }, + "pel-count": { + "description": "total number of unacknowledged entries", + "type": "integer" + }, + "pending": { + "description": "data about all of the unacknowledged entries", + "type": "array", + "items": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "items": [ + { + "description": "Entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Consumer name", + "type": "string" + }, + { + "description": "Delivery timestamp", + "type": "integer" + }, + { + "description": "Delivery count", + "type": "integer" + } + ] + } + }, + "consumers": { + "description": "data about all of the consumers of the group", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "active-time": { + "type": "integer", + "description": "Last time this consumer was active (successful reading/claiming).", + "minimum": 0 + }, + "name": { + "description": "consumer name", + "type": "string" + }, + "seen-time": { + "description": "timestamp of the last interaction attempt of the consumer", + "type": "integer" + }, + "pel-count": { + "description": "number of unacknowledged entries that belong to the consumer", + "type": "integer" + }, + "pending": { + "description": "data about the unacknowledged entries", + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "description": "Entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Delivery timestamp", + "type": "integer" + }, + { + "description": "Delivery count", + "type": "integer" + } + ] + } + } + } + } + } + } + } + } + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/xlen.json b/src/commands/xlen.json index 9adc261f7..b3a82f774 100644 --- a/src/commands/xlen.json +++ b/src/commands/xlen.json @@ -38,6 +38,11 @@ "type": "key", "key_spec_index": 0 } - ] + ], + "reply_schema": { + "description": "The number of entries of the stream at key", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/xpending.json b/src/commands/xpending.json index cd8ee8dd2..04725e1e4 100644 --- a/src/commands/xpending.json +++ b/src/commands/xpending.json @@ -41,6 +41,79 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Extended form, in case `start` was given.", + "type": "array", + "items": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "items": [ + { + "description": "Entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Consumer name", + "type": "string" + }, + { + "description": "Idle time", + "type": "integer" + }, + { + "description": "Delivery count", + "type": "integer" + } + ] + } + }, + { + "description": "Summary form, in case `start` was not given.", + "type": "array", + "minItems": 4, + "maxItems": 4, + "items": [ + { + "description": "Total number of pending messages", + "type": "integer" + }, + { + "description": "Minimal pending entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Maximal pending entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Consumers with pending messages", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Consumer name", + "type": "string" + }, + { + "description": "Number of pending messages", + "type": "string" + } + ] + } + } + ] + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/xrange.json b/src/commands/xrange.json index 9a3ddeac0..325a4564a 100644 --- a/src/commands/xrange.json +++ b/src/commands/xrange.json @@ -38,6 +38,30 @@ } } ], + "reply_schema": { + "description": "Stream entries with IDs matching the specified range.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Data", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/xread.json b/src/commands/xread.json index 8f66d7ee5..e6978794a 100644 --- a/src/commands/xread.json +++ b/src/commands/xread.json @@ -67,6 +67,43 @@ } ] } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "A map of key-value elements when each element composed of key name and the entries reported for that key", + "type": "object", + "patternProperties": { + "^.*$": { + "description": "The entries reported for that key", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "entry id", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "array of field-value pairs", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + } + } + }, + { + "description": "If BLOCK option is given, and a timeout occurs, or there is no stream we can serve", + "type": "null" + } + ] + } } } diff --git a/src/commands/xreadgroup.json b/src/commands/xreadgroup.json index e160f3d00..96d7a89ba 100644 --- a/src/commands/xreadgroup.json +++ b/src/commands/xreadgroup.json @@ -87,6 +87,48 @@ } ] } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "If BLOCK option is specified and the timeout expired", + "type": "null" + }, + { + "description": "A map of key-value elements when each element composed of key name and the entries reported for that key", + "type": "object", + "additionalProperties": { + "description": "The entries reported for that key", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Stream id", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "oneOf": [ + { + "description": "Array of field-value pairs", + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + ] + } + } + } + ] + } } } diff --git a/src/commands/xrevrange.json b/src/commands/xrevrange.json index 65d81db81..41798a16c 100644 --- a/src/commands/xrevrange.json +++ b/src/commands/xrevrange.json @@ -58,6 +58,29 @@ "type": "integer", "optional": true } - ] + ], + "reply_schema": { + "description": "An array of the entries with IDs matching the specified range", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Stream id", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Array of field-value pairs", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + } } } diff --git a/src/commands/xsetid.json b/src/commands/xsetid.json index b94b9601e..b69d80c88 100644 --- a/src/commands/xsetid.json +++ b/src/commands/xsetid.json @@ -64,6 +64,9 @@ "optional": true, "since": "7.0.0" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/xtrim.json b/src/commands/xtrim.json index 03c48ebb5..6abd4903b 100644 --- a/src/commands/xtrim.json +++ b/src/commands/xtrim.json @@ -98,6 +98,11 @@ } ] } - ] + ], + "reply_schema": { + "description": "The number of entries deleted from the stream.", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/zadd.json b/src/commands/zadd.json index 43791deb4..e2cbd98c8 100644 --- a/src/commands/zadd.json +++ b/src/commands/zadd.json @@ -48,6 +48,26 @@ } } ], + "reply_schema": { + "anyOf":[ + { + "description": "Operation was aborted (conflict with one of the `XX`/`NX`/`LT`/`GT` options).", + "type": "null" + }, + { + "description": "The number of new members (when the `CH` option is not used)", + "type": "integer" + }, + { + "description": "The number of new or updated members (when the `CH` option is used)", + "type": "integer" + }, + { + "description": "The updated score of the member (when the `INCR` option is used)", + "type": "number" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zcard.json b/src/commands/zcard.json index 84022a7f5..f80a01007 100644 --- a/src/commands/zcard.json +++ b/src/commands/zcard.json @@ -32,6 +32,10 @@ } } ], + "reply_schema": { + "description": "The cardinality (number of elements) of the sorted set, or 0 if key does not exist", + "type": "integer" + }, "arguments": [ { "name": "key", diff --git a/src/commands/zcount.json b/src/commands/zcount.json index 6572d4a51..9ad8fdb24 100644 --- a/src/commands/zcount.json +++ b/src/commands/zcount.json @@ -33,6 +33,10 @@ } } ], + "reply_schema": { + "description": "The number of elements in the specified score range", + "type": "integer" + }, "arguments": [ { "name": "key", diff --git a/src/commands/zdiff.json b/src/commands/zdiff.json index 3eee28983..a361e249d 100644 --- a/src/commands/zdiff.json +++ b/src/commands/zdiff.json @@ -33,6 +33,36 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "A list of members. Returned in case `WITHSCORES` was not used.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "description": "Members and their scores. Returned in case `WITHSCORES` was used. In RESP2 this is returned as a flat array", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Member", + "type": "string" + }, + { + "description": "Score", + "type": "number" + } + ] + } + } + ] + }, "arguments": [ { "name": "numkeys", diff --git a/src/commands/zdiffstore.json b/src/commands/zdiffstore.json index 7fc0102fd..26f205edc 100644 --- a/src/commands/zdiffstore.json +++ b/src/commands/zdiffstore.json @@ -52,6 +52,10 @@ } } ], + "reply_schema": { + "description": "Number of elements in the resulting sorted set at `destination`", + "type": "integer" + }, "arguments": [ { "name": "destination", diff --git a/src/commands/zincrby.json b/src/commands/zincrby.json index 2ebafe0a1..a3283a3b6 100644 --- a/src/commands/zincrby.json +++ b/src/commands/zincrby.json @@ -35,6 +35,10 @@ } } ], + "reply_schema": { + "description": "The new score of `member`", + "type": "number" + }, "arguments": [ { "name": "key", diff --git a/src/commands/zinter.json b/src/commands/zinter.json index b05dc8d3a..d0dd046ed 100644 --- a/src/commands/zinter.json +++ b/src/commands/zinter.json @@ -33,6 +33,36 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "Result of intersection, containing only the member names. Returned in case `WITHSCORES` was not used.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "description": "Result of intersection, containing members and their scores. Returned in case `WITHSCORES` was used. In RESP2 this is returned as a flat array", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Member", + "type": "string" + }, + { + "description": "Score", + "type": "number" + } + ] + } + } + ] + }, "arguments": [ { "name": "numkeys", diff --git a/src/commands/zintercard.json b/src/commands/zintercard.json index 2c2359968..732bab830 100644 --- a/src/commands/zintercard.json +++ b/src/commands/zintercard.json @@ -33,6 +33,11 @@ } } ], + "reply_schema": { + "description": "Number of elements in the resulting intersection.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "numkeys", diff --git a/src/commands/zinterstore.json b/src/commands/zinterstore.json index bd40460ac..32661c564 100644 --- a/src/commands/zinterstore.json +++ b/src/commands/zinterstore.json @@ -52,6 +52,11 @@ } } ], + "reply_schema": { + "description": "Number of elements in the resulting sorted set.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "destination", diff --git a/src/commands/zlexcount.json b/src/commands/zlexcount.json index 5dff46e4b..5366441a6 100644 --- a/src/commands/zlexcount.json +++ b/src/commands/zlexcount.json @@ -33,6 +33,11 @@ } } ], + "reply_schema": { + "description": "Number of elements in the specified score range.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/zmpop.json b/src/commands/zmpop.json index 2edeaf2cc..58e710170 100644 --- a/src/commands/zmpop.json +++ b/src/commands/zmpop.json @@ -34,6 +34,45 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "No element could be popped.", + "type": "null" + }, + { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "Name of the key that elements were popped." + }, + { + "type": "array", + "description": "Popped elements.", + "items": { + "type": "array", + "uniqueItems": true, + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "Name of the member." + }, + { + "type": "number", + "description": "Score." + } + ] + } + } + ] + } + ] + }, "arguments": [ { "name": "numkeys", diff --git a/src/commands/zmscore.json b/src/commands/zmscore.json index 4db291fa3..fa2fba141 100644 --- a/src/commands/zmscore.json +++ b/src/commands/zmscore.json @@ -33,6 +33,22 @@ } } ], + "reply_schema": { + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "type": "number", + "description": "The score of the member (a double precision floating point number). In RESP2, this is returned as string." + }, + { + "type": "null", + "description": "Member does not exist in the sorted set." + } + ] + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/zpopmax.json b/src/commands/zpopmax.json index 2e792431a..33bb85c51 100644 --- a/src/commands/zpopmax.json +++ b/src/commands/zpopmax.json @@ -34,6 +34,45 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "type": "array", + "description": "List of popped elements and scores when 'COUNT' isn't specified.", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "Popped element." + }, + { + "type": "number", + "description": "Score." + } + ] + }, + { + "type": "array", + "description": "List of popped elements and scores when 'COUNT' is specified.", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "Popped element." + }, + { + "type": "number", + "description": "Score." + } + ] + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zpopmin.json b/src/commands/zpopmin.json index 9ccce2fe1..e583eeea0 100644 --- a/src/commands/zpopmin.json +++ b/src/commands/zpopmin.json @@ -34,6 +34,45 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "type": "array", + "description": "List of popped elements and scores when 'COUNT' isn't specified.", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "Popped element." + }, + { + "type": "number", + "description": "Score." + } + ] + }, + { + "type": "array", + "description": "List of popped elements and scores when 'COUNT' is specified.", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "Popped element." + }, + { + "type": "number", + "description": "Score." + } + ] + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrandmember.json b/src/commands/zrandmember.json index e602a154d..1da1ce68b 100644 --- a/src/commands/zrandmember.json +++ b/src/commands/zrandmember.json @@ -35,6 +35,44 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "type": "null", + "description": "Key does not exist." + }, + { + "type": "string", + "description": "Randomly selected element when 'COUNT' is not used." + }, + { + "type": "array", + "description": "Randomly selected elements when 'COUNT' is used.", + "items": { + "type": "string" + } + }, + { + "type": "array", + "description": "Randomly selected elements when 'COUNT' and 'WITHSCORES' modifiers are used.", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "Element." + }, + { + "type": "number", + "description": "Score." + } + ] + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrange.json b/src/commands/zrange.json index 628be8bfe..24a387160 100644 --- a/src/commands/zrange.json +++ b/src/commands/zrange.json @@ -38,6 +38,38 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "A list of member elements", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + { + "description": "Members and their scores. Returned in case `WITHSCORES` was used. In RESP2 this is returned as a flat array", + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Member", + "type": "string" + }, + { + "description": "Score", + "type": "number" + } + ] + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrangebylex.json b/src/commands/zrangebylex.json index 1f2c755c9..d9d8a3a19 100644 --- a/src/commands/zrangebylex.json +++ b/src/commands/zrangebylex.json @@ -37,6 +37,14 @@ } } ], + "reply_schema": { + "type": "array", + "description": "List of elements in the specified score range.", + "uniqueItems": true, + "items": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrangebyscore.json b/src/commands/zrangebyscore.json index 44650d32f..b603db4da 100644 --- a/src/commands/zrangebyscore.json +++ b/src/commands/zrangebyscore.json @@ -43,6 +43,40 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "type": "array", + "description": "List of the elements in the specified score range, as not WITHSCORES", + "uniqueItems": true, + "items": { + "type": "string", + "description": "Element" + } + }, + { + "type": "array", + "description": "List of the elements and their scores in the specified score range, as WITHSCORES used", + "uniqueItems": true, + "items": { + "type": "array", + "description": "Tuple of element and its score", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "element", + "type": "string" + }, + { + "description": "score", + "type": "number" + } + ] + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrangestore.json b/src/commands/zrangestore.json index 2e4c85583..f072553b7 100644 --- a/src/commands/zrangestore.json +++ b/src/commands/zrangestore.json @@ -51,6 +51,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "Number of elements in the resulting sorted set." + }, "arguments": [ { "name": "dst", diff --git a/src/commands/zrank.json b/src/commands/zrank.json index a15d96c59..d08897e27 100644 --- a/src/commands/zrank.json +++ b/src/commands/zrank.json @@ -39,6 +39,32 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "null", + "description": "Key does not exist or the member does not exist in the sorted set." + }, + { + "type": "integer", + "description": "The rank of the member when 'WITHSCORES' is not used." + }, + { + "type": "array", + "description": "The rank and score of the member when 'WITHSCORES' is used.", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "integer" + }, + { + "type": "number" + } + ] + } + ] + }, "arguments": [ { "name": "key", @@ -57,4 +83,4 @@ } ] } -} \ No newline at end of file +} diff --git a/src/commands/zrem.json b/src/commands/zrem.json index a89940093..f8fceeadb 100644 --- a/src/commands/zrem.json +++ b/src/commands/zrem.json @@ -39,6 +39,11 @@ } } ], + "reply_schema": { + "description": "The number of members removed from the sorted set, not including non existing members.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/zremrangebylex.json b/src/commands/zremrangebylex.json index ad7277723..34eb99980 100644 --- a/src/commands/zremrangebylex.json +++ b/src/commands/zremrangebylex.json @@ -32,6 +32,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "Number of elements removed." + }, "arguments": [ { "name": "key", diff --git a/src/commands/zremrangebyrank.json b/src/commands/zremrangebyrank.json index 62e5055b5..4e7744dff 100644 --- a/src/commands/zremrangebyrank.json +++ b/src/commands/zremrangebyrank.json @@ -32,6 +32,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "Number of elements removed." + }, "arguments": [ { "name": "key", diff --git a/src/commands/zremrangebyscore.json b/src/commands/zremrangebyscore.json index 3f84c3335..d5ca40c42 100644 --- a/src/commands/zremrangebyscore.json +++ b/src/commands/zremrangebyscore.json @@ -32,6 +32,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "Number of elements removed." + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrevrange.json b/src/commands/zrevrange.json index 66ddc1146..725abf8d7 100644 --- a/src/commands/zrevrange.json +++ b/src/commands/zrevrange.json @@ -37,6 +37,38 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "List of member elements.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + { + "description": "List of the members and their scores. Returned in case `WITHSCORES` was used.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "member", + "type": "string" + }, + { + "description": "score", + "type": "number" + } + ] + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrevrangebylex.json b/src/commands/zrevrangebylex.json index 07bad6178..252c96f8c 100644 --- a/src/commands/zrevrangebylex.json +++ b/src/commands/zrevrangebylex.json @@ -37,6 +37,14 @@ } } ], + "reply_schema": { + "type": "array", + "description": "List of the elements in the specified score range.", + "uniqueItems": true, + "items": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrevrangebyscore.json b/src/commands/zrevrangebyscore.json index 9e0bab096..163faec57 100644 --- a/src/commands/zrevrangebyscore.json +++ b/src/commands/zrevrangebyscore.json @@ -43,6 +43,40 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "type": "array", + "description": "List of the elements in the specified score range, as not WITHSCORES", + "uniqueItems": true, + "items": { + "type": "string", + "description": "Element" + } + }, + { + "type": "array", + "description": "List of the elements and their scores in the specified score range, as WITHSCORES used", + "uniqueItems": true, + "items": { + "type": "array", + "description": "Tuple of element and its score", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "element" + }, + { + "type": "number", + "description": "score" + } + ] + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrevrank.json b/src/commands/zrevrank.json index 7d5aa795b..0a025fe0d 100644 --- a/src/commands/zrevrank.json +++ b/src/commands/zrevrank.json @@ -39,6 +39,32 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "null", + "description": "Key does not exist or the member does not exist in the sorted set." + }, + { + "type": "integer", + "description": "The rank of the member when 'WITHSCORES' is not used." + }, + { + "type": "array", + "description": "The rank and score of the member when 'WITHSCORES' is used.", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "integer" + }, + { + "type": "number" + } + ] + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zscan.json b/src/commands/zscan.json index 71054fb52..ca4a7dd2a 100644 --- a/src/commands/zscan.json +++ b/src/commands/zscan.json @@ -57,6 +57,25 @@ "type": "integer", "optional": true } - ] + ], + "reply_schema": { + "description": "cursor and scan response in array form", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "cursor", + "type": "string" + }, + { + "description": "list of elements of the sorted set, where each even element is the member, and each odd value is its associated score", + "type": "array", + "items": { + "type": "string" + } + } + ] + } } } diff --git a/src/commands/zscore.json b/src/commands/zscore.json index 5ed357510..7e07c8248 100644 --- a/src/commands/zscore.json +++ b/src/commands/zscore.json @@ -33,6 +33,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "number", + "description": "The score of the member (a double precision floating point number). In RESP2, this is returned as string." + }, + { + "type": "null", + "description": "Member does not exist in the sorted set, or key does not exist." + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zunion.json b/src/commands/zunion.json index cc6c66c09..395a12741 100644 --- a/src/commands/zunion.json +++ b/src/commands/zunion.json @@ -33,6 +33,36 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "The result of union when 'WITHSCORES' is not used.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + { + "description": "The result of union when 'WITHSCORES' is used.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + } + ] + }, "arguments": [ { "name": "numkeys", diff --git a/src/commands/zunionstore.json b/src/commands/zunionstore.json index 257b06d0e..25273af8c 100644 --- a/src/commands/zunionstore.json +++ b/src/commands/zunionstore.json @@ -52,6 +52,10 @@ } } ], + "reply_schema": { + "description": "The number of elements in the resulting sorted set.", + "type": "integer" + }, "arguments": [ { "name": "destination", diff --git a/src/config.c b/src/config.c index 97c4028ad..bd62ca3db 100644 --- a/src/config.c +++ b/src/config.c @@ -3083,6 +3083,9 @@ standardConfig static_configs[] = { createStringConfig("proc-title-template", NULL, MODIFIABLE_CONFIG, ALLOW_EMPTY_STRING, server.proc_title_template, CONFIG_DEFAULT_PROC_TITLE_TEMPLATE, isValidProcTitleTemplate, updateProcTitleTemplate), createStringConfig("bind-source-addr", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.bind_source_addr, NULL, NULL, NULL), createStringConfig("logfile", NULL, IMMUTABLE_CONFIG, ALLOW_EMPTY_STRING, server.logfile, "", NULL, NULL), +#ifdef LOG_REQ_RES + createStringConfig("req-res-logfile", NULL, IMMUTABLE_CONFIG | HIDDEN_CONFIG, EMPTY_STRING_IS_NULL, server.req_res_logfile, NULL, NULL, NULL), +#endif createStringConfig("locale-collate", NULL, MODIFIABLE_CONFIG, ALLOW_EMPTY_STRING, server.locale_collate, "", NULL, updateLocaleCollate), /* SDS Configs */ @@ -3150,6 +3153,9 @@ standardConfig static_configs[] = { createUIntConfig("maxclients", NULL, MODIFIABLE_CONFIG, 1, UINT_MAX, server.maxclients, 10000, INTEGER_CONFIG, NULL, updateMaxclients), createUIntConfig("unixsocketperm", NULL, IMMUTABLE_CONFIG, 0, 0777, server.unixsocketperm, 0, OCTAL_CONFIG, NULL, NULL), createUIntConfig("socket-mark-id", NULL, IMMUTABLE_CONFIG, 0, UINT_MAX, server.socket_mark_id, 0, INTEGER_CONFIG, NULL, NULL), +#ifdef LOG_REQ_RES + createUIntConfig("client-default-resp", NULL, IMMUTABLE_CONFIG | HIDDEN_CONFIG, 2, 3, server.client_default_resp, 2, INTEGER_CONFIG, NULL, NULL), +#endif /* Unsigned Long configs */ createULongConfig("active-defrag-max-scan-fields", NULL, MODIFIABLE_CONFIG, 1, LONG_MAX, server.active_defrag_max_scan_fields, 1000, INTEGER_CONFIG, NULL, NULL), /* Default: keys with more than 1000 fields will be processed separately */ diff --git a/src/logreqres.c b/src/logreqres.c new file mode 100644 index 000000000..aa54b721d --- /dev/null +++ b/src/logreqres.c @@ -0,0 +1,318 @@ +/* + * Copyright (c) 2021, Redis Ltd. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/* This file implements the interface of logging clients' requests and + * responses into a file. + * This feature needs the LOG_REQ_RES macro to be compiled and is turned + * on by the req-res-logfile config." + * + * Some examples: + * + * PING: + * + * 4 + * ping + * 12 + * __argv_end__ + * +PONG + * + * LRANGE: + * + * 6 + * lrange + * 4 + * list + * 1 + * 0 + * 2 + * -1 + * 12 + * __argv_end__ + * *1 + * $3 + * ele + * + * The request is everything up until the __argv_end__ marker. + * The format is: + * + * + * + * After __argv_end__ the response appears, and the format is + * RESP (2 or 3, depending on what the client has configured) + */ + +#include "server.h" +#include + +#ifdef LOG_REQ_RES + +/* ----- Helpers ----- */ + +static int reqresShouldLog(client *c) { + if (!server.req_res_logfile) + return 0; + + /* Ignore client with streaming non-standard response */ + if (c->flags & (CLIENT_PUBSUB|CLIENT_MONITOR|CLIENT_SLAVE)) + return 0; + + /* We only work on masters (didn't implement reqresAppendResponse to work on shared slave buffers) */ + if (getClientType(c) == CLIENT_TYPE_MASTER) + return 0; + + return 1; +} + +static size_t reqresAppendBuffer(client *c, void *buf, size_t len) { + if (!c->reqres.buf) { + c->reqres.capacity = max(len, 1024); + c->reqres.buf = zmalloc(c->reqres.capacity); + } else if (c->reqres.capacity - c->reqres.used < len) { + c->reqres.capacity += len; + c->reqres.buf = zrealloc(c->reqres.buf, c->reqres.capacity); + } + + memcpy(c->reqres.buf + c->reqres.used, buf, len); + c->reqres.used += len; + return len; +} + +/* Functions for requests */ + +static size_t reqresAppendArg(client *c, char *arg, size_t arg_len) { + char argv_len_buf[LONG_STR_SIZE]; + size_t argv_len_buf_len = ll2string(argv_len_buf,sizeof(argv_len_buf),(long)arg_len); + size_t ret = reqresAppendBuffer(c, argv_len_buf, argv_len_buf_len); + ret += reqresAppendBuffer(c, "\r\n", 2); + ret += reqresAppendBuffer(c, arg, arg_len); + ret += reqresAppendBuffer(c, "\r\n", 2); + return ret; +} + +/* ----- API ----- */ + + +/* Zero out the clientReqResInfo struct inside the client, + * and free the buffer if needed */ +void reqresReset(client *c, int free_buf) { + if (free_buf && c->reqres.buf) + zfree(c->reqres.buf); + memset(&c->reqres, 0, sizeof(c->reqres)); +} + +/* Save the offset of the reply buffer (or the reply list). + * Should be called when adding a reply (but it will only save the offset + * on the very first time it's called, because of c->reqres.offset.saved) + * The idea is: + * 1. When a client is executing a command, we save the reply offset. + * 2. During the execution, the reply offset may grow, as addReply* functions are called. + * 3. When client is done with the command (commandProcessed), reqresAppendResponse + * is called. + * 4. reqresAppendResponse will append the diff between the current offset and the one from step (1) + * 5. When client is reset before the next command, we clear c->reqres.offset.saved and start again + * + * We cannot reply on c->sentlen to keep track because it depends on the network + * (reqresAppendResponse will always write the whole buffer, unlike writeToClient) + * + * Ideally, we would just have this code inside reqresAppendRequest, which is called + * from processCommand, but we cannot save the reply offset inside processCommand + * because of the following pipe-lining scenario: + * set rd [redis_deferring_client] + * set buf "" + * append buf "SET key vale\r\n" + * append buf "BLPOP mylist 0\r\n" + * $rd write $buf + * $rd flush + * + * Let's assume we save the reply offset in processCommand + * When BLPOP is processed the offset is 5 (+OK\r\n from the SET) + * Then beforeSleep is called, the +OK is written to network, and bufpos is 0 + * When the client is finally unblocked, the cached offset is 5, but bufpos is already + * 0, so we would miss the first 5 bytes of the reply. + **/ +void reqresSaveClientReplyOffset(client *c) { + if (!reqresShouldLog(c)) + return; + + if (c->reqres.offset.saved) + return; + + c->reqres.offset.saved = 1; + + c->reqres.offset.bufpos = c->bufpos; + if (listLength(c->reply) && listNodeValue(listLast(c->reply))) { + c->reqres.offset.last_node.index = listLength(c->reply) - 1; + c->reqres.offset.last_node.used = ((clientReplyBlock *)listNodeValue(listLast(c->reply)))->used; + } else { + c->reqres.offset.last_node.index = 0; + c->reqres.offset.last_node.used = 0; + } +} + +size_t reqresAppendRequest(client *c) { + robj **argv = c->argv; + int argc = c->argc; + + serverAssert(argc); + + if (!reqresShouldLog(c)) + return 0; + + /* Ignore commands that have streaming non-standard response */ + sds cmd = argv[0]->ptr; + if (!strcasecmp(cmd,"sync") || + !strcasecmp(cmd,"psync") || + !strcasecmp(cmd,"monitor") || + !strcasecmp(cmd,"subscribe") || + !strcasecmp(cmd,"unsubscribe") || + !strcasecmp(cmd,"ssubscribe") || + !strcasecmp(cmd,"sunsubscribe") || + !strcasecmp(cmd,"psubscribe") || + !strcasecmp(cmd,"punsubscribe") || + !strcasecmp(cmd,"debug") || + !strcasecmp(cmd,"pfdebug") || + !strcasecmp(cmd,"lolwut") || + (!strcasecmp(cmd,"sentinel") && argc > 1 && !strcasecmp(argv[1]->ptr,"debug"))) + { + return 0; + } + + c->reqres.argv_logged = 1; + + size_t ret = 0; + for (int i = 0; i < argc; i++) { + if (sdsEncodedObject(argv[i])) { + ret += reqresAppendArg(c, argv[i]->ptr, sdslen(argv[i]->ptr)); + } else if (argv[i]->encoding == OBJ_ENCODING_INT) { + char buf[LONG_STR_SIZE]; + size_t len = ll2string(buf,sizeof(buf),(long)argv[i]->ptr); + ret += reqresAppendArg(c, buf, len); + } else { + serverPanic("Wrong encoding in reqresAppendRequest()"); + } + } + return ret + reqresAppendArg(c, "__argv_end__", 12); +} + +size_t reqresAppendResponse(client *c) { + size_t ret = 0; + + if (!reqresShouldLog(c)) + return 0; + + if (!c->reqres.argv_logged) /* Example: UNSUBSCRIBE */ + return 0; + + if (!c->reqres.offset.saved) /* Example: module client blocked on keys + CLIENT KILL */ + return 0; + + /* First append the static reply buffer */ + if (c->bufpos > c->reqres.offset.bufpos) { + size_t written = reqresAppendBuffer(c, c->buf + c->reqres.offset.bufpos, c->bufpos - c->reqres.offset.bufpos); + ret += written; + } + + int curr_index = 0; + size_t curr_used = 0; + if (listLength(c->reply)) { + curr_index = listLength(c->reply) - 1; + curr_used = ((clientReplyBlock *)listNodeValue(listLast(c->reply)))->used; + } + + /* Now, append reply bytes from the reply list */ + if (curr_index > c->reqres.offset.last_node.index || + curr_used > c->reqres.offset.last_node.used) + { + int i = 0; + listIter iter; + listNode *curr; + clientReplyBlock *o; + listRewind(c->reply, &iter); + while ((curr = listNext(&iter)) != NULL) { + size_t written; + + /* Skip nodes we had already processed */ + if (i < c->reqres.offset.last_node.index) { + i++; + continue; + } + o = listNodeValue(curr); + if (o->used == 0) { + i++; + continue; + } + if (i == c->reqres.offset.last_node.index) { + /* Write the potentially incomplete node, which had data from + * before the current command started */ + written = reqresAppendBuffer(c, + o->buf + c->reqres.offset.last_node.used, + o->used - c->reqres.offset.last_node.used); + } else { + /* New node */ + written = reqresAppendBuffer(c, o->buf, o->used); + } + ret += written; + i++; + } + } + serverAssert(ret); + + /* Flush both request and response to file */ + FILE *fp = fopen(server.req_res_logfile, "a"); + serverAssert(fp); + fwrite(c->reqres.buf, c->reqres.used, 1, fp); + fclose(fp); + + return ret; +} + +#else /* #ifdef LOG_REQ_RES */ + +/* Just mimic the API without doing anything */ + +void reqresReset(client *c, int free_buf) { + UNUSED(c); + UNUSED(free_buf); +} + +inline void reqresSaveClientReplyOffset(client *c) { + UNUSED(c); +} + +inline size_t reqresAppendRequest(client *c) { + UNUSED(c); + return 0; +} + +inline size_t reqresAppendResponse(client *c) { + UNUSED(c); + return 0; +} + +#endif /* #ifdef LOG_REQ_RES */ diff --git a/src/networking.c b/src/networking.c index 2edb7b72a..3b4caa4af 100644 --- a/src/networking.c +++ b/src/networking.c @@ -139,7 +139,12 @@ client *createClient(connection *conn) { uint64_t client_id; atomicGetIncr(server.next_client_id, client_id, 1); c->id = client_id; +#ifdef LOG_REQ_RES + reqresReset(c, 0); + c->resp = server.client_default_resp; +#else c->resp = 2; +#endif c->conn = conn; c->name = NULL; c->bufpos = 0; @@ -390,6 +395,10 @@ void _addReplyToBufferOrList(client *c, const char *s, size_t len) { return; } + /* We call it here because this function may affect the reply + * buffer offset (see function comment) */ + reqresSaveClientReplyOffset(c); + size_t reply_len = _addReplyToBuffer(c,s,len); if (len > reply_len) _addReplyProtoToList(c,s+reply_len,len-reply_len); } @@ -714,6 +723,10 @@ void *addReplyDeferredLen(client *c) { return NULL; } + /* We call it here because this function conceptually affects the reply + * buffer offset (see function comment) */ + reqresSaveClientReplyOffset(c); + trimReplyUnusedTailSpace(c); listAddNodeTail(c->reply,NULL); /* NULL is our placeholder. */ return listLast(c->reply); @@ -1575,6 +1588,9 @@ void freeClient(client *c) { freeClientOriginalArgv(c); if (c->deferred_reply_errors) listRelease(c->deferred_reply_errors); +#ifdef LOG_REQ_RES + reqresReset(c, 1); +#endif /* Unlink the client: this will close the socket, remove the I/O * handlers, and remove references of the client from different @@ -2000,6 +2016,9 @@ void resetClient(client *c) { c->slot = -1; c->duration = 0; c->flags &= ~CLIENT_EXECUTING_COMMAND; +#ifdef LOG_REQ_RES + reqresReset(c, 1); +#endif if (c->deferred_reply_errors) listRelease(c->deferred_reply_errors); @@ -2357,6 +2376,7 @@ void commandProcessed(client *c) { * since we have not applied the command. */ if (c->flags & CLIENT_BLOCKED) return; + reqresAppendResponse(c); resetClient(c); long long prev_offset = c->reploff; diff --git a/src/replication.c b/src/replication.c index 95ce31d64..33bb8242c 100644 --- a/src/replication.c +++ b/src/replication.c @@ -3540,16 +3540,18 @@ void processClientsWaitingReplicas(void) { if (last_offset && last_offset >= c->bstate.reploffset && last_numreplicas >= c->bstate.numreplicas) { - unblockClient(c); + /* Reply before unblocking, because unblock client calls reqresAppendResponse */ addReplyLongLong(c,last_numreplicas); + unblockClient(c); } else { int numreplicas = replicationCountAcksByOffset(c->bstate.reploffset); if (numreplicas >= c->bstate.numreplicas) { last_offset = c->bstate.reploffset; last_numreplicas = numreplicas; - unblockClient(c); + /* Reply before unblocking, because unblock client calls reqresAppendResponse */ addReplyLongLong(c,numreplicas); + unblockClient(c); } } } diff --git a/src/sentinel.c b/src/sentinel.c index 54e1eff82..035776781 100644 --- a/src/sentinel.c +++ b/src/sentinel.c @@ -2776,7 +2776,9 @@ void sentinelInfoReplyCallback(redisAsyncContext *c, void *reply, void *privdata link->pending_commands--; r = reply; - if (r->type == REDIS_REPLY_STRING) + /* INFO reply type is verbatim in resp3. Normally, sentinel will not use + * resp3 but this is required for testing (see logreqres.c). */ + if (r->type == REDIS_REPLY_STRING || r->type == REDIS_REPLY_VERB) sentinelRefreshInstanceInfo(ri,r->str); } @@ -2987,8 +2989,10 @@ void sentinelReceiveHelloMessages(redisAsyncContext *c, void *reply, void *privd ri->link->pc_last_activity = mstime(); /* Sanity check in the reply we expect, so that the code that follows - * can avoid to check for details. */ - if (r->type != REDIS_REPLY_ARRAY || + * can avoid to check for details. + * Note: Reply type is PUSH in resp3. Normally, sentinel will not use + * resp3 but this is required for testing (see logreqres.c). */ + if ((r->type != REDIS_REPLY_ARRAY && r->type != REDIS_REPLY_PUSH) || r->elements != 3 || r->element[0]->type != REDIS_REPLY_STRING || r->element[1]->type != REDIS_REPLY_STRING || diff --git a/src/server.c b/src/server.c index 60522acb0..b42f22616 100644 --- a/src/server.c +++ b/src/server.c @@ -3782,6 +3782,9 @@ int processCommand(client *c) { * this is a reprocessing of this command, so we do not want to perform some of the actions again. */ int client_reprocessing_command = c->cmd ? 1 : 0; + if (!client_reprocessing_command) + reqresAppendRequest(c); + /* Handle possible security attacks. */ if (!strcasecmp(c->argv[0]->ptr,"host:") || !strcasecmp(c->argv[0]->ptr,"post")) { securityWarningCommand(c); @@ -4641,30 +4644,41 @@ void addReplyCommandArgList(client *c, struct redisCommandArg *args, int num_arg } } -/* Must match redisCommandRESP2Type */ -const char *RESP2_TYPE_STR[] = { - "simple-string", - "error", - "integer", - "bulk-string", - "null-bulk-string", - "array", - "null-array", -}; +#ifdef LOG_REQ_RES -/* Must match redisCommandRESP3Type */ -const char *RESP3_TYPE_STR[] = { - "simple-string", - "error", - "integer", - "double", - "bulk-string", - "array", - "map", - "set", - "bool", - "null", -}; +void addReplyJson(client *c, struct jsonObject *rs) { + addReplyMapLen(c, rs->length); + + for (int i = 0; i < rs->length; i++) { + struct jsonObjectElement *curr = &rs->elements[i]; + addReplyBulkCString(c, curr->key); + switch (curr->type) { + case (JSON_TYPE_BOOLEAN): + addReplyBool(c, curr->value.boolean); + break; + case (JSON_TYPE_INTEGER): + addReplyLongLong(c, curr->value.integer); + break; + case (JSON_TYPE_STRING): + addReplyBulkCString(c, curr->value.string); + break; + case (JSON_TYPE_OBJECT): + addReplyJson(c, curr->value.object); + break; + case (JSON_TYPE_ARRAY): + addReplyArrayLen(c, curr->value.array.length); + for (int k = 0; k < curr->value.array.length; k++) { + struct jsonObject *object = curr->value.array.objects[k]; + addReplyJson(c, object); + } + break; + default: + serverPanic("Invalid JSON type %d", curr->type); + } + } +} + +#endif void addReplyCommandHistory(client *c, struct redisCommand *cmd) { addReplySetLen(c, cmd->num_history); @@ -4862,6 +4876,9 @@ void addReplyCommandDocs(client *c, struct redisCommand *cmd) { if (cmd->deprecated_since) maplen++; if (cmd->replaced_by) maplen++; if (cmd->history) maplen++; +#ifdef LOG_REQ_RES + if (cmd->reply_schema) maplen++; +#endif if (cmd->args) maplen++; if (cmd->subcommands_dict) maplen++; addReplyMapLen(c, maplen); @@ -4903,6 +4920,12 @@ void addReplyCommandDocs(client *c, struct redisCommand *cmd) { addReplyBulkCString(c, "history"); addReplyCommandHistory(c, cmd); } +#ifdef LOG_REQ_RES + if (cmd->reply_schema) { + addReplyBulkCString(c, "reply_schema"); + addReplyJson(c, cmd->reply_schema); + } +#endif if (cmd->args) { addReplyBulkCString(c, "arguments"); addReplyCommandArgList(c, cmd->args, cmd->num_args); diff --git a/src/server.h b/src/server.h index 36cd8c760..056123d0f 100644 --- a/src/server.h +++ b/src/server.h @@ -1101,6 +1101,31 @@ typedef struct { size_t mem_usage_sum; } clientMemUsageBucket; +#ifdef LOG_REQ_RES +/* Structure used to log client's requests and their + * responses (see logreqres.c) */ +typedef struct { + /* General */ + int argv_logged; /* 1 if the command was logged */ + /* Vars for log buffer */ + unsigned char *buf; /* Buffer holding the data (request and response) */ + size_t used; + size_t capacity; + /* Vars for offsets within the client's reply */ + struct { + /* General */ + int saved; /* 1 if we already saved the offset (first time we call addReply*) */ + /* Offset within the static reply buffer */ + int bufpos; + /* Offset within the reply block list */ + struct { + int index; + size_t used; + } last_node; + } offset; +} clientReqResInfo; +#endif + typedef struct client { uint64_t id; /* Client incremental unique ID. */ uint64_t flags; /* Client flags: CLIENT_* macros. */ @@ -1212,6 +1237,9 @@ typedef struct client { int bufpos; size_t buf_usable_size; /* Usable size of buffer. */ char *buf; +#ifdef LOG_REQ_RES + clientReqResInfo reqres; +#endif } client; /* ACL information */ @@ -1540,6 +1568,11 @@ struct redisServer { client *current_client; /* The client that triggered the command execution (External or AOF). */ client *executing_client; /* The client executing the current command (possibly script or module). */ +#ifdef LOG_REQ_RES + char *req_res_logfile; /* Path of log file for logging all requests and their replies. If NULL, no logging will be performed */ + unsigned int client_default_resp; +#endif + /* Stuff for client mem eviction */ clientMemUsageBucket* client_mem_usage_buckets; @@ -2106,30 +2139,38 @@ typedef struct redisCommandArg { int num_args; } redisCommandArg; -/* Must be synced with RESP2_TYPE_STR and generate-command-code.py */ -typedef enum { - RESP2_SIMPLE_STRING, - RESP2_ERROR, - RESP2_INTEGER, - RESP2_BULK_STRING, - RESP2_NULL_BULK_STRING, - RESP2_ARRAY, - RESP2_NULL_ARRAY, -} redisCommandRESP2Type; +#ifdef LOG_REQ_RES -/* Must be synced with RESP3_TYPE_STR and generate-command-code.py */ +/* Must be synced with generate-command-code.py */ typedef enum { - RESP3_SIMPLE_STRING, - RESP3_ERROR, - RESP3_INTEGER, - RESP3_DOUBLE, - RESP3_BULK_STRING, - RESP3_ARRAY, - RESP3_MAP, - RESP3_SET, - RESP3_BOOL, - RESP3_NULL, -} redisCommandRESP3Type; + JSON_TYPE_STRING, + JSON_TYPE_INTEGER, + JSON_TYPE_BOOLEAN, + JSON_TYPE_OBJECT, + JSON_TYPE_ARRAY, +} jsonType; + +typedef struct jsonObjectElement { + jsonType type; + const char *key; + union { + const char *string; + long long integer; + int boolean; + struct jsonObject *object; + struct { + struct jsonObject **objects; + int length; + } array; + } value; +} jsonObjectElement; + +typedef struct jsonObject { + struct jsonObjectElement *elements; + int length; +} jsonObject; + +#endif /* WARNING! This struct must match RedisModuleCommandHistoryEntry */ typedef struct { @@ -2280,6 +2321,10 @@ struct redisCommand { struct redisCommand *subcommands; /* Array of arguments (may be NULL) */ struct redisCommandArg *args; +#ifdef LOG_REQ_RES + /* Reply schema */ + struct jsonObject *reply_schema; +#endif /* Runtime populated data */ long long microseconds, calls, rejected_calls, failed_calls; @@ -2587,6 +2632,12 @@ client *lookupClientByID(uint64_t id); int authRequired(client *c); void putClientInPendingWriteQueue(client *c); +/* logreqres.c - logging of requests and responses */ +void reqresReset(client *c, int free_buf); +void reqresSaveClientReplyOffset(client *c); +size_t reqresAppendRequest(client *c); +size_t reqresAppendResponse(client *c); + #ifdef __GNUC__ void addReplyErrorFormatEx(client *c, int flags, const char *fmt, ...) __attribute__((format(printf, 3, 4))); diff --git a/tests/cluster/tests/00-base.tcl b/tests/cluster/tests/00-base.tcl index 08ecd5e4a..0126303ae 100644 --- a/tests/cluster/tests/00-base.tcl +++ b/tests/cluster/tests/00-base.tcl @@ -74,3 +74,11 @@ test "CLUSTER RESET SOFT test" { R 1 CLUSTER RESET SOFT assert {[get_info_field [R 1 cluster info] cluster_current_epoch] eq $last_epoch_node1} } + +test "Coverage: CLUSTER HELP" { + assert_match "*CLUSTER *" [R 0 CLUSTER HELP] +} + +test "Coverage: ASKING" { + assert_equal {OK} [R 0 ASKING] +} diff --git a/tests/cluster/tests/16-transactions-on-replica.tcl b/tests/cluster/tests/16-transactions-on-replica.tcl index ec5699c98..8bec06ee4 100644 --- a/tests/cluster/tests/16-transactions-on-replica.tcl +++ b/tests/cluster/tests/16-transactions-on-replica.tcl @@ -20,6 +20,12 @@ test "Can't read from replica without READONLY" { assert {[string range $err 0 4] eq {MOVED}} } +test "Can't read from replica after READWRITE" { + $replica READWRITE + catch {$replica GET a} err + assert {[string range $err 0 4] eq {MOVED}} +} + test "Can read from replica after READONLY" { $replica READONLY assert {[$replica GET a] eq {1}} diff --git a/tests/instances.tcl b/tests/instances.tcl index 4e4091c31..56a51a872 100644 --- a/tests/instances.tcl +++ b/tests/instances.tcl @@ -105,6 +105,15 @@ proc spawn_instance {type base_port count {conf {}} {base_conf_file ""}} { } else { puts $cfg "port $port" } + + if {$::log_req_res} { + puts $cfg "req-res-logfile stdout.reqres" + } + + if {$::force_resp3} { + puts $cfg "client-default-resp 3" + } + puts $cfg "repl-diskless-sync-delay 0" puts $cfg "dir ./$dirname" puts $cfg "logfile log.txt" @@ -293,6 +302,10 @@ proc parse_options {} { set ::stop_on_failure 1 } elseif {$opt eq {--loop}} { set ::loop 1 + } elseif {$opt eq {--log-req-res}} { + set ::log_req_res 1 + } elseif {$opt eq {--force-resp3}} { + set ::force_resp3 1 } elseif {$opt eq "--help"} { puts "--single Only runs tests specified by pattern." puts "--dont-clean Keep log files on exit." diff --git a/tests/integration/corrupt-dump.tcl b/tests/integration/corrupt-dump.tcl index 35dca23be..3c9e5ce81 100644 --- a/tests/integration/corrupt-dump.tcl +++ b/tests/integration/corrupt-dump.tcl @@ -827,7 +827,7 @@ test {corrupt payload: fuzzer findings - set with duplicate elements causes sdif assert_equal {0 2 4 6 8 _1 _3 _3 _5 _9} [lsort [r smembers _key]] assert_equal {0 2 4 6 8 _1 _3 _5 _9} [lsort [r sdiff _key]] } -} +} {} {logreqres:skip} ;# This test violates {"uniqueItems": true} } ;# tags diff --git a/tests/integration/rdb.tcl b/tests/integration/rdb.tcl index 1479b500f..2362ef079 100644 --- a/tests/integration/rdb.tcl +++ b/tests/integration/rdb.tcl @@ -218,6 +218,7 @@ start_server {} { test {Test RDB load info} { r debug populate 1000 r save + assert {[r lastsave] <= [lindex [r time] 0]} restart_server 0 true false wait_done_loading r assert {[s rdb_last_load_keys_expired] == 0} diff --git a/tests/integration/redis-benchmark.tcl b/tests/integration/redis-benchmark.tcl index 5e8555b1b..8035632c7 100644 --- a/tests/integration/redis-benchmark.tcl +++ b/tests/integration/redis-benchmark.tcl @@ -25,7 +25,7 @@ proc default_set_get_checks {} { assert_match {} [cmdstat lrange] } -start_server {tags {"benchmark network external:skip"}} { +start_server {tags {"benchmark network external:skip logreqres:skip"}} { start_server {} { set master_host [srv 0 host] set master_port [srv 0 port] diff --git a/tests/modules/blockonkeys.c b/tests/modules/blockonkeys.c index 3011e4170..8f4353a55 100644 --- a/tests/modules/blockonkeys.c +++ b/tests/modules/blockonkeys.c @@ -89,6 +89,7 @@ int get_fsl(RedisModuleCtx *ctx, RedisModuleString *keyname, int mode, int creat create = 0; /* No need to create, key exists in its basic state */ } else { RedisModule_DeleteKey(key); + *fsl = NULL; } } else { /* Key exists, and has elements in it - no need to create anything */ diff --git a/tests/sentinel/tests/07-down-conditions.tcl b/tests/sentinel/tests/07-down-conditions.tcl index bb24d6dff..403f81e73 100644 --- a/tests/sentinel/tests/07-down-conditions.tcl +++ b/tests/sentinel/tests/07-down-conditions.tcl @@ -72,6 +72,7 @@ test "SDOWN is triggered by masters advertising as slaves" { ensure_master_up } +if {!$::log_req_res} { # this test changes 'dir' config to '/' and logreqres.c cannot open protocol dump file under the root directory. test "SDOWN is triggered by misconfigured instance replying with errors" { ensure_master_up set orig_dir [lindex [R 0 config get dir] 1] @@ -90,6 +91,7 @@ test "SDOWN is triggered by misconfigured instance replying with errors" { R 0 bgsave ensure_master_up } +} # We use this test setup to also test command renaming, as a side # effect of the master going down if we send PONG instead of PING diff --git a/tests/support/redis.tcl b/tests/support/redis.tcl index 861e8bc27..53fa9fe91 100644 --- a/tests/support/redis.tcl +++ b/tests/support/redis.tcl @@ -28,6 +28,8 @@ package require Tcl 8.5 package provide redis 0.1 +source [file join [file dirname [info script]] "response_transformers.tcl"] + namespace eval redis {} set ::redis::id 0 array set ::redis::fd {} @@ -41,6 +43,11 @@ array set ::redis::tls {} array set ::redis::callback {} array set ::redis::state {} ;# State in non-blocking reply reading array set ::redis::statestack {} ;# Stack of states, for nested mbulks +array set ::redis::curr_argv {} ;# Remember the current argv, to be used in response_transformers.tcl +array set ::redis::testing_resp3 {} ;# Indicating if the current client is using RESP3 (only if the test is trying to test RESP3 specific behavior. It won't be on in case of force_resp3) + +set ::force_resp3 0 +set ::log_req_res 0 proc redis {{server 127.0.0.1} {port 6379} {defer 0} {tls 0} {tlsoptions {}} {readraw 0}} { if {$tls} { @@ -62,6 +69,8 @@ proc redis {{server 127.0.0.1} {port 6379} {defer 0} {tls 0} {tlsoptions {}} {re set ::redis::deferred($id) $defer set ::redis::readraw($id) $readraw set ::redis::reconnect($id) 0 + set ::redis::curr_argv($id) 0 + set ::redis::testing_resp3($id) 0 set ::redis::tls($id) $tls ::redis::redis_reset_state $id interp alias {} ::redis::redisHandle$id {} ::redis::__dispatch__ $id @@ -123,6 +132,20 @@ proc ::redis::__dispatch__raw__ {id method argv} { set fd $::redis::fd($id) } + # Transform HELLO 2 to HELLO 3 if force_resp3 + # All set the connection var testing_resp3 in case of HELLO 3 + if {[llength $argv] > 0 && [string compare -nocase $method "HELLO"] == 0} { + if {[lindex $argv 0] == 3} { + set ::redis::testing_resp3($id) 1 + } else { + set ::redis::testing_resp3($id) 0 + if {$::force_resp3} { + # If we are in force_resp3 we run HELLO 3 instead of HELLO 2 + lset argv 0 3 + } + } + } + set blocking $::redis::blocking($id) set deferred $::redis::deferred($id) if {$blocking == 0} { @@ -146,6 +169,7 @@ proc ::redis::__dispatch__raw__ {id method argv} { return -code error "I/O error reading reply" } + set ::redis::curr_argv($id) [concat $method $argv] if {!$deferred} { if {$blocking} { ::redis::redis_read_reply $id $fd @@ -200,6 +224,8 @@ proc ::redis::__method__close {id fd} { catch {unset ::redis::state($id)} catch {unset ::redis::statestack($id)} catch {unset ::redis::callback($id)} + catch {unset ::redis::curr_argv($id)} + catch {unset ::redis::testing_resp3($id)} catch {interp alias {} ::redis::redisHandle$id {}} } @@ -253,7 +279,7 @@ proc ::redis::redis_multi_bulk_read {id fd} { set err {} for {set i 0} {$i < $count} {incr i} { if {[catch { - lappend l [redis_read_reply $id $fd] + lappend l [redis_read_reply_logic $id $fd] } e] && $err eq {}} { set err $e } @@ -269,8 +295,8 @@ proc ::redis::redis_read_map {id fd} { set err {} for {set i 0} {$i < $count} {incr i} { if {[catch { - set k [redis_read_reply $id $fd] ; # key - set v [redis_read_reply $id $fd] ; # value + set k [redis_read_reply_logic $id $fd] ; # key + set v [redis_read_reply_logic $id $fd] ; # value dict set d $k $v } e] && $err eq {}} { set err $e @@ -296,13 +322,25 @@ proc ::redis::redis_read_bool fd { return -code error "Bad protocol, '$v' as bool type" } +proc ::redis::redis_read_double {id fd} { + set v [redis_read_line $fd] + # unlike many other DTs, there is a textual difference between double and a string with the same value, + # so we need to transform to double if we are testing RESP3 (i.e. some tests check that a + # double reply is "1.0" and not "1") + if {[should_transform_to_resp2 $id]} { + return $v + } else { + return [expr {double($v)}] + } +} + proc ::redis::redis_read_verbatim_str fd { set v [redis_bulk_read $fd] # strip the first 4 chars ("txt:") return [string range $v 4 end] } -proc ::redis::redis_read_reply {id fd} { +proc ::redis::redis_read_reply_logic {id fd} { if {$::redis::readraw($id)} { return [redis_read_line $fd] } @@ -314,7 +352,7 @@ proc ::redis::redis_read_reply {id fd} { : - ( - + {return [redis_read_line $fd]} - , {return [expr {double([redis_read_line $fd])}]} + , {return [redis_read_double $id $fd]} # {return [redis_read_bool $fd]} = {return [redis_read_verbatim_str $fd]} - {return -code error [redis_read_line $fd]} @@ -340,6 +378,11 @@ proc ::redis::redis_read_reply {id fd} { } } +proc ::redis::redis_read_reply {id fd} { + set response [redis_read_reply_logic $id $fd] + ::response_transformers::transform_response_if_needed $id $::redis::curr_argv($id) $response +} + proc ::redis::redis_reset_state id { set ::redis::state($id) [dict create buf {} mbulk -1 bulk -1 reply {}] set ::redis::statestack($id) {} @@ -416,3 +459,8 @@ proc ::redis::redis_readable {fd id} { } } } + +# when forcing resp3 some tests that rely on resp2 can fail, so we have to translate the resp3 response to resp2 +proc ::redis::should_transform_to_resp2 {id} { + return [expr {$::force_resp3 && !$::redis::testing_resp3($id)}] +} diff --git a/tests/support/response_transformers.tcl b/tests/support/response_transformers.tcl new file mode 100644 index 000000000..45b3cf8f2 --- /dev/null +++ b/tests/support/response_transformers.tcl @@ -0,0 +1,105 @@ +# Tcl client library - used by the Redis test +# Copyright (C) 2009-2023 Redis Ltd. +# Released under the BSD license like Redis itself +# +# This file contains a bunch of commands whose purpose is to transform +# a RESP3 response to RESP2 +# Why is it needed? +# When writing the reply_schema part in COMMAND DOCS we decided to use +# the existing tests in order to verify the schemas (see logreqres.c) +# The problem was that many tests were relying on the RESP2 structure +# of the response (e.g. HRANDFIELD WITHVALUES in RESP2: {f1 v1 f2 v2} +# vs. RESP3: {{f1 v1} {f2 v2}}). +# Instead of adjusting the tests to expect RESP3 responses (a lot of +# changes in many files) we decided to transform the response to RESP2 +# when running with --force-resp3 + +package require Tcl 8.5 + +namespace eval response_transformers {} + +# Transform a map response into an array of tuples (tuple = array with 2 elements) +# Used for XREAD[GROUP] +proc transfrom_map_to_tupple_array {argv response} { + set tuparray {} + foreach {key val} $response { + set tmp {} + lappend tmp $key + lappend tmp $val + lappend tuparray $tmp + } + return $tuparray +} + +# Transform an array of tuples to a flat array +proc transfrom_tuple_array_to_flat_array {argv response} { + set flatarray {} + foreach pair $response { + lappend flatarray {*}$pair + } + return $flatarray +} + +# With HRANDFIELD, we only need to transform the response if the request had WITHVALUES +# (otherwise the returned response is a flat array in both RESPs) +proc transfrom_hrandfield_command {argv response} { + foreach ele $argv { + if {[string compare -nocase $ele "WITHVALUES"] == 0} { + return [transfrom_tuple_array_to_flat_array $argv $response] + } + } + return $response +} + +# With some zset commands, we only need to transform the response if the request had WITHSCORES +# (otherwise the returned response is a flat array in both RESPs) +proc transfrom_zset_withscores_command {argv response} { + foreach ele $argv { + if {[string compare -nocase $ele "WITHSCORES"] == 0} { + return [transfrom_tuple_array_to_flat_array $argv $response] + } + } + return $response +} + +# With ZPOPMIN/ZPOPMAX, we only need to transform the response if the request had COUNT (3rd arg) +# (otherwise the returned response is a flat array in both RESPs) +proc transfrom_zpopmin_zpopmax {argv response} { + if {[llength $argv] == 3} { + return [transfrom_tuple_array_to_flat_array $argv $response] + } + return $response +} + +set ::trasformer_funcs { + XREAD transfrom_map_to_tupple_array + XREADGROUP transfrom_map_to_tupple_array + HRANDFIELD transfrom_hrandfield_command + ZRANDMEMBER transfrom_zset_withscores_command + ZRANGE transfrom_zset_withscores_command + ZRANGEBYSCORE transfrom_zset_withscores_command + ZRANGEBYLEX transfrom_zset_withscores_command + ZREVRANGE transfrom_zset_withscores_command + ZREVRANGEBYSCORE transfrom_zset_withscores_command + ZREVRANGEBYLEX transfrom_zset_withscores_command + ZUNION transfrom_zset_withscores_command + ZDIFF transfrom_zset_withscores_command + ZINTER transfrom_zset_withscores_command + ZPOPMIN transfrom_zpopmin_zpopmax + ZPOPMAX transfrom_zpopmin_zpopmax +} + +proc ::response_transformers::transform_response_if_needed {id argv response} { + if {![::redis::should_transform_to_resp2 $id] || $::redis::readraw($id)} { + return $response + } + + set key [string toupper [lindex $argv 0]] + if {![dict exists $::trasformer_funcs $key]} { + return $response + } + + set transform [dict get $::trasformer_funcs $key] + + return [$transform $argv $response] +} diff --git a/tests/support/server.tcl b/tests/support/server.tcl index a23224bd7..4c596290d 100644 --- a/tests/support/server.tcl +++ b/tests/support/server.tcl @@ -207,6 +207,12 @@ proc tags_acceptable {tags err_return} { } } + # some units mess with the client output buffer so we can't really use the req-res logging mechanism. + if {$::log_req_res && [lsearch $tags "logreqres:skip"] >= 0} { + set err "Not supported when running in log-req-res mode" + return 0 + } + if {$::external && [lsearch $tags "external:skip"] >= 0} { set err "Not supported on external server" return 0 @@ -511,6 +517,14 @@ proc start_server {options {code undefined}} { dict unset config $directive } + if {$::log_req_res} { + dict set config "req-res-logfile" "stdout.reqres" + } + + if {$::force_resp3} { + dict set config "client-default-resp" "3" + } + # write new configuration to temporary file set config_file [tmpfile redis.conf] create_server_config_file $config_file $config $config_lines diff --git a/tests/test_helper.tcl b/tests/test_helper.tcl index 3a612c8c2..807995593 100644 --- a/tests/test_helper.tcl +++ b/tests/test_helper.tcl @@ -100,6 +100,7 @@ set ::all_tests { unit/cluster/hostnames unit/cluster/multi-slot-operations unit/cluster/slot-ownership + unit/cluster/links } # Index to the next test to run in the ::all_tests list. set ::next_test 0 @@ -134,6 +135,7 @@ set ::timeout 1200; # 20 minutes without progresses will quit the test. set ::last_progress [clock seconds] set ::active_servers {} ; # Pids of active Redis instances. set ::dont_clean 0 +set ::dont_pre_clean 0 set ::wait_server 0 set ::stop_on_failure 0 set ::dump_logs 0 @@ -144,6 +146,8 @@ set ::cluster_mode 0 set ::ignoreencoding 0 set ::ignoredigest 0 set ::large_memory 0 +set ::log_req_res 0 +set ::force_resp3 0 # Set to 1 when we are running in client mode. The Redis test uses a # server-client model to run tests simultaneously. The server instance @@ -319,7 +323,7 @@ proc cleanup {} { } proc test_server_main {} { - cleanup + if {!$::dont_pre_clean} cleanup set tclsh [info nameofexecutable] # Open a listening socket, trying different ports in order to find a # non busy one. @@ -650,6 +654,10 @@ for {set j 0} {$j < [llength $argv]} {incr j} { lappend ::global_overrides $arg lappend ::global_overrides $arg2 incr j 2 + } elseif {$opt eq {--log-req-res}} { + set ::log_req_res 1 + } elseif {$opt eq {--force-resp3}} { + set ::force_resp3 1 } elseif {$opt eq {--skipfile}} { incr j set fp [open $arg r] @@ -724,6 +732,8 @@ for {set j 0} {$j < [llength $argv]} {incr j} { set ::durable 1 } elseif {$opt eq {--dont-clean}} { set ::dont_clean 1 + } elseif {$opt eq {--dont-pre-clean}} { + set ::dont_pre_clean 1 } elseif {$opt eq {--no-latency}} { set ::no_latency 1 } elseif {$opt eq {--wait-server}} { diff --git a/tests/unit/acl.tcl b/tests/unit/acl.tcl index 59626caaf..13eea86de 100644 --- a/tests/unit/acl.tcl +++ b/tests/unit/acl.tcl @@ -7,6 +7,10 @@ start_server {tags {"acl external:skip"}} { r ACL setuser newuser } + test {Coverage: ACL USERS} { + r ACL USERS + } {default newuser} + test {Usernames can not contain spaces or null characters} { catch {r ACL setuser "a a"} err set err diff --git a/tests/unit/aofrw.tcl b/tests/unit/aofrw.tcl index 00fc9e3bd..fe07351a3 100644 --- a/tests/unit/aofrw.tcl +++ b/tests/unit/aofrw.tcl @@ -1,4 +1,6 @@ -start_server {tags {"aofrw external:skip"}} { +# This unit has the potential to create huge .reqres files, causing log-req-res-validator.py to run for a very long time... +# Since this unit doesn't do anything worth validating, reply_schema-wise, we decided to skip it +start_server {tags {"aofrw external:skip logreqres:skip"}} { # Enable the AOF r config set appendonly yes r config set auto-aof-rewrite-percentage 0 ; # Disable auto-rewrite. diff --git a/tests/unit/client-eviction.tcl b/tests/unit/client-eviction.tcl index db6a22499..76f7bf0f2 100644 --- a/tests/unit/client-eviction.tcl +++ b/tests/unit/client-eviction.tcl @@ -1,4 +1,4 @@ -tags {"external:skip"} { +tags {"external:skip logreqres:skip"} { # Get info about a redis client connection: # name - name of client we want to query diff --git a/tests/unit/cluster/misc.tcl b/tests/unit/cluster/misc.tcl index 35308b81a..3ff21010a 100644 --- a/tests/unit/cluster/misc.tcl +++ b/tests/unit/cluster/misc.tcl @@ -12,5 +12,16 @@ start_cluster 2 2 {tags {external:skip cluster}} { assert_error {ASK*} {R 0 GET FOO} R 0 ping } {PONG} + + test "Coverage: Basic cluster commands" { + assert_equal {OK} [R 0 CLUSTER saveconfig] + + set id [R 0 CLUSTER MYID] + assert_equal {0} [R 0 CLUSTER count-failure-reports $id] + assert_match "*shard-id*" [R 0 CLUSTER slaves $id] + + R 0 flushall + assert_equal {OK} [R 0 CLUSTER flushslots] + } } diff --git a/tests/unit/geo.tcl b/tests/unit/geo.tcl index e07a6784c..85c9485e4 100644 --- a/tests/unit/geo.tcl +++ b/tests/unit/geo.tcl @@ -222,6 +222,10 @@ start_server {tags {"geo"}} { r georadius nyc -73.9798091 40.7598464 3 km asc } {{central park n/q/r} 4545 {union square}} + test {GEORADIUS_RO simple (sorted)} { + r georadius_ro nyc -73.9798091 40.7598464 3 km asc + } {{central park n/q/r} 4545 {union square}} + test {GEOSEARCH simple (sorted)} { r geosearch nyc fromlonlat -73.9798091 40.7598464 bybox 6 6 km asc } {{central park n/q/r} 4545 {union square} {lic market}} @@ -263,6 +267,12 @@ start_server {tags {"geo"}} { r georadius nyc -73.9798091 40.7598464 10 km COUNT 3 } {{central park n/q/r} 4545 {union square}} + test {GEORADIUS with multiple WITH* tokens} { + assert_match {{{central park n/q/r} 1791875761332224 {-73.97334* 40.76480*}} {4545 1791875796750882 {-73.95641* 40.74809*}}} [r georadius nyc -73.9798091 40.7598464 10 km WITHCOORD WITHHASH COUNT 2] + assert_match {{{central park n/q/r} 1791875761332224 {-73.97334* 40.76480*}} {4545 1791875796750882 {-73.95641* 40.74809*}}} [r georadius nyc -73.9798091 40.7598464 10 km WITHHASH WITHCOORD COUNT 2] + assert_match {{{central park n/q/r} 0.7750 1791875761332224 {-73.97334* 40.76480*}} {4545 2.3651 1791875796750882 {-73.95641* 40.74809*}}} [r georadius nyc -73.9798091 40.7598464 10 km WITHDIST WITHHASH WITHCOORD COUNT 2] + } + test {GEORADIUS with ANY not sorted by default} { r georadius nyc -73.9798091 40.7598464 10 km COUNT 3 ANY } {{wtc one} {union square} {central park n/q/r}} @@ -293,6 +303,10 @@ start_server {tags {"geo"}} { test {GEORADIUSBYMEMBER simple (sorted)} { r georadiusbymember nyc "wtc one" 7 km } {{wtc one} {union square} {central park n/q/r} 4545 {lic market}} + + test {GEORADIUSBYMEMBER_RO simple (sorted)} { + r georadiusbymember_ro nyc "wtc one" 7 km + } {{wtc one} {union square} {central park n/q/r} 4545 {lic market}} test {GEORADIUSBYMEMBER search areas contain satisfied points in oblique direction} { r del k1 diff --git a/tests/unit/introspection.tcl b/tests/unit/introspection.tcl index 4617334e7..097074047 100644 --- a/tests/unit/introspection.tcl +++ b/tests/unit/introspection.tcl @@ -7,7 +7,7 @@ start_server {tags {"introspection"}} { test {CLIENT LIST} { r client list - } {id=* addr=*:* laddr=*:* fd=* name=* age=* idle=* flags=N db=* sub=0 psub=0 ssub=0 multi=-1 qbuf=26 qbuf-free=* argv-mem=* multi-mem=0 rbs=* rbp=* obl=0 oll=0 omem=0 tot-mem=* events=r cmd=client|list user=* redir=-1 resp=2*} + } {id=* addr=*:* laddr=*:* fd=* name=* age=* idle=* flags=N db=* sub=0 psub=0 ssub=0 multi=-1 qbuf=26 qbuf-free=* argv-mem=* multi-mem=0 rbs=* rbp=* obl=0 oll=0 omem=0 tot-mem=* events=r cmd=client|list user=* redir=-1 resp=*} test {CLIENT LIST with IDs} { set myid [r client id] @@ -17,7 +17,7 @@ start_server {tags {"introspection"}} { test {CLIENT INFO} { r client info - } {id=* addr=*:* laddr=*:* fd=* name=* age=* idle=* flags=N db=* sub=0 psub=0 ssub=0 multi=-1 qbuf=26 qbuf-free=* argv-mem=* multi-mem=0 rbs=* rbp=* obl=0 oll=0 omem=0 tot-mem=* events=r cmd=client|info user=* redir=-1 resp=2*} + } {id=* addr=*:* laddr=*:* fd=* name=* age=* idle=* flags=N db=* sub=0 psub=0 ssub=0 multi=-1 qbuf=26 qbuf-free=* argv-mem=* multi-mem=0 rbs=* rbp=* obl=0 oll=0 omem=0 tot-mem=* events=r cmd=client|info user=* redir=-1 resp=*} test {CLIENT KILL with illegal arguments} { assert_error "ERR wrong number of arguments for 'client|kill' command" {r client kill} @@ -138,14 +138,22 @@ start_server {tags {"introspection"}} { r migrate [srv 0 host] [srv 0 port] key 9 5000 AUTH2 user password catch {r auth not-real} _ catch {r auth not-real not-a-password} _ - catch {r hello 2 AUTH not-real not-a-password} _ - + assert_match {*"key"*"9"*"5000"*} [$rd read] assert_match {*"key"*"9"*"5000"*"(redacted)"*} [$rd read] assert_match {*"key"*"9"*"5000"*"(redacted)"*"(redacted)"*} [$rd read] assert_match {*"auth"*"(redacted)"*} [$rd read] assert_match {*"auth"*"(redacted)"*"(redacted)"*} [$rd read] - assert_match {*"hello"*"2"*"AUTH"*"(redacted)"*"(redacted)"*} [$rd read] + + foreach resp {3 2} { + if {[lsearch $::denytags "resp3"] >= 0} { + if {$resp == 3} {continue} + } elseif {$::force_resp3} { + if {$resp == 2} {continue} + } + catch {r hello $resp AUTH not-real not-a-password} _ + assert_match "*\"hello\"*\"$resp\"*\"AUTH\"*\"(redacted)\"*\"(redacted)\"*" [$rd read] + } $rd close } {0} {needs:repl} @@ -225,6 +233,27 @@ start_server {tags {"introspection"}} { r client list } {*name= *} + test {Coverage: Basic CLIENT CACHING} { + set rd_redirection [redis_deferring_client] + $rd_redirection client id + set redir_id [$rd_redirection read] + r CLIENT TRACKING on OPTIN REDIRECT $redir_id + r CLIENT CACHING yes + r CLIENT TRACKING off + } {OK} + + test {Coverage: Basic CLIENT REPLY} { + r CLIENT REPLY on + } {OK} + + test {Coverage: Basic CLIENT TRACKINGINFO} { + r CLIENT TRACKINGINFO + } {flags off redirect -1 prefixes {}} + + test {Coverage: Basic CLIENT GETREDIR} { + r CLIENT GETREDIR + } {-1} + test {CLIENT SETNAME does not accept spaces} { catch {r client setname "foo bar"} e set e @@ -325,6 +354,8 @@ start_server {tags {"introspection"}} { logfile dir socket-mark-id + req-res-logfile + client-default-resp } if {!$::tls} { diff --git a/tests/unit/moduleapi/basics.tcl b/tests/unit/moduleapi/basics.tcl index be606ced0..042e3474a 100644 --- a/tests/unit/moduleapi/basics.tcl +++ b/tests/unit/moduleapi/basics.tcl @@ -21,12 +21,17 @@ start_server {tags {"modules"}} { } test {test get resp} { - r hello 2 - set reply [r test.getresp] - assert_equal $reply 2 - r hello 3 - set reply [r test.getresp] - assert_equal $reply 3 + foreach resp {3 2} { + if {[lsearch $::denytags "resp3"] >= 0} { + if {$resp == 3} {continue} + } elseif {$::force_resp3} { + if {$resp == 2} {continue} + } + r hello $resp + set reply [r test.getresp] + assert_equal $reply $resp + r hello 2 + } } test "Unload the module - test" { diff --git a/tests/unit/moduleapi/blockedclient.tcl b/tests/unit/moduleapi/blockedclient.tcl index 2cb44788e..f0faea5c3 100644 --- a/tests/unit/moduleapi/blockedclient.tcl +++ b/tests/unit/moduleapi/blockedclient.tcl @@ -91,6 +91,11 @@ start_server {tags {"modules"}} { test {RESP version carries through to blocked client} { for {set client_proto 2} {$client_proto <= 3} {incr client_proto} { + if {[lsearch $::denytags "resp3"] >= 0} { + if {$client_proto == 3} {continue} + } elseif {$::force_resp3} { + if {$client_proto == 2} {continue} + } r hello $client_proto r readraw 1 set ret [r do_fake_bg_true] @@ -100,6 +105,7 @@ start_server {tags {"modules"}} { assert_equal $ret "#t" } r readraw 0 + r hello 2 } } diff --git a/tests/unit/moduleapi/cmdintrospection.tcl b/tests/unit/moduleapi/cmdintrospection.tcl index 4d67af1e1..6ba69a1ed 100644 --- a/tests/unit/moduleapi/cmdintrospection.tcl +++ b/tests/unit/moduleapi/cmdintrospection.tcl @@ -37,6 +37,9 @@ start_server {tags {"modules"}} { dict unset redis_reply group dict unset module_reply group dict unset module_reply module + if {$::log_req_res} { + dict unset redis_reply reply_schema + } assert_equal $redis_reply $module_reply } diff --git a/tests/unit/moduleapi/reply.tcl b/tests/unit/moduleapi/reply.tcl index 356d1a0ed..291253d3c 100644 --- a/tests/unit/moduleapi/reply.tcl +++ b/tests/unit/moduleapi/reply.tcl @@ -5,6 +5,11 @@ start_server {tags {"modules"}} { # test all with hello 2/3 for {set proto 2} {$proto <= 3} {incr proto} { + if {[lsearch $::denytags "resp3"] >= 0} { + if {$proto == 3} {continue} + } elseif {$::force_resp3} { + if {$proto == 2} {continue} + } r hello $proto test "RESP$proto: RM_ReplyWithString: an string reply" { @@ -120,6 +125,8 @@ start_server {tags {"modules"}} { catch {r rw.error} e assert_match "An error" $e } + + r hello 2 } test "Unload the module - replywith" { diff --git a/tests/unit/networking.tcl b/tests/unit/networking.tcl index 559a88e74..79d6e399d 100644 --- a/tests/unit/networking.tcl +++ b/tests/unit/networking.tcl @@ -121,7 +121,14 @@ start_server {config "minimal.conf" tags {"external:skip"}} { # Make sure bind parameter is as expected and server handles binding # accordingly. - assert_equal {bind {}} [rediscli_exec 0 config get bind] + # (it seems that rediscli_exec behaves differently in RESP3, possibly + # because CONFIG GET returns a dict instead of a list so redis-cli emits + # it in a single line) + if {$::force_resp3} { + assert_equal {{bind }} [rediscli_exec 0 config get bind] + } else { + assert_equal {bind {}} [rediscli_exec 0 config get bind] + } catch {reconnect 0} err assert_match {*connection refused*} $err diff --git a/tests/unit/obuf-limits.tcl b/tests/unit/obuf-limits.tcl index 7eb6def58..45efc26b4 100644 --- a/tests/unit/obuf-limits.tcl +++ b/tests/unit/obuf-limits.tcl @@ -1,4 +1,4 @@ -start_server {tags {"obuf-limits external:skip"}} { +start_server {tags {"obuf-limits external:skip logreqres:skip"}} { test {CONFIG SET client-output-buffer-limit} { set oldval [lindex [r config get client-output-buffer-limit] 1] diff --git a/tests/unit/other.tcl b/tests/unit/other.tcl index 2ae09b5b7..41e550890 100644 --- a/tests/unit/other.tcl +++ b/tests/unit/other.tcl @@ -6,6 +6,30 @@ start_server {tags {"other"}} { } {ok} } + test {Coverage: HELP commands} { + assert_match "*OBJECT *" [r OBJECT HELP] + assert_match "*MEMORY *" [r MEMORY HELP] + assert_match "*PUBSUB *" [r PUBSUB HELP] + assert_match "*SLOWLOG *" [r SLOWLOG HELP] + assert_match "*CLIENT *" [r CLIENT HELP] + assert_match "*COMMAND *" [r COMMAND HELP] + assert_match "*CONFIG *" [r CONFIG HELP] + assert_match "*FUNCTION *" [r FUNCTION HELP] + assert_match "*MODULE *" [r MODULE HELP] + } + + test {Coverage: MEMORY MALLOC-STATS} { + if {[string match {*jemalloc*} [s mem_allocator]]} { + assert_match "*jemalloc*" [r memory malloc-stats] + } + } + + test {Coverage: MEMORY PURGE} { + if {[string match {*jemalloc*} [s mem_allocator]]} { + assert_equal {OK} [r memory purge] + } + } + test {SAVE - make sure there are all the types as values} { # Wait for a background saving in progress to terminate waitForBgsave r diff --git a/tests/unit/protocol.tcl b/tests/unit/protocol.tcl index 50305bd27..e3b4115a8 100644 --- a/tests/unit/protocol.tcl +++ b/tests/unit/protocol.tcl @@ -110,16 +110,21 @@ start_server {tags {"protocol network"}} { # raw RESP response tests r readraw 1 + set nullres {*-1} + if {$::force_resp3} { + set nullres {_} + } + test "raw protocol response" { r srandmember nonexisting_key - } {*-1} + } "$nullres" r deferred 1 test "raw protocol response - deferred" { r srandmember nonexisting_key r read - } {*-1} + } "$nullres" test "raw protocol response - multiline" { r sadd ss a diff --git a/tests/unit/pubsub.tcl b/tests/unit/pubsub.tcl index ea8964cf3..fe486edf3 100644 --- a/tests/unit/pubsub.tcl +++ b/tests/unit/pubsub.tcl @@ -5,21 +5,41 @@ start_server {tags {"pubsub network"}} { set db 9 } - test "Pub/Sub PING" { + foreach resp {2 3} { set rd1 [redis_deferring_client] - subscribe $rd1 somechannel - # While subscribed to non-zero channels PING works in Pub/Sub mode. - $rd1 ping - $rd1 ping "foo" - set reply1 [$rd1 read] - set reply2 [$rd1 read] - unsubscribe $rd1 somechannel - # Now we are unsubscribed, PING should just return PONG. - $rd1 ping - set reply3 [$rd1 read] + if {[lsearch $::denytags "resp3"] >= 0} { + if {$resp == 3} {continue} + } elseif {$::force_resp3} { + if {$resp == 2} {continue} + } + + $rd1 hello $resp + $rd1 read + + test "Pub/Sub PING on RESP$resp" { + subscribe $rd1 somechannel + # While subscribed to non-zero channels PING works in Pub/Sub mode. + $rd1 ping + $rd1 ping "foo" + # In RESP3, the SUBSCRIBEd client can issue any command and get a reply, so the PINGs are standard + # In RESP2, only a handful of commands are allowed after a client is SUBSCRIBED (PING is one of them). + # For some reason, the reply in that case is an array with two elements: "pong" and argv[1] or an empty string + # God knows why. Done in commit 2264b981 + if {$resp == 3} { + assert_equal {PONG} [$rd1 read] + assert_equal {foo} [$rd1 read] + } else { + assert_equal {pong {}} [$rd1 read] + assert_equal {pong foo} [$rd1 read] + } + unsubscribe $rd1 somechannel + # Now we are unsubscribed, PING should just return PONG. + $rd1 ping + assert_equal {PONG} [$rd1 read] + + } $rd1 close - list $reply1 $reply2 $reply3 - } {{pong {}} {pong foo} PONG} + } test "PUBLISH/SUBSCRIBE basics" { set rd1 [redis_deferring_client] diff --git a/tests/unit/scripting.tcl b/tests/unit/scripting.tcl index e3b1a620b..02459354a 100644 --- a/tests/unit/scripting.tcl +++ b/tests/unit/scripting.tcl @@ -119,6 +119,10 @@ start_server {tags {"scripting"}} { r evalsha fd758d1589d044dd850a6f05d52f2eefd27f033f 1 mykey } {myval} + test {EVALSHA_RO - Can we call a SHA1 if already defined?} { + r evalsha_ro fd758d1589d044dd850a6f05d52f2eefd27f033f 1 mykey + } {myval} + test {EVALSHA - Can we call a SHA1 in uppercase?} { r evalsha FD758D1589D044DD850A6F05D52F2EEFD27F033F 1 mykey } {myval} @@ -703,6 +707,7 @@ start_server {tags {"scripting"}} { assert_equal $res $expected_list } {} {resp3} + if {!$::log_req_res} { # this test creates a huge nested array which python can't handle (RecursionError: maximum recursion depth exceeded in comparison) test {Script return recursive object} { r readraw 1 set res [run_script {local a = {}; local b = {a}; a[1] = b; return a} 0] @@ -718,6 +723,7 @@ start_server {tags {"scripting"}} { # make sure the connection is still valid assert_equal [r ping] {PONG} } + } test {Script check unpack with massive arguments} { run_script { @@ -1257,9 +1263,10 @@ start_server {tags {"scripting needs:debug"}} { for {set client_proto 2} {$client_proto <= 3} {incr client_proto} { if {[lsearch $::denytags "resp3"] >= 0} { if {$client_proto == 3} {continue} - } else { - r hello $client_proto + } elseif {$::force_resp3} { + if {$client_proto == 2} {continue} } + r hello $client_proto set extra "RESP$i/$client_proto" r readraw 1 @@ -1367,6 +1374,7 @@ start_server {tags {"scripting needs:debug"}} { } r readraw 0 + r hello 2 } } diff --git a/tests/unit/tracking.tcl b/tests/unit/tracking.tcl index 13ca8f278..21036352f 100644 --- a/tests/unit/tracking.tcl +++ b/tests/unit/tracking.tcl @@ -1,4 +1,5 @@ -start_server {tags {"tracking network"}} { +# logreqres:skip because it seems many of these tests rely heavily on RESP2 +start_server {tags {"tracking network logreqres:skip"}} { # Create a deferred client we'll use to redirect invalidation # messages to. set rd_redirection [redis_deferring_client] @@ -783,3 +784,28 @@ start_server {tags {"tracking network"}} { $rd_redirection close $rd close } + +# Just some extra covergae for --log-req-res, because we do not +# run the full tracking unit in that mode +start_server {tags {"tracking network"}} { + test {Coverage: Basic CLIENT CACHING} { + set rd_redirection [redis_deferring_client] + $rd_redirection client id + set redir_id [$rd_redirection read] + assert_equal {OK} [r CLIENT TRACKING on OPTIN REDIRECT $redir_id] + assert_equal {OK} [r CLIENT CACHING yes] + r CLIENT TRACKING off + } {OK} + + test {Coverage: Basic CLIENT REPLY} { + r CLIENT REPLY on + } {OK} + + test {Coverage: Basic CLIENT TRACKINGINFO} { + r CLIENT TRACKINGINFO + } {flags off redirect -1 prefixes {}} + + test {Coverage: Basic CLIENT GETREDIR} { + r CLIENT GETREDIR + } {-1} +} diff --git a/tests/unit/type/incr.tcl b/tests/unit/type/incr.tcl index 7732921d9..a64f357ae 100644 --- a/tests/unit/type/incr.tcl +++ b/tests/unit/type/incr.tcl @@ -9,6 +9,10 @@ start_server {tags {"incr"}} { r incr novar } {2} + test {DECR against key created by incr} { + r decr novar + } {1} + test {INCR against key originally set with SET} { r set novar 100 r incr novar diff --git a/tests/unit/type/list.tcl b/tests/unit/type/list.tcl index dee8482bf..dad5e595a 100644 --- a/tests/unit/type/list.tcl +++ b/tests/unit/type/list.tcl @@ -558,9 +558,10 @@ foreach {type large} [array get largevalue] { foreach resp {3 2} { if {[lsearch $::denytags "resp3"] >= 0} { if {$resp == 3} {continue} - } else { - r hello $resp + } elseif {$::force_resp3} { + if {$resp == 2} {continue} } + r hello $resp # Make sure we can distinguish between an empty array and a null response r readraw 1 @@ -589,6 +590,7 @@ foreach {type large} [array get largevalue] { } r readraw 0 + r hello 2 } test {Variadic RPUSH/LPUSH} { diff --git a/tests/unit/type/string.tcl b/tests/unit/type/string.tcl index 3734d1f50..a9fa894dc 100644 --- a/tests/unit/type/string.tcl +++ b/tests/unit/type/string.tcl @@ -459,6 +459,13 @@ start_server {tags {"string"}} { assert_equal [string range $bin $_start $_end] [r getrange bin $start $end] } } + + test "Coverage: SUBSTR" { + r set key abcde + assert_equal "a" [r substr key 0 0] + assert_equal "abcd" [r substr key 0 3] + assert_equal "bcde" [r substr key -4 -1] + } if {[string match {*jemalloc*} [s mem_allocator]]} { test {trim on SET with big value} { diff --git a/tests/unit/type/zset.tcl b/tests/unit/type/zset.tcl index 7a9721905..a52a77f24 100644 --- a/tests/unit/type/zset.tcl +++ b/tests/unit/type/zset.tcl @@ -431,6 +431,10 @@ start_server {tags {"zset"}} { } test "ZRANK/ZREVRANK basics - $encoding" { + set nullres {$-1} + if {$::force_resp3} { + set nullres {_} + } r del zranktmp r zadd zranktmp 10 x r zadd zranktmp 20 y @@ -442,11 +446,15 @@ start_server {tags {"zset"}} { assert_equal 1 [r zrevrank zranktmp y] assert_equal 0 [r zrevrank zranktmp z] r readraw 1 - assert_equal {$-1} [r zrank zranktmp foo] - assert_equal {$-1} [r zrevrank zranktmp foo] + assert_equal $nullres [r zrank zranktmp foo] + assert_equal $nullres [r zrevrank zranktmp foo] r readraw 0 # withscores + set nullres {*-1} + if {$::force_resp3} { + set nullres {_} + } assert_equal {0 10} [r zrank zranktmp x withscore] assert_equal {1 20} [r zrank zranktmp y withscore] assert_equal {2 30} [r zrank zranktmp z withscore] @@ -454,8 +462,8 @@ start_server {tags {"zset"}} { assert_equal {1 20} [r zrevrank zranktmp y withscore] assert_equal {0 30} [r zrevrank zranktmp z withscore] r readraw 1 - assert_equal {*-1} [r zrank zranktmp foo withscore] - assert_equal {*-1} [r zrevrank zranktmp foo withscore] + assert_equal $nullres [r zrank zranktmp foo withscore] + assert_equal $nullres [r zrevrank zranktmp foo withscore] r readraw 0 } @@ -1243,11 +1251,12 @@ start_server {tags {"zset"}} { if {[lsearch $::denytags "resp3"] >= 0} { if {$resp == 3} {continue} - } else { - r hello $resp - $rd hello $resp - $rd read + } elseif {$::force_resp3} { + if {$resp == 2} {continue} } + r hello $resp + $rd hello $resp + $rd read test "ZPOPMIN/ZPOPMAX readraw in RESP$resp" { r del zset{t} @@ -1401,6 +1410,7 @@ start_server {tags {"zset"}} { } $rd close + r hello 2 } test {ZINTERSTORE regression with two sets, intset+hashtable} { diff --git a/utils/generate-command-code.py b/utils/generate-command-code.py index 24ecaef3e..b5847c469 100755 --- a/utils/generate-command-code.py +++ b/utils/generate-command-code.py @@ -2,6 +2,7 @@ import glob import json import os +import argparse ARG_TYPES = { "string": "ARG_TYPE_STRING", @@ -35,29 +36,6 @@ GROUPS = { "bitmap": "COMMAND_GROUP_BITMAP", } -RESP2_TYPES = { - "simple-string": "RESP2_SIMPLE_STRING", - "error": "RESP2_ERROR", - "integer": "RESP2_INTEGER", - "bulk-string": "RESP2_BULK_STRING", - "null-bulk-string": "RESP2_NULL_BULK_STRING", - "array": "RESP2_ARRAY", - "null-array": "RESP2_NULL_ARRAY", -} - -RESP3_TYPES = { - "simple-string": "RESP3_SIMPLE_STRING", - "error": "RESP3_ERROR", - "integer": "RESP3_INTEGER", - "double": "RESP3_DOUBLE", - "bulk-string": "RESP3_BULK_STRING", - "array": "RESP3_ARRAY", - "map": "RESP3_MAP", - "set": "RESP3_SET", - "bool": "RESP3_BOOL", - "null": "RESP3_NULL", -} - def get_optional_desc_string(desc, field, force_uppercase=False): v = desc.get(field, None) @@ -194,7 +172,6 @@ class Argument(object): self.type = self.desc["type"] self.key_spec_index = self.desc.get("key_spec_index", None) self.subargs = [] - self.subargs_name = None if self.type in ["oneof", "block"]: self.display = None for subdesc in self.desc["arguments"]: @@ -264,6 +241,75 @@ class Argument(object): f.write("};\n\n") +def to_c_name(str): + return str.replace(":", "").replace(".", "_").replace("$", "_")\ + .replace("^", "_").replace("*", "_").replace("-", "_") + + +class ReplySchema(object): + def __init__(self, name, desc): + self.name = to_c_name(name) + self.schema = {} + if desc.get("type") == "object": + if desc.get("properties") and desc.get("additionalProperties") is None: + print(f"{self.name}: Any object that has properties should have the additionalProperties field") + exit(1) + elif desc.get("type") == "array": + if desc.get("items") and isinstance(desc["items"], list) and any([desc.get(k) is None for k in ["minItems", "maxItems"]]): + print(f"{self.name}: Any array that has items should have the minItems and maxItems fields") + exit(1) + for k, v in desc.items(): + if isinstance(v, dict): + self.schema[k] = ReplySchema("%s_%s" % (self.name, k), v) + elif isinstance(v, list): + self.schema[k] = [] + for i, subdesc in enumerate(v): + self.schema[k].append(ReplySchema("%s_%s_%i" % (self.name, k,i), subdesc)) + else: + self.schema[k] = v + + def write(self, f): + def struct_code(name, k, v): + if isinstance(v, ReplySchema): + t = "JSON_TYPE_OBJECT" + vstr = ".value.object=&%s" % name + elif isinstance(v, list): + t = "JSON_TYPE_ARRAY" + vstr = ".value.array={.objects=%s,.length=%d}" % (name, len(v)) + elif isinstance(v, bool): + t = "JSON_TYPE_BOOLEAN" + vstr = ".value.boolean=%d" % int(v) + elif isinstance(v, str): + t = "JSON_TYPE_STRING" + vstr = ".value.string=\"%s\"" % v + elif isinstance(v, int): + t = "JSON_TYPE_INTEGER" + vstr = ".value.integer=%d" % v + + return "%s,\"%s\",%s" % (t, k, vstr) + + for k, v in self.schema.items(): + if isinstance(v, ReplySchema): + v.write(f) + elif isinstance(v, list): + for i, schema in enumerate(v): + schema.write(f) + name = to_c_name("%s_%s" % (self.name, k)) + f.write("/* %s array reply schema */\n" % name) + f.write("struct jsonObject *%s[] = {\n" % name) + for i, schema in enumerate(v): + f.write("&%s,\n" % schema.name) + f.write("};\n\n") + + f.write("/* %s reply schema */\n" % self.name) + f.write("struct jsonObjectElement %s_elements[] = {\n" % self.name) + for k, v in self.schema.items(): + name = to_c_name("%s_%s" % (self.name, k)) + f.write("{%s},\n" % struct_code(name, k, v)) + f.write("};\n\n") + f.write("struct jsonObject %s = {%s_elements,.length=%d};\n\n" % (self.name, self.name, len(self.schema))) + + class Command(object): def __init__(self, name, desc): self.name = name.upper() @@ -273,9 +319,11 @@ class Command(object): self.subcommands = [] self.args = [] for arg_desc in self.desc.get("arguments", []): - arg = Argument(self.fullname(), arg_desc) - self.args.append(arg) + self.args.append(Argument(self.fullname(), arg_desc)) verify_no_dup_names(self.fullname(), self.args) + self.reply_schema = None + if "reply_schema" in self.desc: + self.reply_schema = ReplySchema(self.reply_schema_name(), self.desc["reply_schema"]) def fullname(self): return self.name.replace("-", "_").replace(":", "") @@ -296,6 +344,9 @@ class Command(object): def arg_table_name(self): return "%s_Args" % (self.fullname().replace(" ", "_")) + def reply_schema_name(self): + return "%s_ReplySchema" % (self.fullname().replace(" ", "_")) + def struct_name(self): return "%s_Command" % (self.fullname().replace(" ", "_")) @@ -377,6 +428,9 @@ class Command(object): if self.args: s += ".args=%s," % self.arg_table_name() + if self.reply_schema and args.with_reply_schema: + s += ".reply_schema=&%s," % self.reply_schema_name() + return s[:-1] def write_internal_structs(self, f): @@ -423,6 +477,9 @@ class Command(object): f.write("{0}\n") f.write("};\n\n") + if self.reply_schema and args.with_reply_schema: + self.reply_schema.write(f) + class Subcommand(Command): def __init__(self, name, desc): @@ -447,6 +504,10 @@ def create_command(name, desc): # Figure out where the sources are srcdir = os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "/../src") +parser = argparse.ArgumentParser() +parser.add_argument('--with-reply-schema', action='store_true') +args = parser.parse_args() + # Create all command objects print("Processing json files...") for filename in glob.glob('%s/commands/*.json' % srcdir): @@ -481,8 +542,9 @@ if check_command_error_counter != 0: print("Error: There are errors in the commands check, please check the above logs.") exit(1) -print("Generating commands.c...") -with open("%s/commands.c" % srcdir, "w") as f: +commands_filename = "commands_with_reply_schema" if args.with_reply_schema else "commands" +print(f"Generating {commands_filename}.c...") +with open(f"{srcdir}/{commands_filename}.c", "w") as f: f.write("/* Automatically generated by %s, do not edit. */\n\n" % os.path.basename(__file__)) f.write("#include \"server.h\"\n") f.write( diff --git a/utils/reply_schema_linter.js b/utils/reply_schema_linter.js new file mode 100644 index 000000000..e2358d4b9 --- /dev/null +++ b/utils/reply_schema_linter.js @@ -0,0 +1,31 @@ +function validate_schema(command_schema) { + var error_status = false + const Ajv = require("ajv/dist/2019") + const ajv = new Ajv({strict: true, strictTuples: false}) + let json = require('../src/commands/'+ command_schema); + for (var item in json) { + const schema = json[item].reply_schema + if (schema == undefined) + continue; + try { + ajv.compile(schema) + } catch (error) { + console.error(command_schema + " : " + error.toString()) + error_status = true + } + } + return error_status +} + +const schema_directory_path = './src/commands' +const path = require('path') +var fs = require('fs'); +var files = fs.readdirSync(schema_directory_path); +jsonFiles = files.filter(el => path.extname(el) === '.json') +var error_status = false +jsonFiles.forEach(function(file){ + if (validate_schema(file)) + error_status = true +}) +if (error_status) + process.exit(1) diff --git a/utils/req-res-log-validator.py b/utils/req-res-log-validator.py new file mode 100755 index 000000000..e2b9d4f8d --- /dev/null +++ b/utils/req-res-log-validator.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python3 +import os +import glob +import json +import sys + +import jsonschema +import subprocess +import redis +import time +import argparse +import multiprocessing +import collections +import io +import signal +import traceback +from datetime import timedelta +from functools import partial +try: + from jsonschema import Draft201909Validator as schema_validator +except ImportError: + from jsonschema import Draft7Validator as schema_validator + +""" +The purpose of this file is to validate the reply_schema values of COMMAND DOCS. +Basically, this is what it does: +1. Goes over req-res files, generated by redis-servers, spawned by the testsuite (see logreqres.c) +2. For each request-response pair, it validates the response against the request's reply_schema (obtained from COMMAND DOCS) + +This script spins up a redis-server and a redis-cli in order to obtain COMMAND DOCS. + +In order to use this file you must run the redis testsuite with the following flags: +./runtest --dont-clean --force-resp3 --log-req-res + +And then: +./utils/req-res-log-validator.py + +The script will fail only if: +1. One or more of the replies doesn't comply with its schema. +2. One or more of the commands in COMMANDS DOCS doesn't have the reply_schema field (with --fail-missing-reply-schemas) +3. The testsuite didn't execute all of the commands (with --fail-commands-not-all-hit) + +Future validations: +1. Fail the script if one or more of the branches of the reply schema (e.g. oneOf, anyOf) was not hit. +""" + +IGNORED_COMMANDS = [ + "sync", + "psync", + "monitor", + "subscribe", + "unsubscribe", + "ssubscribe", + "sunsubscribe", + "psubscribe", + "punsubscribe", + "debug", + "pfdebug", + "lolwut", +] + + +class Request(object): + """ + This class represents a Redis request (AKA command, argv) + """ + def __init__(self, f, docs, line_counter): + """ + Read lines from `f` (generated by logreqres.c) and populates the argv array + """ + self.command = None + self.schema = None + self.argv = [] + + while True: + line = f.readline() + line_counter[0] += 1 + if not line: + break + length = int(line) + arg = str(f.read(length)) + f.read(2) # read \r\n + line_counter[0] += 1 + if arg == "__argv_end__": + break + self.argv.append(arg) + + if not self.argv: + return + + self.command = self.argv[0].lower() + doc = docs.get(self.command, {}) + if not doc and len(self.argv) > 1: + self.command = f"{self.argv[0].lower()}|{self.argv[1].lower()}" + doc = docs.get(self.command, {}) + + if not doc: + self.command = None + return + + self.schema = doc.get("reply_schema") + + def __str__(self): + return json.dumps(self.argv) + + +class Response(object): + """ + This class represents a Redis response in RESP3 + """ + def __init__(self, f, line_counter): + """ + Read lines from `f` (generated by logreqres.c) and build the JSON representing the response in RESP3 + """ + self.error = False + self.queued = False + self.json = None + + line = f.readline()[:-2] + line_counter[0] += 1 + if line[0] == '+': + self.json = line[1:] + if self.json == "QUEUED": + self.queued = True + elif line[0] == '-': + self.json = line[1:] + self.error = True + elif line[0] == '$': + self.json = str(f.read(int(line[1:]))) + f.read(2) # read \r\n + line_counter[0] += 1 + elif line[0] == ':': + self.json = int(line[1:]) + elif line[0] == ',': + self.json = float(line[1:]) + elif line[0] == '_': + self.json = None + elif line[0] == '#': + self.json = line[1] == 't' + elif line[0] == '!': + self.json = str(f.read(int(line[1:]))) + f.read(2) # read \r\n + line_counter[0] += 1 + self.error = True + elif line[0] == '=': + self.json = str(f.read(int(line[1:])))[4:] # skip "txt:" or "mkd:" + f.read(2) # read \r\n + line_counter[0] += 1 + self.json.count("\r\n") + elif line[0] == '(': + self.json = line[1:] # big-number is actually a string + elif line[0] in ['*', '~', '>']: # unfortunately JSON doesn't tell the difference between a list and a set + self.json = [] + count = int(line[1:]) + for i in range(count): + ele = Response(f, line_counter) + self.json.append(ele.json) + elif line[0] in ['%', '|']: + self.json = {} + count = int(line[1:]) + for i in range(count): + field = Response(f, line_counter) + # Redis allows fields to be non-strings but JSON doesn't. + # Luckily, for any kind of response we can validate, the fields are + # always strings (example: XINFO STREAM) + # The reason we can't always convert to string is because of DEBUG PROTOCOL MAP + # which anyway doesn't have a schema + if isinstance(field.json, str): + field = field.json + value = Response(f, line_counter) + self.json[field] = value.json + if line[0] == '|': + # We don't care abou the attributes, read the real response + real_res = Response(f, line_counter) + self.__dict__.update(real_res.__dict__) + + + def __str__(self): + return json.dumps(self.json) + + +def process_file(docs, path): + """ + This function processes a single filegenerated by logreqres.c + """ + line_counter = [0] # A list with one integer: to force python to pass it by reference + command_counter = dict() + + print(f"Processing {path} ...") + + # Convert file to StringIO in order to minimize IO operations + with open(path, "r", newline="\r\n", encoding="latin-1") as f: + content = f.read() + + with io.StringIO(content) as fakefile: + while True: + try: + req = Request(fakefile, docs, line_counter) + if not req.argv: + # EOF + break + res = Response(fakefile, line_counter) + except json.decoder.JSONDecodeError as err: + print(f"JSON decoder error while processing {path}:{line_counter[0]}: {err}") + print(traceback.format_exc()) + raise + except Exception as err: + print(f"General error while processing {path}:{line_counter[0]}: {err}") + print(traceback.format_exc()) + raise + + if not req.command: + # Unknown command + continue + + command_counter[req.command] = command_counter.get(req.command, 0) + 1 + + if res.error or res.queued: + continue + + try: + jsonschema.validate(instance=res.json, schema=req.schema, cls=schema_validator) + except (jsonschema.ValidationError, jsonschema.exceptions.SchemaError) as err: + print(f"JSON schema validation error on {path}: {err}") + print(f"argv: {req.argv}") + try: + print(f"Response: {res}") + except UnicodeDecodeError as err: + print("Response: (unprintable)") + print(f"Schema: {json.dumps(req.schema, indent=2)}") + print(traceback.format_exc()) + raise + + return command_counter + + +def fetch_schemas(cli, port, args, docs): + redis_proc = subprocess.Popen(args, stdout=subprocess.PIPE) + + while True: + try: + print('Connecting to Redis...') + r = redis.Redis(port=port) + r.ping() + break + except Exception as e: + time.sleep(0.1) + pass + print('Connected') + + cli_proc = subprocess.Popen([cli, '-p', str(port), '--json', 'command', 'docs'], stdout=subprocess.PIPE) + stdout, stderr = cli_proc.communicate() + docs_response = json.loads(stdout) + + for name, doc in docs_response.items(): + if "subcommands" in doc: + for subname, subdoc in doc["subcommands"].items(): + docs[subname] = subdoc + else: + docs[name] = doc + + redis_proc.terminate() + redis_proc.wait() + + +if __name__ == '__main__': + # Figure out where the sources are + srcdir = os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "/../src") + testdir = os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "/../tests") + + parser = argparse.ArgumentParser() + parser.add_argument('--server', type=str, default='%s/redis-server' % srcdir) + parser.add_argument('--port', type=int, default=6534) + parser.add_argument('--cli', type=str, default='%s/redis-cli' % srcdir) + parser.add_argument('--module', type=str, action='append', default=[]) + parser.add_argument('--verbose', action='store_true') + parser.add_argument('--fail-commands-not-all-hit', action='store_true') + parser.add_argument('--fail-missing-reply-schemas', action='store_true') + args = parser.parse_args() + + docs = dict() + + # Fetch schemas from a Redis instance + print('Starting Redis server') + redis_args = [args.server, '--port', str(args.port)] + for module in args.module: + redis_args += ['--loadmodule', 'tests/modules/%s.so' % module] + + fetch_schemas(args.cli, args.port, redis_args, docs) + + missing_schema = [k for k, v in docs.items() + if "reply_schema" not in v and k not in IGNORED_COMMANDS] + if missing_schema: + print("WARNING! The following commands are missing a reply_schema:") + for k in sorted(missing_schema): + print(f" {k}") + if args.fail_missing_reply_schemas: + print("ERROR! at least one command does not have a reply_schema") + sys.exit(1) + + # Fetch schemas from a sentinel + print('Starting Redis sentinel') + + # Sentinel needs a config file to start + config_file = "tmpsentinel.conf" + open(config_file, 'a').close() + + sentinel_args = [args.server, config_file, '--port', str(args.port), "--sentinel"] + fetch_schemas(args.cli, args.port, sentinel_args, docs) + os.unlink(config_file) + + start = time.time() + + # Obtain all the files toprocesses + paths = [] + for path in glob.glob('%s/tmp/*/*.reqres' % testdir): + paths.append(path) + + for path in glob.glob('%s/cluster/tmp/*/*.reqres' % testdir): + paths.append(path) + + for path in glob.glob('%s/sentinel/tmp/*/*.reqres' % testdir): + paths.append(path) + + counter = collections.Counter() + # Spin several processes to handle the files in parallel + with multiprocessing.Pool(multiprocessing.cpu_count()) as pool: + func = partial(process_file, docs) + # pool.map blocks until all the files have been processed + for result in pool.map(func, paths): + counter.update(result) + command_counter = dict(counter) + + elapsed = time.time() - start + print(f"Done. ({timedelta(seconds=elapsed)})") + print("Hits per command:") + for k, v in sorted(command_counter.items()): + print(f" {k}: {v}") + # We don't care about SENTINEL commands + not_hit = set(filter(lambda x: not x.startswith("sentinel"), + set(docs.keys()) - set(command_counter.keys()) - set(IGNORED_COMMANDS))) + if not_hit: + if args.verbose: + print("WARNING! The following commands were not hit at all:") + for k in sorted(not_hit): + print(f" {k}") + if args.fail_commands_not_all_hit: + print("ERROR! at least one command was not hit by the tests") + sys.exit(1) + diff --git a/utils/req-res-validator/requirements.txt b/utils/req-res-validator/requirements.txt new file mode 100644 index 000000000..0e3024b86 --- /dev/null +++ b/utils/req-res-validator/requirements.txt @@ -0,0 +1,2 @@ +jsonschema==4.17.3 +redis==4.5.1 \ No newline at end of file