v2.1 #10
@ -4,8 +4,8 @@ This library allows SQLite and PostgreSQL to be treated as document databases. I
|
||||
|
||||
## Add via Composer
|
||||
|
||||
[
|
||||
](https://packagist.org/packages/bit-badger/pdo-document#v1.0.0-rc1) [
|
||||
[
|
||||
](https://packagist.org/packages/bit-badger/pdo-document#v1.1.0-rc1) [
|
||||
](https://packagist.org/packages/bit-badger/pdo-document)
|
||||
|
||||
`composer require bit-badger/pdo-document`
|
||||
@ -33,4 +33,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
@ -12,7 +12,8 @@ There are several categories of operations that can be accomplished against docu
|
||||
- **Save** adds a new document, updating an existing one if the ID is already present ("upsert")
|
||||
- **Update** updates an existing document, doing nothing if no documents satisfy the criteria
|
||||
- **Patch** updates a portion of an existing document, doing nothing if no documents satisfy the criteria
|
||||
- **Find** returns the documents matching some criteria
|
||||
- **Find** returns the documents matching some criteria as domain objects
|
||||
- **Json** returns documents as JSON strings or outputs that JSON directly
|
||||
- **RemoveFields** removes fields from documents matching some criteria
|
||||
- **Delete** removes documents matching some criteria
|
||||
|
||||
@ -23,7 +24,7 @@ There are several categories of operations that can be accomplished against docu
|
||||
- **byContains** (PostgreSQL only) uses a JSON containment query (the `@>` operator) to find documents where the given sub-document occurs (think of this as an `=` comparison based on one or more properties in the document; looking for hotels with `{ "country": "USA", "rating": 4 }` would find all hotels with a rating of 4 in the United States); applies to all but Update
|
||||
- **byJsonPath** (PostgreSQL only) uses a JSON patch match query (the `@?` operator) to make specific queries against a document's structure (it also supports more operators than a containment query; to find all hotels rated 4 _or higher_ in the United States, we could query for `"$ ? (@.country == \"USA\" && @.rating > 4)"`); applies to all but Update
|
||||
|
||||
Finally, `Find` also has `firstBy*` implementations for all supported criteria types.
|
||||
Finally, `Find` and `Json` also has `firstBy*` implementations for all supported criteria types.
|
||||
|
||||
## Saving Documents
|
||||
|
||||
@ -62,10 +63,15 @@ For SQLite, we can utilize a `Field` query with a between operator. (This will a
|
||||
|
||||
```php
|
||||
// SQLite
|
||||
Patch::byFields('room', [Field::between('roomNumber', 221, 240)], ['inService' => false]);
|
||||
Patch::byFields('room',
|
||||
[Field::equal('hotelId', 'abc'), Field::between('roomNumber', 221, 240)],
|
||||
['inService' => false]);
|
||||
```
|
||||
|
||||
## Finding Documents
|
||||
> [!NOTE]
|
||||
> When multiple fields are provided to any `*byFields` function, the default matching behavior is `FieldMatch.All`; any documents identified will match all criteria. Passing `FieldMatch.Any` modifies this behavior to identify documents where any one of the criteria match.
|
||||
|
||||
## Finding Documents as Domain Items
|
||||
|
||||
Functions to find documents start with `Find::`. There are variants to find all documents in a table, find by ID, find by JSON field comparison, find by JSON containment, or find by JSON Path. The hotel update example above utilizes an ID lookup; the descriptions of JSON containment and JSON Path show examples of the criteria used to retrieve using those techniques.
|
||||
|
||||
@ -109,6 +115,13 @@ foreach ($result->items as $item) {
|
||||
// Do something amazing with $item
|
||||
}
|
||||
```
|
||||
## Finding Documents as JSON
|
||||
|
||||
If an application serves endpoints that return JSON, taking the time to retrieve documents, deserialize them into objects, then turning around and serializing those same documents back to JSON - there's a lot of unnecessary processing going on. Static functions on the `Json` object address this by returning, or directly outputting, the JSON text received from the database.
|
||||
|
||||
`Json` has the same function names as `Find`, and these functions return the JSON as a string. These always return some string with valid JSON; an empty multiple-document request will be `[]`, and a one-or-none document request will be `{}` in the "none" scenario.
|
||||
|
||||
A second set of functions are prefixed with `output` (ex. `Json::outputAll`); these functions `echo` the JSON as it is retrieved from the database. This is the preferred method for JSON APIs, as it incurs no intermediate overhead; take the documents from the database and ship 'em off to the distant end. As these responses end up as a stream of text, we will need to identify this as JSON; `Json::setContentType()` will do this, and should be called before any other output is sent.
|
||||
|
||||
## Deleting Documents
|
||||
|
||||
@ -127,10 +140,10 @@ Functions to check for existence start with `Exists::`. Documents may be checked
|
||||
The table below shows which commands are available for each access method. (X = supported for both, P = PostgreSQL only)
|
||||
|
||||
| Operation | `all` | `byId` | `byFields` | `byContains` | `byJsonPath` | `firstByFields` | `firstByContains` | `firstByJsonPath` |
|
||||
|----------------|:-----:|:------:|:----------:|:------------:|:------------:|:---------------:|:-----------------:|:-----------------:|
|
||||
|-----------------|:-----:|:------:|:----------:|:------------:|:------------:|:---------------:|:-----------------:|:-----------------:|
|
||||
| `Count` | X | | X | P | P | | | |
|
||||
| `Exists` | | X | X | P | P | | | |
|
||||
| `Find` | X | X | X | P | P | X | P | P |
|
||||
| `Find` / `Json` | X | X | X | P | P | X | P | P |
|
||||
| `Patch` | | X | X | P | P | | | |
|
||||
| `RemoveFields` | | X | X | P | P | | | |
|
||||
| `Delete` | | X | X | P | P | | | |
|
||||
|
4
index.md
4
index.md
@ -7,8 +7,8 @@ PDODocument is a PHP library that implements [relational document](/) concepts o
|
||||
|
||||
## Installing
|
||||
|
||||
[
|
||||
](https://packagist.org/packages/bit-badger/pdo-document#v1.0.0) [
|
||||
[
|
||||
](https://packagist.org/packages/bit-badger/pdo-document#v1.1.0) [
|
||||
](https://packagist.org/packages/bit-badger/pdo-document)
|
||||
|
||||
The library is [listed on Packagist][pkg] as `bit-badger/pdo-document`. v1.x targets PHP 8.2 and 8.3, while v2.x targets PHP 8.4 and up. Run `composer require bit-badger/pdo-document` (or add it to your `composer.json` manually), and it should select the appropriate version based on the target PHP version of your project.
|
||||
|
@ -10,6 +10,7 @@ namespace BitBadger\PDODocument;
|
||||
|
||||
use BitBadger\InspiredByFSharp\Option;
|
||||
use BitBadger\PDODocument\Mapper\Mapper;
|
||||
use BitBadger\PDODocument\Mapper\StringMapper;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use PDOStatement;
|
||||
@ -93,7 +94,42 @@ class Custom
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a query that returns one or no results (returns false if not found)
|
||||
* 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
|
||||
*
|
||||
* @template TDoc The domain type of the document to retrieve
|
||||
* @param string $query The query to be executed (will have "LIMIT 1" appended)
|
||||
@ -112,6 +148,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
|
||||
*
|
||||
|
@ -12,7 +12,7 @@ use BitBadger\InspiredByFSharp\Option;
|
||||
use BitBadger\PDODocument\Mapper\DocumentMapper;
|
||||
|
||||
/**
|
||||
* Functions to find documents
|
||||
* Functions to retrieve documents as domain objects
|
||||
*/
|
||||
class Find
|
||||
{
|
||||
|
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');
|
||||
}
|
||||
}
|
23
test_all.sh
Executable file
23
test_all.sh
Executable file
@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
export PDO_DOC_PGSQL_HOST=localhost:8301
|
||||
PG_VERSIONS=('13' '14' '15' '16' 'latest')
|
||||
|
||||
for PG_VERSION in "${PG_VERSIONS[@]}"
|
||||
do
|
||||
echo Starting PostgreSQL:$PG_VERSION
|
||||
docker run -d -p 8301:5432 --name pg_test -e POSTGRES_PASSWORD=postgres postgres:$PG_VERSION
|
||||
sleep 4
|
||||
if [ "$PG_VERSION" = "latest" ]; then
|
||||
echo Testing SQLite and PostgreSQL...
|
||||
./vendor/bin/pest --testdox-text $1-tests.txt tests
|
||||
else
|
||||
echo Testing PostgreSQL v$PG_VERSION...
|
||||
./vendor/bin/pest --testdox-text $1-pg$PG_VERSION-tests.txt tests/Integration/PostgreSQL
|
||||
fi
|
||||
docker stop pg_test
|
||||
sleep 2
|
||||
docker rm pg_test
|
||||
done
|
||||
|
||||
cd .. || exit
|
38
tests/Integration/DocumentTestCase.php
Normal file
38
tests/Integration/DocumentTestCase.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
* @see https://github.com/Zaid-Ajaj/ThrowawayDb The origin concept
|
||||
*/
|
||||
|
||||
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 () {
|
||||
$doc = Custom::single('SELECT data FROM ' . ThrowawayDb::TABLE . " WHERE data->>'id' = :id", [':id' => 'one'],
|
||||
@ -78,6 +102,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, []);
|
||||
|
406
tests/Integration/PostgreSQL/JsonTest.php
Normal file
406
tests/Integration/PostgreSQL/JsonTest.php
Normal file
@ -0,0 +1,406 @@
|
||||
<?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
|
||||
{
|
||||
expect(array_any($ids, fn($it) => strpos($json, '"id": "' . $it . '",') >= 0))
|
||||
->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 () {
|
||||
$doc = 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, []);
|
||||
|
274
tests/Integration/SQLite/JsonTest.php
Normal file
274
tests/Integration/SQLite/JsonTest.php
Normal file
@ -0,0 +1,274 @@
|
||||
<?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(array_any([TWO, FOUR], fn($it) => $doc == $it))
|
||||
->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(array_any([TWO, FOUR], fn($it) => $doc == $it))
|
||||
->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