From 65ec1f678ca5a8e32dc4cd7da88f017cf26e8009 Mon Sep 17 00:00:00 2001 From: Daniel M Date: Tue, 6 Aug 2024 15:55:34 -0400 Subject: [PATCH] Timeseries cmds (#310) --- .github/workflows/test.yml | 10 +- docs/about/changelog.md | 6 + docs/redis-commands/Redis.md | 10 +- docs/redis-commands/RedisTimeSeries.md | 41 +- fakeredis/_commands.py | 16 + fakeredis/_fakesocket.py | 2 + fakeredis/_msgs.py | 29 +- fakeredis/commands.json | 2 +- fakeredis/stack/__init__.py | 2 + fakeredis/stack/_json_mixin.py | 3 +- fakeredis/stack/_timeseries_mixin.py | 561 ++++++++++++++++ fakeredis/stack/_timeseries_model.py | 282 ++++++++ pyproject.toml | 1 + test/test_mixins/test_server_commands.py | 1 + test/test_stack/test_timeseries.py | 791 +++++++++++++++++++++++ 15 files changed, 1718 insertions(+), 39 deletions(-) create mode 100644 fakeredis/stack/_timeseries_mixin.py create mode 100644 fakeredis/stack/_timeseries_model.py create mode 100644 test/test_stack/test_timeseries.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 241db0c3..a939eef5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,7 @@ on: push: branches: - master - pull_request_target: + pull_request: branches: - master @@ -55,17 +55,17 @@ jobs: max-parallel: 8 fail-fast: false matrix: - redis-image: [ "redis:6.2.14", "redis:7.0.15", "redis:7.2.4" ] + redis-image: [ "redis:6.2.14", "redis:7.0.15", "redis:7.2.5", "redis:7.4.0" ] python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ] - redis-py: [ "4.3.6", "4.6.0", "5.0.8", "5.1.0b7" ] + redis-py: ["4.6.0", "5.0.8", "5.1.0b7" ] include: - python-version: "3.12" - redis-image: "redis/redis-stack:6.2.6-v13" + redis-image: "redis/redis-stack:6.2.6-v15" redis-py: "5.0.8" extra: true # json, bf, lupa, cf hypothesis: true - python-version: "3.12" - redis-image: "redis/redis-stack-server:7.2.0-v9" + redis-image: "redis/redis-stack-server:7.2.0-v11" redis-py: "5.0.8" extra: true # json, bf, lupa, cf coverage: true diff --git a/docs/about/changelog.md b/docs/about/changelog.md index 7fed97c6..f01072f8 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -3,6 +3,12 @@ title: Change log description: Change log of all fakeredis releases --- +## v2.24.0 + +### 🚀 Features + +- Support for TIME SERIES commands (no support for align arguments on some commands) #310 + ## v2.23.5 ### 🐛 Bug Fixes diff --git a/docs/redis-commands/Redis.md b/docs/redis-commands/Redis.md index e40c7dcf..fc8c6da8 100644 --- a/docs/redis-commands/Redis.md +++ b/docs/redis-commands/Redis.md @@ -512,7 +512,11 @@ Enables read-only queries for a connection to a Redis Cluster replica node. Enables read-write queries for a connection to a Reids Cluster replica node. -## `connection` commands (3/25 implemented) +## `connection` commands (4/25 implemented) + +### [CLIENT SETINFO](https://redis.io/commands/client-setinfo/) + +Sets information specific to the client or connection. ### [ECHO](https://redis.io/commands/echo/) @@ -582,10 +586,6 @@ Suspends commands processing. Instructs the server whether to reply to commands. -#### [CLIENT SETINFO](https://redis.io/commands/client-setinfo/) (not implemented) - -Sets information specific to the client or connection. - #### [CLIENT SETNAME](https://redis.io/commands/client-setname/) (not implemented) Sets the connection name. diff --git a/docs/redis-commands/RedisTimeSeries.md b/docs/redis-commands/RedisTimeSeries.md index 0fb5511e..333ed0b2 100644 --- a/docs/redis-commands/RedisTimeSeries.md +++ b/docs/redis-commands/RedisTimeSeries.md @@ -1,77 +1,74 @@ # Time Series commands -Module currently not implemented in fakeredis. +## `timeseries` commands (17/17 implemented) - -### Unsupported timeseries commands -> To implement support for a command, see [here](../../guides/implement-command/) - -#### [TS.CREATE](https://redis.io/commands/ts.create/) (not implemented) +### [TS.CREATE](https://redis.io/commands/ts.create/) Create a new time series -#### [TS.DEL](https://redis.io/commands/ts.del/) (not implemented) +### [TS.DEL](https://redis.io/commands/ts.del/) Delete all samples between two timestamps for a given time series -#### [TS.ALTER](https://redis.io/commands/ts.alter/) (not implemented) +### [TS.ALTER](https://redis.io/commands/ts.alter/) Update the retention, chunk size, duplicate policy, and labels of an existing time series -#### [TS.ADD](https://redis.io/commands/ts.add/) (not implemented) +### [TS.ADD](https://redis.io/commands/ts.add/) Append a sample to a time series -#### [TS.MADD](https://redis.io/commands/ts.madd/) (not implemented) +### [TS.MADD](https://redis.io/commands/ts.madd/) Append new samples to one or more time series -#### [TS.INCRBY](https://redis.io/commands/ts.incrby/) (not implemented) +### [TS.INCRBY](https://redis.io/commands/ts.incrby/) Increase the value of the sample with the maximum existing timestamp, or create a new sample with a value equal to the value of the sample with the maximum existing timestamp with a given increment -#### [TS.DECRBY](https://redis.io/commands/ts.decrby/) (not implemented) +### [TS.DECRBY](https://redis.io/commands/ts.decrby/) Decrease the value of the sample with the maximum existing timestamp, or create a new sample with a value equal to the value of the sample with the maximum existing timestamp with a given decrement -#### [TS.CREATERULE](https://redis.io/commands/ts.createrule/) (not implemented) +### [TS.CREATERULE](https://redis.io/commands/ts.createrule/) Create a compaction rule -#### [TS.DELETERULE](https://redis.io/commands/ts.deleterule/) (not implemented) +### [TS.DELETERULE](https://redis.io/commands/ts.deleterule/) Delete a compaction rule -#### [TS.RANGE](https://redis.io/commands/ts.range/) (not implemented) +### [TS.RANGE](https://redis.io/commands/ts.range/) Query a range in forward direction -#### [TS.REVRANGE](https://redis.io/commands/ts.revrange/) (not implemented) +### [TS.REVRANGE](https://redis.io/commands/ts.revrange/) Query a range in reverse direction -#### [TS.MRANGE](https://redis.io/commands/ts.mrange/) (not implemented) +### [TS.MRANGE](https://redis.io/commands/ts.mrange/) Query a range across multiple time series by filters in forward direction -#### [TS.MREVRANGE](https://redis.io/commands/ts.mrevrange/) (not implemented) +### [TS.MREVRANGE](https://redis.io/commands/ts.mrevrange/) Query a range across multiple time-series by filters in reverse direction -#### [TS.GET](https://redis.io/commands/ts.get/) (not implemented) +### [TS.GET](https://redis.io/commands/ts.get/) Get the sample with the highest timestamp from a given time series -#### [TS.MGET](https://redis.io/commands/ts.mget/) (not implemented) +### [TS.MGET](https://redis.io/commands/ts.mget/) Get the sample with the highest timestamp from each time series matching a specific filter -#### [TS.INFO](https://redis.io/commands/ts.info/) (not implemented) +### [TS.INFO](https://redis.io/commands/ts.info/) Returns information and statistics for a time series -#### [TS.QUERYINDEX](https://redis.io/commands/ts.queryindex/) (not implemented) +### [TS.QUERYINDEX](https://redis.io/commands/ts.queryindex/) Get all time series keys matching a filter list + diff --git a/fakeredis/_commands.py b/fakeredis/_commands.py index cf87d9cb..3aa4b81e 100644 --- a/fakeredis/_commands.py +++ b/fakeredis/_commands.py @@ -6,6 +6,8 @@ import functools import math import re +import sys +import time from typing import Tuple, Union, Optional, Any, Type, List, Callable, Sequence, Dict, Set from . import _msgs as msgs @@ -469,3 +471,17 @@ def fix_range_string(start: int, end: int, length: int) -> Tuple[int, int]: end = max(0, end + length) end = min(end, length - 1) return start, end + 1 + + +class Timestamp(Int): + """Argument converter for timestamps""" + + @classmethod + def decode(cls, value: bytes, decode_error: Optional[str] = None) -> int: + if value == b"*": + return int(time.time()) + if value == b"-": + return -1 + if value == b"+": + return sys.maxsize + return super().decode(value, decode_error=msgs.INVALID_EXPIRE_MSG) diff --git a/fakeredis/_fakesocket.py b/fakeredis/_fakesocket.py index d3362173..8aca07a0 100644 --- a/fakeredis/_fakesocket.py +++ b/fakeredis/_fakesocket.py @@ -7,6 +7,7 @@ CMSCommandsMixin, TopkCommandsMixin, TDigestCommandsMixin, + TimeSeriesCommandsMixin, ) from ._server import FakeServer from ._basefakesocket import BaseFakeSocket @@ -58,6 +59,7 @@ class FakeSocket( CMSCommandsMixin, TopkCommandsMixin, TDigestCommandsMixin, + TimeSeriesCommandsMixin, ): def __init__( self, diff --git a/fakeredis/_msgs.py b/fakeredis/_msgs.py index 76c49dc8..1b05039f 100644 --- a/fakeredis/_msgs.py +++ b/fakeredis/_msgs.py @@ -74,10 +74,6 @@ "ERR The XGROUP subcommand requires the key to exist." " Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically." ) -FLAG_NO_SCRIPT = "s" # Command not allowed in scripts -FLAG_LEAVE_EMPTY_VAL = "v" -FLAG_TRANSACTION = "t" -FLAG_DO_NOT_CREATE = "i" GEO_UNSUPPORTED_UNIT = "unsupported unit provided. please use M, KM, FT, MI" LPOS_RANK_CAN_NOT_BE_ZERO = ( "RANK can't be zero: use 1 to start from the first match, 2 from the second ... " @@ -93,8 +89,33 @@ ) INVALID_OVERFLOW_TYPE = "ERR Invalid OVERFLOW type specified" +# TDigest error messages TDIGEST_KEY_EXISTS = "T-Digest: key already exists" TDIGEST_KEY_NOT_EXISTS = "T-Digest: key does not exist" TDIGEST_ERROR_PARSING_VALUE = "T-Digest: error parsing val parameter" TDIGEST_BAD_QUANTILE = "T-Digest: quantile should be in [0,1]" TDIGEST_BAD_RANK = "T-Digest: rank needs to be non negative" + +# TimeSeries error messages +TIMESERIES_KEY_EXISTS = "TSDB: key already exists" +TIMESERIES_INVALID_DUPLICATE_POLICY = "TSDB: Unknown DUPLICATE_POLICY" +TIMESERIES_KEY_DOES_NOT_EXIST = "TSDB: the key does not exist" +TIMESERIES_RULE_EXISTS = "TSDB: the destination key already has a src rule" +TIMESERIES_BAD_AGGREGATION_TYPE = "TSDB: Unknown aggregation type" +TIMESERIES_INVALID_TIMESTAMP = "TSDB: invalid timestamp" +TIMESERIES_TIMESTAMP_OLDER_THAN_RETENTION = "TSDB: Timestamp is older than retention" +TIMESERIES_TIMESTAMP_LOWER_THAN_MAX_V7 = ( + "TSDB: timestamp must be equal to or higher than the maximum existing timestamp" +) +TIMESERIES_TIMESTAMP_LOWER_THAN_MAX_V6 = "TSDB: for incrby/decrby, timestamp should be newer than the lastest one" +TIMESERIES_BAD_CHUNK_SIZE = "TSDB: CHUNK_SIZE value must be a multiple of 8 in the range [48 .. 1048576]" +TIMESERIES_DUPLICATE_POLICY_BLOCK = ( + "TSDB: Error at upsert, update is not supported when DUPLICATE_POLICY is set to BLOCK mode" +) +TIMESERIES_BAD_FILTER_EXPRESSION = "TSDB: failed parsing labels" + +# Command flags +FLAG_NO_SCRIPT = "s" # Command not allowed in scripts +FLAG_LEAVE_EMPTY_VAL = "v" +FLAG_TRANSACTION = "t" +FLAG_DO_NOT_CREATE = "i" diff --git a/fakeredis/commands.json b/fakeredis/commands.json index d1aeee2f..b84f32a9 100644 --- a/fakeredis/commands.json +++ b/fakeredis/commands.json @@ -1 +1 @@ -{"append": ["append", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "bgsave": ["bgsave", -1, ["admin", "noscript", "no_async_loading"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], "bitcount": ["bitcount", -2, ["readonly"], 1, 1, 1, ["@read", "@bitmap", "@slow"], [], [], []], "bitfield": ["bitfield", -2, ["write", "denyoom"], 1, 1, 1, ["@write", "@bitmap", "@slow"], [], [], [["bitfield_ro", -2, ["readonly", "fast"], 1, 1, 1, ["@read", "@bitmap", "@fast"], [], [], []]]], "bitop": ["bitop", -4, ["write", "denyoom"], 2, 3, 1, ["@write", "@bitmap", "@slow"], [], [], []], "bitpos": ["bitpos", -3, ["readonly"], 1, 1, 1, ["@read", "@bitmap", "@slow"], [], [], []], "blmove": ["blmove", 6, ["write", "denyoom", "blocking"], 1, 2, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []], "blmpop": ["blmpop", -5, ["write", "blocking", "movablekeys"], 2, 2, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []], "blpop": ["blpop", -3, ["write", "blocking"], 1, 1, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []], "brpop": ["brpop", -3, ["write", "blocking"], 1, 1, 1, ["@write", "@list", "@slow", "@blocking"], [], [], [["brpoplpush", 4, ["write", "denyoom", "blocking"], 1, 2, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []]]], "brpoplpush": ["brpoplpush", 4, ["write", "denyoom", "blocking"], 1, 2, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []], "bzmpop": ["bzmpop", -5, ["write", "blocking", "movablekeys"], 2, 2, 1, ["@write", "@sortedset", "@slow", "@blocking"], [], [], []], "bzpopmax": ["bzpopmax", -3, ["write", "blocking", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast", "@blocking"], [], [], []], "bzpopmin": ["bzpopmin", -3, ["write", "blocking", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast", "@blocking"], [], [], []], "command": ["command", -1, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], [["command|count", 2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|docs", -2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|getkeys", -3, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], [["command|getkeysandflags", -3, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []]]], ["command|getkeysandflags", -3, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|help", 2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|info", -2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|list", -2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []]]], "dbsize": ["dbsize", 1, ["readonly", "fast"], 0, 0, 0, ["@keyspace", "@read", "@fast"], [], [], []], "decr": ["decr", 2, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], [["decrby", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], "decrby": ["decrby", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "del": ["del", -2, ["write"], 1, 1, 1, ["@keyspace", "@write", "@slow"], [], [], []], "discard": ["discard", 1, ["noscript", "loading", "stale", "fast", "allow_busy"], 0, 0, 0, ["@fast", "@transaction"], [], [], []], "dump": ["dump", 2, ["readonly"], 1, 1, 1, ["@keyspace", "@read", "@slow"], [], [], []], "echo": ["echo", 2, ["loading", "stale", "fast"], 0, 0, 0, ["@fast", "@connection"], [], [], []], "eval": ["eval", -3, ["noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], [["evalsha", -3, ["noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], [["evalsha_ro", -3, ["readonly", "noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], []]]], ["evalsha_ro", -3, ["readonly", "noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], []], ["eval_ro", -3, ["readonly", "noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], []]]], "evalsha": ["evalsha", -3, ["noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], [["evalsha_ro", -3, ["readonly", "noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], []]]], "exec": ["exec", 1, ["noscript", "loading", "stale", "skip_slowlog"], 0, 0, 0, ["@slow", "@transaction"], [], [], []], "exists": ["exists", -2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "expire": ["expire", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], [["expireat", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], ["expiretime", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []]]], "expireat": ["expireat", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "flushall": ["flushall", -1, ["write"], 0, 0, 0, ["@keyspace", "@write", "@slow", "@dangerous"], [], [], []], "flushdb": ["flushdb", -1, ["write"], 0, 0, 0, ["@keyspace", "@write", "@slow", "@dangerous"], [], [], []], "geoadd": ["geoadd", -5, ["write", "denyoom"], 1, 1, 1, ["@write", "@geo", "@slow"], [], [], []], "geodist": ["geodist", -4, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "geohash": ["geohash", -2, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "geopos": ["geopos", -2, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "georadius": ["georadius", -6, ["write", "denyoom", "movablekeys"], 1, 0, 1, ["@write", "@geo", "@slow"], [], [], [["georadiusbymember", -5, ["write", "denyoom", "movablekeys"], 1, 0, 1, ["@write", "@geo", "@slow"], [], [], [["georadiusbymember_ro", -5, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []]]], ["georadiusbymember_ro", -5, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], ["georadius_ro", -6, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []]]], "georadiusbymember": ["georadiusbymember", -5, ["write", "denyoom", "movablekeys"], 1, 0, 1, ["@write", "@geo", "@slow"], [], [], [["georadiusbymember_ro", -5, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []]]], "georadiusbymember_ro": ["georadiusbymember_ro", -5, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "georadius_ro": ["georadius_ro", -6, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "geosearch": ["geosearch", -7, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], [["geosearchstore", -8, ["write", "denyoom"], 1, 2, 1, ["@write", "@geo", "@slow"], [], [], []]]], "geosearchstore": ["geosearchstore", -8, ["write", "denyoom"], 1, 2, 1, ["@write", "@geo", "@slow"], [], [], []], "get": ["get", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@string", "@fast"], [], [], [["getbit", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@bitmap", "@fast"], [], [], []], ["getdel", 2, ["write", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], ["getex", -2, ["write", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], ["getrange", 4, ["readonly"], 1, 1, 1, ["@read", "@string", "@slow"], [], [], []], ["getset", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], "getbit": ["getbit", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@bitmap", "@fast"], [], [], []], "getdel": ["getdel", 2, ["write", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "getex": ["getex", -2, ["write", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "getrange": ["getrange", 4, ["readonly"], 1, 1, 1, ["@read", "@string", "@slow"], [], [], []], "getset": ["getset", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "hdel": ["hdel", -3, ["write", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hexists": ["hexists", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hget": ["hget", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], [["hgetall", 2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []]]], "hgetall": ["hgetall", 2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "hincrby": ["hincrby", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], [["hincrbyfloat", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []]]], "hincrbyfloat": ["hincrbyfloat", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hkeys": ["hkeys", 2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "hlen": ["hlen", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hmget": ["hmget", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hmset": ["hmset", -4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hrandfield": ["hrandfield", -2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "hscan": ["hscan", -3, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "hset": ["hset", -4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], [["hsetnx", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []]]], "hsetnx": ["hsetnx", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hstrlen": ["hstrlen", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hvals": ["hvals", 2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "incr": ["incr", 2, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], [["incrby", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], [["incrbyfloat", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], ["incrbyfloat", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], "incrby": ["incrby", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], [["incrbyfloat", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], "incrbyfloat": ["incrbyfloat", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "keys": ["keys", 2, ["readonly"], 0, 0, 0, ["@keyspace", "@read", "@slow", "@dangerous"], [], [], []], "lastsave": ["lastsave", 1, ["loading", "stale", "fast"], 0, 0, 0, ["@admin", "@fast", "@dangerous"], [], [], []], "lcs": ["lcs", -3, ["readonly"], 1, 1, 1, ["@read", "@string", "@slow"], [], [], []], "lindex": ["lindex", 3, ["readonly"], 1, 1, 1, ["@read", "@list", "@slow"], [], [], []], "linsert": ["linsert", 5, ["write", "denyoom"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "llen": ["llen", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@list", "@fast"], [], [], []], "lmove": ["lmove", 5, ["write", "denyoom"], 1, 2, 1, ["@write", "@list", "@slow"], [], [], []], "lmpop": ["lmpop", -4, ["write", "movablekeys"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "lpop": ["lpop", -2, ["write", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []], "lpos": ["lpos", -3, ["readonly"], 1, 1, 1, ["@read", "@list", "@slow"], [], [], []], "lpush": ["lpush", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], [["lpushx", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []]]], "lpushx": ["lpushx", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []], "lrange": ["lrange", 4, ["readonly"], 1, 1, 1, ["@read", "@list", "@slow"], [], [], []], "lrem": ["lrem", 4, ["write"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "lset": ["lset", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "ltrim": ["ltrim", 4, ["write"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "mget": ["mget", -2, ["readonly", "fast"], 1, 1, 1, ["@read", "@string", "@fast"], [], [], []], "move": ["move", 3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "mset": ["mset", -3, ["write", "denyoom"], 1, 1, 2, ["@write", "@string", "@slow"], [], [], [["msetnx", -3, ["write", "denyoom"], 1, 1, 2, ["@write", "@string", "@slow"], [], [], []]]], "msetnx": ["msetnx", -3, ["write", "denyoom"], 1, 1, 2, ["@write", "@string", "@slow"], [], [], []], "multi": ["multi", 1, ["noscript", "loading", "stale", "fast", "allow_busy"], 0, 0, 0, ["@fast", "@transaction"], [], [], []], "persist": ["persist", 2, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "pexpire": ["pexpire", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], [["pexpireat", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], ["pexpiretime", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []]]], "pexpireat": ["pexpireat", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "pfadd": ["pfadd", -2, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hyperloglog", "@fast"], [], [], []], "pfcount": ["pfcount", -2, ["readonly"], 1, 1, 1, ["@read", "@hyperloglog", "@slow"], [], [], []], "pfmerge": ["pfmerge", -2, ["write", "denyoom"], 1, 2, 1, ["@write", "@hyperloglog", "@slow"], [], [], []], "ping": ["ping", -1, ["fast"], 0, 0, 0, ["@fast", "@connection"], [], [], []], "psetex": ["psetex", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []], "psubscribe": ["psubscribe", -2, ["pubsub", "noscript", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "pttl": ["pttl", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "publish": ["publish", 3, ["pubsub", "loading", "stale", "fast"], 0, 0, 0, ["@pubsub", "@fast"], [], [], []], "pubsub": ["pubsub", -2, [], 0, 0, 0, ["@slow"], [], [], [["pubsub|channels", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|help", 2, ["loading", "stale"], 0, 0, 0, ["@slow"], [], [], []], ["pubsub|numpat", 2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|numsub", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|shardchannels", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|shardnumsub", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []]]], "punsubscribe": ["punsubscribe", -1, ["pubsub", "noscript", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "randomkey": ["randomkey", 1, ["readonly"], 0, 0, 0, ["@keyspace", "@read", "@slow"], [], [], []], "rename": ["rename", 3, ["write"], 1, 2, 1, ["@keyspace", "@write", "@slow"], [], [], [["renamenx", 3, ["write", "fast"], 1, 2, 1, ["@keyspace", "@write", "@fast"], [], [], []]]], "renamenx": ["renamenx", 3, ["write", "fast"], 1, 2, 1, ["@keyspace", "@write", "@fast"], [], [], []], "restore": ["restore", -4, ["write", "denyoom"], 1, 1, 1, ["@keyspace", "@write", "@slow", "@dangerous"], [], [], [["restore-asking", -4, ["write", "denyoom", "asking"], 1, 1, 1, ["@keyspace", "@write", "@slow", "@dangerous"], [], [], []]]], "rpop": ["rpop", -2, ["write", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], [["rpoplpush", 3, ["write", "denyoom"], 1, 2, 1, ["@write", "@list", "@slow"], [], [], []]]], "rpoplpush": ["rpoplpush", 3, ["write", "denyoom"], 1, 2, 1, ["@write", "@list", "@slow"], [], [], []], "rpush": ["rpush", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], [["rpushx", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []]]], "rpushx": ["rpushx", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []], "sadd": ["sadd", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@set", "@fast"], [], [], []], "save": ["save", 1, ["admin", "noscript", "no_async_loading", "no_multi"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], "scan": ["scan", -2, ["readonly"], 0, 0, 0, ["@keyspace", "@read", "@slow"], [], [], []], "scard": ["scard", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@set", "@fast"], [], [], []], "script": ["script", -2, [], 0, 0, 0, ["@slow"], [], [], [["script|debug", 3, ["noscript"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|exists", -3, ["noscript"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|flush", -2, ["noscript"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|help", 2, ["loading", "stale"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|kill", 2, ["noscript", "allow_busy"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|load", 3, ["noscript", "stale"], 0, 0, 0, ["@slow", "@scripting"], [], [], []]]], "sdiff": ["sdiff", -2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], [["sdiffstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []]]], "sdiffstore": ["sdiffstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []], "select": ["select", 2, ["loading", "stale", "fast"], 0, 0, 0, ["@fast", "@connection"], [], [], []], "set": ["set", -3, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], [["setbit", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@bitmap", "@slow"], [], [], []], ["setex", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []], ["setnx", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], ["setrange", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []]]], "setbit": ["setbit", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@bitmap", "@slow"], [], [], []], "setex": ["setex", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []], "setnx": ["setnx", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "setrange": ["setrange", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []], "sinter": ["sinter", -2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], [["sintercard", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], ["sinterstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []]]], "sintercard": ["sintercard", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], "sinterstore": ["sinterstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []], "sismember": ["sismember", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@set", "@fast"], [], [], []], "smembers": ["smembers", 2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], "smismember": ["smismember", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@set", "@fast"], [], [], []], "smove": ["smove", 4, ["write", "fast"], 1, 2, 1, ["@write", "@set", "@fast"], [], [], []], "sort": ["sort", -2, ["write", "denyoom", "movablekeys"], 1, 0, 1, ["@write", "@set", "@sortedset", "@list", "@slow", "@dangerous"], [], [], [["sort_ro", -2, ["readonly", "movablekeys"], 1, 0, 1, ["@read", "@set", "@sortedset", "@list", "@slow", "@dangerous"], [], [], []]]], "spop": ["spop", -2, ["write", "fast"], 1, 1, 1, ["@write", "@set", "@fast"], [], [], []], "spublish": ["spublish", 3, ["pubsub", "loading", "stale", "fast"], 1, 1, 1, ["@pubsub", "@fast"], [], [], []], "srandmember": ["srandmember", -2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], "srem": ["srem", -3, ["write", "fast"], 1, 1, 1, ["@write", "@set", "@fast"], [], [], []], "sscan": ["sscan", -3, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], "ssubscribe": ["ssubscribe", -2, ["pubsub", "noscript", "loading", "stale"], 1, 1, 1, ["@pubsub", "@slow"], [], [], []], "strlen": ["strlen", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@string", "@fast"], [], [], []], "subscribe": ["subscribe", -2, ["pubsub", "noscript", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "substr": ["substr", 4, ["readonly"], 1, 1, 1, ["@read", "@string", "@slow"], [], [], []], "sunion": ["sunion", -2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], [["sunionstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []]]], "sunionstore": ["sunionstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []], "sunsubscribe": ["sunsubscribe", -1, ["pubsub", "noscript", "loading", "stale"], 1, 1, 1, ["@pubsub", "@slow"], [], [], []], "swapdb": ["swapdb", 3, ["write", "fast"], 0, 0, 0, ["@keyspace", "@write", "@fast", "@dangerous"], [], [], []], "time": ["time", 1, ["loading", "stale", "fast"], 0, 0, 0, ["@fast"], [], [], []], "ttl": ["ttl", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "type": ["type", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "unlink": ["unlink", -2, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "unsubscribe": ["unsubscribe", -1, ["pubsub", "noscript", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "unwatch": ["unwatch", 1, ["noscript", "loading", "stale", "fast", "allow_busy"], 0, 0, 0, ["@fast", "@transaction"], [], [], []], "watch": ["watch", -2, ["noscript", "loading", "stale", "fast", "allow_busy"], 1, 1, 1, ["@fast", "@transaction"], [], [], []], "xack": ["xack", -4, ["write", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xadd": ["xadd", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xautoclaim": ["xautoclaim", -6, ["write", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xclaim": ["xclaim", -6, ["write", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xdel": ["xdel", -3, ["write", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xlen": ["xlen", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@stream", "@fast"], [], [], []], "xpending": ["xpending", -3, ["readonly"], 1, 1, 1, ["@read", "@stream", "@slow"], [], [], []], "xrange": ["xrange", -4, ["readonly"], 1, 1, 1, ["@read", "@stream", "@slow"], [], [], []], "xread": ["xread", -4, ["readonly", "blocking", "movablekeys"], 0, 0, 1, ["@read", "@stream", "@slow", "@blocking"], [], [], [["xreadgroup", -7, ["write", "blocking", "movablekeys"], 0, 0, 1, ["@write", "@stream", "@slow", "@blocking"], [], [], []]]], "xreadgroup": ["xreadgroup", -7, ["write", "blocking", "movablekeys"], 0, 0, 1, ["@write", "@stream", "@slow", "@blocking"], [], [], []], "xrevrange": ["xrevrange", -4, ["readonly"], 1, 1, 1, ["@read", "@stream", "@slow"], [], [], []], "xtrim": ["xtrim", -4, ["write"], 1, 1, 1, ["@write", "@stream", "@slow"], [], [], []], "zadd": ["zadd", -4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], []], "zcard": ["zcard", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zcount": ["zcount", 4, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zdiff": ["zdiff", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zdiffstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zdiffstore": ["zdiffstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zincrby": ["zincrby", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], []], "zinter": ["zinter", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zintercard", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], ["zinterstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zintercard": ["zintercard", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zinterstore": ["zinterstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zlexcount": ["zlexcount", 4, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zmpop": ["zmpop", -4, ["write", "movablekeys"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zmscore": ["zmscore", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zpopmax": ["zpopmax", -2, ["write", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], []], "zpopmin": ["zpopmin", -2, ["write", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], []], "zrandmember": ["zrandmember", -2, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrange": ["zrange", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zrangebylex", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], ["zrangebyscore", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], ["zrangestore", -5, ["write", "denyoom"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zrangebylex": ["zrangebylex", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrangebyscore": ["zrangebyscore", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrangestore": ["zrangestore", -5, ["write", "denyoom"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zrank": ["zrank", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zrem": ["zrem", -3, ["write", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], [["zremrangebylex", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], ["zremrangebyrank", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], ["zremrangebyscore", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zremrangebylex": ["zremrangebylex", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zremrangebyrank": ["zremrangebyrank", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zremrangebyscore": ["zremrangebyscore", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zrevrange": ["zrevrange", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zrevrangebylex", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], ["zrevrangebyscore", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []]]], "zrevrangebylex": ["zrevrangebylex", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrevrangebyscore": ["zrevrangebyscore", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrevrank": ["zrevrank", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zscan": ["zscan", -3, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zscore": ["zscore", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zunion": ["zunion", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zunionstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zunionstore": ["zunionstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []], "json.del": ["json.del", -1, [], 0, 0, 0, [], [], [], []], "json.forget": ["json.forget", -1, [], 0, 0, 0, [], [], [], []], "json.get": ["json.get", -1, [], 0, 0, 0, [], [], [], []], "json.toggle": ["json.toggle", -1, [], 0, 0, 0, [], [], [], []], "json.clear": ["json.clear", -1, [], 0, 0, 0, [], [], [], []], "json.set": ["json.set", -1, [], 0, 0, 0, [], [], [], []], "json.mset": ["json.mset", -1, [], 0, 0, 0, [], [], [], []], "json.merge": ["json.merge", -1, [], 0, 0, 0, [], [], [], []], "json.mget": ["json.mget", -1, [], 0, 0, 0, [], [], [], []], "json.numincrby": ["json.numincrby", -1, [], 0, 0, 0, [], [], [], []], "json.nummultby": ["json.nummultby", -1, [], 0, 0, 0, [], [], [], []], "json.strappend": ["json.strappend", -1, [], 0, 0, 0, [], [], [], []], "json.strlen": ["json.strlen", -1, [], 0, 0, 0, [], [], [], []], "json.arrappend": ["json.arrappend", -1, [], 0, 0, 0, [], [], [], []], "json.arrindex": ["json.arrindex", -1, [], 0, 0, 0, [], [], [], []], "json.arrinsert": ["json.arrinsert", -1, [], 0, 0, 0, [], [], [], []], "json.arrlen": ["json.arrlen", -1, [], 0, 0, 0, [], [], [], []], "json.arrpop": ["json.arrpop", -1, [], 0, 0, 0, [], [], [], []], "json.arrtrim": ["json.arrtrim", -1, [], 0, 0, 0, [], [], [], []], "json.objkeys": ["json.objkeys", -1, [], 0, 0, 0, [], [], [], []], "json.objlen": ["json.objlen", -1, [], 0, 0, 0, [], [], [], []], "json.type": ["json.type", -1, [], 0, 0, 0, [], [], [], []], "bf.reserve": ["bf.reserve", -1, [], 0, 0, 0, [], [], [], []], "bf.add": ["bf.add", -1, [], 0, 0, 0, [], [], [], []], "bf.madd": ["bf.madd", -1, [], 0, 0, 0, [], [], [], []], "bf.insert": ["bf.insert", -1, [], 0, 0, 0, [], [], [], []], "bf.exists": ["bf.exists", -1, [], 0, 0, 0, [], [], [], []], "bf.mexists": ["bf.mexists", -1, [], 0, 0, 0, [], [], [], []], "bf.scandump": ["bf.scandump", -1, [], 0, 0, 0, [], [], [], []], "bf.loadchunk": ["bf.loadchunk", -1, [], 0, 0, 0, [], [], [], []], "bf.info": ["bf.info", -1, [], 0, 0, 0, [], [], [], []], "bf.card": ["bf.card", -1, [], 0, 0, 0, [], [], [], []], "cf.reserve": ["cf.reserve", -1, [], 0, 0, 0, [], [], [], []], "cf.add": ["cf.add", -1, [], 0, 0, 0, [], [], [], [["cf.addnx", -1, [], 0, 0, 0, [], [], [], []]]], "cf.addnx": ["cf.addnx", -1, [], 0, 0, 0, [], [], [], []], "cf.insert": ["cf.insert", -1, [], 0, 0, 0, [], [], [], [["cf.insertnx", -1, [], 0, 0, 0, [], [], [], []]]], "cf.insertnx": ["cf.insertnx", -1, [], 0, 0, 0, [], [], [], []], "cf.exists": ["cf.exists", -1, [], 0, 0, 0, [], [], [], []], "cf.mexists": ["cf.mexists", -1, [], 0, 0, 0, [], [], [], []], "cf.del": ["cf.del", -1, [], 0, 0, 0, [], [], [], []], "cf.count": ["cf.count", -1, [], 0, 0, 0, [], [], [], []], "cf.scandump": ["cf.scandump", -1, [], 0, 0, 0, [], [], [], []], "cf.loadchunk": ["cf.loadchunk", -1, [], 0, 0, 0, [], [], [], []], "cf.info": ["cf.info", -1, [], 0, 0, 0, [], [], [], []], "cms.initbydim": ["cms.initbydim", -1, [], 0, 0, 0, [], [], [], []], "cms.initbyprob": ["cms.initbyprob", -1, [], 0, 0, 0, [], [], [], []], "cms.incrby": ["cms.incrby", -1, [], 0, 0, 0, [], [], [], []], "cms.query": ["cms.query", -1, [], 0, 0, 0, [], [], [], []], "cms.merge": ["cms.merge", -1, [], 0, 0, 0, [], [], [], []], "cms.info": ["cms.info", -1, [], 0, 0, 0, [], [], [], []], "topk.reserve": ["topk.reserve", -1, [], 0, 0, 0, [], [], [], []], "topk.add": ["topk.add", -1, [], 0, 0, 0, [], [], [], []], "topk.incrby": ["topk.incrby", -1, [], 0, 0, 0, [], [], [], []], "topk.query": ["topk.query", -1, [], 0, 0, 0, [], [], [], []], "topk.count": ["topk.count", -1, [], 0, 0, 0, [], [], [], []], "topk.list": ["topk.list", -1, [], 0, 0, 0, [], [], [], []], "topk.info": ["topk.info", -1, [], 0, 0, 0, [], [], [], []], "tdigest.create": ["tdigest.create", -1, [], 0, 0, 0, [], [], [], []], "tdigest.reset": ["tdigest.reset", -1, [], 0, 0, 0, [], [], [], []], "tdigest.add": ["tdigest.add", -1, [], 0, 0, 0, [], [], [], []], "tdigest.merge": ["tdigest.merge", -1, [], 0, 0, 0, [], [], [], []], "tdigest.min": ["tdigest.min", -1, [], 0, 0, 0, [], [], [], []], "tdigest.max": ["tdigest.max", -1, [], 0, 0, 0, [], [], [], []], "tdigest.quantile": ["tdigest.quantile", -1, [], 0, 0, 0, [], [], [], []], "tdigest.cdf": ["tdigest.cdf", -1, [], 0, 0, 0, [], [], [], []], "tdigest.trimmed_mean": ["tdigest.trimmed_mean", -1, [], 0, 0, 0, [], [], [], []], "tdigest.rank": ["tdigest.rank", -1, [], 0, 0, 0, [], [], [], []], "tdigest.revrank": ["tdigest.revrank", -1, [], 0, 0, 0, [], [], [], []], "tdigest.byrank": ["tdigest.byrank", -1, [], 0, 0, 0, [], [], [], []], "tdigest.byrevrank": ["tdigest.byrevrank", -1, [], 0, 0, 0, [], [], [], []], "tdigest.info": ["tdigest.info", -1, [], 0, 0, 0, [], [], [], []]} \ No newline at end of file +{"append": ["append", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "bgsave": ["bgsave", -1, ["admin", "noscript", "no_async_loading"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], "bitcount": ["bitcount", -2, ["readonly"], 1, 1, 1, ["@read", "@bitmap", "@slow"], [], [], []], "bitfield": ["bitfield", -2, ["write", "denyoom"], 1, 1, 1, ["@write", "@bitmap", "@slow"], [], [], [["bitfield_ro", -2, ["readonly", "fast"], 1, 1, 1, ["@read", "@bitmap", "@fast"], [], [], []]]], "bitop": ["bitop", -4, ["write", "denyoom"], 2, 3, 1, ["@write", "@bitmap", "@slow"], [], [], []], "bitpos": ["bitpos", -3, ["readonly"], 1, 1, 1, ["@read", "@bitmap", "@slow"], [], [], []], "blmove": ["blmove", 6, ["write", "denyoom", "blocking"], 1, 2, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []], "blmpop": ["blmpop", -5, ["write", "blocking", "movablekeys"], 2, 2, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []], "blpop": ["blpop", -3, ["write", "blocking"], 1, 1, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []], "brpop": ["brpop", -3, ["write", "blocking"], 1, 1, 1, ["@write", "@list", "@slow", "@blocking"], [], [], [["brpoplpush", 4, ["write", "denyoom", "blocking"], 1, 2, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []]]], "brpoplpush": ["brpoplpush", 4, ["write", "denyoom", "blocking"], 1, 2, 1, ["@write", "@list", "@slow", "@blocking"], [], [], []], "bzmpop": ["bzmpop", -5, ["write", "blocking", "movablekeys"], 2, 2, 1, ["@write", "@sortedset", "@slow", "@blocking"], [], [], []], "bzpopmax": ["bzpopmax", -3, ["write", "blocking", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast", "@blocking"], [], [], []], "bzpopmin": ["bzpopmin", -3, ["write", "blocking", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast", "@blocking"], [], [], []], "command": ["command", -1, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], [["command|count", 2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|docs", -2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|getkeys", -3, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], [["command|getkeysandflags", -3, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []]]], ["command|getkeysandflags", -3, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|help", 2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|info", -2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []], ["command|list", -2, ["loading", "stale"], 0, 0, 0, ["@slow", "@connection"], [], [], []]]], "dbsize": ["dbsize", 1, ["readonly", "fast"], 0, 0, 0, ["@keyspace", "@read", "@fast"], [], [], []], "decr": ["decr", 2, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], [["decrby", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], "decrby": ["decrby", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "del": ["del", -2, ["write"], 1, 1, 1, ["@keyspace", "@write", "@slow"], [], [], []], "discard": ["discard", 1, ["noscript", "loading", "stale", "fast", "allow_busy"], 0, 0, 0, ["@fast", "@transaction"], [], [], []], "dump": ["dump", 2, ["readonly"], 1, 1, 1, ["@keyspace", "@read", "@slow"], [], [], []], "echo": ["echo", 2, ["loading", "stale", "fast"], 0, 0, 0, ["@fast", "@connection"], [], [], []], "eval": ["eval", -3, ["noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], [["evalsha", -3, ["noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], [["evalsha_ro", -3, ["readonly", "noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], []]]], ["evalsha_ro", -3, ["readonly", "noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], []], ["eval_ro", -3, ["readonly", "noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], []]]], "evalsha": ["evalsha", -3, ["noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], [["evalsha_ro", -3, ["readonly", "noscript", "stale", "skip_monitor", "no_mandatory_keys", "movablekeys"], 2, 2, 1, ["@slow", "@scripting"], [], [], []]]], "exec": ["exec", 1, ["noscript", "loading", "stale", "skip_slowlog"], 0, 0, 0, ["@slow", "@transaction"], [], [], []], "exists": ["exists", -2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "expire": ["expire", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], [["expireat", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], ["expiretime", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []]]], "expireat": ["expireat", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "flushall": ["flushall", -1, ["write"], 0, 0, 0, ["@keyspace", "@write", "@slow", "@dangerous"], [], [], []], "flushdb": ["flushdb", -1, ["write"], 0, 0, 0, ["@keyspace", "@write", "@slow", "@dangerous"], [], [], []], "geoadd": ["geoadd", -5, ["write", "denyoom"], 1, 1, 1, ["@write", "@geo", "@slow"], [], [], []], "geodist": ["geodist", -4, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "geohash": ["geohash", -2, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "geopos": ["geopos", -2, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "georadius": ["georadius", -6, ["write", "denyoom", "movablekeys"], 1, 0, 1, ["@write", "@geo", "@slow"], [], [], [["georadiusbymember", -5, ["write", "denyoom", "movablekeys"], 1, 0, 1, ["@write", "@geo", "@slow"], [], [], [["georadiusbymember_ro", -5, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []]]], ["georadiusbymember_ro", -5, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], ["georadius_ro", -6, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []]]], "georadiusbymember": ["georadiusbymember", -5, ["write", "denyoom", "movablekeys"], 1, 0, 1, ["@write", "@geo", "@slow"], [], [], [["georadiusbymember_ro", -5, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []]]], "georadiusbymember_ro": ["georadiusbymember_ro", -5, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "georadius_ro": ["georadius_ro", -6, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], []], "geosearch": ["geosearch", -7, ["readonly"], 1, 1, 1, ["@read", "@geo", "@slow"], [], [], [["geosearchstore", -8, ["write", "denyoom"], 1, 2, 1, ["@write", "@geo", "@slow"], [], [], []]]], "geosearchstore": ["geosearchstore", -8, ["write", "denyoom"], 1, 2, 1, ["@write", "@geo", "@slow"], [], [], []], "get": ["get", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@string", "@fast"], [], [], [["getbit", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@bitmap", "@fast"], [], [], []], ["getdel", 2, ["write", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], ["getex", -2, ["write", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], ["getrange", 4, ["readonly"], 1, 1, 1, ["@read", "@string", "@slow"], [], [], []], ["getset", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], "getbit": ["getbit", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@bitmap", "@fast"], [], [], []], "getdel": ["getdel", 2, ["write", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "getex": ["getex", -2, ["write", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "getrange": ["getrange", 4, ["readonly"], 1, 1, 1, ["@read", "@string", "@slow"], [], [], []], "getset": ["getset", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "hdel": ["hdel", -3, ["write", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hexists": ["hexists", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hget": ["hget", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], [["hgetall", 2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []]]], "hgetall": ["hgetall", 2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "hincrby": ["hincrby", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], [["hincrbyfloat", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []]]], "hincrbyfloat": ["hincrbyfloat", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hkeys": ["hkeys", 2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "hlen": ["hlen", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hmget": ["hmget", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hmset": ["hmset", -4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hrandfield": ["hrandfield", -2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "hscan": ["hscan", -3, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "hset": ["hset", -4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], [["hsetnx", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []]]], "hsetnx": ["hsetnx", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hash", "@fast"], [], [], []], "hstrlen": ["hstrlen", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@hash", "@fast"], [], [], []], "hvals": ["hvals", 2, ["readonly"], 1, 1, 1, ["@read", "@hash", "@slow"], [], [], []], "incr": ["incr", 2, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], [["incrby", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], [["incrbyfloat", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], ["incrbyfloat", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], "incrby": ["incrby", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], [["incrbyfloat", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []]]], "incrbyfloat": ["incrbyfloat", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "keys": ["keys", 2, ["readonly"], 0, 0, 0, ["@keyspace", "@read", "@slow", "@dangerous"], [], [], []], "lastsave": ["lastsave", 1, ["loading", "stale", "fast"], 0, 0, 0, ["@admin", "@fast", "@dangerous"], [], [], []], "lcs": ["lcs", -3, ["readonly"], 1, 1, 1, ["@read", "@string", "@slow"], [], [], []], "lindex": ["lindex", 3, ["readonly"], 1, 1, 1, ["@read", "@list", "@slow"], [], [], []], "linsert": ["linsert", 5, ["write", "denyoom"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "llen": ["llen", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@list", "@fast"], [], [], []], "lmove": ["lmove", 5, ["write", "denyoom"], 1, 2, 1, ["@write", "@list", "@slow"], [], [], []], "lmpop": ["lmpop", -4, ["write", "movablekeys"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "lpop": ["lpop", -2, ["write", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []], "lpos": ["lpos", -3, ["readonly"], 1, 1, 1, ["@read", "@list", "@slow"], [], [], []], "lpush": ["lpush", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], [["lpushx", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []]]], "lpushx": ["lpushx", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []], "lrange": ["lrange", 4, ["readonly"], 1, 1, 1, ["@read", "@list", "@slow"], [], [], []], "lrem": ["lrem", 4, ["write"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "lset": ["lset", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "ltrim": ["ltrim", 4, ["write"], 1, 1, 1, ["@write", "@list", "@slow"], [], [], []], "mget": ["mget", -2, ["readonly", "fast"], 1, 1, 1, ["@read", "@string", "@fast"], [], [], []], "move": ["move", 3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "mset": ["mset", -3, ["write", "denyoom"], 1, 1, 2, ["@write", "@string", "@slow"], [], [], [["msetnx", -3, ["write", "denyoom"], 1, 1, 2, ["@write", "@string", "@slow"], [], [], []]]], "msetnx": ["msetnx", -3, ["write", "denyoom"], 1, 1, 2, ["@write", "@string", "@slow"], [], [], []], "multi": ["multi", 1, ["noscript", "loading", "stale", "fast", "allow_busy"], 0, 0, 0, ["@fast", "@transaction"], [], [], []], "persist": ["persist", 2, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "pexpire": ["pexpire", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], [["pexpireat", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], ["pexpiretime", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []]]], "pexpireat": ["pexpireat", -3, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "pfadd": ["pfadd", -2, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@hyperloglog", "@fast"], [], [], []], "pfcount": ["pfcount", -2, ["readonly"], 1, 1, 1, ["@read", "@hyperloglog", "@slow"], [], [], []], "pfmerge": ["pfmerge", -2, ["write", "denyoom"], 1, 2, 1, ["@write", "@hyperloglog", "@slow"], [], [], []], "ping": ["ping", -1, ["fast"], 0, 0, 0, ["@fast", "@connection"], [], [], []], "psetex": ["psetex", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []], "psubscribe": ["psubscribe", -2, ["pubsub", "noscript", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "pttl": ["pttl", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "publish": ["publish", 3, ["pubsub", "loading", "stale", "fast"], 0, 0, 0, ["@pubsub", "@fast"], [], [], []], "pubsub": ["pubsub", -2, [], 0, 0, 0, ["@slow"], [], [], [["pubsub|channels", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|help", 2, ["loading", "stale"], 0, 0, 0, ["@slow"], [], [], []], ["pubsub|numpat", 2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|numsub", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|shardchannels", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], ["pubsub|shardnumsub", -2, ["pubsub", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []]]], "punsubscribe": ["punsubscribe", -1, ["pubsub", "noscript", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "randomkey": ["randomkey", 1, ["readonly"], 0, 0, 0, ["@keyspace", "@read", "@slow"], [], [], []], "rename": ["rename", 3, ["write"], 1, 2, 1, ["@keyspace", "@write", "@slow"], [], [], [["renamenx", 3, ["write", "fast"], 1, 2, 1, ["@keyspace", "@write", "@fast"], [], [], []]]], "renamenx": ["renamenx", 3, ["write", "fast"], 1, 2, 1, ["@keyspace", "@write", "@fast"], [], [], []], "restore": ["restore", -4, ["write", "denyoom"], 1, 1, 1, ["@keyspace", "@write", "@slow", "@dangerous"], [], [], [["restore-asking", -4, ["write", "denyoom", "asking"], 1, 1, 1, ["@keyspace", "@write", "@slow", "@dangerous"], [], [], []]]], "rpop": ["rpop", -2, ["write", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], [["rpoplpush", 3, ["write", "denyoom"], 1, 2, 1, ["@write", "@list", "@slow"], [], [], []]]], "rpoplpush": ["rpoplpush", 3, ["write", "denyoom"], 1, 2, 1, ["@write", "@list", "@slow"], [], [], []], "rpush": ["rpush", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], [["rpushx", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []]]], "rpushx": ["rpushx", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@list", "@fast"], [], [], []], "sadd": ["sadd", -3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@set", "@fast"], [], [], []], "save": ["save", 1, ["admin", "noscript", "no_async_loading", "no_multi"], 0, 0, 0, ["@admin", "@slow", "@dangerous"], [], [], []], "scan": ["scan", -2, ["readonly"], 0, 0, 0, ["@keyspace", "@read", "@slow"], [], [], []], "scard": ["scard", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@set", "@fast"], [], [], []], "script": ["script", -2, [], 0, 0, 0, ["@slow"], [], [], [["script|debug", 3, ["noscript"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|exists", -3, ["noscript"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|flush", -2, ["noscript"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|help", 2, ["loading", "stale"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|kill", 2, ["noscript", "allow_busy"], 0, 0, 0, ["@slow", "@scripting"], [], [], []], ["script|load", 3, ["noscript", "stale"], 0, 0, 0, ["@slow", "@scripting"], [], [], []]]], "sdiff": ["sdiff", -2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], [["sdiffstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []]]], "sdiffstore": ["sdiffstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []], "select": ["select", 2, ["loading", "stale", "fast"], 0, 0, 0, ["@fast", "@connection"], [], [], []], "set": ["set", -3, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], [["setbit", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@bitmap", "@slow"], [], [], []], ["setex", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []], ["setnx", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], ["setrange", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []]]], "setbit": ["setbit", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@bitmap", "@slow"], [], [], []], "setex": ["setex", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []], "setnx": ["setnx", 3, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@string", "@fast"], [], [], []], "setrange": ["setrange", 4, ["write", "denyoom"], 1, 1, 1, ["@write", "@string", "@slow"], [], [], []], "sinter": ["sinter", -2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], [["sintercard", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], ["sinterstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []]]], "sintercard": ["sintercard", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], "sinterstore": ["sinterstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []], "sismember": ["sismember", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@set", "@fast"], [], [], []], "smembers": ["smembers", 2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], "smismember": ["smismember", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@set", "@fast"], [], [], []], "smove": ["smove", 4, ["write", "fast"], 1, 2, 1, ["@write", "@set", "@fast"], [], [], []], "sort": ["sort", -2, ["write", "denyoom", "movablekeys"], 1, 0, 1, ["@write", "@set", "@sortedset", "@list", "@slow", "@dangerous"], [], [], [["sort_ro", -2, ["readonly", "movablekeys"], 1, 0, 1, ["@read", "@set", "@sortedset", "@list", "@slow", "@dangerous"], [], [], []]]], "spop": ["spop", -2, ["write", "fast"], 1, 1, 1, ["@write", "@set", "@fast"], [], [], []], "spublish": ["spublish", 3, ["pubsub", "loading", "stale", "fast"], 1, 1, 1, ["@pubsub", "@fast"], [], [], []], "srandmember": ["srandmember", -2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], "srem": ["srem", -3, ["write", "fast"], 1, 1, 1, ["@write", "@set", "@fast"], [], [], []], "sscan": ["sscan", -3, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], []], "ssubscribe": ["ssubscribe", -2, ["pubsub", "noscript", "loading", "stale"], 1, 1, 1, ["@pubsub", "@slow"], [], [], []], "strlen": ["strlen", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@string", "@fast"], [], [], []], "subscribe": ["subscribe", -2, ["pubsub", "noscript", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "substr": ["substr", 4, ["readonly"], 1, 1, 1, ["@read", "@string", "@slow"], [], [], []], "sunion": ["sunion", -2, ["readonly"], 1, 1, 1, ["@read", "@set", "@slow"], [], [], [["sunionstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []]]], "sunionstore": ["sunionstore", -3, ["write", "denyoom"], 1, 2, 1, ["@write", "@set", "@slow"], [], [], []], "sunsubscribe": ["sunsubscribe", -1, ["pubsub", "noscript", "loading", "stale"], 1, 1, 1, ["@pubsub", "@slow"], [], [], []], "swapdb": ["swapdb", 3, ["write", "fast"], 0, 0, 0, ["@keyspace", "@write", "@fast", "@dangerous"], [], [], []], "time": ["time", 1, ["loading", "stale", "fast"], 0, 0, 0, ["@fast"], [], [], []], "ttl": ["ttl", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "type": ["type", 2, ["readonly", "fast"], 1, 1, 1, ["@keyspace", "@read", "@fast"], [], [], []], "unlink": ["unlink", -2, ["write", "fast"], 1, 1, 1, ["@keyspace", "@write", "@fast"], [], [], []], "unsubscribe": ["unsubscribe", -1, ["pubsub", "noscript", "loading", "stale"], 0, 0, 0, ["@pubsub", "@slow"], [], [], []], "unwatch": ["unwatch", 1, ["noscript", "loading", "stale", "fast", "allow_busy"], 0, 0, 0, ["@fast", "@transaction"], [], [], []], "watch": ["watch", -2, ["noscript", "loading", "stale", "fast", "allow_busy"], 1, 1, 1, ["@fast", "@transaction"], [], [], []], "xack": ["xack", -4, ["write", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xadd": ["xadd", -5, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xautoclaim": ["xautoclaim", -6, ["write", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xclaim": ["xclaim", -6, ["write", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xdel": ["xdel", -3, ["write", "fast"], 1, 1, 1, ["@write", "@stream", "@fast"], [], [], []], "xlen": ["xlen", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@stream", "@fast"], [], [], []], "xpending": ["xpending", -3, ["readonly"], 1, 1, 1, ["@read", "@stream", "@slow"], [], [], []], "xrange": ["xrange", -4, ["readonly"], 1, 1, 1, ["@read", "@stream", "@slow"], [], [], []], "xread": ["xread", -4, ["readonly", "blocking", "movablekeys"], 0, 0, 1, ["@read", "@stream", "@slow", "@blocking"], [], [], [["xreadgroup", -7, ["write", "blocking", "movablekeys"], 0, 0, 1, ["@write", "@stream", "@slow", "@blocking"], [], [], []]]], "xreadgroup": ["xreadgroup", -7, ["write", "blocking", "movablekeys"], 0, 0, 1, ["@write", "@stream", "@slow", "@blocking"], [], [], []], "xrevrange": ["xrevrange", -4, ["readonly"], 1, 1, 1, ["@read", "@stream", "@slow"], [], [], []], "xtrim": ["xtrim", -4, ["write"], 1, 1, 1, ["@write", "@stream", "@slow"], [], [], []], "zadd": ["zadd", -4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], []], "zcard": ["zcard", 2, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zcount": ["zcount", 4, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zdiff": ["zdiff", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zdiffstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zdiffstore": ["zdiffstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zincrby": ["zincrby", 4, ["write", "denyoom", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], []], "zinter": ["zinter", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zintercard", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], ["zinterstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zintercard": ["zintercard", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zinterstore": ["zinterstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zlexcount": ["zlexcount", 4, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zmpop": ["zmpop", -4, ["write", "movablekeys"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zmscore": ["zmscore", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zpopmax": ["zpopmax", -2, ["write", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], []], "zpopmin": ["zpopmin", -2, ["write", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], []], "zrandmember": ["zrandmember", -2, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrange": ["zrange", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zrangebylex", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], ["zrangebyscore", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], ["zrangestore", -5, ["write", "denyoom"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zrangebylex": ["zrangebylex", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrangebyscore": ["zrangebyscore", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrangestore": ["zrangestore", -5, ["write", "denyoom"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zrank": ["zrank", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zrem": ["zrem", -3, ["write", "fast"], 1, 1, 1, ["@write", "@sortedset", "@fast"], [], [], [["zremrangebylex", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], ["zremrangebyrank", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], ["zremrangebyscore", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zremrangebylex": ["zremrangebylex", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zremrangebyrank": ["zremrangebyrank", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zremrangebyscore": ["zremrangebyscore", 4, ["write"], 1, 1, 1, ["@write", "@sortedset", "@slow"], [], [], []], "zrevrange": ["zrevrange", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zrevrangebylex", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], ["zrevrangebyscore", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []]]], "zrevrangebylex": ["zrevrangebylex", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrevrangebyscore": ["zrevrangebyscore", -4, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zrevrank": ["zrevrank", -3, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zscan": ["zscan", -3, ["readonly"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], []], "zscore": ["zscore", 3, ["readonly", "fast"], 1, 1, 1, ["@read", "@sortedset", "@fast"], [], [], []], "zunion": ["zunion", -3, ["readonly", "movablekeys"], 1, 1, 1, ["@read", "@sortedset", "@slow"], [], [], [["zunionstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []]]], "zunionstore": ["zunionstore", -4, ["write", "denyoom", "movablekeys"], 1, 2, 1, ["@write", "@sortedset", "@slow"], [], [], []], "json.del": ["json.del", -1, [], 0, 0, 0, [], [], [], []], "json.forget": ["json.forget", -1, [], 0, 0, 0, [], [], [], []], "json.get": ["json.get", -1, [], 0, 0, 0, [], [], [], []], "json.toggle": ["json.toggle", -1, [], 0, 0, 0, [], [], [], []], "json.clear": ["json.clear", -1, [], 0, 0, 0, [], [], [], []], "json.set": ["json.set", -1, [], 0, 0, 0, [], [], [], []], "json.mset": ["json.mset", -1, [], 0, 0, 0, [], [], [], []], "json.merge": ["json.merge", -1, [], 0, 0, 0, [], [], [], []], "json.mget": ["json.mget", -1, [], 0, 0, 0, [], [], [], []], "json.numincrby": ["json.numincrby", -1, [], 0, 0, 0, [], [], [], []], "json.nummultby": ["json.nummultby", -1, [], 0, 0, 0, [], [], [], []], "json.strappend": ["json.strappend", -1, [], 0, 0, 0, [], [], [], []], "json.strlen": ["json.strlen", -1, [], 0, 0, 0, [], [], [], []], "json.arrappend": ["json.arrappend", -1, [], 0, 0, 0, [], [], [], []], "json.arrindex": ["json.arrindex", -1, [], 0, 0, 0, [], [], [], []], "json.arrinsert": ["json.arrinsert", -1, [], 0, 0, 0, [], [], [], []], "json.arrlen": ["json.arrlen", -1, [], 0, 0, 0, [], [], [], []], "json.arrpop": ["json.arrpop", -1, [], 0, 0, 0, [], [], [], []], "json.arrtrim": ["json.arrtrim", -1, [], 0, 0, 0, [], [], [], []], "json.objkeys": ["json.objkeys", -1, [], 0, 0, 0, [], [], [], []], "json.objlen": ["json.objlen", -1, [], 0, 0, 0, [], [], [], []], "json.type": ["json.type", -1, [], 0, 0, 0, [], [], [], []], "ts.create": ["ts.create", -1, [], 0, 0, 0, [], [], [], [["ts.createrule", -1, [], 0, 0, 0, [], [], [], []]]], "ts.del": ["ts.del", -1, [], 0, 0, 0, [], [], [], [["ts.deleterule", -1, [], 0, 0, 0, [], [], [], []]]], "ts.alter": ["ts.alter", -1, [], 0, 0, 0, [], [], [], []], "ts.add": ["ts.add", -1, [], 0, 0, 0, [], [], [], []], "ts.madd": ["ts.madd", -1, [], 0, 0, 0, [], [], [], []], "ts.incrby": ["ts.incrby", -1, [], 0, 0, 0, [], [], [], []], "ts.decrby": ["ts.decrby", -1, [], 0, 0, 0, [], [], [], []], "ts.createrule": ["ts.createrule", -1, [], 0, 0, 0, [], [], [], []], "ts.deleterule": ["ts.deleterule", -1, [], 0, 0, 0, [], [], [], []], "ts.range": ["ts.range", -1, [], 0, 0, 0, [], [], [], []], "ts.revrange": ["ts.revrange", -1, [], 0, 0, 0, [], [], [], []], "ts.mrange": ["ts.mrange", -1, [], 0, 0, 0, [], [], [], []], "ts.mrevrange": ["ts.mrevrange", -1, [], 0, 0, 0, [], [], [], []], "ts.get": ["ts.get", -1, [], 0, 0, 0, [], [], [], []], "ts.mget": ["ts.mget", -1, [], 0, 0, 0, [], [], [], []], "ts.info": ["ts.info", -1, [], 0, 0, 0, [], [], [], []], "ts.queryindex": ["ts.queryindex", -1, [], 0, 0, 0, [], [], [], []], "bf.reserve": ["bf.reserve", -1, [], 0, 0, 0, [], [], [], []], "bf.add": ["bf.add", -1, [], 0, 0, 0, [], [], [], []], "bf.madd": ["bf.madd", -1, [], 0, 0, 0, [], [], [], []], "bf.insert": ["bf.insert", -1, [], 0, 0, 0, [], [], [], []], "bf.exists": ["bf.exists", -1, [], 0, 0, 0, [], [], [], []], "bf.mexists": ["bf.mexists", -1, [], 0, 0, 0, [], [], [], []], "bf.scandump": ["bf.scandump", -1, [], 0, 0, 0, [], [], [], []], "bf.loadchunk": ["bf.loadchunk", -1, [], 0, 0, 0, [], [], [], []], "bf.info": ["bf.info", -1, [], 0, 0, 0, [], [], [], []], "bf.card": ["bf.card", -1, [], 0, 0, 0, [], [], [], []], "cf.reserve": ["cf.reserve", -1, [], 0, 0, 0, [], [], [], []], "cf.add": ["cf.add", -1, [], 0, 0, 0, [], [], [], [["cf.addnx", -1, [], 0, 0, 0, [], [], [], []]]], "cf.addnx": ["cf.addnx", -1, [], 0, 0, 0, [], [], [], []], "cf.insert": ["cf.insert", -1, [], 0, 0, 0, [], [], [], [["cf.insertnx", -1, [], 0, 0, 0, [], [], [], []]]], "cf.insertnx": ["cf.insertnx", -1, [], 0, 0, 0, [], [], [], []], "cf.exists": ["cf.exists", -1, [], 0, 0, 0, [], [], [], []], "cf.mexists": ["cf.mexists", -1, [], 0, 0, 0, [], [], [], []], "cf.del": ["cf.del", -1, [], 0, 0, 0, [], [], [], []], "cf.count": ["cf.count", -1, [], 0, 0, 0, [], [], [], []], "cf.scandump": ["cf.scandump", -1, [], 0, 0, 0, [], [], [], []], "cf.loadchunk": ["cf.loadchunk", -1, [], 0, 0, 0, [], [], [], []], "cf.info": ["cf.info", -1, [], 0, 0, 0, [], [], [], []], "cms.initbydim": ["cms.initbydim", -1, [], 0, 0, 0, [], [], [], []], "cms.initbyprob": ["cms.initbyprob", -1, [], 0, 0, 0, [], [], [], []], "cms.incrby": ["cms.incrby", -1, [], 0, 0, 0, [], [], [], []], "cms.query": ["cms.query", -1, [], 0, 0, 0, [], [], [], []], "cms.merge": ["cms.merge", -1, [], 0, 0, 0, [], [], [], []], "cms.info": ["cms.info", -1, [], 0, 0, 0, [], [], [], []], "topk.reserve": ["topk.reserve", -1, [], 0, 0, 0, [], [], [], []], "topk.add": ["topk.add", -1, [], 0, 0, 0, [], [], [], []], "topk.incrby": ["topk.incrby", -1, [], 0, 0, 0, [], [], [], []], "topk.query": ["topk.query", -1, [], 0, 0, 0, [], [], [], []], "topk.count": ["topk.count", -1, [], 0, 0, 0, [], [], [], []], "topk.list": ["topk.list", -1, [], 0, 0, 0, [], [], [], []], "topk.info": ["topk.info", -1, [], 0, 0, 0, [], [], [], []], "tdigest.create": ["tdigest.create", -1, [], 0, 0, 0, [], [], [], []], "tdigest.reset": ["tdigest.reset", -1, [], 0, 0, 0, [], [], [], []], "tdigest.add": ["tdigest.add", -1, [], 0, 0, 0, [], [], [], []], "tdigest.merge": ["tdigest.merge", -1, [], 0, 0, 0, [], [], [], []], "tdigest.min": ["tdigest.min", -1, [], 0, 0, 0, [], [], [], []], "tdigest.max": ["tdigest.max", -1, [], 0, 0, 0, [], [], [], []], "tdigest.quantile": ["tdigest.quantile", -1, [], 0, 0, 0, [], [], [], []], "tdigest.cdf": ["tdigest.cdf", -1, [], 0, 0, 0, [], [], [], []], "tdigest.trimmed_mean": ["tdigest.trimmed_mean", -1, [], 0, 0, 0, [], [], [], []], "tdigest.rank": ["tdigest.rank", -1, [], 0, 0, 0, [], [], [], []], "tdigest.revrank": ["tdigest.revrank", -1, [], 0, 0, 0, [], [], [], []], "tdigest.byrank": ["tdigest.byrank", -1, [], 0, 0, 0, [], [], [], []], "tdigest.byrevrank": ["tdigest.byrevrank", -1, [], 0, 0, 0, [], [], [], []], "tdigest.info": ["tdigest.info", -1, [], 0, 0, 0, [], [], [], []]} \ No newline at end of file diff --git a/fakeredis/stack/__init__.py b/fakeredis/stack/__init__.py index 9e3ccbf2..4909f61c 100644 --- a/fakeredis/stack/__init__.py +++ b/fakeredis/stack/__init__.py @@ -1,4 +1,5 @@ from ._tdigest_mixin import TDigestCommandsMixin +from ._timeseries_mixin import TimeSeriesCommandsMixin from ._topk_mixin import TopkCommandsMixin # noqa: F401 try: @@ -41,4 +42,5 @@ class CMSCommandsMixin: # type: ignore # noqa: E303 "CFCommandsMixin", "CMSCommandsMixin", "TDigestCommandsMixin", + "TimeSeriesCommandsMixin", ] diff --git a/fakeredis/stack/_json_mixin.py b/fakeredis/stack/_json_mixin.py index c8d0f855..cf41ff1c 100644 --- a/fakeredis/stack/_json_mixin.py +++ b/fakeredis/stack/_json_mixin.py @@ -167,10 +167,9 @@ class JSONCommandsMixin: ZSet: b"zset", } - _db: helpers.Database - def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) + self._db: helpers.Database @staticmethod def _get_single( diff --git a/fakeredis/stack/_timeseries_mixin.py b/fakeredis/stack/_timeseries_mixin.py new file mode 100644 index 00000000..173fd1b2 --- /dev/null +++ b/fakeredis/stack/_timeseries_mixin.py @@ -0,0 +1,561 @@ +import time +from typing import List, Union, Optional, Any, Set, Dict + +from fakeredis import _msgs as msgs +from fakeredis._command_args_parsing import extract_args +from fakeredis._commands import command, Key, CommandItem, Int, Float, Timestamp +from fakeredis._helpers import Database, SimpleString, OK, SimpleError, casematch +from ._timeseries_model import TimeSeries, TimeSeriesRule, AGGREGATORS + + +class TimeSeriesCommandsMixin: # TimeSeries commands + _timeseries_keys: Set[bytes] = set() + DUPLICATE_POLICIES = [b"BLOCK", b"FIRST", b"LAST", b"MIN", b"MAX", b"SUM"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._db: Database + + @staticmethod + def _filter_expression_check(ts: TimeSeries, filter_expression: bytes) -> bool: + if not filter_expression: + return True + if filter_expression.find(b"!=") != -1: + if len(filter_expression.split(b"!=")) != 2: + raise SimpleError(msgs.TIMESERIES_BAD_FILTER_EXPRESSION) + label, value = filter_expression.split(b"!=") + if value == "-": + return label in ts.labels + + if value[0] == b"(" and value[-1] == b")": + values = set(value[1:-1].split(b",")) + return label in ts.labels and ts.labels[label] not in values + return label not in ts.labels or ts.labels[label] != value + if filter_expression.find(b"=") != -1: + if len(filter_expression.split(b"=")) != 2: + raise SimpleError(msgs.TIMESERIES_BAD_FILTER_EXPRESSION) + label, value = filter_expression.split(b"=") + if value == "-": + return label not in ts.labels + if value[0] == b"(" and value[-1] == b")": + values = set(value[1:-1].split(b",")) + return label in ts.labels and ts.labels[label] in values + return label in ts.labels and ts.labels[label] == value + raise SimpleError(msgs.TIMESERIES_BAD_FILTER_EXPRESSION) + + def _get_timeseries(self, filter_expressions: List[bytes]) -> List["TimeSeries"]: + res: List["TimeSeries"] = list() + TimeSeriesCommandsMixin._timeseries_keys = { + k for k in TimeSeriesCommandsMixin._timeseries_keys if k in self._db + } + for ts_key in TimeSeriesCommandsMixin._timeseries_keys: + ts = self._db.get(ts_key).value + if all([self._filter_expression_check(ts, expr) for expr in filter_expressions]): + res.append(ts) + return res + + @staticmethod + def _validate_duplicate_policy(duplicate_policy: bytes) -> bool: + return duplicate_policy is None or any( + [casematch(duplicate_policy, item) for item in TimeSeriesCommandsMixin.DUPLICATE_POLICIES] + ) + + def _create_timeseries(self, name: bytes, *args) -> TimeSeries: + (retention, encoding, chunk_size, duplicate_policy, (ignore_max_time_diff, ignore_max_val_diff)), left_args = ( + extract_args( + args, + ("+retention", "*encoding", "+chunk_size", "*duplicate_policy", "++ignore"), + error_on_unexpected=False, + ) + ) + retention = retention or 0 + encoding = encoding or b"COMPRESSED" + if not (casematch(encoding, b"COMPRESSED") or casematch(encoding, b"UNCOMPRESSED")): + raise SimpleError(msgs.BAD_SUBCOMMAND_MSG.format("TS.CREATE")) + encoding = encoding.lower() + chunk_size = chunk_size or 4096 + if chunk_size % 8 != 0: + raise SimpleError(msgs.TIMESERIES_BAD_CHUNK_SIZE) + if not self._validate_duplicate_policy(duplicate_policy): + raise SimpleError(msgs.TIMESERIES_INVALID_DUPLICATE_POLICY) + duplicate_policy = duplicate_policy.lower() if duplicate_policy else None + if len(left_args) > 0 and (not casematch(left_args[0], b"LABELS") or len(left_args) % 2 != 1): + raise SimpleError(msgs.BAD_SUBCOMMAND_MSG.format("TS.ADD")) + labels = dict(zip(left_args[1::2], left_args[2::2])) if len(left_args) > 0 else {} + + res = TimeSeries( + name=name, + database=self._db, + retention=retention, + encoding=encoding, + chunk_size=chunk_size, + duplicate_policy=duplicate_policy, + ignore_max_time_diff=ignore_max_time_diff, + ignore_max_val_diff=ignore_max_val_diff, + labels=labels, + ) + self._timeseries_keys.add(name) + return res + + @command(name="TS.INFO", fixed=(Key(TimeSeries),), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE) + def ts_info(self, key: CommandItem, *args: bytes) -> List[Any]: + if key.value is None: + raise SimpleError(msgs.TIMESERIES_KEY_DOES_NOT_EXIST) + return [ + b"totalSamples", + len(key.value.sorted_list), + b"memoryUsage", + len(key.value.sorted_list) * 8 + len(key.value.encoding), + b"firstTimestamp", + key.value.sorted_list[0][0] if len(key.value.sorted_list) > 0 else 0, + b"lastTimestamp", + key.value.sorted_list[-1][0] if len(key.value.sorted_list) > 0 else 0, + b"retentionTime", + key.value.retention, + b"chunkCount", + len(key.value.sorted_list) * 8 // key.value.chunk_size, + b"chunkSize", + key.value.chunk_size, + b"chunkType", + key.value.encoding, + b"duplicatePolicy", + key.value.duplicate_policy, + b"labels", + [[k, v] for k, v in key.value.labels.items()], + b"sourceKey", + key.value.source_key, + b"rules", + [ + [rule.dest_key.name, rule.bucket_duration, rule.aggregator.upper(), rule.align_timestamp] + for rule in key.value.rules + ], + b"keySelfName", + key.value.name, + b"Chunks", + [], + ] + + @command(name="TS.CREATE", fixed=(Key(TimeSeries),), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE) + def ts_create(self, key: CommandItem, *args: bytes) -> SimpleString: + if key.value is not None: + raise SimpleError(msgs.TIMESERIES_KEY_EXISTS) + key.value = self._create_timeseries(key.key, *args) + return OK + + @command(name="TS.ADD", fixed=(Key(TimeSeries), Timestamp, Float), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE) + def ts_add(self, key: CommandItem, timestamp: int, value: float, *args: bytes) -> int: + (on_duplicate,), left_args = extract_args(args, ("*on_duplicate",), error_on_unexpected=False) + if key.value is None: + key.update(self._create_timeseries(key.key, *args)) + if not self._validate_duplicate_policy(on_duplicate): + raise SimpleError(msgs.TIMESERIES_INVALID_DUPLICATE_POLICY) + res = key.value.add(timestamp, value, on_duplicate) + return res + + @command(name="TS.GET", fixed=(Key(TimeSeries),), repeat=(bytes,)) + def ts_get(self, key: CommandItem, *args: bytes) -> Optional[List[Union[int, float]]]: + if key.value is None: + raise SimpleError(msgs.TIMESERIES_KEY_DOES_NOT_EXIST) + return key.value.get() + + @command( + name="TS.MADD", + fixed=(Key(TimeSeries), Timestamp, Float), + repeat=(Key(TimeSeries), Timestamp, Float), + flags=msgs.FLAG_DO_NOT_CREATE, + ) + def ts_madd(self, *args: Any) -> List[int]: + if len(args) % 3 != 0: + raise SimpleError(msgs.WRONG_ARGS_MSG6) + results: List[int] = list() + for i in range(0, len(args), 3): + key, timestamp, value = args[i : i + 3] + if key.value is None: + raise SimpleError(msgs.TIMESERIES_KEY_DOES_NOT_EXIST) + results.append(key.value.add(timestamp, value)) + return results + + @command( + name="TS.DEL", + fixed=(Key(TimeSeries), Int, Int), + repeat=(), + flags=msgs.FLAG_DO_NOT_CREATE, + ) + def ts_del(self, key: CommandItem, from_ts: int, to_ts: int) -> bytes: + if key.value is None: + raise SimpleError(msgs.TIMESERIES_KEY_DOES_NOT_EXIST) + return key.value.delete(from_ts, to_ts) + + @command( + name="TS.CREATERULE", + fixed=(Key(TimeSeries), Key(TimeSeries), bytes, bytes, Int), + repeat=(Int,), + flags=msgs.FLAG_DO_NOT_CREATE, + ) + def ts_createrule( + self, + source_key: CommandItem, + dest_key: CommandItem, + _: bytes, + aggregator: bytes, + bucket_duration: int, + *args: int, + ) -> SimpleString: + if source_key.value is None: + raise SimpleError(msgs.TIMESERIES_KEY_DOES_NOT_EXIST) + if dest_key.value is None: + raise SimpleError(msgs.TIMESERIES_KEY_DOES_NOT_EXIST) + if len(args) > 1: + raise SimpleError(msgs.WRONG_ARGS_MSG6) + try: + align_timestamp = int(args[0]) if len(args) == 1 else 0 + except ValueError: + raise SimpleError(msgs.WRONG_ARGS_MSG6) + existing_rule = source_key.value.get_rule(dest_key.key) + if existing_rule is not None: + raise SimpleError(msgs.TIMESERIES_RULE_EXISTS) + if aggregator not in AGGREGATORS: + raise SimpleError(msgs.TIMESERIES_BAD_AGGREGATION_TYPE) + rule = TimeSeriesRule(source_key.value, dest_key.value, aggregator, bucket_duration, align_timestamp) + source_key.value.add_rule(rule) + return OK + + @command( + name="TS.DELETERULE", + fixed=(Key(TimeSeries), Key(TimeSeries)), + repeat=(), + flags=msgs.FLAG_DO_NOT_CREATE, + ) + def ts_deleterule(self, source_key: CommandItem, dest_key: CommandItem) -> bytes: + if source_key.value is None: + raise SimpleError(msgs.TIMESERIES_KEY_DOES_NOT_EXIST) + res: Optional[TimeSeriesRule] = source_key.value.get_rule(dest_key.key) + if res is None: + raise SimpleError(msgs.NOT_FOUND_MSG) + source_key.value.delete_rule(res) + return OK + + def _ts_inc_or_dec(self, key: CommandItem, addend: float, *args: bytes) -> bytes: + (ts,), left_args = extract_args( + args, + ("+timestamp",), + error_on_unexpected=False, + ) + if key.value is None: + key.update(self._create_timeseries(key.key, *left_args)) + timeseries = key.value + if ts is None: + if len(timeseries.sorted_list) == 0: + ts = int(time.time()) + else: + ts = timeseries.sorted_list[-1][0] + if len(timeseries.sorted_list) > 0 and ts < timeseries.sorted_list[-1][0]: + raise SimpleError(msgs.TIMESERIES_INVALID_TIMESTAMP) + try: + return key.value.incrby(ts, addend) + except ValueError: + msg = ( + msgs.TIMESERIES_TIMESTAMP_LOWER_THAN_MAX_V7 + if self.version >= (7,) + else msgs.TIMESERIES_TIMESTAMP_LOWER_THAN_MAX_V6 + ) + raise SimpleError(msg) + + @command( + name="TS.INCRBY", + fixed=(Key(TimeSeries), Float), + repeat=(bytes,), + flags=msgs.FLAG_DO_NOT_CREATE, + ) + def ts_incrby(self, key: CommandItem, addend: float, *args: bytes) -> bytes: + return self._ts_inc_or_dec(key, addend, *args) + + @command( + name="TS.DECRBY", + fixed=(Key(TimeSeries), Float), + repeat=(bytes,), + flags=msgs.FLAG_DO_NOT_CREATE, + ) + def ts_decrby(self, key: CommandItem, subtrahend: float, *args: bytes) -> bytes: + return self._ts_inc_or_dec(key, -subtrahend, *args) + + @command(name="TS.ALTER", fixed=(Key(TimeSeries),), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE) + def ts_alter(self, key: CommandItem, *args: bytes) -> bytes: + if key.value is None: + raise SimpleError(msgs.TIMESERIES_KEY_DOES_NOT_EXIST) + + ((retention, chunk_size, duplicate_policy, (ignore_max_time_diff, ignore_max_val_diff)), left_args) = ( + extract_args( + args, ("+retention", "+chunk_size", "*duplicate_policy", "++ignore"), error_on_unexpected=False + ) + ) + + if chunk_size is not None and chunk_size % 8 != 0: + raise SimpleError(msgs.TIMESERIES_BAD_CHUNK_SIZE) + if not self._validate_duplicate_policy(duplicate_policy): + raise SimpleError(msgs.TIMESERIES_INVALID_DUPLICATE_POLICY) + duplicate_policy = duplicate_policy.lower() if duplicate_policy else None + if len(left_args) > 0 and (not casematch(left_args[0], b"LABELS") or len(left_args) % 2 != 1): + raise SimpleError(msgs.BAD_SUBCOMMAND_MSG.format("TS.ADD")) + labels = dict(zip(left_args[1::2], left_args[2::2])) if len(left_args) > 0 else {} + + key.value.retention = retention or key.value.retention + key.value.chunk_size = chunk_size or key.value.chunk_size + key.value.duplicate_policy = duplicate_policy or key.value.duplicate_policy + key.value.ignore_max_time_diff = ignore_max_time_diff or key.value.ignore_max_time_diff + key.value.ignore_max_val_diff = ignore_max_val_diff or key.value.ignore_max_val_diff + key.value.labels = labels or key.value.labels + key.updated() + return OK + + def _range( + self, reverse: bool, ts: TimeSeries, from_ts: int, to_ts: int, *args: bytes + ) -> List[List[Union[int, float]]]: + RANGE_ARGS = ("latest", "++filter_by_value", "+count", "*align", "*+aggregation", "*buckettimestamp", "empty") + ( + latest, + (value_min, value_max), + count, + align, + (aggregator, bucket_duration), + bucket_timestamp, + empty, + ), left_args = extract_args(args, RANGE_ARGS, error_on_unexpected=False, left_from_first_unexpected=False) + latest = True + filter_ts: Optional[List[int]] = None + if len(left_args) > 0: + if not casematch(left_args[0], b"FILTER_BY_TS"): + raise SimpleError(msgs.WRONG_ARGS_MSG6) + left_args = left_args[1:] + filter_ts = [int(x) for x in left_args] + if aggregator is None and (align is not None or bucket_timestamp is not None or empty): + raise SimpleError(msgs.WRONG_ARGS_MSG6) + if bucket_timestamp is not None and bucket_timestamp not in (b"-", b"+", b"~"): + raise SimpleError(msgs.WRONG_ARGS_MSG6) + if align is not None: + if align == b"+": + align = to_ts + elif align == b"-": + align = from_ts + else: + align = int(align) + if aggregator is not None and aggregator not in AGGREGATORS: + raise SimpleError(msgs.TIMESERIES_BAD_AGGREGATION_TYPE) + if aggregator is None: + res = ts.range(from_ts, to_ts, value_min, value_max, count, filter_ts, reverse) + else: + res = ts.aggregate( + from_ts, + to_ts, + latest, + value_min, + value_max, + count, + filter_ts, + align, + aggregator, + bucket_duration, + bucket_timestamp, + empty, + reverse, + ) + + res = [[x[0], x[1]] for x in res] + return res + + @command( + name="TS.RANGE", fixed=(Key(TimeSeries), Timestamp, Timestamp), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE + ) + def ts_range(self, key: CommandItem, from_ts: int, to_ts: int, *args: bytes) -> List[List[Union[int, float]]]: + if key.value is None: + raise SimpleError(msgs.TIMESERIES_KEY_DOES_NOT_EXIST) + return self._range(False, key.value, from_ts, to_ts, *args) + + @command( + name="TS.REVRANGE", + fixed=(Key(TimeSeries), Timestamp, Timestamp), + repeat=(bytes,), + flags=msgs.FLAG_DO_NOT_CREATE, + ) + def ts_revrange(self, key: CommandItem, from_ts: int, to_ts: int, *args: bytes) -> List[List[Union[int, float]]]: + if key.value is None: + raise SimpleError(msgs.TIMESERIES_KEY_DOES_NOT_EXIST) + res = self._range(True, key.value, from_ts, to_ts, *args) + return res + + @command(name="TS.MGET", fixed=(bytes,), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE) + def ts_mget(self, *args: bytes) -> List[List[Union[bytes, List[List[Union[int, float]]]]]]: + latest, with_labels, selected_labels, filter_expression = False, False, None, None + i = 0 + while i < len(args): + if casematch(args[i], b"LATEST"): + latest = True # noqa: F841 + i += 1 + elif casematch(args[i], b"WITHLABELS"): + with_labels = True + i += 1 + elif casematch(args[i], b"SELECTED_LABELS"): + selected_labels = list() + i += 1 + while i < len(args) and casematch(args[i], b"FILTER"): + selected_labels.append(args[i]) + elif casematch(args[i], b"FILTER"): + filter_expression = list() + i += 1 + while i < len(args): + filter_expression.append(args[i]) + i += 1 + + if with_labels and selected_labels is not None: + raise SimpleError(msgs.WRONG_ARGS_MSG6.format("ts.mget")) + if filter_expression is None or len(filter_expression) == 0: + raise SimpleError(msgs.WRONG_ARGS_MSG6.format("ts.mget")) + + timeseries = self._get_timeseries(filter_expression) + if with_labels: + return [[ts.name, [[k, v] for (k, v) in ts.labels.items()], ts.get()] for ts in timeseries] + if selected_labels is not None: + res = [ + [ts.name, [[label, ts.labels[label]] for label in selected_labels if label in ts.labels], ts.get()] + for ts in timeseries + ] + else: + res = [[ts.name, [], ts.get()] for ts in timeseries] + return res + + @command(name="TS.QUERYINDEX", fixed=(bytes,), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE) + def ts_queryindex(self, *args: bytes) -> List[bytes]: + filter_expressions = list(args) + timeseries = self._get_timeseries(filter_expressions) + return [ts.name for ts in timeseries] + + def _group_by_label(self, reverse: bool, ts_list: List[Any], label: bytes, reducer: bytes) -> TimeSeries: + # ts_list: [[name, labels, measurements], ...] + reducer = reducer.lower() + if reducer not in AGGREGATORS: + raise SimpleError(msgs.TIMESERIES_BAD_AGGREGATION_TYPE) + ts_map: Dict[bytes, Dict[int, List[float]]] = dict() # label_value -> timestamp -> values + for ts in ts_list: + # Find label value + labels, label_value = ts[1], None + for label_name, current_value in labels: + if label_name == label: + label_value = current_value + break + if not label_value: + raise SimpleError(msgs.TIMESERIES_BAD_FILTER_EXPRESSION) + if label_value not in ts_map: + ts_map[label_value] = dict() + # Collect measurements + for timestamp, value in ts[2]: + if timestamp not in ts_map[label_value]: + ts_map[label_value][timestamp] = list() + ts_map[label_value][timestamp].append(value) + res = [] + for label_value, timestamp_values in ts_map.items(): + sorted_timestamps = sorted(timestamp_values.keys()) + name = f"{label.decode()}={label_value.decode()}" + sources = (", ".join([ts[0].decode() for ts in ts_list])).encode("utf-8") + labels = {label: label_value, b"__reducer__": reducer, b"__source__": sources} + measurements: List[int, float] = [ + [timestamp, float(AGGREGATORS[reducer](timestamp_values[timestamp]))] for timestamp in sorted_timestamps + ] + if reverse: + measurements.reverse() + res.append([name.encode("utf-8"), [[k, v] for (k, v) in labels.items()], measurements]) + return res + + def _mrange(self, reverse: bool, from_ts: int, to_ts: int, *args: bytes): + args_lower = [arg.lower() for arg in args] + arg_words = { + b"latest", + b"withlabels", + b"selected_labels", + b"filter", + b"groupby", + b"reduce", + b"count", + b"aggregation", + b"filter_by_value", + b"filter_by_ts", + b"align", + b"aggregation", + } + left_args = [] + latest, with_labels, selected_labels, filter_expression, group_by, reducer = ( + False, + False, + None, + None, + None, + None, + ) + i = 0 + while i < len(args_lower): + if args_lower[i] == b"latest": + latest = True # noqa: F841 + i += 1 + elif args_lower[i] == b"withlabels": + with_labels = True + i += 1 + elif args_lower[i] == b"selected_labels": + selected_labels = list() + i += 1 + while i < len(args_lower) and args_lower[i] not in arg_words: + selected_labels.append(args_lower[i]) + i += 1 + elif args_lower[i] == b"filter": + filter_expression = list() + i += 1 + while i < len(args_lower) and args_lower[i] not in arg_words: + filter_expression.append(args[i]) + i += 1 + elif i + 3 < len(args_lower) and args_lower[i] == b"groupby" and args_lower[i + 2] == b"reduce": + group_by = args[i + 1] + reducer = args_lower[i + 3] + i += 4 + else: + left_args.append(args[i]) + i += 1 + + if with_labels and selected_labels is not None: + raise SimpleError(msgs.WRONG_ARGS_MSG6.format("ts.mrange")) + if filter_expression is None or len(filter_expression) == 0: + raise SimpleError(msgs.WRONG_ARGS_MSG6.format("ts.mrange")) + + timeseries = self._get_timeseries(filter_expression) + if with_labels or (group_by is not None and reducer is not None): + res = [ + [ + ts.name, + [[k, v] for (k, v) in ts.labels.items()], + self._range(reverse, ts, from_ts, to_ts, *left_args), + ] + for ts in timeseries + ] + elif selected_labels is not None: + res = [ + [ + ts.name, + [[label, ts.labels[label]] for label in selected_labels if label in ts.labels], + self._range(reverse, ts, from_ts, to_ts, *left_args), + ] + for ts in timeseries + ] + else: + res = [[ts.name, [], self._range(reverse, ts, from_ts, to_ts, *left_args)] for ts in timeseries] + if group_by is not None and reducer is not None: + return self._group_by_label(reverse, res, group_by, reducer) + return res + + @command(name="TS.MRANGE", fixed=(Timestamp, Timestamp), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE) + def ts_mrange( + self, from_ts: int, to_ts: int, *args: bytes + ) -> List[List[Union[bytes, List[List[Union[int, float]]]]]]: + return self._mrange(False, from_ts, to_ts, *args) + + @command(name="TS.MREVRANGE", fixed=(Timestamp, Timestamp), repeat=(bytes,), flags=msgs.FLAG_DO_NOT_CREATE) + def ts_mrevrange( + self, from_ts: int, to_ts: int, *args: bytes + ) -> List[List[Union[bytes, List[List[Union[int, float]]]]]]: + return self._mrange(True, from_ts, to_ts, *args) diff --git a/fakeredis/stack/_timeseries_model.py b/fakeredis/stack/_timeseries_model.py new file mode 100644 index 00000000..a224bbf4 --- /dev/null +++ b/fakeredis/stack/_timeseries_model.py @@ -0,0 +1,282 @@ +from typing import List, Dict, Tuple, Union, Optional + +from fakeredis import _msgs as msgs +from fakeredis._helpers import Database, SimpleError + + +class TimeSeries: + + def __init__( + self, + name: bytes, + database: Database, + retention: int = 0, + encoding: bytes = b"compressed", + chunk_size: int = 4096, + duplicate_policy: bytes = b"block", + ignore_max_time_diff: int = 0, + ignore_max_val_diff: int = 0, + labels: Dict[str, str] = None, + source_key: Optional[bytes] = None, + ): + super().__init__() + self.name = name + self._db = database + self.retention = retention + self.encoding = encoding + self.chunk_size = chunk_size + self.duplicate_policy = duplicate_policy + self.ts_ind_map: Dict[int, int] = dict() # Map from timestamp to index in sorted_list + self.sorted_list: List[Tuple[int, float]] = list() + self.max_timestamp: int = 0 + self.labels = labels or {} + self.source_key = source_key + self.ignore_max_time_diff = ignore_max_time_diff + self.ignore_max_val_diff = ignore_max_val_diff + self.rules: List[TimeSeriesRule] = list() + + def add( + self, timestamp: int, value: float, duplicate_policy: Optional[bytes] = None + ) -> Union[int, None, List[None]]: + if self.retention != 0 and self.max_timestamp - timestamp > self.retention: + raise SimpleError(msgs.TIMESERIES_TIMESTAMP_OLDER_THAN_RETENTION) + if duplicate_policy is None: + duplicate_policy = self.duplicate_policy + if timestamp in self.ts_ind_map: # Duplicate policy + if duplicate_policy == b"block": + raise SimpleError(msgs.TIMESERIES_DUPLICATE_POLICY_BLOCK) + if duplicate_policy == b"first": + return timestamp + ind = self.ts_ind_map[timestamp] + curr_value = self.sorted_list[ind][1] + if duplicate_policy == b"max": + value = max(curr_value, value) + elif duplicate_policy == b"min": + value = min(curr_value, value) + self.sorted_list[ind] = (timestamp, value) + return timestamp + self.sorted_list.append((timestamp, value)) + self.ts_ind_map[timestamp] = len(self.sorted_list) - 1 + self.rules = [rule for rule in self.rules if rule.dest_key.name in self._db] + for rule in self.rules: + rule.add_record((timestamp, value)) + self.max_timestamp = max(self.max_timestamp, timestamp) + return timestamp + + def incrby(self, timestamp: int, value: float) -> Union[int, None]: + if len(self.sorted_list) == 0: + return self.add(timestamp, value) + if timestamp == self.max_timestamp: + ind = self.ts_ind_map[timestamp] + self.sorted_list[ind] = (timestamp, self.sorted_list[ind][1] + value) + elif timestamp > self.max_timestamp: + ind = self.ts_ind_map[self.max_timestamp] + self.add(timestamp, self.sorted_list[ind][1] + value) + else: # timestamp < self.sorted_list[ind][0] + raise ValueError() + + return timestamp + + def get(self) -> Optional[List[Union[int, float]]]: + if len(self.sorted_list) == 0: + return None + ind = self.ts_ind_map[self.max_timestamp] + return [self.sorted_list[ind][0], self.sorted_list[ind][1]] + + def delete(self, from_ts: int, to_ts: int) -> int: + prev_size = len(self.sorted_list) + self.sorted_list = [x for x in self.sorted_list if not (from_ts <= x[0] <= to_ts)] + self.ts_ind_map = {k: v for k, v in self.ts_ind_map.items() if not (from_ts <= k <= to_ts)} + return prev_size - len(self.sorted_list) + + def get_rule(self, dest_key: bytes) -> Optional["TimeSeriesRule"]: + for rule in self.rules: + if rule.dest_key.name == dest_key: + return rule + return None + + def add_rule(self, rule: "TimeSeriesRule") -> None: + self.rules.append(rule) + + def delete_rule(self, rule: "TimeSeriesRule") -> None: + self.rules.remove(rule) + rule.dest_key.source_key = None + + def range( + self, + from_ts: int, + to_ts: int, + value_min: Optional[float], + value_max: Optional[float], + count: Optional[int], + filter_ts: Optional[List[int]], + reverse: bool, + ) -> List[Tuple[int, float]]: + value_min = value_min or float("-inf") + value_max = value_max or float("inf") + res: List[Tuple[int, float]] = [ + x + for x in self.sorted_list + if (from_ts <= x[0] <= to_ts) + and value_min <= x[1] <= value_max + and (filter_ts is None or x[0] in filter_ts) + ] + if reverse: + res.reverse() + if count is not None: + return res[:count] + return res + + def aggregate( + self, + from_ts: int, + to_ts: int, + latest: bool, + value_min: Optional[float], + value_max: Optional[float], + count: Optional[int], + filter_ts: Optional[List[int]], + align: Optional[int], + aggregator: bytes, + bucket_duration: int, + bucket_timestamp: Optional[bytes], + empty: Optional[bool], + reverse: bool, + ) -> List[Tuple[int, float]]: + align = align or 0 + value_min = value_min or float("-inf") + value_max = value_max or float("inf") + rule = TimeSeriesRule(self, TimeSeries(b"", self._db), aggregator, bucket_duration) + for x in self.sorted_list: + if from_ts <= x[0] <= to_ts and value_min <= x[1] <= value_max and (filter_ts is None or x[0] in filter_ts): + rule.add_record((x[0], x[1]), bucket_timestamp) + + if latest and len(rule.current_bucket) > 0: + rule.apply_curr_bucket(bucket_timestamp) + if empty: + min_bucket_ts = rule.dest_key.sorted_list[0][0] + for ts in range(min_bucket_ts, rule.current_bucket_start_ts, bucket_duration): + if ts not in rule.dest_key.ts_ind_map: + rule.dest_key.add(ts, float("nan")) + rule.dest_key.sorted_list = sorted(rule.dest_key.sorted_list) + if reverse: + rule.dest_key.sorted_list.reverse() + if count: + return rule.dest_key.sorted_list[:count] + return rule.dest_key.sorted_list + + +class Aggregators: + @staticmethod + def var_p(values: List[float]) -> float: + if len(values) == 0: + return 0 + avg = sum(values) / len(values) + return sum((x - avg) ** 2 for x in values) / len(values) + + @staticmethod + def var_s(values: List[float]) -> float: + if len(values) == 0: + return 0 + avg = sum(values) / len(values) + return sum((x - avg) ** 2 for x in values) / (len(values) - 1) + + @staticmethod + def std_p(values: List[float]) -> float: + return Aggregators.var_p(values) ** 0.5 + + @staticmethod + def std_s(values: List[float]) -> float: + return Aggregators.var_s(values) ** 0.5 + + +AGGREGATORS = { + b"avg": lambda x: sum(x) / len(x), + b"sum": sum, + b"min": min, + b"max": max, + b"range": lambda x: max(x) - min(x), + b"count": len, + b"first": lambda x: x[0], + b"last": lambda x: x[-1], + b"std.p": Aggregators.std_p, + b"std.s": Aggregators.std_s, + b"var.p": Aggregators.var_p, + b"var.s": Aggregators.var_s, + b"twa": lambda x: 0, +} + + +def apply_aggregator( + bucket: List[Tuple[int, float]], bucket_start_ts: int, bucket_duration: int, aggregator: bytes +) -> float: + if len(bucket) == 0: + return 0.0 + if aggregator == b"twa": + total = 0.0 + curr_ts = bucket_start_ts + for i, (ts, val) in enumerate(bucket): + # next_ts = bucket[i + 1][0] if len(bucket) > i + 1 else bucket_start_ts + bucket_duration + total += (ts - curr_ts) * val + curr_ts = ts + total += val * (bucket_start_ts + bucket_duration - curr_ts) + + return total / bucket_duration + + relevant_values = [x[1] for x in bucket] + return AGGREGATORS[aggregator](relevant_values) + + +class TimeSeriesRule: + + def __init__( + self, + source_key: TimeSeries, + dest_key: TimeSeries, + aggregator: bytes, + bucket_duration: int, + align_timestamp: int = 0, + ): + self.source_key = source_key + self.dest_key = dest_key + self.aggregator = aggregator.lower() + self.bucket_duration = bucket_duration + self.align_timestamp = align_timestamp + self.current_bucket_start_ts: int = 0 + self.current_bucket: List[Tuple[int, float]] = list() + self.dest_key.source_key = source_key.name + + def add_record(self, record: Tuple[int, float], bucket_timestamp: Optional[bytes] = None) -> bool: + ts, val = record + bucket_start_ts = ts - (ts % self.bucket_duration) + self.align_timestamp + if self.current_bucket_start_ts == bucket_start_ts: + self.current_bucket.append(record) + if ( + self.current_bucket_start_ts != bucket_start_ts + or ts == self.current_bucket_start_ts + self.bucket_duration - 1 + ): + should_add = self.current_bucket_start_ts != bucket_start_ts + self.apply_curr_bucket(bucket_timestamp) + self.current_bucket_start_ts = ( + bucket_start_ts + if self.current_bucket_start_ts != bucket_start_ts + else self.current_bucket_start_ts + self.bucket_duration + ) + if should_add: + self.current_bucket.append(record) + return True + return False + + def apply_curr_bucket(self, bucket_timestamp: Optional[bytes] = None) -> None: + if len(self.current_bucket) == 0: + return + value = apply_aggregator( + self.current_bucket, self.current_bucket_start_ts, self.bucket_duration, self.aggregator + ) + self.current_bucket = list() + timestamp = self.current_bucket_start_ts + if bucket_timestamp == b"+": + timestamp = int(self.current_bucket_start_ts + self.bucket_duration) + elif bucket_timestamp == b"~": + timestamp = int(self.current_bucket_start_ts + self.bucket_duration / 2) + self.dest_key.add(timestamp, value) diff --git a/pyproject.toml b/pyproject.toml index bc0b6b23..b686f050 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ build-backend = "poetry.core.masonry.api" name = "fakeredis" packages = [ { include = "fakeredis" }, + { include = "LICENSE" }, ] version = "2.23.5" description = "Python implementation of redis API, can be used for testing purposes." diff --git a/test/test_mixins/test_server_commands.py b/test/test_mixins/test_server_commands.py index c7ef989c..b9be46b4 100644 --- a/test/test_mixins/test_server_commands.py +++ b/test/test_mixins/test_server_commands.py @@ -51,6 +51,7 @@ def test_command(r: redis.Redis): assert one_word_commands - set(commands_dict.keys()) == set() +@fake_only def test_command_count(r: redis.Redis): assert r.command_count() >= len([cmd for cmd in SUPPORTED_COMMANDS if " " not in cmd]) diff --git a/test/test_stack/test_timeseries.py b/test/test_stack/test_timeseries.py new file mode 100644 index 00000000..4cde2c1a --- /dev/null +++ b/test/test_stack/test_timeseries.py @@ -0,0 +1,791 @@ +import math +import time +from time import sleep + +import pytest +import redis + +from fakeredis import _msgs as msgs + +timeseries_tests = pytest.importorskip("probables") + + +def test_add_ts_close(r: redis.Redis): + ts1 = r.ts().add(5, "*", 1) + time.sleep(0.001) + ts2 = r.ts().add(5, "*", 1) + assert abs(ts2 - ts1) < 5 + + +def test_create_key_exist(r: redis.Redis): + assert r.ts().create(1) + with pytest.raises(redis.ResponseError) as e: + r.ts().create(1) + assert str(e.value) == msgs.TIMESERIES_KEY_EXISTS + + +def test_create_bad_duplicate_policy(r: redis.Redis): + with pytest.raises(redis.ResponseError) as e: + assert r.ts().create(1, duplicate_policy="bad") + assert str(e.value) == msgs.TIMESERIES_INVALID_DUPLICATE_POLICY + + +def test_create(r: redis.Redis): + assert r.ts().create(1) + assert r.ts().create(2, retention_msecs=5) + assert r.ts().create(3, labels={"Redis": "Labs"}) + assert r.ts().create(4, retention_msecs=20, labels={"Time": "Series"}) + info = r.ts().info(4) + assert 20 == info.get("retention_msecs") + assert "Series" == info["labels"]["Time"] + + # Test for a chunk size of 128 Bytes + assert r.ts().create("time-serie-1", chunk_size=128) + info = r.ts().info("time-serie-1") + assert 128 == info.get("chunk_size") + + +def test_create_duplicate_policy(r: redis.Redis): + # Test for duplicate policy + for duplicate_policy in ["block", "last", "first", "min", "max"]: + ts_name = f"time-serie-ooo-{duplicate_policy}" + assert r.ts().create(ts_name, duplicate_policy=duplicate_policy) + info = r.ts().info(ts_name) + assert duplicate_policy == info.get("duplicate_policy") + + +def test_alter(r: redis.Redis): + assert r.ts().create(1) + info = r.ts().info(1) + assert 0 == info.get("retention_msecs") + assert r.ts().alter(1, retention_msecs=10) + assert {} == r.ts().info(1)["labels"] + info = r.ts().info(1) + assert 10 == info.get("retention_msecs") + + assert r.ts().alter(1, labels={"Time": "Series"}) + assert "Series" == r.ts().info(1)["labels"]["Time"] + info = r.ts().info(1) + assert 10 == info.get("retention_msecs") + + # Test for a chunk size of 50 Bytes on TS.ALTER + with pytest.raises(redis.ResponseError) as e: + r.ts().alter(1, chunk_size=50) + assert str(e.value) == "TSDB: CHUNK_SIZE value must be a multiple of 8 in the range [48 .. 1048576]" + + +def test_alter_diplicate_policy(r: redis.Redis): + assert r.ts().create(1) + info = r.ts().info(1) + assert info.get("duplicate_policy") is None + + assert r.ts().alter(1, duplicate_policy="min") + info = r.ts().info(1) + assert "min" == info.get("duplicate_policy") + + +def test_add(r: redis.Redis): + assert 1 == r.ts().add(1, 1, 1) + assert 2 == r.ts().add(2, 2, 3, retention_msecs=10) + assert 3 == r.ts().add(3, 3, 2, labels={"Redis": "Labs"}) + assert 4 == r.ts().add(4, 4, 2, retention_msecs=10, labels={"Redis": "Labs", "Time": "Series"}) + + info = r.ts().info(4) + assert 10 == info.get("retention_msecs") + assert "Labs" == info["labels"]["Redis"] + + # Test for a chunk size of 128 Bytes on TS.ADD + assert r.ts().add("time-serie-1", 1, 10.0, chunk_size=128) + info = r.ts().info("time-serie-1") + assert 128 == info.get("chunk_size") + + +def test_add_before_retention(r: redis.Redis): + r.ts().create("time-serie-1", retention_msecs=1000) + assert r.ts().add("time-serie-1", 10000, 10.0) + with pytest.raises(redis.ResponseError) as e: + r.ts().add("time-serie-1", 2, 20.0) + assert str(e.value) == msgs.TIMESERIES_TIMESTAMP_OLDER_THAN_RETENTION + + +def test_add_before_last(r: redis.Redis): + r.ts().create("time-serie-1", retention_msecs=1000) + assert r.ts().add("time-serie-1", 100, 10.0) == 100 + assert r.ts().add("time-serie-1", 2, 20.0) == 2 + + assert r.ts().incrby("time-serie-1", 10.0, timestamp=100) == 100 + with pytest.raises(redis.ResponseError) as e: + r.ts().incrby("time-serie-1", 20.0, timestamp=2) + assert ( + str(e.value) == msgs.TIMESERIES_TIMESTAMP_LOWER_THAN_MAX_V7 + or str(e.value) == msgs.TIMESERIES_TIMESTAMP_LOWER_THAN_MAX_V6 + ) + + +def test_add_duplicate_policy(r: redis.Redis): + # Test for duplicate policy BLOCK + assert 1 == r.ts().add("time-serie-add-ooo-block", 1, 5.0) + with pytest.raises(Exception) as e: + r.ts().add("time-serie-add-ooo-block", 1, 5.0, on_duplicate="block") + assert str(e.value) == "TSDB: Error at upsert, update is not supported when DUPLICATE_POLICY is set to BLOCK mode" + + # Test for duplicate policy LAST + assert 1 == r.ts().add("time-serie-add-ooo-last", 1, 5.0) + assert 1 == r.ts().add("time-serie-add-ooo-last", 1, 10.0, on_duplicate="last") + assert 10.0 == r.ts().get("time-serie-add-ooo-last")[1] + + # Test for duplicate policy FIRST + assert 1 == r.ts().add("time-serie-add-ooo-first", 1, 5.0) + assert 1 == r.ts().add("time-serie-add-ooo-first", 1, 10.0, on_duplicate="first") + assert 5.0 == r.ts().get("time-serie-add-ooo-first")[1] + + # Test for duplicate policy MAX + assert 1 == r.ts().add("time-serie-add-ooo-max", 1, 5.0) + assert 1 == r.ts().add("time-serie-add-ooo-max", 1, 10.0, on_duplicate="max") + assert 10.0 == r.ts().get("time-serie-add-ooo-max")[1] + + # Test for duplicate policy MIN + assert 1 == r.ts().add("time-serie-add-ooo-min", 1, 5.0) + assert 1 == r.ts().add("time-serie-add-ooo-min", 1, 10.0, on_duplicate="min") + assert 5.0 == r.ts().get("time-serie-add-ooo-min")[1] + + +def test_madd(r: redis.Redis): + r.ts().create("a") + assert [1, 2, 3] == r.ts().madd([("a", 1, 5), ("a", 2, 10), ("a", 3, 15)]) + + +def test_incrby_decrby(r: redis.Redis): + for _ in range(100): + assert r.ts().incrby(1, 1) + sleep(0.001) + assert 100 == r.ts().get(1)[1] + for _ in range(100): + assert r.ts().decrby(1, 1) + sleep(0.001) + assert 0 == r.ts().get(1)[1] + + assert r.ts().incrby(2, 1.5, timestamp=5) + assert r.ts().get(2) == (5, 1.5) + assert r.ts().incrby(2, 2.25, timestamp=7) + assert r.ts().get(2) == (7, 3.75) + + assert r.ts().decrby(2, 1.5, timestamp=15) + assert r.ts().get(2) == (15, 2.25) + + # Test for a chunk size of 128 Bytes on TS.INCRBY + assert r.ts().incrby("time-serie-1", 10, chunk_size=128) + info = r.ts().info("time-serie-1") + assert 128 == info.get("chunk_size") + + # Test for a chunk size of 128 Bytes on TS.DECRBY + assert r.ts().decrby("time-serie-2", 10, chunk_size=128) + info = r.ts().info("time-serie-2") + assert 128 == info.get("chunk_size") + + +def test_create_and_delete_rule(r: redis.Redis): + # test rule creation + time = 100 + r.ts().create(1) + r.ts().create(2) + r.ts().createrule(1, 2, "avg", 100) + for i in range(50): + r.ts().add(1, time + i * 2, 1) + r.ts().add(1, time + i * 2 + 1, 2) + r.ts().add(1, time * 2, 1.5) + assert round(r.ts().get(2)[1], 1) == 1.5 + info1 = r.ts().info(1) + assert info1.rules[0][1] == 100 + info2 = r.ts().info(2) + assert info2["source_key"] == b"1" + + # test rule deletion + r.ts().deleterule(1, 2) + info = r.ts().info(1) + assert not info["rules"] + info2 = r.ts().info(2) + assert info2["source_key"] is None + + +def test_del_range(r: redis.Redis): + with pytest.raises(redis.ResponseError) as e: + r.ts().delete("test", 0, 100) + assert str(e.value) == msgs.TIMESERIES_KEY_DOES_NOT_EXIST + + for i in range(100): + r.ts().add(1, i, i % 7) + assert 22 == r.ts().delete(1, 0, 21) + assert [] == r.ts().range(1, 0, 21) + assert r.ts().range(1, 22, 22) == [(22, 1.0)] + + assert r.ts().delete(1, 60, 3) == 0 + + +def test_range(r: redis.Redis): + for i in range(100): + r.ts().add(1, i, i % 7) + assert 100 == len(r.ts().range(1, 0, 200)) + for i in range(100): + r.ts().add(1, i + 200, i % 7) + assert 200 == len(r.ts().range(1, 0, 500)) + + range_with_count_result = r.ts().range(1, 0, 500, count=10) + assert 10 == len(range_with_count_result) + assert (0, 0) == range_with_count_result[0] + + # last sample isn't returned + # assert 20 == len(r.ts().range(1, 0, 500, aggregation_type="avg", bucket_size_msec=10)) TODO + + +def test_range_advanced(r: redis.Redis): + for i in range(100): + r.ts().add(1, i, i % 7) + r.ts().add(1, i + 200, i % 7) + + assert 2 == len( + r.ts().range( + 1, + 0, + 500, + filter_by_ts=[i for i in range(10, 20)], + filter_by_min_value=1, + filter_by_max_value=2, + ) + ) + res = r.ts().range(1, 0, 10, aggregation_type="count", bucket_size_msec=10) + assert res == [(0, 10.0), (10, 1.0)] + + res = r.ts().range(1, 0, 10, aggregation_type="twa", bucket_size_msec=10) + assert res == [(0, pytest.approx(2.55, 0.1)), (10, 3.0)] + + +def test_range_latest(r: redis.Redis): + timeseries = r.ts() + timeseries.create("t1") + timeseries.create("t2") + timeseries.createrule("t1", "t2", aggregation_type="sum", bucket_size_msec=10) + timeseries.add("t1", 1, 1) + timeseries.add("t1", 2, 3) + timeseries.add("t1", 11, 7) + timeseries.add("t1", 13, 1) + assert timeseries.range("t1", 0, 20) == [(1, 1.0), (2, 3.0), (11, 7.0), (13, 1.0)] + assert timeseries.range("t2", 0, 10) == [(0, 4.0)] + + res = timeseries.range("t2", 0, 10, latest=True) + assert res == [(0, 4.0)] + assert timeseries.range("t2", 0, 9) == [(0, 4.0)] + + +def test_range_bucket_timestamp(r: redis.Redis): + timeseries = r.ts() + timeseries.create("t1") + timeseries.add("t1", 15, 1) + timeseries.add("t1", 17, 4) + timeseries.add("t1", 51, 3) + timeseries.add("t1", 73, 5) + timeseries.add("t1", 75, 3) + # assert timeseries.range("t1", 0, 100, align=0, aggregation_type="max", bucket_size_msec=10) == [ + # (10, 4.0), + # (50, 3.0), + # (70, 5.0), + # ] + assert timeseries.range( + "t1", + 0, + 100, + align=0, + aggregation_type="max", + bucket_size_msec=10, + bucket_timestamp="+", + ) == [(20, 4.0), (60, 3.0), (80, 5.0)] + + +def test_range_empty(r: redis.Redis): + timeseries = r.ts() + timeseries.create("t1") + timeseries.add("t1", 15, 1) + timeseries.add("t1", 17, 4) + timeseries.add("t1", 51, 3) + timeseries.add("t1", 73, 5) + timeseries.add("t1", 75, 3) + assert timeseries.range("t1", 0, 100, aggregation_type="max", bucket_size_msec=10) == [ + (10, 4.0), + (50, 3.0), + (70, 5.0), + ] + + res = timeseries.range("t1", 0, 100, aggregation_type="max", bucket_size_msec=10, empty=True) + for i in range(len(res)): + if math.isnan(res[i][1]): + res[i] = (res[i][0], None) + resp2_expected = [ + (10, 4.0), + (20, None), + (30, None), + (40, None), + (50, 3.0), + (60, None), + (70, 5.0), + ] + assert res == resp2_expected + + +def test_rev_range(r: redis.Redis): + for i in range(100): + r.ts().add(1, i, i % 7) + assert 100 == len(r.ts().range(1, 0, 200)) + for i in range(100): + r.ts().add(1, i + 200, i % 7) + assert 200 == len(r.ts().range(1, 0, 500)) + assert 20 == len(r.ts().revrange(1, 0, 500, aggregation_type="avg", bucket_size_msec=10)) + assert 10 == len(r.ts().revrange(1, 0, 500, count=10)) + assert 2 == len( + r.ts().revrange( + 1, + 0, + 500, + filter_by_ts=[i for i in range(10, 20)], + filter_by_min_value=1, + filter_by_max_value=2, + ) + ) + assert r.ts().revrange(1, 0, 10, aggregation_type="count", bucket_size_msec=10) == [(10, 1.0), (0, 10.0)] + + assert r.ts().revrange(1, 0, 10, aggregation_type="twa", bucket_size_msec=10) == [ + (10, pytest.approx(3.0, 0.1)), + (0, pytest.approx(2.55, 0.1)), + ] + + +@pytest.mark.onlynoncluster +def test_revrange_latest(r: redis.Redis): + timeseries = r.ts() + timeseries.create("t1") + timeseries.create("t2") + timeseries.createrule("t1", "t2", aggregation_type="sum", bucket_size_msec=10) + timeseries.add("t1", 1, 1) + timeseries.add("t1", 2, 3) + timeseries.add("t1", 11, 7) + timeseries.add("t1", 13, 1) + + assert timeseries.revrange("t2", 0, 10) == [(0, 4.0)] + assert timeseries.revrange("t2", 0, 10, latest=True) == [(0, 4.0)] + assert timeseries.revrange("t2", 0, 9, latest=True) == [(0, 4.0)] + + +def test_revrange_bucket_timestamp(r: redis.Redis): + timeseries = r.ts() + timeseries.create("t1") + timeseries.add("t1", 15, 1) + timeseries.add("t1", 17, 4) + timeseries.add("t1", 51, 3) + timeseries.add("t1", 73, 5) + timeseries.add("t1", 75, 3) + assert timeseries.revrange("t1", 0, 100, align=0, aggregation_type="max", bucket_size_msec=10) == [ + (70, 5.0), + (50, 3.0), + (10, 4.0), + ] + assert timeseries.range( + "t1", + 0, + 100, + align=0, + aggregation_type="max", + bucket_size_msec=10, + bucket_timestamp="+", + ) == [(20, 4.0), (60, 3.0), (80, 5.0)] + + +def test_revrange_empty(r: redis.Redis): + timeseries = r.ts() + timeseries.create("t1") + timeseries.add("t1", 15, 1) + timeseries.add("t1", 17, 4) + timeseries.add("t1", 51, 3) + timeseries.add("t1", 73, 5) + timeseries.add("t1", 75, 3) + assert timeseries.revrange("t1", 0, 100, align=0, aggregation_type="max", bucket_size_msec=10) == [ + (70, 5.0), + (50, 3.0), + (10, 4.0), + ] + res = timeseries.revrange("t1", 0, 100, align=0, aggregation_type="max", bucket_size_msec=10, empty=True) + for i in range(len(res)): + if math.isnan(res[i][1]): + res[i] = (res[i][0], None) + assert res == [ + (70, 5.0), + (60, None), + (50, 3.0), + (40, None), + (30, None), + (20, None), + (10, 4.0), + ] + + +@pytest.mark.onlynoncluster +def test_mrange(r: redis.Redis): + r.ts().create(1, labels={"Test": "This", "team": "ny"}) + r.ts().create(2, labels={"Test": "This", "Taste": "That", "team": "sf"}) + for i in range(100): + r.ts().add(1, i, i % 7) + r.ts().add(2, i, i % 11) + + res = r.ts().mrange(0, 200, filters=["Test=This"]) + assert 2 == len(res) + + assert 100 == len(res[0]["1"][1]) + + res = r.ts().mrange(0, 200, filters=["Test=This"], count=10) + assert 10 == len(res[0]["1"][1]) + + for i in range(100): + r.ts().add(1, i + 200, i % 7) + res = r.ts().mrange(0, 500, filters=["Test=This"], aggregation_type="avg", bucket_size_msec=10) + assert 2 == len(res) + assert 20 == len(res[0]["1"][1]) + + # test withlabels + assert {} == res[0]["1"][0] + res = r.ts().mrange(0, 200, filters=["Test=This"], with_labels=True) + assert {"Test": "This", "team": "ny"} == res[0]["1"][0] + + +def test_multi_range_advanced(r: redis.Redis): + r.ts().create(1, labels={"Test": "This", "team": "ny"}) + r.ts().create(2, labels={"Test": "This", "Taste": "That", "team": "sf"}) + for i in range(100): + r.ts().add(1, i, i % 7) + r.ts().add(2, i, i % 11) + + # test with selected labels + res = r.ts().mrange(0, 200, filters=["Test=This"], select_labels=["team"]) + + assert {"team": "ny"} == res[0]["1"][0] + assert {"team": "sf"} == res[1]["2"][0] + + # test with filterby + res = r.ts().mrange( + 0, + 200, + filters=["Test=This"], + filter_by_ts=[i for i in range(10, 20)], + filter_by_min_value=1, + filter_by_max_value=2, + ) + assert [(15, 1.0), (16, 2.0)] == res[0]["1"][1] + + # test groupby + res = r.ts().mrange(0, 3, filters=["Test=This"], groupby="Test", reduce="sum") + assert [(0, 0.0), (1, 2.0), (2, 4.0), (3, 6.0)] == res[0]["Test=This"][1] + res = r.ts().mrange(0, 3, filters=["Test=This"], groupby="Test", reduce="max") + assert [(0, 0.0), (1, 1.0), (2, 2.0), (3, 3.0)] == res[0]["Test=This"][1] + res = r.ts().mrange(0, 3, filters=["Test=This"], groupby="team", reduce="min") + assert 2 == len(res) + assert [(0, 0.0), (1, 1.0), (2, 2.0), (3, 3.0)] == res[0]["team=ny"][1] + assert [(0, 0.0), (1, 1.0), (2, 2.0), (3, 3.0)] == res[1]["team=sf"][1] + + # test align + res = r.ts().mrange( + 0, + 10, + filters=["team=ny"], + aggregation_type="count", + bucket_size_msec=10, + align="-", + ) + assert [(0, 10.0), (10, 1.0)] == res[0]["1"][1] + # res = r.ts().mrange( + # 0, + # 10, + # filters=["team=ny"], + # aggregation_type="count", + # bucket_size_msec=10, + # align=5, + # ) + # assert [(0, 5.0), (5, 6.0)] == res[0]["1"][1] + + +@pytest.mark.onlynoncluster +def test_mrange_latest(r: redis.Redis): + timeseries = r.ts() + timeseries.create("t1") + timeseries.create("t2", labels={"is_compaction": "true"}) + timeseries.create("t3") + timeseries.create("t4", labels={"is_compaction": "true"}) + timeseries.createrule("t1", "t2", aggregation_type="sum", bucket_size_msec=10) + timeseries.createrule("t3", "t4", aggregation_type="sum", bucket_size_msec=10) + timeseries.add("t1", 1, 1) + timeseries.add("t1", 2, 3) + timeseries.add("t1", 11, 7) + timeseries.add("t1", 13, 1) + timeseries.add("t3", 1, 1) + timeseries.add("t3", 2, 3) + timeseries.add("t3", 11, 7) + timeseries.add("t3", 13, 1) + + assert r.ts().mrange(0, 10, filters=["is_compaction=true"], latest=True) == [ + {"t2": [{}, [(0, 4.0)]]}, + {"t4": [{}, [(0, 4.0)]]}, + ] + + +@pytest.mark.onlynoncluster +def test_multi_reverse_range(r: redis.Redis): + r.ts().create(1, labels={"Test": "This", "team": "ny"}) + r.ts().create(2, labels={"Test": "This", "Taste": "That", "team": "sf"}) + for i in range(100): + r.ts().add(1, i, i % 7) + r.ts().add(2, i, i % 11) + + res = r.ts().mrange(0, 200, filters=["Test=This"]) + assert 2 == len(res) + assert 100 == len(res[0]["1"][1]) + + res = r.ts().mrange(0, 200, filters=["Test=This"], count=10) + assert 10 == len(res[0]["1"][1]) + + for i in range(100): + r.ts().add(1, i + 200, i % 7) + res = r.ts().mrevrange(0, 500, filters=["Test=This"], aggregation_type="avg", bucket_size_msec=10) + assert 2 == len(res) + + assert 20 == len(res[0]["1"][1]) + assert {} == res[0]["1"][0] + + # test withlabels + res = r.ts().mrevrange(0, 200, filters=["Test=This"], with_labels=True) + assert {"Test": "This", "team": "ny"} == res[0]["1"][0] + + # test with selected labels + res = r.ts().mrevrange(0, 200, filters=["Test=This"], select_labels=["team"]) + assert {"team": "ny"} == res[0]["1"][0] + assert {"team": "sf"} == res[1]["2"][0] + + # test filterby + res = r.ts().mrevrange( + 0, + 200, + filters=["Test=This"], + filter_by_ts=[i for i in range(10, 20)], + filter_by_min_value=1, + filter_by_max_value=2, + ) + assert [(16, 2.0), (15, 1.0)] == res[0]["1"][1] + + # test groupby + res = r.ts().mrevrange(0, 3, filters=["Test=This"], groupby="Test", reduce="sum") + assert [(3, 6.0), (2, 4.0), (1, 2.0), (0, 0.0)] == res[0]["Test=This"][1] + res = r.ts().mrevrange(0, 3, filters=["Test=This"], groupby="Test", reduce="max") + assert [(3, 3.0), (2, 2.0), (1, 1.0), (0, 0.0)] == res[0]["Test=This"][1] + res = r.ts().mrevrange(0, 3, filters=["Test=This"], groupby="team", reduce="min") + assert 2 == len(res) + assert [(3, 3.0), (2, 2.0), (1, 1.0), (0, 0.0)] == res[0]["team=ny"][1] + assert [(3, 3.0), (2, 2.0), (1, 1.0), (0, 0.0)] == res[1]["team=sf"][1] + + # test align + res = r.ts().mrevrange( + 0, + 10, + filters=["team=ny"], + aggregation_type="count", + bucket_size_msec=10, + align="-", + ) + assert [(10, 1.0), (0, 10.0)] == res[0]["1"][1] + # res = r.ts().mrevrange( + # 0, + # 10, + # filters=["team=ny"], + # aggregation_type="count", + # bucket_size_msec=10, + # align=1, + # ) + # assert [(1, 10.0), (0, 1.0)] == res[0]["1"][1] + + +@pytest.mark.onlynoncluster +def test_mrevrange_latest(r: redis.Redis): + timeseries = r.ts() + timeseries.create("t1") + timeseries.create("t2", labels={"is_compaction": "true"}) + timeseries.create("t3") + timeseries.create("t4", labels={"is_compaction": "true"}) + timeseries.createrule("t1", "t2", aggregation_type="sum", bucket_size_msec=10) + timeseries.createrule("t3", "t4", aggregation_type="sum", bucket_size_msec=10) + timeseries.add("t1", 1, 1) + timeseries.add("t1", 2, 3) + timeseries.add("t1", 11, 7) + timeseries.add("t1", 13, 1) + timeseries.add("t3", 1, 1) + timeseries.add("t3", 2, 3) + timeseries.add("t3", 11, 7) + timeseries.add("t3", 13, 1) + + assert r.ts().mrevrange(0, 10, filters=["is_compaction=true"], latest=True) == [ + {"t2": [{}, [(0, 4.0)]]}, + {"t4": [{}, [(0, 4.0)]]}, + ] + + +def test_get(r: redis.Redis): + name = "test" + r.ts().create(name) + assert r.ts().get(name) is None + r.ts().add(name, 2, 3) + assert 2 == r.ts().get(name)[0] + r.ts().add(name, 3, 4) + assert 4 == r.ts().get(name)[1] + + +@pytest.mark.onlynoncluster +def test_get_latest(r: redis.Redis): + timeseries = r.ts() + timeseries.create("t1") + timeseries.create("t2") + timeseries.createrule("t1", "t2", aggregation_type="sum", bucket_size_msec=10) + timeseries.add("t1", 1, 1) + timeseries.add("t1", 2, 3) + timeseries.add("t1", 11, 7) + timeseries.add("t1", 13, 1) + assert timeseries.get("t2") == (0, 4.0) + assert timeseries.get("t2", latest=True) == (0, 4.0) + + +def test_mget_errors(r: redis.Redis): + r.ts().create(1, labels={"Test": "This"}) + r.ts().create(2, labels={"Test": "This", "Taste": "That"}) + with pytest.raises(redis.ResponseError) as e: + r.ts().mget([]) + assert str(e.value).lower() == "wrong number of arguments for 'ts.mget' command" + + with pytest.raises(redis.ResponseError) as e: + r.ts().mget(["Test=(Th=is"], with_labels="true") + assert str(e.value) == "TSDB: failed parsing labels" + + +def test_mget(r: redis.Redis): + r.ts().create(1, labels={"Test": "This"}) + r.ts().create(2, labels={"Test": "This", "Taste": "That"}) + act_res = r.ts().mget(["Test=This"]) + exp_res = [{"1": [{}, None, None]}, {"2": [{}, None, None]}] + assert act_res == exp_res + + r.ts().add(1, "*", 15) + r.ts().add(2, "*", 25) + res = r.ts().mget(["Test=This"]) + assert 15 == res[0]["1"][2] + assert 25 == res[1]["2"][2] + res = r.ts().mget(["Taste=That"]) + assert 25 == res[0]["2"][2] + + # test with_labels + assert {} == res[0]["2"][0] + res = r.ts().mget(["Taste=That"], with_labels=True) + assert {"Taste": "That", "Test": "This"} == res[0]["2"][0] + + +@pytest.mark.onlynoncluster +def test_mget_latest(r: redis.Redis): + timeseries = r.ts() + timeseries.create("t1") + timeseries.create("t2", labels={"is_compaction": "true"}) + timeseries.createrule("t1", "t2", aggregation_type="sum", bucket_size_msec=10) + timeseries.add("t1", 1, 1) + timeseries.add("t1", 2, 3) + timeseries.add("t1", 11, 7) + timeseries.add("t1", 13, 1) + res = timeseries.mget(filters=["is_compaction=true"]) + assert res == [{"t2": [{}, 0, 4.0]}] + res = timeseries.mget(filters=["is_compaction=true"], latest=True) + assert res == [{"t2": [{}, 0, 4.0]}] + + +def test_info(r: redis.Redis): + r.ts().create(1, retention_msecs=5, labels={"currentLabel": "currentData"}) + info = r.ts().info(1) + assert 5 == info.get("retention_msecs") + assert info["labels"]["currentLabel"] == "currentData" + + +def testInfoDuplicatePolicy(r: redis.Redis): + r.ts().create(1, retention_msecs=5, labels={"currentLabel": "currentData"}) + info = r.ts().info(1) + assert info.get("duplicate_policy") is None + + r.ts().create("time-serie-2", duplicate_policy="min") + info = r.ts().info("time-serie-2") + assert info.get("duplicate_policy") == "min" + + +@pytest.mark.onlynoncluster +def test_query_index(r: redis.Redis): + r.ts().create(1, labels={"Test": "This"}) + r.ts().create(2, labels={"Test": "This", "Taste": "That"}) + assert 2 == len(r.ts().queryindex(["Test=This"])) + assert 1 == len(r.ts().queryindex(["Taste=That"])) + assert r.ts().queryindex(["Taste=That"]) == [2] + + +def test_pipeline(r: redis.Redis): + pipeline = r.ts().pipeline() + pipeline.create("with_pipeline") + for i in range(100): + pipeline.add("with_pipeline", i, 1.1 * i) + pipeline.execute() + + info = r.ts().info("with_pipeline") + assert 99 == info.get("last_timestamp") + assert 100 == info.get("total_samples") + + assert r.ts().get("with_pipeline")[1] == 99 * 1.1 + + +def test_uncompressed(r: redis.Redis): + r.ts().create("compressed") + r.ts().create("uncompressed", uncompressed=True) + compressed_info = r.ts().info("compressed") + uncompressed_info = r.ts().info("uncompressed") + + assert compressed_info["memory_usage"] != uncompressed_info["memory_usage"] + + +def test_create_rule_green(r: redis.Redis): + r.ts().create(1) + r.ts().create(2) + r.ts().createrule(1, 2, "avg", 100) + for i in range(50): + r.ts().add(1, 100 + i * 2, 1) + r.ts().add(1, 100 + i * 2 + 1, 2) + r.ts().add(1, 200, 1.5) + last_sample = r.ts().get(2) + assert last_sample[0] == 100 + assert round(last_sample[1], 5) == pytest.approx(1.5, 0.1) + info = r.ts().info(2) + assert info["source_key"] == b"1" + + +def test_create_rule_bad_aggregator(r: redis.Redis): + r.ts().create(1) + r.ts().create(2) + with pytest.raises(redis.ResponseError) as e: + r.ts().createrule(1, 2, "bad", 100, align_timestamp=50) + assert str(e.value) == msgs.TIMESERIES_BAD_AGGREGATION_TYPE + + +def test_create_rule_key_not_exist(r: redis.Redis): + with pytest.raises(redis.ResponseError) as e: + r.ts().createrule(1, 2, "avg", 100) + assert str(e.value) == msgs.TIMESERIES_KEY_DOES_NOT_EXIST + + +def test_create_rule_with_rule_to_dest_key_exists(r: redis.Redis): + r.ts().create(1) + r.ts().create(2) + r.ts().createrule(1, 2, "avg", 100) + with pytest.raises(redis.ResponseError) as e: + r.ts().createrule(1, 2, "avg", 100) + assert str(e.value) == msgs.TIMESERIES_RULE_EXISTS