Skip to content

Commit 1d44c91

Browse files
authored
feat(Firestore): Expose read_time field (#5865)
Implemented proper logic changes and exhaustive testing for `read_time` field to work seamlessly with firestore library.
1 parent 41a1b1c commit 1d44c91

10 files changed

Lines changed: 252 additions & 15 deletions

File tree

Firestore/src/CollectionReference.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Google\Cloud\Core\DebugInfoTrait;
2222
use Google\Cloud\Core\Iterator\ItemIterator;
2323
use Google\Cloud\Core\Iterator\PageIterator;
24+
use Google\Cloud\Core\Timestamp;
2425
use Google\Cloud\Firestore\Connection\ConnectionInterface;
2526

2627
/**
@@ -255,6 +256,8 @@ public function add(array $fields = [], array $options = [])
255256
* resume the loading of results from a specific point.
256257
* }
257258
* @return ItemIterator<DocumentReference>
259+
* @throws \InvalidArgumentException if an invalid `$options.readTime` is
260+
* specified.
258261
*/
259262
public function listDocuments(array $options = [])
260263
{
@@ -266,6 +269,17 @@ public function listDocuments(array $options = [])
266269
'mask' => []
267270
];
268271

272+
if (isset($options['readTime'])) {
273+
if (!($options['readTime'] instanceof Timestamp)) {
274+
throw new \InvalidArgumentException(sprintf(
275+
'`$options.readTime` must be an instance of %s',
276+
Timestamp::class
277+
));
278+
}
279+
280+
$options['readTime'] = $options['readTime']->formatForApi();
281+
}
282+
269283
return new ItemIterator(
270284
new PageIterator(
271285
function ($document) {

Firestore/src/Connection/Grpc.php

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,8 @@ public function __construct(array $config = [])
122122
*/
123123
public function batchGetDocuments(array $args)
124124
{
125-
if (isset($args['readTime'])) {
126-
$args['readTime'] = $this->serializer->decodeMessage(
127-
new ProtobufTimestamp(),
128-
$args['readTime']
129-
);
130-
}
125+
$args = $this->decodeTimestamp($args);
126+
131127
return $this->send([$this->firestore, 'batchGetDocuments'], [
132128
$this->pluck('database', $args),
133129
$this->pluck('documents', $args),
@@ -195,6 +191,8 @@ public function batchWrite(array $args)
195191
*/
196192
public function listCollectionIds(array $args)
197193
{
194+
$args = $this->decodeTimestamp($args);
195+
198196
return $this->send([$this->firestore, 'listCollectionIds'], [
199197
$this->pluck('parent', $args),
200198
$this->addRequestHeaders($args)
@@ -211,6 +209,7 @@ public function listDocuments(array $args)
211209
: [];
212210

213211
$args['mask'] = $this->documentMask($mask);
212+
$args = $this->decodeTimestamp($args);
214213

215214
return $this->send([$this->firestore, 'listDocuments'], [
216215
$this->pluck('parent', $args),
@@ -242,6 +241,7 @@ public function runQuery(array $args)
242241
new StructuredQuery,
243242
$this->pluck('structuredQuery', $args)
244243
);
244+
$args = $this->decodeTimestamp($args);
245245

246246
return $this->send([$this->firestore, 'runQuery'], [
247247
$this->pluck('parent', $args),
@@ -278,6 +278,24 @@ private function addRequestHeaders(array $args)
278278
return $args;
279279
}
280280

281+
/**
282+
* Decodes the 'readTime' API format timestamp to Protobuf timestamp if
283+
* it is set.
284+
*
285+
* @param array $args
286+
* @return array
287+
*/
288+
private function decodeTimestamp(array $args)
289+
{
290+
if (isset($args['readTime'])) {
291+
$args['readTime'] = $this->serializer->decodeMessage(
292+
new ProtobufTimestamp(),
293+
$args['readTime']
294+
);
295+
}
296+
return $args;
297+
}
298+
281299
/**
282300
* @access private
283301
* @codeCoverageIgnore

Firestore/src/DocumentReference.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Google\Cloud\Core\DebugInfoTrait;
2121
use Google\Cloud\Core\Iterator\ItemIterator;
2222
use Google\Cloud\Core\Iterator\PageIterator;
23+
use Google\Cloud\Core\Timestamp;
2324
use Google\Cloud\Firestore\Connection\ConnectionInterface;
2425

2526
/**
@@ -381,9 +382,21 @@ public function collection($collectionId)
381382
*
382383
* @param array $options Configuration options.
383384
* @return ItemIterator<CollectionReference>
385+
* @throws \InvalidArgumentException if an invalid `$options.readTime` is
386+
* specified.
384387
*/
385388
public function collections(array $options = [])
386389
{
390+
if (isset($options['readTime'])) {
391+
if (!($options['readTime'] instanceof Timestamp)) {
392+
throw new \InvalidArgumentException(sprintf(
393+
'`$options.readTime` must be an instance of %s',
394+
Timestamp::class
395+
));
396+
}
397+
398+
$options['readTime'] = $options['readTime']->formatForApi();
399+
}
387400
$resultLimit = $this->pluck('resultLimit', $options, false);
388401
return new ItemIterator(
389402
new PageIterator(

Firestore/src/FirestoreClient.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Google\Cloud\Core\Iterator\PageIterator;
2727
use Google\Cloud\Core\Retry;
2828
use Google\Cloud\Core\ValidateTrait;
29+
use Google\Cloud\Core\Timestamp;
2930
use Google\Cloud\Firestore\Connection\Grpc;
3031
use Psr\Cache\CacheItemPoolInterface;
3132
use Psr\Http\Message\StreamInterface;
@@ -287,9 +288,21 @@ public function collection($name)
287288
* resume the loading of results from a specific point.
288289
* }
289290
* @return ItemIterator<CollectionReference>
291+
* @throws \InvalidArgumentException if an invalid `$options.readTime` is
292+
* specified.
290293
*/
291294
public function collections(array $options = [])
292295
{
296+
if (isset($options['readTime'])) {
297+
if (!($options['readTime'] instanceof Timestamp)) {
298+
throw new \InvalidArgumentException(sprintf(
299+
'`$options.readTime` must be an instance of %s',
300+
Timestamp::class
301+
));
302+
}
303+
304+
$options['readTime'] = $options['readTime']->formatForApi();
305+
}
293306
$resultLimit = $this->pluck('resultLimit', $options, false);
294307
return new ItemIterator(
295308
new PageIterator(

Firestore/src/Query.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
use Google\Cloud\Core\DebugInfoTrait;
2121
use Google\Cloud\Core\ExponentialBackoff;
22+
use Google\Cloud\Core\Timestamp;
2223
use Google\Cloud\Firestore\Connection\ConnectionInterface;
2324
use Google\Cloud\Firestore\DocumentSnapshot;
2425
use Google\Cloud\Firestore\FieldValue\FieldValueInterface;
@@ -184,11 +185,23 @@ public function __construct(
184185
* **Defaults to** `5`.
185186
* }
186187
* @return QuerySnapshot<DocumentSnapshot>
188+
* @throws \InvalidArgumentException if an invalid `$options.readTime` is
189+
* specified.
187190
* @throws \RuntimeException If limit-to-last is enabled but no order-by has
188-
* been specified.
191+
* been specified.
189192
*/
190193
public function documents(array $options = [])
191194
{
195+
if (isset($options['readTime'])) {
196+
if (!($options['readTime'] instanceof Timestamp)) {
197+
throw new \InvalidArgumentException(sprintf(
198+
'`$options.readTime` must be an instance of %s',
199+
Timestamp::class
200+
));
201+
}
202+
203+
$options['readTime'] = $options['readTime']->formatForApi();
204+
}
192205
$maxRetries = $this->pluck('maxRetries', $options, false);
193206
$maxRetries = $maxRetries === null
194207
? FirestoreClient::MAX_RETRIES

Firestore/src/SnapshotTrait.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ private function createSnapshotWithData(
100100
* @param string $name The document name.
101101
* @param array $options Configuration options.
102102
* @return array
103-
* @throws \InvalidArgumentException if an invalid `$options.readTime` is specified.
103+
* @throws \InvalidArgumentException if an invalid `$options.readTime` is
104+
* specified.
104105
* @throws NotFoundException If the document does not exist.
105106
*/
106107
private function getSnapshot(ConnectionInterface $connection, $name, array $options = [])

Firestore/tests/System/DocumentAndCollectionTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,4 +196,64 @@ public function testRootCollections()
196196
iterator_to_array($client->collections())
197197
);
198198
}
199+
200+
public function testCollectionsWithReadTime()
201+
{
202+
$childName = uniqid(self::COLLECTION_NAME);
203+
$child = $this->document->collection($childName);
204+
self::$localDeletionQueue->add($child);
205+
$child->add(['name' => 'John']);
206+
// without sleep, emulator system test may fail intermittently
207+
sleep(1);
208+
209+
$readTime = new Timestamp(new \DateTimeImmutable());
210+
$collection = $this->document->collections([
211+
'readTime' => $readTime
212+
])->current();
213+
214+
$this->assertEquals($childName, $collection->id());
215+
}
216+
217+
public function testRootCollectionsWithReadTime()
218+
{
219+
// ListCollectionIds request doesn't support read_time in options
220+
// in emulator, thus skipping the tests for now.
221+
222+
$collection = self::$client->collection(uniqid(self::COLLECTION_NAME));
223+
self::$localDeletionQueue->add($collection);
224+
// without sleep, emulator system test may fail intermittently
225+
sleep(1);
226+
227+
$readTime = new Timestamp(new \DateTimeImmutable());
228+
$expectedCount = count(iterator_to_array(self::$client->collections()));
229+
230+
// Creating a random document
231+
$document = $collection->newDocument();
232+
$document->create(['firstName' => 'Yash']);
233+
234+
// Asserting we still get the collections at readTime instead of current
235+
$collections = self::$client->collections(['readTime' => $readTime]);
236+
$this->assertEquals(
237+
$expectedCount,
238+
count(iterator_to_array($collections))
239+
);
240+
}
241+
242+
public function testListDocumentsWithReadTime()
243+
{
244+
$collection = self::$client->collection(uniqid(self::COLLECTION_NAME));
245+
self::$localDeletionQueue->add($collection);
246+
$collection->add(['a' => 'b']);
247+
// without sleep, emulator system test may fail intermittently
248+
sleep(1);
249+
250+
// Creating a current timestamp and then adding a document
251+
$readTime = new Timestamp(new \DateTimeImmutable());
252+
$collection->add(['c' => 'd']);
253+
254+
// Reading at $readTime to get documents at that time
255+
$list = $collection->listDocuments(['readTime' => $readTime]);
256+
$this->assertCount(1, iterator_to_array($list));
257+
$this->assertContainsOnlyInstancesOf(DocumentReference::class, $list);
258+
}
199259
}

Firestore/tests/System/FirestoreTestCase.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,11 @@ public static function tearDownFixtures()
7474
}
7575
});
7676
}
77+
78+
public static function skipEmulatorTests()
79+
{
80+
if ((bool) getenv("FIRESTORE_EMULATOR_HOST")) {
81+
self::markTestSkipped('This test is not supported by the emulator.');
82+
}
83+
}
7784
}

Firestore/tests/System/QueryTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
namespace Google\Cloud\Firestore\Tests\System;
1919

20+
use Google\Cloud\Core\Timestamp;
2021
use Google\Cloud\Firestore\FieldPath;
2122

2223
/**
@@ -197,6 +198,24 @@ public function testLimitToLastWithCursors()
197198
$this->assertEquals([2, 3, 4], $res);
198199
}
199200

201+
public function testDocumentsWithReadTime()
202+
{
203+
$randomVal = base64_encode(random_bytes(10));
204+
$this->insertDoc(['foo' => $randomVal]);
205+
// without sleep, emulator system test may fail intermittently
206+
sleep(1);
207+
208+
// Creating a current timestamp and then inserting another document
209+
$readTime = new Timestamp(new \DateTimeImmutable());
210+
$this->insertDoc(['foo' => $randomVal]);
211+
212+
$resultCount = $this->query
213+
->where('foo', '=', $randomVal)
214+
->documents(['readTime' => $readTime])
215+
->size();
216+
$this->assertEquals(1, $resultCount);
217+
}
218+
200219
private function insertDoc(array $fields)
201220
{
202221
return $this->query->add($fields);

0 commit comments

Comments
 (0)