From 03b4377a1a0d10d9e4b64c4a834a5719789ff94a Mon Sep 17 00:00:00 2001 From: Martin Georgiev Date: Thu, 13 Mar 2025 11:41:40 +0000 Subject: [PATCH] refactor: validate that variadic functions have only the expected count of arguments --- .../AST/Functions/BaseVariadicFunction.php | 9 ++++ ...idArgumentForVariadicFunctionException.php | 46 +++++++++++++++++++ .../ORM/Query/AST/Functions/Greatest.php | 10 ++++ .../Query/AST/Functions/JsonBuildObject.php | 14 +++++- .../Query/AST/Functions/JsonbBuildObject.php | 14 +++++- .../ORM/Query/AST/Functions/Least.php | 10 ++++ .../Doctrine/ORM/Query/AST/Functions/Row.php | 10 ++++ .../ORM/Query/AST/Functions/ToTsquery.php | 10 ++++ .../ORM/Query/AST/Functions/ToTsvector.php | 10 ++++ .../ORM/Query/AST/Functions/Unaccent.php | 10 ++++ .../ORM/Query/AST/Functions/GreatestTest.php | 12 +++++ .../AST/Functions/JsonBuildObjectTest.php | 12 +++++ .../AST/Functions/JsonbBuildObjectTest.php | 12 +++++ .../ORM/Query/AST/Functions/LeastTest.php | 12 +++++ .../ORM/Query/AST/Functions/TestCase.php | 2 +- .../ORM/Query/AST/Functions/ToTsqueryTest.php | 12 +++++ .../Query/AST/Functions/ToTsvectorTest.php | 12 +++++ .../ORM/Query/AST/Functions/UnaccentTest.php | 12 +++++ 18 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Exception/InvalidArgumentForVariadicFunctionException.php diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BaseVariadicFunction.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BaseVariadicFunction.php index 7777ebcd..6f1ba2e1 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BaseVariadicFunction.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BaseVariadicFunction.php @@ -38,6 +38,8 @@ public function feedParserWithNodes(Parser $parser): void } $aheadType = $lexer->lookahead->type; } + + $this->validateArguments($this->nodes); } public function getSql(SqlWalker $sqlWalker): string @@ -49,4 +51,11 @@ public function getSql(SqlWalker $sqlWalker): string return \sprintf($this->functionPrototype, \implode(', ', $dispatched)); } + + /** + * Validates the arguments passed to the function. + * + * @param mixed[] $arguments The array of arguments to validate + */ + abstract protected function validateArguments(array $arguments): void; } diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Exception/InvalidArgumentForVariadicFunctionException.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Exception/InvalidArgumentForVariadicFunctionException.php new file mode 100644 index 00000000..74b787b4 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Exception/InvalidArgumentForVariadicFunctionException.php @@ -0,0 +1,46 @@ +setFunctionPrototype('greatest(%s)'); } + + protected function validateArguments(array $arguments): void + { + $argumentCount = \count($arguments); + if ($argumentCount < 2) { + throw InvalidArgumentForVariadicFunctionException::atLeast('greatest', 2); + } + } } diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonBuildObject.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonBuildObject.php index 6d15c6b5..47fbca84 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonBuildObject.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonBuildObject.php @@ -4,11 +4,15 @@ namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException; + /** * Implementation of PostgreSQL JSON_BUILD_OBJECT(). * * @see https://www.postgresql.org/docs/17/functions-json.html - * @since 2.9.0 + * @since 2.9 + * + * @author Martin Georgiev */ class JsonBuildObject extends BaseVariadicFunction { @@ -16,4 +20,12 @@ protected function customizeFunction(): void { $this->setFunctionPrototype('json_build_object(%s)'); } + + protected function validateArguments(array $arguments): void + { + $argumentCount = \count($arguments); + if ($argumentCount === 0 || $argumentCount % 2 !== 0) { + throw InvalidArgumentForVariadicFunctionException::evenNumber('json_build_object'); + } + } } diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbBuildObject.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbBuildObject.php index 4f6e65b7..f55fb828 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbBuildObject.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbBuildObject.php @@ -4,11 +4,15 @@ namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException; + /** * Implementation of PostgreSQL JSONB_BUILD_OBJECT(). * * @see https://www.postgresql.org/docs/17/functions-json.html - * @since 2.9.0 + * @since 2.9 + * + * @author Martin Georgiev */ class JsonbBuildObject extends BaseVariadicFunction { @@ -16,4 +20,12 @@ protected function customizeFunction(): void { $this->setFunctionPrototype('jsonb_build_object(%s)'); } + + protected function validateArguments(array $arguments): void + { + $argumentCount = \count($arguments); + if ($argumentCount === 0 || $argumentCount % 2 !== 0) { + throw InvalidArgumentForVariadicFunctionException::evenNumber('jsonb_build_object'); + } + } } diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Least.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Least.php index 70a2a078..a56efb96 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Least.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Least.php @@ -4,6 +4,8 @@ namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException; + /** * Implementation of PostgreSQL LEAST(). * @@ -18,4 +20,12 @@ protected function customizeFunction(): void { $this->setFunctionPrototype('least(%s)'); } + + protected function validateArguments(array $arguments): void + { + $argumentCount = \count($arguments); + if ($argumentCount < 2) { + throw InvalidArgumentForVariadicFunctionException::atLeast('least', 2); + } + } } diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Row.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Row.php index 488619bf..4ffbeca9 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Row.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Row.php @@ -4,6 +4,8 @@ namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException; + /** * Implementation of PostgreSQL Row Constructor expression. * @@ -17,4 +19,12 @@ protected function customizeFunction(): void { $this->setFunctionPrototype('ROW(%s)'); } + + protected function validateArguments(array $arguments): void + { + $argumentCount = \count($arguments); + if ($argumentCount === 0) { + throw InvalidArgumentForVariadicFunctionException::atLeast('ROW', 1); + } + } } diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToTsquery.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToTsquery.php index 191d77b2..25f50ea9 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToTsquery.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToTsquery.php @@ -4,6 +4,8 @@ namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException; + /** * Implementation of PostgreSQL TO_TSQUERY(). * @@ -18,4 +20,12 @@ protected function customizeFunction(): void { $this->setFunctionPrototype('to_tsquery(%s)'); } + + protected function validateArguments(array $arguments): void + { + $argumentCount = \count($arguments); + if ($argumentCount < 1 || $argumentCount > 2) { + throw InvalidArgumentForVariadicFunctionException::between('to_tsquery', 1, 2); + } + } } diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToTsvector.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToTsvector.php index 7314f59c..1f090a25 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToTsvector.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToTsvector.php @@ -4,6 +4,8 @@ namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException; + /** * Implementation of PostgreSQL TO_TSVECTOR(). * @@ -20,4 +22,12 @@ protected function customizeFunction(): void { $this->setFunctionPrototype('to_tsvector(%s)'); } + + protected function validateArguments(array $arguments): void + { + $argumentCount = \count($arguments); + if ($argumentCount < 1 || $argumentCount > 2) { + throw InvalidArgumentForVariadicFunctionException::between('to_tsvector', 1, 2); + } + } } diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Unaccent.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Unaccent.php index 46e3275c..0d8b6d42 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Unaccent.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Unaccent.php @@ -4,6 +4,8 @@ namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException; + /** * Implementation of PostgreSQL UNACCENT. * @@ -19,4 +21,12 @@ protected function customizeFunction(): void { $this->setFunctionPrototype('unaccent(%s)'); } + + protected function validateArguments(array $arguments): void + { + $argumentCount = \count($arguments); + if ($argumentCount < 1 || $argumentCount > 2) { + throw InvalidArgumentForVariadicFunctionException::between('unaccent', 1, 2); + } + } } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/GreatestTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/GreatestTest.php index 568635de..78d66d01 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/GreatestTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/GreatestTest.php @@ -6,6 +6,7 @@ use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsIntegers; use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseComparisonFunction; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException; use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Greatest; class GreatestTest extends BaseComparisonFunctionTestCase @@ -35,4 +36,15 @@ protected function getDqlStatements(): array \sprintf('SELECT GREATEST(e.integer1, e.integer2, e.integer3) FROM %s e', ContainsIntegers::class), ]; } + + /** + * @test + */ + public function throws_exception_when_single_argument_given(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + + $dql = \sprintf('SELECT GREATEST(e.integer1) FROM %s e', ContainsIntegers::class); + $this->assertSqlFromDql('', $dql); + } } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonBuildObjectTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonBuildObjectTest.php index 82178854..7e6e6c0e 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonBuildObjectTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonBuildObjectTest.php @@ -5,6 +5,7 @@ namespace Tests\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsJsons; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException; use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonBuildObject; class JsonBuildObjectTest extends TestCase @@ -33,4 +34,15 @@ protected function getDqlStatements(): array \sprintf("SELECT JSON_BUILD_OBJECT('key1', e.object1, 'key2', e.object2) FROM %s e", ContainsJsons::class), ]; } + + /** + * @test + */ + public function throws_exception_when_odd_number_of_arguments_given(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + + $dql = \sprintf('SELECT JSON_BUILD_OBJECT(\'key1\', e.value1, \'key2\') FROM %s e', ContainsJsons::class); + $this->assertSqlFromDql('', $dql); + } } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbBuildObjectTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbBuildObjectTest.php index 84686f91..f484f26e 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbBuildObjectTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbBuildObjectTest.php @@ -5,6 +5,7 @@ namespace Tests\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsJsons; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException; use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbBuildObject; class JsonbBuildObjectTest extends TestCase @@ -33,4 +34,15 @@ protected function getDqlStatements(): array \sprintf("SELECT JSONB_BUILD_OBJECT('key1', e.object1, 'key2', e.object2) FROM %s e", ContainsJsons::class), ]; } + + /** + * @test + */ + public function throws_exception_when_odd_number_of_arguments_given(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + + $dql = \sprintf('SELECT JSONB_BUILD_OBJECT(\'key1\', e.object1, \'key2\') FROM %s e', ContainsJsons::class); + $this->assertSqlFromDql('', $dql); + } } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/LeastTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/LeastTest.php index f8b00f14..dab36589 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/LeastTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/LeastTest.php @@ -6,6 +6,7 @@ use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsIntegers; use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseComparisonFunction; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException; use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Least; class LeastTest extends BaseComparisonFunctionTestCase @@ -35,4 +36,15 @@ protected function getDqlStatements(): array \sprintf('SELECT LEAST(e.integer1, e.integer2, e.integer3) FROM %s e', ContainsIntegers::class), ]; } + + /** + * @test + */ + public function throws_exception_when_single_argument_given(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + + $dql = \sprintf('SELECT LEAST(e.integer1) FROM %s e', ContainsIntegers::class); + $this->assertSqlFromDql('', $dql); + } } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TestCase.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TestCase.php index 4ad2c39f..0b2c64f9 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TestCase.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TestCase.php @@ -100,7 +100,7 @@ public function dql_is_transformed_to_valid_sql(): void } } - private function assertSqlFromDql(string $expectedSql, string $dql, string $message = ''): void + protected function assertSqlFromDql(string $expectedSql, string $dql, string $message = ''): void { $query = $this->buildEntityManager()->createQuery($dql); $this->assertEquals($expectedSql, $query->getSQL(), $message); diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToTsqueryTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToTsqueryTest.php index 73f8b702..d0461c65 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToTsqueryTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToTsqueryTest.php @@ -5,6 +5,7 @@ namespace Tests\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsTexts; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException; use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ToTsquery; class ToTsqueryTest extends TestCase @@ -33,4 +34,15 @@ protected function getDqlStatements(): array \sprintf('SELECT TO_TSQUERY(\'english\', e.text1) FROM %s e', ContainsTexts::class), ]; } + + /** + * @test + */ + public function throws_exception_when_too_many_arguments_given(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + + $dql = \sprintf('SELECT TO_TSQUERY(\'english\', e.text1, \'extra\') FROM %s e', ContainsTexts::class); + $this->assertSqlFromDql('', $dql); + } } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToTsvectorTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToTsvectorTest.php index edfe3249..a5aed7ba 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToTsvectorTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ToTsvectorTest.php @@ -5,6 +5,7 @@ namespace Tests\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsTexts; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException; use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ToTsvector; class ToTsvectorTest extends TestCase @@ -33,4 +34,15 @@ protected function getDqlStatements(): array \sprintf('SELECT TO_TSVECTOR(\'english\', e.text1) FROM %s e', ContainsTexts::class), ]; } + + /** + * @test + */ + public function throws_exception_when_too_many_arguments_given(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + + $dql = \sprintf('SELECT TO_TSVECTOR(\'english\', e.text1, \'extra\') FROM %s e', ContainsTexts::class); + $this->assertSqlFromDql('', $dql); + } } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UnaccentTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UnaccentTest.php index a80c5777..50a63515 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UnaccentTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/UnaccentTest.php @@ -5,6 +5,7 @@ namespace Tests\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsTexts; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException; use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Unaccent; class UnaccentTest extends TestCase @@ -31,4 +32,15 @@ protected function getDqlStatements(): array \sprintf('SELECT UNACCENT(\'unaccent\', e.text1) FROM %s e', ContainsTexts::class), ]; } + + /** + * @test + */ + public function throws_exception_when_too_many_arguments_given(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + + $dql = \sprintf('SELECT UNACCENT(\'dict\', e.text1, \'extra\') FROM %s e', ContainsTexts::class); + $this->assertSqlFromDql('', $dql); + } }