@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
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+135 -26
+9 -1
src/infrastructure/query/order/PhabricatorQueryOrderVector.php
··· 72 72 'Order vector "%s" specifies order "%s" twice. Each component '. 73 73 'of an ordering must be unique.', 74 74 implode(', ', $vector), 75 - $item)); 75 + $item->getOrderKey())); 76 76 } 77 77 78 78 $items[$item->getOrderKey()] = $item; ··· 82 82 $obj->items = $items; 83 83 $obj->keys = array_keys($items); 84 84 return $obj; 85 + } 86 + 87 + public function getAsString() { 88 + $scalars = array(); 89 + foreach ($this->items as $item) { 90 + $scalars[] = $item->getAsScalar(); 91 + } 92 + return implode(', ', $scalars); 85 93 } 86 94 87 95
+126 -25
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 paging Paging 8 9 * @task order Result Ordering 9 10 */ 10 11 abstract class PhabricatorCursorPagedPolicyAwareQuery ··· 111 112 } 112 113 } 113 114 114 - protected function buildPagingClause( 115 - AphrontDatabaseConnection $conn_r) { 116 - 117 - if ($this->beforeID) { 118 - return qsprintf( 119 - $conn_r, 120 - '%Q %Q %s', 121 - $this->getPagingColumn(), 122 - $this->getReversePaging() ? '<' : '>', 123 - $this->beforeID); 124 - } else if ($this->afterID) { 125 - return qsprintf( 126 - $conn_r, 127 - '%Q %Q %s', 128 - $this->getPagingColumn(), 129 - $this->getReversePaging() ? '>' : '<', 130 - $this->afterID); 131 - } 132 - 133 - return null; 134 - } 135 - 136 115 final protected function didLoadResults(array $results) { 137 116 if ($this->beforeID) { 138 117 $results = array_reverse($results, $preserve_keys = true); ··· 168 147 } 169 148 170 149 150 + /* -( Paging )------------------------------------------------------------- */ 151 + 152 + 153 + protected function buildPagingClause(AphrontDatabaseConnection $conn) { 154 + $orderable = $this->getOrderableColumns(); 155 + 156 + // TODO: Remove this once subqueries modernize. 157 + if (!$orderable) { 158 + if ($this->beforeID) { 159 + return qsprintf( 160 + $conn, 161 + '%Q %Q %s', 162 + $this->getPagingColumn(), 163 + $this->getReversePaging() ? '<' : '>', 164 + $this->beforeID); 165 + } else if ($this->afterID) { 166 + return qsprintf( 167 + $conn, 168 + '%Q %Q %s', 169 + $this->getPagingColumn(), 170 + $this->getReversePaging() ? '>' : '<', 171 + $this->afterID); 172 + } else { 173 + return null; 174 + } 175 + } 176 + 177 + $vector = $this->getOrderVector(); 178 + 179 + if ($this->beforeID !== null) { 180 + $cursor = $this->beforeID; 181 + $reversed = true; 182 + } else if ($this->afterID !== null) { 183 + $cursor = $this->afterID; 184 + $reversed = false; 185 + } else { 186 + // No paging is being applied to this query so we do not need to 187 + // construct a paging clause. 188 + return ''; 189 + } 190 + 191 + $keys = array(); 192 + foreach ($vector as $order) { 193 + $keys[] = $order->getOrderKey(); 194 + } 195 + 196 + $value_map = $this->getPagingValueMap($cursor, $keys); 197 + 198 + $columns = array(); 199 + foreach ($vector as $order) { 200 + $key = $order->getOrderKey(); 201 + 202 + if (!array_key_exists($key, $value_map)) { 203 + throw new Exception( 204 + pht( 205 + 'Query "%s" failed to return a value from getPagingValueMap() '. 206 + 'for column "%s".', 207 + get_class($this), 208 + $key)); 209 + } 210 + 211 + $column = $orderable[$key]; 212 + $column['value'] = $value_map[$key]; 213 + 214 + $columns[] = $column; 215 + } 216 + 217 + return $this->buildPagingClauseFromMultipleColumns( 218 + $conn, 219 + $columns, 220 + array( 221 + 'reversed' => $reversed, 222 + )); 223 + } 224 + 225 + protected function getPagingValueMap($cursor, array $keys) { 226 + // TODO: This is a hack to make this work with existing classes for now. 227 + return array( 228 + 'id' => $cursor, 229 + ); 230 + } 231 + 232 + 171 233 /** 172 234 * Simplifies the task of constructing a paging clause across multiple 173 235 * columns. In the general case, this looks like: ··· 214 276 PhutilTypeSpec::checkMap( 215 277 $column, 216 278 array( 217 - 'table' => 'optional string', 279 + 'table' => 'optional string|null', 218 280 'column' => 'string', 219 281 'value' => 'wild', 220 282 'type' => 'string', 221 283 'reverse' => 'optional bool', 284 + 'unique' => 'optional bool', 222 285 )); 223 286 } 224 287 ··· 298 361 $vector = PhabricatorQueryOrderVector::newFromVector($vector); 299 362 300 363 $orderable = $this->getOrderableColumns(); 364 + 365 + // Make sure that all the components identify valid columns. 366 + $unique = array(); 301 367 foreach ($vector as $order) { 302 - $key = $vector->getOrderKey(); 368 + $key = $order->getOrderKey(); 303 369 if (empty($orderable[$key])) { 304 370 $valid = implode(', ', array_keys($orderable)); 305 371 throw new Exception( ··· 307 373 'This query ("%s") does not support sorting by order key "%s". '. 308 374 'Supported orders are: %s.', 309 375 get_class($this), 376 + $key, 310 377 $valid)); 311 378 } 379 + 380 + $unique[$key] = idx($orderable[$key], 'unique', false); 381 + } 382 + 383 + // Make sure that the last column is unique so that this is a strong 384 + // ordering which can be used for paging. 385 + $last = last($unique); 386 + if ($last !== true) { 387 + throw new Exception( 388 + pht( 389 + 'Order vector "%s" is invalid: the last column in an order must '. 390 + 'be a column with unique values, but "%s" is not unique.', 391 + $vector->getAsString(), 392 + last_key($unique))); 393 + } 394 + 395 + // Make sure that other columns are not unique; an ordering like "id, name" 396 + // does not make sense because only "id" can ever have an effect. 397 + array_pop($unique); 398 + foreach ($unique as $key => $is_unique) { 399 + if ($is_unique) { 400 + throw new Exception( 401 + pht( 402 + 'Order vector "%s" is invalid: only the last column in an order '. 403 + 'may be unique, but "%s" is a unique column and not the last '. 404 + 'column in the order.', 405 + $vector->getAsString(), 406 + $key)); 407 + } 312 408 } 313 409 314 410 $this->orderVector = $vector; ··· 323 419 if (!$this->orderVector) { 324 420 $vector = $this->getDefaultOrderVector(); 325 421 $vector = PhabricatorQueryOrderVector::newFromVector($vector); 326 - $this->orderVector = $vector; 422 + 423 + // We call setOrderVector() here to apply checks to the default vector. 424 + // This catches any errors in the implementation. 425 + $this->setOrderVector($vector); 327 426 } 328 427 329 428 return $this->orderVector; ··· 354 453 'table' => null, 355 454 'column' => 'id', 356 455 'reverse' => false, 456 + 'type' => 'int', 457 + 'unique' => true, 357 458 ), 358 459 ); 359 460 }