require "active_support/hash_with_indifferent_access"
module Fancygrid
class QueryGenerator#:nodoc:
OPERATOR_NAMES = [
:equal, :not_equal, :less, :less_equal, :greater, :greater_equal, :starts_with, :ends_with,
:like, :is_null, :is_not_null, :is_true, :is_not_true, :is_false, :is_not_false, :in, :not_in
]
attr_accessor :query
def initialize(options=nil)
options ||= {}
options = ActiveSupport::HashWithIndifferentAccess.new(options)
self.query = {}
self.select(options[:select])
self.apply_pagination(options[:pagination])
self.apply_search_conditions(options[:operator] || :and, options[:conditions])
self.apply_sort_order(options[:order])
end
def parse_options(options=nil)#:nodoc:
options ||= {}
[:conditions, :order, :group, :having, :limit, :offset, :joins, :include, :select, :from, :readonly, :lock].each do |option|
self.send(option, options[option]) unless options[option].nil?
end
end
# Takes a hash like { :page => 2, :per_page => 20 } and translates it into :limit and :offset options which are
# then applied to the final query
#
def apply_pagination(options=nil)
options ||= {}
options = ActiveSupport::HashWithIndifferentAccess.new(options)
self.limit(options[:per_page].to_i)
self.offset(options[:page].to_i * self.limit())
end
# Takes a hash like { :column => "users.name", :order => "asc" } and translates it into the :order option and
# then applies it to the final query
#
def apply_sort_order(options=nil)
self.order("#{options[:column]} #{options[:order].to_s.upcase}") if options
end
# Takes an operator and an conditions hash like { :
=> { : => [{ :oparator => , :value => }] } }
# and converts them into a query joined by the given operator
#
def apply_search_conditions(operator, search_conditions)
return unless search_conditions
operator = logical_operator(operator)
conditions = []
arguments = []
# backward compatibility
search_conditions = search_conditions.map do |table, columns|
columns.map do |column, value|
if value.is_a?(Hash)
if value.keys.all? { |key| key.to_s.match(/^\d+$/) }
# for hashes like this
# : => {
# : => {
# "0" => { :oparator => , :value => },
# "1" => { :oparator => , :value => },
# "2" => { :oparator => , :value => }
# }
# }
#
value.map{ |key, opts|
{ :column => "#{table}.#{column}", :operator => opts[:operator], :value => opts[:value] }
}
else
# for hashes like this
# : => {
# : => {
# :oparator => , :value =>
# }
# }
#
{ :column => "#{table}.#{column}", :operator => value[:operator], :value => value[:value] }
end
elsif value.is_a?(Array)
# for hashes like this
# : => {
# : => {
# [{ :oparator => , :value => },
# { :oparator => , :value => },
# { :oparator => , :value => }]
# }
# }
#
value.map{ |opts|
{ :column => "#{table}.#{column}", :operator => opts[:operator], :value => opts[:value] }
}
else
# for hashes like this
# : => {
# : =>
# }
#
unless value.blank?
{ :column => "#{table}.#{column}", :operator => :like, :value => value }
else
nil
end
end
end
end
search_conditions = search_conditions.flatten
search_conditions.each do |options|
next unless options
sql_query, value = comparison_operator(options[:column], options[:operator], options[:value])
conditions << sql_query
arguments << value if (value)
end
conditions = [conditions.join(operator)] + arguments
append_conditions(:and, conditions)
end
# Joins two conditions arrays or strings with the given operator
#
# === Example
#
# condition1 = ["first_name = ?", first_name]
# condition2 = ["last_name = ?", last_name]
#
# join_conditions(:and, condition1, condition2)
# # => ["(first_name = ?) AND (last_name = ?)", first_name, last_name]
#
def join_conditions(operator, conditions1, conditions2)
conditions1 = Array(conditions1)
conditions2 = Array(conditions2)
operator = logical_operator(operator).gsub(" ", "")
if conditions1.empty?
return [] if conditions2.empty?
return conditions2
elsif conditions2.empty?
return conditions1
end
left_sql = conditions1.shift
right_sql = conditions2.shift
if left_sql.blank?
return [] if right_sql.blank?
return [right_sql] + conditions2
elsif right_sql.blank?
return [left_sql] + conditions1
end
conditions = "(#{left_sql}) #{operator} (#{right_sql})"
return [conditions] + conditions1 + conditions2
end
def append_conditions(operator, conditions)
self.query[:conditions] = join_conditions(operator, self.query[:conditions], conditions)
end
# An SQL fragment like “administrator = 1”, ["user_name = ?", username], or ["user_name = :user_name", { :user_name => user_name }]
#
def conditions(conditions=nil)
if conditions
append_conditions(:and, conditions)
end
self.query[:conditions]
end
# An SQL fragment like “created_at DESC, name”.
#
def order(order_by=nil)
self.query[:order] = order_by if order_by
self.query[:order]
end
# An SQL fragment like “created_at DESC, name”.
#
def group(group_by=nil)
self.query[:group] = group_by if group_by
self.query[:group]
end
# An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
#
def having(having_sql=nil)
self.query[:having] = having_sql if having_sql
self.query[:having]
end
# An integer determining the limit on the number of rows that should be returned.
#
def limit(num=nil)
self.query[:limit] = num if num
self.query[:limit]
end
# An integer determining the offset from where the rows should be fetched. So at 5, it would skip rows 0 through 4.
#
def offset(num=nil)
self.query[:offset] = num if num
self.query[:offset]
end
# Either an SQL fragment for additional joins like “LEFT JOIN comments ON comments.post_id = id” (rarely needed),
# named associations in the same form used for the :include option, which will perform an INNER JOIN on the
# associated table(s), or an array containing a mixture of both strings and named associations. If the value is a
# string, then the records will be returned read-only since they will have attributes that do not correspond to the
# table’s columns. Pass :readonly => false to override.
#
def joins(to_join_with=nil)
self.query[:joins] = to_join_with if to_join_with
self.query[:joins]
end
# Names associations that should be loaded alongside. The symbols named refer to already defined associations.
# See eager loading under Associations.
#
def include(to_include=nil)
self.query[:include] = to_include if to_include
self.query[:include]
end
# By default, this is “*” as in “SELECT * FROM”, but can be changed if you, for example, want to do a join but not
# include the joined columns. Takes a string with the SELECT SQL fragment (e.g. “id, name”).
#
def select(select = nil)
if select
self.query[:select] = Array(self.query[:select])
self.query[:select] |= Array(select)
if self.query[:select].include?("*")
self.query[:select] = ["*"]
end
end
self.query[:select]
end
# By default, this is the table name of the class, but can be changed to an alternate table name
# (or even the name of a database view).
#
def from(table_name=nil)
self.query[:from] = table_name if table_name
self.query[:from]
end
# Mark the returned records read-only so they cannot be saved or updated.
#
def readonly(value=nil)
self.query[:readonly] = value unless value.nil?
self.query[:readonly]
end
# An SQL fragment like “FOR UPDATE” or “LOCK IN SHARE MODE”. :lock => true gives connection’s default exclusive
# lock, usually “FOR UPDATE”.
#
def lock(value=nil)
self.query[:lock] = value unless value.nil?
self.query[:lock]
end
private
def comparison_operator(column, operator, value)
operator = case operator.to_s
when "equal"
"="
when "not_equal"
"!="
when "less"
"<"
when "less_equal"
"<="
when "greater"
">"
when "greater_equal"
">="
when "starts_with"
value = "#{value.to_param}%"
"LIKE"
when "ends_with"
value = "%#{value.to_param}"
"LIKE"
when "like"
value = "%#{value.to_param}%"
"LIKE"
when "is_null"
value = nil
"IS NULL"
when "is_not_null"
value = nil
"IS NOT NULL"
when "is_true"
value = nil
"IS TRUE"
when "is_not_true"
value = nil
"IS NOT TRUE"
when "is_false"
value = nil
"IS FALSE"
when "is_not_false"
value = nil
"IS NOT FALSE"
when "in"
value = value.split(",")
"IN"
when "not_in"
value = value.split(",")
"NOT IN"
else
"="
end
if value.nil?
return "( #{column} #{operator} )", value
else
return "( #{column} #{operator} (?) )", value
end
end
def logical_operator(name)
case name.to_s
when "all", "and"
" AND "
else
" OR "
end
end
end
end