class Hotdog::Expression::BinaryExpressionNode

Attributes

left[R]
op[R]
right[R]

Public Class Methods

new(op, left, right, options={}) click to toggle source
# File lib/hotdog/expression/semantics.rb, line 170
def initialize(op, left, right, options={})
  case (op || "or").to_s
  when "&&", "&", "AND", "and"
    @op = :AND
  when ",", "||", "|", "OR", "or"
    @op = :OR
  when "^", "XOR", "xor"
    @op = :XOR
  else
    raise(SyntaxError.new("unknown binary operator: #{op.inspect}"))
  end
  @left = left
  @right = right
  @options = {}
end

Public Instance Methods

==(other) click to toggle source
# File lib/hotdog/expression/semantics.rb, line 364
def ==(other)
  self.class === other and @op == other.op and @left == other.left and @right == other.right
end
dump(options={}) click to toggle source
# File lib/hotdog/expression/semantics.rb, line 368
def dump(options={})
  {left: @left.dump(options), binary_op: @op.to_s, right: @right.dump(options)}
end
evaluate(environment, options={}) click to toggle source
# File lib/hotdog/expression/semantics.rb, line 186
def evaluate(environment, options={})
  case @op
  when :AND
    left_values = @left.evaluate(environment, options).tap do |values|
      environment.logger.debug("lhs(#{values.length})")
    end
    if left_values.empty?
      []
    else
      right_values = @right.evaluate(environment, options).tap do |values|
        environment.logger.debug("rhs(#{values.length})")
      end
      if right_values.empty?
        []
      else
        # workaround for "too many terms in compound SELECT"
        min, max = environment.execute("SELECT MIN(id), MAX(id) FROM hosts LIMIT 1;").first.to_a
        sqlite_limit_compound_select = options[:sqlite_limit_compound_select] || SQLITE_LIMIT_COMPOUND_SELECT
        (min / ((sqlite_limit_compound_select - 2) / 2)).upto(max / ((sqlite_limit_compound_select - 2) / 2)).flat_map { |i|
          range = (((sqlite_limit_compound_select - 2) / 2) * i)...(((sqlite_limit_compound_select - 2) / 2) * (i + 1))
          left_selected = left_values.select { |n| range === n }
          right_selected = right_values.select { |n| range === n }
          if 0 < left_selected.length and 0 < right_selected.length
            q = "SELECT id FROM hosts " \
                  "WHERE ? <= id AND id < ? AND ( id IN (%s) AND id IN (%s) );"
            environment.execute(q % [left_selected.map { "?" }.join(", "), right_selected.map { "?" }.join(", ")], [range.first, range.last] + left_selected + right_selected).map { |row| row.first }
          else
            []
          end
        }.tap do |values|
          environment.logger.debug("lhs(#{left_values.length}) AND rhs(#{right_values.length}) => #{values.length}")
        end
      end
    end
  when :OR
    left_values = @left.evaluate(environment, options).tap do |values|
      environment.logger.debug("lhs(#{values.length})")
    end
    right_values = @right.evaluate(environment, options).tap do |values|
      environment.logger.debug("rhs(#{values.length})")
    end
    if left_values.empty?
      right_values
    else
      if right_values.empty?
        []
      else
        # workaround for "too many terms in compound SELECT"
        min, max = environment.execute("SELECT MIN(id), MAX(id) FROM hosts LIMIT 1;").first.to_a
        sqlite_limit_compound_select = options[:sqlite_limit_compound_select] || SQLITE_LIMIT_COMPOUND_SELECT
        (min / ((sqlite_limit_compound_select - 2) / 2)).upto(max / ((sqlite_limit_compound_select - 2) / 2)).flat_map { |i|
          range = (((sqlite_limit_compound_select - 2) / 2) * i)...(((sqlite_limit_compound_select - 2) / 2) * (i + 1))
          left_selected = left_values.select { |n| range === n }
          right_selected = right_values.select { |n| range === n }
          if 0 < left_selected.length or 0 < right_selected.length
            q = "SELECT id FROM hosts " \
                  "WHERE ? <= id AND id < ? AND ( id IN (%s) OR id IN (%s) );"
            environment.execute(q % [left_selected.map { "?" }.join(", "), right_selected.map { "?" }.join(", ")], [range.first, range.last] + left_selected + right_selected).map { |row| row.first }
          else
            []
          end
        }.tap do |values|
          environment.logger.debug("lhs(#{left_values.length}) OR rhs(#{right_values.length}) => #{values.length}")
        end
      end
    end
  when :XOR
    left_values = @left.evaluate(environment, options).tap do |values|
      environment.logger.debug("lhs(#{values.length})")
    end
    right_values = @right.evaluate(environment, options).tap do |values|
      environment.logger.debug("rhs(#{values.length})")
    end
    if left_values.empty?
      right_values
    else
      if right_values.empty?
        []
      else
        # workaround for "too many terms in compound SELECT"
        min, max = environment.execute("SELECT MIN(id), MAX(id) FROM hosts LIMIT 1;").first.to_a
        sqlite_limit_compound_select = options[:sqlite_limit_compound_select] || SQLITE_LIMIT_COMPOUND_SELECT
        (min / ((sqlite_limit_compound_select - 2) / 4)).upto(max / ((sqlite_limit_compound_select - 2) / 4)).flat_map { |i|
          range = (((sqlite_limit_compound_select - 2) / 4) * i)...(((sqlite_limit_compound_select - 2) / 4) * (i + 1))
          left_selected = left_values.select { |n| range === n }
          right_selected = right_values.select { |n| range === n }
          if 0 < left_selected.length or 0 < right_selected.length
            q = "SELECT id FROM hosts " \
                  "WHERE ? <= id AND id < ? AND NOT (id IN (%s) AND id IN (%s)) AND ( id IN (%s) OR id IN (%s) );"
            lq = left_selected.map { "?" }.join(", ")
            rq = right_selected.map { "?" }.join(", ")
            environment.execute(q % [lq, rq, lq, rq], [range.first, range.last] + left_selected + right_selected + left_selected + right_selected).map { |row| row.first }
          else
            []
          end
        }.tap do |values|
          environment.logger.debug("lhs(#{left_values.length}) XOR rhs(#{right_values.length}) => #{values.length}")
        end
      end
    end
  else
    []
  end
end
optimize(options={}) click to toggle source
# File lib/hotdog/expression/semantics.rb, line 291
def optimize(options={})
  o_left = @left.optimize(options)
  o_right = @right.optimize(options)
  case op
  when :AND
    case o_left
    when EverythingNode
      o_right
    when NothingNode
      o_left
    else
      if o_left == o_right
        o_left
      else
        BinaryExpressionNode.new(
          op,
          o_left,
          o_right,
        ).optimize1(options)
      end
    end
  when :OR
    case o_left
    when EverythingNode
      o_left
    when NothingNode
      o_right
    else
      if o_left == o_right
        o_left
      else
        if MultinaryExpressionNode === o_left
          if o_left.op == op
            o_left.merge(o_right, fallback: self)
          else
            BinaryExpressionNode.new(
              op,
              o_left,
              o_right,
            ).optimize1(options)
          end
        else
          if MultinaryExpressionNode === o_right
            if o_right.op == op
              o_right.merge(o_left, fallback: self)
            else
              BinaryExpressionNode.new(
                op,
                o_left,
                o_right,
              ).optimize1(options)
            end
          else
            MultinaryExpressionNode.new(op, [o_left, o_right], fallback: self)
          end
        end
      end
    end
  when :XOR
    if o_left == o_right
      NothingNode.new(options)
    else
      BinaryExpressionNode.new(
        op,
        o_left,
        o_right,
      ).optimize1(options)
    end
  else
    self.dup
  end
end

Protected Instance Methods

optimize1(options) click to toggle source
# File lib/hotdog/expression/semantics.rb, line 373
def optimize1(options)
  if TagExpressionNode === left and TagExpressionNode === right
    lq = left.maybe_query(options)
    lv = left.condition_values(options)
    rq = right.maybe_query(options)
    rv = right.condition_values(options)
    sqlite_limit_compound_select = options[:sqlite_limit_compound_select] || SQLITE_LIMIT_COMPOUND_SELECT
    if lq and rq and lv.length + rv.length <= sqlite_limit_compound_select
      case op
      when :AND
        q = "#{lq.sub(/\s*;\s*\z/, "")} INTERSECT #{rq.sub(/\s*;\s*\z/, "")};"
        QueryExpressionNode.new(q, lv + rv, fallback: self)
      when :OR
        q = "#{lq.sub(/\s*;\s*\z/, "")} UNION #{rq.sub(/\s*;\s*\z/, "")};"
        QueryExpressionNode.new(q, lv + rv, fallback: self)
      when :XOR
        q = "#{lq.sub(/\s*;\s*\z/, "")} UNION #{rq.sub(/\s*;\s*\z/, "")} " \
              "EXCEPT #{lq.sub(/\s*;\s*\z/, "")} " \
                "INTERSECT #{rq.sub(/\s*;\s*\z/, "")};"
        QueryExpressionNode.new(q, lv + rv, fallback: self)
      else
        self.dup
      end
    else
      self.dup
    end
  else
    self.dup
  end
end