Skip to content

Commit e466c08

Browse files
committed
Scope for leaves
1 parent 7fdb8f3 commit e466c08

File tree

4 files changed

+69
-2
lines changed

4 files changed

+69
-2
lines changed

Diff for: lib/ancestry/class_methods.rb

+31
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ def scope_depth depth_options, depth
1717
end
1818
end
1919

20+
# Scope that returns all the leaves
21+
def leaves
22+
id_column = "#{table_name}.id"
23+
id_column_as_text = sql_cast_as_text(id_column)
24+
parent_ancestry = sql_concat("#{table_name}.#{ancestry_column}", "'/'", id_column_as_text)
25+
26+
joins("LEFT JOIN #{table_name} AS c ON c.#{ancestry_column} = #{id_column_as_text} OR c.#{ancestry_column} = #{parent_ancestry}").
27+
group(id_column).
28+
having('COUNT(c.id) = 0')
29+
end
30+
2031
# Orphan strategy writer
2132
def orphan_strategy= orphan_strategy
2233
# Check value of orphan strategy, only rootify, adopt, restrict or destroy is allowed
@@ -197,5 +208,25 @@ def rebuild_depth_cache!
197208
end
198209
end
199210
end
211+
212+
private
213+
214+
def sql_concat *parts
215+
if ActiveRecord::Base.connection.adapter_name.downcase == 'sqlite'
216+
parts.join(' || ')
217+
else
218+
"CONCAT(#{parts.join(', ')})"
219+
end
220+
end
221+
222+
def sql_cast_as_text column
223+
text_type = if ActiveRecord::Base.connection.adapter_name.downcase == 'mysql'
224+
'CHAR'
225+
else
226+
'TEXT'
227+
end
228+
229+
"CAST(#{column} AS #{text_type})"
230+
end
200231
end
201232
end

Diff for: lib/ancestry/has_ancestry.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,4 @@ class << ActiveRecord::Base
8686
alias_method :acts_as_tree, :has_ancestry
8787
end
8888
end
89-
end
89+
end

Diff for: test/concerns/class_methods_test.rb

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
require_relative '../environment'
2+
3+
class ClassMethodsTest < ActiveSupport::TestCase
4+
def test_sql_concat
5+
AncestryTestDatabase.with_model do |model|
6+
result = model.send(:sql_concat, 'table_name.id', "'/'")
7+
8+
case ActiveRecord::Base.connection.adapter_name.downcase.to_sym
9+
when :sqlite
10+
assert_equal result, "table_name.id || '/'"
11+
when :mysql
12+
assert_equal result, "CONCAT(table_name.id, '/')"
13+
when :postgresql
14+
assert_equal result, "CONCAT(table_name.id, '/')"
15+
end
16+
end
17+
end
18+
19+
def text_sql_cast_as_text
20+
AncestryTestDatabase.with_model do |model|
21+
result = model.send(:sql_cast_as_text, 'table_name.id')
22+
23+
case ActiveRecord::Base.connection.adapter_name.downcase.to_sym
24+
when :sqlite
25+
assert_equal result, 'CAST(table_name.id AS TEXT)'
26+
when :mysql
27+
assert_equal result, 'CAST(table_name.id AS CHAR)'
28+
when :postgresql
29+
assert_equal result, 'CAST(table_name.id AS TEXT)'
30+
end
31+
end
32+
end
33+
end

Diff for: test/concerns/scopes_test.rb

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ def test_scopes
66
# Roots assertion
77
assert_equal roots.map(&:first), model.roots.to_a
88

9+
# Leaves assertion
10+
assert_equal model.all.select(&:is_childless?), model.leaves.order(:id).to_a
11+
912
model.all.each do |test_node|
1013
# Assertions for ancestors_of named scope
1114
assert_equal test_node.ancestors.to_a, model.ancestors_of(test_node).to_a
@@ -66,4 +69,4 @@ def test_scoping_in_callbacks
6669
assert child = parent.children.create
6770
end
6871
end
69-
end
72+
end

0 commit comments

Comments
 (0)