oakridgelanl-materialized-path 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README +39 -0
  2. data/lib/acts_as_materialized_path.rb +351 -0
  3. 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
+