Class: NoBrainer::Criteria::Where::IndexFinder

Inherits:
Struct
  • Object
show all
Defined in:
lib/no_brainer/criteria/where.rb

Defined Under Namespace

Classes: IndexStrategy, Strategy

Instance Method Summary collapse

Instance Method Details

#find_strategyObject



530
531
532
533
534
535
536
# 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!Object



538
539
540
# File 'lib/no_brainer/criteria/where.rb', line 538

def find_strategy!
  self.strategy = find_strategy
end

#find_strategy_canonicalObject



406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# 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_compoundObject



438
439
440
441
442
443
444
445
446
447
448
# 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_partialObject



450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# 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_betweenObject



484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# 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_unionObject



510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
# 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) ⇒ Object



393
394
395
# 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 = {}) ⇒ Object



397
398
399
400
401
402
403
404
# 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