*/
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));
*/
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();
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;
- }
-
}
'name' => 'help_post',
'data_type' => 'String',
];
- $fields[] = [
- 'name' => 'column_name',
- 'data_type' => 'String',
- ];
$fields[] = [
'name' => 'custom_field_id',
'data_type' => 'Integer',
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.
*/
public $debugOutput = NULL;
- /**
- * @var array
- */
- public $groupBy = [];
-
- public $forceSelectId = TRUE;
-
- /**
- * @var array
- */
- public $having = [];
-
/**
* @param \Civi\Api4\Generic\DAOGetAction $apiGet
*/
$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();
}
$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);
$this->buildWhereClause();
$this->buildOrderBy();
$this->buildLimit();
- $this->buildGroupBy();
- $this->buildHavingClause();
return $this->query->toSQL();
}
$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);
}
protected function buildSelectClause() {
- // An empty select is the same as *
if (empty($this->select)) {
$this->select = $this->entityFieldNames;
}
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));
}
$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;
+ }
}
}
}
* @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");
}
}
}
}
- /**
- * 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.
*
*
* @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
$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);
$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) . '.' : '';
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;
}
/**
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;
- }
-
}
+++ /dev/null
-<?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);
- }
-
-}
+++ /dev/null
-<?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'];
- }
-
-}
+++ /dev/null
-<?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;
- }
-
-}
+++ /dev/null
-<?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'],
- ],
- ];
-
-}
+++ /dev/null
-<?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' => [],
- ],
- ];
-
-}
+++ /dev/null
-<?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'],
- ],
- ];
-
-}
+++ /dev/null
-<?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'],
- ],
- ];
-
-}
+++ /dev/null
-<?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'],
- ],
- ];
-
-}
+++ /dev/null
-<?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';
- }
-
-}
+++ /dev/null
-<?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;
- }
-
-}
+++ /dev/null
-<?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) . '"';
- }
-
-}
+++ /dev/null
-<?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 '*';
- }
-
-}
<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>
}
};
- // 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;
}
});
+++ /dev/null
-<?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']);
- }
-
-}
+++ /dev/null
-<?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']);
- }
-
-}
+++ /dev/null
-<?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);
- }
-
-}
use Civi\Test\HeadlessInterface;
use Civi\Test\TransactionalInterface;
-require_once 'api/Exception.php';
-
/**
* @group headless
*/