module Rgviz class Executor attr_reader :model_class attr_reader :adapter def initialize(model_class, query) @model_class = model_class @query = query @joins = {} @labels = {} case ActiveRecord::Base.connection.adapter_name.downcase when 'sqlite' @adapter = SqliteAdapter.new when 'mysql' @adapter = MySqlAdapter.new end end def execute(options = {}) @table = Table.new @extra_conditions = options[:conditions] process_labels generate_columns generate_conditions generate_group generate_order generate_rows @table end def process_labels return unless @query.labels.present? @query.labels.each do |label| @labels[label.column.to_s] = label.label end end def add_joins(joins) map = @joins joins.each do |join| key = join.name val = map[key] map[key] = {} unless val map = map[key] end end def generate_columns @selects = [] if @query.select && @query.select.columns.present? # Select the specified columns i = 0 @query.select.columns.each do |col| @table.cols << (Column.new :id => column_id(col, i), :type => column_type(col), :label => column_label(col.to_s)) @selects << "(#{column_select(col)}) as c#{i}" i += 1 end else # Select all columns i = 0 @model_class.send(:columns).each do |col| @table.cols << (Column.new :id => col.name, :type => (rails_column_type col), :label => column_label(col.name)) @selects << "(#{col.name}) as c#{i}" i += 1 end end end def generate_conditions @conditions = to_string @query.where, WhereVisitor if @query.where end def generate_group @group = to_string @query.group_by, ColumnVisitor if @query.group_by end def generate_order @order = to_string @query.order_by, OrderVisitor if @query.order_by end def generate_rows conditions = @conditions if @extra_conditions if conditions if @extra_conditions.kind_of? String conditions = "(#{conditions}) AND #{@extra_conditions}" elsif @extra_conditions.kind_of? Array conditions = ["(#{conditions}) AND #{@extra_conditions[0]}", *@extra_conditions[1 .. -1]] end else conditions = @extra_conditions end end results = @model_class.send :all, :select => @selects.join(','), :conditions => conditions, :group => @group, :order => @order, :limit => @query.limit, :offset => @query.offset, :joins => @joins results.each do |result| row = Row.new @table.rows << row i = 0 @table.cols.each do |col| row.c << (Cell.new :v => column_value(col, result.send("c#{i}"))) i += 1 end end end def column_id(col, i) case col when IdColumn col.name else "c#{i}" end end def column_type(col) case col when IdColumn klass, rails_col, joins = Rgviz::find_rails_col @model_class, col.name raise "Unknown column #{col}" unless rails_col rails_column_type rails_col when NumberColumn :number when StringColumn :string when BooleanColumn :boolean when DateColumn :date when DateTimeColumn :datetime when TimeOfDayColumn :timeofday when ScalarFunctionColumn case col.function when ScalarFunctionColumn::Now :datetime when ScalarFunctionColumn::ToDate :date when ScalarFunctionColumn::Upper, ScalarFunctionColumn::Lower :string else :number end when AggregateColumn :number end end def column_select(col) to_string col, ColumnVisitor end def column_value(col, value) case col.type when :number i = value.to_i f = value.to_f i == f ? i : f when :boolean value == '1' ? true : false else value.to_s end end def column_label(string) @labels[string] || string end def to_string(node, visitor_class) visitor = visitor_class.new self node.accept visitor visitor.string end def rails_column_type(col) case col.type when :integer :number else col.type end end end class ColumnVisitor < Rgviz::Visitor attr_reader :string def initialize(executor) @string = '' @executor = executor end def <<(string) @string += string end def visit_id_column(node) klass, rails_col, joins = Rgviz::find_rails_col @executor.model_class, node.name raise "Unknown column '#{node.name}'" unless rails_col @string += ActiveRecord::Base.connection.quote_column_name(klass.table_name) @string += '.' @string += ActiveRecord::Base.connection.quote_column_name(rails_col.name) @executor.add_joins joins end def visit_number_column(node) @string += node.value.to_s end def visit_string_column(node) @string += escaped_string(node.value) end def visit_boolean_column(node) @string += node.value ? '1' : '0' end def visit_date_column(node) @string += escaped_string(node.value.to_s) end def visit_date_time_column(node) @string += escaped_string(node.value.strftime "%Y-%m-%d %H:%M:%S") end def visit_time_of_day_column(node) @string += escaped_string(node.value.strftime "%H:%M:%S") end def visit_scalar_function_column(node) case node.function when ScalarFunctionColumn::Sum, ScalarFunctionColumn::Difference, ScalarFunctionColumn::Product, ScalarFunctionColumn::Quotient node.arguments[0].accept self @string += node.function.to_s node.arguments[1].accept self else @string += @executor.adapter.accept_scalar_function_column(node, self) end false end def visit_aggregate_column(node) @string += node.function.to_s @string += '(' node.argument.accept self @string += ')' false end def visit_label(node) false end def visit_format(node) false end def visit_option(node) false end def escaped_string(str) str = str.gsub("'", "''") "'#{str}'" end end class WhereVisitor < ColumnVisitor def visit_binary_expression(node) node.left.accept self @string += " #{node.operator} " node.right.accept self false end def visit_unary_expression(node) case node.operator when UnaryExpression::Not @string += 'not (' node.operand.accept self @string += ')' when UnaryExpression::IsNull node.operand.accept self @string += 'is null' when UnaryExpression::IsNotNull node.operand.accept self @string += 'is not null' end false end end class OrderVisitor < ColumnVisitor def visit_order_by(node) i = 0 node.sorts.each do |sort| @string += ',' if i > 0 sort.accept self end false end def visit_sort(node) node.column.accept self @string += node.order == Sort::Asc ? ' asc' : ' desc' false end end def self.find_rails_col(klass, name) joins = [] while true col = klass.send(:columns).select{|x| x.name == name}.first return [klass, col, joins] if col idx = name.index '_' return nil if not idx before = name[0 ... idx] name = name[idx + 1 .. -1] assoc = klass.send :reflect_on_association, before.to_sym raise "Unknown association #{before}" unless assoc klass = assoc.klass joins << assoc end end class NotSupported < ::Exception end end