Skip to content

polymorphic_root and bugfix on insert #419

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions lib/closure_tree/hierarchy_maintenance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ def rebuild!(called_by_rebuild = false)
unless root?
_ct.connection.execute <<-SQL.squish
INSERT INTO #{_ct.quoted_hierarchy_table_name}
(ancestor_id, descendant_id, generations)
SELECT x.ancestor_id, #{_ct.quote(_ct_id)}, x.generations + 1
(ancestor_id, ancestor_type, descendant_id, descendant_type, generations)
SELECT x.ancestor_id, x.ancestor_type, #{_ct.quote(_ct_id)}, #{_ct.quote(_ct.model_class.to_s)}, x.generations + 1
FROM #{_ct.quoted_hierarchy_table_name} x
WHERE x.descendant_id = #{_ct.quote(_ct_parent_id)}
WHERE x.descendant_id = #{_ct.quote(_ct_parent_id)} AND x.descendant_type = #{_ct.quote(parent.class.to_s)}
SQL
end

Expand Down Expand Up @@ -97,8 +97,8 @@ def delete_hierarchy_references
SELECT DISTINCT descendant_id
FROM (SELECT descendant_id
FROM #{_ct.quoted_hierarchy_table_name}
WHERE ancestor_id = #{_ct.quote(id)}
OR descendant_id = #{_ct.quote(id)}
WHERE (ancestor_id = #{_ct.quote(id)} AND ancestor_type = #{_ct.quote(self.class.to_s)})
OR (descendant_id = #{_ct.quote(id)} AND descendant_type = #{_ct.quote(self.class.to_s)})
) #{ _ct.t_alias_keyword } x )
SQL
end
Expand All @@ -119,13 +119,15 @@ def cleanup!
hierarchy_table = hierarchy_class.arel_table

[:descendant_id, :ancestor_id].each do |foreign_key|
alias_name = foreign_key.to_s.split('_').first + "s"
key_prefix = foreign_key.to_s.split('_')
key_as_type = "#{key_prefix}_type".to_sym
alias_name = key_prefix.first + "s"
alias_table = Arel::Table.new(table_name).alias(alias_name)
arel_join = hierarchy_table.join(alias_table, Arel::Nodes::OuterJoin)
.on(alias_table[primary_key].eq(hierarchy_table[foreign_key]))
.join_sources

lonely_childs = hierarchy_class.joins(arel_join).where(alias_table[primary_key].eq(nil))
lonely_childs = hierarchy_class.joins(arel_join).where(alias_table[primary_key].eq(nil)).where(key_as_type => self.class.to_s)
ids = lonely_childs.pluck(foreign_key)

hierarchy_class.where(hierarchy_table[foreign_key].in(ids)).delete_all
Expand Down
42 changes: 36 additions & 6 deletions lib/closure_tree/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@ module Model
extend ActiveSupport::Concern

included do

belongs_to :parent, nil,
class_name: _ct.model_class.to_s,
foreign_key: _ct.parent_column_name,
inverse_of: :children,
touch: _ct.options[:touch],
optional: true
optional: true,
polymorphic: true

where_for_ancestors = { ancestor_type: _ct.model_class.to_s }
where_for_descendants = { descendant_type: _ct.model_class.to_s }
order_by_generations = -> { Arel.sql("#{_ct.quoted_hierarchy_table_name}.generations ASC") }

has_many :children, *_ct.has_many_order_with_option, **{
has_many :children, *_ct.has_many_order_with_option_and_where({ parent_type: _ct.model_class.to_s }), **{
class_name: _ct.model_class.to_s,
foreign_key: _ct.parent_column_name,
dependent: _ct.options[:dependent],
Expand All @@ -28,21 +30,35 @@ def hash_tree(options = {})
end
end

has_many :ancestor_hierarchies, *_ct.has_many_order_without_option(order_by_generations),
has_many :ancestor_hierarchies, *_ct.has_many_order_without_option_and_where(order_by_generations, where_for_ancestors),
class_name: _ct.hierarchy_class_name,
foreign_key: 'descendant_id'

has_many :self_and_ancestors, *_ct.has_many_order_without_option(order_by_generations),
through: :ancestor_hierarchies,
source: :ancestor

has_many :descendant_hierarchies, *_ct.has_many_order_without_option(order_by_generations),
has_many :descendant_hierarchies, *_ct.has_many_order_without_option_and_where(order_by_generations, where_for_descendants),
class_name: _ct.hierarchy_class_name,
foreign_key: 'ancestor_id'

has_many :self_and_descendants, *_ct.has_many_order_with_option(order_by_generations),
through: :descendant_hierarchies,
source: :descendant
source: :descendant, source_type: _ct.model_class.to_s
end

def poly_children
pp hierarchy_class.all.map { |h| h.slice(:ancestor_type, :ancestor_id, :descendant_type, :descendant_id, :generations) }

foo = hierarchy_class
.where(ancestor_id: id, ancestor_type: _ct.model_class.to_s, generations: 1)
.map { |h_data| { type: h_data.descendant_type, id: h_data.descendant_id } }
.group_by { |h_data| h_data[:type] }
.map { |type, h_data_array| type.constantize.where(id: h_data_array.map { |h_data| h_data[:id] }) }
.flatten

p foo
foo
end

# Delegate to the Support instance on the class:
Expand Down Expand Up @@ -72,6 +88,20 @@ def root
self_and_ancestors.where(_ct.parent_column_name.to_sym => nil).first
end

def polymorphic_root
polymorphic_ancestor_lookup = {}

hierarchy_class.where(descendant_id: self.id).pluck(:ancestor_type, :ancestor_id).each do |ancestor_type, ancestor_id|
polymorphic_ancestor_lookup[ancestor_type] ||= []
polymorphic_ancestor_lookup[ancestor_type] << ancestor_id
end

polymorphic_ancestor_lookup.keys.each do |ancestor_class|
no_parent_ancestor = ancestor_class.constantize.where(id: polymorphic_ancestor_lookup[ancestor_class], parent_id: nil).first
return no_parent_ancestor if no_parent_ancestor.present?
end
end

def leaves
self_and_descendants.leaves
end
Expand Down
20 changes: 15 additions & 5 deletions lib/closure_tree/support.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,10 @@ def hierarchy_class_for_model
hierarchy_class = parent_class.const_set(short_hierarchy_class_name, Class.new(model_class.superclass))
use_attr_accessible = use_attr_accessible?
include_forbidden_attributes_protection = include_forbidden_attributes_protection?
model_class_name = model_class.to_s
hierarchy_class.class_eval do
include ActiveModel::ForbiddenAttributesProtection if include_forbidden_attributes_protection
belongs_to :ancestor, class_name: model_class_name
belongs_to :descendant, class_name: model_class_name
belongs_to :ancestor, polymorphic: true
belongs_to :descendant, polymorphic: true
attr_accessible :ancestor, :descendant, :generations if use_attr_accessible
def ==(other)
self.class == other.class && ancestor_id == other.ancestor_id && descendant_id == other.descendant_id
Expand All @@ -58,8 +57,7 @@ def hierarchy_table_name
# We need to use the table_name, not something like ct_class.to_s.demodulize + "_hierarchies",
# because they may have overridden the table name, which is what we want to be consistent with
# in order for the schema to make sense.
tablename = options[:hierarchy_table_name] ||
remove_prefix_and_suffix(table_name).singularize + "_hierarchies"
tablename = "hierarchies"

ActiveRecord::Base.table_name_prefix + tablename + ActiveRecord::Base.table_name_suffix
end
Expand All @@ -84,6 +82,10 @@ def has_many_order_without_option(order_by_opt)
[lambda { order(order_by_opt.call) }]
end

def has_many_order_without_option_and_where(order_by_opt, where_clause)
[lambda { where(where_clause).order(order_by_opt.call) }]
end

def has_many_order_with_option(order_by_opt=nil)
order_options = [order_by_opt, order_by].compact
[lambda {
Expand All @@ -92,6 +94,14 @@ def has_many_order_with_option(order_by_opt=nil)
}]
end

def has_many_order_with_option_and_where(order_by_opt=nil, where_clause)
order_options = [order_by_opt, order_by].compact
[lambda {
order_options = order_options.map { |o| o.is_a?(Proc) ? o.call : o }
where(where_clause).order(order_options)
}]
end

def ids_from(scope)
scope.pluck(model_class.primary_key)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/closure_tree/support_attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def quoted_value(value)
end

def hierarchy_class_name
options[:hierarchy_class_name] || model_class.to_s + "Hierarchy"
"Hierarchy"
end

def primary_key_column
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
def change
create_table :<%= migration_name %>, id: false do |t|
create_table :hierarchies, id: false do |t|
t.<%= primary_key_type %> :ancestor_id, null: false
t.string :ancestor_type, null: false
t.<%= primary_key_type %> :descendant_id, null: false
t.string :descendant_type, null: false
t.integer :generations, null: false
end

add_index :<%= migration_name %>, [:ancestor_id, :descendant_id, :generations],
add_index :hierarchies, [:ancestor_id, :ancestor_type, :descendant_id, :descendant_type, :generations],
unique: true,
name: "<%= file_name %>_anc_desc_idx"
name: "anc_desc_idx"

add_index :<%= migration_name -%>, [:descendant_id],
name: "<%= file_name %>_desc_idx"
add_index :hierarchies, [:descendant_id],
name: "desc_idx"
end
end