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.
- 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
|