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

QueryExecutionPlan.update_fields()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.125

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 3
ccs 1
cts 2
cp 0.5
crap 1.125
rs 10
c 0
b 0
f 0
1
# frozen_string_literal: true
2
3 1
module NoSE
4 1
  module Plans
5
    # Simple DSL for constructing execution plans
6 1
    class ExecutionPlans
7
      # The subdirectory execution plans are loaded from
8 1
      LOAD_PATH = 'plans'
9 1
      include Loader
10
11 1
      attr_reader :groups, :weights, :schema, :mix
12
13 1
      def initialize(&block)
14
        @groups = Hash.new { |h, k| h[k] = [] }
15
        @weights = Hash.new { |h, k| h[k] = {} }
16
        @mix = :default
17
18
        instance_eval(&block) if block_given?
19
20
        # Reset the mix to force weight assignment
21
        self.mix = @mix
22
      end
23
24
      # Populate the cost of each plan
25
      # @return [void]
26 1
      def calculate_cost(cost_model)
27
        @groups.each_value do |plans|
28
          plans.each do |plan|
29
            plan.steps.each do |step|
30
              cost = cost_model.index_lookup_cost step
31
              step.instance_variable_set(:@cost, cost)
32
            end
33
34
            plan.query_plans.each do |query_plan|
35
              query_plan.steps.each do |step|
36
                cost = cost_model.index_lookup_cost step
37
                step.instance_variable_set(:@cost, cost)
38
              end
39
            end
40
41
            # XXX Only bother with insert statements for now
42
            plan.update_steps.each do |step|
43
              cost = cost_model.insert_cost step
44
              step.instance_variable_set(:@cost, cost)
45
            end
46
          end
47
        end
48
      end
49
50
      # Set the weights on plans when the mix is changed
51
      # @return [void]
52 1
      def mix=(mix)
53
        @mix = mix
54
55
        @groups.each do |group, plans|
56
          plans.each do |plan|
57
            plan.instance_variable_set :@weight, @weights[group][@mix]
58
            plan.query_plans.each do |query_plan|
59
              query_plan.instance_variable_set :@weight, @weights[group][@mix]
60
            end
61
          end
62
        end
63
      end
64
65
      # rubocop:disable MethodName
66
67
      # Set the schema to be used by the execution plans
68
      # @return [void]
69 1
      def Schema(name)
70
        @schema = Schema.load name
71
        NoSE::DSL.mixin_fields @schema.model.entities, QueryExecutionPlan
72
        NoSE::DSL.mixin_fields @schema.model.entities, ExecutionPlans
73
      end
74
75
      # Set the default mix for these plans
76
      # @return [void]
77 1
      def DefaultMix(mix)
78
        self.mix = mix
79
      end
80
81
      # Define a group of query execution plans
82
      # @return [void]
83 1
      def Group(name, weight = 1.0, **mixes, &block)
84
        @group = name
85
86
        # Save the weights
87
        if mixes.empty?
88
          @weights[name][:default] = weight
89
        else
90
          @weights[name] = mixes
91
        end
92
93
        instance_eval(&block) if block_given?
94
      end
95
96
      # Define a single plan within a group
97
      # @return [void]
98 1
      def Plan(name, &block)
99
        return unless block_given?
100
101
        plan = QueryExecutionPlan.new(@group, name, @schema, self)
102
103
        # Capture one level of nesting in plans
104
        if @parent_plan.nil?
105
          @parent_plan = plan if @parent_plan.nil?
106
          set_parent = true
107
        else
108
          set_parent = false
109
        end
110
111
        plan.instance_eval(&block)
112
113
        # Reset the parent plan if it was set
114
        @parent_plan = nil if set_parent
115
116
        @groups[@group] << plan
117
      end
118
119
      # Add support queries for updates in a plan
120
      # @return [void]
121 1
      def Support(&block)
122
        # XXX Hack to swap the group name and capture support plans
123
        old_group = @group
124
        @group = '__SUPPORT__'
125
        instance_eval(&block) if block_given?
126
127
        @parent_plan.query_plans = @groups[@group]
128
        @parent_plan.query_plans.each do |plan|
129
          plan.instance_variable_set(:@group, old_group)
130
        end
131
132
        @groups[@group] = []
133
134
        @group = old_group
135
      end
136
137
      # rubocop:enable MethodName
138
    end
139
140
    # DSL to construct query execution plans
141 1
    class QueryExecutionPlan < AbstractPlan
142 1
      attr_reader :group, :name, :params, :select_fields,
143
                  :steps, :update_steps, :index
144 1
      attr_accessor :query_plans
145
146
      # Most of the work is delegated to the array
147 1
      extend Forwardable
148 1
      def_delegators :@steps, :each, :<<, :[], :==, :===, :eql?,
149
                     :inspect, :to_s, :to_a, :to_ary, :last, :length, :count
150
151 1
      def initialize(group, name, schema, plans)
152
        @group = group
153
        @name = name
154
        @schema = schema
155
        @plans = plans
156
        @select_fields = []
157
        @params = {}
158
        @steps = []
159
        @update_steps = []
160
        @query_plans = []
161
      end
162
163
      # Produce the fields updated by this plan
164
      # @return [Array<Fields::Field>]
165 1
      def update_fields
166
        @update_steps.last.fields
167
      end
168
169
      # These plans have no associated query
170
      # @return [nil]
171 1
      def query
172
        nil
173
      end
174
175
      # The estimated cost of executing this plan
176
      # @return [Fixnum]
177 1
      def cost
178
        costs = @steps.map(&:cost) + @update_steps.map(&:cost)
179
        costs += @query_plans.map(&:steps).flatten.map(&:cost)
180
181
        costs.inject(0, &:+)
182
      end
183
184
      # rubocop:disable MethodName
185
186
      # Identify fields to be selected
187
      # @return [void]
188 1
      def Select(*fields)
189
        @select_fields = fields.flatten.to_set
190
      end
191
192
      # Add parameters which are used as input to the plan
193
      # @return [void]
194 1
      def Param(field, operator, value = nil)
195
        operator = :'=' if operator == :==
196
        @params[field.id] = Condition.new(field, operator, value)
197
      end
198
199
      # Pass the support query up to the parent
200 1
      def Support(&block)
201
        @plans.Support(&block)
202
      end
203
204
      # Create a new index lookup step with a particular set of conditions
205
      # @return [void]
206 1
      def Lookup(index_key, *conditions, limit: nil)
0 ignored issues
show
Coding Style introduced by
The Assignment, Branch, Condition size for Lookup is considered too high. [26.57/20]. The ABC size is based on assignments, branches (method calls), and conditions.
Loading history...
Coding Style introduced by
This method is 27 lines long. Your coding style permits a maximum length of 20.
Loading history...
207
        index = @schema.indexes[index_key]
208
209
        step = Plans::IndexLookupPlanStep.new index
210
        eq_fields = Set.new
211
        range_field = nil
212
        conditions.each do |field, operator|
213
          if operator == :==
214
            eq_fields.add field
215
          else
216
            range_field = field
217
          end
218
        end
219
220
        step.instance_variable_set :@eq_filter, eq_fields
221
        step.instance_variable_set :@range_filter, range_field
222
223
        # XXX No ordering supported for now
224
        step.instance_variable_set :@order_by, []
225
226
        step.instance_variable_set :@limit, limit unless limit.nil?
227
228
        # Cardinality calculations adapted from
229
        # IndexLookupPlanStep#update_state
230
        state = OpenStruct.new
231
        if @steps.empty?
232
          state.hash_cardinality = 1
233
        else
234
          state.hash_cardinality = @steps.last.state.cardinality
235
        end
236
        cardinality = index.per_hash_count * state.hash_cardinality
237
        state.cardinality = Cardinality.filter cardinality,
238
                                               eq_fields - index.hash_fields,
239
                                               range_field
240
241
        step.state = state
242
243
        @steps << step
244
      end
245
246
      # Add a new insertion step into an index
247
      # @return [void]
248 1
      def Insert(index_key, *fields)
249
        @index = @schema.indexes[index_key]
250
251
        # Get cardinality from last step of each support query plan
252
        # as in UpdatePlanner#find_plans_for_update
253
        cardinalities = @query_plans.map { |p| p.steps.last.state.cardinality }
254
        cardinality = cardinalities.inject(1, &:*)
255
        state = OpenStruct.new cardinality: cardinality
256
257
        fields = @index.all_fields if fields.empty?
258
        step = Plans::InsertPlanStep.new @index, state, fields
259
260
        @update_steps << step
261
      end
262
263
      # Add a new deletion step from an index
264
      # @return [void]
265 1
      def Delete(index_key)
266
        @index = @schema.indexes[index_key]
267
268
        step = Plans::DeletePlanStep.new @index
269
270
        # Get cardinality from last step of each support query plan
271
        # as in UpdatePlanner#find_plans_for_update
272
        cardinalities = @query_plans.map { |p| p.steps.last.state.cardinality }
273
        cardinality = cardinalities.inject(1, &:*)
274
        step.state = OpenStruct.new cardinality: cardinality
275
276
        @update_steps << step
277
      end
278
279
      # rubocop:enable MethodName
280
    end
281
  end
282
end
283