* @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('{}'); }); });