|
10 | 10 | use Utopia\Database\Exception as DatabaseException; |
11 | 11 | use Utopia\Database\Exception\Duplicate as DuplicateException; |
12 | 12 | use Utopia\Database\Exception\NotFound as NotFoundException; |
13 | | -use Utopia\Database\Exception\Order as OrderException; |
14 | 13 | use Utopia\Database\Exception\Timeout as TimeoutException; |
15 | 14 | use Utopia\Database\Exception\Transaction as TransactionException; |
16 | 15 | use Utopia\Database\Exception\Truncate as TruncateException; |
@@ -1453,84 +1452,69 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, |
1453 | 1452 |
|
1454 | 1453 | $queries = array_map(fn ($query) => clone $query, $queries); |
1455 | 1454 |
|
1456 | | - $hasIdAttribute = false; |
1457 | | - foreach ($orderAttributes as $i => $attribute) { |
1458 | | - $originalAttribute = $attribute; |
| 1455 | + $cursorWhere = []; |
1459 | 1456 |
|
1460 | | - $attribute = $this->getInternalKeyForAttribute($attribute); |
| 1457 | + foreach ($orderAttributes as $i => $originalAttribute) { |
| 1458 | + $attribute = $this->getInternalKeyForAttribute($originalAttribute); |
1461 | 1459 | $attribute = $this->filter($attribute); |
1462 | | - if (\in_array($attribute, ['_uid', '_id'])) { |
1463 | | - $hasIdAttribute = true; |
1464 | | - } |
1465 | 1460 |
|
1466 | 1461 | $orderType = $this->filter($orderTypes[$i] ?? Database::ORDER_ASC); |
| 1462 | + $direction = $orderType; |
1467 | 1463 |
|
1468 | | - // Get most dominant/first order attribute |
1469 | | - if ($i === 0 && !empty($cursor)) { |
1470 | | - $orderMethodSequence = Query::TYPE_GREATER; // To preserve natural order |
1471 | | - $orderMethod = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; |
| 1464 | + if ($cursorDirection === Database::CURSOR_BEFORE) { |
| 1465 | + $direction = ($direction === Database::ORDER_ASC) |
| 1466 | + ? Database::ORDER_DESC |
| 1467 | + : Database::ORDER_ASC; |
| 1468 | + } |
1472 | 1469 |
|
1473 | | - if ($cursorDirection === Database::CURSOR_BEFORE) { |
1474 | | - $orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; |
1475 | | - $orderMethodSequence = $orderType === Database::ORDER_ASC ? Query::TYPE_LESSER : Query::TYPE_GREATER; |
1476 | | - $orderMethod = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; |
1477 | | - } |
| 1470 | + $orders[] = "{$this->quote($attribute)} {$direction}"; |
| 1471 | + |
| 1472 | + // Build pagination WHERE clause only if we have a cursor |
| 1473 | + if (!empty($cursor)) { |
| 1474 | + // Special case: only 1 attribute and it's a unique primary key |
| 1475 | + if (count($orderAttributes) === 1 && $i === 0 && $originalAttribute === '$sequence') { |
| 1476 | + $operator = ($direction === Database::ORDER_DESC) |
| 1477 | + ? Query::TYPE_LESSER |
| 1478 | + : Query::TYPE_GREATER; |
| 1479 | + |
| 1480 | + $bindName = ":cursor_pk"; |
| 1481 | + $binds[$bindName] = $cursor[$originalAttribute]; |
1478 | 1482 |
|
1479 | | - if (\is_null($cursor[$originalAttribute] ?? null)) { |
1480 | | - throw new OrderException( |
1481 | | - message: "Order attribute '{$originalAttribute}' is empty", |
1482 | | - attribute: $originalAttribute |
1483 | | - ); |
| 1483 | + $cursorWhere[] = "{$this->quote($alias)}.{$this->quote($attribute)} {$this->getSQLOperator($operator)} {$bindName}"; |
| 1484 | + break; |
1484 | 1485 | } |
1485 | 1486 |
|
1486 | | - $binds[':cursor'] = $cursor[$originalAttribute]; |
1487 | | - |
1488 | | - $where[] = "( |
1489 | | - {$this->quote($alias)}.{$this->quote($attribute)} {$this->getSQLOperator($orderMethod)} :cursor |
1490 | | - OR ( |
1491 | | - {$this->quote($alias)}.{$this->quote($attribute)} = :cursor |
1492 | | - AND |
1493 | | - {$this->quote($alias)}._id {$this->getSQLOperator($orderMethodSequence)} {$cursor['$sequence']} |
1494 | | - ) |
1495 | | - )"; |
1496 | | - } elseif ($cursorDirection === Database::CURSOR_BEFORE) { |
1497 | | - $orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; |
1498 | | - } |
| 1487 | + $conditions = []; |
1499 | 1488 |
|
1500 | | - $orders[] = "{$this->quote($attribute)} {$orderType}"; |
1501 | | - } |
| 1489 | + // Add equality conditions for previous attributes |
| 1490 | + for ($j = 0; $j < $i; $j++) { |
| 1491 | + $prevOriginal = $orderAttributes[$j]; |
| 1492 | + $prevAttr = $this->filter($this->getInternalKeyForAttribute($prevOriginal)); |
1502 | 1493 |
|
1503 | | - // Allow after pagination without any order |
1504 | | - if (empty($orderAttributes) && !empty($cursor)) { |
1505 | | - $orderType = $orderTypes[0] ?? Database::ORDER_ASC; |
| 1494 | + $bindName = ":cursor_{$j}"; |
| 1495 | + $binds[$bindName] = $cursor[$prevOriginal]; |
1506 | 1496 |
|
1507 | | - if ($cursorDirection === Database::CURSOR_AFTER) { |
1508 | | - $orderMethod = $orderType === Database::ORDER_DESC |
| 1497 | + $conditions[] = "{$this->quote($alias)}.{$this->quote($prevAttr)} = {$bindName}"; |
| 1498 | + } |
| 1499 | + |
| 1500 | + // Add comparison for current attribute |
| 1501 | + $operator = ($direction === Database::ORDER_DESC) |
1509 | 1502 | ? Query::TYPE_LESSER |
1510 | 1503 | : Query::TYPE_GREATER; |
1511 | | - } else { |
1512 | | - $orderMethod = $orderType === Database::ORDER_DESC |
1513 | | - ? Query::TYPE_GREATER |
1514 | | - : Query::TYPE_LESSER; |
1515 | | - } |
1516 | 1504 |
|
1517 | | - $where[] = "({$this->quote($alias)}._id {$this->getSQLOperator($orderMethod)} {$cursor['$sequence']})"; |
1518 | | - } |
| 1505 | + $bindName = ":cursor_{$i}"; |
| 1506 | + $binds[$bindName] = $cursor[$originalAttribute]; |
1519 | 1507 |
|
1520 | | - // Allow order type without any order attribute, fallback to the natural order (_id) |
1521 | | - if (!$hasIdAttribute) { |
1522 | | - if (empty($orderAttributes) && !empty($orderTypes)) { |
1523 | | - $order = $orderTypes[0] ?? Database::ORDER_ASC; |
1524 | | - if ($cursorDirection === Database::CURSOR_BEFORE) { |
1525 | | - $order = $order === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; |
1526 | | - } |
| 1508 | + $conditions[] = "{$this->quote($alias)}.{$this->quote($attribute)} {$this->getSQLOperator($operator)} {$bindName}"; |
1527 | 1509 |
|
1528 | | - $orders[] = "{$this->quote($alias)}._id " . $this->filter($order); |
1529 | | - } else { |
1530 | | - $orders[] = "{$this->quote($alias)}._id " . ($cursorDirection === Database::CURSOR_AFTER ? Database::ORDER_ASC : Database::ORDER_DESC); // Enforce last ORDER by '_id' |
| 1510 | + $cursorWhere[] = '(' . implode(' AND ', $conditions) . ')'; |
1531 | 1511 | } |
1532 | 1512 | } |
1533 | 1513 |
|
| 1514 | + if (!empty($cursorWhere)) { |
| 1515 | + $where[] = '(' . implode(' OR ', $cursorWhere) . ')'; |
| 1516 | + } |
| 1517 | + |
1534 | 1518 | $conditions = $this->getSQLConditions($queries, $binds); |
1535 | 1519 | if (!empty($conditions)) { |
1536 | 1520 | $where[] = $conditions; |
@@ -1599,7 +1583,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, |
1599 | 1583 | unset($results[$index]['_id']); |
1600 | 1584 | } |
1601 | 1585 | if (\array_key_exists('_tenant', $document)) { |
1602 | | - $document['$tenant'] = $document['_tenant'] === null ? null : (int)$document['_tenant']; |
| 1586 | + $results[$index]['$tenant'] = $document['_tenant'] === null ? null : (int)$document['_tenant']; |
1603 | 1587 | unset($results[$index]['_tenant']); |
1604 | 1588 | } |
1605 | 1589 | if (\array_key_exists('_createdAt', $document)) { |
|
0 commit comments