diff --git a/CHANGELOG.md b/CHANGELOG.md index d8c301c7..cc7ba1b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +v2.7.0 (04.12.2023) +------------------- +- Add `varbinary` support in MySQL; optimize `size` attribute by @msmakouz (#146) +- Add the ability to use WHERE IN and WHERE NOT IN with array values + The value sequence may contain `FragmentInterface` objets by @msmakouz and @roxblnfk (#147) + v2.6.0 (02.11.2023) ------------------- - Fix incorrect parameters processing for JOIN subqueries by @smelesh (#133) diff --git a/src/Driver/Compiler.php b/src/Driver/Compiler.php index ffd0d0c9..e05961aa 100644 --- a/src/Driver/Compiler.php +++ b/src/Driver/Compiler.php @@ -475,15 +475,14 @@ protected function condition(QueryParameters $params, Quoter $q, array $context) $placeholder = '?'; if ($value->isArray()) { - if ($operator === '=') { - $operator = 'IN'; - } elseif ($operator === '!=') { - $operator = 'NOT IN'; - } + return $this->arrayToInOperator($params, $q, $value->getValue(), match (\strtoupper($operator)) { + 'IN', '=' => true, + 'NOT IN', '!=' => false, + default => throw CompilerException\UnexpectedOperatorException::sequence($operator), + }); + } - $placeholder = '(' . rtrim(str_repeat('? ,', count($value->getValue())), ', ') . ')'; - $params->push($value); - } elseif ($value->isNull()) { + if ($value->isNull()) { if ($operator === '=') { $operator = 'IS'; } elseif ($operator === '!=') { @@ -521,4 +520,24 @@ protected function optional(string $prefix, string $expression, string $postfix return $prefix . $expression . $postfix; } + + private function arrayToInOperator(QueryParameters $params, Quoter $q, array $values, bool $in): string + { + $operator = $in ? 'IN' : 'NOT IN'; + + $placeholders = $simpleParams = []; + foreach ($values as $value) { + if ($value instanceof FragmentInterface) { + $placeholders[] = $this->fragment($params, $q, $value); + } else { + $placeholders[] = '?'; + $simpleParams[] = $value; + } + } + if ($simpleParams !== []) { + $params->push(new Parameter($simpleParams)); + } + + return \sprintf('%s(%s)', $operator, \implode(',', $placeholders)); + } } diff --git a/src/Driver/CompilerCache.php b/src/Driver/CompilerCache.php index 343e15f8..4340c0d7 100644 --- a/src/Driver/CompilerCache.php +++ b/src/Driver/CompilerCache.php @@ -316,12 +316,27 @@ private function hashParam(QueryParameters $params, ParameterInterface $param): return 'N'; } - $params->push($param); - if ($param->isArray()) { + $simpleParams = []; + foreach ($param->getValue() as $value) { + if ($value instanceof FragmentInterface) { + foreach ($value->getTokens()['parameters'] as $fragmentParam) { + $params->push($fragmentParam); + } + } else { + $simpleParams[] = $value; + } + } + + if ($simpleParams !== []) { + $params->push(new Parameter($simpleParams)); + } + return 'A' . count($param->getValue()); } + $params->push($param); + return '?'; } } diff --git a/src/Exception/CompilerException/UnexpectedOperatorException.php b/src/Exception/CompilerException/UnexpectedOperatorException.php new file mode 100644 index 00000000..fae6306b --- /dev/null +++ b/src/Exception/CompilerException/UnexpectedOperatorException.php @@ -0,0 +1,33 @@ + $parameter instanceof Parameter || $parameter instanceof Fragment + ? $parameter + : new \Cycle\Database\Injection\Parameter($parameter); } } diff --git a/tests/Database/Functional/Driver/Common/Query/SelectQueryTest.php b/tests/Database/Functional/Driver/Common/Query/SelectQueryTest.php index a0aa84c8..90f69101 100644 --- a/tests/Database/Functional/Driver/Common/Query/SelectQueryTest.php +++ b/tests/Database/Functional/Driver/Common/Query/SelectQueryTest.php @@ -5,6 +5,7 @@ namespace Cycle\Database\Tests\Functional\Driver\Common\Query; use Cycle\Database\Exception\BuilderException; +use Cycle\Database\Exception\CompilerException\UnexpectedOperatorException; use Cycle\Database\Injection\Expression; use Cycle\Database\Injection\Fragment; use Cycle\Database\Injection\Parameter; @@ -334,12 +335,13 @@ public function testSelectWithWhereOrWhere(): void public function testSelectInvalidArrayArgument(): void { - $this->expectException(BuilderException::class); + $this->expectException(UnexpectedOperatorException::class); $this->database->select()->distinct() ->from(['users']) ->where('name', 'Anton') - ->orWhere('id', 'like', [1, 2, 3]); + ->orWhere('id', 'like', [1, 2, 3]) + ->sqlStatement(); } public function testSelectWithWhereOrWhereAndWhere(): void @@ -1917,16 +1919,16 @@ public function testInOperatorWithBadArrayParameter(): void public function testBadArrayParameterInShortWhere(): void { - $this->expectException(BuilderException::class); - $this->expectExceptionMessage('Arrays must be wrapped with Parameter instance'); + $this->expectException(UnexpectedOperatorException::class); - $this->database->select() - ->from(['users']) - ->where( - [ - 'status' => ['LIKE' => ['active', 'blocked']], - ] - ); + $this->database + ->select() + ->from(['users']) + ->where( + [ + 'status' => ['LIKE' => ['active', 'blocked']], + ] + )->sqlStatement(); } public function testGoodArrayParameter(): void @@ -2165,4 +2167,104 @@ public function testSelectWithFragmentedColumns(): void $select ); } + + public function testWhereInWithoutSpecifiedOperator(): void + { + $select = $this->database + ->select() + ->from(['users']) + ->where( + 'uuid', + new Parameter(['12345678-1234-1234-1234-123456789012', '12345678-1234-1234-1234-123456789013']) + ); + + $this->assertSameQuery('SELECT * FROM {users} WHERE {uuid} IN (?, ?)', $select); + + $this->assertSameParameters( + ['12345678-1234-1234-1234-123456789012', '12345678-1234-1234-1234-123456789013'], + $select, + ); + } + + public function testWhereInWithEqualSpecifiedOperator(): void + { + $select = $this->database + ->select() + ->from(['users']) + ->where( + 'uuid', + '=', + new Parameter(['12345678-1234-1234-1234-123456789012', '12345678-1234-1234-1234-123456789013']) + )->orWhere( + 'uuid', + '=', + ['23456789-1234-1234-1234-123456789012', '23456789-1234-1234-1234-123456789013'] + ); + + $this->assertSameQuery('SELECT * FROM {users} WHERE {uuid} IN (?, ?) OR {uuid} IN (?, ?)', $select); + + $this->assertSameParameters( + [ + '12345678-1234-1234-1234-123456789012', + '12345678-1234-1234-1234-123456789013', + '23456789-1234-1234-1234-123456789012', + '23456789-1234-1234-1234-123456789013', + ], + $select, + ); + } + + public function testWhereInWithNotEqualSpecifiedOperator(): void + { + $select = $this->database + ->select() + ->from(['users']) + ->where( + 'uuid', + '!=', + new Parameter(['12345678-1234-1234-1234-123456789012', '12345678-1234-1234-1234-123456789013']) + )->orWhere( + 'uuid', + '!=', + ['23456789-1234-1234-1234-123456789012', '23456789-1234-1234-1234-123456789013'] + ); + + $this->assertSameQuery('SELECT * FROM {users} WHERE {uuid} NOT IN (?, ?) OR {uuid} NOT IN (?, ?)', $select); + + $this->assertSameParameters( + [ + '12345678-1234-1234-1234-123456789012', + '12345678-1234-1234-1234-123456789013', + '23456789-1234-1234-1234-123456789012', + '23456789-1234-1234-1234-123456789013', + ], + $select, + ); + } + + public function testFragmentInWhereInClause(): void + { + $select = $this->database + ->select() + ->from(['users']) + ->where('uuid', new Fragment('UUID_TO_BIN(?)', '12345678-1234-1234-1234-123456789012')) + ->andWhere('uuid', 'IN', [ + new Fragment('UUID_TO_BIN(?)', '12345678-1234-1234-1234-123456789013'), + new Fragment('UUID_TO_BIN(?)', '12345678-1234-1234-1234-123456789014'), + ]); + + $this->assertSameQuery( + 'SELECT * FROM {users} WHERE {uuid} = UUID_TO_BIN (?) AND {uuid} IN (UUID_TO_BIN (?), UUID_TO_BIN (?))', + $select + ); + + $this->assertSameParameters( + [ + '12345678-1234-1234-1234-123456789012', + '12345678-1234-1234-1234-123456789013', + '12345678-1234-1234-1234-123456789014', + ], + $select + ); + } }