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
10 changes: 10 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
10 changes: 10 additions & 0 deletions src/Database/Adapter/Postgres.php
Original file line number Diff line number Diff line change
Expand Up @@ -1792,6 +1792,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
17 changes: 16 additions & 1 deletion src/Database/Adapter/SQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -1725,6 +1725,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 @@ -2395,10 +2402,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
10 changes: 10 additions & 0 deletions src/Database/Adapter/SQLite.php
Original file line number Diff line number Diff line change
Expand Up @@ -1294,4 +1294,14 @@ 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 @@ -3459,6 +3459,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
15 changes: 15 additions & 0 deletions tests/unit/QueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ public function testCreate(): void
$this->assertEquals(Query::TYPE_BETWEEN, $query->getMethod());
$this->assertEquals('$updatedAt', $query->getAttribute());
$this->assertEquals(['2023-01-01T00:00:00.000Z', '2023-12-31T23:59:59.999Z'], $query->getValues());

// Test orderRandom query
$query = Query::orderRandom();
$this->assertEquals(Query::TYPE_ORDER_RANDOM, $query->getMethod());
$this->assertEquals('', $query->getAttribute());
$this->assertEquals([], $query->getValues());
}

/**
Expand Down Expand Up @@ -365,6 +371,12 @@ public function testParse(): void
} catch (QueryException $e) {
$this->assertEquals('Invalid query. Must be an array, got boolean', $e->getMessage());
}

// Test orderRandom query parsing
$query = Query::parse(Query::orderRandom()->toString());
$this->assertEquals('orderRandom', $query->getMethod());
$this->assertEquals('', $query->getAttribute());
$this->assertEquals([], $query->getValues());
}

public function testIsMethod(): void
Expand All @@ -389,6 +401,7 @@ public function testIsMethod(): void
$this->assertTrue(Query::isMethod('offset'));
$this->assertTrue(Query::isMethod('cursorAfter'));
$this->assertTrue(Query::isMethod('cursorBefore'));
$this->assertTrue(Query::isMethod('orderRandom'));
$this->assertTrue(Query::isMethod('isNull'));
$this->assertTrue(Query::isMethod('isNotNull'));
$this->assertTrue(Query::isMethod('between'));
Expand Down Expand Up @@ -417,6 +430,7 @@ public function testIsMethod(): void
$this->assertTrue(Query::isMethod(QUERY::TYPE_OFFSET));
$this->assertTrue(Query::isMethod(QUERY::TYPE_CURSOR_AFTER));
$this->assertTrue(Query::isMethod(QUERY::TYPE_CURSOR_BEFORE));
$this->assertTrue(Query::isMethod(QUERY::TYPE_ORDER_RANDOM));
$this->assertTrue(Query::isMethod(QUERY::TYPE_IS_NULL));
$this->assertTrue(Query::isMethod(QUERY::TYPE_IS_NOT_NULL));
$this->assertTrue(Query::isMethod(QUERY::TYPE_BETWEEN));
Expand All @@ -437,5 +451,6 @@ public function testNewQueryTypesInTypesArray(): void
$this->assertContains(Query::TYPE_NOT_STARTS_WITH, Query::TYPES);
$this->assertContains(Query::TYPE_NOT_ENDS_WITH, Query::TYPES);
$this->assertContains(Query::TYPE_NOT_BETWEEN, Query::TYPES);
$this->assertContains(Query::TYPE_ORDER_RANDOM, Query::TYPES);
}
}