diff --git a/docs/book/sql/where-having.md b/docs/book/sql/where-having.md index 31573e4f..d77d15ec 100644 --- a/docs/book/sql/where-having.md +++ b/docs/book/sql/where-having.md @@ -394,6 +394,13 @@ class Expression implements } ``` +> **Note:** Unlike `Operator`, `Like`, `Between`, `In` and `IsNull`, +> `Expression` does not accept a specification override. It always renders +> the string given to its constructor or `setExpression()`, so calling +> `setSpecification()` on an `Expression` (or `Predicate\Expression`) throws +> `PhpDb\Sql\Exception\RuntimeException`. Call `setExpression()` to change +> the SQL it renders. + Expression parameters can be supplied in multiple ways: ```php title="Using Expression with various parameter types" diff --git a/src/Sql/Exception/RuntimeException.php b/src/Sql/Exception/RuntimeException.php index 51ecffc7..846c7347 100644 --- a/src/Sql/Exception/RuntimeException.php +++ b/src/Sql/Exception/RuntimeException.php @@ -34,6 +34,10 @@ class RuntimeException extends Exception\RuntimeException 'A number of parameters (%d) was found' . ' that is not supported by this specification'; + final public const string UNSUPPORTED_SPECIFICATION_OVERRIDE = + '%s renders its expression string and does not accept' + . ' a specification override; use setExpression() instead'; + public static function forReplacementMismatch(): self { return new self(self::REPLACEMENT_MISMATCH); @@ -68,4 +72,9 @@ public static function forUnsupportedParameterCountOf(int $count): self { return new self(sprintf(self::UNSUPPORTED_PARAMETER_COUNT_OF, $count)); } + + public static function forUnsupportedSpecificationOverride(string $class): self + { + return new self(sprintf(self::UNSUPPORTED_SPECIFICATION_OVERRIDE, $class)); + } } diff --git a/src/Sql/Expression.php b/src/Sql/Expression.php index 4450c39b..f44c916d 100644 --- a/src/Sql/Expression.php +++ b/src/Sql/Expression.php @@ -144,4 +144,16 @@ public function setParameters( return $this; } + + /** + * Expression renders the string given to setExpression(), so a + * specification override would be silently ignored. Use setExpression(). + * + * @throws Exception\RuntimeException + */ + #[Override] + public function setSpecification(string $specification): static + { + throw Exception\RuntimeException::forUnsupportedSpecificationOverride(static::class); + } } diff --git a/test/unit/Sql/Exception/RuntimeExceptionTest.php b/test/unit/Sql/Exception/RuntimeExceptionTest.php index 57f965b2..88729bab 100644 --- a/test/unit/Sql/Exception/RuntimeExceptionTest.php +++ b/test/unit/Sql/Exception/RuntimeExceptionTest.php @@ -6,6 +6,7 @@ use PhpDb\Exception\ExceptionInterface; use PhpDb\Sql\Exception\RuntimeException; +use PhpDb\Sql\Expression; use PhpDb\Sql\SqlInterface; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\DataProvider; @@ -23,6 +24,7 @@ #[CoversMethod(RuntimeException::class, 'forSubjectNotSqlInterface')] #[CoversMethod(RuntimeException::class, 'forUnsupportedParameterCount')] #[CoversMethod(RuntimeException::class, 'forUnsupportedParameterCountOf')] +#[CoversMethod(RuntimeException::class, 'forUnsupportedSpecificationOverride')] final class RuntimeExceptionTest extends TestCase { /** @return array, string}> */ @@ -64,6 +66,11 @@ public static function namedConstructorProvider(): array [2], RuntimeException::UNSUPPORTED_PARAMETER_COUNT_OF, ], + 'unsupported specification override' => [ + 'forUnsupportedSpecificationOverride', + [Expression::class], + RuntimeException::UNSUPPORTED_SPECIFICATION_OVERRIDE, + ], ]; } diff --git a/test/unit/Sql/ExpressionTest.php b/test/unit/Sql/ExpressionTest.php index 2237448d..d454cdc6 100644 --- a/test/unit/Sql/ExpressionTest.php +++ b/test/unit/Sql/ExpressionTest.php @@ -18,6 +18,8 @@ use PHPUnit\Framework\TestCase; use TypeError; +use function sprintf; + /** * This is a unit testing test case. * A unit here is a method, there will be at least one test per method @@ -30,6 +32,8 @@ #[CoversMethod(Expression::class, 'setParameters')] #[CoversMethod(Expression::class, 'getParameters')] #[CoversMethod(Expression::class, 'getExpressionData')] +#[CoversMethod(Expression::class, 'setSpecification')] +#[Group('unit')] final class ExpressionTest extends TestCase { /** @psalm-return array */ @@ -262,4 +266,17 @@ public function setParametersWrapsArrayInValuesArgument(): void static::assertCount(2, $data['values']); static::assertInstanceOf(Argument\Values::class, $data['values'][1]); } + + #[Test] + public function setSpecificationIsRejectedInFavourOfSetExpression(): void + { + $expression = new Expression('LOWER(?) = ?', ['x', 'y']); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage( + sprintf(RuntimeException::UNSUPPORTED_SPECIFICATION_OVERRIDE, Expression::class), + ); + + $expression->setSpecification('IGNORED %s %s'); + } } diff --git a/test/unit/Sql/Predicate/CustomSpecificationTest.php b/test/unit/Sql/Predicate/CustomSpecificationTest.php new file mode 100644 index 00000000..bef7fa0f --- /dev/null +++ b/test/unit/Sql/Predicate/CustomSpecificationTest.php @@ -0,0 +1,54 @@ + */ + public static function predicateProvider(): array + { + return [ + 'operator' => [new Operator('a', Operator::OPERATOR_EQUAL_TO, 1), '%1$s <=> %2$s'], + 'like' => [new Like('a', 'b%'), '%1$s SOUNDS LIKE %2$s'], + 'between' => [new Between('a', 1, 2), '%1$s BETWEEN SYMMETRIC %2$s AND %3$s'], + 'is null' => [new IsNull('a'), '%1$s IS UNKNOWN'], + 'in' => [new In('a', [1, 2]), '%s = ANY (%s)'], + ]; + } + + #[Test] + #[DataProvider('predicateProvider')] + public function rendersTheCustomSpecification( + AbstractExpression $predicate, + string $specification, + ): void { + $predicate->setSpecification($specification); + + static::assertSame($specification, $predicate->getExpressionData()['spec']); + } +} diff --git a/test/unit/Sql/Predicate/ExpressionTest.php b/test/unit/Sql/Predicate/ExpressionTest.php index f6bc50ab..590bffc2 100644 --- a/test/unit/Sql/Predicate/ExpressionTest.php +++ b/test/unit/Sql/Predicate/ExpressionTest.php @@ -9,12 +9,18 @@ use PhpDb\Sql\Argument\Value; use PhpDb\Sql\ArgumentInterface; use PhpDb\Sql\ArgumentType; +use PhpDb\Sql\Exception\RuntimeException; use PhpDb\Sql\Predicate\Expression; use PhpDb\Sql\Predicate\IsNull; +use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +use function sprintf; + +#[CoversMethod(Expression::class, 'setSpecification')] +#[Group('unit')] final class ExpressionTest extends TestCase { #[Test] @@ -216,4 +222,17 @@ public function retrievingWherePartsReturnsSpecificationArrayOfLiteralAndParamet static::assertSame('foo.bar = %s AND id != %s', $expressionData['spec']); static::assertEquals([$parameter1, $parameter2], $expressionData['values']); } + + #[Test] + public function setSpecificationIsRejectedInFavourOfSetExpression(): void + { + $expression = new Expression('LOWER(?) = ?', ['x', 'y']); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage( + sprintf(RuntimeException::UNSUPPORTED_SPECIFICATION_OVERRIDE, Expression::class), + ); + + $expression->setSpecification('IGNORED %s %s'); + } }