1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-29 10:12:41 +01:00

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:
epriestley 2013-09-16 13:44:34 -07:00
parent b398ae5504
commit c8574cf6fd
17 changed files with 655 additions and 10 deletions

View 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;

View file

@ -1710,6 +1710,8 @@ phutil_register_library_map(array(
'PhabricatorUserConfiguredCustomField' => 'applications/people/customfield/PhabricatorUserConfiguredCustomField.php', 'PhabricatorUserConfiguredCustomField' => 'applications/people/customfield/PhabricatorUserConfiguredCustomField.php',
'PhabricatorUserConfiguredCustomFieldStorage' => 'applications/people/storage/PhabricatorUserConfiguredCustomFieldStorage.php', 'PhabricatorUserConfiguredCustomFieldStorage' => 'applications/people/storage/PhabricatorUserConfiguredCustomFieldStorage.php',
'PhabricatorUserCustomField' => 'applications/people/customfield/PhabricatorUserCustomField.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', 'PhabricatorUserDAO' => 'applications/people/storage/PhabricatorUserDAO.php',
'PhabricatorUserEditor' => 'applications/people/editor/PhabricatorUserEditor.php', 'PhabricatorUserEditor' => 'applications/people/editor/PhabricatorUserEditor.php',
'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php', 'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php',
@ -3857,6 +3859,8 @@ phutil_register_library_map(array(
), ),
'PhabricatorUserConfiguredCustomFieldStorage' => 'PhabricatorCustomFieldStorage', 'PhabricatorUserConfiguredCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'PhabricatorUserCustomField' => 'PhabricatorCustomField', 'PhabricatorUserCustomField' => 'PhabricatorCustomField',
'PhabricatorUserCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
'PhabricatorUserCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
'PhabricatorUserDAO' => 'PhabricatorLiskDAO', 'PhabricatorUserDAO' => 'PhabricatorLiskDAO',
'PhabricatorUserEditor' => 'PhabricatorEditor', 'PhabricatorUserEditor' => 'PhabricatorEditor',
'PhabricatorUserEmail' => 'PhabricatorUserDAO', 'PhabricatorUserEmail' => 'PhabricatorUserDAO',

View file

@ -18,4 +18,12 @@ final class PhabricatorUserConfiguredCustomField
return new PhabricatorUserConfiguredCustomFieldStorage(); return new PhabricatorUserConfiguredCustomFieldStorage();
} }
protected function newStringIndexStorage() {
return new PhabricatorUserCustomFieldStringIndex();
}
protected function newNumericIndexStorage() {
return new PhabricatorUserCustomFieldNumericIndex();
}
} }

View file

@ -101,11 +101,12 @@ final class PhabricatorPeopleQuery
$data = queryfx_all( $data = queryfx_all(
$conn_r, $conn_r,
'SELECT * FROM %T user %Q %Q %Q %Q', 'SELECT * FROM %T user %Q %Q %Q %Q %Q',
$table->getTableName(), $table->getTableName(),
$this->buildJoinsClause($conn_r), $this->buildJoinsClause($conn_r),
$this->buildWhereClause($conn_r), $this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r), $this->buildOrderClause($conn_r),
$this->buildApplicationSearchGroupClause($conn_r),
$this->buildLimitClause($conn_r)); $this->buildLimitClause($conn_r));
if ($this->needPrimaryEmail) { if ($this->needPrimaryEmail) {
@ -181,6 +182,8 @@ final class PhabricatorPeopleQuery
$email_table->getTableName()); $email_table->getTableName());
} }
$joins[] = $this->buildApplicationSearchJoinClause($conn_r);
$joins = implode(' ', $joins); $joins = implode(' ', $joins);
return $joins; return $joins;
} }
@ -270,4 +273,8 @@ final class PhabricatorPeopleQuery
return 'user.id'; return 'user.id';
} }
protected function getApplicationSearchObjectPHIDColumn() {
return 'user.phid';
}
} }

View file

@ -3,6 +3,10 @@
final class PhabricatorPeopleSearchEngine final class PhabricatorPeopleSearchEngine
extends PhabricatorApplicationSearchEngine { extends PhabricatorApplicationSearchEngine {
public function getCustomFieldObject() {
return new PhabricatorUser();
}
public function buildSavedQueryFromRequest(AphrontRequest $request) { public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery(); $saved = new PhabricatorSavedQuery();
@ -14,6 +18,8 @@ final class PhabricatorPeopleSearchEngine
$saved->setParameter('createdStart', $request->getStr('createdStart')); $saved->setParameter('createdStart', $request->getStr('createdStart'));
$saved->setParameter('createdEnd', $request->getStr('createdEnd')); $saved->setParameter('createdEnd', $request->getStr('createdEnd'));
$this->readCustomFieldsFromRequest($request, $saved);
return $saved; return $saved;
} }
@ -57,6 +63,9 @@ final class PhabricatorPeopleSearchEngine
if ($end) { if ($end) {
$query->withDateCreatedBefore($end); $query->withDateCreatedBefore($end);
} }
$this->applyCustomFieldsToQuery($query, $saved);
return $query; return $query;
} }
@ -101,6 +110,8 @@ final class PhabricatorPeopleSearchEngine
pht('Show only System Agents.'), pht('Show only System Agents.'),
$is_system_agent)); $is_system_agent));
$this->appendCustomFieldsToForm($form, $saved);
$this->buildDateRange( $this->buildDateRange(
$form, $form,
$saved, $saved,

View file

@ -0,0 +1,11 @@
<?php
final class PhabricatorUserCustomFieldNumericIndex
extends PhabricatorCustomFieldNumericIndexStorage {
public function getApplicationName() {
return 'user';
}
}

View file

@ -0,0 +1,11 @@
<?php
final class PhabricatorUserCustomFieldStringIndex
extends PhabricatorCustomFieldStringIndexStorage {
public function getApplicationName() {
return 'user';
}
}

View file

@ -15,6 +15,7 @@ abstract class PhabricatorApplicationSearchEngine {
private $viewer; private $viewer;
private $errors = array(); private $errors = array();
private $customFields = false;
public function setViewer(PhabricatorUser $viewer) { public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $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]));
}
}
} }

View file

@ -479,6 +479,19 @@ abstract class PhabricatorApplicationTransactionEditor
$this->didApplyTransactions($xactions); $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; return $xactions;
} }

View file

@ -569,9 +569,7 @@ abstract class PhabricatorCustomField {
* @task appsearch * @task appsearch
*/ */
protected function newStringIndexStorage() { protected function newStringIndexStorage() {
if ($this->proxy) { // NOTE: This intentionally isn't proxied, to avoid call cycles.
return $this->proxy->newStringIndexStorage();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this); throw new PhabricatorCustomFieldImplementationIncompleteException($this);
} }
@ -585,9 +583,7 @@ abstract class PhabricatorCustomField {
* @task appsearch * @task appsearch
*/ */
protected function newNumericIndexStorage() { protected function newNumericIndexStorage() {
if ($this->proxy) { // NOTE: This intentionally isn't proxied, to avoid call cycles.
return $this->proxy->newStringIndexStorage();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this); throw new PhabricatorCustomFieldImplementationIncompleteException($this);
} }
@ -604,7 +600,7 @@ abstract class PhabricatorCustomField {
return $this->proxy->newStringIndex(); return $this->proxy->newStringIndex();
} }
$key = $this->getFieldIndexKey(); $key = $this->getFieldIndex();
return $this->newStringIndexStorage() return $this->newStringIndexStorage()
->setIndexKey($key) ->setIndexKey($key)
->setIndexValue($value); ->setIndexValue($value);
@ -622,13 +618,103 @@ abstract class PhabricatorCustomField {
if ($this->proxy) { if ($this->proxy) {
return $this->proxy->newNumericIndex(); return $this->proxy->newNumericIndex();
} }
$key = $this->getFieldIndexKey(); $key = $this->getFieldIndex();
return $this->newNumericIndexStorage() return $this->newNumericIndexStorage()
->setIndexKey($key) ->setIndexKey($key)
->setIndexValue($value); ->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 )-------------------------------------------- */ /* -( ApplicationTransactions )-------------------------------------------- */

View file

@ -151,4 +151,72 @@ final class PhabricatorCustomFieldList extends Phobject {
return $xactions; 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();
}
} }

View file

@ -56,6 +56,10 @@ final class PhabricatorStandardCustomField
return $this; return $this;
} }
public function getFieldType() {
return $this->fieldType;
}
public function getFieldValue() { public function getFieldValue() {
return $this->fieldValue; return $this->fieldValue;
} }
@ -71,6 +75,8 @@ final class PhabricatorStandardCustomField
} }
public function setFieldConfig(array $config) { public function setFieldConfig(array $config) {
$this->setFieldType('text');
foreach ($config as $key => $value) { foreach ($config as $key => $value) {
switch ($key) { switch ($key) {
case 'name': case 'name':
@ -79,6 +85,9 @@ final class PhabricatorStandardCustomField
case 'type': case 'type':
$this->setFieldType($value); $this->setFieldType($value);
break; break;
case 'description':
$this->setFieldDescription($value);
break;
} }
} }
$this->fieldConfig = $config; $this->fieldConfig = $config;
@ -90,6 +99,7 @@ final class PhabricatorStandardCustomField
} }
/* -( PhabricatorCustomField )--------------------------------------------- */ /* -( PhabricatorCustomField )--------------------------------------------- */
@ -130,7 +140,7 @@ final class PhabricatorStandardCustomField
} }
public function renderEditControl() { public function renderEditControl() {
$type = $this->getFieldConfigValue('type', 'text'); $type = $this->getFieldType();
switch ($type) { switch ($type) {
case 'text': case 'text':
default: default:
@ -153,5 +163,75 @@ final class PhabricatorStandardCustomField
return $this->getFieldValue(); 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;
}
}
} }

View file

@ -14,5 +14,6 @@ abstract class PhabricatorCustomFieldIndexStorage
} }
abstract public function formatForInsert(AphrontDatabaseConnection $conn); abstract public function formatForInsert(AphrontDatabaseConnection $conn);
abstract public function getIndexValueType();
} }

View file

@ -12,4 +12,8 @@ abstract class PhabricatorCustomFieldNumericIndexStorage
$this->getIndexValue()); $this->getIndexValue());
} }
public function getIndexValueType() {
return 'int';
}
} }

View file

@ -12,4 +12,8 @@ abstract class PhabricatorCustomFieldStringIndexStorage
$this->getIndexValue()); $this->getIndexValue());
} }
public function getIndexValueType() {
return 'string';
}
} }

View file

@ -3,12 +3,15 @@
/** /**
* A query class which uses cursor-based paging. This paging is much more * A query class which uses cursor-based paging. This paging is much more
* performant than offset-based paging in the presence of policy filtering. * performant than offset-based paging in the presence of policy filtering.
*
* @task appsearch Integration with ApplicationSearch
*/ */
abstract class PhabricatorCursorPagedPolicyAwareQuery abstract class PhabricatorCursorPagedPolicyAwareQuery
extends PhabricatorPolicyAwareQuery { extends PhabricatorPolicyAwareQuery {
private $afterID; private $afterID;
private $beforeID; private $beforeID;
private $applicationSearchConstraints = array();
protected function getPagingColumn() { protected function getPagingColumn() {
return 'id'; return 'id';
@ -228,4 +231,163 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
return '('.implode(') OR (', $clauses).')'; 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);
}
} }

View file

@ -1592,6 +1592,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'type' => 'php', 'type' => 'php',
'name' => $this->getPatchPath('20130913.maniphest.1.migratesearch.php'), 'name' => $this->getPatchPath('20130913.maniphest.1.migratesearch.php'),
), ),
'20130914.usercustom.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('20130914.usercustom.sql'),
),
); );
} }
} }