Skip to content

Commit

Permalink
Merge pull request #29 from scoquelin/jl-sets
Browse files Browse the repository at this point in the history
support all of the Redis Set commands #22
  • Loading branch information
72squared authored Aug 9, 2024
2 parents b8b9ed1 + 4591135 commit d899581
Show file tree
Hide file tree
Showing 14 changed files with 724 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ trait RedisCommandsClient[K, V]
with RedisStringAsyncCommands[K, V]
with RedisHashAsyncCommands[K, V]
with RedisListAsyncCommands[K, V]
with RedisSetAsyncCommands[K, V]
with RedisSortedSetAsyncCommands[K, V]
with RedisServerAsyncCommands[K, V]
with RedisPipelineAsyncCommands[K, V]
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@ import scala.concurrent.Future
trait RedisBaseAsyncCommands[K, V] {
def ping: Future[String]
}

object RedisBaseAsyncCommands {
val InitialCursor: String = "0"

final case class ScanResults[T](cursor: String, finished: Boolean, values: T)

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.github.scoquelin.arugula.commands
import scala.collection.immutable.ListMap
import scala.concurrent.Future

import com.github.scoquelin.arugula.commands.RedisKeyAsyncCommands.ScanCursor
import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.{ScanResults, InitialCursor}

/**
* Asynchronous commands for manipulating/querying Hashes (key/value pairs)
Expand Down Expand Up @@ -133,12 +133,20 @@ trait RedisHashAsyncCommands[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 cursor The cursor to resume scanning from previous calls
* (use InitialCursor to start at the beginning).
* @param limit The maximum number of fields to return. If None, the server determines the limit.
* Note that redis may return more fields than the limit or less than the limit. This is
* a hint to the server, not a guarantee.
* @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])]
def hScan(
key: K,
cursor: String = InitialCursor,
limit: Option[Long] = None,
matchPattern: Option[String] = None
): Future[ScanResults[Map[K, V]]]

/**
* Get the length of a hash field value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,3 @@ trait RedisKeyAsyncCommands[K, V] {
def expire(key: K, expiresIn: FiniteDuration): Future[Boolean]
def ttl(key: K): Future[Option[FiniteDuration]]
}

object RedisKeyAsyncCommands {
final case class ScanCursor(cursor: String, finished: Boolean)

object ScanCursor{
def apply(cursor: String) = new ScanCursor(cursor, finished = false)

val Initial: ScanCursor = ScanCursor("0", finished = false)

val Finished: ScanCursor = ScanCursor("0", finished = true)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package com.github.scoquelin.arugula.commands

import scala.concurrent.Future

import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.{InitialCursor, ScanResults}

/**
* Asynchronous commands for manipulating/querying Sets
* @tparam K The key type
* @tparam V The value type
*/

trait RedisSetAsyncCommands[K, V] {
/**
* add one or more members to a set
* @param key The key
* @param values The values to add
* @return The number of elements that were added to the set
*/
def sAdd(key: K, values: V*): Future[Long]

/**
* get the number of members in a set
* @param key The key
* @return The number of elements in the set
*/
def sCard(key: K): Future[Long]

/**
* get the difference between sets
* @param keys The keys
* @return The difference between the sets
*/
def sDiff(keys: K*): Future[Set[V]]

/**
* store the difference between sets
* @param destination The destination key
* @param keys The keys
* @return The number of elements in the resulting set
*/
def sDiffStore(destination: K, keys: K*): Future[Long]

/**
* get the intersection between sets
* @param keys The keys
* @return The intersection between the sets
*/
def sInter(keys: K*): Future[Set[V]]

/**
* get the number of elements in the intersection between sets
* @param keys The keys
* @return The number of elements in the resulting set
*/
def sInterCard(keys: K*): Future[Long]

/**
* get the intersection between sets and store the result
* @param destination The destination key
* @param keys The keys
* @return The number of elements in the resulting set
*/
def sInterStore(destination: K, keys: K*): Future[Long]

/**
* determine if a member is in a set
* @param key The key
* @param value The value
* @return True if the value is in the set, false otherwise
*/
def sIsMember(key: K, value: V): Future[Boolean]

/**
* get all the members in a set
* @param key The key
* @return A list of all the members in the set
*/
def sMembers(key: K): Future[Set[V]]

/**
* determine if members are in a set
* @param key The key
* @param values The values
* @return A list of booleans indicating if the values are in the set
*/
def smIsMember(key: K, values: V*): Future[List[Boolean]]

/**
* move a member from one set to another
* @param source The source key
* @param destination The destination key
* @param member The member to move
* @return True if the member was moved, false otherwise
*/
def sMove(source: K, destination: K, member: V): Future[Boolean]

/**
* Remove and return a random member from a set.
* @param key The key
* @return The removed member, or None if the set is empty
*/
def sPop(key: K): Future[Option[V]]

/**
* Get a random member from a set.
* @param key The key
* @return The random member, or None if the set is empty
*/
def sRandMember(key: K): Future[Option[V]]

/**
* Get one or more random members from a set.
* @param key The key
* @param count The number of members to get
* @return The random members
*/
def sRandMember(key: K, count: Long): Future[Set[V]]

/**
* Remove one or more members from a set
* @param key The key
* @param values The values to remove
* @return The number of members that were removed from the set
*/
def sRem(key: K, values: V*): Future[Long]

/**
* Get the union between sets
* @param keys The keys
* @return The union between the sets
*/
def sUnion(keys: K*): Future[Set[V]]

/**
* Store the union between sets
* @param destination The destination key
* @param keys The keys
* @return The number of elements in the resulting set
*/
def sUnionStore(destination: K, keys: K*): Future[Long]

/**
* Incrementally iterate over a set, retrieving members in batches, using a cursor
* to resume from the last position
* @param key The key
* @param cursor The cursor
* @param limit The maximum number of elements to return
* (note: the actual number of elements returned may be less than the limit)
* (note: if the limit is None, the server will determine the number of elements to return)
* @param matchPattern The pattern to match
* (note: the pattern is a glob-style pattern)
* (note: if the pattern is None, all elements will be returned)
* @return The cursor, whether the cursor is finished, and the elements
*/
def sScan(
key: K,
cursor: String = InitialCursor,
limit: Option[Long] = None,
matchPattern: Option[String] = None
): Future[ScanResults[Set[V]]]
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@ package com.github.scoquelin.arugula.commands

import scala.concurrent.Future

import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.{InitialCursor, ScanResults}

trait RedisSortedSetAsyncCommands[K, V] {
import RedisKeyAsyncCommands.ScanCursor
import RedisSortedSetAsyncCommands._
def zAdd(key: K, args: Option[ZAddOptions], values: ScoreWithValue[V]*): Future[Long]
def zPopMin(key: K, count: Long): Future[List[ScoreWithValue[V]]]
def zPopMax(key: K, count: Long): Future[List[ScoreWithValue[V]]]
def zRangeWithScores(key: K, start: Long, stop: Long): Future[List[ScoreWithValue[V]]]
def zRangeByScore[T: Numeric](key: K, range: ZRange[T], limit: Option[RangeLimit]): Future[List[V]]
def zRevRangeByScore[T: Numeric](key: K, range: ZRange[T], limit: Option[RangeLimit]): Future[List[V]]
def zScan(key: K, cursor: ScanCursor = ScanCursor.Initial, limit: Option[Long] = None, matchPattern: Option[String] = None): Future[ScanCursorWithScoredValues[V]]
def zScan(key: K, cursor: String = InitialCursor, limit: Option[Long] = None, matchPattern: Option[String] = None): Future[ScanResults[List[ScoreWithValue[V]]]]
def zRem(key: K, values: V*): Future[Long]
def zRemRangeByRank(key: K, start: Long, stop: Long): Future[Long]
def zRemRangeByScore[T: Numeric](key: K, range: ZRange[T]): Future[Long]
}

object RedisSortedSetAsyncCommands {
import RedisKeyAsyncCommands.ScanCursor

sealed trait ZAddOptions
object ZAddOptions {
Expand All @@ -37,5 +37,4 @@ object RedisSortedSetAsyncCommands {
final case class ScoreWithValue[V](score: Double, value: V)
final case class ZRange[T](start: T, end: T)
final case class RangeLimit(offset: Long, count: Long)
final case class ScanCursorWithScoredValues[V](cursor: ScanCursor, values: List[ScoreWithValue[V]])
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ private[arugula] class LettuceRedisCommandsClient[K, V](
with LettuceRedisServerAsyncCommands[K, V]
with LettuceRedisListAsyncCommands[K, V]
with LettuceRedisStringAsyncCommands[K, V]
with LettuceRedisSetAsyncCommands[K, V]
with LettuceRedisSortedSetAsyncCommands[K, V]
with LettuceRedisPipelineAsyncCommands[K, V] {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import scala.collection.immutable.ListMap
import scala.concurrent.Future
import scala.jdk.CollectionConverters._

import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.{InitialCursor, ScanResults}
import com.github.scoquelin.arugula.internal.LettuceRedisCommandDelegation
import io.lettuce.core.ScanArgs

Expand Down Expand Up @@ -40,12 +41,7 @@ private[arugula] trait LettuceRedisHashAsyncCommands[K, V] extends RedisHashAsyn
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])] = {
override def hScan(key: K, cursor: String = InitialCursor, limit: Option[Long] = None, matchPattern: Option[String] = None): Future[ScanResults[Map[K, V]]] = {
val scanArgs = (limit, matchPattern) match {
case (Some(limitValue), Some(matchPatternValue)) =>
Some(ScanArgs.Builder.limit(limitValue).`match`(matchPatternValue))
Expand All @@ -56,18 +52,18 @@ private[arugula] trait LettuceRedisHashAsyncCommands[K, V] extends RedisHashAsyn
case _ =>
None
}
val lettuceCursor = io.lettuce.core.ScanCursor.of(cursor.cursor)
lettuceCursor.setFinished(cursor.finished)
val lettuceCursor = io.lettuce.core.ScanCursor.of(cursor)
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
ScanResults(
cursor = result.getCursor,
finished = result.isFinished,
values = result.getMap.asScala.toMap
)
}
}
Expand Down
Loading

0 comments on commit d899581

Please sign in to comment.