mirror of
https://github.com/brianhempel/active_record_union
synced 2023-03-27 23:21:57 -04:00
Support Rails 5
This commit is contained in:
parent
74b21c6ba1
commit
5a47ade141
8 changed files with 126 additions and 39 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,6 +4,7 @@
|
|||
.config
|
||||
.yardoc
|
||||
Gemfile.lock
|
||||
*.gemfile.lock
|
||||
InstalledFiles
|
||||
_yardoc
|
||||
coverage
|
||||
|
|
15
.travis.yml
15
.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
|
||||
|
|
|
@ -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`)
|
||||
|
|
45
Rakefile
45
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
6
rails_5_0.gemfile
Normal 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']
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue