oakridgelanl-materialized-path 0.0.2
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/README +39 -0
- data/lib/acts_as_materialized_path.rb +351 -0
- metadata +63 -0
data/README
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
ActsAsMaterializedPath
|
2
|
+
======================
|
3
|
+
|
4
|
+
This mixin implements a set of trees (nested set) in a single table.
|
5
|
+
|
6
|
+
Within its limitations, this plugin is meant to be more efficient than
|
7
|
+
existing implementations of nested sets.
|
8
|
+
|
9
|
+
|
10
|
+
Example
|
11
|
+
=======
|
12
|
+
|
13
|
+
In the class implementing the trees:
|
14
|
+
|
15
|
+
acts_as_materialized_path :delimiter => '.',
|
16
|
+
:base => 10,
|
17
|
+
:column => 'materialized_path',
|
18
|
+
:places => 3,
|
19
|
+
|
20
|
+
The values show here are the defaults.
|
21
|
+
|
22
|
+
|
23
|
+
In the migration file, ensure the path_column_name is not null and has
|
24
|
+
a unique index:
|
25
|
+
|
26
|
+
t.string :materialized_path, :null => false
|
27
|
+
|
28
|
+
add_index(:tree_table, [:materialized_path], :unique => true)
|
29
|
+
|
30
|
+
|
31
|
+
In addition, the collating sequence for ascii representable
|
32
|
+
characters, c, must be ascii or p < n < a, where ispunct(p) &&
|
33
|
+
isdigit(n) && isalpha(c) (this handles UTF-8 collation).
|
34
|
+
|
35
|
+
|
36
|
+
|
37
|
+
|
38
|
+
|
39
|
+
Copyright (c) 2008 Antony Nigel Donovan, released under the MIT license
|
@@ -0,0 +1,351 @@
|
|
1
|
+
# ActsAsMaterializedPath
|
2
|
+
module ActiveRecord #:nodoc:
|
3
|
+
module Acts #:nodoc:
|
4
|
+
module MaterializedPath #:nodoc:
|
5
|
+
#TODO - What's are appropraite exception types to raise?
|
6
|
+
#Also, add more information to errors raised.
|
7
|
+
class InvalidDelimiter < RangeError
|
8
|
+
end
|
9
|
+
class InvalidBase < RangeError
|
10
|
+
end
|
11
|
+
class InvalidAssignment < ArgumentError
|
12
|
+
end
|
13
|
+
class PathMaxExceeded < StandardError
|
14
|
+
end
|
15
|
+
class PathUpdateDisallowed < StandardError
|
16
|
+
end
|
17
|
+
class DestroyNotLeaf < TypeError
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.included(base)
|
21
|
+
base.extend(ClassMethods)
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
|
26
|
+
#
|
27
|
+
def acts_as_materialized_path(options = {})
|
28
|
+
conf = {
|
29
|
+
:delimiter => '.',
|
30
|
+
:base => 10,
|
31
|
+
:column => 'materialized_path',
|
32
|
+
:places => 3,
|
33
|
+
}
|
34
|
+
conf.update(options) if options.is_a?(Hash)
|
35
|
+
|
36
|
+
unless conf[:delimiter].length == 1 &&
|
37
|
+
(' '..'/') === conf[:delimiter]
|
38
|
+
raise InvalidDelimiter
|
39
|
+
end
|
40
|
+
|
41
|
+
unless (2..36) === conf[:base]
|
42
|
+
raise InvalidBase
|
43
|
+
end
|
44
|
+
|
45
|
+
conf[:adapter] =
|
46
|
+
ActiveRecord::Base.connection.adapter_name
|
47
|
+
|
48
|
+
#configuration settings
|
49
|
+
write_inheritable_attribute :mp_delimiter, conf[:delimiter]
|
50
|
+
class_inheritable_reader :mp_delimiter
|
51
|
+
|
52
|
+
write_inheritable_attribute :mp_base, conf[:base]
|
53
|
+
class_inheritable_reader :mp_base
|
54
|
+
|
55
|
+
write_inheritable_attribute :mp_column, conf[:column]
|
56
|
+
class_inheritable_reader :mp_column
|
57
|
+
|
58
|
+
write_inheritable_attribute :mp_places, conf[:places]
|
59
|
+
class_inheritable_reader :mp_places
|
60
|
+
|
61
|
+
#sql helpers
|
62
|
+
write_inheritable_attribute :mp_like, "#{mp_column} like ?"
|
63
|
+
class_inheritable_reader :mp_like
|
64
|
+
|
65
|
+
write_inheritable_attribute :mp_eq, "#{mp_column} = ?"
|
66
|
+
class_inheritable_reader :mp_eq
|
67
|
+
|
68
|
+
write_inheritable_attribute :mp_gt, "#{mp_column} > ?"
|
69
|
+
class_inheritable_reader :mp_gt
|
70
|
+
|
71
|
+
write_inheritable_attribute :mp_gte, "#{mp_column} >= ?"
|
72
|
+
class_inheritable_reader :mp_gte
|
73
|
+
|
74
|
+
write_inheritable_attribute :mp_lt, "#{mp_column} < ?"
|
75
|
+
class_inheritable_reader :mp_lt
|
76
|
+
|
77
|
+
write_inheritable_attribute :mp_asc, "#{mp_column} ASC"
|
78
|
+
class_inheritable_reader :mp_asc
|
79
|
+
|
80
|
+
write_inheritable_attribute :mp_desc, "#{mp_column} DESC"
|
81
|
+
class_inheritable_reader :mp_desc
|
82
|
+
|
83
|
+
limit_string = '~' # This only works with and asci collating sequence
|
84
|
+
limit_string = 'Z'*(mp_places+1) # This works with UTF-8
|
85
|
+
|
86
|
+
write_inheritable_attribute :mp_ancestor_limit,
|
87
|
+
case conf[:adapter]
|
88
|
+
when 'MySQL'
|
89
|
+
"concat(#{mp_column}, '#{limit_string}')"
|
90
|
+
when 'SQLServer'
|
91
|
+
"#{mp_column} + '#{limit_string}'"
|
92
|
+
else # ANSI SQL Syntax
|
93
|
+
"#{mp_column} || '#{limit_string}'"
|
94
|
+
end
|
95
|
+
class_inheritable_reader :mp_ancestor_limit
|
96
|
+
|
97
|
+
write_inheritable_attribute :mp_between,
|
98
|
+
"? between #{mp_column} and #{mp_ancestor_limit}"
|
99
|
+
class_inheritable_reader :mp_between
|
100
|
+
|
101
|
+
#path manipulation fu
|
102
|
+
write_inheritable_attribute :mp_regexp,
|
103
|
+
Regexp.new("[[:alnum:]]{#{conf[:places]}}\\#{conf[:delimiter]}$")
|
104
|
+
class_inheritable_reader :mp_regexp
|
105
|
+
|
106
|
+
|
107
|
+
include ActiveRecord::Acts::MaterializedPath::InstanceMethods
|
108
|
+
extend ActiveRecord::Acts::MaterializedPath::SingletonMethods
|
109
|
+
|
110
|
+
#before_create :before_create_callback
|
111
|
+
|
112
|
+
attr_protected conf[:column].to_sym
|
113
|
+
|
114
|
+
|
115
|
+
#if parent set, save as child
|
116
|
+
#if sibling set, save as sibling
|
117
|
+
#else save as root
|
118
|
+
attr_accessor :mp_parent_id_for_save
|
119
|
+
attr_accessor :mp_sibling_id_for_save
|
120
|
+
|
121
|
+
#this mucks up emacs indenting, so watch out for that
|
122
|
+
class_eval <<-EOV
|
123
|
+
def #{mp_column}=(newpath)
|
124
|
+
raise InvalidAssignment
|
125
|
+
end
|
126
|
+
EOV
|
127
|
+
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
module SingletonMethods
|
132
|
+
#
|
133
|
+
def roots
|
134
|
+
siblings('')
|
135
|
+
end
|
136
|
+
|
137
|
+
#
|
138
|
+
def num2path_string(num)#:nodoc:
|
139
|
+
str = num.to_s(mp_base)
|
140
|
+
len = str.length
|
141
|
+
|
142
|
+
raise PathMaxExceeded unless len <= mp_places
|
143
|
+
|
144
|
+
'0'*(mp_places-len)+str
|
145
|
+
end
|
146
|
+
|
147
|
+
#utility funtion to return a set of siblings
|
148
|
+
def siblings(path, select = '*')
|
149
|
+
find( :all,
|
150
|
+
:select => select,
|
151
|
+
:conditions =>
|
152
|
+
[ mp_like, path + '_' * mp_places + mp_delimiter],
|
153
|
+
:order => mp_asc )
|
154
|
+
end
|
155
|
+
|
156
|
+
def inner_delete(id)#:nodoc:
|
157
|
+
c = find(id, :select => mp_column)
|
158
|
+
rescue RecordNotFound then
|
159
|
+
return 0
|
160
|
+
else
|
161
|
+
count = 0
|
162
|
+
c.children.each do |child|
|
163
|
+
count += self.delete(child.id)
|
164
|
+
end
|
165
|
+
return count
|
166
|
+
end
|
167
|
+
|
168
|
+
#FIXME - handle arrays of ids
|
169
|
+
def delete(id)
|
170
|
+
transaction do
|
171
|
+
inner_delete(id) + super(id)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
module InstanceMethods
|
177
|
+
#
|
178
|
+
def left_most_child
|
179
|
+
self.class.find(:first,
|
180
|
+
:conditions =>
|
181
|
+
["#{mp_like}",
|
182
|
+
the_path + '_' * mp_places + mp_delimiter],
|
183
|
+
:order => mp_asc)
|
184
|
+
end
|
185
|
+
|
186
|
+
def is_leaf?
|
187
|
+
!left_most_child
|
188
|
+
end
|
189
|
+
|
190
|
+
#
|
191
|
+
def right_sibling
|
192
|
+
self.class.find(:first,
|
193
|
+
:conditions =>
|
194
|
+
["#{mp_gt} and #{mp_lt} and length(#{mp_column}) = ?",
|
195
|
+
the_path,
|
196
|
+
the_path(true)+'~',
|
197
|
+
the_path.length],
|
198
|
+
:order => mp_asc)
|
199
|
+
end
|
200
|
+
|
201
|
+
#
|
202
|
+
def destroy
|
203
|
+
raise DestroyNotLeaf unless children.length == 0
|
204
|
+
#or
|
205
|
+
# self.class.transaction do
|
206
|
+
# children.each do |child|
|
207
|
+
# child.destroy
|
208
|
+
# end
|
209
|
+
# end
|
210
|
+
super
|
211
|
+
end
|
212
|
+
|
213
|
+
#
|
214
|
+
def set_path(path)
|
215
|
+
write_attribute(mp_column.to_sym, path)
|
216
|
+
end
|
217
|
+
|
218
|
+
#
|
219
|
+
def nextvalue(path)
|
220
|
+
last = self.class.find(:first,
|
221
|
+
:select => mp_column,
|
222
|
+
:conditions =>
|
223
|
+
[mp_like, path +
|
224
|
+
'_' * mp_places +
|
225
|
+
mp_delimiter],
|
226
|
+
:order => mp_desc,
|
227
|
+
:lock => true)
|
228
|
+
|
229
|
+
if last
|
230
|
+
last_path = last.the_path
|
231
|
+
if i = last_path.index(mp_regexp)
|
232
|
+
leaf = last_path[i, mp_places]
|
233
|
+
else
|
234
|
+
leaf = last_path
|
235
|
+
end
|
236
|
+
nextval = leaf.to_i(mp_base).next
|
237
|
+
else
|
238
|
+
nextval = 0
|
239
|
+
end
|
240
|
+
|
241
|
+
return nextval
|
242
|
+
end
|
243
|
+
|
244
|
+
#
|
245
|
+
def save
|
246
|
+
if new_record? && !the_path
|
247
|
+
self.class.transaction do
|
248
|
+
basepath = ''
|
249
|
+
if mp_parent_id_for_save.to_i > 0
|
250
|
+
relation = mp_parent_id_for_save
|
251
|
+
sibling = false
|
252
|
+
elsif mp_sibling_id_for_save.to_i > 0
|
253
|
+
relation = mp_sibling_id_for_save
|
254
|
+
sibling = true
|
255
|
+
end
|
256
|
+
basepath =
|
257
|
+
self.class.find(relation).the_path(sibling) if relation
|
258
|
+
|
259
|
+
nextval = nextvalue(basepath)
|
260
|
+
set_path(basepath+self.class.num2path_string(nextval)+mp_delimiter)
|
261
|
+
super
|
262
|
+
end
|
263
|
+
else
|
264
|
+
super
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
#
|
269
|
+
|
270
|
+
#
|
271
|
+
def save_as_relation(parent_or_sibling, sibling)
|
272
|
+
raise PathUpdateDisallowed unless new_record? && !the_path
|
273
|
+
self.class.transaction do
|
274
|
+
basepath = parent_or_sibling.the_path(sibling)
|
275
|
+
nextval = nextvalue(basepath)
|
276
|
+
set_path(basepath+self.class.num2path_string(nextval)+mp_delimiter)
|
277
|
+
save
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
#
|
282
|
+
def save_as_sibling_of(sibling)
|
283
|
+
save_as_relation(sibling, true)
|
284
|
+
end
|
285
|
+
|
286
|
+
#
|
287
|
+
def save_as_child_of(parent)
|
288
|
+
save_as_relation(parent, false)
|
289
|
+
end
|
290
|
+
|
291
|
+
#
|
292
|
+
def the_path(truncate = false)
|
293
|
+
materialized_path = self.send(mp_column)
|
294
|
+
if truncate
|
295
|
+
rex = Regexp.new('[[:alnum:]]{' + mp_places.to_s + '}' +
|
296
|
+
'\\' + mp_delimiter)
|
297
|
+
offset = materialized_path.rindex(rex)
|
298
|
+
end
|
299
|
+
materialized_path = materialized_path[0, offset] if offset
|
300
|
+
return materialized_path
|
301
|
+
end
|
302
|
+
|
303
|
+
#
|
304
|
+
def siblings(include_self = false)
|
305
|
+
res = self.class.siblings(the_path(true))
|
306
|
+
res.delete_if{|mp| mp.the_path == the_path} unless include_self
|
307
|
+
return res
|
308
|
+
end
|
309
|
+
|
310
|
+
# returns an array of children (empty if this is a leaf)
|
311
|
+
def children
|
312
|
+
self.class.siblings(the_path)
|
313
|
+
end
|
314
|
+
|
315
|
+
#
|
316
|
+
def parent
|
317
|
+
self.class.find(:first,
|
318
|
+
:conditions =>
|
319
|
+
[ mp_eq,
|
320
|
+
the_path(true) ])
|
321
|
+
end
|
322
|
+
|
323
|
+
# returns an array of parents to root
|
324
|
+
def ancestors(include_self = false)
|
325
|
+
self.class.find(:all,
|
326
|
+
:conditions =>
|
327
|
+
[ mp_between,
|
328
|
+
the_path(!include_self) ],
|
329
|
+
:order => mp_asc )
|
330
|
+
end
|
331
|
+
|
332
|
+
#returns the depth of this node in the tree (0 based)
|
333
|
+
def depth
|
334
|
+
the_path.count(mp_delimiter)-1
|
335
|
+
end
|
336
|
+
|
337
|
+
#TODO
|
338
|
+
def descendants(include_self = false)
|
339
|
+
self.class.find(:all,
|
340
|
+
:conditions =>
|
341
|
+
[ mp_like,
|
342
|
+
"#{the_path}%" ],
|
343
|
+
:order => mp_asc )
|
344
|
+
end
|
345
|
+
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
ActiveRecord::Base.send(:include, ActiveRecord::Acts::MaterializedPath)
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: oakridgelanl-materialized-path
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Antony Donovan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-05-15 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activerecord
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 2.0.2
|
23
|
+
version:
|
24
|
+
description: An ActsAs mixin implementing a set of trees (nested set) for ActiveRecord
|
25
|
+
email: and@prospectmarkets.com
|
26
|
+
executables: []
|
27
|
+
|
28
|
+
extensions: []
|
29
|
+
|
30
|
+
extra_rdoc_files:
|
31
|
+
- README
|
32
|
+
files:
|
33
|
+
- README
|
34
|
+
- lib/acts_as_materialized_path.rb
|
35
|
+
has_rdoc: true
|
36
|
+
homepage:
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options:
|
39
|
+
- --main
|
40
|
+
- README
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: "0"
|
48
|
+
version:
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
requirements: []
|
56
|
+
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 1.0.1
|
59
|
+
signing_key:
|
60
|
+
specification_version: 2
|
61
|
+
summary: An ActsAs mixin implementing a set of trees (nested set) for ActiveRecord
|
62
|
+
test_files: []
|
63
|
+
|