nose 0.1.0pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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