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
			
			
This commit is contained in:
		
							
								
								
									
										21
									
								
								resources/sql/patches/20130914.usercustom.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								resources/sql/patches/20130914.usercustom.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| CREATE TABLE {$NAMESPACE}_user.user_customfieldstringindex ( | ||||
|   id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, | ||||
|   objectPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, | ||||
|   indexKey VARCHAR(12) NOT NULL COLLATE utf8_bin, | ||||
|   indexValue LONGTEXT NOT NULL COLLATE utf8_general_ci, | ||||
|  | ||||
|   KEY `key_join` (objectPHID, indexKey, indexValue(64)), | ||||
|   KEY `key_find` (indexKey, indexValue(64)) | ||||
|  | ||||
| ) ENGINE=InnoDB, COLLATE utf8_general_ci; | ||||
|  | ||||
| CREATE TABLE {$NAMESPACE}_user.user_customfieldnumericindex ( | ||||
|   id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, | ||||
|   objectPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, | ||||
|   indexKey VARCHAR(12) NOT NULL COLLATE utf8_bin, | ||||
|   indexValue BIGINT NOT NULL, | ||||
|  | ||||
|   KEY `key_join` (objectPHID, indexKey, indexValue), | ||||
|   KEY `key_find` (indexKey, indexValue) | ||||
|  | ||||
| ) ENGINE=InnoDB, COLLATE utf8_general_ci; | ||||
| @@ -1710,6 +1710,8 @@ phutil_register_library_map(array( | ||||
|     'PhabricatorUserConfiguredCustomField' => 'applications/people/customfield/PhabricatorUserConfiguredCustomField.php', | ||||
|     'PhabricatorUserConfiguredCustomFieldStorage' => 'applications/people/storage/PhabricatorUserConfiguredCustomFieldStorage.php', | ||||
|     'PhabricatorUserCustomField' => 'applications/people/customfield/PhabricatorUserCustomField.php', | ||||
|     'PhabricatorUserCustomFieldNumericIndex' => 'applications/people/storage/PhabricatorUserCustomFieldNumericIndex.php', | ||||
|     'PhabricatorUserCustomFieldStringIndex' => 'applications/people/storage/PhabricatorUserCustomFieldStringIndex.php', | ||||
|     'PhabricatorUserDAO' => 'applications/people/storage/PhabricatorUserDAO.php', | ||||
|     'PhabricatorUserEditor' => 'applications/people/editor/PhabricatorUserEditor.php', | ||||
|     'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php', | ||||
| @@ -3857,6 +3859,8 @@ phutil_register_library_map(array( | ||||
|     ), | ||||
|     'PhabricatorUserConfiguredCustomFieldStorage' => 'PhabricatorCustomFieldStorage', | ||||
|     'PhabricatorUserCustomField' => 'PhabricatorCustomField', | ||||
|     'PhabricatorUserCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', | ||||
|     'PhabricatorUserCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', | ||||
|     'PhabricatorUserDAO' => 'PhabricatorLiskDAO', | ||||
|     'PhabricatorUserEditor' => 'PhabricatorEditor', | ||||
|     'PhabricatorUserEmail' => 'PhabricatorUserDAO', | ||||
|   | ||||
| @@ -18,4 +18,12 @@ final class PhabricatorUserConfiguredCustomField | ||||
|     return new PhabricatorUserConfiguredCustomFieldStorage(); | ||||
|   } | ||||
|  | ||||
|   protected function newStringIndexStorage() { | ||||
|     return new PhabricatorUserCustomFieldStringIndex(); | ||||
|   } | ||||
|  | ||||
|   protected function newNumericIndexStorage() { | ||||
|     return new PhabricatorUserCustomFieldNumericIndex(); | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -101,11 +101,12 @@ final class PhabricatorPeopleQuery | ||||
|  | ||||
|     $data = queryfx_all( | ||||
|       $conn_r, | ||||
|       'SELECT * FROM %T user %Q %Q %Q %Q', | ||||
|       'SELECT * FROM %T user %Q %Q %Q %Q %Q', | ||||
|       $table->getTableName(), | ||||
|       $this->buildJoinsClause($conn_r), | ||||
|       $this->buildWhereClause($conn_r), | ||||
|       $this->buildOrderClause($conn_r), | ||||
|       $this->buildApplicationSearchGroupClause($conn_r), | ||||
|       $this->buildLimitClause($conn_r)); | ||||
|  | ||||
|     if ($this->needPrimaryEmail) { | ||||
| @@ -181,6 +182,8 @@ final class PhabricatorPeopleQuery | ||||
|         $email_table->getTableName()); | ||||
|     } | ||||
|  | ||||
|     $joins[] = $this->buildApplicationSearchJoinClause($conn_r); | ||||
|  | ||||
|     $joins = implode(' ', $joins); | ||||
|     return  $joins; | ||||
|   } | ||||
| @@ -270,4 +273,8 @@ final class PhabricatorPeopleQuery | ||||
|     return 'user.id'; | ||||
|   } | ||||
|  | ||||
|   protected function getApplicationSearchObjectPHIDColumn() { | ||||
|     return 'user.phid'; | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,10 @@ | ||||
| final class PhabricatorPeopleSearchEngine | ||||
|   extends PhabricatorApplicationSearchEngine { | ||||
|  | ||||
|   public function getCustomFieldObject() { | ||||
|     return new PhabricatorUser(); | ||||
|   } | ||||
|  | ||||
|   public function buildSavedQueryFromRequest(AphrontRequest $request) { | ||||
|     $saved = new PhabricatorSavedQuery(); | ||||
|  | ||||
| @@ -14,6 +18,8 @@ final class PhabricatorPeopleSearchEngine | ||||
|     $saved->setParameter('createdStart', $request->getStr('createdStart')); | ||||
|     $saved->setParameter('createdEnd', $request->getStr('createdEnd')); | ||||
|  | ||||
|     $this->readCustomFieldsFromRequest($request, $saved); | ||||
|  | ||||
|     return $saved; | ||||
|   } | ||||
|  | ||||
| @@ -57,6 +63,9 @@ final class PhabricatorPeopleSearchEngine | ||||
|     if ($end) { | ||||
|       $query->withDateCreatedBefore($end); | ||||
|     } | ||||
|  | ||||
|     $this->applyCustomFieldsToQuery($query, $saved); | ||||
|  | ||||
|     return $query; | ||||
|   } | ||||
|  | ||||
| @@ -101,6 +110,8 @@ final class PhabricatorPeopleSearchEngine | ||||
|             pht('Show only System Agents.'), | ||||
|             $is_system_agent)); | ||||
|  | ||||
|     $this->appendCustomFieldsToForm($form, $saved); | ||||
|  | ||||
|     $this->buildDateRange( | ||||
|       $form, | ||||
|       $saved, | ||||
|   | ||||
| @@ -0,0 +1,11 @@ | ||||
| <?php | ||||
|  | ||||
| final class PhabricatorUserCustomFieldNumericIndex | ||||
|   extends PhabricatorCustomFieldNumericIndexStorage { | ||||
|  | ||||
|   public function getApplicationName() { | ||||
|     return 'user'; | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| @@ -0,0 +1,11 @@ | ||||
| <?php | ||||
|  | ||||
| final class PhabricatorUserCustomFieldStringIndex | ||||
|   extends PhabricatorCustomFieldStringIndexStorage { | ||||
|  | ||||
|   public function getApplicationName() { | ||||
|     return 'user'; | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| @@ -15,6 +15,7 @@ abstract class PhabricatorApplicationSearchEngine { | ||||
|  | ||||
|   private $viewer; | ||||
|   private $errors = array(); | ||||
|   private $customFields = false; | ||||
|  | ||||
|   public function setViewer(PhabricatorUser $viewer) { | ||||
|     $this->viewer = $viewer; | ||||
| @@ -370,4 +371,153 @@ abstract class PhabricatorApplicationSearchEngine { | ||||
|   } | ||||
|  | ||||
|  | ||||
| /* -(  Application Search  )------------------------------------------------- */ | ||||
|  | ||||
|  | ||||
|   /** | ||||
|    * Retrieve an object to use to define custom fields for this search. | ||||
|    * | ||||
|    * To integrate with custom fields, subclasses should override this method | ||||
|    * and return an instance of the application object which implements | ||||
|    * @{interface:PhabricatorCustomFieldInterface}. | ||||
|    * | ||||
|    * @return PhabricatorCustomFieldInterface|null Object with custom fields. | ||||
|    * @task appsearch | ||||
|    */ | ||||
|   public function getCustomFieldObject() { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /** | ||||
|    * Get the custom fields for this search. | ||||
|    * | ||||
|    * @return PhabricatorCustomFieldList|null Custom fields, if this search | ||||
|    *   supports custom fields. | ||||
|    * @task appsearch | ||||
|    */ | ||||
|   public function getCustomFieldList() { | ||||
|     if ($this->customFields === false) { | ||||
|       $object = $this->getCustomFieldObject(); | ||||
|       if ($object) { | ||||
|         $fields = PhabricatorCustomField::getObjectFields( | ||||
|           $object, | ||||
|           PhabricatorCustomField::ROLE_APPLICATIONSEARCH); | ||||
|       } else { | ||||
|         $fields = null; | ||||
|       } | ||||
|       $this->customFields = $fields; | ||||
|     } | ||||
|     return $this->customFields; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /** | ||||
|    * Moves data from the request into a saved query. | ||||
|    * | ||||
|    * @param AphrontRequest Request to read. | ||||
|    * @param PhabricatorSavedQuery Query to write to. | ||||
|    * @return void | ||||
|    * @task appsearch | ||||
|    */ | ||||
|   protected function readCustomFieldsFromRequest( | ||||
|     AphrontRequest $request, | ||||
|     PhabricatorSavedQuery $saved) { | ||||
|  | ||||
|     $list = $this->getCustomFieldList(); | ||||
|     if (!$list) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     foreach ($list->getFields() as $field) { | ||||
|       $key = $this->getKeyForCustomField($field); | ||||
|       $value = $field->readApplicationSearchValueFromRequest( | ||||
|         $this, | ||||
|         $request); | ||||
|       $saved->setParameter($key, $value); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /** | ||||
|    * Applies data from a saved query to an executable query. | ||||
|    * | ||||
|    * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain. | ||||
|    * @param PhabricatorSavedQuery Saved query to read. | ||||
|    * @return void | ||||
|    */ | ||||
|   protected function applyCustomFieldsToQuery( | ||||
|     PhabricatorCursorPagedPolicyAwareQuery $query, | ||||
|     PhabricatorSavedQuery $saved) { | ||||
|  | ||||
|     $list = $this->getCustomFieldList(); | ||||
|     if (!$list) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     foreach ($list->getFields() as $field) { | ||||
|       $key = $this->getKeyForCustomField($field); | ||||
|       $value = $field->applyApplicationSearchConstraintToQuery( | ||||
|         $this, | ||||
|         $query, | ||||
|         $saved->getParameter($key)); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /** | ||||
|    * Get a unique key identifying a field. | ||||
|    * | ||||
|    * @param PhabricatorCustomField Field to identify. | ||||
|    * @return string Unique identifier, suitable for use as an input name. | ||||
|    */ | ||||
|   public function getKeyForCustomField(PhabricatorCustomField $field) { | ||||
|     return 'custom:'.$field->getFieldIndex(); | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /** | ||||
|    * Add inputs to an application search form so the user can query on custom | ||||
|    * fields. | ||||
|    * | ||||
|    * @param AphrontFormView Form to update. | ||||
|    * @param PhabricatorSavedQuery Values to prefill. | ||||
|    * @return void | ||||
|    */ | ||||
|   protected function appendCustomFieldsToForm( | ||||
|     AphrontFormView $form, | ||||
|     PhabricatorSavedQuery $saved) { | ||||
|  | ||||
|     $list = $this->getCustomFieldList(); | ||||
|     if (!$list) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     $phids = array(); | ||||
|     foreach ($list->getFields() as $field) { | ||||
|       $key = $this->getKeyForCustomField($field); | ||||
|       $value = $saved->getParameter($key); | ||||
|       $phids[$key] = $field->getRequiredHandlePHIDsForApplicationSearch($value); | ||||
|     } | ||||
|     $all_phids = array_mergev($phids); | ||||
|  | ||||
|     $handles = array(); | ||||
|     if ($all_phids) { | ||||
|       $handles = id(new PhabricatorHandleQuery()) | ||||
|         ->setViewer($this->getViewer()) | ||||
|         ->withPHIDs($all_phids) | ||||
|         ->execute(); | ||||
|     } | ||||
|  | ||||
|     foreach ($list->getFields() as $field) { | ||||
|       $key = $this->getKeyForCustomField($field); | ||||
|       $value = $saved->getParameter($key); | ||||
|       $field->appendToApplicationSearchForm( | ||||
|         $this, | ||||
|         $form, | ||||
|         $value, | ||||
|         array_select_keys($handles, $phids[$key])); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -479,6 +479,19 @@ abstract class PhabricatorApplicationTransactionEditor | ||||
|  | ||||
|     $this->didApplyTransactions($xactions); | ||||
|  | ||||
|     if ($object instanceof PhabricatorCustomFieldInterface) { | ||||
|       // Maybe this makes more sense to move into the search index itself? For | ||||
|       // now I'm putting it here since I think we might end up with things that | ||||
|       // need it to be up to date once the next page loads, but if we don't go | ||||
|       // there we we could move it into search once search moves to the daemons. | ||||
|  | ||||
|       $fields = PhabricatorCustomField::getObjectFields( | ||||
|         $object, | ||||
|         PhabricatorCustomField::ROLE_APPLICATIONSEARCH); | ||||
|       $fields->readFieldsFromStorage($object); | ||||
|       $fields->rebuildIndexes($object); | ||||
|     } | ||||
|  | ||||
|     return $xactions; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -569,9 +569,7 @@ abstract class PhabricatorCustomField { | ||||
|    * @task appsearch | ||||
|    */ | ||||
|   protected function newStringIndexStorage() { | ||||
|     if ($this->proxy) { | ||||
|       return $this->proxy->newStringIndexStorage(); | ||||
|     } | ||||
|     // NOTE: This intentionally isn't proxied, to avoid call cycles. | ||||
|     throw new PhabricatorCustomFieldImplementationIncompleteException($this); | ||||
|   } | ||||
|  | ||||
| @@ -585,9 +583,7 @@ abstract class PhabricatorCustomField { | ||||
|    * @task appsearch | ||||
|    */ | ||||
|   protected function newNumericIndexStorage() { | ||||
|     if ($this->proxy) { | ||||
|       return $this->proxy->newStringIndexStorage(); | ||||
|     } | ||||
|     // NOTE: This intentionally isn't proxied, to avoid call cycles. | ||||
|     throw new PhabricatorCustomFieldImplementationIncompleteException($this); | ||||
|   } | ||||
|  | ||||
| @@ -604,7 +600,7 @@ abstract class PhabricatorCustomField { | ||||
|       return $this->proxy->newStringIndex(); | ||||
|     } | ||||
|  | ||||
|     $key = $this->getFieldIndexKey(); | ||||
|     $key = $this->getFieldIndex(); | ||||
|     return $this->newStringIndexStorage() | ||||
|       ->setIndexKey($key) | ||||
|       ->setIndexValue($value); | ||||
| @@ -622,13 +618,103 @@ abstract class PhabricatorCustomField { | ||||
|     if ($this->proxy) { | ||||
|       return $this->proxy->newNumericIndex(); | ||||
|     } | ||||
|     $key = $this->getFieldIndexKey(); | ||||
|     $key = $this->getFieldIndex(); | ||||
|     return $this->newNumericIndexStorage() | ||||
|       ->setIndexKey($key) | ||||
|       ->setIndexValue($value); | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /** | ||||
|    * Read a query value from a request, for storage in a saved query. Normally, | ||||
|    * this method should, e.g., read a string out of the request. | ||||
|    * | ||||
|    * @param PhabricatorApplicationSearchEngine Engine building the query. | ||||
|    * @param AphrontRequest Request to read from. | ||||
|    * @return wild | ||||
|    * @task appsearch | ||||
|    */ | ||||
|   public function readApplicationSearchValueFromRequest( | ||||
|     PhabricatorApplicationSearchEngine $engine, | ||||
|     AphrontRequest $request) { | ||||
|     if ($this->proxy) { | ||||
|       return $this->proxy->readApplicationSearchValueFromRequest( | ||||
|         $engine, | ||||
|         $request); | ||||
|     } | ||||
|     throw new PhabricatorCustomFieldImplementationIncompleteException($this); | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /** | ||||
|    * Constrain a query, given a field value. Generally, this method should | ||||
|    * use `with...()` methods to apply filters or other constraints to the | ||||
|    * query. | ||||
|    * | ||||
|    * @param PhabricatorApplicationSearchEngine Engine executing the query. | ||||
|    * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain. | ||||
|    * @param wild Constraint provided by the user. | ||||
|    * @return void | ||||
|    * @task appsearch | ||||
|    */ | ||||
|   public function applyApplicationSearchConstraintToQuery( | ||||
|     PhabricatorApplicationSearchEngine $engine, | ||||
|     PhabricatorCursorPagedPolicyAwareQuery $query, | ||||
|     $value) { | ||||
|     if ($this->proxy) { | ||||
|       return $this->proxy->applyApplicationSearchConstraintToQuery( | ||||
|         $engine, | ||||
|         $query, | ||||
|         $value); | ||||
|     } | ||||
|     throw new PhabricatorCustomFieldImplementationIncompleteException($this); | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /** | ||||
|    * Append search controls to the interface. If you need handles, use | ||||
|    * @{method:getRequiredHandlePHIDsForApplicationSearch} to get them. | ||||
|    * | ||||
|    * @param PhabricatorApplicationSearchEngine Engine constructing the form. | ||||
|    * @param AphrontFormView The form to update. | ||||
|    * @param wild Value from the saved query. | ||||
|    * @param list<PhabricatorObjectHandle> List of handles. | ||||
|    * @return void | ||||
|    * @task appsearch | ||||
|    */ | ||||
|   public function appendToApplicationSearchForm( | ||||
|     PhabricatorApplicationSearchEngine $engine, | ||||
|     AphrontFormView $form, | ||||
|     $value, | ||||
|     array $handles) { | ||||
|     if ($this->proxy) { | ||||
|       return $this->proxy->appendToApplicationSearchForm( | ||||
|         $engine, | ||||
|         $form, | ||||
|         $value, | ||||
|         $handles); | ||||
|     } | ||||
|     throw new PhabricatorCustomFieldImplementationIncompleteException($this); | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /** | ||||
|    * Return a list of PHIDs which @{method:appendToApplicationSearchForm} needs | ||||
|    * handles for. This is primarily useful if the field stores PHIDs and you | ||||
|    * need to (for example) render a tokenizer control. | ||||
|    * | ||||
|    * @param wild Value from the saved query. | ||||
|    * @return list<phid> List of PHIDs. | ||||
|    * @task appsearch | ||||
|    */ | ||||
|   public function getRequiredHandlePHIDsForApplicationSearch($value) { | ||||
|     if ($this->proxy) { | ||||
|       return $this->proxy->getRequiredHandlePHIDsForApplicationSearch($value); | ||||
|     } | ||||
|     return array(); | ||||
|   } | ||||
|  | ||||
|  | ||||
| /* -(  ApplicationTransactions  )-------------------------------------------- */ | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -151,4 +151,72 @@ final class PhabricatorCustomFieldList extends Phobject { | ||||
|     return $xactions; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /** | ||||
|    * Publish field indexes into index tables, so ApplicationSearch can search | ||||
|    * them. | ||||
|    * | ||||
|    * @return void | ||||
|    */ | ||||
|   public function rebuildIndexes(PhabricatorCustomFieldInterface $object) { | ||||
|     $indexes = array(); | ||||
|     $index_keys = array(); | ||||
|  | ||||
|     $phid = $object->getPHID(); | ||||
|  | ||||
|     $role = PhabricatorCustomField::ROLE_APPLICATIONSEARCH; | ||||
|     foreach ($this->fields as $field) { | ||||
|       if (!$field->shouldEnableForRole($role)) { | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       $index_keys[$field->getFieldIndex()] = true; | ||||
|  | ||||
|       foreach ($field->buildFieldIndexes() as $index) { | ||||
|         $index->setObjectPHID($phid); | ||||
|         $indexes[$index->getTableName()][] = $index; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (!$indexes) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     $any_index = head(head($indexes)); | ||||
|     $conn_w = $any_index->establishConnection('w'); | ||||
|  | ||||
|     foreach ($indexes as $table => $index_list) { | ||||
|       $sql = array(); | ||||
|       foreach ($index_list as $index) { | ||||
|         $sql[] = $index->formatForInsert($conn_w); | ||||
|       } | ||||
|       $indexes[$table] = $sql; | ||||
|     } | ||||
|  | ||||
|     $any_index->openTransaction(); | ||||
|  | ||||
|       foreach ($indexes as $table => $sql_list) { | ||||
|         queryfx( | ||||
|           $conn_w, | ||||
|           'DELETE FROM %T WHERE objectPHID = %s AND indexKey IN (%Ls)', | ||||
|           $table, | ||||
|           $phid, | ||||
|           array_keys($index_keys)); | ||||
|  | ||||
|         if (!$sql_list) { | ||||
|           continue; | ||||
|         } | ||||
|  | ||||
|         foreach (PhabricatorLiskDAO::chunkSQL($sql_list) as $chunk) { | ||||
|           queryfx( | ||||
|             $conn_w, | ||||
|             'INSERT INTO %T (objectPHID, indexKey, indexValue) VALUES %Q', | ||||
|             $table, | ||||
|             $chunk); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|     $any_index->saveTransaction(); | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -56,6 +56,10 @@ final class PhabricatorStandardCustomField | ||||
|     return $this; | ||||
|   } | ||||
|  | ||||
|   public function getFieldType() { | ||||
|     return $this->fieldType; | ||||
|   } | ||||
|  | ||||
|   public function getFieldValue() { | ||||
|     return $this->fieldValue; | ||||
|   } | ||||
| @@ -71,6 +75,8 @@ final class PhabricatorStandardCustomField | ||||
|   } | ||||
|  | ||||
|   public function setFieldConfig(array $config) { | ||||
|     $this->setFieldType('text'); | ||||
|  | ||||
|     foreach ($config as $key => $value) { | ||||
|       switch ($key) { | ||||
|         case 'name': | ||||
| @@ -79,6 +85,9 @@ final class PhabricatorStandardCustomField | ||||
|         case 'type': | ||||
|           $this->setFieldType($value); | ||||
|           break; | ||||
|         case 'description': | ||||
|           $this->setFieldDescription($value); | ||||
|           break; | ||||
|       } | ||||
|     } | ||||
|     $this->fieldConfig = $config; | ||||
| @@ -90,6 +99,7 @@ final class PhabricatorStandardCustomField | ||||
|   } | ||||
|  | ||||
|  | ||||
|  | ||||
| /* -(  PhabricatorCustomField  )--------------------------------------------- */ | ||||
|  | ||||
|  | ||||
| @@ -130,7 +140,7 @@ final class PhabricatorStandardCustomField | ||||
|   } | ||||
|  | ||||
|   public function renderEditControl() { | ||||
|     $type = $this->getFieldConfigValue('type', 'text'); | ||||
|     $type = $this->getFieldType(); | ||||
|     switch ($type) { | ||||
|       case 'text': | ||||
|       default: | ||||
| @@ -153,5 +163,75 @@ final class PhabricatorStandardCustomField | ||||
|     return $this->getFieldValue(); | ||||
|   } | ||||
|  | ||||
|   public function shouldAppearInApplicationSearch() { | ||||
|     return $this->getFieldConfigValue('search', false); | ||||
|   } | ||||
|  | ||||
|   protected function newStringIndexStorage() { | ||||
|     return $this->getApplicationField()->newStringIndexStorage(); | ||||
|   } | ||||
|  | ||||
|   protected function newNumericIndexStorage() { | ||||
|     return $this->getApplicationField()->newNumericIndexStorage(); | ||||
|   } | ||||
|  | ||||
|   public function buildFieldIndexes() { | ||||
|     $type = $this->getFieldType(); | ||||
|     switch ($type) { | ||||
|       case 'text': | ||||
|       default: | ||||
|         return array( | ||||
|           $this->newStringIndex($this->getFieldValue()), | ||||
|         ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public function readApplicationSearchValueFromRequest( | ||||
|     PhabricatorApplicationSearchEngine $engine, | ||||
|     AphrontRequest $request) { | ||||
|     $type = $this->getFieldType(); | ||||
|     switch ($type) { | ||||
|       case 'text': | ||||
|       default: | ||||
|         return $request->getStr('std:'.$this->getFieldIndex()); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public function applyApplicationSearchConstraintToQuery( | ||||
|     PhabricatorApplicationSearchEngine $engine, | ||||
|     PhabricatorCursorPagedPolicyAwareQuery $query, | ||||
|     $value) { | ||||
|     $type = $this->getFieldType(); | ||||
|     switch ($type) { | ||||
|       case 'text': | ||||
|       default: | ||||
|         if (strlen($value)) { | ||||
|           $query->withApplicationSearchContainsConstraint( | ||||
|             $this->newStringIndex(null), | ||||
|             $value); | ||||
|         } | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public function appendToApplicationSearchForm( | ||||
|     PhabricatorApplicationSearchEngine $engine, | ||||
|     AphrontFormView $form, | ||||
|     $value, | ||||
|     array $handles) { | ||||
|  | ||||
|     $type = $this->getFieldType(); | ||||
|     switch ($type) { | ||||
|       case 'text': | ||||
|       default: | ||||
|         $form->appendChild( | ||||
|           id(new AphrontFormTextControl()) | ||||
|             ->setLabel($this->getFieldName()) | ||||
|             ->setName('std:'.$this->getFieldIndex()) | ||||
|             ->setValue($value)); | ||||
|         break; | ||||
|     } | ||||
|  | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -14,5 +14,6 @@ abstract class PhabricatorCustomFieldIndexStorage | ||||
|   } | ||||
|  | ||||
|   abstract public function formatForInsert(AphrontDatabaseConnection $conn); | ||||
|   abstract public function getIndexValueType(); | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -12,4 +12,8 @@ abstract class PhabricatorCustomFieldNumericIndexStorage | ||||
|       $this->getIndexValue()); | ||||
|   } | ||||
|  | ||||
|   public function getIndexValueType() { | ||||
|     return 'int'; | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -12,4 +12,8 @@ abstract class PhabricatorCustomFieldStringIndexStorage | ||||
|       $this->getIndexValue()); | ||||
|   } | ||||
|  | ||||
|   public function getIndexValueType() { | ||||
|     return 'string'; | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -3,12 +3,15 @@ | ||||
| /** | ||||
|  * A query class which uses cursor-based paging. This paging is much more | ||||
|  * performant than offset-based paging in the presence of policy filtering. | ||||
|  * | ||||
|  * @task appsearch Integration with ApplicationSearch | ||||
|  */ | ||||
| abstract class PhabricatorCursorPagedPolicyAwareQuery | ||||
|   extends PhabricatorPolicyAwareQuery { | ||||
|  | ||||
|   private $afterID; | ||||
|   private $beforeID; | ||||
|   private $applicationSearchConstraints = array(); | ||||
|  | ||||
|   protected function getPagingColumn() { | ||||
|     return 'id'; | ||||
| @@ -228,4 +231,163 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery | ||||
|     return '('.implode(') OR (', $clauses).')'; | ||||
|   } | ||||
|  | ||||
|  | ||||
| /* -(  Application Search  )------------------------------------------------- */ | ||||
|  | ||||
|  | ||||
|   /** | ||||
|    * Constrain the query with an ApplicationSearch index. This adds a constraint | ||||
|    * which requires objects to have one or more corresponding rows in the index | ||||
|    * with one of the given values. Combined with appropriate indexes, it can | ||||
|    * build the most common types of queries, like: | ||||
|    * | ||||
|    *   - Find users with shirt sizes "X" or "XL". | ||||
|    *   - Find shoes with size "13". | ||||
|    * | ||||
|    * @param PhabricatorCustomFieldIndexStorage Table where the index is stored. | ||||
|    * @param string|list<string> One or more values to filter by. | ||||
|    * @task appsearch | ||||
|    */ | ||||
|   public function withApplicationSearchContainsConstraint( | ||||
|     PhabricatorCustomFieldIndexStorage $index, | ||||
|     $value) { | ||||
|  | ||||
|     $this->applicationSearchConstraints[] = array( | ||||
|       'type'  => $index->getIndexValueType(), | ||||
|       'cond'  => '=', | ||||
|       'table' => $index->getTableName(), | ||||
|       'index' => $index->getIndexKey(), | ||||
|       'value' => $value, | ||||
|     ); | ||||
|  | ||||
|     return $this; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /** | ||||
|    * Get the name of the query's primary object PHID column, for constructing | ||||
|    * JOIN clauses. Normally (and by default) this is just `"phid"`, but if the | ||||
|    * query construction requires a table alias it may be something like | ||||
|    * `"task.phid"`. | ||||
|    * | ||||
|    * @return string Column name. | ||||
|    * @task appsearch | ||||
|    */ | ||||
|   protected function getApplicationSearchObjectPHIDColumn() { | ||||
|     return 'phid'; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /** | ||||
|    * Determine if the JOINs built by ApplicationSearch might cause each primary | ||||
|    * object to return multiple result rows. Generally, this means the query | ||||
|    * needs an extra GROUP BY clause. | ||||
|    * | ||||
|    * @return bool True if the query may return multiple rows for each object. | ||||
|    * @task appsearch | ||||
|    */ | ||||
|   protected function getApplicationSearchMayJoinMultipleRows() { | ||||
|     foreach ($this->applicationSearchConstraints as $constraint) { | ||||
|       $type = $constraint['type']; | ||||
|       $value = $constraint['value']; | ||||
|  | ||||
|       switch ($type) { | ||||
|         case 'string': | ||||
|         case 'int': | ||||
|           if (count((array)$value) > 1) { | ||||
|             return true; | ||||
|           } | ||||
|           break; | ||||
|         default: | ||||
|           throw new Exception("Unknown constraint type '{$type}!"); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /** | ||||
|    * Construct a GROUP BY clause appropriate for ApplicationSearch constraints. | ||||
|    * | ||||
|    * @param AphrontDatabaseConnection Connection executing the query. | ||||
|    * @return string Group clause. | ||||
|    * @task appsearch | ||||
|    */ | ||||
|   protected function buildApplicationSearchGroupClause( | ||||
|     AphrontDatabaseConnection $conn_r) { | ||||
|  | ||||
|     if ($this->getApplicationSearchMayJoinMultipleRows()) { | ||||
|       return qsprintf( | ||||
|         $conn_r, | ||||
|         'GROUP BY %Q', | ||||
|         $this->getApplicationSearchObjectPHIDColumn()); | ||||
|     } else { | ||||
|       return ''; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /** | ||||
|    * Construct a JOIN clause appropriate for applying ApplicationSearch | ||||
|    * constraints. | ||||
|    * | ||||
|    * @param AphrontDatabaseConnection Connection executing the query. | ||||
|    * @return string Join clause. | ||||
|    * @task appsearch | ||||
|    */ | ||||
|   protected function buildApplicationSearchJoinClause( | ||||
|     AphrontDatabaseConnection $conn_r) { | ||||
|  | ||||
|     $joins = array(); | ||||
|     foreach ($this->applicationSearchConstraints as $key => $constraint) { | ||||
|       $table = $constraint['table']; | ||||
|       $alias = 'appsearch_'.$key; | ||||
|       $index = $constraint['index']; | ||||
|       $cond = $constraint['cond']; | ||||
|       $phid_column = $this->getApplicationSearchObjectPHIDColumn(); | ||||
|       if ($cond !== '=') { | ||||
|         throw new Exception("Unknown constraint condition '{$cond}'!"); | ||||
|       } | ||||
|  | ||||
|       $type = $constraint['type']; | ||||
|       switch ($type) { | ||||
|         case 'string': | ||||
|           $joins[] = qsprintf( | ||||
|             $conn_r, | ||||
|             'JOIN %T %T ON %T.objectPHID = %Q | ||||
|               AND %T.indexKey = %s | ||||
|               AND %T.indexValue IN (%Ls)', | ||||
|             $table, | ||||
|             $alias, | ||||
|             $alias, | ||||
|             $phid_column, | ||||
|             $alias, | ||||
|             $index, | ||||
|             $alias, | ||||
|             (array)$constraint['value']); | ||||
|           break; | ||||
|         case 'int': | ||||
|           $joins[] = qsprintf( | ||||
|             $conn_r, | ||||
|             'JOIN %T %T ON %T.objectPHID = %Q | ||||
|               AND %T.indexKey = %s | ||||
|               AND %T.indexValue IN (%Ld)', | ||||
|             $table, | ||||
|             $alias, | ||||
|             $alias, | ||||
|             $phid_column, | ||||
|             $alias, | ||||
|             $index, | ||||
|             $alias, | ||||
|             (array)$constraint['value']); | ||||
|           break; | ||||
|         default: | ||||
|           throw new Exception("Unknown constraint type '{$type}'!"); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return implode(' ', $joins); | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1592,6 +1592,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { | ||||
|         'type' => 'php', | ||||
|         'name' => $this->getPatchPath('20130913.maniphest.1.migratesearch.php'), | ||||
|       ), | ||||
|       '20130914.usercustom.sql' => array( | ||||
|         'type' => 'sql', | ||||
|         'name' => $this->getPatchPath('20130914.usercustom.sql'), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 epriestley
					epriestley