summaryrefslogtreecommitdiff
path: root/lib/event_filter.rb
blob: 8833207dd1d2057336a04ff34e8661b0e7d24ab7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# frozen_string_literal: true

# rubocop: disable CodeReuse/ActiveRecord
class EventFilter
  include Gitlab::Utils::StrongMemoize

  attr_accessor :filter

  ALL = 'all'
  PUSH = 'push'
  MERGED = 'merged'
  ISSUE = 'issue'
  COMMENTS = 'comments'
  TEAM = 'team'
  WIKI = 'wiki'
  DESIGNS = 'designs'

  def initialize(filter)
    # Split using comma to maintain backward compatibility Ex/ "filter1,filter2"
    filter = filter.to_s.split(',')[0].to_s
    @filter = filters.include?(filter) ? filter : ALL
  end

  def active?(key)
    filter == key.to_s
  end

  def apply_filter(events)
    case filter
    when PUSH
      events.pushed_action
    when MERGED
      events.merged_action
    when COMMENTS
      events.commented_action
    when TEAM
      events.where(action: Event::TEAM_ACTIONS)
    when ISSUE
      events.where(action: Event::ISSUE_ACTIONS, target_type: 'Issue')
    when WIKI
      wiki_events(events)
    when DESIGNS
      design_events(events)
    else
      events
    end
  end

  # rubocop: disable Metrics/CyclomaticComplexity
  # This method build specialized in-operator optimized queries based on different
  # filter parameters. All queries will benefit from the index covering the following columns:
  # author_id target_type action id
  #
  # More context: https://docs.gitlab.com/ee/development/database/efficient_in_operator_queries.html#the-inoperatoroptimization-module
  def in_operator_query_builder_params(user_ids)
    case filter
    when ALL
      in_operator_params(array_scope_ids: user_ids)
    when PUSH
      # Here we need to add an order hint column to force the correct index usage.
      # Without the order hint, the following conditions will use the `index_events_on_author_id_and_id`
      # index which is not as efficient as the `index_events_for_followed_users` index.
      # > target_type IS NULL AND action = 5 AND author_id = X ORDER BY id DESC
      #
      # The order hint adds an extra order by column which doesn't affect the result but forces the planner
      # to use the correct index:
      # > target_type IS NULL AND action = 5 AND author_id = X ORDER BY target_type DESC, id DESC
      in_operator_params(
        array_scope_ids: user_ids,
        scope: Event.where(target_type: nil).pushed_action,
        order_hint_column: :target_type
      )
    when MERGED
      in_operator_params(
        array_scope_ids: user_ids,
        scope: Event.where(target_type: MergeRequest.to_s).merged_action
      )
    when COMMENTS
      in_operator_params(
        array_scope_ids: user_ids,
        scope: Event.commented_action,
        in_column: :target_type,
        in_values: [Note, *Note.descendants].map(&:name) # To make the query efficient we need to list all Note classes
      )
    when TEAM
      in_operator_params(
        array_scope_ids: user_ids,
        scope: Event.where(target_type: nil),
        order_hint_column: :target_type,
        in_column: :action,
        in_values: Event.actions.values_at(*Event::TEAM_ACTIONS)
      )
    when ISSUE
      in_operator_params(
        array_scope_ids: user_ids,
        scope: Event.where(target_type: Issue.name),
        in_column: :action,
        in_values: Event.actions.values_at(*Event::ISSUE_ACTIONS)
      )
    when WIKI
      in_operator_params(
        array_scope_ids: user_ids,
        scope: Event.for_wiki_page,
        in_column: :action,
        in_values: Event.actions.values_at(*Event::WIKI_ACTIONS)
      )
    when DESIGNS
      in_operator_params(
        array_scope_ids: user_ids,
        scope: Event.for_design,
        in_column: :action,
        in_values: Event.actions.values_at(*Event::DESIGN_ACTIONS)
      )
    else
      in_operator_params(array_scope_ids: user_ids)
    end
  end
  # rubocop: enable Metrics/CyclomaticComplexity

  private

  def in_operator_params(array_scope_ids:, scope: nil, in_column: nil, in_values: nil, order_hint_column: nil)
    base_scope = Event.all
    base_scope = base_scope.merge(scope) if scope

    order = { id: :desc }
    finder_query = -> (id_expression) { Event.where(Event.arel_table[:id].eq(id_expression)) }

    if order_hint_column.present?
      order = Gitlab::Pagination::Keyset::Order.build([
        Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
          attribute_name: order_hint_column,
          order_expression: Event.arel_table[order_hint_column].desc,
          nullable: :nulls_last,
          distinct: false
        ),
        Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
          attribute_name: :id,
          order_expression: Event.arel_table[:id].desc
        )
      ])

      finder_query = -> (_order_hint, id_expression) { Event.where(Event.arel_table[:id].eq(id_expression)) }
    end

    base_scope = base_scope.reorder(order)

    array_params = in_operator_array_params(
      array_scope_ids: array_scope_ids,
      scope: base_scope,
      in_column: in_column,
      in_values: in_values
    )

    array_params.merge(
      scope: base_scope,
      finder_query: finder_query
    )
  end

  # This method builds the array_ parameters
  # without in_column parameter: uses one IN filter: author_id
  # with in_column: two IN filters: author_id, (target_type OR action)
  def in_operator_array_params(scope:, array_scope_ids:, in_column: nil, in_values: nil)
    if in_column
      # Builds Carthesian product of the in_values and the array_scope_ids (in this case: user_ids).
      # The process is described here: https://docs.gitlab.com/ee/development/database/efficient_in_operator_queries.html#multiple-in-queries
      # VALUES ((array_scope_ids[0], in_values[0]), (array_scope_ids[1], in_values[0]) ...)
      cartesian = array_scope_ids.product(in_values)
      user_with_column_list = Arel::Nodes::ValuesList.new(cartesian)

      as = "array_ids(id, #{Event.connection.quote_column_name(in_column)})"
      from = Arel::Nodes::Grouping.new(user_with_column_list).as(as)
      {
        array_scope: User.select(:id, in_column).from(from),
        array_mapping_scope: -> (author_id_expression, in_column_expression) do
          Event
            .merge(scope)
            .where(Event.arel_table[:author_id].eq(author_id_expression))
            .where(Event.arel_table[in_column].eq(in_column_expression))
        end
      }
    else
      # Builds a simple query to represent the array_scope_ids
      # VALUES ((array_scope_ids[0]), (array_scope_ids[2])...)
      array_ids_list = Arel::Nodes::ValuesList.new(array_scope_ids.map { |id| [id] })
      from = Arel::Nodes::Grouping.new(array_ids_list).as('array_ids(id)')
      {
        array_scope: User.select(:id).from(from),
        array_mapping_scope: -> (author_id_expression) do
          Event
            .merge(scope)
            .where(Event.arel_table[:author_id].eq(author_id_expression))
        end
      }
    end
  end

  def wiki_events(events)
    events.for_wiki_page
  end

  def design_events(events)
    events.for_design
  end

  def filters
    [ALL, PUSH, MERGED, ISSUE, COMMENTS, TEAM, WIKI, DESIGNS]
  end
end
# rubocop: enable CodeReuse/ActiveRecord

EventFilter.prepend_mod_with('EventFilter')