Merge in 5.25
authorSeamus Lee <seamuslee001@gmail.com>
Mon, 20 Apr 2020 09:16:14 +0000 (19:16 +1000)
committerSeamus Lee <seamuslee001@gmail.com>
Mon, 20 Apr 2020 09:16:14 +0000 (19:16 +1000)
22 files changed:
Civi/Api4/Generic/AbstractGetAction.php
Civi/Api4/Generic/DAOGetAction.php
Civi/Api4/Generic/DAOGetFieldsAction.php
Civi/Api4/Query/Api4SelectQuery.php
Civi/Api4/Query/SqlExpression.php [deleted file]
Civi/Api4/Query/SqlField.php [deleted file]
Civi/Api4/Query/SqlFunction.php [deleted file]
Civi/Api4/Query/SqlFunctionAVG.php [deleted file]
Civi/Api4/Query/SqlFunctionCOUNT.php [deleted file]
Civi/Api4/Query/SqlFunctionMAX.php [deleted file]
Civi/Api4/Query/SqlFunctionMIN.php [deleted file]
Civi/Api4/Query/SqlFunctionSUM.php [deleted file]
Civi/Api4/Query/SqlNull.php [deleted file]
Civi/Api4/Query/SqlNumber.php [deleted file]
Civi/Api4/Query/SqlString.php [deleted file]
Civi/Api4/Query/SqlWild.php [deleted file]
ang/api4Explorer/Explorer.html
ang/api4Explorer/Explorer.js
tests/phpunit/api/v4/Action/SqlExpressionTest.php [deleted file]
tests/phpunit/api/v4/Action/SqlFunctionTest.php [deleted file]
tests/phpunit/api/v4/Query/SqlExpressionParserTest.php [deleted file]
tests/phpunit/api/v4/UnitTestCase.php

index dc020f4af146dd4a8bc88d768c0a33bbebbb319a..d82e7071b1e6f0328d65c8afae9a774d14e242da 100644 (file)
@@ -81,7 +81,7 @@ abstract class AbstractGetAction extends AbstractQueryAction {
    */
   protected function expandSelectClauseWildcards() {
     $wildFields = array_filter($this->select, function($item) {
-      return strpos($item, '*') !== FALSE && strpos($item, '.') === FALSE && strpos($item, '(') === FALSE && strpos($item, ' ') === FALSE;
+      return strpos($item, '*') !== FALSE && strpos($item, '.') === FALSE;
     });
     foreach ($wildFields as $item) {
       $pos = array_search($item, array_values($this->select));
index ab7d85b966d3ab1802b2b0dd2219a95f54b34854..43785e0f52f9c4b9213fac6f81837990eba12e9a 100644 (file)
@@ -47,22 +47,6 @@ class DAOGetAction extends AbstractGetAction {
    */
   protected $select = [];
 
-  /**
-   * Field(s) by which to group the results.
-   *
-   * @var array
-   */
-  protected $groupBy = [];
-
-  /**
-   * Clause for filtering results after grouping and filters are applied.
-   *
-   * Each expression should correspond to an item from the SELECT array.
-   *
-   * @var array
-   */
-  protected $having = [];
-
   public function _run(Result $result) {
     $this->setDefaultWhereClause();
     $this->expandSelectClauseWildcards();
@@ -82,44 +66,4 @@ class DAOGetAction extends AbstractGetAction {
     return $result;
   }
 
-  /**
-   * @return array
-   */
-  public function getGroupBy(): array {
-    return $this->groupBy;
-  }
-
-  /**
-   * @param array $groupBy
-   * @return $this
-   */
-  public function setGroupBy(array $groupBy) {
-    $this->groupBy = $groupBy;
-    return $this;
-  }
-
-  /**
-   * @param string $field
-   * @return $this
-   */
-  public function addGroupBy(string $field) {
-    $this->groupBy[] = $field;
-    return $this;
-  }
-
-  /**
-   * @param string $expr
-   * @param string $op
-   * @param mixed $value
-   * @return $this
-   * @throws \API_Exception
-   */
-  public function addHaving(string $expr, string $op, $value = NULL) {
-    if (!in_array($op, \CRM_Core_DAO::acceptedSQLOperators())) {
-      throw new \API_Exception('Unsupported operator');
-    }
-    $this->having[] = [$expr, $op, $value];
-    return $this;
-  }
-
 }
index 74afc1715fc9d1446475b30b17c79ff804f927ce..7d7ac1849d0a0158340d92774b231b76b4ba32c8 100644 (file)
@@ -63,10 +63,6 @@ class DAOGetFieldsAction extends BasicGetFieldsAction {
       'name' => 'help_post',
       'data_type' => 'String',
     ];
-    $fields[] = [
-      'name' => 'column_name',
-      'data_type' => 'String',
-    ];
     $fields[] = [
       'name' => 'custom_field_id',
       'data_type' => 'Integer',
index 21298de2a0e4f008e055bffcb2d203e6e9550449..898cbbfa9dbb2bc8ae68acb4bf42b0ce5eeee47c 100644 (file)
@@ -42,15 +42,16 @@ class Api4SelectQuery extends SelectQuery {
   protected $apiVersion = 4;
 
   /**
-   * @var \Civi\Api4\Service\Schema\Joinable\Joinable[]
-   *   The joinable tables that have been joined so far
+   * @var array
+   *   Maps select fields to [<table_alias>, <column_alias>]
    */
-  protected $joinedTables = [];
+  protected $fkSelectAliases = [];
 
   /**
-   * @var array
+   * @var \Civi\Api4\Service\Schema\Joinable\Joinable[]
+   *   The joinable tables that have been joined so far
    */
-  protected $selectAliases = [];
+  protected $joinedTables = [];
 
   /**
    * If set to an array, this will start collecting debug info.
@@ -59,18 +60,6 @@ class Api4SelectQuery extends SelectQuery {
    */
   public $debugOutput = NULL;
 
-  /**
-   * @var array
-   */
-  public $groupBy = [];
-
-  public $forceSelectId = TRUE;
-
-  /**
-   * @var array
-   */
-  public $having = [];
-
   /**
    * @param \Civi\Api4\Generic\DAOGetAction $apiGet
    */
@@ -79,7 +68,6 @@ class Api4SelectQuery extends SelectQuery {
     $this->checkPermissions = $apiGet->getCheckPermissions();
     $this->select = $apiGet->getSelect();
     $this->where = $apiGet->getWhere();
-    $this->groupBy = $apiGet->getGroupBy();
     $this->orderBy = $apiGet->getOrderBy();
     $this->limit = $apiGet->getLimit();
     $this->offset = $apiGet->getOffset();
@@ -91,9 +79,9 @@ class Api4SelectQuery extends SelectQuery {
     }
     $baoName = CoreUtil::getBAOFromApiName($this->entity);
     $this->entityFieldNames = array_column($baoName::fields(), 'name');
-    foreach ($apiGet->entityFields() as $path => $field) {
-      $field['sql_name'] = '`' . self::MAIN_TABLE_ALIAS . '`.`' . $field['column_name'] . '`';
-      $this->addSpecField($path, $field);
+    $this->apiFieldSpec = $apiGet->entityFields();
+    foreach ($this->apiFieldSpec as $key => $field) {
+      $this->apiFieldSpec[$key]['sql_name'] = '`' . self::MAIN_TABLE_ALIAS . '`.`' . $field['column_name'] . '`';
     }
 
     $this->constructQueryObject($baoName);
@@ -115,8 +103,6 @@ class Api4SelectQuery extends SelectQuery {
     $this->buildWhereClause();
     $this->buildOrderBy();
     $this->buildLimit();
-    $this->buildGroupBy();
-    $this->buildHavingClause();
     return $this->query->toSQL();
   }
 
@@ -132,19 +118,20 @@ class Api4SelectQuery extends SelectQuery {
       $this->debugOutput['sql'][] = $sql;
     }
     $query = \CRM_Core_DAO::executeQuery($sql);
-    $i = 0;
+
     while ($query->fetch()) {
-      $id = $query->id ?? $i++;
       if (in_array('row_count', $this->select)) {
         $results[]['row_count'] = (int) $query->c;
         break;
       }
-      $results[$id] = [];
-      foreach ($this->selectAliases as $alias => $expr) {
+      $results[$query->id] = [];
+      foreach ($this->select as $alias) {
         $returnName = $alias;
-        $alias = str_replace('.', '_', $alias);
-        $results[$id][$returnName] = property_exists($query, $alias) ? $query->$alias : NULL;
-      }
+        if ($this->isOneToOneField($alias)) {
+          $alias = str_replace('.', '_', $alias);
+          $results[$query->id][$returnName] = property_exists($query, $alias) ? $query->$alias : NULL;
+        }
+      };
     }
     $event = new PostSelectQueryEvent($results, $this);
     \Civi::dispatcher()->dispatch(Events::POST_SELECT_QUERY, $event);
@@ -153,7 +140,6 @@ class Api4SelectQuery extends SelectQuery {
   }
 
   protected function buildSelectClause() {
-    // An empty select is the same as *
     if (empty($this->select)) {
       $this->select = $this->entityFieldNames;
     }
@@ -162,13 +148,12 @@ class Api4SelectQuery extends SelectQuery {
       return;
     }
     else {
-      if ($this->forceSelectId) {
-        $this->select = array_merge(['id'], $this->select);
-      }
+      // Always select id field
+      $this->select = array_merge(['id'], $this->select);
 
       // Expand wildcards in joins (the api wrapper already expanded non-joined wildcards)
       $wildFields = array_filter($this->select, function($item) {
-        return strpos($item, '*') !== FALSE && strpos($item, '.') !== FALSE && strpos($item, '(') === FALSE && strpos($item, ' ') === FALSE;
+        return strpos($item, '*') !== FALSE && strpos($item, '.') !== FALSE;
       });
       foreach ($wildFields as $item) {
         $pos = array_search($item, array_values($this->select));
@@ -178,27 +163,20 @@ class Api4SelectQuery extends SelectQuery {
       }
       $this->select = array_unique($this->select);
     }
-    foreach ($this->select as $item) {
-      $expr = SqlExpression::convert($item, TRUE);
-      $valid = TRUE;
-      foreach ($expr->getFields() as $fieldName) {
-        $field = $this->getField($fieldName);
-        // Remove expressions with unknown fields without raising an error
-        if (!$field) {
-          $this->select = array_diff($this->select, [$item]);
-          if (is_array($this->debugOutput)) {
-            $this->debugOutput['undefined_fields'][] = $fieldName;
-          }
-          $valid = FALSE;
-        }
-        elseif ($field['is_many']) {
-          $valid = FALSE;
-        }
+    foreach ($this->select as $fieldName) {
+      $field = $this->getField($fieldName);
+      if (!$this->isOneToOneField($fieldName)) {
+        continue;
+      }
+      elseif ($field) {
+        $this->query->select($field['sql_name'] . " AS `$fieldName`");
       }
-      if ($valid) {
-        $alias = $expr->getAlias();
-        $this->selectAliases[$alias] = $expr->getExpr();
-        $this->query->select($expr->render($this->apiFieldSpec) . " AS `$alias`");
+      // Remove unknown fields without raising an error
+      else {
+        $this->select = array_diff($this->select, [$fieldName]);
+        if (is_array($this->debugOutput)) {
+          $this->debugOutput['undefined_fields'][] = $fieldName;
+        }
       }
     }
   }
@@ -227,15 +205,11 @@ class Api4SelectQuery extends SelectQuery {
    * @inheritDoc
    */
   protected function buildOrderBy() {
-    foreach ($this->orderBy as $item => $dir) {
+    foreach ($this->orderBy as $fieldName => $dir) {
       if ($dir !== 'ASC' && $dir !== 'DESC') {
-        throw new \API_Exception("Invalid sort direction. Cannot order by $item $dir");
+        throw new \API_Exception("Invalid sort direction. Cannot order by $fieldName $dir");
       }
-      $expr = SqlExpression::convert($item);
-      foreach ($expr->getFields() as $fieldName) {
-        $this->getField($fieldName, TRUE);
-      }
-      $this->query->orderBy($expr->render($this->apiFieldSpec) . " $dir");
+      $this->query->orderBy($this->getField($fieldName, TRUE)['sql_name'] . " $dir");
     }
   }
 
@@ -249,19 +223,6 @@ class Api4SelectQuery extends SelectQuery {
     }
   }
 
-  /**
-   * Adds GROUP BY clause to query
-   */
-  protected function buildGroupBy() {
-    foreach ($this->groupBy as $item) {
-      $expr = SqlExpression::convert($item);
-      foreach ($expr->getFields() as $fieldName) {
-        $this->getField($fieldName, TRUE);
-      }
-      $this->query->groupBy($expr->render($this->apiFieldSpec));
-    }
-  }
-
   /**
    * Recursively validate and transform a branch or leaf clause array to SQL.
    *
@@ -353,7 +314,6 @@ class Api4SelectQuery extends SelectQuery {
    *
    * @param string $fieldName
    * @param bool $strict
-   *   In strict mode, this will throw an exception if the field doesn't exist
    *
    * @return string|null
    * @throws \API_Exception
@@ -364,22 +324,27 @@ class Api4SelectQuery extends SelectQuery {
       $this->joinFK($fieldName);
     }
     $field = $this->apiFieldSpec[$fieldName] ?? NULL;
-    if ($strict && !$field) {
+    // Check if field exists and we have permission to view it
+    if ($field && (!$this->checkPermissions || empty($field['permission']) || \CRM_Core_Permission::check($field['permission']))) {
+      return $field;
+    }
+    elseif ($strict) {
       throw new \API_Exception("Invalid field '$fieldName'");
     }
-    return $field;
+    return NULL;
   }
 
   /**
    * Joins a path and adds all fields in the joined eneity to apiFieldSpec
    *
    * @param $key
+   * @return bool
    * @throws \API_Exception
    * @throws \Exception
    */
   protected function joinFK($key) {
     if (isset($this->apiFieldSpec[$key])) {
-      return;
+      return TRUE;
     }
 
     $pathArray = explode('.', $key);
@@ -391,24 +356,15 @@ class Api4SelectQuery extends SelectQuery {
     $pathString = implode('.', $pathArray);
 
     if (!$joiner->canJoin($this, $pathString)) {
-      return;
+      return FALSE;
     }
 
     $joinPath = $joiner->join($this, $pathString);
-
-    $isMany = FALSE;
-    foreach ($joinPath as $joinable) {
-      if ($joinable->getJoinType() === Joinable::JOIN_TYPE_ONE_TO_MANY) {
-        $isMany = TRUE;
-      }
-    }
-
     /** @var \Civi\Api4\Service\Schema\Joinable\Joinable $lastLink */
     $lastLink = array_pop($joinPath);
 
     // Custom field names are already prefixed
-    $isCustom = $lastLink instanceof CustomGroupJoinable;
-    if ($isCustom) {
+    if ($lastLink instanceof CustomGroupJoinable) {
       array_pop($pathArray);
     }
     $prefix = $pathArray ? implode('.', $pathArray) . '.' : '';
@@ -417,11 +373,10 @@ class Api4SelectQuery extends SelectQuery {
     foreach ($lastLink->getEntityFields() as $fieldObject) {
       $fieldArray = ['entity' => $joinEntity] + $fieldObject->toArray();
       $fieldArray['sql_name'] = '`' . $lastLink->getAlias() . '`.`' . $fieldArray['column_name'] . '`';
-      $fieldArray['is_custom'] = $isCustom;
-      $fieldArray['is_join'] = TRUE;
-      $fieldArray['is_many'] = $isMany;
-      $this->addSpecField($prefix . $fieldArray['name'], $fieldArray);
+      $this->apiFieldSpec[$prefix . $fieldArray['name']] = $fieldArray;
     }
+
+    return TRUE;
   }
 
   /**
@@ -628,20 +583,4 @@ class Api4SelectQuery extends SelectQuery {
     return $path;
   }
 
-  /**
-   * @param $path
-   * @param $field
-   */
-  private function addSpecField($path, $field) {
-    // Only add field to spec if we have permission
-    if ($this->checkPermissions && !empty($field['permission']) && !\CRM_Core_Permission::check($field['permission'])) {
-      $this->apiFieldSpec[$path] = FALSE;
-      return;
-    }
-    $defaults = [];
-    $defaults['is_custom'] = $defaults['is_join'] = $defaults['is_many'] = FALSE;
-    $field += $defaults;
-    $this->apiFieldSpec[$path] = $field;
-  }
-
 }
diff --git a/Civi/Api4/Query/SqlExpression.php b/Civi/Api4/Query/SqlExpression.php
deleted file mode 100644 (file)
index e21f388..0000000
+++ /dev/null
@@ -1,151 +0,0 @@
-<?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;
-
-/**
- * Base class for SqlColumn, SqlString, SqlBool, and SqlFunction classes.
- *
- * These are used to validate and format sql expressions in Api4 select queries.
- *
- * @package Civi\Api4\Query
- */
-abstract class SqlExpression {
-
-  /**
-   * @var array
-   */
-  protected $fields = [];
-
-  /**
-   * The SELECT alias (if null it will be calculated by getAlias)
-   * @var string|null
-   */
-  protected $alias;
-
-  /**
-   * The raw expression, minus the alias.
-   * @var string
-   */
-  protected $expr = '';
-
-  /**
-   * SqlFunction constructor.
-   * @param string $expr
-   * @param string|null $alias
-   */
-  public function __construct(string $expr, $alias = NULL) {
-    $this->expr = $expr;
-    $this->alias = $alias;
-    $this->initialize();
-  }
-
-  abstract protected function initialize();
-
-  /**
-   * Converts a string to a SqlExpression object.
-   *
-   * E.g. the expression "SUM(foo)" would return a SqlFunctionSUM object.
-   *
-   * @param string $expression
-   * @param bool $parseAlias
-   * @param array $mustBe
-   * @param array $cantBe
-   * @return SqlExpression
-   * @throws \API_Exception
-   */
-  public static function convert(string $expression, $parseAlias = FALSE, $mustBe = [], $cantBe = ['SqlWild']) {
-    $as = $parseAlias ? strrpos($expression, ' AS ') : FALSE;
-    $expr = $as ? substr($expression, 0, $as) : $expression;
-    $alias = $as ? \CRM_Utils_String::munge(substr($expression, $as + 4)) : NULL;
-    $bracketPos = strpos($expr, '(');
-    $firstChar = substr($expr, 0, 1);
-    $lastChar = substr($expr, -1);
-    // If there are brackets but not the first character, we have a function
-    if ($bracketPos && $lastChar === ')') {
-      $fnName = substr($expr, 0, $bracketPos);
-      if ($fnName !== strtoupper($fnName)) {
-        throw new \API_Exception('Sql function must be uppercase.');
-      }
-      $className = 'SqlFunction' . $fnName;
-    }
-    // String expression
-    elseif ($firstChar === $lastChar && in_array($firstChar, ['"', "'"], TRUE)) {
-      $className = 'SqlString';
-    }
-    elseif ($expr === 'NULL') {
-      $className = 'SqlNull';
-    }
-    elseif ($expr === '*') {
-      $className = 'SqlWild';
-    }
-    elseif (is_numeric($expr)) {
-      $className = 'SqlNumber';
-    }
-    // If none of the above, assume it's a field name
-    else {
-      $className = 'SqlField';
-    }
-    $className = __NAMESPACE__ . '\\' . $className;
-    if (!class_exists($className)) {
-      throw new \API_Exception('Unable to parse sql expression: ' . $expression);
-    }
-    $sqlExpression = new $className($expr, $alias);
-    foreach ($cantBe as $cant) {
-      if (is_a($sqlExpression, __NAMESPACE__ . '\\' . $cant)) {
-        throw new \API_Exception('Illegal sql expression.');
-      }
-    }
-    if ($mustBe) {
-      foreach ($mustBe as $must) {
-        if (is_a($sqlExpression, __NAMESPACE__ . '\\' . $must)) {
-          return $sqlExpression;
-        }
-      }
-      throw new \API_Exception('Illegal sql expression.');
-    }
-    return $sqlExpression;
-  }
-
-  /**
-   * Returns the field names of all sql columns that are arguments to this expression.
-   *
-   * @return array
-   */
-  public function getFields(): array {
-    return $this->fields;
-  }
-
-  /**
-   * Renders expression to a sql string, replacing field names with column names.
-   *
-   * @param array $fieldList
-   * @return string
-   */
-  abstract public function render(array $fieldList): string;
-
-  /**
-   * @return string
-   */
-  public function getExpr(): string {
-    return $this->expr;
-  }
-
-  /**
-   * Returns the alias to use for SELECT AS.
-   *
-   * @return string
-   */
-  public function getAlias(): string {
-    return $this->alias ?? $this->fields[0] ?? \CRM_Utils_String::munge($this->expr);
-  }
-
-}
diff --git a/Civi/Api4/Query/SqlField.php b/Civi/Api4/Query/SqlField.php
deleted file mode 100644 (file)
index 712bd7b..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-<?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 column expression
- */
-class SqlField extends SqlExpression {
-
-  protected function initialize() {
-    $this->fields[] = $this->expr;
-  }
-
-  public function render(array $fieldList): string {
-    if (empty($fieldList[$this->expr])) {
-      throw new \API_Exception("Invalid field '{$this->expr}'");
-    }
-    return $fieldList[$this->expr]['sql_name'];
-  }
-
-}
diff --git a/Civi/Api4/Query/SqlFunction.php b/Civi/Api4/Query/SqlFunction.php
deleted file mode 100644 (file)
index 19753d9..0000000
+++ /dev/null
@@ -1,189 +0,0 @@
-<?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;
-
-/**
- * Base class for all Sql functions.
- *
- * @package Civi\Api4\Query
- */
-abstract class SqlFunction extends SqlExpression {
-
-  protected static $params = [];
-
-  protected $args = [];
-
-  /**
-   * Parse the argument string into an array of function arguments
-   */
-  protected function initialize() {
-    $arg = trim(substr($this->expr, strpos($this->expr, '(') + 1, -1));
-    foreach ($this->getParams() as $param) {
-      $prefix = $this->captureKeyword($param['prefix'], $arg);
-      if ($param['expr'] && isset($prefix) || in_array('', $param['prefix']) || !$param['optional']) {
-        $this->captureExpressions($arg, $param['expr'], $param['must_be'], $param['cant_be']);
-        $this->captureKeyword($param['suffix'], $arg);
-      }
-    }
-  }
-
-  /**
-   * Shift a keyword off the beginning of the argument string and into the argument array.
-   *
-   * @param array $keywords
-   *   Whitelist of keywords
-   * @param string $arg
-   * @return mixed|null
-   */
-  private function captureKeyword($keywords, &$arg) {
-    foreach (array_filter($keywords) as $key) {
-      if (strpos($arg, $key . ' ') === 0) {
-        $this->args[] = $key;
-        $arg = ltrim(substr($arg, strlen($key)));
-        return $key;
-      }
-    }
-    return NULL;
-  }
-
-  /**
-   * Shifts 0 or more expressions off the argument string and into the argument array
-   *
-   * @param string $arg
-   * @param int $limit
-   * @param array $mustBe
-   * @param array $cantBe
-   * @throws \API_Exception
-   */
-  private function captureExpressions(&$arg, $limit, $mustBe, $cantBe) {
-    $captured = 0;
-    $arg = ltrim($arg);
-    while ($arg) {
-      $item = $this->captureExpression($arg);
-      $arg = ltrim(substr($arg, strlen($item)));
-      $expr = SqlExpression::convert($item, FALSE, $mustBe, $cantBe);
-      $this->fields = array_merge($this->fields, $expr->getFields());
-      if ($captured) {
-        $this->args[] = ',';
-      }
-      $this->args[] = $expr;
-      $captured++;
-      // Keep going if we have a comma indicating another expression follows
-      if ($captured < $limit && substr($arg, 0, 1) === ',') {
-        $arg = ltrim(substr($arg, 1));
-      }
-      else {
-        return;
-      }
-    }
-  }
-
-  /**
-   * Scans the beginning of a string for an expression; stops when it hits delimiter
-   *
-   * @param $arg
-   * @return string
-   */
-  private function captureExpression($arg) {
-    $chars = str_split($arg);
-    $isEscaped = $quote = NULL;
-    $item = '';
-    $quotes = ['"', "'"];
-    $brackets = [
-      ')' => '(',
-    ];
-    $enclosures = array_fill_keys($brackets, 0);
-    foreach ($chars as $index => $char) {
-      if (!$isEscaped && in_array($char, $quotes, TRUE)) {
-        // Open quotes - we'll ignore everything inside
-        if (!$quote) {
-          $quote = $char;
-        }
-        // Close quotes
-        elseif ($char === $quote) {
-          $quote = NULL;
-        }
-      }
-      if (!$quote) {
-        // Delineates end of expression
-        if (($char == ',' || $char == ' ') && !array_filter($enclosures)) {
-          return $item;
-        }
-        // Open brackets - we'll ignore delineators inside
-        if (isset($enclosures[$char])) {
-          $enclosures[$char]++;
-        }
-        // Close brackets
-        if (isset($brackets[$char]) && $enclosures[$brackets[$char]]) {
-          $enclosures[$brackets[$char]]--;
-        }
-      }
-      $item .= $char;
-      // We are escaping the next char if this is a backslash not preceded by an odd number of backslashes
-      $isEscaped = $char === '\\' && ((strlen($item) - strlen(rtrim($item, '\\'))) % 2);
-    }
-    return $item;
-  }
-
-  public function render(array $fieldList): string {
-    $output = $this->getName() . '(';
-    foreach ($this->args as $index => $arg) {
-      if ($index && $arg !== ',') {
-        $output .= ' ';
-      }
-      if (is_object($arg)) {
-        $output .= $arg->render($fieldList);
-      }
-      else {
-        $output .= $arg;
-      }
-    }
-    return $output . ')';
-  }
-
-  /**
-   * @inheritDoc
-   */
-  public function getAlias(): string {
-    return $this->alias ?? $this->getName() . ':' . implode('_', $this->fields);
-  }
-
-  /**
-   * Get the name of this sql function.
-   * @return string
-   */
-  public static function getName(): string {
-    $className = static::class;
-    return substr($className, strrpos($className, 'SqlFunction') + 11);
-  }
-
-  /**
-   * Get the param metadata for this sql function.
-   * @return array
-   */
-  public static function getParams(): array {
-    $params = [];
-    foreach (static::$params as $param) {
-      // Merge in defaults to ensure each param has these properties
-      $params[] = $param + [
-        'prefix' => [],
-        'expr' => 1,
-        'suffix' => [],
-        'optional' => FALSE,
-        'must_be' => [],
-        'cant_be' => ['SqlWild'],
-      ];
-    }
-    return $params;
-  }
-
-}
diff --git a/Civi/Api4/Query/SqlFunctionAVG.php b/Civi/Api4/Query/SqlFunctionAVG.php
deleted file mode 100644 (file)
index 9a06413..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-<?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 SqlFunctionAVG extends SqlFunction {
-
-  protected static $params = [
-    [
-      'prefix' => ['', 'DISTINCT', 'ALL'],
-      'expr' => 1,
-      'must_be' => ['SqlField'],
-    ],
-  ];
-
-}
diff --git a/Civi/Api4/Query/SqlFunctionCOUNT.php b/Civi/Api4/Query/SqlFunctionCOUNT.php
deleted file mode 100644 (file)
index d444675..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<?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 SqlFunctionCOUNT extends SqlFunction {
-
-  protected static $params = [
-    [
-      'prefix' => ['', 'DISTINCT', 'ALL'],
-      'expr' => 1,
-      'must_be' => ['SqlField', 'SqlWild'],
-      'cant_be' => [],
-    ],
-  ];
-
-}
diff --git a/Civi/Api4/Query/SqlFunctionMAX.php b/Civi/Api4/Query/SqlFunctionMAX.php
deleted file mode 100644 (file)
index f80ebee..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-<?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 SqlFunctionMAX extends SqlFunction {
-
-  protected static $params = [
-    [
-      'prefix' => ['', 'DISTINCT', 'ALL'],
-      'expr' => 1,
-      'must_be' => ['SqlField'],
-    ],
-  ];
-
-}
diff --git a/Civi/Api4/Query/SqlFunctionMIN.php b/Civi/Api4/Query/SqlFunctionMIN.php
deleted file mode 100644 (file)
index 993a5b1..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-<?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 SqlFunctionMIN extends SqlFunction {
-
-  protected static $params = [
-    [
-      'prefix' => ['', 'DISTINCT', 'ALL'],
-      'expr' => 1,
-      'must_be' => ['SqlField'],
-    ],
-  ];
-
-}
diff --git a/Civi/Api4/Query/SqlFunctionSUM.php b/Civi/Api4/Query/SqlFunctionSUM.php
deleted file mode 100644 (file)
index 36f4ebb..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-<?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 SqlFunctionSUM extends SqlFunction {
-
-  protected static $params = [
-    [
-      'prefix' => ['', 'DISTINCT', 'ALL'],
-      'expr' => 1,
-      'must_be' => ['SqlField'],
-    ],
-  ];
-
-}
diff --git a/Civi/Api4/Query/SqlNull.php b/Civi/Api4/Query/SqlNull.php
deleted file mode 100644 (file)
index 046d04c..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-<?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;
-
-/**
- * NULL sql expression
- */
-class SqlNull extends SqlExpression {
-
-  protected function initialize() {
-  }
-
-  public function render(array $fieldList): string {
-    return 'NULL';
-  }
-
-}
diff --git a/Civi/Api4/Query/SqlNumber.php b/Civi/Api4/Query/SqlNumber.php
deleted file mode 100644 (file)
index 064121b..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-<?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;
-
-/**
- * Numeric sql expression
- */
-class SqlNumber extends SqlExpression {
-
-  protected function initialize() {
-    \CRM_Utils_Type::validate($this->expr, 'Float');
-  }
-
-  public function render(array $fieldList): string {
-    return $this->expr;
-  }
-
-}
diff --git a/Civi/Api4/Query/SqlString.php b/Civi/Api4/Query/SqlString.php
deleted file mode 100644 (file)
index 8ea9c00..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-<?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;
-
-/**
- * String sql expression
- */
-class SqlString extends SqlExpression {
-
-  protected function initialize() {
-    // Remove surrounding quotes
-    $str = substr($this->expr, 1, -1);
-    // Unescape the outer quote character inside the string to prevent double-escaping in render()
-    $quot = substr($this->expr, 0, 1);
-    $backslash = chr(0) . 'backslash' . chr(0);
-    $this->expr = str_replace(['\\\\', "\\$quot", $backslash], [$backslash, $quot, '\\\\'], $str);
-  }
-
-  public function render(array $fieldList): string {
-    return '"' . \CRM_Core_DAO::escapeString($this->expr) . '"';
-  }
-
-}
diff --git a/Civi/Api4/Query/SqlWild.php b/Civi/Api4/Query/SqlWild.php
deleted file mode 100644 (file)
index 7799042..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-<?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;
-
-/**
- * Wild * sql expression
- */
-class SqlWild extends SqlExpression {
-
-  protected function initialize() {
-  }
-
-  public function render(array $fieldList): string {
-    return '*';
-  }
-
-}
index af771f914532983e29f8e23fcc8a30fbcb42a330..b6c768088a79c7700a2ba100d0ecbbaf63392dbd 100644 (file)
               <input class="collapsible-optgroups form-control" ng-model="controls[name]" crm-ui-select="{formatResult: formatSelect2Item, formatSelection: formatSelect2Item, data: fieldList(name), placeholder: ts('Add %1', {1: name.slice(0, -1)})}"/>
             </div>
           </fieldset>
-          <fieldset ng-if="::availableParams.groupBy" ng-mouseenter="help('groupBy', availableParams.groupBy)" ng-mouseleave="help()">
-            <legend>groupBy<span class="crm-marker" ng-if="::availableParams.groupBy.required"> *</span></legend>
-            <div ng-model="params.groupBy" ui-sortable="{axis: 'y'}">
-              <div class="api4-input form-inline" ng-repeat="item in params.groupBy track by $index">
-                <i class="crm-i fa-arrows"></i>
-                <input class="form-control huge" type="text" ng-model="params.groupBy[$index]" />
-                <a href class="crm-hover-button" title="Clear" ng-click="clearParam('groupBy', $index)"><i class="crm-i fa-times"></i></a>
-              </div>
-            </div>
-            <div class="api4-input form-inline">
-              <input class="collapsible-optgroups form-control huge" ng-model="controls.groupBy" crm-ui-select="{data: fieldsAndJoinsAndFunctions}" placeholder="Add groupBy" />
-            </div>
-          </fieldset>
-          <fieldset ng-if="::availableParams.having" class="api4-clause-fieldset" ng-mouseenter="help('having', availableParams.having)" ng-mouseleave="help()" crm-api4-clause="{type: 'having', clauses: params.having, required: availableParams.having.required, op: 'AND', label: 'having', fields: havingOptions}">
-          </fieldset>
-          <fieldset ng-if="::availableParams.orderBy" ng-mouseenter="help('orderBy', availableParams.orderBy)" ng-mouseleave="help()">
-            <legend>orderBy<span class="crm-marker" ng-if="::availableParams.orderBy.required"> *</span></legend>
-            <div ng-model="params.orderBy" ui-sortable="{axis: 'y'}">
-              <div class="api4-input form-inline" ng-repeat="clause in params.orderBy">
-                <i class="crm-i fa-arrows"></i>
-                <input class="form-control huge" type="text" ng-model="clause[0]" />
-                <select class="form-control" ng-model="clause[1]">
-                  <option value="ASC">ASC</option>
-                  <option value="DESC">DESC</option>
-                </select>
-                <a href class="crm-hover-button" title="Clear" ng-click="clearParam('orderBy', $index)"><i class="crm-i fa-times"></i></a>
-              </div>
+          <fieldset ng-if="availableParams.orderBy" ng-mouseenter="help('orderBy', availableParams.orderBy)" ng-mouseleave="help()">
+            <legend>orderBy<span class="crm-marker" ng-if="availableParams.orderBy.required"> *</span></legend>
+            <div class="api4-input form-inline" ng-repeat="clause in params.orderBy">
+              <input class="collapsible-optgroups form-control" ng-model="clause[0]" crm-ui-select="{data: fieldsAndJoins, allowClear: true, placeholder: 'Field'}" />
+              <select class="form-control" ng-model="clause[1]">
+                <option value="ASC">ASC</option>
+                <option value="DESC">DESC</option>
+              </select>
             </div>
             <div class="api4-input form-inline">
               <input class="collapsible-optgroups form-control huge" ng-model="controls.orderBy" crm-ui-select="{data: fieldsAndJoinsAndFunctions}" placeholder="Add orderBy" />
             </div>
           </fieldset>
-          <fieldset ng-if="::availableParams.limit && availableParams.offset">
-            <div class="api4-input form-inline">
-              <span ng-mouseenter="help('limit', availableParams.limit)" ng-mouseleave="help()">
-                <label for="api4-param-limit">limit<span class="crm-marker" ng-if="::availableParams.limit.required"> *</span></label>
-                <input class="form-control" type="number" min="0" id="api4-param-limit" ng-model="params.limit"/>
-              </span>
-              <span ng-mouseenter="help('offset', availableParams.offset)" ng-mouseleave="help()">
-                <label for="api4-param-offset">offset<span class="crm-marker" ng-if="::availableParams.offset.required"> *</span></label>
-                <input class="form-control" type="number" min="0" id="api4-param-offset" ng-model="params.offset"/>
-              </span>
-              <a href class="crm-hover-button" title="Clear" ng-click="clearParam('limit');clearParam('offset');" ng-show="!!params.limit || !!params.offset"><i class="crm-i fa-times"></i></a>
-            </div>
-          </fieldset>
-          <fieldset ng-if="::availableParams.chain" ng-mouseenter="help('chain', availableParams.chain)" ng-mouseleave="help()">
+          <fieldset ng-if="availableParams.chain" ng-mouseenter="help('chain', availableParams.chain)" ng-mouseleave="help()">
             <legend>chain</legend>
             <div class="api4-input form-inline" ng-repeat="clause in params.chain" api4-exp-chain="clause" entities="::entities" main-entity="::entity" >
             </div>
index 369474fc0bf18476dff942a5ebd839017af3e4e1..1db4d564f80503629b035488940bb9f78c762360 100644 (file)
       }
     };
 
-    // Gets params that should be represented as generic input fields in the explorer
-    // This fn doesn't have to be particularly efficient as its output is cached in one-time bindings
-    $scope.getGenericParams = function(paramType, defaultNull) {
-      // Returns undefined if params are not yet set; one-time bindings will stabilize when this function returns a value
-      if (_.isEmpty($scope.availableParams)) {
-        return;
-      }
-      var specialParams = ['select', 'fields', 'action', 'where', 'values', 'defaults', 'orderBy', 'chain', 'groupBy', 'having'];
-      if ($scope.availableParams.limit && $scope.availableParams.offset) {
-        specialParams.push('limit', 'offset');
-      }
-      return _.transform($scope.availableParams, function(genericParams, param, name) {
-        if (!_.contains(specialParams, name) &&
-          !(typeof paramType !== 'undefined' && !_.contains(paramType, param.type[0])) &&
-          !(typeof defaultNull !== 'undefined' && ((param.default === null) !== defaultNull))
-        ) {
-          genericParams[name] = param;
-        }
-      });
+    $scope.isSpecial = function(name) {
+      var specialParams = ['select', 'fields', 'action', 'where', 'values', 'defaults', 'orderBy', 'chain'];
+      return _.contains(specialParams, name);
     };
 
     $scope.selectRowCount = function() {
               deep: format === 'json'
             });
           }
-          if (typeof objectParams[name] !== 'undefined' && name !== 'orderBy') {
-            $scope.$watch('params.' + name, function (values) {
+          if (typeof objectParams[name] !== 'undefined') {
+            $scope.$watch('params.' + name, function(values) {
               // Remove empty values
               _.each(values, function (clause, index) {
                 if (!clause || !clause[0]) {
               var field = value;
               $timeout(function() {
                 if (field) {
-                  if (typeof objectParams[name] === 'undefined') {
-                    $scope.params[name].push(field);
-                  } else {
-                    var defaultOp = _.cloneDeep(objectParams[name]);
-                    if (name === 'chain') {
-                      var num = $scope.params.chain.length;
-                      defaultOp[0] = field;
-                      field = 'name_me_' + num;
-                    }
-                    $scope.params[name].push([field, defaultOp]);
+                  var defaultOp = _.cloneDeep(objectParams[name]);
+                  if (name === 'chain') {
+                    var num = $scope.params.chain.length;
+                    defaultOp[0] = field;
+                    field = 'name_me_' + num;
                   }
+                  $scope.params[name].push([field, defaultOp]);
                   $scope.controls[name] = null;
                 }
               });
diff --git a/tests/phpunit/api/v4/Action/SqlExpressionTest.php b/tests/phpunit/api/v4/Action/SqlExpressionTest.php
deleted file mode 100644 (file)
index 3145c9d..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-<?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       |
- +--------------------------------------------------------------------+
- */
-
-/**
- *
- * @package CRM
- * @copyright CiviCRM LLC https://civicrm.org/licensing
- *
- */
-
-
-namespace api\v4\Action;
-
-use api\v4\UnitTestCase;
-use Civi\Api4\Contact;
-
-/**
- * @group headless
- */
-class SqlExpressionTest extends UnitTestCase {
-
-  public function testSelectNull() {
-    Contact::create()->addValue('first_name', 'bob')->setCheckPermissions(FALSE)->execute();
-    $result = Contact::get()
-      ->addSelect('NULL AS nothing', 'NULL', 'NULL AS b*d char', 'first_name AS firsty')
-      ->addWhere('first_name', '=', 'bob')
-      ->setLimit(1)
-      ->execute()
-      ->first();
-    $this->assertNull($result['nothing']);
-    $this->assertNull($result['NULL']);
-    $this->assertNull($result['b_d_char']);
-    $this->assertEquals('bob', $result['firsty']);
-    $this->assertArrayNotHasKey('b*d char', $result);
-  }
-
-  public function testSelectNumbers() {
-    Contact::create()->addValue('first_name', 'bob')->setCheckPermissions(FALSE)->execute();
-    $result = Contact::get()
-      ->addSelect('first_name', 123, 45.678, '-55 AS neg')
-      ->addWhere('first_name', '=', 'bob')
-      ->setLimit(1)
-      ->execute()
-      ->first();
-    $this->assertEquals('bob', $result['first_name']);
-    $this->assertEquals('123', $result['123']);
-    $this->assertEquals('-55', $result['neg']);
-    $this->assertEquals('45.678', $result['45_678']);
-  }
-
-  public function testSelectStrings() {
-    Contact::create()->addValue('first_name', 'bob')->setCheckPermissions(FALSE)->execute();
-    $result = Contact::get()
-      ->addSelect('first_name AS bob')
-      ->addSelect('"hello world" AS hi')
-      ->addSelect('"can\'t \"quote\"" AS quot')
-      ->addWhere('first_name', '=', 'bob')
-      ->setLimit(1)
-      ->execute()
-      ->first();
-    $this->assertEquals('bob', $result['bob']);
-    $this->assertEquals('hello world', $result['hi']);
-    $this->assertEquals('can\'t "quote"', $result['quot']);
-  }
-
-}
diff --git a/tests/phpunit/api/v4/Action/SqlFunctionTest.php b/tests/phpunit/api/v4/Action/SqlFunctionTest.php
deleted file mode 100644 (file)
index f948b22..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-<?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       |
- +--------------------------------------------------------------------+
- */
-
-/**
- *
- * @package CRM
- * @copyright CiviCRM LLC https://civicrm.org/licensing
- *
- */
-
-
-namespace api\v4\Action;
-
-use api\v4\UnitTestCase;
-use Civi\Api4\Contact;
-use Civi\Api4\Contribution;
-
-/**
- * @group headless
- */
-class SqlFunctionTest extends UnitTestCase {
-
-  public function testGetFunctions() {
-    $functions = array_column(\CRM_Api4_Page_Api4Explorer::getSqlFunctions(), NULL, 'name');
-    $this->assertArrayHasKey('SUM', $functions);
-    $this->assertArrayNotHasKey('', $functions);
-    $this->assertArrayNotHasKey('SqlFunction', $functions);
-    $this->assertEquals(1, $functions['MAX']['params'][0]['expr']);
-  }
-
-  public function testGroupAggregates() {
-    $cid = Contact::create()->setCheckPermissions(FALSE)->addValue('first_name', 'bill')->execute()->first()['id'];
-    Contribution::save()
-      ->setCheckPermissions(FALSE)
-      ->setDefaults(['contact_id' => $cid, 'financial_type_id' => 1])
-      ->setRecords([
-        ['total_amount' => 100, 'receive_date' => '2020-01-01'],
-        ['total_amount' => 200, 'receive_date' => '2020-01-01'],
-        ['total_amount' => 300, 'receive_date' => '2020-01-01'],
-        ['total_amount' => 400, 'receive_date' => '2020-01-01'],
-      ])
-      ->execute();
-    $agg = Contribution::get()
-      ->setCheckPermissions(FALSE)
-      ->addGroupBy('contact_id')
-      ->addWhere('contact_id', '=', $cid)
-      ->addSelect('AVG(total_amount) AS average')
-      ->addSelect('SUM(total_amount)')
-      ->addSelect('MAX(total_amount)')
-      ->addSelect('MIN(total_amount)')
-      ->addSelect('COUNT(*) AS count')
-      ->execute()
-      ->first();
-    $this->assertEquals(250, $agg['average']);
-    $this->assertEquals(1000, $agg['SUM:total_amount']);
-    $this->assertEquals(400, $agg['MAX:total_amount']);
-    $this->assertEquals(100, $agg['MIN:total_amount']);
-    $this->assertEquals(4, $agg['count']);
-  }
-
-  public function testGroupHaving() {
-    $cid = Contact::create()->setCheckPermissions(FALSE)->addValue('first_name', 'donor')->execute()->first()['id'];
-    Contribution::save()
-      ->setCheckPermissions(FALSE)
-      ->setDefaults(['contact_id' => $cid, 'financial_type_id' => 1])
-      ->setRecords([
-        ['total_amount' => 100, 'receive_date' => '2020-02-02'],
-        ['total_amount' => 200, 'receive_date' => '2020-02-02'],
-        ['total_amount' => 300, 'receive_date' => '2020-03-03'],
-        ['total_amount' => 400, 'receive_date' => '2020-04-04'],
-      ])
-      ->execute();
-    $result = Contribution::get()
-      ->setCheckPermissions(FALSE)
-      ->addGroupBy('contact_id')
-      ->addGroupBy('receive_date')
-      ->addSelect('contact_id')
-      ->addSelect('receive_date')
-      ->addSelect('AVG(total_amount) AS average')
-      ->addSelect('SUM(total_amount)')
-      ->addSelect('MAX(total_amount)')
-      ->addSelect('MIN(total_amount)')
-      ->addSelect('COUNT(*) AS count')
-      ->addOrderBy('receive_date')
-      ->addHaving('contact_id', '=', $cid)
-      ->addHaving('receive_date', '<', '2020-04-01')
-      ->execute();
-    $this->assertCount(2, $result);
-    $this->assertEquals(150, $result[0]['average']);
-    $this->assertEquals(300, $result[1]['average']);
-    $this->assertEquals(300, $result[0]['SUM:total_amount']);
-    $this->assertEquals(300, $result[1]['SUM:total_amount']);
-    $this->assertEquals(200, $result[0]['MAX:total_amount']);
-    $this->assertEquals(100, $result[0]['MIN:total_amount']);
-    $this->assertEquals(2, $result[0]['count']);
-    $this->assertEquals(1, $result[1]['count']);
-  }
-
-}
diff --git a/tests/phpunit/api/v4/Query/SqlExpressionParserTest.php b/tests/phpunit/api/v4/Query/SqlExpressionParserTest.php
deleted file mode 100644 (file)
index 4be10ce..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-<?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       |
- +--------------------------------------------------------------------+
- */
-
-/**
- *
- * @package CRM
- * @copyright CiviCRM LLC https://civicrm.org/licensing
- * $Id$
- *
- */
-
-
-namespace api\v4\Query;
-
-use api\v4\UnitTestCase;
-use Civi\Api4\Query\SqlExpression;
-
-/**
- * @group headless
- */
-class SqlExpressionParserTest extends UnitTestCase {
-
-  public function aggregateFunctions() {
-    return [
-      ['AVG'],
-      ['COUNT'],
-      ['MAX'],
-      ['MIN'],
-      ['SUM'],
-    ];
-  }
-
-  /**
-   * @param string|\Civi\Api4\Query\SqlFunction $fnName
-   * @dataProvider aggregateFunctions
-   */
-  public function testAggregateFuncitons($fnName) {
-    $className = 'Civi\Api4\Query\SqlFunction' . $fnName;
-    $params = $className::getParams();
-    $this->assertNotEmpty($params[0]['prefix']);
-    $this->assertEmpty($params[0]['suffix']);
-
-    $sqlFn = new $className($fnName . '(total)');
-    $this->assertEquals($fnName, $sqlFn->getName());
-    $this->assertEquals(['total'], $sqlFn->getFields());
-    $this->assertCount(1, $this->getArgs($sqlFn));
-
-    $sqlFn = SqlExpression::convert($fnName . '(DISTINCT stuff)');
-    $this->assertEquals($fnName, $sqlFn->getName());
-    $this->assertEquals("Civi\Api4\Query\SqlFunction$fnName", get_class($sqlFn));
-    $this->assertEquals($params, $sqlFn->getParams());
-    $this->assertEquals(['stuff'], $sqlFn->getFields());
-    $this->assertCount(2, $this->getArgs($sqlFn));
-
-    try {
-      $sqlFn = SqlExpression::convert($fnName . '(*)');
-      if ($fnName === 'COUNT') {
-        $this->assertTrue(is_a($this->getArgs($sqlFn)[0], 'Civi\Api4\Query\SqlWild'));
-      }
-      else {
-        $this->fail('SqlWild should only be allowed in COUNT.');
-      }
-    }
-    catch (\API_Exception $e) {
-      $this->assertContains('Illegal', $e->getMessage());
-    }
-  }
-
-  /**
-   * @param \Civi\Api4\Query\SqlFunction $fn
-   * @return array
-   * @throws \ReflectionException
-   */
-  private function getArgs($fn) {
-    $ref = new \ReflectionClass($fn);
-    $args = $ref->getProperty('args');
-    $args->setAccessible(TRUE);
-    return $args->getValue($fn);
-  }
-
-}
index ebe220b5849254a94e245b966d848c35e8987863..977e737a6061eb4431158c1fcf76a1f121a6803a 100644 (file)
@@ -25,8 +25,6 @@ use api\v4\Traits\TestDataLoaderTrait;
 use Civi\Test\HeadlessInterface;
 use Civi\Test\TransactionalInterface;
 
-require_once 'api/Exception.php';
-
 /**
  * @group headless
  */