@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

Integrate ApplicationSearch with CustomField

Summary:
Ref T2625. Ref T3794. Ref T418. Ref T1703.

This is a more general version of D5278. It expands CustomField support to include real integration with ApplicationSearch.

Broadly, custom fields may elect to:

- build indicies when objects are updated;
- populate ApplicationSearch forms with new controls;
- read inputs entered into those controls out of the request; and
- apply constraints to search queries.

Some utility/helper stuff is provided to make this easier. This part could be cleaner, but seems reasonable for a first cut. In particular, the Query and SearchEngine must manually call all the hooks right now instead of everything happening magically. I think that's fine for the moment; they're pretty easy to get right.

Test Plan:
I added a new searchable "Company" field to People:

{F58229}

This also cleaned up the disable/reorder view a little bit:

{F58230}

As it did before, this field appears on the edit screen:

{F58231}

However, because it has `search`, it also appears on the search screen:

{F58232}

When queried, it returns the expected results:

{F58233}

And the actually good bit of all this is that the query can take advantage of indexes:

mysql> explain SELECT * FROM `user` user JOIN `user_customfieldstringindex` `appsearch_0` ON `appsearch_0`.objectPHID = user.phid AND `appsearch_0`.indexKey = 'mk3Ndy476ge6' AND `appsearch_0`.indexValue IN ('phacility') ORDER BY user.id DESC LIMIT 101;
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| 1 | SIMPLE | appsearch_0 | ref | key_join,key_find | key_find | 232 | const,const | 1 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | user | eq_ref | phid | phid | 194 | phabricator2_user.appsearch_0.objectPHID | 1 | |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
2 rows in set (0.00 sec)

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T418, T1703, T2625, T3794

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

+655 -10
+21
resources/sql/patches/20130914.usercustom.sql
··· 1 + CREATE TABLE {$NAMESPACE}_user.user_customfieldstringindex ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + objectPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, 4 + indexKey VARCHAR(12) NOT NULL COLLATE utf8_bin, 5 + indexValue LONGTEXT NOT NULL COLLATE utf8_general_ci, 6 + 7 + KEY `key_join` (objectPHID, indexKey, indexValue(64)), 8 + KEY `key_find` (indexKey, indexValue(64)) 9 + 10 + ) ENGINE=InnoDB, COLLATE utf8_general_ci; 11 + 12 + CREATE TABLE {$NAMESPACE}_user.user_customfieldnumericindex ( 13 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 14 + objectPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, 15 + indexKey VARCHAR(12) NOT NULL COLLATE utf8_bin, 16 + indexValue BIGINT NOT NULL, 17 + 18 + KEY `key_join` (objectPHID, indexKey, indexValue), 19 + KEY `key_find` (indexKey, indexValue) 20 + 21 + ) ENGINE=InnoDB, COLLATE utf8_general_ci;
+4
src/__phutil_library_map__.php
··· 1710 1710 'PhabricatorUserConfiguredCustomField' => 'applications/people/customfield/PhabricatorUserConfiguredCustomField.php', 1711 1711 'PhabricatorUserConfiguredCustomFieldStorage' => 'applications/people/storage/PhabricatorUserConfiguredCustomFieldStorage.php', 1712 1712 'PhabricatorUserCustomField' => 'applications/people/customfield/PhabricatorUserCustomField.php', 1713 + 'PhabricatorUserCustomFieldNumericIndex' => 'applications/people/storage/PhabricatorUserCustomFieldNumericIndex.php', 1714 + 'PhabricatorUserCustomFieldStringIndex' => 'applications/people/storage/PhabricatorUserCustomFieldStringIndex.php', 1713 1715 'PhabricatorUserDAO' => 'applications/people/storage/PhabricatorUserDAO.php', 1714 1716 'PhabricatorUserEditor' => 'applications/people/editor/PhabricatorUserEditor.php', 1715 1717 'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php', ··· 3857 3859 ), 3858 3860 'PhabricatorUserConfiguredCustomFieldStorage' => 'PhabricatorCustomFieldStorage', 3859 3861 'PhabricatorUserCustomField' => 'PhabricatorCustomField', 3862 + 'PhabricatorUserCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', 3863 + 'PhabricatorUserCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', 3860 3864 'PhabricatorUserDAO' => 'PhabricatorLiskDAO', 3861 3865 'PhabricatorUserEditor' => 'PhabricatorEditor', 3862 3866 'PhabricatorUserEmail' => 'PhabricatorUserDAO',
+8
src/applications/people/customfield/PhabricatorUserConfiguredCustomField.php
··· 18 18 return new PhabricatorUserConfiguredCustomFieldStorage(); 19 19 } 20 20 21 + protected function newStringIndexStorage() { 22 + return new PhabricatorUserCustomFieldStringIndex(); 23 + } 24 + 25 + protected function newNumericIndexStorage() { 26 + return new PhabricatorUserCustomFieldNumericIndex(); 27 + } 28 + 21 29 }
+8 -1
src/applications/people/query/PhabricatorPeopleQuery.php
··· 101 101 102 102 $data = queryfx_all( 103 103 $conn_r, 104 - 'SELECT * FROM %T user %Q %Q %Q %Q', 104 + 'SELECT * FROM %T user %Q %Q %Q %Q %Q', 105 105 $table->getTableName(), 106 106 $this->buildJoinsClause($conn_r), 107 107 $this->buildWhereClause($conn_r), 108 108 $this->buildOrderClause($conn_r), 109 + $this->buildApplicationSearchGroupClause($conn_r), 109 110 $this->buildLimitClause($conn_r)); 110 111 111 112 if ($this->needPrimaryEmail) { ··· 180 181 'JOIN %T email ON email.userPHID = user.PHID', 181 182 $email_table->getTableName()); 182 183 } 184 + 185 + $joins[] = $this->buildApplicationSearchJoinClause($conn_r); 183 186 184 187 $joins = implode(' ', $joins); 185 188 return $joins; ··· 268 271 269 272 protected function getPagingColumn() { 270 273 return 'user.id'; 274 + } 275 + 276 + protected function getApplicationSearchObjectPHIDColumn() { 277 + return 'user.phid'; 271 278 } 272 279 273 280 }
+11
src/applications/people/query/PhabricatorPeopleSearchEngine.php
··· 3 3 final class PhabricatorPeopleSearchEngine 4 4 extends PhabricatorApplicationSearchEngine { 5 5 6 + public function getCustomFieldObject() { 7 + return new PhabricatorUser(); 8 + } 9 + 6 10 public function buildSavedQueryFromRequest(AphrontRequest $request) { 7 11 $saved = new PhabricatorSavedQuery(); 8 12 ··· 13 17 $saved->setParameter('isSystemAgent', $request->getStr('isSystemAgent')); 14 18 $saved->setParameter('createdStart', $request->getStr('createdStart')); 15 19 $saved->setParameter('createdEnd', $request->getStr('createdEnd')); 20 + 21 + $this->readCustomFieldsFromRequest($request, $saved); 16 22 17 23 return $saved; 18 24 } ··· 57 63 if ($end) { 58 64 $query->withDateCreatedBefore($end); 59 65 } 66 + 67 + $this->applyCustomFieldsToQuery($query, $saved); 68 + 60 69 return $query; 61 70 } 62 71 ··· 100 109 1, 101 110 pht('Show only System Agents.'), 102 111 $is_system_agent)); 112 + 113 + $this->appendCustomFieldsToForm($form, $saved); 103 114 104 115 $this->buildDateRange( 105 116 $form,
+11
src/applications/people/storage/PhabricatorUserCustomFieldNumericIndex.php
··· 1 + <?php 2 + 3 + final class PhabricatorUserCustomFieldNumericIndex 4 + extends PhabricatorCustomFieldNumericIndexStorage { 5 + 6 + public function getApplicationName() { 7 + return 'user'; 8 + } 9 + 10 + } 11 +
+11
src/applications/people/storage/PhabricatorUserCustomFieldStringIndex.php
··· 1 + <?php 2 + 3 + final class PhabricatorUserCustomFieldStringIndex 4 + extends PhabricatorCustomFieldStringIndexStorage { 5 + 6 + public function getApplicationName() { 7 + return 'user'; 8 + } 9 + 10 + } 11 +
+150
src/applications/search/engine/PhabricatorApplicationSearchEngine.php
··· 15 15 16 16 private $viewer; 17 17 private $errors = array(); 18 + private $customFields = false; 18 19 19 20 public function setViewer(PhabricatorUser $viewer) { 20 21 $this->viewer = $viewer; ··· 369 370 return $saved->getParameter('limit', 100); 370 371 } 371 372 373 + 374 + /* -( Application Search )------------------------------------------------- */ 375 + 376 + 377 + /** 378 + * Retrieve an object to use to define custom fields for this search. 379 + * 380 + * To integrate with custom fields, subclasses should override this method 381 + * and return an instance of the application object which implements 382 + * @{interface:PhabricatorCustomFieldInterface}. 383 + * 384 + * @return PhabricatorCustomFieldInterface|null Object with custom fields. 385 + * @task appsearch 386 + */ 387 + public function getCustomFieldObject() { 388 + return null; 389 + } 390 + 391 + 392 + /** 393 + * Get the custom fields for this search. 394 + * 395 + * @return PhabricatorCustomFieldList|null Custom fields, if this search 396 + * supports custom fields. 397 + * @task appsearch 398 + */ 399 + public function getCustomFieldList() { 400 + if ($this->customFields === false) { 401 + $object = $this->getCustomFieldObject(); 402 + if ($object) { 403 + $fields = PhabricatorCustomField::getObjectFields( 404 + $object, 405 + PhabricatorCustomField::ROLE_APPLICATIONSEARCH); 406 + } else { 407 + $fields = null; 408 + } 409 + $this->customFields = $fields; 410 + } 411 + return $this->customFields; 412 + } 413 + 414 + 415 + /** 416 + * Moves data from the request into a saved query. 417 + * 418 + * @param AphrontRequest Request to read. 419 + * @param PhabricatorSavedQuery Query to write to. 420 + * @return void 421 + * @task appsearch 422 + */ 423 + protected function readCustomFieldsFromRequest( 424 + AphrontRequest $request, 425 + PhabricatorSavedQuery $saved) { 426 + 427 + $list = $this->getCustomFieldList(); 428 + if (!$list) { 429 + return; 430 + } 431 + 432 + foreach ($list->getFields() as $field) { 433 + $key = $this->getKeyForCustomField($field); 434 + $value = $field->readApplicationSearchValueFromRequest( 435 + $this, 436 + $request); 437 + $saved->setParameter($key, $value); 438 + } 439 + } 440 + 441 + 442 + /** 443 + * Applies data from a saved query to an executable query. 444 + * 445 + * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain. 446 + * @param PhabricatorSavedQuery Saved query to read. 447 + * @return void 448 + */ 449 + protected function applyCustomFieldsToQuery( 450 + PhabricatorCursorPagedPolicyAwareQuery $query, 451 + PhabricatorSavedQuery $saved) { 452 + 453 + $list = $this->getCustomFieldList(); 454 + if (!$list) { 455 + return; 456 + } 457 + 458 + foreach ($list->getFields() as $field) { 459 + $key = $this->getKeyForCustomField($field); 460 + $value = $field->applyApplicationSearchConstraintToQuery( 461 + $this, 462 + $query, 463 + $saved->getParameter($key)); 464 + } 465 + } 466 + 467 + 468 + /** 469 + * Get a unique key identifying a field. 470 + * 471 + * @param PhabricatorCustomField Field to identify. 472 + * @return string Unique identifier, suitable for use as an input name. 473 + */ 474 + public function getKeyForCustomField(PhabricatorCustomField $field) { 475 + return 'custom:'.$field->getFieldIndex(); 476 + } 477 + 478 + 479 + /** 480 + * Add inputs to an application search form so the user can query on custom 481 + * fields. 482 + * 483 + * @param AphrontFormView Form to update. 484 + * @param PhabricatorSavedQuery Values to prefill. 485 + * @return void 486 + */ 487 + protected function appendCustomFieldsToForm( 488 + AphrontFormView $form, 489 + PhabricatorSavedQuery $saved) { 490 + 491 + $list = $this->getCustomFieldList(); 492 + if (!$list) { 493 + return; 494 + } 495 + 496 + $phids = array(); 497 + foreach ($list->getFields() as $field) { 498 + $key = $this->getKeyForCustomField($field); 499 + $value = $saved->getParameter($key); 500 + $phids[$key] = $field->getRequiredHandlePHIDsForApplicationSearch($value); 501 + } 502 + $all_phids = array_mergev($phids); 503 + 504 + $handles = array(); 505 + if ($all_phids) { 506 + $handles = id(new PhabricatorHandleQuery()) 507 + ->setViewer($this->getViewer()) 508 + ->withPHIDs($all_phids) 509 + ->execute(); 510 + } 511 + 512 + foreach ($list->getFields() as $field) { 513 + $key = $this->getKeyForCustomField($field); 514 + $value = $saved->getParameter($key); 515 + $field->appendToApplicationSearchForm( 516 + $this, 517 + $form, 518 + $value, 519 + array_select_keys($handles, $phids[$key])); 520 + } 521 + } 372 522 373 523 }
+13
src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
··· 479 479 480 480 $this->didApplyTransactions($xactions); 481 481 482 + if ($object instanceof PhabricatorCustomFieldInterface) { 483 + // Maybe this makes more sense to move into the search index itself? For 484 + // now I'm putting it here since I think we might end up with things that 485 + // need it to be up to date once the next page loads, but if we don't go 486 + // there we we could move it into search once search moves to the daemons. 487 + 488 + $fields = PhabricatorCustomField::getObjectFields( 489 + $object, 490 + PhabricatorCustomField::ROLE_APPLICATIONSEARCH); 491 + $fields->readFieldsFromStorage($object); 492 + $fields->rebuildIndexes($object); 493 + } 494 + 482 495 return $xactions; 483 496 } 484 497
+94 -8
src/infrastructure/customfield/field/PhabricatorCustomField.php
··· 569 569 * @task appsearch 570 570 */ 571 571 protected function newStringIndexStorage() { 572 - if ($this->proxy) { 573 - return $this->proxy->newStringIndexStorage(); 574 - } 572 + // NOTE: This intentionally isn't proxied, to avoid call cycles. 575 573 throw new PhabricatorCustomFieldImplementationIncompleteException($this); 576 574 } 577 575 ··· 585 583 * @task appsearch 586 584 */ 587 585 protected function newNumericIndexStorage() { 588 - if ($this->proxy) { 589 - return $this->proxy->newStringIndexStorage(); 590 - } 586 + // NOTE: This intentionally isn't proxied, to avoid call cycles. 591 587 throw new PhabricatorCustomFieldImplementationIncompleteException($this); 592 588 } 593 589 ··· 604 600 return $this->proxy->newStringIndex(); 605 601 } 606 602 607 - $key = $this->getFieldIndexKey(); 603 + $key = $this->getFieldIndex(); 608 604 return $this->newStringIndexStorage() 609 605 ->setIndexKey($key) 610 606 ->setIndexValue($value); ··· 622 618 if ($this->proxy) { 623 619 return $this->proxy->newNumericIndex(); 624 620 } 625 - $key = $this->getFieldIndexKey(); 621 + $key = $this->getFieldIndex(); 626 622 return $this->newNumericIndexStorage() 627 623 ->setIndexKey($key) 628 624 ->setIndexValue($value); 625 + } 626 + 627 + 628 + /** 629 + * Read a query value from a request, for storage in a saved query. Normally, 630 + * this method should, e.g., read a string out of the request. 631 + * 632 + * @param PhabricatorApplicationSearchEngine Engine building the query. 633 + * @param AphrontRequest Request to read from. 634 + * @return wild 635 + * @task appsearch 636 + */ 637 + public function readApplicationSearchValueFromRequest( 638 + PhabricatorApplicationSearchEngine $engine, 639 + AphrontRequest $request) { 640 + if ($this->proxy) { 641 + return $this->proxy->readApplicationSearchValueFromRequest( 642 + $engine, 643 + $request); 644 + } 645 + throw new PhabricatorCustomFieldImplementationIncompleteException($this); 646 + } 647 + 648 + 649 + /** 650 + * Constrain a query, given a field value. Generally, this method should 651 + * use `with...()` methods to apply filters or other constraints to the 652 + * query. 653 + * 654 + * @param PhabricatorApplicationSearchEngine Engine executing the query. 655 + * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain. 656 + * @param wild Constraint provided by the user. 657 + * @return void 658 + * @task appsearch 659 + */ 660 + public function applyApplicationSearchConstraintToQuery( 661 + PhabricatorApplicationSearchEngine $engine, 662 + PhabricatorCursorPagedPolicyAwareQuery $query, 663 + $value) { 664 + if ($this->proxy) { 665 + return $this->proxy->applyApplicationSearchConstraintToQuery( 666 + $engine, 667 + $query, 668 + $value); 669 + } 670 + throw new PhabricatorCustomFieldImplementationIncompleteException($this); 671 + } 672 + 673 + 674 + /** 675 + * Append search controls to the interface. If you need handles, use 676 + * @{method:getRequiredHandlePHIDsForApplicationSearch} to get them. 677 + * 678 + * @param PhabricatorApplicationSearchEngine Engine constructing the form. 679 + * @param AphrontFormView The form to update. 680 + * @param wild Value from the saved query. 681 + * @param list<PhabricatorObjectHandle> List of handles. 682 + * @return void 683 + * @task appsearch 684 + */ 685 + public function appendToApplicationSearchForm( 686 + PhabricatorApplicationSearchEngine $engine, 687 + AphrontFormView $form, 688 + $value, 689 + array $handles) { 690 + if ($this->proxy) { 691 + return $this->proxy->appendToApplicationSearchForm( 692 + $engine, 693 + $form, 694 + $value, 695 + $handles); 696 + } 697 + throw new PhabricatorCustomFieldImplementationIncompleteException($this); 698 + } 699 + 700 + 701 + /** 702 + * Return a list of PHIDs which @{method:appendToApplicationSearchForm} needs 703 + * handles for. This is primarily useful if the field stores PHIDs and you 704 + * need to (for example) render a tokenizer control. 705 + * 706 + * @param wild Value from the saved query. 707 + * @return list<phid> List of PHIDs. 708 + * @task appsearch 709 + */ 710 + public function getRequiredHandlePHIDsForApplicationSearch($value) { 711 + if ($this->proxy) { 712 + return $this->proxy->getRequiredHandlePHIDsForApplicationSearch($value); 713 + } 714 + return array(); 629 715 } 630 716 631 717
+68
src/infrastructure/customfield/field/PhabricatorCustomFieldList.php
··· 151 151 return $xactions; 152 152 } 153 153 154 + 155 + /** 156 + * Publish field indexes into index tables, so ApplicationSearch can search 157 + * them. 158 + * 159 + * @return void 160 + */ 161 + public function rebuildIndexes(PhabricatorCustomFieldInterface $object) { 162 + $indexes = array(); 163 + $index_keys = array(); 164 + 165 + $phid = $object->getPHID(); 166 + 167 + $role = PhabricatorCustomField::ROLE_APPLICATIONSEARCH; 168 + foreach ($this->fields as $field) { 169 + if (!$field->shouldEnableForRole($role)) { 170 + continue; 171 + } 172 + 173 + $index_keys[$field->getFieldIndex()] = true; 174 + 175 + foreach ($field->buildFieldIndexes() as $index) { 176 + $index->setObjectPHID($phid); 177 + $indexes[$index->getTableName()][] = $index; 178 + } 179 + } 180 + 181 + if (!$indexes) { 182 + return; 183 + } 184 + 185 + $any_index = head(head($indexes)); 186 + $conn_w = $any_index->establishConnection('w'); 187 + 188 + foreach ($indexes as $table => $index_list) { 189 + $sql = array(); 190 + foreach ($index_list as $index) { 191 + $sql[] = $index->formatForInsert($conn_w); 192 + } 193 + $indexes[$table] = $sql; 194 + } 195 + 196 + $any_index->openTransaction(); 197 + 198 + foreach ($indexes as $table => $sql_list) { 199 + queryfx( 200 + $conn_w, 201 + 'DELETE FROM %T WHERE objectPHID = %s AND indexKey IN (%Ls)', 202 + $table, 203 + $phid, 204 + array_keys($index_keys)); 205 + 206 + if (!$sql_list) { 207 + continue; 208 + } 209 + 210 + foreach (PhabricatorLiskDAO::chunkSQL($sql_list) as $chunk) { 211 + queryfx( 212 + $conn_w, 213 + 'INSERT INTO %T (objectPHID, indexKey, indexValue) VALUES %Q', 214 + $table, 215 + $chunk); 216 + } 217 + } 218 + 219 + $any_index->saveTransaction(); 220 + } 221 + 154 222 }
+81 -1
src/infrastructure/customfield/field/PhabricatorStandardCustomField.php
··· 56 56 return $this; 57 57 } 58 58 59 + public function getFieldType() { 60 + return $this->fieldType; 61 + } 62 + 59 63 public function getFieldValue() { 60 64 return $this->fieldValue; 61 65 } ··· 71 75 } 72 76 73 77 public function setFieldConfig(array $config) { 78 + $this->setFieldType('text'); 79 + 74 80 foreach ($config as $key => $value) { 75 81 switch ($key) { 76 82 case 'name': ··· 79 85 case 'type': 80 86 $this->setFieldType($value); 81 87 break; 88 + case 'description': 89 + $this->setFieldDescription($value); 90 + break; 82 91 } 83 92 } 84 93 $this->fieldConfig = $config; ··· 88 97 public function getFieldConfigValue($key, $default = null) { 89 98 return idx($this->fieldConfig, $key, $default); 90 99 } 100 + 91 101 92 102 93 103 /* -( PhabricatorCustomField )--------------------------------------------- */ ··· 130 140 } 131 141 132 142 public function renderEditControl() { 133 - $type = $this->getFieldConfigValue('type', 'text'); 143 + $type = $this->getFieldType(); 134 144 switch ($type) { 135 145 case 'text': 136 146 default: ··· 153 163 return $this->getFieldValue(); 154 164 } 155 165 166 + public function shouldAppearInApplicationSearch() { 167 + return $this->getFieldConfigValue('search', false); 168 + } 169 + 170 + protected function newStringIndexStorage() { 171 + return $this->getApplicationField()->newStringIndexStorage(); 172 + } 173 + 174 + protected function newNumericIndexStorage() { 175 + return $this->getApplicationField()->newNumericIndexStorage(); 176 + } 177 + 178 + public function buildFieldIndexes() { 179 + $type = $this->getFieldType(); 180 + switch ($type) { 181 + case 'text': 182 + default: 183 + return array( 184 + $this->newStringIndex($this->getFieldValue()), 185 + ); 186 + } 187 + } 188 + 189 + public function readApplicationSearchValueFromRequest( 190 + PhabricatorApplicationSearchEngine $engine, 191 + AphrontRequest $request) { 192 + $type = $this->getFieldType(); 193 + switch ($type) { 194 + case 'text': 195 + default: 196 + return $request->getStr('std:'.$this->getFieldIndex()); 197 + } 198 + } 199 + 200 + public function applyApplicationSearchConstraintToQuery( 201 + PhabricatorApplicationSearchEngine $engine, 202 + PhabricatorCursorPagedPolicyAwareQuery $query, 203 + $value) { 204 + $type = $this->getFieldType(); 205 + switch ($type) { 206 + case 'text': 207 + default: 208 + if (strlen($value)) { 209 + $query->withApplicationSearchContainsConstraint( 210 + $this->newStringIndex(null), 211 + $value); 212 + } 213 + break; 214 + } 215 + } 216 + 217 + public function appendToApplicationSearchForm( 218 + PhabricatorApplicationSearchEngine $engine, 219 + AphrontFormView $form, 220 + $value, 221 + array $handles) { 222 + 223 + $type = $this->getFieldType(); 224 + switch ($type) { 225 + case 'text': 226 + default: 227 + $form->appendChild( 228 + id(new AphrontFormTextControl()) 229 + ->setLabel($this->getFieldName()) 230 + ->setName('std:'.$this->getFieldIndex()) 231 + ->setValue($value)); 232 + break; 233 + } 234 + 235 + } 156 236 157 237 }
+1
src/infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php
··· 14 14 } 15 15 16 16 abstract public function formatForInsert(AphrontDatabaseConnection $conn); 17 + abstract public function getIndexValueType(); 17 18 18 19 }
+4
src/infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php
··· 12 12 $this->getIndexValue()); 13 13 } 14 14 15 + public function getIndexValueType() { 16 + return 'int'; 17 + } 18 + 15 19 }
+4
src/infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php
··· 12 12 $this->getIndexValue()); 13 13 } 14 14 15 + public function getIndexValueType() { 16 + return 'string'; 17 + } 18 + 15 19 }
+162
src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
··· 3 3 /** 4 4 * A query class which uses cursor-based paging. This paging is much more 5 5 * performant than offset-based paging in the presence of policy filtering. 6 + * 7 + * @task appsearch Integration with ApplicationSearch 6 8 */ 7 9 abstract class PhabricatorCursorPagedPolicyAwareQuery 8 10 extends PhabricatorPolicyAwareQuery { 9 11 10 12 private $afterID; 11 13 private $beforeID; 14 + private $applicationSearchConstraints = array(); 12 15 13 16 protected function getPagingColumn() { 14 17 return 'id'; ··· 226 229 } 227 230 228 231 return '('.implode(') OR (', $clauses).')'; 232 + } 233 + 234 + 235 + /* -( Application Search )------------------------------------------------- */ 236 + 237 + 238 + /** 239 + * Constrain the query with an ApplicationSearch index. This adds a constraint 240 + * which requires objects to have one or more corresponding rows in the index 241 + * with one of the given values. Combined with appropriate indexes, it can 242 + * build the most common types of queries, like: 243 + * 244 + * - Find users with shirt sizes "X" or "XL". 245 + * - Find shoes with size "13". 246 + * 247 + * @param PhabricatorCustomFieldIndexStorage Table where the index is stored. 248 + * @param string|list<string> One or more values to filter by. 249 + * @task appsearch 250 + */ 251 + public function withApplicationSearchContainsConstraint( 252 + PhabricatorCustomFieldIndexStorage $index, 253 + $value) { 254 + 255 + $this->applicationSearchConstraints[] = array( 256 + 'type' => $index->getIndexValueType(), 257 + 'cond' => '=', 258 + 'table' => $index->getTableName(), 259 + 'index' => $index->getIndexKey(), 260 + 'value' => $value, 261 + ); 262 + 263 + return $this; 264 + } 265 + 266 + 267 + /** 268 + * Get the name of the query's primary object PHID column, for constructing 269 + * JOIN clauses. Normally (and by default) this is just `"phid"`, but if the 270 + * query construction requires a table alias it may be something like 271 + * `"task.phid"`. 272 + * 273 + * @return string Column name. 274 + * @task appsearch 275 + */ 276 + protected function getApplicationSearchObjectPHIDColumn() { 277 + return 'phid'; 278 + } 279 + 280 + 281 + /** 282 + * Determine if the JOINs built by ApplicationSearch might cause each primary 283 + * object to return multiple result rows. Generally, this means the query 284 + * needs an extra GROUP BY clause. 285 + * 286 + * @return bool True if the query may return multiple rows for each object. 287 + * @task appsearch 288 + */ 289 + protected function getApplicationSearchMayJoinMultipleRows() { 290 + foreach ($this->applicationSearchConstraints as $constraint) { 291 + $type = $constraint['type']; 292 + $value = $constraint['value']; 293 + 294 + switch ($type) { 295 + case 'string': 296 + case 'int': 297 + if (count((array)$value) > 1) { 298 + return true; 299 + } 300 + break; 301 + default: 302 + throw new Exception("Unknown constraint type '{$type}!"); 303 + } 304 + } 305 + 306 + return false; 307 + } 308 + 309 + 310 + /** 311 + * Construct a GROUP BY clause appropriate for ApplicationSearch constraints. 312 + * 313 + * @param AphrontDatabaseConnection Connection executing the query. 314 + * @return string Group clause. 315 + * @task appsearch 316 + */ 317 + protected function buildApplicationSearchGroupClause( 318 + AphrontDatabaseConnection $conn_r) { 319 + 320 + if ($this->getApplicationSearchMayJoinMultipleRows()) { 321 + return qsprintf( 322 + $conn_r, 323 + 'GROUP BY %Q', 324 + $this->getApplicationSearchObjectPHIDColumn()); 325 + } else { 326 + return ''; 327 + } 328 + } 329 + 330 + 331 + /** 332 + * Construct a JOIN clause appropriate for applying ApplicationSearch 333 + * constraints. 334 + * 335 + * @param AphrontDatabaseConnection Connection executing the query. 336 + * @return string Join clause. 337 + * @task appsearch 338 + */ 339 + protected function buildApplicationSearchJoinClause( 340 + AphrontDatabaseConnection $conn_r) { 341 + 342 + $joins = array(); 343 + foreach ($this->applicationSearchConstraints as $key => $constraint) { 344 + $table = $constraint['table']; 345 + $alias = 'appsearch_'.$key; 346 + $index = $constraint['index']; 347 + $cond = $constraint['cond']; 348 + $phid_column = $this->getApplicationSearchObjectPHIDColumn(); 349 + if ($cond !== '=') { 350 + throw new Exception("Unknown constraint condition '{$cond}'!"); 351 + } 352 + 353 + $type = $constraint['type']; 354 + switch ($type) { 355 + case 'string': 356 + $joins[] = qsprintf( 357 + $conn_r, 358 + 'JOIN %T %T ON %T.objectPHID = %Q 359 + AND %T.indexKey = %s 360 + AND %T.indexValue IN (%Ls)', 361 + $table, 362 + $alias, 363 + $alias, 364 + $phid_column, 365 + $alias, 366 + $index, 367 + $alias, 368 + (array)$constraint['value']); 369 + break; 370 + case 'int': 371 + $joins[] = qsprintf( 372 + $conn_r, 373 + 'JOIN %T %T ON %T.objectPHID = %Q 374 + AND %T.indexKey = %s 375 + AND %T.indexValue IN (%Ld)', 376 + $table, 377 + $alias, 378 + $alias, 379 + $phid_column, 380 + $alias, 381 + $index, 382 + $alias, 383 + (array)$constraint['value']); 384 + break; 385 + default: 386 + throw new Exception("Unknown constraint type '{$type}'!"); 387 + } 388 + } 389 + 390 + return implode(' ', $joins); 229 391 } 230 392 231 393 }
+4
src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
··· 1592 1592 'type' => 'php', 1593 1593 'name' => $this->getPatchPath('20130913.maniphest.1.migratesearch.php'), 1594 1594 ), 1595 + '20130914.usercustom.sql' => array( 1596 + 'type' => 'sql', 1597 + 'name' => $this->getPatchPath('20130914.usercustom.sql'), 1598 + ), 1595 1599 ); 1596 1600 } 1597 1601 }