diff options
Diffstat (limited to 'lib/arel')
33 files changed, 461 insertions, 238 deletions
diff --git a/lib/arel/attributes/attribute.rb b/lib/arel/attributes/attribute.rb index 0906fa4f1d..cda5a5a3db 100644 --- a/lib/arel/attributes/attribute.rb +++ b/lib/arel/attributes/attribute.rb @@ -12,6 +12,14 @@ module Arel def lower relation.lower self end + + def type_cast_for_database(value) + relation.type_cast_for_database(name, value) + end + + def able_to_type_cast? + relation.able_to_type_cast? + end end class String < Attribute; end diff --git a/lib/arel/collectors/sql_string.rb b/lib/arel/collectors/sql_string.rb index 8ca89ca7bd..fd2faaef3a 100644 --- a/lib/arel/collectors/sql_string.rb +++ b/lib/arel/collectors/sql_string.rb @@ -5,8 +5,14 @@ require 'arel/collectors/plain_string' module Arel module Collectors class SQLString < PlainString + def initialize(*) + super + @bind_index = 1 + end + def add_bind bind - self << bind.to_s + self << yield(@bind_index) + @bind_index += 1 self end diff --git a/lib/arel/crud.rb b/lib/arel/crud.rb index 6f4962cbfe..d310c7381f 100644 --- a/lib/arel/crud.rb +++ b/lib/arel/crud.rb @@ -3,7 +3,7 @@ module Arel # FIXME hopefully we can remove this module Crud def compile_update values, pk - um = UpdateManager.new @engine + um = UpdateManager.new if Nodes::SqlLiteral === values relation = @ctx.from @@ -26,11 +26,12 @@ module Arel end def create_insert - InsertManager.new @engine + InsertManager.new end def compile_delete - dm = DeleteManager.new @engine + dm = DeleteManager.new + dm.take @ast.limit.expr if @ast.limit dm.wheres = @ctx.wheres dm.from @ctx.froms dm diff --git a/lib/arel/delete_manager.rb b/lib/arel/delete_manager.rb index b4c61f708f..20e988e01f 100644 --- a/lib/arel/delete_manager.rb +++ b/lib/arel/delete_manager.rb @@ -1,6 +1,6 @@ module Arel class DeleteManager < Arel::TreeManager - def initialize engine + def initialize super @ast = Nodes::DeleteStatement.new @ctx = @ast @@ -11,6 +11,11 @@ module Arel self end + def take limit + @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit + self + end + def wheres= list @ast.wheres = list end diff --git a/lib/arel/insert_manager.rb b/lib/arel/insert_manager.rb index 8839dd8181..7829c3f4f9 100644 --- a/lib/arel/insert_manager.rb +++ b/lib/arel/insert_manager.rb @@ -1,6 +1,6 @@ module Arel class InsertManager < Arel::TreeManager - def initialize engine + def initialize super @ast = Nodes::InsertStatement.new end diff --git a/lib/arel/nodes.rb b/lib/arel/nodes.rb index ccccd471e2..0e66d2dd0c 100644 --- a/lib/arel/nodes.rb +++ b/lib/arel/nodes.rb @@ -4,6 +4,7 @@ require 'arel/nodes/select_statement' require 'arel/nodes/select_core' require 'arel/nodes/insert_statement' require 'arel/nodes/update_statement' +require 'arel/nodes/bind_param' # terminal @@ -29,6 +30,7 @@ require 'arel/nodes/table_alias' require 'arel/nodes/infix_operation' require 'arel/nodes/over' require 'arel/nodes/matches' +require 'arel/nodes/regexp' # nary require 'arel/nodes/and' @@ -54,41 +56,4 @@ require 'arel/nodes/string_join' require 'arel/nodes/sql_literal' -module Arel - module Nodes - class Casted < Arel::Nodes::Node # :nodoc: - attr_reader :val, :attribute - def initialize val, attribute - @val = val - @attribute = attribute - super() - end - - def nil?; @val.nil?; end - - def eql? other - self.class == other.class && - self.val == other.val && - self.attribute == other.attribute - end - alias :== :eql? - end - - class Quoted < Arel::Nodes::Unary # :nodoc: - end - - def self.build_quoted other, attribute = nil - case other - when Arel::Nodes::Node, Arel::Attributes::Attribute, Arel::Table, Arel::Nodes::BindParam, Arel::SelectManager - other - else - case attribute - when Arel::Attributes::Attribute - Casted.new other, attribute - else - Quoted.new other - end - end - end - end -end +require 'arel/nodes/casted' diff --git a/lib/arel/nodes/binary.rb b/lib/arel/nodes/binary.rb index e35d2fd2e7..763091c267 100644 --- a/lib/arel/nodes/binary.rb +++ b/lib/arel/nodes/binary.rb @@ -16,7 +16,7 @@ module Arel end def hash - [@left, @right].hash + [self.class, @left, @right].hash end def eql? other @@ -38,9 +38,7 @@ module Arel LessThanOrEqual NotEqual NotIn - NotRegexp Or - Regexp Union UnionAll Intersect diff --git a/lib/arel/nodes/bind_param.rb b/lib/arel/nodes/bind_param.rb new file mode 100644 index 0000000000..3a4aedc4ba --- /dev/null +++ b/lib/arel/nodes/bind_param.rb @@ -0,0 +1,9 @@ +module Arel + module Nodes + class BindParam < Node + def ==(other) + other.is_a?(BindParam) + end + end + end +end diff --git a/lib/arel/nodes/casted.rb b/lib/arel/nodes/casted.rb new file mode 100644 index 0000000000..9fa02955ef --- /dev/null +++ b/lib/arel/nodes/casted.rb @@ -0,0 +1,40 @@ +module Arel + module Nodes + class Casted < Arel::Nodes::Node # :nodoc: + attr_reader :val, :attribute + def initialize val, attribute + @val = val + @attribute = attribute + super() + end + + def nil?; @val.nil?; end + + def eql? other + self.class == other.class && + self.val == other.val && + self.attribute == other.attribute + end + alias :== :eql? + end + + class Quoted < Arel::Nodes::Unary # :nodoc: + alias :val :value + def nil?; val.nil?; end + end + + def self.build_quoted other, attribute = nil + case other + when Arel::Nodes::Node, Arel::Attributes::Attribute, Arel::Table, Arel::Nodes::BindParam, Arel::SelectManager, Arel::Nodes::Quoted + other + else + case attribute + when Arel::Attributes::Attribute + Casted.new other, attribute + else + Quoted.new other + end + end + end + end +end diff --git a/lib/arel/nodes/delete_statement.rb b/lib/arel/nodes/delete_statement.rb index 3bac8225ec..8aaf8ca0b6 100644 --- a/lib/arel/nodes/delete_statement.rb +++ b/lib/arel/nodes/delete_statement.rb @@ -1,6 +1,8 @@ module Arel module Nodes class DeleteStatement < Arel::Nodes::Binary + attr_accessor :limit + alias :relation :left alias :relation= :left= alias :wheres :right diff --git a/lib/arel/nodes/function.rb b/lib/arel/nodes/function.rb index 733a00df46..182dfa7329 100644 --- a/lib/arel/nodes/function.rb +++ b/lib/arel/nodes/function.rb @@ -3,6 +3,7 @@ module Arel class Function < Arel::Nodes::Node include Arel::Predications include Arel::WindowPredications + include Arel::OrderPredications attr_accessor :expressions, :alias, :distinct def initialize expr, aliaz = nil diff --git a/lib/arel/nodes/matches.rb b/lib/arel/nodes/matches.rb index 583fb97c9b..0d9c1925dc 100644 --- a/lib/arel/nodes/matches.rb +++ b/lib/arel/nodes/matches.rb @@ -2,10 +2,12 @@ module Arel module Nodes class Matches < Binary attr_reader :escape + attr_accessor :case_sensitive - def initialize(left, right, escape = nil) + def initialize(left, right, escape = nil, case_sensitive = false) super(left, right) @escape = escape && Nodes.build_quoted(escape) + @case_sensitive = case_sensitive end end diff --git a/lib/arel/nodes/regexp.rb b/lib/arel/nodes/regexp.rb new file mode 100644 index 0000000000..784368f5bf --- /dev/null +++ b/lib/arel/nodes/regexp.rb @@ -0,0 +1,14 @@ +module Arel + module Nodes + class Regexp < Binary + attr_accessor :case_sensitive + + def initialize(left, right, case_sensitive = true) + super(left, right) + @case_sensitive = case_sensitive + end + end + + class NotRegexp < Regexp; end + end +end diff --git a/lib/arel/nodes/select_core.rb b/lib/arel/nodes/select_core.rb index 09ae420aa1..3696dd20af 100644 --- a/lib/arel/nodes/select_core.rb +++ b/lib/arel/nodes/select_core.rb @@ -2,7 +2,7 @@ module Arel module Nodes class SelectCore < Arel::Nodes::Node attr_accessor :top, :projections, :wheres, :groups, :windows - attr_accessor :having, :source, :set_quantifier + attr_accessor :havings, :source, :set_quantifier def initialize super() @@ -14,7 +14,7 @@ module Arel @projections = [] @wheres = [] @groups = [] - @having = nil + @havings = [] @windows = [] end @@ -35,14 +35,14 @@ module Arel @projections = @projections.clone @wheres = @wheres.clone @groups = @groups.clone - @having = @having.clone if @having + @havings = @havings.clone @windows = @windows.clone end def hash [ @source, @top, @set_quantifier, @projections, - @wheres, @groups, @having, @windows + @wheres, @groups, @havings, @windows ].hash end @@ -54,7 +54,7 @@ module Arel self.projections == other.projections && self.wheres == other.wheres && self.groups == other.groups && - self.having == other.having && + self.havings == other.havings && self.windows == other.windows end alias :== :eql? diff --git a/lib/arel/nodes/sql_literal.rb b/lib/arel/nodes/sql_literal.rb index b43288b29c..2c56644b99 100644 --- a/lib/arel/nodes/sql_literal.rb +++ b/lib/arel/nodes/sql_literal.rb @@ -10,8 +10,5 @@ module Arel coder.scalar = self.to_s end end - - class BindParam < SqlLiteral - end end end diff --git a/lib/arel/nodes/table_alias.rb b/lib/arel/nodes/table_alias.rb index ebfcb58e64..a5adc0766a 100644 --- a/lib/arel/nodes/table_alias.rb +++ b/lib/arel/nodes/table_alias.rb @@ -13,8 +13,12 @@ module Arel relation.respond_to?(:name) ? relation.name : name end - def engine - relation.engine + def type_cast_for_database(*args) + relation.type_cast_for_database(*args) + end + + def able_to_type_cast? + relation.respond_to?(:able_to_type_cast?) && relation.able_to_type_cast? end end end diff --git a/lib/arel/nodes/unary.rb b/lib/arel/nodes/unary.rb index 3d4a4b014a..a0062ff5be 100644 --- a/lib/arel/nodes/unary.rb +++ b/lib/arel/nodes/unary.rb @@ -23,7 +23,6 @@ module Arel %w{ Bin Group - Having Limit Not Offset diff --git a/lib/arel/predications.rb b/lib/arel/predications.rb index 3050526a43..1d2b0de235 100644 --- a/lib/arel/predications.rb +++ b/lib/arel/predications.rb @@ -1,7 +1,7 @@ module Arel module Predications def not_eq other - Nodes::NotEqual.new self, Nodes.build_quoted(other, self) + Nodes::NotEqual.new self, quoted_node(other) end def not_eq_any others @@ -13,7 +13,7 @@ module Arel end def eq other - Nodes::Equality.new self, Nodes.build_quoted(other, self) + Nodes::Equality.new self, quoted_node(other) end def eq_any others @@ -21,7 +21,27 @@ module Arel end def eq_all others - grouping_all :eq, others.map { |x| Nodes.build_quoted(x, self) } + grouping_all :eq, quoted_array(others) + end + + def between other + if equals_quoted?(other.begin, -Float::INFINITY) + if equals_quoted?(other.end, Float::INFINITY) + not_in([]) + elsif other.exclude_end? + lt(other.end) + else + lteq(other.end) + end + elsif equals_quoted?(other.end, Float::INFINITY) + gteq(other.begin) + elsif other.exclude_end? + gteq(other.begin).and(lt(other.end)) + else + left = quoted_node(other.begin) + right = quoted_node(other.end) + Nodes::Between.new(self, left.and(right)) + end end def in other @@ -29,27 +49,16 @@ module Arel when Arel::SelectManager Arel::Nodes::In.new(self, other.ast) when Range - if other.begin == -Float::INFINITY - if other.end == Float::INFINITY - Nodes::NotIn.new self, [] - elsif other.exclude_end? - Nodes::LessThan.new(self, Nodes.build_quoted(other.end, self)) - else - Nodes::LessThanOrEqual.new(self, Nodes.build_quoted(other.end, self)) - end - elsif other.end == Float::INFINITY - Nodes::GreaterThanOrEqual.new(self, Nodes.build_quoted(other.begin, self)) - elsif other.exclude_end? - left = Nodes::GreaterThanOrEqual.new(self, Nodes.build_quoted(other.begin, self)) - right = Nodes::LessThan.new(self, Nodes.build_quoted(other.end, self)) - Nodes::And.new [left, right] - else - Nodes::Between.new(self, Nodes::And.new([Nodes.build_quoted(other.begin, self), Nodes.build_quoted(other.end, self)])) + if $VERBOSE + warn <<-eowarn +Passing a range to `#in` is deprecated. Call `#between`, instead. + eowarn end - when Array - Nodes::In.new self, other.map { |x| Nodes.build_quoted(x, self) } + between(other) + when Enumerable + Nodes::In.new self, quoted_array(other) else - Nodes::In.new self, Nodes.build_quoted(other, self) + Nodes::In.new self, quoted_node(other) end end @@ -61,34 +70,43 @@ module Arel grouping_all :in, others end + def not_between other + if equals_quoted?(other.begin, -Float::INFINITY) + if equals_quoted?(other.end, Float::INFINITY) + self.in([]) + elsif other.exclude_end? + gteq(other.end) + else + gt(other.end) + end + elsif equals_quoted?(other.end, Float::INFINITY) + lt(other.begin) + else + left = lt(other.begin) + right = if other.exclude_end? + gteq(other.end) + else + gt(other.end) + end + left.or(right) + end + end + def not_in other case other when Arel::SelectManager Arel::Nodes::NotIn.new(self, other.ast) when Range - if other.begin == -Float::INFINITY # The range begins with negative infinity - if other.end == Float::INFINITY - Nodes::In.new self, [] # The range is infinite, so return an empty range - elsif other.exclude_end? - Nodes::GreaterThanOrEqual.new(self, Nodes.build_quoted(other.end, self)) - else - Nodes::GreaterThan.new(self, Nodes.build_quoted(other.end, self)) - end - elsif other.end == Float::INFINITY - Nodes::LessThan.new(self, Nodes.build_quoted(other.begin, self)) - else - left = Nodes::LessThan.new(self, Nodes.build_quoted(other.begin, self)) - if other.exclude_end? - right = Nodes::GreaterThanOrEqual.new(self, Nodes.build_quoted(other.end, self)) - else - right = Nodes::GreaterThan.new(self, Nodes.build_quoted(other.end, self)) - end - Nodes::Or.new left, right + if $VERBOSE + warn <<-eowarn +Passing a range to `#not_in` is deprecated. Call `#not_between`, instead. + eowarn end - when Array - Nodes::NotIn.new self, other.map { |x| Nodes.build_quoted(x, self) } + not_between(other) + when Enumerable + Nodes::NotIn.new self, quoted_array(other) else - Nodes::NotIn.new self, Nodes.build_quoted(other, self) + Nodes::NotIn.new self, quoted_node(other) end end @@ -100,20 +118,28 @@ module Arel grouping_all :not_in, others end - def matches other, escape = nil - Nodes::Matches.new self, Nodes.build_quoted(other, self), escape + def matches other, escape = nil, case_sensitive = false + Nodes::Matches.new self, quoted_node(other), escape, case_sensitive + end + + def matches_regexp other, case_sensitive = true + Nodes::Regexp.new self, quoted_node(other), case_sensitive + end + + def matches_any others, escape = nil, case_sensitive = false + grouping_any :matches, others, escape, case_sensitive end - def matches_any others, escape = nil - grouping_any :matches, others, escape + def matches_all others, escape = nil, case_sensitive = false + grouping_all :matches, others, escape, case_sensitive end - def matches_all others, escape = nil - grouping_all :matches, others, escape + def does_not_match other, escape = nil, case_sensitive = false + Nodes::DoesNotMatch.new self, quoted_node(other), escape, case_sensitive end - def does_not_match other, escape = nil - Nodes::DoesNotMatch.new self, Nodes.build_quoted(other, self), escape + def does_not_match_regexp other, case_sensitive = true + Nodes::NotRegexp.new self, quoted_node(other), case_sensitive end def does_not_match_any others, escape = nil @@ -125,7 +151,7 @@ module Arel end def gteq right - Nodes::GreaterThanOrEqual.new self, Nodes.build_quoted(right, self) + Nodes::GreaterThanOrEqual.new self, quoted_node(right) end def gteq_any others @@ -137,7 +163,7 @@ module Arel end def gt right - Nodes::GreaterThan.new self, Nodes.build_quoted(right, self) + Nodes::GreaterThan.new self, quoted_node(right) end def gt_any others @@ -149,7 +175,7 @@ module Arel end def lt right - Nodes::LessThan.new self, Nodes.build_quoted(right, self) + Nodes::LessThan.new self, quoted_node(right) end def lt_any others @@ -161,7 +187,7 @@ module Arel end def lteq right - Nodes::LessThanOrEqual.new self, Nodes.build_quoted(right, self) + Nodes::LessThanOrEqual.new self, quoted_node(right) end def lteq_any others @@ -185,5 +211,21 @@ module Arel nodes = others.map {|expr| send(method_id, expr, *extras)} Nodes::Grouping.new Nodes::And.new(nodes) end + + def quoted_node(other) + Nodes.build_quoted(other, self) + end + + def quoted_array(others) + others.map { |v| quoted_node(v) } + end + + def equals_quoted?(maybe_quoted, value) + if maybe_quoted.is_a?(Nodes::Quoted) + maybe_quoted.val == value + else + maybe_quoted == value + end + end end end diff --git a/lib/arel/select_manager.rb b/lib/arel/select_manager.rb index 5a05e7e181..f7dec87ca3 100644 --- a/lib/arel/select_manager.rb +++ b/lib/arel/select_manager.rb @@ -6,8 +6,8 @@ module Arel STRING_OR_SYMBOL_CLASS = [Symbol, String] - def initialize engine, table = nil - super(engine) + def initialize table = nil + super() @ast = Nodes::SelectStatement.new @ctx = @ast.cores.last from table @@ -19,7 +19,7 @@ module Arel end def limit - @ast.limit && @ast.limit.expr.expr + @ast.limit && @ast.limit.expr end alias :taken :limit @@ -118,8 +118,8 @@ module Arel join(relation, Nodes::OuterJoin) end - def having *exprs - @ctx.having = Nodes::Having.new(collapse(exprs, @ctx.having)) + def having expr + @ctx.havings << expr self end @@ -176,10 +176,10 @@ module Arel @ast.orders end - def where_sql + def where_sql engine = Table.engine return if @ctx.wheres.empty? - viz = Visitors::WhereSql.new @engine.connection + viz = Visitors::WhereSql.new engine.connection Nodes::SqlLiteral.new viz.accept(@ctx, Collectors::SQLString.new).value end @@ -216,8 +216,8 @@ module Arel def take limit if limit - @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) - @ctx.top = Nodes::Top.new(Nodes.build_quoted(limit)) + @ast.limit = Nodes::Limit.new(limit) + @ctx.top = Nodes::Top.new(limit) else @ast.limit = nil @ctx.top = nil diff --git a/lib/arel/table.rb b/lib/arel/table.rb index 01d4561ff1..b4b4a861b8 100644 --- a/lib/arel/table.rb +++ b/lib/arel/table.rb @@ -6,40 +6,24 @@ module Arel @engine = nil class << self; attr_accessor :engine; end - attr_accessor :name, :engine, :aliases, :table_alias + attr_accessor :name, :aliases, :table_alias # TableAlias and Table both have a #table_name which is the name of the underlying table alias :table_name :name - def initialize name, engine = Table.engine + def initialize(name, as: nil, type_caster: nil) @name = name.to_s - @engine = engine @columns = nil @aliases = [] - @table_alias = nil - @primary_key = nil + @type_caster = type_caster - if Hash === engine - @engine = engine[:engine] || Table.engine - - # Sometime AR sends an :as parameter to table, to let the table know - # that it is an Alias. We may want to override new, and return a - # TableAlias node? - @table_alias = engine[:as] unless engine[:as].to_s == @name - end - end - - def primary_key - if $VERBOSE - warn <<-eowarn -primary_key (#{caller.first}) is deprecated and will be removed in Arel 4.0.0 - eowarn - end - @primary_key ||= begin - primary_key_name = @engine.connection.primary_key(name) - # some tables might be without primary key - primary_key_name && self[primary_key_name] + # Sometime AR sends an :as parameter to table, to let the table know + # that it is an Alias. We may want to override new, and return a + # TableAlias node? + if as.to_s == @name + as = nil end + @table_alias = as end def alias name = "#{self.name}_2" @@ -48,12 +32,12 @@ primary_key (#{caller.first}) is deprecated and will be removed in Arel 4.0.0 end end - def from table - SelectManager.new(@engine, table) + def from + SelectManager.new(self) end def join relation, klass = Nodes::InnerJoin - return from(self) unless relation + return from unless relation case relation when String, Nodes::SqlLiteral @@ -61,7 +45,7 @@ primary_key (#{caller.first}) is deprecated and will be removed in Arel 4.0.0 klass = Nodes::StringJoin end - from(self).join(relation, klass) + from.join(relation, klass) end def outer_join relation @@ -69,55 +53,39 @@ primary_key (#{caller.first}) is deprecated and will be removed in Arel 4.0.0 end def group *columns - from(self).group(*columns) + from.group(*columns) end def order *expr - from(self).order(*expr) + from.order(*expr) end def where condition - from(self).where condition + from.where condition end def project *things - from(self).project(*things) + from.project(*things) end def take amount - from(self).take amount + from.take amount end def skip amount - from(self).skip amount + from.skip amount end def having expr - from(self).having expr + from.having expr end def [] name ::Arel::Attribute.new self, name end - def select_manager - SelectManager.new(@engine) - end - - def insert_manager - InsertManager.new(@engine) - end - - def update_manager - UpdateManager.new(@engine) - end - - def delete_manager - DeleteManager.new(@engine) - end - def hash - # Perf note: aliases, table alias and engine is excluded from the hash + # Perf note: aliases and table alias is excluded from the hash # aliases can have a loop back to this table breaking hashes in parent # relations, for the vast majority of cases @name is unique to a query @name.hash @@ -126,12 +94,23 @@ primary_key (#{caller.first}) is deprecated and will be removed in Arel 4.0.0 def eql? other self.class == other.class && self.name == other.name && - self.engine == other.engine && self.aliases == other.aliases && self.table_alias == other.table_alias end alias :== :eql? + def type_cast_for_database(attribute_name, value) + type_caster.type_cast_for_database(attribute_name, value) + end + + def able_to_type_cast? + !type_caster.nil? + end + + protected + + attr_reader :type_caster + private def attributes_for columns diff --git a/lib/arel/tree_manager.rb b/lib/arel/tree_manager.rb index 8bff97af78..5278ab06a1 100644 --- a/lib/arel/tree_manager.rb +++ b/lib/arel/tree_manager.rb @@ -8,8 +8,7 @@ module Arel attr_accessor :bind_values - def initialize engine - @engine = engine + def initialize @ctx = nil @bind_values = [] end @@ -20,13 +19,9 @@ module Arel collector.value end - def visitor - engine.connection.visitor - end - - def to_sql + def to_sql engine = Table.engine collector = Arel::Collectors::SQLString.new - collector = visitor.accept @ast, collector + collector = engine.connection.visitor.accept @ast, collector collector.value end diff --git a/lib/arel/update_manager.rb b/lib/arel/update_manager.rb index db8cf05f76..36fb74fe7c 100644 --- a/lib/arel/update_manager.rb +++ b/lib/arel/update_manager.rb @@ -1,6 +1,6 @@ module Arel class UpdateManager < Arel::TreeManager - def initialize engine + def initialize super @ast = Nodes::UpdateStatement.new @ctx = @ast diff --git a/lib/arel/visitors.rb b/lib/arel/visitors.rb index 4a8d254ba7..f492ca2d9d 100644 --- a/lib/arel/visitors.rb +++ b/lib/arel/visitors.rb @@ -6,6 +6,7 @@ require 'arel/visitors/postgresql' require 'arel/visitors/mysql' require 'arel/visitors/mssql' require 'arel/visitors/oracle' +require 'arel/visitors/oracle12' require 'arel/visitors/where_sql' require 'arel/visitors/dot' require 'arel/visitors/ibm_db' diff --git a/lib/arel/visitors/depth_first.rb b/lib/arel/visitors/depth_first.rb index a434f404c7..22704dd038 100644 --- a/lib/arel/visitors/depth_first.rb +++ b/lib/arel/visitors/depth_first.rb @@ -146,7 +146,7 @@ module Arel visit o.wheres visit o.groups visit o.windows - visit o.having + visit o.havings end def visit_Arel_Nodes_SelectStatement o @@ -173,6 +173,12 @@ module Arel 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/lib/arel/visitors/informix.rb b/lib/arel/visitors/informix.rb index 7e8a3ea458..c33ef50554 100644 --- a/lib/arel/visitors/informix.rb +++ b/lib/arel/visitors/informix.rb @@ -34,8 +34,13 @@ module Arel collector = inject_join o.groups, collector, ", " end - maybe_visit o.having, collector + 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 diff --git a/lib/arel/visitors/mssql.rb b/lib/arel/visitors/mssql.rb index 0e5b75ec59..92362a0c5f 100644 --- a/lib/arel/visitors/mssql.rb +++ b/lib/arel/visitors/mssql.rb @@ -3,6 +3,11 @@ module Arel 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 @@ -61,6 +66,23 @@ module Arel 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 @@ -81,10 +103,20 @@ module Arel end # FIXME raise exception of there is no pk? - # FIXME!! Table.primary_key will be deprecated. What is the replacement?? def find_left_table_pk o - return o.primary_key if o.instance_of? Arel::Table - find_left_table_pk o.left if o.kind_of? Arel::Nodes::Join + 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 diff --git a/lib/arel/visitors/mysql.rb b/lib/arel/visitors/mysql.rb index f989b8ddef..724e0fc43e 100644 --- a/lib/arel/visitors/mysql.rb +++ b/lib/arel/visitors/mysql.rb @@ -40,7 +40,7 @@ module Arel # 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(Nodes.build_quoted(18446744073709551615)) + o.limit = Arel::Nodes::Limit.new(18446744073709551615) end super end diff --git a/lib/arel/visitors/oracle.rb b/lib/arel/visitors/oracle.rb index 91f6e0223e..875b0e5b6a 100644 --- a/lib/arel/visitors/oracle.rb +++ b/lib/arel/visitors/oracle.rb @@ -17,7 +17,7 @@ module Arel if o.limit && o.offset o = o.dup - limit = o.limit.expr.expr + limit = o.limit.expr offset = o.offset o.offset = nil collector << " @@ -132,6 +132,10 @@ module Arel array end + def visit_Arel_Nodes_BindParam o, collector + collector.add_bind(o) { |i| ":a#{i}" } + end + end end end diff --git a/lib/arel/visitors/oracle12.rb b/lib/arel/visitors/oracle12.rb new file mode 100644 index 0000000000..4a42343c9b --- /dev/null +++ b/lib/arel/visitors/oracle12.rb @@ -0,0 +1,53 @@ +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 + o = o.dup + o.limit = [] + 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 + end + end +end diff --git a/lib/arel/visitors/postgresql.rb b/lib/arel/visitors/postgresql.rb index 60878ddd20..1ef0261bdd 100644 --- a/lib/arel/visitors/postgresql.rb +++ b/lib/arel/visitors/postgresql.rb @@ -4,25 +4,45 @@ module Arel private def visit_Arel_Nodes_Matches o, collector - infix_value o, collector, ' ILIKE ' + 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 - infix_value o, collector, ' NOT ILIKE ' + 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 - infix_value o, collector, ' ~ ' + op = o.case_sensitive ? ' ~ ' : ' ~* ' + infix_value o, collector, op end def visit_Arel_Nodes_NotRegexp o, collector - infix_value 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) { |i| "$#{i}" } + end end end end diff --git a/lib/arel/visitors/reduce.rb b/lib/arel/visitors/reduce.rb index 1d74934fe5..9670cad27c 100644 --- a/lib/arel/visitors/reduce.rb +++ b/lib/arel/visitors/reduce.rb @@ -10,14 +10,14 @@ module Arel private def visit object, collector - send dispatch[object.class.name], object, collector + send dispatch[object.class], object, collector rescue NoMethodError => e - raise e if respond_to?(dispatch[object.class.name], true) + raise e if respond_to?(dispatch[object.class], true) superklass = object.class.ancestors.find { |klass| - respond_to?(dispatch[klass.name], true) + respond_to?(dispatch[klass], true) } raise(TypeError, "Cannot visit #{object.class}") unless superklass - dispatch[object.class.name] = dispatch[superklass.name] + dispatch[object.class] = dispatch[superklass] retry end end diff --git a/lib/arel/visitors/to_sql.rb b/lib/arel/visitors/to_sql.rb index a3f8cb565d..ce1fdf80ce 100644 --- a/lib/arel/visitors/to_sql.rb +++ b/lib/arel/visitors/to_sql.rb @@ -4,6 +4,12 @@ require 'arel/visitors/reduce' 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::Reduce ## # This is some roflscale crazy stuff. I'm roflscaling this because @@ -74,14 +80,14 @@ module Arel end def visit_Arel_Nodes_DeleteStatement o, collector - collector << "DELETE FROM " + collector << 'DELETE FROM ' collector = visit o.relation, collector if o.wheres.any? - collector << " WHERE " - inject_join o.wheres, collector, AND - else - collector + 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 @@ -186,7 +192,8 @@ module Arel len = o.expressions.length - 1 o.expressions.zip(o.columns).each_with_index { |(value, attr), i| - if Nodes::SqlLiteral === value + case value + when Nodes::SqlLiteral, Nodes::BindParam collector = visit value, collector else collector << quote(value, attr && column_for(attr)).to_s @@ -210,7 +217,6 @@ module Arel } unless o.orders.empty? - collector << SPACE collector << ORDER_BY len = o.orders.length - 1 o.orders.each_with_index { |x, i| @@ -219,11 +225,15 @@ module Arel } 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 - - collector end def visit_Arel_Nodes_SelectCore o, collector @@ -265,7 +275,10 @@ module Arel end end - collector = maybe_visit o.having, collector + unless o.havings.empty? + collector << " HAVING " + inject_join o.havings, collector, AND + end unless o.windows.empty? collector << WINDOW @@ -404,11 +417,6 @@ module Arel end end - def visit_Arel_Nodes_Having o, collector - collector << "HAVING " - visit o.expr, collector - end - def visit_Arel_Nodes_Offset o, collector collector << "OFFSET " visit o.expr, collector @@ -574,8 +582,11 @@ module Arel visit o.left, collector end - def visit_Arel_Nodes_FullOuterJoin o - "FULL OUTER JOIN #{visit o.left} #{visit o.right}" + 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 @@ -585,8 +596,11 @@ module Arel visit o.right, collector end - def visit_Arel_Nodes_RightOuterJoin o - "RIGHT OUTER JOIN #{visit o.left} #{visit o.right}" + 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 @@ -713,7 +727,7 @@ module Arel def literal o, collector; collector << o.to_s; end def visit_Arel_Nodes_BindParam o, collector - collector.add_bind o + collector.add_bind(o) { "?" } end alias :visit_Arel_Nodes_SqlLiteral :literal @@ -721,11 +735,15 @@ module Arel alias :visit_Fixnum :literal def quoted o, a - quote(o, column_for(a)) + if a && a.able_to_type_cast? + quote(a.type_cast_for_database(o)) + else + quote(o, column_for(a)) + end end def unsupported o, collector - raise "unsupported: #{o.class.name}" + raise UnsupportedVisitError.new(o) end alias :visit_ActiveSupport_Multibyte_Chars :unsupported @@ -761,6 +779,9 @@ module Arel def quote value, column = nil return value if Arel::Nodes::SqlLiteral === value + if column + print_type_cast_deprecation + end @connection.quote value, column end @@ -810,6 +831,20 @@ module Arel collector end end + + def print_type_cast_deprecation + unless defined?($arel_silence_type_casting_deprecation) && $arel_silence_type_casting_deprecation + warn <<-eowarn +Arel performing automatic type casting is deprecated, and will be removed in Arel 8.0. If you are seeing this, it is because you are manually passing a value to an Arel predicate, and the `Arel::Table` object was constructed manually. The easiest way to remove this warning is to use an `Arel::Table` object returned from calling `arel_table` on an ActiveRecord::Base subclass. + +If you're certain the value is already of the right type, change `attribute.eq(value)` to `attribute.eq(Arel::Nodes::Quoted.new(value))` (you will be able to remove that in Arel 8.0, it is only required to silence this deprecation warning). + +You can also silence this warning globally by setting `$arel_silence_type_casting_deprecation` to `true`. (Do NOT do this if you are a library author) + +If you are passing user input to a predicate, you must either give an appropriate type caster object to the `Arel::Table`, or manually cast the value before passing it to Arel. + eowarn + end + end end end end diff --git a/lib/arel/visitors/visitor.rb b/lib/arel/visitors/visitor.rb index 2317d0c95f..bfe7342f04 100644 --- a/lib/arel/visitors/visitor.rb +++ b/lib/arel/visitors/visitor.rb @@ -2,17 +2,7 @@ module Arel module Visitors class Visitor def initialize - @dispatch = Hash.new do |hash, class_name| - raise if class_name == 'Arel::Nodes::Union' - hash[class_name] = "visit_#{(class_name || '').gsub('::', '_')}" - end - - # pre-populate cache. FIXME: this should be passed in to each - # instance, but we can do that later. - self.class.private_instance_methods.sort.each do |name| - next unless name =~ /^visit_(.*)$/ - @dispatch[$1.gsub('_', '::')] = name - end + @dispatch = get_dispatch_cache end def accept object @@ -21,19 +11,29 @@ module Arel private + 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 dispatch @dispatch end def visit object - send dispatch[object.class.name], object + send dispatch[object.class], object rescue NoMethodError => e - raise e if respond_to?(dispatch[object.class.name], true) + raise e if respond_to?(dispatch[object.class], true) superklass = object.class.ancestors.find { |klass| - respond_to?(dispatch[klass.name], true) + respond_to?(dispatch[klass], true) } raise(TypeError, "Cannot visit #{object.class}") unless superklass - dispatch[object.class.name] = dispatch[superklass.name] + dispatch[object.class] = dispatch[superklass] retry end end |