Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/book/sql/where-having.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 9 additions & 0 deletions src/Sql/Exception/RuntimeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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));
}
}
12 changes: 12 additions & 0 deletions src/Sql/Expression.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
7 changes: 7 additions & 0 deletions test/unit/Sql/Exception/RuntimeExceptionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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, array{string, list<string|int>, string}> */
Expand Down Expand Up @@ -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,
],
];
}

Expand Down
17 changes: 17 additions & 0 deletions test/unit/Sql/ExpressionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<array-key, array{0: mixed}> */
Expand Down Expand Up @@ -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');
}
}
54 changes: 54 additions & 0 deletions test/unit/Sql/Predicate/CustomSpecificationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace PhpDbTest\Sql\Predicate;

use PhpDb\Sql\AbstractExpression;
use PhpDb\Sql\Predicate\Between;
use PhpDb\Sql\Predicate\In;
use PhpDb\Sql\Predicate\IsNull;
use PhpDb\Sql\Predicate\Like;
use PhpDb\Sql\Predicate\Operator;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

/**
* Predicates built on AbstractExpression render a specification set through
* setSpecification() in place of their default format.
*/
#[CoversMethod(AbstractExpression::class, 'setSpecification')]
#[CoversMethod(Between::class, 'getExpressionData')]
#[CoversMethod(In::class, 'getExpressionData')]
#[CoversMethod(IsNull::class, 'getExpressionData')]
#[CoversMethod(Like::class, 'getExpressionData')]
#[CoversMethod(Operator::class, 'getExpressionData')]
#[Group('unit')]
final class CustomSpecificationTest extends TestCase
{
/** @return array<string, array{AbstractExpression, string}> */
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']);
}
}
19 changes: 19 additions & 0 deletions test/unit/Sql/Predicate/ExpressionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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');
}
}
Loading