From 7ce7b1cdc4bda5f519ec6d321b7e57fe1150934f Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Wed, 23 Sep 2020 14:35:40 -0400 Subject: [PATCH] APIv4 Search: Improve GROUP_CONCAT with :label prefix This moves handing of pseudoconstant suffix replacement from the UI level to the API level, which allows APIv4 to return GROUP_CONCAT results as an array (by default, if no separator specified). It also improves post-query formatting in general, with finer-grained formating callbacks for sql functions. --- Civi/Api4/Query/Api4SelectQuery.php | 2 +- Civi/Api4/Query/SqlExpression.php | 8 ++ Civi/Api4/Query/SqlField.php | 2 + Civi/Api4/Query/SqlFunction.php | 92 ++++++++++++++----- Civi/Api4/Query/SqlFunctionCOUNT.php | 11 +++ Civi/Api4/Query/SqlFunctionGREATEST.php | 2 + Civi/Api4/Query/SqlFunctionGROUP_CONCAT.php | 21 +++++ Civi/Api4/Query/SqlFunctionLEAST.php | 2 + Civi/Api4/Query/SqlFunctionMAX.php | 2 + Civi/Api4/Query/SqlFunctionMIN.php | 2 + Civi/Api4/Query/SqlFunctionNULLIF.php | 2 + Civi/Api4/Utils/FormattingUtil.php | 57 +++++++----- ext/search/ang/search/crmSearch.component.js | 26 ++---- .../ang/search/crmSearchFunction.component.js | 2 +- .../phpunit/api/v4/Action/SqlFunctionTest.php | 30 ++++-- .../api/v4/Query/SqlExpressionParserTest.php | 30 +++--- 16 files changed, 202 insertions(+), 89 deletions(-) diff --git a/Civi/Api4/Query/Api4SelectQuery.php b/Civi/Api4/Query/Api4SelectQuery.php index 009095ebc9..a1654769af 100644 --- a/Civi/Api4/Query/Api4SelectQuery.php +++ b/Civi/Api4/Query/Api4SelectQuery.php @@ -138,7 +138,7 @@ class Api4SelectQuery { } $results[] = $result; } - FormattingUtil::formatOutputValues($results, $this->apiFieldSpec, $this->getEntity()); + FormattingUtil::formatOutputValues($results, $this->apiFieldSpec, $this->getEntity(), 'get', $this->selectAliases); return $results; } diff --git a/Civi/Api4/Query/SqlExpression.php b/Civi/Api4/Query/SqlExpression.php index 2759d3baff..bce3d47144 100644 --- a/Civi/Api4/Query/SqlExpression.php +++ b/Civi/Api4/Query/SqlExpression.php @@ -37,6 +37,14 @@ abstract class SqlExpression { */ public $expr = ''; + /** + * Whether or not pseudoconstant suffixes should be evaluated during output. + * + * @var bool + * @see \Civi\Api4\Utils\FormattingUtil::formatOutputValues + */ + public $supportsExpansion = FALSE; + /** * SqlFunction constructor. * @param string $expr diff --git a/Civi/Api4/Query/SqlField.php b/Civi/Api4/Query/SqlField.php index 67f1c206a6..b6f69f4d5f 100644 --- a/Civi/Api4/Query/SqlField.php +++ b/Civi/Api4/Query/SqlField.php @@ -16,6 +16,8 @@ namespace Civi\Api4\Query; */ class SqlField extends SqlExpression { + public $supportsExpansion = TRUE; + protected function initialize() { if ($this->alias && $this->alias !== $this->expr) { throw new \API_Exception("Aliasing field names is not allowed, only expressions can have an alias."); diff --git a/Civi/Api4/Query/SqlFunction.php b/Civi/Api4/Query/SqlFunction.php index dbc245f7bf..ef5d7747f2 100644 --- a/Civi/Api4/Query/SqlFunction.php +++ b/Civi/Api4/Query/SqlFunction.php @@ -18,8 +18,14 @@ namespace Civi\Api4\Query; */ abstract class SqlFunction extends SqlExpression { + /** + * @var array + */ protected static $params = []; + /** + * @var array[] + */ protected $args = []; /** @@ -40,17 +46,22 @@ abstract class SqlFunction extends SqlExpression { */ protected function initialize() { $arg = trim(substr($this->expr, strpos($this->expr, '(') + 1, -1)); - foreach ($this->getParams() as $param) { + foreach ($this->getParams() as $idx => $param) { $prefix = $this->captureKeyword($param['prefix'], $arg); + $this->args[$idx] = [ + 'prefix' => $prefix, + 'expr' => [], + 'suffix' => NULL, + ]; 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); + $this->args[$idx]['expr'] = $this->captureExpressions($arg, $param['expr'], $param['must_be'], $param['cant_be']); + $this->args[$idx]['suffix'] = $this->captureKeyword($param['suffix'], $arg); } } } /** - * Shift a keyword off the beginning of the argument string and into the argument array. + * Shift a keyword off the beginning of the argument string and return it. * * @param array $keywords * Whitelist of keywords @@ -60,7 +71,6 @@ abstract class SqlFunction extends SqlExpression { 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; } @@ -69,35 +79,34 @@ abstract class SqlFunction extends SqlExpression { } /** - * Shifts 0 or more expressions off the argument string and into the argument array + * Shifts 0 or more expressions off the argument string and returns them * * @param string $arg * @param int $limit * @param array $mustBe * @param array $cantBe + * @return array * @throws \API_Exception */ private function captureExpressions(&$arg, $limit, $mustBe, $cantBe) { - $captured = 0; + $captured = []; $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[] = $expr; $captured++; // Keep going if we have a comma indicating another expression follows - if ($captured < $limit && substr($arg, 0, 1) === ',') { + if (count($captured) < $limit && substr($arg, 0, 1) === ',') { $arg = ltrim(substr($arg, 1)); } else { - return; + break; } } + return $captured; } /** @@ -147,20 +156,50 @@ abstract class SqlFunction extends SqlExpression { return $item; } + /** + * Render the expression for insertion into the sql query + * + * @param array $fieldList + * @return string + */ public function render(array $fieldList): string { - $output = $this->getName() . '('; + $output = ''; + $params = $this->getParams(); foreach ($this->args as $index => $arg) { - if ($index && $arg !== ',') { - $output .= ' '; - } - if (is_object($arg)) { - $output .= $arg->render($fieldList); + $rendered = $this->renderArg($arg, $params[$index], $fieldList); + if (strlen($rendered)) { + $output .= (strlen($output) ? ' ' : '') . $rendered; } - else { - $output .= $arg; + } + return $this->getName() . '(' . $output . ')'; + } + + /** + * @param array $arg + * @param array $param + * @param array $fieldList + * @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'] ?? ''; + 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']; } - return $output . ')'; + return $rendered; } /** @@ -194,11 +233,20 @@ abstract class SqlFunction extends SqlExpression { 'optional' => FALSE, 'must_be' => [], 'cant_be' => ['SqlWild'], + 'api_default' => NULL, ]; } return $params; } + /** + * Get the arguments passed to this sql function instance. + * @return array[] + */ + public function getArgs(): array { + return $this->args; + } + /** * @return string */ diff --git a/Civi/Api4/Query/SqlFunctionCOUNT.php b/Civi/Api4/Query/SqlFunctionCOUNT.php index f149108a8d..2ab3d66163 100644 --- a/Civi/Api4/Query/SqlFunctionCOUNT.php +++ b/Civi/Api4/Query/SqlFunctionCOUNT.php @@ -27,6 +27,17 @@ class SqlFunctionCOUNT extends SqlFunction { ], ]; + /** + * Reformat result as array if using default separator + * + * @see \Civi\Api4\Utils\FormattingUtil::formatOutputValues + * @param string $value + * @return string|array + */ + public function formatOutputValue($value) { + return (int) $value; + } + /** * @return string */ diff --git a/Civi/Api4/Query/SqlFunctionGREATEST.php b/Civi/Api4/Query/SqlFunctionGREATEST.php index 755ab28151..5ce1b496a7 100644 --- a/Civi/Api4/Query/SqlFunctionGREATEST.php +++ b/Civi/Api4/Query/SqlFunctionGREATEST.php @@ -16,6 +16,8 @@ namespace Civi\Api4\Query; */ class SqlFunctionGREATEST extends SqlFunction { + public $supportsExpansion = TRUE; + protected static $category = self::CATEGORY_COMPARISON; protected static $params = [ diff --git a/Civi/Api4/Query/SqlFunctionGROUP_CONCAT.php b/Civi/Api4/Query/SqlFunctionGROUP_CONCAT.php index 869bd077bf..b683f15a72 100644 --- a/Civi/Api4/Query/SqlFunctionGROUP_CONCAT.php +++ b/Civi/Api4/Query/SqlFunctionGROUP_CONCAT.php @@ -16,6 +16,8 @@ namespace Civi\Api4\Query; */ class SqlFunctionGROUP_CONCAT extends SqlFunction { + public $supportsExpansion = TRUE; + protected static $category = self::CATEGORY_AGGREGATE; protected static $params = [ @@ -37,9 +39,28 @@ class SqlFunctionGROUP_CONCAT extends SqlFunction { 'expr' => 1, 'must_be' => ['SqlString'], 'optional' => TRUE, + // @see self::formatOutput() + 'api_default' => [ + 'expr' => ['"' . \CRM_Core_DAO::VALUE_SEPARATOR . '"'], + ], ], ]; + /** + * Reformat result as array if using default separator + * + * @see \Civi\Api4\Utils\FormattingUtil::formatOutputValues + * @param string $value + * @return string|array + */ + public function formatOutputValue($value) { + $exprArgs = $this->getArgs(); + if (!$exprArgs[2]['prefix']) { + $value = explode(\CRM_Core_DAO::VALUE_SEPARATOR, $value); + } + return $value; + } + /** * @return string */ diff --git a/Civi/Api4/Query/SqlFunctionLEAST.php b/Civi/Api4/Query/SqlFunctionLEAST.php index ea246a820d..b0f315a988 100644 --- a/Civi/Api4/Query/SqlFunctionLEAST.php +++ b/Civi/Api4/Query/SqlFunctionLEAST.php @@ -16,6 +16,8 @@ namespace Civi\Api4\Query; */ class SqlFunctionLEAST extends SqlFunction { + public $supportsExpansion = TRUE; + protected static $category = self::CATEGORY_COMPARISON; protected static $params = [ diff --git a/Civi/Api4/Query/SqlFunctionMAX.php b/Civi/Api4/Query/SqlFunctionMAX.php index 2116ec19f3..c783d2cab7 100644 --- a/Civi/Api4/Query/SqlFunctionMAX.php +++ b/Civi/Api4/Query/SqlFunctionMAX.php @@ -16,6 +16,8 @@ namespace Civi\Api4\Query; */ class SqlFunctionMAX extends SqlFunction { + public $supportsExpansion = TRUE; + protected static $category = self::CATEGORY_AGGREGATE; protected static $params = [ diff --git a/Civi/Api4/Query/SqlFunctionMIN.php b/Civi/Api4/Query/SqlFunctionMIN.php index e8d4c56ebb..f5fe4e86bd 100644 --- a/Civi/Api4/Query/SqlFunctionMIN.php +++ b/Civi/Api4/Query/SqlFunctionMIN.php @@ -16,6 +16,8 @@ namespace Civi\Api4\Query; */ class SqlFunctionMIN extends SqlFunction { + public $supportsExpansion = TRUE; + protected static $category = self::CATEGORY_AGGREGATE; protected static $params = [ diff --git a/Civi/Api4/Query/SqlFunctionNULLIF.php b/Civi/Api4/Query/SqlFunctionNULLIF.php index 846981c736..53ec601bcc 100644 --- a/Civi/Api4/Query/SqlFunctionNULLIF.php +++ b/Civi/Api4/Query/SqlFunctionNULLIF.php @@ -16,6 +16,8 @@ namespace Civi\Api4\Query; */ class SqlFunctionNULLIF extends SqlFunction { + public $supportsExpansion = TRUE; + protected static $category = self::CATEGORY_COMPARISON; protected static $params = [ diff --git a/Civi/Api4/Utils/FormattingUtil.php b/Civi/Api4/Utils/FormattingUtil.php index f170969a7f..82e1f77005 100644 --- a/Civi/Api4/Utils/FormattingUtil.php +++ b/Civi/Api4/Utils/FormattingUtil.php @@ -19,6 +19,8 @@ namespace Civi\Api4\Utils; +use Civi\Api4\Query\SqlExpression; + require_once 'api/v3/utils.php'; class FormattingUtil { @@ -134,38 +136,47 @@ class FormattingUtil { * @param array $fields * @param string $entity * @param string $action + * @param array $selectAliases * @throws \API_Exception * @throws \CRM_Core_Exception */ - public static function formatOutputValues(&$results, $fields, $entity, $action = 'get') { + public static function formatOutputValues(&$results, $fields, $entity, $action = 'get', $selectAliases = []) { $fieldOptions = []; foreach ($results as &$result) { $contactTypePaths = []; - foreach ($result as $fieldExpr => $value) { - $field = $fields[$fieldExpr] ?? NULL; - $dataType = $field['data_type'] ?? ($fieldExpr == 'id' ? 'Integer' : NULL); - if ($field) { - // Evaluate pseudoconstant suffixes - $suffix = strrpos($fieldExpr, ':'); - if ($suffix) { - $fieldOptions[$fieldExpr] = $fieldOptions[$fieldExpr] ?? self::getPseudoconstantList($field, substr($fieldExpr, $suffix + 1), $result, $action); - $dataType = NULL; - } - if (!empty($field['serialize'])) { - if (is_string($value)) { - $value = \CRM_Core_DAO::unSerializeField($value, $field['serialize']); - } - } - if (isset($fieldOptions[$fieldExpr])) { - $value = self::replacePseudoconstant($fieldOptions[$fieldExpr], $value); + foreach ($result as $key => $value) { + $fieldExpr = SqlExpression::convert($selectAliases[$key] ?? $key); + $fieldName = \CRM_Utils_Array::first($fieldExpr->getFields()); + $field = $fieldName && isset($fields[$fieldName]) ? $fields[$fieldName] : NULL; + $dataType = $field['data_type'] ?? ($fieldName == 'id' ? 'Integer' : NULL); + // If Sql Function e.g. GROUP_CONCAT or COUNT wants to do its own formatting, apply and skip dataType conversion + if (method_exists($fieldExpr, 'formatOutputValue') && is_string($value)) { + $result[$key] = $value = $fieldExpr->formatOutputValue($value); + $dataType = NULL; + } + if (!$field) { + continue; + } + // Evaluate pseudoconstant suffixes + $suffix = strrpos($fieldName, ':'); + if ($suffix) { + $fieldOptions[$fieldName] = $fieldOptions[$fieldName] ?? self::getPseudoconstantList($field, substr($fieldName, $suffix + 1), $result, $action); + $dataType = NULL; + } + if ($fieldExpr->supportsExpansion) { + if (!empty($field['serialize']) && is_string($value)) { + $value = \CRM_Core_DAO::unSerializeField($value, $field['serialize']); } - // Keep track of contact types for self::contactFieldsToRemove - if ($value && isset($field['entity']) && $field['entity'] === 'Contact' && $field['name'] === 'contact_type') { - $prefix = strrpos($fieldExpr, '.'); - $contactTypePaths[$prefix ? substr($fieldExpr, 0, $prefix + 1) : ''] = $value; + if (isset($fieldOptions[$fieldName])) { + $value = self::replacePseudoconstant($fieldOptions[$fieldName], $value); } } - $result[$fieldExpr] = self::convertDataType($value, $dataType); + // Keep track of contact types for self::contactFieldsToRemove + if ($value && isset($field['entity']) && $field['entity'] === 'Contact' && $field['name'] === 'contact_type') { + $prefix = strrpos($fieldName, '.'); + $contactTypePaths[$prefix ? substr($fieldName, 0, $prefix + 1) : ''] = $value; + } + $result[$key] = self::convertDataType($value, $dataType); } // Remove inapplicable contact fields foreach ($contactTypePaths as $prefix => $contactType) { diff --git a/ext/search/ang/search/crmSearch.component.js b/ext/search/ang/search/crmSearch.component.js index 9c7b7854c6..88ba4c7b11 100644 --- a/ext/search/ang/search/crmSearch.component.js +++ b/ext/search/ang/search/crmSearch.component.js @@ -356,11 +356,7 @@ var info = searchMeta.parseExpr(col), key = info.fn ? (info.fn.name + ':' + info.path + info.suffix) : col, value = row[key]; - // Handle grouped results - if (info.fn && info.fn.name === 'GROUP_CONCAT' && value) { - return formatGroupConcatValues(info, value); - } - else if (info.fn && info.fn.name === 'COUNT') { + if (info.fn && info.fn.name === 'COUNT') { return value; } return formatFieldValue(info.field, value); @@ -368,31 +364,23 @@ function formatFieldValue(field, value) { var type = field.data_type; + if (_.isArray(value)) { + return _.map(value, function(val) { + return formatFieldValue(field, val); + }).join(', '); + } if (value && (type === 'Date' || type === 'Timestamp') && /^\d{4}-\d{2}-\d{2}/.test(value)) { return CRM.utils.formatDate(value, null, type === 'Timestamp'); } else if (type === 'Boolean' && typeof value === 'boolean') { return value ? ts('Yes') : ts('No'); } - else if (type === 'Money') { + else if (type === 'Money' && typeof value === 'number') { return CRM.formatMoney(value); } - if (_.isArray(value)) { - return value.join(', '); - } return value; } - function formatGroupConcatValues(info, values) { - return _.transform(values.split(','), function(result, val) { - if (info.field.options && !info.suffix) { - result.push(_.result(getOption(info.field, val), 'label')); - } else { - result.push(formatFieldValue(info.field, val)); - } - }).join(', '); - } - function getOption(field, value) { return _.find(field.options, function(option) { // Type coersion is intentional diff --git a/ext/search/ang/search/crmSearchFunction.component.js b/ext/search/ang/search/crmSearchFunction.component.js index e75bac9571..7a46aff431 100644 --- a/ext/search/ang/search/crmSearchFunction.component.js +++ b/ext/search/ang/search/crmSearchFunction.component.js @@ -14,7 +14,7 @@ this.$onInit = function() { var fieldInfo = searchMeta.parseExpr(ctrl.expr); - ctrl.path = fieldInfo.path; + ctrl.path = fieldInfo.path + fieldInfo.suffix; ctrl.field = fieldInfo.field; ctrl.fn = !fieldInfo.fn ? '' : fieldInfo.fn.name; }; diff --git a/tests/phpunit/api/v4/Action/SqlFunctionTest.php b/tests/phpunit/api/v4/Action/SqlFunctionTest.php index af91acffaf..0ff7f2cec1 100644 --- a/tests/phpunit/api/v4/Action/SqlFunctionTest.php +++ b/tests/phpunit/api/v4/Action/SqlFunctionTest.php @@ -39,14 +39,16 @@ class SqlFunctionTest extends UnitTestCase { public function testGroupAggregates() { $cid = Contact::create(FALSE)->addValue('first_name', 'bill')->execute()->first()['id']; Contribution::save(FALSE) - ->setDefaults(['contact_id' => $cid, 'financial_type_id' => 1]) + ->setDefaults(['contact_id' => $cid, 'financial_type_id:name' => 'Donation']) ->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'], + ['total_amount' => 300, 'receive_date' => '2020-01-01', 'financial_type_id:name' => 'Member Dues'], + ['total_amount' => 400, 'receive_date' => '2020-01-01', 'financial_type_id:name' => 'Event Fee'], ]) ->execute(); + + // Test AVG, SUM, MAX, MIN, COUNT $agg = Contribution::get(FALSE) ->addGroupBy('contact_id') ->addWhere('contact_id', '=', $cid) @@ -57,11 +59,23 @@ class SqlFunctionTest extends UnitTestCase { ->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']); + $this->assertTrue(250.0 === $agg['average']); + $this->assertTrue(1000.0 === $agg['SUM:total_amount']); + $this->assertTrue(400.0 === $agg['MAX:total_amount']); + $this->assertTrue(100.0 === $agg['MIN:total_amount']); + $this->assertTrue(4 === $agg['count']); + + // Test GROUP_CONCAT + $agg = Contribution::get(FALSE) + ->addGroupBy('contact_id') + ->addWhere('contact_id', '=', $cid) + ->addSelect('GROUP_CONCAT(financial_type_id:name)') + ->addSelect('COUNT(*) AS count') + ->execute() + ->first(); + + $this->assertTrue(4 === $agg['count']); + $this->assertContains('Donation', $agg['GROUP_CONCAT:financial_type_id:name']); } public function testGroupHaving() { diff --git a/tests/phpunit/api/v4/Query/SqlExpressionParserTest.php b/tests/phpunit/api/v4/Query/SqlExpressionParserTest.php index 2f18648837..9532529250 100644 --- a/tests/phpunit/api/v4/Query/SqlExpressionParserTest.php +++ b/tests/phpunit/api/v4/Query/SqlExpressionParserTest.php @@ -50,19 +50,31 @@ class SqlExpressionParserTest extends UnitTestCase { $sqlFn = new $className($fnName . '(total)'); $this->assertEquals($fnName, $sqlFn->getName()); $this->assertEquals(['total'], $sqlFn->getFields()); - $this->assertCount(1, $this->getArgs($sqlFn)); + $args = $sqlFn->getArgs(); + $this->assertCount(1, $args); + $this->assertNull($args[0]['prefix']); + $this->assertNull($args[0]['suffix']); + $this->assertTrue(is_a($args[0]['expr'][0], 'Civi\Api4\Query\SqlField')); $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)); + $args = $sqlFn->getArgs(); + $this->assertCount(1, $args); + $this->assertEquals('DISTINCT', $args[0]['prefix']); + $this->assertNull($args[0]['suffix']); + $this->assertTrue(is_a($args[0]['expr'][0], 'Civi\Api4\Query\SqlField')); try { $sqlFn = SqlExpression::convert($fnName . '(*)'); if ($fnName === 'COUNT') { - $this->assertTrue(is_a($this->getArgs($sqlFn)[0], 'Civi\Api4\Query\SqlWild')); + $args = $sqlFn->getArgs(); + $this->assertCount(1, $args); + $this->assertNull($args[0]['prefix']); + $this->assertNull($args[0]['suffix']); + $this->assertTrue(is_a($args[0]['expr'][0], 'Civi\Api4\Query\SqlWild')); } else { $this->fail('SqlWild should only be allowed in COUNT.'); @@ -73,16 +85,4 @@ class SqlExpressionParserTest extends UnitTestCase { } } - /** - * @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); - } - } -- 2.25.1