# = Where clause generator
# == Author: Tim Harper ( "timseeharperATgmail.seeom".gsub("see", "c").gsub("AT", "@") )
#
# Usage example
# === Returning SQL
#
# sql = Where.new('x=?',5).and( Where.new('x=?',6).or('x=?',7)).to_s
# # returns (x=5) and ( ( x=6 ) or ( x=7 ) )
#
# === Building a complicated where clause made easy
#
# def get_search_query_string
#
# where = Where.new
# where.and('users.first_name like ?', params[:search_first_name] + '%') unless params[:search_first_name].blank?
# where.and('users.last_name like ?', params[:search_last_name] + '%') unless params[:search_last_name].blank?
#
# status_where = Where.new
# for status in params[search_statuses].split(',')
# status_where.or 'status=?', status
# end
# where.and status_where unless status_where.blank?
#
# where.to_s
# end
#
# User.find(:all, :conditions => get_search_query_string)
#
# === Inline
#
# User.find(:all, :conditions => Where.new('first_name like ?', 'Tim').and('last_name like ?', 'Harper') )
# # Sweet chaining action!
class Where
# Constructs a new where clause
#
# optionally, you can provide a criteria, like the following:
#
# Where.initialize "joke_title = ?", "He says, 'Under there', to which I reply, 'under where?'"
def initialize(criteria=nil, *params, &block)
@clauses=Array.new
self.and(criteria, *params) unless criteria.nil?
yield(self) if block_given?
end
def initialize_copy(from)
@clauses = from.instance_variable_get("@clauses").clone
end
# Appends an and expression to your where clause
#
# Example:
#
# where = Where.new
# where.and("name = ?", "Tim O'brien")
# where.to_s
#
# # => "(name = 'Tim O''brien')
def and(*params, &block)
append_clause(params, "AND", &block)
end
alias << and
# Appends an or expression to your where clause
#
# Example:
#
# where = Where.new
# where.or("name = ?", "Tim O'brien")
# where.or("name = ?", "Tim O'neal")
# where.to_s
#
# # => "(name = 'Tim O''brien') or (name = 'Tim O''neal')"
def or(*params, &block)
append_clause(params, "OR", &block)
end
# Same as or, but negates the whole expression
def or_not(*params, &block)
append_clause(params, "OR NOT", &block)
end
# Same as and, but negates the whole expression
def and_not(*params, &block)
append_clause(params, "AND NOT", &block)
end
def &(params)
self.and(*params)
end
def |(params)
self.or(*params)
end
def self.&(params)
Where.new(*params)
end
def self.|(params)
Where.new.or(*params)
end
# Converts the where clause to a SQL string.
def to_s(format=nil)
output=""
@clauses.each_index{|index|
omit_conjuction = (index==0)
output << @clauses[index].to_s(omit_conjuction) # Omit the clause if index=0
}
case format
when :where
output.empty? ? "" : " WHERE #{output}"
else
output.empty? ? nil : output
end
end
alias :to_sql :to_s
# Determines if any clauses have been added.
#
# where = Where.new
# where.blank?
# # => true
#
# where.and(nil)
# where.blank?
# # => true
#
# where.and(Where.new(nil))
# where.blank?
# # => true
#
# where.and("name=1")
# where.blank?
# # => false
def blank?
@clauses.empty?
end
alias :empty? :blank?
protected
def append_clause(params, conjuction = "AND", &block) # :nodoc:
if block_given?
yield(w = Where.new)
@clauses << Clause.new(w, conjuction)
else
@clauses << Clause.new(params, conjuction) unless params.first.blank?
end
self
end
# Used internally to +Where+. You shouldn't have any reason to interact with this class.
class Clause
def initialize(criteria, conjuction = "AND") # :nodoc:
@conjuction=conjuction.upcase
criteria = criteria.first if criteria.class==Array && criteria.length==1
if criteria.class==Array # if it's an array, sanitize it
@criteria = ActiveRecord::Base.send(:sanitize_sql, criteria)
else
@criteria = criteria.to_s # otherwise, run to_s. If it's a recursive Where clause, it will return the sql we need
end
end
def to_s(omit_conjuction=false) # :nodoc:
if omit_conjuction
output = @conjuction.include?("NOT") ? "NOT " : ""
output << "(#{@criteria})"
else
" #{@conjuction} (#{@criteria})"
end
end
end
end
def Where(*params, &block)
Where.new(*params, &block)
end