Browse Source

Add Query::whereNotInListOrNull() and QueryExpression::notInOrNull()

Corey Taylor 5 years ago
parent
commit
11245d422f

+ 1 - 1
phpstan-baseline.neon

@@ -222,7 +222,7 @@ parameters:
 
 		-
 			message: "#^Unsafe usage of new static\\(\\)\\.$#"
-			count: 7
+			count: 8
 			path: src/Database/Expression/QueryExpression.php
 
 		-

+ 19 - 0
src/Database/Expression/QueryExpression.php

@@ -367,6 +367,25 @@ class QueryExpression implements ExpressionInterface, Countable
     }
 
     /**
+     * Adds a new condition to the expression object in the form
+     * "(field NOT IN (value1, value2) OR field IS NULL".
+     *
+     * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value
+     * @param string|array|\Cake\Database\ExpressionInterface $values the value to be bound to $field for comparison
+     * @param string|null $type the type name for $value as configured using the Type map.
+     * @return $this
+     */
+    public function notInOrNull($field, $values, $type = null)
+    {
+        $or = new static([], [], 'OR');
+        $or
+            ->notIn($field, $values, $type)
+            ->isNull($field);
+
+        return $this->add($or);
+    }
+
+    /**
      * Adds a new condition to the expression object in the form "EXISTS (...)".
      *
      * @param \Cake\Database\ExpressionInterface $expression the inner query

+ 33 - 0
src/Database/Query.php

@@ -1114,6 +1114,39 @@ class Query implements ExpressionInterface, IteratorAggregate
     }
 
     /**
+     * Adds a NOT IN condition or set of conditions to be used in the WHERE clause for this
+     * query. This also allows the field to be null with a IS NULL condition since the null
+     * value would cause the NOT IN condition to always fail.
+     *
+     * This method does allow empty inputs in contrast to where() if you set
+     * 'allowEmpty' to true.
+     * Be careful about using it without proper sanity checks.
+     *
+     * @param string $field Field
+     * @param array $values Array of values
+     * @param array $options Options
+     * @return $this
+     */
+    public function whereNotInListOrNull(string $field, array $values, array $options = [])
+    {
+        $options += [
+            'types' => [],
+            'allowEmpty' => false,
+        ];
+
+        if ($options['allowEmpty'] && !$values) {
+            return $this->where([$field . ' IS NOT' => null]);
+        }
+
+        return $this->where(
+            [
+                'OR' => [$field . ' NOT IN' => $values, $field . ' IS' => null],
+            ],
+            $options['types']
+        );
+    }
+
+    /**
      * Connects any previously defined set of conditions to the provided list
      * using the AND operator. This function accepts the conditions list in the same
      * format as the method `where` does, hence you can use arrays, expression objects

+ 15 - 0
tests/TestCase/Database/Expression/QueryExpressionTest.php

@@ -203,4 +203,19 @@ class QueryExpressionTest extends TestCase
         $expr = new QueryExpression(['OR' => []]);
         $this->assertCount(0, $expr);
     }
+
+    /**
+     * Tests that both conditions are generated for notInOrNull().
+     *
+     * @return void
+     */
+    public function testNotInOrNull()
+    {
+        $expr = new QueryExpression();
+        $expr->notInOrNull('test', ['one', 'two']);
+        $this->assertEqualsSql(
+            '(test NOT IN (:c0,:c1) OR (test) IS NULL)',
+            $expr->sql(new ValueBinder())
+        );
+    }
 }

+ 51 - 4
tests/TestCase/Database/QueryTest.php

@@ -1731,11 +1731,11 @@ class QueryTest extends TestCase
     }
 
     /**
-     * Tests whereNotInArray() and its input types.
+     * Tests whereNotInList() and its input types.
      *
      * @return void
      */
-    public function testWhereNotInArray()
+    public function testWhereNotInList()
     {
         $this->loadFixtures('Articles');
         $query = new Query($this->connection);
@@ -1754,11 +1754,11 @@ class QueryTest extends TestCase
     }
 
     /**
-     * Tests whereNotInArray() and empty array input.
+     * Tests whereNotInList() and empty array input.
      *
      * @return void
      */
-    public function testWhereNotInArrayEmpty()
+    public function testWhereNotInListEmpty()
     {
         $this->loadFixtures('Articles');
         $query = new Query($this->connection);
@@ -1778,6 +1778,53 @@ class QueryTest extends TestCase
     }
 
     /**
+     * Tests whereNotInListOrNull() and its input types.
+     *
+     * @return void
+     */
+    public function testWhereNotInListOrNull()
+    {
+        $this->loadFixtures('Articles');
+        $query = new Query($this->connection);
+        $query->select(['id'])
+            ->from('articles')
+            ->whereNotInListOrNull('id', [1, 3]);
+
+        $this->assertQuotedQuery(
+            'SELECT <id> FROM <articles> WHERE \\(<id> not in \\(:c0,:c1\\) OR \\(<id>\\) IS NULL\\)',
+            $query->sql(),
+            !$this->autoQuote
+        );
+
+        $result = $query->execute()->fetchAll('assoc');
+        $this->assertEquals(['id' => '2'], $result[0]);
+    }
+
+    /**
+     * Tests whereNotInListOrNull() and empty array input.
+     *
+     * @return void
+     */
+    public function testWhereNotInListOrNullEmpty()
+    {
+        $this->loadFixtures('Articles');
+        $query = new Query($this->connection);
+        $query->select(['id'])
+            ->from('articles')
+            ->whereNotInListOrNull('id', [], ['allowEmpty' => true])
+            ->order(['id']);
+
+        $this->assertQuotedQuery(
+            'SELECT <id> FROM <articles> WHERE \(<id>\) IS NOT NULL',
+            $query->sql(),
+            !$this->autoQuote
+        );
+
+        $result = $query->execute()->fetchAll('assoc');
+        $this->assertEquals(['id' => '1'], $result[0]);
+    }
+
+    /**
      * Tests order() method both with simple fields and expressions
      *
      * @return void