diff options
65 files changed, 567 insertions, 344 deletions
diff --git a/.travis.yml b/.travis.yml index 32aeb83ea9..4e1bca45c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,23 +8,18 @@ env: global: - JRUBY_OPTS='--dev -J-Xmx1024M' rvm: - - rbx-2 - - jruby-9.0.5.0 - - jruby-head - - 2.0.0 - - 2.1 - - 2.2.5 - - 2.3.1 - - 2.4.0 + - 2.2.8 + - 2.3.5 + - 2.4.2 - ruby-head + - jruby-9.1.12.0 + - jruby-head matrix: fast_finish: true allow_failures: - - rvm: jruby-9.0.5.0 - - rvm: jruby-head - rvm: ruby-head + - rvm: jruby-9.1.12.0 - rvm: jruby-head - - rvm: rbx-2 bundler_args: --jobs 3 --retry 3 notifications: email: false diff --git a/History.txt b/History.txt index 6985da4f49..11ca9e8659 100644 --- a/History.txt +++ b/History.txt @@ -1,3 +1,9 @@ +=== 9.0.0 / 2017-11-14 + +* Enhancements + * `InsertManager#insert` is now chainable + * Support multiple inserts + === 8.0.0 / 2017-02-21 * Enhancements @@ -150,7 +150,13 @@ The `OR` operator works like this: users.where(users[:name].eq('bob').or(users[:age].lt(25))) ``` -The `AND` operator behaves similarly. Here is an example of the `DISTINCT` operator: +The `AND` operator behaves similarly (same exact behaviour as chained calls to `.where`): + +```ruby +users.where(users[:name].eq('bob').and(users[:age].lt(25))) +``` + +Here is an example of the `DISTINCT` operator: ```ruby posts = Arel::Table.new(:posts) @@ -188,7 +194,7 @@ users.project(users[:age].average.as("mean_age")) # => SELECT AVG(users.age) AS mean_age FROM users ``` -### The Crazy Features +### The Advanced Features The examples above are fairly simple and other libraries match or come close to matching the expressiveness of Arel (e.g. `Sequel` in Ruby). @@ -215,6 +221,7 @@ products. #### Complex Joins +##### Alias Where Arel really shines is in its ability to handle complex joins and aggregations. As a first example, let's consider an "adjacency list", a tree represented in a table. Suppose we have a table `comments`, representing a threaded discussion: ```ruby @@ -240,6 +247,7 @@ comments_with_replies = \ This will return the reply for the first comment. +##### CTE [Common Table Expressions (CTE)](https://en.wikipedia.org/wiki/Common_table_expressions#Common_table_expression) support via: Create a `CTE` @@ -262,6 +270,7 @@ users. # FROM users INNER JOIN cte_table ON users.id = cte_table.user_id ``` +#### Write SQL strings When your query is too complex for `Arel`, you can use `Arel::SqlLiteral`: ```ruby diff --git a/arel.gemspec b/arel.gemspec index 3924bd15d2..4ed41c3d4c 100644 --- a/arel.gemspec +++ b/arel.gemspec @@ -1,4 +1,3 @@ -# # -*- encoding: utf-8 -*- # frozen_string_literal: true $:.push File.expand_path("../lib", __FILE__) require "arel" @@ -13,14 +12,16 @@ Gem::Specification.new do |s| s.description = "Arel Really Exasperates Logicians\n\nArel is a SQL AST manager for Ruby. It\n\n1. Simplifies the generation of complex SQL queries\n2. Adapts to various RDBMSes\n\nIt is intended to be a framework framework; that is, you can build your own ORM\nwith it, focusing on innovative object and collection modeling as opposed to\ndatabase compatibility and query generation." s.summary = "Arel Really Exasperates Logicians Arel is a SQL AST manager for Ruby" s.license = %q{MIT} + s.required_ruby_version = ">= 2.2.2" s.rdoc_options = ["--main", "README.md"] s.extra_rdoc_files = ["History.txt", "MIT-LICENSE.txt", "README.md"] - s.files = ["History.txt","MIT-LICENSE.txt","README.md","lib/arel.rb","lib/arel/alias_predication.rb","lib/arel/attributes.rb","lib/arel/attributes/attribute.rb","lib/arel/collectors/bind.rb","lib/arel/collectors/plain_string.rb","lib/arel/collectors/sql_string.rb","lib/arel/compatibility/wheres.rb","lib/arel/crud.rb","lib/arel/delete_manager.rb","lib/arel/errors.rb","lib/arel/expressions.rb","lib/arel/factory_methods.rb","lib/arel/insert_manager.rb","lib/arel/math.rb","lib/arel/nodes.rb","lib/arel/nodes/and.rb","lib/arel/nodes/ascending.rb","lib/arel/nodes/binary.rb","lib/arel/nodes/bind_param.rb","lib/arel/nodes/case.rb","lib/arel/nodes/casted.rb","lib/arel/nodes/count.rb","lib/arel/nodes/delete_statement.rb","lib/arel/nodes/descending.rb","lib/arel/nodes/equality.rb","lib/arel/nodes/extract.rb","lib/arel/nodes/false.rb","lib/arel/nodes/full_outer_join.rb","lib/arel/nodes/function.rb","lib/arel/nodes/grouping.rb","lib/arel/nodes/in.rb","lib/arel/nodes/infix_operation.rb","lib/arel/nodes/inner_join.rb","lib/arel/nodes/insert_statement.rb","lib/arel/nodes/join_source.rb","lib/arel/nodes/matches.rb","lib/arel/nodes/named_function.rb","lib/arel/nodes/node.rb","lib/arel/nodes/outer_join.rb","lib/arel/nodes/over.rb","lib/arel/nodes/regexp.rb","lib/arel/nodes/right_outer_join.rb","lib/arel/nodes/select_core.rb","lib/arel/nodes/select_statement.rb","lib/arel/nodes/sql_literal.rb","lib/arel/nodes/string_join.rb","lib/arel/nodes/table_alias.rb","lib/arel/nodes/terminal.rb","lib/arel/nodes/true.rb","lib/arel/nodes/unary.rb","lib/arel/nodes/unary_operation.rb","lib/arel/nodes/unqualified_column.rb","lib/arel/nodes/update_statement.rb","lib/arel/nodes/values.rb","lib/arel/nodes/window.rb","lib/arel/nodes/with.rb","lib/arel/order_predications.rb","lib/arel/predications.rb","lib/arel/select_manager.rb","lib/arel/table.rb","lib/arel/tree_manager.rb","lib/arel/update_manager.rb","lib/arel/visitors.rb","lib/arel/visitors/bind_substitute.rb","lib/arel/visitors/bind_visitor.rb","lib/arel/visitors/depth_first.rb","lib/arel/visitors/dot.rb","lib/arel/visitors/ibm_db.rb","lib/arel/visitors/informix.rb","lib/arel/visitors/mssql.rb","lib/arel/visitors/mysql.rb","lib/arel/visitors/oracle.rb","lib/arel/visitors/oracle12.rb","lib/arel/visitors/postgresql.rb","lib/arel/visitors/reduce.rb","lib/arel/visitors/sqlite.rb","lib/arel/visitors/to_sql.rb","lib/arel/visitors/visitor.rb","lib/arel/visitors/where_sql.rb","lib/arel/window_predications.rb"] + s.files = ["History.txt","MIT-LICENSE.txt","README.md","lib/arel.rb","lib/arel/alias_predication.rb","lib/arel/attributes.rb","lib/arel/attributes/attribute.rb","lib/arel/collectors/bind.rb","lib/arel/collectors/composite.rb","lib/arel/collectors/plain_string.rb","lib/arel/collectors/sql_string.rb","lib/arel/collectors/substitute_binds.rb","lib/arel/compatibility/wheres.rb","lib/arel/crud.rb","lib/arel/delete_manager.rb","lib/arel/errors.rb","lib/arel/expressions.rb","lib/arel/factory_methods.rb","lib/arel/insert_manager.rb","lib/arel/math.rb","lib/arel/nodes.rb","lib/arel/nodes/and.rb","lib/arel/nodes/ascending.rb","lib/arel/nodes/binary.rb","lib/arel/nodes/bind_param.rb","lib/arel/nodes/case.rb","lib/arel/nodes/casted.rb","lib/arel/nodes/count.rb","lib/arel/nodes/delete_statement.rb","lib/arel/nodes/descending.rb","lib/arel/nodes/equality.rb","lib/arel/nodes/extract.rb","lib/arel/nodes/false.rb","lib/arel/nodes/full_outer_join.rb","lib/arel/nodes/function.rb","lib/arel/nodes/grouping.rb","lib/arel/nodes/in.rb","lib/arel/nodes/infix_operation.rb","lib/arel/nodes/inner_join.rb","lib/arel/nodes/insert_statement.rb","lib/arel/nodes/join_source.rb","lib/arel/nodes/matches.rb","lib/arel/nodes/named_function.rb","lib/arel/nodes/node.rb","lib/arel/nodes/outer_join.rb","lib/arel/nodes/over.rb","lib/arel/nodes/regexp.rb","lib/arel/nodes/right_outer_join.rb","lib/arel/nodes/select_core.rb","lib/arel/nodes/select_statement.rb","lib/arel/nodes/sql_literal.rb","lib/arel/nodes/string_join.rb","lib/arel/nodes/table_alias.rb","lib/arel/nodes/terminal.rb","lib/arel/nodes/true.rb","lib/arel/nodes/unary.rb","lib/arel/nodes/unary_operation.rb","lib/arel/nodes/unqualified_column.rb","lib/arel/nodes/update_statement.rb","lib/arel/nodes/values.rb","lib/arel/nodes/values_list.rb","lib/arel/nodes/window.rb","lib/arel/nodes/with.rb","lib/arel/order_predications.rb","lib/arel/predications.rb","lib/arel/select_manager.rb","lib/arel/table.rb","lib/arel/tree_manager.rb","lib/arel/update_manager.rb","lib/arel/visitors.rb","lib/arel/visitors/depth_first.rb","lib/arel/visitors/dot.rb","lib/arel/visitors/ibm_db.rb","lib/arel/visitors/informix.rb","lib/arel/visitors/mssql.rb","lib/arel/visitors/mysql.rb","lib/arel/visitors/oracle.rb","lib/arel/visitors/oracle12.rb","lib/arel/visitors/postgresql.rb","lib/arel/visitors/sqlite.rb","lib/arel/visitors/to_sql.rb","lib/arel/visitors/visitor.rb","lib/arel/visitors/where_sql.rb","lib/arel/window_predications.rb"] s.require_paths = ["lib"] s.add_development_dependency('minitest', '~> 5.4') s.add_development_dependency('rdoc', '~> 4.0') s.add_development_dependency('rake') + s.add_development_dependency('concurrent-ruby', '~> 1.0') end diff --git a/arel.gemspec.erb b/arel.gemspec.erb index f7ff99469c..4698e8bae7 100644 --- a/arel.gemspec.erb +++ b/arel.gemspec.erb @@ -1,4 +1,3 @@ -# # -*- encoding: utf-8 -*- # frozen_string_literal: true $:.push File.expand_path("../lib", __FILE__) require "arel" @@ -13,6 +12,7 @@ Gem::Specification.new do |s| s.description = "Arel Really Exasperates Logicians\n\nArel is a SQL AST manager for Ruby. It\n\n1. Simplifies the generation of complex SQL queries\n2. Adapts to various RDBMSes\n\nIt is intended to be a framework framework; that is, you can build your own ORM\nwith it, focusing on innovative object and collection modeling as opposed to\ndatabase compatibility and query generation." s.summary = "Arel Really Exasperates Logicians Arel is a SQL AST manager for Ruby" s.license = %q{MIT} + s.required_ruby_version = ">= 2.2.2" s.rdoc_options = ["--main", "README.md"] s.extra_rdoc_files = ["History.txt", "MIT-LICENSE.txt", "README.md"] @@ -23,4 +23,5 @@ Gem::Specification.new do |s| s.add_development_dependency('minitest', '~> 5.4') s.add_development_dependency('rdoc', '~> 4.0') s.add_development_dependency('rake') + s.add_development_dependency('concurrent-ruby', '~> 1.0') end diff --git a/lib/arel.rb b/lib/arel.rb index f0d2bdce78..e7c6fc7fd3 100644 --- a/lib/arel.rb +++ b/lib/arel.rb @@ -24,7 +24,7 @@ require 'arel/delete_manager' require 'arel/nodes' module Arel - VERSION = '8.0.0' + VERSION = '9.0.0' def self.sql raw_sql Arel::Nodes::SqlLiteral.new raw_sql diff --git a/lib/arel/collectors/bind.rb b/lib/arel/collectors/bind.rb index dfa79d1001..d816aed90d 100644 --- a/lib/arel/collectors/bind.rb +++ b/lib/arel/collectors/bind.rb @@ -1,36 +1,23 @@ # frozen_string_literal: true + module Arel module Collectors class Bind def initialize - @parts = [] + @binds = [] end def << str - @parts << str self end def add_bind bind - @parts << bind + @binds << bind self end - def value; @parts; end - - def substitute_binds bvs - bvs = bvs.dup - @parts.map do |val| - if Arel::Nodes::BindParam === val - bvs.shift - else - val - end - end - end - - def compile bvs - substitute_binds(bvs).join + def value + @binds end end end diff --git a/lib/arel/collectors/composite.rb b/lib/arel/collectors/composite.rb new file mode 100644 index 0000000000..4f6156fe27 --- /dev/null +++ b/lib/arel/collectors/composite.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Arel + module Collectors + class Composite + def initialize(left, right) + @left = left + @right = right + end + + def << str + left << str + right << str + self + end + + def add_bind bind, &block + left.add_bind bind, &block + right.add_bind bind, &block + self + end + + def value + [left.value, right.value] + end + + protected + + attr_reader :left, :right + end + end +end diff --git a/lib/arel/collectors/sql_string.rb b/lib/arel/collectors/sql_string.rb index 5f42117331..bcb941f6d4 100644 --- a/lib/arel/collectors/sql_string.rb +++ b/lib/arel/collectors/sql_string.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 # frozen_string_literal: true require 'arel/collectors/plain_string' diff --git a/lib/arel/collectors/substitute_binds.rb b/lib/arel/collectors/substitute_binds.rb new file mode 100644 index 0000000000..99d2215aaa --- /dev/null +++ b/lib/arel/collectors/substitute_binds.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true +module Arel + module Collectors + class SubstituteBinds + def initialize(quoter, delegate_collector) + @quoter = quoter + @delegate = delegate_collector + end + + def << str + delegate << str + self + end + + def add_bind bind + self << quoter.quote(bind) + end + + def value + delegate.value + end + + protected + + attr_reader :quoter, :delegate + end + end +end diff --git a/lib/arel/insert_manager.rb b/lib/arel/insert_manager.rb index f9a598e8b7..dcbac6cb43 100644 --- a/lib/arel/insert_manager.rb +++ b/lib/arel/insert_manager.rb @@ -40,5 +40,9 @@ module Arel def create_values values, columns Nodes::Values.new values, columns end + + def create_values_list(rows) + Nodes::ValuesList.new(rows) + end end end diff --git a/lib/arel/nodes.rb b/lib/arel/nodes.rb index 8c9815a96b..8c6572dd6a 100644 --- a/lib/arel/nodes.rb +++ b/lib/arel/nodes.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true # node require 'arel/nodes/node' +require 'arel/nodes/node_expression' require 'arel/nodes/select_statement' require 'arel/nodes/select_core' require 'arel/nodes/insert_statement' @@ -44,6 +45,7 @@ require 'arel/nodes/function' require 'arel/nodes/count' require 'arel/nodes/extract' require 'arel/nodes/values' +require 'arel/nodes/values_list' require 'arel/nodes/named_function' # windows diff --git a/lib/arel/nodes/binary.rb b/lib/arel/nodes/binary.rb index 3001788774..a86d4e4696 100644 --- a/lib/arel/nodes/binary.rb +++ b/lib/arel/nodes/binary.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Arel module Nodes - class Binary < Arel::Nodes::Node + class Binary < Arel::Nodes::NodeExpression attr_accessor :left, :right def initialize left, right diff --git a/lib/arel/nodes/bind_param.rb b/lib/arel/nodes/bind_param.rb index 9e297831cd..efa4f452d4 100644 --- a/lib/arel/nodes/bind_param.rb +++ b/lib/arel/nodes/bind_param.rb @@ -2,8 +2,25 @@ module Arel module Nodes class BindParam < Node - def ==(other) - other.is_a?(BindParam) + attr_accessor :value + + def initialize(value) + @value = value + super() + end + + def hash + [self.class, self.value].hash + end + + def eql?(other) + other.is_a?(BindParam) && + value == other.value + end + alias :== :eql? + + def nil? + value.nil? end end end diff --git a/lib/arel/nodes/case.rb b/lib/arel/nodes/case.rb index 1edca40001..50ea1e0be2 100644 --- a/lib/arel/nodes/case.rb +++ b/lib/arel/nodes/case.rb @@ -2,10 +2,6 @@ module Arel module Nodes class Case < Arel::Nodes::Node - include Arel::OrderPredications - include Arel::Predications - include Arel::AliasPredication - attr_accessor :case, :conditions, :default def initialize expression = nil, default = nil diff --git a/lib/arel/nodes/casted.rb b/lib/arel/nodes/casted.rb index 290e4dd38c..f945063dd2 100644 --- a/lib/arel/nodes/casted.rb +++ b/lib/arel/nodes/casted.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Arel module Nodes - class Casted < Arel::Nodes::Node # :nodoc: + class Casted < Arel::Nodes::NodeExpression # :nodoc: attr_reader :val, :attribute def initialize val, attribute @val = val @@ -30,7 +30,7 @@ module Arel 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 + when Arel::Nodes::Node, Arel::Attributes::Attribute, Arel::Table, Arel::Nodes::BindParam, Arel::SelectManager, Arel::Nodes::Quoted, Arel::Nodes::SqlLiteral other else case attribute diff --git a/lib/arel/nodes/count.rb b/lib/arel/nodes/count.rb index a7c6236a22..4dd9be453f 100644 --- a/lib/arel/nodes/count.rb +++ b/lib/arel/nodes/count.rb @@ -2,6 +2,8 @@ module Arel module Nodes class Count < Arel::Nodes::Function + include Math + def initialize expr, distinct = false, aliaz = nil super(expr, aliaz) @distinct = distinct diff --git a/lib/arel/nodes/delete_statement.rb b/lib/arel/nodes/delete_statement.rb index 593ce9bddf..063a5341e5 100644 --- a/lib/arel/nodes/delete_statement.rb +++ b/lib/arel/nodes/delete_statement.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module Arel module Nodes - class DeleteStatement < Arel::Nodes::Binary + class DeleteStatement < Arel::Nodes::Node + attr_accessor :left, :right attr_accessor :limit alias :relation :left @@ -10,13 +11,27 @@ module Arel alias :wheres= :right= def initialize relation = nil, wheres = [] - super + super() + @left = relation + @right = wheres end def initialize_copy other super - @right = @right.clone + @left = @left.clone if @left + @right = @right.clone if @right + end + + def hash + [self.class, @left, @right].hash + end + + def eql? other + self.class == other.class && + self.left == other.left && + self.right == other.right end + alias :== :eql? end end end diff --git a/lib/arel/nodes/extract.rb b/lib/arel/nodes/extract.rb index 4e797b6770..fdf3004c6a 100644 --- a/lib/arel/nodes/extract.rb +++ b/lib/arel/nodes/extract.rb @@ -2,9 +2,6 @@ module Arel module Nodes class Extract < Arel::Nodes::Unary - include Arel::AliasPredication - include Arel::Predications - attr_accessor :field def initialize expr, field diff --git a/lib/arel/nodes/false.rb b/lib/arel/nodes/false.rb index 26b4e5db97..58132a2f90 100644 --- a/lib/arel/nodes/false.rb +++ b/lib/arel/nodes/false.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Arel module Nodes - class False < Arel::Nodes::Node + class False < Arel::Nodes::NodeExpression def hash self.class.hash end @@ -9,6 +9,7 @@ module Arel def eql? other self.class == other.class end + alias :== :eql? end end end diff --git a/lib/arel/nodes/function.rb b/lib/arel/nodes/function.rb index 28a394e9f3..b3bf8f3e51 100644 --- a/lib/arel/nodes/function.rb +++ b/lib/arel/nodes/function.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true module Arel module Nodes - class Function < Arel::Nodes::Node - include Arel::Predications + class Function < Arel::Nodes::NodeExpression include Arel::WindowPredications - include Arel::OrderPredications attr_accessor :expressions, :alias, :distinct def initialize expr, aliaz = nil @@ -29,6 +27,8 @@ module Arel self.alias == other.alias && self.distinct == other.distinct end + alias :== :eql? + end %w{ diff --git a/lib/arel/nodes/grouping.rb b/lib/arel/nodes/grouping.rb index 16911eb3b6..ffe66654ce 100644 --- a/lib/arel/nodes/grouping.rb +++ b/lib/arel/nodes/grouping.rb @@ -2,7 +2,6 @@ module Arel module Nodes class Grouping < Unary - include Arel::Predications end end end diff --git a/lib/arel/nodes/node.rb b/lib/arel/nodes/node.rb index 34e71063af..d2e6313dda 100644 --- a/lib/arel/nodes/node.rb +++ b/lib/arel/nodes/node.rb @@ -1,6 +1,4 @@ # frozen_string_literal: true -require 'arel/collectors/sql_string' - module Arel module Nodes ### diff --git a/lib/arel/nodes/node_expression.rb b/lib/arel/nodes/node_expression.rb new file mode 100644 index 0000000000..c4d4c8f428 --- /dev/null +++ b/lib/arel/nodes/node_expression.rb @@ -0,0 +1,11 @@ +module Arel + module Nodes + class NodeExpression < Arel::Nodes::Node + include Arel::Expressions + include Arel::Predications + include Arel::AliasPredication + include Arel::OrderPredications + include Arel::Math + end + end +end diff --git a/lib/arel/nodes/select_statement.rb b/lib/arel/nodes/select_statement.rb index 641a08405d..79176d4be5 100644 --- a/lib/arel/nodes/select_statement.rb +++ b/lib/arel/nodes/select_statement.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Arel module Nodes - class SelectStatement < Arel::Nodes::Node + class SelectStatement < Arel::Nodes::NodeExpression attr_reader :cores attr_accessor :limit, :orders, :lock, :offset, :with diff --git a/lib/arel/nodes/terminal.rb b/lib/arel/nodes/terminal.rb index 6f60fe006f..3a1cd7f0e1 100644 --- a/lib/arel/nodes/terminal.rb +++ b/lib/arel/nodes/terminal.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Arel module Nodes - class Distinct < Arel::Nodes::Node + class Distinct < Arel::Nodes::NodeExpression def hash self.class.hash end @@ -9,6 +9,7 @@ module Arel def eql? other self.class == other.class end + alias :== :eql? end end end diff --git a/lib/arel/nodes/true.rb b/lib/arel/nodes/true.rb index 796b5b9348..fdb8ed2095 100644 --- a/lib/arel/nodes/true.rb +++ b/lib/arel/nodes/true.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Arel module Nodes - class True < Arel::Nodes::Node + class True < Arel::Nodes::NodeExpression def hash self.class.hash end @@ -9,6 +9,7 @@ module Arel def eql? other self.class == other.class end + alias :== :eql? end end end diff --git a/lib/arel/nodes/unary.rb b/lib/arel/nodes/unary.rb index 60cff1defe..e458d87ab3 100644 --- a/lib/arel/nodes/unary.rb +++ b/lib/arel/nodes/unary.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Arel module Nodes - class Unary < Arel::Nodes::Node + class Unary < Arel::Nodes::NodeExpression attr_accessor :expr alias :value :expr diff --git a/lib/arel/nodes/unary_operation.rb b/lib/arel/nodes/unary_operation.rb index 3c56ef2026..be4e270e76 100644 --- a/lib/arel/nodes/unary_operation.rb +++ b/lib/arel/nodes/unary_operation.rb @@ -3,12 +3,6 @@ module Arel module Nodes class UnaryOperation < Unary - include Arel::Expressions - include Arel::Predications - include Arel::OrderPredications - include Arel::AliasPredication - include Arel::Math - attr_reader :operator def initialize operator, operand @@ -23,4 +17,4 @@ module Arel end end end -end
\ No newline at end of file +end diff --git a/lib/arel/nodes/values_list.rb b/lib/arel/nodes/values_list.rb new file mode 100644 index 0000000000..89cea1790d --- /dev/null +++ b/lib/arel/nodes/values_list.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +module Arel + module Nodes + class ValuesList < Node + attr_reader :rows + + def initialize(rows) + @rows = rows + super() + end + + def hash + @rows.hash + end + + def eql? other + self.class == other.class && + self.rows == other.rows + end + alias :== :eql? + end + end +end diff --git a/lib/arel/nodes/window.rb b/lib/arel/nodes/window.rb index 535c0c6238..23a005daba 100644 --- a/lib/arel/nodes/window.rb +++ b/lib/arel/nodes/window.rb @@ -107,6 +107,7 @@ module Arel def eql? other self.class == other.class end + alias :== :eql? end class Preceding < Unary diff --git a/lib/arel/select_manager.rb b/lib/arel/select_manager.rb index 73fa5da6ed..0f3b0dc6a0 100644 --- a/lib/arel/select_manager.rb +++ b/lib/arel/select_manager.rb @@ -1,6 +1,4 @@ # frozen_string_literal: true -require 'arel/collectors/sql_string' - module Arel class SelectManager < Arel::TreeManager include Arel::Crud diff --git a/lib/arel/tree_manager.rb b/lib/arel/tree_manager.rb index cc3f1eeee4..b237bf368d 100644 --- a/lib/arel/tree_manager.rb +++ b/lib/arel/tree_manager.rb @@ -1,17 +1,12 @@ # frozen_string_literal: true -require 'arel/collectors/sql_string' - module Arel class TreeManager include Arel::FactoryMethods - attr_reader :ast, :engine - - attr_accessor :bind_values + attr_reader :ast def initialize - @ctx = nil - @bind_values = [] + @ctx = nil end def to_dot diff --git a/lib/arel/visitors/bind_substitute.rb b/lib/arel/visitors/bind_substitute.rb deleted file mode 100644 index 52c96b0d72..0000000000 --- a/lib/arel/visitors/bind_substitute.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true -module Arel - module Visitors - class BindSubstitute - def initialize delegate - @delegate = delegate - end - end - end -end diff --git a/lib/arel/visitors/bind_visitor.rb b/lib/arel/visitors/bind_visitor.rb deleted file mode 100644 index 8a5570cf5c..0000000000 --- a/lib/arel/visitors/bind_visitor.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true -module Arel - module Visitors - module BindVisitor - def initialize target - @block = nil - super - end - - def accept node, collector, &block - @block = block if block_given? - super - end - - private - - def visit_Arel_Nodes_Assignment o, collector - if o.right.is_a? Arel::Nodes::BindParam - collector = visit o.left, collector - collector << " = " - visit o.right, collector - else - super - end - end - - def visit_Arel_Nodes_BindParam o, collector - if @block - val = @block.call - if String === val - collector << val - end - else - super - end - end - - end - end -end diff --git a/lib/arel/visitors/informix.rb b/lib/arel/visitors/informix.rb index b53ab18b82..44b18b550e 100644 --- a/lib/arel/visitors/informix.rb +++ b/lib/arel/visitors/informix.rb @@ -18,9 +18,7 @@ module Arel end def visit_Arel_Nodes_SelectCore o, collector collector = inject_join o.projections, collector, ", " - froms = false if o.source && !o.source.empty? - froms = true collector << " FROM " collector = visit o.source, collector end diff --git a/lib/arel/visitors/oracle.rb b/lib/arel/visitors/oracle.rb index 3b452836db..d4749bbae3 100644 --- a/lib/arel/visitors/oracle.rb +++ b/lib/arel/visitors/oracle.rb @@ -29,12 +29,12 @@ module Arel collector = super(o, collector) if offset.expr.is_a? Nodes::BindParam - offset_bind = nil collector << ') raw_sql_ WHERE rownum <= (' - collector.add_bind(offset.expr) { |i| offset_bind = ":a#{i}" } + collector = visit offset.expr, collector collector << ' + ' - collector.add_bind(limit) { |i| ":a#{i}" } - collector << ") ) WHERE raw_rnum_ > #{offset_bind}" + collector = visit limit, collector + collector << ") ) WHERE raw_rnum_ > " + collector = visit offset.expr, collector return collector else collector << ") raw_sql_ @@ -145,7 +145,7 @@ module Arel end def visit_Arel_Nodes_BindParam o, collector - collector.add_bind(o) { |i| ":a#{i}" } + collector.add_bind(o.value) { |i| ":a#{i}" } end end diff --git a/lib/arel/visitors/oracle12.rb b/lib/arel/visitors/oracle12.rb index ce90e994ae..648047ae61 100644 --- a/lib/arel/visitors/oracle12.rb +++ b/lib/arel/visitors/oracle12.rb @@ -53,7 +53,7 @@ module Arel end def visit_Arel_Nodes_BindParam o, collector - collector.add_bind(o) { |i| ":a#{i}" } + collector.add_bind(o.value) { |i| ":a#{i}" } end end end diff --git a/lib/arel/visitors/postgresql.rb b/lib/arel/visitors/postgresql.rb index bd4421bd58..047f71aaa6 100644 --- a/lib/arel/visitors/postgresql.rb +++ b/lib/arel/visitors/postgresql.rb @@ -47,7 +47,7 @@ module Arel end def visit_Arel_Nodes_BindParam o, collector - collector.add_bind(o) { |i| "$#{i}" } + collector.add_bind(o.value) { |i| "$#{i}" } end def visit_Arel_Nodes_GroupingElement o, collector diff --git a/lib/arel/visitors/reduce.rb b/lib/arel/visitors/reduce.rb deleted file mode 100644 index 7948758e2f..0000000000 --- a/lib/arel/visitors/reduce.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true -require 'arel/visitors/visitor' - -module Arel - module Visitors - class Reduce < Arel::Visitors::Visitor - def accept object, collector - visit object, collector - end - - private - - def visit object, collector - send dispatch[object.class], object, collector - rescue NoMethodError => e - raise e if respond_to?(dispatch[object.class], 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/lib/arel/visitors/to_sql.rb b/lib/arel/visitors/to_sql.rb index 486c51a183..2b5c43b173 100644 --- a/lib/arel/visitors/to_sql.rb +++ b/lib/arel/visitors/to_sql.rb @@ -1,8 +1,4 @@ # frozen_string_literal: true -require 'bigdecimal' -require 'date' -require 'arel/visitors/reduce' - module Arel module Visitors class UnsupportedVisitError < StandardError @@ -11,7 +7,7 @@ module Arel end end - class ToSql < Arel::Visitors::Reduce + 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 @@ -166,6 +162,28 @@ module Arel 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 (" @@ -405,7 +423,8 @@ module Arel end def visit_Arel_SelectManager o, collector - collector << "(#{o.to_sql.rstrip})" + collector << '(' + visit(o.ast, collector) << ')' end def visit_Arel_Nodes_Ascending o, collector @@ -715,7 +734,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.value) { "?" } end alias :visit_Arel_Nodes_SqlLiteral :literal diff --git a/lib/arel/visitors/visitor.rb b/lib/arel/visitors/visitor.rb index b96b8238a7..f156be9a0a 100644 --- a/lib/arel/visitors/visitor.rb +++ b/lib/arel/visitors/visitor.rb @@ -6,12 +6,14 @@ module Arel @dispatch = get_dispatch_cache end - def accept object - visit object + 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('::', '_')}" @@ -22,14 +24,11 @@ module Arel self.class.dispatch_cache end - def dispatch - @dispatch - end - - def visit object - send dispatch[object.class], object + def visit object, *args + dispatch_method = dispatch[object.class] + send dispatch_method, object, *args rescue NoMethodError => e - raise e if respond_to?(dispatch[object.class], true) + raise e if respond_to?(dispatch_method, true) superklass = object.class.ancestors.find { |klass| respond_to?(dispatch[klass], true) } diff --git a/test/attributes/test_attribute.rb b/test/attributes/test_attribute.rb index 2b971ce54a..a67ef53a4c 100644 --- a/test/attributes/test_attribute.rb +++ b/test/attributes/test_attribute.rb @@ -997,6 +997,18 @@ module Arel assert table.able_to_type_cast? condition.to_sql.must_equal %("foo"."id" = 1 AND "foo"."other_id" = '2') end + + it 'does not type cast SqlLiteral nodes' do + fake_caster = Object.new + def fake_caster.type_cast_for_database(attr_name, value) + value.to_i + end + table = Table.new(:foo, type_caster: fake_caster) + condition = table["id"].eq(Arel.sql("(select 1)")) + + assert table.able_to_type_cast? + condition.to_sql.must_equal %("foo"."id" = (select 1)) + end end end end diff --git a/test/collectors/test_bind.rb b/test/collectors/test_bind.rb new file mode 100644 index 0000000000..62fd911a0f --- /dev/null +++ b/test/collectors/test_bind.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +require 'helper' + +module Arel + module Collectors + class TestBind < Arel::Test + def setup + @conn = FakeRecord::Base.new + @visitor = Visitors::ToSql.new @conn.connection + super + end + + def collect node + @visitor.accept(node, Collectors::Bind.new) + end + + def compile node + collect(node).value + end + + def ast_with_binds bvs + table = Table.new(:users) + manager = Arel::SelectManager.new table + manager.where(table[:age].eq(Nodes::BindParam.new(bvs.shift))) + manager.where(table[:name].eq(Nodes::BindParam.new(bvs.shift))) + manager.ast + end + + def test_compile_gathers_all_bind_params + binds = compile(ast_with_binds(["hello", "world"])) + assert_equal ["hello", "world"], binds + + binds = compile(ast_with_binds(["hello2", "world3"])) + assert_equal ["hello2", "world3"], binds + end + end + end +end diff --git a/test/collectors/test_bind_collector.rb b/test/collectors/test_bind_collector.rb deleted file mode 100644 index 877aa20043..0000000000 --- a/test/collectors/test_bind_collector.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true -require 'helper' -require 'arel/collectors/bind' - -module Arel - module Collectors - class TestBindCollector < Arel::Test - def setup - @conn = FakeRecord::Base.new - @visitor = Visitors::ToSql.new @conn.connection - super - end - - def collect node - @visitor.accept(node, Collectors::Bind.new) - end - - def compile node - collect(node).value - end - - def ast_with_binds bv - table = Table.new(:users) - manager = Arel::SelectManager.new table - manager.where(table[:age].eq(bv)) - manager.where(table[:name].eq(bv)) - manager.ast - end - - def test_leaves_binds - node = Nodes::BindParam.new - list = compile node - assert_equal node, list.first - assert_equal node.class, list.first.class - end - - def test_adds_strings - bv = Nodes::BindParam.new - list = compile ast_with_binds bv - assert_operator list.length, :>, 0 - assert_equal bv, list.grep(Nodes::BindParam).first - assert_equal bv.class, list.grep(Nodes::BindParam).first.class - end - - def test_substitute_binds - bv = Nodes::BindParam.new - collector = collect ast_with_binds bv - - values = collector.value - - offsets = values.map.with_index { |v,i| - [v,i] - }.find_all { |(v,_)| Nodes::BindParam === v }.map(&:last) - - list = collector.substitute_binds ["hello", "world"] - assert_equal "hello", list[offsets[0]] - assert_equal "world", list[offsets[1]] - - assert_equal 'SELECT FROM "users" WHERE "users"."age" = hello AND "users"."name" = world', list.join - end - - def test_compile - bv = Nodes::BindParam.new - collector = collect ast_with_binds bv - - sql = collector.compile ["hello", "world"] - assert_equal 'SELECT FROM "users" WHERE "users"."age" = hello AND "users"."name" = world', sql - end - end - end -end diff --git a/test/collectors/test_composite.rb b/test/collectors/test_composite.rb new file mode 100644 index 0000000000..3d49b390e8 --- /dev/null +++ b/test/collectors/test_composite.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true +require 'helper' + +require 'arel/collectors/bind' +require 'arel/collectors/composite' + +module Arel + module Collectors + class TestComposite < Arel::Test + def setup + @conn = FakeRecord::Base.new + @visitor = Visitors::ToSql.new @conn.connection + super + end + + def collect node + sql_collector = Collectors::SQLString.new + bind_collector = Collectors::Bind.new + collector = Collectors::Composite.new(sql_collector, bind_collector) + @visitor.accept(node, collector) + end + + def compile node + collect(node).value + end + + def ast_with_binds bvs + table = Table.new(:users) + manager = Arel::SelectManager.new table + manager.where(table[:age].eq(Nodes::BindParam.new(bvs.shift))) + manager.where(table[:name].eq(Nodes::BindParam.new(bvs.shift))) + manager.ast + end + + def test_composite_collector_performs_multiple_collections_at_once + sql, binds = compile(ast_with_binds(["hello", "world"])) + assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql + assert_equal ["hello", "world"], binds + + sql, binds = compile(ast_with_binds(["hello2", "world3"])) + assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql + assert_equal ["hello2", "world3"], binds + end + end + end +end diff --git a/test/collectors/test_sql_string.rb b/test/collectors/test_sql_string.rb index 92f1bf0fba..0185f2ab17 100644 --- a/test/collectors/test_sql_string.rb +++ b/test/collectors/test_sql_string.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true require 'helper' -require 'arel/collectors/bind' module Arel module Collectors @@ -28,12 +27,20 @@ module Arel end def test_compile - bv = Nodes::BindParam.new + bv = Nodes::BindParam.new(1) collector = collect ast_with_binds bv sql = collector.compile ["hello", "world"] assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql end + + def test_returned_sql_uses_utf8_encoding + bv = Nodes::BindParam.new(1) + collector = collect ast_with_binds bv + + sql = collector.compile ["hello", "world"] + assert_equal sql.encoding, Encoding::UTF_8 + end end end end diff --git a/test/collectors/test_substitute_bind_collector.rb b/test/collectors/test_substitute_bind_collector.rb new file mode 100644 index 0000000000..de3e3655da --- /dev/null +++ b/test/collectors/test_substitute_bind_collector.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true +require 'helper' +require 'arel/collectors/substitute_binds' +require 'arel/collectors/sql_string' + +module Arel + module Collectors + class TestSubstituteBindCollector < Arel::Test + def setup + @conn = FakeRecord::Base.new + @visitor = Visitors::ToSql.new @conn.connection + super + end + + def ast_with_binds + table = Table.new(:users) + manager = Arel::SelectManager.new table + manager.where(table[:age].eq(Nodes::BindParam.new("hello"))) + manager.where(table[:name].eq(Nodes::BindParam.new("world"))) + manager.ast + end + + def compile(node, quoter) + collector = Collectors::SubstituteBinds.new(quoter, Collectors::SQLString.new) + @visitor.accept(node, collector).value + end + + def test_compile + quoter = Object.new + def quoter.quote(val) + val.to_s + end + sql = compile(ast_with_binds, quoter) + assert_equal 'SELECT FROM "users" WHERE "users"."age" = hello AND "users"."name" = world', sql + end + + def test_quoting_is_delegated_to_quoter + quoter = Object.new + def quoter.quote(val) + val.inspect + end + sql = compile(ast_with_binds, quoter) + assert_equal 'SELECT FROM "users" WHERE "users"."age" = "hello" AND "users"."name" = "world"', sql + end + end + end +end diff --git a/test/helper.rb b/test/helper.rb index 022ba1dae6..3eecfb79b6 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'rubygems' require 'minitest/autorun' -require 'fileutils' require 'arel' require 'support/fake_record' diff --git a/test/nodes/test_binary.rb b/test/nodes/test_binary.rb index 8e3025a440..ef23a3930b 100644 --- a/test/nodes/test_binary.rb +++ b/test/nodes/test_binary.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true require 'helper' -require 'set' module Arel module Nodes diff --git a/test/nodes/test_bind_param.rb b/test/nodes/test_bind_param.rb index 011de6410c..482bb24f33 100644 --- a/test/nodes/test_bind_param.rb +++ b/test/nodes/test_bind_param.rb @@ -4,12 +4,17 @@ require 'helper' module Arel module Nodes describe 'BindParam' do - it 'is equal to other bind params' do - BindParam.new.must_equal(BindParam.new) + it 'is equal to other bind params with the same value' do + BindParam.new(1).must_equal(BindParam.new(1)) + BindParam.new("foo").must_equal(BindParam.new("foo")) end it 'is not equal to other nodes' do - BindParam.new.wont_equal(Node.new) + BindParam.new(nil).wont_equal(Node.new) + end + + it 'is not equal to bind params with different values' do + BindParam.new(1).wont_equal(BindParam.new(2)) end end end diff --git a/test/nodes/test_count.rb b/test/nodes/test_count.rb index 85e93b1927..29883df681 100644 --- a/test/nodes/test_count.rb +++ b/test/nodes/test_count.rb @@ -31,4 +31,13 @@ describe Arel::Nodes::Count do assert_equal 2, array.uniq.size end end + + describe 'math' do + it 'allows mathematical functions' do + table = Arel::Table.new :users + (table[:id].count + 1).to_sql.must_be_like %{ + (COUNT("users"."id") + 1) + } + end + end end diff --git a/test/nodes/test_table_alias.rb b/test/nodes/test_table_alias.rb index 39040e6352..911114d938 100644 --- a/test/nodes/test_table_alias.rb +++ b/test/nodes/test_table_alias.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true require 'helper' -require 'ostruct' module Arel module Nodes diff --git a/test/test_insert_manager.rb b/test/test_insert_manager.rb index b9ee6f76ac..8b61a2791e 100644 --- a/test/test_insert_manager.rb +++ b/test/test_insert_manager.rb @@ -28,6 +28,60 @@ module Arel } end + it 'works with multiple values' do + table = Table.new(:users) + manager = Arel::InsertManager.new + manager.into table + + manager.columns << table[:id] + manager.columns << table[:name] + + manager.values = manager.create_values_list([ + %w{1 david}, + %w{2 kir}, + ["3", Arel.sql('DEFAULT')], + ]) + + manager.to_sql.must_be_like %{ + INSERT INTO \"users\" (\"id\", \"name\") VALUES ('1', 'david'), ('2', 'kir'), ('3', DEFAULT) + } + end + + it 'literals in multiple values are not escaped' do + table = Table.new(:users) + manager = Arel::InsertManager.new + manager.into table + + manager.columns << table[:name] + + manager.values = manager.create_values_list([ + [Arel.sql('*')], + [Arel.sql('DEFAULT')], + ]) + + manager.to_sql.must_be_like %{ + INSERT INTO \"users\" (\"name\") VALUES (*), (DEFAULT) + } + end + + it 'works with multiple single values' do + table = Table.new(:users) + manager = Arel::InsertManager.new + manager.into table + + manager.columns << table[:name] + + manager.values = manager.create_values_list([ + %w{david}, + %w{kir}, + [Arel.sql('DEFAULT')], + ]) + + manager.to_sql.must_be_like %{ + INSERT INTO \"users\" (\"name\") VALUES ('david'), ('kir'), (DEFAULT) + } + end + it "inserts false" do table = Table.new(:users) manager = Arel::InsertManager.new @@ -136,6 +190,17 @@ module Arel INSERT INTO "users" VALUES (1) } end + + it "accepts sql literals" do + table = Table.new :users + manager = Arel::InsertManager.new + manager.into table + + manager.values = Arel.sql("DEFAULT VALUES") + manager.to_sql.must_be_like %{ + INSERT INTO "users" DEFAULT VALUES + } + end end describe "combo" do diff --git a/test/test_nodes.rb b/test/test_nodes.rb new file mode 100644 index 0000000000..1060f150ff --- /dev/null +++ b/test/test_nodes.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true +require 'helper' + +module Arel + module Nodes + class TestNodes < Minitest::Test + def test_every_arel_nodes_have_hash_eql_eqeq_from_same_class + # #descendants code from activesupport + node_descendants = [] + ObjectSpace.each_object(Arel::Nodes::Node.singleton_class) do |k| + next if k.respond_to?(:singleton_class?) && k.singleton_class? + node_descendants.unshift k unless k == self + end + node_descendants.delete(Arel::Nodes::Node) + node_descendants.delete(Arel::Nodes::NodeExpression) + + bad_node_descendants = node_descendants.reject do |subnode| + eqeq_owner = subnode.instance_method(:==).owner + eql_owner = subnode.instance_method(:eql?).owner + hash_owner = subnode.instance_method(:hash).owner + + eqeq_owner < Arel::Nodes::Node && + eqeq_owner == eql_owner && + eqeq_owner == hash_owner + end + + problem_msg = 'Some subclasses of Arel::Nodes::Node do not have a' \ + ' #== or #eql? or #hash defined from the same class as the others' + assert_empty bad_node_descendants, problem_msg + end + + + end + end +end diff --git a/test/test_select_manager.rb b/test/test_select_manager.rb index 076b732984..cc7452c239 100644 --- a/test/test_select_manager.rb +++ b/test/test_select_manager.rb @@ -10,13 +10,6 @@ module Arel assert_equal "SELECT FROM 'foo'", manager.to_sql end - def test_manager_stores_bind_values - manager = Arel::SelectManager.new - assert_equal [], manager.bind_values - manager.bind_values = [1] - assert_equal [1], manager.bind_values - end - describe 'backwards compatibility' do describe 'project' do it 'accepts symbols as sql literals' do @@ -226,7 +219,7 @@ module Arel table = Table.new(:users) manager = Arel::SelectManager.new table manager.project Nodes::SqlLiteral.new '*' - m2 = Arel::SelectManager.new(manager.engine) + m2 = Arel::SelectManager.new m2.project manager.exists m2.to_sql.must_be_like %{ SELECT EXISTS (#{manager.to_sql}) } end @@ -235,7 +228,7 @@ module Arel table = Table.new(:users) manager = Arel::SelectManager.new table manager.project Nodes::SqlLiteral.new '*' - m2 = Arel::SelectManager.new(manager.engine) + m2 = Arel::SelectManager.new m2.project manager.exists.as('foo') m2.to_sql.must_be_like %{ SELECT EXISTS (#{manager.to_sql}) AS foo } end diff --git a/test/test_update_manager.rb b/test/test_update_manager.rb index 85abbf3875..4a373d1ff7 100644 --- a/test/test_update_manager.rb +++ b/test/test_update_manager.rb @@ -13,7 +13,7 @@ module Arel table = Table.new(:users) um = Arel::UpdateManager.new um.table table - um.set [[table[:name], Arel::Nodes::BindParam.new]] + um.set [[table[:name], Arel::Nodes::BindParam.new(1)]] um.to_sql.must_be_like %{ UPDATE "users" SET "name" = ? } end diff --git a/test/visitors/test_bind_visitor.rb b/test/visitors/test_bind_visitor.rb deleted file mode 100644 index 3e0578a6a1..0000000000 --- a/test/visitors/test_bind_visitor.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true -require 'helper' -require 'arel/visitors/bind_visitor' -require 'support/fake_record' - -module Arel - module Visitors - class TestBindVisitor < Arel::Test - attr_reader :collector - - def setup - @collector = Collectors::SQLString.new - super - end - - ## - # Tests visit_Arel_Nodes_Assignment correctly - # substitutes binds with values from block - def test_assignment_binds_are_substituted - table = Table.new(:users) - um = Arel::UpdateManager.new - bp = Nodes::BindParam.new - um.set [[table[:name], bp]] - visitor = Class.new(Arel::Visitors::ToSql) { - include Arel::Visitors::BindVisitor - }.new Table.engine.connection - - assignment = um.ast.values[0] - actual = visitor.accept(assignment, collector) { - "replace" - } - assert actual - value = actual.value - assert_like "\"name\" = replace", value - end - - def test_visitor_yields_on_binds - visitor = Class.new(Arel::Visitors::ToSql) { - include Arel::Visitors::BindVisitor - }.new nil - - bp = Nodes::BindParam.new - called = false - visitor.accept(bp, collector) { called = true } - assert called - end - - def test_visitor_only_yields_on_binds - visitor = Class.new(Arel::Visitors::ToSql) { - include Arel::Visitors::BindVisitor - }.new(nil) - - bp = Arel.sql 'omg' - called = false - - visitor.accept(bp, collector) { called = true } - refute called - end - end - end -end diff --git a/test/visitors/test_depth_first.rb b/test/visitors/test_depth_first.rb index 0c7f7ccd34..832843265c 100644 --- a/test/visitors/test_depth_first.rb +++ b/test/visitors/test_depth_first.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true require 'helper' -require 'set' module Arel module Visitors diff --git a/test/visitors/test_dispatch_contamination.rb b/test/visitors/test_dispatch_contamination.rb index 6422a6dff3..71792594d6 100644 --- a/test/visitors/test_dispatch_contamination.rb +++ b/test/visitors/test_dispatch_contamination.rb @@ -1,8 +1,42 @@ # frozen_string_literal: true require 'helper' +require 'concurrent' module Arel module Visitors + class DummyVisitor < Visitor + def initialize + super + @barrier = Concurrent::CyclicBarrier.new(2) + end + + def visit_Arel_Visitors_DummySuperNode node + 42 + end + + # This is terrible, but it's the only way to reliably reproduce + # the possible race where two threads attempt to correct the + # dispatch hash at the same time. + def send *args + super + rescue + # Both threads try (and fail) to dispatch to the subclass's name + @barrier.wait + raise + ensure + # Then one thread successfully completes (updating the dispatch + # table in the process) before the other finishes raising its + # exception. + Thread.current[:delay].wait if Thread.current[:delay] + end + end + + class DummySuperNode + end + + class DummySubNode < DummySuperNode + end + describe 'avoiding contamination between visitor dispatch tables' do before do @connection = Table.engine.connection @@ -17,6 +51,21 @@ module Arel assert_equal "( TRUE UNION FALSE )", node.to_sql end + + it 'is threadsafe when implementing superclass fallback' do + visitor = DummyVisitor.new + main_thread_finished = Concurrent::Event.new + + racing_thread = Thread.new do + Thread.current[:delay] = main_thread_finished + visitor.accept DummySubNode.new + end + + assert_equal 42, visitor.accept(DummySubNode.new) + main_thread_finished.set + + assert_equal 42, racing_thread.value + end end end end diff --git a/test/visitors/test_dot.rb b/test/visitors/test_dot.rb index 1d27d1a5cb..3c4a038116 100644 --- a/test/visitors/test_dot.rb +++ b/test/visitors/test_dot.rb @@ -74,7 +74,7 @@ module Arel end def test_Arel_Nodes_BindParam - node = Arel::Nodes::BindParam.new + node = Arel::Nodes::BindParam.new(1) collector = Collectors::PlainString.new assert_match '[label="<f0>Arel::Nodes::BindParam"]', @visitor.accept(node, collector).value end diff --git a/test/visitors/test_oracle.rb b/test/visitors/test_oracle.rb index b1921f0cbc..fce6606d35 100644 --- a/test/visitors/test_oracle.rb +++ b/test/visitors/test_oracle.rb @@ -127,8 +127,8 @@ module Arel it 'creates a subquery when there is limit and offset with BindParams' do stmt = Nodes::SelectStatement.new - stmt.limit = Nodes::Limit.new(Nodes::BindParam.new) - stmt.offset = Nodes::Offset.new(Nodes::BindParam.new) + stmt.limit = Nodes::Limit.new(Nodes::BindParam.new(1)) + stmt.offset = Nodes::Offset.new(Nodes::BindParam.new(1)) sql = compile stmt sql.must_be_like %{ SELECT * FROM ( @@ -136,7 +136,7 @@ module Arel FROM (SELECT ) raw_sql_ WHERE rownum <= (:a1 + :a2) ) - WHERE raw_rnum_ > :a1 + WHERE raw_rnum_ > :a3 } end @@ -184,8 +184,8 @@ module Arel describe "Nodes::BindParam" do it "increments each bind param" do - query = @table[:name].eq(Arel::Nodes::BindParam.new) - .and(@table[:id].eq(Arel::Nodes::BindParam.new)) + query = @table[:name].eq(Arel::Nodes::BindParam.new(1)) + .and(@table[:id].eq(Arel::Nodes::BindParam.new(1))) compile(query).must_be_like %{ "users"."name" = :a1 AND "users"."id" = :a2 } diff --git a/test/visitors/test_oracle12.rb b/test/visitors/test_oracle12.rb index c908a51d4f..62658bc595 100644 --- a/test/visitors/test_oracle12.rb +++ b/test/visitors/test_oracle12.rb @@ -48,8 +48,8 @@ module Arel describe "Nodes::BindParam" do it "increments each bind param" do - query = @table[:name].eq(Arel::Nodes::BindParam.new) - .and(@table[:id].eq(Arel::Nodes::BindParam.new)) + query = @table[:name].eq(Arel::Nodes::BindParam.new(1)) + .and(@table[:id].eq(Arel::Nodes::BindParam.new(1))) compile(query).must_be_like %{ "users"."name" = :a1 AND "users"."id" = :a2 } diff --git a/test/visitors/test_postgres.rb b/test/visitors/test_postgres.rb index d3cab623c4..b28c0f3c18 100644 --- a/test/visitors/test_postgres.rb +++ b/test/visitors/test_postgres.rb @@ -190,8 +190,8 @@ module Arel describe "Nodes::BindParam" do it "increments each bind param" do - query = @table[:name].eq(Arel::Nodes::BindParam.new) - .and(@table[:id].eq(Arel::Nodes::BindParam.new)) + query = @table[:name].eq(Arel::Nodes::BindParam.new(1)) + .and(@table[:id].eq(Arel::Nodes::BindParam.new(1))) compile(query).must_be_like %{ "users"."name" = $1 AND "users"."id" = $2 } diff --git a/test/visitors/test_to_sql.rb b/test/visitors/test_to_sql.rb index 31279b0ae2..77756b9e99 100644 --- a/test/visitors/test_to_sql.rb +++ b/test/visitors/test_to_sql.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true require 'helper' -require 'set' +require 'bigdecimal' module Arel module Visitors @@ -17,13 +17,13 @@ module Arel end it 'works with BindParams' do - node = Nodes::BindParam.new + node = Nodes::BindParam.new(1) sql = compile node sql.must_be_like '?' end it 'does not quote BindParams used as part of a Values' do - bp = Nodes::BindParam.new + bp = Nodes::BindParam.new(1) values = Nodes::Values.new([bp]) sql = compile values sql.must_be_like 'VALUES (?)' @@ -31,7 +31,7 @@ module Arel it 'can define a dispatch method' do visited = false - viz = Class.new(Arel::Visitors::Reduce) { + viz = Class.new(Arel::Visitors::Visitor) { define_method(:hello) do |node, c| visited = true end |