joiner 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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