This is an automated email from the ASF dual-hosted git repository.

jensdeppe pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/geode.git


The following commit(s) were added to refs/heads/develop by this push:
     new 7835dc6  GEODE-9184: Add Radish ZLEXCOUNT command (#6735)
7835dc6 is described below

commit 7835dc65c4901af38955ca466d098e3ebaed2463
Author: Jens Deppe <[email protected]>
AuthorDate: Thu Aug 19 09:17:01 2021 -0700

    GEODE-9184: Add Radish ZLEXCOUNT command (#6735)
---
 .../ZLexCountNativeRedisAcceptanceTest.java        |  34 +++
 .../server/AbstractHitsMissesIntegrationTest.java  |   7 +-
 .../AbstractZLexCountIntegrationTest.java          | 247 +++++++++++++++++++++
 .../sortedset/ZLexCountIntegrationTest.java        |  30 +++
 .../geode/redis/internal/RedisCommandType.java     |   2 +
 .../redis/internal/data/NullRedisSortedSet.java    |  25 ++-
 .../geode/redis/internal/data/RedisSortedSet.java  |  24 ++
 .../RedisSortedSetCommandsFunctionExecutor.java    |   5 +
 .../executor/sortedset/RedisSortedSetCommands.java |   2 +
 .../sortedset/SortedSetLexRangeOptions.java        |   2 +-
 .../executor/sortedset/ZLexCountExecutor.java      |  50 +++++
 .../redis/internal/SupportedCommandsJUnitTest.java |   1 +
 .../redis/internal/data/RedisSortedSetTest.java    |  79 +++++++
 13 files changed, 496 insertions(+), 12 deletions(-)

diff --git 
a/geode-apis-compatible-with-redis/src/acceptanceTest/java/org/apache/geode/redis/internal/executor/sortedset/ZLexCountNativeRedisAcceptanceTest.java
 
b/geode-apis-compatible-with-redis/src/acceptanceTest/java/org/apache/geode/redis/internal/executor/sortedset/ZLexCountNativeRedisAcceptanceTest.java
new file mode 100644
index 0000000..1a1caf5
--- /dev/null
+++ 
b/geode-apis-compatible-with-redis/src/acceptanceTest/java/org/apache/geode/redis/internal/executor/sortedset/ZLexCountNativeRedisAcceptanceTest.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license
+ * agreements. See the NOTICE file distributed with this work for additional 
information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache 
License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the 
License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software 
distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+ * or implied. See the License for the specific language governing permissions 
and limitations under
+ * the License.
+ */
+package org.apache.geode.redis.internal.executor.sortedset;
+
+import org.junit.ClassRule;
+
+import org.apache.geode.redis.NativeRedisClusterTestRule;
+
+public class ZLexCountNativeRedisAcceptanceTest extends 
AbstractZLexCountIntegrationTest {
+  @ClassRule
+  public static NativeRedisClusterTestRule server = new 
NativeRedisClusterTestRule();
+
+  @Override
+  public int getPort() {
+    return server.getExposedPorts().get(0);
+  }
+
+  @Override
+  public void flushAll() {
+    server.flushAll();
+  }
+}
diff --git 
a/geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/server/AbstractHitsMissesIntegrationTest.java
 
b/geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/server/AbstractHitsMissesIntegrationTest.java
index 0cf091e..c184712 100644
--- 
a/geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/server/AbstractHitsMissesIntegrationTest.java
+++ 
b/geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/server/AbstractHitsMissesIntegrationTest.java
@@ -249,7 +249,12 @@ public abstract class AbstractHitsMissesIntegrationTest 
implements RedisIntegrat
 
   @Test
   public void testZIncrBy() {
-    runCommandAndAssertNoStatUpdates("key", (k, m) -> jedis.zincrby(k, 100.0, 
m));
+    runCommandAndAssertNoStatUpdates(SORTED_SET_KEY, (k, m) -> 
jedis.zincrby(k, 100.0, m));
+  }
+
+  @Test
+  public void testZLexCount() {
+    runCommandAndAssertHitsAndMisses(SORTED_SET_KEY, k -> jedis.zlexcount(k, 
"-", "+"));
   }
 
   @Test
diff --git 
a/geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/sortedset/AbstractZLexCountIntegrationTest.java
 
b/geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/sortedset/AbstractZLexCountIntegrationTest.java
new file mode 100644
index 0000000..b8128b1
--- /dev/null
+++ 
b/geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/sortedset/AbstractZLexCountIntegrationTest.java
@@ -0,0 +1,247 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license
+ * agreements. See the NOTICE file distributed with this work for additional 
information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache 
License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the 
License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software 
distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+ * or implied. See the License for the specific language governing permissions 
and limitations under
+ * the License.
+ */
+package org.apache.geode.redis.internal.executor.sortedset;
+
+import static 
org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertAtLeastNArgs;
+import static 
org.apache.geode.redis.internal.RedisConstants.ERROR_MIN_MAX_NOT_A_VALID_STRING;
+import static 
org.apache.geode.test.dunit.rules.RedisClusterStartupRule.BIND_ADDRESS;
+import static 
org.apache.geode.test.dunit.rules.RedisClusterStartupRule.REDIS_CLIENT_TIMEOUT;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+import redis.clients.jedis.Protocol;
+
+import org.apache.geode.redis.RedisIntegrationTest;
+
+@RunWith(JUnitParamsRunner.class)
+public abstract class AbstractZLexCountIntegrationTest implements 
RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final int SCORE = 1;
+  public static final String BASE_MEMBER_NAME = "v";
+
+  JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), 
REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  @Test
+  public void shouldError_givenWrongNumberOfArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.ZLEXCOUNT, 3);
+  }
+
+  @Test
+  @Parameters({"a", "--", "++"})
+  public void shouldError_givenInvalidMinOrMax(String invalidArgument) {
+    assertThatThrownBy(() -> jedis.zlexcount("fakeKey", invalidArgument, "+"))
+        .hasMessageContaining(ERROR_MIN_MAX_NOT_A_VALID_STRING);
+    assertThatThrownBy(() -> jedis.zlexcount("fakeKey", "-", invalidArgument))
+        .hasMessageContaining(ERROR_MIN_MAX_NOT_A_VALID_STRING);
+    assertThatThrownBy(() -> jedis.zlexcount("fakeKey", invalidArgument, 
invalidArgument))
+        .hasMessageContaining(ERROR_MIN_MAX_NOT_A_VALID_STRING);
+  }
+
+  @Test
+  public void shouldReturnZero_givenNonExistentKey() {
+    assertThat(jedis.zlexcount("fakeKey", "-", "+")).isEqualTo(0);
+  }
+
+  @Test
+  public void shouldReturnZero_givenMinGreaterThanMax() {
+    jedis.zadd(KEY, SCORE, "member");
+
+    // Range + <= result <= -
+    assertThat(jedis.zlexcount(KEY, "+", "-")).isEqualTo(0);
+    // Range z <= result <= a
+    assertThat(jedis.zlexcount(KEY, "[z", "[a")).isEqualTo(0);
+  }
+
+  @Test
+  public void shouldReturnOne_givenMemberNameInRange() {
+    String memberName = "member";
+    jedis.zadd(KEY, SCORE, memberName);
+
+    // Range m <= result <= n
+    assertThat(jedis.zlexcount(KEY, "[m", "[n")).isEqualTo(1);
+    // Range -infinity <= result <= n
+    assertThat(jedis.zlexcount(KEY, "-", "[n")).isEqualTo(1);
+    // Range m <= result <= +infinity
+    assertThat(jedis.zlexcount(KEY, "[m", "+")).isEqualTo(1);
+  }
+
+  @Test
+  public void shouldReturnOne_givenMinEqualToMemberNameAndMinInclusive() {
+    String memberName = "member";
+    jedis.zadd(KEY, SCORE, memberName);
+
+    // Range member <= result <= n
+    assertThat(jedis.zlexcount(KEY, "[" + memberName, "[n")).isEqualTo(1);
+  }
+
+  @Test
+  public void shouldReturnOne_givenMaxEqualToMemberNameAndMaxInclusive() {
+    String memberName = "member";
+    jedis.zadd(KEY, SCORE, memberName);
+
+    // Range a <= result <= member
+    assertThat(jedis.zlexcount(KEY, "[a", "[" + memberName)).isEqualTo(1);
+  }
+
+  @Test
+  public void 
shouldReturnOne_givenMinAndMaxEqualToMemberNameAndMinAndMaxInclusive() {
+    String memberName = "member";
+    jedis.zadd(KEY, SCORE, memberName);
+
+    assertThat(jedis.zlexcount(KEY, "[" + memberName, "[" + 
memberName)).isEqualTo(1);
+  }
+
+  @Test
+  @Parameters({"[", "(", "", "-", "+"})
+  public void shouldReturnOne_givenMemberNameIsSpecialCharacter(String 
memberName) {
+    jedis.zadd(KEY, SCORE, memberName);
+
+    assertThat(jedis.zlexcount(KEY, "[" + memberName, "[" + 
memberName)).isEqualTo(1);
+  }
+
+  @Test
+  public void shouldReturnZero_givenMinEqualToMemberNameAndMinExclusive() {
+    String memberName = "member";
+    jedis.zadd(KEY, SCORE, memberName);
+
+    // Range member < result <= n
+    assertThat(jedis.zlexcount(KEY, "(" + memberName, "[n")).isEqualTo(0);
+  }
+
+  @Test
+  public void shouldReturnZero_givenMaxEqualToMemberNameAndMaxExclusive() {
+    String memberName = "member";
+    jedis.zadd(KEY, SCORE, memberName);
+
+    // Range a <= result < member
+    assertThat(jedis.zlexcount(KEY, "[a", "(" + memberName)).isEqualTo(0);
+  }
+
+  @Test
+  public void shouldReturnZero_givenRangeExcludingMember() {
+    String memberName = "member";
+    jedis.zadd(KEY, SCORE, memberName);
+
+    // Range n <= result <= o
+    assertThat(jedis.zlexcount(KEY, "[n", "[o")).isEqualTo(0);
+  }
+
+  @Test
+  public void 
shouldReturnCount_givenMultipleMembersInRange_withInclusiveMinAndMax() {
+    populateSortedSet();
+
+    int minLength = 3;
+    int maxLength = 6;
+    String min = StringUtils.repeat(BASE_MEMBER_NAME, minLength);
+    String max = StringUtils.repeat(BASE_MEMBER_NAME, maxLength);
+
+    // Range (v * 3) <= result <= (v * 6)
+    assertThat(jedis.zlexcount(KEY, "[" + min, "[" + max)).isEqualTo(4);
+  }
+
+  @Test
+  public void 
shouldReturnCount_givenMultipleMembersInRange_withExclusiveMinAndMax() {
+    populateSortedSet();
+
+    int minLength = 1;
+    int maxLength = 7;
+    String min = StringUtils.repeat(BASE_MEMBER_NAME, minLength);
+    String max = StringUtils.repeat(BASE_MEMBER_NAME, maxLength);
+
+    // Range (v * 1) < result < (v * 7)
+    assertThat(jedis.zlexcount(KEY, "(" + min, "(" + max)).isEqualTo(5);
+  }
+
+  @Test
+  public void 
shouldReturnCount_givenMultipleMembersInRange_withInclusiveMinAndExclusiveMax() 
{
+    populateSortedSet();
+
+    int minLength = 5;
+    int maxLength = 8;
+    String min = StringUtils.repeat(BASE_MEMBER_NAME, minLength);
+    String max = StringUtils.repeat(BASE_MEMBER_NAME, maxLength);
+
+    // Range (v * 5) <= result < (v * 8)
+    assertThat(jedis.zlexcount(KEY, "[" + min, "(" + max)).isEqualTo(3);
+  }
+
+  @Test
+  public void 
shouldReturnCount_givenMultipleMembersInRange_withExclusiveMinAndInclusiveMax() 
{
+    populateSortedSet();
+
+    int minLength = 2;
+    int maxLength = 5;
+    String min = StringUtils.repeat(BASE_MEMBER_NAME, minLength);
+    String max = StringUtils.repeat(BASE_MEMBER_NAME, maxLength);
+
+    // Range (v * 2) < result <= (v * 5)
+    assertThat(jedis.zlexcount(KEY, "(" + min, "[" + max)).isEqualTo(3);
+  }
+
+  @Test
+  public void 
shouldReturnCount_givenMultipleMembersInRangeUsingMinusAndPlusArguments() {
+    populateSortedSet();
+
+    int minLength = 4;
+    int maxLength = 8;
+    String min = StringUtils.repeat(BASE_MEMBER_NAME, minLength);
+    String max = StringUtils.repeat(BASE_MEMBER_NAME, maxLength);
+
+    // Range -infinity <= result <= (v * 8)
+    assertThat(jedis.zlexcount(KEY, "-", "[" + max)).isEqualTo(8);
+
+    // Range (v * 4) <= result < +infinity
+    assertThat(jedis.zlexcount(KEY, "[" + min, "+")).isEqualTo(7);
+
+    // Range -infinity <= result < +infinity
+    assertThat(jedis.zlexcount(KEY, "-", "+")).isEqualTo(10);
+  }
+
+  // Add 10 members with the same score and member names consisting of 'v' 
repeated an increasing
+  // number of times
+  private List<String> populateSortedSet() {
+    List<String> members = new ArrayList<>();
+    String memberName = BASE_MEMBER_NAME;
+    for (int i = 0; i < 10; ++i) {
+      jedis.zadd(KEY, SCORE, memberName);
+      members.add(memberName);
+      memberName += BASE_MEMBER_NAME;
+    }
+    return members;
+  }
+}
diff --git 
a/geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/sortedset/ZLexCountIntegrationTest.java
 
b/geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/sortedset/ZLexCountIntegrationTest.java
new file mode 100644
index 0000000..235750a
--- /dev/null
+++ 
b/geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/sortedset/ZLexCountIntegrationTest.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license
+ * agreements. See the NOTICE file distributed with this work for additional 
information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache 
License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the 
License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software 
distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+ * or implied. See the License for the specific language governing permissions 
and limitations under
+ * the License.
+ */
+package org.apache.geode.redis.internal.executor.sortedset;
+
+import org.junit.ClassRule;
+
+import org.apache.geode.redis.GeodeRedisServerRule;
+
+public class ZLexCountIntegrationTest extends AbstractZLexCountIntegrationTest 
{
+
+  @ClassRule
+  public static GeodeRedisServerRule server = new GeodeRedisServerRule();
+
+  @Override
+  public int getPort() {
+    return server.getPort();
+  }
+}
diff --git 
a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/RedisCommandType.java
 
b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/RedisCommandType.java
index b5c0b43..43d7748 100755
--- 
a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/RedisCommandType.java
+++ 
b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/RedisCommandType.java
@@ -100,6 +100,7 @@ import 
org.apache.geode.redis.internal.executor.sortedset.ZAddExecutor;
 import org.apache.geode.redis.internal.executor.sortedset.ZCardExecutor;
 import org.apache.geode.redis.internal.executor.sortedset.ZCountExecutor;
 import org.apache.geode.redis.internal.executor.sortedset.ZIncrByExecutor;
+import org.apache.geode.redis.internal.executor.sortedset.ZLexCountExecutor;
 import org.apache.geode.redis.internal.executor.sortedset.ZPopMaxExecutor;
 import org.apache.geode.redis.internal.executor.sortedset.ZPopMinExecutor;
 import org.apache.geode.redis.internal.executor.sortedset.ZRangeByLexExecutor;
@@ -216,6 +217,7 @@ public enum RedisCommandType {
   ZCARD(new ZCardExecutor(), SUPPORTED, new ExactParameterRequirements(2)),
   ZCOUNT(new ZCountExecutor(), SUPPORTED, new ExactParameterRequirements(4)),
   ZINCRBY(new ZIncrByExecutor(), SUPPORTED, new ExactParameterRequirements(4)),
+  ZLEXCOUNT(new ZLexCountExecutor(), SUPPORTED, new 
ExactParameterRequirements(4)),
   ZPOPMAX(new ZPopMaxExecutor(), SUPPORTED, new MinimumParameterRequirements(2)
       .and(new MaximumParameterRequirements(3, ERROR_SYNTAX))),
   ZPOPMIN(new ZPopMinExecutor(), SUPPORTED, new MinimumParameterRequirements(2)
diff --git 
a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisSortedSet.java
 
b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisSortedSet.java
index 71a2907..9d2d7ef 100644
--- 
a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisSortedSet.java
+++ 
b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisSortedSet.java
@@ -64,16 +64,6 @@ class NullRedisSortedSet extends RedisSortedSet {
   }
 
   @Override
-  List<byte[]> zrangebylex(SortedSetLexRangeOptions rangeOptions) {
-    return Collections.emptyList();
-  }
-
-  @Override
-  List<byte[]> zrangebyscore(SortedSetScoreRangeOptions rangeOptions, boolean 
withScores) {
-    return Collections.emptyList();
-  }
-
-  @Override
   byte[] zincrby(Region<RedisKey, RedisData> region, RedisKey key, byte[] 
increment,
       byte[] member) {
     List<byte[]> valuesToAdd = new ArrayList<>();
@@ -87,6 +77,11 @@ class NullRedisSortedSet extends RedisSortedSet {
   }
 
   @Override
+  long zlexcount(SortedSetLexRangeOptions rangeOptions) {
+    return 0;
+  }
+
+  @Override
   List<byte[]> zpopmax(Region<RedisKey, RedisData> region, RedisKey key, int 
count) {
     return Collections.emptyList();
   }
@@ -102,6 +97,16 @@ class NullRedisSortedSet extends RedisSortedSet {
   }
 
   @Override
+  List<byte[]> zrangebylex(SortedSetLexRangeOptions rangeOptions) {
+    return Collections.emptyList();
+  }
+
+  @Override
+  List<byte[]> zrangebyscore(SortedSetScoreRangeOptions rangeOptions, boolean 
withScores) {
+    return Collections.emptyList();
+  }
+
+  @Override
   List<byte[]> zrevrange(int min, int max, boolean withScores) {
     return Collections.emptyList();
   }
diff --git 
a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSortedSet.java
 
b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSortedSet.java
index 2fa70d7..fcd6aad 100644
--- 
a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSortedSet.java
+++ 
b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSortedSet.java
@@ -340,6 +340,30 @@ public class RedisSortedSet extends AbstractRedisData {
     return addLimitToRange(rangeOptions, withScores, false, minIndex, 
maxIndex);
   }
 
+  long zlexcount(SortedSetLexRangeOptions lexOptions) {
+    if (scoreSet.isEmpty()) {
+      return 0;
+    }
+
+    // Assume that all members have the same score. Behaviour is unspecified 
otherwise.
+    double score = scoreSet.get(0).score;
+
+    int minIndex =
+        getIndexByLex(score, lexOptions.getStartRange(), 
lexOptions.isStartExclusive(), true);
+    if (minIndex >= scoreSet.size()) {
+      return 0;
+    }
+
+    AbstractOrderedSetEntry maxEntry = new 
MemberDummyOrderedSetEntry(lexOptions.getEndRange(),
+        score, lexOptions.isEndExclusive(), false);
+    int maxIndex = scoreSet.indexOf(maxEntry);
+    if (minIndex >= maxIndex) {
+      return 0;
+    }
+
+    return maxIndex - minIndex;
+  }
+
   long zrank(byte[] member) {
     OrderedSetEntry orderedSetEntry = members.get(member);
     if (orderedSetEntry == null) {
diff --git 
a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSortedSetCommandsFunctionExecutor.java
 
b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSortedSetCommandsFunctionExecutor.java
index 5653586..a6d094b 100644
--- 
a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSortedSetCommandsFunctionExecutor.java
+++ 
b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSortedSetCommandsFunctionExecutor.java
@@ -58,6 +58,11 @@ public class RedisSortedSetCommandsFunctionExecutor extends 
RedisDataCommandsFun
   }
 
   @Override
+  public long zlexcount(RedisKey key, SortedSetLexRangeOptions lexOptions) {
+    return stripedExecute(key, () -> getRedisSortedSet(key, 
true).zlexcount(lexOptions));
+  }
+
+  @Override
   public List<byte[]> zpopmax(RedisKey key, int count) {
     return stripedExecute(key,
         () -> getRedisSortedSet(key, false).zpopmax(getRegion(), key, count));
diff --git 
a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/sortedset/RedisSortedSetCommands.java
 
b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/sortedset/RedisSortedSetCommands.java
index 1283c54..1585d97 100644
--- 
a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/sortedset/RedisSortedSetCommands.java
+++ 
b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/sortedset/RedisSortedSetCommands.java
@@ -29,6 +29,8 @@ public interface RedisSortedSetCommands {
 
   byte[] zincrby(RedisKey key, byte[] increment, byte[] member);
 
+  long zlexcount(RedisKey key, SortedSetLexRangeOptions rangeOptions);
+
   List<byte[]> zpopmax(RedisKey key, int count);
 
   List<byte[]> zpopmin(RedisKey key, int count);
diff --git 
a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/sortedset/SortedSetLexRangeOptions.java
 
b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/sortedset/SortedSetLexRangeOptions.java
index 5bedba0..2512d4b 100644
--- 
a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/sortedset/SortedSetLexRangeOptions.java
+++ 
b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/sortedset/SortedSetLexRangeOptions.java
@@ -27,7 +27,7 @@ import java.util.Arrays;
 
 public class SortedSetLexRangeOptions extends 
AbstractSortedSetRangeOptions<byte[]> {
 
-  SortedSetLexRangeOptions(byte[] minimumBytes, byte[] maximumBytes) {
+  public SortedSetLexRangeOptions(byte[] minimumBytes, byte[] maximumBytes) {
     super(minimumBytes, maximumBytes);
   }
 
diff --git 
a/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/sortedset/ZLexCountExecutor.java
 
b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/sortedset/ZLexCountExecutor.java
new file mode 100644
index 0000000..6740637
--- /dev/null
+++ 
b/geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/sortedset/ZLexCountExecutor.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license
+ * agreements. See the NOTICE file distributed with this work for additional 
information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache 
License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the 
License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software 
distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+ * or implied. See the License for the specific language governing permissions 
and limitations under
+ * the License.
+ */
+package org.apache.geode.redis.internal.executor.sortedset;
+
+import static 
org.apache.geode.redis.internal.RedisConstants.ERROR_MIN_MAX_NOT_A_VALID_STRING;
+
+import java.util.List;
+
+import org.apache.geode.redis.internal.executor.AbstractExecutor;
+import org.apache.geode.redis.internal.executor.RedisResponse;
+import org.apache.geode.redis.internal.netty.Command;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class ZLexCountExecutor extends AbstractExecutor {
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext 
context) {
+    List<byte[]> commandElements = command.getProcessedCommand();
+
+    SortedSetLexRangeOptions rangeOptions;
+
+    try {
+      byte[] minBytes = commandElements.get(2);
+      byte[] maxBytes = commandElements.get(3);
+      rangeOptions = new SortedSetLexRangeOptions(minBytes, maxBytes);
+    } catch (IllegalArgumentException ex) {
+      return RedisResponse.error(ERROR_MIN_MAX_NOT_A_VALID_STRING);
+    }
+
+    // If the range is empty, return early
+    if (rangeOptions.isEmptyRange(false)) {
+      return RedisResponse.integer(0);
+    }
+
+    long result = context.getSortedSetCommands().zlexcount(command.getKey(), 
rangeOptions);
+
+    return RedisResponse.integer(result);
+  }
+}
diff --git 
a/geode-apis-compatible-with-redis/src/test/java/org/apache/geode/redis/internal/SupportedCommandsJUnitTest.java
 
b/geode-apis-compatible-with-redis/src/test/java/org/apache/geode/redis/internal/SupportedCommandsJUnitTest.java
index 976be3b..65e2653 100644
--- 
a/geode-apis-compatible-with-redis/src/test/java/org/apache/geode/redis/internal/SupportedCommandsJUnitTest.java
+++ 
b/geode-apis-compatible-with-redis/src/test/java/org/apache/geode/redis/internal/SupportedCommandsJUnitTest.java
@@ -91,6 +91,7 @@ public class SupportedCommandsJUnitTest {
       "ZINCRBY",
       "ZCARD",
       "ZCOUNT",
+      "ZLEXCOUNT",
       "ZPOPMAX",
       "ZPOPMIN",
       "ZRANGE",
diff --git 
a/geode-apis-compatible-with-redis/src/test/java/org/apache/geode/redis/internal/data/RedisSortedSetTest.java
 
b/geode-apis-compatible-with-redis/src/test/java/org/apache/geode/redis/internal/data/RedisSortedSetTest.java
index 718136f..901bd05 100644
--- 
a/geode-apis-compatible-with-redis/src/test/java/org/apache/geode/redis/internal/data/RedisSortedSetTest.java
+++ 
b/geode-apis-compatible-with-redis/src/test/java/org/apache/geode/redis/internal/data/RedisSortedSetTest.java
@@ -58,6 +58,7 @@ import 
org.apache.geode.internal.serialization.DataSerializableFixedID;
 import org.apache.geode.internal.serialization.SerializationContext;
 import org.apache.geode.internal.size.ReflectionObjectSizer;
 import org.apache.geode.redis.internal.delta.RemsDeltaInfo;
+import 
org.apache.geode.redis.internal.executor.sortedset.SortedSetLexRangeOptions;
 import org.apache.geode.redis.internal.executor.sortedset.ZAddOptions;
 import org.apache.geode.redis.internal.netty.Coder;
 
@@ -275,6 +276,84 @@ public class RedisSortedSetTest {
   }
 
   @Test
+  public void zlexcount_shouldBeInclusiveWhenSpecified() {
+    RedisSortedSet sortedSet = createRedisSortedSet(
+        score1, "member1",
+        score1, "member2",
+        score1, "member3",
+        score1, "member4",
+        score1, "member5");
+    SortedSetLexRangeOptions lexOptions =
+        new SortedSetLexRangeOptions("[member1".getBytes(), 
"[member3".getBytes());
+    assertThat(sortedSet.zlexcount(lexOptions)).isEqualTo(3);
+  }
+
+  @Test
+  public void zlexcount_shouldBeExclusiveWhenSpecified() {
+    RedisSortedSet sortedSet = createRedisSortedSet(
+        score1, "member1",
+        score1, "member2",
+        score1, "member3",
+        score1, "member4",
+        score1, "member5");
+    SortedSetLexRangeOptions lexOptions =
+        new SortedSetLexRangeOptions("(member1".getBytes(), 
"(member3".getBytes());
+    assertThat(sortedSet.zlexcount(lexOptions)).isEqualTo(1);
+  }
+
+  @Test
+  public void zlexcount_shouldBeZero_whenMinIsTooGreat() {
+    RedisSortedSet sortedSet = createRedisSortedSet(
+        score1, "member1",
+        score1, "member2",
+        score1, "member3",
+        score1, "member4",
+        score1, "member5");
+    SortedSetLexRangeOptions lexOptions = new 
SortedSetLexRangeOptions("[member6".getBytes(),
+        "(member8".getBytes());
+    assertThat(sortedSet.zlexcount(lexOptions)).isEqualTo(0);
+  }
+
+  @Test
+  public void zlexcount_shouldBeZero_whenMaxIsTooSmall() {
+    RedisSortedSet sortedSet = createRedisSortedSet(
+        score1, "member1",
+        score1, "member2",
+        score1, "member3",
+        score1, "member4",
+        score1, "member5");
+    SortedSetLexRangeOptions lexOptions = new 
SortedSetLexRangeOptions("[membeq0".getBytes(),
+        "[member0".getBytes());
+    assertThat(sortedSet.zlexcount(lexOptions)).isEqualTo(0);
+  }
+
+  @Test
+  public void zlexcount_shouldBeZero_whenMinAndMaxAreReversed() {
+    RedisSortedSet sortedSet = createRedisSortedSet(
+        score1, "member1",
+        score1, "member2",
+        score1, "member3",
+        score1, "member4",
+        score1, "member5");
+    SortedSetLexRangeOptions lexOptions =
+        new SortedSetLexRangeOptions("[member5".getBytes(), 
"[member0".getBytes());
+    assertThat(sortedSet.zlexcount(lexOptions)).isEqualTo(0);
+  }
+
+  @Test
+  public void zlexcount_shouldBeAbleToCountAllEntries() {
+    RedisSortedSet sortedSet = createRedisSortedSet(
+        score1, "member1",
+        score1, "member2",
+        score1, "member3",
+        score1, "member4",
+        score1, "member5");
+    SortedSetLexRangeOptions lexOptions =
+        new SortedSetLexRangeOptions("-".getBytes(), "+".getBytes());
+    assertThat(sortedSet.zlexcount(lexOptions)).isEqualTo(5);
+  }
+
+  @Test
   public void scoreSet_shouldNotRetainOldEntries_whenEntriesUpdated() {
     Collection<byte[]> rangeList = rangeSortedSet.zrange(0, 100, false);
     assertThat(rangeList).hasSize(12);

Reply via email to