diff --git a/analyzer-baseline.toml b/analyzer-baseline.toml index 01d9232a..0073261d 100644 --- a/analyzer-baseline.toml +++ b/analyzer-baseline.toml @@ -2778,24 +2778,6 @@ code = "impossible-condition" message = "This condition (type `false`) will always evaluate to false." count = 1 -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "imprecise-type" -message = "Type `array` in parameter `$identifier` is imprecise, equivalent to `array`." -count = 2 - -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "imprecise-type" -message = "Type `array` in parameter `$maxValue` is imprecise, equivalent to `array`." -count = 2 - -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "imprecise-type" -message = "Type `array` in parameter `$minValue` is imprecise, equivalent to `array`." -count = 2 - [[issues]] file = "src/Sql/Predicate/Predicate.php" code = "imprecise-type" @@ -2826,54 +2808,12 @@ code = "less-specific-nested-argument-type" message = 'Argument type mismatch for argument #2 of `PhpDb\Sql\Expression::__construct`: expected `PhpDb\Sql\ArgumentInterface|PhpDb\Sql\ExpressionInterface|array|null|scalar>|list|null|scalar`, but provided type `PhpDb\Sql\ArgumentInterface|PhpDb\Sql\ExpressionInterface|float|int|non-empty-array|null|string` is less specific.' count = 1 -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #1 of `PhpDb\Sql\Predicate\Between::__construct`: expected `PhpDb\Sql\ArgumentInterface|null|string`, but possibly received `PhpDb\Sql\ArgumentInterface|array|float|int|null|string`.' -count = 2 - -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #1 of `PhpDb\Sql\Predicate\In::__construct`: expected `PhpDb\Sql\ArgumentInterface|null|string`, but possibly received `PhpDb\Sql\ArgumentInterface|float|int|string`.' -count = 2 - -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #1 of `PhpDb\Sql\Predicate\IsNull::__construct`: expected `PhpDb\Sql\ArgumentInterface|null|string`, but possibly received `PhpDb\Sql\ArgumentInterface|float|int|string`.' -count = 2 - -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #1 of `PhpDb\Sql\Predicate\Like::__construct`: expected `PhpDb\Sql\ArgumentInterface|null|string`, but possibly received `PhpDb\Sql\ArgumentInterface|float|int|null|string`.' -count = 2 - -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #1 of `PhpDb\Sql\Predicate\Operator::__construct`: expected `PhpDb\Sql\ArgumentInterface|PhpDb\Sql\ExpressionInterface|PhpDb\Sql\SqlInterface|null|string`, but possibly received `PhpDb\Sql\ArgumentInterface|float|int|null|string`.' -count = 6 - -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #2 of `PhpDb\Sql\Predicate\Between::__construct`: expected `PhpDb\Sql\ArgumentInterface|float|int|null|string`, but possibly received `PhpDb\Sql\ArgumentInterface|array|float|int|null|string`.' -count = 2 - [[issues]] file = "src/Sql/Predicate/Predicate.php" code = "possibly-invalid-argument" message = 'Possible argument type mismatch for argument #2 of `PhpDb\Sql\Predicate\In::__construct`: expected `PhpDb\Sql\ArgumentInterface|PhpDb\Sql\Select|list|null`, but possibly received `PhpDb\Sql\ArgumentInterface|array`.' count = 2 -[[issues]] -file = "src/Sql/Predicate/Predicate.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #3 of `PhpDb\Sql\Predicate\Between::__construct`: expected `PhpDb\Sql\ArgumentInterface|float|int|null|string`, but possibly received `PhpDb\Sql\ArgumentInterface|array|float|int|null|string`.' -count = 2 - [[issues]] file = "src/Sql/Predicate/PredicateInterface.php" code = "missing-api-or-internal" diff --git a/docs/book/sql/where-having.md b/docs/book/sql/where-having.md index ecb24bf9..02ab5303 100644 --- a/docs/book/sql/where-having.md +++ b/docs/book/sql/where-having.md @@ -132,35 +132,35 @@ class Predicate extends PredicateSet public function setUnnest(?Predicate $predicate = null) : void; public function unnest() : Predicate; public function equalTo( - null|float|int|string|ArgumentInterface $left, + null|string|ArgumentInterface $left, null|float|int|string|ArgumentInterface $right ) : static; public function notEqualTo( - null|float|int|string|ArgumentInterface $left, + null|string|ArgumentInterface $left, null|float|int|string|ArgumentInterface $right ) : static; public function lessThan( - null|float|int|string|ArgumentInterface $left, + null|string|ArgumentInterface $left, null|float|int|string|ArgumentInterface $right ) : static; public function greaterThan( - null|float|int|string|ArgumentInterface $left, + null|string|ArgumentInterface $left, null|float|int|string|ArgumentInterface $right ) : static; public function lessThanOrEqualTo( - null|float|int|string|ArgumentInterface $left, + null|string|ArgumentInterface $left, null|float|int|string|ArgumentInterface $right ) : static; public function greaterThanOrEqualTo( - null|float|int|string|ArgumentInterface $left, + null|string|ArgumentInterface $left, null|float|int|string|ArgumentInterface $right ) : static; public function like( - null|float|int|string|ArgumentInterface $identifier, + null|string|ArgumentInterface $identifier, null|float|int|string|ArgumentInterface $like ) : static; public function notLike( - null|float|int|string|ArgumentInterface $identifier, + null|string|ArgumentInterface $identifier, null|float|int|string|ArgumentInterface $notLike ) : static; public function literal(string $literal) : static; @@ -170,28 +170,28 @@ class Predicate extends PredicateSet |ExpressionInterface $parameters = [] ) : static; public function isNull( - float|int|string|ArgumentInterface $identifier + string|ArgumentInterface $identifier ) : static; public function isNotNull( - float|int|string|ArgumentInterface $identifier + string|ArgumentInterface $identifier ) : static; public function in( - float|int|string|ArgumentInterface $identifier, + string|ArgumentInterface $identifier, array|ArgumentInterface $valueSet ) : static; public function notIn( - float|int|string|ArgumentInterface $identifier, + string|ArgumentInterface $identifier, array|ArgumentInterface $valueSet ) : static; public function between( - null|float|int|string|array|ArgumentInterface $identifier, - null|float|int|string|array|ArgumentInterface $minValue, - null|float|int|string|array|ArgumentInterface $maxValue + null|string|ArgumentInterface $identifier, + null|float|int|string|ArgumentInterface $minValue, + null|float|int|string|ArgumentInterface $maxValue ) : static; public function notBetween( - null|float|int|string|array|ArgumentInterface $identifier, - null|float|int|string|array|ArgumentInterface $minValue, - null|float|int|string|array|ArgumentInterface $maxValue + null|string|ArgumentInterface $identifier, + null|float|int|string|ArgumentInterface $minValue, + null|float|int|string|ArgumentInterface $maxValue ) : static; public function predicate(PredicateInterface $predicate) : static; @@ -222,6 +222,15 @@ class Predicate extends PredicateSet > `ArgumentInterface` implementations. Pass an `Argument\Identifier` for > column names, `Argument\Value` for values, or `Argument\Literal` for raw > SQL fragments directly to control how values are treated. +> +> The left-hand side and identifier parameters accept a string (treated as an +> identifier) or an `ArgumentInterface`. To put a value on the left, wrap it in +> `Argument\Value`: +> +> ```php +> $where->equalTo(new Argument\Value(5), new Argument\Identifier('quantity')); +> // WHERE '5' = "quantity" +> ``` Each method in the API will produce a corresponding `Predicate` object of a similarly named type, as described below. diff --git a/src/Sql/Predicate/Predicate.php b/src/Sql/Predicate/Predicate.php index 2750da93..f2317ba1 100644 --- a/src/Sql/Predicate/Predicate.php +++ b/src/Sql/Predicate/Predicate.php @@ -31,9 +31,9 @@ class Predicate extends PredicateSet * @return $this Provides a fluent interface */ public function between( - float|int|string|array|ArgumentInterface|null $identifier, - float|int|string|array|ArgumentInterface|null $minValue, - float|int|string|array|ArgumentInterface|null $maxValue, + string|ArgumentInterface|null $identifier, + float|int|string|ArgumentInterface|null $minValue, + float|int|string|ArgumentInterface|null $maxValue, ): static { $this->addPredicate( new Between($identifier, $minValue, $maxValue), @@ -48,7 +48,7 @@ public function between( * Utilizes Operator predicate */ public function equalTo( - float|int|string|ArgumentInterface|null $left, + string|ArgumentInterface|null $left, float|int|string|ArgumentInterface|null $right, ): static { $this->addPredicate( @@ -83,7 +83,7 @@ public function expression( * @return $this Provides a fluent interface */ public function greaterThan( - float|int|string|ArgumentInterface|null $left, + string|ArgumentInterface|null $left, float|int|string|ArgumentInterface|null $right, ): static { $this->addPredicate( @@ -101,7 +101,7 @@ public function greaterThan( * @return $this Provides a fluent interface */ public function greaterThanOrEqualTo( - float|int|string|ArgumentInterface|null $left, + string|ArgumentInterface|null $left, float|int|string|ArgumentInterface|null $right, ): static { $this->addPredicate( @@ -118,7 +118,7 @@ public function greaterThanOrEqualTo( * * @return $this Provides a fluent interface */ - public function in(float|int|string|ArgumentInterface $identifier, array|ArgumentInterface $valueSet): static + public function in(string|ArgumentInterface $identifier, array|ArgumentInterface $valueSet): static { $this->addPredicate( new In($identifier, $valueSet), @@ -134,7 +134,7 @@ public function in(float|int|string|ArgumentInterface $identifier, array|Argumen * * @return $this Provides a fluent interface */ - public function isNotNull(float|int|string|ArgumentInterface $identifier): static + public function isNotNull(string|ArgumentInterface $identifier): static { $this->addPredicate( new IsNotNull($identifier), @@ -150,7 +150,7 @@ public function isNotNull(float|int|string|ArgumentInterface $identifier): stati * * @return $this Provides a fluent interface */ - public function isNull(float|int|string|ArgumentInterface $identifier): static + public function isNull(string|ArgumentInterface $identifier): static { $this->addPredicate( new IsNull($identifier), @@ -165,7 +165,7 @@ public function isNull(float|int|string|ArgumentInterface $identifier): static * Utilizes Operator predicate */ public function lessThan( - float|int|string|ArgumentInterface|null $left, + string|ArgumentInterface|null $left, float|int|string|ArgumentInterface|null $right, ): static { $this->addPredicate( @@ -183,7 +183,7 @@ public function lessThan( * @return $this Provides a fluent interface */ public function lessThanOrEqualTo( - float|int|string|ArgumentInterface|null $left, + string|ArgumentInterface|null $left, float|int|string|ArgumentInterface|null $right, ): static { $this->addPredicate( @@ -201,7 +201,7 @@ public function lessThanOrEqualTo( * @return $this Provides a fluent interface */ public function like( - float|int|string|ArgumentInterface|null $identifier, + string|ArgumentInterface|null $identifier, float|int|string|ArgumentInterface|null $like, ): static { $this->addPredicate( @@ -248,9 +248,9 @@ public function nest(): self * @return $this Provides a fluent interface */ public function notBetween( - float|int|string|array|ArgumentInterface|null $identifier, - float|int|string|array|ArgumentInterface|null $minValue, - float|int|string|array|ArgumentInterface|null $maxValue, + string|ArgumentInterface|null $identifier, + float|int|string|ArgumentInterface|null $minValue, + float|int|string|ArgumentInterface|null $maxValue, ): static { $this->addPredicate( new NotBetween($identifier, $minValue, $maxValue), @@ -265,7 +265,7 @@ public function notBetween( * Utilizes Operator predicate */ public function notEqualTo( - float|int|string|ArgumentInterface|null $left, + string|ArgumentInterface|null $left, float|int|string|ArgumentInterface|null $right, ): static { $this->addPredicate( @@ -282,7 +282,7 @@ public function notEqualTo( * * @return $this Provides a fluent interface */ - public function notIn(float|int|string|ArgumentInterface $identifier, array|ArgumentInterface $valueSet): static + public function notIn(string|ArgumentInterface $identifier, array|ArgumentInterface $valueSet): static { $this->addPredicate( new NotIn($identifier, $valueSet), @@ -299,7 +299,7 @@ public function notIn(float|int|string|ArgumentInterface $identifier, array|Argu * @return $this Provides a fluent interface */ public function notLike( - float|int|string|ArgumentInterface|null $identifier, + string|ArgumentInterface|null $identifier, float|int|string|ArgumentInterface|null $notLike, ): static { $this->addPredicate( diff --git a/test/unit/Sql/Predicate/PredicateTest.php b/test/unit/Sql/Predicate/PredicateTest.php index 98c261fb..2d668c55 100644 --- a/test/unit/Sql/Predicate/PredicateTest.php +++ b/test/unit/Sql/Predicate/PredicateTest.php @@ -9,18 +9,72 @@ use PhpDb\Adapter\Platform\Sql92; use PhpDb\Sql\Argument; use PhpDb\Sql\Expression; +use PhpDb\Sql\Predicate\Between; use PhpDb\Sql\Predicate\Exception\RuntimeException; +use PhpDb\Sql\Predicate\In; +use PhpDb\Sql\Predicate\IsNull; +use PhpDb\Sql\Predicate\Like; +use PhpDb\Sql\Predicate\Operator; use PhpDb\Sql\Predicate\Predicate; use PhpDb\Sql\Predicate\PredicateInterface; use PhpDb\Sql\Select; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; +use ReflectionMethod; +use ReflectionNamedType; +use ReflectionParameter; +use ReflectionUnionType; + +use function array_diff; +use function array_map; #[Group('unit')] final class PredicateTest extends TestCase { + /** @return array */ + public static function fluentParameterProvider(): array + { + return [ + 'between identifier' => ['between', 0, Between::class, 0], + 'between minimum' => ['between', 1, Between::class, 1], + 'between maximum' => ['between', 2, Between::class, 2], + 'notBetween identifier' => ['notBetween', 0, Between::class, 0], + 'equalTo left' => ['equalTo', 0, Operator::class, 0], + 'equalTo right' => ['equalTo', 1, Operator::class, 2], + 'notEqualTo left' => ['notEqualTo', 0, Operator::class, 0], + 'greaterThan left' => ['greaterThan', 0, Operator::class, 0], + 'greaterThanOrEqualTo left' => ['greaterThanOrEqualTo', 0, Operator::class, 0], + 'lessThan left' => ['lessThan', 0, Operator::class, 0], + 'lessThanOrEqualTo left' => ['lessThanOrEqualTo', 0, Operator::class, 0], + 'in identifier' => ['in', 0, In::class, 0], + 'in value set' => ['in', 1, In::class, 1], + 'notIn identifier' => ['notIn', 0, In::class, 0], + 'isNull identifier' => ['isNull', 0, IsNull::class, 0], + 'isNotNull identifier' => ['isNotNull', 0, IsNull::class, 0], + 'like identifier' => ['like', 0, Like::class, 0], + 'like pattern' => ['like', 1, Like::class, 1], + 'notLike identifier' => ['notLike', 0, Like::class, 0], + ]; + } + + /** + * @throws ErrorException + */ + /** @return list */ + private static function typeNames(ReflectionParameter $parameter): array + { + $type = $parameter->getType(); + $types = $type instanceof ReflectionUnionType ? $type->getTypes() : [$type]; + + return array_map( + static fn(?ReflectionNamedType $named): string => $named?->getName() ?? 'mixed', + $types, + ); + } + #[Test] public function betweenCreatesBetweenPredicate(): void { @@ -162,6 +216,23 @@ public function expressionNullParameters(): void } } + /** + * @param class-string $predicateClass + */ + #[Test] + #[DataProvider('fluentParameterProvider')] + public function fluentMethodAcceptsOnlyTypesItsPredicateAccepts( + string $method, + int $position, + string $predicateClass, + int $constructorPosition, + ): void { + $fluent = (new ReflectionMethod(Predicate::class, $method))->getParameters()[$position]; + $constructor = (new ReflectionMethod($predicateClass, '__construct'))->getParameters()[$constructorPosition]; + + static::assertSame([], array_diff(self::typeNames($fluent), self::typeNames($constructor))); + } + #[Test] public function greaterThanCreatesOperatorPredicate(): void {