Support Rails 5

This commit is contained in:
Gleb Mazovetskiy 2016-04-09 01:19:13 +01:00 committed by Brian Hempel
parent 74b21c6ba1
commit 5a47ade141
8 changed files with 126 additions and 39 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@
.config
.yardoc
Gemfile.lock
*.gemfile.lock
InstalledFiles
_yardoc
coverage

View File

@ -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

View File

@ -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`)

View File

@ -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

View File

@ -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

View File

@ -2,3 +2,5 @@ source 'https://rubygems.org'
# Specify your gem's dependencies in active_record_union.gemspec
gemspec
gem 'rails', '~> 4.2.6'

6
rails_5_0.gemfile Normal file
View File

@ -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']

View File

@ -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