ar_outer_joins 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ bundler_stubs
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ar_outer_joins.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # ActiveRecord Outer Joins
2
+
3
+ This gem adds support for the missing `.outer_joins` method. For the most part
4
+ it behaves exactly the same as `.joins`, except that it creates an outer join
5
+ instead of an inner join. If you don't know why that would be useful, this gem
6
+ is probably not for you.
7
+
8
+ ## Installation
9
+
10
+ ``` ruby
11
+ gem "ar_outer_joins"
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ``` ruby
17
+ require "active_record"
18
+ require "ar_outer_joins"
19
+
20
+ class Product
21
+ belongs_to :category
22
+
23
+ def published
24
+ outer_joins(:category).where("categories.published = ? OR products.published = ?", true, true)
25
+ end
26
+ end
27
+ ```
28
+
29
+ # License
30
+
31
+ (The MIT License)
32
+
33
+ Copyright (c) 2012 Jonas Nicklas, Elabs AB
34
+
35
+ Permission is hereby granted, free of charge, to any person obtaining
36
+ a copy of this software and associated documentation files (the
37
+ 'Software'), to deal in the Software without restriction, including
38
+ without limitation the rights to use, copy, modify, merge, publish,
39
+ distribute, sublicense, and/or sell copies of the Software, and to
40
+ permit persons to whom the Software is furnished to do so, subject to
41
+ the following conditions:
42
+
43
+ The above copyright notice and this permission notice shall be
44
+ included in all copies or substantial portions of the Software.
45
+
46
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
47
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
48
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
49
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
50
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
51
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
52
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/ar_outer_joins/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Jonas Nicklas", "Elabs AB"]
6
+ gem.email = ["jonas.nicklas@gmail.com", "dev@elabs.se"]
7
+ gem.description = %q{Adds the missing outer_joins method to ActiveRecord}
8
+ gem.summary = %q{outer_joins for ActiveRecord}
9
+ gem.homepage = "http://github.com/elabs/ar_outer_joins"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "ar_outer_joins"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = ArOuterJoins::VERSION
17
+
18
+ gem.add_dependency "activerecord", "~>3.2"
19
+ gem.add_development_dependency "rspec"
20
+ gem.add_development_dependency "sqlite3"
21
+ gem.add_development_dependency "pry"
22
+ end
@@ -0,0 +1,15 @@
1
+ require "active_record"
2
+ require "ar_outer_joins/version"
3
+ require "ar_outer_joins/join_builder"
4
+ require "ar_outer_joins/join"
5
+
6
+ module ArOuterJoins
7
+ def outer_joins(*joins)
8
+ association_joins, regular_joins = joins.partition do |join|
9
+ join.is_a?(Hash) or join.is_a?(Array) or join.is_a?(Symbol)
10
+ end
11
+ Join.new(self).apply(*association_joins).joins(*regular_joins)
12
+ end
13
+ end
14
+
15
+ ActiveRecord::Base.extend ArOuterJoins
@@ -0,0 +1,26 @@
1
+ module ArOuterJoins
2
+ class Join
3
+ attr_reader :klass
4
+
5
+ def initialize(klass)
6
+ @klass = klass
7
+ end
8
+
9
+ def generate(*args)
10
+ args.flatten.compact.map do |arg|
11
+ if arg.is_a?(Hash)
12
+ arg.map do |key, value|
13
+ association = klass.reflect_on_association(key)
14
+ generate(key) + Join.new(association.klass).generate(value)
15
+ end
16
+ else
17
+ JoinBuilder.new(klass.reflect_on_association(arg)).build
18
+ end
19
+ end
20
+ end
21
+
22
+ def apply(*args)
23
+ klass.joins(generate(*args))
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,43 @@
1
+ module ArOuterJoins
2
+ class JoinBuilder
3
+ class OuterJoinError < StandardError; end
4
+
5
+ attr_reader :association
6
+
7
+ def initialize(association)
8
+ @association = association
9
+ end
10
+
11
+ def build
12
+ if association.is_a? ActiveRecord::Reflection::ThroughReflection
13
+ [
14
+ JoinBuilder.new(association.through_reflection).build,
15
+ JoinBuilder.new(association.source_reflection).build
16
+ ].flatten
17
+ else
18
+ table = association.active_record.arel_table
19
+ primary_key = association.active_record.primary_key
20
+ joined_table = association.klass.arel_table
21
+
22
+ case association.macro
23
+ when :belongs_to
24
+ on = Arel::Nodes::On.new(table[association.foreign_key].eq(joined_table[primary_key]))
25
+ [Arel::Nodes::OuterJoin.new(joined_table, on)]
26
+ when :has_and_belongs_to_many
27
+ join_model_table = Arel::Table.new(association.options[:join_table])
28
+ joined_primary_key = association.klass.primary_key
29
+
30
+ on1 = Arel::Nodes::On.new(join_model_table[association.foreign_key].eq(table[primary_key]))
31
+ on2 = Arel::Nodes::On.new(join_model_table[association.association_foreign_key].eq(joined_table[joined_primary_key]))
32
+
33
+ [Arel::Nodes::OuterJoin.new(join_model_table, on1), Arel::Nodes::OuterJoin.new(joined_table, on2)]
34
+ when :has_many, :has_one
35
+ on = Arel::Nodes::On.new(joined_table[association.foreign_key].eq(table[primary_key]))
36
+ [Arel::Nodes::OuterJoin.new(joined_table, on)]
37
+ else
38
+ raise OuterJoinError, "don't know what to do with #{association.macro} association"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,3 @@
1
+ module ArOuterJoins
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,200 @@
1
+ require "spec_helper"
2
+
3
+ class Product < ActiveRecord::Base
4
+ belongs_to :category
5
+ belongs_to :site
6
+ has_many :line_items
7
+ has_many :baskets, :through => :line_items
8
+ has_many :discounts, :through => :line_items
9
+ has_and_belongs_to_many :tags
10
+ has_one :image
11
+ end
12
+
13
+ class LineItem < ActiveRecord::Base
14
+ belongs_to :basket
15
+ belongs_to :product
16
+ has_many :discounts
17
+ end
18
+
19
+ class Image < ActiveRecord::Base
20
+ belongs_to :product
21
+ end
22
+
23
+ class Discount < ActiveRecord::Base
24
+ belongs_to :line_item
25
+ end
26
+
27
+ class Basket < ActiveRecord::Base; end
28
+ class Category < ActiveRecord::Base; end
29
+ class Site < ActiveRecord::Base; end
30
+ class Tag < ActiveRecord::Base; end
31
+
32
+ describe ActiveRecord::Base do
33
+ describe ".outer_joins" do
34
+ context "with belongs_to" do
35
+ it "performs an outer join" do
36
+ category1 = Category.create! :name => "Shoes"
37
+ category2 = Category.create! :name => "Shirts"
38
+ product1 = Product.create! :category => category1
39
+ product2 = Product.create! :category => category2
40
+ product3 = Product.create! :published => true
41
+ query = Product.outer_joins(:category).where("categories.name = ? OR products.published = ?", "Shirts", true)
42
+ query.all.should =~ [product2, product3]
43
+ end
44
+
45
+ it "joins several associations" do
46
+ site1 = Site.create! :name => "Elabs"
47
+ category1 = Category.create! :name => "Shoes"
48
+ category2 = Category.create! :name => "Shirts"
49
+ product1 = Product.create! :category => category1
50
+ product2 = Product.create! :category => category2
51
+ product3 = Product.create! :published => true
52
+ product4 = Product.create! :site => site1
53
+ query = Product.outer_joins(:category, :site).where("sites.name = ? OR categories.name = ? OR products.published = ?", "Elabs", "Shirts", true)
54
+ query.all.should =~ [product2, product3, product4]
55
+ end
56
+ end
57
+
58
+ context "with has_one" do
59
+ it "performs an outer join" do
60
+ product1 = Product.create!
61
+ product2 = Product.create!
62
+ product3 = Product.create! :published => true
63
+ product4 = Product.create!
64
+
65
+ Image.create! :highres => true, :product => product1
66
+ Image.create! :product => product2
67
+
68
+ query = Product.outer_joins(:image).where("images.highres = ? OR products.published = ?", true, true)
69
+ query.all.should =~ [product1, product3]
70
+ end
71
+ end
72
+
73
+ context "with has_many" do
74
+ it "performs an outer join" do
75
+ product1 = Product.create!
76
+ product2 = Product.create!
77
+ product3 = Product.create! :published => true
78
+ product4 = Product.create!
79
+
80
+ LineItem.create! :price => 4, :product => product1
81
+ LineItem.create! :product => product2
82
+
83
+ query = Product.outer_joins(:line_items).where("line_items.price = ? OR products.published = ?", 4, true)
84
+ query.all.should =~ [product1, product3]
85
+ end
86
+ end
87
+
88
+ context "with has_and_belongs_to_many" do
89
+ it "performs an outer join" do
90
+ red = Tag.create! :name => "Red"
91
+ blue = Tag.create! :name => "Blue"
92
+
93
+ product1 = Product.create!
94
+ product2 = Product.create! :tags => [red]
95
+ product3 = Product.create! :tags => [red, blue]
96
+ product4 = Product.create! :published => true
97
+
98
+
99
+ query = Product.outer_joins(:tags).where("tags.name = ? OR products.published = ?", "Red", true)
100
+ query.all.should =~ [product2, product3, product4]
101
+ end
102
+ end
103
+
104
+ context "with has_many :through" do
105
+ it "performs an outer join" do
106
+ product1 = Product.create!
107
+ product2 = Product.create!
108
+ product3 = Product.create! :published => true
109
+ product4 = Product.create!
110
+
111
+ basket1 = Basket.create! :purchased => true
112
+ basket2 = Basket.create! :purchased => false
113
+
114
+ LineItem.create! :product => product1, :basket => basket1
115
+ LineItem.create! :product => product2, :basket => basket2
116
+ LineItem.create! :product => product3
117
+
118
+ query = Product.outer_joins(:baskets).where("baskets.purchased = ? OR products.published = ?", true, true)
119
+ query.all.should =~ [product1, product3]
120
+ end
121
+ end
122
+
123
+ context "with nested associations" do
124
+ it "allows hashes" do
125
+ product1 = Product.create!
126
+ product2 = Product.create!
127
+ product3 = Product.create! :published => true
128
+ product4 = Product.create!
129
+
130
+ basket1 = Basket.create! :purchased => true
131
+ basket2 = Basket.create! :purchased => false
132
+
133
+ LineItem.create! :product => product1, :basket => basket1
134
+ LineItem.create! :product => product2, :basket => basket2
135
+ LineItem.create! :product => product3
136
+
137
+ query = Product.outer_joins(:line_items => :basket).where("baskets.purchased = ? OR products.published = ?", true, true)
138
+ query.all.should =~ [product1, product3]
139
+ end
140
+
141
+ it "allows hashes with arrays" do
142
+ product1 = Product.create!
143
+ product2 = Product.create!
144
+ product3 = Product.create! :published => true
145
+ product4 = Product.create!
146
+
147
+ basket1 = Basket.create! :purchased => true
148
+ basket2 = Basket.create! :purchased => false
149
+
150
+ line_item1 = LineItem.create! :product => product1, :basket => basket1
151
+ line_item2 = LineItem.create! :product => product2, :basket => basket2
152
+ line_item3 = LineItem.create! :product => product4
153
+
154
+ Discount.create! :line_item => line_item3, :percentage => 80
155
+
156
+ query = Product.outer_joins(:line_items => [:basket, :discounts]).where("baskets.purchased = ? OR products.published = ? OR discounts.percentage > ?", true, true, 50)
157
+ query.all.should =~ [product1, product3, product4]
158
+ end
159
+ end
160
+
161
+ context "with raw hash" do
162
+ it "allows a join as a string" do
163
+ category1 = Category.create! :name => "Shoes"
164
+ category2 = Category.create! :name => "Shirts"
165
+ product1 = Product.create! :category => category1
166
+ product2 = Product.create! :category => category2
167
+ product3 = Product.create! :published => true
168
+ query = Product.outer_joins("LEFT OUTER JOIN categories ON products.category_id = categories.id")
169
+ query = query.where("categories.name = ? OR products.published = ?", "Shirts", true)
170
+ query.all.should =~ [product2, product3]
171
+ end
172
+
173
+ it "allows multiple joins" do
174
+ category1 = Category.create! :name => "Shoes"
175
+ category2 = Category.create! :name => "Shirts"
176
+ product1 = Product.create! :category => category1
177
+ product2 = Product.create! :category => category2
178
+ product3 = Product.create! :published => true
179
+ query = Product.outer_joins(:line_items, "LEFT OUTER JOIN categories ON products.category_id = categories.id")
180
+ query = query.where("categories.name = ? OR products.published = ?", "Shirts", true)
181
+ query.all.should =~ [product2, product3]
182
+ end
183
+
184
+ it "allows arel joins" do
185
+ category1 = Category.create! :name => "Shoes"
186
+ category2 = Category.create! :name => "Shirts"
187
+ product1 = Product.create! :category => category1
188
+ product2 = Product.create! :category => category2
189
+ product3 = Product.create! :published => true
190
+
191
+ on = Arel::Nodes::On.new(Product.arel_table[:category_id].eq(Category.arel_table[:id]))
192
+ join = Arel::Nodes::OuterJoin.new(Category.arel_table, on)
193
+
194
+ query = Product.outer_joins(join)
195
+ query = query.where("categories.name = ? OR products.published = ?", "Shirts", true)
196
+ query.all.should =~ [product2, product3]
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,57 @@
1
+ require "active_record"
2
+ require "ar_outer_joins"
3
+ require "pry"
4
+
5
+ ActiveRecord::Base.establish_connection :adapter => "sqlite3", :database => ":memory:"
6
+
7
+ ActiveRecord::Base.connection.create_table :products do |t|
8
+ t.integer :category_id
9
+ t.integer :site_id
10
+ t.boolean :published, :default => false, :null => false
11
+ end
12
+
13
+ ActiveRecord::Base.connection.create_table :line_items do |t|
14
+ t.integer :product_id
15
+ t.integer :basket_id
16
+ t.integer :discount_id
17
+ t.integer :price, :default => 0, :null => false
18
+ end
19
+
20
+ ActiveRecord::Base.connection.create_table :baskets do |t|
21
+ t.boolean :purchased, :default => false, :null => false
22
+ end
23
+
24
+ ActiveRecord::Base.connection.create_table :categories do |t|
25
+ t.string :name
26
+ end
27
+
28
+ ActiveRecord::Base.connection.create_table :sites do |t|
29
+ t.string :name
30
+ end
31
+
32
+ ActiveRecord::Base.connection.create_table :discounts do |t|
33
+ t.integer :percentage, :default => 0, :null => false
34
+ t.integer :line_item_id
35
+ end
36
+
37
+ ActiveRecord::Base.connection.create_table :tags do |t|
38
+ t.string :name
39
+ end
40
+
41
+ ActiveRecord::Base.connection.create_table :products_tags, :id => false do |t|
42
+ t.integer :product_id, :tag_id
43
+ end
44
+
45
+ ActiveRecord::Base.connection.create_table :images do |t|
46
+ t.boolean :highres, :default => false, :null => false
47
+ t.integer :product_id
48
+ end
49
+
50
+ RSpec.configure do |config|
51
+ config.around do |example|
52
+ ActiveRecord::Base.transaction do
53
+ example.run
54
+ raise ActiveRecord::Rollback
55
+ end
56
+ end
57
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ar_outer_joins
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jonas Nicklas
9
+ - Elabs AB
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-11-02 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activerecord
17
+ requirement: &2159798540 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: '3.2'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *2159798540
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ requirement: &2159798120 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: *2159798120
37
+ - !ruby/object:Gem::Dependency
38
+ name: sqlite3
39
+ requirement: &2159797660 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ type: :development
46
+ prerelease: false
47
+ version_requirements: *2159797660
48
+ - !ruby/object:Gem::Dependency
49
+ name: pry
50
+ requirement: &2159797200 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ type: :development
57
+ prerelease: false
58
+ version_requirements: *2159797200
59
+ description: Adds the missing outer_joins method to ActiveRecord
60
+ email:
61
+ - jonas.nicklas@gmail.com
62
+ - dev@elabs.se
63
+ executables: []
64
+ extensions: []
65
+ extra_rdoc_files: []
66
+ files:
67
+ - .gitignore
68
+ - Gemfile
69
+ - README.md
70
+ - Rakefile
71
+ - ar_outer_joins.gemspec
72
+ - lib/ar_outer_joins.rb
73
+ - lib/ar_outer_joins/join.rb
74
+ - lib/ar_outer_joins/join_builder.rb
75
+ - lib/ar_outer_joins/version.rb
76
+ - spec/outer_join_spec.rb
77
+ - spec/spec_helper.rb
78
+ homepage: http://github.com/elabs/ar_outer_joins
79
+ licenses: []
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubyforge_project:
98
+ rubygems_version: 1.8.10
99
+ signing_key:
100
+ specification_version: 3
101
+ summary: outer_joins for ActiveRecord
102
+ test_files:
103
+ - spec/outer_join_spec.rb
104
+ - spec/spec_helper.rb
105
+ has_rdoc: