mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-29 18:22: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:
parent
b398ae5504
commit
c8574cf6fd
17 changed files with 655 additions and 10 deletions
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',
|
'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',
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 $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]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 )-------------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,5 +14,6 @@ abstract class PhabricatorCustomFieldIndexStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract public function formatForInsert(AphrontDatabaseConnection $conn);
|
abstract public function formatForInsert(AphrontDatabaseConnection $conn);
|
||||||
|
abstract public function getIndexValueType();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,4 +12,8 @@ abstract class PhabricatorCustomFieldNumericIndexStorage
|
||||||
$this->getIndexValue());
|
$this->getIndexValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getIndexValueType() {
|
||||||
|
return 'int';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,4 +12,8 @@ abstract class PhabricatorCustomFieldStringIndexStorage
|
||||||
$this->getIndexValue());
|
$this->getIndexValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getIndexValueType() {
|
||||||
|
return 'string';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue