Skip to content

Fix ZRangeArgs Rev+ByScore/ByLex incorrectly swapping Start/Stop#3751

Merged
ofekshenawa merged 2 commits into
masterfrom
copilot/fix-zrangestore-unexpected-value
Mar 25, 2026
Merged

Fix ZRangeArgs Rev+ByScore/ByLex incorrectly swapping Start/Stop#3751
ofekshenawa merged 2 commits into
masterfrom
copilot/fix-zrangestore-unexpected-value

Conversation

Copilot AI commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

ZRangeStore (and ZRangeArgs/ZRangeArgsWithScores) returned 0 instead of the correct count when using Rev: true with ByScore or ByLex. The client was swapping Start/Stop before sending to Redis, but Redis 6.2+ ZRANGE with REV already expects the caller to supply the range in high→low order — the swap was corrupting the query.

Changes

  • sortedset_commands.go — Remove the conditional Start/Stop swap in appendArgs. Values are now forwarded to Redis as-is.
  • commands_test.go — Update existing Rev+ByScore/Rev+ByLex tests to pass Start/Stop in the correct high→low order (previously the tests masked the bug by relying on the swap). Add a regression test for the exact scenario from the bug report.
  • Docs — Clarify on ZRangeArgs that when Rev is set, Start must be the higher bound and Stop the lower bound.

Before / After

// Before: had to pass low→high and rely on client swap (broken for ZRangeStore)
client.ZRangeStore(ctx, dst, redis.ZRangeArgs{
    Key: src, Start: 3, Stop: 5, ByScore: true, Rev: true,
}) // → 0 (wrong)

// After: pass high→low, matching Redis ZRANGE REV semantics
client.ZRangeStore(ctx, dst, redis.ZRangeArgs{
    Key: src, Start: 5, Stop: 3, ByScore: true, Rev: true,
}) // → 3 (correct)
Original prompt

This section details on the original issue you should resolve

<issue_title>bug: The ZRANGESTORE command returns an unexpected value when using the BYSCORE and REV parameter</issue_title>
<issue_description>

Expected Behavior

If you use the following command to write a zset type key
zadd myzset 1 a 2 b 3 c 4 d 5 e
Then use the following zrangestore command to query the data, expecting to return a result of 3
zrangestore {myzset}2 myzset 5 3 BYSCORE REV

> zadd myzset 1 a 2 b 3 c 4 d 5 e
> (integer) 5
> zrangestore {myzset}2 myzset 5 3 BYSCORE REV
> (integer) 3

Current Behavior

Using go-redis actually returns a value of 0

Possible Solution

Due to the processing of SCORE and REV parameters in Redis, there is no need for special handling before the go redis client issues commands.
The bug fixing method only requires modifying the following code

func (z ZRangeArgs) appendArgs(args []interface{}) []interface{} {
	// For Rev+ByScore/ByLex, we need to adjust the position of <Start> and <Stop>.
	if z.Rev && (z.ByScore || z.ByLex) {
		args = append(args, z.Key, z.Stop, z.Start)
	} else {
		args = append(args, z.Key, z.Start, z.Stop)
	}
        ......
	return args
}

The modified code is as follows

func (z ZRangeArgs) appendArgs(args []interface{}) []interface{} {
	args = append(args, z.Key, z.Start, z.Stop)
        ......
	return args
}

Steps to Reproduce

Use the following code to reproduce

client := redis.NewClient(&redis.Options{
		Addr:     "xxxx:6379",
		Password: "xxxxxx",
	})
	defer client.Close()
	ctx := context.Background()

	src := "myzset"
	dst := "{myzset}2"

	client.Del(ctx, src)
	client.ZAdd(ctx, src,
		redis.Z{Score: 1, Member: "a"},
		redis.Z{Score: 2, Member: "b"},
		redis.Z{Score: 3, Member: "c"},
		redis.Z{Score: 4, Member: "d"},
		redis.Z{Score: 5, Member: "e"},
	)
	res, err := client.ZRangeStore(ctx, dst, redis.ZRangeArgs{
		Key:     src,
		Start:   5,
		Stop:    3,
		ByScore: true,
		Rev:     true,
	}).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(res)

Context (Environment)

go-redis version: v9.18.0

Detailed Description

Possible Implementation

</issue_description>

Comments on the Issue (you are @copilot in this section)

@ndyakov @luoming1224 Thank you for reporting this. At first glance it looks correct, I will double check. Feel free to open a PR with the fix or I can do it around end of week.

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.


Note

Medium Risk
Changes argument ordering for ZRANGE/ZRANGESTORE when using Rev with ByScore/ByLex, which can affect query results for existing callers that relied on the previous (incorrect) swap. Covered by updated and expanded regression tests.

Overview
Fixes ZRangeArgs.appendArgs to no longer swap Start/Stop when Rev is combined with ByScore or ByLex, so ZRANGE/ZRANGESTORE match Redis’ expected high→low range semantics.

Updates commands_test.go to pass reversed bounds explicitly for Rev cases and adds a regression test reproducing the ZRANGESTORE ... BYSCORE REV bug. Also refreshes the example/otel module/toolchain and bumps OpenTelemetry-related dependencies.

Written by Cursor Bugbot for commit ecb01d9. This will update automatically on new commits. Configure here.

@jit-ci

jit-ci Bot commented Mar 25, 2026

Copy link
Copy Markdown

🛡️ Jit Security Scan Results

CRITICAL HIGH MEDIUM

✅ No security findings were detected in this PR


Security scan by Jit

Copilot AI changed the title [WIP] Fix ZRANGESTORE command to return expected value with BYSCORE and REV Fix ZRangeArgs Rev+ByScore/ByLex incorrectly swapping Start/Stop Mar 25, 2026
Copilot AI requested a review from ndyakov March 25, 2026 13:42

@ndyakov ndyakov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good, @ofekshenawa feel free to review and merge

@ndyakov ndyakov marked this pull request as ready for review March 25, 2026 13:44
@ndyakov ndyakov added the bug label Mar 25, 2026
@ofekshenawa ofekshenawa merged commit f37dbd0 into master Mar 25, 2026
56 of 57 checks passed
@ofekshenawa ofekshenawa deleted the copilot/fix-zrangestore-unexpected-value branch March 25, 2026 22:53
tmchow added a commit to tmchow/rueidis that referenced this pull request Apr 5, 2026
Port of redis/go-redis#3751. Redis 6.2+ ZRANGE with REV expects the
caller to supply the range in high-to-low order. The client-side swap
was corrupting queries, causing ZRangeStore to return 0 instead of the
correct count.

Fixes redis#978

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rueian pushed a commit to redis/rueidis that referenced this pull request Apr 6, 2026
## Summary

Ports redis/go-redis#3751 to rueidiscompat. `ZRangeStore` (and
`zRangeArgs`/`ZRangeArgsWithScores`) returned 0 instead of the correct
count when using `Rev: true` with `ByScore` or `ByLex`. The client was
swapping `Start`/`Stop` before sending to Redis, but Redis 6.2+ `ZRANGE`
with `REV` already expects the caller to supply the range in high-to-low
order.

Removed the conditional swap in all three locations:
- `Compat.zRangeArgs`
- `Compat.ZRangeStore`
- `CacheCompat.zRangeArgs`

Updated affected tests to pass `Start`/`Stop` in the correct high-to-low
order.

## Before / After

```go
// Before: had to pass low-to-high and rely on client swap (broken for ZRangeStore)
adapter.ZRangeStore(ctx, dst, ZRangeArgs{
    Key: src, Start: 3, Stop: 5, ByScore: true, Rev: true,
}) // returned 0 (wrong)

// After: pass high-to-low, matching Redis ZRANGE REV semantics
adapter.ZRangeStore(ctx, dst, ZRangeArgs{
    Key: src, Start: 5, Stop: 3, ByScore: true, Rev: true,
}) // returns correct count
```

Fixes #978

This contribution was developed with AI assistance (Codex).

[![Compound
Engineering](https://img.shields.io/badge/Built_with-Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes argument ordering sent to Redis for `ZRANGE`/`ZRANGESTORE`
when using `Rev` with `ByScore`/`ByLex`, which can affect query results
and stored set contents if callers relied on the previous (incorrect)
swap.
> 
> **Overview**
> Fixes `ZRangeArgs`/`ZRangeArgsWithScores` and `ZRangeStore` to
**always send `Start` then `Stop`** to Redis, removing the special-case
swap previously done for `Rev` + `ByScore`/`ByLex` (also applied to the
cached adapter).
> 
> Updates tests to pass `Start`/`Stop` in the expected high-to-low order
when `Rev: true`, aligning behavior with Redis 6.2+ `ZRANGE ... REV`
semantics and correcting `ZRANGESTORE` result counts.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
5f52ae6. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rueian pushed a commit to valkey-io/valkey-go that referenced this pull request Apr 9, 2026
## Summary

Ports redis/go-redis#3751 to rueidiscompat. `ZRangeStore` (and
`zRangeArgs`/`ZRangeArgsWithScores`) returned 0 instead of the correct
count when using `Rev: true` with `ByScore` or `ByLex`. The client was
swapping `Start`/`Stop` before sending to Redis, but Redis 6.2+ `ZRANGE`
with `REV` already expects the caller to supply the range in high-to-low
order.

Removed the conditional swap in all three locations:
- `Compat.zRangeArgs`
- `Compat.ZRangeStore`
- `CacheCompat.zRangeArgs`

Updated affected tests to pass `Start`/`Stop` in the correct high-to-low
order.

## Before / After

```go
// Before: had to pass low-to-high and rely on client swap (broken for ZRangeStore)
adapter.ZRangeStore(ctx, dst, ZRangeArgs{
    Key: src, Start: 3, Stop: 5, ByScore: true, Rev: true,
}) // returned 0 (wrong)

// After: pass high-to-low, matching Redis ZRANGE REV semantics
adapter.ZRangeStore(ctx, dst, ZRangeArgs{
    Key: src, Start: 5, Stop: 3, ByScore: true, Rev: true,
}) // returns correct count
```

Fixes #978

This contribution was developed with AI assistance (Codex).

[![Compound
Engineering](https://img.shields.io/badge/Built_with-Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes argument ordering sent to Redis for `ZRANGE`/`ZRANGESTORE`
when using `Rev` with `ByScore`/`ByLex`, which can affect query results
and stored set contents if callers relied on the previous (incorrect)
swap.
> 
> **Overview**
> Fixes `ZRangeArgs`/`ZRangeArgsWithScores` and `ZRangeStore` to
**always send `Start` then `Stop`** to Redis, removing the special-case
swap previously done for `Rev` + `ByScore`/`ByLex` (also applied to the
cached adapter).
> 
> Updates tests to pass `Start`/`Stop` in the expected high-to-low order
when `Rev: true`, aligning behavior with Redis 6.2+ `ZRANGE ... REV`
semantics and correcting `ZRANGESTORE` result counts.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
5f52ae6aad1e2648da29e7684ddd3adce3aacc09. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

bug: The ZRANGESTORE command returns an unexpected value when using the BYSCORE and REV parameter

3 participants