@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator

Begin formalizing query orders

Summary:
Ref T7803. Queries currently have a single `getPagingColumn()`, which is oversimplified and insufficient to describe many ordering operations. Frequently, orders must span multiple columns.

Move toward an "order vector", which is a list of orderable values like "name, id". These map directly to columns, and are sufficient to actually describe orders. The more modern Query classes (Maniphest, Repository) essentially do this manually anyway.

Test Plan:
- Added and executed unit tests.
- Browsed around, verified the correct ORDER BY clauses were generated.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T7803

Differential Revision: https://secure.phabricator.com/D12352

+368 -16
+9
src/__phutil_library_map__.php
··· 2320 2320 'PhabricatorProjectsPolicyRule' => 'applications/policy/rule/PhabricatorProjectsPolicyRule.php', 2321 2321 'PhabricatorPygmentSetupCheck' => 'applications/config/check/PhabricatorPygmentSetupCheck.php', 2322 2322 'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php', 2323 + 'PhabricatorQueryOrderItem' => 'infrastructure/query/order/PhabricatorQueryOrderItem.php', 2324 + 'PhabricatorQueryOrderTestCase' => 'infrastructure/query/order/__tests__/PhabricatorQueryOrderTestCase.php', 2325 + 'PhabricatorQueryOrderVector' => 'infrastructure/query/order/PhabricatorQueryOrderVector.php', 2323 2326 'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php', 2324 2327 'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php', 2325 2328 'PhabricatorRefreshCSRFController' => 'applications/auth/controller/PhabricatorRefreshCSRFController.php', ··· 5685 5688 'PhabricatorProjectWatchController' => 'PhabricatorProjectController', 5686 5689 'PhabricatorProjectsPolicyRule' => 'PhabricatorPolicyRule', 5687 5690 'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck', 5691 + 'PhabricatorQueryOrderItem' => 'Phobject', 5692 + 'PhabricatorQueryOrderTestCase' => 'PhabricatorTestCase', 5693 + 'PhabricatorQueryOrderVector' => array( 5694 + 'Phobject', 5695 + 'Iterator', 5696 + ), 5688 5697 'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions', 5689 5698 'PhabricatorRedirectController' => 'PhabricatorController', 5690 5699 'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController',
+62
src/infrastructure/query/order/PhabricatorQueryOrderItem.php
··· 1 + <?php 2 + 3 + /** 4 + * Structural class representing one item in an order vector. 5 + * 6 + * See @{class:PhabricatorQueryOrderVector} for discussion of order vectors. 7 + * This represents one item in an order vector, like "id". When combined with 8 + * the other items in the vector, a complete ordering (like "name, id") is 9 + * described. 10 + * 11 + * Construct an item using @{method:newFromScalar}: 12 + * 13 + * $item = PhabricatorQueryOrderItem::newFromScalar('id'); 14 + * 15 + * This class is primarily internal to the query infrastructure, and most 16 + * application code should not need to interact with it directly. 17 + */ 18 + final class PhabricatorQueryOrderItem 19 + extends Phobject { 20 + 21 + private $orderKey; 22 + private $isReversed; 23 + 24 + private function __construct() { 25 + // <private> 26 + } 27 + 28 + public static function newFromScalar($scalar) { 29 + // If the string is something like "-id", strip the "-" off and mark it 30 + // as reversed. 31 + $is_reversed = false; 32 + if (!strncmp($scalar, '-', 1)) { 33 + $is_reversed = true; 34 + $scalar = substr($scalar, 1); 35 + } 36 + 37 + $item = new PhabricatorQueryOrderItem(); 38 + $item->orderKey = $scalar; 39 + $item->isReversed = $is_reversed; 40 + 41 + return $item; 42 + } 43 + 44 + public function getIsReversed() { 45 + return $this->isReversed; 46 + } 47 + 48 + public function getOrderKey() { 49 + return $this->orderKey; 50 + } 51 + 52 + public function getAsScalar() { 53 + if ($this->getIsReversed()) { 54 + $prefix = '-'; 55 + } else { 56 + $prefix = ''; 57 + } 58 + 59 + return $prefix.$this->getOrderKey(); 60 + } 61 + 62 + }
+115
src/infrastructure/query/order/PhabricatorQueryOrderVector.php
··· 1 + <?php 2 + 3 + /** 4 + * Structural class representing a column ordering for a query. 5 + * 6 + * Queries often order results on multiple columns. For example, projects might 7 + * be ordered by "name, id". This class wraps a list of column orderings and 8 + * makes them easier to manage. 9 + * 10 + * To construct an order vector, use @{method:newFromVector}: 11 + * 12 + * $vector = PhabricatorQueryOrderVector::newFromVector(array('name', 'id')); 13 + * 14 + * You can iterate over an order vector normally: 15 + * 16 + * foreach ($vector as $item) { 17 + * // ... 18 + * } 19 + * 20 + * The items are objects of class @{class:PhabricatorQueryOrderItem}. 21 + * 22 + * This class is primarily internal to the query infrastructure, and most 23 + * application code should not need to interact with it directly. 24 + */ 25 + final class PhabricatorQueryOrderVector 26 + extends Phobject 27 + implements Iterator { 28 + 29 + private $items; 30 + private $keys; 31 + private $cursor; 32 + 33 + private function __construct() { 34 + // <private> 35 + } 36 + 37 + public static function newFromVector($vector) { 38 + if ($vector instanceof PhabricatorQueryOrderVector) { 39 + return (clone $vector); 40 + } 41 + 42 + if (!is_array($vector)) { 43 + throw new Exception( 44 + pht( 45 + 'An order vector can only be constructed from a list of strings or '. 46 + 'another order vector.')); 47 + } 48 + 49 + if (!$vector) { 50 + throw new Exception( 51 + pht( 52 + 'An order vector must not be empty.')); 53 + } 54 + 55 + $items = array(); 56 + foreach ($vector as $key => $scalar) { 57 + if (!is_string($scalar)) { 58 + throw new Exception( 59 + pht( 60 + 'Value with key "%s" in order vector is not a string (it has '. 61 + 'type "%s"). An order vector must contain only strings.', 62 + $key, 63 + gettype($scalar))); 64 + } 65 + 66 + $item = PhabricatorQueryOrderItem::newFromScalar($scalar); 67 + 68 + // Orderings like "id, id, id" or "id, -id" are meaningless and invalid. 69 + if (isset($items[$item->getOrderKey()])) { 70 + throw new Exception( 71 + pht( 72 + 'Order vector "%s" specifies order "%s" twice. Each component '. 73 + 'of an ordering must be unique.', 74 + implode(', ', $vector), 75 + $item)); 76 + } 77 + 78 + $items[$item->getOrderKey()] = $item; 79 + } 80 + 81 + $obj = new PhabricatorQueryOrderVector(); 82 + $obj->items = $items; 83 + $obj->keys = array_keys($items); 84 + return $obj; 85 + } 86 + 87 + 88 + /* -( Iterator Interface )------------------------------------------------- */ 89 + 90 + 91 + public function rewind() { 92 + $this->cursor = 0; 93 + } 94 + 95 + 96 + public function current() { 97 + return $this->items[$this->key()]; 98 + } 99 + 100 + 101 + public function key() { 102 + return $this->keys[$this->cursor]; 103 + } 104 + 105 + 106 + public function next() { 107 + ++$this->cursor; 108 + } 109 + 110 + 111 + public function valid() { 112 + return isset($this->keys[$this->cursor]); 113 + } 114 + 115 + }
+65
src/infrastructure/query/order/__tests__/PhabricatorQueryOrderTestCase.php
··· 1 + <?php 2 + 3 + final class PhabricatorQueryOrderTestCase extends PhabricatorTestCase { 4 + 5 + public function testQueryOrderItem() { 6 + $item = PhabricatorQueryOrderItem::newFromScalar('id'); 7 + $this->assertEqual('id', $item->getOrderKey()); 8 + $this->assertEqual(false, $item->getIsReversed()); 9 + 10 + $item = PhabricatorQueryOrderItem::newFromScalar('-id'); 11 + $this->assertEqual('id', $item->getOrderKey()); 12 + $this->assertEqual(true, $item->getIsReversed()); 13 + } 14 + 15 + public function testQueryOrderBadVectors() { 16 + $bad = array( 17 + array(), 18 + null, 19 + 1, 20 + array(2), 21 + array('id', 'id'), 22 + array('id', '-id'), 23 + ); 24 + 25 + foreach ($bad as $input) { 26 + $caught = null; 27 + try { 28 + PhabricatorQueryOrderVector::newFromVector($input); 29 + } catch (Exception $ex) { 30 + $caught = $ex; 31 + } 32 + 33 + $this->assertTrue(($caught instanceof Exception)); 34 + } 35 + } 36 + 37 + public function testQueryOrderVector() { 38 + $vector = PhabricatorQueryOrderVector::newFromVector( 39 + array( 40 + 'a', 41 + 'b', 42 + '-c', 43 + 'd', 44 + )); 45 + 46 + $this->assertEqual( 47 + array( 48 + 'a' => 'a', 49 + 'b' => 'b', 50 + 'c' => 'c', 51 + 'd' => 'd', 52 + ), 53 + mpull(iterator_to_array($vector), 'getOrderKey')); 54 + 55 + $this->assertEqual( 56 + array( 57 + 'a' => false, 58 + 'b' => false, 59 + 'c' => true, 60 + 'd' => false, 61 + ), 62 + mpull(iterator_to_array($vector), 'getIsReversed')); 63 + } 64 + 65 + }
+117 -16
src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
··· 5 5 * performant than offset-based paging in the presence of policy filtering. 6 6 * 7 7 * @task appsearch Integration with ApplicationSearch 8 + * @task order Result Ordering 8 9 */ 9 10 abstract class PhabricatorCursorPagedPolicyAwareQuery 10 11 extends PhabricatorPolicyAwareQuery { ··· 14 15 private $applicationSearchConstraints = array(); 15 16 private $applicationSearchOrders = array(); 16 17 private $internalPaging; 18 + private $orderVector; 17 19 18 20 protected function getPagingColumn() { 19 21 return 'id'; ··· 131 133 return null; 132 134 } 133 135 134 - final protected function buildOrderClause(AphrontDatabaseConnection $conn_r) { 135 - if ($this->beforeID) { 136 - return qsprintf( 137 - $conn_r, 138 - 'ORDER BY %Q %Q', 139 - $this->getPagingColumn(), 140 - $this->getReversePaging() ? 'DESC' : 'ASC'); 141 - } else { 142 - return qsprintf( 143 - $conn_r, 144 - 'ORDER BY %Q %Q', 145 - $this->getPagingColumn(), 146 - $this->getReversePaging() ? 'ASC' : 'DESC'); 147 - } 148 - } 149 - 150 136 final protected function didLoadResults(array $results) { 151 137 if ($this->beforeID) { 152 138 $results = array_reverse($results, $preserve_keys = true); ··· 284 270 return '('.implode(') OR (', $clauses).')'; 285 271 } 286 272 273 + 274 + /* -( Result Ordering )---------------------------------------------------- */ 275 + 276 + 277 + /** 278 + * @task order 279 + */ 280 + public function setOrderVector($vector) { 281 + $vector = PhabricatorQueryOrderVector::newFromVector($vector); 282 + 283 + $orderable = $this->getOrderableColumns(); 284 + foreach ($vector as $order) { 285 + $key = $vector->getOrderKey(); 286 + if (empty($orderable[$key])) { 287 + $valid = implode(', ', array_keys($orderable)); 288 + throw new Exception( 289 + pht( 290 + 'This query ("%s") does not support sorting by order key "%s". '. 291 + 'Supported orders are: %s.', 292 + get_class($this), 293 + $valid)); 294 + } 295 + } 296 + 297 + $this->orderVector = $vector; 298 + return $this; 299 + } 300 + 301 + 302 + /** 303 + * @task order 304 + */ 305 + private function getOrderVector() { 306 + if (!$this->orderVector) { 307 + $vector = $this->getDefaultOrderVector(); 308 + $vector = PhabricatorQueryOrderVector::newFromVector($vector); 309 + $this->orderVector = $vector; 310 + } 311 + 312 + return $this->orderVector; 313 + } 314 + 315 + 316 + /** 317 + * @task order 318 + */ 319 + protected function getDefaultOrderVector() { 320 + return array('id'); 321 + } 322 + 323 + 324 + /** 325 + * @task order 326 + */ 327 + public function getOrderableColumns() { 328 + // TODO: Remove this once all subclasses move off the old stuff. 329 + if ($this->getPagingColumn() !== 'id') { 330 + // This class has bad old custom logic around paging, so return nothing 331 + // here. This deactivates the new order code. 332 + return array(); 333 + } 334 + 335 + return array( 336 + 'id' => array( 337 + 'table' => null, 338 + 'column' => 'id', 339 + 'reverse' => false, 340 + ), 341 + ); 342 + } 343 + 344 + 345 + /** 346 + * @task order 347 + */ 348 + final protected function buildOrderClause(AphrontDatabaseConnection $conn) { 349 + $orderable = $this->getOrderableColumns(); 350 + 351 + // TODO: Remove this once all subclasses move off the old stuff. We'll 352 + // only enter this block for code using older ordering mechanisms. New 353 + // code should expose an orderable column list. 354 + if (!$orderable) { 355 + if ($this->beforeID) { 356 + return qsprintf( 357 + $conn, 358 + 'ORDER BY %Q %Q', 359 + $this->getPagingColumn(), 360 + $this->getReversePaging() ? 'DESC' : 'ASC'); 361 + } else { 362 + return qsprintf( 363 + $conn, 364 + 'ORDER BY %Q %Q', 365 + $this->getPagingColumn(), 366 + $this->getReversePaging() ? 'ASC' : 'DESC'); 367 + } 368 + } 369 + 370 + $vector = $this->getOrderVector(); 371 + 372 + $parts = array(); 373 + foreach ($vector as $order) { 374 + $part = $orderable[$order->getOrderKey()]; 375 + if ($order->getIsReversed()) { 376 + $part['reverse'] = !idx($part, 'reverse', false); 377 + } 378 + $parts[] = $part; 379 + } 380 + 381 + return $this->formatOrderClause($conn, $parts); 382 + } 383 + 384 + 385 + /** 386 + * @task order 387 + */ 287 388 protected function formatOrderClause( 288 389 AphrontDatabaseConnection $conn, 289 390 array $parts) {