Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/Database/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,12 @@ abstract public function deleteDocument(string $collection, string $id): bool;
* @param int $offset
* @param array $orderAttributes
* @param array $orderTypes
* @param array $orderAfter
* @param array $cursor Array copy of document used for before/after pagination
* @param string $cursorDirection
*
* @return Document[]
*/
abstract public function find(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = [], array $orderAfter = []): array;
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;

/**
* Count Documents
Expand Down
54 changes: 39 additions & 15 deletions src/Database/Adapter/MariaDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -487,13 +487,14 @@ public function deleteDocument(string $collection, string $id): bool
* @param int $offset
* @param array $orderAttributes
* @param array $orderTypes
* @param array $orderAfter
* @param array $cursor
* @param string $cursorDirection
*
* @return array
* @throws Exception
* @throws PDOException
*/
public function find(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = [], array $orderAfter = []): array
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
{
$name = $this->filter($collection);
$roles = Authorization::getRoles();
Expand All @@ -503,35 +504,54 @@ public function find(string $collection, array $queries = [], int $limit = 25, i
foreach($orderAttributes as $i => $attribute) {
$attribute = $this->filter($attribute);
$orderType = $this->filter($orderTypes[$i] ?? Database::ORDER_ASC);
$orders[] = $attribute.' '.$orderType;

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

if ($cursorDirection === Database::CURSOR_BEFORE) {
$orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC;
$orderOperatorInternalId = $orderType === Database::ORDER_ASC ? Query::TYPE_LESSER : Query::TYPE_GREATER;
$orderOperator = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER;
}

$where[] = "(
{$attribute} {$this->getSQLOperator($orderOperator)} :after
{$attribute} {$this->getSQLOperator($orderOperator)} :cursor
OR (
{$attribute} = :after
{$attribute} = :cursor
AND
_id > {$orderAfter['$internalId']}
_id {$this->getSQLOperator($orderOperatorInternalId)} {$cursor['$internalId']}
)
)";
} else if ($cursorDirection === Database::CURSOR_BEFORE) {
$orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC;
}

$orders[] = $attribute.' '.$orderType;
}

// Allow after pagination without any order
if (empty($orderAttributes) && !empty($orderAfter)) {
if (empty($orderAttributes) && !empty($cursor)) {
$orderType = $orderTypes[0] ?? Database::ORDER_ASC;
$orderOperator = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER;
$where[] = "( _id {$this->getSQLOperator($orderOperator)} {$orderAfter['$internalId']} )";
$orderOperator = $cursorDirection === Database::CURSOR_AFTER ? (
$orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER
) : (
$orderType === Database::ORDER_DESC ? Query::TYPE_GREATER : Query::TYPE_LESSER
);
$where[] = "( _id {$this->getSQLOperator($orderOperator)} {$cursor['$internalId']} )";
}

// Allow order type without any order attribute, fallback to the natural order (_id)
if(empty($orderAttributes) && !empty($orderTypes)) {
$orders[] = '_id '.$this->filter($orderTypes[0] ?? Database::ORDER_ASC);
$order = $orderTypes[0] ?? Database::ORDER_ASC;
if ($cursorDirection === Database::CURSOR_BEFORE) {
$order = $order === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC;
}

$orders[] = '_id '.$this->filter($order);
} else {
$orders[] = '_id '.Database::ORDER_ASC; // Enforce last ORDER by '_id'
$orders[] = '_id '.($cursorDirection === Database::CURSOR_AFTER ? Database::ORDER_ASC : Database::ORDER_DESC); // Enforce last ORDER by '_id'
}

$permissions = (Authorization::$status) ? $this->getSQLPermissions($roles) : '1=1'; // Disable join when no authorization required
Expand Down Expand Up @@ -560,12 +580,12 @@ public function find(string $collection, array $queries = [], int $limit = 25, i
}
}

if (!empty($orderAfter) && !empty($orderAttributes) && array_key_exists(0, $orderAttributes)) {
if (!empty($cursor) && !empty($orderAttributes) && array_key_exists(0, $orderAttributes)) {
$attribute = $orderAttributes[0];
if (is_null($orderAfter[$attribute] ?? null)) {
if (is_null($cursor[$attribute] ?? null)) {
throw new Exception("Order attribute '{$attribute}' is empty.");
}
$stmt->bindValue(':after', $orderAfter[$attribute], $this->getPDOType($orderAfter[$attribute]));
$stmt->bindValue(':cursor', $cursor[$attribute], $this->getPDOType($cursor[$attribute]));
}

$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
Expand All @@ -587,6 +607,10 @@ public function find(string $collection, array $queries = [], int $limit = 25, i
$value = new Document($value);
}

if ($cursorDirection === Database::CURSOR_BEFORE) {
$results = array_reverse($results); //TODO: check impact on array_reverse
}

return $results;
}

Expand Down
53 changes: 38 additions & 15 deletions src/Database/Adapter/MongoDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -416,11 +416,12 @@ public function deleteDocument(string $collection, string $id): bool
* @param int $offset
* @param array $orderAttributes
* @param array $orderTypes
* @param array $orderAfter
* @param array $cursor
* @param array $cursorDirection
*
* @return Document[]
*/
public function find(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = [], array $orderAfter = []): array
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
{
$name = $this->filter($collection);
$collection = $this->getDatabase()->$name;
Expand All @@ -433,52 +434,70 @@ public function find(string $collection, array $queries = [], int $limit = 25, i
// orders
foreach($orderAttributes as $i => $attribute) {
$attribute = $this->filter($attribute);
$orderType = $this->getOrder($this->filter($orderTypes[$i] ?? Database::ORDER_ASC));
$options['sort'][$attribute] = $orderType;
$orderType = $this->filter($orderTypes[$i] ?? Database::ORDER_ASC);
if ($cursorDirection === Database::CURSOR_BEFORE) {
$orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC;
}
$options['sort'][$attribute] = $this->getOrder($orderType);
}

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

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

if (empty($orderAttributes)) {
// Allow after pagination without any order
if(!empty($orderAfter)) {
if(!empty($cursor)) {
$orderType = $orderTypes[0] ?? Database::ORDER_ASC;
$orderOperator = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER;
$orderOperator = $cursorDirection === Database::CURSOR_AFTER ? (
$orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER
) : (
$orderType === Database::ORDER_DESC ? Query::TYPE_GREATER : Query::TYPE_LESSER
);

$filters = array_merge($filters, [
'_id' => [
$this->getQueryOperator($orderOperator) => new \MongoDB\BSON\ObjectId($orderAfter['$internalId'])
$this->getQueryOperator($orderOperator) => new \MongoDB\BSON\ObjectId($cursor['$internalId'])
]
]);
}
// Allow order type without any order attribute, fallback to the natural order (_id)
if(!empty($orderTypes)) {
$orderType = $this->getOrder($this->filter($orderTypes[0] ?? Database::ORDER_ASC));
$options['sort']['_id'] = $orderType;
$orderType = $this->filter($orderTypes[0] ?? Database::ORDER_ASC);
if ($cursorDirection === Database::CURSOR_BEFORE) {
$orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC;
}
$options['sort']['_id'] = $this->getOrder($orderType);
}
}

if (!empty($orderAfter) && !empty($orderAttributes) && array_key_exists(0, $orderAttributes)) {
if (!empty($cursor) && !empty($orderAttributes) && array_key_exists(0, $orderAttributes)) {
$attribute = $orderAttributes[0];
if (is_null($orderAfter[$attribute] ?? null)) {
if (is_null($cursor[$attribute] ?? null)) {
throw new Exception("Order attribute '{$attribute}' is empty.");
}

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

if ($cursorDirection === Database::CURSOR_BEFORE) {
$orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC;
$orderOperatorInternalId = $orderType === Database::ORDER_ASC ? Query::TYPE_LESSER : Query::TYPE_GREATER;
$orderOperator = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER;
}

$filters = array_merge($filters, [
'$or' => [
[
$attribute => [
$this->getQueryOperator($orderOperator) => $orderAfter[$attribute]
$this->getQueryOperator($orderOperator) => $cursor[$attribute]
]
], [
$attribute => $orderAfter[$attribute],
$attribute => $cursor[$attribute],
'_id' => [
$this->getQueryOperator(Query::TYPE_GREATER) => new \MongoDB\BSON\ObjectId($orderAfter['$internalId'])
$this->getQueryOperator($orderOperatorInternalId) => new \MongoDB\BSON\ObjectId($cursor['$internalId'])
]

]
Expand All @@ -502,6 +521,10 @@ public function find(string $collection, array $queries = [], int $limit = 25, i
$found[] = new Document($this->replaceChars('_', '$', $result));
}

if ($cursorDirection === Database::CURSOR_BEFORE) {
$found = array_reverse($found);
}

return $found;
}

Expand Down
24 changes: 15 additions & 9 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ class Database
// Collections
const COLLECTIONS = 'collections';

// Cursor
const CURSOR_BEFORE = 'before';
const CURSOR_AFTER = 'after';

// Lengths
const LENGTH_KEY = 255;

Expand Down Expand Up @@ -990,21 +994,22 @@ public function deleteDocument(string $collection, string $id): bool
* @param int $offset
* @param array $orderAttributes
* @param array $orderTypes
* @param Document|null $orderAfter
* @param Document|null $cursor
* @param string $cursorDirection
*
* @return Document[]
*/
public function find(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = [], Document $orderAfter = null): array
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
{
$collection = $this->getCollection($collection);

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

$orderAfter = empty($orderAfter) ? [] : $orderAfter->getArrayCopy();
$cursor = empty($cursor) ? [] : $cursor->getArrayCopy();

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

foreach ($results as &$node) {
$node = $this->casting($collection, $node);
Expand All @@ -1021,13 +1026,14 @@ public function find(string $collection, array $queries = [], int $limit = 25, i
* @param int $offset
* @param array $orderAttributes
* @param array $orderTypes
* @param Document|null $orderAfter
* @param Document|null $cursor
* @param string $cursorDirection
*
* @return Document|bool
*/
public function findOne(string $collection, array $queries = [], int $offset = 0, array $orderAttributes = [], array $orderTypes = [], Document $orderAfter = null)
public function findOne(string $collection, array $queries = [], int $offset = 0, array $orderAttributes = [], array $orderTypes = [], Document $cursor = null, string $cursorDirection = Database::CURSOR_AFTER)
{
$results = $this->find($collection, $queries, /*limit*/ 1, $offset, $orderAttributes, $orderTypes, $orderAfter);
$results = $this->find($collection, $queries, /*limit*/ 1, $offset, $orderAttributes, $orderTypes, $cursor, $cursorDirection);
return \reset($results);
}

Expand Down
Loading