Completed
Push — master ( d20995...d9224c )
by Michael
03:12
created

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