2018-09-11 15:08:34 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2014-09-02 08:28:27 -04:00
|
|
|
# IssuableFinder
|
2014-01-15 09:16:23 -05:00
|
|
|
#
|
|
|
|
# Used to filter Issues and MergeRequests collections by set of params
|
|
|
|
#
|
|
|
|
# Arguments:
|
|
|
|
# klass - actual class like Issue or MergeRequest
|
|
|
|
# current_user - which user use
|
|
|
|
# params:
|
2018-05-13 22:07:53 -04:00
|
|
|
# scope: 'created_by_me' or 'assigned_to_me' or 'all'
|
2018-06-26 15:30:29 -04:00
|
|
|
# state: 'opened' or 'closed' or 'locked' or 'all'
|
2014-01-15 09:16:23 -05:00
|
|
|
# group_id: integer
|
|
|
|
# project_id: integer
|
2015-06-28 16:12:32 -04:00
|
|
|
# milestone_title: string
|
2019-11-22 13:06:00 -05:00
|
|
|
# release_tag: string
|
2017-07-20 16:44:48 -04:00
|
|
|
# author_id: integer
|
2018-11-09 09:29:45 -05:00
|
|
|
# author_username: string
|
2018-10-25 03:02:28 -04:00
|
|
|
# assignee_id: integer or 'None' or 'Any'
|
2018-11-09 09:29:45 -05:00
|
|
|
# assignee_username: string
|
2014-01-15 09:16:23 -05:00
|
|
|
# search: string
|
2019-02-05 03:32:27 -05:00
|
|
|
# in: 'title', 'description', or a string joining them with comma
|
2014-01-15 09:16:23 -05:00
|
|
|
# label_name: string
|
|
|
|
# sort: string
|
2016-11-27 05:33:15 -05:00
|
|
|
# non_archived: boolean
|
2017-02-17 13:28:32 -05:00
|
|
|
# iids: integer[]
|
2017-08-30 03:48:55 -04:00
|
|
|
# my_reaction_emoji: string
|
2018-02-28 06:16:29 -05:00
|
|
|
# created_after: datetime
|
|
|
|
# created_before: datetime
|
|
|
|
# updated_after: datetime
|
|
|
|
# updated_before: datetime
|
2018-11-29 07:52:48 -05:00
|
|
|
# attempt_group_search_optimizations: boolean
|
2019-05-07 07:08:25 -04:00
|
|
|
# attempt_project_search_optimizations: boolean
|
2014-01-15 09:16:23 -05:00
|
|
|
#
|
2014-09-02 08:28:27 -04:00
|
|
|
class IssuableFinder
|
2017-12-11 09:21:06 -05:00
|
|
|
prepend FinderWithCrossProjectAccess
|
|
|
|
include FinderMethods
|
2017-07-07 12:31:50 -04:00
|
|
|
include CreatedAtFilter
|
2018-11-29 07:52:48 -05:00
|
|
|
include Gitlab::Utils::StrongMemoize
|
2017-07-11 08:19:43 -04:00
|
|
|
|
2020-03-30 17:08:01 -04:00
|
|
|
requires_cross_project_access unless: -> { params.project? }
|
2015-03-26 21:56:42 -04:00
|
|
|
|
2020-05-06 23:09:46 -04:00
|
|
|
NEGATABLE_PARAMS_HELPER_KEYS = %i[project_id scope status include_subgroups].freeze
|
2019-09-17 08:06:48 -04:00
|
|
|
|
2014-02-25 12:15:08 -05:00
|
|
|
attr_accessor :current_user, :params
|
2014-01-15 09:16:23 -05:00
|
|
|
|
2020-03-30 17:08:01 -04:00
|
|
|
delegate(*%i[assignee milestones], to: :params)
|
|
|
|
|
2019-09-17 08:06:48 -04:00
|
|
|
class << self
|
|
|
|
def scalar_params
|
|
|
|
@scalar_params ||= %i[
|
2018-02-20 07:33:49 -05:00
|
|
|
assignee_id
|
|
|
|
assignee_username
|
|
|
|
author_id
|
|
|
|
author_username
|
2019-04-19 02:50:54 -04:00
|
|
|
label_name
|
2018-02-20 07:33:49 -05:00
|
|
|
milestone_title
|
2019-11-22 13:06:00 -05:00
|
|
|
release_tag
|
2018-02-20 07:33:49 -05:00
|
|
|
my_reaction_emoji
|
|
|
|
search
|
2019-01-13 11:24:31 -05:00
|
|
|
in
|
2018-02-20 07:33:49 -05:00
|
|
|
]
|
2019-09-17 08:06:48 -04:00
|
|
|
end
|
2018-02-20 07:33:49 -05:00
|
|
|
|
2019-09-17 08:06:48 -04:00
|
|
|
def array_params
|
|
|
|
@array_params ||= { label_name: [], assignee_username: [] }
|
|
|
|
end
|
|
|
|
|
|
|
|
# This should not be used in controller strong params!
|
|
|
|
def negatable_scalar_params
|
2020-05-06 23:09:46 -04:00
|
|
|
@negatable_scalar_params ||= scalar_params - %i[search in]
|
2019-09-17 08:06:48 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
# This should not be used in controller strong params!
|
|
|
|
def negatable_array_params
|
|
|
|
@negatable_array_params ||= array_params.keys.append(:iids)
|
|
|
|
end
|
2018-02-20 07:33:49 -05:00
|
|
|
|
2019-09-17 08:06:48 -04:00
|
|
|
# This should not be used in controller strong params!
|
|
|
|
def negatable_params
|
|
|
|
@negatable_params ||= negatable_scalar_params + negatable_array_params
|
|
|
|
end
|
|
|
|
|
|
|
|
def valid_params
|
2020-01-02 08:03:23 -05:00
|
|
|
@valid_params ||= scalar_params + [array_params.merge(not: {})]
|
2019-09-17 08:06:48 -04:00
|
|
|
end
|
2018-02-20 07:33:49 -05:00
|
|
|
end
|
|
|
|
|
2020-03-30 17:08:01 -04:00
|
|
|
def params_class
|
|
|
|
IssuableFinder::Params
|
|
|
|
end
|
|
|
|
|
2016-11-22 05:25:04 -05:00
|
|
|
def initialize(current_user, params = {})
|
2014-01-15 09:16:23 -05:00
|
|
|
@current_user = current_user
|
2020-03-30 17:08:01 -04:00
|
|
|
@params = params_class.new(params, current_user, klass)
|
2015-05-25 07:36:28 -04:00
|
|
|
end
|
2014-01-15 09:16:23 -05:00
|
|
|
|
2015-05-25 07:36:28 -04:00
|
|
|
def execute
|
2014-02-03 10:02:44 -05:00
|
|
|
items = init_collection
|
2018-02-20 07:33:49 -05:00
|
|
|
items = filter_items(items)
|
|
|
|
|
2019-09-17 08:06:48 -04:00
|
|
|
# Let's see if we have to negate anything
|
2020-05-06 23:09:46 -04:00
|
|
|
items = filter_negated_items(items)
|
2019-09-17 08:06:48 -04:00
|
|
|
|
2019-02-19 15:51:11 -05:00
|
|
|
# This has to be last as we use a CTE as an optimization fence
|
|
|
|
# for counts by passing the force_cte param and enabling the
|
|
|
|
# attempt_group_search_optimizations feature flag
|
2018-06-05 13:31:32 -04:00
|
|
|
# https://www.postgresql.org/docs/current/static/queries-with.html
|
|
|
|
items = by_search(items)
|
2018-02-20 07:33:49 -05:00
|
|
|
|
Extend CTE search optimisation to projects
When we use the `search` param on an `IssuableFinder`, we can run into
issues. We have trigram indexes to support these searches. On
GitLab.com, we often see Postgres's optimiser prioritise the (global)
trigram indexes over the index on `project_id`. For group and project
searches, we know that it will be quicker to filter by `project_id`
first, as it returns fewer rows in most cases.
For group issues search, we ran into this issue previously, and went
through the following iterations:
1. Use a CTE on the project IDs as an optimisation fence. This prevents
the planner from disregarding the index on `project_id`.
Unfortunately it breaks some types of sorting, like priority and
popularity, as they sort on a joined table.
2. Use a subquery for listing issues, and a CTE for counts. The subquery
- in the case of group lists - didn't help as much as the CTE, but
was faster than not including it. We can safely use a CTE for counts
as they don't have sorting.
Now, however, we're seeing the same issue in a project context. The
subquery doesn't help at all there (it would only return one row, after
all). In an attempt to keep total code complexity under control, this
commit removes the subquery optimisation and applies the CTE
optimisation only for sorts we know that are safe.
This means that for more complicated sorts (like priority and
popularity), the search will continue to be very slow. If this is a
high-priority issue, we can consider introducing further optimisations,
but this finder is already very complicated and additional complexity
has a cost.
The group CTE optimisation is controlled by the same feature flag as
before, `attempt_group_search_optimizations`, which is enabled by
default. The new project CTE optimisation is controlled by a new feature
flag, `attempt_project_search_optimizations`, which is disabled by
default.
2019-04-03 05:46:13 -04:00
|
|
|
items = sort(items)
|
2019-02-19 15:51:11 -05:00
|
|
|
|
|
|
|
items
|
2018-02-20 07:33:49 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def filter_items(items)
|
2018-06-05 13:31:32 -04:00
|
|
|
items = by_project(items)
|
2018-11-29 07:52:48 -05:00
|
|
|
items = by_group(items)
|
2014-02-03 10:02:44 -05:00
|
|
|
items = by_scope(items)
|
2017-07-07 12:31:50 -04:00
|
|
|
items = by_created_at(items)
|
2018-02-28 06:16:29 -05:00
|
|
|
items = by_updated_at(items)
|
2019-01-17 12:49:07 -05:00
|
|
|
items = by_closed_at(items)
|
2014-01-15 09:16:23 -05:00
|
|
|
items = by_state(items)
|
|
|
|
items = by_group(items)
|
|
|
|
items = by_assignee(items)
|
2014-12-05 10:13:07 -05:00
|
|
|
items = by_author(items)
|
2016-11-27 05:33:15 -05:00
|
|
|
items = by_non_archived(items)
|
2017-02-17 13:28:32 -05:00
|
|
|
items = by_iids(items)
|
2017-02-07 08:15:07 -05:00
|
|
|
items = by_milestone(items)
|
2019-11-22 13:06:00 -05:00
|
|
|
items = by_release(items)
|
2017-02-07 08:15:07 -05:00
|
|
|
items = by_label(items)
|
2018-02-20 07:33:49 -05:00
|
|
|
by_my_reaction_emoji(items)
|
2014-01-15 09:16:23 -05:00
|
|
|
end
|
|
|
|
|
2020-05-06 23:09:46 -04:00
|
|
|
# Negates all params found in `negatable_params`
|
|
|
|
def filter_negated_items(items)
|
|
|
|
return items unless Feature.enabled?(:not_issuable_queries, params.group || params.project, default_enabled: true)
|
|
|
|
|
|
|
|
# API endpoints send in `nil` values so we test if there are any non-nil
|
|
|
|
return items unless not_params.present? && not_params.values.any?
|
|
|
|
|
|
|
|
items = by_negated_author(items)
|
|
|
|
items = by_negated_assignee(items)
|
|
|
|
items = by_negated_label(items)
|
|
|
|
items = by_negated_milestone(items)
|
|
|
|
items = by_negated_release(items)
|
|
|
|
items = by_negated_my_reaction_emoji(items)
|
|
|
|
by_negated_iids(items)
|
|
|
|
end
|
|
|
|
|
2017-08-24 12:17:04 -04:00
|
|
|
def row_count
|
|
|
|
Gitlab::IssuablesCountForState.new(self).for_state_or_opened(params[:state])
|
|
|
|
end
|
|
|
|
|
2016-11-25 08:57:56 -05:00
|
|
|
# We often get counts for each state by running a query per state, and
|
|
|
|
# counting those results. This is typically slower than running one query
|
|
|
|
# (even if that query is slower than any of the individual state queries) and
|
|
|
|
# grouping and counting within that query.
|
|
|
|
#
|
2018-08-27 11:31:01 -04:00
|
|
|
# rubocop: disable CodeReuse/ActiveRecord
|
2016-11-25 08:57:56 -05:00
|
|
|
def count_by_state
|
2019-02-19 15:51:11 -05:00
|
|
|
count_params = params.merge(state: nil, sort: nil, force_cte: true)
|
2016-11-25 08:57:56 -05:00
|
|
|
finder = self.class.new(current_user, count_params)
|
2019-02-19 15:51:11 -05:00
|
|
|
|
2016-11-25 08:57:56 -05:00
|
|
|
counts = Hash.new(0)
|
|
|
|
|
|
|
|
# Searching by label includes a GROUP BY in the query, but ours will be last
|
|
|
|
# because it is added last. Searching by multiple labels also includes a row
|
|
|
|
# per issuable, so we have to count those in Ruby - which is bad, but still
|
|
|
|
# better than performing multiple queries.
|
|
|
|
#
|
2018-06-05 13:31:32 -04:00
|
|
|
# This does not apply when we are using a CTE for the search, as the labels
|
|
|
|
# GROUP BY is inside the subquery in that case, so we set labels_count to 1.
|
2019-02-19 15:51:11 -05:00
|
|
|
#
|
Extend CTE search optimisation to projects
When we use the `search` param on an `IssuableFinder`, we can run into
issues. We have trigram indexes to support these searches. On
GitLab.com, we often see Postgres's optimiser prioritise the (global)
trigram indexes over the index on `project_id`. For group and project
searches, we know that it will be quicker to filter by `project_id`
first, as it returns fewer rows in most cases.
For group issues search, we ran into this issue previously, and went
through the following iterations:
1. Use a CTE on the project IDs as an optimisation fence. This prevents
the planner from disregarding the index on `project_id`.
Unfortunately it breaks some types of sorting, like priority and
popularity, as they sort on a joined table.
2. Use a subquery for listing issues, and a CTE for counts. The subquery
- in the case of group lists - didn't help as much as the CTE, but
was faster than not including it. We can safely use a CTE for counts
as they don't have sorting.
Now, however, we're seeing the same issue in a project context. The
subquery doesn't help at all there (it would only return one row, after
all). In an attempt to keep total code complexity under control, this
commit removes the subquery optimisation and applies the CTE
optimisation only for sorts we know that are safe.
This means that for more complicated sorts (like priority and
popularity), the search will continue to be very slow. If this is a
high-priority issue, we can consider introducing further optimisations,
but this finder is already very complicated and additional complexity
has a cost.
The group CTE optimisation is controlled by the same feature flag as
before, `attempt_group_search_optimizations`, which is enabled by
default. The new project CTE optimisation is controlled by a new feature
flag, `attempt_project_search_optimizations`, which is disabled by
default.
2019-04-03 05:46:13 -04:00
|
|
|
# Groups and projects have separate feature flags to suggest the use
|
|
|
|
# of a CTE. The CTE will not be used if the sort doesn't support it,
|
|
|
|
# but will always be used for the counts here as we ignore sorting
|
|
|
|
# anyway.
|
2020-03-30 17:08:01 -04:00
|
|
|
labels_count = params.label_names.any? ? params.label_names.count : 1
|
Extend CTE search optimisation to projects
When we use the `search` param on an `IssuableFinder`, we can run into
issues. We have trigram indexes to support these searches. On
GitLab.com, we often see Postgres's optimiser prioritise the (global)
trigram indexes over the index on `project_id`. For group and project
searches, we know that it will be quicker to filter by `project_id`
first, as it returns fewer rows in most cases.
For group issues search, we ran into this issue previously, and went
through the following iterations:
1. Use a CTE on the project IDs as an optimisation fence. This prevents
the planner from disregarding the index on `project_id`.
Unfortunately it breaks some types of sorting, like priority and
popularity, as they sort on a joined table.
2. Use a subquery for listing issues, and a CTE for counts. The subquery
- in the case of group lists - didn't help as much as the CTE, but
was faster than not including it. We can safely use a CTE for counts
as they don't have sorting.
Now, however, we're seeing the same issue in a project context. The
subquery doesn't help at all there (it would only return one row, after
all). In an attempt to keep total code complexity under control, this
commit removes the subquery optimisation and applies the CTE
optimisation only for sorts we know that are safe.
This means that for more complicated sorts (like priority and
popularity), the search will continue to be very slow. If this is a
high-priority issue, we can consider introducing further optimisations,
but this finder is already very complicated and additional complexity
has a cost.
The group CTE optimisation is controlled by the same feature flag as
before, `attempt_group_search_optimizations`, which is enabled by
default. The new project CTE optimisation is controlled by a new feature
flag, `attempt_project_search_optimizations`, which is disabled by
default.
2019-04-03 05:46:13 -04:00
|
|
|
labels_count = 1 if use_cte_for_search?
|
2018-06-05 13:31:32 -04:00
|
|
|
|
2019-10-18 07:11:44 -04:00
|
|
|
finder.execute.reorder(nil).group(:state_id).count.each do |key, value|
|
2018-10-01 07:45:15 -04:00
|
|
|
counts[count_key(key)] += value / labels_count
|
2016-11-25 08:57:56 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
counts[:all] = counts.values.sum
|
|
|
|
|
2018-07-27 06:13:36 -04:00
|
|
|
counts.with_indifferent_access
|
2016-11-25 08:57:56 -05:00
|
|
|
end
|
2018-08-27 11:31:01 -04:00
|
|
|
# rubocop: enable CodeReuse/ActiveRecord
|
2016-11-25 08:57:56 -05:00
|
|
|
|
2015-05-25 07:36:28 -04:00
|
|
|
def search
|
|
|
|
params[:search].presence
|
|
|
|
end
|
|
|
|
|
Extend CTE search optimisation to projects
When we use the `search` param on an `IssuableFinder`, we can run into
issues. We have trigram indexes to support these searches. On
GitLab.com, we often see Postgres's optimiser prioritise the (global)
trigram indexes over the index on `project_id`. For group and project
searches, we know that it will be quicker to filter by `project_id`
first, as it returns fewer rows in most cases.
For group issues search, we ran into this issue previously, and went
through the following iterations:
1. Use a CTE on the project IDs as an optimisation fence. This prevents
the planner from disregarding the index on `project_id`.
Unfortunately it breaks some types of sorting, like priority and
popularity, as they sort on a joined table.
2. Use a subquery for listing issues, and a CTE for counts. The subquery
- in the case of group lists - didn't help as much as the CTE, but
was faster than not including it. We can safely use a CTE for counts
as they don't have sorting.
Now, however, we're seeing the same issue in a project context. The
subquery doesn't help at all there (it would only return one row, after
all). In an attempt to keep total code complexity under control, this
commit removes the subquery optimisation and applies the CTE
optimisation only for sorts we know that are safe.
This means that for more complicated sorts (like priority and
popularity), the search will continue to be very slow. If this is a
high-priority issue, we can consider introducing further optimisations,
but this finder is already very complicated and additional complexity
has a cost.
The group CTE optimisation is controlled by the same feature flag as
before, `attempt_group_search_optimizations`, which is enabled by
default. The new project CTE optimisation is controlled by a new feature
flag, `attempt_project_search_optimizations`, which is disabled by
default.
2019-04-03 05:46:13 -04:00
|
|
|
def use_cte_for_search?
|
|
|
|
strong_memoize(:use_cte_for_search) do
|
|
|
|
next false unless search
|
|
|
|
# Only simple unsorted & simple sorts can use CTE
|
|
|
|
next false if params[:sort].present? && !params[:sort].in?(klass.simple_sorts.keys)
|
2018-11-29 07:52:48 -05:00
|
|
|
|
Extend CTE search optimisation to projects
When we use the `search` param on an `IssuableFinder`, we can run into
issues. We have trigram indexes to support these searches. On
GitLab.com, we often see Postgres's optimiser prioritise the (global)
trigram indexes over the index on `project_id`. For group and project
searches, we know that it will be quicker to filter by `project_id`
first, as it returns fewer rows in most cases.
For group issues search, we ran into this issue previously, and went
through the following iterations:
1. Use a CTE on the project IDs as an optimisation fence. This prevents
the planner from disregarding the index on `project_id`.
Unfortunately it breaks some types of sorting, like priority and
popularity, as they sort on a joined table.
2. Use a subquery for listing issues, and a CTE for counts. The subquery
- in the case of group lists - didn't help as much as the CTE, but
was faster than not including it. We can safely use a CTE for counts
as they don't have sorting.
Now, however, we're seeing the same issue in a project context. The
subquery doesn't help at all there (it would only return one row, after
all). In an attempt to keep total code complexity under control, this
commit removes the subquery optimisation and applies the CTE
optimisation only for sorts we know that are safe.
This means that for more complicated sorts (like priority and
popularity), the search will continue to be very slow. If this is a
high-priority issue, we can consider introducing further optimisations,
but this finder is already very complicated and additional complexity
has a cost.
The group CTE optimisation is controlled by the same feature flag as
before, `attempt_group_search_optimizations`, which is enabled by
default. The new project CTE optimisation is controlled by a new feature
flag, `attempt_project_search_optimizations`, which is disabled by
default.
2019-04-03 05:46:13 -04:00
|
|
|
attempt_group_search_optimizations? || attempt_project_search_optimizations?
|
2018-11-29 07:52:48 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-01-15 09:16:23 -05:00
|
|
|
private
|
|
|
|
|
2020-05-06 23:09:46 -04:00
|
|
|
def not_params
|
|
|
|
strong_memoize(:not_params) do
|
|
|
|
params_class.new(params[:not].dup, current_user, klass).tap do |not_params|
|
|
|
|
next unless not_params.present?
|
|
|
|
|
|
|
|
# These are "helper" params that modify the results, like :in and :search. They usually come in at the top-level
|
|
|
|
# params, but if they do come in inside the `:not` params, the inner ones should take precedence.
|
|
|
|
not_helpers = params.slice(*NEGATABLE_PARAMS_HELPER_KEYS).merge(params[:not].slice(*NEGATABLE_PARAMS_HELPER_KEYS))
|
|
|
|
not_helpers.each do |key, value|
|
|
|
|
not_params[key] = value unless not_params[key].present?
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-02-19 15:51:11 -05:00
|
|
|
def force_cte?
|
|
|
|
!!params[:force_cte]
|
|
|
|
end
|
|
|
|
|
2014-02-03 10:02:44 -05:00
|
|
|
def init_collection
|
2015-10-14 06:23:49 -04:00
|
|
|
klass.all
|
2014-02-03 10:02:44 -05:00
|
|
|
end
|
|
|
|
|
2018-11-29 07:52:48 -05:00
|
|
|
def attempt_group_search_optimizations?
|
Extend CTE search optimisation to projects
When we use the `search` param on an `IssuableFinder`, we can run into
issues. We have trigram indexes to support these searches. On
GitLab.com, we often see Postgres's optimiser prioritise the (global)
trigram indexes over the index on `project_id`. For group and project
searches, we know that it will be quicker to filter by `project_id`
first, as it returns fewer rows in most cases.
For group issues search, we ran into this issue previously, and went
through the following iterations:
1. Use a CTE on the project IDs as an optimisation fence. This prevents
the planner from disregarding the index on `project_id`.
Unfortunately it breaks some types of sorting, like priority and
popularity, as they sort on a joined table.
2. Use a subquery for listing issues, and a CTE for counts. The subquery
- in the case of group lists - didn't help as much as the CTE, but
was faster than not including it. We can safely use a CTE for counts
as they don't have sorting.
Now, however, we're seeing the same issue in a project context. The
subquery doesn't help at all there (it would only return one row, after
all). In an attempt to keep total code complexity under control, this
commit removes the subquery optimisation and applies the CTE
optimisation only for sorts we know that are safe.
This means that for more complicated sorts (like priority and
popularity), the search will continue to be very slow. If this is a
high-priority issue, we can consider introducing further optimisations,
but this finder is already very complicated and additional complexity
has a cost.
The group CTE optimisation is controlled by the same feature flag as
before, `attempt_group_search_optimizations`, which is enabled by
default. The new project CTE optimisation is controlled by a new feature
flag, `attempt_project_search_optimizations`, which is disabled by
default.
2019-04-03 05:46:13 -04:00
|
|
|
params[:attempt_group_search_optimizations] &&
|
2019-02-19 15:51:11 -05:00
|
|
|
Feature.enabled?(:attempt_group_search_optimizations, default_enabled: true)
|
2018-11-29 07:52:48 -05:00
|
|
|
end
|
|
|
|
|
Extend CTE search optimisation to projects
When we use the `search` param on an `IssuableFinder`, we can run into
issues. We have trigram indexes to support these searches. On
GitLab.com, we often see Postgres's optimiser prioritise the (global)
trigram indexes over the index on `project_id`. For group and project
searches, we know that it will be quicker to filter by `project_id`
first, as it returns fewer rows in most cases.
For group issues search, we ran into this issue previously, and went
through the following iterations:
1. Use a CTE on the project IDs as an optimisation fence. This prevents
the planner from disregarding the index on `project_id`.
Unfortunately it breaks some types of sorting, like priority and
popularity, as they sort on a joined table.
2. Use a subquery for listing issues, and a CTE for counts. The subquery
- in the case of group lists - didn't help as much as the CTE, but
was faster than not including it. We can safely use a CTE for counts
as they don't have sorting.
Now, however, we're seeing the same issue in a project context. The
subquery doesn't help at all there (it would only return one row, after
all). In an attempt to keep total code complexity under control, this
commit removes the subquery optimisation and applies the CTE
optimisation only for sorts we know that are safe.
This means that for more complicated sorts (like priority and
popularity), the search will continue to be very slow. If this is a
high-priority issue, we can consider introducing further optimisations,
but this finder is already very complicated and additional complexity
has a cost.
The group CTE optimisation is controlled by the same feature flag as
before, `attempt_group_search_optimizations`, which is enabled by
default. The new project CTE optimisation is controlled by a new feature
flag, `attempt_project_search_optimizations`, which is disabled by
default.
2019-04-03 05:46:13 -04:00
|
|
|
def attempt_project_search_optimizations?
|
|
|
|
params[:attempt_project_search_optimizations] &&
|
2019-05-21 08:19:35 -04:00
|
|
|
Feature.enabled?(:attempt_project_search_optimizations, default_enabled: true)
|
Extend CTE search optimisation to projects
When we use the `search` param on an `IssuableFinder`, we can run into
issues. We have trigram indexes to support these searches. On
GitLab.com, we often see Postgres's optimiser prioritise the (global)
trigram indexes over the index on `project_id`. For group and project
searches, we know that it will be quicker to filter by `project_id`
first, as it returns fewer rows in most cases.
For group issues search, we ran into this issue previously, and went
through the following iterations:
1. Use a CTE on the project IDs as an optimisation fence. This prevents
the planner from disregarding the index on `project_id`.
Unfortunately it breaks some types of sorting, like priority and
popularity, as they sort on a joined table.
2. Use a subquery for listing issues, and a CTE for counts. The subquery
- in the case of group lists - didn't help as much as the CTE, but
was faster than not including it. We can safely use a CTE for counts
as they don't have sorting.
Now, however, we're seeing the same issue in a project context. The
subquery doesn't help at all there (it would only return one row, after
all). In an attempt to keep total code complexity under control, this
commit removes the subquery optimisation and applies the CTE
optimisation only for sorts we know that are safe.
This means that for more complicated sorts (like priority and
popularity), the search will continue to be very slow. If this is a
high-priority issue, we can consider introducing further optimisations,
but this finder is already very complicated and additional complexity
has a cost.
The group CTE optimisation is controlled by the same feature flag as
before, `attempt_group_search_optimizations`, which is enabled by
default. The new project CTE optimisation is controlled by a new feature
flag, `attempt_project_search_optimizations`, which is disabled by
default.
2019-04-03 05:46:13 -04:00
|
|
|
end
|
|
|
|
|
2018-10-01 07:45:15 -04:00
|
|
|
def count_key(value)
|
2019-11-08 10:06:21 -05:00
|
|
|
# value may be an array if the finder used in `count_by_state` added an
|
|
|
|
# additional `group by`. Anyway we are sure that state will be always the
|
|
|
|
# last item because it's added as the last one to the query.
|
2019-10-18 07:11:44 -04:00
|
|
|
value = Array(value).last
|
|
|
|
klass.available_states.key(value)
|
2018-10-01 07:45:15 -04:00
|
|
|
end
|
|
|
|
|
2018-08-27 11:31:01 -04:00
|
|
|
# rubocop: disable CodeReuse/ActiveRecord
|
2014-02-03 10:02:44 -05:00
|
|
|
def by_scope(items)
|
2020-03-30 17:08:01 -04:00
|
|
|
return items.none if params.current_user_related? && !current_user
|
2017-09-18 13:29:17 -04:00
|
|
|
|
2016-03-21 19:09:20 -04:00
|
|
|
case params[:scope]
|
2018-05-13 22:07:53 -04:00
|
|
|
when 'created_by_me', 'authored'
|
2014-02-10 08:04:52 -05:00
|
|
|
items.where(author_id: current_user.id)
|
2018-05-13 22:07:53 -04:00
|
|
|
when 'assigned_to_me'
|
2017-05-04 08:11:15 -04:00
|
|
|
items.assigned_to(current_user)
|
2014-01-15 09:16:23 -05:00
|
|
|
else
|
2016-03-21 19:09:20 -04:00
|
|
|
items
|
2014-01-15 09:16:23 -05:00
|
|
|
end
|
|
|
|
end
|
2018-08-27 11:31:01 -04:00
|
|
|
# rubocop: enable CodeReuse/ActiveRecord
|
2014-01-15 09:16:23 -05:00
|
|
|
|
2018-02-28 06:16:29 -05:00
|
|
|
def by_updated_at(items)
|
|
|
|
items = items.updated_after(params[:updated_after]) if params[:updated_after].present?
|
|
|
|
items = items.updated_before(params[:updated_before]) if params[:updated_before].present?
|
|
|
|
|
|
|
|
items
|
|
|
|
end
|
|
|
|
|
2019-01-17 12:49:07 -05:00
|
|
|
def by_closed_at(items)
|
|
|
|
items = items.closed_after(params[:closed_after]) if params[:closed_after].present?
|
|
|
|
items = items.closed_before(params[:closed_before]) if params[:closed_before].present?
|
|
|
|
|
|
|
|
items
|
|
|
|
end
|
|
|
|
|
2014-01-15 09:16:23 -05:00
|
|
|
def by_state(items)
|
2016-11-30 03:11:43 -05:00
|
|
|
case params[:state].to_s
|
|
|
|
when 'closed'
|
|
|
|
items.closed
|
|
|
|
when 'merged'
|
|
|
|
items.respond_to?(:merged) ? items.merged : items.closed
|
|
|
|
when 'opened'
|
|
|
|
items.opened
|
2018-06-26 15:30:29 -04:00
|
|
|
when 'locked'
|
2019-10-18 07:11:44 -04:00
|
|
|
items.with_state(:locked)
|
2014-01-15 09:16:23 -05:00
|
|
|
else
|
2016-09-23 09:16:11 -04:00
|
|
|
items
|
2014-01-15 09:16:23 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def by_group(items)
|
2016-03-20 16:14:39 -04:00
|
|
|
# Selection by group is already covered by `by_project` and `projects`
|
2014-01-15 09:16:23 -05:00
|
|
|
items
|
|
|
|
end
|
|
|
|
|
2018-08-27 11:31:01 -04:00
|
|
|
# rubocop: disable CodeReuse/ActiveRecord
|
2014-01-15 09:16:23 -05:00
|
|
|
def by_project(items)
|
2020-03-30 17:08:01 -04:00
|
|
|
if params.project?
|
|
|
|
items.of_projects(params.projects).references_project
|
|
|
|
elsif params.projects
|
|
|
|
items.merge(params.projects.reorder(nil)).join_project
|
|
|
|
else
|
|
|
|
items.none
|
|
|
|
end
|
2014-01-15 09:16:23 -05:00
|
|
|
end
|
2018-08-27 11:31:01 -04:00
|
|
|
# rubocop: enable CodeReuse/ActiveRecord
|
2014-01-15 09:16:23 -05:00
|
|
|
|
2018-08-27 11:31:01 -04:00
|
|
|
# rubocop: disable CodeReuse/ActiveRecord
|
2014-01-15 09:16:23 -05:00
|
|
|
def by_search(items)
|
2018-06-05 13:31:32 -04:00
|
|
|
return items unless search
|
2019-10-23 05:06:03 -04:00
|
|
|
return items if items.is_a?(ActiveRecord::NullRelation)
|
2018-06-05 13:31:32 -04:00
|
|
|
|
Extend CTE search optimisation to projects
When we use the `search` param on an `IssuableFinder`, we can run into
issues. We have trigram indexes to support these searches. On
GitLab.com, we often see Postgres's optimiser prioritise the (global)
trigram indexes over the index on `project_id`. For group and project
searches, we know that it will be quicker to filter by `project_id`
first, as it returns fewer rows in most cases.
For group issues search, we ran into this issue previously, and went
through the following iterations:
1. Use a CTE on the project IDs as an optimisation fence. This prevents
the planner from disregarding the index on `project_id`.
Unfortunately it breaks some types of sorting, like priority and
popularity, as they sort on a joined table.
2. Use a subquery for listing issues, and a CTE for counts. The subquery
- in the case of group lists - didn't help as much as the CTE, but
was faster than not including it. We can safely use a CTE for counts
as they don't have sorting.
Now, however, we're seeing the same issue in a project context. The
subquery doesn't help at all there (it would only return one row, after
all). In an attempt to keep total code complexity under control, this
commit removes the subquery optimisation and applies the CTE
optimisation only for sorts we know that are safe.
This means that for more complicated sorts (like priority and
popularity), the search will continue to be very slow. If this is a
high-priority issue, we can consider introducing further optimisations,
but this finder is already very complicated and additional complexity
has a cost.
The group CTE optimisation is controlled by the same feature flag as
before, `attempt_group_search_optimizations`, which is enabled by
default. The new project CTE optimisation is controlled by a new feature
flag, `attempt_project_search_optimizations`, which is disabled by
default.
2019-04-03 05:46:13 -04:00
|
|
|
if use_cte_for_search?
|
2018-06-05 13:31:32 -04:00
|
|
|
cte = Gitlab::SQL::RecursiveCTE.new(klass.table_name)
|
|
|
|
cte << items
|
|
|
|
|
|
|
|
items = klass.with(cte.to_arel).from(klass.table_name)
|
|
|
|
end
|
|
|
|
|
2019-06-12 20:08:44 -04:00
|
|
|
items.full_search(search, matched_columns: params[:in], use_minimum_char_limit: !use_cte_for_search?)
|
2017-02-17 13:28:32 -05:00
|
|
|
end
|
2018-08-27 11:31:01 -04:00
|
|
|
# rubocop: enable CodeReuse/ActiveRecord
|
2014-01-15 09:16:23 -05:00
|
|
|
|
2018-08-27 11:31:01 -04:00
|
|
|
# rubocop: disable CodeReuse/ActiveRecord
|
2017-02-17 13:28:32 -05:00
|
|
|
def by_iids(items)
|
|
|
|
params[:iids].present? ? items.where(iid: params[:iids]) : items
|
2014-01-15 09:16:23 -05:00
|
|
|
end
|
2018-08-27 11:31:01 -04:00
|
|
|
# rubocop: enable CodeReuse/ActiveRecord
|
2014-01-15 09:16:23 -05:00
|
|
|
|
2020-05-06 23:09:46 -04:00
|
|
|
# rubocop: disable CodeReuse/ActiveRecord
|
|
|
|
def by_negated_iids(items)
|
|
|
|
not_params[:iids].present? ? items.where.not(iid: not_params[:iids]) : items
|
|
|
|
end
|
|
|
|
# rubocop: enable CodeReuse/ActiveRecord
|
|
|
|
|
2018-08-27 11:31:01 -04:00
|
|
|
# rubocop: disable CodeReuse/ActiveRecord
|
2014-01-15 09:16:23 -05:00
|
|
|
def sort(items)
|
2015-11-11 06:50:36 -05:00
|
|
|
# Ensure we always have an explicit sort order (instead of inheriting
|
|
|
|
# multiple orders when combining ActiveRecord::Relation objects).
|
2020-03-30 17:08:01 -04:00
|
|
|
params[:sort] ? items.sort_by_attribute(params[:sort], excluded_labels: params.label_names) : items.reorder(id: :desc)
|
2014-01-15 09:16:23 -05:00
|
|
|
end
|
2018-08-27 11:31:01 -04:00
|
|
|
# rubocop: enable CodeReuse/ActiveRecord
|
2014-01-15 09:16:23 -05:00
|
|
|
|
2018-08-27 11:31:01 -04:00
|
|
|
# rubocop: disable CodeReuse/ActiveRecord
|
2014-12-05 10:13:07 -05:00
|
|
|
def by_author(items)
|
2020-03-30 17:08:01 -04:00
|
|
|
if params.author
|
|
|
|
items.where(author_id: params.author.id)
|
|
|
|
elsif params.no_author?
|
|
|
|
items.where(author_id: nil)
|
|
|
|
elsif params.author_id? || params.author_username? # author not found
|
|
|
|
items.none
|
|
|
|
else
|
|
|
|
items
|
2014-12-05 10:13:07 -05:00
|
|
|
end
|
|
|
|
end
|
2018-08-27 11:31:01 -04:00
|
|
|
# rubocop: enable CodeReuse/ActiveRecord
|
2014-12-05 10:13:07 -05:00
|
|
|
|
2020-05-06 23:09:46 -04:00
|
|
|
# rubocop: disable CodeReuse/ActiveRecord
|
|
|
|
def by_negated_author(items)
|
|
|
|
if not_params.author
|
|
|
|
items.where.not(author_id: not_params.author.id)
|
|
|
|
elsif not_params.author_id? || not_params.author_username? # author not found
|
|
|
|
items.none
|
|
|
|
else
|
|
|
|
items
|
|
|
|
end
|
|
|
|
end
|
|
|
|
# rubocop: enable CodeReuse/ActiveRecord
|
2020-01-28 10:08:36 -05:00
|
|
|
|
2020-05-06 23:09:46 -04:00
|
|
|
def by_assignee(items)
|
2020-03-30 17:08:01 -04:00
|
|
|
if params.filter_by_no_assignee?
|
2019-04-07 14:35:16 -04:00
|
|
|
items.unassigned
|
2020-03-30 17:08:01 -04:00
|
|
|
elsif params.filter_by_any_assignee?
|
2019-04-07 14:35:16 -04:00
|
|
|
items.assigned
|
2020-03-30 17:08:01 -04:00
|
|
|
elsif params.assignee
|
|
|
|
items.assigned_to(params.assignee)
|
|
|
|
elsif params.assignee_id? || params.assignee_username? # assignee not found
|
2019-04-07 14:35:16 -04:00
|
|
|
items.none
|
|
|
|
else
|
|
|
|
items
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-05-06 23:09:46 -04:00
|
|
|
def by_negated_assignee(items)
|
|
|
|
# We want CE users to be able to say "Issues not assigned to either PersonA nor PersonB"
|
|
|
|
if not_params.assignees.present?
|
|
|
|
items.not_assigned_to(not_params.assignees)
|
|
|
|
elsif not_params.assignee_id? || not_params.assignee_username? # assignee not found
|
|
|
|
items.none
|
|
|
|
else
|
|
|
|
items
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-08-27 11:31:01 -04:00
|
|
|
# rubocop: disable CodeReuse/ActiveRecord
|
2015-10-16 05:43:26 -04:00
|
|
|
def by_milestone(items)
|
2020-03-30 17:08:01 -04:00
|
|
|
return items unless params.milestones?
|
|
|
|
|
|
|
|
if params.filter_by_no_milestone?
|
|
|
|
items.left_joins_milestones.where(milestone_id: [-1, nil])
|
|
|
|
elsif params.filter_by_any_milestone?
|
|
|
|
items.any_milestone
|
|
|
|
elsif params.filter_by_upcoming_milestone?
|
|
|
|
upcoming_ids = Milestone.upcoming_ids(params.projects, params.related_groups)
|
|
|
|
items.left_joins_milestones.where(milestone_id: upcoming_ids)
|
|
|
|
elsif params.filter_by_started_milestone?
|
|
|
|
items.left_joins_milestones.merge(Milestone.started)
|
|
|
|
else
|
|
|
|
items.with_milestone(params[:milestone_title])
|
2015-10-16 05:43:26 -04:00
|
|
|
end
|
|
|
|
end
|
2018-08-27 11:31:01 -04:00
|
|
|
# rubocop: enable CodeReuse/ActiveRecord
|
2015-10-16 05:43:26 -04:00
|
|
|
|
2020-05-06 23:09:46 -04:00
|
|
|
# rubocop: disable CodeReuse/ActiveRecord
|
|
|
|
def by_negated_milestone(items)
|
|
|
|
return items unless not_params.milestones?
|
|
|
|
|
|
|
|
if not_params.filter_by_upcoming_milestone?
|
|
|
|
items.joins(:milestone).merge(Milestone.not_upcoming)
|
|
|
|
elsif not_params.filter_by_started_milestone?
|
|
|
|
items.joins(:milestone).merge(Milestone.not_started)
|
|
|
|
else
|
|
|
|
items.without_particular_milestone(not_params[:milestone_title])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
# rubocop: enable CodeReuse/ActiveRecord
|
|
|
|
|
2019-11-22 13:06:00 -05:00
|
|
|
def by_release(items)
|
2020-03-30 17:08:01 -04:00
|
|
|
return items unless params.releases?
|
2019-11-22 13:06:00 -05:00
|
|
|
|
2020-03-30 17:08:01 -04:00
|
|
|
if params.filter_by_no_release?
|
2019-11-22 13:06:00 -05:00
|
|
|
items.without_release
|
2020-03-30 17:08:01 -04:00
|
|
|
elsif params.filter_by_any_release?
|
2019-11-22 13:06:00 -05:00
|
|
|
items.any_release
|
|
|
|
else
|
|
|
|
items.with_release(params[:release_tag], params[:project_id])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-05-06 23:09:46 -04:00
|
|
|
def by_negated_release(items)
|
|
|
|
return items unless not_params.releases?
|
|
|
|
|
|
|
|
items.without_particular_release(not_params[:release_tag], not_params[:project_id])
|
|
|
|
end
|
|
|
|
|
2014-01-15 09:16:23 -05:00
|
|
|
def by_label(items)
|
2020-03-30 17:08:01 -04:00
|
|
|
return items unless params.labels?
|
2014-01-15 09:16:23 -05:00
|
|
|
|
2020-03-30 17:08:01 -04:00
|
|
|
if params.filter_by_no_label?
|
|
|
|
items.without_label
|
|
|
|
elsif params.filter_by_any_label?
|
2020-05-26 05:08:06 -04:00
|
|
|
items.any_label(params[:sort])
|
2020-03-30 17:08:01 -04:00
|
|
|
else
|
2020-05-06 23:09:46 -04:00
|
|
|
items.with_label(params.label_names, params[:sort])
|
2017-08-30 03:48:55 -04:00
|
|
|
end
|
2018-10-26 22:52:06 -04:00
|
|
|
end
|
|
|
|
|
2020-05-06 23:09:46 -04:00
|
|
|
def by_negated_label(items)
|
|
|
|
return items unless not_params.labels?
|
|
|
|
|
|
|
|
items.without_particular_labels(not_params.label_names)
|
|
|
|
end
|
|
|
|
|
2020-03-30 17:08:01 -04:00
|
|
|
def by_my_reaction_emoji(items)
|
|
|
|
return items unless params[:my_reaction_emoji] && current_user
|
2018-10-26 22:52:06 -04:00
|
|
|
|
2020-03-30 17:08:01 -04:00
|
|
|
if params.filter_by_no_reaction?
|
|
|
|
items.not_awarded(current_user)
|
|
|
|
elsif params.filter_by_any_reaction?
|
|
|
|
items.awarded(current_user)
|
2016-03-14 05:46:26 -04:00
|
|
|
else
|
2020-03-30 17:08:01 -04:00
|
|
|
items.awarded(current_user, params[:my_reaction_emoji])
|
2016-03-14 05:46:26 -04:00
|
|
|
end
|
2016-02-19 00:27:41 -05:00
|
|
|
end
|
|
|
|
|
2020-05-06 23:09:46 -04:00
|
|
|
def by_negated_my_reaction_emoji(items)
|
|
|
|
return items unless not_params[:my_reaction_emoji] && current_user
|
|
|
|
|
|
|
|
items.not_awarded(current_user, not_params[:my_reaction_emoji])
|
2016-11-27 05:33:15 -05:00
|
|
|
end
|
|
|
|
|
2020-05-06 23:09:46 -04:00
|
|
|
def by_non_archived(items)
|
|
|
|
params[:non_archived].present? ? items.non_archived : items
|
2020-01-28 10:08:36 -05:00
|
|
|
end
|
2014-01-15 09:16:23 -05:00
|
|
|
end
|