1 | 1 | module NoSE |
|
2 | 1 | module Plans |
|
3 | # Superclass for steps using indices |
||
4 | 1 | class IndexLookupPlanStep < PlanStep |
|
5 | 1 | extend Forwardable |
|
6 | |||
7 | 1 | attr_reader :index, :eq_filter, :range_filter, :limit, :order_by |
|
8 | 1 | delegate hash: :index |
|
9 | |||
10 | 1 | def initialize(index, state = nil, parent = nil) |
|
11 | 1903 | super() |
|
12 | 1903 | @index = index |
|
13 | |||
14 | 1903 | if state && state.query |
|
15 | 1893 | all_fields = state.query.all_fields |
|
16 | 1893 | @fields = (@index.hash_fields + @index.order_fields).to_set + \ |
|
17 | 1893 | (@index.extra.to_set & all_fields) |
|
18 | else |
||
19 | 10 | @fields = @index.all_fields |
|
20 | end |
||
21 | |||
22 | 1903 | return if state.nil? |
|
23 | 1893 | @state = state.dup |
|
24 | 1893 | update_state parent |
|
25 | 1893 | @state.freeze |
|
26 | end |
||
27 | |||
28 | # :nocov: |
||
29 | 1 | def to_color |
|
30 | if @state.nil? |
||
31 | "#{super} #{@index.to_color}" |
||
32 | else |
||
33 | "#{super} #{@index.to_color} * " \ |
||
34 | "#{@state.cardinality}/#{@state.hash_cardinality} " |
||
35 | end |
||
36 | end |
||
37 | # :nocov: |
||
38 | |||
39 | # Two index steps are equal if they use the same index |
||
40 | 1 | def ==(other) |
|
41 | 17 | other.instance_of?(self.class) && @index == other.index |
|
42 | end |
||
43 | 1 | alias eql? == |
|
44 | |||
45 | # Check if this step can be applied for the given index, |
||
46 | # returning a possible application of the step |
||
47 | # @return [IndexLookupPlanStep] |
||
48 | 1 | def self.apply(parent, index, state) |
|
0 ignored issues
–
show
Coding Style
introduced
by
Loading history...
|
|||
49 | # Check that this index is a valid continuation of the set of joins |
||
50 | return nil unless index.graph.entities.include?(state.joins.first) && |
||
51 | (index.graph.unique_edges & |
||
52 | state.graph.unique_edges == |
||
53 | 4626 | index.graph.unique_edges) |
|
54 | |||
55 | # We must move forward on each lookup |
||
56 | # XXX This disallows plans which look up additional attributes |
||
57 | # for entities other than the final one |
||
58 | 4624 | return nil if index.graph.size == 1 && state.graph.size > 1 && |
|
59 | !parent.is_a?(RootPlanStep) |
||
60 | 4452 | return nil if index.identity? && state.graph.size > 1 |
|
61 | |||
62 | 4431 | return nil if invalid_parent_index? state, index, parent.parent_index |
|
63 | |||
64 | # We need all hash fields to perform the lookup |
||
65 | 3033 | return nil unless index.hash_fields.all? do |field| |
|
66 | 3044 | (parent.fields + state.given_fields).include? field |
|
67 | end |
||
68 | |||
69 | # Get fields in the query relevant to this index |
||
70 | # and check that they are provided for us here |
||
71 | 1989 | hash_entity = index.hash_fields.first.parent |
|
72 | 1989 | graph_fields = state.fields_for_graph(index.graph, hash_entity).to_set |
|
73 | 1989 | graph_fields -= parent.fields # exclude fields already fetched |
|
74 | 1989 | return nil unless graph_fields.subset?(index.all_fields) |
|
75 | |||
76 | return IndexLookupPlanStep.new(index, state, parent) \ |
||
77 | 1876 | if last_fields?(index, state) |
|
78 | |||
79 | nil |
||
80 | end |
||
81 | |||
82 | # Check if this index can be used after the current parent |
||
83 | # @return [Boolean] |
||
84 | 1 | def self.invalid_parent_index?(state, index, parent_index) |
|
85 | 4431 | return false if parent_index.nil? |
|
86 | |||
87 | # We don't do multiple lookups by ID for the same entity set |
||
88 | return true if parent_index.identity? && |
||
89 | 2745 | index.graph == parent_index.graph |
|
90 | |||
91 | # If the last step gave an ID, we must use it |
||
92 | # XXX This doesn't cover all cases |
||
93 | 2114 | last_parent_entity = state.joins.reverse.find do |entity| |
|
94 | 3654 | parent_index.graph.entities.include? entity |
|
95 | end |
||
96 | 2114 | parent_ids = Set.new [last_parent_entity.id_field] |
|
97 | 2114 | has_ids = parent_ids.subset? parent_index.all_fields |
|
98 | 2114 | return true if has_ids && index.hash_fields.to_set != parent_ids |
|
99 | |||
100 | # If we're looking up from a previous step, only allow lookup by ID |
||
101 | return true unless (index.graph.size == 1 && |
||
102 | parent_index.graph != index.graph) || |
||
103 | 1347 | index.hash_fields == parent_ids |
|
104 | end |
||
105 | 1 | private_class_method :invalid_parent_index? |
|
106 | |||
107 | # Check that we have the required fields to move on with the next lookup |
||
108 | # @return [Boolean] |
||
109 | 1 | def self.last_fields?(index, state) |
|
110 | 1876 | index_includes = lambda do |fields| |
|
111 | 5830 | fields.all? { |f| index.all_fields.include? f } |
|
112 | end |
||
113 | |||
114 | # We must have either the ID or all the fields |
||
115 | # for leaf entities in the original graph |
||
116 | 1876 | leaf_entities = index.graph.entities.select do |entity| |
|
117 | 3333 | state.graph.leaf_entity?(entity) |
|
118 | end |
||
119 | 1876 | leaf_entities.all? do |entity| |
|
120 | index_includes.call([entity.id_field]) || |
||
121 | 2915 | index_includes.call(state.fields.select { |f| f.parent == entity }) |
|
122 | end |
||
123 | end |
||
124 | 1 | private_class_method :last_fields? |
|
125 | |||
126 | 1 | private |
|
127 | |||
128 | # Get the set of fields which can be filtered by the ordered keys |
||
129 | # @return [Array<Fields::Field>] |
||
130 | 1 | def range_order_prefix |
|
131 | 1893 | order_prefix = (@state.eq - @index.hash_fields) & @index.order_fields |
|
132 | 1893 | order_prefix << @state.range unless @state.range.nil? |
|
133 | 1893 | order_prefix = order_prefix.zip(@index.order_fields) |
|
134 | 1895 | order_prefix.take_while { |x, y| x == y }.map(&:first) |
|
135 | end |
||
136 | |||
137 | # Perform any ordering implicit to this index |
||
138 | # @return [Boolean] whether this index is by ID |
||
139 | 1 | def resolve_order |
|
140 | # We can't resolve ordering if we're doing an ID lookup |
||
141 | # since only one record exists per row (if it's the same entity) |
||
142 | # We also need to have the fields used in order |
||
143 | 1893 | first_join = @state.query.join_order.detect do |entity| |
|
144 | 3797 | @index.graph.entities.include? entity |
|
145 | end |
||
146 | 1893 | indexed_by_id = @index.hash_fields.include?(first_join.id_field) |
|
147 | 1893 | order_prefix = @state.order_by.longest_common_prefix( |
|
148 | @index.order_fields - @eq_filter.to_a |
||
149 | ) |
||
150 | if indexed_by_id && order_prefix.map(&:parent).to_set == |
||
151 | 1893 | Set.new([@index.hash_fields.first.parent]) |
|
152 | 112 | order_prefix = [] |
|
153 | else |
||
154 | 1781 | @state.order_by -= order_prefix |
|
155 | end |
||
156 | 1893 | @order_by = order_prefix |
|
157 | |||
158 | 1893 | indexed_by_id |
|
159 | end |
||
160 | |||
161 | # Strip the graph for this index, but if we haven't fetched all |
||
162 | # fields, leave the last one so we can perform a separate ID lookup |
||
163 | # @return [void] |
||
164 | 1 | def strip_graph |
|
165 | 1893 | hash_entity = @index.hash_fields.first.parent |
|
166 | 1893 | @state.graph = @state.graph.dup |
|
167 | 1893 | required_fields = @state.fields_for_graph(@index.graph, hash_entity, |
|
168 | select: true).to_set |
||
169 | if required_fields.subset?(@index.all_fields) && |
||
170 | 1893 | @state.graph == @index.graph |
|
171 | 1139 | removed_nodes = @state.joins[[email protected]] |
|
172 | 1139 | @state.joins = @state.joins[@index.graph.size..-1] |
|
173 | else |
||
174 | 754 | removed_nodes = if index.graph.size == 1 |
|
175 | 50 | [] |
|
176 | else |
||
177 | 704 | @state.joins[[email protected] - 2] |
|
178 | end |
||
179 | 754 | @state.joins = @state.joins[@index.graph.size - 1..-1] |
|
180 | end |
||
181 | |||
182 | # Remove nodes which have been processed from the graph |
||
183 | 1893 | @state.graph.remove_nodes removed_nodes |
|
184 | end |
||
185 | |||
186 | # Update the cardinality of this step, applying a limit if possible |
||
187 | 1 | def update_cardinality(parent, indexed_by_id) |
|
188 | # Calculate the new cardinality assuming no limit |
||
189 | # Hash cardinality starts at 1 or is the previous cardinality |
||
190 | 1893 | if parent.is_a?(RootPlanStep) |
|
191 | 546 | @state.hash_cardinality = 1 |
|
192 | else |
||
193 | 1347 | @state.hash_cardinality = parent.state.cardinality |
|
194 | end |
||
195 | |||
196 | # Filter the total number of rows by filtering on non-hash fields |
||
197 | 1893 | cardinality = @index.per_hash_count * @state.hash_cardinality |
|
198 | 1893 | @state.cardinality = Cardinality.filter cardinality, |
|
199 | @eq_filter - |
||
200 | @index.hash_fields, |
||
201 | @range_filter |
||
202 | |||
203 | # Check if we can apply the limit from the query |
||
204 | # This occurs either when we are on the first or last index lookup |
||
205 | # and the ordering of the query has already been resolved |
||
206 | 1893 | order_resolved = @state.order_by.empty? && @state.graph.size == 1 |
|
207 | return unless (@state.answered?(check_limit: false) || |
||
208 | parent.is_a?(RootPlanStep) && order_resolved) && |
||
209 | 1893 | [email protected]? |
|
210 | |||
211 | # XXX Assume that everything is limited by the limit value |
||
212 | # which should be fine if the limit is small enough |
||
213 | 6 | @limit = @state.query.limit |
|
214 | 6 | if parent.is_a?(RootPlanStep) |
|
215 | 6 | @state.cardinality = [@limit, @state.cardinality].min |
|
216 | 6 | @state.hash_cardinality = 1 |
|
217 | else |
||
218 | @limit = @state.cardinality = @state.query.limit |
||
219 | |||
220 | # If this is a final lookup by ID, go with the limit |
||
221 | if @index.graph.size == 1 && indexed_by_id |
||
222 | @state.hash_cardinality = @limit |
||
223 | else |
||
224 | @state.hash_cardinality = parent.state.cardinality |
||
225 | end |
||
226 | end |
||
227 | end |
||
228 | |||
229 | # Modify the state to reflect the fields looked up by the index |
||
230 | # @return [void] |
||
231 | 1 | def update_state(parent) |
|
232 | 1893 | order_prefix = range_order_prefix.to_set |
|
233 | |||
234 | # Find fields which are filtered by the index |
||
235 | 1893 | @eq_filter = @index.hash_fields + (@state.eq & order_prefix) |
|
236 | 1893 | if order_prefix.include?(@state.range) |
|
237 | @range_filter = @state.range |
||
238 | @state.range = nil |
||
239 | else |
||
240 | 1893 | @range_filter = nil |
|
241 | end |
||
242 | |||
243 | # Remove fields resolved by this index |
||
244 | 1893 | @state.fields -= @index.all_fields |
|
245 | 1893 | @state.eq -= @eq_filter |
|
246 | |||
247 | 1893 | indexed_by_id = resolve_order |
|
248 | 1893 | strip_graph |
|
249 | 1893 | update_cardinality parent, indexed_by_id |
|
250 | end |
||
251 | end |
||
252 | end |
||
253 | end |
||
254 |