Completed
Push — master ( cb5396...b3a8d6 )
by Michael
03:06
created

lib/nose/plans/index_lookup.rb (3 issues)

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
The Assignment, Branch, Condition size for apply is considered too high. [45.53/20]. The ABC size is based on assignments, branches (method calls), and conditions.
Loading history...
The method apply seems to be too complex. Perceived cyclomatic complexity is 12 with a maxiumum of 10 permitted.
Loading history...
Complexity Coding Style introduced by
The method apply seems to be too complex. Perceived complexity is 12 with a maxiumum of 10 permitted.
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