Skip to content

Commit 820131b

Browse files
committed
support hash_tree from relations
1 parent f8c6867 commit 820131b

File tree

6 files changed

+213
-156
lines changed

6 files changed

+213
-156
lines changed

lib/closure_tree/active_record_support.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
module ClosureTree
22
module ActiveRecordSupport
3+
4+
def quote(field)
5+
connection.quote(field)
6+
end
7+
38
def ensure_fixed_table_name(table_name)
49
[
510
ActiveRecord::Base.table_name_prefix,

lib/closure_tree/hash_tree.rb

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,16 @@ module ClosureTree
22
module HashTree
33
extend ActiveSupport::Concern
44

5-
def hash_tree_scope(limit_depth = nil)
6-
scope = self_and_descendants
7-
if limit_depth
8-
scope.where("#{_ct.quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}")
9-
else
10-
scope
11-
end
12-
end
13-
145
def hash_tree(options = {})
15-
self.class.build_hash_tree(hash_tree_scope(options[:limit_depth]))
6+
_ct.hash_tree(self_and_descendants, options[:limit_depth])
167
end
178

189
module ClassMethods
1910

2011
# There is no default depth limit. This might be crazy-big, depending
2112
# on your tree shape. Hash huge trees at your own peril!
2213
def hash_tree(options = {})
23-
build_hash_tree(hash_tree_scope(options[:limit_depth]))
24-
end
25-
26-
def hash_tree_scope(limit_depth = nil)
27-
# Deepest generation, within limit, for each descendant
28-
# NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
29-
having_clause = limit_depth ? "HAVING MAX(generations) <= #{limit_depth - 1}" : ''
30-
generation_depth = <<-SQL.strip_heredoc
31-
INNER JOIN (
32-
SELECT descendant_id, MAX(generations) as depth
33-
FROM #{_ct.quoted_hierarchy_table_name}
34-
GROUP BY descendant_id
35-
#{having_clause}
36-
) AS generation_depth
37-
ON #{_ct.quoted_table_name}.#{primary_key} = generation_depth.descendant_id
38-
SQL
39-
_ct.scope_with_order(joins(generation_depth), "generation_depth.depth")
40-
end
41-
42-
# Builds nested hash structure using the scope returned from the passed in scope
43-
def build_hash_tree(tree_scope)
44-
tree = ActiveSupport::OrderedHash.new
45-
id_to_hash = {}
46-
47-
tree_scope.each do |ea|
48-
h = id_to_hash[ea.id] = ActiveSupport::OrderedHash.new
49-
if ea.root? || tree.empty? # We're at the top of the tree.
50-
tree[ea] = h
51-
else
52-
id_to_hash[ea._ct_parent_id][ea] = h
53-
end
54-
end
55-
tree
14+
_ct.hash_tree(nil, options[:limit_depth])
5615
end
5716
end
5817
end
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
module ClosureTree
2+
module HashTreeSupport
3+
def default_tree_scope(limit_depth = nil)
4+
# Deepest generation, within limit, for each descendant
5+
# NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
6+
having_clause = limit_depth ? "HAVING MAX(generations) <= #{limit_depth - 1}" : ''
7+
generation_depth = <<-SQL.strip_heredoc
8+
INNER JOIN (
9+
SELECT descendant_id, MAX(generations) as depth
10+
FROM #{quoted_hierarchy_table_name}
11+
GROUP BY descendant_id
12+
#{having_clause}
13+
) AS generation_depth
14+
ON #{quoted_table_name}.#{model_class.primary_key} = generation_depth.descendant_id
15+
SQL
16+
scope_with_order(model_class.joins(generation_depth), 'generation_depth.depth')
17+
end
18+
19+
def hash_tree(tree_scope, limit_depth = nil)
20+
limited_scope = if tree_scope
21+
limit_depth ? tree_scope.where("#{quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}") : tree_scope
22+
else
23+
default_tree_scope(limit_depth)
24+
end
25+
build_hash_tree(limited_scope)
26+
end
27+
28+
# Builds nested hash structure using the scope returned from the passed in scope
29+
def build_hash_tree(tree_scope)
30+
tree = ActiveSupport::OrderedHash.new
31+
id_to_hash = {}
32+
33+
tree_scope.each do |ea|
34+
h = id_to_hash[ea.id] = ActiveSupport::OrderedHash.new
35+
(id_to_hash[ea._ct_parent_id] || tree)[ea] = h
36+
end
37+
tree
38+
end
39+
end
40+
end

lib/closure_tree/model.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@ module Model
1717
class_name: _ct.model_class.to_s,
1818
foreign_key: _ct.parent_column_name,
1919
dependent: _ct.options[:dependent],
20-
inverse_of: :parent)
20+
inverse_of: :parent) do
21+
# We have to redefine hash_tree because the activerecord relation is already scoped to parent_id.
22+
def hash_tree(options = {})
23+
# we want limit_depth + 1 because we don't do self_and_descendants.
24+
limit_depth = options[:limit_depth]
25+
_ct.hash_tree(@association.owner.descendants, limit_depth ? limit_depth + 1 : nil)
26+
end
27+
end
2128

2229
has_many :ancestor_hierarchies, *_ct.has_many_without_order_option(
2330
class_name: _ct.hierarchy_class_name,

lib/closure_tree/support.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require 'closure_tree/support_attributes'
33
require 'closure_tree/numeric_order_support'
44
require 'closure_tree/active_record_support'
5+
require 'closure_tree/hash_tree_support'
56
require 'with_advisory_lock'
67

78
# This class and mixins are an effort to reduce the namespace pollution to models that act_as_tree.
@@ -10,6 +11,7 @@ class Support
1011
include ClosureTree::SupportFlags
1112
include ClosureTree::SupportAttributes
1213
include ClosureTree::ActiveRecordSupport
14+
include ClosureTree::HashTreeSupport
1315

1416
attr_reader :model_class
1517
attr_reader :options
@@ -59,10 +61,6 @@ def hierarchy_table_name
5961
ActiveRecord::Base.table_name_prefix + tablename + ActiveRecord::Base.table_name_suffix
6062
end
6163

62-
def quote(field)
63-
connection.quote(field)
64-
end
65-
6664
def with_order_option(opts)
6765
if order_option?
6866
opts[:order] = [opts[:order], order_by].compact.join(",")

0 commit comments

Comments
 (0)