ActsAsFastNestedSet 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +4 -0
- data/Manifest.txt +7 -0
- data/README.txt +48 -0
- data/Rakefile +16 -0
- data/lib/acts_as_fast_nested_set.rb +217 -0
- data/lib/counter_string.rb +27 -0
- data/test/test_acts_as_fast_nested_set.rb +0 -0
- metadata +60 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
ActsAsFastNestedSet
|
2
|
+
by Adocca AB
|
3
|
+
http://www.adocca.com
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Fast nested set implementation
|
8
|
+
|
9
|
+
== FEATURES/PROBLEMS:
|
10
|
+
|
11
|
+
None!
|
12
|
+
|
13
|
+
== SYNOPSYS:
|
14
|
+
|
15
|
+
Just add acts_as_fast_nested_set in your AR::Base descendant model class
|
16
|
+
|
17
|
+
== REQUIREMENTS:
|
18
|
+
|
19
|
+
* Rails 1.1.6
|
20
|
+
|
21
|
+
== INSTALL:
|
22
|
+
|
23
|
+
* gem install acts_as_fast_nested_set
|
24
|
+
|
25
|
+
== LICENSE:
|
26
|
+
|
27
|
+
(The MIT License)
|
28
|
+
|
29
|
+
Copyright (c) 2006 Adocca AB
|
30
|
+
|
31
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
32
|
+
a copy of this software and associated documentation files (the
|
33
|
+
'Software'), to deal in the Software without restriction, including
|
34
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
35
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
36
|
+
permit persons to whom the Software is furnished to do so, subject to
|
37
|
+
the following conditions:
|
38
|
+
|
39
|
+
The above copyright notice and this permission notice shall be
|
40
|
+
included in all copies or substantial portions of the Software.
|
41
|
+
|
42
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
43
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
44
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
45
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
46
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
47
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
48
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require './lib/counter_string.rb'
|
6
|
+
require './lib/acts_as_fast_nested_set.rb'
|
7
|
+
|
8
|
+
Hoe.new('ActsAsFastNestedSet', Adocca::Acts::ActsAsFastNestedSet::VERSION) do |p|
|
9
|
+
p.rubyforge_name = 'adocca_plugins'
|
10
|
+
p.summary = 'Fast nested set, really fast!'
|
11
|
+
# p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
|
12
|
+
# p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
|
13
|
+
p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
|
14
|
+
end
|
15
|
+
|
16
|
+
# vim: syntax=Ruby
|
@@ -0,0 +1,217 @@
|
|
1
|
+
require 'counter_string'
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
#
|
5
|
+
# Defines an act that behaves a bit like nested_set or tree, but is hopefully much faster.
|
6
|
+
#
|
7
|
+
# Like the nested_set it is much faster than tree when selecting subtrees, but unlike nested_set
|
8
|
+
# it wont have to rewrite the entire tree for each insertion or deletion.
|
9
|
+
#
|
10
|
+
# Each node is connected to a parent, and each node has a node_id.
|
11
|
+
#
|
12
|
+
# The node_id is created by concatenating a new identifier to the parents node_id, or just creating
|
13
|
+
# a brand new identifier if no parent is present, and appending a dot.
|
14
|
+
#
|
15
|
+
# An identifier is alphabetical, and the newest identifier is always alphabetically greater than
|
16
|
+
# the identifiers of its siblings.
|
17
|
+
#
|
18
|
+
# An example list of valid node_ids:
|
19
|
+
# AAAA. <- a top level node, with only one trailing dot
|
20
|
+
# AAAA.AAAA. <- a child of the node above
|
21
|
+
# AAAA.AAAB. <- sibling nr one
|
22
|
+
# AAAA.AAAB.AAAA. <- a child of sibling nr one
|
23
|
+
# AAAA.AAAC. <- sibling nr two
|
24
|
+
# AAAB. <- top level node two
|
25
|
+
# AAAC. <- top level node three
|
26
|
+
# AAAC.AAAA. <- child of top level node three
|
27
|
+
# AAAD. <- top level node four
|
28
|
+
#
|
29
|
+
# This structure makes it easy to find all children of a node:
|
30
|
+
# SELECT * FROM table WHERE node_id LIKE 'AAAA.%' <- selects top level node one and all its children
|
31
|
+
# SELECT * FROM table WHERE node_id LIKE 'AAAA.AAAB.%' <- selects sibling nr two and all its children
|
32
|
+
#
|
33
|
+
# Deleting is naturally equally easy.
|
34
|
+
#
|
35
|
+
# The tricky bit is generating node_ids - for that we use the counter_string, which increments like:
|
36
|
+
# "aaaa".next == "aaab"
|
37
|
+
# "ZZZZ".next == "ZZZa"
|
38
|
+
# "zzzz".next == "zzzzAAAA"
|
39
|
+
#
|
40
|
+
# This keeps the relation between node_ids within the tree intact, as well as provides a big (140000 entries)
|
41
|
+
# namespace for each level of the tree using only 5 chars in the node_id column, and providing an even bigger
|
42
|
+
# (basically unlimited) namespace with more than 5 chars in the node_id column (it will provide 140000 more entries
|
43
|
+
# for each extra 4 chars after the first 5)
|
44
|
+
#
|
45
|
+
# If you want to have several trees with different sets of node_ids (for example, several comment-trees connected
|
46
|
+
# to different models in the system), you can add :uniqueness_scope as a param to the acts_as_fast_nested_set
|
47
|
+
# method.
|
48
|
+
#
|
49
|
+
# Example:
|
50
|
+
# acts_as_fast_nested_set :uniqueness_scope => [:forum_id, :forum_class] <- Will have a separate tree of node_ids
|
51
|
+
# for each unique combination of forum_id
|
52
|
+
# and forum_class in the model.
|
53
|
+
#
|
54
|
+
module Adocca
|
55
|
+
module Acts
|
56
|
+
module ActsAsFastNestedSet
|
57
|
+
VERSION = "0.0.1"
|
58
|
+
|
59
|
+
def self.append_features(base)
|
60
|
+
super
|
61
|
+
base.extend(ClassMethods)
|
62
|
+
end
|
63
|
+
|
64
|
+
module ClassMethods
|
65
|
+
#
|
66
|
+
# Makes this model act as a fast_nested_set. It needs to
|
67
|
+
# have parent_id, node_id and naturally id among its attributes.
|
68
|
+
#
|
69
|
+
# If you want to create different trees (with colliding node_ids, use
|
70
|
+
# the :uniqueness_scope param.
|
71
|
+
#
|
72
|
+
# Example:
|
73
|
+
# acts_as_fast_nested_set :uniqueness_scope => [:forum_id, :forum_class] <- Will have a separate tree of node_ids
|
74
|
+
# for each unique combination of forum_id
|
75
|
+
# and forum_class in the model.
|
76
|
+
#
|
77
|
+
def acts_as_fast_nested_set(options = {})
|
78
|
+
belongs_to :parent, :class_name => self.name, :foreign_key => 'parent_id'
|
79
|
+
has_many :children, :class_name => self.name, :foreign_key => 'parent_id'
|
80
|
+
attr_protected :node_id, :parent_id
|
81
|
+
if options[:uniqueness_scope]
|
82
|
+
validates_uniqueness_of :node_id, :scope => options[:uniqueness_scope]
|
83
|
+
else
|
84
|
+
validates_uniqueness_of :node_id
|
85
|
+
end
|
86
|
+
validate :ensure_node_id
|
87
|
+
class_eval do
|
88
|
+
extend Adocca::Acts::ActsAsFastNestedSet::SingletonMethods
|
89
|
+
end
|
90
|
+
set_options(options)
|
91
|
+
include Adocca::Acts::ActsAsFastNestedSet::InstanceMethods
|
92
|
+
end
|
93
|
+
end
|
94
|
+
module SingletonMethods
|
95
|
+
def set_options(options)
|
96
|
+
@@options = options
|
97
|
+
end
|
98
|
+
def get_options
|
99
|
+
@@options
|
100
|
+
end
|
101
|
+
end
|
102
|
+
module InstanceMethods
|
103
|
+
#
|
104
|
+
# Returns the entire thread
|
105
|
+
#
|
106
|
+
def kin
|
107
|
+
self.class.find(:all, :conditions => ["node_id LIKE ?#{unique_with_and}",
|
108
|
+
"#{root_id}.%"] +
|
109
|
+
unique_params)
|
110
|
+
end
|
111
|
+
#
|
112
|
+
# Destroys all children and their children etc
|
113
|
+
#
|
114
|
+
def destroy_descendants
|
115
|
+
self.class.connection.delete(
|
116
|
+
self.class.sanitizeSQL(
|
117
|
+
["DELETE FROM comments WHERE node_id LIKE ? AND id != ?#{unique_with_and}",
|
118
|
+
"#{self.node_id}%", self.id] + unique_params))
|
119
|
+
end
|
120
|
+
#
|
121
|
+
# All nodes with the same parent
|
122
|
+
#
|
123
|
+
def siblings
|
124
|
+
self.class.find_all_by_parent_id(self.parent_id)
|
125
|
+
end
|
126
|
+
#
|
127
|
+
# All our children and their children etc
|
128
|
+
#
|
129
|
+
def descendants
|
130
|
+
self.class.find(:all,
|
131
|
+
:conditions => ["node_id LIKE ? AND id != ?#{unique_with_and}",
|
132
|
+
"#{self.node_id}%", self.id] + unique_params)
|
133
|
+
end
|
134
|
+
|
135
|
+
def level
|
136
|
+
if self.node_id.nil?
|
137
|
+
1
|
138
|
+
else
|
139
|
+
self.node_id.count "."
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def unique_statement
|
146
|
+
self.class.get_options[:uniqueness_scope].nil? ? "" : "#{self.class.get_options[:uniqueness_scope].join(" = ? AND ")} = ?"
|
147
|
+
end
|
148
|
+
def unique_with_where
|
149
|
+
" WHERE #{unique_statement}"
|
150
|
+
end
|
151
|
+
def unique_with_and
|
152
|
+
" AND #{unique_statement}"
|
153
|
+
end
|
154
|
+
|
155
|
+
def unique_params
|
156
|
+
self.class.get_options[:uniqueness_scope].map do |att| self.send(att) end
|
157
|
+
end
|
158
|
+
|
159
|
+
def ensure_node_id
|
160
|
+
if self.node_id.nil? || self.node_id.empty?
|
161
|
+
if self.parent
|
162
|
+
self.node_id = "#{self.parent.node_id}#{next_node_id(self.parent.node_id)}."
|
163
|
+
else
|
164
|
+
self.node_id = "#{next_node_id}."
|
165
|
+
end
|
166
|
+
end
|
167
|
+
true
|
168
|
+
end
|
169
|
+
|
170
|
+
def root_id
|
171
|
+
if self.node_id.nil?
|
172
|
+
nil
|
173
|
+
else
|
174
|
+
if (match = self.node_id.match(/^([[:alpha:]]+)\..*$/))
|
175
|
+
match[1]
|
176
|
+
else
|
177
|
+
raise "My node_id '#{self.node_id}' is illegal!"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def next_node_id(parentNodeId = nil)
|
183
|
+
if parentNodeId.nil?
|
184
|
+
maxId = self.class.connection.select_value(self.class.sanitizeSQL(["SELECT MAX(node_id) FROM comments#{unique_with_where}"] + unique_params))
|
185
|
+
if maxId.nil? # there are no other root nodes
|
186
|
+
"AAAA"
|
187
|
+
else # there are other root nodes
|
188
|
+
if (match = maxId.match(/^([[:alpha:]]+)\..*$/)) # the latest root-node is properly formed
|
189
|
+
CounterString.new(match[1]).next
|
190
|
+
else # its not properly formed
|
191
|
+
raise "node_id '#{maxId}' is not a legal node_id!"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
else # this is not a root node
|
195
|
+
maxId = self.class.connection.select_value(self.class.sanitizeSQL(["SELECT MAX(node_id) FROM comments WHERE node_id LIKE ?#{unique_with_and}", "#{parentNodeId}%"] + unique_params))
|
196
|
+
if maxId.nil? # there are no nodes like the parent
|
197
|
+
raise "Cant find parent, no node with id '#{parentNodeId}'!"
|
198
|
+
else # there are nodes like the parent
|
199
|
+
restPart = maxId[(parentNodeId.size)..-1]
|
200
|
+
if restPart.empty? # but no children of the parent
|
201
|
+
"AAAA"
|
202
|
+
else # there are children of the parent
|
203
|
+
if (match = restPart.match(/^([[:alpha:]]+)\..*$/)) # and they are properly formed
|
204
|
+
CounterString.new(match[1]).next
|
205
|
+
else # but not properly formed
|
206
|
+
raise "node_id '#{maxId}' is not a legal descendant of '#{parentNodeId}'!"
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end # of def next_node_id
|
212
|
+
end # of module InstanceMethods
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
ActiveRecord::Base.send(:include, Adocca::Acts::ActsAsFastNestedSet)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class CounterString < String
|
2
|
+
|
3
|
+
def next
|
4
|
+
(self.size - 1).downto(0) do |index|
|
5
|
+
this_ord = self[index]
|
6
|
+
if new_ord = next_ord(this_ord)
|
7
|
+
return CounterString.new("#{self[0...index]}#{new_ord.chr}".ljust(self.size, "a"))
|
8
|
+
end
|
9
|
+
end
|
10
|
+
CounterString.new(self.ljust(self.size + 4, "a"))
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def next_ord(ord)
|
16
|
+
if ord < 90
|
17
|
+
ord + 1
|
18
|
+
elsif ord == 90
|
19
|
+
97
|
20
|
+
elsif ord < 122
|
21
|
+
ord + 1
|
22
|
+
else
|
23
|
+
false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
File without changes
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.0
|
3
|
+
specification_version: 1
|
4
|
+
name: ActsAsFastNestedSet
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.0.1
|
7
|
+
date: 2006-11-13 00:00:00 +01:00
|
8
|
+
summary: Fast nested set, really fast!
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: ryand-ruby@zenspider.com
|
12
|
+
homepage: http://www.zenspider.com/ZSS/Products/ActsAsFastNestedSet/
|
13
|
+
rubyforge_project: adocca_plugins
|
14
|
+
description: Ryan Davis is too lazy to write a description
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Ryan Davis
|
31
|
+
files:
|
32
|
+
- History.txt
|
33
|
+
- Manifest.txt
|
34
|
+
- README.txt
|
35
|
+
- Rakefile
|
36
|
+
- lib/acts_as_fast_nested_set.rb
|
37
|
+
- lib/counter_string.rb
|
38
|
+
- test/test_acts_as_fast_nested_set.rb
|
39
|
+
test_files:
|
40
|
+
- test/test_acts_as_fast_nested_set.rb
|
41
|
+
rdoc_options: []
|
42
|
+
|
43
|
+
extra_rdoc_files: []
|
44
|
+
|
45
|
+
executables: []
|
46
|
+
|
47
|
+
extensions: []
|
48
|
+
|
49
|
+
requirements: []
|
50
|
+
|
51
|
+
dependencies:
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: hoe
|
54
|
+
version_requirement:
|
55
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: 1.1.4
|
60
|
+
version:
|