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
8 changes: 8 additions & 0 deletions docs/book/sql/where-having.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,14 @@ class Expression implements
}
```

> **Note:** Unlike `Operator`, `Like`/`NotLike`, `Between`/`NotBetween`,
> `In`/`NotIn` and `IsNull`/`IsNotNull`, `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 @@ -153,6 +153,18 @@ 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);
}

/**
* Values are bound one by one, so each needs its own placeholder inside the parentheses around the `?`
*
Expand Down
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
15 changes: 15 additions & 0 deletions test/unit/Sql/ExpressionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
#[CoversMethod(Expression::class, 'getParameters')]
#[CoversMethod(Expression::class, 'getExpressionData')]
#[CoversMethod(Expression::class, 'placeholderFor')]
#[CoversMethod(Expression::class, 'setSpecification')]
#[Group('unit')]
final class ExpressionTest extends TestCase
{
/** @return array<string, array{string, list<mixed>, string}> */
Expand Down Expand Up @@ -326,4 +328,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');
}
}
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');
}
}
102 changes: 102 additions & 0 deletions test/unit/Sql/Predicate/SpecificationOverrideTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?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\IsNotNull;
use PhpDb\Sql\Predicate\IsNull;
use PhpDb\Sql\Predicate\Like;
use PhpDb\Sql\Predicate\NotBetween;
use PhpDb\Sql\Predicate\NotIn;
use PhpDb\Sql\Predicate\NotLike;
use PhpDb\Sql\Predicate\Operator;
use PhpDb\Sql\Select;
use PhpDbTest\TestAsset\TrustingSql92Platform;
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, including their negated
* variants. In and NotIn expand their value set's placeholder to one per list item.
*/
#[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 SpecificationOverrideTest extends TestCase
{
/** @return array<string, array{AbstractExpression, string, string}> */
public static function predicateProvider(): array
{
return [
'operator' => [
new Operator('a', Operator::OPERATOR_EQUAL_TO, 1),
'%1$s <=> %2$s',
'SELECT "t".* FROM "t" WHERE "a" <=> \'1\'',
],
'like' => [
new Like('a', 'b%'),
'%1$s SOUNDS LIKE %2$s',
'SELECT "t".* FROM "t" WHERE "a" SOUNDS LIKE \'b%\'',
],
'not like' => [
new NotLike('a', 'b%'),
'NOT (%1$s SOUNDS LIKE %2$s)',
'SELECT "t".* FROM "t" WHERE NOT ("a" SOUNDS LIKE \'b%\')',
],
'between' => [
new Between('a', 1, 2),
'%1$s BETWEEN SYMMETRIC %2$s AND %3$s',
'SELECT "t".* FROM "t" WHERE "a" BETWEEN SYMMETRIC \'1\' AND \'2\'',
],
'not between' => [
new NotBetween('a', 1, 2),
'%1$s NOT BETWEEN SYMMETRIC %2$s AND %3$s',
'SELECT "t".* FROM "t" WHERE "a" NOT BETWEEN SYMMETRIC \'1\' AND \'2\'',
],
'is null' => [new IsNull('a'), '%1$s IS UNKNOWN', 'SELECT "t".* FROM "t" WHERE "a" IS UNKNOWN'],
'is not null' => [
new IsNotNull('a'),
'%1$s IS NOT UNKNOWN',
'SELECT "t".* FROM "t" WHERE "a" IS NOT UNKNOWN',
],
'in' => [
new In('a', [1, 2]),
'%s = ANY (%s)',
'SELECT "t".* FROM "t" WHERE "a" = ANY (\'1\', \'2\')',
],
'not in' => [
new NotIn('a', [1, 2]),
'%s <> ALL (%s)',
'SELECT "t".* FROM "t" WHERE "a" <> ALL (\'1\', \'2\')',
],
];
}

#[Test]
#[DataProvider('predicateProvider')]
public function rendersTheCustomSpecification(
AbstractExpression $predicate,
string $specification,
string $expectedSql,
): void {
$predicate->setSpecification($specification);

static::assertSame(
$expectedSql,
(new Select('t'))->where($predicate)
->getSqlString(new TrustingSql92Platform()),
);
}
}
Loading