Skip to content

Commit

Permalink
CRM-20345 - CRM_Utils_SQL_Select::orderBy() (#4)
Browse files Browse the repository at this point in the history
* CRM-20345 - CRM_Utils_Array::crmArraySortByField - Add test. Allow multiple fields.

* CRM-20345 - CRM_Utils_SQL_Select::orderBy - Use more deterministic ordering

The technique of computing default `$weight = count($this->orderBys)`
addresses a valid point: we need to preserve ordering for existing callers
who don't specify weights -- while also allowing weights.

However, it feels weird in my gut. Not sure why -- maybe it's something like this:

```php
// A1: Non-deterministic ordering
$select->orderBy('alpha', 1);
$select->orderBy('beta');
$select->orderBy('delta', 2);
$select->orderBy('gamma', 3);

// A2: Deterministic ordering
$select->orderBy('alpha', 10);
$select->orderBy('beta');
$select->orderBy('delta', 20);
$select->orderBy('gamma', 30);

// B1: Deterministic ordering
$select->orderBy('alpha');
$select->orderBy('beta');
$select->orderBy('delta');
$select->orderBy('gamma');

// B2: Non-deterministic ordering
$select->orderBy('alpha', 1);
$select->orderBy('beta', 1);
$select->orderBy('delta', 1);
$select->orderBy('gamma', 1);
```

As a reader, I would expect A1/A2 to be the same, and I would expect B1/B2
to be the same.  But they're not.  If there's a collision in the `weight`s,
the ordering becomes non-deterministic (depending on obscure details or
happenstance of the PHP runtime).

Of course, there's no right answer: in A1/A2, you can plausibly put `beta`
before `alpha` or after `alpha` or after `gamma`.  But it should be
determinstic so that it always winds up in the same place.
  • Loading branch information
totten authored and colemanw committed Apr 1, 2017
1 parent 7a51786 commit 6db7061
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 9 deletions.
14 changes: 11 additions & 3 deletions CRM/Utils/Array.php
Original file line number Diff line number Diff line change
Expand Up @@ -473,15 +473,23 @@ public static function crmIsEmptyArray($array = array()) {
*
* @param array $array
* Array to be sorted.
* @param string $field
* @param string|array $field
* Name of the attribute used for sorting.
*
* @return array
* Sorted array
*/
public static function crmArraySortByField($array, $field) {
$code = "return strnatcmp(\$a['$field'], \$b['$field']);";
uasort($array, create_function('$a,$b', $code));
$fields = (array) $field;
uasort($array, function ($a, $b) use ($fields) {
foreach ($fields as $f) {
$v = strnatcmp($a[$f], $b[$f]);
if ($v !== 0) {
return $v;
}
}
return 0;
});
return $array;
}

Expand Down
11 changes: 5 additions & 6 deletions CRM/Utils/SQL/Select.php
Original file line number Diff line number Diff line change
Expand Up @@ -326,14 +326,12 @@ public function having($exprs, $args = NULL) {
* @param int $weight
* @return \CRM_Utils_SQL_Select
*/
public function orderBy($exprs, $args = NULL, $weight = NULL) {
public function orderBy($exprs, $args = NULL, $weight = 0) {
static $guid = 0;
$exprs = (array) $exprs;
if ($weight === NULL) {
$weight = count($this->orderBys);
}
foreach ($exprs as $expr) {
$evaluatedExpr = $this->interpolate($expr, $args);
$this->orderBys[$evaluatedExpr] = array('value' => $evaluatedExpr, 'weight' => $weight++);
$this->orderBys[$evaluatedExpr] = array('value' => $evaluatedExpr, 'weight' => $weight, 'guid' => $guid++);
}
return $this;
}
Expand Down Expand Up @@ -578,7 +576,8 @@ public function toSQL() {
$sql .= 'HAVING (' . implode(') AND (', $this->havings) . ")\n";
}
if ($this->orderBys) {
$orderBys = CRM_Utils_Array::crmArraySortByField($this->orderBys, 'weight');
$orderBys = CRM_Utils_Array::crmArraySortByField($this->orderBys,
array('weight', 'guid'));
$orderBys = CRM_Utils_Array::collect('value', $orderBys);
$sql .= 'ORDER BY ' . implode(', ', $orderBys) . "\n";
}
Expand Down
88 changes: 88 additions & 0 deletions tests/phpunit/CRM/Utils/ArrayTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,92 @@ public function testGetSetPathParts() {
$this->assertEquals(3, $arr['zoo']['half']);
}

public function getSortExamples() {
$red = array('label' => 'Red', 'id' => 1, 'weight' => '90');
$orange = array('label' => 'Orange', 'id' => 2, 'weight' => '70');
$yellow = array('label' => 'Yellow', 'id' => 3, 'weight' => '10');
$green = array('label' => 'Green', 'id' => 4, 'weight' => '70');
$blue = array('label' => 'Blue', 'id' => 5, 'weight' => '70');

$examples = array();
$examples[] = array(
array(
'r' => $red,
'y' => $yellow,
'g' => $green,
'o' => $orange,
'b' => $blue,
),
'id',
array(
'r' => $red,
'o' => $orange,
'y' => $yellow,
'g' => $green,
'b' => $blue,
),
);
$examples[] = array(
array(
'r' => $red,
'y' => $yellow,
'g' => $green,
'o' => $orange,
'b' => $blue,
),
'label',
array(
'b' => $blue,
'g' => $green,
'o' => $orange,
'r' => $red,
'y' => $yellow,
),
);
$examples[] = array(
array(
'r' => $red,
'g' => $green,
'y' => $yellow,
'o' => $orange,
'b' => $blue,
),
array('weight', 'id'),
array(
'y' => $yellow,
'o' => $orange,
'g' => $green,
'b' => $blue,
'r' => $red,
),
);

return $examples;
}

/**
* @param array $array
* @param string|array $field
* @param $expected
* @dataProvider getSortExamples
*/
public function testCrmArraySortByField($array, $field, $expected) {
$actual = CRM_Utils_Array::crmArraySortByField($array, $field);

// assertEquals() has nicer error output, but it's not precise about order.
$this->assertEquals($expected, $actual);

$aIter = new ArrayIterator($actual);
$eIter = new ArrayIterator($expected);
$this->assertEquals($eIter->count(), $aIter->count());
$pos = 0;
while ($aIter->valid()) {
$this->assertEquals($eIter->key(), $aIter->key(), "Keys at offset $pos do not match");
$this->assertEquals($eIter->current(), $aIter->current(), "Values at offset $pos do not match");
$aIter->next();
$eIter->next();
$pos++;
}
}

}

0 comments on commit 6db7061

Please sign in to comment.