From 5a47ade1413b2871f50ebffd4d1ec5ea0a5d3b8c Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Sat, 9 Apr 2016 01:19:13 +0100 Subject: [PATCH] Support Rails 5 --- .gitignore | 1 + .travis.yml | 15 +++++- README.md | 6 ++- Rakefile | 45 ++++++++++++++++-- .../active_record/relation/union.rb | 44 ++++++++++++------ Gemfile => rails_4_2.gemfile | 2 + rails_5_0.gemfile | 6 +++ spec/union_spec.rb | 46 ++++++++++++------- 8 files changed, 126 insertions(+), 39 deletions(-) rename Gemfile => rails_4_2.gemfile (80%) create mode 100644 rails_5_0.gemfile diff --git a/.gitignore b/.gitignore index 31cafb5..71baf29 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .config .yardoc Gemfile.lock +*.gemfile.lock InstalledFiles _yardoc coverage diff --git a/.travis.yml b/.travis.yml index 8a5d9f3..210131a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,18 @@ language: ruby addons: postgresql: "9.4" rvm: - - 2.3.0 - - 2.2.4 + - 2.3.1 + - 2.2.5 - 2.1.8 - 2.0.0 +gemfile: + - rails_4_2.gemfile + - rails_5_0.gemfile +matrix: + exclude: + # Rails 5 requires Ruby 2.2+: + - rvm: 2.1.8 + gemfile: rails_5_0.gemfile + - rvm: 2.0.0 + gemfile: rails_5_0.gemfile +script: bundle exec rspec diff --git a/README.md b/README.md index 9b2d81c..5340789 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,11 @@ This public domain dedication follows the the CC0 1.0 at https://creativecommons 1. Fork it ( https://github.com/brianhempel/active_record_union/fork ) 2. Create your feature branch (`git checkout -b my-new-feature`) -3. Run the tests with `rspec` +3. Run the tests: + 1. Install MySQL and PostgreSQL. + 2. You may need to create a `test_active_record_union` database on each under the default user. + 3. Run `rake` to test with all supported Rails versions. + 4. Run `rake test_rails_4_2` or `rake test_rails_5_0` to test a specific Rails version. 4. There is also a `bin/console` command to load up a REPL for playing around 5. Commit your changes (`git commit -am 'Add some feature'`) 6. Push to the branch (`git push origin my-new-feature`) diff --git a/Rakefile b/Rakefile index f24a307..6bd119d 100644 --- a/Rakefile +++ b/Rakefile @@ -1,8 +1,45 @@ require "bundler/gem_tasks" -task :default => :spec +require 'rspec/core/rake_task' +RSpec::Core::RakeTask.new(:spec) +task :default => :test_all_gemfiles -desc "Run the tests" -task :spec do - exec("bundle exec rspec") +module TestTasks + module_function + + TEST_CMD = 'bundle exec rspec' + + def run_all(envs, cmd = "bundle install && #{TEST_CMD}", success_message) + statuses = envs.map { |env| run(env, cmd) } + failed = statuses.reject(&:first).map(&:last) + if failed.empty? + $stderr.puts success_message + else + $stderr.puts "❌ FAILING (#{failed.size}):\n#{failed.map { |env| to_bash_cmd_with_env(cmd, env) } * "\n"}" + exit 1 + end + end + + def run(env, cmd) + Bundler.with_clean_env do + full_cmd = to_bash_cmd_with_env(cmd, env) + $stderr.puts full_cmd + isSuccess = system(full_cmd) + [isSuccess, env] + end + end + + def gemfiles + Dir.glob('*.gemfile').sort + end + + def to_bash_cmd_with_env(cmd, env) + "(export #{env.map { |k, v| "#{k}=#{v}" }.join(' ')}; #{cmd})" + end +end + +desc 'Test all Gemfiles' +task :test_all_gemfiles do + envs = TestTasks.gemfiles.map { |gemfile| { 'BUNDLE_GEMFILE' => gemfile } } + TestTasks.run_all envs, "✓ Tests pass with all #{envs.size} gemfiles" end diff --git a/lib/active_record_union/active_record/relation/union.rb b/lib/active_record_union/active_record/relation/union.rb index 066b65b..d37910a 100644 --- a/lib/active_record_union/active_record/relation/union.rb +++ b/lib/active_record_union/active_record/relation/union.rb @@ -7,11 +7,11 @@ module ActiveRecord union_all: Arel::Nodes::UnionAll } - def union(relation_or_where_arg, *args) + def union(relation_or_where_arg, *args) set_operation(:union, relation_or_where_arg, *args) end - def union_all(relation_or_where_arg, *args) + def union_all(relation_or_where_arg, *args) set_operation(:union_all, relation_or_where_arg, *args) end @@ -19,29 +19,30 @@ module ActiveRecord def set_operation(operation, relation_or_where_arg, *args) other = if args.size == 0 && Relation === relation_or_where_arg - relation_or_where_arg - else - @klass.where(relation_or_where_arg, *args) - end + relation_or_where_arg + else + @klass.where(relation_or_where_arg, *args) + end verify_relations_for_set_operation!(operation, self, other) # Postgres allows ORDER BY in the UNION subqueries if each subquery is surrounded by parenthesis # but SQLite does not allow parens around the subqueries; you will have to explicitly do `relation.reorder(nil)` in SQLite - if Arel::Visitors::SQLite === self.visitor + if Arel::Visitors::SQLite === self.connection.visitor left, right = self.ast, other.ast else left, right = Arel::Nodes::Grouping.new(self.ast), Arel::Nodes::Grouping.new(other.ast) end - set = SET_OPERATION_TO_AREL_CLASS[operation].new(left, right) - from = Arel::Nodes::TableAlias.new( - set, - @klass.arel_table.name - ) - - relation = @klass.unscoped.from(from) - relation.bind_values = self.arel.bind_values + self.bind_values + other.arel.bind_values + other.bind_values + set = SET_OPERATION_TO_AREL_CLASS[operation].new(left, right) + from = Arel::Nodes::TableAlias.new(set, @klass.arel_table.name) + if ActiveRecord::VERSION::MAJOR >= 5 + relation = @klass.unscoped.spawn + relation.from_clause = UnionFromClause.new(from, nil, self.bound_attributes + other.bound_attributes) + else + relation = @klass.unscoped.from(from) + relation.bind_values = self.arel.bind_values + self.bind_values + other.arel.bind_values + other.bind_values + end relation end @@ -62,6 +63,19 @@ module ActiveRecord raise ArgumentError.new("Cannot #{operation} relation with eager load.") end end + + if ActiveRecord::VERSION::MAJOR >= 5 + class UnionFromClause < ActiveRecord::Relation::FromClause + def initialize(value, name, bound_attributes) + super(value, name) + @bound_attributes = bound_attributes + end + + def binds + @bound_attributes + end + end + end end end end diff --git a/Gemfile b/rails_4_2.gemfile similarity index 80% rename from Gemfile rename to rails_4_2.gemfile index bb2da2b..9dae8eb 100644 --- a/Gemfile +++ b/rails_4_2.gemfile @@ -2,3 +2,5 @@ source 'https://rubygems.org' # Specify your gem's dependencies in active_record_union.gemspec gemspec + +gem 'rails', '~> 4.2.6' diff --git a/rails_5_0.gemfile b/rails_5_0.gemfile new file mode 100644 index 0000000..bd4707a --- /dev/null +++ b/rails_5_0.gemfile @@ -0,0 +1,6 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in active_record_union.gemspec +gemspec + +gem 'rails', ['>= 5.0.0.rc2', '< 5.1'] diff --git a/spec/union_spec.rb b/spec/union_spec.rb index 62472a8..a339939 100644 --- a/spec/union_spec.rb +++ b/spec/union_spec.rb @@ -1,6 +1,9 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Relation do + TIME = Time.utc(2014, 7, 19, 0, 0, 0) + SQL_TIME = ActiveRecord::VERSION::MAJOR >= 5 ? "2014-07-19 00:00:00" : "2014-07-19 00:00:00.000000" + describe ".union" do it "returns an ActiveRecord::Relation" do expect(User.all.union(User.all)).to be_kind_of(ActiveRecord::Relation) @@ -26,17 +29,25 @@ describe ActiveRecord::Relation do end it "works" do - union = User.new(id: 1).posts.union(Post.where("created_at > ?", Time.utc(2014, 7, 19, 0, 0, 0))) + union = User.new(id: 1).posts.union(Post.where("created_at > ?", TIME)) expect(union.to_sql.squish).to eq( - "SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 UNION SELECT \"posts\".* FROM \"posts\" WHERE (created_at > '2014-07-19 00:00:00.000000') ) \"posts\"" + "SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 UNION SELECT \"posts\".* FROM \"posts\" WHERE (created_at > '#{SQL_TIME}') ) \"posts\"" ) expect(union.arel.to_sql.squish).to eq( - "SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = ? UNION SELECT \"posts\".* FROM \"posts\" WHERE (created_at > '2014-07-19 00:00:00.000000') ) \"posts\"" + "SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = ? UNION SELECT \"posts\".* FROM \"posts\" WHERE (created_at > '#{SQL_TIME}') ) \"posts\"" ) expect{union.to_a}.to_not raise_error end + def bind_values_from_relation(relation) + if ActiveRecord::VERSION::MAJOR >= 5 + relation.bound_attributes.map { |a| a.value_for_database } + else + (relation.arel.bind_values + relation.bind_values).map { |_column, value| value } + end + end + it "binds values properly" do user1 = User.new(id: 1) user2 = User.new(id: 2) @@ -46,7 +57,7 @@ describe ActiveRecord::Relation do # Inside ActiveRecord the bind value list is # (union.arel.bind_values + union.bind_values) - bind_values = (union.arel.bind_values + union.bind_values).map { |column, value| value } + bind_values = bind_values_from_relation union expect(bind_values).to eq([1, 2, 3]) end @@ -54,7 +65,7 @@ describe ActiveRecord::Relation do it "binds values properly on joins" do union = User.joins(:drafts).union(User.where(id: 11)) - bind_values = (union.arel.bind_values + union.bind_values).map { |column, value| value } + bind_values = bind_values_from_relation union expect(bind_values).to eq([true, 11]) @@ -66,31 +77,32 @@ describe ActiveRecord::Relation do it "doesn't repeat default scopes" do expect(Time).to receive(:now) { Time.utc(2014, 7, 24, 0, 0, 0) } + sql_now = "2014-07-24 00:00:00#{".000000" if ActiveRecord::VERSION::MAJOR < 5}" class PublishedPost < ActiveRecord::Base self.table_name = "posts" default_scope { where("published_at < ?", Time.now) } end - union = PublishedPost.where("created_at > ?", Time.utc(2014, 7, 19, 0, 0, 0)).union(User.new(id: 1).posts) + union = PublishedPost.where("created_at > ?", TIME).union(User.new(id: 1).posts) expect(union.to_sql.squish).to eq( - "SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE (published_at < '2014-07-24 00:00:00.000000') AND (created_at > '2014-07-19 00:00:00.000000') UNION SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 ) \"posts\"" + "SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE (published_at < '#{sql_now}') AND (created_at > '#{SQL_TIME}') UNION SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 ) \"posts\"" ) expect{union.to_a}.to_not raise_error end context "with ORDER BY in subselects" do - def union + let :union do User.new(id: 1).posts.order(:created_at).union( - Post.where("created_at > ?", Time.utc(2014, 7, 19, 0, 0, 0)).order(:created_at) + Post.where("created_at > ?", TIME).order(:created_at) ).order(:created_at) end context "in SQLite" do it "lets ORDER BY in query subselects throw a syntax error" do expect(union.to_sql.squish).to eq( - "SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 ORDER BY \"posts\".\"created_at\" ASC UNION SELECT \"posts\".* FROM \"posts\" WHERE (created_at > '2014-07-19 00:00:00.000000') ORDER BY \"posts\".\"created_at\" ASC ) \"posts\" ORDER BY \"posts\".\"created_at\" ASC" + "SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 ORDER BY \"posts\".\"created_at\" ASC UNION SELECT \"posts\".* FROM \"posts\" WHERE (created_at > '#{SQL_TIME}') ORDER BY \"posts\".\"created_at\" ASC ) \"posts\" ORDER BY \"posts\".\"created_at\" ASC" ) expect{union.to_a}.to raise_error(ActiveRecord::StatementInvalid) end @@ -100,7 +112,7 @@ describe ActiveRecord::Relation do it "wraps query subselects in parentheses to allow ORDER BY clauses" do Databases.with_postgres do expect(union.to_sql.squish).to eq( - "SELECT \"posts\".* FROM ( (SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 ORDER BY \"posts\".\"created_at\" ASC) UNION (SELECT \"posts\".* FROM \"posts\" WHERE (created_at > '2014-07-19 00:00:00.000000') ORDER BY \"posts\".\"created_at\" ASC) ) \"posts\" ORDER BY \"posts\".\"created_at\" ASC" + "SELECT \"posts\".* FROM ( (SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 ORDER BY \"posts\".\"created_at\" ASC) UNION (SELECT \"posts\".* FROM \"posts\" WHERE (created_at > '#{SQL_TIME}') ORDER BY \"posts\".\"created_at\" ASC) ) \"posts\" ORDER BY \"posts\".\"created_at\" ASC" ) expect{union.to_a}.to_not raise_error end @@ -111,7 +123,7 @@ describe ActiveRecord::Relation do it "wraps query subselects in parentheses to allow ORDER BY clauses" do Databases.with_mysql do expect(union.to_sql.squish).to eq( - "SELECT `posts`.* FROM ( (SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 1 ORDER BY `posts`.`created_at` ASC) UNION (SELECT `posts`.* FROM `posts` WHERE (created_at > '2014-07-19 00:00:00.000000') ORDER BY `posts`.`created_at` ASC) ) `posts` ORDER BY `posts`.`created_at` ASC" + "SELECT `posts`.* FROM ( (SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 1 ORDER BY `posts`.`created_at` ASC) UNION (SELECT `posts`.* FROM `posts` WHERE (created_at > '#{SQL_TIME}') ORDER BY `posts`.`created_at` ASC) ) `posts` ORDER BY `posts`.`created_at` ASC" ) expect{union.to_a}.to_not raise_error end @@ -130,10 +142,10 @@ describe ActiveRecord::Relation do end it "multiple arguments" do - union = User.new(id: 1).posts.union("created_at > ?", Time.utc(2014, 7, 19, 0, 0, 0)) + union = User.new(id: 1).posts.union("created_at > ?", TIME) expect(union.to_sql.squish).to eq( - "SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 UNION SELECT \"posts\".* FROM \"posts\" WHERE (created_at > '2014-07-19 00:00:00.000000') ) \"posts\"" + "SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 UNION SELECT \"posts\".* FROM \"posts\" WHERE (created_at > '#{SQL_TIME}') ) \"posts\"" ) expect{union.to_a}.to_not raise_error end @@ -151,10 +163,10 @@ describe ActiveRecord::Relation do describe ".union_all" do it "works" do - union = User.new(id: 1).posts.union_all(Post.where("created_at > ?", Time.utc(2014, 7, 19, 0, 0, 0))) + union = User.new(id: 1).posts.union_all(Post.where("created_at > ?", TIME)) expect(union.to_sql.squish).to eq( - "SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 UNION ALL SELECT \"posts\".* FROM \"posts\" WHERE (created_at > '2014-07-19 00:00:00.000000') ) \"posts\"" + "SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 UNION ALL SELECT \"posts\".* FROM \"posts\" WHERE (created_at > '#{SQL_TIME}') ) \"posts\"" ) expect{union.to_a}.to_not raise_error end