Completed
Push — mongo-graph ( 87a116...a5e079 )
by Michael
03:26
created

UpdatePlan.update_support_fields()   A

Complexity

Conditions 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.2963

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 6
ccs 1
cts 3
cp 0.3333
crap 1.2963
rs 9.4285
c 0
b 0
f 0
1
# frozen_string_literal: true
2
3 1
module NoSE
4 1
  module Plans
5
    # A simple state class to hold the cardinality for updates
6 1
    class UpdateState
7 1
      attr_reader :statement, :cardinality
8
9 1
      def initialize(statement, cardinality)
10
        @statement = statement
11
        @cardinality = cardinality
12
      end
13
14
      # XXX This is just a placeholder since this is
15
      #     currently used by the query planner
16
      # @return [Boolean]
17 1
      def answered?
18
        true
19
      end
20
    end
21
22
    # A plan for executing an update
23 1
    class UpdatePlan < AbstractPlan
24 1
      attr_reader :statement, :index, :query_plans, :update_steps, :cost_model,
25
                  :update_fields
26
27 1
      include Comparable
28
29 1 View Code Duplication
      def initialize(statement, index, trees, update_steps, cost_model)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
30
        @statement = statement
31
        @index = index
32
        @trees = trees
33
        @query_plans = nil # these will be set later when we pick indexes
34
        update_steps.each { |step| step.calculate_cost cost_model }
35
        @update_steps = update_steps
36
        @cost_model = cost_model
37
38
        # Update with fields specified in the settings and conditions
39
        # (rewrite from foreign keys to IDs if needed)
40
        @update_fields = if statement.is_a?(Connection) ||
41
                            statement.is_a?(Delete)
42
                           []
43
                         else
44
                           statement.settings.map(&:field)
45
                         end
46
        @update_fields += statement.conditions.each_value.map(&:field)
47
        @update_fields.map! do |field|
48
          field.is_a?(Fields::ForeignKeyField) ? field.entity.id_field : field
49
        end
50
      end
51
52
      # The weight of this query for a given workload
53
      # @return [Fixnum]
54 1
      def weight
55
        return 1 if @workload.nil?
56
57
        @workload.statement_weights[@statement]
58
      end
59
60
      # The group of the associated statement
61
      # @return [String]
62 1
      def group
63
        @statement.group
64
      end
65
66
      # Name the plan by the statement
67
      # @return [String]
68 1
      def name
69
        "#{@statement.text} for #{@index.key}"
70
      end
71
72
      # The steps for this plan are the update steps
73
      # @return [Array<UpdatePlanStep>]
74 1
      def steps
75
        @update_steps
76
      end
77
78
      # Parameters to this update plan
79
      # @return [Hash]
80 1
      def params
81
        conditions = if @statement.respond_to?(:conditions)
82
                       @statement.conditions
83
                     else
84
                       {}
85
                     end
86
        settings = if @statement.respond_to?(:settings)
87
                     @statement.settings
88
                   else
89
                     []
90
                   end
91
92
        params = conditions.merge Hash[settings.map do |setting|
93
          [setting.field.id, Condition.new(setting.field, :'=', setting.value)]
94
        end]
95
96
        convert_param_keys params
97
      end
98
99
      # Select query plans to actually use here
100
      # @return [void]
101 1
      def select_query_plans(indexes = nil, &block)
102
        if block_given?
103
          @query_plans = @trees.map(&block)
104
        else
105
          @query_plans = @trees.map do |tree|
106
            plan = tree.select_using_indexes(indexes).min_by(&:cost)
107
            fail if plan.nil?
108
            plan
109
          end
110
        end
111
112
        update_support_fields
113
114
        @trees = nil
115
      end
116
117
      # Compare all the fields for the plan for equality
118
      # @return [Boolean]
119 1
      def eql?(other)
120
        return false unless other.is_a? UpdatePlan
121
        fail 'plans must be resolved before checking equality' \
122
          if @query_plans.nil? || other.query_plans.nil?
123
124
        @statement == other.statement &&
125
          @index == other.index &&
126
          @query_plans == other.query_plans &&
127
          @update_steps == other.update_steps &&
128
          @cost_model == other.cost_model
129
      end
130
131
      # :nocov:
132 1
      def to_color
133
        "\n   statement: " + @statement.to_color +
134
          "\n       index: " + @index.to_color +
135
          "\n query_plans: " + @query_plans.to_color +
136
          "\nupdate_steps: " + @update_steps.to_color +
137
          "\n  cost_model: " + @cost_model.to_color
138
      end
139
      # :nocov:
140
141
      # Two plans are compared by their execution cost
142
      # @return [Boolean]
143 1
      def <=>(other)
144
        cost <=> other.cost
145
      end
146
147
      # The cost of performing the update on this index
148
      # @return [Fixnum]
149 1
      def update_cost
150
        @update_steps.sum_by(&:cost)
151
      end
152
153
      # The cost is the sum of all the query costs plus the update costs
154
      # @return [Fixnum]
155 1
      def cost
156
        @query_plans.sum_by(&:cost) + update_cost
157
      end
158
159 1
      private
160
161
      # Add fields from support queries to those which should be updated
162
      # @return [void]
163 1
      def update_support_fields
164
        # Add fields fetched from support queries
165
        @update_fields += @query_plans.flat_map do |query_plan|
166
          query_plan.query.select.to_a
167
        end.compact
168
      end
169
170
      # Ensure we only use primary keys for conditions
171
      # @return [Hash]
172 1
      def convert_param_keys(params)
173
        Hash[params.each_value.map do |condition|
174
          field = condition.field
175
          if field.is_a?(Fields::ForeignKeyField)
176
            field = field.entity.id_field
177
            condition = Condition.new field, condition.operator,
178
                                      condition.value
179
          end
180
181
          [field.id, condition]
182
        end]
183
      end
184
    end
185
186
    # A planner for update statements in the workload
187 1
    class UpdatePlanner
188 1 View Code Duplication
      def initialize(model, trees, cost_model, by_id_graph = false)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
189
        @logger = Logging.logger['nose::update_planner']
190
191
        @model = model
192
        @cost_model = cost_model
193
        @by_id_graph = by_id_graph
194
195
        # Remove anything not a support query then group by statement and index
196
        @query_plans = trees.select do |tree|
197
          tree.query.is_a? SupportQuery
198
        end
199
        @query_plans = @query_plans.group_by { |tree| tree.query.statement }
200
        @query_plans.each do |plan_stmt, plan_trees|
201
          @query_plans[plan_stmt] = plan_trees.group_by do |tree|
202
            index = tree.query.index
203
            index = index.to_id_path if @by_id_path
204
205
            index
206
          end
207
        end
208
      end
209
210
      # Find the necessary update plans for a given set of indexes
211
      # @return [Array<UpdatePlan>]
212 1
      def find_plans_for_update(statement, indexes)
0 ignored issues
show
Coding Style introduced by
The Assignment, Branch, Condition size for find_plans_for_update is considered too high. [27.96/20]. The ABC size is based on assignments, branches (method calls), and conditions.
Loading history...
Coding Style introduced by
This method is 24 lines long. Your coding style permits a maximum length of 20.
Loading history...
213
        indexes = indexes.map(&:to_id_graph).to_set if @by_id_graph
214
215
        indexes.map do |index|
216
          next unless statement.modifies_index?(index)
217
218
          if (@query_plans[statement] &&
219
              @query_plans[statement][index]).nil?
220
            trees = []
221
222
            if statement.is_a? Insert
223
              cardinality = 1
224
            else
225
              cardinality = Cardinality.filter index.entries,
226
                                               statement.eq_fields,
227
                                               statement.range_field
228
            end
229
          else
230
            # Get the cardinality of the last step to use for the update state
231
            trees = @query_plans[statement][index]
232
            plans = trees.map do |tree|
233
              tree.select_using_indexes(indexes).min_by(&:cost)
234
            end
235
236
            # Multiply the cardinalities because we are crossing multiple
237
            # relationships and need the cross-product
238
            cardinality = plans.product_by { |p| p.last.state.cardinality }
239
          end
240
241
          state = UpdateState.new statement, cardinality
242
          update_steps = update_steps statement, index, state
243
          UpdatePlan.new statement, index, trees, update_steps, @cost_model
244
        end.compact
245
      end
246
247 1
      private
248
249
      # Find the required update steps
250
      # @return [Array<UpdatePlanStep>]
251 1
      def update_steps(statement, index, state)
252
        update_steps = []
253
        update_steps << DeletePlanStep.new(index, state) \
254
          if statement.requires_delete?(index)
255
256
        if statement.requires_insert?(index)
257
          fields = if statement.is_a?(Connect)
258
                     statement.conditions.each_value.map(&:field)
259
                   else
260
                     statement.settings.map(&:field)
261
                   end
262
263
          update_steps << InsertPlanStep.new(index, state, fields)
264
        end
265
266
        update_steps
267
      end
268
    end
269
  end
270
end
271