$permitted = ['SqlField', 'SqlString', 'SqlNumber', 'SqlNull'];
$operators = array_merge(self::$arithmeticOperators, self::$comparisonOperators);
while (strlen($arg)) {
- $this->args = array_merge($this->args, $this->captureExpressions($arg, $permitted, FALSE));
+ $this->args = array_merge($this->args, $this->captureExpressions($arg, $permitted, 1));
$op = $this->captureKeyword($operators, $arg);
if ($op) {
$this->args[] = $op;
*/
protected function captureKeyword($keywords, &$arg) {
foreach ($keywords as $key) {
- if (strpos($arg, $key . ' ') === 0) {
+ // Match keyword followed by a space or eol
+ if (strpos($arg, $key . ' ') === 0 || rtrim($arg) === $key) {
$arg = ltrim(substr($arg, strlen($key)));
return $key;
}
*
* @param string $arg
* @param array $mustBe
- * @param bool $multi
+ * @param int $max
* @return SqlExpression[]
* @throws \API_Exception
*/
- protected function captureExpressions(string &$arg, array $mustBe, bool $multi) {
+ protected function captureExpressions(string &$arg, array $mustBe, int $max) {
$captured = [];
$arg = ltrim($arg);
while ($arg) {
$this->fields = array_merge($this->fields, $expr->getFields());
$captured[] = $expr;
// Keep going if we have a comma indicating another expression follows
- if ($multi && substr($arg, 0, 1) === ',') {
+ if (count($captured) < $max && substr($arg, 0, 1) === ',') {
$arg = ltrim(substr($arg, 1));
}
else {
$arg = trim(substr($this->expr, strpos($this->expr, '(') + 1, -1));
foreach ($this->getParams() as $idx => $param) {
$prefix = NULL;
- if ($param['name']) {
- $prefix = $this->captureKeyword([$param['name']], $arg);
+ $name = $param['name'] ?: ($idx + 1);
+ // If this isn't the first param it needs to start with something;
+ // either the name (e.g. "ORDER BY") if it has one, or a comma separating it from the previous param.
+ $start = $param['name'] ?: ($idx ? ',' : NULL);
+ if ($start) {
+ $prefix = $this->captureKeyword([$start], $arg);
// Supply api_default
if (!$prefix && isset($param['api_default'])) {
$this->args[$idx] = [
- 'prefix' => [$param['name']],
+ 'prefix' => [$start],
'expr' => array_map([parent::class, 'convert'], $param['api_default']['expr']),
'suffix' => [],
];
continue;
}
if (!$prefix && !$param['optional']) {
- throw new \API_Exception("Missing {$param['name']} for SQL function " . static::getName());
+ throw new \API_Exception("Missing param $name for SQL function " . static::getName());
}
}
elseif ($param['flag_before']) {
'suffix' => [],
];
if ($param['max_expr'] && (!$param['name'] || $param['name'] === $prefix)) {
- $exprs = $this->captureExpressions($arg, $param['must_be'], TRUE);
+ $exprs = $this->captureExpressions($arg, $param['must_be'], $param['max_expr']);
if (
- (count($exprs) < $param['min_expr'] || count($exprs) > $param['max_expr']) &&
+ count($exprs) < $param['min_expr'] &&
!(!$exprs && $param['optional'])
) {
- throw new \API_Exception('Incorrect number of arguments for SQL function ' . static::getName());
+ throw new \API_Exception("Too few arguments to param $name for SQL function " . static::getName());
}
$this->args[$idx]['expr'] = $exprs;
$this->args[$idx]['suffix'] = (array) $this->captureKeyword(array_keys($param['flag_after']), $arg);
}
}
+ if (trim($arg)) {
+ throw new \API_Exception("Too many arguments given for SQL function " . static::getName());
+ }
}
/**
// Merge in defaults to ensure each param has these properties
$params[] = $param + [
'name' => NULL,
+ 'label' => ts('Select'),
'min_expr' => 1,
'max_expr' => 1,
'flag_before' => [],
[
'max_expr' => 99,
'optional' => FALSE,
- 'ui_defaults' => [
- ['type' => 'SqlField', 'placeholder' => ts('If')],
- ['type' => 'SqlField', 'placeholder' => ts('Else')],
- ],
+ 'label' => ts('Value?'),
],
];
}
'max_expr' => 99,
'optional' => FALSE,
'must_be' => ['SqlField', 'SqlString'],
- 'ui_defaults' => [
- ['placeholder' => ts('Plus')],
- ],
+ 'label' => ts('And'),
],
];
}
protected static function params(): array {
return [
+ [
+ 'optional' => FALSE,
+ 'must_be' => ['SqlString'],
+ 'label' => ts('Separator'),
+ ],
[
'max_expr' => 99,
'optional' => FALSE,
'must_be' => ['SqlField', 'SqlString'],
- 'ui_defaults' => [
- ['type' => 'SqlString', 'placeholder' => ts('Separator')],
- ['type' => 'SqlField', 'placeholder' => ts('Plus')],
- ],
+ 'label' => ts('Plus'),
],
];
}
'max_expr' => 99,
'min_expr' => 2,
'optional' => FALSE,
- 'ui_defaults' => [
- ['type' => 'SqlField', 'placeholder' => ts('If')],
- ['type' => 'SqlField', 'placeholder' => ts('Else')],
- ],
+ 'label' => ts('Else'),
],
];
}
],
[
'name' => 'ORDER BY',
+ 'label' => ts('Order by'),
'max_expr' => 1,
'flag_after' => ['ASC' => ts('Ascending'), 'DESC' => ts('Descending')],
'must_be' => ['SqlField'],
protected static function params(): array {
return [
[
- 'min_expr' => 3,
- 'max_expr' => 3,
'optional' => FALSE,
- 'must_be' => ['SqlEquation', 'SqlField', 'SqlFunction', 'SqlString', 'SqlNumber', 'SqlNull'],
- 'ui_defaults' => [
- ['type' => 'SqlField', 'placeholder' => ts('If')],
- ['type' => 'SqlField', 'placeholder' => ts('Then')],
- ['type' => 'SqlField', 'placeholder' => ts('Else')],
- ],
+ 'must_be' => ['SqlEquation', 'SqlField'],
+ 'label' => ts('If'),
+ ],
+ [
+ 'optional' => FALSE,
+ 'must_be' => ['SqlField', 'SqlString', 'SqlNumber', 'SqlNull'],
+ 'label' => ts('Then'),
+ ],
+ [
+ 'optional' => FALSE,
+ 'must_be' => ['SqlField', 'SqlString', 'SqlNumber', 'SqlNull'],
+ 'label' => ts('Else'),
],
];
}
'max_expr' => 99,
'min_expr' => 2,
'optional' => FALSE,
- 'ui_defaults' => [
- ['type' => 'SqlField', 'placeholder' => ts('If')],
- ['type' => 'SqlField', 'placeholder' => ts('Else')],
- ],
+ 'label' => ts('Else'),
],
];
}
'min_expr' => 2,
'max_expr' => 2,
'optional' => FALSE,
- 'ui_defaults' => [
- ['type' => 'SqlField', 'placeholder' => ts('Preferred')],
- ['type' => 'SqlField', 'placeholder' => ts('Alternate')],
- ],
+ 'label' => ts('Compare with'),
],
];
}
protected static function params(): array {
return [
[
- 'min_expr' => 3,
- 'max_expr' => 3,
+ 'optional' => FALSE,
+ 'must_be' => ['SqlField', 'SqlString'],
+ 'label' => ts('Source'),
+ ],
+ [
+ 'optional' => FALSE,
+ 'must_be' => ['SqlString', 'SqlField'],
+ 'label' => ts('Find'),
+ ],
+ [
'optional' => FALSE,
'must_be' => ['SqlString', 'SqlField'],
- 'ui_defaults' => [
- ['type' => 'SqlField', 'placeholder' => ts('Source')],
- ['type' => 'SqlString', 'placeholder' => ts('Find')],
- ['type' => 'SqlString', 'placeholder' => ts('Replace')],
- ],
+ 'label' => ts('Replace'),
],
];
}
return [
[
'optional' => FALSE,
- 'min_expr' => 1,
- 'max_expr' => 2,
- 'must_be' => ['SqlNumber', 'SqlField'],
- 'ui_defaults' => [
- ['type' => 'SqlField', 'placeholder' => ts('Number')],
- ['type' => 'SqlNumber', 'placeholder' => ts('Decimals')],
- ],
+ 'must_be' => ['SqlField', 'SqlNumber'],
+ 'label' => ts('Number'),
+ ],
+ [
+ 'optional' => TRUE,
+ 'must_be' => ['SqlNumber'],
+ 'label' => ts('Decimal places'),
],
];
}
var ts = $scope.ts = CRM.ts('org.civicrm.search_kit'),
ctrl = this;
- var defaultUiDefaults = {type: 'SqlField', placeholder: ts('Select')};
-
var allTypes = {
aggregate: ts('Aggregate'),
comparison: ts('Comparison'),
};
this.addArg = function(exprType) {
- exprType = exprType || ctrl.getUiDefault(ctrl.args.length).type;
ctrl.args.push({
type: ctrl.exprTypes[exprType].type,
value: exprType === 'SqlNumber' ? 0 : ''
ctrl.modifier = null;
}
// Push args to reach the minimum
- while (ctrl.args.length < ctrl.fn.params[0].min_expr) {
- ctrl.addArg();
- }
+ _.each(ctrl.fn.params, function(param, index) {
+ while (
+ (ctrl.args.length - index < param.min_expr) &&
+ // TODO: Handle named params like "ORDER BY"
+ !param.name &&
+ (!param.optional || param.must_be.length === 1)
+ ) {
+ ctrl.addArg(param.must_be[0]);
+ }
+ });
}
- this.getUiDefault = function(index) {
- if (ctrl.fn.params[0].ui_defaults) {
- return ctrl.fn.params[0].ui_defaults[index] || _.last(ctrl.fn.params[0].ui_defaults);
+ this.getParam = function(index) {
+ return ctrl.fn.params[index] || _.last(ctrl.fn.params);
+ };
+
+ this.canAddArg = function() {
+ if (!ctrl.fn) {
+ return false;
+ }
+ var param = ctrl.getParam(ctrl.args.length),
+ index = ctrl.fn.params.indexOf(param);
+ // TODO: Handle named params like "ORDER BY"
+ if (param.name) {
+ return false;
}
- defaultUiDefaults.type = ctrl.fn.params[0].must_be[0];
- return defaultUiDefaults;
+ return ctrl.args.length - index < param.max_expr;
};
// On-demand options for dropdown function selector
</label>
<div class="form-group" ng-repeat="arg in $ctrl.args" ng-if="arg !== $ctrl.fieldArg">
<span ng-switch="arg.type">
- <input ng-switch-when="number" class="form-control" type="number" ng-model="arg.value" placeholder="{{ $ctrl.getUiDefault($index).placeholder }}" ng-change="$ctrl.changeArg($index)" ng-model-options="{updateOn: 'blur'}">
- <input ng-switch-when="string" class="form-control" ng-model="arg.value" placeholder="{{ $ctrl.getUiDefault($index).placeholder }}" ng-change="$ctrl.changeArg($index)" ng-trim="false" ng-model-options="{updateOn: 'blur'}">
- <input ng-switch-default class="form-control" ng-model="arg.value" crm-ui-select="{data: $ctrl.getFields, placeholder: $ctrl.getUiDefault($index).placeholder}" ng-change="$ctrl.changeArg($index)">
+ <input ng-switch-when="number" class="form-control" type="number" ng-model="arg.value" placeholder="{{ $ctrl.getParam($index).label }}" ng-change="$ctrl.changeArg($index)" ng-model-options="{updateOn: 'blur'}">
+ <input ng-switch-when="string" class="form-control" ng-model="arg.value" placeholder="{{ $ctrl.getParam($index).label }}" ng-change="$ctrl.changeArg($index)" ng-trim="false" ng-model-options="{updateOn: 'blur'}">
+ <input ng-switch-default class="form-control" ng-model="arg.value" crm-ui-select="{data: $ctrl.getFields, placeholder: $ctrl.getParam($index).label}" ng-change="$ctrl.changeArg($index)">
</span>
</div>
- <div class="btn-group" ng-if="$ctrl.args.length < $ctrl.fn.params[0].max_expr">
+ <div class="btn-group" ng-if="$ctrl.canAddArg()">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="crm-i fa-plus"></i> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
- <li ng-repeat="(name, type) in $ctrl.exprTypes" ng-show="$ctrl.fn.params[0].must_be.indexOf(name) >= 0">
+ <li ng-repeat="(name, type) in $ctrl.exprTypes" ng-show="$ctrl.getParam($ctrl.args.length).must_be.indexOf(name) >= 0">
<a href ng-click="$ctrl.addArg(name)">{{ type.label }}</a>
</li>
</ul>
public function testStringFunctions() {
$sampleData = [
- ['first_name' => 'abc', 'middle_name' => 'q', 'last_name' => 'tester1', 'source' => '123'],
+ ['first_name' => 'abc', 'middle_name' => 'Q', 'last_name' => 'tester1', 'source' => '123'],
];
$cid = Contact::save(FALSE)
->setRecords($sampleData)
$result = Contact::get(FALSE)
->addWhere('id', '=', $cid)
->addSelect('CONCAT_WS("|", first_name, middle_name, last_name) AS concat_ws')
+ ->addSelect('REPLACE(first_name, "c", "cdef") AS new_first')
+ ->addSelect('UPPER(first_name)')
+ ->addSelect('LOWER(middle_name)')
->execute()->first();
- $this->assertEquals('abc|q|tester1', $result['concat_ws']);
+ $this->assertEquals('abc|Q|tester1', $result['concat_ws']);
+ $this->assertEquals('abcdef', $result['new_first']);
+ $this->assertEquals('ABC', $result['UPPER:first_name']);
+ $this->assertEquals('q', $result['LOWER:middle_name']);
}
public function testIncorrectNumberOfArguments() {
$this->fail('Api should have thrown exception');
}
catch (\API_Exception $e) {
- $this->assertEquals('Incorrect number of arguments for SQL function IF', $e->getMessage());
+ $this->assertEquals('Missing param 2 for SQL function IF', $e->getMessage());
}
try {
$this->fail('Api should have thrown exception');
}
catch (\API_Exception $e) {
- $this->assertEquals('Incorrect number of arguments for SQL function NULLIF', $e->getMessage());
+ $this->assertEquals('Too many arguments given for SQL function NULLIF', $e->getMessage());
+ }
+
+ try {
+ Activity::get(FALSE)
+ ->addSelect('CONCAT_WS(",", ) AS whoops')
+ ->execute();
+ $this->fail('Api should have thrown exception');
+ }
+ catch (\API_Exception $e) {
+ $this->assertEquals('Too few arguments to param 2 for SQL function CONCAT_WS', $e->getMessage());
}
}