Skip to content

Commit d93d1dd

Browse files
Merge pull request #59 from utopia-php/before-pagination
feat(query): before pagination
2 parents 5c3508e + 968d564 commit d93d1dd

5 files changed

Lines changed: 207 additions & 45 deletions

File tree

src/Database/Adapter.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,12 @@ abstract public function deleteDocument(string $collection, string $id): bool;
233233
* @param int $offset
234234
* @param array $orderAttributes
235235
* @param array $orderTypes
236-
* @param array $orderAfter
236+
* @param array $cursor Array copy of document used for before/after pagination
237+
* @param string $cursorDirection
237238
*
238239
* @return Document[]
239240
*/
240-
abstract public function find(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = [], array $orderAfter = []): array;
241+
abstract public function find(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER): array;
241242

242243
/**
243244
* Sum an attribute

src/Database/Adapter/MariaDB.php

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -501,13 +501,14 @@ public function deleteDocument(string $collection, string $id): bool
501501
* @param int $offset
502502
* @param array $orderAttributes
503503
* @param array $orderTypes
504-
* @param array $orderAfter
504+
* @param array $cursor
505+
* @param string $cursorDirection
505506
*
506507
* @return array
507508
* @throws Exception
508509
* @throws PDOException
509510
*/
510-
public function find(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = [], array $orderAfter = []): array
511+
public function find(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER): array
511512
{
512513
$name = $this->filter($collection);
513514
$roles = Authorization::getRoles();
@@ -517,35 +518,54 @@ public function find(string $collection, array $queries = [], int $limit = 25, i
517518
foreach($orderAttributes as $i => $attribute) {
518519
$attribute = $this->filter($attribute);
519520
$orderType = $this->filter($orderTypes[$i] ?? Database::ORDER_ASC);
520-
$orders[] = $attribute.' '.$orderType;
521521

522522
// Get most dominant/first order attribute
523-
if ($i === 0 && !empty($orderAfter)) {
523+
if ($i === 0 && !empty($cursor)) {
524+
$orderOperatorInternalId = Query::TYPE_GREATER; // To preserve natural order
524525
$orderOperator = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER;
525526

527+
if ($cursorDirection === Database::CURSOR_BEFORE) {
528+
$orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC;
529+
$orderOperatorInternalId = $orderType === Database::ORDER_ASC ? Query::TYPE_LESSER : Query::TYPE_GREATER;
530+
$orderOperator = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER;
531+
}
532+
526533
$where[] = "(
527-
{$attribute} {$this->getSQLOperator($orderOperator)} :after
534+
{$attribute} {$this->getSQLOperator($orderOperator)} :cursor
528535
OR (
529-
{$attribute} = :after
536+
{$attribute} = :cursor
530537
AND
531-
_id > {$orderAfter['$internalId']}
538+
_id {$this->getSQLOperator($orderOperatorInternalId)} {$cursor['$internalId']}
532539
)
533540
)";
541+
} else if ($cursorDirection === Database::CURSOR_BEFORE) {
542+
$orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC;
534543
}
544+
545+
$orders[] = $attribute.' '.$orderType;
535546
}
536547

537548
// Allow after pagination without any order
538-
if (empty($orderAttributes) && !empty($orderAfter)) {
549+
if (empty($orderAttributes) && !empty($cursor)) {
539550
$orderType = $orderTypes[0] ?? Database::ORDER_ASC;
540-
$orderOperator = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER;
541-
$where[] = "( _id {$this->getSQLOperator($orderOperator)} {$orderAfter['$internalId']} )";
551+
$orderOperator = $cursorDirection === Database::CURSOR_AFTER ? (
552+
$orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER
553+
) : (
554+
$orderType === Database::ORDER_DESC ? Query::TYPE_GREATER : Query::TYPE_LESSER
555+
);
556+
$where[] = "( _id {$this->getSQLOperator($orderOperator)} {$cursor['$internalId']} )";
542557
}
543558

544559
// Allow order type without any order attribute, fallback to the natural order (_id)
545560
if(empty($orderAttributes) && !empty($orderTypes)) {
546-
$orders[] = '_id '.$this->filter($orderTypes[0] ?? Database::ORDER_ASC);
561+
$order = $orderTypes[0] ?? Database::ORDER_ASC;
562+
if ($cursorDirection === Database::CURSOR_BEFORE) {
563+
$order = $order === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC;
564+
}
565+
566+
$orders[] = '_id '.$this->filter($order);
547567
} else {
548-
$orders[] = '_id '.Database::ORDER_ASC; // Enforce last ORDER by '_id'
568+
$orders[] = '_id '.($cursorDirection === Database::CURSOR_AFTER ? Database::ORDER_ASC : Database::ORDER_DESC); // Enforce last ORDER by '_id'
549569
}
550570

551571
$permissions = (Authorization::$status) ? $this->getSQLPermissions($roles) : '1=1'; // Disable join when no authorization required
@@ -574,12 +594,12 @@ public function find(string $collection, array $queries = [], int $limit = 25, i
574594
}
575595
}
576596

577-
if (!empty($orderAfter) && !empty($orderAttributes) && array_key_exists(0, $orderAttributes)) {
597+
if (!empty($cursor) && !empty($orderAttributes) && array_key_exists(0, $orderAttributes)) {
578598
$attribute = $orderAttributes[0];
579-
if (is_null($orderAfter[$attribute] ?? null)) {
599+
if (is_null($cursor[$attribute] ?? null)) {
580600
throw new Exception("Order attribute '{$attribute}' is empty.");
581601
}
582-
$stmt->bindValue(':after', $orderAfter[$attribute], $this->getPDOType($orderAfter[$attribute]));
602+
$stmt->bindValue(':cursor', $cursor[$attribute], $this->getPDOType($cursor[$attribute]));
583603
}
584604

585605
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
@@ -601,6 +621,10 @@ public function find(string $collection, array $queries = [], int $limit = 25, i
601621
$value = new Document($value);
602622
}
603623

624+
if ($cursorDirection === Database::CURSOR_BEFORE) {
625+
$results = array_reverse($results); //TODO: check impact on array_reverse
626+
}
627+
604628
return $results;
605629
}
606630

src/Database/Adapter/MongoDB.php

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -427,11 +427,12 @@ public function deleteDocument(string $collection, string $id): bool
427427
* @param int $offset
428428
* @param array $orderAttributes
429429
* @param array $orderTypes
430-
* @param array $orderAfter
430+
* @param array $cursor
431+
* @param array $cursorDirection
431432
*
432433
* @return Document[]
433434
*/
434-
public function find(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = [], array $orderAfter = []): array
435+
public function find(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = [], array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER): array
435436
{
436437
$name = $this->filter($collection);
437438
$collection = $this->getDatabase()->$name;
@@ -444,52 +445,70 @@ public function find(string $collection, array $queries = [], int $limit = 25, i
444445
// orders
445446
foreach($orderAttributes as $i => $attribute) {
446447
$attribute = $this->filter($attribute);
447-
$orderType = $this->getOrder($this->filter($orderTypes[$i] ?? Database::ORDER_ASC));
448-
$options['sort'][$attribute] = $orderType;
448+
$orderType = $this->filter($orderTypes[$i] ?? Database::ORDER_ASC);
449+
if ($cursorDirection === Database::CURSOR_BEFORE) {
450+
$orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC;
451+
}
452+
$options['sort'][$attribute] = $this->getOrder($orderType);
449453
}
450454

451-
$options['sort']['_id'] = $this->getOrder(Database::ORDER_ASC);
455+
$options['sort']['_id'] = $this->getOrder($cursorDirection === Database::CURSOR_AFTER ? Database::ORDER_ASC : Database::ORDER_DESC);
452456

453457
// queries
454458
$filters = $this->buildFilters($queries);
455459

456460
if (empty($orderAttributes)) {
457461
// Allow after pagination without any order
458-
if(!empty($orderAfter)) {
462+
if(!empty($cursor)) {
459463
$orderType = $orderTypes[0] ?? Database::ORDER_ASC;
460-
$orderOperator = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER;
464+
$orderOperator = $cursorDirection === Database::CURSOR_AFTER ? (
465+
$orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER
466+
) : (
467+
$orderType === Database::ORDER_DESC ? Query::TYPE_GREATER : Query::TYPE_LESSER
468+
);
469+
461470
$filters = array_merge($filters, [
462471
'_id' => [
463-
$this->getQueryOperator($orderOperator) => new \MongoDB\BSON\ObjectId($orderAfter['$internalId'])
472+
$this->getQueryOperator($orderOperator) => new \MongoDB\BSON\ObjectId($cursor['$internalId'])
464473
]
465474
]);
466475
}
467476
// Allow order type without any order attribute, fallback to the natural order (_id)
468477
if(!empty($orderTypes)) {
469-
$orderType = $this->getOrder($this->filter($orderTypes[0] ?? Database::ORDER_ASC));
470-
$options['sort']['_id'] = $orderType;
478+
$orderType = $this->filter($orderTypes[0] ?? Database::ORDER_ASC);
479+
if ($cursorDirection === Database::CURSOR_BEFORE) {
480+
$orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC;
481+
}
482+
$options['sort']['_id'] = $this->getOrder($orderType);
471483
}
472484
}
473485

474-
if (!empty($orderAfter) && !empty($orderAttributes) && array_key_exists(0, $orderAttributes)) {
486+
if (!empty($cursor) && !empty($orderAttributes) && array_key_exists(0, $orderAttributes)) {
475487
$attribute = $orderAttributes[0];
476-
if (is_null($orderAfter[$attribute] ?? null)) {
488+
if (is_null($cursor[$attribute] ?? null)) {
477489
throw new Exception("Order attribute '{$attribute}' is empty.");
478490
}
479491

492+
$orderOperatorInternalId = Query::TYPE_GREATER;
480493
$orderType = $this->filter($orderTypes[0] ?? Database::ORDER_ASC);
481494
$orderOperator = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER;
482495

496+
if ($cursorDirection === Database::CURSOR_BEFORE) {
497+
$orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC;
498+
$orderOperatorInternalId = $orderType === Database::ORDER_ASC ? Query::TYPE_LESSER : Query::TYPE_GREATER;
499+
$orderOperator = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER;
500+
}
501+
483502
$filters = array_merge($filters, [
484503
'$or' => [
485504
[
486505
$attribute => [
487-
$this->getQueryOperator($orderOperator) => $orderAfter[$attribute]
506+
$this->getQueryOperator($orderOperator) => $cursor[$attribute]
488507
]
489508
], [
490-
$attribute => $orderAfter[$attribute],
509+
$attribute => $cursor[$attribute],
491510
'_id' => [
492-
$this->getQueryOperator(Query::TYPE_GREATER) => new \MongoDB\BSON\ObjectId($orderAfter['$internalId'])
511+
$this->getQueryOperator($orderOperatorInternalId) => new \MongoDB\BSON\ObjectId($cursor['$internalId'])
493512
]
494513

495514
]
@@ -513,6 +532,10 @@ public function find(string $collection, array $queries = [], int $limit = 25, i
513532
$found[] = new Document($this->replaceChars('_', '$', $result));
514533
}
515534

535+
if ($cursorDirection === Database::CURSOR_BEFORE) {
536+
$found = array_reverse($found);
537+
}
538+
516539
return $found;
517540
}
518541

src/Database/Database.php

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ class Database
4040
// Collections
4141
const METADATA = '_metadata';
4242

43+
// Cursor
44+
const CURSOR_BEFORE = 'before';
45+
const CURSOR_AFTER = 'after';
46+
4347
// Lengths
4448
const LENGTH_KEY = 255;
4549

@@ -841,21 +845,22 @@ public function deleteCachedDocument(string $collection, string $id): bool
841845
* @param int $offset
842846
* @param array $orderAttributes
843847
* @param array $orderTypes
844-
* @param Document|null $orderAfter
848+
* @param Document|null $cursor
849+
* @param string $cursorDirection
845850
*
846851
* @return Document[]
847852
*/
848-
public function find(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = [], Document $orderAfter = null): array
853+
public function find(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = [], Document $cursor = null, string $cursorDirection = self::CURSOR_AFTER): array
849854
{
850855
$collection = $this->getCollection($collection);
851856

852-
if (!empty($orderAfter) && $orderAfter->getCollection() !== $collection->getId()) {
853-
throw new Exception("orderAfter Document must be from the same Collection.");
857+
if (!empty($cursor) && $cursor->getCollection() !== $collection->getId()) {
858+
throw new Exception("cursor Document must be from the same Collection.");
854859
}
855860

856-
$orderAfter = empty($orderAfter) ? [] : $orderAfter->getArrayCopy();
861+
$cursor = empty($cursor) ? [] : $cursor->getArrayCopy();
857862

858-
$results = $this->adapter->find($collection->getId(), $queries, $limit, $offset, $orderAttributes, $orderTypes, $orderAfter);
863+
$results = $this->adapter->find($collection->getId(), $queries, $limit, $offset, $orderAttributes, $orderTypes, $cursor, $cursorDirection);
859864

860865
foreach ($results as &$node) {
861866
$node = $this->casting($collection, $node);
@@ -872,13 +877,14 @@ public function find(string $collection, array $queries = [], int $limit = 25, i
872877
* @param int $offset
873878
* @param array $orderAttributes
874879
* @param array $orderTypes
875-
* @param Document|null $orderAfter
880+
* @param Document|null $cursor
881+
* @param string $cursorDirection
876882
*
877883
* @return Document|bool
878884
*/
879-
public function findOne(string $collection, array $queries = [], int $offset = 0, array $orderAttributes = [], array $orderTypes = [], Document $orderAfter = null)
885+
public function findOne(string $collection, array $queries = [], int $offset = 0, array $orderAttributes = [], array $orderTypes = [], Document $cursor = null, string $cursorDirection = Database::CURSOR_AFTER)
880886
{
881-
$results = $this->find($collection, $queries, /*limit*/ 1, $offset, $orderAttributes, $orderTypes, $orderAfter);
887+
$results = $this->find($collection, $queries, /*limit*/ 1, $offset, $orderAttributes, $orderTypes, $cursor, $cursorDirection);
882888
return \reset($results);
883889
}
884890

0 commit comments

Comments
 (0)