require 'sql/maker/util' require 'sql/query_maker' class SQL::Maker::Condition include SQL::Maker::Util attr_accessor :quote_char, :name_sep, :strict, :auto_bind attr_accessor :sql, :bind def initialize(args = {}) @quote_char = args[:quote_char] || '' @name_sep = args[:name_sep] || '.' @strict = args[:strict] || false @auto_bind = args[:auto_bind] || false @sql = args[:sql] || [] @bind = args[:bind] || [] end def new_condition(args = {}) SQL::Maker::Condition.new({ :quote_char => self.quote_char, :name_sep => self.name_sep, :strict => self.strict, }.merge(args)) end def &(other) self.compose_and(other) end def |(other) self.compose_or(other) end def _quote(label) quote_identifier(label, self.quote_char, self.name_sep) end # _make_term(:x => 1) def _make_term(*args) col, val = parse_args(*args) col = col.to_s if val.is_a?(SQL::QueryMaker) return [val.as_sql(col, self.method(:_quote)), val.bind] elsif self.strict croak("can pass only SQL::QueryMaker as an argument in strict mode") end if val.is_a?(Array) if val.first.is_a?(Hash) # {'foo'=>[{'>' => 'bar'},{'<' => 'baz'}]} => (`foo` > ?) OR (`foo` < ?) return self._make_or_term(col, 'OR', val) else # {'foo'=>['bar','baz']} => `foo` IN (?, ?) return self._make_in_term(col, 'IN', val) end elsif val.is_a?(Hash) op, v = val.each.first op = op.upcase.to_s if ( op == 'AND' || op == 'OR' ) && v.is_a?(Array) # {'foo'=>[{'>' => 'bar'},{'<' => 'baz'}]} => (`foo` > ?) OR (`foo` < ?) return self._make_or_term(col, op, v) elsif ( op == 'IN' || op == 'NOT IN' ) return self._make_in_term(col, op, v) elsif ( op == 'BETWEEN' ) && v.is_a?(Array) croak("USAGE: make_term(foo => {BETWEEN => [a, b]})") if v.size != 2 return [self._quote(col) + " BETWEEN ? AND ?", v] else # make_term(foo => { '<' => \"DATE_SUB(NOW(), INTERVAL 3 DAY)"}) => 'foo < DATE_SUB(NOW(), INTERVAL 3 DAY)' # return [self._quote(col) + " op " + v, []] # make_term(foo => { '<' => 3 }) => foo < 3 return [self._quote(col) + " #{op} ?", [v]] end elsif val # make_term(foo => "3") => foo = 3 return [self._quote(col) + " = ?", [val]] else # make_term(foo => nil) => foo IS NULL return [self._quote(col) + " IS NULL", []] end end def _make_or_term(col, op, values) binds = [] terms = [] values.each do |v| term, bind = self._make_term(col => v) terms.push "(#{term})" binds.push bind end term = terms.join(" #{op} ") bind = binds.flatten return [term, bind] end def _make_in_term(col, op, v) if v.respond_to?(:as_sql) # make_term(foo => { 'IN' => sql_raw('SELECT foo FROM bar') }) => foo IN (SELECT foo FROM bar) term = "#{self._quote(col)} #{op} (#{v.as_sql})" [term, v.bind] elsif v.is_a?(Array) if v.size == 0 if op == 'IN' # make_term(foo => {'IN' => []}) => 0=1 return ['0=1', []] else # make_term(foo => {'NOT IN' => []}) => 1=1 return ['1=1', []] end else # make_term(foo => { 'IN' => [1,2,3] }) => [foo IN (?,?,?), [1,2,3]] term = "#{self._quote(col)} #{op} (#{(['?'] * v.size).join(', ')})" return [term, v] end else croad("_make_in_term: arguments must be either of query instance or array") end end def add(*args) term, bind = self._make_term(*args) self.sql.push "(#{term})" if term self.bind += array_wrap(bind) if bind return self # for influent interface end def add_raw(*args) term, bind = parse_args(*args) self.sql.push "(#{term})" self.bind += array_wrap(bind) if bind return self end def compose_and(other) if self.sql.empty? if other.sql.empty? return new_condition end return new_condition( :sql => ['(' + other.as_sql() + ')'], :bind => other.bind, ) end if other.sql.empty? return new_condition( :sql => ['(' + self.as_sql() + ')'], :bind => self.bind, ) end return new_condition( :sql => ['(' + self.as_sql() + ') AND (' + other.as_sql() + ')'], :bind => self.bind + other.bind, ) end def compose_or(other) if self.sql.empty? if other.sql.empty? return new_condition end return new_condition( :sql => ['(' + other.as_sql() + ')'], :bind => other.bind, ) end if other.sql.empty? return new_condition( :sql => ['(' + self.as_sql() + ')'], :bind => self.bind, ) end # return value is enclosed with '()'. # because 'OR' operator priority less than 'AND'. return new_condition( :sql => ['((' + self.as_sql() + ') OR (' + other.as_sql() + '))'], :bind => self.bind + other.bind, ) end def as_sql sql = self.sql.join(' AND ') @auto_bind ? bind_param(sql, self.bind) : sql end end