class NoBrainer::Criteria::Where::IndexFinder

Public Instance Methods

find_strategy() click to toggle source
# File lib/no_brainer/criteria/where.rb, line 530
def find_strategy
  return nil unless ast.try(:clauses).present? && !criteria.without_index?
  case ast.op
  when :and then find_strategy_compound || find_strategy_compound_partial || find_strategy_canonical || find_strategy_hidden_between
  when :or  then find_strategy_union
  end
end
find_strategy!() click to toggle source
# File lib/no_brainer/criteria/where.rb, line 538
def find_strategy!
  self.strategy = find_strategy
end
find_strategy_canonical() click to toggle source
# File lib/no_brainer/criteria/where.rb, line 406
def find_strategy_canonical
  clauses = get_candidate_clauses(:eq, :in, :between, :near, :intersects, :defined)
  return nil unless clauses.present?

  usable_indexes = Hash[get_usable_indexes.map { |i| [[i.name], i] }]
  clauses.map do |clause|
    index = usable_indexes[clause.key_path]
    next unless index && clause.compatible_with_index?(index)
    next unless index.geo == [:near, :intersects].include?(clause.op)

    args = case clause.op
      when :intersects then [:get_intersecting, clause.value.to_rql]
      when :near
        options = clause.value.dup
        circle = options.delete(:circle)
        options.delete(:max_results) if options[:max_results].nil?
        options[:max_dist] = circle.radius
        [:get_nearest, circle.center.to_rql, circle.options.merge(options)]
      when :eq      then [:get_all, [clause.value]]
      when :in      then [:get_all, clause.value]
      when :defined then
        next unless clause.value == true
        next unless clause.key_modifier == :scalar && index.multi == false
        [:between, [RethinkDB::RQL.new.minval, RethinkDB::RQL.new.maxval],
         :left_bound => :open, :right_bound => :open]
      when :between then [:between, [clause.value.min, clause.value.max],
                          :left_bound => :closed, :right_bound => :closed]
    end
    IndexStrategy.new(self, ast, [clause], index, *args)
  end.compact.sort_by { |strat| usable_indexes.values.index(strat.index) }.first
end
find_strategy_compound() click to toggle source
# File lib/no_brainer/criteria/where.rb, line 438
def find_strategy_compound
  clauses = Hash[get_candidate_clauses(:eq).map { |c| [c.key_path, c] }]
  return nil unless clauses.present?

  get_usable_indexes(:kind => :compound, :geo => false, :multi => false).each do |index|
    indexed_clauses = index.what.map { |field| clauses[[field]] }
    next unless indexed_clauses.all? { |c| c.try(:compatible_with_index?, index) }
    return IndexStrategy.new(self, ast, indexed_clauses, index, :get_all, [indexed_clauses.map(&:value)])
  end
  return nil
end
find_strategy_compound_partial() click to toggle source
# File lib/no_brainer/criteria/where.rb, line 450
def find_strategy_compound_partial
  clauses = get_candidate_clauses(:eq, :between).map { |c| [c.key_path, c] }.to_h
  return nil unless clauses.present?

  get_usable_indexes(:kind => :compound, :geo => false, :multi => false).each do |index|
    indexed_clauses = index.what.map { |field| clauses[[field]] }
    partial_clauses = indexed_clauses.compact
    pad = indexed_clauses.length - partial_clauses.length
    if partial_clauses.any? && partial_clauses.all? { |c| c.try(:compatible_with_index?, index) }
      # can only use partial compound index if:
      #   * index contains all clause fields
      next unless (clauses.values & partial_clauses) == clauses.values
      #   * all clause fields come first in the indexed clauses (unused indexed fields are at the end)
      next unless indexed_clauses.last(pad).all?(&:nil?)
      #   * all clause fields are :eq, except the last (which may be :between)
      next unless partial_clauses[0..-2].all? { |c|  c.op == :eq }

      # use range query to cover unused index fields
      left_bound  = partial_clauses.map(&:value)
      right_bound = partial_clauses.map(&:value)
      if (clause = partial_clauses[-1]).op == :between
        left_bound[-1] = clause.value.min
        right_bound[-1] = clause.value.max
      end
      if pad > 0
        left_bound.append *Array.new(pad, RethinkDB::RQL.new.minval)
        right_bound.append *Array.new(pad, RethinkDB::RQL.new.maxval)
      end
      return IndexStrategy.new(self, ast, partial_clauses, index, :between, [left_bound, right_bound], :left_bound => :closed, :right_bound => :closed)
    end
  end
  nil
end
find_strategy_hidden_between() click to toggle source
# File lib/no_brainer/criteria/where.rb, line 484
def find_strategy_hidden_between
  clauses = get_candidate_clauses(:gt, :ge, :lt, :le).group_by(&:key_path)
  return nil unless clauses.present?

  get_usable_indexes(:geo => false).each do |index|
    matched_clauses = clauses[[index.name]].try(:select) { |c| c.compatible_with_index?(index) }
    next unless matched_clauses.present?

    op_clauses = Hash[matched_clauses.map { |c| [c.op, c] }]
    left_bound  = op_clauses[:gt] || op_clauses[:ge]
    right_bound = op_clauses[:lt] || op_clauses[:le]

    # XXX we must keep only one bound when using `any', otherwise we get different semantics.
    right_bound = nil if index.multi && left_bound && right_bound

    options = {}
    options[:left_bound]  = {:gt => :open, :ge => :closed}[left_bound.op]  if left_bound
    options[:right_bound] = {:lt => :open, :le => :closed}[right_bound.op] if right_bound

    return IndexStrategy.new(self, ast, [left_bound, right_bound].compact, index, :between,
                             [left_bound  ? left_bound.try(:value)  : RethinkDB::RQL.new.minval,
                              right_bound ? right_bound.try(:value) : RethinkDB::RQL.new.maxval], options)
  end
  return nil
end
find_strategy_union() click to toggle source
# File lib/no_brainer/criteria/where.rb, line 510
def find_strategy_union
  strategies = ast.clauses.map do |inner_ast|
    inner_ast = MultiOperator.new(:and, [inner_ast]) unless inner_ast.is_a?(MultiOperator)
    raise 'fatal' unless inner_ast.op == :and
    self.class.new(criteria, inner_ast).find_strategy
  end

  return nil if strategies.any?(&:nil?)

  rql_proc = lambda do |base_rql|
    strategies.map do |strategy|
      rql = strategy.rql_proc.call(base_rql)
      rql = rql.filter { |doc| strategy.ast.to_rql(doc) } if strategy.ast.try(:clauses).present?
      rql
    end.reduce(:union).distinct
  end

  Strategy.new(self, :union, strategies.map(&:index), nil, rql_proc)
end
get_candidate_clauses(*types) click to toggle source
# File lib/no_brainer/criteria/where.rb, line 393
def get_candidate_clauses(*types)
  BinaryOperator.get_candidate_clauses(ast.clauses, *types)
end
get_usable_indexes(options={}) click to toggle source
# File lib/no_brainer/criteria/where.rb, line 397
def get_usable_indexes(options={})
  indexes = criteria.model.indexes.values
  options.each { |k,v| indexes = indexes.select { |i| v == i.__send__(k) } }
  if criteria.options[:use_index] && criteria.options[:use_index] != true
    indexes = indexes.select { |i| i.name == criteria.options[:use_index].to_sym }
  end
  indexes
end