diff --git a/modules/api/src/main/scala/com/github/scoquelin/arugula/RedisCommandsClient.scala b/modules/api/src/main/scala/com/github/scoquelin/arugula/RedisCommandsClient.scala index 71dd1b6..f4eee96 100644 --- a/modules/api/src/main/scala/com/github/scoquelin/arugula/RedisCommandsClient.scala +++ b/modules/api/src/main/scala/com/github/scoquelin/arugula/RedisCommandsClient.scala @@ -13,6 +13,7 @@ trait RedisCommandsClient[K, V] with RedisKeyAsyncCommands[K, V] with RedisStringAsyncCommands[K, V] with RedisHashAsyncCommands[K, V] + with RedisHLLAsyncCommands[K, V] with RedisListAsyncCommands[K, V] with RedisSetAsyncCommands[K, V] with RedisSortedSetAsyncCommands[K, V] diff --git a/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisHLLAsyncCommands.scala b/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisHLLAsyncCommands.scala new file mode 100644 index 0000000..b1df0fe --- /dev/null +++ b/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisHLLAsyncCommands.scala @@ -0,0 +1,34 @@ +package com.github.scoquelin.arugula.commands + +import scala.concurrent.Future + +/** + * This trait provides Redis HyperLogLog commands. + * HyperLogLog is a probabilistic data structure used to estimate the cardinality of a set. + * @see https://redis.io/docs/latest/develop/data-types/probabilistic/hyperloglogs/ + * @tparam K The key type (usually String) + * @tparam V The value type (usually String) + */ +trait RedisHLLAsyncCommands[K, V] { + /** + * Adds the specified elements to the specified HyperLogLog + * @param key The key of the HyperLogLog + * @param values The values to add + * @return The number of elements that were added to the HyperLogLog + */ + def pfAdd(key: K, values: V*): Future[Long] + + /** + * Merge multiple HyperLogLogs into a single one + * @param destinationKey The key of the HyperLogLog to merge into + * @param sourceKeys The keys of the HyperLogLogs to merge + */ + def pfMerge(destinationKey: K, sourceKeys: K*): Future[Unit] + + /** + * Get the number of elements in the HyperLogLog + * @param keys The keys of the HyperLogLogs to count + * @return The number of elements in the HyperLogLog + */ + def pfCount(keys: K*): Future[Long] +} diff --git a/modules/core/src/main/scala/com/github/scoquelin/arugula/LettuceRedisCommandsClient.scala b/modules/core/src/main/scala/com/github/scoquelin/arugula/LettuceRedisCommandsClient.scala index 6c2100a..0fa1243 100644 --- a/modules/core/src/main/scala/com/github/scoquelin/arugula/LettuceRedisCommandsClient.scala +++ b/modules/core/src/main/scala/com/github/scoquelin/arugula/LettuceRedisCommandsClient.scala @@ -31,6 +31,7 @@ private[arugula] class LettuceRedisCommandsClient[K, V]( with LettuceRedisBaseAsyncCommands[K, V] with LettuceRedisKeyAsyncCommands[K, V] with LettuceRedisHashAsyncCommands[K, V] + with LettuceRedisHLLAsyncCommands[K, V] with LettuceRedisServerAsyncCommands[K, V] with LettuceRedisListAsyncCommands[K, V] with LettuceRedisScriptingAsyncCommands[K, V] diff --git a/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisHLLAsyncCommands.scala b/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisHLLAsyncCommands.scala new file mode 100644 index 0000000..a4ab9a7 --- /dev/null +++ b/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisHLLAsyncCommands.scala @@ -0,0 +1,16 @@ +package com.github.scoquelin.arugula.commands + +import scala.concurrent.Future + +import com.github.scoquelin.arugula.internal.LettuceRedisCommandDelegation + +trait LettuceRedisHLLAsyncCommands[K, V] extends RedisHLLAsyncCommands[K, V] with LettuceRedisCommandDelegation[K, V] { + override def pfAdd(key: K, values: V*): Future[Long] = + delegateRedisClusterCommandAndLift(_.pfadd(key, values: _*)).map(Long2long) + + override def pfMerge(destinationKey: K, sourceKeys: K*): Future[Unit] = + delegateRedisClusterCommandAndLift(_.pfmerge(destinationKey, sourceKeys: _*)).map(_ => ()) + + override def pfCount(keys: K*): Future[Long] = + delegateRedisClusterCommandAndLift(_.pfcount(keys: _*)).map(Long2long) +} diff --git a/modules/tests/it/src/test/scala/com/github/scoquelin/arugula/RedisCommandsIntegrationSpec.scala b/modules/tests/it/src/test/scala/com/github/scoquelin/arugula/RedisCommandsIntegrationSpec.scala index ab5b41b..33c43bd 100644 --- a/modules/tests/it/src/test/scala/com/github/scoquelin/arugula/RedisCommandsIntegrationSpec.scala +++ b/modules/tests/it/src/test/scala/com/github/scoquelin/arugula/RedisCommandsIntegrationSpec.scala @@ -1036,6 +1036,25 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with } } + "leveraging RedisHLLAsyncCommands" should { + "add, count and merge HyperLogLog keys" in { + withRedisSingleNodeAndCluster(RedisCodec.Utf8WithValueAsStringCodec) { client => + + val key1 = randomKey("hll-key1", suffix = "{user1}") + val key2 = randomKey("hll-key2", suffix = "{user1}") + for { + _ <- client.pfAdd(key1, "a", "b", "c", "d", "e") + _ <- client.pfAdd(key2, "a", "b", "f", "g", "h") + count <- client.pfCount(key1, key2) + _ <- count shouldBe 8L + _ <- client.pfMerge(key1, key2) + count <- client.pfCount(key1) + _ <- count shouldBe 8L + } yield succeed + } + } + } + "leveraging RedisSetAsyncCommands" should { "create, retrieve, pop, and remove values in a set" in { withRedisSingleNodeAndCluster(RedisCodec.Utf8WithValueAsStringCodec) { client => diff --git a/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisHLLAsyncCommandsSpec.scala b/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisHLLAsyncCommandsSpec.scala new file mode 100644 index 0000000..f647536 --- /dev/null +++ b/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisHLLAsyncCommandsSpec.scala @@ -0,0 +1,57 @@ +package com.github.scoquelin.arugula + +import io.lettuce.core.RedisFuture +import org.mockito.Mockito.{verify, when} +import org.scalatest.matchers.must.Matchers +import org.scalatest.{FutureOutcome, wordspec} + +class LettuceRedisHLLAsyncCommandsSpec extends wordspec.FixtureAsyncWordSpec with Matchers { + + override type FixtureParam = LettuceRedisCommandsClientFixture.TestContext + + override def withFixture(test: OneArgAsyncTest): FutureOutcome = + withFixture(test.toNoArgAsyncTest(new LettuceRedisCommandsClientFixture.TestContext)) + + "LettuceRedisHLLAsyncCommands" should { + "delegate PFADD command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = 1L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.pfadd("key", "value")).thenReturn(mockRedisFuture) + + testClass.pfAdd("key", "value").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).pfadd("key", "value") + succeed + } + } + + "delegate PFMERGE command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn("OK") + when(lettuceAsyncCommands.pfmerge("destinationKey", "sourceKey")).thenReturn(mockRedisFuture) + + testClass.pfMerge("destinationKey", "sourceKey").map { result => + result mustBe () + verify(lettuceAsyncCommands).pfmerge("destinationKey", "sourceKey") + succeed + } + } + + "delegate PFCOUNT command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = 1L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.pfcount("key")).thenReturn(mockRedisFuture) + + testClass.pfCount("key").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).pfcount("key") + succeed + } + } + } +}