absorb polyamorous
This commit is contained in:
parent
0fe5eace73
commit
d0805ac0f1
10
.travis.yml
10
.travis.yml
|
@ -11,6 +11,10 @@ env:
|
|||
- RAILS=5-2-stable DB=mysql
|
||||
- RAILS=5-2-stable DB=postgres
|
||||
|
||||
- RAILS=v5.2.0 DB=sqlite3
|
||||
- RAILS=v5.2.0 DB=mysql
|
||||
- RAILS=v5.2.0 DB=postgres
|
||||
|
||||
- RAILS=5-0-stable DB=sqlite3
|
||||
- RAILS=5-0-stable DB=mysql
|
||||
- RAILS=5-0-stable DB=postgres
|
||||
|
@ -18,7 +22,11 @@ env:
|
|||
- RAILS=4-2-stable DB=sqlite3
|
||||
- RAILS=4-2-stable DB=mysql
|
||||
- RAILS=4-2-stable DB=postgres
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- env: RAILS=5-2-stable DB=sqlite3
|
||||
- env: RAILS=5-2-stable DB=mysql
|
||||
- env: RAILS=5-2-stable DB=postgres
|
||||
before_script:
|
||||
- mysql -e 'create database ransack collate utf8_general_ci;'
|
||||
- mysql -e 'use ransack;show variables like "%character%";show variables like "%collation%";'
|
||||
|
|
9
Gemfile
9
Gemfile
|
@ -5,12 +5,6 @@ gem 'rake'
|
|||
|
||||
rails = ENV['RAILS'] || '5-0-stable'
|
||||
|
||||
if rails == 'master'
|
||||
gem 'polyamorous', github: 'activerecord-hackery/polyamorous'
|
||||
else
|
||||
gem 'polyamorous', '~> 1.3'
|
||||
end
|
||||
|
||||
gem 'pry'
|
||||
|
||||
# Provide timezone information on Windows
|
||||
|
@ -28,6 +22,9 @@ when /^v/ # A tagged version
|
|||
gem 'activerecord', require: false
|
||||
gem 'actionpack'
|
||||
end
|
||||
if rails == 'v5.2.0'
|
||||
gem 'mysql2', '~> 0.4.4'
|
||||
end
|
||||
else
|
||||
git 'git://github.com/rails/rails.git', :branch => rails do
|
||||
gem 'activesupport'
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
if defined?(::ActiveRecord)
|
||||
module Polyamorous
|
||||
if defined?(Arel::InnerJoin)
|
||||
InnerJoin = Arel::InnerJoin
|
||||
OuterJoin = Arel::OuterJoin
|
||||
else
|
||||
InnerJoin = Arel::Nodes::InnerJoin
|
||||
OuterJoin = Arel::Nodes::OuterJoin
|
||||
end
|
||||
|
||||
if defined?(::ActiveRecord::Associations::JoinDependency)
|
||||
JoinDependency = ::ActiveRecord::Associations::JoinDependency
|
||||
JoinAssociation = ::ActiveRecord::Associations::JoinDependency::JoinAssociation
|
||||
JoinBase = ::ActiveRecord::Associations::JoinDependency::JoinBase
|
||||
else
|
||||
JoinDependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency
|
||||
JoinAssociation = ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation
|
||||
JoinBase = ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinBase
|
||||
end
|
||||
end
|
||||
|
||||
require 'polyamorous/tree_node'
|
||||
require 'polyamorous/join'
|
||||
require 'polyamorous/swapping_reflection_class'
|
||||
|
||||
ar_version = ::ActiveRecord::VERSION::STRING[0,3]
|
||||
ar_version = '3_and_4.0' if ar_version < '4.1'
|
||||
ar_version = ::ActiveRecord::VERSION::STRING[0,5] if ar_version == '5.2'
|
||||
|
||||
method, ruby_version =
|
||||
if RUBY_VERSION >= '2.0' && ar_version >= '4.1'
|
||||
# Ruby 2; we can use `prepend` to patch Active Record cleanly.
|
||||
[:prepend, '2']
|
||||
else
|
||||
# Ruby 1.9; we must use `alias_method` to patch Active Record.
|
||||
[:include, '1.9']
|
||||
end
|
||||
|
||||
%w(join_association join_dependency).each do |file|
|
||||
require "polyamorous/activerecord_#{ar_version}_ruby_#{ruby_version}/#{file}"
|
||||
end
|
||||
|
||||
Polyamorous::JoinDependency.send(method, Polyamorous::JoinDependencyExtensions)
|
||||
if method == :prepend
|
||||
Polyamorous::JoinDependency.singleton_class
|
||||
.send(:prepend, Polyamorous::JoinDependencyExtensions::ClassMethods)
|
||||
end
|
||||
Polyamorous::JoinAssociation.send(method, Polyamorous::JoinAssociationExtensions)
|
||||
|
||||
Polyamorous::JoinBase.class_eval do
|
||||
if method_defined?(:active_record)
|
||||
alias_method :base_klass, :active_record
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,76 @@
|
|||
# active_record_3_and_4.0_ruby_1.9/join_association.rb
|
||||
module Polyamorous
|
||||
module JoinAssociationExtensions
|
||||
include SwappingReflectionClass
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
alias_method_chain :initialize, :polymorphism
|
||||
alias_method :equality_without_polymorphism, :==
|
||||
alias_method :==, :equality_with_polymorphism
|
||||
if base.method_defined?(:active_record)
|
||||
alias_method :base_klass, :active_record
|
||||
end
|
||||
|
||||
if ActiveRecord::VERSION::STRING =~ /^3\.0\./
|
||||
alias_method_chain :association_join, :polymorphism
|
||||
else
|
||||
alias_method_chain :build_constraint, :polymorphism
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize_with_polymorphism(
|
||||
reflection, join_dependency, parent = nil, polymorphic_class = nil
|
||||
)
|
||||
if polymorphic_class && ::ActiveRecord::Base > polymorphic_class
|
||||
swapping_reflection_klass(reflection, polymorphic_class) do |reflection|
|
||||
initialize_without_polymorphism(reflection, join_dependency, parent)
|
||||
self.reflection.options[:polymorphic] = true
|
||||
end
|
||||
else
|
||||
initialize_without_polymorphism(reflection, join_dependency, parent)
|
||||
end
|
||||
end
|
||||
|
||||
def equality_with_polymorphism(other)
|
||||
equality_without_polymorphism(other) && base_klass == other.base_klass
|
||||
end
|
||||
|
||||
def build_constraint_with_polymorphism(
|
||||
reflection, table, key, foreign_table, foreign_key
|
||||
)
|
||||
if reflection.options[:polymorphic]
|
||||
build_constraint_without_polymorphism(
|
||||
reflection, table, key, foreign_table, foreign_key
|
||||
)
|
||||
.and(foreign_table[reflection.foreign_type].eq(reflection.klass.name))
|
||||
else
|
||||
build_constraint_without_polymorphism(
|
||||
reflection, table, key, foreign_table, foreign_key
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def association_join_with_polymorphism
|
||||
return @join if @Join
|
||||
@join = association_join_without_polymorphism
|
||||
if reflection.macro == :belongs_to && reflection.options[:polymorphic]
|
||||
aliased_table = Arel::Table.new(
|
||||
table_name,
|
||||
as: @aliased_table_name,
|
||||
engine: arel_engine,
|
||||
columns: klass.columns
|
||||
)
|
||||
parent_table = Arel::Table.new(
|
||||
parent.table_name,
|
||||
as: parent.aliased_table_name,
|
||||
engine: arel_engine,
|
||||
columns: parent.base_klass.columns
|
||||
)
|
||||
@join << parent_table[reflection.options[:foreign_type]]
|
||||
.eq(reflection.klass.name)
|
||||
end
|
||||
@join
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,96 @@
|
|||
# active_record_3_and_4.0_ruby_1.9/join_dependency.rb
|
||||
module Polyamorous
|
||||
module JoinDependencyExtensions
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
alias_method_chain :build, :polymorphism
|
||||
alias_method_chain :graft, :polymorphism
|
||||
if base.method_defined?(:active_record)
|
||||
alias_method :base_klass, :active_record
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def graft_with_polymorphism(*associations)
|
||||
associations.each do |association|
|
||||
unless join_associations.detect { |a| association == a }
|
||||
if association.reflection.options[:polymorphic]
|
||||
build(
|
||||
Join.new(
|
||||
association.reflection.name,
|
||||
association.join_type,
|
||||
association.reflection.klass
|
||||
),
|
||||
association.find_parent_in(self) || join_base,
|
||||
association.join_type
|
||||
)
|
||||
else
|
||||
build(
|
||||
association.reflection.name,
|
||||
association.find_parent_in(self) || join_base,
|
||||
association.join_type
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
if ActiveRecord::VERSION::STRING =~ /^3\.0\./
|
||||
def _join_parts
|
||||
@joins
|
||||
end
|
||||
else
|
||||
def _join_parts
|
||||
@join_parts
|
||||
end
|
||||
end
|
||||
|
||||
def build_with_polymorphism(
|
||||
associations, parent = nil, join_type = InnerJoin
|
||||
)
|
||||
case associations
|
||||
when Join
|
||||
parent ||= _join_parts.last
|
||||
reflection = parent.reflections[associations.name] or
|
||||
raise ::ActiveRecord::ConfigurationError,
|
||||
"Association named '#{associations.name
|
||||
}' was not found; perhaps you misspelled it?"
|
||||
|
||||
unless join_association = find_join_association_respecting_polymorphism(
|
||||
reflection, parent, associations.klass
|
||||
)
|
||||
@reflections << reflection
|
||||
join_association = build_join_association_respecting_polymorphism(
|
||||
reflection, parent, associations.klass
|
||||
)
|
||||
join_association.join_type = associations.type
|
||||
_join_parts << join_association
|
||||
cache_joined_association(join_association)
|
||||
end
|
||||
|
||||
join_association
|
||||
else
|
||||
build_without_polymorphism(associations, parent, join_type)
|
||||
end
|
||||
end
|
||||
|
||||
def find_join_association_respecting_polymorphism(reflection, parent, klass)
|
||||
if association = find_join_association(reflection, parent)
|
||||
unless reflection.options[:polymorphic]
|
||||
association
|
||||
else
|
||||
association if association.base_klass == klass
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_join_association_respecting_polymorphism(reflection, parent, klass)
|
||||
if reflection.options[:polymorphic] && klass
|
||||
JoinAssociation.new(reflection, self, parent, klass)
|
||||
else
|
||||
JoinAssociation.new(reflection, self, parent)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,2 @@
|
|||
# active_record_4.1_ruby_1.9/join_association.rb
|
||||
require 'polyamorous/activerecord_4.2_ruby_1.9/join_association'
|
|
@ -0,0 +1,4 @@
|
|||
# active_record_4.1_ruby_1.9/join_dependency.rb
|
||||
require 'polyamorous/activerecord_4.2_ruby_2/join_dependency'
|
||||
require 'polyamorous/activerecord_4.2_ruby_1.9/join_dependency'
|
||||
require 'polyamorous/activerecord_4.1_ruby_2/make_polyamorous_inner_joins'
|
|
@ -0,0 +1,2 @@
|
|||
# active_record_4.1_ruby_2/join_association.rb
|
||||
require 'polyamorous/activerecord_5.0_ruby_2/join_association'
|
|
@ -0,0 +1,3 @@
|
|||
# active_record_4.1_ruby_2/join_dependency.rb
|
||||
require 'polyamorous/activerecord_4.2_ruby_2/join_dependency'
|
||||
require 'polyamorous/activerecord_4.1_ruby_2/make_polyamorous_inner_joins'
|
|
@ -0,0 +1,14 @@
|
|||
module Polyamorous
|
||||
module JoinDependencyExtensions
|
||||
# Replaces ActiveRecord::Associations::JoinDependency#make_inner_joins
|
||||
#
|
||||
def make_polyamorous_inner_joins(parent, child)
|
||||
make_constraints(
|
||||
parent, child, child.tables, child.join_type || Arel::Nodes::InnerJoin
|
||||
)
|
||||
.concat child.children.flat_map { |c|
|
||||
make_polyamorous_inner_joins(child, c)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,46 @@
|
|||
# active_record_4.2_ruby_1.9/join_association.rb
|
||||
module Polyamorous
|
||||
module JoinAssociationExtensions
|
||||
include SwappingReflectionClass
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
attr_reader :join_type
|
||||
alias_method_chain :initialize, :polymorphism
|
||||
alias_method_chain :build_constraint, :polymorphism
|
||||
end
|
||||
end
|
||||
|
||||
def initialize_with_polymorphism(reflection, children,
|
||||
polymorphic_class = nil, join_type = Arel::Nodes::InnerJoin)
|
||||
@join_type = join_type
|
||||
if polymorphic_class && ::ActiveRecord::Base > polymorphic_class
|
||||
swapping_reflection_klass(reflection, polymorphic_class) do |reflection|
|
||||
initialize_without_polymorphism(reflection, children)
|
||||
self.reflection.options[:polymorphic] = true
|
||||
end
|
||||
else
|
||||
initialize_without_polymorphism(reflection, children)
|
||||
end
|
||||
end
|
||||
|
||||
# Reference https://github.com/rails/rails/commit/9b15db51b78028bfecdb85595624de4b838adbd1
|
||||
def ==(other)
|
||||
base_klass == other.base_klass
|
||||
end
|
||||
|
||||
def build_constraint_with_polymorphism(
|
||||
klass, table, key, foreign_table, foreign_key
|
||||
)
|
||||
if reflection.polymorphic?
|
||||
build_constraint_without_polymorphism(
|
||||
klass, table, key, foreign_table, foreign_key
|
||||
)
|
||||
.and(foreign_table[reflection.foreign_type].eq(reflection.klass.name))
|
||||
else
|
||||
build_constraint_without_polymorphism(
|
||||
klass, table, key, foreign_table, foreign_key
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,87 @@
|
|||
# active_record_4.2_ruby_1.9/join_dependency.rb
|
||||
require 'polyamorous/activerecord_4.2_ruby_2/join_dependency'
|
||||
|
||||
module Polyamorous
|
||||
module JoinDependencyExtensions
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
base.class_eval do
|
||||
class << self
|
||||
alias_method :walk_tree_without_polymorphism, :walk_tree
|
||||
alias_method :walk_tree, :walk_tree_with_polymorphism
|
||||
end
|
||||
|
||||
alias_method :build_without_polymorphism, :build
|
||||
alias_method :build, :build_with_polymorphism
|
||||
|
||||
alias_method :join_constraints_without_polymorphism, :join_constraints
|
||||
alias_method :join_constraints, :join_constraints_with_polymorphism
|
||||
end
|
||||
end
|
||||
|
||||
# Replaces ActiveRecord::Associations::JoinDependency#build
|
||||
#
|
||||
def build_with_polymorphism(associations, base_klass)
|
||||
associations.map do |name, right|
|
||||
if name.is_a? Join
|
||||
reflection = find_reflection base_klass, name.name
|
||||
reflection.check_validity!
|
||||
klass = if reflection.polymorphic?
|
||||
name.klass || base_klass
|
||||
else
|
||||
reflection.klass
|
||||
end
|
||||
JoinAssociation.new(reflection, build(right, klass), name.klass, name.type)
|
||||
else
|
||||
reflection = find_reflection base_klass, name
|
||||
reflection.check_validity!
|
||||
if reflection.polymorphic?
|
||||
raise ActiveRecord::EagerLoadPolymorphicError.new(reflection)
|
||||
end
|
||||
JoinAssociation.new reflection, build(right, reflection.klass)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Replaces ActiveRecord::Associations::JoinDependency#join_constraints
|
||||
# to call #make_polyamorous_inner_joins instead of #make_inner_joins
|
||||
#
|
||||
def join_constraints_with_polymorphism(outer_joins)
|
||||
joins = join_root.children.flat_map { |child|
|
||||
make_polyamorous_inner_joins join_root, child
|
||||
}
|
||||
joins.concat outer_joins.flat_map { |oj|
|
||||
if join_root.match? oj.join_root
|
||||
walk(join_root, oj.join_root)
|
||||
else
|
||||
oj.join_root.children.flat_map { |child|
|
||||
make_outer_joins(oj.join_root, child)
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Replaces ActiveRecord::Associations::JoinDependency#self.walk_tree
|
||||
#
|
||||
def walk_tree_with_polymorphism(associations, hash)
|
||||
case associations
|
||||
when TreeNode
|
||||
associations.add_to_tree(hash)
|
||||
when Hash
|
||||
associations.each do |k, v|
|
||||
cache =
|
||||
if TreeNode === k
|
||||
k.add_to_tree(hash)
|
||||
else
|
||||
hash[k] ||= {}
|
||||
end
|
||||
walk_tree(v, cache)
|
||||
end
|
||||
else
|
||||
walk_tree_without_polymorphism(associations, hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,2 @@
|
|||
# active_record_4.2_ruby_2/join_association.rb
|
||||
require 'polyamorous/activerecord_5.0_ruby_2/join_association'
|
|
@ -0,0 +1,24 @@
|
|||
# active_record_4.2_ruby_2/join_dependency.rb
|
||||
require 'polyamorous/activerecord_5.0_ruby_2/join_dependency'
|
||||
|
||||
module Polyamorous
|
||||
module JoinDependencyExtensions
|
||||
# Replaces ActiveRecord::Associations::JoinDependency#join_constraints
|
||||
# to call #make_polyamorous_inner_joins instead of #make_inner_joins.
|
||||
#
|
||||
def join_constraints(outer_joins)
|
||||
joins = join_root.children.flat_map { |child|
|
||||
make_polyamorous_inner_joins join_root, child
|
||||
}
|
||||
joins.concat outer_joins.flat_map { |oj|
|
||||
if join_root.match? oj.join_root
|
||||
walk(join_root, oj.join_root)
|
||||
else
|
||||
oj.join_root.children.flat_map { |child|
|
||||
make_outer_joins(oj.join_root, child)
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,2 @@
|
|||
# active_record_5.0_ruby_2/join_association.rb
|
||||
require 'polyamorous/activerecord_5.1_ruby_2/join_association'
|
|
@ -0,0 +1,2 @@
|
|||
# active_record_5.0_ruby_2/join_dependency.rb
|
||||
require 'polyamorous/activerecord_5.1_ruby_2/join_dependency'
|
|
@ -0,0 +1,39 @@
|
|||
# active_record_5.1_ruby_2/join_association.rb
|
||||
|
||||
module Polyamorous
|
||||
module JoinAssociationExtensions
|
||||
include SwappingReflectionClass
|
||||
def self.prepended(base)
|
||||
base.class_eval { attr_reader :join_type }
|
||||
end
|
||||
|
||||
def initialize(reflection, children, polymorphic_class = nil,
|
||||
join_type = Arel::Nodes::InnerJoin)
|
||||
@join_type = join_type
|
||||
if polymorphic_class && ::ActiveRecord::Base > polymorphic_class
|
||||
swapping_reflection_klass(reflection, polymorphic_class) do |reflection|
|
||||
super(reflection, children)
|
||||
self.reflection.options[:polymorphic] = true
|
||||
end
|
||||
else
|
||||
super(reflection, children)
|
||||
end
|
||||
end
|
||||
|
||||
# Reference: https://github.com/rails/rails/commit/9b15db5
|
||||
# NOTE: Not sure we still need it?
|
||||
#
|
||||
def ==(other)
|
||||
base_klass == other.base_klass
|
||||
end
|
||||
|
||||
def build_constraint(klass, table, key, foreign_table, foreign_key)
|
||||
if reflection.polymorphic?
|
||||
super(klass, table, key, foreign_table, foreign_key)
|
||||
.and(foreign_table[reflection.foreign_type].eq(reflection.klass.name))
|
||||
else
|
||||
super(klass, table, key, foreign_table, foreign_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,130 @@
|
|||
# active_record_5.1_ruby_2/join_dependency.rb
|
||||
|
||||
module Polyamorous
|
||||
module JoinDependencyExtensions
|
||||
# Replaces ActiveRecord::Associations::JoinDependency#build
|
||||
#
|
||||
def build(associations, base_klass)
|
||||
associations.map do |name, right|
|
||||
if name.is_a? Join
|
||||
reflection = find_reflection base_klass, name.name
|
||||
reflection.check_validity!
|
||||
reflection.check_eager_loadable! if ActiveRecord::VERSION::MAJOR >= 5
|
||||
|
||||
klass = if reflection.polymorphic?
|
||||
name.klass || base_klass
|
||||
else
|
||||
reflection.klass
|
||||
end
|
||||
JoinAssociation.new(reflection, build(right, klass), name.klass, name.type)
|
||||
else
|
||||
reflection = find_reflection base_klass, name
|
||||
reflection.check_validity!
|
||||
reflection.check_eager_loadable! if ActiveRecord::VERSION::MAJOR >= 5
|
||||
|
||||
if reflection.polymorphic?
|
||||
raise ActiveRecord::EagerLoadPolymorphicError.new(reflection)
|
||||
end
|
||||
JoinAssociation.new reflection, build(right, reflection.klass)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def find_join_association_respecting_polymorphism(reflection, parent, klass)
|
||||
if association = parent.children.find { |j| j.reflection == reflection }
|
||||
unless reflection.polymorphic?
|
||||
association
|
||||
else
|
||||
association if association.base_klass == klass
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_join_association_respecting_polymorphism(reflection, parent, klass)
|
||||
if reflection.polymorphic? && klass
|
||||
JoinAssociation.new(reflection, self, klass)
|
||||
else
|
||||
JoinAssociation.new(reflection, self)
|
||||
end
|
||||
end
|
||||
|
||||
# Replaces ActiveRecord::Associations::JoinDependency#join_constraints
|
||||
#
|
||||
# This internal method was changed in Rails 5.0 by commit
|
||||
# https://github.com/rails/rails/commit/e038975 which added
|
||||
# left_outer_joins (see #make_polyamorous_left_outer_joins below) and added
|
||||
# passing an additional argument, `join_type`, to #join_constraints.
|
||||
#
|
||||
def join_constraints(outer_joins, join_type)
|
||||
joins = join_root.children.flat_map { |child|
|
||||
if join_type == Arel::Nodes::OuterJoin
|
||||
make_polyamorous_left_outer_joins join_root, child
|
||||
else
|
||||
make_polyamorous_inner_joins join_root, child
|
||||
end
|
||||
}
|
||||
|
||||
joins.concat outer_joins.flat_map { |oj|
|
||||
if join_root.match? oj.join_root
|
||||
walk(join_root, oj.join_root)
|
||||
else
|
||||
oj.join_root.children.flat_map { |child|
|
||||
make_outer_joins(oj.join_root, child)
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
# Replaces ActiveRecord::Associations::JoinDependency#make_left_outer_joins,
|
||||
# a new method that was added in Rails 5.0 with the following commit:
|
||||
# https://github.com/rails/rails/commit/e038975
|
||||
#
|
||||
def make_polyamorous_left_outer_joins(parent, child)
|
||||
tables = child.tables
|
||||
join_type = Arel::Nodes::OuterJoin
|
||||
info = make_constraints parent, child, tables, join_type
|
||||
|
||||
[info] + child.children.flat_map { |c|
|
||||
make_polyamorous_left_outer_joins(child, c)
|
||||
}
|
||||
end
|
||||
|
||||
# Replaces ActiveRecord::Associations::JoinDependency#make_inner_joins
|
||||
#
|
||||
def make_polyamorous_inner_joins(parent, child)
|
||||
tables = child.tables
|
||||
join_type = child.join_type || Arel::Nodes::InnerJoin
|
||||
info = make_constraints parent, child, tables, join_type
|
||||
|
||||
[info] + child.children.flat_map { |c|
|
||||
make_polyamorous_inner_joins(child, c)
|
||||
}
|
||||
end
|
||||
|
||||
private :make_polyamorous_inner_joins, :make_polyamorous_left_outer_joins
|
||||
|
||||
module ClassMethods
|
||||
# Prepended before ActiveRecord::Associations::JoinDependency#walk_tree
|
||||
#
|
||||
def walk_tree(associations, hash)
|
||||
case associations
|
||||
when TreeNode
|
||||
associations.add_to_tree(hash)
|
||||
when Hash
|
||||
associations.each do |k, v|
|
||||
cache =
|
||||
if TreeNode === k
|
||||
k.add_to_tree(hash)
|
||||
else
|
||||
hash[k] ||= {}
|
||||
end
|
||||
walk_tree(v, cache)
|
||||
end
|
||||
else
|
||||
super(associations, hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,39 @@
|
|||
# active_record_5.2_ruby_2/join_association.rb
|
||||
|
||||
module Polyamorous
|
||||
module JoinAssociationExtensions
|
||||
include SwappingReflectionClass
|
||||
def self.prepended(base)
|
||||
base.class_eval { attr_reader :join_type }
|
||||
end
|
||||
|
||||
def initialize(reflection, children, alias_tracker, polymorphic_class = nil,
|
||||
join_type = Arel::Nodes::InnerJoin)
|
||||
@join_type = join_type
|
||||
if polymorphic_class && ::ActiveRecord::Base > polymorphic_class
|
||||
swapping_reflection_klass(reflection, polymorphic_class) do |reflection|
|
||||
super(reflection, children, alias_tracker)
|
||||
self.reflection.options[:polymorphic] = true
|
||||
end
|
||||
else
|
||||
super(reflection, children, alias_tracker)
|
||||
end
|
||||
end
|
||||
|
||||
# Reference: https://github.com/rails/rails/commit/9b15db5
|
||||
# NOTE: Not sure we still need it?
|
||||
#
|
||||
def ==(other)
|
||||
base_klass == other.base_klass
|
||||
end
|
||||
|
||||
def build_constraint(klass, table, key, foreign_table, foreign_key)
|
||||
if reflection.polymorphic?
|
||||
super(klass, table, key, foreign_table, foreign_key)
|
||||
.and(foreign_table[reflection.foreign_type].eq(reflection.klass.name))
|
||||
else
|
||||
super(klass, table, key, foreign_table, foreign_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,131 @@
|
|||
# active_record_5.2_ruby_2/join_dependency.rb
|
||||
|
||||
module Polyamorous
|
||||
module JoinDependencyExtensions
|
||||
# Replaces ActiveRecord::Associations::JoinDependency#build
|
||||
#
|
||||
def build(associations, base_klass)
|
||||
associations.map do |name, right|
|
||||
if name.is_a? Join
|
||||
reflection = find_reflection base_klass, name.name
|
||||
reflection.check_validity!
|
||||
reflection.check_eager_loadable! if ActiveRecord::VERSION::MAJOR >= 5
|
||||
|
||||
klass = if reflection.polymorphic?
|
||||
name.klass || base_klass
|
||||
else
|
||||
reflection.klass
|
||||
end
|
||||
JoinAssociation.new(reflection, build(right, klass), alias_tracker, name.klass, name.type)
|
||||
else
|
||||
reflection = find_reflection base_klass, name
|
||||
reflection.check_validity!
|
||||
reflection.check_eager_loadable! if ActiveRecord::VERSION::MAJOR >= 5
|
||||
|
||||
if reflection.polymorphic?
|
||||
raise ActiveRecord::EagerLoadPolymorphicError.new(reflection)
|
||||
end
|
||||
JoinAssociation.new(reflection, build(right, reflection.klass), alias_tracker)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def find_join_association_respecting_polymorphism(reflection, parent, klass)
|
||||
if association = parent.children.find { |j| j.reflection == reflection }
|
||||
unless reflection.polymorphic?
|
||||
association
|
||||
else
|
||||
association if association.base_klass == klass
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_join_association_respecting_polymorphism(reflection, parent, klass)
|
||||
if reflection.polymorphic? && klass
|
||||
JoinAssociation.new(reflection, self, alias_tracker, klass)
|
||||
else
|
||||
JoinAssociation.new(reflection, self, alias_tracker)
|
||||
end
|
||||
end
|
||||
|
||||
# Replaces ActiveRecord::Associations::JoinDependency#join_constraints
|
||||
#
|
||||
# This internal method was changed in Rails 5.0 by commit
|
||||
# https://github.com/rails/rails/commit/e038975 which added
|
||||
# left_outer_joins (see #make_polyamorous_left_outer_joins below) and added
|
||||
# passing an additional argument, `join_type`, to #join_constraints.
|
||||
#
|
||||
def join_constraints(outer_joins, join_type)
|
||||
@alias_tracker = alias_tracker
|
||||
joins = join_root.children.flat_map { |child|
|
||||
if join_type == Arel::Nodes::OuterJoin
|
||||
make_polyamorous_left_outer_joins join_root, child
|
||||
else
|
||||
make_polyamorous_inner_joins join_root, child
|
||||
end
|
||||
}
|
||||
|
||||
joins.concat outer_joins.flat_map { |oj|
|
||||
if join_root.match? oj.join_root
|
||||
walk(join_root, oj.join_root)
|
||||
else
|
||||
oj.join_root.children.flat_map { |child|
|
||||
make_outer_joins(oj.join_root, child)
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
# Replaces ActiveRecord::Associations::JoinDependency#make_left_outer_joins,
|
||||
# a new method that was added in Rails 5.0 with the following commit:
|
||||
# https://github.com/rails/rails/commit/e038975
|
||||
#
|
||||
def make_polyamorous_left_outer_joins(parent, child)
|
||||
tables = child.tables
|
||||
join_type = Arel::Nodes::OuterJoin
|
||||
info = make_constraints parent, child, tables, join_type
|
||||
|
||||
info + child.children.flat_map { |c|
|
||||
make_polyamorous_left_outer_joins(child, c)
|
||||
}
|
||||
end
|
||||
|
||||
# Replaces ActiveRecord::Associations::JoinDependency#make_inner_joins
|
||||
#
|
||||
def make_polyamorous_inner_joins(parent, child)
|
||||
tables = child.tables
|
||||
join_type = child.join_type || Arel::Nodes::InnerJoin
|
||||
info = make_constraints parent, child, tables, join_type
|
||||
|
||||
info + child.children.flat_map { |c|
|
||||
make_polyamorous_inner_joins(child, c)
|
||||
}
|
||||
end
|
||||
|
||||
private :make_polyamorous_inner_joins, :make_polyamorous_left_outer_joins
|
||||
|
||||
module ClassMethods
|
||||
# Prepended before ActiveRecord::Associations::JoinDependency#walk_tree
|
||||
#
|
||||
def walk_tree(associations, hash)
|
||||
case associations
|
||||
when TreeNode
|
||||
associations.add_to_tree(hash)
|
||||
when Hash
|
||||
associations.each do |k, v|
|
||||
cache =
|
||||
if TreeNode === k
|
||||
k.add_to_tree(hash)
|
||||
else
|
||||
hash[k] ||= {}
|
||||
end
|
||||
walk_tree(v, cache)
|
||||
end
|
||||
else
|
||||
super(associations, hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,70 @@
|
|||
module Polyamorous
|
||||
class Join
|
||||
include TreeNode
|
||||
|
||||
attr_accessor :name
|
||||
attr_reader :type, :klass
|
||||
|
||||
def initialize(name, type = InnerJoin, klass = nil)
|
||||
@name = name
|
||||
@type = convert_to_arel_join_type(type)
|
||||
@klass = convert_to_class(klass) if klass
|
||||
end
|
||||
|
||||
def klass=(klass)
|
||||
@klass = convert_to_class(klass) if klass
|
||||
end
|
||||
|
||||
def type=(type)
|
||||
@type = convert_to_arel_join_type(type) if type
|
||||
end
|
||||
|
||||
def hash
|
||||
[@name, @type, @klass].hash
|
||||
end
|
||||
|
||||
def eql?(other)
|
||||
self.class == other.class &&
|
||||
self.name == other.name &&
|
||||
self.type == other.type &&
|
||||
self.klass == other.klass
|
||||
end
|
||||
|
||||
alias :== :eql?
|
||||
|
||||
def add_to_tree(hash)
|
||||
hash[self] ||= {}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def convert_to_arel_join_type(type)
|
||||
case type
|
||||
when 'inner', :inner
|
||||
InnerJoin
|
||||
when 'outer', :outer
|
||||
OuterJoin
|
||||
when Class
|
||||
if [InnerJoin, OuterJoin].include? type
|
||||
type
|
||||
else
|
||||
raise ArgumentError, "#{type} cannot be converted to an ARel join type"
|
||||
end
|
||||
else
|
||||
raise ArgumentError, "#{type} cannot be converted to an ARel join type"
|
||||
end
|
||||
end
|
||||
|
||||
def convert_to_class(value)
|
||||
case value
|
||||
when String, Symbol
|
||||
Kernel.const_get(value)
|
||||
when Class
|
||||
value
|
||||
else
|
||||
raise ArgumentError, "#{value} cannot be converted to a Class"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
module Polyamorous
|
||||
module SwappingReflectionClass
|
||||
def swapping_reflection_klass(reflection, klass)
|
||||
new_reflection = reflection.clone
|
||||
new_reflection.instance_variable_set(:@options, reflection.options.clone)
|
||||
new_reflection.options.delete(:polymorphic)
|
||||
new_reflection.instance_variable_set(:@klass, klass)
|
||||
yield new_reflection
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
module Polyamorous
|
||||
module TreeNode
|
||||
def add_to_tree(hash)
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
end
|
|
@ -20,7 +20,6 @@ Gem::Specification.new do |s|
|
|||
s.add_dependency 'activerecord', '>= 3.0'
|
||||
s.add_dependency 'activesupport', '>= 3.0'
|
||||
s.add_dependency 'i18n'
|
||||
s.add_dependency 'polyamorous', '~> 1.3.2'
|
||||
s.add_development_dependency 'rspec', '~> 3'
|
||||
s.add_development_dependency 'machinist', '~> 1.0.6'
|
||||
s.add_development_dependency 'faker', '~> 0.9.5'
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
module PolyamorousHelper
|
||||
if ActiveRecord::VERSION::STRING >= "4.1"
|
||||
def new_join_association(reflection, children, klass)
|
||||
Polyamorous::JoinAssociation.new reflection, children, klass
|
||||
end
|
||||
else
|
||||
def new_join_association(reflection, join_dependency, parent, klass)
|
||||
Polyamorous::JoinAssociation.new reflection, join_dependency, parent, klass
|
||||
end
|
||||
end
|
||||
|
||||
if ActiveRecord::VERSION::STRING >= "5.2"
|
||||
def new_join_dependency(klass, associations = {})
|
||||
alias_tracker = ::ActiveRecord::Associations::AliasTracker.create(klass.connection, klass.table_name, [])
|
||||
Polyamorous::JoinDependency.new klass, klass.arel_table, associations, alias_tracker
|
||||
end
|
||||
else
|
||||
def new_join_dependency(klass, associations = {})
|
||||
Polyamorous::JoinDependency.new klass, associations, []
|
||||
end
|
||||
end
|
||||
|
||||
def new_join(name, type = Polyamorous::InnerJoin, klass = nil)
|
||||
Polyamorous::Join.new name, type, klass
|
||||
end
|
||||
end
|
|
@ -0,0 +1,54 @@
|
|||
require 'spec_helper'
|
||||
|
||||
module Polyamorous
|
||||
describe JoinAssociation do
|
||||
|
||||
join_base, join_association_args, polymorphic =
|
||||
if ActiveRecord::VERSION::STRING >= '4.1'
|
||||
[:join_root, 'parent.children', 'reflection.options[:polymorphic]']
|
||||
else
|
||||
[:join_base, 'join_dependency, parent', 'options[:polymorphic]']
|
||||
end
|
||||
|
||||
let(:join_dependency) { new_join_dependency Note, {} }
|
||||
let(:reflection) { Note.reflect_on_association(:notable) }
|
||||
let(:parent) { join_dependency.send(join_base) }
|
||||
let(:join_association) {
|
||||
eval("new_join_association(reflection, #{join_association_args}, Article)")
|
||||
}
|
||||
|
||||
subject {
|
||||
join_dependency.build_join_association_respecting_polymorphism(
|
||||
reflection, parent, Person
|
||||
)
|
||||
}
|
||||
|
||||
it 'respects polymorphism on equality test' do
|
||||
expect(subject).to eq(
|
||||
join_dependency.build_join_association_respecting_polymorphism(
|
||||
reflection, parent, Person
|
||||
)
|
||||
)
|
||||
expect(subject).not_to eq(
|
||||
join_dependency.build_join_association_respecting_polymorphism(
|
||||
reflection, parent, Article
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
it 'leaves the orginal reflection intact for thread safety' do
|
||||
reflection.instance_variable_set(:@klass, Article)
|
||||
join_association
|
||||
.swapping_reflection_klass(reflection, Person) do |new_reflection|
|
||||
expect(new_reflection.options).not_to equal reflection.options
|
||||
expect(new_reflection.options).not_to have_key(:polymorphic)
|
||||
expect(new_reflection.klass).to eq(Person)
|
||||
expect(reflection.klass).to eq(Article)
|
||||
end
|
||||
end
|
||||
|
||||
it 'sets the polymorphic option to true after initializing' do
|
||||
expect(join_association.instance_eval(polymorphic)).to be true
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,102 @@
|
|||
require 'spec_helper'
|
||||
|
||||
module Polyamorous
|
||||
describe JoinDependency do
|
||||
|
||||
method, join_associations, join_base =
|
||||
if ActiveRecord::VERSION::STRING >= '4.1'
|
||||
[:instance_eval, 'join_root.drop(1)', :join_root]
|
||||
else
|
||||
[:send, 'join_associations', :join_base]
|
||||
end
|
||||
|
||||
context 'with symbol joins' do
|
||||
subject { new_join_dependency Person, articles: :comments }
|
||||
|
||||
specify { expect(subject.send(method, join_associations).size)
|
||||
.to eq(2) }
|
||||
specify { expect(subject.send(method, join_associations).map(&:join_type))
|
||||
.to be_all { Polyamorous::InnerJoin } }
|
||||
end
|
||||
|
||||
context 'with has_many :through association' do
|
||||
subject { new_join_dependency Person, :authored_article_comments }
|
||||
|
||||
specify { expect(subject.send(method, join_associations).size)
|
||||
.to eq 1 }
|
||||
specify { expect(subject.send(method, join_associations).first.table_name)
|
||||
.to eq 'comments' }
|
||||
end
|
||||
|
||||
context 'with outer join' do
|
||||
subject { new_join_dependency Person, new_join(:articles, :outer) }
|
||||
|
||||
specify { expect(subject.send(method, join_associations).size)
|
||||
.to eq 1 }
|
||||
specify { expect(subject.send(method, join_associations).first.join_type)
|
||||
.to eq Polyamorous::OuterJoin }
|
||||
end
|
||||
|
||||
context 'with nested outer joins' do
|
||||
subject { new_join_dependency Person,
|
||||
new_join(:articles, :outer) => new_join(:comments, :outer) }
|
||||
|
||||
specify { expect(subject.send(method, join_associations).size)
|
||||
.to eq 2 }
|
||||
specify { expect(subject.send(method, join_associations).map(&:join_type))
|
||||
.to eq [Polyamorous::OuterJoin, Polyamorous::OuterJoin] }
|
||||
specify { expect(subject.send(method, join_associations).map(&:join_type))
|
||||
.to be_all { Polyamorous::OuterJoin } }
|
||||
end
|
||||
|
||||
context 'with polymorphic belongs_to join' do
|
||||
subject { new_join_dependency Note, new_join(:notable, :inner, Person) }
|
||||
|
||||
specify { expect(subject.send(method, join_associations).size)
|
||||
.to eq 1 }
|
||||
specify { expect(subject.send(method, join_associations).first.join_type)
|
||||
.to eq Polyamorous::InnerJoin }
|
||||
specify { expect(subject.send(method, join_associations).first.table_name)
|
||||
.to eq 'people' }
|
||||
|
||||
it 'finds a join association respecting polymorphism' do
|
||||
parent = subject.send(join_base)
|
||||
reflection = Note.reflect_on_association(:notable)
|
||||
|
||||
expect(subject.find_join_association_respecting_polymorphism(
|
||||
reflection, parent, Person))
|
||||
.to eq subject.send(method, join_associations).first
|
||||
end
|
||||
end
|
||||
|
||||
context 'with polymorphic belongs_to join and nested symbol join' do
|
||||
subject { new_join_dependency Note,
|
||||
new_join(:notable, :inner, Person) => :comments }
|
||||
|
||||
specify { expect(subject.send(method, join_associations).size)
|
||||
.to eq 2 }
|
||||
specify { expect(subject.send(method, join_associations).map(&:join_type))
|
||||
.to be_all { Polyamorous::InnerJoin } }
|
||||
specify { expect(subject.send(method, join_associations).first.table_name)
|
||||
.to eq 'people' }
|
||||
specify { expect(subject.send(method, join_associations)[1].table_name)
|
||||
.to eq 'comments' }
|
||||
end
|
||||
|
||||
context '#left_outer_join in Rails 5 overrides join type specified',
|
||||
if: ActiveRecord::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR < 2 do
|
||||
|
||||
let(:join_type_class) do
|
||||
new_join_dependency(
|
||||
Person,
|
||||
new_join(:articles)
|
||||
).join_constraints(
|
||||
[],
|
||||
Arel::Nodes::OuterJoin
|
||||
).first.joins.map(&:class)
|
||||
end
|
||||
|
||||
specify { expect(join_type_class).to eq [Arel::Nodes::OuterJoin] }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
require 'spec_helper'
|
||||
|
||||
module Polyamorous
|
||||
describe Join do
|
||||
it 'is a tree node' do
|
||||
join = new_join(:articles, :outer)
|
||||
expect(join).to be_kind_of(TreeNode)
|
||||
end
|
||||
|
||||
it 'can be added to a tree' do
|
||||
join = new_join(:articles, :outer)
|
||||
|
||||
tree_hash = {}
|
||||
join.add_to_tree(tree_hash)
|
||||
|
||||
expect(tree_hash[join]).to be {}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -44,6 +44,7 @@ RSpec.configure do |config|
|
|||
config.before(:each) { Sham.reset(:before_each) }
|
||||
|
||||
config.include RansackHelper
|
||||
config.include PolyamorousHelper
|
||||
end
|
||||
|
||||
RSpec::Matchers.define :be_like do |expected|
|
||||
|
|
Loading…
Reference in New Issue