From d067f8983feccf22fa04dac8ab303d4e935b9731 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 27 Sep 2024 02:15:00 +0000 Subject: [PATCH] Changes for beta10 (#5) - Add In/InArray support - Add ORDER BY support for `Find` functions - Update dependencies - Implement fixes identified via static analysis Reviewed-on: https://git.bitbadger.solutions/bit-badger/pdo-document/pulls/5 --- composer.json | 3 +- composer.lock | 186 ++++--- src/Configuration.php | 2 +- src/Count.php | 10 +- src/Custom.php | 12 +- src/Definition.php | 2 +- src/Delete.php | 9 +- src/Document.php | 6 +- src/DocumentList.php | 23 +- src/Exists.php | 8 +- src/Field.php | 300 ++++++++-- src/Find.php | 62 ++- src/Mapper/ArrayMapper.php | 7 +- src/Mapper/CountMapper.php | 2 + src/Mapper/DocumentMapper.php | 2 +- src/Mapper/ExistsMapper.php | 2 + src/Mapper/Mapper.php | 2 +- src/Op.php | 44 +- src/Parameters.php | 20 +- src/Patch.php | 18 +- src/Query.php | 44 +- src/Query/Definition.php | 2 +- src/Query/RemoveFields.php | 12 +- src/RemoveFields.php | 18 +- tests/PjsonId.php | 7 +- tests/integration/ArrayDocument.php | 35 ++ tests/integration/postgresql/CountTest.php | 13 +- tests/integration/postgresql/CustomTest.php | 29 +- .../integration/postgresql/DefinitionTest.php | 4 + tests/integration/postgresql/DeleteTest.php | 16 +- .../postgresql/DocumentListTest.php | 8 + tests/integration/postgresql/DocumentTest.php | 55 +- tests/integration/postgresql/ExistsTest.php | 16 +- tests/integration/postgresql/FindTest.php | 166 +++++- tests/integration/postgresql/PatchTest.php | 18 +- .../postgresql/RemoveFieldsTest.php | 26 +- tests/integration/sqlite/CountTest.php | 10 +- tests/integration/sqlite/CustomTest.php | 29 +- tests/integration/sqlite/DefinitionTest.php | 3 + tests/integration/sqlite/DeleteTest.php | 13 +- tests/integration/sqlite/DocumentListTest.php | 9 + tests/integration/sqlite/DocumentTest.php | 55 +- tests/integration/sqlite/ExistsTest.php | 13 +- tests/integration/sqlite/FindTest.php | 116 +++- tests/integration/sqlite/PatchTest.php | 15 +- tests/integration/sqlite/RemoveFieldsTest.php | 20 +- tests/unit/ConfigurationTest.php | 12 +- tests/unit/DocumentExceptionTest.php | 2 + tests/unit/FieldMatchTest.php | 4 +- tests/unit/FieldTest.php | 524 +++++++++++++----- tests/unit/Mapper/ArrayMapperTest.php | 1 + tests/unit/Mapper/CountMapperTest.php | 1 + tests/unit/Mapper/DocumentMapperTest.php | 8 +- tests/unit/Mapper/ExistsMapperTest.php | 5 +- tests/unit/Mapper/StringMapperTest.php | 9 +- tests/unit/ModeTest.php | 6 +- tests/unit/OpTest.php | 64 ++- tests/unit/ParametersTest.php | 24 +- tests/unit/Query/CountTest.php | 12 +- tests/unit/Query/DefinitionTest.php | 12 +- tests/unit/Query/DeleteTest.php | 14 +- tests/unit/Query/ExistsTest.php | 14 +- tests/unit/Query/FindTest.php | 13 +- tests/unit/Query/PatchTest.php | 24 +- tests/unit/Query/RemoveFieldsTest.php | 36 +- tests/unit/QueryTest.php | 135 ++++- 66 files changed, 1728 insertions(+), 664 deletions(-) create mode 100644 tests/integration/ArrayDocument.php diff --git a/composer.json b/composer.json index 26ecd61..133d40b 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,8 @@ }, "require-dev": { "phpunit/phpunit": "^11", - "square/pjson": "^0.5.0" + "square/pjson": "^0.5.0", + "phpstan/phpstan": "^1.12" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 9943487..6331c2e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f3351c794dd1261ee4bf18380f58463d", + "content-hash": "7f9c6e825e6f0968b2dedefec770f558", "packages": [ { "name": "bit-badger/inspired-by-fsharp", @@ -53,16 +53,16 @@ }, { "name": "netresearch/jsonmapper", - "version": "v4.4.1", + "version": "v4.5.0", "source": { "type": "git", "url": "https://github.com/cweiske/jsonmapper.git", - "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0" + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/132c75c7dd83e45353ebb9c6c9f591952995bbf0", - "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8e76efb98ee8b6afc54687045e1b8dba55ac76e5", + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5", "shasum": "" }, "require": { @@ -98,9 +98,9 @@ "support": { "email": "cweiske@cweiske.de", "issues": "https://github.com/cweiske/jsonmapper/issues", - "source": "https://github.com/cweiske/jsonmapper/tree/v4.4.1" + "source": "https://github.com/cweiske/jsonmapper/tree/v4.5.0" }, - "time": "2024-01-31T06:18:54+00:00" + "time": "2024-09-08T10:13:13+00:00" } ], "packages-dev": [ @@ -166,16 +166,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.1.0", + "version": "v5.2.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" + "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", - "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", + "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", "shasum": "" }, "require": { @@ -218,9 +218,9 @@ ], "support": { "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", @@ -341,33 +341,91 @@ "time": "2022-02-21T01:04:05+00:00" }, { - "name": "phpunit/php-code-coverage", - "version": "11.0.5", + "name": "phpstan/phpstan", + "version": "1.12.5", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "19b6365ab8b59a64438c0c3f4241feeb480c9861" + "url": "https://github.com/phpstan/phpstan.git", + "reference": "7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/19b6365ab8b59a64438c0c3f4241feeb480c9861", - "reference": "19b6365ab8b59a64438c0c3f4241feeb480c9861", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17", + "reference": "7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2024-09-26T12:45:22+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "ebdffc9e09585dafa71b9bffcdb0a229d4704c45" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ebdffc9e09585dafa71b9bffcdb0a229d4704c45", + "reference": "ebdffc9e09585dafa71b9bffcdb0a229d4704c45", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.0", + "nikic/php-parser": "^5.1.0", "php": ">=8.2", - "phpunit/php-file-iterator": "^5.0", - "phpunit/php-text-template": "^4.0", - "sebastian/code-unit-reverse-lookup": "^4.0", - "sebastian/complexity": "^4.0", - "sebastian/environment": "^7.0", - "sebastian/lines-of-code": "^3.0", - "sebastian/version": "^5.0", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.0", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.1", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { "phpunit/phpunit": "^11.0" @@ -379,7 +437,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "11.0-dev" + "dev-main": "11.0.x-dev" } }, "autoload": { @@ -408,7 +466,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.5" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.6" }, "funding": [ { @@ -416,20 +474,20 @@ "type": "github" } ], - "time": "2024-07-03T05:05:37+00:00" + "time": "2024-08-22T04:37:56+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "5.0.1", + "version": "5.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "6ed896bf50bbbfe4d504a33ed5886278c78e4a26" + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6ed896bf50bbbfe4d504a33ed5886278c78e4a26", - "reference": "6ed896bf50bbbfe4d504a33ed5886278c78e4a26", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", "shasum": "" }, "require": { @@ -469,7 +527,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.0.1" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" }, "funding": [ { @@ -477,7 +535,7 @@ "type": "github" } ], - "time": "2024-07-03T05:06:37+00:00" + "time": "2024-08-27T05:02:59+00:00" }, { "name": "phpunit/php-invoker", @@ -665,16 +723,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.2.8", + "version": "11.3.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a7a29e8d3113806f18f99d670f580a30e8ffff39" + "reference": "d62c45a19c665bb872c2a47023a0baf41a98bb2b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a7a29e8d3113806f18f99d670f580a30e8ffff39", - "reference": "a7a29e8d3113806f18f99d670f580a30e8ffff39", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d62c45a19c665bb872c2a47023a0baf41a98bb2b", + "reference": "d62c45a19c665bb872c2a47023a0baf41a98bb2b", "shasum": "" }, "require": { @@ -688,20 +746,20 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.5", - "phpunit/php-file-iterator": "^5.0.1", + "phpunit/php-code-coverage": "^11.0.6", + "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", "sebastian/cli-parser": "^3.0.2", "sebastian/code-unit": "^3.0.1", - "sebastian/comparator": "^6.0.1", + "sebastian/comparator": "^6.1.0", "sebastian/diff": "^6.0.2", "sebastian/environment": "^7.2.0", "sebastian/exporter": "^6.1.3", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", - "sebastian/type": "^5.0.1", + "sebastian/type": "^5.1.0", "sebastian/version": "^5.0.1" }, "suggest": { @@ -713,7 +771,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "11.2-dev" + "dev-main": "11.3-dev" } }, "autoload": { @@ -745,7 +803,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.2.8" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.3.6" }, "funding": [ { @@ -761,7 +819,7 @@ "type": "tidelift" } ], - "time": "2024-07-18T14:56:37+00:00" + "time": "2024-09-19T10:54:28+00:00" }, { "name": "sebastian/cli-parser", @@ -935,16 +993,16 @@ }, { "name": "sebastian/comparator", - "version": "6.0.1", + "version": "6.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "131942b86d3587291067a94f295498ab6ac79c20" + "reference": "fa37b9e2ca618cb051d71b60120952ee8ca8b03d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/131942b86d3587291067a94f295498ab6ac79c20", - "reference": "131942b86d3587291067a94f295498ab6ac79c20", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa37b9e2ca618cb051d71b60120952ee8ca8b03d", + "reference": "fa37b9e2ca618cb051d71b60120952ee8ca8b03d", "shasum": "" }, "require": { @@ -955,12 +1013,12 @@ "sebastian/exporter": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "6.1-dev" } }, "autoload": { @@ -1000,7 +1058,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.1.0" }, "funding": [ { @@ -1008,7 +1066,7 @@ "type": "github" } ], - "time": "2024-07-03T04:48:07+00:00" + "time": "2024-09-11T15:42:56+00:00" }, { "name": "sebastian/complexity", @@ -1577,28 +1635,28 @@ }, { "name": "sebastian/type", - "version": "5.0.1", + "version": "5.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa" + "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb6a6566f9589e86661291d13eba708cce5eb4aa", - "reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac", + "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -1622,7 +1680,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "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": [ { @@ -1630,7 +1688,7 @@ "type": "github" } ], - "time": "2024-07-03T05:11:49+00:00" + "time": "2024-09-17T13:12:04+00:00" }, { "name": "sebastian/version", diff --git a/src/Configuration.php b/src/Configuration.php index 97bdc87..2c996f1 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -34,7 +34,7 @@ class Configuration /** @var string|null The password to use to establish a data connection (use env PDO_DOC_PASSWORD if possible) */ public static ?string $password = null; - /** @var array|null Options to use for connections (driver-specific) */ + /** @var mixed[]|null Options to use for connections (driver-specific) */ public static ?array $options = null; /** @var Option The mode in which the library is operating */ diff --git a/src/Count.php b/src/Count.php index 23cf2e5..44df8f6 100644 --- a/src/Count.php +++ b/src/Count.php @@ -31,23 +31,23 @@ class Count * Count matching documents using a comparison on JSON fields * * @param string $tableName The name of the table in which documents should be counted - * @param array|Field[] $fields The field comparison to match + * @param Field[] $fields The field comparison to match * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @return int The count of documents matching the field comparison * @throws DocumentException If one is encountered */ public static function byFields(string $tableName, array $fields, ?FieldMatch $match = null): int { - $namedFields = Parameters::nameFields($fields); - return Custom::scalar(Query\Count::byFields($tableName, $namedFields, $match), - Parameters::addFields($namedFields, []), new CountMapper()); + Parameters::nameFields($fields); + return Custom::scalar(Query\Count::byFields($tableName, $fields, $match), Parameters::addFields($fields, []), + new CountMapper()); } /** * Count matching documents using a JSON containment query (`@>`; PostgreSQL only) * * @param string $tableName The name of the table in which documents should be counted - * @param array|object $criteria The criteria for the JSON containment query + * @param mixed[]|object $criteria The criteria for the JSON containment query * @return int The number of documents matching the JSON containment query * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs */ diff --git a/src/Custom.php b/src/Custom.php index e01147c..d7f4fac 100644 --- a/src/Custom.php +++ b/src/Custom.php @@ -23,7 +23,7 @@ class Custom * Prepare a query for execution and run it * * @param string $query The query to be run - * @param array $parameters The parameters for the query + * @param array $parameters The parameters for the query * @return PDOStatement The result of executing the query * @throws DocumentException If the query execution is unsuccessful */ @@ -67,7 +67,7 @@ class Custom * * @template TDoc The domain type of the document to retrieve * @param string $query The query to be executed - * @param array $parameters Parameters to use in executing the query + * @param array $parameters Parameters to use in executing the query * @param Mapper $mapper Mapper to deserialize the result * @return DocumentList The items matching the query * @throws DocumentException If any is encountered @@ -82,7 +82,7 @@ class Custom * * @template TDoc The domain type of the document to retrieve * @param string $query The query to be executed - * @param array $parameters Parameters to use in executing the query + * @param array $parameters Parameters to use in executing the query * @param Mapper $mapper Mapper to deserialize the result * @return TDoc[] The items matching the query * @throws DocumentException If any is encountered @@ -97,7 +97,7 @@ class Custom * * @template TDoc The domain type of the document to retrieve * @param string $query The query to be executed (will have "LIMIT 1" appended) - * @param array $parameters Parameters to use in executing the query + * @param array $parameters Parameters to use in executing the query * @param Mapper $mapper Mapper to deserialize the result * @return Option A `Some` instance if the item is found, `None` otherwise * @throws DocumentException If any is encountered @@ -116,7 +116,7 @@ class Custom * Execute a query that does not return a value * * @param string $query The query to execute - * @param array $parameters Parameters to use in executing the query + * @param array $parameters Parameters to use in executing the query * @throws DocumentException If any is encountered */ public static function nonQuery(string $query, array $parameters): void @@ -133,7 +133,7 @@ class Custom * * @template T The scalar type to return * @param string $query The query to retrieve the value - * @param array $parameters Parameters to use in executing the query + * @param array $parameters Parameters to use in executing the query * @param Mapper $mapper The mapper to obtain the result * @return mixed|false|T The scalar value if found, false if not * @throws DocumentException If any is encountered diff --git a/src/Definition.php b/src/Definition.php index 72b975d..0839c6d 100644 --- a/src/Definition.php +++ b/src/Definition.php @@ -30,7 +30,7 @@ class Definition * * @param string $tableName The name of the table which should be indexed * @param string $indexName The name of the index - * @param array $fields Fields which should be a part of this index + * @param string[] $fields Fields which should be a part of this index * @throws DocumentException If any is encountered */ public static function ensureFieldIndex(string $tableName, string $indexName, array $fields): void diff --git a/src/Delete.php b/src/Delete.php index b5eba2d..554a5ba 100644 --- a/src/Delete.php +++ b/src/Delete.php @@ -29,22 +29,21 @@ class Delete * Delete documents by matching a comparison on JSON fields * * @param string $tableName The table from which documents should be deleted - * @param array|Field[] $fields The field comparison to match + * @param Field[] $fields The field comparison to match * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @throws DocumentException If any is encountered */ public static function byFields(string $tableName, array $fields, ?FieldMatch $match = null): void { - $namedFields = Parameters::nameFields($fields); - Custom::nonQuery(Query\Delete::byFields($tableName, $namedFields, $match), - Parameters::addFields($namedFields, [])); + Parameters::nameFields($fields); + Custom::nonQuery(Query\Delete::byFields($tableName, $fields, $match), Parameters::addFields($fields, [])); } /** * Delete documents matching a JSON containment query (`@>`; PostgreSQL only) * * @param string $tableName The table from which documents should be deleted - * @param array|object $criteria The JSON containment query values + * @param mixed[]|object $criteria The JSON containment query values * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs */ public static function byContains(string $tableName, array|object $criteria): void diff --git a/src/Document.php b/src/Document.php index 83c5ae2..e7c8ff7 100644 --- a/src/Document.php +++ b/src/Document.php @@ -17,7 +17,7 @@ class Document * Insert a new document * * @param string $tableName The name of the table into which the document should be inserted - * @param array|object $document The document to be inserted + * @param mixed[]|object $document The document to be inserted * @throws DocumentException If any is encountered */ public static function insert(string $tableName, array|object $document): void @@ -47,7 +47,7 @@ class Document * Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") * * @param string $tableName The name of the table to which the document should be saved - * @param array|object $document The document to be saved + * @param mixed[]|object $document The document to be saved * @throws DocumentException If any is encountered */ public static function save(string $tableName, array|object $document): void @@ -60,7 +60,7 @@ class Document * * @param string $tableName The table in which the document should be updated * @param mixed $docId The ID of the document to be updated - * @param array|object $document The document to be updated + * @param mixed[]|object $document The document to be updated * @throws DocumentException If any is encountered */ public static function update(string $tableName, mixed $docId, array|object $document): void diff --git a/src/DocumentList.php b/src/DocumentList.php index c72ea7f..d351e48 100644 --- a/src/DocumentList.php +++ b/src/DocumentList.php @@ -35,10 +35,12 @@ class DocumentList */ private function __construct(private ?PDOStatement &$result, private readonly Mapper $mapper) { - if ($row = $this->result->fetch(PDO::FETCH_ASSOC)) { - $this->first = $this->mapper->map($row); - } else { - $this->result = null; + if (!is_null($this->result)) { + if ($row = $this->result->fetch(PDO::FETCH_ASSOC)) { + $this->first = $this->mapper->map($row); + } else { + $this->result = null; + } } } @@ -67,6 +69,11 @@ class DocumentList $this->isConsumed = true; return; } + if (!$this->first) { + $this->isConsumed = true; + $this->result = null; + return; + } yield $this->first; while ($row = $this->result->fetch(PDO::FETCH_ASSOC)) { yield $this->mapper->map($row); @@ -133,14 +140,14 @@ class DocumentList * Construct a new document list * * @param string $query The query to run to retrieve results - * @param array $parameters An associative array of parameters for the query + * @param array $parameters An associative array of parameters for the query * @param Mapper $mapper A mapper to deserialize JSON documents - * @return static The document list instance + * @return self The document list instance * @throws DocumentException If any is encountered */ - public static function create(string $query, array $parameters, Mapper $mapper): static + public static function create(string $query, array $parameters, Mapper $mapper): self { $stmt = &Custom::runQuery($query, $parameters); - return new static($stmt, $mapper); + return new self($stmt, $mapper); } } diff --git a/src/Exists.php b/src/Exists.php index 3eac9c3..1b2d4c8 100644 --- a/src/Exists.php +++ b/src/Exists.php @@ -39,16 +39,16 @@ class Exists */ public static function byFields(string $tableName, array $fields, ?FieldMatch $match = null): bool { - $namedFields = Parameters::nameFields($fields); - return Custom::scalar(Query\Exists::byFields($tableName, $namedFields, $match), - Parameters::addFields($namedFields, []), new ExistsMapper()); + Parameters::nameFields($fields); + return Custom::scalar(Query\Exists::byFields($tableName, $fields, $match), Parameters::addFields($fields, []), + new ExistsMapper()); } /** * Determine if documents exist by a JSON containment query (`@>`; PostgreSQL only) * * @param string $tableName The name of the table in which document existence should be determined - * @param array|object $criteria The criteria for the JSON containment query + * @param mixed[]|object $criteria The criteria for the JSON containment query * @return bool True if any documents match the JSON containment query, false if not * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs */ diff --git a/src/Field.php b/src/Field.php index b8accb2..cecaf40 100644 --- a/src/Field.php +++ b/src/Field.php @@ -27,31 +27,67 @@ class Field * @param string $paramName The name of the parameter to which this should be bound * @param string $qualifier A table qualifier for the `data` column */ - public function __construct(public string $fieldName = '', public Op $op = Op::EQ, public mixed $value = '', + public function __construct(public string $fieldName = '', public Op $op = Op::Equal, public mixed $value = '', public string $paramName = '', public string $qualifier = '') { } /** * Append the parameter name and value to the given associative array * - * @param array $existing The existing parameters - * @return array The given parameter array with this field's name and value appended + * @param array $existing The existing parameters + * @return array The given parameter array with this field's name and value(s) appended */ public function appendParameter(array $existing): array { switch ($this->op) { - case Op::EX: - case Op::NEX: + case Op::Exists: + case Op::NotExists: break; - case Op::BT: + case Op::Between: $existing["{$this->paramName}min"] = $this->value[0]; $existing["{$this->paramName}max"] = $this->value[1]; break; + case Op::In: + for ($idx = 0; $idx < count($this->value); $idx++) { + $existing["{$this->paramName}_$idx"] = $this->value[$idx]; + } + break; + case Op::InArray: + $mkString = Configuration::mode("Append parameters for InArray condition") === Mode::PgSQL; + $values = $this->value['values']; + for ($idx = 0; $idx < count($values); $idx++) { + $existing["{$this->paramName}_$idx"] = $mkString ? "$values[$idx]" : $values[$idx]; + } + break; default: $existing[$this->paramName] = $this->value; } return $existing; } + /** + * Derive the path for this field + * + * @param bool $asJSON Whether the field should be treated as JSON in the query (optional, default false) + * @return string The path for this field + * @throws Exception If the database mode has not been set + */ + public function path(bool $asJSON = false): string + { + $extra = $asJSON ? '' : '>'; + if (str_contains($this->fieldName, '.')) { + $mode = Configuration::mode('determine field path'); + if ($mode === Mode::PgSQL) { + return "data#>$extra'{" . implode(',', explode('.', $this->fieldName)) . "}'"; + } + if ($mode === Mode::SQLite) { + $parts = explode('.', $this->fieldName); + $last = array_pop($parts); + return "data->'" . implode("'->'", $parts) . "'->$extra'$last'"; + } + } + return "data->$extra'$this->fieldName'"; + } + /** * Get the WHERE clause fragment for this parameter * @@ -60,26 +96,41 @@ class Field */ public function toWhere(): string { - $mode = Configuration::mode('make field WHERE clause'); - $fieldName = (empty($this->qualifier) ? '' : "$this->qualifier.") . 'data' . match (true) { - !str_contains($this->fieldName, '.') => "->>'$this->fieldName'", - $mode === Mode::PgSQL => "#>>'{" . implode(',', explode('.', $this->fieldName)) . "}'", - $mode === Mode::SQLite => "->>'" . implode("'->>'", explode('.', $this->fieldName)) . "'", - }; - $fieldPath = match ($mode) { + $mode = Configuration::mode('make field WHERE clause'); + $fieldName = (empty($this->qualifier) ? '' : "$this->qualifier.") . $this->path($this->op === Op::InArray); + $fieldPath = match ($mode) { Mode::PgSQL => match (true) { - $this->op === Op::BT => is_numeric($this->value[0]) ? "($fieldName)::numeric" : $fieldName, - is_numeric($this->value) => "($fieldName)::numeric", - default => $fieldName, + $this->op === Op::Between, + $this->op === Op::In => is_numeric($this->value[0]) ? "($fieldName)::numeric" : $fieldName, + is_numeric($this->value) => "($fieldName)::numeric", + default => $fieldName, }, default => $fieldName, }; $criteria = match ($this->op) { - Op::EX, Op::NEX => '', - Op::BT => " {$this->paramName}min AND {$this->paramName}max", - default => " $this->paramName", + Op::Exists, + Op::NotExists => '', + Op::Between => " {$this->paramName}min AND {$this->paramName}max", + Op::In => ' (' . $this->inParameterNames() . ')', + Op::InArray => $mode === Mode::PgSQL ? ' ARRAY[' . $this->inParameterNames() . ']' : '', + default => " $this->paramName", }; - return $fieldPath . ' ' . $this->op->toSQL() . $criteria; + return $mode === Mode::SQLite && $this->op === Op::InArray + ? "EXISTS (SELECT 1 FROM json_each({$this->value['table']}.data, '\$.$this->fieldName') WHERE value IN (" + . $this->inParameterNames() . '))' + : $fieldPath . ' ' . $this->op->toSQL() . $criteria; + } + + /** + * Create parameter names for an IN clause + * + * @return string A comma-delimited string of parameter names + */ + private function inParameterNames(): string + { + $values = $this->op === Op::In ? $this->value : $this->value['values']; + return implode(', ', + array_map(fn($value, $key) => "{$this->paramName}_$key", $values, range(0, count($values) - 1))); } /** @@ -88,11 +139,24 @@ class Field * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for which equality will be checked * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) - * @return static The field with the requested criterion + * @return self The field with the requested criterion */ - public static function EQ(string $fieldName, mixed $value, string $paramName = ''): static + public static function equal(string $fieldName, mixed $value, string $paramName = ''): self { - return new static($fieldName, Op::EQ, $value, $paramName); + return new self($fieldName, Op::Equal, $value, $paramName); + } + + /** + * Create an equals (=) field criterion _(alias for `Field.equal`)_ + * + * @param string $fieldName The name of the field against which the value will be compared + * @param mixed $value The value for which equality will be checked + * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) + * @return self The field with the requested criterion + */ + public static function EQ(string $fieldName, mixed $value, string $paramName = ''): self + { + return self::equal($fieldName, $value, $paramName); } /** @@ -101,11 +165,24 @@ class Field * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for the greater than comparison * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) - * @return static The field with the requested criterion + * @return self The field with the requested criterion */ - public static function GT(string $fieldName, mixed $value, string $paramName = ''): static + public static function greater(string $fieldName, mixed $value, string $paramName = ''): self { - return new static($fieldName, Op::GT, $value, $paramName); + return new self($fieldName, Op::Greater, $value, $paramName); + } + + /** + * Create a greater than (>) field criterion _(alias for `Field.greater`)_ + * + * @param string $fieldName The name of the field against which the value will be compared + * @param mixed $value The value for the greater than comparison + * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) + * @return self The field with the requested criterion + */ + public static function GT(string $fieldName, mixed $value, string $paramName = ''): self + { + return self::greater($fieldName, $value, $paramName); } /** @@ -114,11 +191,24 @@ class Field * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for the greater than or equal to comparison * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) - * @return static The field with the requested criterion + * @return self The field with the requested criterion */ - public static function GE(string $fieldName, mixed $value, string $paramName = ''): static + public static function greaterOrEqual(string $fieldName, mixed $value, string $paramName = ''): self { - return new static($fieldName, Op::GE, $value, $paramName); + return new self($fieldName, Op::GreaterOrEqual, $value, $paramName); + } + + /** + * Create a greater than or equal to (>=) field criterion _(alias for `Field.greaterOrEqual`)_ + * + * @param string $fieldName The name of the field against which the value will be compared + * @param mixed $value The value for the greater than or equal to comparison + * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) + * @return self The field with the requested criterion + */ + public static function GE(string $fieldName, mixed $value, string $paramName = ''): self + { + return self::greaterOrEqual($fieldName, $value, $paramName); } /** @@ -127,11 +217,24 @@ class Field * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for the less than comparison * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) - * @return static The field with the requested criterion + * @return self The field with the requested criterion */ - public static function LT(string $fieldName, mixed $value, string $paramName = ''): static + public static function less(string $fieldName, mixed $value, string $paramName = ''): self { - return new static($fieldName, Op::LT, $value, $paramName); + return new self($fieldName, Op::Less, $value, $paramName); + } + + /** + * Create a less than (<) field criterion _(alias for `Field.less`)_ + * + * @param string $fieldName The name of the field against which the value will be compared + * @param mixed $value The value for the less than comparison + * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) + * @return self The field with the requested criterion + */ + public static function LT(string $fieldName, mixed $value, string $paramName = ''): self + { + return self::less($fieldName, $value, $paramName); } /** @@ -140,11 +243,24 @@ class Field * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for the less than or equal to comparison * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) - * @return static The field with the requested criterion + * @return self The field with the requested criterion */ - public static function LE(string $fieldName, mixed $value, string $paramName = ''): static + public static function lessOrEqual(string $fieldName, mixed $value, string $paramName = ''): self { - return new static($fieldName, Op::LE, $value, $paramName); + return new self($fieldName, Op::LessOrEqual, $value, $paramName); + } + + /** + * Create a less than or equal to (<=) field criterion _(alias for `Field.lessOrEqual`)_ + * + * @param string $fieldName The name of the field against which the value will be compared + * @param mixed $value The value for the less than or equal to comparison + * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) + * @return self The field with the requested criterion + */ + public static function LE(string $fieldName, mixed $value, string $paramName = ''): self + { + return self::lessOrEqual($fieldName, $value, $paramName); } /** @@ -153,11 +269,24 @@ class Field * @param string $fieldName The name of the field against which the value will be compared * @param mixed $value The value for the not equals comparison * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) - * @return static The field with the requested criterion + * @return self The field with the requested criterion */ - public static function NE(string $fieldName, mixed $value, string $paramName = ''): static + public static function notEqual(string $fieldName, mixed $value, string $paramName = ''): self { - return new static($fieldName, Op::NE, $value, $paramName); + return new self($fieldName, Op::NotEqual, $value, $paramName); + } + + /** + * Create a not equals (<>) field criterion _(alias for `Field.notEqual`)_ + * + * @param string $fieldName The name of the field against which the value will be compared + * @param mixed $value The value for the not equals comparison + * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) + * @return self The field with the requested criterion + */ + public static function NE(string $fieldName, mixed $value, string $paramName = ''): self + { + return self::notEqual($fieldName, $value, $paramName); } /** @@ -167,32 +296,109 @@ class Field * @param mixed $minValue The lower value for range * @param mixed $maxValue The upper value for the range * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) - * @return static The field with the requested criterion + * @return self The field with the requested criterion */ - public static function BT(string $fieldName, mixed $minValue, mixed $maxValue, string $paramName = ''): static + public static function between(string $fieldName, mixed $minValue, mixed $maxValue, string $paramName = ''): self { - return new static($fieldName, Op::BT, [$minValue, $maxValue], $paramName); + return new self($fieldName, Op::Between, [$minValue, $maxValue], $paramName); + } + + /** + * Create a BETWEEN field criterion _(alias for `Field.between`)_ + * + * @param string $fieldName The name of the field against which the value will be compared + * @param mixed $minValue The lower value for range + * @param mixed $maxValue The upper value for the range + * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) + * @return self The field with the requested criterion + */ + public static function BT(string $fieldName, mixed $minValue, mixed $maxValue, string $paramName = ''): self + { + return self::between($fieldName, $minValue, $maxValue, $paramName); + } + + /** + * Create an IN field criterion + * + * @param string $fieldName The name of the field against which the values will be compared + * @param mixed[] $values The potential matching values for the field + * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) + * @return self The field with the requested criterion + */ + public static function in(string $fieldName, array $values, string $paramName = ''): self + { + return new self($fieldName, Op::In, $values, $paramName); + } + + /** + * Create an IN ARRAY field criterion + * + * @param string $fieldName The name of the field against which the values will be compared + * @param string $tableName The table name where this field is located + * @param mixed[] $values The potential matching values for the field + * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) + * @return self The field with the requested criterion + */ + public static function inArray(string $fieldName, string $tableName, array $values, string $paramName = ''): self + { + return new self($fieldName, Op::InArray, ['table' => $tableName, 'values' => $values], $paramName); } /** * Create an exists (IS NOT NULL) field criterion * * @param string $fieldName The name of the field for which existence will be checked - * @return static The field with the requested criterion + * @return self The field with the requested criterion */ - public static function EX(string $fieldName): static + public static function exists(string $fieldName): self { - return new static($fieldName, Op::EX, '', ''); + return new self($fieldName, Op::Exists, '', ''); + } + + /** + * Create an exists (IS NOT NULL) field criterion _(alias for `Field.exists`)_ + * + * @param string $fieldName The name of the field for which existence will be checked + * @return self The field with the requested criterion + */ + public static function EX(string $fieldName): self + { + return self::exists($fieldName); } /** * Create a not exists (IS NULL) field criterion * * @param string $fieldName The name of the field for which non-existence will be checked - * @return static The field with the requested criterion + * @return self The field with the requested criterion */ - public static function NEX(string $fieldName): static + public static function notExists(string $fieldName): self { - return new static($fieldName, Op::NEX, '', ''); + return new self($fieldName, Op::NotExists, '', ''); + } + + /** + * Create a not exists (IS NULL) field criterion _(alias for `Field.notExists`)_ + * + * @param string $fieldName The name of the field for which non-existence will be checked + * @return self The field with the requested criterion + */ + public static function NEX(string $fieldName): self + { + 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, '', ''); } } diff --git a/src/Find.php b/src/Find.php index c39c414..257662d 100644 --- a/src/Find.php +++ b/src/Find.php @@ -22,12 +22,14 @@ class Find * @template TDoc The type of document to be retrieved * @param string $tableName The table from which documents should be retrieved * @param class-string $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 A list of all documents from the table * @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)); } /** @@ -51,18 +53,19 @@ class Find * * @template TDoc The type of document to be retrieved * @param string $tableName The table from which documents should be retrieved - * @param array|Field[] $fields The field comparison to match + * @param Field[] $fields The field comparison to match * @param class-string $className The name of the class to be retrieved * @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 A list of documents matching the given field comparison * @throws DocumentException If any is encountered */ public static function byFields(string $tableName, array $fields, string $className, - ?FieldMatch $match = null): DocumentList + ?FieldMatch $match = null, array $orderBy = []): DocumentList { - $namedFields = Parameters::nameFields($fields); - return Custom::list(Query\Find::byFields($tableName, $namedFields, $match), - Parameters::addFields($namedFields, []), new DocumentMapper($className)); + Parameters::nameFields($fields); + return Custom::list(Query\Find::byFields($tableName, $fields, $match) . Query::orderBy($orderBy), + Parameters::addFields($fields, []), new DocumentMapper($className)); } /** @@ -70,15 +73,17 @@ class Find * * @template TDoc The type of document to be retrieved * @param string $tableName The name of the table from which documents should be retrieved - * @param array|object $criteria The criteria for the JSON containment query + * @param mixed[]|object $criteria The criteria for the JSON containment query * @param class-string $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 A list of documents matching the JSON containment query * @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), - new DocumentMapper($className)); + return Custom::list(Query\Find::byContains($tableName) . Query::orderBy($orderBy), + 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 $path The JSON Path match string * @param class-string $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 A list of documents matching the JSON Path * @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)); } /** @@ -101,18 +109,19 @@ class Find * * @template TDoc The type of document to be retrieved * @param string $tableName The table from which the document should be retrieved - * @param array|Field[] $fields The field comparison to match + * @param Field[] $fields The field comparison to match * @param class-string $className The name of the class to be retrieved * @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 A `Some` instance with the first document if any matches are found, `None` otherwise * @throws DocumentException If any is encountered */ public static function firstByFields(string $tableName, array $fields, string $className, - ?FieldMatch $match = null): Option + ?FieldMatch $match = null, array $orderBy = []): Option { - $namedFields = Parameters::nameFields($fields); - return Custom::single(Query\Find::byFields($tableName, $namedFields, $match), - Parameters::addFields($namedFields, []), new DocumentMapper($className)); + Parameters::nameFields($fields); + return Custom::single(Query\Find::byFields($tableName, $fields, $match) . Query::orderBy($orderBy), + Parameters::addFields($fields, []), new DocumentMapper($className)); } /** @@ -120,15 +129,17 @@ class Find * * @template TDoc The type of document to be retrieved * @param string $tableName The name of the table from which documents should be retrieved - * @param array|object $criteria The criteria for the JSON containment query + * @param mixed[]|object $criteria The criteria for the JSON containment query * @param class-string $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 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 */ - 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), - new DocumentMapper($className)); + return Custom::single(Query\Find::byContains($tableName) . Query::orderBy($orderBy), + 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 $path The JSON Path match string * @param class-string $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 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 */ - 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)); } } diff --git a/src/Mapper/ArrayMapper.php b/src/Mapper/ArrayMapper.php index baaf9c1..29d4be1 100644 --- a/src/Mapper/ArrayMapper.php +++ b/src/Mapper/ArrayMapper.php @@ -10,10 +10,15 @@ namespace BitBadger\PDODocument\Mapper; /** * A mapper that returns the associative array from the database + * + * @implements Mapper> */ class ArrayMapper implements Mapper { - /** @inheritDoc */ + /** + * @inheritDoc + * @return array The array given as the parameter + */ public function map(array $result): array { return $result; diff --git a/src/Mapper/CountMapper.php b/src/Mapper/CountMapper.php index 91695d3..7a60019 100644 --- a/src/Mapper/CountMapper.php +++ b/src/Mapper/CountMapper.php @@ -10,6 +10,8 @@ namespace BitBadger\PDODocument\Mapper; /** * A mapper that returns the integer value of the first item in the results + * + * @implements Mapper */ class CountMapper implements Mapper { diff --git a/src/Mapper/DocumentMapper.php b/src/Mapper/DocumentMapper.php index 9bf4859..80e34ef 100644 --- a/src/Mapper/DocumentMapper.php +++ b/src/Mapper/DocumentMapper.php @@ -31,7 +31,7 @@ class DocumentMapper implements Mapper /** * Map a result to a domain class instance * - * @param array $result An associative array representing a single database result + * @param array $result An associative array representing a single database result * @return TDoc The document, deserialized from its JSON representation * @throws DocumentException If the JSON cannot be deserialized */ diff --git a/src/Mapper/ExistsMapper.php b/src/Mapper/ExistsMapper.php index 56f6a26..42aee1f 100644 --- a/src/Mapper/ExistsMapper.php +++ b/src/Mapper/ExistsMapper.php @@ -13,6 +13,8 @@ use Exception; /** * Map an EXISTS result to a boolean value + * + * @implements Mapper */ class ExistsMapper implements Mapper { diff --git a/src/Mapper/Mapper.php b/src/Mapper/Mapper.php index d0af9ca..6c93de1 100644 --- a/src/Mapper/Mapper.php +++ b/src/Mapper/Mapper.php @@ -18,7 +18,7 @@ interface Mapper /** * Map a result to the specified type * - * @param array $result An associative array representing a single database result + * @param array $result An associative array representing a single database result * @return T The item mapped from the given result */ public function map(array $result): mixed; diff --git a/src/Op.php b/src/Op.php index 1da71ae..97c1c8e 100644 --- a/src/Op.php +++ b/src/Op.php @@ -9,28 +9,32 @@ declare(strict_types=1); namespace BitBadger\PDODocument; /** - * The types of logical operations allowed for JSON fields + * The types of comparison operators allowed for JSON fields */ enum Op { /** Equals (=) */ - case EQ; + case Equal; /** Greater Than (>) */ - case GT; + case Greater; /** Greater Than or Equal To (>=) */ - case GE; + case GreaterOrEqual; /** Less Than (<) */ - case LT; + case Less; /** Less Than or Equal To (<=) */ - case LE; + case LessOrEqual; /** Not Equal to (<>) */ - case NE; + case NotEqual; /** Between (BETWEEN) */ - case BT; + case Between; + /** In (IN) */ + case In; + /** In Array (PostgreSQL - ?|, SQLite - EXISTS / json_each / IN) */ + case InArray; /** Exists (IS NOT NULL) */ - case EX; + case Exists; /** Does Not Exist (IS NULL) */ - case NEX; + case NotExists; /** * Get the SQL representation of this operator @@ -40,15 +44,17 @@ enum Op public function toSQL(): string { return match ($this) { - Op::EQ => "=", - Op::GT => ">", - Op::GE => ">=", - Op::LT => "<", - Op::LE => "<=", - Op::NE => "<>", - Op::BT => "BETWEEN", - Op::EX => "IS NOT NULL", - Op::NEX => "IS NULL", + Op::Equal => "=", + Op::Greater => ">", + Op::GreaterOrEqual => ">=", + Op::Less => "<", + Op::LessOrEqual => "<=", + Op::NotEqual => "<>", + Op::Between => "BETWEEN", + Op::In => "IN", + Op::InArray => "??|", // The actual operator is ?|, but needs to be escaped by doubling + Op::Exists => "IS NOT NULL", + Op::NotExists => "IS NULL", }; } } diff --git a/src/Parameters.php b/src/Parameters.php index 6dc21e9..5ffa4f6 100644 --- a/src/Parameters.php +++ b/src/Parameters.php @@ -19,19 +19,19 @@ class Parameters * Create an ID parameter (name ":id", key will be treated as a string) * * @param mixed $key The key representing the ID of the document - * @return array|string[] An associative array with an "@id" parameter/value pair + * @return array An associative array with an "@id" parameter/value pair */ public static function id(mixed $key): array { - return [':id' => is_int($key) || is_string($key) ? $key : "$key"]; + return [':id' => ((is_int($key) || is_string($key)) ? $key : "$key")]; } /** * Create a parameter with a JSON value * * @param string $name The name of the JSON parameter - * @param object|array $document The value that should be passed as a JSON string - * @return array An associative array with the named parameter/value pair + * @param mixed[]|object $document The value that should be passed as a JSON string + * @return array An associative array with the named parameter/value pair */ public static function json(string $name, object|array $document): array { @@ -59,23 +59,21 @@ class Parameters /** * Fill in parameter names for any fields missing one * - * @param Field[] $fields The fields for the query - * @return Field[] The fields, all with non-blank parameter names + * @param Field[] $fields The fields for the query (entries with no names will be modified) */ - public static function nameFields(array $fields): array + public static function nameFields(array &$fields): void { array_walk($fields, function (Field $field, int $idx) { if (empty($field->paramName)) $field->paramName =":field$idx"; }); - return $fields; } /** * Add field parameters to the given set of parameters * * @param Field[] $fields The fields being compared in the query - * @param array $parameters An associative array of parameters to which the fields should be added - * @return array An associative array of parameter names and values with the fields added + * @param array $parameters An associative array of parameters to which the fields should be added + * @return array An associative array of parameter names and values with the fields added */ public static function addFields(array $fields, array $parameters): array { @@ -87,7 +85,7 @@ class Parameters * * @param string $paramName The name of the parameter for the field names * @param string[] $fieldNames The names of the fields for the parameter - * @return array An associative array of parameter/value pairs for the field names + * @return array An associative array of parameter/value pairs for the field names * @throws Exception If the database mode has not been set */ public static function fieldNames(string $paramName, array $fieldNames): array diff --git a/src/Patch.php b/src/Patch.php index 6e862a8..ab918e9 100644 --- a/src/Patch.php +++ b/src/Patch.php @@ -18,7 +18,7 @@ class Patch * * @param string $tableName The table in which the document should be patched * @param mixed $docId The ID of the document to be patched - * @param array|object $patch The object with which the document should be patched (will be JSON-encoded) + * @param mixed[]|object $patch The object with which the document should be patched (will be JSON-encoded) * @throws DocumentException If any is encountered (database mode must be set) */ public static function byId(string $tableName, mixed $docId, array|object $patch): void @@ -31,25 +31,25 @@ class Patch * Patch documents using a comparison on JSON fields * * @param string $tableName The table in which documents should be patched - * @param array|Field[] $fields The field comparison to match - * @param array|object $patch The object with which the documents should be patched (will be JSON-encoded) + * @param Field[] $fields The field comparison to match + * @param mixed[]|object $patch The object with which the documents should be patched (will be JSON-encoded) * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @throws DocumentException If any is encountered */ public static function byFields(string $tableName, array $fields, array|object $patch, ?FieldMatch $match = null): void { - $namedFields = Parameters::nameFields($fields); - Custom::nonQuery(Query\Patch::byFields($tableName, $namedFields, $match), - Parameters::addFields($namedFields, Parameters::json(':data', $patch))); + Parameters::nameFields($fields); + Custom::nonQuery(Query\Patch::byFields($tableName, $fields, $match), + Parameters::addFields($fields, Parameters::json(':data', $patch))); } /** * Patch documents using a JSON containment query (`@>`; PostgreSQL only) * * @param string $tableName The table in which documents should be patched - * @param array|object $criteria The JSON containment query values to match - * @param array|object $patch The object with which the documents should be patched (will be JSON-encoded) + * @param mixed[]|object $criteria The JSON containment query values to match + * @param mixed[]|object $patch The object with which the documents should be patched (will be JSON-encoded) * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs */ public static function byContains(string $tableName, array|object $criteria, array|object $patch): void @@ -63,7 +63,7 @@ class Patch * * @param string $tableName The table in which documents should be patched * @param string $path The JSON Path match string - * @param array|object $patch The object with which the documents should be patched (will be JSON-encoded) + * @param mixed[]|object $patch The object with which the documents should be patched (will be JSON-encoded) * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs */ public static function byJsonPath(string $tableName, string $path, array|object $patch): void diff --git a/src/Query.php b/src/Query.php index f6be31f..ebe6211 100644 --- a/src/Query.php +++ b/src/Query.php @@ -51,7 +51,7 @@ class Query */ public static function whereById(string $paramName = ':id', mixed $docId = null): string { - return self::whereByFields([Field::EQ(Configuration::$idField, $docId ?? '', $paramName)]); + return self::whereByFields([Field::equal(Configuration::$idField, $docId ?? '', $paramName)]); } /** @@ -142,4 +142,46 @@ class Query { 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); + } } diff --git a/src/Query/Definition.php b/src/Query/Definition.php index 1f1f953..b221ad1 100644 --- a/src/Query/Definition.php +++ b/src/Query/Definition.php @@ -49,7 +49,7 @@ class Definition * * @param string $tableName The name of the table which should be indexed * @param string $indexName The name of the index to create - * @param array $fields An array of fields to be indexed; may contain direction (ex. 'salary DESC') + * @param string[] $fields An array of fields to be indexed; may contain direction (ex. 'salary DESC') * @return string The CREATE INDEX statement to ensure the index exists */ public static function ensureIndexOn(string $tableName, string $indexName, array $fields): string diff --git a/src/Query/RemoveFields.php b/src/Query/RemoveFields.php index 16bded7..b167c71 100644 --- a/src/Query/RemoveFields.php +++ b/src/Query/RemoveFields.php @@ -24,7 +24,7 @@ class RemoveFields * Create an UPDATE statement to remove fields from a JSON document * * @param string $tableName The name of the table in which documents should be manipulated - * @param array $parameters The parameter list for the query + * @param array $parameters The parameter list for the query * @param string $whereClause The body of the WHERE clause for the update * @return string The UPDATE statement to remove fields from a JSON document * @throws Exception If the database mode has not been set @@ -43,7 +43,7 @@ class RemoveFields * Query to remove fields from a document by the document's ID * * @param string $tableName The name of the table in which the document should be manipulated - * @param array $parameters The parameter list for the query + * @param array $parameters The parameter list for the query * @param mixed $docId The ID of the document from which fields should be removed (optional; string ID assumed) * @return string The UPDATE statement to remove fields from a document by its ID * @throws DocumentException If the database mode has not been set @@ -57,8 +57,8 @@ class RemoveFields * Query to remove fields from documents via a comparison on JSON fields within the document * * @param string $tableName The name of the table in which documents should be manipulated - * @param array|Field[] $fields The field comparison to match - * @param array $parameters The parameter list for the query + * @param Field[] $fields The field comparison to match + * @param array $parameters The parameter list for the query * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @return string The UPDATE statement to remove fields from documents via field comparison * @throws DocumentException If the database mode has not been set @@ -73,7 +73,7 @@ class RemoveFields * Query to remove fields from documents via a JSON containment query (PostgreSQL only) * * @param string $tableName The name of the table in which documents should be manipulated - * @param array $parameters The parameter list for the query + * @param array $parameters The parameter list for the query * @return string The UPDATE statement to remove fields from documents via a JSON containment query * @throws DocumentException If the database mode is not PostgreSQL */ @@ -86,7 +86,7 @@ class RemoveFields * Query to remove fields from documents via a JSON Path match query (PostgreSQL only) * * @param string $tableName The name of the table in which documents should be manipulated - * @param array $parameters The parameter list for the query + * @param array $parameters The parameter list for the query * @return string The UPDATE statement to remove fields from documents via a JSON Path match * @throws DocumentException */ diff --git a/src/RemoveFields.php b/src/RemoveFields.php index d719937..38b0257 100644 --- a/src/RemoveFields.php +++ b/src/RemoveFields.php @@ -18,7 +18,7 @@ class RemoveFields * * @param string $tableName The table in which the document should have fields removed * @param mixed $docId The ID of the document from which fields should be removed - * @param array|string[] $fieldNames The names of the fields to be removed + * @param string[] $fieldNames The names of the fields to be removed * @throws DocumentException If any is encountered */ public static function byId(string $tableName, mixed $docId, array $fieldNames): void @@ -32,8 +32,8 @@ class RemoveFields * Remove fields from documents via a comparison on a JSON field in the document * * @param string $tableName The table in which documents should have fields removed - * @param array|Field[] $fields The field comparison to match - * @param array|string[] $fieldNames The names of the fields to be removed + * @param Field[] $fields The field comparison to match + * @param string[] $fieldNames The names of the fields to be removed * @param FieldMatch|null $match How to handle multiple conditions (optional; defaults to All) * @throws DocumentException If any is encountered */ @@ -41,17 +41,17 @@ class RemoveFields ?FieldMatch $match = null): void { $nameParams = Parameters::fieldNames(':name', $fieldNames); - $namedFields = Parameters::nameFields($fields); - Custom::nonQuery(Query\RemoveFields::byFields($tableName, $namedFields, $nameParams, $match), - Parameters::addFields($namedFields, $nameParams)); + Parameters::nameFields($fields); + Custom::nonQuery(Query\RemoveFields::byFields($tableName, $fields, $nameParams, $match), + Parameters::addFields($fields, $nameParams)); } /** * Remove fields from documents via a JSON containment query (`@>`; PostgreSQL only) * * @param string $tableName The table in which documents should have fields removed - * @param array|object $criteria The JSON containment query values - * @param array|string[] $fieldNames The names of the fields to be removed + * @param mixed[]|object $criteria The JSON containment query values + * @param string[] $fieldNames The names of the fields to be removed * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs */ public static function byContains(string $tableName, array|object $criteria, array $fieldNames): void @@ -66,7 +66,7 @@ class RemoveFields * * @param string $tableName The table in which documents should have fields removed * @param string $path The JSON Path match string - * @param array|string[] $fieldNames The names of the fields to be removed + * @param string[] $fieldNames The names of the fields to be removed * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs */ public static function byJsonPath(string $tableName, string $path, array $fieldNames): void diff --git a/tests/PjsonId.php b/tests/PjsonId.php index 0e5de4e..6e9430b 100644 --- a/tests/PjsonId.php +++ b/tests/PjsonId.php @@ -13,7 +13,7 @@ use Square\Pjson\JsonDataSerializable; /** * A serializable ID wrapper class */ -class PjsonId implements JsonDataSerializable +final class PjsonId implements JsonDataSerializable { public function __construct(protected string $value) { } @@ -22,6 +22,11 @@ class PjsonId implements JsonDataSerializable return $this->value; } + /** + * @param mixed $jd JSON data + * @param mixed[]|string $path path segments + * @return static + */ public static function fromJsonData($jd, array|string $path = []): static { return new static($jd); diff --git a/tests/integration/ArrayDocument.php b/tests/integration/ArrayDocument.php new file mode 100644 index 0000000..59e5320 --- /dev/null +++ b/tests/integration/ArrayDocument.php @@ -0,0 +1,35 @@ + + * @license MIT + */ + +declare(strict_types=1); + +namespace Test\Integration; + +/** + * A document with an array of values + */ +class ArrayDocument +{ + /** + * @param string $id The ID of the document + * @param string[] $values The values for the document + */ + public function __construct(public string $id = '', public array $values = []) { } + + /** + * A set of documents used for integration tests + * + * @return ArrayDocument[] Test documents for InArray tests + */ + public static function testDocuments(): array + { + return [ + new ArrayDocument('first', ['a', 'b', 'c']), + new ArrayDocument('second', ['c', 'd', 'e']), + new ArrayDocument('third', ['x', 'y', 'z']) + ]; + } +} diff --git a/tests/integration/postgresql/CountTest.php b/tests/integration/postgresql/CountTest.php index 5f67c01..ccda872 100644 --- a/tests/integration/postgresql/CountTest.php +++ b/tests/integration/postgresql/CountTest.php @@ -33,44 +33,49 @@ class CountTest extends TestCase parent::tearDown(); } + #[TestDox('all() succeeds')] public function testAllSucceeds(): void { $count = Count::all(ThrowawayDb::TABLE); $this->assertEquals(5, $count, 'There should have been 5 matching documents'); } + #[TestDox('byFields() succeeds for a numeric range')] public function testByFieldsSucceedsForANumericRange(): void { - $count = Count::byFields(ThrowawayDb::TABLE, [Field::BT('num_value', 10, 20)]); + $count = Count::byFields(ThrowawayDb::TABLE, [Field::between('num_value', 10, 20)]); $this->assertEquals(3, $count, 'There should have been 3 matching documents'); } + #[TestDox('byFields() succeeds for a non-numeric range')] public function testByFieldsSucceedsForANonNumericRange(): void { - $count = Count::byFields(ThrowawayDb::TABLE, [Field::BT('value', 'aardvark', 'apple')]); + $count = Count::byFields(ThrowawayDb::TABLE, [Field::between('value', 'aardvark', 'apple')]); $this->assertEquals(1, $count, 'There should have been 1 matching document'); } + #[TestDox('byContains() succeeds when documents match')] public function testByContainsSucceedsWhenDocumentsMatch(): void { $this->assertEquals(2, Count::byContains(ThrowawayDb::TABLE, ['value' => 'purple']), 'There should have been 2 matching documents'); } + #[TestDox('byContains() succeeds when no documents match')] public function testByContainsSucceedsWhenNoDocumentsMatch(): void { $this->assertEquals(0, Count::byContains(ThrowawayDb::TABLE, ['value' => 'magenta']), 'There should have been no matching documents'); } - #[TestDox('By JSON Path succeeds when documents match')] + #[TestDox('byJsonPath() succeeds when documents match')] public function testByJsonPathSucceedsWhenDocumentsMatch(): void { $this->assertEquals(2, Count::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ < 5)'), 'There should have been 2 matching documents'); } - #[TestDox('By JSON Path succeeds when no documents match')] + #[TestDox('byJsonPath() succeeds when no documents match')] public function testByJsonPathSucceedsWhenNoDocumentsMatch(): void { $this->assertEquals(0, Count::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 100)'), diff --git a/tests/integration/postgresql/CustomTest.php b/tests/integration/postgresql/CustomTest.php index 52f5c36..a12b9d6 100644 --- a/tests/integration/postgresql/CustomTest.php +++ b/tests/integration/postgresql/CustomTest.php @@ -34,7 +34,8 @@ class CustomTest extends TestCase ThrowawayDb::destroy($this->dbName); } - public function testRunQuerySucceedsWithAValidQuery() + #[TestDox('runQuery() succeeds with a valid query')] + public function testRunQuerySucceedsWithAValidQuery(): void { $stmt = &Custom::runQuery('SELECT data FROM ' . ThrowawayDb::TABLE . ' LIMIT 1', []); try { @@ -44,7 +45,8 @@ class CustomTest extends TestCase } } - public function testRunQueryFailsWithAnInvalidQuery() + #[TestDox('runQuery() fails with an invalid query')] + public function testRunQueryFailsWithAnInvalidQuery(): void { $this->expectException(DocumentException::class); $stmt = &Custom::runQuery('GRAB stuff FROM over_there UNTIL done', []); @@ -55,7 +57,8 @@ class CustomTest extends TestCase } } - public function testListSucceedsWhenDataIsFound() + #[TestDox('list() succeeds when data is found')] + public function testListSucceedsWhenDataIsFound(): void { $list = Custom::list(Query::selectFromTable(ThrowawayDb::TABLE), [], new DocumentMapper(TestDocument::class)); $this->assertNotNull($list, 'The document list should not be null'); @@ -64,7 +67,8 @@ class CustomTest extends TestCase $this->assertEquals(5, $count, 'There should have been 5 documents in the list'); } - public function testListSucceedsWhenNoDataIsFound() + #[TestDox('list() succeeds when no data is found')] + public function testListSucceedsWhenNoDataIsFound(): void { $list = Custom::list( Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE (data->>'num_value')::numeric > :value", @@ -73,7 +77,8 @@ class CustomTest extends TestCase $this->assertFalse($list->hasItems(), 'There should have been no documents in the list'); } - public function testArraySucceedsWhenDataIsFound() + #[TestDox('array() succeeds when data is found')] + public function testArraySucceedsWhenDataIsFound(): void { $array = Custom::array(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'sub' IS NOT NULL", [], new DocumentMapper(TestDocument::class)); @@ -81,7 +86,8 @@ class CustomTest extends TestCase $this->assertCount(2, $array, 'There should have been 2 documents in the array'); } - public function testArraySucceedsWhenNoDataIsFound() + #[TestDox('array() succeeds when no data is found')] + public function testArraySucceedsWhenNoDataIsFound(): void { $array = Custom::array(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'value' = :value", [':value' => 'not there'], new DocumentMapper(TestDocument::class)); @@ -89,6 +95,7 @@ class CustomTest extends TestCase $this->assertCount(0, $array, 'There should have been no documents in the array'); } + #[TestDox('single() succeeds when a row is found')] public function testSingleSucceedsWhenARowIsFound(): void { $doc = Custom::single('SELECT data FROM ' . ThrowawayDb::TABLE . " WHERE data->>'id' = :id", [':id' => 'one'], @@ -97,6 +104,7 @@ class CustomTest extends TestCase $this->assertEquals('one', $doc->get()->id, 'The incorrect document was returned'); } + #[TestDox('single() succeeds when a row is not found')] public function testSingleSucceedsWhenARowIsNotFound(): void { $doc = Custom::single('SELECT data FROM ' . ThrowawayDb::TABLE . " WHERE data->>'id' = :id", @@ -104,14 +112,16 @@ class CustomTest extends TestCase $this->assertTrue($doc->isNone(), 'There should not have been a document returned'); } - public function testNonQuerySucceedsWhenOperatingOnData() + #[TestDox('nonQuery() succeeds when operating on data')] + public function testNonQuerySucceedsWhenOperatingOnData(): void { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); $remaining = Count::all(ThrowawayDb::TABLE); $this->assertEquals(0, $remaining, 'There should be no documents remaining in the table'); } - public function testNonQuerySucceedsWhenNoDataMatchesWhereClause() + #[TestDox('nonQuery() succeeds when no data matches WHERE clause')] + public function testNonQuerySucceedsWhenNoDataMatchesWhereClause(): void { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE . " WHERE (data->>'num_value')::numeric > :value", [':value' => 100]); @@ -119,7 +129,8 @@ class CustomTest extends TestCase $this->assertEquals(5, $remaining, 'There should be 5 documents remaining in the table'); } - public function testScalarSucceeds() + #[TestDox('scalar() succeeds')] + public function testScalarSucceeds(): void { $value = Custom::scalar("SELECT 5 AS it", [], new CountMapper()); $this->assertEquals(5, $value, 'The scalar value was not returned correctly'); diff --git a/tests/integration/postgresql/DefinitionTest.php b/tests/integration/postgresql/DefinitionTest.php index 01dccb4..0aa7433 100644 --- a/tests/integration/postgresql/DefinitionTest.php +++ b/tests/integration/postgresql/DefinitionTest.php @@ -47,6 +47,7 @@ class DefinitionTest extends TestCase [':name' => $name], new ExistsMapper()); } + #[TestDox('ensureTable() succeeds')] public function testEnsureTableSucceeds(): void { $this->assertFalse($this->itExists('ensured'), 'The table should not exist already'); @@ -56,6 +57,7 @@ class DefinitionTest extends TestCase $this->assertTrue($this->itExists('idx_ensured_key'), 'The key index should now exist'); } + #[TestDox('ensureFieldIndex() succeeds')] public function testEnsureFieldIndexSucceeds(): void { $this->assertFalse($this->itExists('idx_ensured_test'), 'The index should not exist already'); @@ -64,6 +66,7 @@ class DefinitionTest extends TestCase $this->assertTrue($this->itExists('idx_ensured_test'), 'The index should now exist'); } + #[TestDox('ensureDocumentIndex() succeeds for Full')] public function testEnsureDocumentIndexSucceedsForFull(): void { $docIdx = 'idx_' . ThrowawayDb::TABLE . '_document'; @@ -73,6 +76,7 @@ class DefinitionTest extends TestCase $this->assertTrue($this->itExists($docIdx), 'The document index should now exist'); } + #[TestDox('ensureDocumentIndex() succeeds for Optimized')] public function testEnsureDocumentIndexSucceedsForOptimized(): void { $docIdx = 'idx_' . ThrowawayDb::TABLE . '_document'; diff --git a/tests/integration/postgresql/DeleteTest.php b/tests/integration/postgresql/DeleteTest.php index db68256..d354fe0 100644 --- a/tests/integration/postgresql/DeleteTest.php +++ b/tests/integration/postgresql/DeleteTest.php @@ -33,7 +33,7 @@ class DeleteTest extends TestCase parent::tearDown(); } - #[TestDox('By ID succeeds when a document is deleted')] + #[TestDox('byId() succeeds when a document is deleted')] public function testByIdSucceedsWhenADocumentIsDeleted(): void { $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents to start'); @@ -41,7 +41,7 @@ class DeleteTest extends TestCase $this->assertEquals(4, Count::all(ThrowawayDb::TABLE), 'There should have been 4 documents remaining'); } - #[TestDox('By ID succeeds when a document is not deleted')] + #[TestDox('byId() succeeds when a document is not deleted')] public function testByIdSucceedsWhenADocumentIsNotDeleted(): void { $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents to start'); @@ -49,20 +49,23 @@ class DeleteTest extends TestCase $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents remaining'); } + #[TestDox('byFields() succeeds when documents are deleted')] public function testByFieldsSucceedsWhenDocumentsAreDeleted(): void { $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents to start'); - Delete::byFields(ThrowawayDb::TABLE, [Field::NE('value', 'purple')]); + Delete::byFields(ThrowawayDb::TABLE, [Field::notEqual('value', 'purple')]); $this->assertEquals(2, Count::all(ThrowawayDb::TABLE), 'There should have been 2 documents remaining'); } + #[TestDox('byFields() succeeds when documents are not deleted')] public function testByFieldsSucceedsWhenDocumentsAreNotDeleted(): void { $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents to start'); - Delete::byFields(ThrowawayDb::TABLE, [Field::EQ('value', 'crimson')]); + Delete::byFields(ThrowawayDb::TABLE, [Field::equal('value', 'crimson')]); $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents remaining'); } + #[TestDox('byContains() succeeds when documents are deleted')] public function testByContainsSucceedsWhenDocumentsAreDeleted(): void { $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents to start'); @@ -70,6 +73,7 @@ class DeleteTest extends TestCase $this->assertEquals(3, Count::all(ThrowawayDb::TABLE), 'There should have been 3 documents remaining'); } + #[TestDox('byContains() succeeds when documents are not deleted')] public function testByContainsSucceedsWhenDocumentsAreNotDeleted(): void { $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents to start'); @@ -77,7 +81,7 @@ class DeleteTest extends TestCase $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents remaining'); } - #[TestDox('By JSON Path succeeds when documents are deleted')] + #[TestDox('byJsonPath() succeeds when documents are deleted')] public function testByJsonPathSucceedsWhenDocumentsAreDeleted(): void { $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents to start'); @@ -85,7 +89,7 @@ class DeleteTest extends TestCase $this->assertEquals(1, Count::all(ThrowawayDb::TABLE), 'There should have been 1 document remaining'); } - #[TestDox('By JSON Path succeeds when documents are not deleted')] + #[TestDox('byJsonPath() succeeds when documents are not deleted')] public function testByJsonPathSucceedsWhenDocumentsAreNotDeleted(): void { $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents to start'); diff --git a/tests/integration/postgresql/DocumentListTest.php b/tests/integration/postgresql/DocumentListTest.php index 88154e5..827247b 100644 --- a/tests/integration/postgresql/DocumentListTest.php +++ b/tests/integration/postgresql/DocumentListTest.php @@ -35,6 +35,7 @@ class DocumentListTest extends TestCase parent::tearDown(); } + #[TestDox('create() succeeds')] public function testCreateSucceeds(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], @@ -43,6 +44,7 @@ class DocumentListTest extends TestCase $list = null; } + #[TestDox('items() succeeds')] public function testItemsSucceeds(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], @@ -57,6 +59,7 @@ class DocumentListTest extends TestCase $this->assertEquals(5, $count, 'There should have been 5 documents returned'); } + #[TestDox('items() fails when already consumed')] public function testItemsFailsWhenAlreadyConsumed(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], @@ -69,6 +72,7 @@ class DocumentListTest extends TestCase iterator_to_array($list->items()); } + #[TestDox('hasItems() succeeds with empty results')] public function testHasItemsSucceedsWithEmptyResults(): void { $list = DocumentList::create( @@ -78,6 +82,7 @@ class DocumentListTest extends TestCase $this->assertFalse($list->hasItems(), 'There should be no items in the list'); } + #[TestDox('hasItems() succeeds with non-empty results')] public function testHasItemsSucceedsWithNonEmptyResults(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], @@ -90,6 +95,7 @@ class DocumentListTest extends TestCase $this->assertFalse($list->hasItems(), 'There should be no remaining items in the list'); } + #[TestDox('map() succeeds')] public function testMapSucceeds(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], @@ -102,6 +108,7 @@ class DocumentListTest extends TestCase } } + #[TestDox('iter() succeeds')] public function testIterSucceeds(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], @@ -114,6 +121,7 @@ class DocumentListTest extends TestCase 'Iteration did not have the expected result'); } + #[TestDox('mapToArray() succeeds')] public function testMapToArraySucceeds(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], diff --git a/tests/integration/postgresql/DocumentTest.php b/tests/integration/postgresql/DocumentTest.php index 01d09d4..0997b5a 100644 --- a/tests/integration/postgresql/DocumentTest.php +++ b/tests/integration/postgresql/DocumentTest.php @@ -35,7 +35,7 @@ class DocumentTest extends TestCase parent::tearDown(); } - #[TestDox('Insert succeeds for array no auto ID')] + #[TestDox('insert() succeeds for array no auto ID')] public function testInsertSucceedsForArrayNoAutoId(): void { Document::insert(ThrowawayDb::TABLE, ['id' => 'turkey', 'sub' => ['foo' => 'gobble', 'bar' => 'gobble']]); @@ -50,7 +50,7 @@ class DocumentTest extends TestCase $this->assertEquals('gobble', $doc->sub->bar, 'The sub-document bar property was incorrect'); } - #[TestDox('Insert succeeds for array with auto number ID not provided')] + #[TestDox('insert() succeeds for array with auto number ID not provided')] public function testInsertSucceedsForArrayWithAutoNumberIdNotProvided(): void { Configuration::$autoId = AutoId::Number; @@ -74,7 +74,7 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for array with auto number ID with ID provided')] + #[TestDox('insert() succeeds for array with auto number ID with ID provided')] public function testInsertSucceedsForArrayWithAutoNumberIdWithIdProvided(): void { Configuration::$autoId = AutoId::Number; @@ -90,14 +90,14 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for array with auto UUID ID not provided')] + #[TestDox('insert() succeeds for array with auto UUID ID not provided')] public function testInsertSucceedsForArrayWithAutoUuidIdNotProvided(): void { Configuration::$autoId = AutoId::UUID; try { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); Document::insert(ThrowawayDb::TABLE, ['id' => '', 'num_value' => 5]); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 5)], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('num_value', 5)], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertNotEmpty($doc->get()->id, 'The ID should have been auto-generated'); } finally { @@ -105,7 +105,7 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for array with auto UUID ID with ID provided')] + #[TestDox('insert() succeeds for array with auto UUID ID with ID provided')] public function testInsertSucceedsForArrayWithAutoUuidIdWithIdProvided(): void { Configuration::$autoId = AutoId::UUID; @@ -113,7 +113,7 @@ class DocumentTest extends TestCase Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); $uuid = AutoId::generateUUID(); Document::insert(ThrowawayDb::TABLE, ['id' => $uuid, 'value' => 'uuid', 'num_value' => 12]); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 12)], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('num_value', 12)], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals($uuid, $doc->get()->id, 'The ID should not have been changed'); } finally { @@ -121,7 +121,7 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for array with auto string ID not provided')] + #[TestDox('insert() succeeds for array with auto string ID not provided')] public function testInsertSucceedsForArrayWithAutoStringIdNotProvided(): void { Configuration::$autoId = AutoId::RandomString; @@ -129,7 +129,7 @@ class DocumentTest extends TestCase try { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); Document::insert(ThrowawayDb::TABLE, ['id' => '', 'value' => 'new', 'num_value' => 8]); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 8)], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('num_value', 8)], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals(6, strlen($doc->get()->id), 'The ID should have been auto-generated and had 6 characters'); @@ -139,14 +139,14 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for array with auto string ID with ID provided')] + #[TestDox('insert() succeeds for array with auto string ID with ID provided')] public function testInsertSucceedsForArrayWithAutoStringIdWithIdProvided(): void { Configuration::$autoId = AutoId::RandomString; try { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); Document::insert(ThrowawayDb::TABLE, ['id' => 'my-key', 'value' => 'old', 'num_value' => 3]); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 3)], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('num_value', 3)], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals('my-key', $doc->get()->id, 'The ID should not have been changed'); } finally { @@ -154,7 +154,7 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for object no auto ID')] + #[TestDox('insert() succeeds for object no auto ID')] public function testInsertSucceedsForObjectNoAutoId(): void { Document::insert(ThrowawayDb::TABLE, new TestDocument('turkey', sub: new SubDocument('gobble', 'gobble'))); @@ -169,7 +169,7 @@ class DocumentTest extends TestCase $this->assertEquals('gobble', $doc->sub->bar, 'The sub-document bar property was incorrect'); } - #[TestDox('Insert succeeds for object with auto number ID not provided')] + #[TestDox('insert() succeeds for object with auto number ID not provided')] public function testInsertSucceedsForObjectWithAutoNumberIdNotProvided(): void { Configuration::$autoId = AutoId::Number; @@ -177,12 +177,12 @@ class DocumentTest extends TestCase Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); Document::insert(ThrowawayDb::TABLE, new NumDocument(value: 'taco')); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'taco')], NumDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'taco')], NumDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals(1, $doc->get()->id, 'The ID 1 should have been auto-generated'); Document::insert(ThrowawayDb::TABLE, new NumDocument(value: 'burrito')); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'burrito')], NumDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'burrito')], NumDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals(2, $doc->get()->id, 'The ID 2 should have been auto-generated'); } finally { @@ -190,14 +190,14 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for object with auto number ID with ID provided')] + #[TestDox('insert() succeeds for object with auto number ID with ID provided')] public function testInsertSucceedsForObjectWithAutoNumberIdWithIdProvided(): void { Configuration::$autoId = AutoId::Number; try { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); Document::insert(ThrowawayDb::TABLE, new NumDocument(64, 'large')); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'large')], NumDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'large')], NumDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals(64, $doc->get()->id, 'The ID 64 should have been stored'); } finally { @@ -205,14 +205,14 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for object with auto UUID ID not provided')] + #[TestDox('insert() succeeds for object with auto UUID ID not provided')] public function testInsertSucceedsForObjectWithAutoUuidIdNotProvided(): void { Configuration::$autoId = AutoId::UUID; try { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); Document::insert(ThrowawayDb::TABLE, new TestDocument(value: 'something', num_value: 9)); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EX('value')], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::exists('value')], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertNotEmpty($doc->get()->id, 'The ID should have been auto-generated'); } finally { @@ -220,7 +220,7 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for object with auto UUID ID with ID provided')] + #[TestDox('insert() succeeds for object with auto UUID ID with ID provided')] public function testInsertSucceedsForObjectWithAutoUuidIdWithIdProvided(): void { Configuration::$autoId = AutoId::UUID; @@ -228,7 +228,7 @@ class DocumentTest extends TestCase Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); $uuid = AutoId::generateUUID(); Document::insert(ThrowawayDb::TABLE, new TestDocument($uuid, num_value: 14)); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 14)], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('num_value', 14)], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals($uuid, $doc->get()->id, 'The ID should not have been changed'); } finally { @@ -236,7 +236,7 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for object with auto string ID not provided')] + #[TestDox('insert() succeeds for object with auto string ID not provided')] public function testInsertSucceedsForObjectWithAutoStringIdNotProvided(): void { Configuration::$autoId = AutoId::RandomString; @@ -244,7 +244,7 @@ class DocumentTest extends TestCase try { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); Document::insert(ThrowawayDb::TABLE, new TestDocument(num_value: 55)); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 55)], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('num_value', 55)], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals(40, strlen($doc->get()->id), 'The ID should have been auto-generated and had 40 characters'); @@ -254,14 +254,14 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for object with auto string ID with ID provided')] + #[TestDox('insert() succeeds for object with auto string ID with ID provided')] public function testInsertSucceedsForObjectWithAutoStringIdWithIdProvided(): void { Configuration::$autoId = AutoId::RandomString; try { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); Document::insert(ThrowawayDb::TABLE, new TestDocument('my-key', num_value: 3)); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 3)], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('num_value', 3)], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals('my-key', $doc->get()->id, 'The ID should not have been changed'); } finally { @@ -269,12 +269,14 @@ class DocumentTest extends TestCase } } + #[TestDox('insert() fails for duplicate key')] public function testInsertFailsForDuplicateKey(): void { $this->expectException(DocumentException::class); Document::insert(ThrowawayDb::TABLE, new TestDocument('one')); } + #[TestDox('save() succeeds when a document is inserted')] public function testSaveSucceedsWhenADocumentIsInserted(): void { Document::save(ThrowawayDb::TABLE, new TestDocument('test', sub: new SubDocument('a', 'b'))); @@ -282,6 +284,7 @@ class DocumentTest extends TestCase $this->assertTrue($doc->isSome(), 'There should have been a document returned'); } + #[TestDox('save() succeeds when a document is updated')] public function testSaveSucceedsWhenADocumentIsUpdated(): void { Document::save(ThrowawayDb::TABLE, new TestDocument('two', num_value: 44)); @@ -292,6 +295,7 @@ class DocumentTest extends TestCase $this->assertNull($doc->sub, 'The sub-document should have been null'); } + #[TestDox('update() succeeds when replacing a document')] public function testUpdateSucceedsWhenReplacingADocument(): void { Document::update(ThrowawayDb::TABLE, 'one', new TestDocument('one', 'howdy', 8, new SubDocument('y', 'z'))); @@ -305,6 +309,7 @@ class DocumentTest extends TestCase $this->assertEquals('z', $doc->sub->bar, 'The sub-document bar property was incorrect'); } + #[TestDox('update() succeeds when no document is replaced')] public function testUpdateSucceedsWhenNoDocumentIsReplaced(): void { Document::update(ThrowawayDb::TABLE, 'two-hundred', new TestDocument('200')); diff --git a/tests/integration/postgresql/ExistsTest.php b/tests/integration/postgresql/ExistsTest.php index b508517..097d08c 100644 --- a/tests/integration/postgresql/ExistsTest.php +++ b/tests/integration/postgresql/ExistsTest.php @@ -33,51 +33,55 @@ class ExistsTest extends TestCase parent::tearDown(); } - #[TestDox('By ID succeeds when a document exists')] + #[TestDox('byId() succeeds when a document exists')] public function testByIdSucceedsWhenADocumentExists(): void { $this->assertTrue(Exists::byId(ThrowawayDb::TABLE, 'three'), 'There should have been an existing document'); } - #[TestDox('By ID succeeds when a document does not exist')] + #[TestDox('byId() succeeds when a document does not exist')] public function testByIdSucceedsWhenADocumentDoesNotExist(): void { $this->assertFalse(Exists::byId(ThrowawayDb::TABLE, 'seven'), 'There should not have been an existing document'); } + #[TestDox('byFields() succeeds when documents exist')] public function testByFieldsSucceedsWhenDocumentsExist(): void { - $this->assertTrue(Exists::byFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 10)]), + $this->assertTrue(Exists::byFields(ThrowawayDb::TABLE, [Field::equal('num_value', 10)]), 'There should have been existing documents'); } + #[TestDox('byFields() succeeds when no matching documents exist')] public function testByFieldsSucceedsWhenNoMatchingDocumentsExist(): void { - $this->assertFalse(Exists::byFields(ThrowawayDb::TABLE, [Field::LT('nothing', 'none')]), + $this->assertFalse(Exists::byFields(ThrowawayDb::TABLE, [Field::less('nothing', 'none')]), 'There should not have been any existing documents'); } + #[TestDox('byContains() succeeds when documents exist')] public function testByContainsSucceedsWhenDocumentsExist(): void { $this->assertTrue(Exists::byContains(ThrowawayDb::TABLE, ['value' => 'purple']), 'There should have been existing documents'); } + #[TestDox('byContains() succeeds when no matching documents exist')] public function testByContainsSucceedsWhenNoMatchingDocumentsExist(): void { $this->assertFalse(Exists::byContains(ThrowawayDb::TABLE, ['value' => 'violet']), 'There should not have been existing documents'); } - #[TestDox('By JSON Path succeeds when documents exist')] + #[TestDox('byJsonPath() succeeds when documents exist')] public function testByJsonPathSucceedsWhenDocumentsExist(): void { $this->assertTrue(Exists::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ == 10)'), 'There should have been existing documents'); } - #[TestDox('By JSON Path succeeds when no matching documents exist')] + #[TestDox('byJsonPath() succeeds when no matching documents exist')] public function testByJsonPathSucceedsWhenNoMatchingDocumentsExist(): void { $this->assertFalse(Exists::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ == 10.1)'), diff --git a/tests/integration/postgresql/FindTest.php b/tests/integration/postgresql/FindTest.php index bb81b97..3fd2683 100644 --- a/tests/integration/postgresql/FindTest.php +++ b/tests/integration/postgresql/FindTest.php @@ -8,10 +8,10 @@ declare(strict_types=1); namespace Test\Integration\PostgreSQL; -use BitBadger\PDODocument\{Custom, Delete, Document, Field, Find}; +use BitBadger\PDODocument\{Custom, Delete, Document, Field, FieldMatch, Find}; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; -use Test\Integration\{NumDocument, TestDocument}; +use Test\Integration\{ArrayDocument, NumDocument, TestDocument}; /** * PostgreSQL integration tests for the Find class @@ -34,6 +34,7 @@ class FindTest extends TestCase parent::tearDown(); } + #[TestDox('all() succeeds when there is data')] public function testAllSucceedsWhenThereIsData(): void { $docs = Find::all(ThrowawayDb::TABLE, TestDocument::class); @@ -43,6 +44,35 @@ class FindTest extends TestCase $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')] public function testAllSucceedsWhenThereIsNoData(): void { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); @@ -51,7 +81,7 @@ class FindTest extends TestCase $this->assertFalse($docs->hasItems(), 'There should have been no documents in the list'); } - #[TestDox('By ID succeeds when a document is found')] + #[TestDox('byId() succeeds when a document is found')] public function testByIdSucceedsWhenADocumentIsFound(): void { $doc = Find::byId(ThrowawayDb::TABLE, 'two', TestDocument::class); @@ -59,39 +89,87 @@ class FindTest extends TestCase $this->assertEquals('two', $doc->get()->id, 'An incorrect document was returned'); } - #[TestDox('By ID succeeds when a document is found with numeric ID')] + #[TestDox('byId() succeeds when a document is found with numeric ID')] public function testByIdSucceedsWhenADocumentIsFoundWithNumericId(): void { - Delete::byFields(ThrowawayDb::TABLE, [Field::NEX('absent')]); + Delete::byFields(ThrowawayDb::TABLE, [Field::notExists('absent')]); Document::insert(ThrowawayDb::TABLE, ['id' => 18, 'value' => 'howdy']); $doc = Find::byId(ThrowawayDb::TABLE, 18, NumDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals(18, $doc->get()->id, 'An incorrect document was returned'); } - #[TestDox('By ID succeeds when a document is not found')] + #[TestDox('byId() succeeds when a document is not found')] public function testByIdSucceedsWhenADocumentIsNotFound(): void { $doc = Find::byId(ThrowawayDb::TABLE, 'seventy-five', TestDocument::class); $this->assertTrue($doc->isNone(), 'There should not have been a document returned'); } + #[TestDox('byFields() succeeds when documents are found')] public function testByFieldsSucceedsWhenDocumentsAreFound(): void { - $docs = Find::byFields(ThrowawayDb::TABLE, [Field::GT('num_value', 15)], TestDocument::class); + $docs = Find::byFields(ThrowawayDb::TABLE, [Field::in('value', ['blue', 'purple']), Field::exists('sub')], + TestDocument::class, FieldMatch::All); + $this->assertNotNull($docs, 'There should have been a document list returned'); + $count = 0; + foreach ($docs->items() as $ignored) $count++; + $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')] + public function testByFieldsSucceedsWhenDocumentsAreFoundUsingInWithNumericField(): void + { + $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'); + $count = 0; + foreach ($docs->items() as $ignored) $count++; + $this->assertEquals(1, $count, 'There should have been 1 document in the list'); + } + + #[TestDox('byFields() succeeds when no documents are found')] + public function testByFieldsSucceedsWhenNoDocumentsAreFound(): void + { + $docs = Find::byFields(ThrowawayDb::TABLE, [Field::greater('num_value', 100)], TestDocument::class); + $this->assertNotNull($docs, 'There should have been a document list returned'); + $this->assertFalse($docs->hasItems(), 'There should have been no documents in the list'); + } + + #[TestDox('byFields() succeeds for inArray when matching documents exist')] + public function testByFieldsSucceedsForInArrayWhenMatchingDocumentsExist(): void + { + Delete::byFields(ThrowawayDb::TABLE, [Field::notExists('absentField')]); + foreach (ArrayDocument::testDocuments() as $doc) Document::insert(ThrowawayDb::TABLE, $doc); + $docs = Find::byFields(ThrowawayDb::TABLE, [Field::inArray('values', ThrowawayDb::TABLE, ['c'])], + ArrayDocument::class); $this->assertNotNull($docs, 'There should have been a document list returned'); $count = 0; foreach ($docs->items() as $ignored) $count++; $this->assertEquals(2, $count, 'There should have been 2 documents in the list'); } - public function testByFieldsSucceedsWhenNoDocumentsAreFound(): void + #[TestDox('byFields() succeeds for inArray when no matching documents exist')] + public function testByFieldsSucceedsForInArrayWhenNoMatchingDocumentsExist(): void { - $docs = Find::byFields(ThrowawayDb::TABLE, [Field::GT('num_value', 100)], TestDocument::class); + Delete::byFields(ThrowawayDb::TABLE, [Field::notExists('absentField')]); + foreach (ArrayDocument::testDocuments() as $doc) Document::insert(ThrowawayDb::TABLE, $doc); + $docs = Find::byFields(ThrowawayDb::TABLE, [Field::inArray('values', ThrowawayDb::TABLE, ['j'])], + ArrayDocument::class); $this->assertNotNull($docs, 'There should have been a document list returned'); $this->assertFalse($docs->hasItems(), 'There should have been no documents in the list'); } + #[TestDox('byContains() succeeds when documents are found')] public function testByContainsSucceedsWhenDocumentsAreFound(): void { $docs = Find::byContains(ThrowawayDb::TABLE, ['value' => 'purple'], TestDocument::class); @@ -101,6 +179,17 @@ class FindTest extends TestCase $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')] public function testByContainsSucceedsWhenNoDocumentsAreFound(): void { $docs = Find::byContains(ThrowawayDb::TABLE, ['value' => 'indigo'], TestDocument::class); @@ -108,7 +197,7 @@ class FindTest extends TestCase $this->assertFalse($docs->hasItems(), 'The document list should be empty'); } - #[TestDox('By JSON Path succeeds when documents are found')] + #[TestDox('byJsonPath() succeeds when documents are found')] public function testByJsonPathSucceedsWhenDocumentsAreFound(): void { $docs = Find::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 10)', TestDocument::class); @@ -118,7 +207,17 @@ class FindTest extends TestCase $this->assertEquals(2, $count, 'There should have been 2 documents in the list'); } - #[TestDox('By JSON Path succeeds when no documents are found')] + #[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')] public function testByJsonPathSucceedsWhenNoDocumentsAreFound(): void { $docs = Find::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 100)', TestDocument::class); @@ -126,26 +225,39 @@ class FindTest extends TestCase $this->assertFalse($docs->hasItems(), 'The document list should be empty'); } + #[TestDox('firstByFields() succeeds when a document is found')] public function testFirstByFieldsSucceedsWhenADocumentIsFound(): void { - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'another')], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'another')], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals('two', $doc->get()->id, 'The incorrect document was returned'); } + #[TestDox('firstByFields() succeeds when multiple documents are found')] public function testFirstByFieldsSucceedsWhenMultipleDocumentsAreFound(): void { - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('sub.foo', 'green')], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('sub.foo', 'green')], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document 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')] public function testFirstByFieldsSucceedsWhenADocumentIsNotFound(): void { - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'absent')], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'absent')], TestDocument::class); $this->assertTrue($doc->isNone(), 'There should not have been a document returned'); } + #[TestDox('firstByContains() succeeds when a document is found')] public function testFirstByContainsSucceedsWhenADocumentIsFound(): void { $doc = Find::firstByContains(ThrowawayDb::TABLE, ['value' => 'FIRST!'], TestDocument::class); @@ -153,6 +265,7 @@ class FindTest extends TestCase $this->assertEquals('one', $doc->get()->id, 'The incorrect document was returned'); } + #[TestDox('firstByContains() succeeds when multiple documents are found')] public function testFirstByContainsSucceedsWhenMultipleDocumentsAreFound(): void { $doc = Find::firstByContains(ThrowawayDb::TABLE, ['value' => 'purple'], TestDocument::class); @@ -160,13 +273,23 @@ class FindTest extends TestCase $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')] public function testFirstByContainsSucceedsWhenADocumentIsNotFound(): void { $doc = Find::firstByContains(ThrowawayDb::TABLE, ['value' => 'indigo'], TestDocument::class); $this->assertTrue($doc->isNone(), 'There should not have been a document returned'); } - #[TestDox('First by JSON Path succeeds when a document is found')] + #[TestDox('firstByJsonPath() succeeds when a document is found')] public function testFirstByJsonPathSucceedsWhenADocumentIsFound(): void { $doc = Find::firstByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ == 10)', TestDocument::class); @@ -174,7 +297,7 @@ class FindTest extends TestCase $this->assertEquals('two', $doc->get()->id, 'The incorrect document was returned'); } - #[TestDox('First by JSON Path succeeds when multiple documents are found')] + #[TestDox('firstByJsonPath() succeeds when multiple documents are found')] public function testFirstByJsonPathSucceedsWhenMultipleDocumentsAreFound(): void { $doc = Find::firstByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 10)', TestDocument::class); @@ -182,7 +305,16 @@ class FindTest extends TestCase $this->assertContains($doc->get()->id, ['four', 'five'], 'An incorrect document was returned'); } - #[TestDox('First by JSON Path succeeds when a document is not found')] + #[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')] public function testFirstByJsonPathSucceedsWhenADocumentIsNotFound(): void { $doc = Find::firstByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 100)', TestDocument::class); diff --git a/tests/integration/postgresql/PatchTest.php b/tests/integration/postgresql/PatchTest.php index 10ae185..9955441 100644 --- a/tests/integration/postgresql/PatchTest.php +++ b/tests/integration/postgresql/PatchTest.php @@ -34,7 +34,7 @@ class PatchTest extends TestCase parent::tearDown(); } - #[TestDox('By ID succeeds when a document is updated')] + #[TestDox('byId() succeeds when a document is updated')] public function testByIdSucceedsWhenADocumentIsUpdated(): void { Patch::byId(ThrowawayDb::TABLE, 'one', ['num_value' => 44]); @@ -43,7 +43,7 @@ class PatchTest extends TestCase $this->assertEquals(44, $doc->get()->num_value, 'The updated document is not correct'); } - #[TestDox('By ID succeeds when no document is updated')] + #[TestDox('byId() succeeds when no document is updated')] public function testByIdSucceedsWhenNoDocumentIsUpdated(): void { $id = 'forty-seven'; @@ -52,21 +52,24 @@ class PatchTest extends TestCase $this->assertTrue(true, 'The above not throwing an exception is the test'); } + #[TestDox('byFields() succeeds when a document is updated')] public function testByFieldsSucceedsWhenADocumentIsUpdated(): void { - Patch::byFields(ThrowawayDb::TABLE, [Field::EQ('value', 'purple')], ['num_value' => 77]); - $after = Count::byFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 77)]); + Patch::byFields(ThrowawayDb::TABLE, [Field::equal('value', 'purple')], ['num_value' => 77]); + $after = Count::byFields(ThrowawayDb::TABLE, [Field::equal('num_value', 77)]); $this->assertEquals(2, $after, 'There should have been 2 documents updated'); } + #[TestDox('byFields() succeeds when no document is updated')] public function testByFieldsSucceedsWhenNoDocumentIsUpdated(): void { - $fields = [Field::EQ('value', 'burgundy')]; + $fields = [Field::equal('value', 'burgundy')]; $this->assertEquals(0, Count::byFields(ThrowawayDb::TABLE, $fields), 'There should be no matching documents'); Patch::byFields(ThrowawayDb::TABLE, $fields, ['foo' => 'green']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } + #[TestDox('byContains() succeeds when documents are updated')] public function testByContainsSucceedsWhenDocumentsAreUpdated(): void { Patch::byContains(ThrowawayDb::TABLE, ['value' => 'another'], ['num_value' => 12]); @@ -77,6 +80,7 @@ class PatchTest extends TestCase $this->assertEquals(12, $doc->num_value, 'The document was not patched'); } + #[TestDox('byContains() succeeds when no documents are updated')] public function testByContainsSucceedsWhenNoDocumentsAreUpdated(): void { $criteria = ['value' => 'updated']; @@ -86,7 +90,7 @@ class PatchTest extends TestCase $this->assertTrue(true, 'The above not throwing an exception is the test'); } - #[TestDox('By JSON Path succeeds when documents are updated')] + #[TestDox('byJsonPath() succeeds when documents are updated')] public function testByJsonPathSucceedsWhenDocumentsAreUpdated(): void { Patch::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 10)', ['value' => 'blue']); @@ -99,7 +103,7 @@ class PatchTest extends TestCase } } - #[TestDox('By JSON Path succeeds when documents are not updated')] + #[TestDox('byJsonPath() succeeds when documents are not updated')] public function testByJsonPathSucceedsWhenDocumentsAreNotUpdated(): void { $path = '$.num_value ? (@ > 100)'; diff --git a/tests/integration/postgresql/RemoveFieldsTest.php b/tests/integration/postgresql/RemoveFieldsTest.php index dc68e8a..f83a200 100644 --- a/tests/integration/postgresql/RemoveFieldsTest.php +++ b/tests/integration/postgresql/RemoveFieldsTest.php @@ -34,7 +34,7 @@ class RemoveFieldsTest extends TestCase parent::tearDown(); } - #[TestDox('By ID succeeds when fields are removed')] + #[TestDox('byId() succeeds when fields are removed')] public function testByIdSucceedsWhenFieldsAreRemoved(): void { RemoveFields::byId(ThrowawayDb::TABLE, 'two', ['sub', 'value']); @@ -45,40 +45,44 @@ class RemoveFieldsTest extends TestCase $this->assertNull($doc->sub, 'Sub-document should have been null'); } - #[TestDox('By ID succeeds when a field is not removed')] + #[TestDox('byId() succeeds when a field is not removed')] public function testByIdSucceedsWhenAFieldIsNotRemoved(): void { RemoveFields::byId(ThrowawayDb::TABLE, 'one', ['a_field_that_does_not_exist']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } - #[TestDox('By ID succeeds when no document is matched')] + #[TestDox('byId() succeeds when no document is matched')] public function testByIdSucceedsWhenNoDocumentIsMatched(): void { RemoveFields::byId(ThrowawayDb::TABLE, 'fifty', ['sub']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } + #[TestDox('byFields() succeeds when a field is removed')] public function testByFieldsSucceedsWhenAFieldIsRemoved(): void { - RemoveFields::byFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 17)], ['sub']); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 17)], TestDocument::class); + RemoveFields::byFields(ThrowawayDb::TABLE, [Field::equal('num_value', 17)], ['sub']); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('num_value', 17)], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertNull($doc->get()->sub, 'Sub-document should have been null'); } + #[TestDox('byFields() succeeds when a field is not removed')] public function testByFieldsSucceedsWhenAFieldIsNotRemoved(): void { - RemoveFields::byFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 17)], ['nada']); + RemoveFields::byFields(ThrowawayDb::TABLE, [Field::equal('num_value', 17)], ['nada']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } + #[TestDox('byFields() succeeds when no document is matched')] public function testByFieldsSucceedsWhenNoDocumentIsMatched(): void { - RemoveFields::byFields(ThrowawayDb::TABLE, [Field::NE('missing', 'nope')], ['value']); + RemoveFields::byFields(ThrowawayDb::TABLE, [Field::notEqual('missing', 'nope')], ['value']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } + #[TestDox('byContains() succeeds when a field is removed')] public function testByContainsSucceedsWhenAFieldIsRemoved(): void { $criteria = ['sub' => ['foo' => 'green']]; @@ -92,19 +96,21 @@ class RemoveFieldsTest extends TestCase } } + #[TestDox('byContains() succeeds when a field is not removed')] public function testByContainsSucceedsWhenAFieldIsNotRemoved(): void { RemoveFields::byContains(ThrowawayDb::TABLE, ['sub' => ['foo' => 'green']], ['invalid_field']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } + #[TestDox('byContains() succeeds when no document is matched')] public function testByContainsSucceedsWhenNoDocumentIsMatched(): void { RemoveFields::byContains(ThrowawayDb::TABLE, ['value' => 'substantial'], ['num_value']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } - #[TestDox('By JSON Path succeeds when a field is removed')] + #[TestDox('byJsonPath() succeeds when a field is removed')] public function testByJsonPathSucceedsWhenAFieldIsRemoved(): void { $path = '$.value ? (@ == "purple")'; @@ -118,14 +124,14 @@ class RemoveFieldsTest extends TestCase } } - #[TestDox('By JSON Path succeeds when a field is not removed')] + #[TestDox('byJsonPath() succeeds when a field is not removed')] public function testByJsonPathSucceedsWhenAFieldIsNotRemoved(): void { RemoveFields::byJsonPath(ThrowawayDb::TABLE, '$.value ? (@ == "purple")', ['submarine']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } - #[TestDox('By JSON Path succeeds when no document is matched')] + #[TestDox('byJsonPath() succeeds when no document is matched')] public function testByJsonPathSucceedsWhenNoDocumentIsMatched(): void { RemoveFields::byJsonPath(ThrowawayDb::TABLE, '$.value ? (@ == "mauve")', ['value']); diff --git a/tests/integration/sqlite/CountTest.php b/tests/integration/sqlite/CountTest.php index 0f654c7..b94e3d1 100644 --- a/tests/integration/sqlite/CountTest.php +++ b/tests/integration/sqlite/CountTest.php @@ -33,31 +33,35 @@ class CountTest extends TestCase parent::tearDown(); } + #[TestDox('all() succeeds')] public function testAllSucceeds(): void { $count = Count::all(ThrowawayDb::TABLE); $this->assertEquals(5, $count, 'There should have been 5 matching documents'); } + #[TestDox('byFields() succeeds for a numeric range')] public function testByFieldsSucceedsForANumericRange(): void { - $count = Count::byFields(ThrowawayDb::TABLE, [Field::BT('num_value', 10, 20)]); + $count = Count::byFields(ThrowawayDb::TABLE, [Field::between('num_value', 10, 20)]); $this->assertEquals(3, $count, 'There should have been 3 matching documents'); } + #[TestDox('byFields() succeeds for a non-numeric range')] public function testByFieldsSucceedsForANonNumericRange(): void { - $count = Count::byFields(ThrowawayDb::TABLE, [Field::BT('value', 'aardvark', 'apple')]); + $count = Count::byFields(ThrowawayDb::TABLE, [Field::between('value', 'aardvark', 'apple')]); $this->assertEquals(1, $count, 'There should have been 1 matching document'); } + #[TestDox('byContains() fails')] public function testByContainsFails(): void { $this->expectException(DocumentException::class); Count::byContains('', []); } - #[TestDox('By JSON Path fails')] + #[TestDox('byJsonPath() fails')] public function testByJsonPathFails(): void { $this->expectException(DocumentException::class); diff --git a/tests/integration/sqlite/CustomTest.php b/tests/integration/sqlite/CustomTest.php index e4bc343..a167975 100644 --- a/tests/integration/sqlite/CustomTest.php +++ b/tests/integration/sqlite/CustomTest.php @@ -34,7 +34,8 @@ class CustomTest extends TestCase ThrowawayDb::destroy($this->dbName); } - public function testRunQuerySucceedsWithAValidQuery() + #[TestDox('runQuery() succeeds with a valid query')] + public function testRunQuerySucceedsWithAValidQuery(): void { $stmt = &Custom::runQuery('SELECT data FROM ' . ThrowawayDb::TABLE . ' LIMIT 1', []); try { @@ -44,7 +45,8 @@ class CustomTest extends TestCase } } - public function testRunQueryFailsWithAnInvalidQuery() + #[TestDox('runQuery() fails with an invalid query')] + public function testRunQueryFailsWithAnInvalidQuery(): void { $this->expectException(DocumentException::class); $stmt = &Custom::runQuery('GRAB stuff FROM over_there UNTIL done', []); @@ -55,7 +57,8 @@ class CustomTest extends TestCase } } - public function testListSucceedsWhenDataIsFound() + #[TestDox('list() succeeds when data is found')] + public function testListSucceedsWhenDataIsFound(): void { $list = Custom::list(Query::selectFromTable(ThrowawayDb::TABLE), [], new DocumentMapper(TestDocument::class)); $this->assertNotNull($list, 'The document list should not be null'); @@ -64,7 +67,8 @@ class CustomTest extends TestCase $this->assertEquals(5, $count, 'There should have been 5 documents in the list'); } - public function testListSucceedsWhenNoDataIsFound() + #[TestDox('list() succeeds when no data is found')] + public function testListSucceedsWhenNoDataIsFound(): void { $list = Custom::list(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'num_value' > :value", [':value' => 100], new DocumentMapper(TestDocument::class)); @@ -72,7 +76,8 @@ class CustomTest extends TestCase $this->assertFalse($list->hasItems(), 'There should have been no documents in the list'); } - public function testArraySucceedsWhenDataIsFound() + #[TestDox('array() succeeds when data is found')] + public function testArraySucceedsWhenDataIsFound(): void { $array = Custom::array(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'sub' IS NOT NULL", [], new DocumentMapper(TestDocument::class)); @@ -80,7 +85,8 @@ class CustomTest extends TestCase $this->assertCount(2, $array, 'There should have been 2 documents in the array'); } - public function testArraySucceedsWhenNoDataIsFound() + #[TestDox('array() succeeds when no data is found')] + public function testArraySucceedsWhenNoDataIsFound(): void { $array = Custom::array(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'value' = :value", [':value' => 'not there'], new DocumentMapper(TestDocument::class)); @@ -88,6 +94,7 @@ class CustomTest extends TestCase $this->assertCount(0, $array, 'There should have been no documents in the array'); } + #[TestDox('single() succeeds when a row is found')] public function testSingleSucceedsWhenARowIsFound(): void { $doc = Custom::single('SELECT data FROM ' . ThrowawayDb::TABLE . " WHERE data->>'id' = :id", [':id' => 'one'], @@ -96,6 +103,7 @@ class CustomTest extends TestCase $this->assertEquals('one', $doc->get()->id, 'The incorrect document was returned'); } + #[TestDox('single() succeeds when a row is not found')] public function testSingleSucceedsWhenARowIsNotFound(): void { $doc = Custom::single('SELECT data FROM ' . ThrowawayDb::TABLE . " WHERE data->>'id' = :id", @@ -103,21 +111,24 @@ class CustomTest extends TestCase $this->assertTrue($doc->isNone(), 'There should not have been a document returned'); } - public function testNonQuerySucceedsWhenOperatingOnData() + #[TestDox('nonQuery() succeeds when operating on data')] + public function testNonQuerySucceedsWhenOperatingOnData(): void { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); $remaining = Count::all(ThrowawayDb::TABLE); $this->assertEquals(0, $remaining, 'There should be no documents remaining in the table'); } - public function testNonQuerySucceedsWhenNoDataMatchesWhereClause() + #[TestDox('nonQuery() succeeds when no data matches WHERE clause')] + public function testNonQuerySucceedsWhenNoDataMatchesWhereClause(): void { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE . " WHERE data->>'num_value' > :value", [':value' => 100]); $remaining = Count::all(ThrowawayDb::TABLE); $this->assertEquals(5, $remaining, 'There should be 5 documents remaining in the table'); } - public function testScalarSucceeds() + #[TestDox('scalar() succeeds')] + public function testScalarSucceeds(): void { $value = Custom::scalar("SELECT 5 AS it", [], new CountMapper()); $this->assertEquals(5, $value, 'The scalar value was not returned correctly'); diff --git a/tests/integration/sqlite/DefinitionTest.php b/tests/integration/sqlite/DefinitionTest.php index a9f20a2..d861126 100644 --- a/tests/integration/sqlite/DefinitionTest.php +++ b/tests/integration/sqlite/DefinitionTest.php @@ -47,6 +47,7 @@ class DefinitionTest extends TestCase [':name' => $name], new ExistsMapper()); } + #[TestDox('ensureTable() succeeds')] public function testEnsureTableSucceeds(): void { $this->assertFalse($this->itExists('ensured'), 'The table should not exist already'); @@ -56,6 +57,7 @@ class DefinitionTest extends TestCase $this->assertTrue($this->itExists('idx_ensured_key'), 'The key index should now exist'); } + #[TestDox('ensureFieldIndex() succeeds')] public function testEnsureFieldIndexSucceeds(): void { $this->assertFalse($this->itExists('idx_ensured_test'), 'The index should not exist already'); @@ -64,6 +66,7 @@ class DefinitionTest extends TestCase $this->assertTrue($this->itExists('idx_ensured_test'), 'The index should now exist'); } + #[TestDox('ensureDocumentIndex() fails')] public function testEnsureDocumentIndexFails(): void { $this->expectException(DocumentException::class); diff --git a/tests/integration/sqlite/DeleteTest.php b/tests/integration/sqlite/DeleteTest.php index a160e9b..11d5dd9 100644 --- a/tests/integration/sqlite/DeleteTest.php +++ b/tests/integration/sqlite/DeleteTest.php @@ -33,7 +33,7 @@ class DeleteTest extends TestCase parent::tearDown(); } - #[TestDox('By ID succeeds when a document is deleted')] + #[TestDox('byId() succeeds when a document is deleted')] public function testByIdSucceedsWhenADocumentIsDeleted(): void { $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents to start'); @@ -41,7 +41,7 @@ class DeleteTest extends TestCase $this->assertEquals(4, Count::all(ThrowawayDb::TABLE), 'There should have been 4 documents remaining'); } - #[TestDox('By ID succeeds when a document is not deleted')] + #[TestDox('byId() succeeds when a document is not deleted')] public function testByIdSucceedsWhenADocumentIsNotDeleted(): void { $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents to start'); @@ -49,27 +49,30 @@ class DeleteTest extends TestCase $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents remaining'); } + #[TestDox('byFields() succeeds when documents are deleted')] public function testByFieldsSucceedsWhenDocumentsAreDeleted(): void { $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents to start'); - Delete::byFields(ThrowawayDb::TABLE, [Field::NE('value', 'purple')]); + Delete::byFields(ThrowawayDb::TABLE, [Field::notEqual('value', 'purple')]); $this->assertEquals(2, Count::all(ThrowawayDb::TABLE), 'There should have been 2 documents remaining'); } + #[TestDox('byFields() succeeds when documents are not deleted')] public function testByFieldsSucceedsWhenDocumentsAreNotDeleted(): void { $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents to start'); - Delete::byFields(ThrowawayDb::TABLE, [Field::EQ('value', 'crimson')]); + Delete::byFields(ThrowawayDb::TABLE, [Field::equal('value', 'crimson')]); $this->assertEquals(5, Count::all(ThrowawayDb::TABLE), 'There should have been 5 documents remaining'); } + #[TestDox('byContains() fails')] public function testByContainsFails(): void { $this->expectException(DocumentException::class); Delete::byContains('', []); } - #[TestDox('By JSON Path fails')] + #[TestDox('byJsonPath() fails')] public function testByJsonPathFails(): void { $this->expectException(DocumentException::class); diff --git a/tests/integration/sqlite/DocumentListTest.php b/tests/integration/sqlite/DocumentListTest.php index 8962d69..8ea2647 100644 --- a/tests/integration/sqlite/DocumentListTest.php +++ b/tests/integration/sqlite/DocumentListTest.php @@ -35,6 +35,7 @@ class DocumentListTest extends TestCase parent::tearDown(); } + #[TestDox('create() succeeds')] public function testCreateSucceeds(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], @@ -43,6 +44,7 @@ class DocumentListTest extends TestCase $list = null; } + #[TestDox('items() succeeds')] public function testItemsSucceeds(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], @@ -57,6 +59,7 @@ class DocumentListTest extends TestCase $this->assertEquals(5, $count, 'There should have been 5 documents returned'); } + #[TestDox('items() fails when already consumed')] public function testItemsFailsWhenAlreadyConsumed(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], @@ -69,6 +72,7 @@ class DocumentListTest extends TestCase iterator_to_array($list->items()); } + #[TestDox('hasItems() succeeds with empty results')] public function testHasItemsSucceedsWithEmptyResults(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'num_value' < 0", [], @@ -77,6 +81,7 @@ class DocumentListTest extends TestCase $this->assertFalse($list->hasItems(), 'There should be no items in the list'); } + #[TestDox('hasItems() succeeds with non-empty results')] public function testHasItemsSucceedsWithNonEmptyResults(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], @@ -88,6 +93,8 @@ class DocumentListTest extends TestCase } $this->assertFalse($list->hasItems(), 'There should be no remaining items in the list'); } + + #[TestDox('map() succeeds')] public function testMapSucceeds(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], @@ -100,6 +107,7 @@ class DocumentListTest extends TestCase } } + #[TestDox('iter() succeeds')] public function testIterSucceeds(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], @@ -112,6 +120,7 @@ class DocumentListTest extends TestCase 'Iteration did not have the expected result'); } + #[TestDox('mapToArray() succeeds')] public function testMapToArraySucceeds(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], diff --git a/tests/integration/sqlite/DocumentTest.php b/tests/integration/sqlite/DocumentTest.php index b0b8f01..6abbbde 100644 --- a/tests/integration/sqlite/DocumentTest.php +++ b/tests/integration/sqlite/DocumentTest.php @@ -35,7 +35,7 @@ class DocumentTest extends TestCase parent::tearDown(); } - #[TestDox('Insert succeeds for array no auto ID')] + #[TestDox('insert() succeeds for array no auto ID')] public function testInsertSucceedsForArrayNoAutoId(): void { Document::insert(ThrowawayDb::TABLE, ['id' => 'turkey', 'sub' => ['foo' => 'gobble', 'bar' => 'gobble']]); @@ -50,7 +50,7 @@ class DocumentTest extends TestCase $this->assertEquals('gobble', $doc->sub->bar, 'The sub-document bar property was incorrect'); } - #[TestDox('Insert succeeds for array with auto number ID not provided')] + #[TestDox('insert() succeeds for array with auto number ID not provided')] public function testInsertSucceedsForArrayWithAutoNumberIdNotProvided(): void { Configuration::$autoId = AutoId::Number; @@ -74,7 +74,7 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for array with auto number ID with ID provided')] + #[TestDox('insert() succeeds for array with auto number ID with ID provided')] public function testInsertSucceedsForArrayWithAutoNumberIdWithIdProvided(): void { Configuration::$autoId = AutoId::Number; @@ -90,14 +90,14 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for array with auto UUID ID not provided')] + #[TestDox('insert() succeeds for array with auto UUID ID not provided')] public function testInsertSucceedsForArrayWithAutoUuidIdNotProvided(): void { Configuration::$autoId = AutoId::UUID; try { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); Document::insert(ThrowawayDb::TABLE, ['id' => '', 'num_value' => 5]); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 5)], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('num_value', 5)], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertNotEmpty($doc->get()->id, 'The ID should have been auto-generated'); } finally { @@ -105,7 +105,7 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for array with auto UUID ID with ID provided')] + #[TestDox('insert() succeeds for array with auto UUID ID with ID provided')] public function testInsertSucceedsForArrayWithAutoUuidIdWithIdProvided(): void { Configuration::$autoId = AutoId::UUID; @@ -113,7 +113,7 @@ class DocumentTest extends TestCase Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); $uuid = AutoId::generateUUID(); Document::insert(ThrowawayDb::TABLE, ['id' => $uuid, 'value' => 'uuid', 'num_value' => 12]); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 12)], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('num_value', 12)], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals($uuid, $doc->get()->id, 'The ID should not have been changed'); } finally { @@ -121,7 +121,7 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for array with auto string ID not provided')] + #[TestDox('insert() succeeds for array with auto string ID not provided')] public function testInsertSucceedsForArrayWithAutoStringIdNotProvided(): void { Configuration::$autoId = AutoId::RandomString; @@ -129,7 +129,7 @@ class DocumentTest extends TestCase try { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); Document::insert(ThrowawayDb::TABLE, ['id' => '', 'value' => 'new', 'num_value' => 8]); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 8)], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('num_value', 8)], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals(6, strlen($doc->get()->id), 'The ID should have been auto-generated and had 6 characters'); @@ -139,14 +139,14 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for array with auto string ID with ID provided')] + #[TestDox('insert() succeeds for array with auto string ID with ID provided')] public function testInsertSucceedsForArrayWithAutoStringIdWithIdProvided(): void { Configuration::$autoId = AutoId::RandomString; try { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); Document::insert(ThrowawayDb::TABLE, ['id' => 'my-key', 'value' => 'old', 'num_value' => 3]); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 3)], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('num_value', 3)], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals('my-key', $doc->get()->id, 'The ID should not have been changed'); } finally { @@ -154,7 +154,7 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for object no auto ID')] + #[TestDox('insert() succeeds for object no auto ID')] public function testInsertSucceedsForObjectNoAutoId(): void { Document::insert(ThrowawayDb::TABLE, new TestDocument('turkey', sub: new SubDocument('gobble', 'gobble'))); @@ -169,7 +169,7 @@ class DocumentTest extends TestCase $this->assertEquals('gobble', $doc->sub->bar, 'The sub-document bar property was incorrect'); } - #[TestDox('Insert succeeds for object with auto number ID not provided')] + #[TestDox('insert() succeeds for object with auto number ID not provided')] public function testInsertSucceedsForObjectWithAutoNumberIdNotProvided(): void { Configuration::$autoId = AutoId::Number; @@ -177,12 +177,12 @@ class DocumentTest extends TestCase Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); Document::insert(ThrowawayDb::TABLE, new NumDocument(value: 'taco')); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'taco')], NumDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'taco')], NumDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals(1, $doc->get()->id, 'The ID 1 should have been auto-generated'); Document::insert(ThrowawayDb::TABLE, new NumDocument(value: 'burrito')); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'burrito')], NumDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'burrito')], NumDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals(2, $doc->get()->id, 'The ID 2 should have been auto-generated'); } finally { @@ -190,14 +190,14 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for object with auto number ID with ID provided')] + #[TestDox('insert() succeeds for object with auto number ID with ID provided')] public function testInsertSucceedsForObjectWithAutoNumberIdWithIdProvided(): void { Configuration::$autoId = AutoId::Number; try { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); Document::insert(ThrowawayDb::TABLE, new NumDocument(64, 'large')); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'large')], NumDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'large')], NumDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals(64, $doc->get()->id, 'The ID 64 should have been stored'); } finally { @@ -205,14 +205,14 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for object with auto UUID ID not provided')] + #[TestDox('insert() succeeds for object with auto UUID ID not provided')] public function testInsertSucceedsForObjectWithAutoUuidIdNotProvided(): void { Configuration::$autoId = AutoId::UUID; try { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); Document::insert(ThrowawayDb::TABLE, new TestDocument(value: 'something', num_value: 9)); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EX('value')], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::exists('value')], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertNotEmpty($doc->get()->id, 'The ID should have been auto-generated'); } finally { @@ -220,7 +220,7 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for object with auto UUID ID with ID provided')] + #[TestDox('insert() succeeds for object with auto UUID ID with ID provided')] public function testInsertSucceedsForObjectWithAutoUuidIdWithIdProvided(): void { Configuration::$autoId = AutoId::UUID; @@ -228,7 +228,7 @@ class DocumentTest extends TestCase Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); $uuid = AutoId::generateUUID(); Document::insert(ThrowawayDb::TABLE, new TestDocument($uuid, num_value: 14)); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 14)], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('num_value', 14)], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals($uuid, $doc->get()->id, 'The ID should not have been changed'); } finally { @@ -236,7 +236,7 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for object with auto string ID not provided')] + #[TestDox('insert() succeeds for object with auto string ID not provided')] public function testInsertSucceedsForObjectWithAutoStringIdNotProvided(): void { Configuration::$autoId = AutoId::RandomString; @@ -244,7 +244,7 @@ class DocumentTest extends TestCase try { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); Document::insert(ThrowawayDb::TABLE, new TestDocument(num_value: 55)); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 55)], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('num_value', 55)], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals(40, strlen($doc->get()->id), 'The ID should have been auto-generated and had 40 characters'); @@ -254,14 +254,14 @@ class DocumentTest extends TestCase } } - #[TestDox('Insert succeeds for object with auto string ID with ID provided')] + #[TestDox('insert() succeeds for object with auto string ID with ID provided')] public function testInsertSucceedsForObjectWithAutoStringIdWithIdProvided(): void { Configuration::$autoId = AutoId::RandomString; try { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); Document::insert(ThrowawayDb::TABLE, new TestDocument('my-key', num_value: 3)); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 3)], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('num_value', 3)], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals('my-key', $doc->get()->id, 'The ID should not have been changed'); } finally { @@ -269,12 +269,14 @@ class DocumentTest extends TestCase } } + #[TestDox('insert() fails for duplicate key')] public function testInsertFailsForDuplicateKey(): void { $this->expectException(DocumentException::class); Document::insert(ThrowawayDb::TABLE, new TestDocument('one')); } + #[TestDox('save() succeeds when a document is inserted')] public function testSaveSucceedsWhenADocumentIsInserted(): void { Document::save(ThrowawayDb::TABLE, new TestDocument('test', sub: new SubDocument('a', 'b'))); @@ -282,6 +284,7 @@ class DocumentTest extends TestCase $this->assertTrue($doc->isSome(), 'There should have been a document returned'); } + #[TestDox('save() succeeds when a document is updated')] public function testSaveSucceedsWhenADocumentIsUpdated(): void { Document::save(ThrowawayDb::TABLE, new TestDocument('two', num_value: 44)); @@ -292,6 +295,7 @@ class DocumentTest extends TestCase $this->assertNull($doc->sub, 'The sub-document should have been null'); } + #[TestDox('update() succeeds when replacing a document')] public function testUpdateSucceedsWhenReplacingADocument(): void { Document::update(ThrowawayDb::TABLE, 'one', new TestDocument('one', 'howdy', 8, new SubDocument('y', 'z'))); @@ -305,6 +309,7 @@ class DocumentTest extends TestCase $this->assertEquals('z', $doc->sub->bar, 'The sub-document bar property was incorrect'); } + #[TestDox('update() succeeds when no document is replaced')] public function testUpdateSucceedsWhenNoDocumentIsReplaced(): void { Document::update(ThrowawayDb::TABLE, 'two-hundred', new TestDocument('200')); diff --git a/tests/integration/sqlite/ExistsTest.php b/tests/integration/sqlite/ExistsTest.php index 6b9911a..3d99d65 100644 --- a/tests/integration/sqlite/ExistsTest.php +++ b/tests/integration/sqlite/ExistsTest.php @@ -33,38 +33,41 @@ class ExistsTest extends TestCase parent::tearDown(); } - #[TestDox('By ID succeeds when a document exists')] + #[TestDox('byId() succeeds when a document exists')] public function testByIdSucceedsWhenADocumentExists(): void { $this->assertTrue(Exists::byId(ThrowawayDb::TABLE, 'three'), 'There should have been an existing document'); } - #[TestDox('By ID succeeds when a document does not exist')] + #[TestDox('byId() succeeds when a document does not exist')] public function testByIdSucceedsWhenADocumentDoesNotExist(): void { $this->assertFalse(Exists::byId(ThrowawayDb::TABLE, 'seven'), 'There should not have been an existing document'); } + #[TestDox('byFields() succeeds when documents exist')] public function testByFieldsSucceedsWhenDocumentsExist(): void { - $this->assertTrue(Exists::byFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 10)]), + $this->assertTrue(Exists::byFields(ThrowawayDb::TABLE, [Field::equal('num_value', 10)]), 'There should have been existing documents'); } + #[TestDox('byFields() succeeds when no matching documents exist')] public function testByFieldsSucceedsWhenNoMatchingDocumentsExist(): void { - $this->assertFalse(Exists::byFields(ThrowawayDb::TABLE, [Field::LT('nothing', 'none')]), + $this->assertFalse(Exists::byFields(ThrowawayDb::TABLE, [Field::less('nothing', 'none')]), 'There should not have been any existing documents'); } + #[TestDox('byContains() fails')] public function testByContainsFails(): void { $this->expectException(DocumentException::class); Exists::byContains('', []); } - #[TestDox('By JSON Path fails')] + #[TestDox('byJsonPath() fails')] public function testByJsonPathFails(): void { $this->expectException(DocumentException::class); diff --git a/tests/integration/sqlite/FindTest.php b/tests/integration/sqlite/FindTest.php index ed3f175..8f7a42d 100644 --- a/tests/integration/sqlite/FindTest.php +++ b/tests/integration/sqlite/FindTest.php @@ -8,9 +8,10 @@ declare(strict_types=1); namespace Test\Integration\SQLite; -use BitBadger\PDODocument\{Custom, Document, DocumentException, Field, Find}; +use BitBadger\PDODocument\{Custom, Delete, Document, DocumentException, Field, FieldMatch, Find}; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; +use Test\Integration\ArrayDocument; use Test\Integration\TestDocument; /** @@ -34,6 +35,7 @@ class FindTest extends TestCase parent::tearDown(); } + #[TestDox('all() succeeds when there is data')] public function testAllSucceedsWhenThereIsData(): void { $docs = Find::all(ThrowawayDb::TABLE, TestDocument::class); @@ -43,6 +45,35 @@ class FindTest extends TestCase $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')] public function testAllSucceedsWhenThereIsNoData(): void { Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); @@ -51,7 +82,7 @@ class FindTest extends TestCase $this->assertFalse($docs->hasItems(), 'There should have been no documents in the list'); } - #[TestDox('By ID succeeds when a document is found')] + #[TestDox('byId() succeeds when a document is found')] public function testByIdSucceedsWhenADocumentIsFound(): void { $doc = Find::byId(ThrowawayDb::TABLE, 'two', TestDocument::class); @@ -59,7 +90,7 @@ class FindTest extends TestCase $this->assertEquals('two', $doc->get()->id, 'An incorrect document was returned'); } - #[TestDox('By ID succeeds when a document is found with numeric ID')] + #[TestDox('byId() succeeds when a document is found with numeric ID')] public function testByIdSucceedsWhenADocumentIsFoundWithNumericId(): void { Document::insert(ThrowawayDb::TABLE, ['id' => 18, 'value' => 'howdy']); @@ -68,69 +99,130 @@ class FindTest extends TestCase $this->assertEquals('18', $doc->get()->id, 'An incorrect document was returned'); } - #[TestDox('By ID succeeds when a document is not found')] + #[TestDox('byId() succeeds when a document is not found')] public function testByIdSucceedsWhenADocumentIsNotFound(): void { $doc = Find::byId(ThrowawayDb::TABLE, 'seventy-five', TestDocument::class); $this->assertTrue($doc->isNone(), 'There should not have been a document returned'); } + #[TestDox('byFields() succeeds when documents are found')] public function testByFieldsSucceedsWhenDocumentsAreFound(): void { - $docs = Find::byFields(ThrowawayDb::TABLE, [Field::GT('num_value', 15)], TestDocument::class); + $docs = Find::byFields(ThrowawayDb::TABLE, [Field::in('value', ['blue', 'purple']), Field::exists('sub')], + TestDocument::class, FieldMatch::All); + $this->assertNotNull($docs, 'There should have been a document list returned'); + $count = 0; + foreach ($docs->items() as $ignored) $count++; + $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')] + public function testByFieldsSucceedsWhenDocumentsAreFoundUsingInWithNumericField(): void + { + $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'); + $count = 0; + foreach ($docs->items() as $ignored) $count++; + $this->assertEquals(1, $count, 'There should have been 1 document in the list'); + } + + #[TestDox('byFields() succeeds when no documents are found')] + public function testByFieldsSucceedsWhenNoDocumentsAreFound(): void + { + $docs = Find::byFields(ThrowawayDb::TABLE, [Field::greater('num_value', 100)], TestDocument::class); + $this->assertNotNull($docs, 'There should have been a document list returned'); + $this->assertFalse($docs->hasItems(), 'There should have been no documents in the list'); + } + + #[TestDox('byFields() succeeds for inArray when matching documents exist')] + public function testByFieldsSucceedsForInArrayWhenMatchingDocumentsExist(): void + { + Delete::byFields(ThrowawayDb::TABLE, [Field::notExists('absentField')]); + foreach (ArrayDocument::testDocuments() as $doc) Document::insert(ThrowawayDb::TABLE, $doc); + $docs = Find::byFields(ThrowawayDb::TABLE, [Field::inArray('values', ThrowawayDb::TABLE, ['c'])], + ArrayDocument::class); $this->assertNotNull($docs, 'There should have been a document list returned'); $count = 0; foreach ($docs->items() as $ignored) $count++; $this->assertEquals(2, $count, 'There should have been 2 documents in the list'); } - public function testByFieldsSucceedsWhenNoDocumentsAreFound(): void + #[TestDox('byFields() succeeds for inArray when no matching documents exist')] + public function testByFieldsSucceedsForInArrayWhenNoMatchingDocumentsExist(): void { - $docs = Find::byFields(ThrowawayDb::TABLE, [Field::GT('num_value', 100)], TestDocument::class); + Delete::byFields(ThrowawayDb::TABLE, [Field::notExists('absentField')]); + foreach (ArrayDocument::testDocuments() as $doc) Document::insert(ThrowawayDb::TABLE, $doc); + $docs = Find::byFields(ThrowawayDb::TABLE, [Field::inArray('values', ThrowawayDb::TABLE, ['j'])], + ArrayDocument::class); $this->assertNotNull($docs, 'There should have been a document list returned'); $this->assertFalse($docs->hasItems(), 'There should have been no documents in the list'); } + #[TestDox('byContains() fails')] public function testByContainsFails(): void { $this->expectException(DocumentException::class); Find::byContains('', [], TestDocument::class); } - #[TestDox('By JSON Path fails')] + #[TestDox('byJsonPath() fails')] public function testByJsonPathFails(): void { $this->expectException(DocumentException::class); Find::byJsonPath('', '', TestDocument::class); } + #[TestDox('firstByFields() succeeds when a document is found')] public function testFirstByFieldsSucceedsWhenADocumentIsFound(): void { - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'another')], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'another')], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertEquals('two', $doc->get()->id, 'The incorrect document was returned'); } + #[TestDox('firstByFields() succeeds when multiple documents are found')] public function testFirstByFieldsSucceedsWhenMultipleDocumentsAreFound(): void { - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('sub.foo', 'green')], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('sub.foo', 'green')], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document 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')] public function testFirstByFieldsSucceedsWhenADocumentIsNotFound(): void { - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('value', 'absent')], TestDocument::class); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'absent')], TestDocument::class); $this->assertTrue($doc->isNone(), 'There should not have been a document returned'); } + #[TestDox('firstByContains() fails')] public function testFirstByContainsFails(): void { $this->expectException(DocumentException::class); Find::firstByContains('', [], TestDocument::class); } - #[TestDox('First by JSON Path fails')] + #[TestDox('firstByJsonPath() fails')] public function testFirstByJsonPathFails(): void { $this->expectException(DocumentException::class); diff --git a/tests/integration/sqlite/PatchTest.php b/tests/integration/sqlite/PatchTest.php index 059b0cd..0a31936 100644 --- a/tests/integration/sqlite/PatchTest.php +++ b/tests/integration/sqlite/PatchTest.php @@ -34,7 +34,7 @@ class PatchTest extends TestCase parent::tearDown(); } - #[TestDox('By ID succeeds when a document is updated')] + #[TestDox('byId() succeeds when a document is updated')] public function testByIdSucceedsWhenADocumentIsUpdated(): void { Patch::byId(ThrowawayDb::TABLE, 'one', ['num_value' => 44]); @@ -43,33 +43,36 @@ class PatchTest extends TestCase $this->assertEquals(44, $doc->get()->num_value, 'The updated document is not correct'); } - #[TestDox('By ID succeeds when no document is updated')] + #[TestDox('byId() succeeds when no document is updated')] public function testByIdSucceedsWhenNoDocumentIsUpdated(): void { Patch::byId(ThrowawayDb::TABLE, 'forty-seven', ['foo' => 'green']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } + #[TestDox('byFields() succeeds when a document is updated')] public function testByFieldsSucceedsWhenADocumentIsUpdated(): void { - Patch::byFields(ThrowawayDb::TABLE, [Field::EQ('value', 'purple')], ['num_value' => 77]); - $after = Count::byFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 77)]); + Patch::byFields(ThrowawayDb::TABLE, [Field::equal('value', 'purple')], ['num_value' => 77]); + $after = Count::byFields(ThrowawayDb::TABLE, [Field::equal('num_value', 77)]); $this->assertEquals(2, $after, 'There should have been 2 documents updated'); } + #[TestDox('byFields() succeeds when no document is updated')] public function testByFieldsSucceedsWhenNoDocumentIsUpdated(): void { - Patch::byFields(ThrowawayDb::TABLE, [Field::EQ('value', 'burgundy')], ['foo' => 'green']); + Patch::byFields(ThrowawayDb::TABLE, [Field::equal('value', 'burgundy')], ['foo' => 'green']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } + #[TestDox('byContains() fails')] public function testByContainsFails(): void { $this->expectException(DocumentException::class); Patch::byContains('', [], []); } - #[TestDox('By JSON Path fails')] + #[TestDox('byJsonPath() fails')] public function testByJsonPathFails(): void { $this->expectException(DocumentException::class); diff --git a/tests/integration/sqlite/RemoveFieldsTest.php b/tests/integration/sqlite/RemoveFieldsTest.php index f0bdeb3..a0c46d8 100644 --- a/tests/integration/sqlite/RemoveFieldsTest.php +++ b/tests/integration/sqlite/RemoveFieldsTest.php @@ -34,7 +34,7 @@ class RemoveFieldsTest extends TestCase parent::tearDown(); } - #[TestDox('By ID succeeds when fields are removed')] + #[TestDox('byId() succeeds when fields are removed')] public function testByIdSucceedsWhenFieldsAreRemoved(): void { RemoveFields::byId(ThrowawayDb::TABLE, 'two', ['sub', 'value']); @@ -45,47 +45,51 @@ class RemoveFieldsTest extends TestCase $this->assertNull($doc->sub, 'Sub-document should have been null'); } - #[TestDox('By ID succeeds when a field is not removed')] + #[TestDox('byId() succeeds when a field is not removed')] public function testByIdSucceedsWhenAFieldIsNotRemoved(): void { RemoveFields::byId(ThrowawayDb::TABLE, 'one', ['a_field_that_does_not_exist']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } - #[TestDox('By ID succeeds when no document is matched')] + #[TestDox('byId() succeeds when no document is matched')] public function testByIdSucceedsWhenNoDocumentIsMatched(): void { RemoveFields::byId(ThrowawayDb::TABLE, 'fifty', ['sub']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } + #[TestDox('byFields() succeeds when a field is removed')] public function testByFieldsSucceedsWhenAFieldIsRemoved(): void { - RemoveFields::byFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 17)], ['sub']); - $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 17)], TestDocument::class); + RemoveFields::byFields(ThrowawayDb::TABLE, [Field::equal('num_value', 17)], ['sub']); + $doc = Find::firstByFields(ThrowawayDb::TABLE, [Field::equal('num_value', 17)], TestDocument::class); $this->assertTrue($doc->isSome(), 'There should have been a document returned'); $this->assertNull($doc->get()->sub, 'Sub-document should have been null'); } + #[TestDox('byFields() succeeds when a field is not removed')] public function testByFieldsSucceedsWhenAFieldIsNotRemoved(): void { - RemoveFields::byFields(ThrowawayDb::TABLE, [Field::EQ('num_value', 17)], ['nada']); + RemoveFields::byFields(ThrowawayDb::TABLE, [Field::equal('num_value', 17)], ['nada']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } + #[TestDox('byFields() succeeds when no document is matched')] public function testByFieldsSucceedsWhenNoDocumentIsMatched(): void { - RemoveFields::byFields(ThrowawayDb::TABLE, [Field::NE('missing', 'nope')], ['value']); + RemoveFields::byFields(ThrowawayDb::TABLE, [Field::notEqual('missing', 'nope')], ['value']); $this->assertTrue(true, 'The above not throwing an exception is the test'); } + #[TestDox('byContains() fails')] public function testByContainsFails(): void { $this->expectException(DocumentException::class); RemoveFields::byContains('', [], []); } - #[TestDox('By JSON Path fails')] + #[TestDox('byJsonPath() fails')] public function testByJsonPathFails(): void { $this->expectException(DocumentException::class); diff --git a/tests/unit/ConfigurationTest.php b/tests/unit/ConfigurationTest.php index e32b48a..acbffe1 100644 --- a/tests/unit/ConfigurationTest.php +++ b/tests/unit/ConfigurationTest.php @@ -18,14 +18,14 @@ use PHPUnit\Framework\TestCase; #[TestDox('Configuration (Unit tests)')] class ConfigurationTest extends TestCase { - #[TestDox('ID field default succeeds')] + #[TestDox('id default succeeds')] public function testIdFieldDefaultSucceeds(): void { $this->assertEquals('id', Configuration::$idField, 'Default ID field should be "id"'); } - #[TestDox('ID field change succeeds')] - public function testIdFieldChangeSucceeds() + #[TestDox('id change succeeds')] + public function testIdFieldChangeSucceeds(): void { try { Configuration::$idField = 'EyeDee'; @@ -36,19 +36,19 @@ class ConfigurationTest extends TestCase } } - #[TestDox('Auto ID default succeeds')] + #[TestDox('autoId default succeeds')] public function testAutoIdDefaultSucceeds(): void { $this->assertEquals(AutoId::None, Configuration::$autoId, 'Auto ID should default to None'); } - #[TestDox('ID string length default succeeds')] + #[TestDox('idStringLength default succeeds')] public function testIdStringLengthDefaultSucceeds(): void { $this->assertEquals(16, Configuration::$idStringLength, 'ID string length should default to 16'); } - #[TestDox("Db conn fails when no DSN specified")] + #[TestDox("dbConn() fails when no DSN specified")] public function testDbConnFailsWhenNoDSNSpecified(): void { $this->expectException(DocumentException::class); diff --git a/tests/unit/DocumentExceptionTest.php b/tests/unit/DocumentExceptionTest.php index 27df03d..c656055 100644 --- a/tests/unit/DocumentExceptionTest.php +++ b/tests/unit/DocumentExceptionTest.php @@ -38,6 +38,7 @@ class DocumentExceptionTest extends TestCase $this->assertNull($ex->getPrevious(), 'Prior exception should have been null'); } + #[TestDox('toString() succeeds without code')] public function testToStringSucceedsWithoutCode(): void { $ex = new DocumentException('Test failure'); @@ -45,6 +46,7 @@ class DocumentExceptionTest extends TestCase 'toString not generated correctly'); } + #[TestDox('toString() succeeds with code')] public function testToStringSucceedsWithCode(): void { $ex = new DocumentException('Oof', -6); diff --git a/tests/unit/FieldMatchTest.php b/tests/unit/FieldMatchTest.php index f2ea0f0..f1d0193 100644 --- a/tests/unit/FieldMatchTest.php +++ b/tests/unit/FieldMatchTest.php @@ -18,13 +18,13 @@ use PHPUnit\Framework\TestCase; #[TestDox('Field Match (Unit tests)')] class FieldMatchTest extends TestCase { - #[TestDox('To SQL succeeds for all')] + #[TestDox('toSQL() succeeds for All')] public function testToSQLSucceedsForAll(): void { $this->assertEquals('AND', FieldMatch::All->toSQL(), 'All should have returned AND'); } - #[TestDox('To SQL succeeds for any')] + #[TestDox('toSQL() succeeds for Any')] public function testToSQLSucceedsForAny(): void { $this->assertEquals('OR', FieldMatch::Any->toSQL(), 'Any should have returned OR'); diff --git a/tests/unit/FieldTest.php b/tests/unit/FieldTest.php index 0289b08..4bf06ec 100644 --- a/tests/unit/FieldTest.php +++ b/tests/unit/FieldTest.php @@ -18,121 +18,256 @@ use PHPUnit\Framework\TestCase; #[TestDox('Field (Unit tests)')] class FieldTest extends TestCase { - #[TestDox('Append parameter succeeds for EX')] - public function testAppendParameterSucceedsForEX(): void + #[TestDox('appendParameter() succeeds for exists')] + public function testAppendParameterSucceedsForExists(): void { - $this->assertEquals([], Field::EX('exists')->appendParameter([]), 'EX should not have appended a parameter'); + $this->assertEquals([], Field::exists('exists')->appendParameter([]), + 'exists should not have appended a parameter'); } - #[TestDox('Append parameter succeeds for NEX')] - public function testAppendParameterSucceedsForNEX(): void + #[TestDox('appendParameter() succeeds for notExists')] + public function testAppendParameterSucceedsForNotExists(): void { - $this->assertEquals([], Field::NEX('absent')->appendParameter([]), 'NEX should not have appended a parameter'); + $this->assertEquals([], Field::notExists('absent')->appendParameter([]), + 'notExists should not have appended a parameter'); } - #[TestDox('Append parameter succeeds for BT')] - public function testAppendParameterSucceedsForBT(): void + #[TestDox('appendParameter() succeeds for between')] + public function testAppendParameterSucceedsForBetween(): void { - $this->assertEquals(['@nummin' => 5, '@nummax' => 9], Field::BT('exists', 5, 9, '@num')->appendParameter([]), - 'BT should have appended min and max parameters'); + $this->assertEquals(['@nummin' => 5, '@nummax' => 9], + Field::between('exists', 5, 9, '@num')->appendParameter([]), + 'Between should have appended min and max parameters'); } + #[TestDox('appendParameter() succeeds for in')] + public function testAppendParameterSucceedsForIn(): void + { + $this->assertEquals([':val_0' => 'test', ':val_1' => 'unit', ':val_2' => 'great'], + Field::in('it', ['test', 'unit', 'great'], ':val')->appendParameter([]), + 'In should have appended 3 parameters for the input values'); + } + + #[TestDox('appendParameter() succeeds for inArray for PostgreSQL')] + public function testAppendParameterSucceedsForInArrayForPostgreSQL(): void + { + Configuration::overrideMode(Mode::PgSQL); + try { + $this->assertEquals([':bit_0' => '2', ':bit_1' => '8', ':bit_2' => '64'], + Field::inArray('it', 'table', [2, 8, 64], ':bit')->appendParameter([]), + 'InArray should have appended 3 string parameters for the input values'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('appendParameter() succeeds for inArray for SQLite')] + public function testAppendParameterSucceedsForInArrayForSQLite(): void + { + Configuration::overrideMode(Mode::SQLite); + try { + $this->assertEquals([':bit_0' => 2, ':bit_1' => 8, ':bit_2' => 64], + Field::inArray('it', 'table', [2, 8, 64], ':bit')->appendParameter([]), + 'InArray should have appended 3 parameters for the input values'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('appendParameter() succeeds for others')] public function testAppendParameterSucceedsForOthers(): void { - $this->assertEquals(['@test' => 33], Field::EQ('the_field', 33, '@test')->appendParameter([]), + $this->assertEquals(['@test' => 33], Field::equal('the_field', 33, '@test')->appendParameter([]), 'Field parameter not returned correctly'); } - #[TestDox('To where succeeds for EX without qualifier for PostgreSQL')] - public function testToWhereSucceedsForEXWithoutQualifierForPostgreSQL(): void + #[TestDox('path() succeeds for simple SQL path for PostgreSQL')] + public function testPathSucceedsForSimpleSqlPathForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); try { - $this->assertEquals("data->>'that_field' IS NOT NULL", Field::EX('that_field')->toWhere(), - 'WHERE fragment not generated correctly'); + $this->assertEquals("data->>'it'", Field::equal('it', 'that')->path(), + 'SQL value path not generated correctly'); } finally { Configuration::overrideMode(null); } } - #[TestDox('To where succeeds for EX without qualifier for SQLite')] - public function testToWhereSucceedsForEXWithoutQualifierForSQLite(): void + #[TestDox('path() succeeds for simple SQL path for SQLite')] + public function testPathSucceedsForSimpleSqlPathForSQLite(): void { Configuration::overrideMode(Mode::SQLite); try { - $this->assertEquals("data->>'that_field' IS NOT NULL", Field::EX('that_field')->toWhere(), - 'WHERE fragment not generated correctly'); + $this->assertEquals("data->>'top'", Field::equal('top', 'that')->path(), + 'SQL value path not generated correctly'); } finally { Configuration::overrideMode(null); } } - #[TestDox('To where succeeds for NEX without qualifier for PostgreSQL')] - public function testToWhereSucceedsForNEXWithoutQualifierForPostgreSQL(): void + #[TestDox('path() succeeds for nested SQL path for PostgreSQL')] + public function testPathSucceedsForNestedSqlPathForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); try { - $this->assertEquals("data->>'a_field' IS NULL", Field::NEX('a_field')->toWhere(), - 'WHERE fragment not generated correctly'); + $this->assertEquals("data#>>'{parts,to,the,path}'", Field::equal('parts.to.the.path', '')->path(), + 'SQL value path not generated correctly'); } finally { Configuration::overrideMode(null); } } - #[TestDox('To where succeeds for NEX without qualifier for SQLite')] - public function testToWhereSucceedsForNEXWithoutQualifierForSQLite(): void + #[TestDox('path() succeeds for nested SQL path for SQLite')] + public function testPathSucceedsForNestedSqlPathForSQLite(): void { Configuration::overrideMode(Mode::SQLite); try { - $this->assertEquals("data->>'a_field' IS NULL", Field::NEX('a_field')->toWhere(), - 'WHERE fragment not generated correctly'); + $this->assertEquals("data->'one'->'two'->>'three'", Field::equal('one.two.three', '')->path(), + 'SQL value path not generated correctly'); } finally { Configuration::overrideMode(null); } } - #[TestDox('To where succeeds for BT without qualifier for SQLite')] - public function testToWhereSucceedsForBTWithoutQualifierForSQLite(): void + #[TestDox('path() succeeds for simple JSON path for PostgreSQL')] + public function testPathSucceedsForSimpleJsonPathForPostgreSQL(): void + { + Configuration::overrideMode(Mode::PgSQL); + try { + $this->assertEquals("data->'it'", Field::equal('it', 'that')->path(true), + 'JSON value path not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('path() succeeds for simple JSON path for SQLite')] + public function testPathSucceedsForSimpleJsonPathForSQLite(): void { Configuration::overrideMode(Mode::SQLite); try { - $this->assertEquals("data->>'age' BETWEEN @agemin AND @agemax", Field::BT('age', 13, 17, '@age')->toWhere(), + $this->assertEquals("data->'top'", Field::equal('top', 'that')->path(true), + 'JSON value path not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('path() succeeds for nested JSON path for PostgreSQL')] + public function testPathSucceedsForNestedJsonPathForPostgreSQL(): void + { + Configuration::overrideMode(Mode::PgSQL); + try { + $this->assertEquals("data#>'{parts,to,the,path}'", Field::equal('parts.to.the.path', '')->path(true), + 'JSON value path not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('path() succeeds for nested JSON path for SQLite')] + public function testPathSucceedsForNestedJsonPathForSQLite(): void + { + Configuration::overrideMode(Mode::SQLite); + try { + $this->assertEquals("data->'one'->'two'->'three'", Field::equal('one.two.three', '')->path(true), + 'SQL value path not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('toWhere() succeeds for exists without qualifier for PostgreSQL')] + public function testToWhereSucceedsForExistsWithoutQualifierForPostgreSQL(): void + { + Configuration::overrideMode(Mode::PgSQL); + try { + $this->assertEquals("data->>'that_field' IS NOT NULL", Field::exists('that_field')->toWhere(), 'WHERE fragment not generated correctly'); } finally { Configuration::overrideMode(null); } } - #[TestDox('To where succeeds for BT without qualifier for PostgreSQL with numeric range')] - public function testToWhereSucceedsForBTWithoutQualifierForPostgreSQLWithNumericRange(): void + #[TestDox('toWhere() succeeds for exists without qualifier for SQLite')] + public function testToWhereSucceedsForExistsWithoutQualifierForSQLite(): void + { + Configuration::overrideMode(Mode::SQLite); + try { + $this->assertEquals("data->>'that_field' IS NOT NULL", Field::exists('that_field')->toWhere(), + 'WHERE fragment not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('toWhere() succeeds for notExists without qualifier for PostgreSQL')] + public function testToWhereSucceedsForNotExistsWithoutQualifierForPostgreSQL(): void + { + Configuration::overrideMode(Mode::PgSQL); + try { + $this->assertEquals("data->>'a_field' IS NULL", Field::notExists('a_field')->toWhere(), + 'WHERE fragment not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('toWhere() succeeds for notExists without qualifier for SQLite')] + public function testToWhereSucceedsForNotExistsWithoutQualifierForSQLite(): void + { + Configuration::overrideMode(Mode::SQLite); + try { + $this->assertEquals("data->>'a_field' IS NULL", Field::notExists('a_field')->toWhere(), + 'WHERE fragment not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('toWhere() succeeds for between without qualifier for SQLite')] + public function testToWhereSucceedsForBetweenWithoutQualifierForSQLite(): void + { + Configuration::overrideMode(Mode::SQLite); + try { + $this->assertEquals("data->>'age' BETWEEN @agemin AND @agemax", + Field::between('age', 13, 17, '@age')->toWhere(), 'WHERE fragment not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('toWhere() succeeds for between without qualifier for PostgreSQL with numeric range')] + public function testToWhereSucceedsForBetweenWithoutQualifierForPostgreSQLWithNumericRange(): void { Configuration::overrideMode(Mode::PgSQL); try { $this->assertEquals("(data->>'age')::numeric BETWEEN @agemin AND @agemax", - Field::BT('age', 13, 17, '@age')->toWhere(), 'WHERE fragment not generated correctly'); + Field::between('age', 13, 17, '@age')->toWhere(), 'WHERE fragment not generated correctly'); } finally { Configuration::overrideMode(null); } } - #[TestDox('To where succeeds for BT without qualifier for PostgreSQL with non-numeric range')] - public function testToWhereSucceedsForBTWithoutQualifierForPostgreSQLWithNonNumericRange(): void + #[TestDox('toWhere() succeeds for between without qualifier for PostgreSQL with non-numeric range')] + public function testToWhereSucceedsForBetweenWithoutQualifierForPostgreSQLWithNonNumericRange(): void { Configuration::overrideMode(Mode::PgSQL); try { $this->assertEquals("data->>'city' BETWEEN :citymin AND :citymax", - Field::BT('city', 'Atlanta', 'Chicago', ':city')->toWhere(), 'WHERE fragment not generated correctly'); + Field::between('city', 'Atlanta', 'Chicago', ':city')->toWhere(), + 'WHERE fragment not generated correctly'); } finally { Configuration::overrideMode(null); } } - #[TestDox('To where succeeds for BT with qualifier for SQLite')] - public function testToWhereSucceedsForBTWithQualifierForSQLite(): void + #[TestDox('toWhere() succeeds for between with qualifier for SQLite')] + public function testToWhereSucceedsForBetweenWithQualifierForSQLite(): void { Configuration::overrideMode(Mode::SQLite); try { - $field = Field::BT('age', 13, 17, '@age'); + $field = Field::between('age', 13, 17, '@age'); $field->qualifier = 'me'; $this->assertEquals("me.data->>'age' BETWEEN @agemin AND @agemax", $field->toWhere(), 'WHERE fragment not generated correctly'); @@ -141,12 +276,12 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds for BT with qualifier for PostgreSQL with numeric range')] - public function testToWhereSucceedsForBTWithQualifierForPostgreSQLWithNumericRange(): void + #[TestDox('toWhere() succeeds for between with qualifier for PostgreSQL with numeric range')] + public function testToWhereSucceedsForBetweenWithQualifierForPostgreSQLWithNumericRange(): void { Configuration::overrideMode(Mode::PgSQL); try { - $field = Field::BT('age', 13, 17, '@age'); + $field = Field::between('age', 13, 17, '@age'); $field->qualifier = 'me'; $this->assertEquals("(me.data->>'age')::numeric BETWEEN @agemin AND @agemax", $field->toWhere(), 'WHERE fragment not generated correctly'); @@ -155,12 +290,12 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds for BT with qualifier for PostgreSQL with non-numeric range')] - public function testToWhereSucceedsForBTWithQualifierForPostgreSQLWithNonNumericRange(): void + #[TestDox('toWhere() succeeds for between with qualifier for PostgreSQL with non-numeric range')] + public function testToWhereSucceedsForBetweenWithQualifierForPostgreSQLWithNonNumericRange(): void { Configuration::overrideMode(Mode::PgSQL); try { - $field = Field::BT('city', 'Atlanta', 'Chicago', ':city'); + $field = Field::between('city', 'Atlanta', 'Chicago', ':city'); $field->qualifier = 'me'; $this->assertEquals("me.data->>'city' BETWEEN :citymin AND :citymax", $field->toWhere(), 'WHERE fragment not generated correctly'); @@ -169,36 +304,102 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds for others without qualifier for PostgreSQL')] + #[TestDox('toWhere() succeeds for in for PostgreSQL with non-numeric values')] + public function testToWhereSucceedsForInForPostgreSQLWithNonNumericValues(): void + { + Configuration::overrideMode(Mode::PgSQL); + try { + $field = Field::in('test', ['Atlanta', 'Chicago'], ':city'); + $this->assertEquals("data->>'test' IN (:city_0, :city_1)", $field->toWhere(), + 'WHERE fragment not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('toWhere() succeeds for in for PostgreSQL with numeric values')] + public function testToWhereSucceedsForInForPostgreSQLWithNumericValues(): void + { + Configuration::overrideMode(Mode::PgSQL); + try { + $field = Field::in('even', [2, 4, 6], ':nbr'); + $this->assertEquals("(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)", $field->toWhere(), + 'WHERE fragment not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('toWhere() succeeds for in for SQLite')] + public function testToWhereSucceedsForInForSQLite(): void + { + Configuration::overrideMode(Mode::SQLite); + try { + $field = Field::in('test', ['Atlanta', 'Chicago'], ':city'); + $this->assertEquals("data->>'test' IN (:city_0, :city_1)", $field->toWhere(), + 'WHERE fragment not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('toWhere() succeeds for inArray for PostgreSQL')] + public function testToWhereSucceedsForInArrayForPostgreSQL(): void + { + Configuration::overrideMode(Mode::PgSQL); + try { + $field = Field::inArray('even', 'tbl', [2, 4, 6, 8], ':it'); + $this->assertEquals("data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]", $field->toWhere(), + 'WHERE fragment not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('toWhere() succeeds for inArray for SQLite')] + public function testToWhereSucceedsForInArrayForSQLite(): void + { + Configuration::overrideMode(Mode::SQLite); + try { + $field = Field::inArray('test', 'tbl', ['Atlanta', 'Chicago'], ':city'); + $this->assertEquals( + "EXISTS (SELECT 1 FROM json_each(tbl.data, '\$.test') WHERE value IN (:city_0, :city_1))", + $field->toWhere(), 'WHERE fragment not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('toWhere() succeeds for others without qualifier for PostgreSQL')] public function testToWhereSucceedsForOthersWithoutQualifierForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); try { - $this->assertEquals("data->>'some_field' = @value", Field::EQ('some_field', '', '@value')->toWhere(), + $this->assertEquals("data->>'some_field' = @value", Field::equal('some_field', '', '@value')->toWhere(), 'WHERE fragment not generated correctly'); } finally { Configuration::overrideMode(null); } } - #[TestDox('To where succeeds for others without qualifier for SQLite')] + #[TestDox('toWhere() succeeds for others without qualifier for SQLite')] public function testToWhereSucceedsForOthersWithoutQualifierForSQLite(): void { Configuration::overrideMode(Mode::SQLite); try { - $this->assertEquals("data->>'some_field' = @value", Field::EQ('some_field', '', '@value')->toWhere(), + $this->assertEquals("data->>'some_field' = @value", Field::equal('some_field', '', '@value')->toWhere(), 'WHERE fragment not generated correctly'); } finally { Configuration::overrideMode(null); } } - #[TestDox('To where succeeds with qualifier no parameter for PostgreSQL')] + #[TestDox('toWhere() succeeds with qualifier no parameter for PostgreSQL')] public function testToWhereSucceedsWithQualifierNoParameterForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); try { - $field = Field::EX('no_field'); + $field = Field::exists('no_field'); $field->qualifier = 'test'; $this->assertEquals("test.data->>'no_field' IS NOT NULL", $field->toWhere(), 'WHERE fragment not generated correctly'); @@ -207,12 +408,12 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds with qualifier no parameter for SQLite')] + #[TestDox('toWhere() succeeds with qualifier no parameter for SQLite')] public function testToWhereSucceedsWithQualifierNoParameterForSQLite(): void { Configuration::overrideMode(Mode::SQLite); try { - $field = Field::EX('no_field'); + $field = Field::exists('no_field'); $field->qualifier = 'test'; $this->assertEquals("test.data->>'no_field' IS NOT NULL", $field->toWhere(), 'WHERE fragment not generated correctly'); @@ -221,12 +422,12 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds with qualifier and parameter for PostgreSQL')] + #[TestDox('toWhere() succeeds with qualifier and parameter for PostgreSQL')] public function testToWhereSucceedsWithQualifierAndParameterForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); try { - $field = Field::LE('le_field', 18, '@it'); + $field = Field::lessOrEqual('le_field', 18, '@it'); $field->qualifier = 'q'; $this->assertEquals("(q.data->>'le_field')::numeric <= @it", $field->toWhere(), 'WHERE fragment not generated correctly'); @@ -235,12 +436,12 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds with qualifier and parameter for SQLite')] + #[TestDox('toWhere() succeeds with qualifier and parameter for SQLite')] public function testToWhereSucceedsWithQualifierAndParameterForSQLite(): void { Configuration::overrideMode(Mode::SQLite); try { - $field = Field::LE('le_field', 18, '@it'); + $field = Field::lessOrEqual('le_field', 18, '@it'); $field->qualifier = 'q'; $this->assertEquals("q.data->>'le_field' <= @it", $field->toWhere(), 'WHERE fragment not generated correctly'); @@ -249,204 +450,233 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds with sub-document for PostgreSQL')] - public function testToWhereSucceedsWithSubDocumentForPostgreSQL(): void + #[TestDox('equal() succeeds without parameter')] + public function testEqualSucceedsWithoutParameter(): void { - Configuration::overrideMode(Mode::PgSQL); - try { - $field = Field::EQ('sub.foo.bar', 22, '@it'); - $this->assertEquals("(data#>>'{sub,foo,bar}')::numeric = @it", $field->toWhere(), - 'WHERE fragment not generated correctly'); - } finally { - Configuration::overrideMode(null); - } - } - - #[TestDox('To where succeeds with sub-document for SQLite')] - public function testToWhereSucceedsWithSubDocumentForSQLite(): void - { - Configuration::overrideMode(Mode::SQLite); - try { - $field = Field::EQ('sub.foo.bar', 22, '@it'); - $this->assertEquals("data->>'sub'->>'foo'->>'bar' = @it", $field->toWhere(), - 'WHERE fragment not generated correctly'); - } finally { - Configuration::overrideMode(null); - } - } - - #[TestDox('EQ succeeds without parameter')] - public function testEQSucceedsWithoutParameter(): void - { - $field = Field::EQ('my_test', 9); + $field = Field::equal('my_test', 9); $this->assertNotNull($field, 'The field should not have been null'); $this->assertEquals('my_test', $field->fieldName, 'Field name not filled correctly'); - $this->assertEquals(Op::EQ, $field->op, 'Operation not filled correctly'); + $this->assertEquals(Op::Equal, $field->op, 'Operation not filled correctly'); $this->assertEquals(9, $field->value, 'Value not filled correctly'); $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); } - #[TestDox('EQ succeeds with parameter')] - public function testEQSucceedsWithParameter(): void + #[TestDox('equal() succeeds with parameter')] + public function testEqualSucceedsWithParameter(): void { - $field = Field::EQ('another_test', 'turkey', '@test'); + $field = Field::equal('another_test', 'turkey', '@test'); $this->assertNotNull($field, 'The field should not have been null'); $this->assertEquals('another_test', $field->fieldName, 'Field name not filled correctly'); - $this->assertEquals(Op::EQ, $field->op, 'Operation not filled correctly'); + $this->assertEquals(Op::Equal, $field->op, 'Operation not filled correctly'); $this->assertEquals('turkey', $field->value, 'Value not filled correctly'); $this->assertEquals('@test', $field->paramName, 'Parameter name not filled correctly'); } - #[TestDox('GT succeeds without parameter')] - public function testGTSucceedsWithoutParameter(): void + #[TestDox('greater() succeeds without parameter')] + public function testGreaterSucceedsWithoutParameter(): void { - $field = Field::GT('your_test', 4); + $field = Field::greater('your_test', 4); $this->assertNotNull($field, 'The field should not have been null'); $this->assertEquals('your_test', $field->fieldName, 'Field name not filled correctly'); - $this->assertEquals(Op::GT, $field->op, 'Operation not filled correctly'); + $this->assertEquals(Op::Greater, $field->op, 'Operation not filled correctly'); $this->assertEquals(4, $field->value, 'Value not filled correctly'); $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); } - #[TestDox('GT succeeds with parameter')] - public function testGTSucceedsWithParameter(): void + #[TestDox('greater() succeeds with parameter')] + public function testGreaterSucceedsWithParameter(): void { - $field = Field::GT('more_test', 'chicken', '@value'); + $field = Field::greater('more_test', 'chicken', '@value'); $this->assertNotNull($field, 'The field should not have been null'); $this->assertEquals('more_test', $field->fieldName, 'Field name not filled correctly'); - $this->assertEquals(Op::GT, $field->op, 'Operation not filled correctly'); + $this->assertEquals(Op::Greater, $field->op, 'Operation not filled correctly'); $this->assertEquals('chicken', $field->value, 'Value not filled correctly'); $this->assertEquals('@value', $field->paramName, 'Parameter name not filled correctly'); } - #[TestDox('GE succeeds without parameter')] - public function testGESucceedsWithoutParameter(): void + #[TestDox('greaterOrEqual() succeeds without parameter')] + public function testGreaterOrEqualSucceedsWithoutParameter(): void { - $field = Field::GE('their_test', 6); + $field = Field::greaterOrEqual('their_test', 6); $this->assertNotNull($field, 'The field should not have been null'); $this->assertEquals('their_test', $field->fieldName, 'Field name not filled correctly'); - $this->assertEquals(Op::GE, $field->op, 'Operation not filled correctly'); + $this->assertEquals(Op::GreaterOrEqual, $field->op, 'Operation not filled correctly'); $this->assertEquals(6, $field->value, 'Value not filled correctly'); $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); } - #[TestDox('GE succeeds with parameter')] - public function testGESucceedsWithParameter(): void + #[TestDox('greaterOrEqual() succeeds with parameter')] + public function testGreaterOrEqualSucceedsWithParameter(): void { - $field = Field::GE('greater_test', 'poultry', '@cluck'); + $field = Field::greaterOrEqual('greater_test', 'poultry', '@cluck'); $this->assertNotNull($field, 'The field should not have been null'); $this->assertEquals('greater_test', $field->fieldName, 'Field name not filled correctly'); - $this->assertEquals(Op::GE, $field->op, 'Operation not filled correctly'); + $this->assertEquals(Op::GreaterOrEqual, $field->op, 'Operation not filled correctly'); $this->assertEquals('poultry', $field->value, 'Value not filled correctly'); $this->assertEquals('@cluck', $field->paramName, 'Parameter name not filled correctly'); } - #[TestDox('LT succeeds without parameter')] - public function testLTSucceedsWithoutParameter(): void + #[TestDox('less() succeeds without parameter')] + public function testLessSucceedsWithoutParameter(): void { - $field = Field::LT('z', 32); + $field = Field::less('z', 32); $this->assertNotNull($field, 'The field should not have been null'); $this->assertEquals('z', $field->fieldName, 'Field name not filled correctly'); - $this->assertEquals(Op::LT, $field->op, 'Operation not filled correctly'); + $this->assertEquals(Op::Less, $field->op, 'Operation not filled correctly'); $this->assertEquals(32, $field->value, 'Value not filled correctly'); $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); } - #[TestDox('LT succeeds with parameter')] - public function testLTSucceedsWithParameter(): void + #[TestDox('less() succeeds with parameter')] + public function testLessSucceedsWithParameter(): void { - $field = Field::LT('additional_test', 'fowl', '@boo'); + $field = Field::less('additional_test', 'fowl', '@boo'); $this->assertNotNull($field, 'The field should not have been null'); $this->assertEquals('additional_test', $field->fieldName, 'Field name not filled correctly'); - $this->assertEquals(Op::LT, $field->op, 'Operation not filled correctly'); + $this->assertEquals(Op::Less, $field->op, 'Operation not filled correctly'); $this->assertEquals('fowl', $field->value, 'Value not filled correctly'); $this->assertEquals('@boo', $field->paramName, 'Parameter name not filled correctly'); } - #[TestDox('LE succeeds without parameter')] - public function testLESucceedsWithoutParameter(): void + #[TestDox('lessOrEqual() succeeds without parameter')] + public function testLessOrEqualSucceedsWithoutParameter(): void { - $field = Field::LE('g', 87); + $field = Field::lessOrEqual('g', 87); $this->assertNotNull($field, 'The field should not have been null'); $this->assertEquals('g', $field->fieldName, 'Field name not filled correctly'); - $this->assertEquals(Op::LE, $field->op, 'Operation not filled correctly'); + $this->assertEquals(Op::LessOrEqual, $field->op, 'Operation not filled correctly'); $this->assertEquals(87, $field->value, 'Value not filled correctly'); $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); } - #[TestDox('LE succeeds with parameter')] - public function testLESucceedsWithParameter(): void + #[TestDox('lessOrEqual() succeeds with parameter')] + public function testLessOrEqualSucceedsWithParameter(): void { - $field = Field::LE('lesser_test', 'hen', '@woo'); + $field = Field::lessOrEqual('lesser_test', 'hen', '@woo'); $this->assertNotNull($field, 'The field should not have been null'); $this->assertEquals('lesser_test', $field->fieldName, 'Field name not filled correctly'); - $this->assertEquals(Op::LE, $field->op, 'Operation not filled correctly'); + $this->assertEquals(Op::LessOrEqual, $field->op, 'Operation not filled correctly'); $this->assertEquals('hen', $field->value, 'Value not filled correctly'); $this->assertEquals('@woo', $field->paramName, 'Parameter name not filled correctly'); } - #[TestDox('NE succeeds without parameter')] - public function testNESucceedsWithoutParameter(): void + #[TestDox('notEqual() succeeds without parameter')] + public function testNotEqualSucceedsWithoutParameter(): void { - $field = Field::NE('j', 65); + $field = Field::notEqual('j', 65); $this->assertNotNull($field, 'The field should not have been null'); $this->assertEquals('j', $field->fieldName, 'Field name not filled correctly'); - $this->assertEquals(Op::NE, $field->op, 'Operation not filled correctly'); + $this->assertEquals(Op::NotEqual, $field->op, 'Operation not filled correctly'); $this->assertEquals(65, $field->value, 'Value not filled correctly'); $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); } - #[TestDox('NE succeeds with parameter')] - public function testNESucceedsWithParameter(): void + #[TestDox('notEqual() succeeds with parameter')] + public function testNotEqualSucceedsWithParameter(): void { - $field = Field::NE('unequal_test', 'egg', '@zoo'); + $field = Field::notEqual('unequal_test', 'egg', '@zoo'); $this->assertNotNull($field, 'The field should not have been null'); $this->assertEquals('unequal_test', $field->fieldName, 'Field name not filled correctly'); - $this->assertEquals(Op::NE, $field->op, 'Operation not filled correctly'); + $this->assertEquals(Op::NotEqual, $field->op, 'Operation not filled correctly'); $this->assertEquals('egg', $field->value, 'Value not filled correctly'); $this->assertEquals('@zoo', $field->paramName, 'Parameter name not filled correctly'); } - #[TestDox('BT succeeds without parameter')] - public function testBTSucceedsWithoutParameter(): void + #[TestDox('between() succeeds without parameter')] + public function testBetweenSucceedsWithoutParameter(): void { - $field = Field::BT('k', 'alpha', 'zed'); + $field = Field::between('k', 'alpha', 'zed'); $this->assertNotNull($field, 'The field should not have been null'); $this->assertEquals('k', $field->fieldName, 'Field name not filled correctly'); - $this->assertEquals(Op::BT, $field->op, 'Operation not filled correctly'); + $this->assertEquals(Op::Between, $field->op, 'Operation not filled correctly'); $this->assertEquals(['alpha', 'zed'], $field->value, 'Value not filled correctly'); $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); } - #[TestDox('BT succeeds with parameter')] - public function testBTSucceedsWithParameter(): void + #[TestDox('between() succeeds with parameter')] + public function testBetweenSucceedsWithParameter(): void { - $field = Field::BT('between_test', 18, 49, '@count'); + $field = Field::between('between_test', 18, 49, '@count'); $this->assertNotNull($field, 'The field should not have been null'); $this->assertEquals('between_test', $field->fieldName, 'Field name not filled correctly'); - $this->assertEquals(Op::BT, $field->op, 'Operation not filled correctly'); + $this->assertEquals(Op::Between, $field->op, 'Operation not filled correctly'); $this->assertEquals([18, 49], $field->value, 'Value not filled correctly'); $this->assertEquals('@count', $field->paramName, 'Parameter name not filled correctly'); } - #[TestDox('EX succeeds')] - public function testEXSucceeds(): void + #[TestDox('in() succeeds without parameter')] + public function testInSucceedsWithoutParameter(): void { - $field = Field::EX('be_there'); + $field = Field::in('test', [1, 2, 3]); + $this->assertNotNull($field, 'The field should not have been null'); + $this->assertEquals('test', $field->fieldName, 'Field name not filled correctly'); + $this->assertEquals(Op::In, $field->op, 'Operation not filled correctly'); + $this->assertEquals([1, 2, 3], $field->value, 'Value not filled correctly'); + $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); + } + + #[TestDox('in() succeeds with parameter')] + public function testInSucceedsWithParameter(): void + { + $field = Field::in('unit', ['a', 'b'], '@inParam'); + $this->assertNotNull($field, 'The field should not have been null'); + $this->assertEquals('unit', $field->fieldName, 'Field name not filled correctly'); + $this->assertEquals(Op::In, $field->op, 'Operation not filled correctly'); + $this->assertEquals(['a', 'b'], $field->value, 'Value not filled correctly'); + $this->assertEquals('@inParam', $field->paramName, 'Parameter name not filled correctly'); + } + + #[TestDox('inArray() succeeds without parameter')] + public function testInArraySucceedsWithoutParameter(): void + { + $field = Field::inArray('test', 'tbl', [1, 2, 3]); + $this->assertNotNull($field, 'The field should not have been null'); + $this->assertEquals('test', $field->fieldName, 'Field name not filled correctly'); + $this->assertEquals(Op::InArray, $field->op, 'Operation not filled correctly'); + $this->assertEquals(['table' => 'tbl', 'values' => [1, 2, 3]], $field->value, 'Value not filled correctly'); + $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); + } + + #[TestDox('inArray() succeeds with parameter')] + public function testInArraySucceedsWithParameter(): void + { + $field = Field::inArray('unit', 'tab', ['a', 'b'], '@inAParam'); + $this->assertNotNull($field, 'The field should not have been null'); + $this->assertEquals('unit', $field->fieldName, 'Field name not filled correctly'); + $this->assertEquals(Op::InArray, $field->op, 'Operation not filled correctly'); + $this->assertEquals(['table' => 'tab', 'values' => ['a', 'b']], $field->value, 'Value not filled correctly'); + $this->assertEquals('@inAParam', $field->paramName, 'Parameter name not filled correctly'); + } + + #[TestDox('exists() succeeds')] + public function testExistsSucceeds(): void + { + $field = Field::exists('be_there'); $this->assertNotNull($field, 'The field should not have been null'); $this->assertEquals('be_there', $field->fieldName, 'Field name not filled correctly'); - $this->assertEquals(Op::EX, $field->op, 'Operation not filled correctly'); + $this->assertEquals(Op::Exists, $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'); } - #[TestDox('NEX succeeds')] - public function testNEXSucceeds(): void + #[TestDox('notExists() succeeds')] + public function testNotExistsSucceeds(): void { - $field = Field::NEX('be_absent'); + $field = Field::notExists('be_absent'); $this->assertNotNull($field, 'The field should not have been null'); $this->assertEquals('be_absent', $field->fieldName, 'Field name not filled correctly'); - $this->assertEquals(Op::NEX, $field->op, 'Operation not filled correctly'); + $this->assertEquals(Op::NotExists, $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'); + } + + #[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'); } diff --git a/tests/unit/Mapper/ArrayMapperTest.php b/tests/unit/Mapper/ArrayMapperTest.php index b0537a6..d7e84f9 100644 --- a/tests/unit/Mapper/ArrayMapperTest.php +++ b/tests/unit/Mapper/ArrayMapperTest.php @@ -18,6 +18,7 @@ use PHPUnit\Framework\TestCase; #[TestDox('Array Mapper (Unit tests)')] class ArrayMapperTest extends TestCase { + #[TestDox('map() succeeds')] public function testMapSucceeds(): void { $result = ['one' => 2, 'three' => 4, 'eight' => 'five']; diff --git a/tests/unit/Mapper/CountMapperTest.php b/tests/unit/Mapper/CountMapperTest.php index e59694d..67155c9 100644 --- a/tests/unit/Mapper/CountMapperTest.php +++ b/tests/unit/Mapper/CountMapperTest.php @@ -18,6 +18,7 @@ use PHPUnit\Framework\TestCase; #[TestDox('Count Mapper (Unit tests)')] class CountMapperTest extends TestCase { + #[TestDox('map() succeeds')] public function testMapSucceeds(): void { $this->assertEquals(5, (new CountMapper())->map([5, 8, 10]), 'Count not correct'); diff --git a/tests/unit/Mapper/DocumentMapperTest.php b/tests/unit/Mapper/DocumentMapperTest.php index fa7bc95..7a03f4a 100644 --- a/tests/unit/Mapper/DocumentMapperTest.php +++ b/tests/unit/Mapper/DocumentMapperTest.php @@ -44,7 +44,7 @@ class DocumentMapperTest extends TestCase $this->assertEquals('json', $mapper->fieldName, 'Field name not recorded correctly'); } - #[TestDox('Map succeeds with valid JSON')] + #[TestDox('map() succeeds with valid JSON')] public function testMapSucceedsWithValidJSON(): void { $doc = (new DocumentMapper(TestDocument::class))->map(['data' => '{"id":7,"subDoc":{"id":22,"name":"tester"}}']); @@ -55,7 +55,7 @@ class DocumentMapperTest extends TestCase $this->assertEquals('tester', $doc->subDoc->name, 'Sub-document name not filled correctly'); } - #[TestDox('Map succeeds with valid JSON for pjson class')] + #[TestDox('map() succeeds with valid JSON for Pjson class')] public function testMapSucceedsWithValidJSONForPjsonClass(): void { $doc = (new DocumentMapper(PjsonDocument::class))->map(['data' => '{"id":"seven","name":"bob","num_value":8}']); @@ -66,14 +66,14 @@ class DocumentMapperTest extends TestCase $this->assertFalse(isset($doc->skipped), 'Non-JSON field has not been set'); } - #[TestDox('Map fails with invalid JSON')] + #[TestDox('map() fails with invalid JSON')] public function testMapFailsWithInvalidJSON(): void { $this->expectException(DocumentException::class); (new DocumentMapper(TestDocument::class))->map(['data' => 'this is not valid']); } - #[TestDox('Map fails with invalid JSON for pjson class')] + #[TestDox('map() fails with invalid JSON for Pjson class')] public function testMapFailsWithInvalidJSONForPjsonClass(): void { $this->expectException(DocumentException::class); diff --git a/tests/unit/Mapper/ExistsMapperTest.php b/tests/unit/Mapper/ExistsMapperTest.php index 25d81d4..22e3f45 100644 --- a/tests/unit/Mapper/ExistsMapperTest.php +++ b/tests/unit/Mapper/ExistsMapperTest.php @@ -19,7 +19,7 @@ use PHPUnit\Framework\TestCase; #[TestDox('Exists Mapper (Unit tests)')] class ExistsMapperTest extends TestCase { - #[TestDox('Map succeeds for PostgreSQL')] + #[TestDox('map() succeeds for PostgreSQL')] public function testMapSucceedsForPostgreSQL(): void { try { @@ -30,7 +30,7 @@ class ExistsMapperTest extends TestCase } } - #[TestDox('Map succeeds for SQLite')] + #[TestDox('map() succeeds for SQLite')] public function testMapSucceedsForSQLite(): void { try { @@ -41,6 +41,7 @@ class ExistsMapperTest extends TestCase } } + #[TestDox('map() fails when mode not set')] public function testMapFailsWhenModeNotSet(): void { $this->expectException(DocumentException::class); diff --git a/tests/unit/Mapper/StringMapperTest.php b/tests/unit/Mapper/StringMapperTest.php index a0805cc..3a4d0bc 100644 --- a/tests/unit/Mapper/StringMapperTest.php +++ b/tests/unit/Mapper/StringMapperTest.php @@ -18,21 +18,24 @@ use PHPUnit\Framework\TestCase; #[TestDox('String Mapper (Unit tests)')] class StringMapperTest extends TestCase { - public function testMapSucceedsWhenFieldIsPresentAndString() + #[TestDox('map() succeeds when field is present and string')] + public function testMapSucceedsWhenFieldIsPresentAndString(): void { $result = ['test_field' => 'test_value']; $mapper = new StringMapper('test_field'); $this->assertEquals('test_value', $mapper->map($result), 'String value not returned correctly'); } - public function testMapSucceedsWhenFieldIsPresentAndNotString() + #[TestDox('map() succeeds when field is present and not string')] + public function testMapSucceedsWhenFieldIsPresentAndNotString(): void { $result = ['a_number' => 6.7]; $mapper = new StringMapper('a_number'); $this->assertEquals('6.7', $mapper->map($result), 'Number value not returned correctly'); } - public function testMapSucceedsWhenFieldIsNotPresent() + #[TestDox('map() succeeds when field is not present')] + public function testMapSucceedsWhenFieldIsNotPresent(): void { $mapper = new StringMapper('something_else'); $this->assertNull($mapper->map([]), 'Missing value not returned correctly'); diff --git a/tests/unit/ModeTest.php b/tests/unit/ModeTest.php index aaede62..0987bcd 100644 --- a/tests/unit/ModeTest.php +++ b/tests/unit/ModeTest.php @@ -18,19 +18,19 @@ use PHPUnit\Framework\TestCase; #[TestDox('Mode (Unit tests)')] class ModeTest extends TestCase { - #[TestDox('Derive from DSN succeeds for PostgreSQL')] + #[TestDox('deriveFromDSN() succeeds for PostgreSQL')] public function testDeriveFromDSNSucceedsForPostgreSQL(): void { $this->assertEquals(Mode::PgSQL, Mode::deriveFromDSN('pgsql:Host=localhost'), 'PostgreSQL mode incorrect'); } - #[TestDox('Derive from DSN succeeds for SQLite')] + #[TestDox('deriveFromDSN() succeeds for SQLite')] public function testDeriveFromDSNSucceedsForSQLite(): void { $this->assertEquals(Mode::SQLite, Mode::deriveFromDSN('sqlite:data.db'), 'SQLite mode incorrect'); } - #[TestDox('Derive from DSN fails for MySQL')] + #[TestDox('deriveFromDSN() fails for MySQL')] public function testDeriveFromDSNFailsForMySQL(): void { $this->expectException(DocumentException::class); diff --git a/tests/unit/OpTest.php b/tests/unit/OpTest.php index 120ee32..380d24f 100644 --- a/tests/unit/OpTest.php +++ b/tests/unit/OpTest.php @@ -18,57 +18,69 @@ use PHPUnit\Framework\TestCase; #[TestDox('Op (Unit tests)')] class OpTest extends TestCase { - #[TestDox('To SQL succeeds for EQ')] - public function testToSQLSucceedsForEQ(): void + #[TestDox('toSQL() succeeds for Equal')] + public function testToSQLSucceedsForEqual(): void { - $this->assertEquals('=', Op::EQ->toSQL(), 'EQ operator incorrect'); + $this->assertEquals('=', Op::Equal->toSQL(), 'Equal SQL operator incorrect'); } - #[TestDox('To SQL succeeds for GT')] - public function testToSQLSucceedsForGT(): void + #[TestDox('toSQL() succeeds for Greater')] + public function testToSQLSucceedsForGreater(): void { - $this->assertEquals('>', Op::GT->toSQL(), 'GT operator incorrect'); + $this->assertEquals('>', Op::Greater->toSQL(), 'Greater SQL operator incorrect'); } - #[TestDox('To SQL succeeds for GE')] - public function testToSQLSucceedsForGE(): void + #[TestDox('toSQL() succeeds for GreaterOrEqual')] + public function testToSQLSucceedsForGreaterOrEqual(): void { - $this->assertEquals('>=', Op::GE->toSQL(), 'GE operator incorrect'); + $this->assertEquals('>=', Op::GreaterOrEqual->toSQL(), 'GreaterOrEqual SQL operator incorrect'); } - #[TestDox('To SQL succeeds for LT')] - public function testToSQLSucceedsForLT(): void + #[TestDox('toSQL() succeeds for Less')] + public function testToSQLSucceedsForLess(): void { - $this->assertEquals('<', Op::LT->toSQL(), 'LT operator incorrect'); + $this->assertEquals('<', Op::Less->toSQL(), 'Less SQL operator incorrect'); } - #[TestDox('To SQL succeeds for LE')] - public function testToSQLSucceedsForLE(): void + #[TestDox('toSQL() succeeds for LessOrEqual')] + public function testToSQLSucceedsForLessOrEqual(): void { - $this->assertEquals('<=', Op::LE->toSQL(), 'LE operator incorrect'); + $this->assertEquals('<=', Op::LessOrEqual->toSQL(), 'LessOrEqual SQL operator incorrect'); } - #[TestDox('To SQL succeeds for NE')] - public function testToSQLSucceedsForNE(): void + #[TestDox('toSQL() succeeds for NotEqual')] + public function testToSQLSucceedsForNotEqual(): void { - $this->assertEquals('<>', Op::NE->toSQL(), 'NE operator incorrect'); + $this->assertEquals('<>', Op::NotEqual->toSQL(), 'NotEqual SQL operator incorrect'); } - #[TestDox('To SQL succeeds for BT')] - public function testToSQLSucceedsForBT(): void + #[TestDox('toSQL() succeeds for Between')] + public function testToSQLSucceedsForBetween(): void { - $this->assertEquals('BETWEEN', Op::BT->toSQL(), 'BT operator incorrect'); + $this->assertEquals('BETWEEN', Op::Between->toSQL(), 'Between SQL operator incorrect'); } - #[TestDox('To SQL succeeds for EX')] - public function testToSQLSucceedsForEX(): void + #[TestDox('toSQL() succeeds for In')] + public function testToSQLSucceedsForIn(): void { - $this->assertEquals('IS NOT NULL', Op::EX->toSQL(), 'EX operator incorrect'); + $this->assertEquals('IN', Op::In->toSQL(), 'In SQL operator incorrect'); } - #[TestDox('To SQL succeeds for NEX')] + #[TestDox('toSQL() succeeds for InArray')] + public function testToSQLSucceedsForInArray(): void + { + $this->assertEquals('??|', Op::InArray->toSQL(), 'InArray SQL operator incorrect'); + } + + #[TestDox('toSQL() succeeds for Exists')] + public function testToSQLSucceedsForExists(): void + { + $this->assertEquals('IS NOT NULL', Op::Exists->toSQL(), 'Exists SQL operator incorrect'); + } + + #[TestDox('toSQL() succeeds for NotExists')] public function testToSQLSucceedsForNEX(): void { - $this->assertEquals('IS NULL', Op::NEX->toSQL(), 'NEX operator incorrect'); + $this->assertEquals('IS NULL', Op::NotExists->toSQL(), 'NotExists SQL operator incorrect'); } } diff --git a/tests/unit/ParametersTest.php b/tests/unit/ParametersTest.php index 31b06ba..68da6d9 100644 --- a/tests/unit/ParametersTest.php +++ b/tests/unit/ParametersTest.php @@ -20,18 +20,19 @@ use Test\{PjsonDocument, PjsonId}; #[TestDox('Parameters (Unit tests)')] class ParametersTest extends TestCase { - #[TestDox('ID succeeds with string')] + #[TestDox('id() succeeds with string')] public function testIdSucceedsWithString(): void { $this->assertEquals([':id' => 'key'], Parameters::id('key'), 'ID parameter not constructed correctly'); } - #[TestDox('ID succeeds with non string')] + #[TestDox('id() succeeds with non string')] public function testIdSucceedsWithNonString(): void { $this->assertEquals([':id' => '7'], Parameters::id(7), 'ID parameter not constructed correctly'); } + #[TestDox('json() succeeds for array')] public function testJsonSucceedsForArray(): void { $this->assertEquals([':it' => '{"id":18,"url":"https://www.unittest.com"}'], @@ -39,20 +40,21 @@ class ParametersTest extends TestCase 'JSON parameter not constructed correctly'); } + #[TestDox('json() succeeds for array with empty array parameter')] public function testJsonSucceedsForArrayWithEmptyArrayParameter(): void { $this->assertEquals([':it' => '{"id":18,"urls":[]}'], Parameters::json(':it', ['id' => 18, 'urls' => []]), 'JSON parameter not constructed correctly'); } - #[TestDox('json succeeds for 1D array with empty array parameter')] + #[TestDox('json() succeeds for 1D array with empty array parameter')] public function testJsonSucceedsFor1DArrayWithEmptyArrayParameter(): void { $this->assertEquals([':it' => '{"urls":[]}'], Parameters::json(':it', ['urls' => []]), 'JSON parameter not constructed correctly'); } - #[TestDox('json succeeds for stdClass')] + #[TestDox('json() succeeds for stdClass')] public function testJsonSucceedsForStdClass(): void { $obj = new stdClass(); @@ -62,6 +64,7 @@ class ParametersTest extends TestCase 'JSON parameter not constructed correctly'); } + #[TestDox('json() succeeds for Pjson class')] public function testJsonSucceedsForPjsonClass(): void { $this->assertEquals([':it' => '{"id":"999","name":"a test","num_value":98}'], @@ -69,6 +72,7 @@ class ParametersTest extends TestCase 'JSON parameter not constructed correctly'); } + #[TestDox('json() succeeds for array of Pjson class')] public function testJsonSucceedsForArrayOfPjsonClass(): void { $this->assertEquals([':it' => '{"pjson":[{"id":"997","name":"another test","num_value":94}]}'], @@ -77,23 +81,26 @@ class ParametersTest extends TestCase 'JSON parameter not constructed correctly'); } + #[TestDox('nameFields() succeeds')] public function testNameFieldsSucceeds(): void { - $named = Parameters::nameFields([Field::EQ('it', 17), Field::EQ('also', 22, ':also'), Field::EQ('other', 24)]); + $named = [Field::equal('it', 17), Field::equal('also', 22, ':also'), Field::equal('other', 24)]; + Parameters::nameFields($named); $this->assertCount(3, $named, 'There should be 3 parameters in the array'); $this->assertEquals(':field0', $named[0]->paramName, 'Parameter 1 not named correctly'); $this->assertEquals(':also', $named[1]->paramName, 'Parameter 2 not named correctly'); $this->assertEquals(':field2', $named[2]->paramName, 'Parameter 3 not named correctly'); } + #[TestDox('addFields() succeeds')] public function testAddFieldsSucceeds(): void { $this->assertEquals([':a' => 1, ':b' => 'two', ':z' => 18], - Parameters::addFields([Field::EQ('b', 'two', ':b'), Field::EQ('z', 18, ':z')], [':a' => 1]), + Parameters::addFields([Field::equal('b', 'two', ':b'), Field::equal('z', 18, ':z')], [':a' => 1]), 'Field parameters not added correctly'); } - #[TestDox('Field names succeeds for PostgreSQL')] + #[TestDox('fieldNames() succeeds for PostgreSQL')] public function testFieldNamesSucceedsForPostgreSQL(): void { try { @@ -105,7 +112,7 @@ class ParametersTest extends TestCase } } - #[TestDox('Field names succeeds for SQLite')] + #[TestDox('fieldNames() succeeds for SQLite')] public function testFieldNamesSucceedsForSQLite(): void { try { @@ -117,6 +124,7 @@ class ParametersTest extends TestCase } } + #[TestDox('fieldNames() fails when mode not set')] public function testFieldNamesFailsWhenModeNotSet(): void { $this->expectException(DocumentException::class); diff --git a/tests/unit/Query/CountTest.php b/tests/unit/Query/CountTest.php index 89a2726..cdad3ac 100644 --- a/tests/unit/Query/CountTest.php +++ b/tests/unit/Query/CountTest.php @@ -25,21 +25,23 @@ class CountTest extends TestCase parent::tearDown(); } + #[TestDox('all() succeeds')] public function testAllSucceeds(): void { $this->assertEquals('SELECT COUNT(*) FROM a_table', Count::all('a_table'), 'SELECT statement not generated correctly'); } + #[TestDox('byFields() succeeds')] public function testByFieldsSucceeds(): void { Configuration::overrideMode(Mode::SQLite); $this->assertEquals("SELECT COUNT(*) FROM somewhere WHERE data->>'errors' > :errors", - Count::byFields('somewhere', [Field::GT('errors', 10, ':errors')]), + Count::byFields('somewhere', [Field::greater('errors', 10, ':errors')]), 'SELECT statement not generated correctly'); } - #[TestDox('By contains succeeds for PostgreSQL')] + #[TestDox('byContains() succeeds for PostgreSQL')] public function testByContainsSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -47,14 +49,14 @@ class CountTest extends TestCase 'SELECT statement not generated correctly'); } - #[TestDox('By contains fails for non PostgreSQL')] + #[TestDox('byContains() fails for non PostgreSQL')] public function testByContainsFailsForNonPostgreSQL(): void { $this->expectException(DocumentException::class); Count::byContains(''); } - #[TestDox('By JSON Path succeeds for PostgreSQL')] + #[TestDox('byJsonPath() succeeds for PostgreSQL')] public function testByJsonPathSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -62,7 +64,7 @@ class CountTest extends TestCase Count::byJsonPath('a_table'), 'SELECT statement not generated correctly'); } - #[TestDox('By JSON Path fails for non PostgreSQL')] + #[TestDox('byJsonPath() fails for non PostgreSQL')] public function testByJsonPathFailsForNonPostgreSQL(): void { $this->expectException(DocumentException::class); diff --git a/tests/unit/Query/DefinitionTest.php b/tests/unit/Query/DefinitionTest.php index 879fa3b..3af4c42 100644 --- a/tests/unit/Query/DefinitionTest.php +++ b/tests/unit/Query/DefinitionTest.php @@ -25,7 +25,7 @@ class DefinitionTest extends TestCase parent::tearDown(); } - #[TestDox('Ensure table succeeds for PosgtreSQL')] + #[TestDox('ensureTable() succeeds for PosgtreSQL')] public function testEnsureTableSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -33,7 +33,7 @@ class DefinitionTest extends TestCase Definition::ensureTable('documents'), 'CREATE TABLE statement not generated correctly'); } - #[TestDox('Ensure table succeeds for SQLite')] + #[TestDox('ensureTable() succeeds for SQLite')] public function testEnsureTableSucceedsForSQLite(): void { Configuration::overrideMode(Mode::SQLite); @@ -41,18 +41,21 @@ class DefinitionTest extends TestCase 'CREATE TABLE statement not generated correctly'); } + #[TestDox('ensureTable() fails when mode not set')] public function testEnsureTableFailsWhenModeNotSet(): void { $this->expectException(DocumentException::class); Definition::ensureTable('boom'); } + #[TestDox('ensureIndexOn() succeeds without schema single ascending field')] public function testEnsureIndexOnSucceedsWithoutSchemaSingleAscendingField(): void { $this->assertEquals("CREATE INDEX IF NOT EXISTS idx_test_fields ON test ((data->>'details'))", Definition::ensureIndexOn('test', 'fields', ['details']), 'CREATE INDEX statement not generated correctly'); } + #[TestDox('ensureIndexOn() succeeds with schema multiple fields')] public function testEnsureIndexOnSucceedsWithSchemaMultipleFields(): void { $this->assertEquals( @@ -61,12 +64,14 @@ class DefinitionTest extends TestCase 'CREATE INDEX statement not generated correctly'); } + #[TestDox('ensureKey() succeeds')] public function testEnsureKeySucceeds(): void { $this->assertEquals("CREATE UNIQUE INDEX IF NOT EXISTS idx_tbl_key ON tbl ((data->>'id'))", Definition::ensureKey('tbl'), 'CREATE INDEX statement for document key not generated correctly'); } + #[TestDox('ensureDocumentIndexOn() succeeds for schema and Full')] public function testEnsureDocumentIndexOnSucceedsForSchemaAndFull(): void { Configuration::overrideMode(Mode::PgSQL); @@ -74,6 +79,7 @@ class DefinitionTest extends TestCase Definition::ensureDocumentIndexOn('my.tbl', DocumentIndex::Full)); } + #[TestDox('ensureDocumentIndexOn() succeeds for no schema and Optimized')] public function testEnsureDocumentIndexOnSucceedsForNoSchemaAndOptimized(): void { Configuration::overrideMode(Mode::PgSQL); @@ -81,7 +87,7 @@ class DefinitionTest extends TestCase Definition::ensureDocumentIndexOn('it', DocumentIndex::Optimized)); } - #[TestDox('Ensure document index on fails for non PostgreSQL')] + #[TestDox('ensureDocumentIndexOn() fails for non PostgreSQL')] public function testEnsureDocumentIndexOnFailsForNonPostgreSQL(): void { $this->expectException(DocumentException::class); diff --git a/tests/unit/Query/DeleteTest.php b/tests/unit/Query/DeleteTest.php index a9a2424..0c3adce 100644 --- a/tests/unit/Query/DeleteTest.php +++ b/tests/unit/Query/DeleteTest.php @@ -24,7 +24,7 @@ class DeleteTest extends TestCase Configuration::overrideMode(null); } - #[TestDox('By ID succeeds')] + #[TestDox('byId() succeeds')] public function testByIdSucceeds(): void { Configuration::overrideMode(Mode::SQLite); @@ -32,15 +32,17 @@ class DeleteTest extends TestCase 'DELETE statement not constructed correctly'); } + #[TestDox('byFields() succeeds')] public function testByFieldsSucceeds(): void { Configuration::overrideMode(Mode::SQLite); $this->assertEquals("DELETE FROM my_table WHERE data->>'value' < :max AND data->>'value' >= :min", - Delete::byFields('my_table', [Field::LT('value', 99, ':max'), Field::GE('value', 18, ':min')]), + Delete::byFields('my_table', + [Field::less('value', 99, ':max'), Field::greaterOrEqual('value', 18, ':min')]), 'DELETE statement not constructed correctly'); } - #[TestDox('By contains succeeds for PostgreSQL')] + #[TestDox('byContains() succeeds for PostgreSQL')] public function testByContainsSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -48,14 +50,14 @@ class DeleteTest extends TestCase 'DELETE statement not constructed correctly'); } - #[TestDox('By contains fails for non PostgreSQL')] + #[TestDox('byContains() fails for non PostgreSQL')] public function testByContainsFailsForNonPostgreSQL(): void { $this->expectException(DocumentException::class); Delete::byContains(''); } - #[TestDox('By JSON Path succeeds for PostgreSQL')] + #[TestDox('byJsonPath() succeeds for PostgreSQL')] public function testByJsonPathSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -63,7 +65,7 @@ class DeleteTest extends TestCase Delete::byJsonPath('here'), 'DELETE statement not constructed correctly'); } - #[TestDox('By JSON Path fails for non PostgreSQL')] + #[TestDox('byJsonPath() fails for non PostgreSQL')] public function testByJsonPathFailsForNonPostgreSQL(): void { $this->expectException(DocumentException::class); diff --git a/tests/unit/Query/ExistsTest.php b/tests/unit/Query/ExistsTest.php index ffa9ab5..1eb0fa4 100644 --- a/tests/unit/Query/ExistsTest.php +++ b/tests/unit/Query/ExistsTest.php @@ -24,6 +24,7 @@ class ExistsTest extends TestCase Configuration::overrideMode(null); } + #[TestDox('query() succeeds')] public function testQuerySucceeds(): void { Configuration::overrideMode(Mode::SQLite); @@ -31,7 +32,7 @@ class ExistsTest extends TestCase 'Existence query not generated correctly'); } - #[TestDox('By ID succeeds')] + #[TestDox('byId() succeeds')] public function testByIdSucceeds(): void { Configuration::overrideMode(Mode::SQLite); @@ -39,15 +40,16 @@ class ExistsTest extends TestCase 'Existence query not generated correctly'); } + #[TestDox('byFields() succeeds')] public function testByFieldsSucceeds(): void { Configuration::overrideMode(Mode::SQLite); $this->assertEquals("SELECT EXISTS (SELECT 1 FROM box WHERE data->>'status' <> :status)", - Exists::byFields('box', [Field::NE('status', 'occupied', ':status')]), + Exists::byFields('box', [Field::notEqual('status', 'occupied', ':status')]), 'Existence query not generated correctly'); } - #[TestDox('By contains succeeds for PostgreSQL')] + #[TestDox('byContains() succeeds for PostgreSQL')] public function testByContainsSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -55,14 +57,14 @@ class ExistsTest extends TestCase Exists::byContains('pocket'), 'Existence query not generated correctly'); } - #[TestDox('By contains fails for non PostgreSQL')] + #[TestDox('byContains() fails for non PostgreSQL')] public function testByContainsFailsForNonPostgreSQL(): void { $this->expectException(DocumentException::class); Exists::byContains(''); } - #[TestDox('By JSON Path succeeds for PostgreSQL')] + #[TestDox('byJsonPath() succeeds for PostgreSQL')] public function testByJsonPathSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -70,7 +72,7 @@ class ExistsTest extends TestCase Exists::byJsonPath('lint'), 'Existence query not generated correctly'); } - #[TestDox('By JSON Path fails for non PostgreSQL')] + #[TestDox('byJsonPath() fails for non PostgreSQL')] public function testByJsonPathFailsForNonPostgreSQL(): void { $this->expectException(DocumentException::class); diff --git a/tests/unit/Query/FindTest.php b/tests/unit/Query/FindTest.php index 76a02e3..c1f471b 100644 --- a/tests/unit/Query/FindTest.php +++ b/tests/unit/Query/FindTest.php @@ -24,7 +24,7 @@ class FindTest extends TestCase Configuration::overrideMode(null); } - #[TestDox('By ID succeeds')] + #[TestDox('byId() succeeds')] public function testByIdSucceeds(): void { Configuration::overrideMode(Mode::SQLite); @@ -32,16 +32,17 @@ class FindTest extends TestCase 'SELECT query not generated correctly'); } + #[TestDox('byFields() succeeds')] public function testByFieldsSucceeds(): void { Configuration::overrideMode(Mode::SQLite); $this->assertEquals("SELECT data FROM there WHERE data->>'active' = :act OR data->>'locked' = :lock", - Find::byFields('there', [Field::EQ('active', true, ':act'), Field::EQ('locked', true, ':lock')], + Find::byFields('there', [Field::equal('active', true, ':act'), Field::equal('locked', true, ':lock')], FieldMatch::Any), 'SELECT query not generated correctly'); } - #[TestDox('By contains succeeds for PostgreSQL')] + #[TestDox('byContains() succeeds for PostgreSQL')] public function testByContainsSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -49,14 +50,14 @@ class FindTest extends TestCase 'SELECT query not generated correctly'); } - #[TestDox('By contains fails for non PostgreSQL')] + #[TestDox('byContains() fails for non PostgreSQL')] public function testByContainsFailsForNonPostgreSQL(): void { $this->expectException(DocumentException::class); Find::byContains(''); } - #[TestDox('By JSON Path succeeds for PostgreSQL')] + #[TestDox('byJsonPath() succeeds for PostgreSQL')] public function testByJsonPathSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -64,7 +65,7 @@ class FindTest extends TestCase Find::byJsonPath('light'), 'SELECT query not generated correctly'); } - #[TestDox('By JSON Path fails for non PostgreSQL')] + #[TestDox('byJsonPath() fails for non PostgreSQL')] public function testByJsonPathFailsForNonPostgreSQL(): void { $this->expectException(DocumentException::class); diff --git a/tests/unit/Query/PatchTest.php b/tests/unit/Query/PatchTest.php index c805681..415ff0c 100644 --- a/tests/unit/Query/PatchTest.php +++ b/tests/unit/Query/PatchTest.php @@ -24,7 +24,7 @@ class PatchTest extends TestCase Configuration::overrideMode(null); parent::tearDown(); } - #[TestDox('By ID succeeds for PostgreSQL')] + #[TestDox('byId() succeeds for PostgreSQL')] public function testByIdSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -32,7 +32,7 @@ class PatchTest extends TestCase Patch::byId('doc_table'), 'Patch UPDATE statement is not correct'); } - #[TestDox('By ID succeeds for SQLite')] + #[TestDox('byId() succeeds for SQLite')] public function testByIdSucceedsForSQLite(): void { Configuration::overrideMode(Mode::SQLite); @@ -40,37 +40,39 @@ class PatchTest extends TestCase Patch::byId('my_table'), 'Patch UPDATE statement is not correct'); } - #[TestDox('By ID fails when mode not set')] + #[TestDox('byId() fails when mode not set')] public function testByIdFailsWhenModeNotSet(): void { $this->expectException(DocumentException::class); Patch::byId('oof'); } - #[TestDox('By fields succeeds for PostgreSQL')] + #[TestDox('byFields() succeeds for PostgreSQL')] public function testByFieldsSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); $this->assertEquals("UPDATE that SET data = data || :data WHERE (data->>'something')::numeric < :some", - Patch::byFields('that', [Field::LT('something', 17, ':some')]), 'Patch UPDATE statement is not correct'); + Patch::byFields('that', [Field::less('something', 17, ':some')]), 'Patch UPDATE statement is not correct'); } - #[TestDox('By fields succeeds for SQLite')] + #[TestDox('byFields() succeeds for SQLite')] public function testByFieldsSucceedsForSQLite(): void { Configuration::overrideMode(Mode::SQLite); $this->assertEquals( "UPDATE a_table SET data = json_patch(data, json(:data)) WHERE data->>'something' > :it", - Patch::byFields('a_table', [Field::GT('something', 17, ':it')]), 'Patch UPDATE statement is not correct'); + Patch::byFields('a_table', [Field::greater('something', 17, ':it')]), + 'Patch UPDATE statement is not correct'); } + #[TestDox('byFields() fails when mode not set')] public function testByFieldsFailsWhenModeNotSet(): void { $this->expectException(DocumentException::class); Patch::byFields('oops', []); } - #[TestDox('By contains succeeds for PostgreSQL')] + #[TestDox('byContains() succeeds for PostgreSQL')] public function testByContainsSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -78,14 +80,14 @@ class PatchTest extends TestCase 'Patch UPDATE statement is not correct'); } - #[TestDox('By contains fails for non PostgreSQL')] + #[TestDox('byContains() fails for non PostgreSQL')] public function testByContainsFailsForNonPostgreSQL(): void { $this->expectException(DocumentException::class); Patch::byContains(''); } - #[TestDox('By JSON Path succeeds for PostgreSQL')] + #[TestDox('byJsonPath() succeeds for PostgreSQL')] public function testByJsonPathSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -93,7 +95,7 @@ class PatchTest extends TestCase Patch::byJsonPath('that'), 'Patch UPDATE statement is not correct'); } - #[TestDox('By JSON Path fails for non PostgreSQL')] + #[TestDox('byJsonPath() fails for non PostgreSQL')] public function testByJsonPathFailsForNonPostgreSQL(): void { $this->expectException(DocumentException::class); diff --git a/tests/unit/Query/RemoveFieldsTest.php b/tests/unit/Query/RemoveFieldsTest.php index 2ab9da2..7f8d0a9 100644 --- a/tests/unit/Query/RemoveFieldsTest.php +++ b/tests/unit/Query/RemoveFieldsTest.php @@ -24,7 +24,7 @@ class RemoveFieldsTest extends TestCase Configuration::overrideMode(null); } - #[TestDox('Update succeeds for PostgreSQL')] + #[TestDox('update() succeeds for PostgreSQL')] public function testUpdateSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -32,7 +32,7 @@ class RemoveFieldsTest extends TestCase RemoveFields::update('taco', [':names' => "{one,two}"], 'it = true'), 'UPDATE statement not correct'); } - #[TestDox('Update succeeds for SQLite')] + #[TestDox('update() succeeds for SQLite')] public function testUpdateSucceedsForSQLite(): void { Configuration::overrideMode(Mode::SQLite); @@ -41,22 +41,23 @@ class RemoveFieldsTest extends TestCase 'UPDATE statement not correct'); } + #[TestDox('update() fails when mode not set')] public function testUpdateFailsWhenModeNotSet(): void { $this->expectException(DocumentException::class); RemoveFields::update('wow', [], ''); } - #[TestDox('By ID succeeds for PostgreSQL')] - public function testByIdSucceedsForPostgreSQL() + #[TestDox('byId() succeeds for PostgreSQL')] + public function testByIdSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); $this->assertEquals("UPDATE churro SET data = data - :bite::text[] WHERE data->>'id' = :id", RemoveFields::byId('churro', Parameters::fieldNames(':bite', ['byte'])), 'UPDATE statement not correct'); } - #[TestDox('By ID succeeds for SQLite')] - public function testByIdSucceedsForSQLite() + #[TestDox('byId() succeeds for SQLite')] + public function testByIdSucceedsForSQLite(): void { Configuration::overrideMode(Mode::SQLite); $this->assertEquals("UPDATE quesadilla SET data = json_remove(data, :bite0) WHERE data->>'id' = :id", @@ -64,41 +65,42 @@ class RemoveFieldsTest extends TestCase 'UPDATE statement not correct'); } - #[TestDox('By ID fails when mode not set')] + #[TestDox('byId() fails when mode not set')] public function testByIdFailsWhenModeNotSet(): void { $this->expectException(DocumentException::class); RemoveFields::byId('oof', []); } - #[TestDox('By fields succeeds for PostgreSQL')] - public function testByFieldsSucceedsForPostgreSQL() + #[TestDox('byFields() succeeds for PostgreSQL')] + public function testByFieldsSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); $this->assertEquals("UPDATE enchilada SET data = data - :sauce::text[] WHERE data->>'cheese' = :queso", - RemoveFields::byFields('enchilada', [Field::EQ('cheese', 'jack', ':queso')], + RemoveFields::byFields('enchilada', [Field::equal('cheese', 'jack', ':queso')], Parameters::fieldNames(':sauce', ['white'])), 'UPDATE statement not correct'); } - #[TestDox('By fields succeeds for SQLite')] - public function testByFieldsSucceedsForSQLite() + #[TestDox('byFields() succeeds for SQLite')] + public function testByFieldsSucceedsForSQLite(): void { Configuration::overrideMode(Mode::SQLite); $this->assertEquals( "UPDATE chimichanga SET data = json_remove(data, :filling0) WHERE data->>'side' = :rice", - RemoveFields::byFields('chimichanga', [Field::EQ('side', 'beans', ':rice')], + RemoveFields::byFields('chimichanga', [Field::equal('side', 'beans', ':rice')], Parameters::fieldNames(':filling', ['beef'])), 'UPDATE statement not correct'); } + #[TestDox('byFields() fails when mode not set')] public function testByFieldsFailsWhenModeNotSet(): void { $this->expectException(DocumentException::class); RemoveFields::byFields('boing', [], []); } - #[TestDox('By contains succeeds for PostgreSQL')] + #[TestDox('byContains() succeeds for PostgreSQL')] public function testByContainsSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -107,14 +109,14 @@ class RemoveFieldsTest extends TestCase 'UPDATE statement not correct'); } - #[TestDox('By contains fails for non PostgreSQL')] + #[TestDox('byContains() fails for non PostgreSQL')] public function testByContainsFailsForNonPostgreSQL(): void { $this->expectException(DocumentException::class); RemoveFields::byContains('', []); } - #[TestDox('By JSON Path succeeds for PostgreSQL')] + #[TestDox('byJsonPath() succeeds for PostgreSQL')] public function testByJsonPathSucceedsForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -124,7 +126,7 @@ class RemoveFieldsTest extends TestCase 'UPDATE statement not correct'); } - #[TestDox('By JSON Path fails for non PostgreSQL')] + #[TestDox('byJsonPath() fails for non PostgreSQL')] public function testByJsonPathFailsForNonPostgreSQL(): void { $this->expectException(DocumentException::class); diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index f81b2f1..66eb63f 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -28,45 +28,53 @@ class QueryTest extends TestCase Configuration::overrideMode(null); } + #[TestDox('selectFromTable() succeeds')] public function testSelectFromTableSucceeds(): void { $this->assertEquals('SELECT data FROM testing', Query::selectFromTable('testing'), 'Query not constructed correctly'); } + #[TestDox('whereByFields() succeeds for single field')] public function testWhereByFieldsSucceedsForSingleField(): void { $this->assertEquals("data->>'test_field' <= :it", - Query::whereByFields([Field::LE('test_field', '', ':it')]), 'WHERE fragment not constructed correctly'); - } - - public function testWhereByFieldsSucceedsForMultipleFieldsAll(): void - { - $this->assertEquals("data->>'test_field' <= :it AND data->>'other_field' = :other", - Query::whereByFields([Field::LE('test_field', '', ':it'), Field::EQ('other_field', '', ':other')]), + Query::whereByFields([Field::lessOrEqual('test_field', '', ':it')]), 'WHERE fragment not constructed correctly'); } + #[TestDox('whereByFields() succeeds for multiple fields All')] + public function testWhereByFieldsSucceedsForMultipleFieldsAll(): void + { + $this->assertEquals("data->>'test_field' <= :it AND data->>'other_field' = :other", + Query::whereByFields( + [Field::lessOrEqual('test_field', '', ':it'), Field::equal('other_field', '', ':other')]), + 'WHERE fragment not constructed correctly'); + } + + #[TestDox('whereByFields() succeeds for multiple fields Any')] public function testWhereByFieldsSucceedsForMultipleFieldsAny(): void { $this->assertEquals("data->>'test_field' <= :it OR data->>'other_field' = :other", - Query::whereByFields([Field::LE('test_field', '', ':it'), Field::EQ('other_field', '', ':other')], + Query::whereByFields( + [Field::lessOrEqual('test_field', '', ':it'), Field::equal('other_field', '', ':other')], FieldMatch::Any), 'WHERE fragment not constructed correctly'); } - #[TestDox('Where by ID succeeds with default parameter')] + #[TestDox('whereById() succeeds with default parameter')] public function testWhereByIdSucceedsWithDefaultParameter(): void { $this->assertEquals("data->>'id' = :id", Query::whereById(), 'WHERE fragment not constructed correctly'); } - #[TestDox('Where by ID succeeds with specific parameter')] + #[TestDox('whereById() succeeds with specific parameter')] public function testWhereByIdSucceedsWithSpecificParameter(): void { $this->assertEquals("data->>'id' = :di", Query::whereById(':di'), 'WHERE fragment not constructed correctly'); } + #[TestDox('whereDataContains() succeeds with default parameter')] public function testWhereDataContainsSucceedsWithDefaultParameter(): void { Configuration::overrideMode(Mode::PgSQL); @@ -74,13 +82,14 @@ class QueryTest extends TestCase 'WHERE fragment not constructed correctly'); } + #[TestDox('whereDataContains() succeeds with specific parameter')] public function testWhereDataContainsSucceedsWithSpecifiedParameter(): void { Configuration::overrideMode(Mode::PgSQL); $this->assertEquals('data @> :it', Query::whereDataContains(':it'), 'WHERE fragment not constructed correctly'); } - #[TestDox('Where data contains fails if not PostgreSQL')] + #[TestDox('whereDataContains() fails if not PostgreSQL')] public function testWhereDataContainsFailsIfNotPostgreSQL(): void { Configuration::overrideMode(null); @@ -88,7 +97,7 @@ class QueryTest extends TestCase Query::whereDataContains(); } - #[TestDox('Where JSON Path matches succeeds with default parameter')] + #[TestDox('whereJsonPathMatches() succeeds with default parameter')] public function testWhereJsonPathMatchesSucceedsWithDefaultParameter(): void { Configuration::overrideMode(Mode::PgSQL); @@ -96,7 +105,7 @@ class QueryTest extends TestCase 'WHERE fragment not constructed correctly'); } - #[TestDox('Where JSON Path matches succeeds with specified parameter')] + #[TestDox('whereJsonPathMatches() succeeds with specified parameter')] public function testWhereJsonPathMatchesSucceedsWithSpecifiedParameter(): void { Configuration::overrideMode(Mode::PgSQL); @@ -104,7 +113,7 @@ class QueryTest extends TestCase 'WHERE fragment not constructed correctly'); } - #[TestDox('Where JSON Path matches fails if not PostgreSQL')] + #[TestDox('whereJsonPathMatches() fails if not PostgreSQL')] public function testWhereJsonPathMatchesFailsIfNotPostgreSQL(): void { Configuration::overrideMode(null); @@ -112,7 +121,7 @@ class QueryTest extends TestCase Query::whereJsonPathMatches(); } - #[TestDox('Insert succeeds with no auto-ID for PostgreSQL')] + #[TestDox('insert() succeeds with no auto-ID for PostgreSQL')] public function testInsertSucceedsWithNoAutoIdForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -120,14 +129,14 @@ class QueryTest extends TestCase 'INSERT statement not constructed correctly'); } - #[TestDox('Insert succeeds with no auto-ID for SQLite')] + #[TestDox('insert() succeeds with no auto-ID for SQLite')] public function testInsertSucceedsWithNoAutoIdForSQLite(): void { $this->assertEquals('INSERT INTO test_tbl VALUES (:data)', Query::insert('test_tbl'), 'INSERT statement not constructed correctly'); } - #[TestDox('Insert succeeds with auto numeric ID for PostgreSQL')] + #[TestDox('insert() succeeds with auto numeric ID for PostgreSQL')] public function testInsertSucceedsWithAutoNumericIdForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -137,7 +146,7 @@ class QueryTest extends TestCase Query::insert('test_tbl', AutoId::Number), 'INSERT statement not constructed correctly'); } - #[TestDox('Insert succeeds with auto numeric ID for SQLite')] + #[TestDox('insert() succeeds with auto numeric ID for SQLite')] public function testInsertSucceedsWithAutoNumericIdForSQLite(): void { $this->assertEquals( @@ -146,7 +155,7 @@ class QueryTest extends TestCase Query::insert('test_tbl', AutoId::Number), 'INSERT statement not constructed correctly'); } - #[TestDox('Insert succeeds with auto UUID for PostgreSQL')] + #[TestDox('insert() succeeds with auto UUID for PostgreSQL')] public function testInsertSucceedsWithAutoUuidForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -156,7 +165,7 @@ class QueryTest extends TestCase $this->assertStringEndsWith("\"}')", $query, 'INSERT statement not constructed correctly'); } - #[TestDox('Insert succeeds with auto UUID for SQLite')] + #[TestDox('insert() succeeds with auto UUID for SQLite')] public function testInsertSucceedsWithAutoUuidForSQLite(): void { $query = Query::insert('test_tbl', AutoId::UUID); @@ -165,7 +174,7 @@ class QueryTest extends TestCase $this->assertStringEndsWith("'))", $query, 'INSERT statement not constructed correctly'); } - #[TestDox('Insert succeeds with auto random string for PostgreSQL')] + #[TestDox('insert() succeeds with auto random string for PostgreSQL')] public function testInsertSucceedsWithAutoRandomStringForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -182,7 +191,7 @@ class QueryTest extends TestCase } } - #[TestDox('Insert succeeds with auto random string for SQLite')] + #[TestDox('insert() succeeds with auto random string for SQLite')] public function testInsertSucceedsWithAutoRandomStringForSQLite(): void { $query = Query::insert('test_tbl', AutoId::RandomString); @@ -193,6 +202,7 @@ class QueryTest extends TestCase $this->assertEquals(16, strlen($id), "Generated ID [$id] should have been 16 characters long"); } + #[TestDox('insert() fails when mode not set')] public function testInsertFailsWhenModeNotSet(): void { $this->expectException(DocumentException::class); @@ -200,6 +210,7 @@ class QueryTest extends TestCase Query::insert('kaboom'); } + #[TestDox('save() succeeds')] public function testSaveSucceeds(): void { $this->assertEquals( @@ -207,9 +218,87 @@ class QueryTest extends TestCase Query::save('test_tbl'), 'INSERT ON CONFLICT statement not constructed correctly'); } - public function testUpdateSucceeds() + #[TestDox('update() succeeds')] + public function testUpdateSucceeds(): void { $this->assertEquals("UPDATE testing SET data = :data WHERE data->>'id' = :id", Query::update('testing'), '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'); + } }