ar_outer_joins 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: