Skip to content
32 changes: 19 additions & 13 deletions src/Database/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -388,19 +388,6 @@ public function withTransaction(callable $callback): mixed
} catch (\Throwable $action) {
try {
$this->rollbackTransaction();

if (
$action instanceof DuplicateException ||
$action instanceof RestrictedException ||
$action instanceof AuthorizationException ||
$action instanceof RelationshipException ||
$action instanceof ConflictException ||
$action instanceof LimitException
) {
$this->inTransaction = 0;
throw $action;
}

} catch (\Throwable $rollback) {
if ($attempts < $retries) {
\usleep($sleep * ($attempts + 1));
Expand All @@ -411,6 +398,18 @@ public function withTransaction(callable $callback): mixed
throw $rollback;
}

if (
$action instanceof DuplicateException ||
$action instanceof RestrictedException ||
$action instanceof AuthorizationException ||
$action instanceof RelationshipException ||
$action instanceof ConflictException ||
$action instanceof LimitException
) {
$this->inTransaction = 0;
throw $action;
}

if ($attempts < $retries) {
\usleep($sleep * ($attempts + 1));
continue;
Expand Down Expand Up @@ -1064,6 +1063,13 @@ abstract public function getSupportForSpatialAttributes(): bool;
*/
abstract public function getSupportForSpatialIndexNull(): bool;

/**
* Adapter supports optional spatial attributes with existing rows.
*
* @return bool
*/
abstract public function getSupportForOptionalSpatialAttributeWithExistingRows(): bool;

/**
* Does the adapter support order attribute in spatial indexes?
*
Expand Down
20 changes: 20 additions & 0 deletions src/Database/Adapter/MariaDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -1664,6 +1664,16 @@ protected function getPDOType(mixed $value): int
};
}

/**
* Get the SQL function for random ordering
*
* @return string
*/
protected function getRandomOrder(): string
{
return 'RAND()';
}

/**
* Size of POINT spatial type
*
Expand Down Expand Up @@ -1917,4 +1927,14 @@ public function getSupportForSpatialAxisOrder(): bool
{
return false;
}

/**
* Adapter supports optional spatial attributes with existing rows.
*
* @return bool
*/
public function getSupportForOptionalSpatialAttributeWithExistingRows(): bool
{
return true;
}
}
15 changes: 15 additions & 0 deletions src/Database/Adapter/MySQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Utopia\Database\Database;
use Utopia\Database\Exception as DatabaseException;
use Utopia\Database\Exception\Dependency as DependencyException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Exception\Timeout as TimeoutException;
use Utopia\Database\Query;

Expand Down Expand Up @@ -155,6 +156,10 @@ protected function processException(PDOException $e): \Exception
return new DependencyException('Attribute cannot be deleted because it is used in an index', $e->getCode(), $e);
}

if ($e->getCode() === '22004' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1138) {
return new StructureException('Attribute does not allow null values', $e->getCode(), $e);
}

return parent::processException($e);
}
/**
Expand Down Expand Up @@ -249,4 +254,14 @@ protected function getSpatialAxisOrderSpec(): string
{
return "'axis-order=long-lat'";
}

/**
* Adapter supports optional spatial attributes with existing rows.
*
* @return bool
*/
public function getSupportForOptionalSpatialAttributeWithExistingRows(): bool
{
return false;
}
}
10 changes: 10 additions & 0 deletions src/Database/Adapter/Pool.php
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,16 @@ public function getSupportForSpatialAxisOrder(): bool
return $this->delegate(__FUNCTION__, \func_get_args());
}

/**
* Adapter supports optional spatial attributes with existing rows.
*
* @return bool
*/
public function getSupportForOptionalSpatialAttributeWithExistingRows(): bool
{
return $this->delegate(__FUNCTION__, \func_get_args());
}

public function decodePoint(string $wkb): array
{
return $this->delegate(__FUNCTION__, \func_get_args());
Expand Down
21 changes: 20 additions & 1 deletion src/Database/Adapter/Postgres.php
Original file line number Diff line number Diff line change
Expand Up @@ -1790,6 +1790,16 @@ protected function getPDOType(mixed $value): int
};
}

/**
* Get the SQL function for random ordering
*
* @return string
*/
protected function getRandomOrder(): string
{
return 'RANDOM()';
}

/**
* Size of POINT spatial type
*
Expand Down Expand Up @@ -2001,6 +2011,16 @@ public function getSupportForSpatialAxisOrder(): bool
return false;
}

/**
* Adapter supports optional spatial attributes with existing rows.
*
* @return bool
*/
public function getSupportForOptionalSpatialAttributeWithExistingRows(): bool
{
return false;
}

public function decodePoint(string $wkb): array
{
if (str_starts_with(strtoupper($wkb), 'POINT(')) {
Expand Down Expand Up @@ -2238,5 +2258,4 @@ public function decodePolygon(string $wkb): array

return $rings; // array of rings, each ring is array of [x,y]
}

}
17 changes: 16 additions & 1 deletion src/Database/Adapter/SQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -1724,6 +1724,13 @@ protected function getPDO(): mixed
*/
abstract protected function getPDOType(mixed $value): int;

/**
* Get the SQL function for random ordering
*
* @return string
*/
abstract protected function getRandomOrder(): string;

/**
* Returns default PDO configuration
*
Expand Down Expand Up @@ -2363,10 +2370,18 @@ public function find(Document $collection, array $queries = [], ?int $limit = 25
$cursorWhere = [];

foreach ($orderAttributes as $i => $originalAttribute) {
$orderType = $orderTypes[$i] ?? Database::ORDER_ASC;

// Handle random ordering specially
if ($orderType === Database::ORDER_RANDOM) {
$orders[] = $this->getRandomOrder();
continue;
}

$attribute = $this->getInternalKeyForAttribute($originalAttribute);
$attribute = $this->filter($attribute);

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

if ($cursorDirection === Database::CURSOR_BEFORE) {
Expand Down
20 changes: 20 additions & 0 deletions src/Database/Adapter/SQLite.php
Original file line number Diff line number Diff line change
Expand Up @@ -1284,4 +1284,24 @@ public function getSupportForSpatialAxisOrder(): bool
{
return false;
}

/**
* Adapter supports optionalspatial attributes with existing rows.
*
* @return bool
*/
public function getSupportForOptionalSpatialAttributeWithExistingRows(): bool
{
return true;
}

/**
* Get the SQL function for random ordering
*
* @return string
*/
protected function getRandomOrder(): string
{
return 'RANDOM()';
}
}
2 changes: 2 additions & 0 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ class Database
public const ORDER_ASC = 'ASC';
public const ORDER_DESC = 'DESC';

public const ORDER_RANDOM = 'RANDOM';

// Permissions
public const PERMISSION_CREATE = 'create';
public const PERMISSION_READ = 'read';
Expand Down
22 changes: 19 additions & 3 deletions src/Database/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class Query
// Order methods
public const TYPE_ORDER_DESC = 'orderDesc';
public const TYPE_ORDER_ASC = 'orderAsc';
public const TYPE_ORDER_RANDOM = 'orderRandom';

// Pagination methods
public const TYPE_LIMIT = 'limit';
Expand Down Expand Up @@ -93,6 +94,7 @@ class Query
self::TYPE_SELECT,
self::TYPE_ORDER_DESC,
self::TYPE_ORDER_ASC,
self::TYPE_ORDER_RANDOM,
self::TYPE_LIMIT,
self::TYPE_OFFSET,
self::TYPE_CURSOR_AFTER,
Expand Down Expand Up @@ -247,6 +249,7 @@ public static function isMethod(string $value): bool
self::TYPE_NOT_SEARCH,
self::TYPE_ORDER_ASC,
self::TYPE_ORDER_DESC,
self::TYPE_ORDER_RANDOM,
self::TYPE_LIMIT,
self::TYPE_OFFSET,
self::TYPE_CURSOR_AFTER,
Expand Down Expand Up @@ -600,6 +603,16 @@ public static function orderAsc(string $attribute = ''): self
return new self(self::TYPE_ORDER_ASC, $attribute);
}

/**
* Helper method to create Query with orderRandom method
*
* @return Query
*/
public static function orderRandom(): self
{
return new self(self::TYPE_ORDER_RANDOM);
}

/**
* Helper method to create Query with limit method
*
Expand Down Expand Up @@ -830,13 +843,16 @@ public static function groupByType(array $queries): array
switch ($method) {
case Query::TYPE_ORDER_ASC:
case Query::TYPE_ORDER_DESC:
case Query::TYPE_ORDER_RANDOM:
if (!empty($attribute)) {
$orderAttributes[] = $attribute;
}

$orderTypes[] = $method === Query::TYPE_ORDER_ASC
? Database::ORDER_ASC
: Database::ORDER_DESC;
$orderTypes[] = match ($method) {
Query::TYPE_ORDER_ASC => Database::ORDER_ASC,
Query::TYPE_ORDER_DESC => Database::ORDER_DESC,
Query::TYPE_ORDER_RANDOM => Database::ORDER_RANDOM,
};

break;
case Query::TYPE_LIMIT:
Expand Down
3 changes: 2 additions & 1 deletion src/Database/Validator/Queries.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ public function isValid($value): bool
Query::TYPE_CURSOR_AFTER,
Query::TYPE_CURSOR_BEFORE => Base::METHOD_TYPE_CURSOR,
Query::TYPE_ORDER_ASC,
Query::TYPE_ORDER_DESC => Base::METHOD_TYPE_ORDER,
Query::TYPE_ORDER_DESC,
Query::TYPE_ORDER_RANDOM => Base::METHOD_TYPE_ORDER,
Query::TYPE_EQUAL,
Query::TYPE_NOT_EQUAL,
Query::TYPE_LESSER,
Expand Down
4 changes: 4 additions & 0 deletions src/Database/Validator/Query/Order.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ public function isValid($value): bool
return $this->isValidAttribute($attribute);
}

if ($method === Query::TYPE_ORDER_RANDOM) {
return true; // orderRandom doesn't need an attribute
}

return false;
}

Expand Down
66 changes: 66 additions & 0 deletions tests/e2e/Adapter/Scopes/DocumentTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -3484,6 +3484,72 @@ public function testFindNotEndsWith(): void
$this->assertLessThanOrEqual(5, count($documents)); // But still excluding Marvel movies
}

public function testFindOrderRandom(): void
{
/** @var Database $database */
$database = static::getDatabase();

// Test orderRandom with default limit
$documents = $database->find('movies', [
Query::orderRandom(),
Query::limit(1),
]);
$this->assertEquals(1, count($documents));
$this->assertNotEmpty($documents[0]['name']); // Ensure we got a valid document

// Test orderRandom with multiple documents
$documents = $database->find('movies', [
Query::orderRandom(),
Query::limit(3),
]);
$this->assertEquals(3, count($documents));

// Test that orderRandom returns different results (not guaranteed but highly likely)
$firstSet = $database->find('movies', [
Query::orderRandom(),
Query::limit(3),
]);
$secondSet = $database->find('movies', [
Query::orderRandom(),
Query::limit(3),
]);

// Extract IDs for comparison
$firstIds = array_map(fn ($doc) => $doc['$id'], $firstSet);
$secondIds = array_map(fn ($doc) => $doc['$id'], $secondSet);

// While not guaranteed to be different, with 6 movies and selecting 3,
// the probability of getting the same set in the same order is very low
// We'll just check that we got valid results
$this->assertEquals(3, count($firstIds));
$this->assertEquals(3, count($secondIds));

// Test orderRandom with more than available documents
$documents = $database->find('movies', [
Query::orderRandom(),
Query::limit(10), // We only have 6 movies
]);
$this->assertLessThanOrEqual(6, count($documents)); // Should return all available documents

// Test orderRandom with filters
$documents = $database->find('movies', [
Query::greaterThan('price', 10),
Query::orderRandom(),
Query::limit(2),
]);
$this->assertLessThanOrEqual(2, count($documents));
foreach ($documents as $document) {
$this->assertGreaterThan(10, $document['price']);
}

// Test orderRandom without explicit limit (should use default)
$documents = $database->find('movies', [
Query::orderRandom(),
]);
$this->assertGreaterThan(0, count($documents));
$this->assertLessThanOrEqual(25, count($documents)); // Default limit is 25
}

public function testFindNotBetween(): void
{
/** @var Database $database */
Expand Down
Loading