Last active
November 13, 2015 01:53
-
-
Save sidot3291/77b999ffeaa5d1d290b2 to your computer and use it in GitHub Desktop.
Lambda Scopes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Note: the lambda function's primary utility is scoping. We allow joins() | |
# in the lambda because it makes for readable nesting. | |
# Our "machine readable" tree takes this: | |
# joins({:assoc => -> { unscoped.joins(:nested_assoc) } }) | |
# And effectively converts it to this: | |
# joins({:assoc => {:nested_assoc=>{}, __scope__ => ->{ unscoped } }) | |
module ActiveRecord | |
class Relation | |
class HashMerger | |
def merge_joins | |
return if values[:joins].blank? | |
if other.klass == relation.klass | |
relation.joins!(*values[:joins]) | |
else | |
joins_dependency, rest = values[:joins].partition do |join| | |
case join | |
when Hash, Symbol, Array | |
true | |
else | |
false | |
end | |
end | |
join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass, | |
joins_dependency, | |
[], | |
:joins) | |
relation.joins! rest | |
@relation = relation.joins join_dependency | |
end | |
end | |
end | |
end | |
end | |
module ActiveRecord | |
module FinderMethods | |
def construct_join_dependency(joins = []) | |
including = eager_load_values + includes_values | |
puts "using eager load_type" | |
ActiveRecord::Associations::JoinDependency.new(@klass, including, joins, :eager_load) | |
end | |
end | |
end | |
module ActiveRecord | |
module QueryMethods | |
def build_joins(manager, joins) | |
buckets = joins.group_by do |join| | |
case join | |
when String | |
:string_join | |
when Hash, Symbol, Array | |
:association_join | |
when ActiveRecord::Associations::JoinDependency | |
:stashed_join | |
when Arel::Nodes::Join | |
:join_node | |
else | |
raise 'unknown class: %s' % join.class.name | |
end | |
end | |
association_joins = buckets[:association_join] || [] | |
stashed_association_joins = buckets[:stashed_join] || [] | |
join_nodes = (buckets[:join_node] || []).uniq | |
string_joins = (buckets[:string_join] || []).map(&:strip).uniq | |
join_list = join_nodes + custom_join_ast(manager, string_joins) | |
join_dependency = ActiveRecord::Associations::JoinDependency.new( | |
@klass, | |
association_joins, | |
join_list, | |
:joins | |
) | |
joins = join_dependency.join_constraints stashed_association_joins | |
joins.each { |join| manager.from(join) } | |
manager.join_sources.concat(join_list) | |
manager | |
end | |
def collapse_wheres(arel, wheres) | |
predicates = wheres.map do |where| | |
next where if ::Arel::Nodes::Equality === where | |
next where if ::Arel::Nodes::NotEqual === where | |
where = Arel.sql(where) if String === where | |
Arel::Nodes::Grouping.new(where) | |
end | |
arel.where(Arel::Nodes::And.new(predicates)) if predicates.present? | |
end | |
end | |
end | |
module ActiveRecord | |
module Associations | |
class JoinDependency | |
class JoinDummy | |
attr_accessor :klass | |
def initialize(klass, table) | |
self.klass = klass | |
klass.columns_hash.each do |col_name, data| | |
define_singleton_method(col_name.to_sym) do | |
table[__method__] | |
end | |
end | |
end | |
end | |
def initialize(base, associations, joins, load_type) | |
@alias_tracker = AliasTracker.create(base.connection, joins) | |
@alias_tracker.aliased_name_for(base.table_name, base.table_name) # Updates the count for base.table_name to 1 | |
tree = self.class.make_tree associations | |
@join_root = JoinBase.new base, build(tree, base, load_type) | |
@join_root.children.each { |child| construct_tables! @join_root, child } | |
end | |
#updated so the normalized tree's right hand side can | |
#either be an empty hash (old) OR a scoping lambda (new) | |
def self.walk_tree(associations, hash) | |
case associations | |
when Symbol, String | |
hash[associations.to_sym] ||= {} | |
when Array | |
associations.each do |assoc| | |
walk_tree assoc, hash | |
end | |
when Hash | |
associations.each do |k,v| | |
if v.is_a? Proc | |
hash[k] = v | |
else | |
cache = hash[k] ||= {} | |
walk_tree v, cache | |
end | |
end | |
else | |
raise ConfigurationError, associations.inspect | |
end | |
end | |
def build(associations, base_klass, load_type) | |
associations.map do |name, right| | |
reflection = find_reflection base_klass, name | |
reflection.check_validity! | |
if reflection.polymorphic? | |
raise EagerLoadPolymorphicError.new(reflection) | |
end | |
scoped_relation = reflection.klass.default_scoped | |
if right.is_a? Proc | |
scoped_relation = scoped_relation.instance_exec(&right) | |
additional_scope = right | |
else | |
scoped_relation = scoped_relation.send(load_type, right) | |
additional_scope = nil | |
end | |
scoped_relation_loads = scoped_relation.send(load_type.to_s+"_values") | |
if load_type == :eager_load | |
scoped_relation_loads.concat(scoped_relation.includes_values) | |
end | |
tree = self.class.make_tree scoped_relation_loads | |
children = build(tree, reflection.klass, load_type) | |
JoinAssociation.new reflection, additional_scope, children | |
end | |
end | |
end | |
end | |
end | |
require 'active_record/associations/join_dependency/join_part' | |
module ActiveRecord | |
module Associations | |
class JoinDependency | |
class JoinAssociation < JoinPart | |
attr_reader :additional_scope | |
def initialize(reflection, additional_scope, children) | |
super(reflection.klass, children) | |
@reflection = reflection | |
@additional_scope = additional_scope | |
@tables = nil | |
end | |
def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain) | |
joins = [] | |
tables = tables.reverse | |
scope_chain_index = 0 | |
scope_chain = scope_chain.reverse | |
# The chain starts with the target table, but we want to end with it here (makes | |
# more sense in this context), so we reverse | |
chain.reverse_each do |reflection| | |
table = tables.shift | |
klass = reflection.klass | |
case reflection.source_macro | |
when :belongs_to | |
key = reflection.association_primary_key | |
foreign_key = reflection.foreign_key | |
else | |
key = reflection.foreign_key | |
foreign_key = reflection.active_record_primary_key | |
end | |
constraint = build_constraint(klass, table, key, foreign_table, foreign_key) | |
rel = klass.send(:build_default_scope, ActiveRecord::Relation.create(klass, table)) | |
if rel.nil? | |
rel = ActiveRecord::Relation.create(klass, table) | |
end | |
scope_chain[scope_chain_index].each do |item| | |
if item.is_a?(Relation) | |
rel = rel.where(item.arel.constraints) | |
else | |
dummy = JoinDummy.new(foreign_klass, foreign_table) | |
rel = rel.instance_exec(dummy, &item) | |
end | |
end | |
if !additional_scope.nil? and scope_chain_index==(scope_chain.length-1) | |
rel = rel.instance_exec(&additional_scope) | |
end | |
scope_chain_index += 1 | |
if reflection.type | |
constraint = constraint.and table[reflection.type].eq foreign_klass.base_class.name | |
end | |
if rel && !rel.arel.constraints.empty? | |
constraint = constraint.and rel.arel.constraints | |
end | |
joins << table.create_join(table, table.create_on(constraint), join_type) | |
# The current table in this iteration becomes the foreign table in the next | |
foreign_table, foreign_klass = table, klass | |
end | |
joins | |
end | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment