c80_active_record_union 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +24 -0
- data/.travis.yml +19 -0
- data/LICENSE.txt +3 -0
- data/README.md +220 -0
- data/Rakefile +61 -0
- data/bin/console +36 -0
- data/c80_active_record_union.gemspec +30 -0
- data/lib/c80_active_record_union.rb +9 -0
- data/lib/c80_active_record_union/active_record/relation/union.rb +82 -0
- data/lib/c80_active_record_union/version.rb +3 -0
- data/rails_4_2.gemfile +6 -0
- data/rails_5_0.gemfile +6 -0
- data/spec/spec_helper.rb +41 -0
- data/spec/support/databases.rb +49 -0
- data/spec/support/models.rb +18 -0
- data/spec/union_spec.rb +174 -0
- metadata +178 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: fd19d96b28ba3d5890207d1e3111a60c75697a45
|
4
|
+
data.tar.gz: 73b46e8ed3b35ff0db3887bba2b4bdba39f74182
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 07c4b25ed7676c9fb4c8ee2091c5fb07e64f72c75e74e952302b0fdbcc08f4725886a19a2608bd34d64150de22b1fbce132fa26bedec376745dfabd680f96c09
|
7
|
+
data.tar.gz: 26b9395b0434eba2098a433dc1f7f6654135e3b97ea5b01cde302592753c5fb8f539dfc2cbe7e6128839b2b94f334ec9893e7d53b57a15353bffe633b7258a6f
|
data/.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
*.gemfile.lock
|
8
|
+
InstalledFiles
|
9
|
+
_yardoc
|
10
|
+
coverage
|
11
|
+
doc/
|
12
|
+
lib/bundler/man
|
13
|
+
pkg
|
14
|
+
rdoc
|
15
|
+
spec/reports
|
16
|
+
test/tmp
|
17
|
+
test/version_tmp
|
18
|
+
tmp
|
19
|
+
*.bundle
|
20
|
+
*.so
|
21
|
+
*.o
|
22
|
+
*.a
|
23
|
+
mkmf.log
|
24
|
+
.idea/
|
data/.travis.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
language: ruby
|
2
|
+
addons:
|
3
|
+
postgresql: "9.4"
|
4
|
+
rvm:
|
5
|
+
- 2.3.1
|
6
|
+
- 2.2.5
|
7
|
+
- 2.1.8
|
8
|
+
- 2.0.0
|
9
|
+
gemfile:
|
10
|
+
- rails_4_2.gemfile
|
11
|
+
- rails_5_0.gemfile
|
12
|
+
matrix:
|
13
|
+
exclude:
|
14
|
+
# Rails 5 requires Ruby 2.2+:
|
15
|
+
- rvm: 2.1.8
|
16
|
+
gemfile: rails_5_0.gemfile
|
17
|
+
- rvm: 2.0.0
|
18
|
+
gemfile: rails_5_0.gemfile
|
19
|
+
script: bundle exec rspec
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,3 @@
|
|
1
|
+
ActiveRecordUnion is dedicated to the public domain by its author, Brian Hempel. No rights are reserved. No restrictions are placed on the use of ActiveRecordUnion. That freedom also means, of course, that no warrenty of fitness is claimed; use ActiveRecordUnion at your own risk.
|
2
|
+
|
3
|
+
This public domain dedication follows the the CC0 1.0 at https://creativecommons.org/publicdomain/zero/1.0/
|
data/README.md
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
# ActiveRecordUnion
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/active_record_union.svg)](http://badge.fury.io/rb/active_record_union)
|
4
|
+
[![Build Status](https://travis-ci.org/brianhempel/active_record_union.svg)](https://travis-ci.org/brianhempel/active_record_union)
|
5
|
+
|
6
|
+
Use unions on ActiveRecord scopes without ugliness.
|
7
|
+
|
8
|
+
If you find yourself writing `pluck(:id)` and then feeding that into another query, you may be able to reduce the number of database requests by using a nested query or a UNION without writing crazy JOIN statements.
|
9
|
+
|
10
|
+
Quick usage examples:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
current_user.posts.union(Post.published)
|
14
|
+
current_user.posts.union(Post.published).where(id: [6, 7])
|
15
|
+
current_user.posts.union("published_at < ?", Time.now)
|
16
|
+
user_1.posts.union(user_2.posts).union(Post.published)
|
17
|
+
user_1.posts.union_all(user_2.posts)
|
18
|
+
```
|
19
|
+
|
20
|
+
ActiveRecordUnion is tested against Rails 4.2 and Rails 5.0. It may or may not work on Rails 4.0/4.1.
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Add this line to your application's Gemfile:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
gem 'active_record_union'
|
28
|
+
```
|
29
|
+
|
30
|
+
And then execute:
|
31
|
+
|
32
|
+
$ bundle
|
33
|
+
|
34
|
+
Or install it yourself as:
|
35
|
+
|
36
|
+
$ gem install active_record_union
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
ActiveRecordUnion adds `union` and `union_all` methods to `ActiveRecord::Relation` so we can easily gather together queries on mutiple scopes.
|
41
|
+
|
42
|
+
Consider some users with posts:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
class User < ActiveRecord::Base
|
46
|
+
has_many :posts
|
47
|
+
end
|
48
|
+
|
49
|
+
class Post < ActiveRecord::Base
|
50
|
+
belongs_to :user
|
51
|
+
|
52
|
+
scope :published, -> { where("published_at < ?", Time.now) }
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
With ActiveRecordUnion, we can do:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
# the current user's (draft) posts and all published posts from anyone
|
60
|
+
current_user.posts.union(Post.published)
|
61
|
+
```
|
62
|
+
|
63
|
+
Which is equivalent to the following SQL:
|
64
|
+
|
65
|
+
```sql
|
66
|
+
SELECT "posts".* FROM (
|
67
|
+
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1
|
68
|
+
UNION
|
69
|
+
SELECT "posts".* FROM "posts" WHERE (published_at < '2014-07-19 16:04:21.918366')
|
70
|
+
) "posts"
|
71
|
+
```
|
72
|
+
|
73
|
+
Because the `union` method returns another `ActiveRecord::Relation`, we can run further queries on the union.
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
current_user.posts.union(Post.published).where(id: [6, 7])
|
77
|
+
```
|
78
|
+
```sql
|
79
|
+
SELECT "posts".* FROM (
|
80
|
+
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1
|
81
|
+
UNION
|
82
|
+
SELECT "posts".* FROM "posts" WHERE (published_at < '2014-07-19 16:06:04.460771')
|
83
|
+
) "posts" WHERE "posts"."id" IN (6, 7)
|
84
|
+
```
|
85
|
+
|
86
|
+
The `union` method can also accept anything that `where` does.
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
current_user.posts.union("published_at < ?", Time.now)
|
90
|
+
# equivalent to...
|
91
|
+
current_user.posts.union(Post.where("published_at < ?", Time.now))
|
92
|
+
```
|
93
|
+
|
94
|
+
We can also chain `union` calls to UNION more than two scopes, though the UNIONs will be nested which may not be the prettiest SQL.
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
user_1.posts.union(user_2.posts).union(Post.published)
|
98
|
+
# equivalent to...
|
99
|
+
[user_1.posts, user_2.posts, Post.published].inject(:union)
|
100
|
+
```
|
101
|
+
```sql
|
102
|
+
SELECT "posts".* FROM (
|
103
|
+
SELECT "posts".* FROM (
|
104
|
+
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1
|
105
|
+
UNION
|
106
|
+
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 2
|
107
|
+
) "posts"
|
108
|
+
UNION
|
109
|
+
SELECT "posts".* FROM "posts" WHERE (published_at < '2014-07-19 16:12:45.882648')
|
110
|
+
) "posts"
|
111
|
+
```
|
112
|
+
|
113
|
+
### UNION ALL
|
114
|
+
|
115
|
+
By default, UNION will remove any duplicates from the result set. If you don't care about duplicates or you know that the two queries you are combining will not have duplicates, you call use UNION ALL to tell the database to skip its deduplication step. In some cases this can give significant performance improvements.
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
user_1.posts.union_all(user_2.posts)
|
119
|
+
```
|
120
|
+
```sql
|
121
|
+
SELECT "posts".* FROM (
|
122
|
+
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1
|
123
|
+
UNION ALL
|
124
|
+
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 2
|
125
|
+
) "posts"
|
126
|
+
```
|
127
|
+
|
128
|
+
## Caveats
|
129
|
+
|
130
|
+
There's a couple things to be aware of when using ActiveRecordUnion:
|
131
|
+
|
132
|
+
1. ActiveRecordUnion with raise an error if you try to UNION any relations that do any preloading/eager-loading. There's no sensible way to do the preloading in the subselects. If enough people complain maybe we can change ActiveRecordUnion to let the queries run anyway but without preloading any records.
|
133
|
+
2. There's no easy way to get SQLite to allow ORDER BY in the UNION subselects. If you get a syntax error, you can either write `my_relation.reorder(nil).union(other.reorder(nil))` or switch to Postgres.
|
134
|
+
|
135
|
+
## Another nifty way to reduce extra queries
|
136
|
+
|
137
|
+
ActiveRecord already supports turning scopes into nested queries in WHERE clauses. The nested relation defaults to selecting `id` by default.
|
138
|
+
|
139
|
+
For example, if a user `has_and_belongs_to_many :favorited_posts`, we can quickly find which of the current user's posts are liked by a certain other user.
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
current_user.posts.where(id: other_user.favorited_posts)
|
143
|
+
```
|
144
|
+
```sql
|
145
|
+
SELECT "posts".* FROM "posts"
|
146
|
+
WHERE "posts"."user_id" = 1
|
147
|
+
AND "posts"."id" IN (
|
148
|
+
SELECT "posts"."id"
|
149
|
+
FROM "posts" INNER JOIN "user_favorited_posts" ON "posts"."id" = "user_favorited_posts"."post_id"
|
150
|
+
WHERE "user_favorited_posts"."user_id" = 2
|
151
|
+
)
|
152
|
+
```
|
153
|
+
|
154
|
+
If we want to select something other than `id`, we use `select` to specify. The following is equivalent to the above, but the query is done against the join table.
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
current_user.posts.where(id: UserFavoritedPost.where(user_id: other_user.id).select(:post_id))
|
158
|
+
```
|
159
|
+
```sql
|
160
|
+
SELECT "posts".* FROM "posts"
|
161
|
+
WHERE "posts"."user_id" = 1
|
162
|
+
AND "posts"."id" IN (
|
163
|
+
SELECT "user_favorited_posts"."post_id"
|
164
|
+
FROM "user_favorited_posts"
|
165
|
+
WHERE "user_favorited_posts"."user_id" = 2
|
166
|
+
)
|
167
|
+
```
|
168
|
+
|
169
|
+
(The above example is illustrative only. It might be better with a JOIN.)
|
170
|
+
|
171
|
+
## State of the Union in ActiveRecord
|
172
|
+
|
173
|
+
Why does this gem exist?
|
174
|
+
|
175
|
+
Right now in ActiveRecord, if we call `scope.union` we get an `Arel::Nodes::Union` object instead of an `ActiveRecord::Relation`.
|
176
|
+
|
177
|
+
We could call `to_sql` on the Arel object and then use `find_by_sql`, but that's not super clean. Also, on Rails 4.0 and 4.1 if the original scopes included an association then the `to_sql` may produce a query with values that need to be bound (represented by `?`s in the SQL) and we have to provide those ourselves. (E.g. `user.posts.to_sql` produces `SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?`.) Rails 4.2's `to_sql` replaces the bind values before showing the SQL string and thus can more readily be used with `find_by_sql`. (E.g. Rails 4.2 `to_sql` would say `WHERE "posts"."user_id" = 1` instead of `WHERE "posts"."user_id" = ?`.)
|
178
|
+
|
179
|
+
While ActiveRecord may eventually have the ability to cleanly perform UNIONs, it's currently stalled. If you're interested, the relevant URLs as of July 2014 are:
|
180
|
+
|
181
|
+
https://github.com/rails/rails/issues/939 and
|
182
|
+
https://github.com/rails/arel/pull/239 and
|
183
|
+
https://github.com/yakaz/rails/commit/29b8ebd187e0888d5e71b2e1e4a12334860bc76c
|
184
|
+
|
185
|
+
This is a gem not a Rails pull request because the standard of code quality for a PR is a bit higher, and we'd have to wait for the PR to be merged and relased to use UNIONs. That said, the code here is fairly clean and it may end up in a PR sometime.
|
186
|
+
|
187
|
+
## Changelog
|
188
|
+
|
189
|
+
**1.2.0** - June 26, 2016
|
190
|
+
- Ready for Rails 5.0! Updates provided by [@glebm](https://github.com/glebm).
|
191
|
+
|
192
|
+
**1.1.1** - Mar 19, 2016
|
193
|
+
- Fix broken polymorphic associations and joins due to improper handling of bind values. Fix by [@efradelos](https://github.com/efradelos), reported by [@Machiaweliczny](https://github.com/Machiaweliczny) and [@seandougall](https://github.com/seandougall).
|
194
|
+
- Quote table name aliases properly. Reported by [@odedniv](https://github.com/odedniv).
|
195
|
+
|
196
|
+
**1.1.0** - Mar 29, 2015 - Add UNION ALL support, courtesy of [@pic](https://github.com/pic).
|
197
|
+
|
198
|
+
**1.0.1** - Sept 2, 2014 - Allow ORDER BY in UNION subselects for databases that support it (not SQLite).
|
199
|
+
|
200
|
+
**1.0.0** - July 24, 2014 - Initial release.
|
201
|
+
|
202
|
+
## License
|
203
|
+
|
204
|
+
ActiveRecordUnion is dedicated to the public domain by its author, Brian Hempel. No rights are reserved. No restrictions are placed on the use of ActiveRecordUnion. That freedom also means, of course, that no warrenty of fitness is claimed; use ActiveRecordUnion at your own risk.
|
205
|
+
|
206
|
+
This public domain dedication follows the the CC0 1.0 at https://creativecommons.org/publicdomain/zero/1.0/
|
207
|
+
|
208
|
+
## Contributing
|
209
|
+
|
210
|
+
1. Fork it ( https://github.com/brianhempel/active_record_union/fork )
|
211
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
212
|
+
3. Run the tests:
|
213
|
+
1. Install MySQL and PostgreSQL.
|
214
|
+
2. You may need to create a `test_active_record_union` database on each under the default user.
|
215
|
+
3. Run `rake` to test with all supported Rails versions.
|
216
|
+
4. Run `rake test_rails_4_2` or `rake test_rails_5_0` to test a specific Rails version.
|
217
|
+
4. There is also a `bin/console` command to load up a REPL for playing around
|
218
|
+
5. Commit your changes (`git commit -am 'Add some feature'`)
|
219
|
+
6. Push to the branch (`git push origin my-new-feature`)
|
220
|
+
7. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
RSpec::Core::RakeTask.new(:spec)
|
5
|
+
task :default => :test_all_gemfiles
|
6
|
+
|
7
|
+
module TestTasks
|
8
|
+
module_function
|
9
|
+
|
10
|
+
TEST_CMD = 'bundle exec rspec'
|
11
|
+
|
12
|
+
def run_all(envs, cmd = "bundle install && #{TEST_CMD}", success_message)
|
13
|
+
statuses = envs.map { |env| run(env, cmd) }
|
14
|
+
failed = statuses.reject(&:first).map(&:last)
|
15
|
+
if failed.empty?
|
16
|
+
$stderr.puts success_message
|
17
|
+
else
|
18
|
+
$stderr.puts "❌ FAILING (#{failed.size}):\n#{failed.map { |env| to_bash_cmd_with_env(cmd, env) } * "\n"}"
|
19
|
+
exit 1
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def run_one(env, cmd = "bundle install && #{TEST_CMD}")
|
24
|
+
full_cmd = to_bash_cmd_with_env(cmd, env)
|
25
|
+
exec(full_cmd)
|
26
|
+
end
|
27
|
+
|
28
|
+
def run(env, cmd)
|
29
|
+
Bundler.with_clean_env do
|
30
|
+
full_cmd = to_bash_cmd_with_env(cmd, env)
|
31
|
+
$stderr.puts full_cmd
|
32
|
+
isSuccess = system(full_cmd)
|
33
|
+
[isSuccess, env]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def gemfiles
|
38
|
+
Dir.glob('*.gemfile').sort
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_bash_cmd_with_env(cmd, env)
|
42
|
+
"(export #{env.map { |k, v| "#{k}=#{v}" }.join(' ')}; #{cmd})"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
desc 'Test all Gemfiles'
|
47
|
+
task :test_all_gemfiles do
|
48
|
+
envs = TestTasks.gemfiles.map { |gemfile| { 'BUNDLE_GEMFILE' => gemfile } }
|
49
|
+
TestTasks.run_all envs, "✓ Tests pass with all #{envs.size} gemfiles"
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
TestTasks.gemfiles.each do |gemfile|
|
54
|
+
rails_version_underscored = gemfile[/rails_(.+)\.gemfile/, 1]
|
55
|
+
|
56
|
+
desc "Test Rails #{rails_version_underscored.gsub("_", ".")}"
|
57
|
+
task :"test_rails_#{rails_version_underscored}" do
|
58
|
+
env = { 'BUNDLE_GEMFILE' => gemfile }
|
59
|
+
TestTasks.run_one(env)
|
60
|
+
end
|
61
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# This is for development only.
|
4
|
+
|
5
|
+
require "bundler/setup"
|
6
|
+
|
7
|
+
Bundler.require(:development)
|
8
|
+
require "c80_active_record_union"
|
9
|
+
|
10
|
+
ActiveRecord::Base.establish_connection(
|
11
|
+
adapter: "sqlite3",
|
12
|
+
database: ":memory:"
|
13
|
+
)
|
14
|
+
|
15
|
+
require File.expand_path("../../spec/support/models.rb", __FILE__)
|
16
|
+
|
17
|
+
# extensions over models.rb to help with making the README
|
18
|
+
|
19
|
+
class User
|
20
|
+
has_and_belongs_to_many :favorited_posts,
|
21
|
+
class_name: "Post",
|
22
|
+
join_table: "user_favorited_posts"
|
23
|
+
end
|
24
|
+
|
25
|
+
class UserFavoritedPost < ActiveRecord::Base
|
26
|
+
connection.create_table :user_favorited_posts, force: true do |t|
|
27
|
+
t.integer :post_id
|
28
|
+
t.integer :user_id
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Post
|
33
|
+
scope :published, -> { where("published_at < ?", Time.now) }
|
34
|
+
end
|
35
|
+
|
36
|
+
binding.pry
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'c80_active_record_union/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "c80_active_record_union"
|
8
|
+
spec.version = C80ActiveRecordUnion::VERSION
|
9
|
+
spec.authors = ["Brian Hempel"]
|
10
|
+
spec.email = ["plasticchicken@gmail.com"]
|
11
|
+
spec.summary = %q{UNIONs in ActiveRecord! Adds proper union and union_all methods to ActiveRecord::Relation.}
|
12
|
+
spec.description = spec.summary
|
13
|
+
spec.homepage = "https://github.com/brianhempel/active_record_union"
|
14
|
+
spec.license = "Public Domain"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
# spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) + spec.files.grep(%r{^bin/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activerecord", ">= 4.0"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
26
|
+
spec.add_development_dependency "pry"
|
27
|
+
spec.add_development_dependency "sqlite3"
|
28
|
+
spec.add_development_dependency "pg"
|
29
|
+
spec.add_development_dependency "mysql2"
|
30
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
class Relation
|
3
|
+
module Union
|
4
|
+
|
5
|
+
SET_OPERATION_TO_AREL_CLASS = {
|
6
|
+
union: Arel::Nodes::Union,
|
7
|
+
union_all: Arel::Nodes::UnionAll
|
8
|
+
}
|
9
|
+
|
10
|
+
def union(relation_or_where_arg, *args)
|
11
|
+
set_operation(:union, relation_or_where_arg, *args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def union_all(relation_or_where_arg, *args)
|
15
|
+
set_operation(:union_all, relation_or_where_arg, *args)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def set_operation(operation, relation_or_where_arg, *args)
|
21
|
+
other = if args.size == 0 && Relation === relation_or_where_arg
|
22
|
+
relation_or_where_arg
|
23
|
+
else
|
24
|
+
@klass.where(relation_or_where_arg, *args)
|
25
|
+
end
|
26
|
+
|
27
|
+
# NB::2016-08-10
|
28
|
+
# verify_relations_for_set_operation!(operation, self, other)
|
29
|
+
|
30
|
+
# Postgres allows ORDER BY in the UNION subqueries if each subquery is surrounded by parenthesis
|
31
|
+
# but SQLite does not allow parens around the subqueries; you will have to explicitly do `relation.reorder(nil)` in SQLite
|
32
|
+
if Arel::Visitors::SQLite === self.connection.visitor
|
33
|
+
left, right = self.ast, other.ast
|
34
|
+
else
|
35
|
+
left, right = Arel::Nodes::Grouping.new(self.ast), Arel::Nodes::Grouping.new(other.ast)
|
36
|
+
end
|
37
|
+
|
38
|
+
set = SET_OPERATION_TO_AREL_CLASS[operation].new(left, right)
|
39
|
+
from = Arel::Nodes::TableAlias.new(set, @klass.arel_table.name)
|
40
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
41
|
+
relation = @klass.unscoped.spawn
|
42
|
+
relation.from_clause = UnionFromClause.new(from, nil, self.bound_attributes + other.bound_attributes)
|
43
|
+
else
|
44
|
+
relation = @klass.unscoped.from(from)
|
45
|
+
relation.bind_values = self.arel.bind_values + self.bind_values + other.arel.bind_values + other.bind_values
|
46
|
+
end
|
47
|
+
relation
|
48
|
+
end
|
49
|
+
|
50
|
+
def verify_relations_for_set_operation!(operation, *relations)
|
51
|
+
includes_relations = relations.select { |r| r.includes_values.any? }
|
52
|
+
|
53
|
+
if includes_relations.any?
|
54
|
+
raise ArgumentError.new("Cannot #{operation} relation with includes.")
|
55
|
+
end
|
56
|
+
|
57
|
+
preload_relations = relations.select { |r| r.preload_values.any? }
|
58
|
+
if preload_relations.any?
|
59
|
+
raise ArgumentError.new("Cannot #{operation} relation with preload.")
|
60
|
+
end
|
61
|
+
|
62
|
+
eager_load_relations = relations.select { |r| r.eager_load_values.any? }
|
63
|
+
if eager_load_relations.any?
|
64
|
+
raise ArgumentError.new("Cannot #{operation} relation with eager load.")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
69
|
+
class UnionFromClause < ActiveRecord::Relation::FromClause
|
70
|
+
def initialize(value, name, bound_attributes)
|
71
|
+
super(value, name)
|
72
|
+
@bound_attributes = bound_attributes
|
73
|
+
end
|
74
|
+
|
75
|
+
def binds
|
76
|
+
@bound_attributes
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/rails_4_2.gemfile
ADDED
data/rails_5_0.gemfile
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
|
3
|
+
Bundler.require(:development)
|
4
|
+
require "c80_active_record_union"
|
5
|
+
|
6
|
+
require "support/databases"
|
7
|
+
|
8
|
+
Databases.connect_to_sqlite
|
9
|
+
|
10
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
11
|
+
RSpec.configure do |config|
|
12
|
+
# Run specs in random order to surface order dependencies. If you find an
|
13
|
+
# order dependency and want to debug it, you can fix the order by providing
|
14
|
+
# the seed, which is printed after each run.
|
15
|
+
# --seed 1234
|
16
|
+
config.order = :random
|
17
|
+
|
18
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
19
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
20
|
+
# test failures related to randomization by passing the same `--seed` value
|
21
|
+
# as the one that triggered the failure.
|
22
|
+
Kernel.srand config.seed
|
23
|
+
|
24
|
+
config.expect_with :rspec do |expectations|
|
25
|
+
# Enable only the newer, non-monkey-patching expect syntax.
|
26
|
+
# For more details, see:
|
27
|
+
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
28
|
+
expectations.syntax = :expect
|
29
|
+
end
|
30
|
+
|
31
|
+
config.mock_with :rspec do |mocks|
|
32
|
+
# Enable only the newer, non-monkey-patching expect syntax.
|
33
|
+
# For more details, see:
|
34
|
+
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
35
|
+
mocks.syntax = :expect
|
36
|
+
|
37
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
38
|
+
# a real object. This is generally recommended.
|
39
|
+
mocks.verify_partial_doubles = true
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Databases
|
2
|
+
extend self
|
3
|
+
|
4
|
+
def connect_to_sqlite
|
5
|
+
ActiveRecord::Base.establish_connection(
|
6
|
+
adapter: "sqlite3",
|
7
|
+
database: ":memory:"
|
8
|
+
)
|
9
|
+
load("support/models.rb")
|
10
|
+
end
|
11
|
+
|
12
|
+
def connect_to_postgres
|
13
|
+
ActiveRecord::Base.establish_connection(
|
14
|
+
adapter: "postgresql"
|
15
|
+
)
|
16
|
+
ActiveRecord::Base.connection.recreate_database("test_active_record_union")
|
17
|
+
ActiveRecord::Base.establish_connection(
|
18
|
+
adapter: "postgresql",
|
19
|
+
database: "test_active_record_union"
|
20
|
+
)
|
21
|
+
load("support/models.rb")
|
22
|
+
end
|
23
|
+
|
24
|
+
def connect_to_mysql
|
25
|
+
ActiveRecord::Base.establish_connection(
|
26
|
+
adapter: "mysql2"
|
27
|
+
)
|
28
|
+
ActiveRecord::Base.connection.recreate_database("test_active_record_union")
|
29
|
+
ActiveRecord::Base.establish_connection(
|
30
|
+
adapter: "mysql2",
|
31
|
+
database: "test_active_record_union"
|
32
|
+
)
|
33
|
+
load("support/models.rb")
|
34
|
+
end
|
35
|
+
|
36
|
+
def with_postgres(&block)
|
37
|
+
connect_to_postgres
|
38
|
+
yield
|
39
|
+
ensure
|
40
|
+
connect_to_sqlite
|
41
|
+
end
|
42
|
+
|
43
|
+
def with_mysql(&block)
|
44
|
+
connect_to_mysql
|
45
|
+
yield
|
46
|
+
ensure
|
47
|
+
connect_to_sqlite
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
ActiveRecord::Base.connection.create_table :users, force: true do |t|
|
2
|
+
end
|
3
|
+
|
4
|
+
class User < ActiveRecord::Base
|
5
|
+
has_many :posts
|
6
|
+
has_many :drafts, -> { where draft: true }, class_name: "Post"
|
7
|
+
end unless defined?(User)
|
8
|
+
|
9
|
+
ActiveRecord::Base.connection.create_table :posts, force: true do |t|
|
10
|
+
t.integer :user_id
|
11
|
+
t.boolean :draft
|
12
|
+
t.timestamp :published_at
|
13
|
+
t.timestamps null: false
|
14
|
+
end
|
15
|
+
|
16
|
+
class Post < ActiveRecord::Base
|
17
|
+
belongs_to :user
|
18
|
+
end unless defined?(Post)
|
data/spec/union_spec.rb
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe ActiveRecord::Relation do
|
4
|
+
TIME = Time.utc(2014, 7, 19, 0, 0, 0)
|
5
|
+
SQL_TIME = ActiveRecord::VERSION::MAJOR >= 5 ? "2014-07-19 00:00:00" : "2014-07-19 00:00:00.000000"
|
6
|
+
|
7
|
+
describe ".union" do
|
8
|
+
it "returns an ActiveRecord::Relation" do
|
9
|
+
expect(User.all.union(User.all)).to be_kind_of(ActiveRecord::Relation)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "requires an argument" do
|
13
|
+
expect{User.all.union}.to raise_error(ArgumentError)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "explodes if asked to union a relation with includes" do
|
17
|
+
expect{User.all.union(User.includes(:posts))}.to raise_error(ArgumentError)
|
18
|
+
expect{User.includes(:posts).union(User.all)}.to raise_error(ArgumentError)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "explodes if asked to union a relation with preload values" do
|
22
|
+
expect{User.all.union(User.preload(:posts))}.to raise_error(ArgumentError)
|
23
|
+
expect{User.preload(:posts).union(User.all)}.to raise_error(ArgumentError)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "explodes if asked to union a relation with eager loading" do
|
27
|
+
expect{User.all.union(User.eager_load(:posts))}.to raise_error(ArgumentError)
|
28
|
+
expect{User.eager_load(:posts).union(User.all)}.to raise_error(ArgumentError)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "works" do
|
32
|
+
union = User.new(id: 1).posts.union(Post.where("created_at > ?", TIME))
|
33
|
+
|
34
|
+
expect(union.to_sql.squish).to eq(
|
35
|
+
"SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 UNION SELECT \"posts\".* FROM \"posts\" WHERE (created_at > '#{SQL_TIME}') ) \"posts\""
|
36
|
+
)
|
37
|
+
expect(union.arel.to_sql.squish).to eq(
|
38
|
+
"SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = ? UNION SELECT \"posts\".* FROM \"posts\" WHERE (created_at > '#{SQL_TIME}') ) \"posts\""
|
39
|
+
)
|
40
|
+
expect{union.to_a}.to_not raise_error
|
41
|
+
end
|
42
|
+
|
43
|
+
def bind_values_from_relation(relation)
|
44
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
45
|
+
relation.bound_attributes.map { |a| a.value_for_database }
|
46
|
+
else
|
47
|
+
(relation.arel.bind_values + relation.bind_values).map { |_column, value| value }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it "binds values properly" do
|
52
|
+
user1 = User.new(id: 1)
|
53
|
+
user2 = User.new(id: 2)
|
54
|
+
user3 = User.new(id: 3)
|
55
|
+
|
56
|
+
union = user1.posts.union(user2.posts).where.not(id: user3.posts)
|
57
|
+
|
58
|
+
# Inside ActiveRecord the bind value list is
|
59
|
+
# (union.arel.bind_values + union.bind_values)
|
60
|
+
bind_values = bind_values_from_relation union
|
61
|
+
|
62
|
+
expect(bind_values).to eq([1, 2, 3])
|
63
|
+
end
|
64
|
+
|
65
|
+
it "binds values properly on joins" do
|
66
|
+
union = User.joins(:drafts).union(User.where(id: 11))
|
67
|
+
|
68
|
+
bind_values = bind_values_from_relation union
|
69
|
+
expect(bind_values).to eq([true, 11])
|
70
|
+
|
71
|
+
|
72
|
+
expect(union.to_sql.squish).to eq(
|
73
|
+
"SELECT \"users\".* FROM ( SELECT \"users\".* FROM \"users\" INNER JOIN \"posts\" ON \"posts\".\"user_id\" = \"users\".\"id\" AND \"posts\".\"draft\" = 't' UNION SELECT \"users\".* FROM \"users\" WHERE \"users\".\"id\" = 11 ) \"users\""
|
74
|
+
)
|
75
|
+
expect{union.to_a}.to_not raise_error
|
76
|
+
end
|
77
|
+
|
78
|
+
it "doesn't repeat default scopes" do
|
79
|
+
expect(Time).to receive(:now) { Time.utc(2014, 7, 24, 0, 0, 0) }
|
80
|
+
sql_now = "2014-07-24 00:00:00#{".000000" if ActiveRecord::VERSION::MAJOR < 5}"
|
81
|
+
|
82
|
+
class PublishedPost < ActiveRecord::Base
|
83
|
+
self.table_name = "posts"
|
84
|
+
default_scope { where("published_at < ?", Time.now) }
|
85
|
+
end
|
86
|
+
|
87
|
+
union = PublishedPost.where("created_at > ?", TIME).union(User.new(id: 1).posts)
|
88
|
+
|
89
|
+
expect(union.to_sql.squish).to eq(
|
90
|
+
"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\""
|
91
|
+
)
|
92
|
+
expect{union.to_a}.to_not raise_error
|
93
|
+
end
|
94
|
+
|
95
|
+
context "with ORDER BY in subselects" do
|
96
|
+
let :union do
|
97
|
+
User.new(id: 1).posts.order(:created_at).union(
|
98
|
+
Post.where("created_at > ?", TIME).order(:created_at)
|
99
|
+
).order(:created_at)
|
100
|
+
end
|
101
|
+
|
102
|
+
context "in SQLite" do
|
103
|
+
it "lets ORDER BY in query subselects throw a syntax error" do
|
104
|
+
expect(union.to_sql.squish).to eq(
|
105
|
+
"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"
|
106
|
+
)
|
107
|
+
expect{union.to_a}.to raise_error(ActiveRecord::StatementInvalid)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
context "in Postgres" do
|
112
|
+
it "wraps query subselects in parentheses to allow ORDER BY clauses" do
|
113
|
+
Databases.with_postgres do
|
114
|
+
expect(union.to_sql.squish).to eq(
|
115
|
+
"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"
|
116
|
+
)
|
117
|
+
expect{union.to_a}.to_not raise_error
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context "in MySQL" do
|
123
|
+
it "wraps query subselects in parentheses to allow ORDER BY clauses" do
|
124
|
+
Databases.with_mysql do
|
125
|
+
expect(union.to_sql.squish).to eq(
|
126
|
+
"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"
|
127
|
+
)
|
128
|
+
expect{union.to_a}.to_not raise_error
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context "builds a scope when given" do
|
135
|
+
it "a hash" do
|
136
|
+
union = User.new(id: 1).posts.union(id: 2)
|
137
|
+
|
138
|
+
expect(union.to_sql.squish).to eq(
|
139
|
+
"SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 UNION SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"id\" = 2 ) \"posts\""
|
140
|
+
)
|
141
|
+
expect{union.to_a}.to_not raise_error
|
142
|
+
end
|
143
|
+
|
144
|
+
it "multiple arguments" do
|
145
|
+
union = User.new(id: 1).posts.union("created_at > ?", TIME)
|
146
|
+
|
147
|
+
expect(union.to_sql.squish).to eq(
|
148
|
+
"SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 UNION SELECT \"posts\".* FROM \"posts\" WHERE (created_at > '#{SQL_TIME}') ) \"posts\""
|
149
|
+
)
|
150
|
+
expect{union.to_a}.to_not raise_error
|
151
|
+
end
|
152
|
+
|
153
|
+
it "arel" do
|
154
|
+
union = User.new(id: 1).posts.union(Post.arel_table[:id].eq(2).or(Post.arel_table[:id].eq(3)))
|
155
|
+
|
156
|
+
expect(union.to_sql.squish).to eq(
|
157
|
+
"SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 UNION SELECT \"posts\".* FROM \"posts\" WHERE (\"posts\".\"id\" = 2 OR \"posts\".\"id\" = 3) ) \"posts\""
|
158
|
+
)
|
159
|
+
expect{union.to_a}.to_not raise_error
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
describe ".union_all" do
|
165
|
+
it "works" do
|
166
|
+
union = User.new(id: 1).posts.union_all(Post.where("created_at > ?", TIME))
|
167
|
+
|
168
|
+
expect(union.to_sql.squish).to eq(
|
169
|
+
"SELECT \"posts\".* FROM ( SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"user_id\" = 1 UNION ALL SELECT \"posts\".* FROM \"posts\" WHERE (created_at > '#{SQL_TIME}') ) \"posts\""
|
170
|
+
)
|
171
|
+
expect{union.to_a}.to_not raise_error
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
metadata
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: c80_active_record_union
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Hempel
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-08-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.6'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.6'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sqlite3
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pg
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: mysql2
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: UNIONs in ActiveRecord! Adds proper union and union_all methods to ActiveRecord::Relation.
|
126
|
+
email:
|
127
|
+
- plasticchicken@gmail.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- ".gitignore"
|
133
|
+
- ".idea/.rakeTasks"
|
134
|
+
- ".travis.yml"
|
135
|
+
- LICENSE.txt
|
136
|
+
- README.md
|
137
|
+
- Rakefile
|
138
|
+
- bin/console
|
139
|
+
- c80_active_record_union.gemspec
|
140
|
+
- lib/c80_active_record_union.rb
|
141
|
+
- lib/c80_active_record_union/active_record/relation/union.rb
|
142
|
+
- lib/c80_active_record_union/version.rb
|
143
|
+
- rails_4_2.gemfile
|
144
|
+
- rails_5_0.gemfile
|
145
|
+
- spec/spec_helper.rb
|
146
|
+
- spec/support/databases.rb
|
147
|
+
- spec/support/models.rb
|
148
|
+
- spec/union_spec.rb
|
149
|
+
homepage: https://github.com/brianhempel/active_record_union
|
150
|
+
licenses:
|
151
|
+
- Public Domain
|
152
|
+
metadata: {}
|
153
|
+
post_install_message:
|
154
|
+
rdoc_options: []
|
155
|
+
require_paths:
|
156
|
+
- lib
|
157
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
158
|
+
requirements:
|
159
|
+
- - ">="
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: '0'
|
162
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
requirements: []
|
168
|
+
rubyforge_project:
|
169
|
+
rubygems_version: 2.5.1
|
170
|
+
signing_key:
|
171
|
+
specification_version: 4
|
172
|
+
summary: UNIONs in ActiveRecord! Adds proper union and union_all methods to ActiveRecord::Relation.
|
173
|
+
test_files:
|
174
|
+
- spec/spec_helper.rb
|
175
|
+
- spec/support/databases.rb
|
176
|
+
- spec/support/models.rb
|
177
|
+
- spec/union_spec.rb
|
178
|
+
- bin/console
|