Skip to content

Commit

Permalink
Merge pull request #20947 from colemanw/sqlFunctions
Browse files Browse the repository at this point in the history
dev/core#2704 SearchKit - Add support for SQL functions
  • Loading branch information
eileenmcnaughton authored Aug 2, 2021
2 parents ae9e7f7 + 54633fb commit 1ec58fe
Show file tree
Hide file tree
Showing 32 changed files with 482 additions and 306 deletions.
1 change: 1 addition & 0 deletions CRM/Api4/Page/Api4Explorer.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public static function getSqlFunctions() {
'title' => $className::getTitle(),
'params' => $className::getParams(),
'category' => $className::getCategory(),
'dataType' => $className::getDataType(),
];
}
}
Expand Down
90 changes: 64 additions & 26 deletions Civi/Api4/Query/SqlFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@
*/
abstract class SqlFunction extends SqlExpression {

/**
* @var array
*/
protected static $params = [];

/**
* @var array[]
*/
Expand All @@ -35,6 +30,13 @@ abstract class SqlFunction extends SqlExpression {
*/
protected static $category;

/**
* Data type output by this function
*
* @var string
*/
protected static $dataType;

const CATEGORY_AGGREGATE = 'aggregate',
CATEGORY_COMPARISON = 'comparison',
CATEGORY_DATE = 'date',
Expand All @@ -47,23 +49,57 @@ abstract class SqlFunction extends SqlExpression {
protected function initialize() {
$arg = trim(substr($this->expr, strpos($this->expr, '(') + 1, -1));
foreach ($this->getParams() as $idx => $param) {
$prefix = $this->captureKeyword($param['prefix'], $arg);
$prefix = NULL;
if ($param['prefix']) {
$prefix = $this->captureKeyword([$param['prefix']], $arg);
// Supply api_default
if (!$prefix && isset($param['api_default'])) {
$this->args[$idx] = [
'prefix' => $param['api_default']['prefix'] ?? [$param['prefix']],
'expr' => array_map([parent::class, 'convert'], $param['api_default']['expr']),
'suffix' => $param['api_default']['suffix'] ?? [],
];
continue;
}
if (!$prefix && !$param['optional']) {
throw new \API_Exception("Missing {$param['prefix']} for SQL function " . static::getName());
}
}
elseif ($param['flag_before']) {
$prefix = $this->captureKeyword(array_keys($param['flag_before']), $arg);
}
$this->args[$idx] = [
'prefix' => $prefix,
'prefix' => (array) $prefix,
'expr' => [],
'suffix' => NULL,
'suffix' => [],
];
if ($param['max_expr'] && isset($prefix) || in_array('', $param['prefix']) || !$param['optional']) {
if ($param['max_expr'] && (!$param['prefix'] || $param['prefix'] === $prefix)) {
$exprs = $this->captureExpressions($arg, $param['must_be'], $param['cant_be']);
if (count($exprs) < $param['min_expr'] || count($exprs) > $param['max_expr']) {
throw new \API_Exception('Incorrect number of arguments for SQL function ' . static::getName());
}
$this->args[$idx]['expr'] = $exprs;
$this->args[$idx]['suffix'] = $this->captureKeyword($param['suffix'], $arg);

$this->args[$idx]['suffix'] = (array) $this->captureKeyword(array_keys($param['flag_after']), $arg);
}
}
}

/**
* Change $dataType according to output of function
*
* @see \Civi\Api4\Utils\FormattingUtil::formatOutputValues
* @param string $value
* @param string $dataType
* @return string
*/
public function formatOutputValue($value, &$dataType) {
if (static::$dataType) {
$dataType = static::$dataType;
}
return $value;
}

/**
* Shift a keyword off the beginning of the argument string and return it.
*
Expand All @@ -73,7 +109,7 @@ protected function initialize() {
* @return mixed|null
*/
private function captureKeyword($keywords, &$arg) {
foreach (array_filter($keywords) as $key) {
foreach ($keywords as $key) {
if (strpos($arg, $key . ' ') === 0) {
$arg = ltrim(substr($arg, strlen($key)));
return $key;
Expand Down Expand Up @@ -183,23 +219,15 @@ public function render(array $fieldList): string {
* @return string
*/
private function renderArg($arg, $param, $fieldList): string {
// Supply api_default
if (!isset($arg['prefix']) && !isset($arg['suffix']) && empty($arg['expr']) && !empty($param['api_default'])) {
$arg = [
'prefix' => $param['api_default']['prefix'] ?? reset($param['prefix']),
'expr' => array_map([parent::class, 'convert'], $param['api_default']['expr'] ?? []),
'suffix' => $param['api_default']['suffix'] ?? reset($param['suffix']),
];
}
$rendered = $arg['prefix'] ?? '';
$rendered = implode(' ', $arg['prefix']);
foreach ($arg['expr'] ?? [] as $idx => $expr) {
if (strlen($rendered) || $idx) {
$rendered .= $idx ? ', ' : ' ';
}
$rendered .= $expr->render($fieldList);
}
if (isset($arg['suffix'])) {
$rendered .= (strlen($rendered) ? ' ' : '') . $arg['suffix'];
if ($arg['suffix']) {
$rendered .= (strlen($rendered) ? ' ' : '') . implode(' ', $arg['suffix']);
}
return $rendered;
}
Expand All @@ -224,15 +252,16 @@ public static function getName(): string {
* Get the param metadata for this sql function.
* @return array
*/
public static function getParams(): array {
final public static function getParams(): array {
$params = [];
foreach (static::$params as $param) {
foreach (static::params() as $param) {
// Merge in defaults to ensure each param has these properties
$params[] = $param + [
'prefix' => [],
'prefix' => NULL,
'min_expr' => 1,
'max_expr' => 1,
'suffix' => [],
'flag_before' => [],
'flag_after' => [],
'optional' => FALSE,
'must_be' => [],
'cant_be' => ['SqlWild'],
Expand All @@ -242,6 +271,8 @@ public static function getParams(): array {
return $params;
}

abstract protected static function params(): array;

/**
* Get the arguments passed to this sql function instance.
* @return array[]
Expand All @@ -257,6 +288,13 @@ public static function getCategory(): string {
return static::$category;
}

/**
* @return string|NULL
*/
public static function getDataType():? string {
return static::$dataType;
}

/**
* @return string
*/
Expand Down
14 changes: 8 additions & 6 deletions Civi/Api4/Query/SqlFunctionABS.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ class SqlFunctionABS extends SqlFunction {

protected static $category = self::CATEGORY_MATH;

protected static $params = [
[
'optional' => FALSE,
'must_be' => ['SqlField', 'SqlNumber'],
],
];
protected static function params(): array {
return [
[
'optional' => FALSE,
'must_be' => ['SqlField', 'SqlNumber'],
],
];
}

/**
* @return string
Expand Down
14 changes: 8 additions & 6 deletions Civi/Api4/Query/SqlFunctionAVG.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ class SqlFunctionAVG extends SqlFunction {

protected static $category = self::CATEGORY_AGGREGATE;

protected static $params = [
[
'prefix' => ['', 'DISTINCT', 'ALL'],
'must_be' => ['SqlField'],
],
];
protected static function params(): array {
return [
[
'flag_before' => ['DISTINCT' => ts('Distinct')],
'must_be' => ['SqlField'],
],
];
}

/**
* @return string
Expand Down
29 changes: 10 additions & 19 deletions Civi/Api4/Query/SqlFunctionCOALESCE.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ class SqlFunctionCOALESCE extends SqlFunction {

protected static $category = self::CATEGORY_COMPARISON;

protected static $params = [
[
'max_expr' => 99,
'optional' => FALSE,
],
];
protected static $dataType = 'String';

protected static function params(): array {
return [
[
'max_expr' => 99,
'optional' => FALSE,
],
];
}

/**
* @return string
Expand All @@ -32,17 +36,4 @@ public static function getTitle(): string {
return ts('Coalesce');
}

/**
* Prevent reformatting
*
* @see \Civi\Api4\Utils\FormattingUtil::formatOutputValues
* @param string $value
* @param string $dataType
* @return string|array
*/
public function formatOutputValue($value, &$dataType) {
$dataType = NULL;
return $value;
}

}
31 changes: 11 additions & 20 deletions Civi/Api4/Query/SqlFunctionCONCAT.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ class SqlFunctionCONCAT extends SqlFunction {

protected static $category = self::CATEGORY_STRING;

protected static $params = [
[
'max_expr' => 99,
'optional' => FALSE,
'must_be' => ['SqlField', 'SqlString'],
],
];
protected static $dataType = 'String';

protected static function params(): array {
return [
[
'max_expr' => 99,
'optional' => FALSE,
'must_be' => ['SqlField', 'SqlString'],
],
];
}

/**
* @return string
Expand All @@ -33,17 +37,4 @@ public static function getTitle(): string {
return ts('Combine');
}

/**
* Prevent reformatting of result
*
* @see \Civi\Api4\Utils\FormattingUtil::formatOutputValues
* @param string $value
* @param string $dataType
* @return string|array
*/
public function formatOutputValue($value, &$dataType) {
$dataType = NULL;
return $value;
}

}
30 changes: 10 additions & 20 deletions Civi/Api4/Query/SqlFunctionCOUNT.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,17 @@ class SqlFunctionCOUNT extends SqlFunction {

protected static $category = self::CATEGORY_AGGREGATE;

protected static $params = [
[
'prefix' => ['', 'DISTINCT', 'ALL'],
'max_expr' => 1,
'must_be' => ['SqlField', 'SqlWild'],
'cant_be' => [],
],
];
protected static $dataType = 'Integer';

/**
* Reformat result as integer
*
* @see \Civi\Api4\Utils\FormattingUtil::formatOutputValues
* @param string $value
* @param string $dataType
* @return string|array
*/
public function formatOutputValue($value, &$dataType) {
// Count is always an integer
$dataType = 'Integer';
return (int) $value;
protected static function params(): array {
return [
[
'flag_before' => ['DISTINCT' => ts('Distinct')],
'max_expr' => 1,
'must_be' => ['SqlField', 'SqlWild'],
'cant_be' => [],
],
];
}

/**
Expand Down
4 changes: 4 additions & 0 deletions Civi/Api4/Query/SqlFunctionCURDATE.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class SqlFunctionCURDATE extends SqlFunction {

protected static $category = self::CATEGORY_DATE;

protected static function params(): array {
return [];
}

/**
* @return string
*/
Expand Down
39 changes: 39 additions & 0 deletions Civi/Api4/Query/SqlFunctionDATE.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?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 SqlFunctionDATE extends SqlFunction {

protected static $category = self::CATEGORY_DATE;

protected static $dataType = 'Date';

protected static function params(): array {
return [
[
'max_expr' => 1,
'optional' => FALSE,
],
];
}

/**
* @return string
*/
public static function getTitle(): string {
return ts('Date Only');
}

}
Loading

0 comments on commit 1ec58fe

Please sign in to comment.