Skip to content

Commit

Permalink
Implement depth as a subquery
Browse files Browse the repository at this point in the history
The main reason I introduced depth sql is to improve rebuild_depth_cache_sql!
Throwing this into the scopes is a bonus. (but not really)

This works great for the update, but the extra scopes will not perform well.
If you need to use them, consider adding an index on that equation
  • Loading branch information
kbrock committed Mar 26, 2023
1 parent de55f99 commit ed20bfa
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 10 deletions.
4 changes: 4 additions & 0 deletions lib/ancestry/class_methods.rb
Expand Up @@ -225,6 +225,10 @@ def rebuild_depth_cache!
end
end

def rebuild_depth_cache_sql!
update_all("#{depth_cache_column} = #{ancestry_depth_sql}")
end

def unscoped_where
yield ancestry_base_class.default_scoped.unscope(:where)
end
Expand Down
7 changes: 6 additions & 1 deletion lib/ancestry/has_ancestry.rb
Expand Up @@ -103,7 +103,12 @@ def has_ancestry options = {}
scope :from_depth, lambda { |depth| where("#{depth_cache_column} >= ?", depth) }
scope :after_depth, lambda { |depth| where("#{depth_cache_column} > ?", depth) }
else
# TODO: pure sql implementation of these scopse around depth_sql (from strategy)
# this is not efficient, but it works
scope :before_depth, lambda { |depth| where("#{ancestry_depth_sql} < ?", depth) }
scope :to_depth, lambda { |depth| where("#{ancestry_depth_sql} <= ?", depth) }
scope :at_depth, lambda { |depth| where("#{ancestry_depth_sql} = ?", depth) }
scope :from_depth, lambda { |depth| where("#{ancestry_depth_sql} >= ?", depth) }
scope :after_depth, lambda { |depth| where("#{ancestry_depth_sql} > ?", depth) }
end

# Create counter cache column accessor and set to option or default
Expand Down
9 changes: 9 additions & 0 deletions lib/ancestry/materialized_path.rb
Expand Up @@ -92,6 +92,15 @@ def ancestry_root
nil
end

def ancestry_depth_sql
@ancestry_depth_sql ||=
begin
tmp = %{(LENGTH(#{table_name}.#{ancestry_column}) - LENGTH(REPLACE(#{table_name}.#{ancestry_column},'#{ancestry_delimiter}','')))}
tmp = tmp + "/#{ancestry_delimiter.size}" if ancestry_delimiter.size > 1
"(CASE WHEN #{table_name}.#{ancestry_column} IS NULL THEN 0 ELSE 1 + #{tmp} END)"
end
end

private

def ancestry_validation_options(ancestry_primary_key_format)
Expand Down
9 changes: 9 additions & 0 deletions lib/ancestry/materialized_path2.rb
Expand Up @@ -28,6 +28,15 @@ def ancestry_root
ancestry_delimiter
end

def ancestry_depth_sql
@ancestry_depth_sql ||=
begin
tmp = %{(LENGTH(#{table_name}.#{ancestry_column}) - LENGTH(REPLACE(#{table_name}.#{ancestry_column},'#{ancestry_delimiter}','')))}
tmp = tmp + "/#{ancestry_delimiter.size}" if ancestry_delimiter.size > 1
"(#{tmp} -1)"
end
end

private

def ancestry_nil_allowed?
Expand Down
39 changes: 30 additions & 9 deletions test/concerns/depth_caching_test.rb
Expand Up @@ -36,19 +36,19 @@ def test_depth_scopes
end
end

def test_depth_scopes_unavailable
AncestryTestDatabase.with_model do |model|
refute model.respond_to?(:before_depth)
refute model.respond_to?(:to_depth)
refute model.respond_to?(:at_depth)
refute model.respond_to?(:from_depth)
refute model.respond_to?(:after_depth)
def test_depth_scopes_without_depth_cache
AncestryTestDatabase.with_model :depth => 4, :width => 2 do |model, _roots|
model.before_depth(2).all? { |node| assert node.depth < 2 }
model.to_depth(2).all? { |node| assert node.depth <= 2 }
model.at_depth(2).all? { |node| assert node.depth == 2 }
model.from_depth(2).all? { |node| assert node.depth >= 2 }
model.after_depth(2).all? { |node| assert node.depth > 2 }
end
end

def test_rebuild_depth_cache
AncestryTestDatabase.with_model :depth => 3, :width => 3, :cache_depth => :depth_cache do |model, _roots|
model.connection.execute("update test_nodes set depth_cache = null;")
model.update_all(:depth_cache => nil)

# Assert cache was emptied correctly
model.all.each do |test_node|
Expand All @@ -65,6 +65,27 @@ def test_rebuild_depth_cache
end
end

def test_rebuild_depth_cache_with_sql
AncestryTestDatabase.with_model :depth => 3, :width => 3, :cache_depth => :depth_cache do |model, _roots|
model.update_all(:depth_cache => nil)

# Assert cache was emptied correctly
model.all.each do |test_node|
assert_nil test_node.depth_cache
end

# Rebuild cache
# require "byebug"
# byebug
model.rebuild_depth_cache_sql!

# Assert cache was rebuild correctly
model.all.each do |test_node|
assert_equal test_node.depth, test_node.depth_cache
end
end
end

def test_exception_when_rebuilding_depth_cache_for_model_without_depth_caching
AncestryTestDatabase.with_model do |model|
assert_raise Ancestry::AncestryException do
Expand All @@ -80,4 +101,4 @@ def test_exception_on_unknown_depth_column
end
end
end
end
end

0 comments on commit ed20bfa

Please sign in to comment.