From bfb7a332bb54e01f1f63381d7613eb23c74dd96f Mon Sep 17 00:00:00 2001 From: Jan Skrasek Date: Mon, 26 Jul 2021 20:11:17 +0200 Subject: [PATCH 1/2] sqlprocessor: implement spread modifier [closes #141] --- docs/param-modifiers.md | 39 +++++++++++++------- src/SqlProcessor.php | 21 +++++++++-- tests/cases/unit/SqlProcessorTest.array.phpt | 24 ++++++++++++ 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/docs/param-modifiers.md b/docs/param-modifiers.md index 7b581037..88a2d8ab 100644 --- a/docs/param-modifiers.md +++ b/docs/param-modifiers.md @@ -2,20 +2,21 @@ Dbal allows you to escape and build safe SQL query. It provides these powerful parameter modifiers: -| Modifier | Type | Description -|-------------------------------|----------|------------ -| `%s`, `%?s`, `%s[]` | string | not nullable, nullable, array of -| `%i`, `%?i`, `%i[]` | integer | not nullable, nullable, array of -| `%f`, `%?f`, `%f[]` | float | not nullable, nullable, array of -| `%b`, `%?b`, `%b[]` | boolean | not nullable, nullable, array of -| `%dt`, `%?dt`, `%dt[]` | datetime | not nullable, nullable, array of
read more about [datetime handling](datetime); using wrong modifier may damage your data -| `%ldt`, `%?ldt`, `%ldt[]` | local datetime | datetime without timezone conversion
read more about [datetime handling](datetime); using wrong modifier may damage your data -| `%blob`, `%?blob`, `%blob[]` | binary string | not nullable, nullable, array of -| `%any` | | any value -| `%_like`, `%like_`, `%_like_` | string | like left, like right, like both sides -| `%json`, `%?json`, `%json[]` | any | not nullable, nullable, array of - -All modifiers require an argument of the specific data type - eg. `%f` accepts only floats and integers. +| Modifier | Type | Description +|--------------------------------------------|----------------|------------ +| `%s`, `%?s`, `%s[]`, `%...s[]` | string | not nullable, nullable, array of +| `%i`, `%?i`, `%i[]`, `%...i[]` | integer | not nullable, nullable, array of +| `%f`, `%?f`, `%f[]`, `%...f[]` | float | not nullable, nullable, array of +| `%b`, `%?b`, `%b[]`, `%...b[]` | boolean | not nullable, nullable, array of +| `%dt`, `%?dt`, `%dt[]`, `%...dt[]` | datetime | not nullable, nullable, array of
read more about [datetime handling](datetime); using wrong modifier may damage your data +| `%ldt`, `%?ldt`, `%ldt[]`, `%...ldt[]` | local datetime | datetime without timezone conversion
read more about [datetime handling](datetime); using wrong modifier may damage your data +| `%di`, `%?di`, `%di[]`, `%...di[]` | date interval | DateInterval instance +| `%blob`, `%?blob`, `%blob[]` | binary string | not nullable, nullable, array of +| `%json`, `%?json`, `%json[]`, `%...json[]` | any | not nullable, nullable, array of +| `%any ` | | any value +| `%_like`, `%like_`, `%_like_` | string | like left, like right, like both sides + +All modifiers require an argument of the specific data type - e.g. `%f` accepts only floats and integers. ```php $connection->query('id = %i AND name IN (%?s, %?s)', 1, NULL, 'foo'); @@ -26,6 +27,16 @@ $connection->query('name LIKE %_like_', $query); // name LIKE '%escaped query expression%' ``` +Array modifiers are able to process array of the required type. The basic `[]` suffix syntax denotes such array. This way Dbal also adds wrapping parenthesis. You may want to omit them for more complex SQL. To do so, use a "spread" variant of array operator -- add three dots after the `%` character. + +```php +$connection->query('WHERE id IN %i[]', [1, 3, 4]); +// WHERE `id` IN (1, 3, 4) + +$connection->query('WHERE [roles.privileges] ?| ARRAY[%...s[]]', ['backend', 'frontend']); +// WHERE "roles"."privileges" ?| ARRAY['backend', 'frontend'] +``` + Other available modifiers: | Modifier | Description diff --git a/src/SqlProcessor.php b/src/SqlProcessor.php index 32f706d9..bb27d853 100644 --- a/src/SqlProcessor.php +++ b/src/SqlProcessor.php @@ -127,7 +127,7 @@ public function process(array $args): string $i = $j; $fragments[] = preg_replace_callback( - '#%(\??+\w++(?:\[\]){0,2}+)|(%%)|(\[\[)|(\]\])|\[(.+?)\]#S', // %modifier | %% | %[ | %] | [identifier] + '#%((?:\.\.\.)?+\??+\w++(?:\[]){0,2}+)|(%%)|(\[\[)|(]])|\[(.+?)]#S', // %modifier | %% | %[ | %] | [identifier] function ($matches) use ($args, &$j, $last): string { if ($matches[1] !== '') { if ($j === $last) { @@ -344,7 +344,9 @@ public function processModifier(string $type, $value): string // normal case 'column[]': + case '...column[]': case 'table[]': + case '...table[]': $subType = substr($type, 0, -2); foreach ($value as &$subValue) { $subValue = $this->processModifier($subType, $subValue); @@ -372,14 +374,14 @@ public function processModifier(string $type, $value): string } if (substr($type, -1) === ']') { - $baseType = trim($type, '[]?'); + $baseType = trim(trim($type, '.'), '[]?'); if (isset($this->modifiers[$baseType]) && $this->modifiers[$baseType][1]) { return $this->processArray($type, $value); } } } - $baseType = trim($type, '[]?'); + $baseType = trim(trim($type, '.'), '[]?'); if (isset($this->customModifiers[$baseType])) { return $this->customModifiers[$baseType]($this, $value, $type); @@ -450,11 +452,22 @@ protected function throwWrongModifierException(string $type, $value, string $hin protected function processArray(string $type, array $value): string { $subType = substr($type, 0, -2); + $wrapped = true; + + if (strncmp($subType, '...', 3) === 0) { + $subType = substr($subType, 3); + $wrapped = false; + } + foreach ($value as &$subValue) { $subValue = $this->processModifier($subType, $subValue); } - return '(' . implode(', ', $value) . ')'; + if ($wrapped) { + return '(' . implode(', ', $value) . ')'; + } else { + return implode(', ', $value); + } } diff --git a/tests/cases/unit/SqlProcessorTest.array.phpt b/tests/cases/unit/SqlProcessorTest.array.phpt index 00b7413b..f8d6d113 100644 --- a/tests/cases/unit/SqlProcessorTest.array.phpt +++ b/tests/cases/unit/SqlProcessorTest.array.phpt @@ -50,6 +50,30 @@ class SqlProcessorArrayTest extends TestCase } + public function testArraySpread() + { + Assert::same( + 'SELECT FROM test WHERE id IN (1, 2, 3)', + $this->convert('SELECT FROM test WHERE id IN (%...i[])', [1, 2, 3]) + ); + + Assert::same( + 'SELECT FROM test WHERE id IN (1, 2, 3)', + $this->convert('SELECT FROM test WHERE id IN (%...i[])', ['foo' => 1, 12 => 2, 0 => 3]) + ); + + Assert::same( + 'SELECT FROM test WHERE id IN ()', + $this->convert('SELECT FROM test WHERE id IN (%...i[])', []) + ); + + Assert::same( + 'SELECT FROM test WHERE id IN (NULL, 2, 3)', + $this->convert('SELECT FROM test WHERE id IN (%...?i[])', [null, 2, 3]) + ); + } + + public function testWhereTuplets() { Assert::same( From d869622de48d339d3dcea0eedd1f4d8cff9cacb9 Mon Sep 17 00:00:00 2001 From: Jan Skrasek Date: Wed, 13 Oct 2021 20:41:13 +0200 Subject: [PATCH 2/2] phpstan: fix issues with type in latest nette/di --- src/Bridges/NetteDI/DbalExtension.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Bridges/NetteDI/DbalExtension.php b/src/Bridges/NetteDI/DbalExtension.php index 8b44293a..73ccca3c 100644 --- a/src/Bridges/NetteDI/DbalExtension.php +++ b/src/Bridges/NetteDI/DbalExtension.php @@ -4,6 +4,7 @@ use Nette\DI\CompilerExtension; +use Nette\DI\Definitions\ServiceDefinition; use Nextras\Dbal\Bridges\NetteTracy\BluescreenQueryPanel; use Nextras\Dbal\Bridges\NetteTracy\ConnectionPanel; use Nextras\Dbal\Connection; @@ -27,7 +28,9 @@ protected function setupConnection(array $config): void { $builder = $this->getContainerBuilder(); - $definition = $builder->addDefinition($this->prefix('connection')) + /** @var ServiceDefinition */ + $definition = $builder->addDefinition($this->prefix('connection')); // @phpstan-ignore-line + $definition = $definition ->setType(Connection::class) ->setArguments([ 'config' => $config,