joiner-0.3.4/0000755000004100000410000000000012576036453013047 5ustar www-datawww-datajoiner-0.3.4/Rakefile0000644000004100000410000000003412576036453014511 0ustar www-datawww-datarequire 'bundler/gem_tasks' joiner-0.3.4/Gemfile0000644000004100000410000000004712576036453014343 0ustar www-datawww-datasource 'https://rubygems.org' gemspec joiner-0.3.4/LICENSE.txt0000644000004100000410000000205212576036453014671 0ustar www-datawww-dataCopyright (c) 2014 Pat Allan MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. joiner-0.3.4/spec/0000755000004100000410000000000012576036453014001 5ustar www-datawww-datajoiner-0.3.4/spec/spec_helper.rb0000644000004100000410000000030112576036453016611 0ustar www-datawww-datarequire 'rubygems' require 'bundler/setup' require 'combustion' Combustion.initialize! :all require 'rspec/rails' RSpec.configure do |config| config.use_transactional_fixtures = true end joiner-0.3.4/spec/internal/0000755000004100000410000000000012576036453015615 5ustar www-datawww-datajoiner-0.3.4/spec/internal/log/0000755000004100000410000000000012576036453016376 5ustar www-datawww-datajoiner-0.3.4/spec/internal/log/.gitignore0000644000004100000410000000000512576036453020361 0ustar www-datawww-data*.logjoiner-0.3.4/spec/internal/db/0000755000004100000410000000000012576036453016202 5ustar www-datawww-datajoiner-0.3.4/spec/internal/db/schema.rb0000644000004100000410000000047712576036453017777 0ustar www-datawww-dataActiveRecord::Schema.define do create_table :articles, :force => true do |t| t.integer :user_id t.timestamps end create_table :comments, :force => true do |t| t.integer :article_id t.integer :user_id t.timestamps end create_table :users, :force => true do |t| t.timestamps end end joiner-0.3.4/spec/internal/config/0000755000004100000410000000000012576036453017062 5ustar www-datawww-datajoiner-0.3.4/spec/internal/config/database.yml0000644000004100000410000000010012576036453021340 0ustar www-datawww-datatest: adapter: sqlite3 database: db/combustion_test.sqlite joiner-0.3.4/spec/internal/app/0000755000004100000410000000000012576036453016375 5ustar www-datawww-datajoiner-0.3.4/spec/internal/app/models/0000755000004100000410000000000012576036453017660 5ustar www-datawww-datajoiner-0.3.4/spec/internal/app/models/comment.rb0000644000004100000410000000012012576036453021640 0ustar www-datawww-dataclass Comment < ActiveRecord::Base belongs_to :article belongs_to :user end joiner-0.3.4/spec/internal/app/models/user.rb0000644000004100000410000000011612576036453021161 0ustar www-datawww-dataclass User < ActiveRecord::Base has_many :articles has_many :comments end joiner-0.3.4/spec/internal/app/models/article.rb0000644000004100000410000000011712576036453021627 0ustar www-datawww-dataclass Article < ActiveRecord::Base has_many :comments belongs_to :user end joiner-0.3.4/spec/acceptance/0000755000004100000410000000000012576036453016067 5ustar www-datawww-datajoiner-0.3.4/spec/acceptance/joiner_spec.rb0000644000004100000410000000465112576036453020722 0ustar www-datawww-datarequire 'spec_helper' describe 'Joiner' do it "handles has many associations" do joiner = Joiner::Joins.new User joiner.add_join_to [:articles] sql = User.joins(joiner.join_values).to_sql expect(sql).to match(/LEFT OUTER JOIN \"articles\"/) end it "handles multiple has many associations separately" do joiner = Joiner::Joins.new User joiner.add_join_to [:articles] joiner.add_join_to [:articles, :comments] sql = User.joins(joiner.join_values).to_sql expect(sql).to match(/LEFT OUTER JOIN \"articles\"/) expect(sql).to match(/LEFT OUTER JOIN \"comments\"/) end it "handles multiple has many associations together" do joiner = Joiner::Joins.new User joiner.add_join_to [:articles, :comments] sql = User.joins(joiner.join_values).to_sql expect(sql).to match(/LEFT OUTER JOIN \"articles\"/) expect(sql).to match(/LEFT OUTER JOIN \"comments\"/) end it "handles a belongs to association" do joiner = Joiner::Joins.new Comment joiner.add_join_to [:article] sql = Comment.joins(joiner.join_values).to_sql expect(sql).to match(/LEFT OUTER JOIN \"articles\"/) end it "handles both belongs to and has many associations separately" do joiner = Joiner::Joins.new Article joiner.add_join_to [:user] joiner.add_join_to [:comments] sql = Article.joins(joiner.join_values).to_sql expect(sql).to match(/LEFT OUTER JOIN \"users\"/) expect(sql).to match(/LEFT OUTER JOIN \"comments\"/) end it "handles both belongs to and has many associations together" do joiner = Joiner::Joins.new Article joiner.add_join_to [:user, :comments] sql = Article.joins(joiner.join_values).to_sql expect(sql).to match(/LEFT OUTER JOIN \"users\"/) expect(sql).to match(/LEFT OUTER JOIN \"comments\"/) end it "distinguishes joins via different relationships" do joiner = Joiner::Joins.new Article joiner.add_join_to [:comments] joiner.add_join_to [:user, :comments] expect(joiner.alias_for([:comments])).to eq('comments') expect(joiner.alias_for([:user, :comments])).to eq('comments_users') end it 'handles simple and deep chains' do joiner = Joiner::Joins.new Article joiner.add_join_to [:comments] joiner.add_join_to [:comments, :user, :articles] expect(joiner.alias_for([:comments])).to eq('comments') expect(joiner.alias_for([:comments, :user, :articles])).to eq( 'articles_users' ) end end joiner-0.3.4/spec/acceptance/paths_spec.rb0000644000004100000410000000210012576036453020536 0ustar www-datawww-datarequire 'spec_helper' describe 'Paths' do describe 'Aggregations' do it "indicates aggregation for has many associations" do path = Joiner::Path.new User, [:articles] expect(path).to be_aggregate end it "indicates non-aggregation for belongs to association" do path = Joiner::Path.new Article, [:user] expect(path).to_not be_aggregate end it "indicates non-aggregation when the path is empty" do path = Joiner::Path.new Article, [] expect(path).to_not be_aggregate end end describe 'models' do it "determines the underlying model for an association path" do path = Joiner::Path.new User, [:articles, :comments] expect(path.model).to eq(Comment) end it "returns the base model if the path is empty" do path = Joiner::Path.new User, [] expect(path.model).to eq(User) end it "raises an exception if the path is invalid" do path = Joiner::Path.new User, [:articles, :likes] expect { path.model }.to raise_error(Joiner::AssociationNotFound) end end end joiner-0.3.4/lib/0000755000004100000410000000000012576036453013615 5ustar www-datawww-datajoiner-0.3.4/lib/joiner.rb0000644000004100000410000000020112576036453015421 0ustar www-datawww-datarequire 'set' module Joiner class AssociationNotFound < StandardError end end require 'joiner/joins' require 'joiner/path' joiner-0.3.4/lib/joiner/0000755000004100000410000000000012576036453015103 5ustar www-datawww-datajoiner-0.3.4/lib/joiner/path.rb0000644000004100000410000000134212576036453016364 0ustar www-datawww-dataclass Joiner::Path AGGREGATE_MACROS = [:has_many, :has_and_belongs_to_many] def initialize(base, path) @base, @path = base, path end def aggregate? macros.any? { |macro| AGGREGATE_MACROS.include? macro } end def macros reflections.collect(&:macro) end def model path.empty? ? base : reflections.last.try(:klass) end private attr_reader :base, :path def reflections klass = base path.collect { |reference| klass.reflect_on_association(reference).tap { |reflection| if reflection.nil? raise Joiner::AssociationNotFound, "No association matching #{base.name}, #{path.join(', ')}" end klass = reflection.klass } } end end joiner-0.3.4/lib/joiner/joins.rb0000644000004100000410000000241212576036453016551 0ustar www-datawww-datarequire 'active_record' require 'active_support/ordered_hash' class Joiner::Joins JoinDependency = ActiveRecord::Associations::JoinDependency JoinAssociation = JoinDependency::JoinAssociation attr_reader :model attr_accessor :join_association_class def initialize(model) @model = model @joins_cache = Set.new end def add_join_to(path) return if path.empty? joins_cache.add path_as_hash(path) end def alias_for(path) return model.table_name if path.empty? add_join_to path join_association_for(path).tables.first.name end def join_values switch_join_dependency join_association_class result = JoinDependency.new model, joins_cache.to_a, [] switch_join_dependency JoinAssociation result end private attr_reader :joins_cache def join_association_for(path) path.inject(join_values.join_root) do |node, piece| node.children.detect { |child| child.reflection.name == piece } end end def path_as_hash(path) path[0..-2].reverse.inject(path.last) { |key, item| {item => key} } end def switch_join_dependency(klass) return unless join_association_class JoinDependency.send :remove_const, :JoinAssociation JoinDependency.const_set :JoinAssociation, klass end end joiner-0.3.4/metadata.yml0000644000004100000410000000673612576036453015366 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: joiner version: !ruby/object:Gem::Version version: 0.3.4 platform: ruby authors: - Pat Allan autorequire: bindir: bin cert_chain: [] date: 2014-11-17 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: activerecord requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 4.1.0 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 4.1.0 - !ruby/object:Gem::Dependency name: combustion requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 0.5.1 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 0.5.1 - !ruby/object:Gem::Dependency name: rails requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 4.1.2 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 4.1.2 - !ruby/object:Gem::Dependency name: rspec-rails requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 2.14.1 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 2.14.1 - !ruby/object:Gem::Dependency name: sqlite3 requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 1.3.8 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: 1.3.8 description: Builds ActiveRecord outer joins from association paths and provides references to table aliases. email: - pat@freelancing-gods.com executables: [] extensions: [] extra_rdoc_files: [] files: - ".gitignore" - Gemfile - LICENSE.txt - README.md - Rakefile - joiner.gemspec - lib/joiner.rb - lib/joiner/joins.rb - lib/joiner/path.rb - spec/acceptance/joiner_spec.rb - spec/acceptance/paths_spec.rb - spec/internal/app/models/article.rb - spec/internal/app/models/comment.rb - spec/internal/app/models/user.rb - spec/internal/config/database.yml - spec/internal/db/schema.rb - spec/internal/log/.gitignore - spec/spec_helper.rb homepage: https://github.com/pat/joiner licenses: - MIT metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 2.3.0 signing_key: specification_version: 4 summary: Builds ActiveRecord joins from association paths test_files: - spec/acceptance/joiner_spec.rb - spec/acceptance/paths_spec.rb - spec/internal/app/models/article.rb - spec/internal/app/models/comment.rb - spec/internal/app/models/user.rb - spec/internal/config/database.yml - spec/internal/db/schema.rb - spec/internal/log/.gitignore - spec/spec_helper.rb joiner-0.3.4/joiner.gemspec0000644000004100000410000000163212576036453015704 0ustar www-datawww-data# coding: utf-8 Gem::Specification.new do |spec| spec.name = 'joiner' spec.version = '0.3.4' spec.authors = ['Pat Allan'] spec.email = ['pat@freelancing-gods.com'] spec.summary = %q{Builds ActiveRecord joins from association paths} spec.description = %q{Builds ActiveRecord outer joins from association paths and provides references to table aliases.} spec.homepage = 'https://github.com/pat/joiner' spec.license = 'MIT' spec.files = `git ls-files`.split($/) spec.test_files = spec.files.grep(%r{^(spec)/}) spec.require_paths = ['lib'] spec.add_runtime_dependency 'activerecord', '>= 4.1.0' spec.add_development_dependency 'combustion', '~> 0.5.1' spec.add_development_dependency 'rails', '>= 4.1.2' spec.add_development_dependency 'rspec-rails', '~> 2.14.1' spec.add_development_dependency 'sqlite3', '~> 1.3.8' end joiner-0.3.4/.gitignore0000644000004100000410000000025012576036453015034 0ustar www-datawww-data*.gem *.rbc .bundle .config .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/internal/db/combustion_test.sqlite spec/reports tmp joiner-0.3.4/README.md0000644000004100000410000000412512576036453014330 0ustar www-datawww-data# Joiner 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. 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). ## Installation It's a gem - so you can either install it yourself, or add it to the appropriate Gemfile or gemspec. ```term gem install joiner --version 0.3.4 ``` ## Usage First, create a join collection, based on an ActiveRecord model: ```ruby joiner = Joiner::Joins.new user ``` Then you can add joins for a given association path. For example, if User has many articles, and articles have many comments: ```ruby joiner.add_join_to [:articles] joiner.add_join_to [:articles, :comments] ``` If you need the table/join alias for a given association path, just ask for it: ```ruby joiner.alias_for([:articles, :comments]) ``` And once you've loaded up all the joins, you'll want something you can push out into `ActiveRecord::Relation#joins`: ```ruby User.joins(joiner.join_values) ``` 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: ```ruby path = Joiner::Path.new(User, [:articles, :comments]) path.aggregate? #=> true path.model #=> Comment ``` ## Contributing 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request ## Licence Copyright (c) 2013, Joiner is developed and maintained by [Pat Allan](http://freelancing-gods.com), and is released under the open MIT Licence.