mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-27 09:12:41 +01:00
Drive query ordering and paging more cohesively
Summary: Ref T7803. Ordering and paging are inherently intertwined, but they often aren't driven by the same data right now. Start driving them through the same data: - `getOrderableColumns()` defines orderable and pageable columns. - `getPagingValueMap()` reads values from a cursor. This is generally sufficient to implement both paging and ordering. Also, add some more sanity checks to try to curtail the number of ambiguous/invalid orderings applications produce, since these cause subtle/messy bugs. Test Plan: - Paged through pastes and a few other object types. - Intentionally changed defaults to be invalid and hit some of the errors. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T7803 Differential Revision: https://secure.phabricator.com/D12355
This commit is contained in:
parent
e6174ed45c
commit
a40c40fade
2 changed files with 135 additions and 26 deletions
|
@ -72,7 +72,7 @@ final class PhabricatorQueryOrderVector
|
||||||
'Order vector "%s" specifies order "%s" twice. Each component '.
|
'Order vector "%s" specifies order "%s" twice. Each component '.
|
||||||
'of an ordering must be unique.',
|
'of an ordering must be unique.',
|
||||||
implode(', ', $vector),
|
implode(', ', $vector),
|
||||||
$item));
|
$item->getOrderKey()));
|
||||||
}
|
}
|
||||||
|
|
||||||
$items[$item->getOrderKey()] = $item;
|
$items[$item->getOrderKey()] = $item;
|
||||||
|
@ -84,6 +84,14 @@ final class PhabricatorQueryOrderVector
|
||||||
return $obj;
|
return $obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getAsString() {
|
||||||
|
$scalars = array();
|
||||||
|
foreach ($this->items as $item) {
|
||||||
|
$scalars[] = $item->getAsScalar();
|
||||||
|
}
|
||||||
|
return implode(', ', $scalars);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* -( Iterator Interface )------------------------------------------------- */
|
/* -( Iterator Interface )------------------------------------------------- */
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
* performant than offset-based paging in the presence of policy filtering.
|
* performant than offset-based paging in the presence of policy filtering.
|
||||||
*
|
*
|
||||||
* @task appsearch Integration with ApplicationSearch
|
* @task appsearch Integration with ApplicationSearch
|
||||||
|
* @task paging Paging
|
||||||
* @task order Result Ordering
|
* @task order Result Ordering
|
||||||
*/
|
*/
|
||||||
abstract class PhabricatorCursorPagedPolicyAwareQuery
|
abstract class PhabricatorCursorPagedPolicyAwareQuery
|
||||||
|
@ -111,28 +112,6 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function buildPagingClause(
|
|
||||||
AphrontDatabaseConnection $conn_r) {
|
|
||||||
|
|
||||||
if ($this->beforeID) {
|
|
||||||
return qsprintf(
|
|
||||||
$conn_r,
|
|
||||||
'%Q %Q %s',
|
|
||||||
$this->getPagingColumn(),
|
|
||||||
$this->getReversePaging() ? '<' : '>',
|
|
||||||
$this->beforeID);
|
|
||||||
} else if ($this->afterID) {
|
|
||||||
return qsprintf(
|
|
||||||
$conn_r,
|
|
||||||
'%Q %Q %s',
|
|
||||||
$this->getPagingColumn(),
|
|
||||||
$this->getReversePaging() ? '>' : '<',
|
|
||||||
$this->afterID);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final protected function didLoadResults(array $results) {
|
final protected function didLoadResults(array $results) {
|
||||||
if ($this->beforeID) {
|
if ($this->beforeID) {
|
||||||
$results = array_reverse($results, $preserve_keys = true);
|
$results = array_reverse($results, $preserve_keys = true);
|
||||||
|
@ -168,6 +147,89 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -( Paging )------------------------------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
protected function buildPagingClause(AphrontDatabaseConnection $conn) {
|
||||||
|
$orderable = $this->getOrderableColumns();
|
||||||
|
|
||||||
|
// TODO: Remove this once subqueries modernize.
|
||||||
|
if (!$orderable) {
|
||||||
|
if ($this->beforeID) {
|
||||||
|
return qsprintf(
|
||||||
|
$conn,
|
||||||
|
'%Q %Q %s',
|
||||||
|
$this->getPagingColumn(),
|
||||||
|
$this->getReversePaging() ? '<' : '>',
|
||||||
|
$this->beforeID);
|
||||||
|
} else if ($this->afterID) {
|
||||||
|
return qsprintf(
|
||||||
|
$conn,
|
||||||
|
'%Q %Q %s',
|
||||||
|
$this->getPagingColumn(),
|
||||||
|
$this->getReversePaging() ? '>' : '<',
|
||||||
|
$this->afterID);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$vector = $this->getOrderVector();
|
||||||
|
|
||||||
|
if ($this->beforeID !== null) {
|
||||||
|
$cursor = $this->beforeID;
|
||||||
|
$reversed = true;
|
||||||
|
} else if ($this->afterID !== null) {
|
||||||
|
$cursor = $this->afterID;
|
||||||
|
$reversed = false;
|
||||||
|
} else {
|
||||||
|
// No paging is being applied to this query so we do not need to
|
||||||
|
// construct a paging clause.
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$keys = array();
|
||||||
|
foreach ($vector as $order) {
|
||||||
|
$keys[] = $order->getOrderKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
$value_map = $this->getPagingValueMap($cursor, $keys);
|
||||||
|
|
||||||
|
$columns = array();
|
||||||
|
foreach ($vector as $order) {
|
||||||
|
$key = $order->getOrderKey();
|
||||||
|
|
||||||
|
if (!array_key_exists($key, $value_map)) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Query "%s" failed to return a value from getPagingValueMap() '.
|
||||||
|
'for column "%s".',
|
||||||
|
get_class($this),
|
||||||
|
$key));
|
||||||
|
}
|
||||||
|
|
||||||
|
$column = $orderable[$key];
|
||||||
|
$column['value'] = $value_map[$key];
|
||||||
|
|
||||||
|
$columns[] = $column;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->buildPagingClauseFromMultipleColumns(
|
||||||
|
$conn,
|
||||||
|
$columns,
|
||||||
|
array(
|
||||||
|
'reversed' => $reversed,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getPagingValueMap($cursor, array $keys) {
|
||||||
|
// TODO: This is a hack to make this work with existing classes for now.
|
||||||
|
return array(
|
||||||
|
'id' => $cursor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simplifies the task of constructing a paging clause across multiple
|
* Simplifies the task of constructing a paging clause across multiple
|
||||||
* columns. In the general case, this looks like:
|
* columns. In the general case, this looks like:
|
||||||
|
@ -214,11 +276,12 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
|
||||||
PhutilTypeSpec::checkMap(
|
PhutilTypeSpec::checkMap(
|
||||||
$column,
|
$column,
|
||||||
array(
|
array(
|
||||||
'table' => 'optional string',
|
'table' => 'optional string|null',
|
||||||
'column' => 'string',
|
'column' => 'string',
|
||||||
'value' => 'wild',
|
'value' => 'wild',
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'reverse' => 'optional bool',
|
'reverse' => 'optional bool',
|
||||||
|
'unique' => 'optional bool',
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -298,8 +361,11 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
|
||||||
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
|
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
|
||||||
|
|
||||||
$orderable = $this->getOrderableColumns();
|
$orderable = $this->getOrderableColumns();
|
||||||
|
|
||||||
|
// Make sure that all the components identify valid columns.
|
||||||
|
$unique = array();
|
||||||
foreach ($vector as $order) {
|
foreach ($vector as $order) {
|
||||||
$key = $vector->getOrderKey();
|
$key = $order->getOrderKey();
|
||||||
if (empty($orderable[$key])) {
|
if (empty($orderable[$key])) {
|
||||||
$valid = implode(', ', array_keys($orderable));
|
$valid = implode(', ', array_keys($orderable));
|
||||||
throw new Exception(
|
throw new Exception(
|
||||||
|
@ -307,8 +373,38 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
|
||||||
'This query ("%s") does not support sorting by order key "%s". '.
|
'This query ("%s") does not support sorting by order key "%s". '.
|
||||||
'Supported orders are: %s.',
|
'Supported orders are: %s.',
|
||||||
get_class($this),
|
get_class($this),
|
||||||
|
$key,
|
||||||
$valid));
|
$valid));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$unique[$key] = idx($orderable[$key], 'unique', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that the last column is unique so that this is a strong
|
||||||
|
// ordering which can be used for paging.
|
||||||
|
$last = last($unique);
|
||||||
|
if ($last !== true) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Order vector "%s" is invalid: the last column in an order must '.
|
||||||
|
'be a column with unique values, but "%s" is not unique.',
|
||||||
|
$vector->getAsString(),
|
||||||
|
last_key($unique)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that other columns are not unique; an ordering like "id, name"
|
||||||
|
// does not make sense because only "id" can ever have an effect.
|
||||||
|
array_pop($unique);
|
||||||
|
foreach ($unique as $key => $is_unique) {
|
||||||
|
if ($is_unique) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Order vector "%s" is invalid: only the last column in an order '.
|
||||||
|
'may be unique, but "%s" is a unique column and not the last '.
|
||||||
|
'column in the order.',
|
||||||
|
$vector->getAsString(),
|
||||||
|
$key));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->orderVector = $vector;
|
$this->orderVector = $vector;
|
||||||
|
@ -323,7 +419,10 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
|
||||||
if (!$this->orderVector) {
|
if (!$this->orderVector) {
|
||||||
$vector = $this->getDefaultOrderVector();
|
$vector = $this->getDefaultOrderVector();
|
||||||
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
|
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
|
||||||
$this->orderVector = $vector;
|
|
||||||
|
// We call setOrderVector() here to apply checks to the default vector.
|
||||||
|
// This catches any errors in the implementation.
|
||||||
|
$this->setOrderVector($vector);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->orderVector;
|
return $this->orderVector;
|
||||||
|
@ -354,6 +453,8 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
|
||||||
'table' => null,
|
'table' => null,
|
||||||
'column' => 'id',
|
'column' => 'id',
|
||||||
'reverse' => false,
|
'reverse' => false,
|
||||||
|
'type' => 'int',
|
||||||
|
'unique' => true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue