Add SqlFunction support in addWhere
authorPatrick Figel <pfigel@greenpeace.org>
Sat, 4 Sep 2021 12:51:42 +0000 (14:51 +0200)
committerPatrick Figel <pfigel@greenpeace.org>
Sat, 4 Sep 2021 12:51:42 +0000 (14:51 +0200)
This adds an optional $isExpression parameter in addWhere that
allows use of SqlFunctions in WHERE expressions:

Contact::get(FALSE)
  ->addWhere('last_name', '=', $last_name)
  ->addWhere('LOWER(first_name)', '=', "BINARY('ALICE')", TRUE)
  ->execute()->indexBy('id');

This also adds a few tests to the existing expression support for
ON and HAVING clauses as well as a new BINARY function.

Civi/Api4/Generic/DAOGetAction.php
Civi/Api4/Query/Api4SelectQuery.php
Civi/Api4/Query/SqlFunctionBINARY.php [new file with mode: 0644]
tests/phpunit/api/v4/Action/ContactGetTest.php
tests/phpunit/api/v4/Action/FkJoinTest.php
tests/phpunit/api/v4/Action/SqlFunctionTest.php

index f374047486b853dea3bf37222471b2999a097f0a..aedc6362ca4ac5e99607970a09cd0b9785a7c67f 100644 (file)
@@ -123,6 +123,22 @@ class DAOGetAction extends AbstractGetAction {
     }
   }
 
+  /**
+   * @param string $fieldName
+   * @param string $op
+   * @param mixed $value
+   * @param bool $isExpression
+   * @return $this
+   * @throws \API_Exception
+   */
+  public function addWhere(string $fieldName, string $op, $value = NULL, bool $isExpression = FALSE) {
+    if (!in_array($op, CoreUtil::getOperators())) {
+      throw new \API_Exception('Unsupported operator');
+    }
+    $this->where[] = [$fieldName, $op, $value, $isExpression];
+    return $this;
+  }
+
   /**
    * @return array
    */
index 119abe49a90fd809f62c4568caf8ba717c6c6884..3f44491bde38eb5edfea37840e0b9eab6a1e4a52 100644 (file)
@@ -431,7 +431,7 @@ class Api4SelectQuery {
 
   /**
    * Validate and transform a leaf clause array to SQL.
-   * @param array $clause [$fieldName, $operator, $criteria]
+   * @param array $clause [$fieldName, $operator, $criteria, $isExpression]
    * @param string $type
    *   WHERE|HAVING|ON
    * @param int $depth
@@ -443,12 +443,13 @@ class Api4SelectQuery {
     $field = NULL;
     // Pad array for unary operators
     [$expr, $operator, $value] = array_pad($clause, 3, NULL);
+    $isExpression = $clause[3] ?? FALSE;
     if (!in_array($operator, CoreUtil::getOperators(), TRUE)) {
       throw new \API_Exception('Illegal operator');
     }
 
     // For WHERE clause, expr must be the name of a field.
-    if ($type === 'WHERE') {
+    if ($type === 'WHERE' && !$isExpression) {
       $field = $this->getField($expr, TRUE);
       FormattingUtil::formatInputValue($value, $expr, $field, $operator);
       $fieldAlias = $this->getExpression($expr)->render($this->apiFieldSpec);
@@ -491,7 +492,7 @@ class Api4SelectQuery {
       }
       $fieldAlias = '`' . $fieldAlias . '`';
     }
-    elseif ($type === 'ON') {
+    elseif ($type === 'ON' || ($type === 'WHERE' && $isExpression)) {
       $expr = $this->getExpression($expr);
       $fieldName = count($expr->getFields()) === 1 ? $expr->getFields()[0] : NULL;
       $fieldAlias = $expr->render($this->apiFieldSpec);
diff --git a/Civi/Api4/Query/SqlFunctionBINARY.php b/Civi/Api4/Query/SqlFunctionBINARY.php
new file mode 100644 (file)
index 0000000..e2500d8
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Query;
+
+/**
+ * Sql function
+ */
+class SqlFunctionBINARY extends SqlFunction {
+
+  protected static $category = self::CATEGORY_STRING;
+
+  protected static function params(): array {
+    return [
+      [
+        'optional' => FALSE,
+        'must_be' => ['SqlField', 'SqlString'],
+      ],
+    ];
+  }
+
+  /**
+   * @return string
+   */
+  public static function getTitle(): string {
+    return ts('Binary');
+  }
+
+}
index 2e5075ea03258b8fe1761c44ce011c3cf079c88c..b47a69480d58a11ab71af2ac85607b110cdb8fdb 100644 (file)
@@ -261,6 +261,26 @@ class ContactGetTest extends \api\v4\UnitTestCase {
     $this->assertEquals(['Student'], $result['Contact_RelationshipCache_Contact_01.contact_sub_type:label']);
   }
 
+  public function testGetWithWhereExpression() {
+    $last_name = uniqid(__FUNCTION__);
+
+    $alice = Contact::create()
+      ->setValues(['first_name' => 'Alice', 'last_name' => $last_name])
+      ->execute()->first();
+
+    $result = Contact::get(FALSE)
+      ->addWhere('last_name', '=', $last_name)
+      ->addWhere('LOWER(first_name)', '=', "BINARY('ALICE')", TRUE)
+      ->execute()->indexBy('id');
+    $this->assertCount(0, $result);
+
+    $result = Contact::get(FALSE)
+      ->addWhere('last_name', '=', $last_name)
+      ->addWhere('LOWER(first_name)', '=', "BINARY('alice')", TRUE)
+      ->execute()->indexBy('id');
+    $this->assertArrayHasKey($alice['id'], (array) $result);
+  }
+
   /**
    * @throws \API_Exception
    */
index d7d95d5f8b229cdf1705636271b165d108b0dc07..960b638ad4c7c1b62fe37829f50ff4f2de03a76c 100644 (file)
@@ -426,4 +426,19 @@ class FkJoinTest extends UnitTestCase {
     $this->assertStringContainsString("Deprecated join alias 'contact' used in APIv4 get. Should be changed to 'contact_id'", $message);
   }
 
+  public function testJoinWithExpression() {
+    Phone::create(FALSE)
+      ->setValues(['contact_id' => $this->getReference('test_contact_1')['id'], 'phone' => '654321'])
+      ->execute();
+    $contacts = Contact::get(FALSE)
+      ->addSelect('id', 'phone.phone')
+      ->addJoin('Phone', 'INNER', ['LOWER(phone.phone)', '=', "CONCAT('6', '5', '4', '3', '2', '1')"])
+      ->addWhere('id', 'IN', [$this->getReference('test_contact_1')['id'], $this->getReference('test_contact_2')['id']])
+      ->addOrderBy('phone.id')
+      ->execute();
+    $this->assertCount(1, $contacts);
+    $this->assertEquals($this->getReference('test_contact_1')['id'], $contacts[0]['id']);
+    $this->assertEquals('654321', $contacts[0]['phone.phone']);
+  }
+
 }
index 5cf7373770d89da00fb1ae6544bd90ecdf3186a5..ebac8d5d30cb7a81772039e6552763c335d2ea8b 100644 (file)
@@ -131,6 +131,20 @@ class SqlFunctionTest extends UnitTestCase {
     $this->assertEquals(100, $result[0]['MIN:total_amount']);
     $this->assertEquals(2, $result[0]['count']);
     $this->assertEquals(1, $result[1]['count']);
+
+    $result = Contribution::get(FALSE)
+      ->addGroupBy('contact_id')
+      ->addGroupBy('receive_date')
+      ->addSelect('contact_id')
+      ->addSelect('receive_date')
+      ->addSelect('SUM(total_amount)')
+      ->addOrderBy('receive_date')
+      ->addWhere('contact_id', '=', $cid)
+      ->addHaving('SUM(total_amount)', '>', 300)
+      ->execute();
+    $this->assertCount(1, $result);
+    $this->assertStringStartsWith('2020-04-04', $result[0]['receive_date']);
+    $this->assertEquals(400, $result[0]['SUM:total_amount']);
   }
 
   public function testComparisonFunctions() {