Add basic per-object privacy policies
Summary:
Provides a basic start for access policies. Objects expose various capabilities, like CAN_VIEW, CAN_EDIT, etc., and set a policy for each capability. We currently implement three policies, PUBLIC (anyone, including logged-out), USERS (any logged-in) and NOONE (nobody). There's also a way to provide automatic capability grants (e.g., the owner of an object can always see it, even if some capability is set to "NOONE"), but I'm not sure how great the implementation feels and it might change.
Most of the code here is providing a primitive for efficient policy-aware list queries. The problem with doing queries naively is that you have to do crazy amounts of filtering, e.g. to show the user page 6, you need to filter at least 600 objects (and likely more) before you can figure out which ones are 500-600 for them. You can't just do "LIMIT 500, 100" because that might have only 50 results, or no results. Instead, the query looks like "WHERE id > last_visible_id", and then we fetch additional pages as necessary to satisfy the request.
The general idea is that we move all data access to Query classes and have them do object filtering. The ID paging primitive allows efficient paging in most cases, and the executeOne() method provides a concise way to do policy checks for edit/view screens.
We'll probably end up with mostly broader policy UIs or configuration-based policies, but there are at least a few cases for per-object privacy (e.g., marking tasks as "Security", and restricting things to the members of projects) so I figured we'd start with a flexible primitive and the simplify it in the UI where we can.
Test Plan: Unit tests, played around in the UI with various policy settings.
Reviewers: btrahan, vrana, jungejason
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D2210
2012-04-14 10:13:29 -07:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
/**
|
Rename "IDPaged" to "CursorPaged", "executeWithPager" to "executeWith[Cursor|Offset]Pager"
Summary:
I'm trying to make progress on the policy/visibility stuff since it's a blocker for Wikimedia.
First, I want to improve Projects so they can serve as policy groups (e.g., an object can have a visibility policy like "Visible to: members of project 'security'"). However, doing this without breaking anything or snowballing into a bigger change is a bit awkward because Projects are name-ordered and we have a Conduit API which does offset paging. Rather than breaking or rewriting this stuff, I want to just continue offset paging them for now.
So I'm going to make PhabricatorPolicyQuery extend PhabricatorOffsetPagedQuery, but can't currently since the `executeWithPager` methods would clash. These methods do different things anyway and are probably better with different names.
This also generally improves the names of these classes, since cursors are not necessarily IDs (in the feed case, they're "chronlogicalKeys", for example). I did leave some of the interals as "ID" since calling them "Cursor"s (e.g., `setAfterCursor()`) seemed a little wrong -- it should maybe be `setAfterCursorPosition()`. These APIs have very limited use and can easily be made more consistent later.
Test Plan: Browsed around various affected tools; any issues here should throw/fail in a loud/obvious way.
Reviewers: vrana, btrahan
Reviewed By: vrana
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D3177
2012-08-07 11:54:06 -07:00
|
|
|
* A query class which uses cursor-based paging. This paging is much more
|
|
|
|
|
* performant than offset-based paging in the presence of policy filtering.
|
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
2013-09-16 13:44:34 -07:00
|
|
|
*
|
|
|
|
|
* @task appsearch Integration with ApplicationSearch
|
2015-04-11 18:38:54 -07:00
|
|
|
* @task paging Paging
|
2015-04-11 12:10:35 -07:00
|
|
|
* @task order Result Ordering
|
Add basic per-object privacy policies
Summary:
Provides a basic start for access policies. Objects expose various capabilities, like CAN_VIEW, CAN_EDIT, etc., and set a policy for each capability. We currently implement three policies, PUBLIC (anyone, including logged-out), USERS (any logged-in) and NOONE (nobody). There's also a way to provide automatic capability grants (e.g., the owner of an object can always see it, even if some capability is set to "NOONE"), but I'm not sure how great the implementation feels and it might change.
Most of the code here is providing a primitive for efficient policy-aware list queries. The problem with doing queries naively is that you have to do crazy amounts of filtering, e.g. to show the user page 6, you need to filter at least 600 objects (and likely more) before you can figure out which ones are 500-600 for them. You can't just do "LIMIT 500, 100" because that might have only 50 results, or no results. Instead, the query looks like "WHERE id > last_visible_id", and then we fetch additional pages as necessary to satisfy the request.
The general idea is that we move all data access to Query classes and have them do object filtering. The ID paging primitive allows efficient paging in most cases, and the executeOne() method provides a concise way to do policy checks for edit/view screens.
We'll probably end up with mostly broader policy UIs or configuration-based policies, but there are at least a few cases for per-object privacy (e.g., marking tasks as "Security", and restricting things to the members of projects) so I figured we'd start with a flexible primitive and the simplify it in the UI where we can.
Test Plan: Unit tests, played around in the UI with various policy settings.
Reviewers: btrahan, vrana, jungejason
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D2210
2012-04-14 10:13:29 -07:00
|
|
|
*/
|
2012-09-13 10:15:08 -07:00
|
|
|
abstract class PhabricatorCursorPagedPolicyAwareQuery
|
|
|
|
|
extends PhabricatorPolicyAwareQuery {
|
Add basic per-object privacy policies
Summary:
Provides a basic start for access policies. Objects expose various capabilities, like CAN_VIEW, CAN_EDIT, etc., and set a policy for each capability. We currently implement three policies, PUBLIC (anyone, including logged-out), USERS (any logged-in) and NOONE (nobody). There's also a way to provide automatic capability grants (e.g., the owner of an object can always see it, even if some capability is set to "NOONE"), but I'm not sure how great the implementation feels and it might change.
Most of the code here is providing a primitive for efficient policy-aware list queries. The problem with doing queries naively is that you have to do crazy amounts of filtering, e.g. to show the user page 6, you need to filter at least 600 objects (and likely more) before you can figure out which ones are 500-600 for them. You can't just do "LIMIT 500, 100" because that might have only 50 results, or no results. Instead, the query looks like "WHERE id > last_visible_id", and then we fetch additional pages as necessary to satisfy the request.
The general idea is that we move all data access to Query classes and have them do object filtering. The ID paging primitive allows efficient paging in most cases, and the executeOne() method provides a concise way to do policy checks for edit/view screens.
We'll probably end up with mostly broader policy UIs or configuration-based policies, but there are at least a few cases for per-object privacy (e.g., marking tasks as "Security", and restricting things to the members of projects) so I figured we'd start with a flexible primitive and the simplify it in the UI where we can.
Test Plan: Unit tests, played around in the UI with various policy settings.
Reviewers: btrahan, vrana, jungejason
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D2210
2012-04-14 10:13:29 -07:00
|
|
|
|
|
|
|
|
private $afterID;
|
|
|
|
|
private $beforeID;
|
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
2013-09-16 13:44:34 -07:00
|
|
|
private $applicationSearchConstraints = array();
|
2015-04-12 13:01:21 -07:00
|
|
|
protected $applicationSearchOrders = array();
|
2013-09-21 16:23:44 -07:00
|
|
|
private $internalPaging;
|
2015-04-11 12:10:35 -07:00
|
|
|
private $orderVector;
|
2015-04-12 15:16:55 -07:00
|
|
|
private $builtinOrder;
|
Add basic per-object privacy policies
Summary:
Provides a basic start for access policies. Objects expose various capabilities, like CAN_VIEW, CAN_EDIT, etc., and set a policy for each capability. We currently implement three policies, PUBLIC (anyone, including logged-out), USERS (any logged-in) and NOONE (nobody). There's also a way to provide automatic capability grants (e.g., the owner of an object can always see it, even if some capability is set to "NOONE"), but I'm not sure how great the implementation feels and it might change.
Most of the code here is providing a primitive for efficient policy-aware list queries. The problem with doing queries naively is that you have to do crazy amounts of filtering, e.g. to show the user page 6, you need to filter at least 600 objects (and likely more) before you can figure out which ones are 500-600 for them. You can't just do "LIMIT 500, 100" because that might have only 50 results, or no results. Instead, the query looks like "WHERE id > last_visible_id", and then we fetch additional pages as necessary to satisfy the request.
The general idea is that we move all data access to Query classes and have them do object filtering. The ID paging primitive allows efficient paging in most cases, and the executeOne() method provides a concise way to do policy checks for edit/view screens.
We'll probably end up with mostly broader policy UIs or configuration-based policies, but there are at least a few cases for per-object privacy (e.g., marking tasks as "Security", and restricting things to the members of projects) so I figured we'd start with a flexible primitive and the simplify it in the UI where we can.
Test Plan: Unit tests, played around in the UI with various policy settings.
Reviewers: btrahan, vrana, jungejason
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D2210
2012-04-14 10:13:29 -07:00
|
|
|
|
2015-04-12 16:44:15 -07:00
|
|
|
protected function getPageCursors(array $page) {
|
|
|
|
|
return array(
|
|
|
|
|
$this->getResultCursor(head($page)),
|
|
|
|
|
$this->getResultCursor(last($page)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function getResultCursor($object) {
|
|
|
|
|
if (!is_object($object)) {
|
|
|
|
|
throw new Exception(
|
|
|
|
|
pht(
|
|
|
|
|
'Expected object, got "%s".',
|
|
|
|
|
gettype($object)));
|
2013-11-22 12:34:52 -08:00
|
|
|
}
|
2015-04-12 16:44:15 -07:00
|
|
|
|
|
|
|
|
return $object->getID();
|
Add basic per-object privacy policies
Summary:
Provides a basic start for access policies. Objects expose various capabilities, like CAN_VIEW, CAN_EDIT, etc., and set a policy for each capability. We currently implement three policies, PUBLIC (anyone, including logged-out), USERS (any logged-in) and NOONE (nobody). There's also a way to provide automatic capability grants (e.g., the owner of an object can always see it, even if some capability is set to "NOONE"), but I'm not sure how great the implementation feels and it might change.
Most of the code here is providing a primitive for efficient policy-aware list queries. The problem with doing queries naively is that you have to do crazy amounts of filtering, e.g. to show the user page 6, you need to filter at least 600 objects (and likely more) before you can figure out which ones are 500-600 for them. You can't just do "LIMIT 500, 100" because that might have only 50 results, or no results. Instead, the query looks like "WHERE id > last_visible_id", and then we fetch additional pages as necessary to satisfy the request.
The general idea is that we move all data access to Query classes and have them do object filtering. The ID paging primitive allows efficient paging in most cases, and the executeOne() method provides a concise way to do policy checks for edit/view screens.
We'll probably end up with mostly broader policy UIs or configuration-based policies, but there are at least a few cases for per-object privacy (e.g., marking tasks as "Security", and restricting things to the members of projects) so I figured we'd start with a flexible primitive and the simplify it in the UI where we can.
Test Plan: Unit tests, played around in the UI with various policy settings.
Reviewers: btrahan, vrana, jungejason
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D2210
2012-04-14 10:13:29 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function nextPage(array $page) {
|
2013-09-21 16:23:44 -07:00
|
|
|
// See getPagingViewer() for a description of this flag.
|
|
|
|
|
$this->internalPaging = true;
|
|
|
|
|
|
2015-04-12 16:44:15 -07:00
|
|
|
if ($this->beforeID !== null) {
|
|
|
|
|
$page = array_reverse($page, $preserve_keys = true);
|
|
|
|
|
list($before, $after) = $this->getPageCursors($page);
|
|
|
|
|
$this->beforeID = $before;
|
Add basic per-object privacy policies
Summary:
Provides a basic start for access policies. Objects expose various capabilities, like CAN_VIEW, CAN_EDIT, etc., and set a policy for each capability. We currently implement three policies, PUBLIC (anyone, including logged-out), USERS (any logged-in) and NOONE (nobody). There's also a way to provide automatic capability grants (e.g., the owner of an object can always see it, even if some capability is set to "NOONE"), but I'm not sure how great the implementation feels and it might change.
Most of the code here is providing a primitive for efficient policy-aware list queries. The problem with doing queries naively is that you have to do crazy amounts of filtering, e.g. to show the user page 6, you need to filter at least 600 objects (and likely more) before you can figure out which ones are 500-600 for them. You can't just do "LIMIT 500, 100" because that might have only 50 results, or no results. Instead, the query looks like "WHERE id > last_visible_id", and then we fetch additional pages as necessary to satisfy the request.
The general idea is that we move all data access to Query classes and have them do object filtering. The ID paging primitive allows efficient paging in most cases, and the executeOne() method provides a concise way to do policy checks for edit/view screens.
We'll probably end up with mostly broader policy UIs or configuration-based policies, but there are at least a few cases for per-object privacy (e.g., marking tasks as "Security", and restricting things to the members of projects) so I figured we'd start with a flexible primitive and the simplify it in the UI where we can.
Test Plan: Unit tests, played around in the UI with various policy settings.
Reviewers: btrahan, vrana, jungejason
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D2210
2012-04-14 10:13:29 -07:00
|
|
|
} else {
|
2015-04-12 16:44:15 -07:00
|
|
|
list($before, $after) = $this->getPageCursors($page);
|
|
|
|
|
$this->afterID = $after;
|
Add basic per-object privacy policies
Summary:
Provides a basic start for access policies. Objects expose various capabilities, like CAN_VIEW, CAN_EDIT, etc., and set a policy for each capability. We currently implement three policies, PUBLIC (anyone, including logged-out), USERS (any logged-in) and NOONE (nobody). There's also a way to provide automatic capability grants (e.g., the owner of an object can always see it, even if some capability is set to "NOONE"), but I'm not sure how great the implementation feels and it might change.
Most of the code here is providing a primitive for efficient policy-aware list queries. The problem with doing queries naively is that you have to do crazy amounts of filtering, e.g. to show the user page 6, you need to filter at least 600 objects (and likely more) before you can figure out which ones are 500-600 for them. You can't just do "LIMIT 500, 100" because that might have only 50 results, or no results. Instead, the query looks like "WHERE id > last_visible_id", and then we fetch additional pages as necessary to satisfy the request.
The general idea is that we move all data access to Query classes and have them do object filtering. The ID paging primitive allows efficient paging in most cases, and the executeOne() method provides a concise way to do policy checks for edit/view screens.
We'll probably end up with mostly broader policy UIs or configuration-based policies, but there are at least a few cases for per-object privacy (e.g., marking tasks as "Security", and restricting things to the members of projects) so I figured we'd start with a flexible primitive and the simplify it in the UI where we can.
Test Plan: Unit tests, played around in the UI with various policy settings.
Reviewers: btrahan, vrana, jungejason
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D2210
2012-04-14 10:13:29 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final public function setAfterID($object_id) {
|
|
|
|
|
$this->afterID = $object_id;
|
|
|
|
|
return $this;
|
|
|
|
|
}
|
|
|
|
|
|
2013-04-18 16:54:06 -07:00
|
|
|
final protected function getAfterID() {
|
|
|
|
|
return $this->afterID;
|
|
|
|
|
}
|
|
|
|
|
|
Add basic per-object privacy policies
Summary:
Provides a basic start for access policies. Objects expose various capabilities, like CAN_VIEW, CAN_EDIT, etc., and set a policy for each capability. We currently implement three policies, PUBLIC (anyone, including logged-out), USERS (any logged-in) and NOONE (nobody). There's also a way to provide automatic capability grants (e.g., the owner of an object can always see it, even if some capability is set to "NOONE"), but I'm not sure how great the implementation feels and it might change.
Most of the code here is providing a primitive for efficient policy-aware list queries. The problem with doing queries naively is that you have to do crazy amounts of filtering, e.g. to show the user page 6, you need to filter at least 600 objects (and likely more) before you can figure out which ones are 500-600 for them. You can't just do "LIMIT 500, 100" because that might have only 50 results, or no results. Instead, the query looks like "WHERE id > last_visible_id", and then we fetch additional pages as necessary to satisfy the request.
The general idea is that we move all data access to Query classes and have them do object filtering. The ID paging primitive allows efficient paging in most cases, and the executeOne() method provides a concise way to do policy checks for edit/view screens.
We'll probably end up with mostly broader policy UIs or configuration-based policies, but there are at least a few cases for per-object privacy (e.g., marking tasks as "Security", and restricting things to the members of projects) so I figured we'd start with a flexible primitive and the simplify it in the UI where we can.
Test Plan: Unit tests, played around in the UI with various policy settings.
Reviewers: btrahan, vrana, jungejason
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D2210
2012-04-14 10:13:29 -07:00
|
|
|
final public function setBeforeID($object_id) {
|
|
|
|
|
$this->beforeID = $object_id;
|
|
|
|
|
return $this;
|
|
|
|
|
}
|
|
|
|
|
|
2013-04-18 16:54:06 -07:00
|
|
|
final protected function getBeforeID() {
|
|
|
|
|
return $this->beforeID;
|
|
|
|
|
}
|
|
|
|
|
|
2013-09-21 16:23:44 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the viewer for making cursor paging queries.
|
|
|
|
|
*
|
|
|
|
|
* NOTE: You should ONLY use this viewer to load cursor objects while
|
|
|
|
|
* building paging queries.
|
|
|
|
|
*
|
|
|
|
|
* Cursor paging can happen in two ways. First, the user can request a page
|
|
|
|
|
* like `/stuff/?after=33`, which explicitly causes paging. Otherwise, we
|
|
|
|
|
* can fall back to implicit paging if we filter some results out of a
|
|
|
|
|
* result list because the user can't see them and need to go fetch some more
|
|
|
|
|
* results to generate a large enough result list.
|
|
|
|
|
*
|
|
|
|
|
* In the first case, want to use the viewer's policies to load the object.
|
|
|
|
|
* This prevents an attacker from figuring out information about an object
|
|
|
|
|
* they can't see by executing queries like `/stuff/?after=33&order=name`,
|
|
|
|
|
* which would otherwise give them a hint about the name of the object.
|
|
|
|
|
* Generally, if a user can't see an object, they can't use it to page.
|
|
|
|
|
*
|
|
|
|
|
* In the second case, we need to load the object whether the user can see
|
|
|
|
|
* it or not, because we need to examine new results. For example, if a user
|
|
|
|
|
* loads `/stuff/` and we run a query for the first 100 items that they can
|
|
|
|
|
* see, but the first 100 rows in the database aren't visible, we need to
|
|
|
|
|
* be able to issue a query for the next 100 results. If we can't load the
|
|
|
|
|
* cursor object, we'll fail or issue the same query over and over again.
|
|
|
|
|
* So, generally, internal paging must bypass policy controls.
|
|
|
|
|
*
|
|
|
|
|
* This method returns the appropriate viewer, based on the context in which
|
|
|
|
|
* the paging is occuring.
|
|
|
|
|
*
|
|
|
|
|
* @return PhabricatorUser Viewer for executing paging queries.
|
|
|
|
|
*/
|
|
|
|
|
final protected function getPagingViewer() {
|
|
|
|
|
if ($this->internalPaging) {
|
|
|
|
|
return PhabricatorUser::getOmnipotentUser();
|
|
|
|
|
} else {
|
|
|
|
|
return $this->getViewer();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Add basic per-object privacy policies
Summary:
Provides a basic start for access policies. Objects expose various capabilities, like CAN_VIEW, CAN_EDIT, etc., and set a policy for each capability. We currently implement three policies, PUBLIC (anyone, including logged-out), USERS (any logged-in) and NOONE (nobody). There's also a way to provide automatic capability grants (e.g., the owner of an object can always see it, even if some capability is set to "NOONE"), but I'm not sure how great the implementation feels and it might change.
Most of the code here is providing a primitive for efficient policy-aware list queries. The problem with doing queries naively is that you have to do crazy amounts of filtering, e.g. to show the user page 6, you need to filter at least 600 objects (and likely more) before you can figure out which ones are 500-600 for them. You can't just do "LIMIT 500, 100" because that might have only 50 results, or no results. Instead, the query looks like "WHERE id > last_visible_id", and then we fetch additional pages as necessary to satisfy the request.
The general idea is that we move all data access to Query classes and have them do object filtering. The ID paging primitive allows efficient paging in most cases, and the executeOne() method provides a concise way to do policy checks for edit/view screens.
We'll probably end up with mostly broader policy UIs or configuration-based policies, but there are at least a few cases for per-object privacy (e.g., marking tasks as "Security", and restricting things to the members of projects) so I figured we'd start with a flexible primitive and the simplify it in the UI where we can.
Test Plan: Unit tests, played around in the UI with various policy settings.
Reviewers: btrahan, vrana, jungejason
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D2210
2012-04-14 10:13:29 -07:00
|
|
|
final protected function buildLimitClause(AphrontDatabaseConnection $conn_r) {
|
2012-08-08 12:15:58 -07:00
|
|
|
if ($this->getRawResultLimit()) {
|
|
|
|
|
return qsprintf($conn_r, 'LIMIT %d', $this->getRawResultLimit());
|
Add basic per-object privacy policies
Summary:
Provides a basic start for access policies. Objects expose various capabilities, like CAN_VIEW, CAN_EDIT, etc., and set a policy for each capability. We currently implement three policies, PUBLIC (anyone, including logged-out), USERS (any logged-in) and NOONE (nobody). There's also a way to provide automatic capability grants (e.g., the owner of an object can always see it, even if some capability is set to "NOONE"), but I'm not sure how great the implementation feels and it might change.
Most of the code here is providing a primitive for efficient policy-aware list queries. The problem with doing queries naively is that you have to do crazy amounts of filtering, e.g. to show the user page 6, you need to filter at least 600 objects (and likely more) before you can figure out which ones are 500-600 for them. You can't just do "LIMIT 500, 100" because that might have only 50 results, or no results. Instead, the query looks like "WHERE id > last_visible_id", and then we fetch additional pages as necessary to satisfy the request.
The general idea is that we move all data access to Query classes and have them do object filtering. The ID paging primitive allows efficient paging in most cases, and the executeOne() method provides a concise way to do policy checks for edit/view screens.
We'll probably end up with mostly broader policy UIs or configuration-based policies, but there are at least a few cases for per-object privacy (e.g., marking tasks as "Security", and restricting things to the members of projects) so I figured we'd start with a flexible primitive and the simplify it in the UI where we can.
Test Plan: Unit tests, played around in the UI with various policy settings.
Reviewers: btrahan, vrana, jungejason
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D2210
2012-04-14 10:13:29 -07:00
|
|
|
} else {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Allow policy-aware queries to prefilter results
Summary:
Provides a simple way for policy-aware queries to pre-filter results without needing to maintain separate cursors, and fixes a bunch of filter-related edge cases.
- For reverse-paged cursor queries, we previously reversed each individual set of results. If the final result set is built out of multiple pages, it's in the wrong order overall, with each page in the correct order in sequence. Instead, reverse everything at the end. This also simplifies construction of queries.
- `AphrontCursorPagerView` would always render a "<< First" link when paging backward, even if we were on the first page of results.
- Add a filtering hook to let queries perform in-application pre-policy filtering as simply as possible (i.e., without maintaing their own cursors over the result sets).
Test Plan: Made feed randomly prefilter half the results, and paged forward and backward. Observed correct result ordering, pagination, and next/previous links.
Reviewers: btrahan, vrana
Reviewed By: btrahan
CC: aran
Differential Revision: https://secure.phabricator.com/D3787
2012-10-23 12:01:11 -07:00
|
|
|
final protected function didLoadResults(array $results) {
|
Add basic per-object privacy policies
Summary:
Provides a basic start for access policies. Objects expose various capabilities, like CAN_VIEW, CAN_EDIT, etc., and set a policy for each capability. We currently implement three policies, PUBLIC (anyone, including logged-out), USERS (any logged-in) and NOONE (nobody). There's also a way to provide automatic capability grants (e.g., the owner of an object can always see it, even if some capability is set to "NOONE"), but I'm not sure how great the implementation feels and it might change.
Most of the code here is providing a primitive for efficient policy-aware list queries. The problem with doing queries naively is that you have to do crazy amounts of filtering, e.g. to show the user page 6, you need to filter at least 600 objects (and likely more) before you can figure out which ones are 500-600 for them. You can't just do "LIMIT 500, 100" because that might have only 50 results, or no results. Instead, the query looks like "WHERE id > last_visible_id", and then we fetch additional pages as necessary to satisfy the request.
The general idea is that we move all data access to Query classes and have them do object filtering. The ID paging primitive allows efficient paging in most cases, and the executeOne() method provides a concise way to do policy checks for edit/view screens.
We'll probably end up with mostly broader policy UIs or configuration-based policies, but there are at least a few cases for per-object privacy (e.g., marking tasks as "Security", and restricting things to the members of projects) so I figured we'd start with a flexible primitive and the simplify it in the UI where we can.
Test Plan: Unit tests, played around in the UI with various policy settings.
Reviewers: btrahan, vrana, jungejason
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D2210
2012-04-14 10:13:29 -07:00
|
|
|
if ($this->beforeID) {
|
|
|
|
|
$results = array_reverse($results, $preserve_keys = true);
|
|
|
|
|
}
|
|
|
|
|
return $results;
|
|
|
|
|
}
|
|
|
|
|
|
Rename "IDPaged" to "CursorPaged", "executeWithPager" to "executeWith[Cursor|Offset]Pager"
Summary:
I'm trying to make progress on the policy/visibility stuff since it's a blocker for Wikimedia.
First, I want to improve Projects so they can serve as policy groups (e.g., an object can have a visibility policy like "Visible to: members of project 'security'"). However, doing this without breaking anything or snowballing into a bigger change is a bit awkward because Projects are name-ordered and we have a Conduit API which does offset paging. Rather than breaking or rewriting this stuff, I want to just continue offset paging them for now.
So I'm going to make PhabricatorPolicyQuery extend PhabricatorOffsetPagedQuery, but can't currently since the `executeWithPager` methods would clash. These methods do different things anyway and are probably better with different names.
This also generally improves the names of these classes, since cursors are not necessarily IDs (in the feed case, they're "chronlogicalKeys", for example). I did leave some of the interals as "ID" since calling them "Cursor"s (e.g., `setAfterCursor()`) seemed a little wrong -- it should maybe be `setAfterCursorPosition()`. These APIs have very limited use and can easily be made more consistent later.
Test Plan: Browsed around various affected tools; any issues here should throw/fail in a loud/obvious way.
Reviewers: vrana, btrahan
Reviewed By: vrana
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D3177
2012-08-07 11:54:06 -07:00
|
|
|
final public function executeWithCursorPager(AphrontCursorPagerView $pager) {
|
2015-04-12 16:44:15 -07:00
|
|
|
$limit = $pager->getPageSize();
|
|
|
|
|
|
|
|
|
|
$this->setLimit($limit + 1);
|
Add basic per-object privacy policies
Summary:
Provides a basic start for access policies. Objects expose various capabilities, like CAN_VIEW, CAN_EDIT, etc., and set a policy for each capability. We currently implement three policies, PUBLIC (anyone, including logged-out), USERS (any logged-in) and NOONE (nobody). There's also a way to provide automatic capability grants (e.g., the owner of an object can always see it, even if some capability is set to "NOONE"), but I'm not sure how great the implementation feels and it might change.
Most of the code here is providing a primitive for efficient policy-aware list queries. The problem with doing queries naively is that you have to do crazy amounts of filtering, e.g. to show the user page 6, you need to filter at least 600 objects (and likely more) before you can figure out which ones are 500-600 for them. You can't just do "LIMIT 500, 100" because that might have only 50 results, or no results. Instead, the query looks like "WHERE id > last_visible_id", and then we fetch additional pages as necessary to satisfy the request.
The general idea is that we move all data access to Query classes and have them do object filtering. The ID paging primitive allows efficient paging in most cases, and the executeOne() method provides a concise way to do policy checks for edit/view screens.
We'll probably end up with mostly broader policy UIs or configuration-based policies, but there are at least a few cases for per-object privacy (e.g., marking tasks as "Security", and restricting things to the members of projects) so I figured we'd start with a flexible primitive and the simplify it in the UI where we can.
Test Plan: Unit tests, played around in the UI with various policy settings.
Reviewers: btrahan, vrana, jungejason
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D2210
2012-04-14 10:13:29 -07:00
|
|
|
|
|
|
|
|
if ($pager->getAfterID()) {
|
|
|
|
|
$this->setAfterID($pager->getAfterID());
|
|
|
|
|
} else if ($pager->getBeforeID()) {
|
|
|
|
|
$this->setBeforeID($pager->getBeforeID());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$results = $this->execute();
|
2015-04-12 16:44:15 -07:00
|
|
|
$count = count($results);
|
Add basic per-object privacy policies
Summary:
Provides a basic start for access policies. Objects expose various capabilities, like CAN_VIEW, CAN_EDIT, etc., and set a policy for each capability. We currently implement three policies, PUBLIC (anyone, including logged-out), USERS (any logged-in) and NOONE (nobody). There's also a way to provide automatic capability grants (e.g., the owner of an object can always see it, even if some capability is set to "NOONE"), but I'm not sure how great the implementation feels and it might change.
Most of the code here is providing a primitive for efficient policy-aware list queries. The problem with doing queries naively is that you have to do crazy amounts of filtering, e.g. to show the user page 6, you need to filter at least 600 objects (and likely more) before you can figure out which ones are 500-600 for them. You can't just do "LIMIT 500, 100" because that might have only 50 results, or no results. Instead, the query looks like "WHERE id > last_visible_id", and then we fetch additional pages as necessary to satisfy the request.
The general idea is that we move all data access to Query classes and have them do object filtering. The ID paging primitive allows efficient paging in most cases, and the executeOne() method provides a concise way to do policy checks for edit/view screens.
We'll probably end up with mostly broader policy UIs or configuration-based policies, but there are at least a few cases for per-object privacy (e.g., marking tasks as "Security", and restricting things to the members of projects) so I figured we'd start with a flexible primitive and the simplify it in the UI where we can.
Test Plan: Unit tests, played around in the UI with various policy settings.
Reviewers: btrahan, vrana, jungejason
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D2210
2012-04-14 10:13:29 -07:00
|
|
|
|
|
|
|
|
$sliced_results = $pager->sliceResults($results);
|
2013-09-13 07:13:06 -07:00
|
|
|
if ($sliced_results) {
|
2015-04-12 16:44:15 -07:00
|
|
|
list($before, $after) = $this->getPageCursors($sliced_results);
|
|
|
|
|
|
|
|
|
|
if ($pager->getBeforeID() || ($count > $limit)) {
|
|
|
|
|
$pager->setNextPageID($after);
|
2013-09-13 07:13:06 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($pager->getAfterID() ||
|
2015-04-12 16:44:15 -07:00
|
|
|
($pager->getBeforeID() && ($count > $limit))) {
|
|
|
|
|
$pager->setPrevPageID($before);
|
2013-09-13 07:13:06 -07:00
|
|
|
}
|
Add basic per-object privacy policies
Summary:
Provides a basic start for access policies. Objects expose various capabilities, like CAN_VIEW, CAN_EDIT, etc., and set a policy for each capability. We currently implement three policies, PUBLIC (anyone, including logged-out), USERS (any logged-in) and NOONE (nobody). There's also a way to provide automatic capability grants (e.g., the owner of an object can always see it, even if some capability is set to "NOONE"), but I'm not sure how great the implementation feels and it might change.
Most of the code here is providing a primitive for efficient policy-aware list queries. The problem with doing queries naively is that you have to do crazy amounts of filtering, e.g. to show the user page 6, you need to filter at least 600 objects (and likely more) before you can figure out which ones are 500-600 for them. You can't just do "LIMIT 500, 100" because that might have only 50 results, or no results. Instead, the query looks like "WHERE id > last_visible_id", and then we fetch additional pages as necessary to satisfy the request.
The general idea is that we move all data access to Query classes and have them do object filtering. The ID paging primitive allows efficient paging in most cases, and the executeOne() method provides a concise way to do policy checks for edit/view screens.
We'll probably end up with mostly broader policy UIs or configuration-based policies, but there are at least a few cases for per-object privacy (e.g., marking tasks as "Security", and restricting things to the members of projects) so I figured we'd start with a flexible primitive and the simplify it in the UI where we can.
Test Plan: Unit tests, played around in the UI with various policy settings.
Reviewers: btrahan, vrana, jungejason
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D2210
2012-04-14 10:13:29 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $sliced_results;
|
|
|
|
|
}
|
|
|
|
|
|
2013-09-13 06:23:09 -07:00
|
|
|
|
2015-04-11 19:00:53 -07:00
|
|
|
/**
|
|
|
|
|
* Return the alias this query uses to identify the primary table.
|
|
|
|
|
*
|
|
|
|
|
* Some automatic query constructions may need to be qualified with a table
|
|
|
|
|
* alias if the query performs joins which make column names ambiguous. If
|
|
|
|
|
* this is the case, return the alias for the primary table the query
|
|
|
|
|
* uses; generally the object table which has `id` and `phid` columns.
|
|
|
|
|
*
|
|
|
|
|
* @return string Alias for the primary table.
|
|
|
|
|
*/
|
|
|
|
|
protected function getPrimaryTableAlias() {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-12 13:01:21 -07:00
|
|
|
protected function newResultObject() {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-11 19:00:53 -07:00
|
|
|
|
2015-04-11 18:38:54 -07:00
|
|
|
/* -( Paging )------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function buildPagingClause(AphrontDatabaseConnection $conn) {
|
|
|
|
|
$orderable = $this->getOrderableColumns();
|
|
|
|
|
$vector = $this->getOrderVector();
|
|
|
|
|
|
|
|
|
|
if ($this->beforeID !== null) {
|
|
|
|
|
$cursor = $this->beforeID;
|
|
|
|
|
$reversed = true;
|
|
|
|
|
} else if ($this->afterID !== null) {
|
|
|
|
|
$cursor = $this->afterID;
|
|
|
|
|
$reversed = false;
|
|
|
|
|
} else {
|
|
|
|
|
// No paging is being applied to this query so we do not need to
|
|
|
|
|
// construct a paging clause.
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$keys = array();
|
|
|
|
|
foreach ($vector as $order) {
|
|
|
|
|
$keys[] = $order->getOrderKey();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$value_map = $this->getPagingValueMap($cursor, $keys);
|
|
|
|
|
|
|
|
|
|
$columns = array();
|
|
|
|
|
foreach ($vector as $order) {
|
|
|
|
|
$key = $order->getOrderKey();
|
|
|
|
|
|
|
|
|
|
if (!array_key_exists($key, $value_map)) {
|
|
|
|
|
throw new Exception(
|
|
|
|
|
pht(
|
|
|
|
|
'Query "%s" failed to return a value from getPagingValueMap() '.
|
|
|
|
|
'for column "%s".',
|
|
|
|
|
get_class($this),
|
|
|
|
|
$key));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$column = $orderable[$key];
|
|
|
|
|
$column['value'] = $value_map[$key];
|
|
|
|
|
|
|
|
|
|
$columns[] = $column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->buildPagingClauseFromMultipleColumns(
|
|
|
|
|
$conn,
|
|
|
|
|
$columns,
|
|
|
|
|
array(
|
|
|
|
|
'reversed' => $reversed,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function getPagingValueMap($cursor, array $keys) {
|
|
|
|
|
// TODO: This is a hack to make this work with existing classes for now.
|
|
|
|
|
return array(
|
|
|
|
|
'id' => $cursor,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-11 19:25:25 -07:00
|
|
|
protected function loadCursorObject($cursor) {
|
|
|
|
|
$query = newv(get_class($this), array())
|
|
|
|
|
->setViewer($this->getPagingViewer())
|
|
|
|
|
->withIDs(array((int)$cursor));
|
|
|
|
|
|
2015-04-11 19:58:18 -07:00
|
|
|
$this->willExecuteCursorQuery($query);
|
|
|
|
|
|
2015-04-11 19:25:25 -07:00
|
|
|
$object = $query->executeOne();
|
|
|
|
|
if (!$object) {
|
|
|
|
|
throw new Exception(
|
|
|
|
|
pht(
|
|
|
|
|
'Cursor "%s" does not identify a valid object.',
|
|
|
|
|
$cursor));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $object;
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-11 19:58:18 -07:00
|
|
|
protected function willExecuteCursorQuery(
|
|
|
|
|
PhabricatorCursorPagedPolicyAwareQuery $query) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-11 18:38:54 -07:00
|
|
|
|
2013-09-13 06:23:09 -07:00
|
|
|
/**
|
|
|
|
|
* Simplifies the task of constructing a paging clause across multiple
|
|
|
|
|
* columns. In the general case, this looks like:
|
|
|
|
|
*
|
|
|
|
|
* A > a OR (A = a AND B > b) OR (A = a AND B = b AND C > c)
|
|
|
|
|
*
|
|
|
|
|
* To build a clause, specify the name, type, and value of each column
|
|
|
|
|
* to include:
|
|
|
|
|
*
|
|
|
|
|
* $this->buildPagingClauseFromMultipleColumns(
|
|
|
|
|
* $conn_r,
|
|
|
|
|
* array(
|
|
|
|
|
* array(
|
2015-04-11 17:39:05 -07:00
|
|
|
* 'table' => 't',
|
|
|
|
|
* 'column' => 'title',
|
2013-09-13 06:23:09 -07:00
|
|
|
* 'type' => 'string',
|
|
|
|
|
* 'value' => $cursor->getTitle(),
|
|
|
|
|
* 'reverse' => true,
|
|
|
|
|
* ),
|
|
|
|
|
* array(
|
2015-04-11 17:39:05 -07:00
|
|
|
* 'table' => 't',
|
|
|
|
|
* 'column' => 'id',
|
2013-09-13 06:23:09 -07:00
|
|
|
* 'type' => 'int',
|
|
|
|
|
* 'value' => $cursor->getID(),
|
|
|
|
|
* ),
|
|
|
|
|
* ),
|
|
|
|
|
* array(
|
|
|
|
|
* 'reversed' => $is_reversed,
|
|
|
|
|
* ));
|
|
|
|
|
*
|
|
|
|
|
* This method will then return a composable clause for inclusion in WHERE.
|
|
|
|
|
*
|
|
|
|
|
* @param AphrontDatabaseConnection Connection query will execute on.
|
|
|
|
|
* @param list<map> Column description dictionaries.
|
|
|
|
|
* @param map Additional constuction options.
|
|
|
|
|
* @return string Query clause.
|
|
|
|
|
*/
|
|
|
|
|
final protected function buildPagingClauseFromMultipleColumns(
|
|
|
|
|
AphrontDatabaseConnection $conn,
|
|
|
|
|
array $columns,
|
|
|
|
|
array $options) {
|
|
|
|
|
|
|
|
|
|
foreach ($columns as $column) {
|
|
|
|
|
PhutilTypeSpec::checkMap(
|
|
|
|
|
$column,
|
|
|
|
|
array(
|
2015-04-11 18:38:54 -07:00
|
|
|
'table' => 'optional string|null',
|
2015-04-11 17:39:05 -07:00
|
|
|
'column' => 'string',
|
2013-09-13 06:23:09 -07:00
|
|
|
'value' => 'wild',
|
|
|
|
|
'type' => 'string',
|
|
|
|
|
'reverse' => 'optional bool',
|
2015-04-11 18:38:54 -07:00
|
|
|
'unique' => 'optional bool',
|
2015-04-11 22:26:19 -07:00
|
|
|
'null' => 'optional string|null',
|
2013-09-13 06:23:09 -07:00
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PhutilTypeSpec::checkMap(
|
|
|
|
|
$options,
|
|
|
|
|
array(
|
|
|
|
|
'reversed' => 'optional bool',
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
$is_query_reversed = idx($options, 'reversed', false);
|
|
|
|
|
|
|
|
|
|
$clauses = array();
|
|
|
|
|
$accumulated = array();
|
|
|
|
|
$last_key = last_key($columns);
|
|
|
|
|
foreach ($columns as $key => $column) {
|
|
|
|
|
$type = $column['type'];
|
2015-04-11 22:26:19 -07:00
|
|
|
|
|
|
|
|
$null = idx($column, 'null');
|
|
|
|
|
if ($column['value'] === null) {
|
|
|
|
|
if ($null) {
|
|
|
|
|
$value = null;
|
|
|
|
|
} else {
|
|
|
|
|
throw new Exception(
|
|
|
|
|
pht(
|
|
|
|
|
'Column "%s" has null value, but does not specify a null '.
|
|
|
|
|
'behavior.',
|
|
|
|
|
$key));
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
switch ($type) {
|
|
|
|
|
case 'int':
|
|
|
|
|
$value = qsprintf($conn, '%d', $column['value']);
|
|
|
|
|
break;
|
|
|
|
|
case 'float':
|
|
|
|
|
$value = qsprintf($conn, '%f', $column['value']);
|
|
|
|
|
break;
|
|
|
|
|
case 'string':
|
|
|
|
|
$value = qsprintf($conn, '%s', $column['value']);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new Exception(
|
|
|
|
|
pht(
|
|
|
|
|
'Column "%s" has unknown column type "%s".',
|
|
|
|
|
$column['column'],
|
|
|
|
|
$type));
|
|
|
|
|
}
|
2013-09-13 06:23:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$is_column_reversed = idx($column, 'reverse', false);
|
|
|
|
|
$reverse = ($is_query_reversed xor $is_column_reversed);
|
|
|
|
|
|
|
|
|
|
$clause = $accumulated;
|
2015-04-11 17:39:05 -07:00
|
|
|
|
|
|
|
|
$table_name = idx($column, 'table');
|
|
|
|
|
$column_name = $column['column'];
|
|
|
|
|
if ($table_name !== null) {
|
|
|
|
|
$field = qsprintf($conn, '%T.%T', $table_name, $column_name);
|
|
|
|
|
} else {
|
|
|
|
|
$field = qsprintf($conn, '%T', $column_name);
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-11 22:26:19 -07:00
|
|
|
$parts = array();
|
|
|
|
|
if ($null) {
|
|
|
|
|
$can_page_if_null = ($null === 'head');
|
|
|
|
|
$can_page_if_nonnull = ($null === 'tail');
|
|
|
|
|
|
|
|
|
|
if ($reverse) {
|
|
|
|
|
$can_page_if_null = !$can_page_if_null;
|
|
|
|
|
$can_page_if_nonnull = !$can_page_if_nonnull;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$subclause = null;
|
|
|
|
|
if ($can_page_if_null && $value === null) {
|
|
|
|
|
$parts[] = qsprintf(
|
|
|
|
|
$conn,
|
|
|
|
|
'(%Q IS NOT NULL)',
|
|
|
|
|
$field);
|
|
|
|
|
} else if ($can_page_if_nonnull && $value !== null) {
|
|
|
|
|
$parts[] = qsprintf(
|
|
|
|
|
$conn,
|
|
|
|
|
'(%Q IS NULL)',
|
|
|
|
|
$field);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($value !== null) {
|
|
|
|
|
$parts[] = qsprintf(
|
|
|
|
|
$conn,
|
|
|
|
|
'%Q %Q %Q',
|
|
|
|
|
$field,
|
|
|
|
|
$reverse ? '>' : '<',
|
|
|
|
|
$value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($parts) {
|
|
|
|
|
if (count($parts) > 1) {
|
|
|
|
|
$clause[] = '('.implode(') OR (', $parts).')';
|
|
|
|
|
} else {
|
|
|
|
|
$clause[] = head($parts);
|
|
|
|
|
}
|
2015-04-11 17:39:05 -07:00
|
|
|
}
|
|
|
|
|
|
2015-04-11 22:26:19 -07:00
|
|
|
if ($clause) {
|
|
|
|
|
if (count($clause) > 1) {
|
|
|
|
|
$clauses[] = '('.implode(') AND (', $clause).')';
|
|
|
|
|
} else {
|
|
|
|
|
$clauses[] = head($clause);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($value === null) {
|
|
|
|
|
$accumulated[] = qsprintf(
|
|
|
|
|
$conn,
|
|
|
|
|
'%Q IS NULL',
|
|
|
|
|
$field);
|
|
|
|
|
} else {
|
|
|
|
|
$accumulated[] = qsprintf(
|
|
|
|
|
$conn,
|
|
|
|
|
'%Q = %Q',
|
|
|
|
|
$field,
|
|
|
|
|
$value);
|
|
|
|
|
}
|
2013-09-13 06:23:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return '('.implode(') OR (', $clauses).')';
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-11 12:10:35 -07:00
|
|
|
|
|
|
|
|
/* -( Result Ordering )---------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2015-04-12 15:16:55 -07:00
|
|
|
* Select a result ordering.
|
|
|
|
|
*
|
|
|
|
|
* This is a high-level method which selects an ordering from a predefined
|
|
|
|
|
* list of builtin orders, as provided by @{method:getBuiltinOrders}. These
|
|
|
|
|
* options are user-facing and not exhaustive, but are generally convenient
|
|
|
|
|
* and meaningful.
|
|
|
|
|
*
|
|
|
|
|
* You can also use @{method:setOrderVector} to specify a low-level ordering
|
|
|
|
|
* across individual orderable columns. This offers greater control but is
|
|
|
|
|
* also more involved.
|
|
|
|
|
*
|
|
|
|
|
* @param string Key of a builtin order supported by this query.
|
|
|
|
|
* @return this
|
|
|
|
|
* @task order
|
|
|
|
|
*/
|
|
|
|
|
public function setOrder($order) {
|
|
|
|
|
$orders = $this->getBuiltinOrders();
|
|
|
|
|
|
|
|
|
|
if (empty($orders[$order])) {
|
|
|
|
|
throw new Exception(
|
|
|
|
|
pht(
|
|
|
|
|
'Query "%s" does not support a builtin order "%s". Supported orders '.
|
|
|
|
|
'are: %s.',
|
|
|
|
|
get_class($this),
|
|
|
|
|
$order,
|
|
|
|
|
implode(', ', array_keys($orders))));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->builtinOrder = $order;
|
|
|
|
|
$this->orderVector = null;
|
|
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Select the default builtin result ordering.
|
|
|
|
|
*
|
|
|
|
|
* This sets the result order to the default order among the builtin result
|
|
|
|
|
* orders (see @{method:getBuiltinOrders}). This is often the same as the
|
|
|
|
|
* query's builtin default order vector, but some objects have different
|
|
|
|
|
* default vectors (which are internally-facing) and builtin orders (which
|
|
|
|
|
* are user-facing).
|
|
|
|
|
*
|
|
|
|
|
* For example, repositories sort by ID internally (which is efficient and
|
|
|
|
|
* consistent), but sort by most recent commit as a default builtin (which
|
|
|
|
|
* better aligns with user expectations).
|
|
|
|
|
*
|
|
|
|
|
* @return this
|
|
|
|
|
*/
|
|
|
|
|
public function setDefaultBuiltinOrder() {
|
|
|
|
|
return $this->setOrder(head_key($this->getBuiltinOrders()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get builtin orders for this class.
|
|
|
|
|
*
|
|
|
|
|
* In application UIs, we want to be able to present users with a small
|
|
|
|
|
* selection of meaningful order options (like "Order by Title") rather than
|
|
|
|
|
* an exhaustive set of column ordering options.
|
|
|
|
|
*
|
|
|
|
|
* Meaningful user-facing orders are often really orders across multiple
|
|
|
|
|
* columns: for example, a "title" ordering is usually implemented as a
|
|
|
|
|
* "title, id" ordering under the hood.
|
|
|
|
|
*
|
|
|
|
|
* Builtin orders provide a mapping from convenient, understandable
|
|
|
|
|
* user-facing orders to implementations.
|
|
|
|
|
*
|
|
|
|
|
* A builtin order should provide these keys:
|
|
|
|
|
*
|
|
|
|
|
* - `vector` (`list<string>`): The actual order vector to use.
|
|
|
|
|
* - `name` (`string`): Human-readable order name.
|
|
|
|
|
*
|
|
|
|
|
* @return map<string, wild> Map from builtin order keys to specification.
|
|
|
|
|
* @task order
|
|
|
|
|
*/
|
|
|
|
|
public function getBuiltinOrders() {
|
|
|
|
|
$orders = array(
|
|
|
|
|
'newest' => array(
|
|
|
|
|
'vector' => array('id'),
|
|
|
|
|
'name' => pht('Creation (Newest First)'),
|
|
|
|
|
'aliases' => array('created'),
|
|
|
|
|
),
|
|
|
|
|
'oldest' => array(
|
|
|
|
|
'vector' => array('-id'),
|
|
|
|
|
'name' => pht('Creation (Oldest First)'),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$object = $this->newResultObject();
|
|
|
|
|
if ($object instanceof PhabricatorCustomFieldInterface) {
|
|
|
|
|
$list = PhabricatorCustomField::getObjectFields(
|
|
|
|
|
$object,
|
|
|
|
|
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
|
|
|
|
|
foreach ($list->getFields() as $field) {
|
|
|
|
|
$index = $field->buildOrderIndex();
|
|
|
|
|
if (!$index) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$key = $field->getFieldKey();
|
|
|
|
|
$digest = $field->getFieldIndex();
|
|
|
|
|
|
|
|
|
|
$full_key = 'custom:'.$key;
|
|
|
|
|
$orders[$full_key] = array(
|
|
|
|
|
'vector' => array($full_key, 'id'),
|
|
|
|
|
'name' => $field->getFieldName(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $orders;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set a low-level column ordering.
|
|
|
|
|
*
|
|
|
|
|
* This is a low-level method which offers granular control over column
|
|
|
|
|
* ordering. In most cases, applications can more easily use
|
|
|
|
|
* @{method:setOrder} to choose a high-level builtin order.
|
|
|
|
|
*
|
|
|
|
|
* To set an order vector, specify a list of order keys as provided by
|
|
|
|
|
* @{method:getOrderableColumns}.
|
|
|
|
|
*
|
|
|
|
|
* @param PhabricatorQueryOrderVector|list<string> List of order keys.
|
|
|
|
|
* @return this
|
2015-04-11 12:10:35 -07:00
|
|
|
* @task order
|
|
|
|
|
*/
|
|
|
|
|
public function setOrderVector($vector) {
|
|
|
|
|
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
|
|
|
|
|
|
|
|
|
|
$orderable = $this->getOrderableColumns();
|
2015-04-11 18:38:54 -07:00
|
|
|
|
|
|
|
|
// Make sure that all the components identify valid columns.
|
|
|
|
|
$unique = array();
|
2015-04-11 12:10:35 -07:00
|
|
|
foreach ($vector as $order) {
|
2015-04-11 18:38:54 -07:00
|
|
|
$key = $order->getOrderKey();
|
2015-04-11 12:10:35 -07:00
|
|
|
if (empty($orderable[$key])) {
|
|
|
|
|
$valid = implode(', ', array_keys($orderable));
|
|
|
|
|
throw new Exception(
|
|
|
|
|
pht(
|
|
|
|
|
'This query ("%s") does not support sorting by order key "%s". '.
|
|
|
|
|
'Supported orders are: %s.',
|
|
|
|
|
get_class($this),
|
2015-04-11 18:38:54 -07:00
|
|
|
$key,
|
2015-04-11 12:10:35 -07:00
|
|
|
$valid));
|
|
|
|
|
}
|
2015-04-11 18:38:54 -07:00
|
|
|
|
|
|
|
|
$unique[$key] = idx($orderable[$key], 'unique', false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Make sure that the last column is unique so that this is a strong
|
|
|
|
|
// ordering which can be used for paging.
|
|
|
|
|
$last = last($unique);
|
|
|
|
|
if ($last !== true) {
|
|
|
|
|
throw new Exception(
|
|
|
|
|
pht(
|
|
|
|
|
'Order vector "%s" is invalid: the last column in an order must '.
|
|
|
|
|
'be a column with unique values, but "%s" is not unique.',
|
|
|
|
|
$vector->getAsString(),
|
|
|
|
|
last_key($unique)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Make sure that other columns are not unique; an ordering like "id, name"
|
|
|
|
|
// does not make sense because only "id" can ever have an effect.
|
|
|
|
|
array_pop($unique);
|
|
|
|
|
foreach ($unique as $key => $is_unique) {
|
|
|
|
|
if ($is_unique) {
|
|
|
|
|
throw new Exception(
|
|
|
|
|
pht(
|
|
|
|
|
'Order vector "%s" is invalid: only the last column in an order '.
|
|
|
|
|
'may be unique, but "%s" is a unique column and not the last '.
|
|
|
|
|
'column in the order.',
|
|
|
|
|
$vector->getAsString(),
|
|
|
|
|
$key));
|
|
|
|
|
}
|
2015-04-11 12:10:35 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->orderVector = $vector;
|
|
|
|
|
return $this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2015-04-12 15:16:55 -07:00
|
|
|
* Get the effective order vector.
|
|
|
|
|
*
|
|
|
|
|
* @return PhabricatorQueryOrderVector Effective vector.
|
2015-04-11 12:10:35 -07:00
|
|
|
* @task order
|
|
|
|
|
*/
|
2015-04-11 19:58:18 -07:00
|
|
|
protected function getOrderVector() {
|
2015-04-11 12:10:35 -07:00
|
|
|
if (!$this->orderVector) {
|
2015-04-12 15:16:55 -07:00
|
|
|
if ($this->builtinOrder !== null) {
|
|
|
|
|
$builtin_order = idx($this->getBuiltinOrders(), $this->builtinOrder);
|
|
|
|
|
$vector = $builtin_order['vector'];
|
|
|
|
|
} else {
|
|
|
|
|
$vector = $this->getDefaultOrderVector();
|
|
|
|
|
}
|
2015-04-11 12:10:35 -07:00
|
|
|
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
|
2015-04-11 18:38:54 -07:00
|
|
|
|
|
|
|
|
// We call setOrderVector() here to apply checks to the default vector.
|
|
|
|
|
// This catches any errors in the implementation.
|
|
|
|
|
$this->setOrderVector($vector);
|
2015-04-11 12:10:35 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->orderVector;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @task order
|
|
|
|
|
*/
|
|
|
|
|
protected function getDefaultOrderVector() {
|
|
|
|
|
return array('id');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @task order
|
|
|
|
|
*/
|
|
|
|
|
public function getOrderableColumns() {
|
2015-04-12 13:01:21 -07:00
|
|
|
$columns = array(
|
2015-04-11 12:10:35 -07:00
|
|
|
'id' => array(
|
2015-04-11 19:00:53 -07:00
|
|
|
'table' => $this->getPrimaryTableAlias(),
|
2015-04-11 12:10:35 -07:00
|
|
|
'column' => 'id',
|
|
|
|
|
'reverse' => false,
|
2015-04-11 18:38:54 -07:00
|
|
|
'type' => 'int',
|
|
|
|
|
'unique' => true,
|
2015-04-11 12:10:35 -07:00
|
|
|
),
|
|
|
|
|
);
|
2015-04-12 13:01:21 -07:00
|
|
|
|
|
|
|
|
$object = $this->newResultObject();
|
|
|
|
|
if ($object instanceof PhabricatorCustomFieldInterface) {
|
|
|
|
|
$list = PhabricatorCustomField::getObjectFields(
|
|
|
|
|
$object,
|
|
|
|
|
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
|
|
|
|
|
foreach ($list->getFields() as $field) {
|
|
|
|
|
$index = $field->buildOrderIndex();
|
|
|
|
|
if (!$index) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$key = $field->getFieldKey();
|
|
|
|
|
$digest = $field->getFieldIndex();
|
|
|
|
|
|
|
|
|
|
$full_key = 'custom:'.$key;
|
|
|
|
|
$columns[$full_key] = array(
|
|
|
|
|
'table' => 'appsearch_order_'.$digest,
|
|
|
|
|
'column' => 'indexValue',
|
|
|
|
|
'type' => $index->getIndexValueType(),
|
|
|
|
|
'null' => 'tail',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $columns;
|
2015-04-11 12:10:35 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @task order
|
|
|
|
|
*/
|
|
|
|
|
final protected function buildOrderClause(AphrontDatabaseConnection $conn) {
|
|
|
|
|
$orderable = $this->getOrderableColumns();
|
|
|
|
|
$vector = $this->getOrderVector();
|
|
|
|
|
|
|
|
|
|
$parts = array();
|
|
|
|
|
foreach ($vector as $order) {
|
|
|
|
|
$part = $orderable[$order->getOrderKey()];
|
|
|
|
|
if ($order->getIsReversed()) {
|
|
|
|
|
$part['reverse'] = !idx($part, 'reverse', false);
|
|
|
|
|
}
|
|
|
|
|
$parts[] = $part;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->formatOrderClause($conn, $parts);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @task order
|
|
|
|
|
*/
|
2015-03-23 09:10:34 -07:00
|
|
|
protected function formatOrderClause(
|
|
|
|
|
AphrontDatabaseConnection $conn,
|
|
|
|
|
array $parts) {
|
|
|
|
|
|
|
|
|
|
$is_query_reversed = false;
|
|
|
|
|
if ($this->getBeforeID()) {
|
|
|
|
|
$is_query_reversed = !$is_query_reversed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$sql = array();
|
|
|
|
|
foreach ($parts as $key => $part) {
|
|
|
|
|
$is_column_reversed = !empty($part['reverse']);
|
|
|
|
|
|
|
|
|
|
$descending = true;
|
|
|
|
|
if ($is_query_reversed) {
|
|
|
|
|
$descending = !$descending;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($is_column_reversed) {
|
|
|
|
|
$descending = !$descending;
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-11 09:06:11 -07:00
|
|
|
$table = idx($part, 'table');
|
|
|
|
|
$column = $part['column'];
|
2015-03-23 09:10:34 -07:00
|
|
|
|
2015-04-11 20:50:57 -07:00
|
|
|
if ($table !== null) {
|
|
|
|
|
$field = qsprintf($conn, '%T.%T', $table, $column);
|
|
|
|
|
} else {
|
|
|
|
|
$field = qsprintf($conn, '%T', $column);
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-11 22:26:19 -07:00
|
|
|
$null = idx($part, 'null');
|
|
|
|
|
if ($null) {
|
|
|
|
|
switch ($null) {
|
|
|
|
|
case 'head':
|
|
|
|
|
$null_field = qsprintf($conn, '(%Q IS NULL)', $field);
|
|
|
|
|
break;
|
|
|
|
|
case 'tail':
|
|
|
|
|
$null_field = qsprintf($conn, '(%Q IS NOT NULL)', $field);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new Exception(
|
|
|
|
|
pht(
|
|
|
|
|
'NULL value "%s" is invalid. Valid values are "head" and '.
|
|
|
|
|
'"tail".',
|
|
|
|
|
$null));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($descending) {
|
|
|
|
|
$sql[] = qsprintf($conn, '%Q DESC', $null_field);
|
|
|
|
|
} else {
|
|
|
|
|
$sql[] = qsprintf($conn, '%Q ASC', $null_field);
|
|
|
|
|
}
|
2015-04-11 20:50:57 -07:00
|
|
|
}
|
|
|
|
|
|
2015-03-23 09:10:34 -07:00
|
|
|
if ($descending) {
|
2015-04-11 20:50:57 -07:00
|
|
|
$sql[] = qsprintf($conn, '%Q DESC', $field);
|
2015-03-23 09:10:34 -07:00
|
|
|
} else {
|
2015-04-11 20:50:57 -07:00
|
|
|
$sql[] = qsprintf($conn, '%Q ASC', $field);
|
2015-03-23 09:10:34 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return qsprintf($conn, 'ORDER BY %Q', implode(', ', $sql));
|
|
|
|
|
}
|
|
|
|
|
|
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
2013-09-16 13:44:34 -07:00
|
|
|
|
|
|
|
|
/* -( Application Search )------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2014-03-25 14:21:32 -07:00
|
|
|
* Constrain the query with an ApplicationSearch index, requiring field values
|
|
|
|
|
* contain at least one of the values in a set.
|
|
|
|
|
*
|
|
|
|
|
* This constraint can build the most common types of queries, like:
|
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
2013-09-16 13:44:34 -07:00
|
|
|
*
|
|
|
|
|
* - 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.
|
2014-03-25 14:21:32 -07:00
|
|
|
* @return this
|
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
2013-09-16 13:44:34 -07:00
|
|
|
* @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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2014-03-25 14:21:32 -07:00
|
|
|
/**
|
|
|
|
|
* Constrain the query with an ApplicationSearch index, requiring values
|
|
|
|
|
* exist in a given range.
|
|
|
|
|
*
|
|
|
|
|
* This constraint is useful for expressing date ranges:
|
|
|
|
|
*
|
|
|
|
|
* - Find events between July 1st and July 7th.
|
|
|
|
|
*
|
|
|
|
|
* The ends of the range are inclusive, so a `$min` of `3` and a `$max` of
|
|
|
|
|
* `5` will match fields with values `3`, `4`, or `5`. Providing `null` for
|
|
|
|
|
* either end of the range will leave that end of the constraint open.
|
|
|
|
|
*
|
|
|
|
|
* @param PhabricatorCustomFieldIndexStorage Table where the index is stored.
|
|
|
|
|
* @param int|null Minimum permissible value, inclusive.
|
|
|
|
|
* @param int|null Maximum permissible value, inclusive.
|
|
|
|
|
* @return this
|
|
|
|
|
* @task appsearch
|
|
|
|
|
*/
|
|
|
|
|
public function withApplicationSearchRangeConstraint(
|
|
|
|
|
PhabricatorCustomFieldIndexStorage $index,
|
|
|
|
|
$min,
|
|
|
|
|
$max) {
|
|
|
|
|
|
|
|
|
|
$index_type = $index->getIndexValueType();
|
|
|
|
|
if ($index_type != 'int') {
|
|
|
|
|
throw new Exception(
|
|
|
|
|
pht(
|
|
|
|
|
'Attempting to apply a range constraint to a field with index type '.
|
|
|
|
|
'"%s", expected type "%s".',
|
|
|
|
|
$index_type,
|
|
|
|
|
'int'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->applicationSearchConstraints[] = array(
|
|
|
|
|
'type' => $index->getIndexValueType(),
|
|
|
|
|
'cond' => 'range',
|
|
|
|
|
'table' => $index->getTableName(),
|
|
|
|
|
'index' => $index->getIndexKey(),
|
|
|
|
|
'value' => array($min, $max),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2014-08-02 18:22:16 +10:00
|
|
|
/**
|
|
|
|
|
* Order the results by an ApplicationSearch index.
|
|
|
|
|
*
|
|
|
|
|
* @param PhabricatorCustomField Field to which the index belongs.
|
|
|
|
|
* @param PhabricatorCustomFieldIndexStorage Table where the index is stored.
|
|
|
|
|
* @param bool True to sort ascending.
|
|
|
|
|
* @return this
|
|
|
|
|
* @task appsearch
|
|
|
|
|
*/
|
|
|
|
|
public function withApplicationSearchOrder(
|
|
|
|
|
PhabricatorCustomField $field,
|
|
|
|
|
PhabricatorCustomFieldIndexStorage $index,
|
|
|
|
|
$ascending) {
|
|
|
|
|
|
|
|
|
|
$this->applicationSearchOrders[] = array(
|
|
|
|
|
'key' => $field->getFieldKey(),
|
|
|
|
|
'type' => $index->getIndexValueType(),
|
|
|
|
|
'table' => $index->getTableName(),
|
|
|
|
|
'index' => $index->getIndexKey(),
|
|
|
|
|
'ascending' => $ascending,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
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
2013-09-16 13:44:34 -07:00
|
|
|
/**
|
|
|
|
|
* Get the name of the query's primary object PHID column, for constructing
|
2015-04-11 19:00:53 -07:00
|
|
|
* JOIN clauses. Normally (and by default) this is just `"phid"`, but it may
|
|
|
|
|
* be something more exotic.
|
|
|
|
|
*
|
|
|
|
|
* See @{method:getPrimaryTableAlias} if the column needs to be qualified with
|
|
|
|
|
* a table alias.
|
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
2013-09-16 13:44:34 -07:00
|
|
|
*
|
|
|
|
|
* @return string Column name.
|
|
|
|
|
* @task appsearch
|
|
|
|
|
*/
|
|
|
|
|
protected function getApplicationSearchObjectPHIDColumn() {
|
2015-04-11 19:00:53 -07:00
|
|
|
if ($this->getPrimaryTableAlias()) {
|
|
|
|
|
$prefix = $this->getPrimaryTableAlias().'.';
|
|
|
|
|
} else {
|
|
|
|
|
$prefix = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $prefix.'phid';
|
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
2013-09-16 13:44:34 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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'];
|
2014-03-25 14:21:32 -07:00
|
|
|
$cond = $constraint['cond'];
|
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
2013-09-16 13:44:34 -07:00
|
|
|
|
2014-03-25 14:21:32 -07:00
|
|
|
switch ($cond) {
|
|
|
|
|
case '=':
|
|
|
|
|
switch ($type) {
|
|
|
|
|
case 'string':
|
|
|
|
|
case 'int':
|
|
|
|
|
if (count((array)$value) > 1) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new Exception(pht('Unknown index type "%s"!', $type));
|
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
2013-09-16 13:44:34 -07:00
|
|
|
}
|
|
|
|
|
break;
|
2014-03-25 14:21:32 -07:00
|
|
|
case 'range':
|
|
|
|
|
// NOTE: It's possible to write a custom field where multiple rows
|
|
|
|
|
// match a range constraint, but we don't currently ship any in the
|
|
|
|
|
// upstream and I can't immediately come up with cases where this
|
|
|
|
|
// would make sense.
|
|
|
|
|
break;
|
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
2013-09-16 13:44:34 -07:00
|
|
|
default:
|
2014-03-25 14:21:32 -07:00
|
|
|
throw new Exception(pht('Unknown constraint condition "%s"!', $cond));
|
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
2013-09-16 13:44:34 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
2014-03-25 14:21:32 -07:00
|
|
|
switch ($cond) {
|
|
|
|
|
case '=':
|
|
|
|
|
$type = $constraint['type'];
|
|
|
|
|
switch ($type) {
|
|
|
|
|
case 'string':
|
|
|
|
|
$constraint_clause = qsprintf(
|
|
|
|
|
$conn_r,
|
|
|
|
|
'%T.indexValue IN (%Ls)',
|
|
|
|
|
$alias,
|
|
|
|
|
(array)$constraint['value']);
|
|
|
|
|
break;
|
|
|
|
|
case 'int':
|
|
|
|
|
$constraint_clause = qsprintf(
|
|
|
|
|
$conn_r,
|
|
|
|
|
'%T.indexValue IN (%Ld)',
|
|
|
|
|
$alias,
|
|
|
|
|
(array)$constraint['value']);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new Exception(pht('Unknown index type "%s"!', $type));
|
|
|
|
|
}
|
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
2013-09-16 13:44:34 -07:00
|
|
|
|
|
|
|
|
$joins[] = qsprintf(
|
|
|
|
|
$conn_r,
|
|
|
|
|
'JOIN %T %T ON %T.objectPHID = %Q
|
|
|
|
|
AND %T.indexKey = %s
|
2014-03-25 14:21:32 -07:00
|
|
|
AND (%Q)',
|
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
2013-09-16 13:44:34 -07:00
|
|
|
$table,
|
|
|
|
|
$alias,
|
|
|
|
|
$alias,
|
|
|
|
|
$phid_column,
|
|
|
|
|
$alias,
|
|
|
|
|
$index,
|
2014-03-25 14:21:32 -07:00
|
|
|
$constraint_clause);
|
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
2013-09-16 13:44:34 -07:00
|
|
|
break;
|
2014-03-25 14:21:32 -07:00
|
|
|
case 'range':
|
|
|
|
|
list($min, $max) = $constraint['value'];
|
|
|
|
|
if (($min === null) && ($max === null)) {
|
|
|
|
|
// If there's no actual range constraint, just move on.
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($min === null) {
|
|
|
|
|
$constraint_clause = qsprintf(
|
|
|
|
|
$conn_r,
|
|
|
|
|
'%T.indexValue <= %d',
|
|
|
|
|
$alias,
|
|
|
|
|
$max);
|
|
|
|
|
} else if ($max === null) {
|
|
|
|
|
$constraint_clause = qsprintf(
|
|
|
|
|
$conn_r,
|
|
|
|
|
'%T.indexValue >= %d',
|
|
|
|
|
$alias,
|
|
|
|
|
$min);
|
|
|
|
|
} else {
|
|
|
|
|
$constraint_clause = qsprintf(
|
|
|
|
|
$conn_r,
|
|
|
|
|
'%T.indexValue BETWEEN %d AND %d',
|
|
|
|
|
$alias,
|
|
|
|
|
$min,
|
|
|
|
|
$max);
|
|
|
|
|
}
|
|
|
|
|
|
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
2013-09-16 13:44:34 -07:00
|
|
|
$joins[] = qsprintf(
|
|
|
|
|
$conn_r,
|
|
|
|
|
'JOIN %T %T ON %T.objectPHID = %Q
|
|
|
|
|
AND %T.indexKey = %s
|
2014-03-25 14:21:32 -07:00
|
|
|
AND (%Q)',
|
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
2013-09-16 13:44:34 -07:00
|
|
|
$table,
|
|
|
|
|
$alias,
|
|
|
|
|
$alias,
|
|
|
|
|
$phid_column,
|
|
|
|
|
$alias,
|
|
|
|
|
$index,
|
2014-03-25 14:21:32 -07:00
|
|
|
$constraint_clause);
|
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
2013-09-16 13:44:34 -07:00
|
|
|
break;
|
|
|
|
|
default:
|
2014-03-25 14:21:32 -07:00
|
|
|
throw new Exception(pht('Unknown constraint condition "%s"!', $cond));
|
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
2013-09-16 13:44:34 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2014-08-02 18:22:16 +10:00
|
|
|
foreach ($this->applicationSearchOrders as $key => $order) {
|
|
|
|
|
$table = $order['table'];
|
|
|
|
|
$index = $order['index'];
|
2015-04-12 13:01:21 -07:00
|
|
|
$alias = 'appsearch_order_'.$index;
|
2014-08-02 18:22:16 +10:00
|
|
|
$phid_column = $this->getApplicationSearchObjectPHIDColumn();
|
|
|
|
|
|
|
|
|
|
$joins[] = qsprintf(
|
|
|
|
|
$conn_r,
|
2014-08-02 10:17:10 -07:00
|
|
|
'LEFT JOIN %T %T ON %T.objectPHID = %Q
|
2014-08-02 18:22:16 +10:00
|
|
|
AND %T.indexKey = %s',
|
|
|
|
|
$table,
|
|
|
|
|
$alias,
|
|
|
|
|
$alias,
|
|
|
|
|
$phid_column,
|
|
|
|
|
$alias,
|
|
|
|
|
$index);
|
|
|
|
|
}
|
|
|
|
|
|
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
2013-09-16 13:44:34 -07:00
|
|
|
return implode(' ', $joins);
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-12 13:01:21 -07:00
|
|
|
protected function getPagingValueMapForCustomFields(
|
|
|
|
|
PhabricatorCustomFieldInterface $object) {
|
2014-08-02 18:22:16 +10:00
|
|
|
|
|
|
|
|
// We have to get the current field values on the cursor object.
|
|
|
|
|
$fields = PhabricatorCustomField::getObjectFields(
|
2015-04-12 13:01:21 -07:00
|
|
|
$object,
|
2014-08-02 18:22:16 +10:00
|
|
|
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
|
|
|
|
|
$fields->setViewer($this->getViewer());
|
2015-04-12 13:01:21 -07:00
|
|
|
$fields->readFieldsFromStorage($object);
|
2014-08-02 18:22:16 +10:00
|
|
|
|
2015-04-12 13:01:21 -07:00
|
|
|
$map = array();
|
|
|
|
|
foreach ($fields->getFields() as $field) {
|
|
|
|
|
$map['custom:'.$field->getFieldKey()] = $field->getValueForStorage();
|
2014-08-02 18:22:16 +10:00
|
|
|
}
|
|
|
|
|
|
2015-04-12 13:01:21 -07:00
|
|
|
return $map;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function isCustomFieldOrderKey($key) {
|
|
|
|
|
$prefix = 'custom:';
|
|
|
|
|
return !strncmp($key, $prefix, strlen($prefix));
|
2014-08-02 18:22:16 +10:00
|
|
|
}
|
|
|
|
|
|
Add basic per-object privacy policies
Summary:
Provides a basic start for access policies. Objects expose various capabilities, like CAN_VIEW, CAN_EDIT, etc., and set a policy for each capability. We currently implement three policies, PUBLIC (anyone, including logged-out), USERS (any logged-in) and NOONE (nobody). There's also a way to provide automatic capability grants (e.g., the owner of an object can always see it, even if some capability is set to "NOONE"), but I'm not sure how great the implementation feels and it might change.
Most of the code here is providing a primitive for efficient policy-aware list queries. The problem with doing queries naively is that you have to do crazy amounts of filtering, e.g. to show the user page 6, you need to filter at least 600 objects (and likely more) before you can figure out which ones are 500-600 for them. You can't just do "LIMIT 500, 100" because that might have only 50 results, or no results. Instead, the query looks like "WHERE id > last_visible_id", and then we fetch additional pages as necessary to satisfy the request.
The general idea is that we move all data access to Query classes and have them do object filtering. The ID paging primitive allows efficient paging in most cases, and the executeOne() method provides a concise way to do policy checks for edit/view screens.
We'll probably end up with mostly broader policy UIs or configuration-based policies, but there are at least a few cases for per-object privacy (e.g., marking tasks as "Security", and restricting things to the members of projects) so I figured we'd start with a flexible primitive and the simplify it in the UI where we can.
Test Plan: Unit tests, played around in the UI with various policy settings.
Reviewers: btrahan, vrana, jungejason
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D2210
2012-04-14 10:13:29 -07:00
|
|
|
}
|