Skip to content

Pass zrevrange key as a list in options["keys"] to match sibling range commands - #4244

Merged
petyaslavova merged 5 commits into
redis:masterfrom
uttam12331:fix/zrevrange-keys-list
Aug 5, 2026
Merged

petyaslavova merged 5 commits into
redis:masterfrom
uttam12331:fix/zrevrange-keys-list

Conversation

@uttam12331

@uttam12331 uttam12331 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

SortedSetCommands.zrevrange stores its key in options["keys"] as a bare string, while every sibling range command passes it as a list:

# zrange / _zrange
options["keys"] = [name]
# zrangebyscore
options["keys"] = [name]
# zrevrangebyscore
options["keys"] = [name]
# zrevrange  <-- inconsistent
options["keys"] = name

Impact

options["keys"] is consumed as a sequence of keys. When it is a bare string, consumers iterate it character by character. The clearest case is client-side caching in redis/connection.py:

self._current_command_cache_key = CacheKey(
    command=args[0], redis_keys=tuple(kwargs.get("keys")), redis_args=args
)

For zrevrange("myzset", 0, -1) this builds redis_keys=('m', 'y', 'z', 's', 'e', 't') instead of ('myzset',), so the cache key / invalidation mapping for zrevrange is wrong.

Fix

Wrap the key in a list, matching the other range commands:

-        options["keys"] = name
+        options["keys"] = [name]

Tests

Added test_zrevrange_passes_key_as_list, which mocks execute_command and asserts zrevrange passes keys=["myzset"]. It fails on the previous behavior ("myzset" != ["myzset"]) and passes with the fix. Verified locally without a server.


Note

Low Risk
One-line behavioral fix in command options plus cache regression tests; no auth, security, or broad API changes.

Overview
zrevrange now sets options["keys"] to [name] instead of a bare string, aligning with zrange, zrangebyscore, and other range commands.

That fixes client-side caching: redis_keys is built from tuple(kwargs["keys"]), so a string key was iterated per character (('m','y','z',...)) rather than as a single Redis key (('myzset',)), breaking cache lookup and invalidation for ZREVRANGE.

A regression test exercises cached zrevrange results and invalidation when the sorted set is updated from another client (single-connection and pool modes).

Reviewed by Cursor Bugbot for commit 9f0d891. Bugbot is set up for automated code reviews on this repo. Configure here.

uttam12331 and others added 2 commits August 3, 2026 20:46
ZREVRANGE stored its key in options["keys"] as a bare string, while every
sibling range command (zrange, zrangebyscore, zrevrangebyscore) passes it as
a list: options["keys"] = [name].

Consumers of options["keys"] iterate it as a sequence of keys. For a bare
string that walks the key character by character -- for example client-side
cache key construction builds CacheKey(redis_keys=tuple(keys)), turning key
"myzset" into ('m', 'y', 'z', 's', 'e', 't') instead of ('myzset',).

Wrap the key in a list to match the other range commands, and add a
regression test asserting zrevrange passes keys=[name].

@petyaslavova petyaslavova left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @uttam12331, thank you for your contribution!
That was a nice catch - the issue is a real defect, and it's really nice that you have found and fixed it!

Two things before we can merge:

  1. Please check linters errors.
  2. Please add the regression test at the level where the defect is observable, rather than asserting the internal keys kwarg. tests/test_cache.py::test_get_from_given_cache is a good model: cache a zrevrange result, mutate the sorted set from a second client, and assert both the CacheKey shape (redis_keys=("myzset",)) and that the entry is invalidated. That test fails on master and passes with your fix, and it protects the behavior rather than the plumbing.

Separately, and please do not expand this PR for it, in case you are interested in another contribution :) -->ZRANK and ZREVRANK are in the cache allow list but pass no keys at all, so they currently will raise ValueError("Cannot create cache key.") when client-side caching is enabled.

Comment thread tests/test_commands.py Outdated
# character instead of treating it as a single key.
with mock.patch.object(r, "execute_command", return_value=[]) as execute_command:
r.zrevrange("myzset", 0, -1)
assert execute_command.call_args.kwargs["keys"] == ["myzset"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be great if you also add a test that goes through the client-side caching case.. - this one can go into tests/test_cache.py

Address review: replace the test that asserted the internal `keys` kwarg with
a behavior-level regression test in tests/test_cache.py. It caches a
`zrevrange` result, asserts the entry is stored under the whole-key cache key
(`redis_keys=("myzset",)`) rather than per-character, and that mutating the
sorted set from a second client invalidates it. This fails on master and
passes with the fix, and protects the behavior rather than the plumbing.
@uttam12331

Copy link
Copy Markdown
Contributor Author

Thanks for the review @petyaslavova, and glad it was a useful catch!

Addressed both points:

  • Linters: fixed (removed the over-long line; ruff check + ruff format --check now clean).
  • Regression test: moved it to the behavior level in tests/test_cache.py (test_zrevrange_cache_key_uses_whole_key, modeled on test_get_from_given_cache). It caches a zrevrange result, asserts the entry is stored under redis_keys=("myzset",) (rather than per-character), and that mutating the sorted set from a second client invalidates it. Removed the old test that asserted the internal keys kwarg.

And thanks for the ZRANK/ZREVRANK pointer — I'd be happy to take that on in a separate PR so this one stays focused.

@petyaslavova petyaslavova left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@petyaslavova
petyaslavova merged commit 854660d into redis:master Aug 5, 2026
730 of 731 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants