From ed20bfa6c3215a00c0da8faf4482e98da6dd8be3 Mon Sep 17 00:00:00 2001 From: Keenan Brock Date: Sat, 25 Mar 2023 23:22:40 -0400 Subject: [PATCH] Implement depth as a subquery 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 --- lib/ancestry/class_methods.rb | 4 +++ lib/ancestry/has_ancestry.rb | 7 +++++- lib/ancestry/materialized_path.rb | 9 +++++++ lib/ancestry/materialized_path2.rb | 9 +++++++ test/concerns/depth_caching_test.rb | 39 ++++++++++++++++++++++------- 5 files changed, 58 insertions(+), 10 deletions(-) diff --git a/lib/ancestry/class_methods.rb b/lib/ancestry/class_methods.rb index 19908452..cd01ff3c 100644 --- a/lib/ancestry/class_methods.rb +++ b/lib/ancestry/class_methods.rb @@ -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 diff --git a/lib/ancestry/has_ancestry.rb b/lib/ancestry/has_ancestry.rb index 37b07018..ea959438 100644 --- a/lib/ancestry/has_ancestry.rb +++ b/lib/ancestry/has_ancestry.rb @@ -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 diff --git a/lib/ancestry/materialized_path.rb b/lib/ancestry/materialized_path.rb index c0f0fd00..acc167a6 100644 --- a/lib/ancestry/materialized_path.rb +++ b/lib/ancestry/materialized_path.rb @@ -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) diff --git a/lib/ancestry/materialized_path2.rb b/lib/ancestry/materialized_path2.rb index 01c64e0f..9c44f2e7 100644 --- a/lib/ancestry/materialized_path2.rb +++ b/lib/ancestry/materialized_path2.rb @@ -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? diff --git a/test/concerns/depth_caching_test.rb b/test/concerns/depth_caching_test.rb index 565edbbf..51a82a24 100644 --- a/test/concerns/depth_caching_test.rb +++ b/test/concerns/depth_caching_test.rb @@ -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| @@ -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 @@ -80,4 +101,4 @@ def test_exception_on_unknown_depth_column end end end -end \ No newline at end of file +end