From a9c50095980e6f24ca23c811fa55752e0f50dd6c Mon Sep 17 00:00:00 2001 From: Marc-Etienne Barrut Date: Fri, 21 Jan 2022 01:57:12 +0100 Subject: [PATCH 1/2] Add better bitwise operators support --- src/Illuminate/Database/Query/Builder.php | 23 +++++++++ .../Database/Query/Grammars/Grammar.php | 50 ++++++++++++++++++- .../Query/Grammars/PostgresGrammar.php | 9 ++++ tests/Database/DatabaseQueryBuilderTest.php | 19 +++++++ 4 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index 2d2d96ff76bc..a43abbbdf1d1 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -203,6 +203,15 @@ class Builder 'not similar to', 'not ilike', '~~*', '!~~*', ]; + /** + * All of the available binary operators. + * + * @var string[] + */ + public $binaryOperators = [ + '&', '|', '^', '<<', '>>', '&~', + ]; + /** * Whether to use write pdo for the select. * @@ -754,6 +763,10 @@ public function where($column, $operator = null, $value = null, $boolean = 'and' } } + if ($this->isBinaryOperator($operator)) { + $type = 'Binary'; + } + // Now that we are working with just a simple query we can put the elements // in our array and add the query binding to our array of bindings that // will be bound to each SQL statements when it is finally executed. @@ -837,6 +850,12 @@ protected function invalidOperator($operator) ! in_array(strtolower($operator), $this->grammar->getOperators(), true); } + protected function isBinaryOperator($operator) + { + return in_array(strtolower($operator), $this->binaryOperators, true) || + in_array(strtolower($operator), $this->grammar->getBinaryOperators(), true); + } + /** * Add an "or where" clause to the query. * @@ -1915,6 +1934,10 @@ public function having($column, $operator = null, $value = null, $boolean = 'and [$value, $operator] = [$operator, '=']; } + if ($this->isBinaryOperator($operator)) { + $type = 'binary'; + } + $this->havings[] = compact('type', 'column', 'operator', 'value', 'boolean'); if (! $value instanceof Expression) { diff --git a/src/Illuminate/Database/Query/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index ea670a2e9646..056883075ab4 100755 --- a/src/Illuminate/Database/Query/Grammars/Grammar.php +++ b/src/Illuminate/Database/Query/Grammars/Grammar.php @@ -18,6 +18,13 @@ class Grammar extends BaseGrammar */ protected $operators = []; + /** + * The grammar specific binary operators. + * + * @var array + */ + protected $binaryOperators = []; + /** * The components that make up a select clause. * @@ -255,6 +262,15 @@ protected function whereBasic(Builder $query, $where) return $this->wrap($where['column']).' '.$operator.' '.$value; } + protected function whereBinary(Builder $query, $where) + { + $value = $this->parameter($where['value']); + + $operator = str_replace('?', '??', $where['operator']); + + return '('.$this->wrap($where['column']).' '.$operator.' '.$value.') != 0'; + } + /** * Compile a "where in" clause. * @@ -571,7 +587,8 @@ protected function whereJsonContains(Builder $query, $where) $not = $where['not'] ? 'not ' : ''; return $not.$this->compileJsonContains( - $where['column'], $this->parameter($where['value']) + $where['column'], + $this->parameter($where['value']) ); } @@ -610,7 +627,9 @@ public function prepareBindingForJsonContains($binding) protected function whereJsonLength(Builder $query, $where) { return $this->compileJsonLength( - $where['column'], $where['operator'], $this->parameter($where['value']) + $where['column'], + $where['operator'], + $this->parameter($where['value']) ); } @@ -682,6 +701,8 @@ protected function compileHaving(array $having) return $having['boolean'].' '.$having['sql']; } elseif ($having['type'] === 'between') { return $this->compileHavingBetween($having); + } elseif ($having['type'] === 'binary') { + return $this->compileHavingBinary($having); } return $this->compileBasicHaving($having); @@ -721,6 +742,21 @@ protected function compileHavingBetween($having) return $having['boolean'].' '.$column.' '.$between.' '.$min.' and '.$max; } + /** + * Compile a having clause involving a binary operator. + * + * @param array $having + * @return string + */ + protected function compileHavingBinary($having) + { + $column = $this->wrap($having['column']); + + $parameter = $this->parameter($having['value']); + + return $having['boolean'].' ('.$column.' '.$having['operator'].' '.$parameter.') != 0'; + } + /** * Compile the "order by" portions of the query. * @@ -1296,4 +1332,14 @@ public function getOperators() { return $this->operators; } + + /** + * Get the grammar specific binary operators. + * + * @return array + */ + public function getBinaryOperators() + { + return $this->binaryOperators; + } } diff --git a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php index 47f498e54708..c40a41f35e08 100755 --- a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php @@ -21,6 +21,15 @@ class PostgresGrammar extends Grammar 'is distinct from', 'is not distinct from', ]; + /** + * The grammar specific binary operators. + * + * @var array + */ + protected $binaryOperators = [ + '~', '&', '|', '#', '<<', '>>', '<<=', '>>=', + ]; + /** * {@inheritdoc} * diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php index d27a38a10276..45e7abff44c4 100755 --- a/tests/Database/DatabaseQueryBuilderTest.php +++ b/tests/Database/DatabaseQueryBuilderTest.php @@ -3209,6 +3209,25 @@ public function testMySqlSoundsLikeOperator() $this->assertEquals(['John Doe'], $builder->getBindings()); } + public function testBinaryOperators() + { + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->where('bar', '&', 1); + $this->assertSame('select * from "users" where ("bar" & ?) != 0', $builder->toSql()); + + $builder = $this->getPostgresBuilder(); + $builder->select('*')->from('users')->where('bar', '#', 1); + $this->assertSame('select * from "users" where ("bar" # ?) != 0', $builder->toSql()); + + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->having('bar', '&', 1); + $this->assertSame('select * from "users" having ("bar" & ?) != 0', $builder->toSql()); + + $builder = $this->getPostgresBuilder(); + $builder->select('*')->from('users')->having('bar', '#', 1); + $this->assertSame('select * from "users" having ("bar" # ?) != 0', $builder->toSql()); + } + public function testMergeWheresCanMergeWheresAndBindings() { $builder = $this->getBuilder(); From 676954b58822895cc31c35c975a65473feb79458 Mon Sep 17 00:00:00 2001 From: Marc-Etienne Barrut Date: Fri, 21 Jan 2022 02:39:10 +0100 Subject: [PATCH 2/2] Fix missing mock expectation for tests --- tests/Database/DatabaseEloquentModelTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index 76fa651e855c..94f4c2e877c3 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -2153,6 +2153,7 @@ protected function addMockConnection($model) $model->setConnectionResolver($resolver = m::mock(ConnectionResolverInterface::class)); $resolver->shouldReceive('connection')->andReturn($connection = m::mock(Connection::class)); $connection->shouldReceive('getQueryGrammar')->andReturn($grammar = m::mock(Grammar::class)); + $grammar->shouldReceive('getBinaryOperators')->andReturn([]); $connection->shouldReceive('getPostProcessor')->andReturn($processor = m::mock(Processor::class)); $connection->shouldReceive('query')->andReturnUsing(function () use ($connection, $grammar, $processor) { return new BaseBuilder($connection, $grammar, $processor); @@ -2440,6 +2441,7 @@ public function getConnection() { $mock = m::mock(Connection::class); $mock->shouldReceive('getQueryGrammar')->andReturn($grammar = m::mock(Grammar::class)); + $grammar->shouldReceive('getBinaryOperators')->andReturn([]); $mock->shouldReceive('getPostProcessor')->andReturn($processor = m::mock(Processor::class)); $mock->shouldReceive('getName')->andReturn('name'); $mock->shouldReceive('query')->andReturnUsing(function () use ($mock, $grammar, $processor) {