nose 0.1.0pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/nose/backend/cassandra.rb +390 -0
- data/lib/nose/backend/file.rb +185 -0
- data/lib/nose/backend/mongo.rb +242 -0
- data/lib/nose/backend.rb +557 -0
- data/lib/nose/cost/cassandra.rb +33 -0
- data/lib/nose/cost/entity_count.rb +27 -0
- data/lib/nose/cost/field_size.rb +31 -0
- data/lib/nose/cost/request_count.rb +32 -0
- data/lib/nose/cost.rb +68 -0
- data/lib/nose/debug.rb +45 -0
- data/lib/nose/enumerator.rb +199 -0
- data/lib/nose/indexes.rb +239 -0
- data/lib/nose/loader/csv.rb +99 -0
- data/lib/nose/loader/mysql.rb +199 -0
- data/lib/nose/loader/random.rb +48 -0
- data/lib/nose/loader/sql.rb +105 -0
- data/lib/nose/loader.rb +38 -0
- data/lib/nose/model/entity.rb +136 -0
- data/lib/nose/model/fields.rb +293 -0
- data/lib/nose/model.rb +113 -0
- data/lib/nose/parser.rb +202 -0
- data/lib/nose/plans/execution_plan.rb +282 -0
- data/lib/nose/plans/filter.rb +99 -0
- data/lib/nose/plans/index_lookup.rb +302 -0
- data/lib/nose/plans/limit.rb +42 -0
- data/lib/nose/plans/query_planner.rb +361 -0
- data/lib/nose/plans/sort.rb +49 -0
- data/lib/nose/plans/update.rb +60 -0
- data/lib/nose/plans/update_planner.rb +270 -0
- data/lib/nose/plans.rb +135 -0
- data/lib/nose/proxy/mysql.rb +275 -0
- data/lib/nose/proxy.rb +102 -0
- data/lib/nose/query_graph.rb +481 -0
- data/lib/nose/random/barbasi_albert.rb +48 -0
- data/lib/nose/random/watts_strogatz.rb +50 -0
- data/lib/nose/random.rb +391 -0
- data/lib/nose/schema.rb +89 -0
- data/lib/nose/search/constraints.rb +143 -0
- data/lib/nose/search/problem.rb +328 -0
- data/lib/nose/search/results.rb +200 -0
- data/lib/nose/search.rb +266 -0
- data/lib/nose/serialize.rb +747 -0
- data/lib/nose/statements/connection.rb +160 -0
- data/lib/nose/statements/delete.rb +83 -0
- data/lib/nose/statements/insert.rb +146 -0
- data/lib/nose/statements/query.rb +161 -0
- data/lib/nose/statements/update.rb +101 -0
- data/lib/nose/statements.rb +645 -0
- data/lib/nose/timing.rb +79 -0
- data/lib/nose/util.rb +305 -0
- data/lib/nose/workload.rb +244 -0
- data/lib/nose.rb +37 -0
- data/templates/workload.erb +42 -0
- 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
|