Add is empty filter to search / api
[civicrm-core.git] / Civi / Api4 / Query / Api4SelectQuery.php
index e604e99bea50d44c7b238c782b1a2d9ab84e72e5..e588e710853bc946af3c6f5631f5c3338b1c447c 100644 (file)
@@ -103,6 +103,9 @@ class Api4SelectQuery {
     // Add ACLs first to avoid redundant subclauses
     $baoName = CoreUtil::getBAOFromApiName($this->getEntity());
     $this->query->where($this->getAclClause(self::MAIN_TABLE_ALIAS, $baoName));
+
+    // Add explicit joins. Other joins implied by dot notation may be added later
+    $this->addExplicitJoins();
   }
 
   /**
@@ -113,8 +116,6 @@ class Api4SelectQuery {
    * @throws \CRM_Core_Exception
    */
   public function getSql() {
-    // Add explicit joins. Other joins implied by dot notation may be added later
-    $this->addExplicitJoins();
     $this->buildSelectClause();
     $this->buildWhereClause();
     $this->buildOrderBy();
@@ -152,7 +153,6 @@ class Api4SelectQuery {
    * @throws \API_Exception
    */
   public function getCount() {
-    $this->addExplicitJoins();
     $this->buildWhereClause();
     // If no having or groupBy, we only need to select count
     if (!$this->getHaving() && !$this->getGroupBy()) {
@@ -434,7 +434,7 @@ class Api4SelectQuery {
         if ($fieldName && $valExpr->getType() === 'SqlString') {
           $value = $valExpr->getExpr();
           FormattingUtil::formatInputValue($value, $fieldName, $this->apiFieldSpec[$fieldName], $operator);
-          return \CRM_Core_DAO::createSQLFilter($fieldAlias, [$operator => $value]);
+          return $this->createSQLClause($fieldAlias, $operator, $value, $this->apiFieldSpec[$fieldName]);
         }
         else {
           $value = $valExpr->render($this->apiFieldSpec);
@@ -447,6 +447,22 @@ class Api4SelectQuery {
       }
     }
 
+    $sqlClause = $this->createSQLClause($fieldAlias, $operator, $value, $field ?? NULL);
+    if ($sqlClause === NULL) {
+      throw new \API_Exception("Invalid value in $type clause for '$expr'");
+    }
+    return $sqlClause;
+  }
+
+  /**
+   * @param string $fieldAlias
+   * @param string $operator
+   * @param mixed $value
+   * @param array|null $field
+   * @return array|string|NULL
+   * @throws \Exception
+   */
+  protected function createSQLClause($fieldAlias, $operator, $value, $field) {
     if ($operator === 'CONTAINS') {
       switch ($field['serialize'] ?? NULL) {
         case \CRM_Core_DAO::SERIALIZE_JSON:
@@ -468,11 +484,18 @@ class Api4SelectQuery {
       }
     }
 
-    $sql_clause = \CRM_Core_DAO::createSQLFilter($fieldAlias, [$operator => $value]);
-    if ($sql_clause === NULL) {
-      throw new \API_Exception("Invalid value in $type clause for '$expr'");
+    if ($operator === 'IS EMPTY' || $operator === 'IS NOT EMPTY') {
+      // If field is not a string or number, this will pass through and use IS NULL/IS NOT NULL
+      $operator = str_replace('EMPTY', 'NULL', $operator);
+      // For strings & numbers, create an OR grouping of empty value OR null
+      if (in_array($field['data_type'] ?? NULL, ['String', 'Integer', 'Float'], TRUE)) {
+        $emptyVal = $field['data_type'] === 'String' ? '""' : '0';
+        $isEmptyClause = $operator === 'IS NULL' ? "= $emptyVal OR" : "<> $emptyVal AND";
+        return "($fieldAlias $isEmptyClause $fieldAlias $operator)";
+      }
     }
-    return $sql_clause;
+
+    return \CRM_Core_DAO::createSQLFilter($fieldAlias, [$operator => $value]);
   }
 
   /**