From 294b608ac8a4569242a450f75c2017d7d52b7d0a Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 22 Sep 2024 17:44:07 -0400 Subject: [PATCH] Implement In/InArray - WIP on testdox name changes --- src/Field.php | 81 +++++-- tests/unit/ConfigurationTest.php | 10 +- tests/unit/DocumentExceptionTest.php | 2 + tests/unit/FieldMatchTest.php | 4 +- tests/unit/FieldTest.php | 337 ++++++++++++++++++++++----- tests/unit/ModeTest.php | 6 +- tests/unit/OpTest.php | 22 +- tests/unit/ParametersTest.php | 19 +- tests/unit/QueryTest.php | 37 +-- 9 files changed, 402 insertions(+), 116 deletions(-) diff --git a/src/Field.php b/src/Field.php index f3104cc..005f056 100644 --- a/src/Field.php +++ b/src/Field.php @@ -46,12 +46,48 @@ class Field $existing["{$this->paramName}min"] = $this->value[0]; $existing["{$this->paramName}max"] = $this->value[1]; break; + case Op::In: + for ($idx = 0; $idx < count($this->value); $idx++) { + $existing["{$this->paramName}_$idx"] = $this->value[$idx]; + } + break; + case Op::InArray: + $mkString = Configuration::mode("Append parameters for InArray condition") === Mode::PgSQL; + $values = $this->value['values']; + for ($idx = 0; $idx < count($values); $idx++) { + $existing["{$this->paramName}_$idx"] = $mkString ? "$values[$idx]" : $values[$idx]; + } + break; default: $existing[$this->paramName] = $this->value; } return $existing; } + /** + * Derive the path for this field + * + * @param bool $asJSON Whether the field should be treated as JSON in the query (optional, default false) + * @return string The path for this field + * @throws Exception If the database mode has not been set + */ + public function path(bool $asJSON = false): string + { + $extra = $asJSON ? '' : '>'; + if (str_contains($this->fieldName, '.')) { + $mode = Configuration::mode('determine field path'); + if ($mode === Mode::PgSQL) { + return "data#>$extra'{" . implode(',', explode('.', $this->fieldName)) . "}'"; + } + if ($mode === Mode::SQLite) { + $parts = explode('.', $this->fieldName); + $last = array_pop($parts); + return "data->'" . implode("'->'", $parts) . "'->$extra'$last'"; + } + } + return "data->$extra'$this->fieldName'"; + } + /** * Get the WHERE clause fragment for this parameter * @@ -60,27 +96,41 @@ class Field */ public function toWhere(): string { - $mode = Configuration::mode('make field WHERE clause'); - $fieldName = (empty($this->qualifier) ? '' : "$this->qualifier.") . 'data' . match (true) { - !str_contains($this->fieldName, '.') => "->>'$this->fieldName'", - $mode === Mode::PgSQL => "#>>'{" . implode(',', explode('.', $this->fieldName)) . "}'", - $mode === Mode::SQLite => "->>'" . implode("'->>'", explode('.', $this->fieldName)) . "'", - }; - $fieldPath = match ($mode) { + $mode = Configuration::mode('make field WHERE clause'); + $fieldName = (empty($this->qualifier) ? '' : "$this->qualifier.") . $this->path($this->op === Op::InArray); + $fieldPath = match ($mode) { Mode::PgSQL => match (true) { - $this->op === Op::Between => is_numeric($this->value[0]) ? "($fieldName)::numeric" : $fieldName, + $this->op === Op::Between, + $this->op === Op::In => is_numeric($this->value[0]) ? "($fieldName)::numeric" : $fieldName, is_numeric($this->value) => "($fieldName)::numeric", default => $fieldName, }, default => $fieldName, }; $criteria = match ($this->op) { - Op::Exists, Op::NotExists => '', - Op::Between => " {$this->paramName}min AND {$this->paramName}max", - Op::In => "TODO", - default => " $this->paramName", + Op::Exists, + Op::NotExists => '', + Op::Between => " {$this->paramName}min AND {$this->paramName}max", + Op::In => ' (' . $this->inParameterNames() . ')', + Op::InArray => $mode === Mode::PgSQL ? ' ARRAY[' . $this->inParameterNames() . ']' : '', + default => " $this->paramName", }; - return $fieldPath . ' ' . $this->op->toSQL() . $criteria; + return $mode === Mode::SQLite && $this->op === Op::InArray + ? "EXISTS (SELECT 1 FROM json_each({$this->value['table']}.data, '\$.$this->fieldName') WHERE value IN (" + . $this->inParameterNames() . '))' + : $fieldPath . ' ' . $this->op->toSQL() . $criteria; + } + + /** + * Create parameter names for an IN clause + * + * @return string A comma-delimited string of parameter names + */ + private function inParameterNames(): string + { + $values = $this->op === Op::In ? $this->value : $this->value['values']; + return implode(', ', + array_map(fn($value, $key) => "{$this->paramName}_$key", $values, range(0, count($values) - 1))); } /** @@ -284,13 +334,14 @@ class Field * Create an IN ARRAY field criterion * * @param string $fieldName The name of the field against which the values will be compared + * @param string $tableName The table name where this field is located * @param mixed[] $values The potential matching values for the field * @param string $paramName The name of the parameter to which this should be bound (optional; generated if blank) * @return self The field with the requested criterion */ - public static function inArray(string $fieldName, array $values, string $paramName = ''): self + public static function inArray(string $fieldName, string $tableName, array $values, string $paramName = ''): self { - return new self($fieldName, Op::InArray, $values, $paramName); + return new self($fieldName, Op::InArray, ['table' => $tableName, 'values' => $values], $paramName); } /** diff --git a/tests/unit/ConfigurationTest.php b/tests/unit/ConfigurationTest.php index 938fcdc..acbffe1 100644 --- a/tests/unit/ConfigurationTest.php +++ b/tests/unit/ConfigurationTest.php @@ -18,13 +18,13 @@ use PHPUnit\Framework\TestCase; #[TestDox('Configuration (Unit tests)')] class ConfigurationTest extends TestCase { - #[TestDox('ID field default succeeds')] + #[TestDox('id default succeeds')] public function testIdFieldDefaultSucceeds(): void { $this->assertEquals('id', Configuration::$idField, 'Default ID field should be "id"'); } - #[TestDox('ID field change succeeds')] + #[TestDox('id change succeeds')] public function testIdFieldChangeSucceeds(): void { try { @@ -36,19 +36,19 @@ class ConfigurationTest extends TestCase } } - #[TestDox('Auto ID default succeeds')] + #[TestDox('autoId default succeeds')] public function testAutoIdDefaultSucceeds(): void { $this->assertEquals(AutoId::None, Configuration::$autoId, 'Auto ID should default to None'); } - #[TestDox('ID string length default succeeds')] + #[TestDox('idStringLength default succeeds')] public function testIdStringLengthDefaultSucceeds(): void { $this->assertEquals(16, Configuration::$idStringLength, 'ID string length should default to 16'); } - #[TestDox("Db conn fails when no DSN specified")] + #[TestDox("dbConn() fails when no DSN specified")] public function testDbConnFailsWhenNoDSNSpecified(): void { $this->expectException(DocumentException::class); diff --git a/tests/unit/DocumentExceptionTest.php b/tests/unit/DocumentExceptionTest.php index 27df03d..c656055 100644 --- a/tests/unit/DocumentExceptionTest.php +++ b/tests/unit/DocumentExceptionTest.php @@ -38,6 +38,7 @@ class DocumentExceptionTest extends TestCase $this->assertNull($ex->getPrevious(), 'Prior exception should have been null'); } + #[TestDox('toString() succeeds without code')] public function testToStringSucceedsWithoutCode(): void { $ex = new DocumentException('Test failure'); @@ -45,6 +46,7 @@ class DocumentExceptionTest extends TestCase 'toString not generated correctly'); } + #[TestDox('toString() succeeds with code')] public function testToStringSucceedsWithCode(): void { $ex = new DocumentException('Oof', -6); diff --git a/tests/unit/FieldMatchTest.php b/tests/unit/FieldMatchTest.php index f2ea0f0..f1d0193 100644 --- a/tests/unit/FieldMatchTest.php +++ b/tests/unit/FieldMatchTest.php @@ -18,13 +18,13 @@ use PHPUnit\Framework\TestCase; #[TestDox('Field Match (Unit tests)')] class FieldMatchTest extends TestCase { - #[TestDox('To SQL succeeds for all')] + #[TestDox('toSQL() succeeds for All')] public function testToSQLSucceedsForAll(): void { $this->assertEquals('AND', FieldMatch::All->toSQL(), 'All should have returned AND'); } - #[TestDox('To SQL succeeds for any')] + #[TestDox('toSQL() succeeds for Any')] public function testToSQLSucceedsForAny(): void { $this->assertEquals('OR', FieldMatch::Any->toSQL(), 'Any should have returned OR'); diff --git a/tests/unit/FieldTest.php b/tests/unit/FieldTest.php index a327e47..a8c78ee 100644 --- a/tests/unit/FieldTest.php +++ b/tests/unit/FieldTest.php @@ -18,33 +18,166 @@ use PHPUnit\Framework\TestCase; #[TestDox('Field (Unit tests)')] class FieldTest extends TestCase { + #[TestDox('appendParameter() succeeds for exists')] public function testAppendParameterSucceedsForExists(): void { $this->assertEquals([], Field::exists('exists')->appendParameter([]), 'exists should not have appended a parameter'); } - #[TestDox('Append parameter succeeds for notExists')] + #[TestDox('appendParameter() succeeds for notExists')] public function testAppendParameterSucceedsForNotExists(): void { $this->assertEquals([], Field::notExists('absent')->appendParameter([]), 'notExists should not have appended a parameter'); } + #[TestDox('appendParameter() succeeds for between')] public function testAppendParameterSucceedsForBetween(): void { $this->assertEquals(['@nummin' => 5, '@nummax' => 9], Field::between('exists', 5, 9, '@num')->appendParameter([]), - 'BT should have appended min and max parameters'); + 'Between should have appended min and max parameters'); } + #[TestDox('appendParameter() succeeds for in')] + public function testAppendParameterSucceedsForIn(): void + { + $this->assertEquals([':val_0' => 'test', ':val_1' => 'unit', ':val_2' => 'great'], + Field::in('it', ['test', 'unit', 'great'], ':val')->appendParameter([]), + 'In should have appended 3 parameters for the input values'); + } + + #[TestDox('appendParameter() succeeds for inArray for PostgreSQL')] + public function testAppendParameterSucceedsForInArrayForPostgreSQL(): void + { + Configuration::overrideMode(Mode::PgSQL); + try { + $this->assertEquals([':bit_0' => '2', ':bit_1' => '8', ':bit_2' => '64'], + Field::inArray('it', 'table', [2, 8, 64], ':bit')->appendParameter([]), + 'InArray should have appended 3 string parameters for the input values'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('appendParameter() succeeds for inArray for SQLite')] + public function testAppendParameterSucceedsForInArrayForSQLite(): void + { + Configuration::overrideMode(Mode::SQLite); + try { + $this->assertEquals([':bit_0' => 2, ':bit_1' => 8, ':bit_2' => 64], + Field::inArray('it', 'table', [2, 8, 64], ':bit')->appendParameter([]), + 'InArray should have appended 3 parameters for the input values'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('appendParameter() succeeds for others')] public function testAppendParameterSucceedsForOthers(): void { $this->assertEquals(['@test' => 33], Field::equal('the_field', 33, '@test')->appendParameter([]), 'Field parameter not returned correctly'); } - #[TestDox('To where succeeds for exists without qualifier for PostgreSQL')] + #[TestDox('path() succeeds for simple SQL path for PostgreSQL')] + public function testPathSucceedsForSimpleSqlPathForPostgreSQL() + { + Configuration::overrideMode(Mode::PgSQL); + try { + $this->assertEquals("data->>'it'", Field::equal('it', 'that')->path(), + 'SQL value path not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('path() succeeds for simple SQL path for SQLite')] + public function testPathSucceedsForSimpleSqlPathForSQLite() + { + Configuration::overrideMode(Mode::SQLite); + try { + $this->assertEquals("data->>'top'", Field::equal('top', 'that')->path(), + 'SQL value path not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('path() succeeds for nested SQL path for PostgreSQL')] + public function testPathSucceedsForNestedSqlPathForPostgreSQL() + { + Configuration::overrideMode(Mode::PgSQL); + try { + $this->assertEquals("data#>>'{parts,to,the,path}'", Field::equal('parts.to.the.path', '')->path(), + 'SQL value path not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('path() succeeds for nested SQL path for SQLite')] + public function testPathSucceedsForNestedSqlPathForSQLite() + { + Configuration::overrideMode(Mode::SQLite); + try { + $this->assertEquals("data->'one'->'two'->>'three'", Field::equal('one.two.three', '')->path(), + 'SQL value path not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('path() succeeds for simple JSON path for PostgreSQL')] + public function testPathSucceedsForSimpleJsonPathForPostgreSQL() + { + Configuration::overrideMode(Mode::PgSQL); + try { + $this->assertEquals("data->'it'", Field::equal('it', 'that')->path(true), + 'JSON value path not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('path() succeeds for simple JSON path for SQLite')] + public function testPathSucceedsForSimpleJsonPathForSQLite() + { + Configuration::overrideMode(Mode::SQLite); + try { + $this->assertEquals("data->'top'", Field::equal('top', 'that')->path(true), + 'JSON value path not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('path() succeeds for nested JSON path for PostgreSQL')] + public function testPathSucceedsForNestedJsonPathForPostgreSQL() + { + Configuration::overrideMode(Mode::PgSQL); + try { + $this->assertEquals("data#>'{parts,to,the,path}'", Field::equal('parts.to.the.path', '')->path(true), + 'JSON value path not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('path() succeeds for nested JSON path for SQLite')] + public function testPathSucceedsForNestedJsonPathForSQLite() + { + Configuration::overrideMode(Mode::SQLite); + try { + $this->assertEquals("data->'one'->'two'->'three'", Field::equal('one.two.three', '')->path(true), + 'SQL value path not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('toWhere() succeeds for exists without qualifier for PostgreSQL')] public function testToWhereSucceedsForExistsWithoutQualifierForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -56,7 +189,7 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds for exists without qualifier for SQLite')] + #[TestDox('toWhere() succeeds for exists without qualifier for SQLite')] public function testToWhereSucceedsForExistsWithoutQualifierForSQLite(): void { Configuration::overrideMode(Mode::SQLite); @@ -68,7 +201,7 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds for notExists without qualifier for PostgreSQL')] + #[TestDox('toWhere() succeeds for notExists without qualifier for PostgreSQL')] public function testToWhereSucceedsForNotExistsWithoutQualifierForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -80,7 +213,7 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds for notExists without qualifier for SQLite')] + #[TestDox('toWhere() succeeds for notExists without qualifier for SQLite')] public function testToWhereSucceedsForNotExistsWithoutQualifierForSQLite(): void { Configuration::overrideMode(Mode::SQLite); @@ -92,7 +225,7 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds for between without qualifier for SQLite')] + #[TestDox('toWhere() succeeds for between without qualifier for SQLite')] public function testToWhereSucceedsForBetweenWithoutQualifierForSQLite(): void { Configuration::overrideMode(Mode::SQLite); @@ -104,7 +237,7 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds for between without qualifier for PostgreSQL with numeric range')] + #[TestDox('toWhere() succeeds for between without qualifier for PostgreSQL with numeric range')] public function testToWhereSucceedsForBetweenWithoutQualifierForPostgreSQLWithNumericRange(): void { Configuration::overrideMode(Mode::PgSQL); @@ -116,7 +249,7 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds for between without qualifier for PostgreSQL with non-numeric range')] + #[TestDox('toWhere() succeeds for between without qualifier for PostgreSQL with non-numeric range')] public function testToWhereSucceedsForBetweenWithoutQualifierForPostgreSQLWithNonNumericRange(): void { Configuration::overrideMode(Mode::PgSQL); @@ -129,7 +262,7 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds for between with qualifier for SQLite')] + #[TestDox('toWhere() succeeds for between with qualifier for SQLite')] public function testToWhereSucceedsForBetweenWithQualifierForSQLite(): void { Configuration::overrideMode(Mode::SQLite); @@ -143,7 +276,7 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds for between with qualifier for PostgreSQL with numeric range')] + #[TestDox('toWhere() succeeds for between with qualifier for PostgreSQL with numeric range')] public function testToWhereSucceedsForBetweenWithQualifierForPostgreSQLWithNumericRange(): void { Configuration::overrideMode(Mode::PgSQL); @@ -157,7 +290,7 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds for between with qualifier for PostgreSQL with non-numeric range')] + #[TestDox('toWhere() succeeds for between with qualifier for PostgreSQL with non-numeric range')] public function testToWhereSucceedsForBetweenWithQualifierForPostgreSQLWithNonNumericRange(): void { Configuration::overrideMode(Mode::PgSQL); @@ -171,7 +304,73 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds for others without qualifier for PostgreSQL')] + #[TestDox('toWhere() succeeds for in for PostgreSQL with non-numeric values')] + public function testToWhereSucceedsForInForPostgreSQLWithNonNumericValues(): void + { + Configuration::overrideMode(Mode::PgSQL); + try { + $field = Field::in('test', ['Atlanta', 'Chicago'], ':city'); + $this->assertEquals("data->>'test' IN (:city_0, :city_1)", $field->toWhere(), + 'WHERE fragment not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('toWhere() succeeds for in for PostgreSQL with numeric values')] + public function testToWhereSucceedsForInForPostgreSQLWithNumericValues(): void + { + Configuration::overrideMode(Mode::PgSQL); + try { + $field = Field::in('even', [2, 4, 6], ':nbr'); + $this->assertEquals("(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)", $field->toWhere(), + 'WHERE fragment not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('toWhere() succeeds for in for SQLite')] + public function testToWhereSucceedsForInForSQLite(): void + { + Configuration::overrideMode(Mode::SQLite); + try { + $field = Field::in('test', ['Atlanta', 'Chicago'], ':city'); + $this->assertEquals("data->>'test' IN (:city_0, :city_1)", $field->toWhere(), + 'WHERE fragment not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('toWhere() succeeds for inArray for PostgreSQL')] + public function testToWhereSucceedsForInArrayForPostgreSQL(): void + { + Configuration::overrideMode(Mode::PgSQL); + try { + $field = Field::inArray('even', 'tbl', [2, 4, 6, 8], ':it'); + $this->assertEquals("data->'even' ?| ARRAY[:it_0, :it_1, :it_2, :it_3]", $field->toWhere(), + 'WHERE fragment not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('toWhere() succeeds for inArray for SQLite')] + public function testToWhereSucceedsForInArrayForSQLite(): void + { + Configuration::overrideMode(Mode::SQLite); + try { + $field = Field::inArray('test', 'tbl', ['Atlanta', 'Chicago'], ':city'); + $this->assertEquals( + "EXISTS (SELECT 1 FROM json_each(tbl.data, '\$.test') WHERE value IN (:city_0, :city_1))", + $field->toWhere(), 'WHERE fragment not generated correctly'); + } finally { + Configuration::overrideMode(null); + } + } + + #[TestDox('toWhere() succeeds for others without qualifier for PostgreSQL')] public function testToWhereSucceedsForOthersWithoutQualifierForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -183,7 +382,7 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds for others without qualifier for SQLite')] + #[TestDox('toWhere() succeeds for others without qualifier for SQLite')] public function testToWhereSucceedsForOthersWithoutQualifierForSQLite(): void { Configuration::overrideMode(Mode::SQLite); @@ -195,7 +394,7 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds with qualifier no parameter for PostgreSQL')] + #[TestDox('toWhere() succeeds with qualifier no parameter for PostgreSQL')] public function testToWhereSucceedsWithQualifierNoParameterForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -209,7 +408,7 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds with qualifier no parameter for SQLite')] + #[TestDox('toWhere() succeeds with qualifier no parameter for SQLite')] public function testToWhereSucceedsWithQualifierNoParameterForSQLite(): void { Configuration::overrideMode(Mode::SQLite); @@ -223,7 +422,7 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds with qualifier and parameter for PostgreSQL')] + #[TestDox('toWhere() succeeds with qualifier and parameter for PostgreSQL')] public function testToWhereSucceedsWithQualifierAndParameterForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -237,7 +436,7 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds with qualifier and parameter for SQLite')] + #[TestDox('toWhere() succeeds with qualifier and parameter for SQLite')] public function testToWhereSucceedsWithQualifierAndParameterForSQLite(): void { Configuration::overrideMode(Mode::SQLite); @@ -251,33 +450,7 @@ class FieldTest extends TestCase } } - #[TestDox('To where succeeds with sub-document for PostgreSQL')] - public function testToWhereSucceedsWithSubDocumentForPostgreSQL(): void - { - Configuration::overrideMode(Mode::PgSQL); - try { - $field = Field::equal('sub.foo.bar', 22, '@it'); - $this->assertEquals("(data#>>'{sub,foo,bar}')::numeric = @it", $field->toWhere(), - 'WHERE fragment not generated correctly'); - } finally { - Configuration::overrideMode(null); - } - } - - #[TestDox('To where succeeds with sub-document for SQLite')] - public function testToWhereSucceedsWithSubDocumentForSQLite(): void - { - Configuration::overrideMode(Mode::SQLite); - try { - $field = Field::equal('sub.foo.bar', 22, '@it'); - $this->assertEquals("data->>'sub'->>'foo'->>'bar' = @it", $field->toWhere(), - 'WHERE fragment not generated correctly'); - } finally { - Configuration::overrideMode(null); - } - } - - #[TestDox('equal succeeds without parameter')] + #[TestDox('equal() succeeds without parameter')] public function testEqualSucceedsWithoutParameter(): void { $field = Field::equal('my_test', 9); @@ -288,7 +461,7 @@ class FieldTest extends TestCase $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); } - #[TestDox('equal succeeds with parameter')] + #[TestDox('equal() succeeds with parameter')] public function testEqualSucceedsWithParameter(): void { $field = Field::equal('another_test', 'turkey', '@test'); @@ -299,7 +472,7 @@ class FieldTest extends TestCase $this->assertEquals('@test', $field->paramName, 'Parameter name not filled correctly'); } - #[TestDox('greater succeeds without parameter')] + #[TestDox('greater() succeeds without parameter')] public function testGreaterSucceedsWithoutParameter(): void { $field = Field::greater('your_test', 4); @@ -310,7 +483,7 @@ class FieldTest extends TestCase $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); } - #[TestDox('greater succeeds with parameter')] + #[TestDox('greater() succeeds with parameter')] public function testGreaterSucceedsWithParameter(): void { $field = Field::greater('more_test', 'chicken', '@value'); @@ -321,7 +494,7 @@ class FieldTest extends TestCase $this->assertEquals('@value', $field->paramName, 'Parameter name not filled correctly'); } - #[TestDox('greaterOrEqual succeeds without parameter')] + #[TestDox('greaterOrEqual() succeeds without parameter')] public function testGreaterOrEqualSucceedsWithoutParameter(): void { $field = Field::greaterOrEqual('their_test', 6); @@ -332,7 +505,7 @@ class FieldTest extends TestCase $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); } - #[TestDox('greaterOrEqual succeeds with parameter')] + #[TestDox('greaterOrEqual() succeeds with parameter')] public function testGreaterOrEqualSucceedsWithParameter(): void { $field = Field::greaterOrEqual('greater_test', 'poultry', '@cluck'); @@ -343,7 +516,7 @@ class FieldTest extends TestCase $this->assertEquals('@cluck', $field->paramName, 'Parameter name not filled correctly'); } - #[TestDox('less succeeds without parameter')] + #[TestDox('less() succeeds without parameter')] public function testLessSucceedsWithoutParameter(): void { $field = Field::less('z', 32); @@ -354,7 +527,7 @@ class FieldTest extends TestCase $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); } - #[TestDox('less succeeds with parameter')] + #[TestDox('less() succeeds with parameter')] public function testLessSucceedsWithParameter(): void { $field = Field::less('additional_test', 'fowl', '@boo'); @@ -365,7 +538,7 @@ class FieldTest extends TestCase $this->assertEquals('@boo', $field->paramName, 'Parameter name not filled correctly'); } - #[TestDox('lessOrEqual succeeds without parameter')] + #[TestDox('lessOrEqual() succeeds without parameter')] public function testLessOrEqualSucceedsWithoutParameter(): void { $field = Field::lessOrEqual('g', 87); @@ -376,7 +549,7 @@ class FieldTest extends TestCase $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); } - #[TestDox('lessOrEqual succeeds with parameter')] + #[TestDox('lessOrEqual() succeeds with parameter')] public function testLessOrEqualSucceedsWithParameter(): void { $field = Field::lessOrEqual('lesser_test', 'hen', '@woo'); @@ -387,7 +560,7 @@ class FieldTest extends TestCase $this->assertEquals('@woo', $field->paramName, 'Parameter name not filled correctly'); } - #[TestDox('notEqual succeeds without parameter')] + #[TestDox('notEqual() succeeds without parameter')] public function testNotEqualSucceedsWithoutParameter(): void { $field = Field::notEqual('j', 65); @@ -398,7 +571,7 @@ class FieldTest extends TestCase $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); } - #[TestDox('notEqual succeeds with parameter')] + #[TestDox('notEqual() succeeds with parameter')] public function testNotEqualSucceedsWithParameter(): void { $field = Field::notEqual('unequal_test', 'egg', '@zoo'); @@ -409,7 +582,7 @@ class FieldTest extends TestCase $this->assertEquals('@zoo', $field->paramName, 'Parameter name not filled correctly'); } - #[TestDox('between succeeds without parameter')] + #[TestDox('between() succeeds without parameter')] public function testBetweenSucceedsWithoutParameter(): void { $field = Field::between('k', 'alpha', 'zed'); @@ -420,7 +593,7 @@ class FieldTest extends TestCase $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); } - #[TestDox('between succeeds with parameter')] + #[TestDox('between() succeeds with parameter')] public function testBetweenSucceedsWithParameter(): void { $field = Field::between('between_test', 18, 49, '@count'); @@ -431,7 +604,51 @@ class FieldTest extends TestCase $this->assertEquals('@count', $field->paramName, 'Parameter name not filled correctly'); } - #[TestDox('exists succeeds')] + #[TestDox('in() succeeds without parameter')] + public function testInSucceedsWithoutParameter(): void + { + $field = Field::in('test', [1, 2, 3]); + $this->assertNotNull($field, 'The field should not have been null'); + $this->assertEquals('test', $field->fieldName, 'Field name not filled correctly'); + $this->assertEquals(Op::In, $field->op, 'Operation not filled correctly'); + $this->assertEquals([1, 2, 3], $field->value, 'Value not filled correctly'); + $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); + } + + #[TestDox('in() succeeds with parameter')] + public function testInSucceedsWithParameter(): void + { + $field = Field::in('unit', ['a', 'b'], '@inParam'); + $this->assertNotNull($field, 'The field should not have been null'); + $this->assertEquals('unit', $field->fieldName, 'Field name not filled correctly'); + $this->assertEquals(Op::In, $field->op, 'Operation not filled correctly'); + $this->assertEquals(['a', 'b'], $field->value, 'Value not filled correctly'); + $this->assertEquals('@inParam', $field->paramName, 'Parameter name not filled correctly'); + } + + #[TestDox('inArray() succeeds without parameter')] + public function testInArraySucceedsWithoutParameter(): void + { + $field = Field::inArray('test', 'tbl', [1, 2, 3]); + $this->assertNotNull($field, 'The field should not have been null'); + $this->assertEquals('test', $field->fieldName, 'Field name not filled correctly'); + $this->assertEquals(Op::InArray, $field->op, 'Operation not filled correctly'); + $this->assertEquals(['table' => 'tbl', 'values' => [1, 2, 3]], $field->value, 'Value not filled correctly'); + $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); + } + + #[TestDox('inArray() succeeds with parameter')] + public function testInArraySucceedsWithParameter(): void + { + $field = Field::inArray('unit', 'tab', ['a', 'b'], '@inAParam'); + $this->assertNotNull($field, 'The field should not have been null'); + $this->assertEquals('unit', $field->fieldName, 'Field name not filled correctly'); + $this->assertEquals(Op::InArray, $field->op, 'Operation not filled correctly'); + $this->assertEquals(['table' => 'tab', 'values' => ['a', 'b']], $field->value, 'Value not filled correctly'); + $this->assertEquals('@inAParam', $field->paramName, 'Parameter name not filled correctly'); + } + + #[TestDox('exists() succeeds')] public function testExistsSucceeds(): void { $field = Field::exists('be_there'); @@ -442,7 +659,7 @@ class FieldTest extends TestCase $this->assertEquals('', $field->paramName, 'Parameter name should have been blank'); } - #[TestDox('notExists succeeds')] + #[TestDox('notExists() succeeds')] public function testNotExistsSucceeds(): void { $field = Field::notExists('be_absent'); diff --git a/tests/unit/ModeTest.php b/tests/unit/ModeTest.php index aaede62..0987bcd 100644 --- a/tests/unit/ModeTest.php +++ b/tests/unit/ModeTest.php @@ -18,19 +18,19 @@ use PHPUnit\Framework\TestCase; #[TestDox('Mode (Unit tests)')] class ModeTest extends TestCase { - #[TestDox('Derive from DSN succeeds for PostgreSQL')] + #[TestDox('deriveFromDSN() succeeds for PostgreSQL')] public function testDeriveFromDSNSucceedsForPostgreSQL(): void { $this->assertEquals(Mode::PgSQL, Mode::deriveFromDSN('pgsql:Host=localhost'), 'PostgreSQL mode incorrect'); } - #[TestDox('Derive from DSN succeeds for SQLite')] + #[TestDox('deriveFromDSN() succeeds for SQLite')] public function testDeriveFromDSNSucceedsForSQLite(): void { $this->assertEquals(Mode::SQLite, Mode::deriveFromDSN('sqlite:data.db'), 'SQLite mode incorrect'); } - #[TestDox('Derive from DSN fails for MySQL')] + #[TestDox('deriveFromDSN() fails for MySQL')] public function testDeriveFromDSNFailsForMySQL(): void { $this->expectException(DocumentException::class); diff --git a/tests/unit/OpTest.php b/tests/unit/OpTest.php index 0d888f9..baff4df 100644 --- a/tests/unit/OpTest.php +++ b/tests/unit/OpTest.php @@ -18,67 +18,67 @@ use PHPUnit\Framework\TestCase; #[TestDox('Op (Unit tests)')] class OpTest extends TestCase { - #[TestDox('To SQL succeeds for Equal')] + #[TestDox('toSQL() succeeds for Equal')] public function testToSQLSucceedsForEqual(): void { $this->assertEquals('=', Op::Equal->toSQL(), 'Equal SQL operator incorrect'); } - #[TestDox('To SQL succeeds for Greater')] + #[TestDox('toSQL() succeeds for Greater')] public function testToSQLSucceedsForGreater(): void { $this->assertEquals('>', Op::Greater->toSQL(), 'Greater SQL operator incorrect'); } - #[TestDox('To SQL succeeds for GreaterOrEqual')] + #[TestDox('toSQL() succeeds for GreaterOrEqual')] public function testToSQLSucceedsForGreaterOrEqual(): void { $this->assertEquals('>=', Op::GreaterOrEqual->toSQL(), 'GreaterOrEqual SQL operator incorrect'); } - #[TestDox('To SQL succeeds for Less')] + #[TestDox('toSQL() succeeds for Less')] public function testToSQLSucceedsForLess(): void { $this->assertEquals('<', Op::Less->toSQL(), 'Less SQL operator incorrect'); } - #[TestDox('To SQL succeeds for LessOrEqual')] + #[TestDox('toSQL() succeeds for LessOrEqual')] public function testToSQLSucceedsForLessOrEqual(): void { $this->assertEquals('<=', Op::LessOrEqual->toSQL(), 'LessOrEqual SQL operator incorrect'); } - #[TestDox('To SQL succeeds for NotEqual')] + #[TestDox('toSQL() succeeds for NotEqual')] public function testToSQLSucceedsForNotEqual(): void { $this->assertEquals('<>', Op::NotEqual->toSQL(), 'NotEqual SQL operator incorrect'); } - #[TestDox('To SQL succeeds for Between')] + #[TestDox('toSQL() succeeds for Between')] public function testToSQLSucceedsForBetween(): void { $this->assertEquals('BETWEEN', Op::Between->toSQL(), 'Between SQL operator incorrect'); } - #[TestDox('To SQL succeeds for In')] + #[TestDox('toSQL() succeeds for In')] public function testToSQLSucceedsForIn(): void { $this->assertEquals('IN', Op::In->toSQL(), 'In SQL operator incorrect'); } - #[TestDox('To SQL succeeds for InArray')] + #[TestDox('toSQL() succeeds for InArray')] public function testToSQLSucceedsForInArray(): void { $this->assertEquals('?|', Op::InArray->toSQL(), 'InArray SQL operator incorrect'); } - #[TestDox('To SQL succeeds for Exists')] + #[TestDox('toSQL() succeeds for Exists')] public function testToSQLSucceedsForExists(): void { $this->assertEquals('IS NOT NULL', Op::Exists->toSQL(), 'Exists SQL operator incorrect'); } - #[TestDox('To SQL succeeds for NotExists')] + #[TestDox('toSQL() succeeds for NotExists')] public function testToSQLSucceedsForNEX(): void { $this->assertEquals('IS NULL', Op::NotExists->toSQL(), 'NotExists SQL operator incorrect'); diff --git a/tests/unit/ParametersTest.php b/tests/unit/ParametersTest.php index 16046eb..1761adc 100644 --- a/tests/unit/ParametersTest.php +++ b/tests/unit/ParametersTest.php @@ -20,18 +20,19 @@ use Test\{PjsonDocument, PjsonId}; #[TestDox('Parameters (Unit tests)')] class ParametersTest extends TestCase { - #[TestDox('ID succeeds with string')] + #[TestDox('id() succeeds with string')] public function testIdSucceedsWithString(): void { $this->assertEquals([':id' => 'key'], Parameters::id('key'), 'ID parameter not constructed correctly'); } - #[TestDox('ID succeeds with non string')] + #[TestDox('id() succeeds with non string')] public function testIdSucceedsWithNonString(): void { $this->assertEquals([':id' => '7'], Parameters::id(7), 'ID parameter not constructed correctly'); } + #[TestDox('json() succeeds for array')] public function testJsonSucceedsForArray(): void { $this->assertEquals([':it' => '{"id":18,"url":"https://www.unittest.com"}'], @@ -39,20 +40,21 @@ class ParametersTest extends TestCase 'JSON parameter not constructed correctly'); } + #[TestDox('json() succeeds for array with empty array parameter')] public function testJsonSucceedsForArrayWithEmptyArrayParameter(): void { $this->assertEquals([':it' => '{"id":18,"urls":[]}'], Parameters::json(':it', ['id' => 18, 'urls' => []]), 'JSON parameter not constructed correctly'); } - #[TestDox('json succeeds for 1D array with empty array parameter')] + #[TestDox('json() succeeds for 1D array with empty array parameter')] public function testJsonSucceedsFor1DArrayWithEmptyArrayParameter(): void { $this->assertEquals([':it' => '{"urls":[]}'], Parameters::json(':it', ['urls' => []]), 'JSON parameter not constructed correctly'); } - #[TestDox('json succeeds for stdClass')] + #[TestDox('json() succeeds for stdClass')] public function testJsonSucceedsForStdClass(): void { $obj = new stdClass(); @@ -62,6 +64,7 @@ class ParametersTest extends TestCase 'JSON parameter not constructed correctly'); } + #[TestDox('json() succeeds for Pjson class')] public function testJsonSucceedsForPjsonClass(): void { $this->assertEquals([':it' => '{"id":"999","name":"a test","num_value":98}'], @@ -69,6 +72,7 @@ class ParametersTest extends TestCase 'JSON parameter not constructed correctly'); } + #[TestDox('json() succeeds for array of Pjson class')] public function testJsonSucceedsForArrayOfPjsonClass(): void { $this->assertEquals([':it' => '{"pjson":[{"id":"997","name":"another test","num_value":94}]}'], @@ -77,6 +81,7 @@ class ParametersTest extends TestCase 'JSON parameter not constructed correctly'); } + #[TestDox('nameFields() succeeds')] public function testNameFieldsSucceeds(): void { $named = Parameters::nameFields( @@ -87,6 +92,7 @@ class ParametersTest extends TestCase $this->assertEquals(':field2', $named[2]->paramName, 'Parameter 3 not named correctly'); } + #[TestDox('addFields() succeeds')] public function testAddFieldsSucceeds(): void { $this->assertEquals([':a' => 1, ':b' => 'two', ':z' => 18], @@ -94,7 +100,7 @@ class ParametersTest extends TestCase 'Field parameters not added correctly'); } - #[TestDox('Field names succeeds for PostgreSQL')] + #[TestDox('fieldNames() succeeds for PostgreSQL')] public function testFieldNamesSucceedsForPostgreSQL(): void { try { @@ -106,7 +112,7 @@ class ParametersTest extends TestCase } } - #[TestDox('Field names succeeds for SQLite')] + #[TestDox('fieldNames() succeeds for SQLite')] public function testFieldNamesSucceedsForSQLite(): void { try { @@ -118,6 +124,7 @@ class ParametersTest extends TestCase } } + #[TestDox('fieldNames() fails when mode not set')] public function testFieldNamesFailsWhenModeNotSet(): void { $this->expectException(DocumentException::class); diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index f4389d4..f325e38 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -28,12 +28,14 @@ class QueryTest extends TestCase Configuration::overrideMode(null); } + #[TestDox('selectFromTable() succeeds')] public function testSelectFromTableSucceeds(): void { $this->assertEquals('SELECT data FROM testing', Query::selectFromTable('testing'), 'Query not constructed correctly'); } + #[TestDox('whereByFields() succeeds for single field')] public function testWhereByFieldsSucceedsForSingleField(): void { $this->assertEquals("data->>'test_field' <= :it", @@ -41,6 +43,7 @@ class QueryTest extends TestCase '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", @@ -49,6 +52,7 @@ class QueryTest extends TestCase '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", @@ -58,18 +62,19 @@ class QueryTest extends TestCase 'WHERE fragment not constructed correctly'); } - #[TestDox('Where by ID succeeds with default parameter')] + #[TestDox('whereById() succeeds with default parameter')] public function testWhereByIdSucceedsWithDefaultParameter(): void { $this->assertEquals("data->>'id' = :id", Query::whereById(), 'WHERE fragment not constructed correctly'); } - #[TestDox('Where by ID succeeds with specific parameter')] + #[TestDox('whereById() succeeds with specific parameter')] public function testWhereByIdSucceedsWithSpecificParameter(): void { $this->assertEquals("data->>'id' = :di", Query::whereById(':di'), 'WHERE fragment not constructed correctly'); } + #[TestDox('whereDataContains() succeeds with default parameter')] public function testWhereDataContainsSucceedsWithDefaultParameter(): void { Configuration::overrideMode(Mode::PgSQL); @@ -77,13 +82,14 @@ class QueryTest extends TestCase 'WHERE fragment not constructed correctly'); } + #[TestDox('whereDataContains() succeeds with specific parameter')] public function testWhereDataContainsSucceedsWithSpecifiedParameter(): void { Configuration::overrideMode(Mode::PgSQL); $this->assertEquals('data @> :it', Query::whereDataContains(':it'), 'WHERE fragment not constructed correctly'); } - #[TestDox('Where data contains fails if not PostgreSQL')] + #[TestDox('whereDataContains() fails if not PostgreSQL')] public function testWhereDataContainsFailsIfNotPostgreSQL(): void { Configuration::overrideMode(null); @@ -91,7 +97,7 @@ class QueryTest extends TestCase Query::whereDataContains(); } - #[TestDox('Where JSON Path matches succeeds with default parameter')] + #[TestDox('whereJsonPathMatches() succeeds with default parameter')] public function testWhereJsonPathMatchesSucceedsWithDefaultParameter(): void { Configuration::overrideMode(Mode::PgSQL); @@ -99,7 +105,7 @@ class QueryTest extends TestCase 'WHERE fragment not constructed correctly'); } - #[TestDox('Where JSON Path matches succeeds with specified parameter')] + #[TestDox('whereJsonPathMatches() succeeds with specified parameter')] public function testWhereJsonPathMatchesSucceedsWithSpecifiedParameter(): void { Configuration::overrideMode(Mode::PgSQL); @@ -107,7 +113,7 @@ class QueryTest extends TestCase 'WHERE fragment not constructed correctly'); } - #[TestDox('Where JSON Path matches fails if not PostgreSQL')] + #[TestDox('whereJsonPathMatches() fails if not PostgreSQL')] public function testWhereJsonPathMatchesFailsIfNotPostgreSQL(): void { Configuration::overrideMode(null); @@ -115,7 +121,7 @@ class QueryTest extends TestCase Query::whereJsonPathMatches(); } - #[TestDox('Insert succeeds with no auto-ID for PostgreSQL')] + #[TestDox('insert() succeeds with no auto-ID for PostgreSQL')] public function testInsertSucceedsWithNoAutoIdForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -123,14 +129,14 @@ class QueryTest extends TestCase 'INSERT statement not constructed correctly'); } - #[TestDox('Insert succeeds with no auto-ID for SQLite')] + #[TestDox('insert() succeeds with no auto-ID for SQLite')] public function testInsertSucceedsWithNoAutoIdForSQLite(): void { $this->assertEquals('INSERT INTO test_tbl VALUES (:data)', Query::insert('test_tbl'), 'INSERT statement not constructed correctly'); } - #[TestDox('Insert succeeds with auto numeric ID for PostgreSQL')] + #[TestDox('insert() succeeds with auto numeric ID for PostgreSQL')] public function testInsertSucceedsWithAutoNumericIdForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -140,7 +146,7 @@ class QueryTest extends TestCase Query::insert('test_tbl', AutoId::Number), 'INSERT statement not constructed correctly'); } - #[TestDox('Insert succeeds with auto numeric ID for SQLite')] + #[TestDox('insert() succeeds with auto numeric ID for SQLite')] public function testInsertSucceedsWithAutoNumericIdForSQLite(): void { $this->assertEquals( @@ -149,7 +155,7 @@ class QueryTest extends TestCase Query::insert('test_tbl', AutoId::Number), 'INSERT statement not constructed correctly'); } - #[TestDox('Insert succeeds with auto UUID for PostgreSQL')] + #[TestDox('insert() succeeds with auto UUID for PostgreSQL')] public function testInsertSucceedsWithAutoUuidForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -159,7 +165,7 @@ class QueryTest extends TestCase $this->assertStringEndsWith("\"}')", $query, 'INSERT statement not constructed correctly'); } - #[TestDox('Insert succeeds with auto UUID for SQLite')] + #[TestDox('insert() succeeds with auto UUID for SQLite')] public function testInsertSucceedsWithAutoUuidForSQLite(): void { $query = Query::insert('test_tbl', AutoId::UUID); @@ -168,7 +174,7 @@ class QueryTest extends TestCase $this->assertStringEndsWith("'))", $query, 'INSERT statement not constructed correctly'); } - #[TestDox('Insert succeeds with auto random string for PostgreSQL')] + #[TestDox('insert() succeeds with auto random string for PostgreSQL')] public function testInsertSucceedsWithAutoRandomStringForPostgreSQL(): void { Configuration::overrideMode(Mode::PgSQL); @@ -185,7 +191,7 @@ class QueryTest extends TestCase } } - #[TestDox('Insert succeeds with auto random string for SQLite')] + #[TestDox('insert() succeeds with auto random string for SQLite')] public function testInsertSucceedsWithAutoRandomStringForSQLite(): void { $query = Query::insert('test_tbl', AutoId::RandomString); @@ -196,6 +202,7 @@ class QueryTest extends TestCase $this->assertEquals(16, strlen($id), "Generated ID [$id] should have been 16 characters long"); } + #[TestDox('insert() fails when mode not set')] public function testInsertFailsWhenModeNotSet(): void { $this->expectException(DocumentException::class); @@ -203,6 +210,7 @@ class QueryTest extends TestCase Query::insert('kaboom'); } + #[TestDox('save() succeeds')] public function testSaveSucceeds(): void { $this->assertEquals( @@ -210,6 +218,7 @@ class QueryTest extends TestCase 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'),