Skip to content

Commit

Permalink
feat(Firestore): Expose read_time field (#5865)
Browse files Browse the repository at this point in the history
Implemented proper logic changes and exhaustive testing for `read_time` field to work seamlessly with firestore library.
  • Loading branch information
yash30201 committed Mar 30, 2023
1 parent 41a1b1c commit 1d44c91
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 15 deletions.
14 changes: 14 additions & 0 deletions Firestore/src/CollectionReference.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Google\Cloud\Core\DebugInfoTrait;
use Google\Cloud\Core\Iterator\ItemIterator;
use Google\Cloud\Core\Iterator\PageIterator;
use Google\Cloud\Core\Timestamp;
use Google\Cloud\Firestore\Connection\ConnectionInterface;

/**
Expand Down Expand Up @@ -255,6 +256,8 @@ public function add(array $fields = [], array $options = [])
* resume the loading of results from a specific point.
* }
* @return ItemIterator<DocumentReference>
* @throws \InvalidArgumentException if an invalid `$options.readTime` is
* specified.
*/
public function listDocuments(array $options = [])
{
Expand All @@ -266,6 +269,17 @@ public function listDocuments(array $options = [])
'mask' => []
];

if (isset($options['readTime'])) {
if (!($options['readTime'] instanceof Timestamp)) {
throw new \InvalidArgumentException(sprintf(
'`$options.readTime` must be an instance of %s',
Timestamp::class
));
}

$options['readTime'] = $options['readTime']->formatForApi();
}

return new ItemIterator(
new PageIterator(
function ($document) {
Expand Down
30 changes: 24 additions & 6 deletions Firestore/src/Connection/Grpc.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,8 @@ public function __construct(array $config = [])
*/
public function batchGetDocuments(array $args)
{
if (isset($args['readTime'])) {
$args['readTime'] = $this->serializer->decodeMessage(
new ProtobufTimestamp(),
$args['readTime']
);
}
$args = $this->decodeTimestamp($args);

return $this->send([$this->firestore, 'batchGetDocuments'], [
$this->pluck('database', $args),
$this->pluck('documents', $args),
Expand Down Expand Up @@ -195,6 +191,8 @@ public function batchWrite(array $args)
*/
public function listCollectionIds(array $args)
{
$args = $this->decodeTimestamp($args);

return $this->send([$this->firestore, 'listCollectionIds'], [
$this->pluck('parent', $args),
$this->addRequestHeaders($args)
Expand All @@ -211,6 +209,7 @@ public function listDocuments(array $args)
: [];

$args['mask'] = $this->documentMask($mask);
$args = $this->decodeTimestamp($args);

return $this->send([$this->firestore, 'listDocuments'], [
$this->pluck('parent', $args),
Expand Down Expand Up @@ -242,6 +241,7 @@ public function runQuery(array $args)
new StructuredQuery,
$this->pluck('structuredQuery', $args)
);
$args = $this->decodeTimestamp($args);

return $this->send([$this->firestore, 'runQuery'], [
$this->pluck('parent', $args),
Expand Down Expand Up @@ -278,6 +278,24 @@ private function addRequestHeaders(array $args)
return $args;
}

/**
* Decodes the 'readTime' API format timestamp to Protobuf timestamp if
* it is set.
*
* @param array $args
* @return array
*/
private function decodeTimestamp(array $args)
{
if (isset($args['readTime'])) {
$args['readTime'] = $this->serializer->decodeMessage(
new ProtobufTimestamp(),
$args['readTime']
);
}
return $args;
}

/**
* @access private
* @codeCoverageIgnore
Expand Down
13 changes: 13 additions & 0 deletions Firestore/src/DocumentReference.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Google\Cloud\Core\DebugInfoTrait;
use Google\Cloud\Core\Iterator\ItemIterator;
use Google\Cloud\Core\Iterator\PageIterator;
use Google\Cloud\Core\Timestamp;
use Google\Cloud\Firestore\Connection\ConnectionInterface;

/**
Expand Down Expand Up @@ -381,9 +382,21 @@ public function collection($collectionId)
*
* @param array $options Configuration options.
* @return ItemIterator<CollectionReference>
* @throws \InvalidArgumentException if an invalid `$options.readTime` is
* specified.
*/
public function collections(array $options = [])
{
if (isset($options['readTime'])) {
if (!($options['readTime'] instanceof Timestamp)) {
throw new \InvalidArgumentException(sprintf(
'`$options.readTime` must be an instance of %s',
Timestamp::class
));
}

$options['readTime'] = $options['readTime']->formatForApi();
}
$resultLimit = $this->pluck('resultLimit', $options, false);
return new ItemIterator(
new PageIterator(
Expand Down
13 changes: 13 additions & 0 deletions Firestore/src/FirestoreClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use Google\Cloud\Core\Iterator\PageIterator;
use Google\Cloud\Core\Retry;
use Google\Cloud\Core\ValidateTrait;
use Google\Cloud\Core\Timestamp;
use Google\Cloud\Firestore\Connection\Grpc;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Message\StreamInterface;
Expand Down Expand Up @@ -287,9 +288,21 @@ public function collection($name)
* resume the loading of results from a specific point.
* }
* @return ItemIterator<CollectionReference>
* @throws \InvalidArgumentException if an invalid `$options.readTime` is
* specified.
*/
public function collections(array $options = [])
{
if (isset($options['readTime'])) {
if (!($options['readTime'] instanceof Timestamp)) {
throw new \InvalidArgumentException(sprintf(
'`$options.readTime` must be an instance of %s',
Timestamp::class
));
}

$options['readTime'] = $options['readTime']->formatForApi();
}
$resultLimit = $this->pluck('resultLimit', $options, false);
return new ItemIterator(
new PageIterator(
Expand Down
15 changes: 14 additions & 1 deletion Firestore/src/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

use Google\Cloud\Core\DebugInfoTrait;
use Google\Cloud\Core\ExponentialBackoff;
use Google\Cloud\Core\Timestamp;
use Google\Cloud\Firestore\Connection\ConnectionInterface;
use Google\Cloud\Firestore\DocumentSnapshot;
use Google\Cloud\Firestore\FieldValue\FieldValueInterface;
Expand Down Expand Up @@ -184,11 +185,23 @@ public function __construct(
* **Defaults to** `5`.
* }
* @return QuerySnapshot<DocumentSnapshot>
* @throws \InvalidArgumentException if an invalid `$options.readTime` is
* specified.
* @throws \RuntimeException If limit-to-last is enabled but no order-by has
* been specified.
* been specified.
*/
public function documents(array $options = [])
{
if (isset($options['readTime'])) {
if (!($options['readTime'] instanceof Timestamp)) {
throw new \InvalidArgumentException(sprintf(
'`$options.readTime` must be an instance of %s',
Timestamp::class
));
}

$options['readTime'] = $options['readTime']->formatForApi();
}
$maxRetries = $this->pluck('maxRetries', $options, false);
$maxRetries = $maxRetries === null
? FirestoreClient::MAX_RETRIES
Expand Down
3 changes: 2 additions & 1 deletion Firestore/src/SnapshotTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ private function createSnapshotWithData(
* @param string $name The document name.
* @param array $options Configuration options.
* @return array
* @throws \InvalidArgumentException if an invalid `$options.readTime` is specified.
* @throws \InvalidArgumentException if an invalid `$options.readTime` is
* specified.
* @throws NotFoundException If the document does not exist.
*/
private function getSnapshot(ConnectionInterface $connection, $name, array $options = [])
Expand Down
60 changes: 60 additions & 0 deletions Firestore/tests/System/DocumentAndCollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,64 @@ public function testRootCollections()
iterator_to_array($client->collections())
);
}

public function testCollectionsWithReadTime()
{
$childName = uniqid(self::COLLECTION_NAME);
$child = $this->document->collection($childName);
self::$localDeletionQueue->add($child);
$child->add(['name' => 'John']);
// without sleep, emulator system test may fail intermittently
sleep(1);

$readTime = new Timestamp(new \DateTimeImmutable());
$collection = $this->document->collections([
'readTime' => $readTime
])->current();

$this->assertEquals($childName, $collection->id());
}

public function testRootCollectionsWithReadTime()
{
// ListCollectionIds request doesn't support read_time in options
// in emulator, thus skipping the tests for now.

$collection = self::$client->collection(uniqid(self::COLLECTION_NAME));
self::$localDeletionQueue->add($collection);
// without sleep, emulator system test may fail intermittently
sleep(1);

$readTime = new Timestamp(new \DateTimeImmutable());
$expectedCount = count(iterator_to_array(self::$client->collections()));

// Creating a random document
$document = $collection->newDocument();
$document->create(['firstName' => 'Yash']);

// Asserting we still get the collections at readTime instead of current
$collections = self::$client->collections(['readTime' => $readTime]);
$this->assertEquals(
$expectedCount,
count(iterator_to_array($collections))
);
}

public function testListDocumentsWithReadTime()
{
$collection = self::$client->collection(uniqid(self::COLLECTION_NAME));
self::$localDeletionQueue->add($collection);
$collection->add(['a' => 'b']);
// without sleep, emulator system test may fail intermittently
sleep(1);

// Creating a current timestamp and then adding a document
$readTime = new Timestamp(new \DateTimeImmutable());
$collection->add(['c' => 'd']);

// Reading at $readTime to get documents at that time
$list = $collection->listDocuments(['readTime' => $readTime]);
$this->assertCount(1, iterator_to_array($list));
$this->assertContainsOnlyInstancesOf(DocumentReference::class, $list);
}
}
7 changes: 7 additions & 0 deletions Firestore/tests/System/FirestoreTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,11 @@ public static function tearDownFixtures()
}
});
}

public static function skipEmulatorTests()
{
if ((bool) getenv("FIRESTORE_EMULATOR_HOST")) {
self::markTestSkipped('This test is not supported by the emulator.');
}
}
}
19 changes: 19 additions & 0 deletions Firestore/tests/System/QueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

namespace Google\Cloud\Firestore\Tests\System;

use Google\Cloud\Core\Timestamp;
use Google\Cloud\Firestore\FieldPath;

/**
Expand Down Expand Up @@ -197,6 +198,24 @@ public function testLimitToLastWithCursors()
$this->assertEquals([2, 3, 4], $res);
}

public function testDocumentsWithReadTime()
{
$randomVal = base64_encode(random_bytes(10));
$this->insertDoc(['foo' => $randomVal]);
// without sleep, emulator system test may fail intermittently
sleep(1);

// Creating a current timestamp and then inserting another document
$readTime = new Timestamp(new \DateTimeImmutable());
$this->insertDoc(['foo' => $randomVal]);

$resultCount = $this->query
->where('foo', '=', $randomVal)
->documents(['readTime' => $readTime])
->size();
$this->assertEquals(1, $resultCount);
}

private function insertDoc(array $fields)
{
return $this->query->add($fields);
Expand Down
Loading

0 comments on commit 1d44c91

Please sign in to comment.