Skip to content

Commit eea59c2

Browse files
Merge branch 'main' into dat-557
2 parents ebede51 + 83278d6 commit eea59c2

12 files changed

Lines changed: 398 additions & 305 deletions

File tree

composer.lock

Lines changed: 108 additions & 118 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Database/Adapter.php

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,20 @@ abstract public function getSupportForSchemaAttributes(): bool;
892892
*/
893893
abstract public function getSupportForIndex(): bool;
894894

895+
/**
896+
* Is indexing array supported?
897+
*
898+
* @return bool
899+
*/
900+
abstract public function getSupportForIndexArray(): bool;
901+
902+
/**
903+
* Is cast index as array supported?
904+
*
905+
* @return bool
906+
*/
907+
abstract public function getSupportForCastIndexArray(): bool;
908+
895909
/**
896910
* Is unique index supported?
897911
*
@@ -965,13 +979,6 @@ abstract public function getSupportForAttributeResizing(): bool;
965979
*/
966980
abstract public function getSupportForGetConnectionId(): bool;
967981

968-
/**
969-
* Is cast index as array supported?
970-
*
971-
* @return bool
972-
*/
973-
abstract public function getSupportForCastIndexArray(): bool;
974-
975982
/**
976983
* Is upserting supported?
977984
*

src/Database/Adapter/MariaDB.php

Lines changed: 44 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use Utopia\Database\Exception as DatabaseException;
1111
use Utopia\Database\Exception\Duplicate as DuplicateException;
1212
use Utopia\Database\Exception\NotFound as NotFoundException;
13-
use Utopia\Database\Exception\Order as OrderException;
1413
use Utopia\Database\Exception\Timeout as TimeoutException;
1514
use Utopia\Database\Exception\Truncate as TruncateException;
1615
use Utopia\Database\Helpers\ID;
@@ -1359,84 +1358,69 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
13591358

13601359
$queries = array_map(fn ($query) => clone $query, $queries);
13611360

1362-
$hasIdAttribute = false;
1363-
foreach ($orderAttributes as $i => $attribute) {
1364-
$originalAttribute = $attribute;
1361+
$cursorWhere = [];
13651362

1366-
$attribute = $this->getInternalKeyForAttribute($attribute);
1363+
foreach ($orderAttributes as $i => $originalAttribute) {
1364+
$attribute = $this->getInternalKeyForAttribute($originalAttribute);
13671365
$attribute = $this->filter($attribute);
1368-
if (\in_array($attribute, ['_uid', '_id'])) {
1369-
$hasIdAttribute = true;
1370-
}
13711366

13721367
$orderType = $this->filter($orderTypes[$i] ?? Database::ORDER_ASC);
1368+
$direction = $orderType;
13731369

1374-
// Get most dominant/first order attribute
1375-
if ($i === 0 && !empty($cursor)) {
1376-
$orderMethodSequence = Query::TYPE_GREATER; // To preserve natural order
1377-
$orderMethod = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER;
1370+
if ($cursorDirection === Database::CURSOR_BEFORE) {
1371+
$direction = ($direction === Database::ORDER_ASC)
1372+
? Database::ORDER_DESC
1373+
: Database::ORDER_ASC;
1374+
}
13781375

1379-
if ($cursorDirection === Database::CURSOR_BEFORE) {
1380-
$orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC;
1381-
$orderMethodSequence = $orderType === Database::ORDER_ASC ? Query::TYPE_LESSER : Query::TYPE_GREATER;
1382-
$orderMethod = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER;
1383-
}
1376+
$orders[] = "{$this->quote($attribute)} {$direction}";
1377+
1378+
// Build pagination WHERE clause only if we have a cursor
1379+
if (!empty($cursor)) {
1380+
// Special case: No tie breaks. only 1 attribute and it's a unique primary key
1381+
if (count($orderAttributes) === 1 && $i === 0 && $originalAttribute === '$sequence') {
1382+
$operator = ($direction === Database::ORDER_DESC)
1383+
? Query::TYPE_LESSER
1384+
: Query::TYPE_GREATER;
1385+
1386+
$bindName = ":cursor_pk";
1387+
$binds[$bindName] = $cursor[$originalAttribute];
13841388

1385-
if (\is_null($cursor[$originalAttribute] ?? null)) {
1386-
throw new OrderException(
1387-
message: "Order attribute '{$originalAttribute}' is empty",
1388-
attribute: $originalAttribute
1389-
);
1389+
$cursorWhere[] = "{$this->quote($alias)}.{$this->quote($attribute)} {$this->getSQLOperator($operator)} {$bindName}";
1390+
break;
13901391
}
13911392

1392-
$binds[':cursor'] = $cursor[$originalAttribute];
1393-
1394-
$where[] = "(
1395-
{$this->quote($alias)}.{$this->quote($attribute)} {$this->getSQLOperator($orderMethod)} :cursor
1396-
OR (
1397-
{$this->quote($alias)}.{$this->quote($attribute)} = :cursor
1398-
AND
1399-
{$this->quote($alias)}._id {$this->getSQLOperator($orderMethodSequence)} {$cursor['$sequence']}
1400-
)
1401-
)";
1402-
} elseif ($cursorDirection === Database::CURSOR_BEFORE) {
1403-
$orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC;
1404-
}
1393+
$conditions = [];
14051394

1406-
$orders[] = "{$this->quote($attribute)} {$orderType}";
1407-
}
1395+
// Add equality conditions for previous attributes
1396+
for ($j = 0; $j < $i; $j++) {
1397+
$prevOriginal = $orderAttributes[$j];
1398+
$prevAttr = $this->filter($this->getInternalKeyForAttribute($prevOriginal));
14081399

1409-
// Allow after pagination without any order
1410-
if (empty($orderAttributes) && !empty($cursor)) {
1411-
$orderType = $orderTypes[0] ?? Database::ORDER_ASC;
1400+
$bindName = ":cursor_{$j}";
1401+
$binds[$bindName] = $cursor[$prevOriginal];
14121402

1413-
if ($cursorDirection === Database::CURSOR_AFTER) {
1414-
$orderMethod = $orderType === Database::ORDER_DESC
1403+
$conditions[] = "{$this->quote($alias)}.{$this->quote($prevAttr)} = {$bindName}";
1404+
}
1405+
1406+
// Add comparison for current attribute
1407+
$operator = ($direction === Database::ORDER_DESC)
14151408
? Query::TYPE_LESSER
14161409
: Query::TYPE_GREATER;
1417-
} else {
1418-
$orderMethod = $orderType === Database::ORDER_DESC
1419-
? Query::TYPE_GREATER
1420-
: Query::TYPE_LESSER;
1421-
}
14221410

1423-
$where[] = "({$this->quote($alias)}._id {$this->getSQLOperator($orderMethod)} {$cursor['$sequence']})";
1424-
}
1411+
$bindName = ":cursor_{$i}";
1412+
$binds[$bindName] = $cursor[$originalAttribute];
14251413

1426-
// Allow order type without any order attribute, fallback to the natural order (_id)
1427-
if (!$hasIdAttribute) {
1428-
if (empty($orderAttributes) && !empty($orderTypes)) {
1429-
$order = $orderTypes[0] ?? Database::ORDER_ASC;
1430-
if ($cursorDirection === Database::CURSOR_BEFORE) {
1431-
$order = $order === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC;
1432-
}
1414+
$conditions[] = "{$this->quote($alias)}.{$this->quote($attribute)} {$this->getSQLOperator($operator)} {$bindName}";
14331415

1434-
$orders[] = "{$this->quote($alias)}._id ".$this->filter($order);
1435-
} else {
1436-
$orders[] = "{$this->quote($alias)}._id " . ($cursorDirection === Database::CURSOR_AFTER ? Database::ORDER_ASC : Database::ORDER_DESC); // Enforce last ORDER by '_id'
1416+
$cursorWhere[] = '(' . implode(' AND ', $conditions) . ')';
14371417
}
14381418
}
14391419

1420+
if (!empty($cursorWhere)) {
1421+
$where[] = '(' . implode(' OR ', $cursorWhere) . ')';
1422+
}
1423+
14401424
$conditions = $this->getSQLConditions($queries, $binds);
14411425
if (!empty($conditions)) {
14421426
$where[] = $conditions;
@@ -1502,7 +1486,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
15021486
unset($results[$index]['_id']);
15031487
}
15041488
if (\array_key_exists('_tenant', $document)) {
1505-
$document['$tenant'] = $document['_tenant'] === null ? null : (int)$document['_tenant'];
1489+
$results[$index]['$tenant'] = $document['_tenant'] === null ? null : (int)$document['_tenant'];
15061490
unset($results[$index]['_tenant']);
15071491
}
15081492
if (\array_key_exists('_createdAt', $document)) {

src/Database/Adapter/MySQL.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,21 @@ public function getSizeOfCollectionOnDisk(string $collection): int
7878
return $size;
7979
}
8080

81+
public function getSupportForIndexArray(): bool
82+
{
83+
/**
84+
* Disabling index creation due to Mysql bug
85+
* @link https://bugs.mysql.com/bug.php?id=111037
86+
*/
87+
return false;
88+
}
89+
8190
public function getSupportForCastIndexArray(): bool
8291
{
92+
if (!$this->getSupportForIndexArray()) {
93+
return false;
94+
}
95+
8396
return true;
8497
}
8598

src/Database/Adapter/Pool.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,16 @@ public function getSupportForIndex(): bool
335335
return $this->delegate(__FUNCTION__, \func_get_args());
336336
}
337337

338+
public function getSupportForIndexArray(): bool
339+
{
340+
return $this->delegate(__FUNCTION__, \func_get_args());
341+
}
342+
343+
public function getSupportForCastIndexArray(): bool
344+
{
345+
return $this->delegate(__FUNCTION__, \func_get_args());
346+
}
347+
338348
public function getSupportForUniqueIndex(): bool
339349
{
340350
return $this->delegate(__FUNCTION__, \func_get_args());
@@ -390,11 +400,6 @@ public function getSupportForGetConnectionId(): bool
390400
return $this->delegate(__FUNCTION__, \func_get_args());
391401
}
392402

393-
public function getSupportForCastIndexArray(): bool
394-
{
395-
return $this->delegate(__FUNCTION__, \func_get_args());
396-
}
397-
398403
public function getSupportForUpserts(): bool
399404
{
400405
return $this->delegate(__FUNCTION__, \func_get_args());

src/Database/Adapter/Postgres.php

Lines changed: 44 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use Utopia\Database\Exception as DatabaseException;
1111
use Utopia\Database\Exception\Duplicate as DuplicateException;
1212
use Utopia\Database\Exception\NotFound as NotFoundException;
13-
use Utopia\Database\Exception\Order as OrderException;
1413
use Utopia\Database\Exception\Timeout as TimeoutException;
1514
use Utopia\Database\Exception\Transaction as TransactionException;
1615
use Utopia\Database\Exception\Truncate as TruncateException;
@@ -1453,84 +1452,69 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
14531452

14541453
$queries = array_map(fn ($query) => clone $query, $queries);
14551454

1456-
$hasIdAttribute = false;
1457-
foreach ($orderAttributes as $i => $attribute) {
1458-
$originalAttribute = $attribute;
1455+
$cursorWhere = [];
14591456

1460-
$attribute = $this->getInternalKeyForAttribute($attribute);
1457+
foreach ($orderAttributes as $i => $originalAttribute) {
1458+
$attribute = $this->getInternalKeyForAttribute($originalAttribute);
14611459
$attribute = $this->filter($attribute);
1462-
if (\in_array($attribute, ['_uid', '_id'])) {
1463-
$hasIdAttribute = true;
1464-
}
14651460

14661461
$orderType = $this->filter($orderTypes[$i] ?? Database::ORDER_ASC);
1462+
$direction = $orderType;
14671463

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+
}
14721469

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];
14781482

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;
14841485
}
14851486

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 = [];
14991488

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));
15021493

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];
15061496

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)
15091502
? Query::TYPE_LESSER
15101503
: Query::TYPE_GREATER;
1511-
} else {
1512-
$orderMethod = $orderType === Database::ORDER_DESC
1513-
? Query::TYPE_GREATER
1514-
: Query::TYPE_LESSER;
1515-
}
15161504

1517-
$where[] = "({$this->quote($alias)}._id {$this->getSQLOperator($orderMethod)} {$cursor['$sequence']})";
1518-
}
1505+
$bindName = ":cursor_{$i}";
1506+
$binds[$bindName] = $cursor[$originalAttribute];
15191507

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}";
15271509

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) . ')';
15311511
}
15321512
}
15331513

1514+
if (!empty($cursorWhere)) {
1515+
$where[] = '(' . implode(' OR ', $cursorWhere) . ')';
1516+
}
1517+
15341518
$conditions = $this->getSQLConditions($queries, $binds);
15351519
if (!empty($conditions)) {
15361520
$where[] = $conditions;
@@ -1599,7 +1583,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25,
15991583
unset($results[$index]['_id']);
16001584
}
16011585
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'];
16031587
unset($results[$index]['_tenant']);
16041588
}
16051589
if (\array_key_exists('_createdAt', $document)) {

src/Database/Adapter/SQL.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1375,6 +1375,11 @@ public function getSupportForQueryContains(): bool
13751375
*/
13761376
abstract public function getSupportForJSONOverlaps(): bool;
13771377

1378+
public function getSupportForIndexArray(): bool
1379+
{
1380+
return true;
1381+
}
1382+
13781383
public function getSupportForCastIndexArray(): bool
13791384
{
13801385
return false;

0 commit comments

Comments
 (0)