├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── polyamorous.rb └── polyamorous │ ├── activerecord_3_and_4.0_ruby_1.9 │ ├── join_association.rb │ └── join_dependency.rb │ ├── activerecord_4.1_ruby_1.9 │ ├── join_association.rb │ └── join_dependency.rb │ ├── activerecord_4.1_ruby_2 │ ├── join_association.rb │ ├── join_dependency.rb │ └── make_polyamorous_inner_joins.rb │ ├── activerecord_4.2_ruby_1.9 │ ├── join_association.rb │ └── join_dependency.rb │ ├── activerecord_4.2_ruby_2 │ ├── join_association.rb │ └── join_dependency.rb │ ├── activerecord_5.0_ruby_2 │ ├── join_association.rb │ └── join_dependency.rb │ ├── activerecord_5.1_ruby_2 │ ├── join_association.rb │ └── join_dependency.rb │ ├── activerecord_5.2_ruby_2 │ ├── join_association.rb │ └── join_dependency.rb │ ├── join.rb │ ├── swapping_reflection_class.rb │ ├── tree_node.rb │ └── version.rb ├── polyamorous.gemspec └── spec ├── blueprints ├── articles.rb ├── comments.rb ├── notes.rb ├── people.rb └── tags.rb ├── helpers └── polyamorous_helper.rb ├── polyamorous ├── join_association_spec.rb ├── join_dependency_spec.rb └── join_spec.rb ├── spec_helper.rb └── support └── schema.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | .ruby-version 6 | .ruby-gemset 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | sudo: false 4 | cache: bundler 5 | 6 | before_install: 7 | - travis_retry gem install bundler 8 | 9 | rvm: 10 | - 2.5.0 11 | - 2.4.3 12 | - 2.3.6 13 | - 2.2.9 14 | 15 | env: 16 | - RAILS=4-2-stable AREL=6-0-stable DB=sqlite 17 | - RAILS=4-2-stable AREL=6-0-stable DB=mysql 18 | - RAILS=4-2-stable AREL=6-0-stable DB=postgres 19 | - RAILS=5-0-stable AREL=7-0-stable DB=sqlite 20 | - RAILS=5-0-stable AREL=7-0-stable DB=mysql 21 | - RAILS=5-0-stable AREL=7-0-stable DB=postgres 22 | - RAILS=5-1-stable AREL=8-0-stable DB=sqlite 23 | - RAILS=5-1-stable AREL=8-0-stable DB=mysql 24 | - RAILS=5-1-stable AREL=8-0-stable DB=postgres 25 | 26 | matrix: 27 | include: 28 | - rvm: 2.5.0 29 | env: RAILS=master DB=sqlite3 30 | - rvm: 2.5.0 31 | env: RAILS=master DB=mysql 32 | - rvm: 2.5.0 33 | env: RAILS=master DB=postgres 34 | 35 | - rvm: 2.4.3 36 | env: RAILS=master DB=sqlite3 37 | - rvm: 2.4.3 38 | env: RAILS=master DB=mysql 39 | - rvm: 2.4.3 40 | env: RAILS=master DB=postgres 41 | 42 | - rvm: 2.3.6 43 | env: RAILS=master DB=sqlite3 44 | - rvm: 2.3.6 45 | env: RAILS=master DB=mysql 46 | - rvm: 2.3.6 47 | env: RAILS=master DB=postgres 48 | 49 | - rvm: 2.2.9 50 | env: RAILS=master DB=sqlite3 51 | - rvm: 2.2.9 52 | env: RAILS=master DB=mysql 53 | - rvm: 2.2.9 54 | env: RAILS=master DB=postgres 55 | 56 | before_script: 57 | - mysql -e 'create database ransack collate utf8_general_ci;' 58 | - mysql -e 'use ransack;show variables like "%character%";show variables like "%collation%";' 59 | - psql -c 'create database ransack;' -U postgres 60 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | 4 | gem 'rake' 5 | 6 | rails = ENV['RAILS'] || 'master' 7 | 8 | if rails == 'master' 9 | arel = ENV['AREL'] || 'master' 10 | arel_opts = 11 | case arel 12 | when /\// # A path 13 | { path: arel } 14 | when /^v/ # A tagged version 15 | { git: 'git://github.com/rails/arel.git', tag: arel } 16 | else 17 | { git: 'git://github.com/rails/arel.git', branch: arel } 18 | end 19 | gem 'arel', arel_opts 20 | end 21 | 22 | case rails 23 | when /\// # A path 24 | gem 'activerecord', path: "#{rails}/activerecord" 25 | when /^v/ # A tagged version 26 | git 'git://github.com/rails/rails.git', tag: rails do 27 | gem 'activerecord' 28 | end 29 | else 30 | git 'git://github.com/rails/rails.git', branch: rails do 31 | gem 'activerecord' 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Ernie Miller 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Polyamorous 2 | 3 | ## Announcement 4 | 5 | Polyamorous is merged into Ransack since Squeel and MetaSearch is not maintained anymore. 6 | 7 | 8 | [![Build Status](https://travis-ci.org/activerecord-hackery/polyamorous.svg?branch=master)](https://travis-ci.org/activerecord-hackery/polyamorous) 9 | [![Gem Version](https://badge.fury.io/rb/polyamorous.svg)](https://badge.fury.io/rb/polyamorous) 10 | [![Code Climate](https://codeclimate.com/github/activerecord-hackery/polyamorous/badges/gpa.svg)](https://codeclimate.com/github/activerecord-hackery/polyamorous) 11 | 12 | Polyamorous is an extraction of shared code from the 13 | [Active Record Hackery](https://github.com/activerecord-hackery) gems 14 | [Ransack](https://github.com/activerecord-hackery/ransack), 15 | [Squeel](https://github.com/activerecord-hackery/squeel) and 16 | [MetaSearch](https://github.com/activerecord-hackery/meta_search) by 17 | [Ernie Miller](http://twitter.com/erniemiller) and maintained by 18 | [Ryan Bigg](http://twitter.com/ryanbigg), 19 | [Xiang Li](http://bigxiang.github.io), 20 | [Jon Atack](http://twitter.com/jonatack), 21 | [Sean Carroll](https://github.com/seanfcarroll) and a great little group of 22 | [contributors] 23 | (https://github.com/activerecord-hackery/polyamorous/graphs/contributors). 24 | 25 | It is an internal library for extending various versions of Active Record with 26 | polymorphism. There is no public API, so it's `:nodoc:`. Move along. Nothing to 27 | see here. 28 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) do |rspec| 5 | rspec.rspec_opts = ['--backtrace'] 6 | end 7 | 8 | task :default => :spec 9 | 10 | desc "Open an irb session with Ransack and the sample data used in specs" 11 | task :console do 12 | require 'irb' 13 | require 'irb/completion' 14 | require 'console' 15 | ARGV.clear 16 | IRB.start 17 | end -------------------------------------------------------------------------------- /lib/polyamorous.rb: -------------------------------------------------------------------------------- 1 | require 'polyamorous/version' 2 | 3 | if defined?(::ActiveRecord) 4 | module Polyamorous 5 | if defined?(Arel::InnerJoin) 6 | InnerJoin = Arel::InnerJoin 7 | OuterJoin = Arel::OuterJoin 8 | else 9 | InnerJoin = Arel::Nodes::InnerJoin 10 | OuterJoin = Arel::Nodes::OuterJoin 11 | end 12 | 13 | if defined?(::ActiveRecord::Associations::JoinDependency) 14 | JoinDependency = ::ActiveRecord::Associations::JoinDependency 15 | JoinAssociation = ::ActiveRecord::Associations::JoinDependency::JoinAssociation 16 | JoinBase = ::ActiveRecord::Associations::JoinDependency::JoinBase 17 | else 18 | JoinDependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency 19 | JoinAssociation = ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation 20 | JoinBase = ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinBase 21 | end 22 | end 23 | 24 | require 'polyamorous/tree_node' 25 | require 'polyamorous/join' 26 | require 'polyamorous/swapping_reflection_class' 27 | 28 | ar_version = ::ActiveRecord::VERSION::STRING[0,3] 29 | ar_version = '3_and_4.0' if ar_version < '4.1' 30 | 31 | method, ruby_version = 32 | if RUBY_VERSION >= '2.0' && ar_version >= '4.1' 33 | # Ruby 2; we can use `prepend` to patch Active Record cleanly. 34 | [:prepend, '2'] 35 | else 36 | # Ruby 1.9; we must use `alias_method` to patch Active Record. 37 | [:include, '1.9'] 38 | end 39 | 40 | %w(join_association join_dependency).each do |file| 41 | require "polyamorous/activerecord_#{ar_version}_ruby_#{ruby_version}/#{file}" 42 | end 43 | 44 | Polyamorous::JoinDependency.send(method, Polyamorous::JoinDependencyExtensions) 45 | if method == :prepend 46 | Polyamorous::JoinDependency.singleton_class 47 | .send(:prepend, Polyamorous::JoinDependencyExtensions::ClassMethods) 48 | end 49 | Polyamorous::JoinAssociation.send(method, Polyamorous::JoinAssociationExtensions) 50 | 51 | Polyamorous::JoinBase.class_eval do 52 | if method_defined?(:active_record) 53 | alias_method :base_klass, :active_record 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/polyamorous/activerecord_3_and_4.0_ruby_1.9/join_association.rb: -------------------------------------------------------------------------------- 1 | # active_record_3_and_4.0_ruby_1.9/join_association.rb 2 | module Polyamorous 3 | module JoinAssociationExtensions 4 | include SwappingReflectionClass 5 | def self.included(base) 6 | base.class_eval do 7 | alias_method_chain :initialize, :polymorphism 8 | alias_method :equality_without_polymorphism, :== 9 | alias_method :==, :equality_with_polymorphism 10 | if base.method_defined?(:active_record) 11 | alias_method :base_klass, :active_record 12 | end 13 | 14 | if ActiveRecord::VERSION::STRING =~ /^3\.0\./ 15 | alias_method_chain :association_join, :polymorphism 16 | else 17 | alias_method_chain :build_constraint, :polymorphism 18 | end 19 | end 20 | end 21 | 22 | def initialize_with_polymorphism( 23 | reflection, join_dependency, parent = nil, polymorphic_class = nil 24 | ) 25 | if polymorphic_class && ::ActiveRecord::Base > polymorphic_class 26 | swapping_reflection_klass(reflection, polymorphic_class) do |reflection| 27 | initialize_without_polymorphism(reflection, join_dependency, parent) 28 | self.reflection.options[:polymorphic] = true 29 | end 30 | else 31 | initialize_without_polymorphism(reflection, join_dependency, parent) 32 | end 33 | end 34 | 35 | def equality_with_polymorphism(other) 36 | equality_without_polymorphism(other) && base_klass == other.base_klass 37 | end 38 | 39 | def build_constraint_with_polymorphism( 40 | reflection, table, key, foreign_table, foreign_key 41 | ) 42 | if reflection.options[:polymorphic] 43 | build_constraint_without_polymorphism( 44 | reflection, table, key, foreign_table, foreign_key 45 | ) 46 | .and(foreign_table[reflection.foreign_type].eq(reflection.klass.name)) 47 | else 48 | build_constraint_without_polymorphism( 49 | reflection, table, key, foreign_table, foreign_key 50 | ) 51 | end 52 | end 53 | 54 | def association_join_with_polymorphism 55 | return @join if @Join 56 | @join = association_join_without_polymorphism 57 | if reflection.macro == :belongs_to && reflection.options[:polymorphic] 58 | aliased_table = Arel::Table.new( 59 | table_name, 60 | as: @aliased_table_name, 61 | engine: arel_engine, 62 | columns: klass.columns 63 | ) 64 | parent_table = Arel::Table.new( 65 | parent.table_name, 66 | as: parent.aliased_table_name, 67 | engine: arel_engine, 68 | columns: parent.base_klass.columns 69 | ) 70 | @join << parent_table[reflection.options[:foreign_type]] 71 | .eq(reflection.klass.name) 72 | end 73 | @join 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/polyamorous/activerecord_3_and_4.0_ruby_1.9/join_dependency.rb: -------------------------------------------------------------------------------- 1 | # active_record_3_and_4.0_ruby_1.9/join_dependency.rb 2 | module Polyamorous 3 | module JoinDependencyExtensions 4 | def self.included(base) 5 | base.class_eval do 6 | alias_method_chain :build, :polymorphism 7 | alias_method_chain :graft, :polymorphism 8 | if base.method_defined?(:active_record) 9 | alias_method :base_klass, :active_record 10 | end 11 | end 12 | end 13 | 14 | def graft_with_polymorphism(*associations) 15 | associations.each do |association| 16 | unless join_associations.detect { |a| association == a } 17 | if association.reflection.options[:polymorphic] 18 | build( 19 | Join.new( 20 | association.reflection.name, 21 | association.join_type, 22 | association.reflection.klass 23 | ), 24 | association.find_parent_in(self) || join_base, 25 | association.join_type 26 | ) 27 | else 28 | build( 29 | association.reflection.name, 30 | association.find_parent_in(self) || join_base, 31 | association.join_type 32 | ) 33 | end 34 | end 35 | end 36 | self 37 | end 38 | 39 | if ActiveRecord::VERSION::STRING =~ /^3\.0\./ 40 | def _join_parts 41 | @joins 42 | end 43 | else 44 | def _join_parts 45 | @join_parts 46 | end 47 | end 48 | 49 | def build_with_polymorphism( 50 | associations, parent = nil, join_type = InnerJoin 51 | ) 52 | case associations 53 | when Join 54 | parent ||= _join_parts.last 55 | reflection = parent.reflections[associations.name] or 56 | raise ::ActiveRecord::ConfigurationError, 57 | "Association named '#{associations.name 58 | }' was not found; perhaps you misspelled it?" 59 | 60 | unless join_association = find_join_association_respecting_polymorphism( 61 | reflection, parent, associations.klass 62 | ) 63 | @reflections << reflection 64 | join_association = build_join_association_respecting_polymorphism( 65 | reflection, parent, associations.klass 66 | ) 67 | join_association.join_type = associations.type 68 | _join_parts << join_association 69 | cache_joined_association(join_association) 70 | end 71 | 72 | join_association 73 | else 74 | build_without_polymorphism(associations, parent, join_type) 75 | end 76 | end 77 | 78 | def find_join_association_respecting_polymorphism(reflection, parent, klass) 79 | if association = find_join_association(reflection, parent) 80 | unless reflection.options[:polymorphic] 81 | association 82 | else 83 | association if association.base_klass == klass 84 | end 85 | end 86 | end 87 | 88 | def build_join_association_respecting_polymorphism(reflection, parent, klass) 89 | if reflection.options[:polymorphic] && klass 90 | JoinAssociation.new(reflection, self, parent, klass) 91 | else 92 | JoinAssociation.new(reflection, self, parent) 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/polyamorous/activerecord_4.1_ruby_1.9/join_association.rb: -------------------------------------------------------------------------------- 1 | # active_record_4.1_ruby_1.9/join_association.rb 2 | require 'polyamorous/activerecord_4.2_ruby_1.9/join_association' 3 | -------------------------------------------------------------------------------- /lib/polyamorous/activerecord_4.1_ruby_1.9/join_dependency.rb: -------------------------------------------------------------------------------- 1 | # active_record_4.1_ruby_1.9/join_dependency.rb 2 | require 'polyamorous/activerecord_4.2_ruby_2/join_dependency' 3 | require 'polyamorous/activerecord_4.2_ruby_1.9/join_dependency' 4 | require 'polyamorous/activerecord_4.1_ruby_2/make_polyamorous_inner_joins' 5 | -------------------------------------------------------------------------------- /lib/polyamorous/activerecord_4.1_ruby_2/join_association.rb: -------------------------------------------------------------------------------- 1 | # active_record_4.1_ruby_2/join_association.rb 2 | require 'polyamorous/activerecord_5.0_ruby_2/join_association' 3 | -------------------------------------------------------------------------------- /lib/polyamorous/activerecord_4.1_ruby_2/join_dependency.rb: -------------------------------------------------------------------------------- 1 | # active_record_4.1_ruby_2/join_dependency.rb 2 | require 'polyamorous/activerecord_4.2_ruby_2/join_dependency' 3 | require 'polyamorous/activerecord_4.1_ruby_2/make_polyamorous_inner_joins' 4 | -------------------------------------------------------------------------------- /lib/polyamorous/activerecord_4.1_ruby_2/make_polyamorous_inner_joins.rb: -------------------------------------------------------------------------------- 1 | module Polyamorous 2 | module JoinDependencyExtensions 3 | # Replaces ActiveRecord::Associations::JoinDependency#make_inner_joins 4 | # 5 | def make_polyamorous_inner_joins(parent, child) 6 | make_constraints( 7 | parent, child, child.tables, child.join_type || Arel::Nodes::InnerJoin 8 | ) 9 | .concat child.children.flat_map { |c| 10 | make_polyamorous_inner_joins(child, c) 11 | } 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/polyamorous/activerecord_4.2_ruby_1.9/join_association.rb: -------------------------------------------------------------------------------- 1 | # active_record_4.2_ruby_1.9/join_association.rb 2 | module Polyamorous 3 | module JoinAssociationExtensions 4 | include SwappingReflectionClass 5 | def self.included(base) 6 | base.class_eval do 7 | attr_reader :join_type 8 | alias_method_chain :initialize, :polymorphism 9 | alias_method_chain :build_constraint, :polymorphism 10 | end 11 | end 12 | 13 | def initialize_with_polymorphism(reflection, children, 14 | polymorphic_class = nil, join_type = Arel::Nodes::InnerJoin) 15 | @join_type = join_type 16 | if polymorphic_class && ::ActiveRecord::Base > polymorphic_class 17 | swapping_reflection_klass(reflection, polymorphic_class) do |reflection| 18 | initialize_without_polymorphism(reflection, children) 19 | self.reflection.options[:polymorphic] = true 20 | end 21 | else 22 | initialize_without_polymorphism(reflection, children) 23 | end 24 | end 25 | 26 | # Reference https://github.com/rails/rails/commit/9b15db51b78028bfecdb85595624de4b838adbd1 27 | def ==(other) 28 | base_klass == other.base_klass 29 | end 30 | 31 | def build_constraint_with_polymorphism( 32 | klass, table, key, foreign_table, foreign_key 33 | ) 34 | if reflection.polymorphic? 35 | build_constraint_without_polymorphism( 36 | klass, table, key, foreign_table, foreign_key 37 | ) 38 | .and(foreign_table[reflection.foreign_type].eq(reflection.klass.name)) 39 | else 40 | build_constraint_without_polymorphism( 41 | klass, table, key, foreign_table, foreign_key 42 | ) 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/polyamorous/activerecord_4.2_ruby_1.9/join_dependency.rb: -------------------------------------------------------------------------------- 1 | # active_record_4.2_ruby_1.9/join_dependency.rb 2 | require 'polyamorous/activerecord_4.2_ruby_2/join_dependency' 3 | 4 | module Polyamorous 5 | module JoinDependencyExtensions 6 | def self.included(base) 7 | base.extend ClassMethods 8 | base.class_eval do 9 | class << self 10 | alias_method :walk_tree_without_polymorphism, :walk_tree 11 | alias_method :walk_tree, :walk_tree_with_polymorphism 12 | end 13 | 14 | alias_method :build_without_polymorphism, :build 15 | alias_method :build, :build_with_polymorphism 16 | 17 | alias_method :join_constraints_without_polymorphism, :join_constraints 18 | alias_method :join_constraints, :join_constraints_with_polymorphism 19 | end 20 | end 21 | 22 | # Replaces ActiveRecord::Associations::JoinDependency#build 23 | # 24 | def build_with_polymorphism(associations, base_klass) 25 | associations.map do |name, right| 26 | if name.is_a? Join 27 | reflection = find_reflection base_klass, name.name 28 | reflection.check_validity! 29 | klass = if reflection.polymorphic? 30 | name.klass || base_klass 31 | else 32 | reflection.klass 33 | end 34 | JoinAssociation.new(reflection, build(right, klass), name.klass, name.type) 35 | else 36 | reflection = find_reflection base_klass, name 37 | reflection.check_validity! 38 | if reflection.polymorphic? 39 | raise ActiveRecord::EagerLoadPolymorphicError.new(reflection) 40 | end 41 | JoinAssociation.new reflection, build(right, reflection.klass) 42 | end 43 | end 44 | end 45 | 46 | # Replaces ActiveRecord::Associations::JoinDependency#join_constraints 47 | # to call #make_polyamorous_inner_joins instead of #make_inner_joins 48 | # 49 | def join_constraints_with_polymorphism(outer_joins) 50 | joins = join_root.children.flat_map { |child| 51 | make_polyamorous_inner_joins join_root, child 52 | } 53 | joins.concat outer_joins.flat_map { |oj| 54 | if join_root.match? oj.join_root 55 | walk(join_root, oj.join_root) 56 | else 57 | oj.join_root.children.flat_map { |child| 58 | make_outer_joins(oj.join_root, child) 59 | } 60 | end 61 | } 62 | end 63 | 64 | module ClassMethods 65 | # Replaces ActiveRecord::Associations::JoinDependency#self.walk_tree 66 | # 67 | def walk_tree_with_polymorphism(associations, hash) 68 | case associations 69 | when TreeNode 70 | associations.add_to_tree(hash) 71 | when Hash 72 | associations.each do |k, v| 73 | cache = 74 | if TreeNode === k 75 | k.add_to_tree(hash) 76 | else 77 | hash[k] ||= {} 78 | end 79 | walk_tree(v, cache) 80 | end 81 | else 82 | walk_tree_without_polymorphism(associations, hash) 83 | end 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/polyamorous/activerecord_4.2_ruby_2/join_association.rb: -------------------------------------------------------------------------------- 1 | # active_record_4.2_ruby_2/join_association.rb 2 | require 'polyamorous/activerecord_5.0_ruby_2/join_association' 3 | -------------------------------------------------------------------------------- /lib/polyamorous/activerecord_4.2_ruby_2/join_dependency.rb: -------------------------------------------------------------------------------- 1 | # active_record_4.2_ruby_2/join_dependency.rb 2 | require 'polyamorous/activerecord_5.0_ruby_2/join_dependency' 3 | 4 | module Polyamorous 5 | module JoinDependencyExtensions 6 | # Replaces ActiveRecord::Associations::JoinDependency#join_constraints 7 | # to call #make_polyamorous_inner_joins instead of #make_inner_joins. 8 | # 9 | def join_constraints(outer_joins) 10 | joins = join_root.children.flat_map { |child| 11 | make_polyamorous_inner_joins join_root, child 12 | } 13 | joins.concat outer_joins.flat_map { |oj| 14 | if join_root.match? oj.join_root 15 | walk(join_root, oj.join_root) 16 | else 17 | oj.join_root.children.flat_map { |child| 18 | make_outer_joins(oj.join_root, child) 19 | } 20 | end 21 | } 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/polyamorous/activerecord_5.0_ruby_2/join_association.rb: -------------------------------------------------------------------------------- 1 | # active_record_5.0_ruby_2/join_association.rb 2 | require 'polyamorous/activerecord_5.1_ruby_2/join_association' 3 | -------------------------------------------------------------------------------- /lib/polyamorous/activerecord_5.0_ruby_2/join_dependency.rb: -------------------------------------------------------------------------------- 1 | # active_record_5.0_ruby_2/join_dependency.rb 2 | require 'polyamorous/activerecord_5.1_ruby_2/join_dependency' 3 | -------------------------------------------------------------------------------- /lib/polyamorous/activerecord_5.1_ruby_2/join_association.rb: -------------------------------------------------------------------------------- 1 | # active_record_5.1_ruby_2/join_association.rb 2 | 3 | module Polyamorous 4 | module JoinAssociationExtensions 5 | include SwappingReflectionClass 6 | def self.prepended(base) 7 | base.class_eval { attr_reader :join_type } 8 | end 9 | 10 | def initialize(reflection, children, polymorphic_class = nil, 11 | join_type = Arel::Nodes::InnerJoin) 12 | @join_type = join_type 13 | if polymorphic_class && ::ActiveRecord::Base > polymorphic_class 14 | swapping_reflection_klass(reflection, polymorphic_class) do |reflection| 15 | super(reflection, children) 16 | self.reflection.options[:polymorphic] = true 17 | end 18 | else 19 | super(reflection, children) 20 | end 21 | end 22 | 23 | # Reference: https://github.com/rails/rails/commit/9b15db5 24 | # NOTE: Not sure we still need it? 25 | # 26 | def ==(other) 27 | base_klass == other.base_klass 28 | end 29 | 30 | def build_constraint(klass, table, key, foreign_table, foreign_key) 31 | if reflection.polymorphic? 32 | super(klass, table, key, foreign_table, foreign_key) 33 | .and(foreign_table[reflection.foreign_type].eq(reflection.klass.name)) 34 | else 35 | super(klass, table, key, foreign_table, foreign_key) 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/polyamorous/activerecord_5.1_ruby_2/join_dependency.rb: -------------------------------------------------------------------------------- 1 | # active_record_5.1_ruby_2/join_dependency.rb 2 | 3 | module Polyamorous 4 | module JoinDependencyExtensions 5 | # Replaces ActiveRecord::Associations::JoinDependency#build 6 | # 7 | def build(associations, base_klass) 8 | associations.map do |name, right| 9 | if name.is_a? Join 10 | reflection = find_reflection base_klass, name.name 11 | reflection.check_validity! 12 | reflection.check_eager_loadable! if ActiveRecord::VERSION::MAJOR >= 5 13 | 14 | klass = if reflection.polymorphic? 15 | name.klass || base_klass 16 | else 17 | reflection.klass 18 | end 19 | JoinAssociation.new(reflection, build(right, klass), name.klass, name.type) 20 | else 21 | reflection = find_reflection base_klass, name 22 | reflection.check_validity! 23 | reflection.check_eager_loadable! if ActiveRecord::VERSION::MAJOR >= 5 24 | 25 | if reflection.polymorphic? 26 | raise ActiveRecord::EagerLoadPolymorphicError.new(reflection) 27 | end 28 | JoinAssociation.new reflection, build(right, reflection.klass) 29 | end 30 | end 31 | end 32 | 33 | def find_join_association_respecting_polymorphism(reflection, parent, klass) 34 | if association = parent.children.find { |j| j.reflection == reflection } 35 | unless reflection.polymorphic? 36 | association 37 | else 38 | association if association.base_klass == klass 39 | end 40 | end 41 | end 42 | 43 | def build_join_association_respecting_polymorphism(reflection, parent, klass) 44 | if reflection.polymorphic? && klass 45 | JoinAssociation.new(reflection, self, klass) 46 | else 47 | JoinAssociation.new(reflection, self) 48 | end 49 | end 50 | 51 | # Replaces ActiveRecord::Associations::JoinDependency#join_constraints 52 | # 53 | # This internal method was changed in Rails 5.0 by commit 54 | # https://github.com/rails/rails/commit/e038975 which added 55 | # left_outer_joins (see #make_polyamorous_left_outer_joins below) and added 56 | # passing an additional argument, `join_type`, to #join_constraints. 57 | # 58 | def join_constraints(outer_joins, join_type) 59 | joins = join_root.children.flat_map { |child| 60 | if join_type == Arel::Nodes::OuterJoin 61 | make_polyamorous_left_outer_joins join_root, child 62 | else 63 | make_polyamorous_inner_joins join_root, child 64 | end 65 | } 66 | 67 | joins.concat outer_joins.flat_map { |oj| 68 | if join_root.match? oj.join_root 69 | walk(join_root, oj.join_root) 70 | else 71 | oj.join_root.children.flat_map { |child| 72 | make_outer_joins(oj.join_root, child) 73 | } 74 | end 75 | } 76 | end 77 | 78 | # Replaces ActiveRecord::Associations::JoinDependency#make_left_outer_joins, 79 | # a new method that was added in Rails 5.0 with the following commit: 80 | # https://github.com/rails/rails/commit/e038975 81 | # 82 | def make_polyamorous_left_outer_joins(parent, child) 83 | tables = child.tables 84 | join_type = Arel::Nodes::OuterJoin 85 | info = make_constraints parent, child, tables, join_type 86 | 87 | [info] + child.children.flat_map { |c| 88 | make_polyamorous_left_outer_joins(child, c) 89 | } 90 | end 91 | 92 | # Replaces ActiveRecord::Associations::JoinDependency#make_inner_joins 93 | # 94 | def make_polyamorous_inner_joins(parent, child) 95 | tables = child.tables 96 | join_type = child.join_type || Arel::Nodes::InnerJoin 97 | info = make_constraints parent, child, tables, join_type 98 | 99 | [info] + child.children.flat_map { |c| 100 | make_polyamorous_inner_joins(child, c) 101 | } 102 | end 103 | 104 | private :make_polyamorous_inner_joins, :make_polyamorous_left_outer_joins 105 | 106 | module ClassMethods 107 | # Prepended before ActiveRecord::Associations::JoinDependency#walk_tree 108 | # 109 | def walk_tree(associations, hash) 110 | case associations 111 | when TreeNode 112 | associations.add_to_tree(hash) 113 | when Hash 114 | associations.each do |k, v| 115 | cache = 116 | if TreeNode === k 117 | k.add_to_tree(hash) 118 | else 119 | hash[k] ||= {} 120 | end 121 | walk_tree(v, cache) 122 | end 123 | else 124 | super(associations, hash) 125 | end 126 | end 127 | end 128 | 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/polyamorous/activerecord_5.2_ruby_2/join_association.rb: -------------------------------------------------------------------------------- 1 | # active_record_5.2_ruby_2/join_association.rb 2 | 3 | module Polyamorous 4 | module JoinAssociationExtensions 5 | include SwappingReflectionClass 6 | def self.prepended(base) 7 | base.class_eval { attr_reader :join_type } 8 | end 9 | 10 | def initialize(reflection, children, alias_tracker, polymorphic_class = nil, 11 | join_type = Arel::Nodes::InnerJoin) 12 | @join_type = join_type 13 | if polymorphic_class && ::ActiveRecord::Base > polymorphic_class 14 | swapping_reflection_klass(reflection, polymorphic_class) do |reflection| 15 | super(reflection, children, alias_tracker) 16 | self.reflection.options[:polymorphic] = true 17 | end 18 | else 19 | super(reflection, children, alias_tracker) 20 | end 21 | end 22 | 23 | # Reference: https://github.com/rails/rails/commit/9b15db5 24 | # NOTE: Not sure we still need it? 25 | # 26 | def ==(other) 27 | base_klass == other.base_klass 28 | end 29 | 30 | def build_constraint(klass, table, key, foreign_table, foreign_key) 31 | if reflection.polymorphic? 32 | super(klass, table, key, foreign_table, foreign_key) 33 | .and(foreign_table[reflection.foreign_type].eq(reflection.klass.name)) 34 | else 35 | super(klass, table, key, foreign_table, foreign_key) 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/polyamorous/activerecord_5.2_ruby_2/join_dependency.rb: -------------------------------------------------------------------------------- 1 | # active_record_5.2_ruby_2/join_dependency.rb 2 | 3 | module Polyamorous 4 | module JoinDependencyExtensions 5 | # Replaces ActiveRecord::Associations::JoinDependency#build 6 | # 7 | def build(associations, base_klass) 8 | associations.map do |name, right| 9 | if name.is_a? Join 10 | reflection = find_reflection base_klass, name.name 11 | reflection.check_validity! 12 | reflection.check_eager_loadable! if ActiveRecord::VERSION::MAJOR >= 5 13 | 14 | klass = if reflection.polymorphic? 15 | name.klass || base_klass 16 | else 17 | reflection.klass 18 | end 19 | JoinAssociation.new(reflection, build(right, klass), alias_tracker, name.klass, name.type) 20 | else 21 | reflection = find_reflection base_klass, name 22 | reflection.check_validity! 23 | reflection.check_eager_loadable! if ActiveRecord::VERSION::MAJOR >= 5 24 | 25 | if reflection.polymorphic? 26 | raise ActiveRecord::EagerLoadPolymorphicError.new(reflection) 27 | end 28 | JoinAssociation.new(reflection, build(right, reflection.klass), alias_tracker) 29 | end 30 | end 31 | end 32 | 33 | def find_join_association_respecting_polymorphism(reflection, parent, klass) 34 | if association = parent.children.find { |j| j.reflection == reflection } 35 | unless reflection.polymorphic? 36 | association 37 | else 38 | association if association.base_klass == klass 39 | end 40 | end 41 | end 42 | 43 | def build_join_association_respecting_polymorphism(reflection, parent, klass) 44 | if reflection.polymorphic? && klass 45 | JoinAssociation.new(reflection, self, alias_tracker, klass) 46 | else 47 | JoinAssociation.new(reflection, self, alias_tracker) 48 | end 49 | end 50 | 51 | # Replaces ActiveRecord::Associations::JoinDependency#join_constraints 52 | # 53 | # This internal method was changed in Rails 5.0 by commit 54 | # https://github.com/rails/rails/commit/e038975 which added 55 | # left_outer_joins (see #make_polyamorous_left_outer_joins below) and added 56 | # passing an additional argument, `join_type`, to #join_constraints. 57 | # 58 | def join_constraints(outer_joins, join_type) 59 | joins = join_root.children.flat_map { |child| 60 | if join_type == Arel::Nodes::OuterJoin 61 | make_polyamorous_left_outer_joins join_root, child 62 | else 63 | make_polyamorous_inner_joins join_root, child 64 | end 65 | } 66 | 67 | joins.concat outer_joins.flat_map { |oj| 68 | if join_root.match? oj.join_root 69 | walk(join_root, oj.join_root) 70 | else 71 | oj.join_root.children.flat_map { |child| 72 | make_outer_joins(oj.join_root, child) 73 | } 74 | end 75 | } 76 | end 77 | 78 | # Replaces ActiveRecord::Associations::JoinDependency#make_left_outer_joins, 79 | # a new method that was added in Rails 5.0 with the following commit: 80 | # https://github.com/rails/rails/commit/e038975 81 | # 82 | def make_polyamorous_left_outer_joins(parent, child) 83 | tables = child.tables 84 | join_type = Arel::Nodes::OuterJoin 85 | info = make_constraints parent, child, tables, join_type 86 | 87 | info + child.children.flat_map { |c| 88 | make_polyamorous_left_outer_joins(child, c) 89 | } 90 | end 91 | 92 | # Replaces ActiveRecord::Associations::JoinDependency#make_inner_joins 93 | # 94 | def make_polyamorous_inner_joins(parent, child) 95 | tables = child.tables 96 | join_type = child.join_type || Arel::Nodes::InnerJoin 97 | info = make_constraints parent, child, tables, join_type 98 | 99 | info + child.children.flat_map { |c| 100 | make_polyamorous_inner_joins(child, c) 101 | } 102 | end 103 | 104 | private :make_polyamorous_inner_joins, :make_polyamorous_left_outer_joins 105 | 106 | module ClassMethods 107 | # Prepended before ActiveRecord::Associations::JoinDependency#walk_tree 108 | # 109 | def walk_tree(associations, hash) 110 | case associations 111 | when TreeNode 112 | associations.add_to_tree(hash) 113 | when Hash 114 | associations.each do |k, v| 115 | cache = 116 | if TreeNode === k 117 | k.add_to_tree(hash) 118 | else 119 | hash[k] ||= {} 120 | end 121 | walk_tree(v, cache) 122 | end 123 | else 124 | super(associations, hash) 125 | end 126 | end 127 | end 128 | 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/polyamorous/join.rb: -------------------------------------------------------------------------------- 1 | module Polyamorous 2 | class Join 3 | include TreeNode 4 | 5 | attr_accessor :name 6 | attr_reader :type, :klass 7 | 8 | def initialize(name, type = InnerJoin, klass = nil) 9 | @name = name 10 | @type = convert_to_arel_join_type(type) 11 | @klass = convert_to_class(klass) if klass 12 | end 13 | 14 | def klass=(klass) 15 | @klass = convert_to_class(klass) if klass 16 | end 17 | 18 | def type=(type) 19 | @type = convert_to_arel_join_type(type) if type 20 | end 21 | 22 | def hash 23 | [@name, @type, @klass].hash 24 | end 25 | 26 | def eql?(other) 27 | self.class == other.class && 28 | self.name == other.name && 29 | self.type == other.type && 30 | self.klass == other.klass 31 | end 32 | 33 | alias :== :eql? 34 | 35 | def add_to_tree(hash) 36 | hash[self] ||= {} 37 | end 38 | 39 | private 40 | 41 | def convert_to_arel_join_type(type) 42 | case type 43 | when 'inner', :inner 44 | InnerJoin 45 | when 'outer', :outer 46 | OuterJoin 47 | when Class 48 | if [InnerJoin, OuterJoin].include? type 49 | type 50 | else 51 | raise ArgumentError, "#{type} cannot be converted to an ARel join type" 52 | end 53 | else 54 | raise ArgumentError, "#{type} cannot be converted to an ARel join type" 55 | end 56 | end 57 | 58 | def convert_to_class(value) 59 | case value 60 | when String, Symbol 61 | Kernel.const_get(value) 62 | when Class 63 | value 64 | else 65 | raise ArgumentError, "#{value} cannot be converted to a Class" 66 | end 67 | end 68 | 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/polyamorous/swapping_reflection_class.rb: -------------------------------------------------------------------------------- 1 | module Polyamorous 2 | module SwappingReflectionClass 3 | def swapping_reflection_klass(reflection, klass) 4 | new_reflection = reflection.clone 5 | new_reflection.instance_variable_set(:@options, reflection.options.clone) 6 | new_reflection.options.delete(:polymorphic) 7 | new_reflection.instance_variable_set(:@klass, klass) 8 | yield new_reflection 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/polyamorous/tree_node.rb: -------------------------------------------------------------------------------- 1 | module Polyamorous 2 | module TreeNode 3 | def add_to_tree(hash) 4 | raise NotImplementedError 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/polyamorous/version.rb: -------------------------------------------------------------------------------- 1 | module Polyamorous 2 | VERSION = '1.3.3' 3 | end 4 | -------------------------------------------------------------------------------- /polyamorous.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "polyamorous/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "polyamorous" 7 | s.version = Polyamorous::VERSION 8 | s.authors = ["Ernie Miller", "Ryan Bigg", "Jon Atack", "Xiang Li"] 9 | s.email = ["ernie@erniemiller.org", "radarlistener@gmail.com", "jonnyatack@gmail.com", "bigxiang@gmail.com"] 10 | s.homepage = "https://github.com/activerecord-hackery/polyamorous" 11 | s.license = "MIT" 12 | s.summary = %q{ 13 | Loves/is loved by polymorphic belongs_to associations, Ransack, Squeel, MetaSearch... 14 | } 15 | s.description = %q{ 16 | This is just an extraction from Ransack/Squeel. You probably don't want to use this 17 | directly. It extends ActiveRecord's associations to support polymorphic belongs_to 18 | associations. 19 | } 20 | 21 | s.rubyforge_project = "polyamorous" 22 | 23 | s.add_dependency 'activerecord', '>= 3.0' 24 | s.add_development_dependency 'rspec', '~> 3' 25 | s.add_development_dependency 'machinist', '~> 1.0.6' 26 | s.add_development_dependency 'faker', '~> 1.6.5' 27 | s.add_development_dependency 'sqlite3', '~> 1.3.3' 28 | 29 | s.files = `git ls-files`.split("\n") 30 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 31 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 32 | s.require_paths = ["lib"] 33 | 34 | # specify any dependencies here; for example: 35 | # s.add_development_dependency "rspec" 36 | # s.add_runtime_dependency "rest-client" 37 | end 38 | -------------------------------------------------------------------------------- /spec/blueprints/articles.rb: -------------------------------------------------------------------------------- 1 | Article.blueprint do 2 | person 3 | title 4 | body 5 | end -------------------------------------------------------------------------------- /spec/blueprints/comments.rb: -------------------------------------------------------------------------------- 1 | Comment.blueprint do 2 | article 3 | person 4 | body 5 | end -------------------------------------------------------------------------------- /spec/blueprints/notes.rb: -------------------------------------------------------------------------------- 1 | Note.blueprint do 2 | note 3 | end -------------------------------------------------------------------------------- /spec/blueprints/people.rb: -------------------------------------------------------------------------------- 1 | Person.blueprint do 2 | name 3 | salary 4 | end -------------------------------------------------------------------------------- /spec/blueprints/tags.rb: -------------------------------------------------------------------------------- 1 | Tag.blueprint do 2 | name { Sham.tag_name } 3 | end -------------------------------------------------------------------------------- /spec/helpers/polyamorous_helper.rb: -------------------------------------------------------------------------------- 1 | module PolyamorousHelper 2 | if ActiveRecord::VERSION::STRING >= "4.1" 3 | def new_join_association(reflection, children, klass) 4 | Polyamorous::JoinAssociation.new reflection, children, klass 5 | end 6 | else 7 | def new_join_association(reflection, join_dependency, parent, klass) 8 | Polyamorous::JoinAssociation.new reflection, join_dependency, parent, klass 9 | end 10 | end 11 | 12 | if ActiveRecord::VERSION::STRING >= "5.2" 13 | def new_join_dependency(klass, associations = {}) 14 | alias_tracker = ::ActiveRecord::Associations::AliasTracker.create(klass.connection, klass.table_name, []) 15 | Polyamorous::JoinDependency.new klass, klass.arel_table, associations, alias_tracker 16 | end 17 | else 18 | def new_join_dependency(klass, associations = {}) 19 | Polyamorous::JoinDependency.new klass, associations, [] 20 | end 21 | end 22 | 23 | def new_join(name, type = Polyamorous::InnerJoin, klass = nil) 24 | Polyamorous::Join.new name, type, klass 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/polyamorous/join_association_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Polyamorous 4 | describe JoinAssociation do 5 | 6 | join_base, join_association_args, polymorphic = 7 | if ActiveRecord::VERSION::STRING >= '4.1' 8 | [:join_root, 'parent.children', 'reflection.options[:polymorphic]'] 9 | else 10 | [:join_base, 'join_dependency, parent', 'options[:polymorphic]'] 11 | end 12 | 13 | let(:join_dependency) { new_join_dependency Note, {} } 14 | let(:reflection) { Note.reflect_on_association(:notable) } 15 | let(:parent) { join_dependency.send(join_base) } 16 | let(:join_association) { 17 | eval("new_join_association(reflection, #{join_association_args}, Article)") 18 | } 19 | 20 | subject { 21 | join_dependency.build_join_association_respecting_polymorphism( 22 | reflection, parent, Person 23 | ) 24 | } 25 | 26 | it 'respects polymorphism on equality test' do 27 | expect(subject).to eq( 28 | join_dependency.build_join_association_respecting_polymorphism( 29 | reflection, parent, Person 30 | ) 31 | ) 32 | expect(subject).not_to eq( 33 | join_dependency.build_join_association_respecting_polymorphism( 34 | reflection, parent, Article 35 | ) 36 | ) 37 | end 38 | 39 | it 'leaves the orginal reflection intact for thread safety' do 40 | reflection.instance_variable_set(:@klass, Article) 41 | join_association 42 | .swapping_reflection_klass(reflection, Person) do |new_reflection| 43 | expect(new_reflection.options).not_to equal reflection.options 44 | expect(new_reflection.options).not_to have_key(:polymorphic) 45 | expect(new_reflection.klass).to eq(Person) 46 | expect(reflection.klass).to eq(Article) 47 | end 48 | end 49 | 50 | it 'sets the polymorphic option to true after initializing' do 51 | expect(join_association.instance_eval(polymorphic)).to be true 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/polyamorous/join_dependency_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Polyamorous 4 | describe JoinDependency do 5 | 6 | method, join_associations, join_base = 7 | if ActiveRecord::VERSION::STRING >= '4.1' 8 | [:instance_eval, 'join_root.drop(1)', :join_root] 9 | else 10 | [:send, 'join_associations', :join_base] 11 | end 12 | 13 | context 'with symbol joins' do 14 | subject { new_join_dependency Person, articles: :comments } 15 | 16 | specify { expect(subject.send(method, join_associations).size) 17 | .to eq(2) } 18 | specify { expect(subject.send(method, join_associations).map(&:join_type)) 19 | .to be_all { Polyamorous::InnerJoin } } 20 | end 21 | 22 | context 'with has_many :through association' do 23 | subject { new_join_dependency Person, :authored_article_comments } 24 | 25 | specify { expect(subject.send(method, join_associations).size) 26 | .to eq 1 } 27 | specify { expect(subject.send(method, join_associations).first.table_name) 28 | .to eq 'comments' } 29 | end 30 | 31 | context 'with outer join' do 32 | subject { new_join_dependency Person, new_join(:articles, :outer) } 33 | 34 | specify { expect(subject.send(method, join_associations).size) 35 | .to eq 1 } 36 | specify { expect(subject.send(method, join_associations).first.join_type) 37 | .to eq Polyamorous::OuterJoin } 38 | end 39 | 40 | context 'with nested outer joins' do 41 | subject { new_join_dependency Person, 42 | new_join(:articles, :outer) => new_join(:comments, :outer) } 43 | 44 | specify { expect(subject.send(method, join_associations).size) 45 | .to eq 2 } 46 | specify { expect(subject.send(method, join_associations).map(&:join_type)) 47 | .to eq [Polyamorous::OuterJoin, Polyamorous::OuterJoin] } 48 | specify { expect(subject.send(method, join_associations).map(&:join_type)) 49 | .to be_all { Polyamorous::OuterJoin } } 50 | end 51 | 52 | context 'with polymorphic belongs_to join' do 53 | subject { new_join_dependency Note, new_join(:notable, :inner, Person) } 54 | 55 | specify { expect(subject.send(method, join_associations).size) 56 | .to eq 1 } 57 | specify { expect(subject.send(method, join_associations).first.join_type) 58 | .to eq Polyamorous::InnerJoin } 59 | specify { expect(subject.send(method, join_associations).first.table_name) 60 | .to eq 'people' } 61 | 62 | it 'finds a join association respecting polymorphism' do 63 | parent = subject.send(join_base) 64 | reflection = Note.reflect_on_association(:notable) 65 | 66 | expect(subject.find_join_association_respecting_polymorphism( 67 | reflection, parent, Person)) 68 | .to eq subject.send(method, join_associations).first 69 | end 70 | end 71 | 72 | context 'with polymorphic belongs_to join and nested symbol join' do 73 | subject { new_join_dependency Note, 74 | new_join(:notable, :inner, Person) => :comments } 75 | 76 | specify { expect(subject.send(method, join_associations).size) 77 | .to eq 2 } 78 | specify { expect(subject.send(method, join_associations).map(&:join_type)) 79 | .to be_all { Polyamorous::InnerJoin } } 80 | specify { expect(subject.send(method, join_associations).first.table_name) 81 | .to eq 'people' } 82 | specify { expect(subject.send(method, join_associations)[1].table_name) 83 | .to eq 'comments' } 84 | end 85 | 86 | context '#left_outer_join in Rails 5 overrides join type specified', 87 | if: ActiveRecord::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR < 2 do 88 | 89 | let(:join_type_class) do 90 | new_join_dependency( 91 | Person, 92 | new_join(:articles) 93 | ).join_constraints( 94 | [], 95 | Arel::Nodes::OuterJoin 96 | ).first.joins.map(&:class) 97 | end 98 | 99 | specify { expect(join_type_class).to eq [Arel::Nodes::OuterJoin] } 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /spec/polyamorous/join_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Polyamorous 4 | describe Join do 5 | it 'is a tree node' do 6 | join = new_join(:articles, :outer) 7 | expect(join).to be_kind_of(TreeNode) 8 | end 9 | 10 | it 'can be added to a tree' do 11 | join = new_join(:articles, :outer) 12 | 13 | tree_hash = {} 14 | join.add_to_tree(tree_hash) 15 | 16 | expect(tree_hash[join]).to be {} 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'machinist/active_record' 2 | require 'sham' 3 | require 'faker' 4 | require 'polyamorous' 5 | 6 | Time.zone = 'Eastern Time (US & Canada)' 7 | 8 | Dir[File.expand_path('../{helpers,support,blueprints}/**/*.rb', __FILE__)] 9 | .each do |f| 10 | require f 11 | end 12 | 13 | Sham.define do 14 | name { Faker::Name.name } 15 | title { Faker::Lorem.sentence } 16 | body { Faker::Lorem.paragraph } 17 | salary { |index| 30000 + (index * 1000) } 18 | tag_name { Faker::Lorem.words(3).join(' ') } 19 | note { Faker::Lorem.words(7).join(' ') } 20 | end 21 | 22 | RSpec.configure do |config| 23 | config.before(:suite) do 24 | message = "Running Polyamorous specs with #{ 25 | ActiveRecord::Base.connection.adapter_name 26 | }, Active Record #{::ActiveRecord::VERSION::STRING}, Arel #{Arel::VERSION 27 | } and Ruby #{RUBY_VERSION}" 28 | line = '=' * message.length 29 | puts line, message, line 30 | Schema.create 31 | end 32 | config.before(:all) { Sham.reset(:before_all) } 33 | config.before(:each) { Sham.reset(:before_each) } 34 | 35 | config.include PolyamorousHelper 36 | end 37 | 38 | RSpec::Matchers.define :be_like do |expected| 39 | match do |actual| 40 | actual.gsub(/^\s+|\s+$/, '').gsub(/\s+/, ' ').strip == 41 | expected.gsub(/^\s+|\s+$/, '').gsub(/\s+/, ' ').strip 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/support/schema.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | 3 | ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') 4 | 5 | class Person < ActiveRecord::Base 6 | belongs_to :parent, class_name: 'Person', foreign_key: :parent_id 7 | has_many :children, class_name: 'Person', foreign_key: :parent_id 8 | has_many :articles 9 | has_many :comments 10 | has_many :authored_article_comments, through: :articles, 11 | foreign_key: :person_id, source: :comments 12 | has_many :notes, as: :notable 13 | end 14 | 15 | class Article < ActiveRecord::Base 16 | belongs_to :person 17 | has_many :comments 18 | has_and_belongs_to_many :tags 19 | has_many :notes, as: :notable 20 | end 21 | 22 | class Comment < ActiveRecord::Base 23 | belongs_to :article 24 | belongs_to :person 25 | end 26 | 27 | class Tag < ActiveRecord::Base 28 | has_and_belongs_to_many :articles 29 | end 30 | 31 | class Note < ActiveRecord::Base 32 | belongs_to :notable, polymorphic: true 33 | end 34 | 35 | module Schema 36 | def self.create 37 | ActiveRecord::Migration.verbose = false 38 | 39 | ActiveRecord::Schema.define do 40 | create_table :people, force: true do |t| 41 | t.integer :parent_id 42 | t.string :name 43 | t.integer :salary 44 | t.boolean :awesome, default: false 45 | t.timestamps null: false 46 | end 47 | 48 | create_table :articles, force: true do |t| 49 | t.integer :person_id 50 | t.string :title 51 | t.text :body 52 | end 53 | 54 | create_table :comments, force: true do |t| 55 | t.integer :article_id 56 | t.integer :person_id 57 | t.text :body 58 | end 59 | 60 | create_table :tags, force: true do |t| 61 | t.string :name 62 | end 63 | 64 | create_table :articles_tags, force: true, id: false do |t| 65 | t.integer :article_id 66 | t.integer :tag_id 67 | end 68 | 69 | create_table :notes, force: true do |t| 70 | t.integer :notable_id 71 | t.string :notable_type 72 | t.string :note 73 | end 74 | end 75 | 76 | 10.times do 77 | person = Person.make 78 | Note.make(notable: person) 79 | 3.times do 80 | article = Article.make(person: person) 81 | 3.times do 82 | article.tags = [Tag.make, Tag.make, Tag.make] 83 | end 84 | Note.make(notable: article) 85 | 10.times do 86 | Comment.make(article: article) 87 | end 88 | end 89 | 2.times do 90 | Comment.make(person: person) 91 | end 92 | end 93 | 94 | Comment.make( 95 | body: 'First post!', article: Article.make(title: 'Hello, world!') 96 | ) 97 | end 98 | end 99 | --------------------------------------------------------------------------------