Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support full set of redis hash commands #20 #21

Merged
merged 1 commit into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,19 +1,157 @@
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[List[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]]]

/**
* 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[List[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[List[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,66 @@ 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[List[K]] =
delegateRedisClusterCommandAndLift(_.hrandfield(key, count)).map(_.asScala.toList)

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[List[K]] =
delegateRedisClusterCommandAndLift(_.hkeys(key)).map(_.asScala.toList)

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 hSet(key: K, field: K, value: V): Future[Boolean] =
delegateRedisClusterCommandAndLift(_.hset(key, field, value)).map(Boolean2boolean)

Expand All @@ -28,4 +93,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[List[V]] =
delegateRedisClusterCommandAndLift(_.hvals(key)).map(_.asScala.toList)

}
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
Loading