Unlike DISTINCT which dedupes by the value of a field, UNIQUE will dedupe by the id of the record
$expr = SqlExpression::convert($item, TRUE);
$alias = $expr->getAlias();
$this->selectAliases[$alias] = $expr->getExpr();
- $this->query->select($expr->render($this) . " AS `$alias`");
+ $this->query->select($expr->render($this, TRUE));
}
}
throw new \CRM_Core_Exception('Cannot use existing field name as alias');
}
$this->selectAliases[$alias] = $expr->getExpr();
- $this->query->select($expr->render($this) . " AS `$alias`");
+ $this->query->select($expr->render($this, TRUE));
}
}
}
protected function initialize() {
}
- public function render(Api4Query $query): string {
- return $this->expr === 'TRUE' ? '1' : '0';
+ public function render(Api4Query $query, bool $includeAlias = FALSE): string {
+ return ($this->expr === 'TRUE' ? '1' : '0') . ($includeAlias ? " AS `{$this->getAlias()}`" : '');
}
public static function getTitle(): string {
* Render the expression for insertion into the sql query
*
* @param \Civi\Api4\Query\Api4Query $query
+ * @param bool $includeAlias
* @return string
*/
- public function render(Api4Query $query): string {
+ public function render(Api4Query $query, bool $includeAlias = FALSE): string {
$output = [];
foreach ($this->args as $i => $arg) {
// Just an operator
$output[] = $arg->render($query);
}
}
- return '(' . implode(' ', $output) . ')';
+ return '(' . implode(' ', $output) . ')' . ($includeAlias ? " AS `{$this->getAlias()}`" : '');
}
/**
* Renders expression to a sql string, replacing field names with column names.
*
* @param \Civi\Api4\Query\Api4Query $query
+ * @param bool $includeAlias
* @return string
*/
- abstract public function render(Api4Query $query): string;
+ public function render(Api4Query $query, bool $includeAlias = FALSE): string {
+ return $this->expr . ($includeAlias ? " AS `{$this->getAlias()}`" : '');
+ }
/**
* @return string
$this->fields[] = $this->expr;
}
- public function render(Api4Query $query): string {
+ public function render(Api4Query $query, bool $includeAlias = FALSE): string {
$field = $query->getField($this->expr, TRUE);
+ $rendered = $field['sql_name'];
if (!empty($field['sql_renderer'])) {
- $renderer = $field['sql_renderer'];
- return $renderer($field, $query);
+ $rendered = $field['sql_renderer']($field, $query);
}
- return $field['sql_name'];
+ return $rendered . ($includeAlias ? " AS `{$this->getAlias()}`" : '');
}
public static function getTitle(): string {
* Render the expression for insertion into the sql query
*
* @param \Civi\Api4\Query\Api4Query $query
+ * @param bool $includeAlias
* @return string
*/
- public function render(Api4Query $query): string {
+ public function render(Api4Query $query, bool $includeAlias = FALSE): string {
$output = '';
foreach ($this->args as $arg) {
$rendered = $this->renderArg($arg, $query);
$output .= (strlen($output) ? ' ' : '') . $rendered;
}
}
- return $this->renderExpression($output);
+ return $this->renderExpression($output) . ($includeAlias ? " AS `{$this->getAlias()}`" : '');
}
/**
* @param string $output
* @return string
*/
- protected function renderExpression($output): string {
+ protected function renderExpression(string $output): string {
return $this->getName() . '(' . $output . ')';
}
/**
* @inheritDoc
*/
- protected function renderExpression($output): string {
+ protected function renderExpression(string $output): string {
return "DATEDIFF(
IF(
DATE(CONCAT(YEAR(CURDATE()), '-', MONTH({$output}), '-', DAY({$output}))) < CURDATE(),
namespace Civi\Api4\Query;
+use Civi\Api4\Utils\CoreUtil;
+
/**
* Sql function
*/
protected static function params(): array {
return [
[
- 'flag_before' => ['' => NULL, 'DISTINCT' => ts('Distinct')],
+ 'flag_before' => ['' => NULL, 'DISTINCT' => ts('Distinct Value'), 'UNIQUE' => ts('Unique Record')],
'max_expr' => 1,
'must_be' => ['SqlField', 'SqlFunction', 'SqlEquation'],
'optional' => FALSE,
$exprArgs[0]['expr'][0]->formatOutputValue($dataType, $values[$key], $index);
}
}
+ // Perform deduping by unique id
+ if ($this->args[0]['prefix'] === ['UNIQUE'] && isset($values["_$key"])) {
+ $ids = \CRM_Utils_Array::explodePadded($values["_$key"]);
+ unset($values["_$key"]);
+ foreach ($ids as $index => $id) {
+ if (in_array($id, array_slice($ids, 0, $index))) {
+ unset($values[$key][$index]);
+ }
+ }
+ $values[$key] = array_values($values[$key]);
+ }
}
// If using custom separator, preserve raw string
else {
return ts('All values in the grouping.');
}
+ public function render(Api4Query $query, bool $includeAlias = FALSE): string {
+ $result = '';
+ // Handle pseudo-prefix `UNIQUE` which is like `DISTINCT` but based on the record id rather than the field value
+ if ($this->args[0]['prefix'] === ['UNIQUE']) {
+ $this->args[0]['prefix'] = [];
+ $expr = $this->args[0]['expr'][0];
+ $field = $query->getField($expr->getFields()[0]);
+ if ($field) {
+ $idField = CoreUtil::getIdFieldName($field['entity']);
+ $idFieldKey = substr($expr->getFields()[0], 0, 0 - strlen($field['name'])) . $idField;
+ // Keep the ordering consistent
+ if (empty($this->args[1]['prefix'])) {
+ $this->args[1] = [
+ 'prefix' => ['ORDER BY'],
+ 'expr' => [SqlExpression::convert($idFieldKey)],
+ 'suffix' => [],
+ ];
+ }
+ // Already a unique field, so DISTINCT will work fine
+ if ($field['name'] === $idField) {
+ $this->args[0]['prefix'] = ['DISTINCT'];
+ }
+ // Add a unique field on which to dedupe in postprocessing (@see self::formatOutputValue)
+ elseif ($includeAlias) {
+ $orderByKey = $this->args[1]['expr'][0]->getFields()[0];
+ $extraSelectAlias = '_' . $this->getAlias();
+ $extraSelect = SqlExpression::convert("GROUP_CONCAT($idFieldKey ORDER BY $orderByKey) AS $extraSelectAlias", TRUE);
+ $query->selectAliases[$extraSelectAlias] = $extraSelect->getExpr();
+ $result .= $extraSelect->render($query, TRUE) . ',';
+ }
+ }
+ }
+ $result .= parent::render($query, $includeAlias);
+ return $result;
+ }
+
}
protected function initialize() {
}
- public function render(Api4Query $query): string {
- return 'NULL';
- }
-
public static function getTitle(): string {
return ts('Null');
}
\CRM_Utils_Type::validate($this->expr, 'Float');
}
- public function render(Api4Query $query): string {
- return $this->expr;
- }
-
public static function getTitle(): string {
return ts('Number');
}
$this->expr = str_replace(['\\\\', "\\$quot", $backslash], [$backslash, $quot, '\\\\'], $str);
}
- public function render(Api4Query $query): string {
- return '"' . \CRM_Core_DAO::escapeString($this->expr) . '"';
+ public function render(Api4Query $query, bool $includeAlias = FALSE): string {
+ return '"' . \CRM_Core_DAO::escapeString($this->expr) . '"' . ($includeAlias ? " AS `{$this->getAlias()}`" : '');
}
/**
protected function initialize() {
}
- public function render(Api4Query $query): string {
- return '*';
- }
-
public static function getTitle(): string {
return ts('Wild');
}
}
}
foreach ($result as $key => $value) {
+ // Skip null values or values that have already been unset by `formatOutputValue` functions
+ if (!isset($result[$key])) {
+ continue;
+ }
$fieldExpr = SqlExpression::convert($selectAliases[$key] ?? $key);
$fieldName = \CRM_Utils_Array::first($fieldExpr->getFields() ?? '');
$baseName = $fieldName ? \CRM_Utils_Array::first(explode(':', $fieldName)) : NULL;
$this->assertEquals(['January', 'February', 'March', 'April'], $agg['months']);
}
+ public function testGroupConcatUnique(): void {
+ $cid1 = $this->createTestRecord('Contact')['id'];
+ $cid2 = $this->createTestRecord('Contact')['id'];
+
+ $this->saveTestRecords('Address', [
+ 'records' => [
+ ['contact_id' => $cid1, 'city' => 'A', 'location_type_id' => 1],
+ ['contact_id' => $cid1, 'city' => 'A', 'location_type_id' => 2],
+ ['contact_id' => $cid1, 'city' => 'B', 'location_type_id' => 3],
+ ],
+ ]);
+ $this->saveTestRecords('Email', [
+ 'records' => [
+ ['contact_id' => $cid1, 'email' => 'test1@example.org', 'location_type_id' => 1],
+ ['contact_id' => $cid1, 'email' => 'test2@example.org', 'location_type_id' => 2],
+ ],
+ ]);
+
+ $result = Contact::get(FALSE)
+ ->addSelect('GROUP_CONCAT(UNIQUE address.id) AS address_id')
+ ->addSelect('GROUP_CONCAT(UNIQUE address.city) AS address_city')
+ ->addSelect('GROUP_CONCAT(UNIQUE email.email) AS email')
+ ->addGroupBy('id')
+ ->addJoin('Address AS address', 'LEFT', ['id', '=', 'address.contact_id'])
+ ->addJoin('Email AS email', 'LEFT', ['id', '=', 'email.contact_id'])
+ ->addOrderBy('id')
+ ->addWhere('id', 'IN', [$cid1, $cid2])
+ ->execute();
+
+ $this->assertEquals(['A', 'A', 'B'], $result[0]['address_city']);
+ $this->assertEquals(['test1@example.org', 'test2@example.org'], $result[0]['email']);
+ }
+
public function testGroupHaving(): void {
$cid = Contact::create(FALSE)->addValue('first_name', 'donor')->execute()->first()['id'];
Contribution::save(FALSE)