class NoBrainer::Criteria::Where::BinaryOperator

Public Class Methods

get_candidate_clauses(clauses, *types) click to toggle source
# File lib/no_brainer/criteria/where.rb, line 70
def self.get_candidate_clauses(clauses, *types)
  clauses.select { |c| c.is_a?(self) && types.include?(c.op) }
end
simplify_clauses(op, ast_clauses) click to toggle source
# File lib/no_brainer/criteria/where.rb, line 74
def self.simplify_clauses(op, ast_clauses)
  # This code assumes that simplfy() has already been called on all clauses.
  if op == :or
    eq_clauses = get_candidate_clauses(ast_clauses, :in, :eq)
    new_clauses = eq_clauses.group_by { |c| [c.key_path, c.key_modifier] }.map do |(key_path, key_modifier), clauses|
      if key_modifier.in?([:scalar, :any]) && clauses.size > 1
        values = clauses.flat_map { |c| c.op == :in ? c.value : [c.value] }.uniq
        [BinaryOperator.new(key_path, key_modifier, :in, values, clauses.first.model, true)]
      else
        clauses
      end
    end.flatten(1)

    if new_clauses.size != eq_clauses.size
      ast_clauses = ast_clauses - eq_clauses + new_clauses
    end
  end

  ast_clauses
end

Public Instance Methods

compatible_with_index?(index) click to toggle source
# File lib/no_brainer/criteria/where.rb, line 160
def compatible_with_index?(index)
  [key_modifier, index.multi].in?([[:any, true], [:scalar, false]])
end
simplify() click to toggle source
# File lib/no_brainer/criteria/where.rb, line 95
def simplify
  new_key_path = cast_key_path(key_path)
  new_key_modifier, new_op, new_value = case op
    when :in then
      case value
      when Range then [key_modifier, :between, (cast_value(value.min)..cast_value(value.max))]
      when Array then [key_modifier, :in, value.map(&method(:cast_value)).uniq]
      else raise ArgumentError.new "`in' takes an array/range, not #{value}"
      end
    when :between then [key_modifier, op, (cast_value(value.min)..cast_value(value.max))]
    when :include then ensure_scalar_for(op); [:any, :eq, cast_value(value)]
    when :defined, :undefined then ensure_scalar_for(op); [key_modifier, op, cast_value(value)]
    when :during then [key_modifier, op, [cast_value(value.first), cast_value(value.last)]]
    else [key_modifier, op, cast_value(value)]
  end

  # When key_path relates to a polymorphic associatoin, the new_key_path is
  # an Array containing the foreign_type and then the foreign_key.
  if new_key_path.first.is_a?(Array)
    foreign_type, foreign_key = new_key_path.first

    MultiOperator.new(
      :and,
      [
        BinaryOperator.new([foreign_type], new_key_modifier, new_op, value.class.to_s, model, true),
        BinaryOperator.new([foreign_key], new_key_modifier, new_op, value.__send__(value.class.pk_name), model, true)
      ]
    )
  else
    BinaryOperator.new(new_key_path, new_key_modifier, new_op, new_value, model, true)
  end
end
to_rql(doc) click to toggle source
# File lib/no_brainer/criteria/where.rb, line 128
def to_rql(doc)
  key_path = [model.lookup_field_alias(self.key_path.first), *self.key_path[1..-1]]

  doc = key_path[0..-2].reduce(doc) { |d,k| d[k] }
  key = key_path.last

  case key_modifier
  when :scalar then
    case op
    when :defined   then  value ? doc.has_fields(key) : doc.has_fields(key).not
    when :undefined then !value ? doc.has_fields(key) : doc.has_fields(key).not
    else to_rql_scalar(doc[key])
    end
  when :any then doc[key].map { |lvalue| to_rql_scalar(lvalue) }.contains(true)
  when :all then doc[key].map { |lvalue| to_rql_scalar(lvalue) }.contains(false).not
  end
end
to_rql_scalar(lvalue) click to toggle source
# File lib/no_brainer/criteria/where.rb, line 146
def to_rql_scalar(lvalue)
  case op
  when :between    then (lvalue >= value.min) & (lvalue <= value.max)
  when :in         then RethinkDB::RQL.new.expr(value).contains(lvalue)
  when :intersects then lvalue.intersects(value.to_rql)
  when :during     then lvalue.during(value.first, value.last)
  when :near
    # XXX options[:max_results] is not used, seems to be a workaround of rethinkdb index implementation.
    circle = value[:circle]
    RethinkDB::RQL.new.distance(lvalue, circle.center.to_rql, circle.options) <= circle.radius
  else lvalue.__send__(op, value)
  end
end

Private Instance Methods

cast_key_path(key_path) click to toggle source

This method is used in order to transform association from the passed ‘key_path` in to model’s field(s).

When ‘key_path` contains a field, this method just ensures the given field is well defined in the owner model. When `key_path` contains the name of an association, this method updates the `key_path` in order to replace the association name with the field(s) behind that association.

In the case of :

  • a polymorphic association name passed as ‘key_path`, the association name will be replaced by the 2 fields representing it (foreign_key and foreign_type)

  • all other association the association name is replaced by the primary_key or the foreign_key depending on the association type

# File lib/no_brainer/criteria/where.rb, line 227
def cast_key_path(key_path)
  return key_path if casted_values

  # key_path is an Array of symbols representing the path to the key being
  # queried.
  #
  # The Array size can be greater that 1 when quering from a field with,
  # the type `Hash` and the query is targetting a nested key from the Hash.
  #
  # The first Array element is always the field name.
  if key_path.size == 1
    # With fields and non-polymorphic associations `keys` will be a symbol
    # while with a polymorphic association it will be an Array of symbols
    # being the two fields used by the association.
    keys, _v = model.association_user_to_model_cast(key_path.first, nil, value.class)
    key_path = [keys]
  end

  # Ensures fields exist on the model
  model.ensure_valid_key!(key_path.first)

  key_path
end
cast_value(value) click to toggle source
# File lib/no_brainer/criteria/where.rb, line 170
def cast_value(value)
  return value if casted_values

  case op
  when :defined, :undefined then NoBrainer::Boolean.nobrainer_cast_user_to_model(value)
  when :intersects
    raise "Use a geo object with `intersects`" unless value.is_a?(NoBrainer::Geo::Base)
    value
  when :near
    # TODO enforce key is a geo type
    case value
    when Hash
      options = NoBrainer::Geo::Base.normalize_geo_options(value)

      options[:radius] = options.delete(:max_distance) if options[:max_distance]
      options[:radius] = options.delete(:max_dist) if options[:max_dist]
      options[:center] = options.delete(:point) if options[:point]

      unless options[:circle]
        unless options[:center] && options[:radius]
          raise "`near' takes something like {:center => P, :radius => d}"
        end
        { :circle => NoBrainer::Geo::Circle.new(options), :max_results => options[:max_results] }
      end
    when NoBrainer::Geo::Circle then { :circle => value }
    else raise "Incorrect use of `near': rvalue must be a hash or a circle"
    end
  else
    # 1) Box value in array if we have an any/all modifier
    # 2) Box value in hash if we have a nested query.
    box_value = key_modifier.in?([:any, :all]) || op == :include
    value = [value] if box_value
    k_v = key_path.reverse.reduce(value) { |v,k| {k => v} }.first
    k_v = model.association_user_to_model_cast(*k_v, value.class)
    value = model.cast_user_to_db_for(*k_v)
    value = key_path[1..-1].reduce(value) { |h,k| h[k] }
    value = value.first if box_value
    value
  end
end
ensure_scalar_for(op) click to toggle source
# File lib/no_brainer/criteria/where.rb, line 166
def ensure_scalar_for(op)
  raise "Incorrect use of `#{op}' and `#{key_modifier}'" if key_modifier != :scalar
end