Backport JSON funcs from v2
This commit is contained in:
		
							parent
							
								
									3b2d2ced98
								
							
						
					
					
						commit
						1e23afb3db
					
				| @ -29,4 +29,4 @@ In all generated scenarios, if the ID value is not 0 or blank, that ID will be u | ||||
| 
 | ||||
| ## Usage | ||||
| 
 | ||||
| Full documentation [is available on the project site](https://bitbadger.solutions/open-source/pdo-document/). | ||||
| Full documentation [is available on the project site](https://relationaldocs.bitbadger.solutions/php/). | ||||
|  | ||||
							
								
								
									
										538
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										538
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -9,7 +9,7 @@ declare(strict_types=1); | ||||
| namespace BitBadger\PDODocument; | ||||
| 
 | ||||
| use BitBadger\InspiredByFSharp\Option; | ||||
| use BitBadger\PDODocument\Mapper\Mapper; | ||||
| use BitBadger\PDODocument\Mapper\{Mapper, StringMapper}; | ||||
| use PDO; | ||||
| use PDOException; | ||||
| use PDOStatement; | ||||
| @ -92,6 +92,41 @@ class Custom | ||||
|         return iterator_to_array(self::list($query, $parameters, $mapper)->items()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Execute a query that returns a JSON string of results | ||||
|      * | ||||
|      * @param string $query The query to be executed | ||||
|      * @param array<string, mixed> $parameters Parameters to use in executing the query | ||||
|      * @return string A JSON array with the results (empty results will be `[]`) | ||||
|      * @throws DocumentException If any is encountered | ||||
|      */ | ||||
|     public static function jsonArray(string $query, array $parameters): string | ||||
|     { | ||||
|         return '[' . implode(',', self::array($query, $parameters, new StringMapper('data'))) . ']'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Execute a query, echoing the results to the output | ||||
|      * | ||||
|      * @param string $query The query to be executed | ||||
|      * @param array<string, mixed> $parameters Parameters to use in executing the query | ||||
|      * @throws DocumentException If any is encountered | ||||
|      */ | ||||
|     public static function outputJsonArray(string $query, array $parameters): void | ||||
|     { | ||||
|         $isFirst = true; | ||||
|         echo '['; | ||||
|         foreach (self::list($query, $parameters, new StringMapper('data'))->items() as $doc) { | ||||
|             if ($isFirst) { | ||||
|                 $isFirst = false; | ||||
|             } else { | ||||
|                 echo ','; | ||||
|             } | ||||
|             echo $doc; | ||||
|         } | ||||
|         echo ']'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Execute a query that returns one or no results (returns false if not found) | ||||
|      * | ||||
| @ -112,6 +147,19 @@ class Custom | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Execute a query that returns one or no JSON results | ||||
|      * | ||||
|      * @param string $query The query to be executed (will have "LIMIT 1" appended) | ||||
|      * @param array<string, mixed> $parameters Parameters to use in executing the query | ||||
|      * @return string The JSON document (returns `{}` if no document is found) | ||||
|      * @throws DocumentException If any is encountered | ||||
|      */ | ||||
|     public static function jsonSingle(string $query, array $parameters): string | ||||
|     { | ||||
|         return self::single($query, $parameters, new StringMapper('data'))->getOrDefault('{}'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Execute a query that does not return a value | ||||
|      * | ||||
|  | ||||
							
								
								
									
										252
									
								
								src/Json.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								src/Json.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,252 @@ | ||||
| <?php | ||||
| /** | ||||
|  * @author Daniel J. Summers <daniel@bitbadger.solutions> | ||||
|  * @license MIT | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace BitBadger\PDODocument; | ||||
| 
 | ||||
| /** | ||||
|  * Functions to retrieve and output documents as JSON | ||||
|  */ | ||||
| class Json | ||||
| { | ||||
|     /** | ||||
|      * Retrieve all JSON documents in the given table | ||||
|      * | ||||
|      * @param string $tableName The table from which documents should be retrieved | ||||
|      * @param Field[] $orderBy Fields by which the results should be ordered (optional, default no ordering) | ||||
|      * @return string A JSON array of all documents from the table | ||||
|      * @throws DocumentException If any is encountered | ||||
|      */ | ||||
|     public static function all(string $tableName, array $orderBy = []): string | ||||
|     { | ||||
|         return Custom::jsonArray(Query::selectFromTable($tableName) . Query::orderBy($orderBy), []); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieve a JSON document by its ID | ||||
|      * | ||||
|      * @param string $tableName The table from which the document should be retrieved | ||||
|      * @param mixed $docId The ID of the document to retrieve | ||||
|      * @return string The JSON document if found, `{}` otherwise | ||||
|      * @throws DocumentException If any is encountered | ||||
|      */ | ||||
|     public static function byId(string $tableName, mixed $docId): string | ||||
|     { | ||||
|         return Custom::jsonSingle(Query\Find::byId($tableName, $docId), Parameters::id($docId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieve JSON documents via a comparison on JSON fields | ||||
|      * | ||||
|      * @param string $tableName The table from which documents should be retrieved | ||||
|      * @param Field[] $fields The field comparison to match | ||||
|      * @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 string A JSON array of documents matching the given field comparison | ||||
|      * @throws DocumentException If any is encountered | ||||
|      */ | ||||
|     public static function byFields(string $tableName, array $fields, ?FieldMatch $match = null, | ||||
|                                     array $orderBy = []): string | ||||
|     { | ||||
|         Parameters::nameFields($fields); | ||||
|         return Custom::jsonArray(Query\Find::byFields($tableName, $fields, $match) . Query::orderBy($orderBy), | ||||
|             Parameters::addFields($fields, [])); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieve JSON documents via a JSON containment query (`@>`; PostgreSQL only) | ||||
|      * | ||||
|      * @param string $tableName The name of the table from which documents should be retrieved | ||||
|      * @param mixed[]|object $criteria The criteria for the JSON containment query | ||||
|      * @param Field[] $orderBy Fields by which the results should be ordered (optional, default no ordering) | ||||
|      * @return string A JSON array 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, array $orderBy = []): string | ||||
|     { | ||||
|         return Custom::jsonArray(Query\Find::byContains($tableName) . Query::orderBy($orderBy), | ||||
|             Parameters::json(':criteria', $criteria)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieve JSON documents via a JSON Path match query (`@?`; PostgreSQL only) | ||||
|      * | ||||
|      * @param string $tableName The name of the table from which documents should be retrieved | ||||
|      * @param string $path The JSON Path match string | ||||
|      * @param Field[] $orderBy Fields by which the results should be ordered (optional, default no ordering) | ||||
|      * @return string A JSON array 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, array $orderBy = []): string | ||||
|     { | ||||
|         return Custom::jsonArray(Query\Find::byJsonPath($tableName) . Query::orderBy($orderBy), [':path' => $path]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieve JSON documents via a comparison on JSON fields, returning only the first result | ||||
|      * | ||||
|      * @param string $tableName The table from which the document should be retrieved | ||||
|      * @param Field[] $fields The field comparison to match | ||||
|      * @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 string The first JSON document if any matches are found, `{}` otherwise | ||||
|      * @throws DocumentException If any is encountered | ||||
|      */ | ||||
|     public static function firstByFields(string $tableName, array $fields, ?FieldMatch $match = null, | ||||
|                                          array $orderBy = []): string | ||||
|     { | ||||
|         Parameters::nameFields($fields); | ||||
|         return Custom::jsonSingle(Query\Find::byFields($tableName, $fields, $match) . Query::orderBy($orderBy), | ||||
|             Parameters::addFields($fields, [])); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieve JSON documents via a JSON containment query (`@>`), returning only the first result (PostgreSQL only) | ||||
|      * | ||||
|      * @param string $tableName The name of the table from which documents should be retrieved | ||||
|      * @param mixed[]|object $criteria The criteria for the JSON containment query | ||||
|      * @param Field[] $orderBy Fields by which the results should be ordered (optional, default no ordering) | ||||
|      * @return string The first JSON document if any matches are found, `{}` otherwise | ||||
|      * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs | ||||
|      */ | ||||
|     public static function firstByContains(string $tableName, array|object $criteria, array $orderBy = []): string | ||||
|     { | ||||
|         return Custom::jsonSingle(Query\Find::byContains($tableName) . Query::orderBy($orderBy), | ||||
|             Parameters::json(':criteria', $criteria)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieve JSON documents via a JSON Path match query (`@?`), returning only the first result (PostgreSQL only) | ||||
|      * | ||||
|      * @param string $tableName The name of the table from which documents should be retrieved | ||||
|      * @param string $path The JSON Path match string | ||||
|      * @param Field[] $orderBy Fields by which the results should be ordered (optional, default no ordering) | ||||
|      * @return string The first JSON document if any matches are found, `{}` otherwise | ||||
|      * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs | ||||
|      */ | ||||
|     public static function firstByJsonPath(string $tableName, string $path, array $orderBy = []): string | ||||
|     { | ||||
|         return Custom::jsonSingle(Query\Find::byJsonPath($tableName) . Query::orderBy($orderBy), [':path' => $path]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Output all JSON documents in the given table | ||||
|      * | ||||
|      * @param string $tableName The table from which documents should be retrieved | ||||
|      * @param Field[] $orderBy Fields by which the results should be ordered (optional, default no ordering) | ||||
|      * @throws DocumentException If any is encountered | ||||
|      */ | ||||
|     public static function outputAll(string $tableName, array $orderBy = []): void | ||||
|     { | ||||
|         Custom::outputJsonArray(Query::selectFromTable($tableName) . Query::orderBy($orderBy), []); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Output a JSON document by its ID | ||||
|      * | ||||
|      * @param string $tableName The table from which the document should be retrieved | ||||
|      * @param mixed $docId The ID of the document to retrieve | ||||
|      * @throws DocumentException If any is encountered | ||||
|      */ | ||||
|     public static function outputById(string $tableName, mixed $docId): void | ||||
|     { | ||||
|         echo self::byId($tableName, $docId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Output JSON documents via a comparison on JSON fields | ||||
|      * | ||||
|      * @param string $tableName The table from which documents should be retrieved | ||||
|      * @param Field[] $fields The field comparison to match | ||||
|      * @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) | ||||
|      * @throws DocumentException If any is encountered | ||||
|      */ | ||||
|     public static function outputByFields(string $tableName, array $fields, ?FieldMatch $match = null, | ||||
|                                           array $orderBy = []): void | ||||
|     { | ||||
|         Parameters::nameFields($fields); | ||||
|         Custom::outputJsonArray(Query\Find::byFields($tableName, $fields, $match) . Query::orderBy($orderBy), | ||||
|             Parameters::addFields($fields, [])); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Output JSON documents via a JSON containment query (`@>`; PostgreSQL only) | ||||
|      * | ||||
|      * @param string $tableName The name of the table from which documents should be retrieved | ||||
|      * @param mixed[]|object $criteria The criteria for the JSON containment query | ||||
|      * @param Field[] $orderBy Fields by which the results should be ordered (optional, default no ordering) | ||||
|      * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs | ||||
|      */ | ||||
|     public static function outputByContains(string $tableName, array|object $criteria, array $orderBy = []): void | ||||
|     { | ||||
|         Custom::outputJsonArray(Query\Find::byContains($tableName) . Query::orderBy($orderBy), | ||||
|             Parameters::json(':criteria', $criteria)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Output JSON documents via a JSON Path match query (`@?`; PostgreSQL only) | ||||
|      * | ||||
|      * @param string $tableName The name of the table from which documents should be retrieved | ||||
|      * @param string $path The JSON Path match string | ||||
|      * @param Field[] $orderBy Fields by which the results should be ordered (optional, default no ordering) | ||||
|      * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs | ||||
|      */ | ||||
|     public static function outputByJsonPath(string $tableName, string $path, array $orderBy = []): void | ||||
|     { | ||||
|         Custom::outputJsonArray(Query\Find::byJsonPath($tableName) . Query::orderBy($orderBy), [':path' => $path]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Output JSON documents via a comparison on JSON fields, returning only the first result | ||||
|      * | ||||
|      * @param string $tableName The table from which the document should be retrieved | ||||
|      * @param Field[] $fields The field comparison to match | ||||
|      * @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) | ||||
|      * @throws DocumentException If any is encountered | ||||
|      */ | ||||
|     public static function outputFirstByFields(string $tableName, array $fields, ?FieldMatch $match = null, | ||||
|                                                array $orderBy = []): void | ||||
|     { | ||||
|         echo self::firstByFields($tableName, $fields, $match, $orderBy); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Output JSON documents via a JSON containment query (`@>`), returning only the first result (PostgreSQL only) | ||||
|      * | ||||
|      * @param string $tableName The name of the table from which documents should be retrieved | ||||
|      * @param mixed[]|object $criteria The criteria for the JSON containment query | ||||
|      * @param Field[] $orderBy Fields by which the results should be ordered (optional, default no ordering) | ||||
|      * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs | ||||
|      */ | ||||
|     public static function outputFirstByContains(string $tableName, array|object $criteria, array $orderBy = []): void | ||||
|     { | ||||
|         echo self::firstByContains($tableName, $criteria, $orderBy); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Output JSON documents via a JSON Path match query (`@?`), returning only the first result (PostgreSQL only) | ||||
|      * | ||||
|      * @param string $tableName The name of the table from which documents should be retrieved | ||||
|      * @param string $path The JSON Path match string | ||||
|      * @param Field[] $orderBy Fields by which the results should be ordered (optional, default no ordering) | ||||
|      * @throws DocumentException If the database mode is not PostgreSQL, or if an error occurs | ||||
|      */ | ||||
|     public static function outputFirstByJsonPath(string $tableName, string $path, array $orderBy = []): void | ||||
|     { | ||||
|         echo self::firstByJsonPath($tableName, $path, $orderBy); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the content type of this page's output to JSON | ||||
|      */ | ||||
|     public static function setContentType(): void | ||||
|     { | ||||
|         header('Content-Type: application/json; charset=UTF-8'); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										37
									
								
								tests/Integration/DocumentTestCase.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								tests/Integration/DocumentTestCase.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| <?php | ||||
| /** | ||||
|  * @author Daniel J. Summers <daniel@bitbadger.solutions> | ||||
|  * @license MIT | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace Test\Integration; | ||||
| 
 | ||||
| use PHPUnit\Framework\TestCase; | ||||
| 
 | ||||
| /** | ||||
|  * Base test case class for document integration tests | ||||
|  */ | ||||
| class DocumentTestCase extends TestCase | ||||
| { | ||||
|     /** | ||||
|      * Clear the output buffer | ||||
|      */ | ||||
|     public function clearBuffer(): void | ||||
|     { | ||||
|         ob_clean(); | ||||
|         ob_start(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the contents of the output buffer and end buffering | ||||
|      * | ||||
|      * @return string The contents of the output buffer | ||||
|      */ | ||||
|     public function getBufferContents(): string { | ||||
|         $contents = ob_get_contents(); | ||||
|         ob_end_clean(); | ||||
|         return $contents; | ||||
|     } | ||||
| } | ||||
| @ -11,13 +11,12 @@ namespace Test\Integration; | ||||
| 
 | ||||
| use BitBadger\PDODocument\{Configuration, Custom, Delete, DocumentException, Field}; | ||||
| use BitBadger\PDODocument\Mapper\ExistsMapper; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Test\Integration\PostgreSQL\ThrowawayDb; | ||||
| 
 | ||||
| /** | ||||
|  * Integration Test Class wrapper for PostgreSQL integration tests | ||||
|  */ | ||||
| class PgIntegrationTest extends TestCase | ||||
| class PgIntegrationTest extends DocumentTestCase | ||||
| { | ||||
|     /** @var string Database name for throwaway database */ | ||||
|     static private string $dbName = ''; | ||||
|  | ||||
| @ -65,6 +65,30 @@ describe('::array()', function () { | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::jsonArray()', function () { | ||||
|     test('returns non-empty array when data found', function () { | ||||
|         expect(Custom::jsonArray(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'sub' IS NOT NULL", [])) | ||||
|             ->toContain('[{', '},{', '}]'); | ||||
|     }); | ||||
|     test('returns empty array when no data found', function () { | ||||
|         expect(Custom::jsonArray(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'nothing' = '7'", [])) | ||||
|             ->toBe('[]'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputJsonArray()', function () { | ||||
|     test('outputs non-empty array when data found', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Custom::outputJsonArray(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'sub' IS NOT NULL", []); | ||||
|         expect($this->getBufferContents())->toContain('[{', '},{', '}]'); | ||||
|     }); | ||||
|     test('outputs empty array when no data found', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Custom::outputJsonArray(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'nothing' = '7'", []); | ||||
|         expect($this->getBufferContents())->toBe('[]'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::single()', function () { | ||||
|     test('returns a document when one is found', function () { | ||||
|         expect(Custom::single('SELECT data FROM ' . ThrowawayDb::TABLE . " WHERE data->>'id' = :id", [':id' => 'one'], | ||||
| @ -79,6 +103,19 @@ describe('::single()', function () { | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::jsonSingle()', function () { | ||||
|     test('returns a document when one is found', function () { | ||||
|         expect(Custom::jsonSingle('SELECT data FROM ' . ThrowawayDb::TABLE . " WHERE data->>'id' = :id", | ||||
|             [':id' => 'one'])) | ||||
|             ->toStartWith('{"id":')->toContain('"one",')->toEndWith('}'); | ||||
|     }); | ||||
|     test('returns no document when one is not found', function () { | ||||
|         expect(Custom::jsonSingle('SELECT data FROM ' . ThrowawayDb::TABLE . " WHERE data->>'id' = :id", | ||||
|             [':id' => 'eighty'])) | ||||
|             ->toBe('{}'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::nonQuery()', function () { | ||||
|     test('works when documents match the WHERE clause', function () { | ||||
|         Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); | ||||
|  | ||||
							
								
								
									
										410
									
								
								tests/Integration/PostgreSQL/JsonTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										410
									
								
								tests/Integration/PostgreSQL/JsonTest.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,410 @@ | ||||
| <?php | ||||
| /** | ||||
|  * @author Daniel J. Summers <daniel@bitbadger.solutions> | ||||
|  * @license MIT | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| use BitBadger\PDODocument\{Custom, Delete, Document, Field, FieldMatch, Json}; | ||||
| use Test\Integration\ArrayDocument; | ||||
| use Test\Integration\PostgreSQL\ThrowawayDb; | ||||
| 
 | ||||
| pest()->group('integration', 'postgresql'); | ||||
| 
 | ||||
| // NOTE: PostgreSQL's `JSONB` type stores JSON in a binary representation which is more easily indexed and stored. One
 | ||||
| //       side effect of this is that the document returned may not have fields in the same order as the document which
 | ||||
| //       was originally stored. The ID will always be first, though, so these tests check for presence of IDs, sorting
 | ||||
| //       by IDs, etc., instead of checking the entire JSON string that should be either returned or output.
 | ||||
| 
 | ||||
| /** | ||||
|  * Expect document ordering by verifying the index of IDs against others | ||||
|  * | ||||
|  * @param string $json The JSON string to be searched | ||||
|  * @param array $ids The IDs to be verified | ||||
|  */ | ||||
| function expect_doc_order(string $json, array $ids): void | ||||
| { | ||||
|     for ($idx = 0; $idx < sizeof($ids) - 1; $idx++) { | ||||
|         expect(strpos($json, '"' . $ids[$idx] . '",')) | ||||
|             ->toBeLessThan(strpos($json, '"' . $ids[$idx + 1] . '",'), | ||||
|                 "ID $ids[$idx] should have occurred before ID {$ids[$idx + 1]} in JSON $json"); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Expect to find one of several document IDs in the given JSON | ||||
|  * @param string $json The JSON string to be searched | ||||
|  * @param string ...$ids One or more IDs to be searched | ||||
|  */ | ||||
| function expect_any(string $json, string... $ids): void | ||||
| { | ||||
|     $exists = false; | ||||
|     foreach ($ids as $it) { | ||||
|         $exists = strpos($json, '"id": "' . $it . '",') >= 0; | ||||
|         if ($exists) break; | ||||
|     } | ||||
|     expect($exists)->toBeTrue('Could not find any of IDs [' . implode(', ', $ids) . "] in $json"); | ||||
| } | ||||
| 
 | ||||
| describe('::all()', function () { | ||||
|     test('retrieves data', function () { | ||||
|         expect(Json::all(ThrowawayDb::TABLE)) | ||||
|             ->toContain('{"id": "one",', '{"id": "two",', '{"id": "three",', '{"id": "four",', '{"id": "five",'); | ||||
|     }); | ||||
|     test('sorts data ascending', function () { | ||||
|         expect_doc_order(Json::all(ThrowawayDb::TABLE, [Field::named('id')]), ['five', 'four', 'one', 'three', 'two']); | ||||
|     }); | ||||
|     test('sorts data descending', function () { | ||||
|         expect_doc_order(Json::all(ThrowawayDb::TABLE, [Field::named('id DESC')]), | ||||
|             ['two', 'three', 'one', 'four', 'five']); | ||||
|     }); | ||||
|     test('sorts data numerically', function () { | ||||
|         expect_doc_order( | ||||
|             Json::all(ThrowawayDb::TABLE, [Field::named('sub.foo NULLS LAST'), Field::named('n:num_value')]), | ||||
|             ['two', 'four', 'one', 'three', 'five']); | ||||
|     }); | ||||
|     test('retrieves empty results', function () { | ||||
|         Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); | ||||
|         expect(Json::all(ThrowawayDb::TABLE))->toBe('[]'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::byId()', function () { | ||||
|     test('retrieves a document via string ID', function () { | ||||
|         expect(Json::byId(ThrowawayDb::TABLE, 'two'))->toStartWith('{"id": "two",')->toEndWith('}'); | ||||
|     }); | ||||
|     test('retrieves a document via numeric ID', function () { | ||||
|         Delete::byFields(ThrowawayDb::TABLE, [Field::notExists('absent')]); | ||||
|         Document::insert(ThrowawayDb::TABLE, ['id' => 18, 'value' => 'howdy']); | ||||
|         expect(Json::byId(ThrowawayDb::TABLE, 18))->toStartWith('{"id": 18,')->toEndWith('}'); | ||||
|     }); | ||||
|     test('returns "{}" when a document is not found', function () { | ||||
|         expect(Json::byId(ThrowawayDb::TABLE, 'seventy-five'))->toBe('{}'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::byFields()', function () { | ||||
|     test('retrieves matching documents', function () { | ||||
|         expect( | ||||
|             Json::byFields(ThrowawayDb::TABLE, [Field::in('value', ['blue', 'purple']), Field::exists('sub')], | ||||
|                 FieldMatch::All)) | ||||
|             ->toStartWith('[{"id": "four",')->toEndWith('}]'); | ||||
|     }); | ||||
|     test('retrieves ordered matching documents', function () { | ||||
|         expect_doc_order( | ||||
|             Json::byFields(ThrowawayDb::TABLE, [Field::equal('value', 'purple')], FieldMatch::All, | ||||
|                 [Field::named('id')]), | ||||
|             ['five', 'four']); | ||||
|     }); | ||||
|     test('retrieves documents matching a numeric IN clause', function () { | ||||
|         expect(Json::byFields(ThrowawayDb::TABLE, [Field::in('num_value', [2, 4, 6, 8])])) | ||||
|             ->toStartWith('[{"id": "three",')->toEndWith('}]'); | ||||
|     }); | ||||
|     test('returns an empty array when no matching documents are found', function () { | ||||
|         expect(Json::byFields(ThrowawayDb::TABLE, [Field::greater('num_value', 100)]))->toBe('[]'); | ||||
|     }); | ||||
|     test('retrieves documents matching an inArray condition', function () { | ||||
|         Delete::byFields(ThrowawayDb::TABLE, [Field::notExists('absentField')]); | ||||
|         foreach (ArrayDocument::testDocuments() as $doc) Document::insert(ThrowawayDb::TABLE, $doc); | ||||
|         expect(Json::byFields(ThrowawayDb::TABLE, [Field::inArray('values', ThrowawayDb::TABLE, ['c'])])) | ||||
|             ->toStartWith('[')->toContain('{"id": "first",')->toContain('{"id": "second",')->toEndWith(']'); | ||||
|     }); | ||||
|     test('returns an empty array when no documents match an inArray condition', function () { | ||||
|         Delete::byFields(ThrowawayDb::TABLE, [Field::notExists('absentField')]); | ||||
|         foreach (ArrayDocument::testDocuments() as $doc) Document::insert(ThrowawayDb::TABLE, $doc); | ||||
|         expect(Json::byFields(ThrowawayDb::TABLE, [Field::inArray('values', ThrowawayDb::TABLE, ['j'])])) | ||||
|             ->toBe('[]'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::byContains()', function () { | ||||
|     test('retrieves matching documents', function () { | ||||
|         expect(Json::byContains(ThrowawayDb::TABLE, ['value' => 'purple'])) | ||||
|             ->toStartWith('[')->toContain('{"id": "four",', '{"id": "five",')->toEndWith(']'); | ||||
|     }); | ||||
|     test('retrieves ordered matching documents', function () { | ||||
|         expect_doc_order(Json::byContains(ThrowawayDb::TABLE, ['sub' => ['foo' => 'green']], [Field::named('value')]), | ||||
|             ['two', 'four']); | ||||
|     }); | ||||
|     test('returns an empty array when no documents match', function () { | ||||
|         expect(Json::byContains(ThrowawayDb::TABLE, ['value' => 'indigo']))->toBe('[]'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::byJsonPath()', function () { | ||||
|     test('retrieves matching documents', function () { | ||||
|         expect(Json::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 10)')) | ||||
|             ->toStartWith('[')->toContain('{"id": "four",', '{"id": "five",')->toEndWith(']'); | ||||
|     }); | ||||
|     test('retrieves ordered matching documents', function () { | ||||
|         expect_doc_order(Json::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 10)', [Field::named('id')]), | ||||
|             ['five', 'four']); | ||||
|     }); | ||||
|     test('returns an empty array when no documents match', function () { | ||||
|         expect(Json::byJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 100)'))->toBe('[]'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::firstByFields()', function () { | ||||
|     test('retrieves a matching document', function () { | ||||
|         expect(Json::firstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'another')])) | ||||
|             ->toStartWith('{"id": "two",')->toEndWith('}'); | ||||
|     }); | ||||
|     test('retrieves a document for multiple results', function () { | ||||
|         $doc = Json::firstByFields(ThrowawayDb::TABLE, [Field::equal('sub.foo', 'green')]); | ||||
|         expect($doc)->toStartWith('{')->toEndWith('}'); | ||||
|         expect_any($doc, 'two', 'four'); | ||||
|     }); | ||||
|     test('retrieves a document for multiple ordered results', function () { | ||||
|         expect( | ||||
|             Json::firstByFields(ThrowawayDb::TABLE, [Field::equal('sub.foo', 'green')], | ||||
|                 orderBy: [Field::named('n:num_value DESC')])) | ||||
|             ->toStartWith('{"id": "four",')->toEndWith('}'); | ||||
|     }); | ||||
|     test('returns "{}" when no documents match', function () { | ||||
|         expect(Json::firstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'absent')]))->toBe('{}'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::firstByContains()', function () { | ||||
|     test('retrieves a matching document', function () { | ||||
|         expect(Json::firstByContains(ThrowawayDb::TABLE, ['value' => 'FIRST!'])) | ||||
|             ->toStartWith('{"id": "one",')->toEndWith('}'); | ||||
|     }); | ||||
|     test('retrieves a document for multiple results', function () { | ||||
|         $doc = Json::firstByContains(ThrowawayDb::TABLE, ['value' => 'purple']); | ||||
|         expect($doc)->toStartWith('{')->toEndWith('}'); | ||||
|         expect_any($doc, 'four', 'five'); | ||||
|     }); | ||||
|     test('retrieves a document for multiple ordered results', function () { | ||||
|         expect(Json::firstByContains(ThrowawayDb::TABLE, ['value' => 'purple'], [Field::named('sub.bar NULLS FIRST')])) | ||||
|             ->toStartWith('{"id": "five",')->toEndWith('}'); | ||||
|     }); | ||||
|     test('returns "{}" when no documents match', function () { | ||||
|         expect(Json::firstByContains(ThrowawayDb::TABLE, ['value' => 'indigo']))->toBe('{}'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::firstByJsonPath()', function () { | ||||
|     test('retrieves a matching document', function () { | ||||
|         expect(Json::firstByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ == 10)')) | ||||
|             ->toStartWith('{"id": "two",')->toEndWith('}'); | ||||
|     }); | ||||
|     test('retrieves a document for multiple results', function () { | ||||
|         $doc = Json::firstByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 10)'); | ||||
|         expect($doc)->toStartWith('{')->toEndWith('}'); | ||||
|         expect_any($doc, 'four', 'five'); | ||||
|     }); | ||||
|     test('retrieves a document for multiple ordered results', function () { | ||||
|         expect(Json::firstByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 10)', [Field::named('id DESC')])) | ||||
|             ->toStartWith('{"id": "four",')->toEndWith('}'); | ||||
|     }); | ||||
|     test('returns "{}" when no documents match', function () { | ||||
|         expect(Json::firstByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 100)'))->toBe('{}'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputAll()', function () { | ||||
|     test('outputs data', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputAll(ThrowawayDb::TABLE); | ||||
|         expect($this->getBufferContents()) | ||||
|             ->toContain('{"id": "one",', '{"id": "two",', '{"id": "three",', '{"id": "four",', '{"id": "five",'); | ||||
|     }); | ||||
|     test('sorts data ascending', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputAll(ThrowawayDb::TABLE, [Field::named('id')]); | ||||
|         expect_doc_order($this->getBufferContents(), ['five', 'four', 'one', 'three', 'two']); | ||||
|     }); | ||||
|     test('sorts data descending', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputAll(ThrowawayDb::TABLE, [Field::named('id DESC')]); | ||||
|         expect_doc_order($this->getBufferContents(), ['two', 'three', 'one', 'four', 'five']); | ||||
|     }); | ||||
|     test('sorts data numerically', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputAll(ThrowawayDb::TABLE, [Field::named('sub.foo NULLS LAST'), Field::named('n:num_value')]); | ||||
|         expect_doc_order($this->getBufferContents(), ['two', 'four', 'one', 'three', 'five']); | ||||
|     }); | ||||
|     test('outputs empty results', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); | ||||
|         Json::outputAll(ThrowawayDb::TABLE); | ||||
|         expect($this->getBufferContents())->toBe('[]'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputById()', function () { | ||||
|     test('outputs a document via string ID', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputById(ThrowawayDb::TABLE, 'two'); | ||||
|         expect($this->getBufferContents())->toStartWith('{"id": "two",')->toEndWith('}'); | ||||
|     }); | ||||
|     test('outputs a document via numeric ID', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Delete::byFields(ThrowawayDb::TABLE, [Field::notExists('absent')]); | ||||
|         Document::insert(ThrowawayDb::TABLE, ['id' => 18, 'value' => 'howdy']); | ||||
|         Json::outputById(ThrowawayDb::TABLE, 18); | ||||
|         expect($this->getBufferContents())->toStartWith('{"id": 18,')->toEndWith('}'); | ||||
|     }); | ||||
|     test('outputs "{}" when a document is not found', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputById(ThrowawayDb::TABLE, 'seventy-five'); | ||||
|         expect($this->getBufferContents())->toBe('{}'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputByFields()', function () { | ||||
|     test('outputs matching documents', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputByFields(ThrowawayDb::TABLE, [Field::in('value', ['blue', 'purple']), Field::exists('sub')], | ||||
|             FieldMatch::All); | ||||
|         expect($this->getBufferContents())->toStartWith('[{"id": "four",')->toEndWith('}]'); | ||||
|     }); | ||||
|     test('outputs ordered matching documents', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputByFields(ThrowawayDb::TABLE, [Field::equal('value', 'purple')], FieldMatch::All, | ||||
|             [Field::named('id')]); | ||||
|         expect_doc_order($this->getBufferContents(), ['five', 'four']); | ||||
|     }); | ||||
|     test('outputs documents matching a numeric IN clause', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputByFields(ThrowawayDb::TABLE, [Field::in('num_value', [2, 4, 6, 8])]); | ||||
|         expect($this->getBufferContents())->toStartWith('[{"id": "three",')->toEndWith('}]'); | ||||
|     }); | ||||
|     test('outputs an empty array when no matching documents are found', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputByFields(ThrowawayDb::TABLE, [Field::greater('num_value', 100)]); | ||||
|         expect($this->getBufferContents())->toBe('[]'); | ||||
|     }); | ||||
|     test('outputs documents matching an inArray condition', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Delete::byFields(ThrowawayDb::TABLE, [Field::notExists('absentField')]); | ||||
|         foreach (ArrayDocument::testDocuments() as $doc) Document::insert(ThrowawayDb::TABLE, $doc); | ||||
|         Json::outputByFields(ThrowawayDb::TABLE, [Field::inArray('values', ThrowawayDb::TABLE, ['c'])]); | ||||
|         expect($this->getBufferContents()) | ||||
|             ->toStartWith('[')->toContain('{"id": "first",', '{"id": "second",')->toEndWith(']'); | ||||
|     }); | ||||
|     test('outputs an empty array when no documents match an inArray condition', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Delete::byFields(ThrowawayDb::TABLE, [Field::notExists('absentField')]); | ||||
|         foreach (ArrayDocument::testDocuments() as $doc) Document::insert(ThrowawayDb::TABLE, $doc); | ||||
|         Json::outputByFields(ThrowawayDb::TABLE, [Field::inArray('values', ThrowawayDb::TABLE, ['j'])]); | ||||
|         expect($this->getBufferContents())->toBe('[]'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputByContains()', function () { | ||||
|     test('outputs matching documents', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputByContains(ThrowawayDb::TABLE, ['value' => 'purple']); | ||||
|         expect($this->getBufferContents()) | ||||
|             ->toStartWith('[')->toContain('{"id": "four",', '{"id": "five",')->toEndWith(']'); | ||||
|     }); | ||||
|     test('outputs ordered matching documents', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputByContains(ThrowawayDb::TABLE, ['sub' => ['foo' => 'green']], [Field::named('value')]); | ||||
|         expect_doc_order($this->getBufferContents(), ['two', 'four']); | ||||
|     }); | ||||
|     test('outputs an empty array when no documents match', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputByContains(ThrowawayDb::TABLE, ['value' => 'indigo']); | ||||
|         expect($this->getBufferContents())->toBe('[]'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputByJsonPath()', function () { | ||||
|     test('outputs matching documents', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 10)'); | ||||
|         expect($this->getBufferContents()) | ||||
|             ->toStartWith('[')->toContain('{"id": "four",', '{"id": "five",')->toEndWith(']'); | ||||
|     }); | ||||
|     test('outputs ordered matching documents', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 10)', [Field::named('id')]); | ||||
|         expect_doc_order($this->getBufferContents(), ['five', 'four']); | ||||
|     }); | ||||
|     test('outputs an empty array when no documents match', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 100)'); | ||||
|         expect($this->getBufferContents())->toBe('[]'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputFirstByFields()', function () { | ||||
|     test('outputs a matching document', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputFirstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'another')]); | ||||
|         expect($this->getBufferContents())->toStartWith('{"id": "two",')->toEndWith('}'); | ||||
|     }); | ||||
|     test('outputs a document for multiple results', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputFirstByFields(ThrowawayDb::TABLE, [Field::equal('sub.foo', 'green')]); | ||||
|         $doc = $this->getBufferContents(); | ||||
|         expect($doc)->toStartWith('{')->toEndWith('}'); | ||||
|         expect_any($doc, 'two', 'four'); | ||||
|     }); | ||||
|     test('outputs a document for multiple ordered results', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputFirstByFields(ThrowawayDb::TABLE, [Field::equal('sub.foo', 'green')], | ||||
|             orderBy: [Field::named('n:num_value DESC')]); | ||||
|         expect($this->getBufferContents())->toStartWith('{"id": "four",')->toEndWith('}'); | ||||
|     }); | ||||
|     test('outputs "{}" when no documents match', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputFirstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'absent')]); | ||||
|         expect($this->getBufferContents())->toBe('{}'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputFirstByContains()', function () { | ||||
|     test('outputs a matching document', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputFirstByContains(ThrowawayDb::TABLE, ['value' => 'FIRST!']); | ||||
|         expect($this->getBufferContents())->toStartWith('{"id": "one",')->toEndWith('}'); | ||||
|     }); | ||||
|     test('outputs a document for multiple results', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputFirstByContains(ThrowawayDb::TABLE, ['value' => 'purple']); | ||||
|         $doc = $this->getBufferContents(); | ||||
|         expect($doc)->toStartWith('{')->toEndWith('}'); | ||||
|         expect_any($doc, 'four', 'five'); | ||||
|     }); | ||||
|     test('outputs a document for multiple ordered results', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputFirstByContains(ThrowawayDb::TABLE, ['value' => 'purple'], [Field::named('sub.bar NULLS FIRST')]); | ||||
|         expect($this->getBufferContents())->toStartWith('{"id": "five",')->toEndWith('}'); | ||||
|     }); | ||||
|     test('outputs "{}" when no documents match', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputFirstByContains(ThrowawayDb::TABLE, ['value' => 'indigo']); | ||||
|         expect($this->getBufferContents())->toBe('{}'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputFirstByJsonPath()', function () { | ||||
|     test('outputs a matching document', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputFirstByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ == 10)'); | ||||
|         expect($this->getBufferContents())->toStartWith('{"id": "two",')->toEndWith('}'); | ||||
|     }); | ||||
|     test('outputs a document for multiple results', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputFirstByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 10)'); | ||||
|         $doc = $this->getBufferContents(); | ||||
|         expect($doc)->toStartWith('{')->toEndWith('}'); | ||||
|         expect_any($doc, 'four', 'five'); | ||||
|     }); | ||||
|     test('outputs a document for multiple ordered results', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputFirstByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 10)', [Field::named('id DESC')]); | ||||
|         expect($this->getBufferContents())->toStartWith('{"id": "four",')->toEndWith('}'); | ||||
|     }); | ||||
|     test('outputs "{}" when no documents match', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputFirstByJsonPath(ThrowawayDb::TABLE, '$.num_value ? (@ > 100)'); | ||||
|         expect($this->getBufferContents())->toBe('{}'); | ||||
|     }); | ||||
| }); | ||||
| @ -63,6 +63,30 @@ describe('::array()', function () { | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::jsonArray()', function () { | ||||
|     test('returns non-empty array when data found', function () { | ||||
|         expect(Custom::jsonArray(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'sub' IS NOT NULL", [])) | ||||
|             ->toContain('[{', '},{', '}]'); | ||||
|     }); | ||||
|     test('returns empty array when no data found', function () { | ||||
|         expect(Custom::jsonArray(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'nothing' = '7'", [])) | ||||
|             ->toBe('[]'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputJsonArray()', function () { | ||||
|     test('outputs non-empty array when data found', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Custom::outputJsonArray(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'sub' IS NOT NULL", []); | ||||
|         expect($this->getBufferContents())->toContain('[{', '},{', '}]'); | ||||
|     }); | ||||
|     test('outputs empty array when no data found', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Custom::outputJsonArray(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'nothing' = '7'", []); | ||||
|         expect($this->getBufferContents())->toBe('[]'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::single()', function () { | ||||
|     test('returns a document when one is found', function () { | ||||
|         expect(Custom::single('SELECT data FROM ' . ThrowawayDb::TABLE . " WHERE data->>'id' = :id", [':id' => 'one'], | ||||
| @ -76,6 +100,19 @@ describe('::single()', function () { | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::jsonSingle()', function () { | ||||
|     test('returns a document when one is found', function () { | ||||
|         expect(Custom::jsonSingle('SELECT data FROM ' . ThrowawayDb::TABLE . " WHERE data->>'id' = :id", | ||||
|             [':id' => 'one'])) | ||||
|             ->toStartWith('{"id":"one",')->toEndWith('}'); | ||||
|     }); | ||||
|     test('returns no document when one is not found', function () { | ||||
|         expect(Custom::jsonSingle('SELECT data FROM ' . ThrowawayDb::TABLE . " WHERE data->>'id' = :id", | ||||
|             [':id' => 'eighty'])) | ||||
|             ->toBe('{}'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::nonQuery()', function () { | ||||
|     test('works when documents match the WHERE clause', function () { | ||||
|         Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); | ||||
|  | ||||
							
								
								
									
										272
									
								
								tests/Integration/SQLite/JsonTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										272
									
								
								tests/Integration/SQLite/JsonTest.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,272 @@ | ||||
| <?php | ||||
| /** | ||||
|  * @author Daniel J. Summers <daniel@bitbadger.solutions> | ||||
|  * @license MIT | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| use BitBadger\PDODocument\{Custom, Delete, Document, DocumentException, Field, FieldMatch, Json}; | ||||
| use Test\Integration\ArrayDocument; | ||||
| use Test\Integration\SQLite\ThrowawayDb; | ||||
| 
 | ||||
| pest()->group('integration', 'sqlite'); | ||||
| 
 | ||||
| /** JSON for document ID "one" */ | ||||
| const ONE = '{"id":"one","value":"FIRST!","num_value":0,"sub":null}'; | ||||
| 
 | ||||
| /** JSON for document ID "two" */ | ||||
| const TWO = '{"id":"two","value":"another","num_value":10,"sub":{"foo":"green","bar":"blue"}}'; | ||||
| 
 | ||||
| /** JSON for document ID "three" */ | ||||
| const THREE = '{"id":"three","value":"","num_value":4,"sub":null}'; | ||||
| 
 | ||||
| /** JSON for document ID "four" */ | ||||
| const FOUR = '{"id":"four","value":"purple","num_value":17,"sub":{"foo":"green","bar":"red"}}'; | ||||
| 
 | ||||
| /** JSON for document ID "five" */ | ||||
| const FIVE = '{"id":"five","value":"purple","num_value":18,"sub":null}'; | ||||
| 
 | ||||
| describe('::all()', function () { | ||||
|     test('retrieves data', function () { | ||||
|         expect(Json::all(ThrowawayDb::TABLE))->toStartWith('[')->toContain(ONE, TWO, THREE, FOUR, FIVE)->toEndWith(']'); | ||||
|     }); | ||||
|     test('sorts data ascending', function () { | ||||
|         expect(Json::all(ThrowawayDb::TABLE, [Field::named('id')])) | ||||
|             ->toBe('[' . implode(',', [FIVE, FOUR, ONE, THREE, TWO]) . ']'); | ||||
|     }); | ||||
|     test('sorts data descending', function () { | ||||
|         expect(Json::all(ThrowawayDb::TABLE, [Field::named('id DESC')])) | ||||
|             ->toBe('[' . implode(',', [TWO, THREE, ONE, FOUR, FIVE]) . ']'); | ||||
|     }); | ||||
|     test('sorts data numerically', function () { | ||||
|         expect(Json::all(ThrowawayDb::TABLE, [Field::named('sub.foo NULLS LAST'), Field::named('n:num_value')])) | ||||
|             ->toBe('[' . implode(',', [TWO, FOUR, ONE, THREE, FIVE]) . ']'); | ||||
|     }); | ||||
|     test('returns an empty array when no data exists', function () { | ||||
|         Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); | ||||
|         expect(Json::all(ThrowawayDb::TABLE))->toBe('[]'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::byId()', function () { | ||||
|     test('returns a document when it exists', function () { | ||||
|         expect(Json::byId(ThrowawayDb::TABLE, 'two'))->toBe(TWO); | ||||
|     }); | ||||
|     test('returns a document with a numeric ID', function () { | ||||
|         Document::insert(ThrowawayDb::TABLE, ['id' => 18, 'value' => 'howdy']); | ||||
|         expect(Json::byId(ThrowawayDb::TABLE, 18))->toBe('{"id":18,"value":"howdy"}'); | ||||
|     }); | ||||
|     test('returns "{}" when no document exists', function () { | ||||
|         expect(Json::byId(ThrowawayDb::TABLE, 'seventy-five'))->toBe('{}'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::byFields()', function () { | ||||
|     test('returns matching documents', function () { | ||||
|         expect(Json::byFields(ThrowawayDb::TABLE, [Field::in('value', ['blue', 'purple']), Field::exists('sub')], | ||||
|             FieldMatch::All)) | ||||
|             ->toBe('[' . FOUR . ']'); | ||||
|     }); | ||||
|     test('returns ordered matching documents', function () { | ||||
|         expect(Json::byFields(ThrowawayDb::TABLE, [Field::equal('value', 'purple')], FieldMatch::All, | ||||
|             [Field::named('id')])) | ||||
|             ->toBe('[' . implode(',', [FIVE, FOUR]) . ']'); | ||||
|     }); | ||||
|     test('returns documents matching numeric IN clause', function () { | ||||
|         expect(Json::byFields(ThrowawayDb::TABLE, [Field::in('num_value', [2, 4, 6, 8])]))->toBe('[' . THREE . ']'); | ||||
|     }); | ||||
|     test('returns empty array when no documents match', function () { | ||||
|         expect(Json::byFields(ThrowawayDb::TABLE, [Field::greater('num_value', 100)]))->toBe('[]'); | ||||
|     }); | ||||
|     test('returns matching documents for inArray comparison', function () { | ||||
|         Delete::byFields(ThrowawayDb::TABLE, [Field::notExists('absentField')]); | ||||
|         foreach (ArrayDocument::testDocuments() as $doc) Document::insert(ThrowawayDb::TABLE, $doc); | ||||
|         expect(Json::byFields(ThrowawayDb::TABLE, [Field::inArray('values', ThrowawayDb::TABLE, ['c'])])) | ||||
|             ->toBe('[{"id":"first","values":["a","b","c"]},{"id":"second","values":["c","d","e"]}]'); | ||||
|     }); | ||||
|     test('returns empty array when no documents match inArray comparison', function () { | ||||
|         Delete::byFields(ThrowawayDb::TABLE, [Field::notExists('absentField')]); | ||||
|         foreach (ArrayDocument::testDocuments() as $doc) Document::insert(ThrowawayDb::TABLE, $doc); | ||||
|         expect(Json::byFields(ThrowawayDb::TABLE, [Field::inArray('values', ThrowawayDb::TABLE, ['j'])]))->toBe('[]'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::byContains()', function () { | ||||
|     test('throws an exception', function () { | ||||
|         expect(fn () => Json::byContains('', []))->toThrow(DocumentException::class); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::byJsonPath()', function () { | ||||
|     test('throws an exception', function () { | ||||
|         expect(fn () => Json::byJsonPath('', ''))->toThrow(DocumentException::class); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::firstByFields()', function () { | ||||
|     test('returns a matching document', function () { | ||||
|         expect(Json::firstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'another')]))->toBe(TWO); | ||||
|     }); | ||||
|     test('returns one of several matching documents', function () { | ||||
|         $doc = Json::firstByFields(ThrowawayDb::TABLE, [Field::equal('sub.foo', 'green')]); | ||||
|         expect($doc == TWO || $doc == FOUR)->toBeTrue("Document should have been two or four (actual $doc)"); | ||||
|     }); | ||||
|     test('returns first of ordered matching documents', function () { | ||||
|         expect(Json::firstByFields(ThrowawayDb::TABLE, [Field::equal('sub.foo', 'green')], | ||||
|             orderBy: [Field::named('n:num_value DESC')])) | ||||
|             ->toBe(FOUR); | ||||
|     }); | ||||
|     test('returns "{}" when no documents match', function () { | ||||
|         expect(Json::firstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'absent')]))->toBe('{}'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::firstByContains()', function () { | ||||
|     test('throws an exception', function () { | ||||
|         expect(fn () => Json::firstByContains('', []))->toThrow(DocumentException::class); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::firstByJsonPath()', function () { | ||||
|     test('throws an exception', function () { | ||||
|         expect(fn () => Json::firstByJsonPath('', ''))->toThrow(DocumentException::class); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputAll()', function () { | ||||
|     test('outputs data', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputAll(ThrowawayDb::TABLE); | ||||
|         expect($this->getBufferContents())->toStartWith('[')->toContain(ONE, TWO, THREE, FOUR, FIVE)->toEndWith(']'); | ||||
|     }); | ||||
|     test('sorts data ascending', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputAll(ThrowawayDb::TABLE, [Field::named('id')]); | ||||
|         expect($this->getBufferContents())->toBe('[' . implode(',', [FIVE, FOUR, ONE, THREE, TWO]) . ']'); | ||||
|     }); | ||||
|     test('sorts data descending', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputAll(ThrowawayDb::TABLE, [Field::named('id DESC')]); | ||||
|         expect($this->getBufferContents())->toBe('[' . implode(',', [TWO, THREE, ONE, FOUR, FIVE]) . ']'); | ||||
|     }); | ||||
|     test('sorts data numerically', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputAll(ThrowawayDb::TABLE, [Field::named('sub.foo NULLS LAST'), Field::named('n:num_value')]); | ||||
|         expect($this->getBufferContents())->toBe('[' . implode(',', [TWO, FOUR, ONE, THREE, FIVE]) . ']'); | ||||
|     }); | ||||
|     test('outputs an empty array when no data exists', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []); | ||||
|         Json::outputAll(ThrowawayDb::TABLE); | ||||
|         expect($this->getBufferContents())->toBe('[]'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputById()', function () { | ||||
|     test('outputs a document when it exists', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputById(ThrowawayDb::TABLE, 'two'); | ||||
|         expect($this->getBufferContents())->toBe(TWO); | ||||
|     }); | ||||
|     test('outputs a document with a numeric ID', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Document::insert(ThrowawayDb::TABLE, ['id' => 18, 'value' => 'howdy']); | ||||
|         Json::outputById(ThrowawayDb::TABLE, 18); | ||||
|         expect($this->getBufferContents())->toBe('{"id":18,"value":"howdy"}'); | ||||
|     }); | ||||
|     test('outputs "{}" when no document exists', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputById(ThrowawayDb::TABLE, 'seventy-five'); | ||||
|         expect($this->getBufferContents())->toBe('{}'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputByFields()', function () { | ||||
|     test('outputs matching documents', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputByFields(ThrowawayDb::TABLE, [Field::in('value', ['blue', 'purple']), Field::exists('sub')], | ||||
|             FieldMatch::All); | ||||
|         expect($this->getBufferContents())->toBe('[' . FOUR . ']'); | ||||
|     }); | ||||
|     test('outputs ordered matching documents', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputByFields(ThrowawayDb::TABLE, [Field::equal('value', 'purple')], FieldMatch::All, | ||||
|             [Field::named('id')]); | ||||
|         expect($this->getBufferContents())->toBe('[' . implode(',', [FIVE, FOUR]) . ']'); | ||||
|     }); | ||||
|     test('outputs documents matching numeric IN clause', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputByFields(ThrowawayDb::TABLE, [Field::in('num_value', [2, 4, 6, 8])]); | ||||
|         expect($this->getBufferContents())->toBe('[' . THREE . ']'); | ||||
|     }); | ||||
|     test('outputs empty array when no documents match', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputByFields(ThrowawayDb::TABLE, [Field::greater('num_value', 100)]); | ||||
|         expect($this->getBufferContents())->toBe('[]'); | ||||
|     }); | ||||
|     test('outputs matching documents for inArray comparison', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Delete::byFields(ThrowawayDb::TABLE, [Field::notExists('absentField')]); | ||||
|         foreach (ArrayDocument::testDocuments() as $doc) Document::insert(ThrowawayDb::TABLE, $doc); | ||||
|         Json::outputByFields(ThrowawayDb::TABLE, [Field::inArray('values', ThrowawayDb::TABLE, ['c'])]); | ||||
|         expect($this->getBufferContents()) | ||||
|             ->toBe('[{"id":"first","values":["a","b","c"]},{"id":"second","values":["c","d","e"]}]'); | ||||
|     }); | ||||
|     test('outputs empty array when no documents match inArray comparison', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Delete::byFields(ThrowawayDb::TABLE, [Field::notExists('absentField')]); | ||||
|         foreach (ArrayDocument::testDocuments() as $doc) Document::insert(ThrowawayDb::TABLE, $doc); | ||||
|         Json::outputByFields(ThrowawayDb::TABLE, [Field::inArray('values', ThrowawayDb::TABLE, ['j'])]); | ||||
|         expect($this->getBufferContents())->toBe('[]'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputByContains()', function () { | ||||
|     test('throws an exception', function () { | ||||
|         expect(fn () => Json::outputByContains('', []))->toThrow(DocumentException::class); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputByJsonPath()', function () { | ||||
|     test('throws an exception', function () { | ||||
|         expect(fn () => Json::outputByJsonPath('', ''))->toThrow(DocumentException::class); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputFirstByFields()', function () { | ||||
|     test('outputs a matching document', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputFirstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'another')]); | ||||
|         expect($this->getBufferContents())->toBe(TWO); | ||||
|     }); | ||||
|     test('outputs one of several matching documents', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputFirstByFields(ThrowawayDb::TABLE, [Field::equal('sub.foo', 'green')]); | ||||
|         $doc = $this->getBufferContents(); | ||||
|         expect($doc == TWO || $doc == FOUR)->toBeTrue("Document should have been two or four (actual $doc)"); | ||||
|     }); | ||||
|     test('outputs first of ordered matching documents', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputFirstByFields(ThrowawayDb::TABLE, [Field::equal('sub.foo', 'green')], | ||||
|             orderBy: [Field::named('n:num_value DESC')]); | ||||
|         expect($this->getBufferContents())->toBe(FOUR); | ||||
|     }); | ||||
|     test('outputs "{}" when no documents match', function () { | ||||
|         $this->clearBuffer(); | ||||
|         Json::outputFirstByFields(ThrowawayDb::TABLE, [Field::equal('value', 'absent')]); | ||||
|         expect($this->getBufferContents())->toBe('{}'); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputFirstByContains()', function () { | ||||
|     test('throws an exception', function () { | ||||
|         expect(fn () => Json::outputFirstByContains('', []))->toThrow(DocumentException::class); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe('::outputFirstByJsonPath()', function () { | ||||
|     test('throws an exception', function () { | ||||
|         expect(fn () => Json::outputFirstByJsonPath('', ''))->toThrow(DocumentException::class); | ||||
|     }); | ||||
| }); | ||||
| @ -11,13 +11,12 @@ namespace Test\Integration; | ||||
| 
 | ||||
| use BitBadger\PDODocument\{Configuration, Custom, Delete, DocumentException, Field}; | ||||
| use BitBadger\PDODocument\Mapper\ExistsMapper; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Test\Integration\SQLite\ThrowawayDb; | ||||
| 
 | ||||
| /** | ||||
|  * Integration Test Class wrapper for SQLite integration tests | ||||
|  */ | ||||
| class SQLiteIntegrationTest extends TestCase | ||||
| class SQLiteIntegrationTest extends DocumentTestCase | ||||
| { | ||||
|     /** @var string Database name for throwaway database */ | ||||
|     static private string $dbName = ''; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user