Skip to content

Commit

Permalink
feat: add sortedSetLengthByScore (#234)
Browse files Browse the repository at this point in the history
* feat: add sortedSetLengthByScore

Add the sorted set length by score API, integration tests, and
documentation.

* Switch sorted set length by score to hit/miss/error
  • Loading branch information
nand4011 authored Oct 19, 2024
1 parent 9e62367 commit 7b4ff9e
Show file tree
Hide file tree
Showing 4 changed files with 324 additions and 4 deletions.
69 changes: 67 additions & 2 deletions src/Cache/CacheClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
use Momento\Cache\CacheOperationTypes\SetResponse;
use Momento\Cache\CacheOperationTypes\SortedSetFetchResponse;
use Momento\Cache\CacheOperationTypes\SortedSetIncrementScoreResponse;
use Momento\Cache\CacheOperationTypes\SortedSetLengthByScoreResponse;
use Momento\Cache\CacheOperationTypes\SortedSetPutElementResponse;
use Momento\Cache\CacheOperationTypes\SortedSetGetScoreResponse;
use Momento\Cache\CacheOperationTypes\CreateCacheResponse;
Expand Down Expand Up @@ -1677,8 +1678,6 @@ public function setLength(string $cacheName, string $setName): SetLengthResponse
return $this->setLengthAsync($cacheName, $setName)->wait();
}

// placeholder: sortedSetLengthByScore

/**
* Remove an element from a set.
*
Expand Down Expand Up @@ -2196,6 +2195,72 @@ public function sortedSetRemoveElements(string $cacheName, string $sortedSetName
return $this->sortedSetRemoveElementsAsync($cacheName, $sortedSetName, $values)->wait();
}

/**
* Get the length of a sorted set, optionally limited by a minimum and maximum score.
*
* @param string $cacheName Name of the cache that contains the sorted set.
* @param string $sortedSetName The name of the sorted set whose length should be returned.
* @param ?float $minScore The minimum score (inclusive) of the
* elements to fetch. Defaults to negative infinity.
* @param ?float $maxScore The maximum score (inclusive) of the
* elements to fetch. Defaults to positive infinity.
* @return ResponseFuture<SortedSetLengthByScoreResponse> A waitable future which will
* provide the result of the sorted set length by score operation upon a blocking call to wait:<br />
* <code>$response = $responseFuture->wait();</code><br />
* The response represents the result of the sorted set length by score operation.
* This result is resolved to a type-safe object of one of the following types:<br>
* * SortedSetLengthByScoreHit<br>
* * SortedSetLengthByScoreMiss<br>
* * SortedSetLengthByScoreError<br>
* Pattern matching can be to operate on the appropriate subtype:<br>
* <code>
* if ($success = $response->asHit()) {
* return $success->length();
* } elseif ($response->asMiss())
* // handle miss response
* } elseif ($error = $response->asError())
* // handle error condition
* }
* </code>
* If inspection of the response is not required, one need not call wait as
* we implicitly wait for completion of the request on destruction of the
* response future.
*/
public function sortedSetLengthByScoreAsync(string $cacheName, string $sortedSetName, ?float $minScore = null, ?float $maxScore = null): ResponseFuture
{
return $this->getNextDataClient()->sortedSetLengthByScore($cacheName, $sortedSetName, $minScore, $maxScore);
}

/**
* Get the length of a sorted set, optionally limited by a minimum and maximum score.
*
* @param string $cacheName Name of the cache that contains the sorted set.
* @param string $sortedSetName The name of the sorted set whose length should be returned.
* @param ?float $minScore The minimum score (inclusive) of the
* elements to fetch. Defaults to negative infinity.
* @param ?float $maxScore The maximum score (inclusive) of the
* elements to fetch. Defaults to positive infinity.
* @return SortedSetLengthByScoreResponse Represents the result of the sorted set length by score operation.
* This result is resolved to a type-safe object of one of the following types:<br>
* * SortedSetLengthByScoreHit<br>
* * SortedSetLengthByScoreMiss<br>
* * SortedSetLengthByScoreError<br>
* Pattern matching can be to operate on the appropriate subtype:<br>
* <code>
* if ($success = $response->asHit()) {
* return $success->length();
* } elseif ($response->asMiss())
* // handle miss response
* } elseif ($error = $response->asError())
* // handle error condition
* }
* </code>
*/
public function sortedSetLengthByScore(string $cacheName, string $sortedSetName, ?float $minScore = null, ?float $maxScore = null): SortedSetLengthByScoreResponse
{
return $this->sortedSetLengthByScoreAsync($cacheName, $sortedSetName, $minScore, $maxScore)->wait();
}

/**
* Gets the cache values stored for given keys.
*
Expand Down
101 changes: 99 additions & 2 deletions src/Cache/CacheOperationTypes/CacheOperationTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
use Cache_client\_SetFetchResponse;
use Cache_client\_SetLengthResponse;
use Cache_client\_SortedSetFetchResponse;
use Cache_client\_SortedSetLengthByScoreResponse;
use Cache_client\ECacheResult;
use Closure;
use Control_client\_ListCachesResponse;
use Generator;
use Momento\Cache\Errors\SdkError;
use Momento\Cache\Errors\UnknownError;
use Throwable;
Expand Down Expand Up @@ -3239,7 +3239,104 @@ class SetRemoveElementError extends SetRemoveElementResponse
use ErrorBody;
}

// placeholder: sortedSetLengthByScore
/**
* Parent response type for a sorted set length by score request. The
* response object is resolved to a type-safe object of one of
* the following subtypes:
*
* * SortedSetLengthByScoreHit
* * SortedSetLengthByScoreMiss
* * SortedSetLengthByScoreError
*
* Pattern matching can be used to operate on the appropriate subtype.
* For example:
* <code>
* if ($success = $response->asHit()) {
* return $success->length();
* } elseif ($response->asMiss())
* // handle miss as appropriate
* } elseif ($error = $response->asError())
* // handle error as appropriate
* }
* </code>
*/
abstract class SortedSetLengthByScoreResponse extends ResponseBase
{
/**
* @return SortedSetLengthByScoreHit|null Returns the hit subtype if the request returned a hit and null otherwise.
*/
public function asHit(): ?SortedSetLengthByScoreHit
{
if ($this->isHit()) {
return $this;
}
return null;
}

/**
* @return SortedSetLengthByScoreMiss|null Returns the miss subtype if the request returned a miss and null otherwise.
*/
public function asMiss(): ?SortedSetLengthByScoreMiss
{
if ($this->isMiss()) {
return $this;
}
return null;
}

/**
* @return SortedSetLengthByScoreError|null Returns the error subtype if the request returned an error and null otherwise.
*/
public function asError(): ?SortedSetLengthByScoreError
{
if ($this->isError()) {
return $this;
}
return null;
}
}

/**
* Indicates that the request that generated it was successful.
*/
class SortedSetLengthByScoreHit extends SortedSetLengthByScoreResponse
{
private int $length;

public function __construct(_SortedSetLengthByScoreResponse $response)
{
parent::__construct();
$this->length = $response->getFound() ? $response->getFound()->getLength() : 0;
}

/**
* @return int Length of the specified sorted set.
*/
public function length(): int
{
return $this->length;
}

public function __toString()
{
return parent::__toString() . ": {$this->length}";
}
}

/**
* Indicates that the request that generated it was a cache miss.
*/
class SortedSetLengthByScoreMiss extends SortedSetLengthByScoreResponse
{
}

/**
* Contains information about an error returned from the request.
*/
class SortedSetLengthByScoreError extends SortedSetLengthByScoreResponse
{
use ErrorBody;
}

/**
* Parent response type for a sorted set put element request. The
Expand Down
58 changes: 58 additions & 0 deletions src/Cache/Internal/ScsDataClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
use Cache_client\_SortedSetFetchRequest;
use Cache_client\_SortedSetGetScoreRequest;
use Cache_client\_SortedSetIncrementRequest;
use Cache_client\_SortedSetLengthByScoreRequest;
use Cache_client\_SortedSetPutRequest;
use Cache_client\_SortedSetRemoveRequest;
use Cache_client\_UpdateTtlRequest;
Expand Down Expand Up @@ -200,6 +201,11 @@
use Momento\Cache\CacheOperationTypes\SortedSetIncrementScoreError;
use Momento\Cache\CacheOperationTypes\SortedSetIncrementScoreResponse;
use Momento\Cache\CacheOperationTypes\SortedSetIncrementScoreSuccess;
use Momento\Cache\CacheOperationTypes\SortedSetLengthByScoreError;
use Momento\Cache\CacheOperationTypes\SortedSetLengthByScoreHit;
use Momento\Cache\CacheOperationTypes\SortedSetLengthByScoreMiss;
use Momento\Cache\CacheOperationTypes\SortedSetLengthByScoreResponse;
use Momento\Cache\CacheOperationTypes\SortedSetLengthByScoreSuccess;
use Momento\Cache\CacheOperationTypes\SortedSetPutElementError;
use Momento\Cache\CacheOperationTypes\SortedSetPutElementResponse;
use Momento\Cache\CacheOperationTypes\SortedSetPutElementsError;
Expand Down Expand Up @@ -1644,6 +1650,58 @@ function () use ($call): SortedSetPutElementsResponse {
);
}

/**
* @return ResponseFuture<SortedSetLengthByScoreResponse>
*/
public function sortedSetLengthByScore(string $cacheName, string $sortedSetName, ?float $minScore = null, ?float $maxScore = null): ResponseFuture
{
try {
validateCacheName($cacheName);
validateSortedSetName($sortedSetName);
validateSortedSetScores($minScore, $maxScore);
$sortedSetLengthByScoreRequest = new _SortedSetLengthByScoreRequest();
$sortedSetLengthByScoreRequest->setSetName($sortedSetName);

if (!is_null($minScore)) {
$sortedSetLengthByScoreRequest->setInclusiveMin($minScore);
} else {
$sortedSetLengthByScoreRequest->setUnboundedMin(new _Unbounded());
}
if (!is_null($maxScore)) {
$sortedSetLengthByScoreRequest->setInclusiveMax($maxScore);
} else {
$sortedSetLengthByScoreRequest->setUnboundedMax(new _Unbounded());
}

$call = $this->grpcManager->client->SortedSetLengthByScore(
$sortedSetLengthByScoreRequest,
["cache" => [$cacheName]],
["timeout" => $this->timeout],
);
} catch (SdkError $e) {
return ResponseFuture::createResolved(new SortedSetLengthByScoreError($e));
} catch (Exception $e) {
return ResponseFuture::createResolved(new SortedSetLengthByScoreError(new UnknownError($e->getMessage())));
}

return ResponseFuture::createPending(
function () use ($call): SortedSetLengthByScoreResponse {
try {
$response = $this->processCall($call);

if ($response->hasFound()) {
return new SortedSetLengthByScoreHit($response);
}
return new SortedSetLengthByScoreMiss();
} catch (SdkError $e) {
return new SortedSetLengthByScoreError($e);
} catch (Exception $e) {
return new SortedSetLengthByScoreError(new UnknownError($e->getMessage(), 0, $e));
}
}
);
}

public function sortedSetIncrementScore(string $cacheName, string $sortedSetName, string $value, float $amount, ?CollectionTtl $ttl): ResponseFuture
{
try {
Expand Down
100 changes: 100 additions & 0 deletions tests/Cache/CacheClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3936,6 +3936,106 @@ public function testSortedSetGetScore_NonexistentCache()
$this->assertEquals(MomentoErrorCode::CACHE_NOT_FOUND_ERROR, $response->asError()->errorCode());
}

public function testSortedSetLengthByScore_HappyPath()
{
$sortedSetName = uniqid();

$elements = [
"foo" => 1.0,
"bar" => 2.0,
"baz" => 3.0,
"qux" => 4.0,
];

// the length is 0 when the set does not yet exist
$response = $this->client->sortedSetLengthByScore($this->TEST_CACHE_NAME, $sortedSetName);
$this->assertNull($response->asError(), "Error occurred while fetching sorted set '$sortedSetName'");
$this->assertNotNull($response->asMiss(), "Expected a miss but got: $response");

$response = $this->client->sortedSetPutElements($this->TEST_CACHE_NAME, $sortedSetName, $elements);
$this->assertNotNull($response->asSuccess(), "Expected a success but got: $response");

// full set
$response = $this->client->sortedSetLengthByScore($this->TEST_CACHE_NAME, $sortedSetName);
$this->assertNull($response->asError(), "Error occurred while fetching sorted set '$sortedSetName'");
$this->assertNotNull($response->asHit(), "Expected a success but got: $response");

$fetchedLength = $response->asHit()->length();
$expectedLength = 4;
$this->assertEquals($expectedLength, $fetchedLength, "expected length of non-existent sorted set to be $expectedLength, not $fetchedLength");

// limit by min score
$response = $this->client->sortedSetLengthByScore($this->TEST_CACHE_NAME, $sortedSetName, 1.1);
$this->assertNull($response->asError(), "Error occurred while fetching sorted set '$sortedSetName'");
$this->assertNotNull($response->asHit(), "Expected a success but got: $response");

$fetchedLength = $response->asHit()->length();
$expectedLength = 3;
$this->assertEquals($expectedLength, $fetchedLength, "expected length of non-existent sorted set to be $expectedLength, not $fetchedLength");

// limit by max score
$response = $this->client->sortedSetLengthByScore($this->TEST_CACHE_NAME, $sortedSetName, null, 3.9);
$this->assertNull($response->asError(), "Error occurred while fetching sorted set '$sortedSetName'");
$this->assertNotNull($response->asHit(), "Expected a success but got: $response");

$fetchedLength = $response->asHit()->length();
$expectedLength = 3;
$this->assertEquals($expectedLength, $fetchedLength, "expected length of non-existent sorted set to be $expectedLength, not $fetchedLength");

// limit by min and max score
$response = $this->client->sortedSetLengthByScore($this->TEST_CACHE_NAME, $sortedSetName, 1.1, 3.9);
$this->assertNull($response->asError(), "Error occurred while fetching sorted set '$sortedSetName'");
$this->assertNotNull($response->asHit(), "Expected a success but got: $response");

$fetchedLength = $response->asHit()->length();
$expectedLength = 2;
$this->assertEquals($expectedLength, $fetchedLength, "expected length of non-existent sorted set to be $expectedLength, not $fetchedLength");

// no elements in score range
$response = $this->client->sortedSetLengthByScore($this->TEST_CACHE_NAME, $sortedSetName, 100.0);
$this->assertNull($response->asError(), "Error occurred while fetching sorted set '$sortedSetName'");
$this->assertNotNull($response->asHit(), "Expected a success but got: $response");

$fetchedLength = $response->asHit()->length();
$expectedLength = 0;
$this->assertEquals($expectedLength, $fetchedLength, "expected length of non-existent sorted set to be $expectedLength, not $fetchedLength");
}

public function testSortedSetLengthByScoreWithNonexistantCache_ThrowsException()
{
$cacheName = uniqid();
$sortedSetName = uniqid();
$response = $this->client->sortedSetLengthByScore($cacheName, $sortedSetName);
$this->assertNotNull($response->asError(), "Expected error but got: $response");
$this->assertEquals(MomentoErrorCode::CACHE_NOT_FOUND_ERROR, $response->asError()->errorCode());
}

public function testSortedSetLengthByScoreWithNullCacheName_ThrowsException()
{
$sortedSetName = uniqid();
$response = $this->client->sortedSetLengthByScore((string)null, $sortedSetName);
$this->assertNotNull($response->asError(), "Expected error but got: $response");
$this->assertEquals(MomentoErrorCode::INVALID_ARGUMENT_ERROR, $response->asError()->errorCode());
}

public function testSortedSetLengthByScoreWithEmptyCacheName_ThrowsException()
{
$sortedSetName = uniqid();
$response = $this->client->sortedSetLengthByScore("", $sortedSetName);
$this->assertNotNull($response->asError(), "Expected error but got: $response");
$this->assertEquals(MomentoErrorCode::INVALID_ARGUMENT_ERROR, $response->asError()->errorCode());
}

public function testSortedSetLengthByScoreWithMinScoreLargerThanMaxScore_ThrowsException()
{
$sortedSetName = uniqid();
$minScore = 100.0;
$maxScore = 1.0;
$response = $this->client->sortedSetLengthByScore($this->TEST_CACHE_NAME, $sortedSetName, $minScore, $maxScore);
$this->assertNotNull($response->asError(), "Expected error but got: $response");
$this->assertEquals(MomentoErrorCode::INVALID_ARGUMENT_ERROR, $response->asError()->errorCode());
}

public function testGetBatch_HappyPath()
{
$cacheName = $this->TEST_CACHE_NAME;
Expand Down

0 comments on commit 7b4ff9e

Please sign in to comment.