diff options
Diffstat (limited to 'activerecord/lib/arel/visitors')
-rw-r--r-- | activerecord/lib/arel/visitors/depth_first.rb | 199 | ||||
-rw-r--r-- | activerecord/lib/arel/visitors/dot.rb | 291 | ||||
-rw-r--r-- | activerecord/lib/arel/visitors/ibm_db.rb | 15 | ||||
-rw-r--r-- | activerecord/lib/arel/visitors/informix.rb | 55 | ||||
-rw-r--r-- | activerecord/lib/arel/visitors/mssql.rb | 124 | ||||
-rw-r--r-- | activerecord/lib/arel/visitors/mysql.rb | 86 | ||||
-rw-r--r-- | activerecord/lib/arel/visitors/oracle.rb | 153 | ||||
-rw-r--r-- | activerecord/lib/arel/visitors/oracle12.rb | 60 | ||||
-rw-r--r-- | activerecord/lib/arel/visitors/postgresql.rb | 103 | ||||
-rw-r--r-- | activerecord/lib/arel/visitors/sqlite.rb | 27 | ||||
-rw-r--r-- | activerecord/lib/arel/visitors/to_sql.rb | 846 | ||||
-rw-r--r-- | activerecord/lib/arel/visitors/visitor.rb | 41 | ||||
-rw-r--r-- | activerecord/lib/arel/visitors/where_sql.rb | 22 |
13 files changed, 2022 insertions, 0 deletions
diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb new file mode 100644 index 0000000000..b3bbc9bd40 --- /dev/null +++ b/activerecord/lib/arel/visitors/depth_first.rb @@ -0,0 +1,199 @@ +# frozen_string_literal: true +module Arel + module Visitors + class DepthFirst < Arel::Visitors::Visitor + def initialize block = nil + @block = block || Proc.new + super() + end + + private + + def visit o + super + @block.call o + end + + def unary o + visit o.expr + end + alias :visit_Arel_Nodes_Else :unary + alias :visit_Arel_Nodes_Group :unary + alias :visit_Arel_Nodes_Cube :unary + alias :visit_Arel_Nodes_RollUp :unary + alias :visit_Arel_Nodes_GroupingSet :unary + alias :visit_Arel_Nodes_GroupingElement :unary + alias :visit_Arel_Nodes_Grouping :unary + alias :visit_Arel_Nodes_Having :unary + alias :visit_Arel_Nodes_Lateral :unary + alias :visit_Arel_Nodes_Limit :unary + alias :visit_Arel_Nodes_Not :unary + alias :visit_Arel_Nodes_Offset :unary + alias :visit_Arel_Nodes_On :unary + alias :visit_Arel_Nodes_Ordering :unary + alias :visit_Arel_Nodes_Ascending :unary + alias :visit_Arel_Nodes_Descending :unary + alias :visit_Arel_Nodes_Top :unary + alias :visit_Arel_Nodes_UnqualifiedColumn :unary + + def function o + visit o.expressions + visit o.alias + visit o.distinct + end + alias :visit_Arel_Nodes_Avg :function + alias :visit_Arel_Nodes_Exists :function + alias :visit_Arel_Nodes_Max :function + alias :visit_Arel_Nodes_Min :function + alias :visit_Arel_Nodes_Sum :function + + def visit_Arel_Nodes_NamedFunction o + visit o.name + visit o.expressions + visit o.distinct + visit o.alias + end + + def visit_Arel_Nodes_Count o + visit o.expressions + visit o.alias + visit o.distinct + end + + def visit_Arel_Nodes_Case o + visit o.case + visit o.conditions + visit o.default + end + + def nary o + o.children.each { |child| visit child} + end + alias :visit_Arel_Nodes_And :nary + + def binary o + visit o.left + visit o.right + end + alias :visit_Arel_Nodes_As :binary + alias :visit_Arel_Nodes_Assignment :binary + alias :visit_Arel_Nodes_Between :binary + alias :visit_Arel_Nodes_Concat :binary + alias :visit_Arel_Nodes_DeleteStatement :binary + alias :visit_Arel_Nodes_DoesNotMatch :binary + alias :visit_Arel_Nodes_Equality :binary + alias :visit_Arel_Nodes_FullOuterJoin :binary + alias :visit_Arel_Nodes_GreaterThan :binary + alias :visit_Arel_Nodes_GreaterThanOrEqual :binary + alias :visit_Arel_Nodes_In :binary + alias :visit_Arel_Nodes_InfixOperation :binary + alias :visit_Arel_Nodes_JoinSource :binary + alias :visit_Arel_Nodes_InnerJoin :binary + alias :visit_Arel_Nodes_LessThan :binary + alias :visit_Arel_Nodes_LessThanOrEqual :binary + alias :visit_Arel_Nodes_Matches :binary + alias :visit_Arel_Nodes_NotEqual :binary + alias :visit_Arel_Nodes_NotIn :binary + alias :visit_Arel_Nodes_NotRegexp :binary + alias :visit_Arel_Nodes_Or :binary + alias :visit_Arel_Nodes_OuterJoin :binary + alias :visit_Arel_Nodes_Regexp :binary + alias :visit_Arel_Nodes_RightOuterJoin :binary + alias :visit_Arel_Nodes_TableAlias :binary + alias :visit_Arel_Nodes_Values :binary + alias :visit_Arel_Nodes_When :binary + + def visit_Arel_Nodes_StringJoin o + visit o.left + end + + def visit_Arel_Attribute o + visit o.relation + visit o.name + end + alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute + alias :visit_Arel_Attributes_Float :visit_Arel_Attribute + alias :visit_Arel_Attributes_String :visit_Arel_Attribute + alias :visit_Arel_Attributes_Time :visit_Arel_Attribute + alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute + alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute + alias :visit_Arel_Attributes_Decimal :visit_Arel_Attribute + + def visit_Arel_Table o + visit o.name + end + + def terminal o + end + alias :visit_ActiveSupport_Multibyte_Chars :terminal + alias :visit_ActiveSupport_StringInquirer :terminal + alias :visit_Arel_Nodes_Lock :terminal + alias :visit_Arel_Nodes_Node :terminal + alias :visit_Arel_Nodes_SqlLiteral :terminal + alias :visit_Arel_Nodes_BindParam :terminal + alias :visit_Arel_Nodes_Window :terminal + alias :visit_Arel_Nodes_True :terminal + alias :visit_Arel_Nodes_False :terminal + alias :visit_BigDecimal :terminal + alias :visit_Bignum :terminal + alias :visit_Class :terminal + alias :visit_Date :terminal + alias :visit_DateTime :terminal + alias :visit_FalseClass :terminal + alias :visit_Fixnum :terminal + alias :visit_Float :terminal + alias :visit_Integer :terminal + alias :visit_NilClass :terminal + alias :visit_String :terminal + alias :visit_Symbol :terminal + alias :visit_Time :terminal + alias :visit_TrueClass :terminal + + def visit_Arel_Nodes_InsertStatement o + visit o.relation + visit o.columns + visit o.values + end + + def visit_Arel_Nodes_SelectCore o + visit o.projections + visit o.source + visit o.wheres + visit o.groups + visit o.windows + visit o.havings + end + + def visit_Arel_Nodes_SelectStatement o + visit o.cores + visit o.orders + visit o.limit + visit o.lock + visit o.offset + end + + def visit_Arel_Nodes_UpdateStatement o + visit o.relation + visit o.values + visit o.wheres + visit o.orders + visit o.limit + end + + def visit_Array o + o.each { |i| visit i } + end + alias :visit_Set :visit_Array + + def visit_Hash o + o.each { |k,v| visit(k); visit(v) } + end + + DISPATCH = dispatch_cache + + def get_dispatch_cache + DISPATCH + end + end + end +end diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb new file mode 100644 index 0000000000..9aa22d33f6 --- /dev/null +++ b/activerecord/lib/arel/visitors/dot.rb @@ -0,0 +1,291 @@ +# frozen_string_literal: true +module Arel + module Visitors + class Dot < Arel::Visitors::Visitor + class Node # :nodoc: + attr_accessor :name, :id, :fields + + def initialize name, id, fields = [] + @name = name + @id = id + @fields = fields + end + end + + class Edge < Struct.new :name, :from, :to # :nodoc: + end + + def initialize + super() + @nodes = [] + @edges = [] + @node_stack = [] + @edge_stack = [] + @seen = {} + end + + def accept object, collector + visit object + collector << to_dot + end + + private + + def visit_Arel_Nodes_Ordering o + visit_edge o, "expr" + end + + def visit_Arel_Nodes_TableAlias o + visit_edge o, "name" + visit_edge o, "relation" + end + + def visit_Arel_Nodes_Count o + visit_edge o, "expressions" + visit_edge o, "distinct" + end + + def visit_Arel_Nodes_Values o + visit_edge o, "expressions" + end + + def visit_Arel_Nodes_StringJoin o + visit_edge o, "left" + end + + def visit_Arel_Nodes_InnerJoin o + visit_edge o, "left" + visit_edge o, "right" + end + alias :visit_Arel_Nodes_FullOuterJoin :visit_Arel_Nodes_InnerJoin + alias :visit_Arel_Nodes_OuterJoin :visit_Arel_Nodes_InnerJoin + alias :visit_Arel_Nodes_RightOuterJoin :visit_Arel_Nodes_InnerJoin + + def visit_Arel_Nodes_DeleteStatement o + visit_edge o, "relation" + visit_edge o, "wheres" + end + + def unary o + visit_edge o, "expr" + end + alias :visit_Arel_Nodes_Group :unary + alias :visit_Arel_Nodes_Cube :unary + alias :visit_Arel_Nodes_RollUp :unary + alias :visit_Arel_Nodes_GroupingSet :unary + alias :visit_Arel_Nodes_GroupingElement :unary + alias :visit_Arel_Nodes_Grouping :unary + alias :visit_Arel_Nodes_Having :unary + alias :visit_Arel_Nodes_Limit :unary + alias :visit_Arel_Nodes_Not :unary + alias :visit_Arel_Nodes_Offset :unary + alias :visit_Arel_Nodes_On :unary + alias :visit_Arel_Nodes_Top :unary + alias :visit_Arel_Nodes_UnqualifiedColumn :unary + alias :visit_Arel_Nodes_Preceding :unary + alias :visit_Arel_Nodes_Following :unary + alias :visit_Arel_Nodes_Rows :unary + alias :visit_Arel_Nodes_Range :unary + + def window o + visit_edge o, "partitions" + visit_edge o, "orders" + visit_edge o, "framing" + end + alias :visit_Arel_Nodes_Window :window + + def named_window o + visit_edge o, "partitions" + visit_edge o, "orders" + visit_edge o, "framing" + visit_edge o, "name" + end + alias :visit_Arel_Nodes_NamedWindow :named_window + + def function o + visit_edge o, "expressions" + visit_edge o, "distinct" + visit_edge o, "alias" + end + alias :visit_Arel_Nodes_Exists :function + alias :visit_Arel_Nodes_Min :function + alias :visit_Arel_Nodes_Max :function + alias :visit_Arel_Nodes_Avg :function + alias :visit_Arel_Nodes_Sum :function + + def extract o + visit_edge o, "expressions" + visit_edge o, "alias" + end + alias :visit_Arel_Nodes_Extract :extract + + def visit_Arel_Nodes_NamedFunction o + visit_edge o, "name" + visit_edge o, "expressions" + visit_edge o, "distinct" + visit_edge o, "alias" + end + + def visit_Arel_Nodes_InsertStatement o + visit_edge o, "relation" + visit_edge o, "columns" + visit_edge o, "values" + end + + def visit_Arel_Nodes_SelectCore o + visit_edge o, "source" + visit_edge o, "projections" + visit_edge o, "wheres" + visit_edge o, "windows" + end + + def visit_Arel_Nodes_SelectStatement o + visit_edge o, "cores" + visit_edge o, "limit" + visit_edge o, "orders" + visit_edge o, "offset" + end + + def visit_Arel_Nodes_UpdateStatement o + visit_edge o, "relation" + visit_edge o, "wheres" + visit_edge o, "values" + end + + def visit_Arel_Table o + visit_edge o, "name" + end + + def visit_Arel_Nodes_Casted o + visit_edge o, 'val' + visit_edge o, 'attribute' + end + + def visit_Arel_Attribute o + visit_edge o, "relation" + visit_edge o, "name" + end + alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute + alias :visit_Arel_Attributes_Float :visit_Arel_Attribute + alias :visit_Arel_Attributes_String :visit_Arel_Attribute + alias :visit_Arel_Attributes_Time :visit_Arel_Attribute + alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute + alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute + + def nary o + o.children.each_with_index do |x,i| + edge(i) { visit x } + end + end + alias :visit_Arel_Nodes_And :nary + + def binary o + visit_edge o, "left" + visit_edge o, "right" + end + alias :visit_Arel_Nodes_As :binary + alias :visit_Arel_Nodes_Assignment :binary + alias :visit_Arel_Nodes_Between :binary + alias :visit_Arel_Nodes_Concat :binary + alias :visit_Arel_Nodes_DoesNotMatch :binary + alias :visit_Arel_Nodes_Equality :binary + alias :visit_Arel_Nodes_GreaterThan :binary + alias :visit_Arel_Nodes_GreaterThanOrEqual :binary + alias :visit_Arel_Nodes_In :binary + alias :visit_Arel_Nodes_JoinSource :binary + alias :visit_Arel_Nodes_LessThan :binary + alias :visit_Arel_Nodes_LessThanOrEqual :binary + alias :visit_Arel_Nodes_Matches :binary + alias :visit_Arel_Nodes_NotEqual :binary + alias :visit_Arel_Nodes_NotIn :binary + alias :visit_Arel_Nodes_Or :binary + alias :visit_Arel_Nodes_Over :binary + + def visit_String o + @node_stack.last.fields << o + end + alias :visit_Time :visit_String + alias :visit_Date :visit_String + alias :visit_DateTime :visit_String + alias :visit_NilClass :visit_String + alias :visit_TrueClass :visit_String + alias :visit_FalseClass :visit_String + alias :visit_Integer :visit_String + alias :visit_Fixnum :visit_String + alias :visit_BigDecimal :visit_String + alias :visit_Float :visit_String + alias :visit_Symbol :visit_String + alias :visit_Arel_Nodes_SqlLiteral :visit_String + + def visit_Arel_Nodes_BindParam o; end + + def visit_Hash o + o.each_with_index do |pair, i| + edge("pair_#{i}") { visit pair } + end + end + + def visit_Array o + o.each_with_index do |x,i| + edge(i) { visit x } + end + end + alias :visit_Set :visit_Array + + def visit_edge o, method + edge(method) { visit o.send(method) } + end + + def visit o + if node = @seen[o.object_id] + @edge_stack.last.to = node + return + end + + node = Node.new(o.class.name, o.object_id) + @seen[node.id] = node + @nodes << node + with_node node do + super + end + end + + def edge name + edge = Edge.new(name, @node_stack.last) + @edge_stack.push edge + @edges << edge + yield + @edge_stack.pop + end + + def with_node node + if edge = @edge_stack.last + edge.to = node + end + + @node_stack.push node + yield + @node_stack.pop + end + + def quote string + string.to_s.gsub('"', '\"') + end + + def to_dot + "digraph \"Arel\" {\nnode [width=0.375,height=0.25,shape=record];\n" + + @nodes.map { |node| + label = "<f0>#{node.name}" + + node.fields.each_with_index do |field, i| + label += "|<f#{i + 1}>#{quote field}" + end + + "#{node.id} [label=\"#{label}\"];" + }.join("\n") + "\n" + @edges.map { |edge| + "#{edge.from.id} -> #{edge.to.id} [label=\"#{edge.name}\"];" + }.join("\n") + "\n}" + end + end + end +end diff --git a/activerecord/lib/arel/visitors/ibm_db.rb b/activerecord/lib/arel/visitors/ibm_db.rb new file mode 100644 index 0000000000..e85a5a08a7 --- /dev/null +++ b/activerecord/lib/arel/visitors/ibm_db.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true +module Arel + module Visitors + class IBM_DB < Arel::Visitors::ToSql + private + + def visit_Arel_Nodes_Limit o, collector + collector << "FETCH FIRST " + collector = visit o.expr, collector + collector << " ROWS ONLY" + end + + end + end +end diff --git a/activerecord/lib/arel/visitors/informix.rb b/activerecord/lib/arel/visitors/informix.rb new file mode 100644 index 0000000000..44b18b550e --- /dev/null +++ b/activerecord/lib/arel/visitors/informix.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true +module Arel + module Visitors + class Informix < Arel::Visitors::ToSql + private + def visit_Arel_Nodes_SelectStatement o, collector + collector << "SELECT " + collector = maybe_visit o.offset, collector + collector = maybe_visit o.limit, collector + collector = o.cores.inject(collector) { |c,x| + visit_Arel_Nodes_SelectCore x, c + } + if o.orders.any? + collector << "ORDER BY " + collector = inject_join o.orders, collector, ", " + end + collector = maybe_visit o.lock, collector + end + def visit_Arel_Nodes_SelectCore o, collector + collector = inject_join o.projections, collector, ", " + if o.source && !o.source.empty? + collector << " FROM " + collector = visit o.source, collector + end + + if o.wheres.any? + collector << " WHERE " + collector = inject_join o.wheres, collector, " AND " + end + + if o.groups.any? + collector << "GROUP BY " + collector = inject_join o.groups, collector, ", " + end + + if o.havings.any? + collector << " HAVING " + collector = inject_join o.havings, collector, " AND " + end + collector + end + + def visit_Arel_Nodes_Offset o, collector + collector << "SKIP " + visit o.expr, collector + end + def visit_Arel_Nodes_Limit o, collector + collector << "FIRST " + visit o.expr, collector + collector << " " + end + end + end +end + diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb new file mode 100644 index 0000000000..8347d05d06 --- /dev/null +++ b/activerecord/lib/arel/visitors/mssql.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true +module Arel + module Visitors + class MSSQL < Arel::Visitors::ToSql + RowNumber = Struct.new :children + + def initialize(*) + @primary_keys = {} + super + end + + private + + # `top` wouldn't really work here. I.e. User.select("distinct first_name").limit(10) would generate + # "select top 10 distinct first_name from users", which is invalid query! it should be + # "select distinct top 10 first_name from users" + def visit_Arel_Nodes_Top o + "" + end + + def visit_Arel_Visitors_MSSQL_RowNumber o, collector + collector << "ROW_NUMBER() OVER (ORDER BY " + inject_join(o.children, collector, ', ') << ") as _row_num" + end + + def visit_Arel_Nodes_SelectStatement o, collector + if !o.limit && !o.offset + return super + end + + is_select_count = false + o.cores.each { |x| + core_order_by = row_num_literal determine_order_by(o.orders, x) + if select_count? x + x.projections = [core_order_by] + is_select_count = true + else + x.projections << core_order_by + end + } + + if is_select_count + # fixme count distinct wouldn't work with limit or offset + collector << "SELECT COUNT(1) as count_id FROM (" + end + + collector << "SELECT _t.* FROM (" + collector = o.cores.inject(collector) { |c,x| + visit_Arel_Nodes_SelectCore x, c + } + collector << ") as _t WHERE #{get_offset_limit_clause(o)}" + + if is_select_count + collector << ") AS subquery" + else + collector + end + end + + def get_offset_limit_clause o + first_row = o.offset ? o.offset.expr.to_i + 1 : 1 + last_row = o.limit ? o.limit.expr.to_i - 1 + first_row : nil + if last_row + " _row_num BETWEEN #{first_row} AND #{last_row}" + else + " _row_num >= #{first_row}" + end + end + + def visit_Arel_Nodes_DeleteStatement o, collector + collector << 'DELETE ' + if o.limit + collector << 'TOP (' + visit o.limit.expr, collector + collector << ') ' + end + collector << 'FROM ' + collector = visit o.relation, collector + if o.wheres.any? + collector << ' WHERE ' + inject_join o.wheres, collector, AND + else + collector + end + end + + def determine_order_by orders, x + if orders.any? + orders + elsif x.groups.any? + x.groups + else + pk = find_left_table_pk(x.froms) + pk ? [pk] : [] + end + end + + def row_num_literal order_by + RowNumber.new order_by + end + + def select_count? x + x.projections.length == 1 && Arel::Nodes::Count === x.projections.first + end + + # FIXME raise exception of there is no pk? + def find_left_table_pk o + if o.kind_of?(Arel::Nodes::Join) + find_left_table_pk(o.left) + elsif o.instance_of?(Arel::Table) + find_primary_key(o) + end + end + + def find_primary_key(o) + @primary_keys[o.name] ||= begin + primary_key_name = @connection.primary_key(o.name) + # some tables might be without primary key + primary_key_name && o[primary_key_name] + end + end + end + end +end diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb new file mode 100644 index 0000000000..4c734f6292 --- /dev/null +++ b/activerecord/lib/arel/visitors/mysql.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true +module Arel + module Visitors + class MySQL < Arel::Visitors::ToSql + private + def visit_Arel_Nodes_Union o, collector, suppress_parens = false + unless suppress_parens + collector << "( " + end + + collector = case o.left + when Arel::Nodes::Union + visit_Arel_Nodes_Union o.left, collector, true + else + visit o.left, collector + end + + collector << " UNION " + + collector = case o.right + when Arel::Nodes::Union + visit_Arel_Nodes_Union o.right, collector, true + else + visit o.right, collector + end + + if suppress_parens + collector + else + collector << " )" + end + end + + def visit_Arel_Nodes_Bin o, collector + collector << "BINARY " + visit o.expr, collector + end + + ### + # :'( + # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214 + def visit_Arel_Nodes_SelectStatement o, collector + if o.offset && !o.limit + o.limit = Arel::Nodes::Limit.new(18446744073709551615) + end + super + end + + def visit_Arel_Nodes_SelectCore o, collector + o.froms ||= Arel.sql('DUAL') + super + end + + def visit_Arel_Nodes_UpdateStatement o, collector + collector << "UPDATE " + collector = visit o.relation, collector + + unless o.values.empty? + collector << " SET " + collector = inject_join o.values, collector, ', ' + end + + unless o.wheres.empty? + collector << " WHERE " + collector = inject_join o.wheres, collector, ' AND ' + end + + unless o.orders.empty? + collector << " ORDER BY " + collector = inject_join o.orders, collector, ', ' + end + + maybe_visit o.limit, collector + end + + def visit_Arel_Nodes_Concat o, collector + collector << " CONCAT(" + visit o.left, collector + collector << ", " + visit o.right, collector + collector << ") " + collector + end + end + end +end diff --git a/activerecord/lib/arel/visitors/oracle.rb b/activerecord/lib/arel/visitors/oracle.rb new file mode 100644 index 0000000000..d4749bbae3 --- /dev/null +++ b/activerecord/lib/arel/visitors/oracle.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true +module Arel + module Visitors + class Oracle < Arel::Visitors::ToSql + private + + def visit_Arel_Nodes_SelectStatement o, collector + o = order_hacks(o) + + # if need to select first records without ORDER BY and GROUP BY and without DISTINCT + # then can use simple ROWNUM in WHERE clause + if o.limit && o.orders.empty? && o.cores.first.groups.empty? && !o.offset && o.cores.first.set_quantifier.class.to_s !~ /Distinct/ + o.cores.last.wheres.push Nodes::LessThanOrEqual.new( + Nodes::SqlLiteral.new('ROWNUM'), o.limit.expr + ) + return super + end + + if o.limit && o.offset + o = o.dup + limit = o.limit.expr + offset = o.offset + o.offset = nil + collector << " + SELECT * FROM ( + SELECT raw_sql_.*, rownum raw_rnum_ + FROM (" + + collector = super(o, collector) + + if offset.expr.is_a? Nodes::BindParam + collector << ') raw_sql_ WHERE rownum <= (' + collector = visit offset.expr, collector + collector << ' + ' + collector = visit limit, collector + collector << ") ) WHERE raw_rnum_ > " + collector = visit offset.expr, collector + return collector + else + collector << ") raw_sql_ + WHERE rownum <= #{offset.expr.to_i + limit} + ) + WHERE " + return visit(offset, collector) + end + end + + if o.limit + o = o.dup + limit = o.limit.expr + collector << "SELECT * FROM (" + collector = super(o, collector) + collector << ") WHERE ROWNUM <= " + return visit limit, collector + end + + if o.offset + o = o.dup + offset = o.offset + o.offset = nil + collector << "SELECT * FROM ( + SELECT raw_sql_.*, rownum raw_rnum_ + FROM (" + collector = super(o, collector) + collector << ") raw_sql_ + ) + WHERE " + return visit offset, collector + end + + super + end + + def visit_Arel_Nodes_Limit o, collector + collector + end + + def visit_Arel_Nodes_Offset o, collector + collector << "raw_rnum_ > " + visit o.expr, collector + end + + def visit_Arel_Nodes_Except o, collector + collector << "( " + collector = infix_value o, collector, " MINUS " + collector << " )" + end + + def visit_Arel_Nodes_UpdateStatement o, collector + # Oracle does not allow ORDER BY/LIMIT in UPDATEs. + if o.orders.any? && o.limit.nil? + # However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided, + # otherwise let the user deal with the error + o = o.dup + o.orders = [] + end + + super + end + + ### + # Hacks for the order clauses specific to Oracle + def order_hacks o + return o if o.orders.empty? + return o unless o.cores.any? do |core| + core.projections.any? do |projection| + /FIRST_VALUE/ === projection + end + end + # Previous version with join and split broke ORDER BY clause + # if it contained functions with several arguments (separated by ','). + # + # orders = o.orders.map { |x| visit x }.join(', ').split(',') + orders = o.orders.map do |x| + string = visit(x, Arel::Collectors::SQLString.new).value + if string.include?(',') + split_order_string(string) + else + string + end + end.flatten + o.orders = [] + orders.each_with_index do |order, i| + o.orders << + Nodes::SqlLiteral.new("alias_#{i}__#{' DESC' if /\bdesc$/i === order}") + end + o + end + + # Split string by commas but count opening and closing brackets + # and ignore commas inside brackets. + def split_order_string(string) + array = [] + i = 0 + string.split(',').each do |part| + if array[i] + array[i] << ',' << part + else + # to ensure that array[i] will be String and not Arel::Nodes::SqlLiteral + array[i] = part.to_s + end + i += 1 if array[i].count('(') == array[i].count(')') + end + array + end + + def visit_Arel_Nodes_BindParam o, collector + collector.add_bind(o.value) { |i| ":a#{i}" } + end + + end + end +end diff --git a/activerecord/lib/arel/visitors/oracle12.rb b/activerecord/lib/arel/visitors/oracle12.rb new file mode 100644 index 0000000000..648047ae61 --- /dev/null +++ b/activerecord/lib/arel/visitors/oracle12.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true +module Arel + module Visitors + class Oracle12 < Arel::Visitors::ToSql + private + + def visit_Arel_Nodes_SelectStatement o, collector + # Oracle does not allow LIMIT clause with select for update + if o.limit && o.lock + raise ArgumentError, <<-MSG + 'Combination of limit and lock is not supported. + because generated SQL statements + `SELECT FOR UPDATE and FETCH FIRST n ROWS` generates ORA-02014.` + MSG + end + super + end + + def visit_Arel_Nodes_SelectOptions o, collector + collector = maybe_visit o.offset, collector + collector = maybe_visit o.limit, collector + collector = maybe_visit o.lock, collector + end + + def visit_Arel_Nodes_Limit o, collector + collector << "FETCH FIRST " + collector = visit o.expr, collector + collector << " ROWS ONLY" + end + + def visit_Arel_Nodes_Offset o, collector + collector << "OFFSET " + visit o.expr, collector + collector << " ROWS" + end + + def visit_Arel_Nodes_Except o, collector + collector << "( " + collector = infix_value o, collector, " MINUS " + collector << " )" + end + + def visit_Arel_Nodes_UpdateStatement o, collector + # Oracle does not allow ORDER BY/LIMIT in UPDATEs. + if o.orders.any? && o.limit.nil? + # However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided, + # otherwise let the user deal with the error + o = o.dup + o.orders = [] + end + + super + end + + def visit_Arel_Nodes_BindParam o, collector + collector.add_bind(o.value) { |i| ":a#{i}" } + end + end + end +end diff --git a/activerecord/lib/arel/visitors/postgresql.rb b/activerecord/lib/arel/visitors/postgresql.rb new file mode 100644 index 0000000000..047f71aaa6 --- /dev/null +++ b/activerecord/lib/arel/visitors/postgresql.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true +module Arel + module Visitors + class PostgreSQL < Arel::Visitors::ToSql + CUBE = 'CUBE' + ROLLUP = 'ROLLUP' + GROUPING_SET = 'GROUPING SET' + LATERAL = 'LATERAL' + + private + + def visit_Arel_Nodes_Matches o, collector + op = o.case_sensitive ? ' LIKE ' : ' ILIKE ' + collector = infix_value o, collector, op + if o.escape + collector << ' ESCAPE ' + visit o.escape, collector + else + collector + end + end + + def visit_Arel_Nodes_DoesNotMatch o, collector + op = o.case_sensitive ? ' NOT LIKE ' : ' NOT ILIKE ' + collector = infix_value o, collector, op + if o.escape + collector << ' ESCAPE ' + visit o.escape, collector + else + collector + end + end + + def visit_Arel_Nodes_Regexp o, collector + op = o.case_sensitive ? ' ~ ' : ' ~* ' + infix_value o, collector, op + end + + def visit_Arel_Nodes_NotRegexp o, collector + op = o.case_sensitive ? ' !~ ' : ' !~* ' + infix_value o, collector, op + end + + def visit_Arel_Nodes_DistinctOn o, collector + collector << "DISTINCT ON ( " + visit(o.expr, collector) << " )" + end + + def visit_Arel_Nodes_BindParam o, collector + collector.add_bind(o.value) { |i| "$#{i}" } + end + + def visit_Arel_Nodes_GroupingElement o, collector + collector << "( " + visit(o.expr, collector) << " )" + end + + def visit_Arel_Nodes_Cube o, collector + collector << CUBE + grouping_array_or_grouping_element o, collector + end + + def visit_Arel_Nodes_RollUp o, collector + collector << ROLLUP + grouping_array_or_grouping_element o, collector + end + + def visit_Arel_Nodes_GroupingSet o, collector + collector << GROUPING_SET + grouping_array_or_grouping_element o, collector + end + + def visit_Arel_Nodes_Lateral o, collector + collector << LATERAL + collector << SPACE + grouping_parentheses o, collector + end + + # Used by Lateral visitor to enclose select queries in parentheses + def grouping_parentheses o, collector + if o.expr.is_a? Nodes::SelectStatement + collector << "(" + visit o.expr, collector + collector << ")" + else + visit o.expr, collector + end + end + + # Utilized by GroupingSet, Cube & RollUp visitors to + # handle grouping aggregation semantics + def grouping_array_or_grouping_element o, collector + if o.expr.is_a? Array + collector << "( " + visit o.expr, collector + collector << " )" + else + visit o.expr, collector + end + end + end + end +end diff --git a/activerecord/lib/arel/visitors/sqlite.rb b/activerecord/lib/arel/visitors/sqlite.rb new file mode 100644 index 0000000000..4ae093968b --- /dev/null +++ b/activerecord/lib/arel/visitors/sqlite.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +module Arel + module Visitors + class SQLite < Arel::Visitors::ToSql + private + + # Locks are not supported in SQLite + def visit_Arel_Nodes_Lock o, collector + collector + end + + def visit_Arel_Nodes_SelectStatement o, collector + o.limit = Arel::Nodes::Limit.new(-1) if o.offset && !o.limit + super + end + + def visit_Arel_Nodes_True o, collector + collector << "1" + end + + def visit_Arel_Nodes_False o, collector + collector << "0" + end + + end + end +end diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb new file mode 100644 index 0000000000..2b5c43b173 --- /dev/null +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -0,0 +1,846 @@ +# frozen_string_literal: true +module Arel + module Visitors + class UnsupportedVisitError < StandardError + def initialize(object) + super "Unsupported argument type: #{object.class.name}. Construct an Arel node instead." + end + end + + class ToSql < Arel::Visitors::Visitor + ## + # This is some roflscale crazy stuff. I'm roflscaling this because + # building SQL queries is a hotspot. I will explain the roflscale so that + # others will not rm this code. + # + # In YARV, string literals in a method body will get duped when the byte + # code is executed. Let's take a look: + # + # > puts RubyVM::InstructionSequence.new('def foo; "bar"; end').disasm + # + # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>===== + # 0000 trace 8 + # 0002 trace 1 + # 0004 putstring "bar" + # 0006 trace 16 + # 0008 leave + # + # The `putstring` bytecode will dup the string and push it on the stack. + # In many cases in our SQL visitor, that string is never mutated, so there + # is no need to dup the literal. + # + # If we change to a constant lookup, the string will not be duped, and we + # can reduce the objects in our system: + # + # > puts RubyVM::InstructionSequence.new('BAR = "bar"; def foo; BAR; end').disasm + # + # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>======== + # 0000 trace 8 + # 0002 trace 1 + # 0004 getinlinecache 11, <ic:0> + # 0007 getconstant :BAR + # 0009 setinlinecache <ic:0> + # 0011 trace 16 + # 0013 leave + # + # `getconstant` should be a hash lookup, and no object is duped when the + # value of the constant is pushed on the stack. Hence the crazy + # constants below. + # + # `matches` and `doesNotMatch` operate case-insensitively via Visitor subclasses + # specialized for specific databases when necessary. + # + + WHERE = ' WHERE ' # :nodoc: + SPACE = ' ' # :nodoc: + COMMA = ', ' # :nodoc: + GROUP_BY = ' GROUP BY ' # :nodoc: + ORDER_BY = ' ORDER BY ' # :nodoc: + WINDOW = ' WINDOW ' # :nodoc: + AND = ' AND ' # :nodoc: + + DISTINCT = 'DISTINCT' # :nodoc: + + def initialize connection + super() + @connection = connection + end + + def compile node, &block + accept(node, Arel::Collectors::SQLString.new, &block).value + end + + private + + def visit_Arel_Nodes_DeleteStatement o, collector + collector << 'DELETE FROM ' + collector = visit o.relation, collector + if o.wheres.any? + collector << WHERE + collector = inject_join o.wheres, collector, AND + end + + maybe_visit o.limit, collector + end + + # FIXME: we should probably have a 2-pass visitor for this + def build_subselect key, o + stmt = Nodes::SelectStatement.new + core = stmt.cores.first + core.froms = o.relation + core.wheres = o.wheres + core.projections = [key] + stmt.limit = o.limit + stmt.orders = o.orders + stmt + end + + def visit_Arel_Nodes_UpdateStatement o, collector + if o.orders.empty? && o.limit.nil? + wheres = o.wheres + else + wheres = [Nodes::In.new(o.key, [build_subselect(o.key, o)])] + end + + collector << "UPDATE " + collector = visit o.relation, collector + unless o.values.empty? + collector << " SET " + collector = inject_join o.values, collector, ", " + end + + unless wheres.empty? + collector << " WHERE " + collector = inject_join wheres, collector, " AND " + end + + collector + end + + def visit_Arel_Nodes_InsertStatement o, collector + collector << "INSERT INTO " + collector = visit o.relation, collector + if o.columns.any? + collector << " (#{o.columns.map { |x| + quote_column_name x.name + }.join ', '})" + end + + if o.values + maybe_visit o.values, collector + elsif o.select + maybe_visit o.select, collector + else + collector + end + end + + def visit_Arel_Nodes_Exists o, collector + collector << "EXISTS (" + collector = visit(o.expressions, collector) << ")" + if o.alias + collector << " AS " + visit o.alias, collector + else + collector + end + end + + def visit_Arel_Nodes_Casted o, collector + collector << quoted(o.val, o.attribute).to_s + end + + def visit_Arel_Nodes_Quoted o, collector + collector << quoted(o.expr, nil).to_s + end + + def visit_Arel_Nodes_True o, collector + collector << "TRUE" + end + + def visit_Arel_Nodes_False o, collector + collector << "FALSE" + end + + def visit_Arel_Nodes_ValuesList o, collector + collector << "VALUES " + + len = o.rows.length - 1 + o.rows.each_with_index { |row, i| + collector << '(' + row_len = row.length - 1 + row.each_with_index do |value, k| + case value + when Nodes::SqlLiteral, Nodes::BindParam + collector = visit(value, collector) + else + collector << quote(value) + end + collector << COMMA unless k == row_len + end + collector << ')' + collector << COMMA unless i == len + } + collector + end + + def visit_Arel_Nodes_Values o, collector + collector << "VALUES (" + + len = o.expressions.length - 1 + o.expressions.each_with_index { |value, i| + case value + when Nodes::SqlLiteral, Nodes::BindParam + collector = visit value, collector + else + collector << quote(value).to_s + end + unless i == len + collector << COMMA + end + } + + collector << ")" + end + + def visit_Arel_Nodes_SelectStatement o, collector + if o.with + collector = visit o.with, collector + collector << SPACE + end + + collector = o.cores.inject(collector) { |c,x| + visit_Arel_Nodes_SelectCore(x, c) + } + + unless o.orders.empty? + collector << ORDER_BY + len = o.orders.length - 1 + o.orders.each_with_index { |x, i| + collector = visit(x, collector) + collector << COMMA unless len == i + } + end + + visit_Arel_Nodes_SelectOptions(o, collector) + + collector + end + + def visit_Arel_Nodes_SelectOptions o, collector + collector = maybe_visit o.limit, collector + collector = maybe_visit o.offset, collector + collector = maybe_visit o.lock, collector + end + + def visit_Arel_Nodes_SelectCore o, collector + collector << "SELECT" + + collector = maybe_visit o.top, collector + + collector = maybe_visit o.set_quantifier, collector + + collect_nodes_for o.projections, collector, SPACE + + if o.source && !o.source.empty? + collector << " FROM " + collector = visit o.source, collector + end + + collect_nodes_for o.wheres, collector, WHERE, AND + collect_nodes_for o.groups, collector, GROUP_BY + unless o.havings.empty? + collector << " HAVING " + inject_join o.havings, collector, AND + end + collect_nodes_for o.windows, collector, WINDOW + + collector + end + + def collect_nodes_for nodes, collector, spacer, connector = COMMA + unless nodes.empty? + collector << spacer + len = nodes.length - 1 + nodes.each_with_index do |x, i| + collector = visit(x, collector) + collector << connector unless len == i + end + end + end + + def visit_Arel_Nodes_Bin o, collector + visit o.expr, collector + end + + def visit_Arel_Nodes_Distinct o, collector + collector << DISTINCT + end + + def visit_Arel_Nodes_DistinctOn o, collector + raise NotImplementedError, 'DISTINCT ON not implemented for this db' + end + + def visit_Arel_Nodes_With o, collector + collector << "WITH " + inject_join o.children, collector, COMMA + end + + def visit_Arel_Nodes_WithRecursive o, collector + collector << "WITH RECURSIVE " + inject_join o.children, collector, COMMA + end + + def visit_Arel_Nodes_Union o, collector + collector << "( " + infix_value(o, collector, " UNION ") << " )" + end + + def visit_Arel_Nodes_UnionAll o, collector + collector << "( " + infix_value(o, collector, " UNION ALL ") << " )" + end + + def visit_Arel_Nodes_Intersect o, collector + collector << "( " + infix_value(o, collector, " INTERSECT ") << " )" + end + + def visit_Arel_Nodes_Except o, collector + collector << "( " + infix_value(o, collector, " EXCEPT ") << " )" + end + + def visit_Arel_Nodes_NamedWindow o, collector + collector << quote_column_name(o.name) + collector << " AS " + visit_Arel_Nodes_Window o, collector + end + + def visit_Arel_Nodes_Window o, collector + collector << "(" + + if o.partitions.any? + collector << "PARTITION BY " + collector = inject_join o.partitions, collector, ", " + end + + if o.orders.any? + collector << SPACE if o.partitions.any? + collector << "ORDER BY " + collector = inject_join o.orders, collector, ", " + end + + if o.framing + collector << SPACE if o.partitions.any? or o.orders.any? + collector = visit o.framing, collector + end + + collector << ")" + end + + def visit_Arel_Nodes_Rows o, collector + if o.expr + collector << "ROWS " + visit o.expr, collector + else + collector << "ROWS" + end + end + + def visit_Arel_Nodes_Range o, collector + if o.expr + collector << "RANGE " + visit o.expr, collector + else + collector << "RANGE" + end + end + + def visit_Arel_Nodes_Preceding o, collector + collector = if o.expr + visit o.expr, collector + else + collector << "UNBOUNDED" + end + + collector << " PRECEDING" + end + + def visit_Arel_Nodes_Following o, collector + collector = if o.expr + visit o.expr, collector + else + collector << "UNBOUNDED" + end + + collector << " FOLLOWING" + end + + def visit_Arel_Nodes_CurrentRow o, collector + collector << "CURRENT ROW" + end + + def visit_Arel_Nodes_Over o, collector + case o.right + when nil + visit(o.left, collector) << " OVER ()" + when Arel::Nodes::SqlLiteral + infix_value o, collector, " OVER " + when String, Symbol + visit(o.left, collector) << " OVER #{quote_column_name o.right.to_s}" + else + infix_value o, collector, " OVER " + end + end + + def visit_Arel_Nodes_Offset o, collector + collector << "OFFSET " + visit o.expr, collector + end + + def visit_Arel_Nodes_Limit o, collector + collector << "LIMIT " + visit o.expr, collector + end + + # FIXME: this does nothing on most databases, but does on MSSQL + def visit_Arel_Nodes_Top o, collector + collector + end + + def visit_Arel_Nodes_Lock o, collector + visit o.expr, collector + end + + def visit_Arel_Nodes_Grouping o, collector + if o.expr.is_a? Nodes::Grouping + visit(o.expr, collector) + else + collector << "(" + visit(o.expr, collector) << ")" + end + end + + def visit_Arel_SelectManager o, collector + collector << '(' + visit(o.ast, collector) << ')' + end + + def visit_Arel_Nodes_Ascending o, collector + visit(o.expr, collector) << " ASC" + end + + def visit_Arel_Nodes_Descending o, collector + visit(o.expr, collector) << " DESC" + end + + def visit_Arel_Nodes_Group o, collector + visit o.expr, collector + end + + def visit_Arel_Nodes_NamedFunction o, collector + collector << o.name + collector << "(" + collector << "DISTINCT " if o.distinct + collector = inject_join(o.expressions, collector, ", ") << ")" + if o.alias + collector << " AS " + visit o.alias, collector + else + collector + end + end + + def visit_Arel_Nodes_Extract o, collector + collector << "EXTRACT(#{o.field.to_s.upcase} FROM " + visit(o.expr, collector) << ")" + end + + def visit_Arel_Nodes_Count o, collector + aggregate "COUNT", o, collector + end + + def visit_Arel_Nodes_Sum o, collector + aggregate "SUM", o, collector + end + + def visit_Arel_Nodes_Max o, collector + aggregate "MAX", o, collector + end + + def visit_Arel_Nodes_Min o, collector + aggregate "MIN", o, collector + end + + def visit_Arel_Nodes_Avg o, collector + aggregate "AVG", o, collector + end + + def visit_Arel_Nodes_TableAlias o, collector + collector = visit o.relation, collector + collector << " " + collector << quote_table_name(o.name) + end + + def visit_Arel_Nodes_Between o, collector + collector = visit o.left, collector + collector << " BETWEEN " + visit o.right, collector + end + + def visit_Arel_Nodes_GreaterThanOrEqual o, collector + collector = visit o.left, collector + collector << " >= " + visit o.right, collector + end + + def visit_Arel_Nodes_GreaterThan o, collector + collector = visit o.left, collector + collector << " > " + visit o.right, collector + end + + def visit_Arel_Nodes_LessThanOrEqual o, collector + collector = visit o.left, collector + collector << " <= " + visit o.right, collector + end + + def visit_Arel_Nodes_LessThan o, collector + collector = visit o.left, collector + collector << " < " + visit o.right, collector + end + + def visit_Arel_Nodes_Matches o, collector + collector = visit o.left, collector + collector << " LIKE " + collector = visit o.right, collector + if o.escape + collector << ' ESCAPE ' + visit o.escape, collector + else + collector + end + end + + def visit_Arel_Nodes_DoesNotMatch o, collector + collector = visit o.left, collector + collector << " NOT LIKE " + collector = visit o.right, collector + if o.escape + collector << ' ESCAPE ' + visit o.escape, collector + else + collector + end + end + + def visit_Arel_Nodes_JoinSource o, collector + if o.left + collector = visit o.left, collector + end + if o.right.any? + collector << SPACE if o.left + collector = inject_join o.right, collector, SPACE + end + collector + end + + def visit_Arel_Nodes_Regexp o, collector + raise NotImplementedError, '~ not implemented for this db' + end + + def visit_Arel_Nodes_NotRegexp o, collector + raise NotImplementedError, '!~ not implemented for this db' + end + + def visit_Arel_Nodes_StringJoin o, collector + visit o.left, collector + end + + def visit_Arel_Nodes_FullOuterJoin o, collector + collector << "FULL OUTER JOIN " + collector = visit o.left, collector + collector << SPACE + visit o.right, collector + end + + def visit_Arel_Nodes_OuterJoin o, collector + collector << "LEFT OUTER JOIN " + collector = visit o.left, collector + collector << " " + visit o.right, collector + end + + def visit_Arel_Nodes_RightOuterJoin o, collector + collector << "RIGHT OUTER JOIN " + collector = visit o.left, collector + collector << SPACE + visit o.right, collector + end + + def visit_Arel_Nodes_InnerJoin o, collector + collector << "INNER JOIN " + collector = visit o.left, collector + if o.right + collector << SPACE + visit(o.right, collector) + else + collector + end + end + + def visit_Arel_Nodes_On o, collector + collector << "ON " + visit o.expr, collector + end + + def visit_Arel_Nodes_Not o, collector + collector << "NOT (" + visit(o.expr, collector) << ")" + end + + def visit_Arel_Table o, collector + if o.table_alias + collector << "#{quote_table_name o.name} #{quote_table_name o.table_alias}" + else + collector << quote_table_name(o.name) + end + end + + def visit_Arel_Nodes_In o, collector + if Array === o.right && o.right.empty? + collector << '1=0' + else + collector = visit o.left, collector + collector << " IN (" + visit(o.right, collector) << ")" + end + end + + def visit_Arel_Nodes_NotIn o, collector + if Array === o.right && o.right.empty? + collector << '1=1' + else + collector = visit o.left, collector + collector << " NOT IN (" + collector = visit o.right, collector + collector << ")" + end + end + + def visit_Arel_Nodes_And o, collector + inject_join o.children, collector, " AND " + end + + def visit_Arel_Nodes_Or o, collector + collector = visit o.left, collector + collector << " OR " + visit o.right, collector + end + + def visit_Arel_Nodes_Assignment o, collector + case o.right + when Arel::Nodes::UnqualifiedColumn, Arel::Attributes::Attribute, Arel::Nodes::BindParam + collector = visit o.left, collector + collector << " = " + visit o.right, collector + else + collector = visit o.left, collector + collector << " = " + collector << quote(o.right).to_s + end + end + + def visit_Arel_Nodes_Equality o, collector + right = o.right + + collector = visit o.left, collector + + if right.nil? + collector << " IS NULL" + else + collector << " = " + visit right, collector + end + end + + def visit_Arel_Nodes_NotEqual o, collector + right = o.right + + collector = visit o.left, collector + + if right.nil? + collector << " IS NOT NULL" + else + collector << " != " + visit right, collector + end + end + + def visit_Arel_Nodes_As o, collector + collector = visit o.left, collector + collector << " AS " + visit o.right, collector + end + + def visit_Arel_Nodes_Case o, collector + collector << "CASE " + if o.case + visit o.case, collector + collector << " " + end + o.conditions.each do |condition| + visit condition, collector + collector << " " + end + if o.default + visit o.default, collector + collector << " " + end + collector << "END" + end + + def visit_Arel_Nodes_When o, collector + collector << "WHEN " + visit o.left, collector + collector << " THEN " + visit o.right, collector + end + + def visit_Arel_Nodes_Else o, collector + collector << "ELSE " + visit o.expr, collector + end + + def visit_Arel_Nodes_UnqualifiedColumn o, collector + collector << "#{quote_column_name o.name}" + collector + end + + def visit_Arel_Attributes_Attribute o, collector + join_name = o.relation.table_alias || o.relation.name + collector << "#{quote_table_name join_name}.#{quote_column_name o.name}" + end + alias :visit_Arel_Attributes_Integer :visit_Arel_Attributes_Attribute + alias :visit_Arel_Attributes_Float :visit_Arel_Attributes_Attribute + alias :visit_Arel_Attributes_Decimal :visit_Arel_Attributes_Attribute + alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute + alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute + alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute + + def literal o, collector; collector << o.to_s; end + + def visit_Arel_Nodes_BindParam o, collector + collector.add_bind(o.value) { "?" } + end + + alias :visit_Arel_Nodes_SqlLiteral :literal + alias :visit_Bignum :literal + alias :visit_Fixnum :literal + alias :visit_Integer :literal + + def quoted o, a + if a && a.able_to_type_cast? + quote(a.type_cast_for_database(o)) + else + quote(o) + end + end + + def unsupported o, collector + raise UnsupportedVisitError.new(o) + end + + alias :visit_ActiveSupport_Multibyte_Chars :unsupported + alias :visit_ActiveSupport_StringInquirer :unsupported + alias :visit_BigDecimal :unsupported + alias :visit_Class :unsupported + alias :visit_Date :unsupported + alias :visit_DateTime :unsupported + alias :visit_FalseClass :unsupported + alias :visit_Float :unsupported + alias :visit_Hash :unsupported + alias :visit_NilClass :unsupported + alias :visit_String :unsupported + alias :visit_Symbol :unsupported + alias :visit_Time :unsupported + alias :visit_TrueClass :unsupported + + def visit_Arel_Nodes_InfixOperation o, collector + collector = visit o.left, collector + collector << " #{o.operator} " + visit o.right, collector + end + + alias :visit_Arel_Nodes_Addition :visit_Arel_Nodes_InfixOperation + alias :visit_Arel_Nodes_Subtraction :visit_Arel_Nodes_InfixOperation + alias :visit_Arel_Nodes_Multiplication :visit_Arel_Nodes_InfixOperation + alias :visit_Arel_Nodes_Division :visit_Arel_Nodes_InfixOperation + + def visit_Arel_Nodes_UnaryOperation o, collector + collector << " #{o.operator} " + visit o.expr, collector + end + + def visit_Array o, collector + inject_join o, collector, ", " + end + alias :visit_Set :visit_Array + + def quote value + return value if Arel::Nodes::SqlLiteral === value + @connection.quote value + end + + def quote_table_name name + return name if Arel::Nodes::SqlLiteral === name + @connection.quote_table_name(name) + end + + def quote_column_name name + return name if Arel::Nodes::SqlLiteral === name + @connection.quote_column_name(name) + end + + def maybe_visit thing, collector + return collector unless thing + collector << " " + visit thing, collector + end + + def inject_join list, collector, join_str + len = list.length - 1 + list.each_with_index.inject(collector) { |c, (x,i)| + if i == len + visit x, c + else + visit(x, c) << join_str + end + } + end + + def infix_value o, collector, value + collector = visit o.left, collector + collector << value + visit o.right, collector + end + + def aggregate name, o, collector + collector << "#{name}(" + if o.distinct + collector << "DISTINCT " + end + collector = inject_join(o.expressions, collector, ", ") << ")" + if o.alias + collector << " AS " + visit o.alias, collector + else + collector + end + end + end + end +end diff --git a/activerecord/lib/arel/visitors/visitor.rb b/activerecord/lib/arel/visitors/visitor.rb new file mode 100644 index 0000000000..f156be9a0a --- /dev/null +++ b/activerecord/lib/arel/visitors/visitor.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true +module Arel + module Visitors + class Visitor + def initialize + @dispatch = get_dispatch_cache + end + + def accept object, *args + visit object, *args + end + + private + + attr_reader :dispatch + + def self.dispatch_cache + Hash.new do |hash, klass| + hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}" + end + end + + def get_dispatch_cache + self.class.dispatch_cache + end + + def visit object, *args + dispatch_method = dispatch[object.class] + send dispatch_method, object, *args + rescue NoMethodError => e + raise e if respond_to?(dispatch_method, true) + superklass = object.class.ancestors.find { |klass| + respond_to?(dispatch[klass], true) + } + raise(TypeError, "Cannot visit #{object.class}") unless superklass + dispatch[object.class] = dispatch[superklass] + retry + end + end + end +end diff --git a/activerecord/lib/arel/visitors/where_sql.rb b/activerecord/lib/arel/visitors/where_sql.rb new file mode 100644 index 0000000000..55e6ca9a21 --- /dev/null +++ b/activerecord/lib/arel/visitors/where_sql.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +module Arel + module Visitors + class WhereSql < Arel::Visitors::ToSql + def initialize(inner_visitor, *args, &block) + @inner_visitor = inner_visitor + super(*args, &block) + end + + private + + def visit_Arel_Nodes_SelectCore o, collector + collector << "WHERE " + wheres = o.wheres.map do |where| + Nodes::SqlLiteral.new(@inner_visitor.accept(where, collector.class.new).value) + end + + inject_join wheres, collector, ' AND ' + end + end + end +end |