nose 0.1.0pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/lib/nose/backend/cassandra.rb +390 -0
  3. data/lib/nose/backend/file.rb +185 -0
  4. data/lib/nose/backend/mongo.rb +242 -0
  5. data/lib/nose/backend.rb +557 -0
  6. data/lib/nose/cost/cassandra.rb +33 -0
  7. data/lib/nose/cost/entity_count.rb +27 -0
  8. data/lib/nose/cost/field_size.rb +31 -0
  9. data/lib/nose/cost/request_count.rb +32 -0
  10. data/lib/nose/cost.rb +68 -0
  11. data/lib/nose/debug.rb +45 -0
  12. data/lib/nose/enumerator.rb +199 -0
  13. data/lib/nose/indexes.rb +239 -0
  14. data/lib/nose/loader/csv.rb +99 -0
  15. data/lib/nose/loader/mysql.rb +199 -0
  16. data/lib/nose/loader/random.rb +48 -0
  17. data/lib/nose/loader/sql.rb +105 -0
  18. data/lib/nose/loader.rb +38 -0
  19. data/lib/nose/model/entity.rb +136 -0
  20. data/lib/nose/model/fields.rb +293 -0
  21. data/lib/nose/model.rb +113 -0
  22. data/lib/nose/parser.rb +202 -0
  23. data/lib/nose/plans/execution_plan.rb +282 -0
  24. data/lib/nose/plans/filter.rb +99 -0
  25. data/lib/nose/plans/index_lookup.rb +302 -0
  26. data/lib/nose/plans/limit.rb +42 -0
  27. data/lib/nose/plans/query_planner.rb +361 -0
  28. data/lib/nose/plans/sort.rb +49 -0
  29. data/lib/nose/plans/update.rb +60 -0
  30. data/lib/nose/plans/update_planner.rb +270 -0
  31. data/lib/nose/plans.rb +135 -0
  32. data/lib/nose/proxy/mysql.rb +275 -0
  33. data/lib/nose/proxy.rb +102 -0
  34. data/lib/nose/query_graph.rb +481 -0
  35. data/lib/nose/random/barbasi_albert.rb +48 -0
  36. data/lib/nose/random/watts_strogatz.rb +50 -0
  37. data/lib/nose/random.rb +391 -0
  38. data/lib/nose/schema.rb +89 -0
  39. data/lib/nose/search/constraints.rb +143 -0
  40. data/lib/nose/search/problem.rb +328 -0
  41. data/lib/nose/search/results.rb +200 -0
  42. data/lib/nose/search.rb +266 -0
  43. data/lib/nose/serialize.rb +747 -0
  44. data/lib/nose/statements/connection.rb +160 -0
  45. data/lib/nose/statements/delete.rb +83 -0
  46. data/lib/nose/statements/insert.rb +146 -0
  47. data/lib/nose/statements/query.rb +161 -0
  48. data/lib/nose/statements/update.rb +101 -0
  49. data/lib/nose/statements.rb +645 -0
  50. data/lib/nose/timing.rb +79 -0
  51. data/lib/nose/util.rb +305 -0
  52. data/lib/nose/workload.rb +244 -0
  53. data/lib/nose.rb +37 -0
  54. data/templates/workload.erb +42 -0
  55. metadata +700 -0
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoSE
4
+ module Plans
5
+ # Simple DSL for constructing execution plans
6
+ class ExecutionPlans
7
+ # The subdirectory execution plans are loaded from
8
+ LOAD_PATH = 'plans'
9
+ include Loader
10
+
11
+ attr_reader :groups, :weights, :schema, :mix
12
+
13
+ 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
+ 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
+ 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
+ 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
+ def DefaultMix(mix)
78
+ self.mix = mix
79
+ end
80
+
81
+ # Define a group of query execution plans
82
+ # @return [void]
83
+ 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
+ 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
+ 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
+ class QueryExecutionPlan < AbstractPlan
142
+ attr_reader :group, :name, :params, :select_fields,
143
+ :steps, :update_steps, :index
144
+ attr_accessor :query_plans
145
+
146
+ # Most of the work is delegated to the array
147
+ extend Forwardable
148
+ def_delegators :@steps, :each, :<<, :[], :==, :===, :eql?,
149
+ :inspect, :to_s, :to_a, :to_ary, :last, :length, :count
150
+
151
+ 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
+ def update_fields
166
+ @update_steps.last.fields
167
+ end
168
+
169
+ # These plans have no associated query
170
+ # @return [nil]
171
+ def query
172
+ nil
173
+ end
174
+
175
+ # The estimated cost of executing this plan
176
+ # @return [Fixnum]
177
+ 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
+ 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
+ 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
+ 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
+ def Lookup(index_key, *conditions, limit: nil)
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
+ 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
+ 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
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoSE
4
+ module Plans
5
+ # A query plan performing a filter without an index
6
+ class FilterPlanStep < PlanStep
7
+ attr_reader :eq, :range
8
+
9
+ def initialize(eq, range, state = nil)
10
+ @eq = eq
11
+ @range = range
12
+ super()
13
+
14
+ return if state.nil?
15
+ @state = state.dup
16
+ update_state
17
+ @state.freeze
18
+ end
19
+
20
+ # Two filtering steps are equal if they filter on the same fields
21
+ # @return [Boolean]
22
+ def ==(other)
23
+ other.instance_of?(self.class) && \
24
+ @eq == other.eq && @range == other.range
25
+ end
26
+
27
+ def hash
28
+ [@eq.map(&:id), @range.nil? ? nil : @range.id].hash
29
+ end
30
+
31
+ # :nocov:
32
+ def to_color
33
+ "#{super} #{@eq.to_color} #{@range.to_color} " +
34
+ begin
35
+ "#{@parent.state.cardinality} " \
36
+ "-> #{state.cardinality}"
37
+ rescue NoMethodError
38
+ ''
39
+ end
40
+ end
41
+ # :nocov:
42
+
43
+ # Check if filtering can be done (we have all the necessary fields)
44
+ def self.apply(parent, state)
45
+ # Get fields and check for possible filtering
46
+ filter_fields, eq_filter, range_filter = filter_fields parent, state
47
+ return nil if filter_fields.empty?
48
+
49
+ FilterPlanStep.new eq_filter, range_filter, state \
50
+ if required_fields?(filter_fields, parent)
51
+ end
52
+
53
+ # Get the fields we can possibly filter on
54
+ def self.filter_fields(parent, state)
55
+ eq_filter = state.eq.select { |field| parent.fields.include? field }
56
+ filter_fields = eq_filter.dup
57
+ if state.range && parent.fields.include?(state.range)
58
+ range_filter = state.range
59
+ filter_fields << range_filter
60
+ else
61
+ range_filter = nil
62
+ end
63
+
64
+ [filter_fields, eq_filter, range_filter]
65
+ end
66
+ private_class_method :filter_fields
67
+
68
+ # Check that we have all the fields we are filtering
69
+ # @return [Boolean]
70
+ def self.required_fields?(filter_fields, parent)
71
+ filter_fields.map do |field|
72
+ next true if parent.fields.member? field
73
+
74
+ # We can also filter if we have a foreign key
75
+ # XXX for now we assume this value is the same
76
+ next unless field.is_a? IDField
77
+ parent.fields.any? do |pfield|
78
+ pfield.is_a?(ForeignKeyField) && pfield.entity == field.parent
79
+ end
80
+ end.all?
81
+ end
82
+ private_class_method :required_fields?
83
+
84
+ private
85
+
86
+ # Apply the filters and perform a uniform estimate on the cardinality
87
+ # @return [void]
88
+ def update_state
89
+ @state.eq -= @eq
90
+ @state.cardinality *= @eq.map { |field| 1.0 / field.cardinality } \
91
+ .inject(1.0, &:*)
92
+ return unless @range
93
+
94
+ @state.range = nil
95
+ @state.cardinality *= 0.1
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,302 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoSE
4
+ module Plans
5
+ # Superclass for steps using indices
6
+ class IndexLookupPlanStep < PlanStep
7
+ extend Forwardable
8
+
9
+ attr_reader :index, :eq_filter, :range_filter, :limit, :order_by
10
+ delegate hash: :index
11
+
12
+ def initialize(index, state = nil, parent = nil)
13
+ super()
14
+ @index = index
15
+
16
+ if state && state.query
17
+ all_fields = state.query.all_fields
18
+ @fields = (@index.hash_fields + @index.order_fields).to_set + \
19
+ (@index.extra.to_set & all_fields)
20
+ else
21
+ @fields = @index.all_fields
22
+ end
23
+
24
+ return if state.nil?
25
+ @state = state.dup
26
+ update_state parent
27
+ @state.freeze
28
+ end
29
+
30
+ # :nocov:
31
+ def to_color
32
+ if @state.nil?
33
+ "#{super} #{@index.to_color}"
34
+ else
35
+ "#{super} #{@index.to_color} * " \
36
+ "#{@state.cardinality}/#{@state.hash_cardinality} "
37
+ end
38
+ end
39
+ # :nocov:
40
+
41
+ # Two index steps are equal if they use the same index
42
+ def ==(other)
43
+ other.instance_of?(self.class) && @index == other.index
44
+ end
45
+ alias eql? ==
46
+
47
+ # Check if this step can be applied for the given index,
48
+ # returning a possible application of the step
49
+ # @return [IndexLookupPlanStep]
50
+ def self.apply(parent, index, state)
51
+ # Validate several conditions which identify if this index is usable
52
+ begin
53
+ check_joins index, state
54
+ check_forward_lookup parent, index, state
55
+ check_parent_index parent, index, state
56
+ check_all_hash_fields parent, index, state
57
+ check_graph_fields parent, index, state
58
+ check_last_fields index, state
59
+ rescue InvalidIndex
60
+ return nil
61
+ end
62
+
63
+ IndexLookupPlanStep.new(index, state, parent)
64
+ end
65
+
66
+ # Check that this index is a valid continuation of the set of joins
67
+ # @raise [InvalidIndex]
68
+ # @return [void]
69
+ def self.check_joins(index, state)
70
+ fail InvalidIndex \
71
+ unless index.graph.entities.include?(state.joins.first) &&
72
+ (index.graph.unique_edges &
73
+ state.graph.unique_edges ==
74
+ index.graph.unique_edges)
75
+ end
76
+ private_class_method :check_joins
77
+
78
+ # Check that this index moves forward on the list of joins
79
+ # @raise [InvalidIndex]
80
+ # @return [void]
81
+ def self.check_forward_lookup(parent, index, state)
82
+ # XXX This disallows plans which look up additional attributes
83
+ # for entities other than the final one
84
+ fail InvalidIndex if index.graph.size == 1 && state.graph.size > 1 &&
85
+ !parent.is_a?(RootPlanStep)
86
+ fail InvalidIndex if index.identity? && state.graph.size > 1
87
+ end
88
+ private_class_method :check_forward_lookup
89
+
90
+ # Check if this index can be used after the current parent
91
+ # @return [Boolean]
92
+ def self.invalid_parent_index?(state, index, parent_index)
93
+ return false if parent_index.nil?
94
+
95
+ # We don't do multiple lookups by ID for the same entity set
96
+ return true if parent_index.identity? &&
97
+ index.graph == parent_index.graph
98
+
99
+ # If the last step gave an ID, we must use it
100
+ # XXX This doesn't cover all cases
101
+ last_parent_entity = state.joins.reverse.find do |entity|
102
+ parent_index.graph.entities.include? entity
103
+ end
104
+ parent_ids = Set.new [last_parent_entity.id_field]
105
+ has_ids = parent_ids.subset? parent_index.all_fields
106
+ return true if has_ids && index.hash_fields.to_set != parent_ids
107
+
108
+ # If we're looking up from a previous step, only allow lookup by ID
109
+ return true unless (index.graph.size == 1 &&
110
+ parent_index.graph != index.graph) ||
111
+ index.hash_fields == parent_ids
112
+ end
113
+ private_class_method :invalid_parent_index?
114
+
115
+ # Check that this index is a valid continuation of the set of joins
116
+ # @raise [InvalidIndex]
117
+ # @return [void]
118
+ def self.check_parent_index(parent, index, state)
119
+ fail InvalidIndex \
120
+ if invalid_parent_index? state, index, parent.parent_index
121
+ end
122
+ private_class_method :check_parent_index
123
+
124
+ # Check that we have all hash fields needed to perform the lookup
125
+ # @raise [InvalidIndex]
126
+ # @return [void]
127
+ def self.check_all_hash_fields(parent, index, state)
128
+ fail InvalidIndex unless index.hash_fields.all? do |field|
129
+ (parent.fields + state.given_fields).include? field
130
+ end
131
+ end
132
+ private_class_method :check_all_hash_fields
133
+
134
+ # Get fields in the query relevant to this index
135
+ # and check that they are provided for us here
136
+ # @raise [InvalidIndex]
137
+ # @return [void]
138
+ def self.check_graph_fields(parent, index, state)
139
+ hash_entity = index.hash_fields.first.parent
140
+ graph_fields = state.fields_for_graph(index.graph, hash_entity).to_set
141
+ graph_fields -= parent.fields # exclude fields already fetched
142
+ fail InvalidIndex unless graph_fields.subset?(index.all_fields)
143
+ end
144
+ private_class_method :check_graph_fields
145
+
146
+ # Check that we have the required fields to move on with the next lookup
147
+ # @return [Boolean]
148
+ def self.last_fields?(index, state)
149
+ index_includes = lambda do |fields|
150
+ fields.all? { |f| index.all_fields.include? f }
151
+ end
152
+
153
+ # We must have either the ID or all the fields
154
+ # for leaf entities in the original graph
155
+ leaf_entities = index.graph.entities.select do |entity|
156
+ state.graph.leaf_entity?(entity)
157
+ end
158
+ leaf_entities.all? do |entity|
159
+ index_includes.call([entity.id_field]) ||
160
+ index_includes.call(state.fields.select { |f| f.parent == entity })
161
+ end
162
+ end
163
+ private_class_method :last_fields?
164
+
165
+ # @raise [InvalidIndex]
166
+ # @return [void]
167
+ def self.check_last_fields(index, state)
168
+ fail InvalidIndex unless last_fields?(index, state)
169
+ end
170
+ private_class_method :check_last_fields
171
+
172
+ private
173
+
174
+ # Get the set of fields which can be filtered by the ordered keys
175
+ # @return [Array<Fields::Field>]
176
+ def range_order_prefix
177
+ order_prefix = (@state.eq - @index.hash_fields) & @index.order_fields
178
+ order_prefix << @state.range unless @state.range.nil?
179
+ order_prefix = order_prefix.zip(@index.order_fields)
180
+ order_prefix.take_while { |x, y| x == y }.map(&:first)
181
+ end
182
+
183
+ # Perform any ordering implicit to this index
184
+ # @return [Boolean] whether this index is by ID
185
+ def resolve_order
186
+ # We can't resolve ordering if we're doing an ID lookup
187
+ # since only one record exists per row (if it's the same entity)
188
+ # We also need to have the fields used in order
189
+ first_join = @state.query.join_order.detect do |entity|
190
+ @index.graph.entities.include? entity
191
+ end
192
+ indexed_by_id = @index.hash_fields.include?(first_join.id_field)
193
+ order_prefix = @state.order_by.longest_common_prefix(
194
+ @index.order_fields - @eq_filter.to_a
195
+ )
196
+ if indexed_by_id && order_prefix.map(&:parent).to_set ==
197
+ Set.new([@index.hash_fields.first.parent])
198
+ order_prefix = []
199
+ else
200
+ @state.order_by -= order_prefix
201
+ end
202
+ @order_by = order_prefix
203
+
204
+ indexed_by_id
205
+ end
206
+
207
+ # Strip the graph for this index, but if we haven't fetched all
208
+ # fields, leave the last one so we can perform a separate ID lookup
209
+ # @return [void]
210
+ def strip_graph
211
+ hash_entity = @index.hash_fields.first.parent
212
+ @state.graph = @state.graph.dup
213
+ required_fields = @state.fields_for_graph(@index.graph, hash_entity,
214
+ select: true).to_set
215
+ if required_fields.subset?(@index.all_fields) &&
216
+ @state.graph == @index.graph
217
+ removed_nodes = @state.joins[0..@index.graph.size]
218
+ @state.joins = @state.joins[@index.graph.size..-1]
219
+ else
220
+ removed_nodes = if index.graph.size == 1
221
+ []
222
+ else
223
+ @state.joins[0..@index.graph.size - 2]
224
+ end
225
+ @state.joins = @state.joins[@index.graph.size - 1..-1]
226
+ end
227
+
228
+ # Remove nodes which have been processed from the graph
229
+ @state.graph.remove_nodes removed_nodes
230
+ end
231
+
232
+ # Update the cardinality of this step, applying a limit if possible
233
+ def update_cardinality(parent, indexed_by_id)
234
+ # Calculate the new cardinality assuming no limit
235
+ # Hash cardinality starts at 1 or is the previous cardinality
236
+ if parent.is_a?(RootPlanStep)
237
+ @state.hash_cardinality = 1
238
+ else
239
+ @state.hash_cardinality = parent.state.cardinality
240
+ end
241
+
242
+ # Filter the total number of rows by filtering on non-hash fields
243
+ cardinality = @index.per_hash_count * @state.hash_cardinality
244
+ @state.cardinality = Cardinality.filter cardinality,
245
+ @eq_filter -
246
+ @index.hash_fields,
247
+ @range_filter
248
+
249
+ # Check if we can apply the limit from the query
250
+ # This occurs either when we are on the first or last index lookup
251
+ # and the ordering of the query has already been resolved
252
+ order_resolved = @state.order_by.empty? && @state.graph.size == 1
253
+ return unless (@state.answered?(check_limit: false) ||
254
+ parent.is_a?(RootPlanStep) && order_resolved) &&
255
+ !@state.query.limit.nil?
256
+
257
+ # XXX Assume that everything is limited by the limit value
258
+ # which should be fine if the limit is small enough
259
+ @limit = @state.query.limit
260
+ if parent.is_a?(RootPlanStep)
261
+ @state.cardinality = [@limit, @state.cardinality].min
262
+ @state.hash_cardinality = 1
263
+ else
264
+ @limit = @state.cardinality = @state.query.limit
265
+
266
+ # If this is a final lookup by ID, go with the limit
267
+ if @index.graph.size == 1 && indexed_by_id
268
+ @state.hash_cardinality = @limit
269
+ else
270
+ @state.hash_cardinality = parent.state.cardinality
271
+ end
272
+ end
273
+ end
274
+
275
+ # Modify the state to reflect the fields looked up by the index
276
+ # @return [void]
277
+ def update_state(parent)
278
+ order_prefix = range_order_prefix.to_set
279
+
280
+ # Find fields which are filtered by the index
281
+ @eq_filter = @index.hash_fields + (@state.eq & order_prefix)
282
+ if order_prefix.include?(@state.range)
283
+ @range_filter = @state.range
284
+ @state.range = nil
285
+ else
286
+ @range_filter = nil
287
+ end
288
+
289
+ # Remove fields resolved by this index
290
+ @state.fields -= @index.all_fields
291
+ @state.eq -= @eq_filter
292
+
293
+ indexed_by_id = resolve_order
294
+ strip_graph
295
+ update_cardinality parent, indexed_by_id
296
+ end
297
+ end
298
+
299
+ class InvalidIndex < StandardError
300
+ end
301
+ end
302
+ end