DonalEvans commented on a change in pull request #6861:
URL: https://github.com/apache/geode/pull/6861#discussion_r713239683



##########
File path: 
geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSortedSetCommandsFunctionExecutor.java
##########
@@ -62,6 +62,24 @@ public long zcount(RedisKey key, SortedSetScoreRangeOptions 
rangeOptions) {
         () -> getRedisSortedSet(key, false).zincrby(getRegion(), key, 
increment, member));
   }
 
+  @Override
+  public long zinterstore(RedisKey destinationKey, List<ZKeyWeight> keyWeights,
+      ZAggregator aggregator) {
+    List<RedisKey> keysToLock = new ArrayList<>(keyWeights.size());
+    for (ZKeyWeight kw : keyWeights) {
+      getRegionProvider().ensureKeyIsLocal(kw.getKey());
+      keysToLock.add(kw.getKey());
+    }
+    getRegionProvider().ensureKeyIsLocal(destinationKey);
+    keysToLock.add(destinationKey);
+
+    getRegionProvider().orderForLocking(keysToLock);

Review comment:
       This block is duplicated in the `zunionstore()` method and can probably 
be pulled out into a "getKeysToLock()" method (or some similar name).

##########
File path: 
geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSortedSet.java
##########
@@ -578,6 +627,91 @@ private int 
getMaxElementsToReturn(AbstractSortedSetRangeOptions<?> rangeOptions
     return result;
   }
 
+  private RedisSortedSet getIntersection(List<RedisSortedSet> sets, 
ZAggregator aggregator) {
+    RedisSortedSet retVal = new RedisSortedSet(Collections.emptyList());

Review comment:
       A more efficient approach might be to start with a collection (in this 
case it would be a copy of the smallest set in `sets`) and then iterate it and 
remove members from it if they're not found in all of the other sets. This has 
the benefit that if we don't find the member in one of the other sets we can 
immediately go to the next member, and if we ever remove all the members from 
the collection, we know we can immediately return since the returned set will 
be empty. This would need some extra logic to handle Redis' behaviour when one 
of the input sets is empty though (i.e. the key doesn't exist), since although 
logically the intersection with an empty set should be an empty set, Redis 
treats these as no-ops and effectively ignores them in the intersection 
computation.
   
   We could also do the weighting at this point as part of the SUM/MAX/MIN 
calculation, rather than weighting every member in every set first, then 
calculating the intersection, since then we're not weighting any members that 
we know aren't going to contribute to the final outcome.

##########
File path: 
geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSortedSet.java
##########
@@ -288,6 +289,54 @@ long zcount(SortedSetScoreRangeOptions rangeOptions) {
     return byteIncr;
   }
 
+  long zinterstore(RegionProvider regionProvider, RedisKey key, 
List<ZKeyWeight> keyWeights,
+      ZAggregator aggregator) {
+    List<RedisSortedSet> sets = new ArrayList<>(keyWeights.size());
+    for (ZKeyWeight keyWeight : keyWeights) {
+      RedisSortedSet set =
+          regionProvider.getTypedRedisData(REDIS_SORTED_SET, 
keyWeight.getKey(), false);
+
+      if (set == NULL_REDIS_SORTED_SET) {
+        continue;
+      }
+
+      double weight = keyWeight.getWeight();
+      RedisSortedSet weightedSet = new RedisSortedSet(Collections.emptyList());
+
+      for (AbstractOrderedSetEntry entry : set.members.values()) {
+        OrderedSetEntry existingValue = members.get(entry.member);
+        if (existingValue == null) {
+          double score;
+          // Redis math and Java math are different when handling infinity. 
Specifically:
+          // Java: INFINITY * 0 = NaN
+          // Redis: INFINITY * 0 = 0
+          if (weight == 0) {
+            score = 0;
+          } else if (weight == 1) {
+            score = entry.getScore();
+          } else if (Double.isInfinite(weight) && entry.score == 0D) {
+            score = 0D;
+          } else {
+            double newScore = entry.score * weight;
+            if (Double.isNaN(newScore)) {
+              throw new ArithmeticException(ERROR_OPERATION_PRODUCED_NAN);

Review comment:
       I think that given the Redis/Java math handling above, it's not possible 
for `newScore` to be NaN here, and that even if it was, it would not be correct 
to throw here, since Redis does not have that behaviour. Having some 
integration tests to handle cases where NaN might appear (both here, and when 
doing the SUM aggregation) would be good, to validate that we're matching 
Redis' behaviour in that respect. For example, the below test fails when run 
against Radish but passes for native Redis:
   ```
     @Test
     public void testNaNSums() {
       String member = "member";
       jedis.zadd(KEY1, Double.POSITIVE_INFINITY, member);
       jedis.zadd(KEY2, Double.NEGATIVE_INFINITY, member);
       assertThat(jedis.zinterstore(NEW_SET, KEY1, KEY2)).isOne();
       assertThat(jedis.zscore(NEW_SET, member)).isZero();
     }
   ```

##########
File path: 
geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/sortedset/AbstractZInterStoreIntegrationTest.java
##########
@@ -0,0 +1,696 @@
+/*
+ * 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 java.lang.Math.max;
+import static java.lang.Math.min;
+import static 
org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertAtLeastNArgs;
+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.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Random;
+import java.util.Set;
+import java.util.function.BiFunction;
+
+import org.assertj.core.data.Offset;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+import redis.clients.jedis.Protocol;
+import redis.clients.jedis.Tuple;
+import redis.clients.jedis.ZParams;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+import org.apache.geode.redis.internal.RedisConstants;
+
+public abstract class AbstractZInterStoreIntegrationTest implements 
RedisIntegrationTest {
+
+  private static final String NEW_SET = "{user1}new";
+  private static final String KEY1 = "{user1}sset1";
+  private static final String KEY2 = "{user1}sset2";
+  private static final String KEY3 = "{user1}sset3";
+
+  private 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_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.ZINTERSTORE, 3);
+  }
+
+  @Test
+  public void shouldError_givenWrongKeyType() {
+    final String STRING_KEY = "{user1}stringKey";
+    jedis.set(STRING_KEY, "value");
+    assertThatThrownBy(() -> jedis.sendCommand(NEW_SET, 
Protocol.Command.ZINTERSTORE, NEW_SET, "2",
+        STRING_KEY, KEY1)).hasMessage("WRONGTYPE " + 
RedisConstants.ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void shouldError_givenSetsCrossSlots() {
+    final String WRONG_KEY = "{user2}another";
+    assertThatThrownBy(
+        () -> jedis.sendCommand(NEW_SET, Protocol.Command.ZINTERSTORE, 
NEW_SET, "2", WRONG_KEY,
+            KEY1)).hasMessage("CROSSSLOT " + RedisConstants.ERROR_WRONG_SLOT);
+  }
+
+  @Test
+  public void shouldError_givenNumkeysTooLarge() {
+    assertThatThrownBy(
+        () -> jedis.sendCommand(NEW_SET, Protocol.Command.ZINTERSTORE, 
NEW_SET, "2", KEY1))
+            .hasMessage("ERR " + RedisConstants.ERROR_SYNTAX);
+  }
+
+  @Test
+  public void shouldError_givenNumkeysTooSmall() {
+    assertThatThrownBy(
+        () -> jedis.sendCommand(NEW_SET, Protocol.Command.ZINTERSTORE, 
NEW_SET, "1", KEY1, KEY2))
+            .hasMessage("ERR " + RedisConstants.ERROR_SYNTAX);
+  }
+
+  @Test
+  public void shouldError_givenTooManyWeights() {
+    assertThatThrownBy(
+        () -> jedis.sendCommand(NEW_SET, Protocol.Command.ZINTERSTORE, 
NEW_SET, "1",
+            KEY1, "WEIGHTS", "2", "3")).hasMessage("ERR " + 
RedisConstants.ERROR_SYNTAX);
+  }
+
+  @Test
+  public void shouldError_givenTooFewWeights() {
+    assertThatThrownBy(
+        () -> jedis.sendCommand(NEW_SET, Protocol.Command.ZINTERSTORE, 
NEW_SET, "2",
+            KEY1, KEY2, "WEIGHTS", "1")).hasMessage("ERR " + 
RedisConstants.ERROR_SYNTAX);
+  }
+
+  @Test
+  public void shouldError_givenWeightNotANumber() {
+    assertThatThrownBy(
+        () -> jedis.sendCommand(NEW_SET, Protocol.Command.ZINTERSTORE, 
NEW_SET, "1",
+            KEY1, "WEIGHTS", "not-a-number"))
+                .hasMessage("ERR " + RedisConstants.ERROR_WEIGHT_NOT_A_FLOAT);
+  }
+
+  @Test
+  public void shouldError_givenWeightsWithoutAnyValues() {
+    assertThatThrownBy(
+        () -> jedis.sendCommand(NEW_SET, Protocol.Command.ZINTERSTORE, 
NEW_SET, "1",
+            KEY1, "WEIGHTS")).hasMessage("ERR " + RedisConstants.ERROR_SYNTAX);
+  }
+
+  @Test
+  public void shouldError_givenMultipleWeightKeywords() {
+    assertThatThrownBy(
+        () -> jedis.sendCommand(NEW_SET, Protocol.Command.ZINTERSTORE, 
NEW_SET, "1",
+            KEY1, "WEIGHT", "1.0", "WEIGHT", "2.0"))
+                .hasMessage("ERR " + RedisConstants.ERROR_SYNTAX);
+  }
+
+  @Test
+  public void shouldError_givenUnknownAggregate() {
+    assertThatThrownBy(
+        () -> jedis.sendCommand(NEW_SET, Protocol.Command.ZINTERSTORE, 
NEW_SET, "1",
+            KEY1, "AGGREGATE", "UNKNOWN", "WEIGHTS", "1"))
+                .hasMessage("ERR " + RedisConstants.ERROR_SYNTAX);
+  }
+
+  @Test
+  public void shouldError_givenAggregateKeywordWithoutValue() {
+    assertThatThrownBy(
+        () -> jedis.sendCommand(NEW_SET, Protocol.Command.ZINTERSTORE, 
NEW_SET, "1",
+            KEY1, "AGGREGATE")).hasMessage("ERR " + 
RedisConstants.ERROR_SYNTAX);
+  }
+
+  @Test
+  public void shouldError_givenMultipleAggregates() {
+    assertThatThrownBy(
+        () -> jedis.sendCommand(NEW_SET, Protocol.Command.ZINTERSTORE, 
NEW_SET, "1",
+            KEY1, "WEIGHTS", "1", "AGGREGATE", "SUM", "MIN"))
+                .hasMessage("ERR " + RedisConstants.ERROR_SYNTAX);
+  }
+
+  @Test
+  public void shouldStoreIntersection_givenWeightOfOne_andOneRedisSortedSet() {
+    Map<String, Double> scores = buildMapOfMembersAndScores();
+    Set<Tuple> expectedResults = convertToTuples(scores, (ignore, value) -> 
value);
+    jedis.zadd(KEY1, scores);
+
+    assertThat(jedis.zinterstore(NEW_SET, new ZParams().weights(1), KEY1))
+        .isEqualTo(expectedResults.size());
+
+    Set<Tuple> results = jedis.zrangeWithScores(NEW_SET, 0, scores.size());
+    assertThat(results).containsExactlyInAnyOrderElementsOf(expectedResults);
+  }
+
+  @Test
+  public void shouldStoreIntersection_givenWeightOfZero_andOneRedisSortedSet() 
{
+    Map<String, Double> scores = buildMapOfMembersAndScores();
+    Set<Tuple> expectedResults = convertToTuples(scores, (ignore, value) -> 
0D);
+    jedis.zadd(KEY1, scores);
+
+    assertThat(jedis.zinterstore(NEW_SET, new ZParams().weights(0), KEY1))
+        .isEqualTo(scores.size());
+
+    Set<Tuple> results = jedis.zrangeWithScores(NEW_SET, 0, scores.size());
+    assertThat(results).containsExactlyInAnyOrderElementsOf(expectedResults);
+  }
+
+  @Test
+  public void 
shouldStoreIntersection_givenWeightOfPositiveInfinity_andOneRedisSortedSet() {
+    Map<String, Double> scores = buildMapOfMembersAndScores();
+    Set<Tuple> expectedResults =
+        convertToTuples(scores, (ignore, value) -> value > 0 ? 
Double.POSITIVE_INFINITY : value);
+    jedis.zadd(KEY1, scores);
+
+    assertThat(jedis.zinterstore(NEW_SET, new 
ZParams().weights(Double.POSITIVE_INFINITY),
+        KEY1)).isEqualTo(scores.size());
+
+    Set<Tuple> results = jedis.zrangeWithScores(NEW_SET, 0, scores.size());
+    assertThat(results).containsExactlyInAnyOrderElementsOf(expectedResults);
+  }
+
+  @Test
+  public void 
shouldStoreIntersection_givenWeightOfNegativeInfinity_andOneRedisSortedSet() {
+    Map<String, Double> scores = buildMapOfMembersAndScores();
+    jedis.zadd(KEY1, scores);
+
+    Set<Tuple> expectedResults = new LinkedHashSet<>();
+    expectedResults.add(new Tuple("player1", Double.POSITIVE_INFINITY));
+    expectedResults.add(new Tuple("player2", 0D));
+    expectedResults.add(new Tuple("player3", Double.NEGATIVE_INFINITY));
+    expectedResults.add(new Tuple("player4", Double.NEGATIVE_INFINITY));
+    expectedResults.add(new Tuple("player5", Double.NEGATIVE_INFINITY));
+
+    assertThat(jedis.zinterstore(NEW_SET, new 
ZParams().weights(Double.NEGATIVE_INFINITY),
+        KEY1)).isEqualTo(scores.size());
+
+    Set<Tuple> results = jedis.zrangeWithScores(NEW_SET, 0, scores.size());
+    assertThat(results).containsExactlyInAnyOrderElementsOf(expectedResults);
+  }
+
+  @Test
+  public void shouldStoreIntersection_givenWeightOfN_andOneRedisSortedSet() {
+    Map<String, Double> scores = buildMapOfMembersAndScores();
+    jedis.zadd(KEY1, scores);
+
+    double multiplier = 2.71D;
+
+    Set<Tuple> expectedResults = new LinkedHashSet<>();
+    expectedResults.add(new Tuple("player1", Double.NEGATIVE_INFINITY));
+    expectedResults.add(new Tuple("player2", 0D));
+    expectedResults.add(new Tuple("player3", multiplier));
+    expectedResults.add(new Tuple("player4", Double.POSITIVE_INFINITY));
+    expectedResults.add(new Tuple("player5", 3.2D * multiplier));
+
+    assertThat(jedis.zinterstore(NEW_SET, new ZParams().weights(multiplier), 
KEY1))
+        .isEqualTo(scores.size());
+
+    Set<Tuple> results = jedis.zrangeWithScores(NEW_SET, 0, scores.size());
+    assertThat(results).containsExactlyInAnyOrderElementsOf(expectedResults);
+  }
+
+  @Test
+  public void shouldStoreIntersection_givenMultipleRedisSortedSets() {
+    Map<String, Double> scores = buildMapOfMembersAndScores();
+    Set<Tuple> expectedResults = new LinkedHashSet<>();
+    expectedResults.add(new Tuple("player1", Double.NEGATIVE_INFINITY));
+    expectedResults.add(new Tuple("player2", 0D));
+    expectedResults.add(new Tuple("player3", 2D));
+    expectedResults.add(new Tuple("player4", Double.POSITIVE_INFINITY));
+    expectedResults.add(new Tuple("player5", 3.2D * 2));
+
+    jedis.zadd(KEY1, scores);
+    jedis.zadd(KEY2, scores);
+
+    assertThat(jedis.zinterstore(NEW_SET, new ZParams(), KEY1, KEY2))
+        .isEqualTo(expectedResults.size());
+
+    Set<Tuple> results = jedis.zrangeWithScores(NEW_SET, 0, scores.size());
+    assertThat(results).containsExactlyInAnyOrderElementsOf(expectedResults);
+  }
+
+  @Test
+  public void 
shouldStoreIntersection_givenTwoRedisSortedSets_withDifferentWeights() {
+    Map<String, Double> scores = buildMapOfMembersAndScores();
+    Set<Tuple> expectedResults = new LinkedHashSet<>();
+    expectedResults.add(new Tuple("player1", Double.NEGATIVE_INFINITY));
+    expectedResults.add(new Tuple("player2", 0D));
+    expectedResults.add(new Tuple("player3", 3D));
+    expectedResults.add(new Tuple("player4", Double.POSITIVE_INFINITY));
+    expectedResults.add(new Tuple("player5", 3.2D * 3));
+
+    jedis.zadd(KEY1, scores);
+    jedis.zadd(KEY2, scores);
+
+    assertThat(jedis.zinterstore(NEW_SET, new ZParams().weights(1, 2), KEY1, 
KEY2))
+        .isEqualTo(expectedResults.size());
+
+    Set<Tuple> results = jedis.zrangeWithScores(NEW_SET, 0, scores.size());
+    assertThat(results).containsExactlyInAnyOrderElementsOf(expectedResults);
+  }
+
+  @Test
+  public void 
shouldStoreIntersection_givenMultipleIdenticalRedisSortedSets_withDifferentPositiveWeights()
 {
+    Map<String, Double> scores = buildMapOfMembersAndScores();
+    Set<Tuple> expectedResults = new LinkedHashSet<>();
+    expectedResults.add(new Tuple("player1", Double.NEGATIVE_INFINITY));
+    expectedResults.add(new Tuple("player2", 0D));
+    expectedResults.add(new Tuple("player3", 4.5D));
+    expectedResults.add(new Tuple("player4", Double.POSITIVE_INFINITY));
+    expectedResults.add(new Tuple("player5", 3.2D * 4.5D));
+
+    jedis.zadd(KEY1, scores);
+    jedis.zadd(KEY2, scores);
+    jedis.zadd(KEY3, scores);
+
+    assertThat(jedis.zinterstore(NEW_SET, new ZParams().weights(1D, 2D, 1.5D), 
KEY1, KEY2, KEY3))
+        .isEqualTo(expectedResults.size());
+
+    Set<Tuple> actualResults = jedis.zrangeWithScores(NEW_SET, 0, 
scores.size());
+    assertThatActualScoresAreVeryCloseToExpectedScores(expectedResults, 
actualResults);
+  }
+
+  @Test
+  public void shouldStoreIntersection_givenOneSetDoesNotExist() {
+    Map<String, Double> scores = buildMapOfMembersAndScores(1, 10);
+    Set<Tuple> expectedResults = convertToTuples(scores, (i, x) -> x);
+    jedis.zadd(KEY1, scores);
+
+    assertThat(jedis.zinterstore(NEW_SET, KEY1, 
KEY2)).isEqualTo(scores.size());
+
+    Set<Tuple> results = jedis.zrangeWithScores(NEW_SET, 0, 10);
+
+    assertThatActualScoresAreVeryCloseToExpectedScores(expectedResults, 
results);
+  }
+
+  @Test
+  public void shouldStoreNothingAtDestinationKey_givenTwoNonIntersectingSets() 
{
+    Map<String, Double> scores = buildMapOfMembersAndScores(1, 5);
+    Map<String, Double> nonIntersectionScores = buildMapOfMembersAndScores(6, 
10);
+    jedis.zadd(KEY1, scores);
+    jedis.zadd(KEY2, nonIntersectionScores);
+
+    assertThat(jedis.zinterstore(NEW_SET, KEY1, KEY2)).isZero();
+
+    assertThat(jedis.zrangeWithScores(NEW_SET, 0, 10)).isEmpty();
+  }
+
+  @Test
+  public void 
shouldStoreSumOfIntersection_givenThreePartiallyOverlappingSets() {
+    Map<String, Double> scores1 = buildMapOfMembersAndScores(1, 10);
+    Map<String, Double> scores2 = buildMapOfMembersAndScores(6, 13);
+    Map<String, Double> scores3 = buildMapOfMembersAndScores(4, 11);
+
+    jedis.zadd(KEY1, scores1);
+    jedis.zadd(KEY2, scores2);
+    jedis.zadd(KEY3, scores3);
+
+    assertThat(jedis.zinterstore(NEW_SET, new 
ZParams().aggregate(ZParams.Aggregate.SUM),
+        KEY1, KEY2, KEY3)).isEqualTo(5);
+
+    Set<Tuple> expected = new HashSet<>();
+    for (int i = 6; i <= 10; i++) {
+      expected.add(tupleSumOfScores("player" + i, scores1, scores2, scores3));
+    }
+
+    Set<Tuple> actual = jedis.zrangeWithScores(NEW_SET, 0, 10);
+
+    assertThatActualScoresAreVeryCloseToExpectedScores(expected, actual);
+  }
+
+  @Test
+  public void 
shouldStoreMaxOfIntersection_givenThreePartiallyOverlappingSets() {
+    Map<String, Double> scores1 = buildMapOfMembersAndScores(1, 10);
+    Map<String, Double> scores2 = buildMapOfMembersAndScores(6, 13);
+    Map<String, Double> scores3 = buildMapOfMembersAndScores(4, 11);
+
+    jedis.zadd(KEY1, scores1);
+    jedis.zadd(KEY2, scores2);
+    jedis.zadd(KEY3, scores3);
+
+    assertThat(jedis.zinterstore(NEW_SET, new 
ZParams().aggregate(ZParams.Aggregate.MAX),
+        KEY1, KEY2, KEY3)).isEqualTo(5);
+
+    Set<Tuple> expected = new HashSet<>();
+    for (int i = 6; i <= 10; i++) {
+      expected.add(tupleMaxOfScores("player" + i, scores1, scores2, scores3));
+    }
+
+    Set<Tuple> actual = jedis.zrangeWithScores(NEW_SET, 0, 10);
+
+    assertThatActualScoresAreVeryCloseToExpectedScores(expected, actual);
+  }
+
+  @Test
+  public void 
shouldStoreMinOfIntersection_givenThreePartiallyOverlappingSets() {
+    Map<String, Double> scores1 = buildMapOfMembersAndScores(1, 10);
+    Map<String, Double> scores2 = buildMapOfMembersAndScores(6, 13);
+    Map<String, Double> scores3 = buildMapOfMembersAndScores(4, 11);
+
+    jedis.zadd(KEY1, scores1);
+    jedis.zadd(KEY2, scores2);
+    jedis.zadd(KEY3, scores3);
+
+    assertThat(jedis.zinterstore(NEW_SET, new 
ZParams().aggregate(ZParams.Aggregate.MIN),
+        KEY1, KEY2, KEY3)).isEqualTo(5);
+
+    Set<Tuple> expected = new HashSet<>();
+    for (int i = 6; i <= 10; i++) {
+      expected.add(tupleMinOfScores("player" + i, scores1, scores2, scores3));
+    }
+
+    Set<Tuple> actual = jedis.zrangeWithScores(NEW_SET, 0, 10);
+
+    assertThatActualScoresAreVeryCloseToExpectedScores(expected, actual);
+  }
+
+  @Test
+  public void 
shouldStoreSumOfIntersection_givenThreePartiallyOverlappingSets_andWeights() {
+    double weight1 = 0D;
+    double weight2 = 42D;
+    double weight3 = -7.3D;
+
+    Map<String, Double> scores1 = buildMapOfMembersAndScores(1, 10);
+    Map<String, Double> scores2 = buildMapOfMembersAndScores(6, 13);
+    Map<String, Double> scores3 = buildMapOfMembersAndScores(4, 11);
+
+    jedis.zadd(KEY1, scores1);
+    jedis.zadd(KEY2, scores2);
+    jedis.zadd(KEY3, scores3);
+
+    ZParams zParams = new ZParams().aggregate(ZParams.Aggregate.SUM)
+        .weights(weight1, weight2, weight3);
+
+    assertThat(jedis.zinterstore(NEW_SET, zParams, KEY1, KEY2, 
KEY3)).isEqualTo(5);
+
+    Set<Tuple> expected = new HashSet<>();
+    for (int i = 6; i <= 10; i++) {
+      expected.add(tupleSumOfScoresWithWeights("player" + i, scores1, scores2, 
scores3, weight1,
+          weight2, weight3));
+    }
+
+    Set<Tuple> actual = jedis.zrangeWithScores(NEW_SET, 0, 10);
+
+    assertThatActualScoresAreVeryCloseToExpectedScores(expected, actual);
+  }
+
+  @Test
+  public void 
shouldStoreMaxOfIntersection_givenThreePartiallyOverlappingSets_andWeights() {
+    double weight1 = 0D;
+    double weight2 = 42D;
+    double weight3 = -7.3D;
+
+    Map<String, Double> scores1 = buildMapOfMembersAndScores(1, 10);
+    Map<String, Double> scores2 = buildMapOfMembersAndScores(6, 13);
+    Map<String, Double> scores3 = buildMapOfMembersAndScores(4, 11);
+
+    jedis.zadd(KEY1, scores1);
+    jedis.zadd(KEY2, scores2);
+    jedis.zadd(KEY3, scores3);
+
+    ZParams zParams = new ZParams().aggregate(ZParams.Aggregate.MAX)
+        .weights(weight1, weight2, weight3);
+
+    assertThat(jedis.zinterstore(NEW_SET, zParams, KEY1, KEY2, 
KEY3)).isEqualTo(5);
+
+    Set<Tuple> expected = new HashSet<>();
+    for (int i = 6; i <= 10; i++) {
+      expected.add(tupleMaxOfScoresWithWeights("player" + i, scores1, scores2, 
scores3, weight1,
+          weight2, weight3));
+    }
+
+    Set<Tuple> actual = jedis.zrangeWithScores(NEW_SET, 0, 10);
+
+    assertThatActualScoresAreVeryCloseToExpectedScores(expected, actual);
+  }
+
+  @Test
+  public void 
shouldStoreMinOfIntersection_givenThreePartiallyOverlappingSets_andWeights() {
+    double weight1 = 0D;
+    double weight2 = 42D;
+    double weight3 = -7.3D;
+
+    Map<String, Double> scores1 = buildMapOfMembersAndScores(1, 10);
+    Map<String, Double> scores2 = buildMapOfMembersAndScores(6, 13);
+    Map<String, Double> scores3 = buildMapOfMembersAndScores(4, 11);
+
+    jedis.zadd(KEY1, scores1);
+    jedis.zadd(KEY2, scores2);
+    jedis.zadd(KEY3, scores3);
+
+    ZParams zParams = new ZParams().aggregate(ZParams.Aggregate.MIN)
+        .weights(weight1, weight2, weight3);
+
+    assertThat(jedis.zinterstore(NEW_SET, zParams, KEY1, KEY2, 
KEY3)).isEqualTo(5);
+
+    Set<Tuple> expected = new HashSet<>();
+    for (int i = 6; i <= 10; i++) {
+      expected.add(tupleMinOfScoresWithWeights("player" + i, scores1, scores2, 
scores3, weight1,
+          weight2, weight3));
+    }
+
+    Set<Tuple> actual = jedis.zrangeWithScores(NEW_SET, 0, 10);
+
+    assertThatActualScoresAreVeryCloseToExpectedScores(expected, actual);
+  }
+
+  @Test
+  public void 
shouldStoreMaxOfIntersection_givenThreePartiallyOverlappingSetsWithIdenticalScores()
 {
+    double score = 3.141592;
+    Map<String, Double> scores1 = buildMapOfMembersAndIdenticalScores(1, 10, 
score);
+    Map<String, Double> scores2 = buildMapOfMembersAndIdenticalScores(6, 13, 
score);
+    Map<String, Double> scores3 = buildMapOfMembersAndIdenticalScores(4, 11, 
score);
+
+    jedis.zadd(KEY1, scores1);
+    jedis.zadd(KEY2, scores2);
+    jedis.zadd(KEY3, scores3);
+
+    ZParams zParams = new ZParams().aggregate(ZParams.Aggregate.MAX);
+
+    assertThat(jedis.zinterstore(NEW_SET, zParams, KEY1, KEY2, 
KEY3)).isEqualTo(5);
+
+    Set<Tuple> expected = new HashSet<>();
+    for (int i = 6; i <= 10; i++) {
+      expected.add(tupleMaxOfScores("player" + i, scores1, scores2, scores3));

Review comment:
       This can be simplified to just
   ```
   expected.add(new Tuple("player" + i, score));
   ```

##########
File path: 
geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/executor/sortedset/ZInterStoreExecutor.java
##########
@@ -0,0 +1,126 @@
+/*
+ * 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_SYNTAX;
+import static 
org.apache.geode.redis.internal.RedisConstants.ERROR_WEIGHT_NOT_A_FLOAT;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_SLOT;
+import static org.apache.geode.redis.internal.netty.Coder.toUpperCaseBytes;
+import static 
org.apache.geode.redis.internal.netty.StringBytesGlossary.bAGGREGATE;
+import static 
org.apache.geode.redis.internal.netty.StringBytesGlossary.bWEIGHTS;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.executor.AbstractExecutor;
+import org.apache.geode.redis.internal.executor.RedisResponse;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.redis.internal.netty.Command;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class ZInterStoreExecutor extends AbstractExecutor {

Review comment:
       This class and the `ZUnionStoreExecutor` class should be modified to 
extend a parent class to prevent duplication of code, since the only difference 
between the files is what method is called on line 121. This would make 
implementing other similar sorted set comments in the future much simpler too, 
as they could also extend the parent class and just override the single method.

##########
File path: 
geode-apis-compatible-with-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/sortedset/AbstractZInterStoreIntegrationTest.java
##########
@@ -0,0 +1,696 @@
+/*
+ * 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 java.lang.Math.max;
+import static java.lang.Math.min;
+import static 
org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertAtLeastNArgs;
+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.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Random;
+import java.util.Set;
+import java.util.function.BiFunction;
+
+import org.assertj.core.data.Offset;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+import redis.clients.jedis.Protocol;
+import redis.clients.jedis.Tuple;
+import redis.clients.jedis.ZParams;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+import org.apache.geode.redis.internal.RedisConstants;
+
+public abstract class AbstractZInterStoreIntegrationTest implements 
RedisIntegrationTest {
+
+  private static final String NEW_SET = "{user1}new";
+  private static final String KEY1 = "{user1}sset1";
+  private static final String KEY2 = "{user1}sset2";
+  private static final String KEY3 = "{user1}sset3";
+
+  private 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_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.ZINTERSTORE, 3);
+  }
+
+  @Test
+  public void shouldError_givenWrongKeyType() {
+    final String STRING_KEY = "{user1}stringKey";
+    jedis.set(STRING_KEY, "value");
+    assertThatThrownBy(() -> jedis.sendCommand(NEW_SET, 
Protocol.Command.ZINTERSTORE, NEW_SET, "2",
+        STRING_KEY, KEY1)).hasMessage("WRONGTYPE " + 
RedisConstants.ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void shouldError_givenSetsCrossSlots() {
+    final String WRONG_KEY = "{user2}another";
+    assertThatThrownBy(
+        () -> jedis.sendCommand(NEW_SET, Protocol.Command.ZINTERSTORE, 
NEW_SET, "2", WRONG_KEY,
+            KEY1)).hasMessage("CROSSSLOT " + RedisConstants.ERROR_WRONG_SLOT);
+  }
+
+  @Test
+  public void shouldError_givenNumkeysTooLarge() {
+    assertThatThrownBy(
+        () -> jedis.sendCommand(NEW_SET, Protocol.Command.ZINTERSTORE, 
NEW_SET, "2", KEY1))
+            .hasMessage("ERR " + RedisConstants.ERROR_SYNTAX);
+  }
+
+  @Test
+  public void shouldError_givenNumkeysTooSmall() {
+    assertThatThrownBy(
+        () -> jedis.sendCommand(NEW_SET, Protocol.Command.ZINTERSTORE, 
NEW_SET, "1", KEY1, KEY2))
+            .hasMessage("ERR " + RedisConstants.ERROR_SYNTAX);
+  }
+
+  @Test
+  public void shouldError_givenTooManyWeights() {
+    assertThatThrownBy(
+        () -> jedis.sendCommand(NEW_SET, Protocol.Command.ZINTERSTORE, 
NEW_SET, "1",
+            KEY1, "WEIGHTS", "2", "3")).hasMessage("ERR " + 
RedisConstants.ERROR_SYNTAX);
+  }
+
+  @Test
+  public void shouldError_givenTooFewWeights() {
+    assertThatThrownBy(
+        () -> jedis.sendCommand(NEW_SET, Protocol.Command.ZINTERSTORE, 
NEW_SET, "2",
+            KEY1, KEY2, "WEIGHTS", "1")).hasMessage("ERR " + 
RedisConstants.ERROR_SYNTAX);
+  }
+
+  @Test
+  public void shouldError_givenWeightNotANumber() {

Review comment:
       Could this be "givenWeightNotAFloat" to avoid confusion with `NaN`, 
which while not a number is technically a float value?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to