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
6 changes: 6 additions & 0 deletions docs/book/sql/select.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ $select->limit(5);
$select->offset(10);
```

Both take a non-negative integer, as an `int` or a string of digits such as
`'10'`. A digit string is kept as given, so a count larger than `PHP_INT_MAX`
still works. Anything else, including negative numbers, fractions, exponents
and surrounding whitespace, throws
`PhpDb\Sql\Exception\InvalidArgumentException`.

## group()

The `group()` method specifies columns for GROUP BY clauses,
Expand Down
51 changes: 36 additions & 15 deletions src/Sql/AbstractSql.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use PhpDb\Sql\Argument\Value;
use PhpDb\Sql\Argument\Values;
use PhpDb\Sql\Platform\PlatformDecoratorInterface;
use Stringable;

use function array_key_exists;
use function array_key_first;
Expand All @@ -27,6 +28,7 @@
use function preg_match;
use function preg_replace;
use function rtrim;
use function sprintf;
use function str_replace;
use function strtoupper;
use function vsprintf;
Expand All @@ -36,7 +38,11 @@
*
* @psalm-type ParameterSpecification = array<int|string, string>|null
* @psalm-type Specification = string|array<string, list<ParameterSpecification>>
* @psalm-type SqlParameters = array<array-key, string|array<array-key, string|array<array-key, string>>>
* @psalm-type SqlParameterValue = string|int|float|bool|Stringable|null
* @psalm-type SqlParameters = array<
* array-key,
* SqlParameterValue|array<array-key, SqlParameterValue|array<array-key, SqlParameterValue>>,
* >
* @psalm-type ColumnValue = Select|ExpressionInterface|string|int|float|bool|null
* @psalm-type ColumnReference = array{
* column: ColumnValue,
Expand Down Expand Up @@ -76,7 +82,7 @@
{
$adapterPlatform ??= new DefaultAdapterPlatform();

$this->resetRenderState();

Check warning on line 85 in src/Sql/AbstractSql.php

View workflow job for this annotation

GitHub Actions / qa / Mutation testing

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ { $adapterPlatform ??= new DefaultAdapterPlatform(); - $this->resetRenderState(); + return $this->buildSqlString($adapterPlatform); }

return $this->buildSqlString($adapterPlatform);
}
Expand Down Expand Up @@ -111,7 +117,7 @@
}
}

return rtrim(implode(' ', $sqls), characters: "\n ,");

Check warning on line 120 in src/Sql/AbstractSql.php

View workflow job for this annotation

GitHub Actions / qa / Mutation testing

Escaped Mutant for Mutator "UnwrapRtrim": @@ @@ } } - return rtrim(implode(' ', $sqls), characters: "\n ,"); + return implode(' ', $sqls); } /**
}

/**
Expand All @@ -119,7 +125,7 @@
* @param SqlParameters $parameters
* @throws Exception\RuntimeException
*/
protected function createSqlFromSpecificationAndParameters(array|string $specifications, array $parameters): string

Check warning on line 128 in src/Sql/AbstractSql.php

View workflow job for this annotation

GitHub Actions / qa / Mutation testing

Escaped Mutant for Mutator "ProtectedVisibility": @@ @@ * @PARAM SqlParameters $parameters * @throws Exception\RuntimeException */ - protected function createSqlFromSpecificationAndParameters(array|string $specifications, array $parameters): string + private function createSqlFromSpecificationAndParameters(array|string $specifications, array $parameters): string { if (is_string($specifications)) { return vsprintf($specifications, $this->flattenParameterList($parameters));
{
if (is_string($specifications)) {
return vsprintf($specifications, $this->flattenParameterList($parameters));
Expand All @@ -141,11 +147,11 @@
* @param list<ArgumentInterface> $arguments
* @return list<ArgumentInterface>
*/
protected function flattenExpressionValues(array $arguments): array

Check warning on line 150 in src/Sql/AbstractSql.php

View workflow job for this annotation

GitHub Actions / qa / Mutation testing

Escaped Mutant for Mutator "ProtectedVisibility": @@ @@ * @PARAM list<ArgumentInterface> $arguments * @return list<ArgumentInterface> */ - protected function flattenExpressionValues(array $arguments): array + private function flattenExpressionValues(array $arguments): array { $hasValues = false; foreach ($arguments as $argument) {
{
$hasValues = false;

Check warning on line 152 in src/Sql/AbstractSql.php

View workflow job for this annotation

GitHub Actions / qa / Mutation testing

Escaped Mutant for Mutator "FalseValue": @@ @@ */ protected function flattenExpressionValues(array $arguments): array { - $hasValues = false; + $hasValues = true; foreach ($arguments as $argument) { if (! $argument instanceof Values) { continue;
foreach ($arguments as $argument) {
if (! $argument instanceof Values) {

Check warning on line 154 in src/Sql/AbstractSql.php

View workflow job for this annotation

GitHub Actions / qa / Mutation testing

Escaped Mutant for Mutator "LogicalNot": @@ @@ { $hasValues = false; foreach ($arguments as $argument) { - if (! $argument instanceof Values) { + if ($argument instanceof Values) { continue; }

Check warning on line 154 in src/Sql/AbstractSql.php

View workflow job for this annotation

GitHub Actions / qa / Mutation testing

Escaped Mutant for Mutator "InstanceOf_": @@ @@ { $hasValues = false; foreach ($arguments as $argument) { - if (! $argument instanceof Values) { + if ($argument instanceof Values) { continue; }
continue;
}

Expand Down Expand Up @@ -572,27 +578,27 @@
}

/**
* @param array<array-key, string|array<array-key, mixed>> $values
* @param array<array-key, SqlParameterValue|array<array-key, mixed>> $values
* @return list<string>
* @throws Exception\RuntimeException When a value is a nested list rather than a string.
* @throws Exception\RuntimeException When a value is a nested list rather than a scalar.
*/
private function flattenParameterList(array $values): array
{
$strings = [];
foreach ($values as $value) {
if (! is_string($value)) {
if (is_array($value)) {
throw Exception\RuntimeException::forUnsupportedParameterCountOf(count($value));
}

$strings[] = $value;
$strings[] = $this->stringifyParameter($value);
}

return $strings;
}

/**
* @param array<int|string, string> $parameterSpecification
* @param array<array-key, string|array<array-key, mixed>> $values
* @param array<array-key, SqlParameterValue|array<array-key, mixed>> $values
* @throws Exception\RuntimeException When no format accepts the number of values.
*/
private function formatParameterList(array $parameterSpecification, array $values): string
Expand Down Expand Up @@ -633,27 +639,42 @@

/**
* @param ParameterSpecification $parameterSpecification
* @param string|array<array-key, string|array<array-key, string>> $parameter
* @param SqlParameterValue|array<array-key, SqlParameterValue|array<array-key, SqlParameterValue>> $parameter
* @throws Exception\RuntimeException
*/
private function renderParameter(?array $parameterSpecification, string|array $parameter): string
{
private function renderParameter(
?array $parameterSpecification,
string|int|float|bool|Stringable|array|null $parameter,
): string {
if (null === $parameterSpecification) {
return is_string($parameter)
? $parameter
: throw Exception\RuntimeException::forUnsupportedParameterCountOf(count($parameter));
return is_array($parameter)
? throw Exception\RuntimeException::forUnsupportedParameterCountOf(count($parameter))
: $this->stringifyParameter($parameter);
}

$parameters = is_array($parameter) ? $parameter : [$parameter];

$combinedBy = $parameterSpecification['combinedby'] ?? null;
if (null === $combinedBy) {
return $this->formatParameterList($parameterSpecification, (array) $parameter);
return $this->formatParameterList($parameterSpecification, $parameters);
}

$rendered = [];
foreach ((array) $parameter as $values) {
$rendered[] = $this->formatParameterList($parameterSpecification, (array) $values);
foreach ($parameters as $values) {
$rendered[] = $this->formatParameterList(
$parameterSpecification,
is_array($values) ? $values : [$values],
);
}

return implode($combinedBy, $rendered);
}

/**
* Converts a scalar clause parameter to the text vsprintf's %s conversion would produce.
*/
private function stringifyParameter(string|int|float|bool|Stringable|null $value): string
{
return is_string($value) ? $value : sprintf('%s', $value);
}
}
6 changes: 3 additions & 3 deletions src/Sql/Exception/InvalidArgumentException.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class InvalidArgumentException extends Exception\InvalidArgumentException

final public const string MISSING_VALUES_OR_SELECT = 'values or select should be present';

final public const string NON_NUMERIC_PARAMETER = '%s expects parameter to be numeric, "%s" given';
final public const string NON_NEGATIVE_INTEGER_PARAMETER = '%s expects a non-negative integer, "%s" given';

final public const string NON_STRING_VALUE_KEY = 'set() expects a string for the value key';

Expand Down Expand Up @@ -172,9 +172,9 @@ public static function forMissingValuesOrSelect(): self
return new self(self::MISSING_VALUES_OR_SELECT);
}

public static function forNonNumericParameter(string $method, string $givenType): self
public static function forNonNegativeIntegerParameter(string $method, int|string $given): self
{
return new self(sprintf(self::NON_NUMERIC_PARAMETER, $method, $givenType));
return new self(sprintf(self::NON_NEGATIVE_INTEGER_PARAMETER, $method, $given));
}

public static function forNonStringValueKey(): self
Expand Down
34 changes: 26 additions & 8 deletions src/Sql/Select.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@
use function array_key_first;
use function count;
use function explode;
use function gettype;
use function is_array;
use function is_int;
use function is_numeric;
use function is_scalar;
use function is_string;
use function key;
use function method_exists;
use function preg_match;
use function preg_split;
use function strcasecmp;
use function stripos;
Expand All @@ -36,6 +35,7 @@
* @property ?Having $having
* @property ?Join $joins
* @psalm-import-type Specification from AbstractSql
* @psalm-import-type SqlParameterValue from AbstractSql
* @psalm-import-type PredicateValue from Predicate\PredicateSet
* @psalm-import-type JoinName from Join
* @psalm-type TableReference = string|TableIdentifier|array<string, string|TableIdentifier|Select>
Expand Down Expand Up @@ -187,6 +187,16 @@ public function __construct(array|string|TableIdentifier|null $table = null)
}
}

/**
* Whether a row count is a non-negative integer, given as an int or as a string of digits
*
* A digit string is kept as given, so a count beyond PHP_INT_MAX is still accepted.
*/
private static function isNonNegativeInteger(int|string $value): bool
{
return is_int($value) ? $value >= 0 : 1 === preg_match('/^\d+\z/', $value);
}

/**
* Specify columns from which to select
* Possible valid states:
Expand Down Expand Up @@ -335,8 +345,8 @@ public function join(
*/
public function limit(int|string $limit): static
{
if (! is_numeric($limit)) {
throw Exception\InvalidArgumentException::forNonNumericParameter(__METHOD__, gettype($limit));
if (! self::isNonNegativeInteger($limit)) {
throw Exception\InvalidArgumentException::forNonNegativeIntegerParameter(__METHOD__, $limit);
}

$this->limit = $limit;
Expand All @@ -348,8 +358,8 @@ public function limit(int|string $limit): static
*/
public function offset(int|string $offset): static
{
if (! is_numeric($offset)) {
throw Exception\InvalidArgumentException::forNonNumericParameter(__METHOD__, gettype($offset));
if (! self::isNonNegativeInteger($offset)) {
throw Exception\InvalidArgumentException::forNonNegativeIntegerParameter(__METHOD__, $offset);
}

$this->offset = $offset;
Expand Down Expand Up @@ -537,7 +547,11 @@ protected function processJoins(
return $this->processJoin($this->joins, $platform, $driver, $parameterContainer);
}

/** @return array{0: string}|null */
/**
* Platform overrides may return the raw limit or offset unquoted; any scalar is rendered as vsprintf's %s would.
*
* @return array{0: SqlParameterValue}|null
*/
protected function processLimit(
PlatformInterface $platform,
?DriverInterface $driver = null,
Expand All @@ -556,7 +570,11 @@ protected function processLimit(
return [$platform->quoteValue((string) $this->limit)];
}

/** @return array{0: string}|null */
/**
* Platform overrides may return the raw limit or offset unquoted; any scalar is rendered as vsprintf's %s would.
*
* @return array{0: SqlParameterValue}|null
*/
protected function processOffset(
PlatformInterface $platform,
?DriverInterface $driver = null,
Expand Down
Loading
Loading