diff --git a/tests/Unit/QueryTest.php b/tests/Unit/QueryTest.php index f7b93ad..5c78624 100644 --- a/tests/Unit/QueryTest.php +++ b/tests/Unit/QueryTest.php @@ -6,10 +6,11 @@ declare(strict_types=1); -use BitBadger\PDODocument\{Configuration, Query}; +use BitBadger\PDODocument\{AutoId, Configuration, DocumentException, Field, FieldMatch, Mode, Query}; pest()->group('unit'); +beforeEach(function () { Configuration::overrideMode(Mode::SQLite); }); afterEach(function () { Configuration::overrideMode(null); }); describe('::selectFromTable()', function () { @@ -17,3 +18,184 @@ describe('::selectFromTable()', function () { expect(Query::selectFromTable('testing'))->toBe('SELECT data FROM testing'); }); }); + +describe('::whereByFields()', function () { + test('generates a single field correctly', function () { + expect(Query::whereByFields([Field::lessOrEqual('test_field', '', ':it')]))->toBe("data->>'test_field' <= :it"); + }); + test('generates all fields correctly', function () { + expect(Query::whereByFields( + + [Field::lessOrEqual('test_field', '', ':it'), Field::equal('other_field', '', ':other')])) + ->toBe("data->>'test_field' <= :it AND data->>'other_field' = :other",); + }); + test('generates any field correctly', function () { + expect(Query::whereByFields( + [Field::lessOrEqual('test_field', '', ':it'), Field::equal('other_field', '', ':other')], + FieldMatch::Any)) + ->toBe("data->>'test_field' <= :it OR data->>'other_field' = :other"); + }); +}); + +describe('::whereById()', function () { + test('uses default parameter name', function () { + expect(Query::whereById())->toBe("data->>'id' = :id"); + }); + test('uses provided parameter name', function () { + expect(Query::whereById(':di'))->toBe("data->>'id' = :di"); + }); +}); + +describe('::whereDataContains()', function () { + test('uses default parameter [PostgreSQL]', function () { + Configuration::overrideMode(Mode::PgSQL); + expect(Query::whereDataContains())->toBe('data @> :criteria'); + }); + test('uses provided parameter [PostgreSQL]', function () { + Configuration::overrideMode(Mode::PgSQL); + expect(Query::whereDataContains(':it'))->toBe('data @> :it'); + }); + test('throws [SQLite]', function () { + expect(fn () => Query::whereDataContains())->toThrow(DocumentException::class); + }); +}); + +describe('::whereJsonPathMatches()', function () { + test('uses default parameter [PostgreSQL]', function () { + Configuration::overrideMode(Mode::PgSQL); + expect(Query::whereJsonPathMatches())->toBe('jsonb_path_exists(data, :path::jsonpath)'); + }); + test('uses provided parameter [PostgreSQL]', function () { + Configuration::overrideMode(Mode::PgSQL); + expect(Query::whereJsonPathMatches(':road'))->toBe('jsonb_path_exists(data, :road::jsonpath)'); + }); + test('throws [SQLite]', function () { + expect(fn () => Query::whereJsonPathMatches())->toThrow(DocumentException::class); + }); +}); + +describe('::insert()', function () { + test('generates with no auto-ID [PostgreSQL]', function () { + Configuration::overrideMode(Mode::PgSQL); + expect(Query::insert('test_tbl'))->toBe('INSERT INTO test_tbl VALUES (:data)'); + }); + test('generates with no auto-ID [SQLite]', function () { + expect(Query::insert('test_tbl'))->toBe('INSERT INTO test_tbl VALUES (:data)'); + }); + test('generates with auto numeric ID [PostgreSQL]', function () { + Configuration::overrideMode(Mode::PgSQL); + expect(Query::insert('test_tbl', AutoId::Number)) + ->toBe("INSERT INTO test_tbl VALUES (:data::jsonb || ('{\"id\":' " + . "|| (SELECT COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM test_tbl) || '}')::jsonb)"); + }); + test('generates with auto numeric ID [SQLite]', function () { + expect(Query::insert('test_tbl', AutoId::Number)) + ->toBe("INSERT INTO test_tbl VALUES (json_set(:data, '$.id', " + . "(SELECT coalesce(max(data->>'id'), 0) + 1 FROM test_tbl)))"); + }); + test('generates with auto UUID [PostgreSQL]', function () { + Configuration::overrideMode(Mode::PgSQL); + expect(Query::insert('test_tbl', AutoId::UUID)) + ->toStartWith("INSERT INTO test_tbl VALUES (:data::jsonb || '{\"id\":\"") + ->toEndWith("\"}')"); + }); + test('generates with auto UUID [SQLite]', function () { + expect(Query::insert('test_tbl', AutoId::UUID)) + ->toStartWith("INSERT INTO test_tbl VALUES (json_set(:data, '$.id', '") + ->toEndWith("'))"); + }); + test('generates with auto random string [PostgreSQL]', function () { + Configuration::overrideMode(Mode::PgSQL); + Configuration::$idStringLength = 8; + try { + $query = Query::insert('test_tbl', AutoId::RandomString); + expect($query) + ->toStartWith("INSERT INTO test_tbl VALUES (:data::jsonb || '{\"id\":\"") + ->toEndWith("\"}')") + ->and(str_replace(["INSERT INTO test_tbl VALUES (:data::jsonb || '{\"id\":\"", "\"}')"], '', $query)) + ->toHaveLength(8); + } finally { + Configuration::$idStringLength = 16; + } + }); + test('generates with auto random string [SQLite]', function () { + $query = Query::insert('test_tbl', AutoId::RandomString); + expect($query) + ->toStartWith("INSERT INTO test_tbl VALUES (json_set(:data, '$.id', '") + ->toEndWith("'))") + ->and(str_replace(["INSERT INTO test_tbl VALUES (json_set(:data, '$.id', '", "'))"], '', $query)) + ->toHaveLength(16); + }); + test('throws when mode not set', function () { + Configuration::overrideMode(null); + expect(fn () => Query::insert('kaboom'))->toThrow(DocumentException::class); + }); +}); + +describe('::save()', function () { + test('generates the correct query', function () { + expect(Query::save('test_tbl')) + ->toBe("INSERT INTO test_tbl VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data"); + }); +}); + +describe('::update()', function () { + test('generates the correct query', function () { + expect(Query::update('testing'))->toBe("UPDATE testing SET data = :data WHERE data->>'id' = :id"); + }); +}); + +describe('::orderBy()', function () { + test('returns blank for no criteria [PostgreSQL]', function () { + Configuration::overrideMode(Mode::PgSQL); + expect(Query::orderBy([]))->toBeEmpty(); + }); + test('returns blank for no criteria [SQLite]', function () { + expect(Query::orderBy([]))->toBeEmpty(); + }); + test('generates one field with no direction [PostgreSQL]', function () { + Configuration::overrideMode(Mode::PgSQL); + expect(Query::orderBy([Field::named('TestField')]))->toBe(" ORDER BY data->>'TestField'"); + }); + test('generates one field with no direction [SQLite]', function () { + expect(Query::orderBy([Field::named('TestField')]))->toBe(" ORDER BY data->>'TestField'"); + }); + test('generates with one qualified field [PostgreSQL]', function () { + Configuration::overrideMode(Mode::PgSQL); + $field = Field::named('TestField'); + $field->qualifier = 'qual'; + expect(Query::orderBy([$field]))->toBe(" ORDER BY qual.data->>'TestField'"); + }); + test('generates with one qualified field [SQLite]', function () { + $field = Field::named('TestField'); + $field->qualifier = 'qual'; + expect(Query::orderBy([$field]))->toBe(" ORDER BY qual.data->>'TestField'"); + }); + test('generates with multiple fields and direction [PostgreSQL]', function () { + Configuration::overrideMode(Mode::PgSQL); + expect(Query::orderBy( + [Field::named('Nested.Test.Field DESC'), Field::named('AnotherField'), Field::named('It DESC')])) + ->toBe(" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC"); + }); + test('generates with multiple fields and direction [SQLite]', function () { + expect(Query::orderBy( + [Field::named('Nested.Test.Field DESC'), Field::named('AnotherField'), Field::named('It DESC')])) + ->toBe(" ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC"); + }); + test('generates with numeric field [PostgreSQL]', function () { + Configuration::overrideMode(Mode::PgSQL); + expect(Query::orderBy([Field::named('n:Test')]))->toBe(" ORDER BY (data->>'Test')::numeric"); + }); + test('generates with numeric field [SQLite]', function () { + expect(Query::orderBy([Field::named('n:Test')]))->toBe(" ORDER BY data->>'Test'"); + }); + test('generates case-insensitive ordering [PostgreSQL]', function () { + Configuration::overrideMode(Mode::PgSQL); + expect(Query::orderBy([Field::named('i:Test.Field DESC NULLS FIRST')])) + ->toBe(" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST"); + }); + test('generates case-insensitive ordering [SQLite]', function () { + expect(Query::orderBy([Field::named('i:Test.Field ASC NULLS LAST')])) + ->toBe(" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST"); + }); +}); diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php deleted file mode 100644 index 75606a4..0000000 --- a/tests/unit/QueryTest.php +++ /dev/null @@ -1,323 +0,0 @@ - - * @license MIT - */ - -declare(strict_types=1); - -namespace Test\Unit; - -use BitBadger\PDODocument\{AutoId, Configuration, DocumentException, Field, FieldMatch, Mode, Query}; -use PHPUnit\Framework\Attributes\TestDox; -use PHPUnit\Framework\TestCase; - -/** - * Unit tests for the Query class - */ -#[TestDox('Query (Unit tests)')] -class QueryTest extends TestCase -{ - protected function setUp(): void - { - Configuration::overrideMode(Mode::SQLite); - } - - protected function tearDown(): void - { - Configuration::overrideMode(null); - } - - #[TestDox('selectFromTable() succeeds')] - public function testSelectFromTableSucceeds(): void - { - $this->assertEquals('SELECT data FROM testing', Query::selectFromTable('testing'), - 'Query not constructed correctly'); - } - - #[TestDox('whereByFields() succeeds for single field')] - public function testWhereByFieldsSucceedsForSingleField(): void - { - $this->assertEquals("data->>'test_field' <= :it", - Query::whereByFields([Field::lessOrEqual('test_field', '', ':it')]), - 'WHERE fragment not constructed correctly'); - } - - #[TestDox('whereByFields() succeeds for multiple fields All')] - public function testWhereByFieldsSucceedsForMultipleFieldsAll(): void - { - $this->assertEquals("data->>'test_field' <= :it AND data->>'other_field' = :other", - Query::whereByFields( - [Field::lessOrEqual('test_field', '', ':it'), Field::equal('other_field', '', ':other')]), - 'WHERE fragment not constructed correctly'); - } - - #[TestDox('whereByFields() succeeds for multiple fields Any')] - public function testWhereByFieldsSucceedsForMultipleFieldsAny(): void - { - $this->assertEquals("data->>'test_field' <= :it OR data->>'other_field' = :other", - Query::whereByFields( - [Field::lessOrEqual('test_field', '', ':it'), Field::equal('other_field', '', ':other')], - FieldMatch::Any), - 'WHERE fragment not constructed correctly'); - } - - #[TestDox('whereById() succeeds with default parameter')] - public function testWhereByIdSucceedsWithDefaultParameter(): void - { - $this->assertEquals("data->>'id' = :id", Query::whereById(), 'WHERE fragment not constructed correctly'); - } - - #[TestDox('whereById() succeeds with specific parameter')] - public function testWhereByIdSucceedsWithSpecificParameter(): void - { - $this->assertEquals("data->>'id' = :di", Query::whereById(':di'), 'WHERE fragment not constructed correctly'); - } - - #[TestDox('whereDataContains() succeeds with default parameter')] - public function testWhereDataContainsSucceedsWithDefaultParameter(): void - { - Configuration::overrideMode(Mode::PgSQL); - $this->assertEquals('data @> :criteria', Query::whereDataContains(), - 'WHERE fragment not constructed correctly'); - } - - #[TestDox('whereDataContains() succeeds with specific parameter')] - public function testWhereDataContainsSucceedsWithSpecifiedParameter(): void - { - Configuration::overrideMode(Mode::PgSQL); - $this->assertEquals('data @> :it', Query::whereDataContains(':it'), 'WHERE fragment not constructed correctly'); - } - - #[TestDox('whereDataContains() fails if not PostgreSQL')] - public function testWhereDataContainsFailsIfNotPostgreSQL(): void - { - Configuration::overrideMode(null); - $this->expectException(DocumentException::class); - Query::whereDataContains(); - } - - #[TestDox('whereJsonPathMatches() succeeds with default parameter')] - public function testWhereJsonPathMatchesSucceedsWithDefaultParameter(): void - { - Configuration::overrideMode(Mode::PgSQL); - $this->assertEquals('jsonb_path_exists(data, :path::jsonpath)', Query::whereJsonPathMatches(), - 'WHERE fragment not constructed correctly'); - } - - #[TestDox('whereJsonPathMatches() succeeds with specified parameter')] - public function testWhereJsonPathMatchesSucceedsWithSpecifiedParameter(): void - { - Configuration::overrideMode(Mode::PgSQL); - $this->assertEquals('jsonb_path_exists(data, :road::jsonpath)', Query::whereJsonPathMatches(':road'), - 'WHERE fragment not constructed correctly'); - } - - #[TestDox('whereJsonPathMatches() fails if not PostgreSQL')] - public function testWhereJsonPathMatchesFailsIfNotPostgreSQL(): void - { - Configuration::overrideMode(null); - $this->expectException(DocumentException::class); - Query::whereJsonPathMatches(); - } - - #[TestDox('insert() succeeds with no auto-ID for PostgreSQL')] - public function testInsertSucceedsWithNoAutoIdForPostgreSQL(): void - { - Configuration::overrideMode(Mode::PgSQL); - $this->assertEquals('INSERT INTO test_tbl VALUES (:data)', Query::insert('test_tbl'), - 'INSERT statement not constructed correctly'); - } - - #[TestDox('insert() succeeds with no auto-ID for SQLite')] - public function testInsertSucceedsWithNoAutoIdForSQLite(): void - { - $this->assertEquals('INSERT INTO test_tbl VALUES (:data)', Query::insert('test_tbl'), - 'INSERT statement not constructed correctly'); - } - - #[TestDox('insert() succeeds with auto numeric ID for PostgreSQL')] - public function testInsertSucceedsWithAutoNumericIdForPostgreSQL(): void - { - Configuration::overrideMode(Mode::PgSQL); - $this->assertEquals( - "INSERT INTO test_tbl VALUES (:data::jsonb || ('{\"id\":' " - . "|| (SELECT COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM test_tbl) || '}')::jsonb)", - Query::insert('test_tbl', AutoId::Number), 'INSERT statement not constructed correctly'); - } - - #[TestDox('insert() succeeds with auto numeric ID for SQLite')] - public function testInsertSucceedsWithAutoNumericIdForSQLite(): void - { - $this->assertEquals( - "INSERT INTO test_tbl VALUES (json_set(:data, '$.id', " - . "(SELECT coalesce(max(data->>'id'), 0) + 1 FROM test_tbl)))", - Query::insert('test_tbl', AutoId::Number), 'INSERT statement not constructed correctly'); - } - - #[TestDox('insert() succeeds with auto UUID for PostgreSQL')] - public function testInsertSucceedsWithAutoUuidForPostgreSQL(): void - { - Configuration::overrideMode(Mode::PgSQL); - $query = Query::insert('test_tbl', AutoId::UUID); - $this->assertStringStartsWith("INSERT INTO test_tbl VALUES (:data::jsonb || '{\"id\":\"", $query, - 'INSERT statement not constructed correctly'); - $this->assertStringEndsWith("\"}')", $query, 'INSERT statement not constructed correctly'); - } - - #[TestDox('insert() succeeds with auto UUID for SQLite')] - public function testInsertSucceedsWithAutoUuidForSQLite(): void - { - $query = Query::insert('test_tbl', AutoId::UUID); - $this->assertStringStartsWith("INSERT INTO test_tbl VALUES (json_set(:data, '$.id', '", $query, - 'INSERT statement not constructed correctly'); - $this->assertStringEndsWith("'))", $query, 'INSERT statement not constructed correctly'); - } - - #[TestDox('insert() succeeds with auto random string for PostgreSQL')] - public function testInsertSucceedsWithAutoRandomStringForPostgreSQL(): void - { - Configuration::overrideMode(Mode::PgSQL); - Configuration::$idStringLength = 8; - try { - $query = Query::insert('test_tbl', AutoId::RandomString); - $this->assertStringStartsWith("INSERT INTO test_tbl VALUES (:data::jsonb || '{\"id\":\"", $query, - 'INSERT statement not constructed correctly'); - $this->assertStringEndsWith("\"}')", $query, 'INSERT statement not constructed correctly'); - $id = str_replace(["INSERT INTO test_tbl VALUES (:data::jsonb || '{\"id\":\"", "\"}')"], '', $query); - $this->assertEquals(8, strlen($id), "Generated ID [$id] should have been 8 characters long"); - } finally { - Configuration::$idStringLength = 16; - } - } - - #[TestDox('insert() succeeds with auto random string for SQLite')] - public function testInsertSucceedsWithAutoRandomStringForSQLite(): void - { - $query = Query::insert('test_tbl', AutoId::RandomString); - $this->assertStringStartsWith("INSERT INTO test_tbl VALUES (json_set(:data, '$.id', '", $query, - 'INSERT statement not constructed correctly'); - $this->assertStringEndsWith("'))", $query, 'INSERT statement not constructed correctly'); - $id = str_replace(["INSERT INTO test_tbl VALUES (json_set(:data, '$.id', '", "'))"], '', $query); - $this->assertEquals(16, strlen($id), "Generated ID [$id] should have been 16 characters long"); - } - - #[TestDox('insert() fails when mode not set')] - public function testInsertFailsWhenModeNotSet(): void - { - $this->expectException(DocumentException::class); - Configuration::overrideMode(null); - Query::insert('kaboom'); - } - - #[TestDox('save() succeeds')] - public function testSaveSucceeds(): void - { - $this->assertEquals( - "INSERT INTO test_tbl VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data", - Query::save('test_tbl'), 'INSERT ON CONFLICT statement not constructed correctly'); - } - - #[TestDox('update() succeeds')] - public function testUpdateSucceeds(): void - { - $this->assertEquals("UPDATE testing SET data = :data WHERE data->>'id' = :id", Query::update('testing'), - 'UPDATE statement not constructed correctly'); - } - - #[TestDox('orderBy() succeeds with no fields for PostgreSQL')] - public function testOrderBySucceedsWithNoFieldsForPostgreSQL(): void - { - Configuration::overrideMode(Mode::PgSQL); - $this->assertEquals('', Query::orderBy([]), 'ORDER BY should have been blank'); - } - - #[TestDox('orderBy() succeeds with no fields for SQLite')] - public function testOrderBySucceedsWithNoFieldsForSQLite(): void - { - $this->assertEquals('', Query::orderBy([]), 'ORDER BY should have been blank'); - } - - #[TestDox('orderBy() succeeds with one field and no direction for PostgreSQL')] - public function testOrderBySucceedsWithOneFieldAndNoDirectionForPostgreSQL(): void - { - Configuration::overrideMode(Mode::PgSQL); - $this->assertEquals(" ORDER BY data->>'TestField'", Query::orderBy([Field::named('TestField')]), - 'ORDER BY not constructed correctly'); - } - - #[TestDox('orderBy() succeeds with one field and no direction for SQLite')] - public function testOrderBySucceedsWithOneFieldAndNoDirectionForSQLite(): void - { - $this->assertEquals(" ORDER BY data->>'TestField'", Query::orderBy([Field::named('TestField')]), - 'ORDER BY not constructed correctly'); - } - - #[TestDox('orderBy() succeeds with one qualified field for PostgreSQL')] - public function testOrderBySucceedsWithOneQualifiedFieldForPostgreSQL(): void - { - Configuration::overrideMode(Mode::PgSQL); - $field = Field::named('TestField'); - $field->qualifier = 'qual'; - $this->assertEquals(" ORDER BY qual.data->>'TestField'", Query::orderBy([$field]), - 'ORDER BY not constructed correctly'); - } - - #[TestDox('orderBy() succeeds with one qualified field for SQLite')] - public function testOrderBySucceedsWithOneQualifiedFieldForSQLite(): void - { - $field = Field::named('TestField'); - $field->qualifier = 'qual'; - $this->assertEquals(" ORDER BY qual.data->>'TestField'", Query::orderBy([$field]), - 'ORDER BY not constructed correctly'); - } - - #[TestDox('orderBy() succeeds with multiple fields and direction for PostgreSQL')] - public function testOrderBySucceedsWithMultipleFieldsAndDirectionForPostgreSQL(): void - { - Configuration::overrideMode(Mode::PgSQL); - $this->assertEquals(" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", - Query::orderBy( - [Field::named('Nested.Test.Field DESC'), Field::named('AnotherField'), Field::named('It DESC')]), - 'ORDER BY not constructed correctly'); - } - - #[TestDox('orderBy() succeeds with multiple fields and direction for SQLite')] - public function testOrderBySucceedsWithMultipleFieldsAndDirectionForSQLite(): void - { - $this->assertEquals(" ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC", - Query::orderBy( - [Field::named('Nested.Test.Field DESC'), Field::named('AnotherField'), Field::named('It DESC')]), - 'ORDER BY not constructed correctly'); - } - - #[TestDox('orderBy() succeeds with numeric field for PostgreSQL')] - public function testOrderBySucceedsWithNumericFieldForPostgreSQL(): void - { - Configuration::overrideMode(Mode::PgSQL); - $this->assertEquals(" ORDER BY (data->>'Test')::numeric", Query::orderBy([Field::named('n:Test')]), - 'ORDER BY not constructed correctly'); - } - - #[TestDox('orderBy() succeeds with numeric field for SQLite')] - public function testOrderBySucceedsWithNumericFieldForSQLite(): void - { - $this->assertEquals(" ORDER BY data->>'Test'", Query::orderBy([Field::named('n:Test')]), - 'ORDER BY not constructed correctly'); - } - - #[TestDox('orderBy() succeeds with case-insensitive ordering for PostgreSQL')] - public function testOrderBySucceedsWithCaseInsensitiveOrderingForPostgreSQL(): void - { - Configuration::overrideMode(Mode::PgSQL); - $this->assertEquals(" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST", - Query::orderBy([Field::named('i:Test.Field DESC NULLS FIRST')]), 'ORDER BY not constructed correctly'); - } - - #[TestDox('orderBy() succeeds with case-insensitive ordering for SQLite')] - public function testOrderBySucceedsWithCaseInsensitiveOrderingForSQLite(): void - { - $this->assertEquals(" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST", - Query::orderBy([Field::named('i:Test.Field ASC NULLS LAST')]), 'ORDER BY not constructed correctly'); - } -}