Add ORDER BY support

- Update deps
This commit is contained in:
Daniel J. Summers 2024-09-26 21:52:06 -04:00
parent a3ad158dfe
commit 9693054fec
9 changed files with 363 additions and 66 deletions

70
composer.lock generated
View File

@ -166,16 +166,16 @@
}, },
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
"version": "v5.1.0", "version": "v5.2.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nikic/PHP-Parser.git", "url": "https://github.com/nikic/PHP-Parser.git",
"reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb",
"reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -218,9 +218,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/nikic/PHP-Parser/issues", "issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0" "source": "https://github.com/nikic/PHP-Parser/tree/v5.2.0"
}, },
"time": "2024-07-01T20:03:41+00:00" "time": "2024-09-15T16:40:33+00:00"
}, },
{ {
"name": "phar-io/manifest", "name": "phar-io/manifest",
@ -342,16 +342,16 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "1.12.2", "version": "1.12.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/phpstan/phpstan.git",
"reference": "0ca1c7bb55fca8fe6448f16fff0f311ccec960a1" "reference": "7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/0ca1c7bb55fca8fe6448f16fff0f311ccec960a1", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17",
"reference": "0ca1c7bb55fca8fe6448f16fff0f311ccec960a1", "reference": "7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -396,7 +396,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-09-05T16:09:28+00:00" "time": "2024-09-26T12:45:22+00:00"
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
@ -723,16 +723,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "11.3.3", "version": "11.3.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "8ed08766d9a2ed979a2f5fdbb95a0671523419c1" "reference": "d62c45a19c665bb872c2a47023a0baf41a98bb2b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8ed08766d9a2ed979a2f5fdbb95a0671523419c1", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d62c45a19c665bb872c2a47023a0baf41a98bb2b",
"reference": "8ed08766d9a2ed979a2f5fdbb95a0671523419c1", "reference": "d62c45a19c665bb872c2a47023a0baf41a98bb2b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -753,13 +753,13 @@
"phpunit/php-timer": "^7.0.1", "phpunit/php-timer": "^7.0.1",
"sebastian/cli-parser": "^3.0.2", "sebastian/cli-parser": "^3.0.2",
"sebastian/code-unit": "^3.0.1", "sebastian/code-unit": "^3.0.1",
"sebastian/comparator": "^6.0.2", "sebastian/comparator": "^6.1.0",
"sebastian/diff": "^6.0.2", "sebastian/diff": "^6.0.2",
"sebastian/environment": "^7.2.0", "sebastian/environment": "^7.2.0",
"sebastian/exporter": "^6.1.3", "sebastian/exporter": "^6.1.3",
"sebastian/global-state": "^7.0.2", "sebastian/global-state": "^7.0.2",
"sebastian/object-enumerator": "^6.0.1", "sebastian/object-enumerator": "^6.0.1",
"sebastian/type": "^5.0.1", "sebastian/type": "^5.1.0",
"sebastian/version": "^5.0.1" "sebastian/version": "^5.0.1"
}, },
"suggest": { "suggest": {
@ -803,7 +803,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues", "issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy", "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.3.3" "source": "https://github.com/sebastianbergmann/phpunit/tree/11.3.6"
}, },
"funding": [ "funding": [
{ {
@ -819,7 +819,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-09-04T13:34:52+00:00" "time": "2024-09-19T10:54:28+00:00"
}, },
{ {
"name": "sebastian/cli-parser", "name": "sebastian/cli-parser",
@ -993,16 +993,16 @@
}, },
{ {
"name": "sebastian/comparator", "name": "sebastian/comparator",
"version": "6.0.2", "version": "6.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git", "url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "450d8f237bd611c45b5acf0733ce43e6bb280f81" "reference": "fa37b9e2ca618cb051d71b60120952ee8ca8b03d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/450d8f237bd611c45b5acf0733ce43e6bb280f81", "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa37b9e2ca618cb051d71b60120952ee8ca8b03d",
"reference": "450d8f237bd611c45b5acf0733ce43e6bb280f81", "reference": "fa37b9e2ca618cb051d71b60120952ee8ca8b03d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1013,12 +1013,12 @@
"sebastian/exporter": "^6.0" "sebastian/exporter": "^6.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^11.0" "phpunit/phpunit": "^11.3"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "6.0-dev" "dev-main": "6.1-dev"
} }
}, },
"autoload": { "autoload": {
@ -1058,7 +1058,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues", "issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy", "security": "https://github.com/sebastianbergmann/comparator/security/policy",
"source": "https://github.com/sebastianbergmann/comparator/tree/6.0.2" "source": "https://github.com/sebastianbergmann/comparator/tree/6.1.0"
}, },
"funding": [ "funding": [
{ {
@ -1066,7 +1066,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-08-12T06:07:25+00:00" "time": "2024-09-11T15:42:56+00:00"
}, },
{ {
"name": "sebastian/complexity", "name": "sebastian/complexity",
@ -1635,28 +1635,28 @@
}, },
{ {
"name": "sebastian/type", "name": "sebastian/type",
"version": "5.0.1", "version": "5.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/type.git", "url": "https://github.com/sebastianbergmann/type.git",
"reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa" "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb6a6566f9589e86661291d13eba708cce5eb4aa", "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac",
"reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa", "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.2" "php": ">=8.2"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^11.0" "phpunit/phpunit": "^11.3"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "5.0-dev" "dev-main": "5.1-dev"
} }
}, },
"autoload": { "autoload": {
@ -1680,7 +1680,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/type/issues", "issues": "https://github.com/sebastianbergmann/type/issues",
"security": "https://github.com/sebastianbergmann/type/security/policy", "security": "https://github.com/sebastianbergmann/type/security/policy",
"source": "https://github.com/sebastianbergmann/type/tree/5.0.1" "source": "https://github.com/sebastianbergmann/type/tree/5.1.0"
}, },
"funding": [ "funding": [
{ {
@ -1688,7 +1688,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-07-03T05:11:49+00:00" "time": "2024-09-17T13:12:04+00:00"
}, },
{ {
"name": "sebastian/version", "name": "sebastian/version",

View File

@ -35,10 +35,12 @@ class DocumentList
*/ */
private function __construct(private ?PDOStatement &$result, private readonly Mapper $mapper) private function __construct(private ?PDOStatement &$result, private readonly Mapper $mapper)
{ {
if ($row = $this->result->fetch(PDO::FETCH_ASSOC)) { if (!is_null($this->result)) {
$this->first = $this->mapper->map($row); if ($row = $this->result->fetch(PDO::FETCH_ASSOC)) {
} else { $this->first = $this->mapper->map($row);
$this->result = null; } else {
$this->result = null;
}
} }
} }
@ -67,6 +69,11 @@ class DocumentList
$this->isConsumed = true; $this->isConsumed = true;
return; return;
} }
if (!$this->first) {
$this->isConsumed = true;
$this->result = null;
return;
}
yield $this->first; yield $this->first;
while ($row = $this->result->fetch(PDO::FETCH_ASSOC)) { while ($row = $this->result->fetch(PDO::FETCH_ASSOC)) {
yield $this->mapper->map($row); yield $this->mapper->map($row);

View File

@ -387,4 +387,18 @@ class Field
{ {
return self::notExists($fieldName); return self::notExists($fieldName);
} }
/**
* Create a named fields (used for creating fields for ORDER BY clauses)
*
* Prepend the field name with 'n:' to treat the field as a number; prepend the field name with 'i:' to perform
* a case-insensitive ordering.
*
* @param string $name The name of the field, plus any direction for the ordering
* @return self
*/
public static function named(string $name): self
{
return new self($name, Op::Equal, '', '');
}
} }

View File

@ -22,12 +22,14 @@ class Find
* @template TDoc The type of document to be retrieved * @template TDoc The type of document to be retrieved
* @param string $tableName The table from which documents should be retrieved * @param string $tableName The table from which documents should be retrieved
* @param class-string<TDoc> $className The name of the class to be retrieved * @param class-string<TDoc> $className The name of the class to be retrieved
* @param Field[] $orderBy Fields by which the results should be ordered (optional, default no ordering)
* @return DocumentList<TDoc> A list of all documents from the table * @return DocumentList<TDoc> A list of all documents from the table
* @throws DocumentException If any is encountered * @throws DocumentException If any is encountered
*/ */
public static function all(string $tableName, string $className): DocumentList public static function all(string $tableName, string $className, array $orderBy = []): DocumentList
{ {
return Custom::list(Query::selectFromTable($tableName), [], new DocumentMapper($className)); return Custom::list(Query::selectFromTable($tableName) . Query::orderBy($orderBy), [],
new DocumentMapper($className));
} }
/** /**
@ -54,15 +56,16 @@ class Find
* @param Field[] $fields The field comparison to match * @param Field[] $fields The field comparison to match
* @param class-string<TDoc> $className The name of the class to be retrieved * @param class-string<TDoc> $className The name of the class to be retrieved
* @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All)
* @param Field[] $orderBy Fields by which the results should be ordered (optional, default no ordering)
* @return DocumentList<TDoc> A list of documents matching the given field comparison * @return DocumentList<TDoc> A list of documents matching the given field comparison
* @throws DocumentException If any is encountered * @throws DocumentException If any is encountered
*/ */
public static function byFields(string $tableName, array $fields, string $className, public static function byFields(string $tableName, array $fields, string $className,
?FieldMatch $match = null): DocumentList ?FieldMatch $match = null, array $orderBy = []): DocumentList
{ {
Parameters::nameFields($fields); Parameters::nameFields($fields);
return Custom::list(Query\Find::byFields($tableName, $fields, $match), Parameters::addFields($fields, []), return Custom::list(Query\Find::byFields($tableName, $fields, $match) . Query::orderBy($orderBy),
new DocumentMapper($className)); Parameters::addFields($fields, []), new DocumentMapper($className));
} }
/** /**
@ -72,13 +75,15 @@ class Find
* @param string $tableName The name of the table from which documents should be retrieved * @param string $tableName The name of the table from which documents should be retrieved
* @param mixed[]|object $criteria The criteria for the JSON containment query * @param mixed[]|object $criteria The criteria for the JSON containment query
* @param class-string<TDoc> $className The name of the class to be retrieved * @param class-string<TDoc> $className The name of the class to be retrieved
* @param Field[] $orderBy Fields by which the results should be ordered (optional, default no ordering)
* @return DocumentList<TDoc> A list of documents matching the JSON containment query * @return DocumentList<TDoc> A list of documents matching the JSON containment query
* @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs
*/ */
public static function byContains(string $tableName, array|object $criteria, string $className): DocumentList public static function byContains(string $tableName, array|object $criteria, string $className,
array $orderBy = []): DocumentList
{ {
return Custom::list(Query\Find::byContains($tableName), Parameters::json(':criteria', $criteria), return Custom::list(Query\Find::byContains($tableName) . Query::orderBy($orderBy),
new DocumentMapper($className)); Parameters::json(':criteria', $criteria), new DocumentMapper($className));
} }
/** /**
@ -88,12 +93,15 @@ class Find
* @param string $tableName The name of the table from which documents should be retrieved * @param string $tableName The name of the table from which documents should be retrieved
* @param string $path The JSON Path match string * @param string $path The JSON Path match string
* @param class-string<TDoc> $className The name of the class to be retrieved * @param class-string<TDoc> $className The name of the class to be retrieved
* @param Field[] $orderBy Fields by which the results should be ordered (optional, default no ordering)
* @return DocumentList<TDoc> A list of documents matching the JSON Path * @return DocumentList<TDoc> A list of documents matching the JSON Path
* @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs
*/ */
public static function byJsonPath(string $tableName, string $path, string $className): DocumentList public static function byJsonPath(string $tableName, string $path, string $className,
array $orderBy = []): DocumentList
{ {
return Custom::list(Query\Find::byJsonPath($tableName), [':path' => $path], new DocumentMapper($className)); return Custom::list(Query\Find::byJsonPath($tableName) . Query::orderBy($orderBy), [':path' => $path],
new DocumentMapper($className));
} }
/** /**
@ -104,14 +112,15 @@ class Find
* @param Field[] $fields The field comparison to match * @param Field[] $fields The field comparison to match
* @param class-string<TDoc> $className The name of the class to be retrieved * @param class-string<TDoc> $className The name of the class to be retrieved
* @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All)
* @param Field[] $orderBy Fields by which the results should be ordered (optional, default no ordering)
* @return Option<TDoc> A `Some` instance with the first document if any matches are found, `None` otherwise * @return Option<TDoc> A `Some` instance with the first document if any matches are found, `None` otherwise
* @throws DocumentException If any is encountered * @throws DocumentException If any is encountered
*/ */
public static function firstByFields(string $tableName, array $fields, string $className, public static function firstByFields(string $tableName, array $fields, string $className,
?FieldMatch $match = null): Option ?FieldMatch $match = null, array $orderBy = []): Option
{ {
Parameters::nameFields($fields); Parameters::nameFields($fields);
return Custom::single(Query\Find::byFields($tableName, $fields, $match), return Custom::single(Query\Find::byFields($tableName, $fields, $match) . Query::orderBy($orderBy),
Parameters::addFields($fields, []), new DocumentMapper($className)); Parameters::addFields($fields, []), new DocumentMapper($className));
} }
@ -122,13 +131,15 @@ class Find
* @param string $tableName The name of the table from which documents should be retrieved * @param string $tableName The name of the table from which documents should be retrieved
* @param mixed[]|object $criteria The criteria for the JSON containment query * @param mixed[]|object $criteria The criteria for the JSON containment query
* @param class-string<TDoc> $className The name of the class to be retrieved * @param class-string<TDoc> $className The name of the class to be retrieved
* @param Field[] $orderBy Fields by which the results should be ordered (optional, default no ordering)
* @return Option<TDoc> A `Some` instance with the first document if any matches are found, `None` otherwise * @return Option<TDoc> A `Some` instance with the first document if any matches are found, `None` otherwise
* @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs
*/ */
public static function firstByContains(string $tableName, array|object $criteria, string $className): Option public static function firstByContains(string $tableName, array|object $criteria, string $className,
array $orderBy = []): Option
{ {
return Custom::single(Query\Find::byContains($tableName), Parameters::json(':criteria', $criteria), return Custom::single(Query\Find::byContains($tableName) . Query::orderBy($orderBy),
new DocumentMapper($className)); Parameters::json(':criteria', $criteria), new DocumentMapper($className));
} }
/** /**
@ -138,11 +149,14 @@ class Find
* @param string $tableName The name of the table from which documents should be retrieved * @param string $tableName The name of the table from which documents should be retrieved
* @param string $path The JSON Path match string * @param string $path The JSON Path match string
* @param class-string<TDoc> $className The name of the class to be retrieved * @param class-string<TDoc> $className The name of the class to be retrieved
* @param Field[] $orderBy Fields by which the results should be ordered (optional, default no ordering)
* @return Option<TDoc> A `Some` instance with the first document if any matches are found, `None` otherwise * @return Option<TDoc> A `Some` instance with the first document if any matches are found, `None` otherwise
* @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs
*/ */
public static function firstByJsonPath(string $tableName, string $path, string $className): Option public static function firstByJsonPath(string $tableName, string $path, string $className,
array $orderBy = []): Option
{ {
return Custom::single(Query\Find::byJsonPath($tableName), [':path' => $path], new DocumentMapper($className)); return Custom::single(Query\Find::byJsonPath($tableName) . Query::orderBy($orderBy), [':path' => $path],
new DocumentMapper($className));
} }
} }

View File

@ -142,4 +142,46 @@ class Query
{ {
return "UPDATE $tableName SET data = :data WHERE " . self::whereById(); return "UPDATE $tableName SET data = :data WHERE " . self::whereById();
} }
/**
* Create an `ORDER BY` clause ('n:' treats field as number, 'i:' does case-insensitive ordering)
*
* @param Field[] $fields The fields, named for the field plus directions (ex. 'field DESC NULLS FIRST')
* @return string The ORDER BY clause with the given fields
* @throws Exception If the database mode has not been set
*/
public static function orderBy(array $fields): string
{
if (empty($fields)) return "";
$mode = Configuration::mode('render ORDER BY clause');
$sqlFields = array_map(function (Field $it) use ($mode) {
if (str_contains($it->fieldName, ' ')) {
$parts = explode(' ', $it->fieldName);
$it->fieldName = array_shift($parts);
$direction = ' ' . implode(' ', $parts);
} else {
$direction = '';
}
if (str_starts_with($it->fieldName, 'n:')) {
$it->fieldName = substr($it->fieldName, 2);
$value = match ($mode) {
Mode::PgSQL => '(' . $it->path() . ')::numeric',
Mode::SQLite => $it->path()
};
} elseif (str_starts_with($it->fieldName, 'i:')) {
$it->fieldName = substr($it->fieldName, 2);
$value = match ($mode) {
Mode::PgSQL => 'LOWER(' . $it->path() . ')',
Mode::SQLite => $it->path() . ' COLLATE NOCASE'
};
} else {
$value = $it->path();
}
return $value . $direction;
}, $fields);
return ' ORDER BY ' . implode(', ', $sqlFields);
}
} }

View File

@ -44,6 +44,34 @@ class FindTest extends TestCase
$this->assertEquals(5, $count, 'There should have been 5 documents in the list'); $this->assertEquals(5, $count, 'There should have been 5 documents in the list');
} }
#[TestDox('all() succeeds when ordering data ascending')]
public function testAllSucceedsWhenOrderingDataAscending(): void
{
$docs = Find::all(ThrowawayDb::TABLE, TestDocument::class, [Field::named('id')]);
$this->assertNotNull($docs, 'There should have been a document list returned');
$ids = iterator_to_array($docs->map(fn ($it) => $it->id), false);
$this->assertEquals(['five', 'four', 'one', 'three', 'two'], $ids, 'The documents were not ordered correctly');
}
#[TestDox('all() succeeds when ordering data descending')]
public function testAllSucceedsWhenOrderingDataDescending(): void
{
$docs = Find::all(ThrowawayDb::TABLE, TestDocument::class, [Field::named('id DESC')]);
$this->assertNotNull($docs, 'There should have been a document list returned');
$ids = iterator_to_array($docs->map(fn ($it) => $it->id), false);
$this->assertEquals(['two', 'three', 'one', 'four', 'five'], $ids, 'The documents were not ordered correctly');
}
#[TestDox('all() succeeds when ordering data numerically')]
public function testAllSucceedsWhenOrderingDataNumerically(): void
{
$docs = Find::all(ThrowawayDb::TABLE, TestDocument::class,
[Field::named('sub.foo NULLS LAST'), Field::named('n:num_value')]);
$this->assertNotNull($docs, 'There should have been a document list returned');
$ids = iterator_to_array($docs->map(fn ($it) => $it->id), false);
$this->assertEquals(['two', 'four', 'one', 'three', 'five'], $ids, 'The documents were not ordered correctly');
}
#[TestDox('all() succeeds when there is no data')] #[TestDox('all() succeeds when there is no data')]
public function testAllSucceedsWhenThereIsNoData(): void public function testAllSucceedsWhenThereIsNoData(): void
{ {
@ -89,8 +117,18 @@ class FindTest extends TestCase
$this->assertEquals(1, $count, 'There should have been 1 document in the list'); $this->assertEquals(1, $count, 'There should have been 1 document in the list');
} }
#[TestDox('byFields() succeeds when documents are found and ordered')]
public function testByFieldsSucceedsWhenDocumentsAreFoundAndOrdered(): void
{
$docs = Find::byFields(ThrowawayDb::TABLE, [Field::equal('value', 'purple')], TestDocument::class,
FieldMatch::All, [Field::named('id')]);
$this->assertNotNull($docs, 'There should have been a document list returned');
$ids = iterator_to_array($docs->map(fn ($it) => $it->id), false);
$this->assertEquals(['five', 'four'], $ids, 'The documents were not ordered correctly');
}
#[TestDox('byFields() succeeds when documents are found using IN with numeric field')] #[TestDox('byFields() succeeds when documents are found using IN with numeric field')]
public function testByFieldsSucceedsWhenDocumentsAreFoundUsingInWithNumericField() public function testByFieldsSucceedsWhenDocumentsAreFoundUsingInWithNumericField(): void
{ {
$docs = Find::byFields(ThrowawayDb::TABLE, [Field::in('num_value', [2, 4, 6, 8])], TestDocument::class); $docs = Find::byFields(ThrowawayDb::TABLE, [Field::in('num_value', [2, 4, 6, 8])], TestDocument::class);
$this->assertNotNull($docs, 'There should have been a document list returned'); $this->assertNotNull($docs, 'There should have been a document list returned');
@ -141,6 +179,16 @@ class FindTest extends TestCase
$this->assertEquals(2, $count, 'There should have been 2 documents in the list'); $this->assertEquals(2, $count, 'There should have been 2 documents in the list');
} }
#[TestDox('byContains() succeeds when documents are found and ordered')]
public function testByContainsSucceedsWhenDocumentsAreFoundAndOrdered(): void
{
$docs = Find::byContains(ThrowawayDb::TABLE, ['sub' => ['foo' => 'green']], TestDocument::class,
[Field::named('value')]);
$this->assertNotNull($docs, 'There should have been a document list returned');
$ids = iterator_to_array($docs->map(fn ($it) => $it->id), false);
$this->assertEquals(['two', 'four'], $ids, 'The documents were not ordered correctly');
}
#[TestDox('byContains() succeeds when no documents are found')] #[TestDox('byContains() succeeds when no documents are found')]
public function testByContainsSucceedsWhenNoDocumentsAreFound(): void public function testByContainsSucceedsWhenNoDocumentsAreFound(): void
{ {
@ -159,6 +207,16 @@ class FindTest extends TestCase
$this->assertEquals(2, $count, 'There should have been 2 documents in the list'); $this->assertEquals(2, $count, 'There should have been 2 documents in the list');
} }
#[TestDox('byJsonPath() succeeds when documents are found and ordered')]
public function testByJsonPathSucceedsWhenDocumentsAreFoundAndOrdered(): void
{
$docs = Find::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 10)', TestDocument::class,
[Field::named('id')]);
$this->assertNotNull($docs, 'There should have been a document list returned');
$ids = iterator_to_array($docs->map(fn ($it) => $it->id), false);
$this->assertEquals(['five', 'four'], $ids, 'The documents were not ordered correctly');
}
#[TestDox('byJsonPath() succeeds when no documents are found')] #[TestDox('byJsonPath() succeeds when no documents are found')]
public function testByJsonPathSucceedsWhenNoDocumentsAreFound(): void public function testByJsonPathSucceedsWhenNoDocumentsAreFound(): void
{ {
@ -183,6 +241,15 @@ class FindTest extends TestCase
$this->assertContains($doc->get()->id, ['two', 'four'], 'An incorrect document was returned'); $this->assertContains($doc->get()->id, ['two', 'four'], 'An incorrect document was returned');
} }
#[TestDox('firstByFields() succeeds when multiple ordered documents are found')]
public function testFirstByFieldsSucceedsWhenMultipleOrderedDocumentsAreFound(): void
{
$doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('sub.foo', 'green')], TestDocument::class,
orderBy: [Field::named('n:num_value DESC')]);
$this->assertTrue($doc->isSome(), 'There should have been a document returned');
$this->assertEquals('four', $doc->get()->id, 'The incorrect document was returned');
}
#[TestDox('firstByFields() succeeds when a document is not found')] #[TestDox('firstByFields() succeeds when a document is not found')]
public function testFirstByFieldsSucceedsWhenADocumentIsNotFound(): void public function testFirstByFieldsSucceedsWhenADocumentIsNotFound(): void
{ {
@ -206,6 +273,15 @@ class FindTest extends TestCase
$this->assertContains($doc->get()->id, ['four', 'five'], 'An incorrect document was returned'); $this->assertContains($doc->get()->id, ['four', 'five'], 'An incorrect document was returned');
} }
#[TestDox('firstByContains() succeeds when multiple ordered documents are found')]
public function testFirstByContainsSucceedsWhenMultipleOrderedDocumentsAreFound(): void
{
$doc = Find::firstByContains(ThrowawayDb::TABLE, ['value' => 'purple'], TestDocument::class,
[Field::named('sub.bar NULLS FIRST')]);
$this->assertTrue($doc->isSome(), 'There should have been a document returned');
$this->assertEquals('five', $doc->get()->id, 'The incorrect document was returned');
}
#[TestDox('firstByContains() succeeds when a document is not found')] #[TestDox('firstByContains() succeeds when a document is not found')]
public function testFirstByContainsSucceedsWhenADocumentIsNotFound(): void public function testFirstByContainsSucceedsWhenADocumentIsNotFound(): void
{ {
@ -229,6 +305,15 @@ class FindTest extends TestCase
$this->assertContains($doc->get()->id, ['four', 'five'], 'An incorrect document was returned'); $this->assertContains($doc->get()->id, ['four', 'five'], 'An incorrect document was returned');
} }
#[TestDox('firstByJsonPath() succeeds when multiple ordered documents are found')]
public function testFirstByJsonPathSucceedsWhenMultipleOrderedDocumentsAreFound(): void
{
$doc = Find::firstByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 10)', TestDocument::class,
[Field::named('id DESC')]);
$this->assertTrue($doc->isSome(), 'There should have been a document returned');
$this->assertEquals('four', $doc->get()->id, 'The incorrect document was returned');
}
#[TestDox('firstByJsonPath() succeeds when a document is not found')] #[TestDox('firstByJsonPath() succeeds when a document is not found')]
public function testFirstByJsonPathSucceedsWhenADocumentIsNotFound(): void public function testFirstByJsonPathSucceedsWhenADocumentIsNotFound(): void
{ {

View File

@ -45,6 +45,34 @@ class FindTest extends TestCase
$this->assertEquals(5, $count, 'There should have been 5 documents in the list'); $this->assertEquals(5, $count, 'There should have been 5 documents in the list');
} }
#[TestDox('all() succeeds when ordering data ascending')]
public function testAllSucceedsWhenOrderingDataAscending(): void
{
$docs = Find::all(ThrowawayDb::TABLE, TestDocument::class, [Field::named('id')]);
$this->assertNotNull($docs, 'There should have been a document list returned');
$ids = iterator_to_array($docs->map(fn ($it) => $it->id), false);
$this->assertEquals(['five', 'four', 'one', 'three', 'two'], $ids, 'The documents were not ordered correctly');
}
#[TestDox('all() succeeds when ordering data descending')]
public function testAllSucceedsWhenOrderingDataDescending(): void
{
$docs = Find::all(ThrowawayDb::TABLE, TestDocument::class, [Field::named('id DESC')]);
$this->assertNotNull($docs, 'There should have been a document list returned');
$ids = iterator_to_array($docs->map(fn ($it) => $it->id), false);
$this->assertEquals(['two', 'three', 'one', 'four', 'five'], $ids, 'The documents were not ordered correctly');
}
#[TestDox('all() succeeds when ordering data numerically')]
public function testAllSucceedsWhenOrderingDataNumerically(): void
{
$docs = Find::all(ThrowawayDb::TABLE, TestDocument::class,
[Field::named('sub.foo NULLS LAST'), Field::named('n:num_value')]);
$this->assertNotNull($docs, 'There should have been a document list returned');
$ids = iterator_to_array($docs->map(fn ($it) => $it->id), false);
$this->assertEquals(['two', 'four', 'one', 'three', 'five'], $ids, 'The documents were not ordered correctly');
}
#[TestDox('all() succeeds when there is no data')] #[TestDox('all() succeeds when there is no data')]
public function testAllSucceedsWhenThereIsNoData(): void public function testAllSucceedsWhenThereIsNoData(): void
{ {
@ -89,8 +117,18 @@ class FindTest extends TestCase
$this->assertEquals(1, $count, 'There should have been 1 document in the list'); $this->assertEquals(1, $count, 'There should have been 1 document in the list');
} }
#[TestDox('byFields() succeeds when documents are found and ordered')]
public function testByFieldsSucceedsWhenDocumentsAreFoundAndOrdered(): void
{
$docs = Find::byFields(ThrowawayDb::TABLE, [Field::equal('value', 'purple')], TestDocument::class,
FieldMatch::All, [Field::named('id')]);
$this->assertNotNull($docs, 'There should have been a document list returned');
$ids = iterator_to_array($docs->map(fn ($it) => $it->id), false);
$this->assertEquals(['five', 'four'], $ids, 'The documents were not ordered correctly');
}
#[TestDox('byFields() succeeds when documents are found using IN with numeric field')] #[TestDox('byFields() succeeds when documents are found using IN with numeric field')]
public function testByFieldsSucceedsWhenDocumentsAreFoundUsingInWithNumericField() public function testByFieldsSucceedsWhenDocumentsAreFoundUsingInWithNumericField(): void
{ {
$docs = Find::byFields(ThrowawayDb::TABLE, [Field::in('num_value', [2, 4, 6, 8])], TestDocument::class); $docs = Find::byFields(ThrowawayDb::TABLE, [Field::in('num_value', [2, 4, 6, 8])], TestDocument::class);
$this->assertNotNull($docs, 'There should have been a document list returned'); $this->assertNotNull($docs, 'There should have been a document list returned');
@ -161,6 +199,15 @@ class FindTest extends TestCase
$this->assertContains($doc->get()->id, ['two', 'four'], 'An incorrect document was returned'); $this->assertContains($doc->get()->id, ['two', 'four'], 'An incorrect document was returned');
} }
#[TestDox('firstByFields() succeeds when multiple ordered documents are found')]
public function testFirstByFieldsSucceedsWhenMultipleOrderedDocumentsAreFound(): void
{
$doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('sub.foo', 'green')], TestDocument::class,
orderBy: [Field::named('n:num_value DESC')]);
$this->assertTrue($doc->isSome(), 'There should have been a document returned');
$this->assertEquals('four', $doc->get()->id, 'The incorrect document was returned');
}
#[TestDox('firstByFields() succeeds when a document is not found')] #[TestDox('firstByFields() succeeds when a document is not found')]
public function testFirstByFieldsSucceedsWhenADocumentIsNotFound(): void public function testFirstByFieldsSucceedsWhenADocumentIsNotFound(): void
{ {

View File

@ -82,7 +82,7 @@ class FieldTest extends TestCase
} }
#[TestDox('path() succeeds for simple SQL path for PostgreSQL')] #[TestDox('path() succeeds for simple SQL path for PostgreSQL')]
public function testPathSucceedsForSimpleSqlPathForPostgreSQL() public function testPathSucceedsForSimpleSqlPathForPostgreSQL(): void
{ {
Configuration::overrideMode(Mode::PgSQL); Configuration::overrideMode(Mode::PgSQL);
try { try {
@ -94,7 +94,7 @@ class FieldTest extends TestCase
} }
#[TestDox('path() succeeds for simple SQL path for SQLite')] #[TestDox('path() succeeds for simple SQL path for SQLite')]
public function testPathSucceedsForSimpleSqlPathForSQLite() public function testPathSucceedsForSimpleSqlPathForSQLite(): void
{ {
Configuration::overrideMode(Mode::SQLite); Configuration::overrideMode(Mode::SQLite);
try { try {
@ -106,7 +106,7 @@ class FieldTest extends TestCase
} }
#[TestDox('path() succeeds for nested SQL path for PostgreSQL')] #[TestDox('path() succeeds for nested SQL path for PostgreSQL')]
public function testPathSucceedsForNestedSqlPathForPostgreSQL() public function testPathSucceedsForNestedSqlPathForPostgreSQL(): void
{ {
Configuration::overrideMode(Mode::PgSQL); Configuration::overrideMode(Mode::PgSQL);
try { try {
@ -118,7 +118,7 @@ class FieldTest extends TestCase
} }
#[TestDox('path() succeeds for nested SQL path for SQLite')] #[TestDox('path() succeeds for nested SQL path for SQLite')]
public function testPathSucceedsForNestedSqlPathForSQLite() public function testPathSucceedsForNestedSqlPathForSQLite(): void
{ {
Configuration::overrideMode(Mode::SQLite); Configuration::overrideMode(Mode::SQLite);
try { try {
@ -130,7 +130,7 @@ class FieldTest extends TestCase
} }
#[TestDox('path() succeeds for simple JSON path for PostgreSQL')] #[TestDox('path() succeeds for simple JSON path for PostgreSQL')]
public function testPathSucceedsForSimpleJsonPathForPostgreSQL() public function testPathSucceedsForSimpleJsonPathForPostgreSQL(): void
{ {
Configuration::overrideMode(Mode::PgSQL); Configuration::overrideMode(Mode::PgSQL);
try { try {
@ -142,7 +142,7 @@ class FieldTest extends TestCase
} }
#[TestDox('path() succeeds for simple JSON path for SQLite')] #[TestDox('path() succeeds for simple JSON path for SQLite')]
public function testPathSucceedsForSimpleJsonPathForSQLite() public function testPathSucceedsForSimpleJsonPathForSQLite(): void
{ {
Configuration::overrideMode(Mode::SQLite); Configuration::overrideMode(Mode::SQLite);
try { try {
@ -154,7 +154,7 @@ class FieldTest extends TestCase
} }
#[TestDox('path() succeeds for nested JSON path for PostgreSQL')] #[TestDox('path() succeeds for nested JSON path for PostgreSQL')]
public function testPathSucceedsForNestedJsonPathForPostgreSQL() public function testPathSucceedsForNestedJsonPathForPostgreSQL(): void
{ {
Configuration::overrideMode(Mode::PgSQL); Configuration::overrideMode(Mode::PgSQL);
try { try {
@ -166,7 +166,7 @@ class FieldTest extends TestCase
} }
#[TestDox('path() succeeds for nested JSON path for SQLite')] #[TestDox('path() succeeds for nested JSON path for SQLite')]
public function testPathSucceedsForNestedJsonPathForSQLite() public function testPathSucceedsForNestedJsonPathForSQLite(): void
{ {
Configuration::overrideMode(Mode::SQLite); Configuration::overrideMode(Mode::SQLite);
try { try {
@ -669,4 +669,15 @@ class FieldTest extends TestCase
$this->assertEquals('', $field->value, 'Value should have been blank'); $this->assertEquals('', $field->value, 'Value should have been blank');
$this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); $this->assertEquals('', $field->paramName, 'Parameter name should have been blank');
} }
#[TestDox('named() succeeds')]
public function testNamedSucceeds(): void
{
$field = Field::named('the_field');
$this->assertNotNull($field, 'The field should not have been null');
$this->assertEquals('the_field', $field->fieldName, 'Field name not filled correctly');
$this->assertEquals(Op::Equal, $field->op, 'Operation not filled correctly');
$this->assertEquals('', $field->value, 'Value should have been blank');
$this->assertEquals('', $field->paramName, 'Parameter name should have been blank');
}
} }

View File

@ -224,4 +224,81 @@ class QueryTest extends TestCase
$this->assertEquals("UPDATE testing SET data = :data WHERE data->>'id' = :id", Query::update('testing'), $this->assertEquals("UPDATE testing SET data = :data WHERE data->>'id' = :id", Query::update('testing'),
'UPDATE statement not constructed correctly'); 'UPDATE statement not constructed correctly');
} }
#[TestDox('orderBy() succeeds with no fields for PostgreSQL')]
public function testOrderBySucceedsWithNoFieldsForPostgreSQL(): void
{
Configuration::overrideMode(Mode::PgSQL);
$this->assertEquals('', Query::orderBy([]), 'ORDER BY should have been blank');
}
#[TestDox('orderBy() succeeds with no fields for SQLite')]
public function testOrderBySucceedsWithNoFieldsForSQLite(): void
{
$this->assertEquals('', Query::orderBy([]), 'ORDER BY should have been blank');
}
#[TestDox('orderBy() succeeds with one field and no direction for PostgreSQL')]
public function testOrderBySucceedsWithOneFieldAndNoDirectionForPostgreSQL(): void
{
Configuration::overrideMode(Mode::PgSQL);
$this->assertEquals(" ORDER BY data->>'TestField'", Query::orderBy([Field::named('TestField')]),
'ORDER BY not constructed correctly');
}
#[TestDox('orderBy() succeeds with one field and no direction for SQLite')]
public function testOrderBySucceedsWithOneFieldAndNoDirectionForSQLite(): void
{
$this->assertEquals(" ORDER BY data->>'TestField'", Query::orderBy([Field::named('TestField')]),
'ORDER BY not constructed correctly');
}
#[TestDox('orderBy() succeeds with multiple fields and direction for PostgreSQL')]
public function testOrderBySucceedsWithMultipleFieldsAndDirectionForPostgreSQL(): void
{
Configuration::overrideMode(Mode::PgSQL);
$this->assertEquals(" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC",
Query::orderBy(
[Field::named('Nested.Test.Field DESC'), Field::named('AnotherField'), Field::named('It DESC')]),
'ORDER BY not constructed correctly');
}
#[TestDox('orderBy() succeeds with multiple fields and direction for SQLite')]
public function testOrderBySucceedsWithMultipleFieldsAndDirectionForSQLite(): void
{
$this->assertEquals(" ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC",
Query::orderBy(
[Field::named('Nested.Test.Field DESC'), Field::named('AnotherField'), Field::named('It DESC')]),
'ORDER BY not constructed correctly');
}
#[TestDox('orderBy() succeeds with numeric field for PostgreSQL')]
public function testOrderBySucceedsWithNumericFieldForPostgreSQL(): void
{
Configuration::overrideMode(Mode::PgSQL);
$this->assertEquals(" ORDER BY (data->>'Test')::numeric", Query::orderBy([Field::named('n:Test')]),
'ORDER BY not constructed correctly');
}
#[TestDox('orderBy() succeeds with numeric field for SQLite')]
public function testOrderBySucceedsWithNumericFieldForSQLite(): void
{
$this->assertEquals(" ORDER BY data->>'Test'", Query::orderBy([Field::named('n:Test')]),
'ORDER BY not constructed correctly');
}
#[TestDox('orderBy() succeeds with case-insensitive ordering for PostgreSQL')]
public function testOrderBySucceedsWithCaseInsensitiveOrderingForPostgreSQL(): void
{
Configuration::overrideMode(Mode::PgSQL);
$this->assertEquals(" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST",
Query::orderBy([Field::named('i:Test.Field DESC NULLS FIRST')]), 'ORDER BY not constructed correctly');
}
#[TestDox('orderBy() succeeds with case-insensitive ordering for SQLite')]
public function testOrderBySucceedsWithCaseInsensitiveOrderingForSQLite(): void
{
$this->assertEquals(" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST",
Query::orderBy([Field::named('i:Test.Field ASC NULLS LAST')]), 'ORDER BY not constructed correctly');
}
} }