Skip to content

Commit

Permalink
support full set of redis hash commands #20
Browse files Browse the repository at this point in the history
  • Loading branch information
John Loehrer committed Aug 7, 2024
1 parent 476ad06 commit 846a0b1
Show file tree
Hide file tree
Showing 4 changed files with 455 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,19 +1,165 @@
package com.github.scoquelin.arugula.commands

import scala.collection.immutable.ListMap
import scala.concurrent.Future

import com.github.scoquelin.arugula.commands.RedisKeyAsyncCommands.ScanCursor

/**
* Asynchronous commands for manipulating/querying Hashes (key/value pairs)
*
* @tparam K The key type
* @tparam V The value type
*/
trait RedisHashAsyncCommands[K, V] {
/**
* Delete one or more hash fields
* @param key The key
* @param fields The fields to delete
* @return The number of fields that were removed from the hash
*/
def hDel(key: K, fields: K*): Future[Long]

/**
* Determine if a hash field exists
* @param key The key
* @param field The field
* @return True if the field exists, false otherwise
*/
def hExists(key: K, field: K): Future[Boolean]

/**
* Get the value of a hash field
* @param key The key
* @param field The field
* @return The value of the field, or None if the field does not exist
*/
def hGet(key: K, field: K): Future[Option[V]]

/**
* get all the keys in a hash
* @param key The key
* @return A list of all the keys in the hash
*/
def hKeys(key: K): Future[Seq[K]]

/**
* Get the number of fields in a hash
* @param key The key
* @return The number of fields in the hash
*/
def hLen(key: K): Future[Long]

/**
* Get the values of all the fields in a hash
* @param key The key
* @return A list of all the values in the hash
*/
def hGetAll(key: K): Future[Map[K, V]]

/**
* Set the value of a hash field
* @param key The key
* @param field The field
* @param value The value
* @return True if the field is new, false if it was updated
*/
def hSet(key: K, field: K, value: V): Future[Boolean]

/**
* Get the values of multiple hash fields
* @param key The key
* @param fields The fields
* @return A map of field -> value for each field that exists or None if the field does not exist
*/
def hMGet(key: K, fields: K*): Future[ListMap[K, Option[V]]]

/**
* Get the values of multiple hash fields
* @param key The key
* @param fields The fields
* @return A map of field -> value for each field that exists or None if the field does not exist
*/
def hMGet(key: K, fields: => Seq[K]): Future[Map[K, Option[V]]]

/**
* Set the values of multiple hash fields
* @param key The key
* @param values A map of field -> value
*/
def hMSet(key: K, values: Map[K, V]): Future[Unit]

/**
* Set the value of a hash field, only if the field does not exist
* @param key The key
* @param field The field
* @param value The value
* @return True if the field was set, false if the field already existed
*/
def hSetNx(key: K, field: K, value: V): Future[Boolean]

/**
* Increment the integer value of a hash field by the given number
* @param key The key
* @param field The field
* @param amount The amount to increment by
* @return The new value of the field
*/
def hIncrBy(key: K, field: K, amount: Long): Future[Long]

/**
* Get a random field from a hash
* @param key The key
* @return A random field from the hash, or None if the hash is empty
*/
def hRandField(key: K): Future[Option[K]]

/**
* Get multiple random fields from a hash
* @param key The key
* @param count The number of fields to get
* @return A list of random fields from the hash
*/
def hRandField(key: K, count: Long): Future[Seq[K]]

/**
* Get a random field and its value from a hash
* @param key The key
* @return A random field and its value from the hash, or None if the hash is empty
*/
def hRandFieldWithValues(key: K): Future[Option[(K, V)]]

/**
* Get multiple random fields and their values from a hash
* @param key The key
* @param count The number of fields to get
* @return A map of random fields and their values from the hash
*/
def hRandFieldWithValues(key: K, count: Long): Future[Map[K, V]]

/**
* scan the fields of a hash, returning the cursor and a map of field -> value
* Repeat calls with the returned cursor to get all fields until the cursor is finished
* @param key The key
* @param cursor The cursor
* @param limit The maximum number of fields to return
* @param matchPattern A glob-style pattern to match fields against
* @return The next cursor and a map of field -> value
*/
def hScan(key: K, cursor: ScanCursor = ScanCursor.Initial, limit: Option[Long] = None, matchPattern: Option[String] = None): Future[(ScanCursor, Map[K, V])]

/**
* Get the length of a hash field value
* @param key The key
* @param field The field
* @return The length of the field value
*/
def hStrLen(key: K, field: K): Future[Long]

/**
* Get the values of all the fields in a hash
* @param key The key
* @return A list of all the values in the hash
*/
def hVals(key: K): Future[Seq[V]]
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package com.github.scoquelin.arugula.commands

import scala.collection.immutable.ListMap
import scala.concurrent.Future
import scala.jdk.CollectionConverters._

import com.github.scoquelin.arugula.internal.LettuceRedisCommandDelegation
import io.lettuce.core.ScanArgs

private[arugula] trait LettuceRedisHashAsyncCommands[K, V] extends RedisHashAsyncCommands[K, V] with LettuceRedisCommandDelegation[K, V]{

override def hDel(key: K, fields: K*): Future[Long] =
delegateRedisClusterCommandAndLift(_.hdel(key, fields: _*)).map(Long2long)

override def hExists(key: K, field: K): Future[Boolean] =
delegateRedisClusterCommandAndLift(_.hexists(key, field)).map(Boolean2boolean)

override def hGet(key: K, field: K): Future[Option[V]] =
delegateRedisClusterCommandAndLift(_.hget(key, field)).map(Option.apply)

Expand All @@ -19,6 +24,71 @@ private[arugula] trait LettuceRedisHashAsyncCommands[K, V] extends RedisHashAsyn
override def hIncrBy(key: K, field: K, amount: Long): Future[Long] =
delegateRedisClusterCommandAndLift(_.hincrby(key, field, amount)).map(Long2long)

override def hRandField(key: K): Future[Option[K]] =
delegateRedisClusterCommandAndLift(_.hrandfield(key)).map(Option.apply)

override def hRandField(key: K, count: Long): Future[Seq[K]] =
delegateRedisClusterCommandAndLift(_.hrandfield(key, count)).map(_.asScala.toSeq)

override def hRandFieldWithValues(key: K): Future[Option[(K, V)]] =
delegateRedisClusterCommandAndLift(_.hrandfieldWithvalues(key)).map{ kv =>
if(kv.hasValue) Some(kv.getKey -> kv.getValue) else None
}

override def hRandFieldWithValues(key: K, count: Long): Future[Map[K, V]] =
delegateRedisClusterCommandAndLift(_.hrandfieldWithvalues(key, count)).map(_.asScala.collect {
case kv if kv.hasValue => kv.getKey -> kv.getValue
}.toMap)

override def hScan(
key: K,
cursor: RedisKeyAsyncCommands.ScanCursor = RedisKeyAsyncCommands.ScanCursor.Initial,
limit: Option[Long] = None,
matchPattern: Option[String] = None
): Future[(RedisKeyAsyncCommands.ScanCursor, Map[K, V])] = {
val scanArgs = (limit, matchPattern) match {
case (Some(limitValue), Some(matchPatternValue)) =>
Some(ScanArgs.Builder.limit(limitValue).`match`(matchPatternValue))
case (Some(limitValue), None) =>
Some(ScanArgs.Builder.limit(limitValue))
case (None, Some(matchPatternValue)) =>
Some(ScanArgs.Builder.matches(matchPatternValue))
case _ =>
None
}
val lettuceCursor = io.lettuce.core.ScanCursor.of(cursor.cursor)
lettuceCursor.setFinished(cursor.finished)
val response = scanArgs match {
case Some(scanArgs) =>
delegateRedisClusterCommandAndLift(_.hscan(key, lettuceCursor, scanArgs))
case None =>
delegateRedisClusterCommandAndLift(_.hscan(key, lettuceCursor))
}
response.map{ result =>
(
RedisKeyAsyncCommands.ScanCursor(result.getCursor, finished = result.isFinished),
result.getMap.asScala.toMap
)
}
}


override def hKeys(key: K): Future[Seq[K]] =
delegateRedisClusterCommandAndLift(_.hkeys(key)).map(_.asScala.toSeq)

override def hLen(key: K): Future[Long] =
delegateRedisClusterCommandAndLift(_.hlen(key)).map(Long2long)

override def hMGet(key: K, fields: K*): Future[ListMap[K, Option[V]]] =
delegateRedisClusterCommandAndLift(_.hmget(key, fields: _*)).map(_.asScala.map{ kv =>
kv.getKey -> (if(kv.hasValue) Some(kv.getValue) else None)
}).map(ListMap.from)

override def hMGet(key: K, fields: => Seq[K]): Future[Map[K, Option[V]]] =
delegateRedisClusterCommandAndLift(_.hmget(key, fields: _*)).map(_.asScala.map{ kv =>
kv.getKey -> (if(kv.hasValue) Some(kv.getValue) else None)
}.toMap)

override def hSet(key: K, field: K, value: V): Future[Boolean] =
delegateRedisClusterCommandAndLift(_.hset(key, field, value)).map(Boolean2boolean)

Expand All @@ -28,4 +98,10 @@ private[arugula] trait LettuceRedisHashAsyncCommands[K, V] extends RedisHashAsyn
override def hSetNx(key: K, field: K, value: V): Future[Boolean] =
delegateRedisClusterCommandAndLift(_.hsetnx(key, field, value)).map(Boolean2boolean)

override def hStrLen(key: K, field: K): Future[Long] =
delegateRedisClusterCommandAndLift(_.hstrlen(key, field)).map(Long2long)

override def hVals(key: K): Future[Seq[V]] =
delegateRedisClusterCommandAndLift(_.hvals(key)).map(_.asScala.toSeq)

}
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with
withRedisSingleNodeAndCluster(RedisCodec.Utf8WithValueAsLongCodec) { client =>
val key = randomKey("increment-key")
for {
_ <- client.set(key, 0L)
_ <- client.incr(key)
value <- client.get(key)
_ <- value match {
Expand Down Expand Up @@ -444,6 +445,50 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with
}
}

"support complex hash operations" in {
withRedisSingleNodeAndCluster(RedisCodec.Utf8WithValueAsStringCodec) { client =>
val key = randomKey("hash-key")
val field = "field"
val value = "value"
for {
_ <- client.hSet(key, field, value)
fieldExists <- client.hExists(key, field)
_ <- fieldExists shouldBe true
randomField <- client.hRandField(key)
_ <- randomField shouldBe Some(field)
randomFields <- client.hRandField(key, 2)
_ <- randomFields shouldBe Seq(field)
randomFieldWithValue <- client.hRandFieldWithValues(key)
_ <- randomFieldWithValue shouldBe Some(field -> value)
fieldValues <- client.hKeys(key)
_ <- fieldValues shouldBe Seq(field)
fieldValues <- client.hGetAll(key)
_ <- fieldValues shouldBe Map(field -> value)
fieldValues <- client.hVals(key)
_ <- fieldValues shouldBe Seq(value)
len <- client.hLen(key)
_ <- len shouldBe 1L
hStrLen <- client.hStrLen(key, field)
_ <- hStrLen shouldBe 5L
hScanResults <- client.hScan(key)
_ <- hScanResults._1.finished shouldBe true
_ <- hScanResults._2 shouldBe Map(field -> value)
_ <- client.del(key)
_ <- client.hMSet(key, Map("field1" -> "value1", "field2" -> "value2", "extraField3" -> "value3"))
fieldValues <- client.hGetAll(key)
_ <- fieldValues shouldBe Map("field1" -> "value1", "field2" -> "value2", "extraField3" -> "value3")
scanResultsWithFilter <- client.hScan(key, matchPattern = Some("field*"))
_ <- scanResultsWithFilter._1.finished shouldBe true
_ <- scanResultsWithFilter._2 shouldBe Map("field1" -> "value1", "field2" -> "value2")
scanResultsWithLimit <- client.hScan(key, limit = Some(10))
_ <- scanResultsWithLimit._1.finished shouldBe true
_ <- scanResultsWithLimit._2 shouldBe Map("field1" -> "value1", "field2" -> "value2", "extraField3" -> "value3")
randomFieldsWithValues <- client.hRandFieldWithValues(key, 3)
_ <- randomFieldsWithValues shouldBe Map("field1" -> "value1", "field2" -> "value2", "extraField3" -> "value3")
} yield succeed
}
}

"create, retrieve, and delete a field with an integer value for a hash key" in {
withRedisSingleNodeAndCluster(RedisCodec.Utf8WithValueAsStringCodec) { client =>
val key = randomKey("int-hash-key")
Expand All @@ -466,7 +511,6 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with
} yield succeed
}
}

}

"leveraging RedisPipelineAsyncCommands" should {
Expand Down
Loading

0 comments on commit 846a0b1

Please sign in to comment.