require 'active_record' # Ruby Way Finder # Proof of concept rather but who knows what will happen in the future # Usage: # 1. place this file in lib/ # 2. add two lines in model class, first: [require 'ruby_way_finder'] # and second somwhere in class body: [enable_rubyway_finder] # e.g.: # require 'ruby_way_finder' # class User < ActiveRecord::Base # enable_rubyway_finder # end # # Author:: Daniel Owsianski (daniel-at-jarmark-dot-org) module JarmarkOrg module RubyWayFinder def self.included(base) # :nodoc: base.extend ClassMethods end module ClassMethods def enable_rubyway_finder extend JarmarkOrg::RubyWayFinder::SingletonMethods end end module SingletonMethods def finder(*args, &block) if block_given? query = QueryBuilder.new query.find(&block) # convert query to sql via dialect-or # TODO: dialect as class level attr? dialect = PostgresqlDialect.new where = build_sql(query, dialect) puts "SQL WHERE clause: #{where}" end sql = "SELECT * FROM #{table_name}" sql += " WHERE #{where}" if where find_by_sql(sql) end private # Convert query predicates to SQL WHERE clause def build_sql(query, dialect) subquery_no = nil subquery_type = nil sql_buff = nil or_sql = nil query.predicates.each do |pred| previous_subquery_no = subquery_no previous_subquery_type = subquery_type subquery_no = pred.subquery_no subquery_type = pred.subquery_type if previous_subquery_no == subquery_no or_sql = '( '+or_sql+' OR '+ dialect.sqlize(pred) +')' else if previous_subquery_type == :or if previous_subquery_no == 1 sql_buff = or_sql else sql_buff = '( '+sql_buff +' AND '+ or_sql+' )' end end if subquery_type == :and if subquery_no == 1 sql_buff = dialect.sqlize(pred) else sql_buff = '( '+sql_buff +' AND '+dialect.sqlize(pred)+' )' end else or_sql = dialect.sqlize(pred) end end end if subquery_type == :or if subquery_no == 1 sql_buff = or_sql else sql_buff = sql_buff +' AND '+ or_sql end end sql_buff end end # Builds array of predicates from block contents class QueryBuilder attr_reader :predicates def initialize @predicates = [] @subquery_type = :and @subquery_no = 0 end def find(&block) yield self end def add_predicate(pred) @predicates << pred end def method_missing(name, *args) @subquery_no += 1 if @subquery_type == :and Property.new(name, self, @subquery_no, @subquery_type) end def any(&block) @subquery_type = :or @subquery_no += 1 yield @subquery_type = :and end end # Single property operation (mostly comparisions) class Property attr_reader :name, :subquery_no, :subquery_type, :operator, :arg def initialize(propname, query, subquery_no, subquery_type) @name = propname @query = query @subquery_no = subquery_no @subquery_type = subquery_type end [:>, :<=, :==, :<=, :<, :between, :eq].each do |operator| define_method(operator) do |*arg| @operator = operator @arg = arg @query.add_predicate(self) end end end # Encapsulation of sql dialects differences # Current implementation is just a simple begin class AbstractSQLDialect # Convert single predicate to proper sql expression def sqlize(predicate) prop = predicate.name op = predicate.operator arg = predicate.arg.first case op when :between if predicate.arg.size == 2 op, arg = between(predicate.arg.first, predicate.arg.last) else raise ArgumentError, "Operator 'between' needs exactly 2 arguments lower and upper limit" end when :==, :eq op = '=' case arg when Array, Range op, arg = in_list(arg.to_a) when Regexp op, arg = regexp(arg) when nil op, arg = is_null when String arg = quote(arg) end else arg = quote(arg) end "(#{prop} #{op} #{arg})" end end class PostgresqlDialect < AbstractSQLDialect def in_list(arr) [ op = 'IN', "('"+arr.map{ |v| v.to_s}.join("', '")+"')"] end def is_null ['IS', 'NULL'] end def regexp(re) ['~', "'#{re.source}'"] end def between(lo, hi) ['BETWEEN', "#{lo} AND #{hi}"] end def quote(arg) "'#{arg}'" end end end end # reopen ActiveRecord and include all the above to make # them available to all our models if they want it ActiveRecord::Base.class_eval do include JarmarkOrg::RubyWayFinder end