joiner 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 95831ffeed5e8c01635e9d2a9719daf72b7db24f
4
+ data.tar.gz: b76e140e99fa969a716e69f35396ff7a1f323cbb
5
+ SHA512:
6
+ metadata.gz: 94ff8d0bf4e2acce3ce4d3fd4f0fd6ad46bd2ce91936564a6ab970ce9a7db518dac9cbc4d25c41a6eeb06e060595d0285033477b0a560a1372d0cbdb3e61e8ec
7
+ data.tar.gz: a1927657a5db57b18891242ee5def0c1c9d1f111fa3b75eeeda9b404ba6f6d9319d849714ecd6373e33912376c7de0d0b4edb480da158b9368b01bed2a79c5cb
@@ -0,0 +1,16 @@
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/internal/db/combustion_test.sqlite
15
+ spec/reports
16
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Pat Allan
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,60 @@
1
+ # Joiner
2
+
3
+ This gem, abstracted out from [Thinking Sphinx](http://pat.github.io/thinking-sphinx), turns a bunch of association trees from the perspective of a single model and builds a bunch of OUTER JOINs that can be passed into ActiveRecord::Relation's `join` method. You can also find out the generated table aliases for each join, in case you're referring to columns from those joins at some other point.
4
+
5
+ If this gem is used by anyone other than myself/Thinking Sphinx, I'll be surprised. My reason for pulling it out is so I can more cleanly support Rails' changing approaches to join generation (see v3.1-4.0 compared to v4.1).
6
+
7
+ ## Installation
8
+
9
+ It's a gem - so you can either install it yourself, or add it to the appropriate Gemfile or gemspec.
10
+
11
+ ```term
12
+ gem install joiner --version 0.1.0
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ First, create a join collection, based on an ActiveRecord model:
18
+
19
+ ```ruby
20
+ joiner = Joiner::Joins.new user
21
+ ```
22
+
23
+ Then you can add joins for a given association path. For example, if User has many articles, and articles have many comments:
24
+
25
+ ```ruby
26
+ joiner.add_join_to [:articles]
27
+ joiner.add_join_to [:articles, :comments]
28
+ ```
29
+
30
+ If you need the table/join alias for a given association path, just ask for it:
31
+
32
+ ```ruby
33
+ joiner.alias_for([:articles, :comments])
34
+ ```
35
+
36
+ And once you've loaded up all the joins, you'll want something you can push out into `ActiveRecord::Relation#joins`:
37
+
38
+ ```ruby
39
+ User.joins(joiner.join_values)
40
+ ```
41
+
42
+ You can also check if a given association path will return potentially more than one record (thus perhaps requiring aggregation), or find out what the model at the end of the path is:
43
+
44
+ ```ruby
45
+ path = Joiner::Path.new(User, [:articles, :comments])
46
+ path.aggregate? #=> true
47
+ path.model #=> Comment
48
+ ```
49
+
50
+ ## Contributing
51
+
52
+ 1. Fork it
53
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
54
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
55
+ 4. Push to the branch (`git push origin my-new-feature`)
56
+ 5. Create new Pull Request
57
+
58
+ ## Licence
59
+
60
+ Copyright (c) 2013, Joiner is developed and maintained by [Pat Allan](http://freelancing-gods.com), and is released under the open MIT Licence.
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,21 @@
1
+ # coding: utf-8
2
+ Gem::Specification.new do |spec|
3
+ spec.name = 'joiner'
4
+ spec.version = '0.1.0'
5
+ spec.authors = ['Pat Allan']
6
+ spec.email = ['pat@freelancing-gods.com']
7
+ spec.summary = %q{Builds ActiveRecord joins from association paths}
8
+ spec.description = %q{Builds ActiveRecord outer joins from association paths and provides references to table aliases.}
9
+ spec.homepage = 'https://github.com/pat/joiner'
10
+ spec.license = 'MIT'
11
+
12
+ spec.files = `git ls-files`.split($/)
13
+ spec.test_files = spec.files.grep(%r{^(spec)/})
14
+ spec.require_paths = ['lib']
15
+
16
+ spec.add_runtime_dependency 'activerecord', ['>= 3.1.0', '< 4.1.0']
17
+
18
+ spec.add_development_dependency 'combustion', '~> 0.5.1'
19
+ spec.add_development_dependency 'rspec-rails', '~> 2.14.1'
20
+ spec.add_development_dependency 'sqlite3', '~> 1.3.8'
21
+ end
@@ -0,0 +1,6 @@
1
+ module Joiner
2
+ #
3
+ end
4
+
5
+ require 'joiner/joins'
6
+ require 'joiner/path'
@@ -0,0 +1,81 @@
1
+ class Joiner::Joins
2
+ JoinDependency = ::ActiveRecord::Associations::JoinDependency
3
+
4
+ attr_reader :model
5
+
6
+ def initialize(model)
7
+ @model = model
8
+ @joins = ActiveSupport::OrderedHash.new
9
+ end
10
+
11
+ def add_join_to(path)
12
+ join_for(path)
13
+ end
14
+
15
+ def alias_for(path)
16
+ return model.quoted_table_name if path.empty?
17
+
18
+ join_for(path).aliased_table_name
19
+ end
20
+
21
+ def join_values
22
+ @joins.values.compact
23
+ end
24
+
25
+ private
26
+
27
+ def base
28
+ @base ||= JoinDependency.new model, [], []
29
+ end
30
+
31
+ def join_for(path)
32
+ @joins[path] ||= begin
33
+ reflection = reflection_for path
34
+ reflection.nil? ? nil : JoinDependency::JoinAssociation.new(
35
+ reflection, base, parent_join_for(path)
36
+ ).tap { |join|
37
+ join.join_type = Arel::OuterJoin
38
+
39
+ rewrite_conditions_for join
40
+ }
41
+ end
42
+ end
43
+
44
+ def joins_for(path)
45
+ if path.length == 1
46
+ [join_for(path)]
47
+ else
48
+ [joins_for(path[0..-2]), join_for(path)].flatten
49
+ end
50
+ end
51
+
52
+ def parent_for(path)
53
+ path.length == 1 ? base : join_for(path[0..-2])
54
+ end
55
+
56
+ def parent_join_for(path)
57
+ path.length == 1 ? base.join_base : parent_for(path)
58
+ end
59
+
60
+ def reflection_for(path)
61
+ parent = parent_for(path)
62
+ klass = parent.respond_to?(:base_klass) ? parent.base_klass :
63
+ parent.active_record
64
+ klass.reflections[path.last]
65
+ end
66
+
67
+ def rewrite_conditions_for(join)
68
+ if join.respond_to?(:scope_chain)
69
+ conditions = Array(join.scope_chain).flatten
70
+ else
71
+ conditions = Array(join.conditions).flatten
72
+ end
73
+
74
+ conditions.each do |condition|
75
+ next unless condition.is_a?(String)
76
+
77
+ condition.gsub! /::ts_join_alias::/,
78
+ model.connection.quote_table_name(join.parent.aliased_table_name)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,33 @@
1
+ class Joiner::Path
2
+ AGGREGATE_MACROS = [:has_many, :has_and_belongs_to_many]
3
+
4
+ def initialize(base, path)
5
+ @base, @path = base, path
6
+ end
7
+
8
+ def aggregate?
9
+ macros.any? { |macro| AGGREGATE_MACROS.include? macro }
10
+ end
11
+
12
+ def macros
13
+ reflections.collect(&:macro)
14
+ end
15
+
16
+ def model
17
+ path.empty? ? base : reflections.last.try(:klass)
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :base, :path
23
+
24
+ def reflections
25
+ klass = base
26
+ path.collect { |reference|
27
+ klass.reflect_on_association(reference).tap { |reflection|
28
+ return [] if reflection.nil?
29
+ klass = reflection.klass
30
+ }
31
+ }
32
+ end
33
+ end
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Joiner' do
4
+ it "handles has many associations" do
5
+ joiner = Joiner::Joins.new User
6
+ joiner.add_join_to [:articles]
7
+
8
+ sql = User.joins(joiner.join_values).to_sql
9
+ expect(sql).to match(/LEFT OUTER JOIN \"articles\"/)
10
+ end
11
+
12
+ it "handles multiple has many associations separately" do
13
+ joiner = Joiner::Joins.new User
14
+ joiner.add_join_to [:articles]
15
+ joiner.add_join_to [:articles, :comments]
16
+
17
+ sql = User.joins(joiner.join_values).to_sql
18
+ expect(sql).to match(/LEFT OUTER JOIN \"articles\"/)
19
+ expect(sql).to match(/LEFT OUTER JOIN \"comments\"/)
20
+ end
21
+
22
+ it "handles multiple has many associations together" do
23
+ joiner = Joiner::Joins.new User
24
+ joiner.add_join_to [:articles, :comments]
25
+
26
+ sql = User.joins(joiner.join_values).to_sql
27
+ expect(sql).to match(/LEFT OUTER JOIN \"articles\"/)
28
+ expect(sql).to match(/LEFT OUTER JOIN \"comments\"/)
29
+ end
30
+
31
+ it "handles a belongs to association" do
32
+ joiner = Joiner::Joins.new Comment
33
+ joiner.add_join_to [:article]
34
+
35
+ sql = Comment.joins(joiner.join_values).to_sql
36
+ expect(sql).to match(/LEFT OUTER JOIN \"articles\"/)
37
+ end
38
+
39
+ it "handles both belongs to and has many associations separately" do
40
+ joiner = Joiner::Joins.new Article
41
+ joiner.add_join_to [:user]
42
+ joiner.add_join_to [:comments]
43
+
44
+ sql = Article.joins(joiner.join_values).to_sql
45
+ expect(sql).to match(/LEFT OUTER JOIN \"users\"/)
46
+ expect(sql).to match(/LEFT OUTER JOIN \"comments\"/)
47
+ end
48
+
49
+ it "handles both belongs to and has many associations together" do
50
+ joiner = Joiner::Joins.new Article
51
+ joiner.add_join_to [:user, :comments]
52
+
53
+ sql = Article.joins(joiner.join_values).to_sql
54
+ expect(sql).to match(/LEFT OUTER JOIN \"users\"/)
55
+ expect(sql).to match(/LEFT OUTER JOIN \"comments\"/)
56
+ end
57
+
58
+ it "distinguishes joins via different relationships" do
59
+ joiner = Joiner::Joins.new Article
60
+ joiner.add_join_to [:comments]
61
+ joiner.add_join_to [:user, :comments]
62
+
63
+ expect(joiner.alias_for([:comments])).to eq('comments')
64
+ expect(joiner.alias_for([:user, :comments])).to eq('comments_users')
65
+ end
66
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Paths' do
4
+ describe 'Aggregations' do
5
+ it "indicates aggregation for has many associations" do
6
+ path = Joiner::Path.new User, [:articles]
7
+
8
+ expect(path).to be_aggregate
9
+ end
10
+
11
+ it "indicates non-aggregation for belongs to association" do
12
+ path = Joiner::Path.new Article, [:user]
13
+
14
+ expect(path).to_not be_aggregate
15
+ end
16
+
17
+ it "indicates non-aggregation when the path is empty" do
18
+ path = Joiner::Path.new Article, []
19
+
20
+ expect(path).to_not be_aggregate
21
+ end
22
+ end
23
+
24
+ describe 'models' do
25
+ it "determines the underlying model for an association path" do
26
+ path = Joiner::Path.new User, [:articles, :comments]
27
+
28
+ expect(path.model).to eq(Comment)
29
+ end
30
+
31
+ it "returns the base model if the path is empty" do
32
+ path = Joiner::Path.new User, []
33
+
34
+ expect(path.model).to eq(User)
35
+ end
36
+
37
+ it "returns nil if the path is invalid" do
38
+ path = Joiner::Path.new User, [:articles, :likes]
39
+
40
+ expect(path.model).to be_nil
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,4 @@
1
+ class Article < ActiveRecord::Base
2
+ has_many :comments
3
+ belongs_to :user
4
+ end
@@ -0,0 +1,4 @@
1
+ class Comment < ActiveRecord::Base
2
+ belongs_to :article
3
+ belongs_to :comments
4
+ end
@@ -0,0 +1,4 @@
1
+ class User < ActiveRecord::Base
2
+ has_many :articles
3
+ has_many :comments
4
+ end
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: db/combustion_test.sqlite
@@ -0,0 +1,16 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table :articles, :force => true do |t|
3
+ t.integer :user_id
4
+ t.timestamps
5
+ end
6
+
7
+ create_table :comments, :force => true do |t|
8
+ t.integer :article_id
9
+ t.integer :user_id
10
+ t.timestamps
11
+ end
12
+
13
+ create_table :users, :force => true do |t|
14
+ t.timestamps
15
+ end
16
+ end
@@ -0,0 +1 @@
1
+ *.log
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'combustion'
5
+
6
+ Combustion.initialize! :active_record
7
+
8
+ require 'rspec/rails'
9
+
10
+ RSpec.configure do |config|
11
+ config.use_transactional_fixtures = true
12
+ end
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: joiner
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Pat Allan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-11 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: 3.1.0
20
+ - - <
21
+ - !ruby/object:Gem::Version
22
+ version: 4.1.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 3.1.0
30
+ - - <
31
+ - !ruby/object:Gem::Version
32
+ version: 4.1.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: combustion
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ~>
38
+ - !ruby/object:Gem::Version
39
+ version: 0.5.1
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: 0.5.1
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec-rails
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 2.14.1
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ version: 2.14.1
61
+ - !ruby/object:Gem::Dependency
62
+ name: sqlite3
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ~>
66
+ - !ruby/object:Gem::Version
67
+ version: 1.3.8
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ~>
73
+ - !ruby/object:Gem::Version
74
+ version: 1.3.8
75
+ description: Builds ActiveRecord outer joins from association paths and provides references
76
+ to table aliases.
77
+ email:
78
+ - pat@freelancing-gods.com
79
+ executables: []
80
+ extensions: []
81
+ extra_rdoc_files: []
82
+ files:
83
+ - .gitignore
84
+ - Gemfile
85
+ - LICENSE.txt
86
+ - README.md
87
+ - Rakefile
88
+ - joiner.gemspec
89
+ - lib/joiner.rb
90
+ - lib/joiner/joins.rb
91
+ - lib/joiner/path.rb
92
+ - spec/acceptance/joiner_spec.rb
93
+ - spec/acceptance/paths_spec.rb
94
+ - spec/internal/app/models/article.rb
95
+ - spec/internal/app/models/comment.rb
96
+ - spec/internal/app/models/user.rb
97
+ - spec/internal/config/database.yml
98
+ - spec/internal/db/schema.rb
99
+ - spec/internal/log/.gitignore
100
+ - spec/spec_helper.rb
101
+ homepage: https://github.com/pat/joiner
102
+ licenses:
103
+ - MIT
104
+ metadata: {}
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - '>='
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubyforge_project:
121
+ rubygems_version: 2.1.11
122
+ signing_key:
123
+ specification_version: 4
124
+ summary: Builds ActiveRecord joins from association paths
125
+ test_files:
126
+ - spec/acceptance/joiner_spec.rb
127
+ - spec/acceptance/paths_spec.rb
128
+ - spec/internal/app/models/article.rb
129
+ - spec/internal/app/models/comment.rb
130
+ - spec/internal/app/models/user.rb
131
+ - spec/internal/config/database.yml
132
+ - spec/internal/db/schema.rb
133
+ - spec/internal/log/.gitignore
134
+ - spec/spec_helper.rb