Permalink
Browse files

Tree conditions now rely on nested set for performance reasons

  • Loading branch information...
binarylogic committed Dec 8, 2008
1 parent 1134bf4 commit 09a887d1afe840df37cf71d1b02e69f8892d35e1
View
@@ -48,7 +48,7 @@
# Condition
require "searchlogic/condition/base"
-require "searchlogic/condition/tree"
+require "searchlogic/condition/nested_set"
SEARCHLOGIC_CONDITIONS = [:begins_with, :blank, :child_of, :descendant_of, :ends_with, :equals, :greater_than, :greater_than_or_equal_to, :inclusive_descendant_of, :like, :nil, :not_begin_with, :not_blank, :not_end_with, :not_equal, :not_have_keywords, :not_nil, :keywords, :less_than, :less_than_or_equal_to, :sibling_of]
SEARCHLOGIC_CONDITIONS.each { |condition| require "searchlogic/condition/#{condition}" }
@@ -1,6 +1,6 @@
module Searchlogic
module Condition
- class ChildOf < Tree
+ class ChildOf < NestedSet
def to_conditions(value)
parent_association = klass.reflect_on_association(:parent)
foreign_key_name = (parent_association && parent_association.options[:foreign_key]) || "parent_id"
@@ -1,24 +1,11 @@
module Searchlogic
module Condition
- class DescendantOf < Tree
+ class DescendantOf < NestedSet
def to_conditions(value)
- # Wish I knew how to do this in SQL
- root = (value.is_a?(klass) ? value : klass.find(value)) rescue return
- strs = []
- subs = []
- all_children_ids(root).each do |child_id|
- strs << "#{quoted_table_name}.#{quote_column_name(klass.primary_key)} = ?"
- subs << child_id
- end
- [strs.join(" OR "), *subs]
+ condition = InclusiveDescendantOf.new(klass, options)
+ condition.value = value
+ merge_conditions(["#{quoted_table_name}.#{quote_column_name(klass.primary_key)} != ?", (value.is_a?(klass) ? value.send(klass.primary_key) : value)], condition.sanitize)
end
-
- private
- def all_children_ids(record)
- ids = record.children.collect { |child| child.send(klass.primary_key) }
- record.children.each { |child| ids += all_children_ids(child) }
- ids
- end
end
end
end
@@ -1,10 +1,9 @@
module Searchlogic
module Condition
- class InclusiveDescendantOf < Tree
+ class InclusiveDescendantOf < NestedSet
def to_conditions(value)
- condition = DescendantOf.new(klass, options)
- condition.value = value
- merge_conditions(["#{quoted_table_name}.#{quote_column_name(klass.primary_key)} = ?", (value.is_a?(klass) ? value.send(klass.primary_key) : value)], condition.sanitize, :any => true)
+ root = (value.is_a?(klass) ? value : klass.find(value)) rescue return
+ ["#{quoted_table_name}.#{quote_column_name(klass.left_column_name)} >= ? AND #{quoted_table_name}.#{quote_column_name(klass.right_column_name)} <= ?", root.left, root.right]
end
end
end
@@ -1,6 +1,6 @@
module Searchlogic
module Condition
- class Tree < Base # :nodoc:
+ class NestedSet < Base # :nodoc:
self.join_arrays_with_or = true
class << self
@@ -1,6 +1,6 @@
module Searchlogic
module Condition
- class SiblingOf < Tree
+ class SiblingOf < NestedSet
def to_conditions(value)
parent_association = klass.reflect_on_association(:parent)
foreign_key_name = (parent_association && parent_association.options[:foreign_key]) || "parent_id"
@@ -12,8 +12,8 @@ def test_has_many
assert_equal User, search.klass
assert_equal({:conditions => "\"users\".account_id = #{binary_logic.id}"}, search.scope)
- assert_equal [jennifer, ben], search.all
- assert_equal jennifer, search.first
+ assert_equal [ben, jennifer], search.all
+ assert_equal ben, search.first
assert_equal ((ben.id + jennifer.id) / 2.0), search.average("id")
assert_equal 2, search.count
@@ -70,10 +70,9 @@ def test_habtm
assert_kind_of Searchlogic::Search::Base, search
assert_equal User, search.klass
assert_equal({:conditions => "\"user_groups_users\".user_group_id = #{neco.id} ", :joins => "INNER JOIN \"user_groups_users\" ON \"users\".id = \"user_groups_users\".user_id"}, search.scope)
+ assert_equal [drew, ben], search.all
- assert_equal [ben, drew], search.all
-
- assert_equal ben, search.first
+ assert_equal drew, search.first
assert_equal ((ben.id + drew.id) / 2.0).to_s, search.average("id").to_s
assert_equal 2, search.count
@@ -4,13 +4,9 @@ module ConditionTests
class DescendantOfTest < ActiveSupport::TestCase
def test_sanitize
ben = users(:ben)
- drew = users(:drew)
- jennifer = users(:jennifer)
- tren = users(:tren)
-
condition = Searchlogic::Condition::DescendantOf.new(User)
condition.value = ben
- assert_equal ["\"users\".\"id\" = ? OR \"users\".\"id\" = ? OR \"users\".\"id\" = ?", drew.id, tren.id, jennifer.id], condition.sanitize
+ assert_equal ["\"users\".\"id\" != ? AND \"users\".\"lft\" >= ? AND \"users\".\"rgt\" <= ?", ben.id, ben.left, ben.right], condition.sanitize
end
end
end
@@ -4,13 +4,9 @@ module ConditionTests
class InclusiveDescendantOfTest < ActiveSupport::TestCase
def test_sanitize
ben = users(:ben)
- drew = users(:drew)
- jennifer = users(:jennifer)
- tren = users(:tren)
-
condition = Searchlogic::Condition::InclusiveDescendantOf.new(User)
condition.value = ben
- assert_equal ["\"users\".\"id\" = ? OR \"users\".\"id\" = ? OR \"users\".\"id\" = ? OR \"users\".\"id\" = ?", ben.id, drew.id, tren.id, jennifer.id], condition.sanitize
+ assert_equal ["\"users\".\"lft\" >= ? AND \"users\".\"rgt\" <= ?", ben.left, ben.right], condition.sanitize
end
end
end
@@ -4,17 +4,14 @@ module ConditionsTests
class MagicMethodsTest < ActiveSupport::TestCase
def test_class_level_conditions
ben = users(:ben)
- drew = users(:drew)
- jennifer = users(:jennifer)
- tren = users(:tren)
conditions = Searchlogic::Cache::UserConditions.new
conditions.descendant_of = "21"
assert_equal 21, conditions.descendant_of
conditions.descendant_of = ["21", "22"]
assert_equal [21, 22], conditions.descendant_of
conditions.descendant_of = ben
- assert_equal ["\"users\".\"id\" = ? OR \"users\".\"id\" = ? OR \"users\".\"id\" = ?", drew.id, tren.id, jennifer.id], conditions.sanitize
+ assert_equal ["\"users\".\"id\" != ? AND \"users\".\"lft\" >= ? AND \"users\".\"rgt\" <= ?", ben.id, ben.left, ben.right], conditions.sanitize
end
def test_virtual_columns
View
@@ -1,11 +1,11 @@
bens_order:
- user: ben
+ user_id: 1
total: 500.23
description: A bunch of apple products, etc.
receipt: some binary text
drews_order:
- user: drew
+ user_id: 2
total: 2.12
description: Some more apple projects, ipod, etc
receipt: some more binary text
@@ -1,7 +1,5 @@
neco:
name: NECO
- users: ben, drew
johnsons:
- name: Johnsons
- users: ben, jennifer
+ name: Johnsons
View
@@ -1,28 +1,43 @@
ben:
+ id: 1
account: binary_logic
+ lft: 1
+ rgt: 8
first_name: Ben
last_name: Johnson
active: true
bio: Totally awesome!
+ user_groups: neco, johnsons
drew:
+ id: 2
account: neco
- parent: ben
+ parent_id: 1
+ lft: 2
+ rgt: 5
first_name: Drew
last_name: Mills
active: false
bio: Totally not awesome!
+ user_groups: neco
jennifer:
+ id: 3
account: binary_logic
- parent: drew
+ parent_id: 2
+ lft: 3
+ rgt: 4
first_name: Jennifer
last_name: Hopkins
active: false
bio: Totally not awesome at all!
+ user_groups: johnsons
tren:
- parent: ben
+ id: 4
+ parent_id: 1
+ lft: 6
+ rgt: 7
first_name: Tren
last_name: Garfield
active: false
View
@@ -1,98 +0,0 @@
-module ActiveRecord
- module Acts
- module Tree
- def self.included(base)
- base.extend(ClassMethods)
- end
-
- # Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children
- # association. This requires that you have a foreign key column, which by default is called +parent_id+.
- #
- # class Category < ActiveRecord::Base
- # acts_as_tree :order => "name"
- # end
- #
- # Example:
- # root
- # \_ child1
- # \_ subchild1
- # \_ subchild2
- #
- # root = Category.create("name" => "root")
- # child1 = root.children.create("name" => "child1")
- # subchild1 = child1.children.create("name" => "subchild1")
- #
- # root.parent # => nil
- # child1.parent # => root
- # root.children # => [child1]
- # root.children.first.children.first # => subchild1
- #
- # In addition to the parent and children associations, the following instance methods are added to the class
- # after calling <tt>acts_as_tree</tt>:
- # * <tt>siblings</tt> - Returns all the children of the parent, excluding the current node (<tt>[subchild2]</tt> when called on <tt>subchild1</tt>)
- # * <tt>self_and_siblings</tt> - Returns all the children of the parent, including the current node (<tt>[subchild1, subchild2]</tt> when called on <tt>subchild1</tt>)
- # * <tt>ancestors</tt> - Returns all the ancestors of the current node (<tt>[child1, root]</tt> when called on <tt>subchild2</tt>)
- # * <tt>root</tt> - Returns the root of the current node (<tt>root</tt> when called on <tt>subchild2</tt>)
- module ClassMethods
- # Configuration options are:
- #
- # * <tt>foreign_key</tt> - specifies the column name to use for tracking of the tree (default: +parent_id+)
- # * <tt>order</tt> - makes it possible to sort the children according to this SQL snippet.
- # * <tt>counter_cache</tt> - keeps a count in a +children_count+ column if set to +true+ (default: +false+).
- def acts_as_tree(options = {})
- configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil }
- configuration.update(options) if options.is_a?(Hash)
-
- belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache]
- has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => :destroy
-
- class_eval <<-EOV
- include ActiveRecord::Acts::Tree::InstanceMethods
-
- def self.roots
- find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
- end
-
- def self.root
- find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
- end
- EOV
- end
- end
-
- module InstanceMethods
- # Returns list of ancestors, starting from parent until root.
- #
- # subchild1.ancestors # => [child1, root]
- def ancestors
- node, nodes = self, []
- nodes << node = node.parent while node.parent
- nodes
- end
-
- # Returns the root node of the tree.
- def root
- node = self
- node = node.parent while node.parent
- node
- end
-
- # Returns all siblings of the current node.
- #
- # subchild1.siblings # => [subchild2]
- def siblings
- self_and_siblings - [self]
- end
-
- # Returns all siblings and a reference to the current node.
- #
- # subchild1.self_and_siblings # => [subchild1, subchild2]
- def self_and_siblings
- parent ? parent.children : self.class.roots
- end
- end
- end
- end
-end
-
-ActiveRecord::Base.send :include, ActiveRecord::Acts::Tree
Oops, something went wrong.

0 comments on commit 09a887d

Please sign in to comment.