diff --git a/resources/sql/patches/20130914.usercustom.sql b/resources/sql/patches/20130914.usercustom.sql new file mode 100644 index 0000000000..3e7cbb40be --- /dev/null +++ b/resources/sql/patches/20130914.usercustom.sql @@ -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; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 819634dc54..e54a375b71 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -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', diff --git a/src/applications/people/customfield/PhabricatorUserConfiguredCustomField.php b/src/applications/people/customfield/PhabricatorUserConfiguredCustomField.php index 456f76f609..f324ca313a 100644 --- a/src/applications/people/customfield/PhabricatorUserConfiguredCustomField.php +++ b/src/applications/people/customfield/PhabricatorUserConfiguredCustomField.php @@ -18,4 +18,12 @@ final class PhabricatorUserConfiguredCustomField return new PhabricatorUserConfiguredCustomFieldStorage(); } + protected function newStringIndexStorage() { + return new PhabricatorUserCustomFieldStringIndex(); + } + + protected function newNumericIndexStorage() { + return new PhabricatorUserCustomFieldNumericIndex(); + } + } diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php index afb5a67542..0fafe6c484 100644 --- a/src/applications/people/query/PhabricatorPeopleQuery.php +++ b/src/applications/people/query/PhabricatorPeopleQuery.php @@ -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'; + } + } diff --git a/src/applications/people/query/PhabricatorPeopleSearchEngine.php b/src/applications/people/query/PhabricatorPeopleSearchEngine.php index 0a52a437ec..2c9074957a 100644 --- a/src/applications/people/query/PhabricatorPeopleSearchEngine.php +++ b/src/applications/people/query/PhabricatorPeopleSearchEngine.php @@ -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, diff --git a/src/applications/people/storage/PhabricatorUserCustomFieldNumericIndex.php b/src/applications/people/storage/PhabricatorUserCustomFieldNumericIndex.php new file mode 100644 index 0000000000..0f36bd8e57 --- /dev/null +++ b/src/applications/people/storage/PhabricatorUserCustomFieldNumericIndex.php @@ -0,0 +1,11 @@ +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])); + } + } + } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 67cd14eb81..a44ae8e4c7 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -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; } diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php index 3538bc7053..5621e24f10 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php @@ -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 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 List of PHIDs. + * @task appsearch + */ + public function getRequiredHandlePHIDsForApplicationSearch($value) { + if ($this->proxy) { + return $this->proxy->getRequiredHandlePHIDsForApplicationSearch($value); + } + return array(); + } + + /* -( ApplicationTransactions )-------------------------------------------- */ diff --git a/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php b/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php index e5d17822f8..d4b9f0747e 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php @@ -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(); + } + } diff --git a/src/infrastructure/customfield/field/PhabricatorStandardCustomField.php b/src/infrastructure/customfield/field/PhabricatorStandardCustomField.php index 20c5526895..6fedc13e77 100644 --- a/src/infrastructure/customfield/field/PhabricatorStandardCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorStandardCustomField.php @@ -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; + } + + } } diff --git a/src/infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php b/src/infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php index 7a01fd4df7..b0780bd1c9 100644 --- a/src/infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php +++ b/src/infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php @@ -14,5 +14,6 @@ abstract class PhabricatorCustomFieldIndexStorage } abstract public function formatForInsert(AphrontDatabaseConnection $conn); + abstract public function getIndexValueType(); } diff --git a/src/infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php b/src/infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php index f7d93456f8..0b98bad40a 100644 --- a/src/infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php +++ b/src/infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php @@ -12,4 +12,8 @@ abstract class PhabricatorCustomFieldNumericIndexStorage $this->getIndexValue()); } + public function getIndexValueType() { + return 'int'; + } + } diff --git a/src/infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php b/src/infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php index 73ccc7597a..1cb840a89f 100644 --- a/src/infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php +++ b/src/infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php @@ -12,4 +12,8 @@ abstract class PhabricatorCustomFieldStringIndexStorage $this->getIndexValue()); } + public function getIndexValueType() { + return 'string'; + } + } diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index 93b0d96412..b05ea23d32 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -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 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); + } + } diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index de0b01b8a7..eb5b7cf99b 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -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'), + ), ); } }