ancestry 4.2.0 → 4.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 82bcd1895093ab9b569806ef05fc307fc2ca9ab53ffeb09199bd66fe3672ecfb
4
- data.tar.gz: fe7d0d356641658be2953309c27dd28a42e81ed569690cc5e26b2721b1aa6a37
3
+ metadata.gz: 19e9d786304fcb6d41f135572b0475167d378bafd24f1e084bab1aa87b759dc5
4
+ data.tar.gz: 34d82c19057c6036e0e05080fa84569cf4343b8a2ebb19f718ea5093e398e4bf
5
5
  SHA512:
6
- metadata.gz: f33384a1114d865662be0133c532249be0e7ea438a66e469947b3aaba3172378ae50309e02c4f219c1f161281a9e060bff6087a496850c26351fd83348627e79
7
- data.tar.gz: 2a6cb3fb28f6c9a8228b522c590c2125c4e31056a97ac6a6cda96ea4152725fe89e375b59c894091ebcd3d5be36ad18b348c6691211bc6bea2b0a49502c9f423
6
+ metadata.gz: e954ca39aabe660070650e2917f5efdd115a10da25c8d7874b565278d2ba6b69b2c17b3bc6138452fec2823675f6ff7c767b10d33c999ac568133736c08fbccf
7
+ data.tar.gz: 4b3561088670192638cfd7016936d80cb547cf6b1f8f642af62800876fe49de22373ff6f2802442d5f5330668bca487b0b232491eb28d24090fcc6a531f6e45a
data/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
3
3
  Doing our best at supporting [SemVer](http://semver.org/) with
4
4
  a nice looking [Changelog](http://keepachangelog.com).
5
5
 
6
+ ## Version [4.3.0] <sub><sup>2023-03-09</sub></sup>
7
+
8
+ * Fix: materialized_path2 strategy [#597](https://github.com/stefankroes/ancestry/pull/597) (thx @kshnurov)
9
+ * Fix: descendants ancestry is now updated in after_update callbacks [#589](https://github.com/stefankroes/ancestry/pull/589) (thx @kshnurov)
10
+ * Document updated grammar [#594](https://github.com/stefankroes/ancestry/pull/594) (thx @omarr-gamal)
11
+ * Documented `update_strategy` [#588](https://github.com/stefankroes/ancestry/pull/588) (thx @victorfgs)
12
+ * Fix: fixed has_parent? when non-default primary id [#585](https://github.com/stefankroes/ancestry/pull/585) (thx @Zhong-z)
13
+ * Documented column collation and testing [#601](https://github.com/stefankroes/ancestry/pull/601) [#607](https://github.com/stefankroes/ancestry/pull/607) (thx @kshnurov)
14
+ * Added initializer with default_ancestry_format [#612](https://github.com/stefankroes/ancestry/pull/612) [#613](https://github.com/stefankroes/ancestry/pull/613)
15
+ * ruby 3.2 support [#596](https://github.com/stefankroes/ancestry/pull/596) (thx @petergoldstein)
16
+ * arrange is 3x faster and uses 20-30x less memory [#415](https://github.com/stefankroes/ancestry/pull/415)
17
+
6
18
  ## Version [4.2.0] <sub><sup>2022-06-09</sub></sup>
7
19
 
8
20
  * added strategy: materialized_path2 [#571](https://github.com/stefankroes/ancestry/pull/571)
@@ -270,7 +282,8 @@ Missed 2 commits (which are feature adds)
270
282
  * Validations
271
283
 
272
284
 
273
- [HEAD]: https://github.com/stefankroes/ancestry/compare/v4.2.0...HEAD
285
+ [HEAD]: https://github.com/stefankroes/ancestry/compare/v4.3.0...HEAD
286
+ [4.3.0]: https://github.com/stefankroes/ancestry/compare/v4.2.0...v4.3.0
274
287
  [4.2.0]: https://github.com/stefankroes/ancestry/compare/v4.1.0...v4.2.0
275
288
  [4.1.0]: https://github.com/stefankroes/ancestry/compare/v4.0.0...v4.1.0
276
289
  [4.0.0]: https://github.com/stefankroes/ancestry/compare/v3.2.1...v4.0.0
data/README.md CHANGED
@@ -2,29 +2,58 @@
2
2
 
3
3
  # Ancestry
4
4
 
5
- Ancestry is a gem that allows the records of a Ruby on Rails
6
- ActiveRecord model to be organised as a tree structure (or hierarchy). It employs
7
- the materialised path pattern and exposes all the standard tree structure
8
- relations (ancestors, parent, root, children, siblings, descendants), allowing all
9
- of them to be fetched in a single SQL query. Additional features include STI
10
- support, scopes, depth caching, depth constraints, easy migration from older
11
- gems, integrity checking, integrity restoration, arrangement of
12
- (sub)trees into hashes, and various strategies for dealing with orphaned
13
- records.
14
-
15
- NOTE:
5
+ ## Overview
6
+
7
+ Ancestry is a gem that allows rails ActiveRecord models to be organized as
8
+ a tree structure (or hierarchy). It employs the materialized path pattern
9
+ which allows operations to be performed efficiently.
10
+
11
+ # Features
12
+
13
+ There are a few common ways of storing hierarchical data in a database:
14
+ materialized path, closure tree table, adjacency lists, nested sets, and adjacency list with recursive queries.
15
+
16
+ ## Features from Materialized Path
17
+
18
+ - Store hierarchy in an easy to understand format. (e.g.: `/1/2/3/`)
19
+ - Store hierarchy in the original table with no additional tables.
20
+ - Single SQL queries for relations (`ancestors`, `parent`, `root`, `children`, `siblings`, `descendants`)
21
+ - Single query for creating records.
22
+ - Moving/deleting nodes only affect child nodes (rather than updating all nodes in the tree)
23
+
24
+ ## Features from Ancestry gem Implementation
25
+
26
+ - relations are implemented as `scopes`
27
+ - `STI` support
28
+ - Arrangement of subtrees into hashes
29
+ - Multiple strategies for querying materialized_path
30
+ - Multiple strategies for dealing with orphaned records
31
+ - depth caching
32
+ - depth constraints
33
+ - counter caches
34
+ - Multiple strategies for moving nodes
35
+ - Easy migration from `parent_id` based gems
36
+ - Integrity checking
37
+ - Integrity restoration
38
+ - Most queries use indexes on `id` or `ancestry` column. (e.g.: `LIKE '#{ancestry}/%'`)
39
+
40
+ Since a Btree index has a limitaton of 2704 characters for the `ancestry` column,
41
+ the maximum depth of an ancestry tree is 900 items at most. If ids are 4 digits long,
42
+ then the max depth is 540 items.
43
+
44
+ When using `STI` all classes are returned from the scopes unless you specify otherwise using `where(:type => "ChildClass")`.
45
+
46
+ ## Supported Rails versions
16
47
 
17
48
  - Ancestry 2.x supports Rails 4.1 and earlier
18
49
  - Ancestry 3.x supports Rails 5.0 and 4.2
19
- - Ancestry 4.0 only supports rails 5.0 and higher
50
+ - Ancestry 4.x only supports rails 5.2 and higher
20
51
 
21
52
  # Installation
22
53
 
23
- Follow these simple steps to apply Ancestry to any ActiveRecord model:
54
+ Follow these steps to apply Ancestry to any ActiveRecord model:
24
55
 
25
- ## Install
26
-
27
- * Add to Gemfile:
56
+ ## Add to Gemfile
28
57
 
29
58
  ```ruby
30
59
  # Gemfile
@@ -32,73 +61,68 @@ Follow these simple steps to apply Ancestry to any ActiveRecord model:
32
61
  gem 'ancestry'
33
62
  ```
34
63
 
35
- * Install required gems:
36
-
37
64
  ```bash
38
65
  $ bundle install
39
66
  ```
40
67
 
41
-
42
68
  ## Add ancestry column to your table
43
- * Create migration:
44
69
 
45
70
  ```bash
46
- $ rails g migration add_ancestry_to_[table] ancestry:string:index
47
- # or use different column name of your choosing. e.g. name:
48
- # rails g migration add_name_to_[people] name:string:index
71
+ $ rails g migration add_[ancestry]_to_[table] ancestry:string:index
72
+ ```
73
+
74
+ ```ruby
75
+ class AddAncestryToTable < ActiveRecord::Migration[6.1]
76
+ def change
77
+ change_table(:table) do |t|
78
+ # postgrel
79
+ t.string "ancestry", collation: 'C', null: false
80
+ t.index "ancestry"
81
+ # mysql
82
+ t.string "ancestry", collation: 'utf8mb4_bin', null: false
83
+ t.index "ancestry"
84
+ end
85
+ end
86
+ end
49
87
  ```
50
88
 
51
- * Migrate your database:
89
+ There are additional options for the columns in [Ancestry Database Columnl](#ancestry-database-column) and
90
+ an explanation for `opclass` and `collation`.
52
91
 
53
92
  ```bash
54
93
  $ rake db:migrate
55
94
  ```
56
95
 
57
- Depending upon your comfort with databases, you may want to create the column
58
- with `C` or `POSIX` encoding. This is a more primitive encoding and just compares
59
- bytes. Since this column will just contains numbers and slashes, it works much
60
- better. It also works better for the uuid case as well.
61
-
62
- Alternatively, if you create a [`text_pattern_ops`](https://www.postgresql.org/docs/current/indexes-opclass.html) index for your postgresql column, subtree selection will use an efficient index for you regardless of whether you created the column with `POSIX` encoding.
96
+ ## Configure ancestry defaults
63
97
 
64
- If you opt out of this, and are trying to run tests on postgres, you may need to
65
- set the environment variable `COLLATE_SYMBOLS=false`. Sorry to say that a discussion
66
- on this topic is out of scope. The important take away is postgres sort order is
67
- not consistent across operating systems but other databases do not have this same
68
- issue.
98
+ ```ruby
99
+ # config/initializers/ancestry.rb
69
100
 
70
- NOTE: A Btree index (as is recommended) has a limitaton of 2704 characters for the ancestry column. This means you can't have an tree with a depth that is too great (~> 900 items at most).
101
+ # use the newer format
102
+ Ancestry.default_ancestry_format = :materialized_path2
103
+ # Ancestry.default_update_strategy = :sql
104
+ ```
71
105
 
72
106
  ## Add ancestry to your model
73
- * Add to app/models/[model.rb]:
74
107
 
75
108
  ```ruby
76
109
  # app/models/[model.rb]
77
110
 
78
111
  class [Model] < ActiveRecord::Base
79
- has_ancestry # or alternatively as below:
80
- # has_ancestry ancestry_column: :name ## if you've used a different column name
112
+ has_ancestry
81
113
  end
82
114
  ```
83
115
 
84
116
  Your model is now a tree!
85
117
 
86
- # Using acts_as_tree instead of has_ancestry
87
-
88
- In version 1.2.0, the **acts_as_tree** method was **renamed to has_ancestry**
89
- in order to allow usage of both the acts_as_tree gem and the ancestry gem in a
90
- single application. The `acts_as_tree` method will continue to be supported in the future.
91
-
92
118
  # Organising records into a tree
93
119
 
94
- You can use the parent attribute to organise your records into a tree. If you
95
- have the id of the record you want to use as a parent and don't want to fetch
96
- it, you can also use parent_id. Like any virtual model attributes, parent and
97
- parent_id can be set using parent= and parent_id= on a record or by including
98
- them in the hash passed to new, create, create!, update_attributes and
99
- update_attributes!. For example:
120
+ You can use `parent_id` and `parent` to add a node into a tree. They can be
121
+ set as attributes or passed into methods like `new`, `create`, and `update`.
100
122
 
101
- `TreeNode.create! :name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky')`.
123
+ ```ruby
124
+ TreeNode.create! :name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky')
125
+ ```
102
126
 
103
127
  Children can be created through the children relation on a node: `node.children.create :name => 'Stinky'`.
104
128
 
@@ -127,31 +151,47 @@ The yellow nodes are those returned by the method.
127
151
  |`has_siblings?` | | |
128
152
  |`sibling_of?(node)` | | |
129
153
 
154
+ When using `STI` all classes are returned from the scopes unless you specify otherwise using `where(:type => "ChildClass")`.
155
+
130
156
  <sup id="fn1">1. [other root records are considered siblings]<a href="#ref1" title="Jump back to footnote 1.">↩</a></sup>
131
157
 
132
- # `has_ancestry` options
158
+ # has_ancestry options
133
159
 
134
- The has_ancestry method supports the following options:
160
+ The `has_ancestry` method supports the following options:
135
161
 
136
- :ancestry_column Pass in a symbol to store ancestry in a different column
162
+ :ancestry_column Column name to store ancestry
163
+ 'ancestry' (default)
164
+ :ancestry_format Format for ancestry column (see Ancestry Formats section):
165
+ :materialized_path (default) 1/2/3, root nodes ancestry=nil
166
+ :materialized_path2 (preferred) /1/2/3/, root nodes ancestry=/
137
167
  :orphan_strategy Instruct Ancestry what to do with children of a node that is destroyed:
138
168
  :destroy All children are destroyed as well (default)
139
169
  :rootify The children of the destroyed node become root nodes
140
170
  :restrict An AncestryException is raised if any children exist
141
171
  :adopt The orphan subtree is added to the parent of the deleted node
142
172
  If the deleted node is Root, then rootify the orphan subtree
143
- :cache_depth Cache the depth of each node in the 'ancestry_depth' column (default: false)
144
- If you turn depth_caching on for an existing model:
145
- - Migrate: add_column [table], :ancestry_depth, :integer, :default => 0
146
- - Build cache: TreeNode.rebuild_depth_cache!
147
- :depth_cache_column Pass in a symbol to store depth cache in a different column
148
- :primary_key_format Supply a regular expression that matches the format of your primary key
149
- By default, primary keys only match integers ([0-9]+)
150
- :touch Instruct Ancestry to touch the ancestors of a node when it changes, to
151
- invalidate nested key-based caches. (default: false)
152
- :counter_cache Boolean whether to create counter cache column accessor.
153
- Default column name is `children_count`.
154
- Pass symbol to use different column name (default: false)
173
+ :cache_depth Cache the depth of each node (See Depth Cache section)
174
+ false (default)
175
+
176
+ :depth_cache_column column name to store depth cache
177
+ 'ancestry_depth' (default)
178
+ :primary_key_format regular expression that matches the format of the primary key
179
+ '[0-9]+' (default) integer ids
180
+ '[-A-Fa-f0-9]{36}' UUIDs
181
+ :touch Instruct Ancestry to touch the ancestors of a node when it changes
182
+ false (default) don't invalide nested key-based caches
183
+ :counter_cache Whether to create counter cache column accessor.
184
+ false (default) don't store a counter cache
185
+ true store counter cache in `children_count`.
186
+ String name of column to store counter cache.
187
+ :update_strategy Choose the strategy to update descendants nodes
188
+ :ruby (default) All descendants are updated using the ruby algorithm.
189
+ This triggers update callbacks for each descendant node
190
+ :sql All descendants are updated using a single SQL statement.
191
+ This strategy does not trigger update callbacks for the descendants.
192
+ This strategy is available only for PostgreSql implementations
193
+
194
+ Legacy configuration using `acts_as_tree` is still available. Ancestry defers to `acts_as_tree` if that gem is installed.
155
195
 
156
196
  # (Named) Scopes
157
197
 
@@ -183,7 +223,7 @@ It is possible thanks to some convenient rails magic to create nodes through the
183
223
 
184
224
  # Selecting nodes by depth
185
225
 
186
- With depth caching enabled (see has_ancestry options), an additional five named
226
+ With depth caching enabled (see [has_ancestry options](#has_ancestry-options)), an additional five named
187
227
  scopes can be used to select nodes by depth:
188
228
 
189
229
  before_depth(depth) Return nodes that are less deep than depth (node.depth < depth)
@@ -203,16 +243,13 @@ can be fetched directly from the ancestry column without needing a query. Use
203
243
  node.descendants(:from_depth => 2, :to_depth => 4)
204
244
  node.subtree.from_depth(10).to_depth(12)
205
245
 
206
- # STI support
207
-
208
- To use with STI: create a STI inheritance hierarchy and build a tree from the different
209
- classes/models. All Ancestry relations that were described above will return nodes of any model type. If
210
- you do only want nodes of a specific subclass, a type condition is required.
211
-
212
246
  # Arrangement
213
247
 
248
+ ## `arrange`
249
+
214
250
  A subtree can be arranged into nested hashes for easy navigation after database retrieval.
215
- `TreeNode.arrange` could, for instance, return:
251
+
252
+ The resulting format is a hash of hashes
216
253
 
217
254
  ```ruby
218
255
  {
@@ -225,24 +262,22 @@ A subtree can be arranged into nested hashes for easy navigation after database
225
262
  }
226
263
  ```
227
264
 
228
- The `arrange` method can work on a scoped class (`TreeNode.find_by(:name => 'Crunchy').subtree.arrange`),
229
- and can take ActiveRecord find options. If you want ordered hashes, pass the order to the method instead of
230
- the scope as follows:
231
-
232
- `TreeNode.find_by(:name => 'Crunchy').subtree.arrange(:order => :name)`.
265
+ There are many ways to call `arrange`:
233
266
 
234
- The `arrange_serializable` method returns the arranged nodes as a nested array of hashes. Order
235
- can be passed in the same fashion as to the `arrange` method:
236
- `TreeNode.arrange_serializable(:order => :name)` The result can easily be serialized to json with `to_json`
237
- or other formats. You can also supply your own serialization logic with blocks.
238
-
239
- Using `ActiveModel` serializers:
267
+ ```ruby
268
+ TreeNode.find_by(:name => 'Crunchy').subtree.arrange
269
+ TreeNode.find_by(:name => 'Crunchy').subtree.arrange(:order => :name)
270
+ ```
240
271
 
241
- `TreeNode.arrange_serializable { |parent, children| MySerializer.new(parent, children: children) }`.
272
+ ## `arrange_serializable`
242
273
 
243
- Or plain hashes:
274
+ If a hash of arrays is preferred, `arrange_serializable` can be used. The results
275
+ work well with `to_json`.
244
276
 
245
277
  ```ruby
278
+ TreeNode.arrange_serializable(:order => :name)
279
+ # use an active model serializer
280
+ TreeNode.arrange_serializable { |parent, children| MySerializer.new(parent, children: children) }
246
281
  TreeNode.arrange_serializable do |parent, children|
247
282
  {
248
283
  my_id: parent.id,
@@ -254,54 +289,232 @@ end
254
289
  # Sorting
255
290
 
256
291
  The `sort_by_ancestry` class method: `TreeNode.sort_by_ancestry(array_of_nodes)` can be used
257
- to sort an array of nodes as if traversing in preorder. (Note that since materialised path
292
+ to sort an array of nodes as if traversing in preorder. (Note that since materialized path
258
293
  trees do not support ordering within a rank, the order of siblings is
259
294
  dependant upon their original array order.)
260
295
 
296
+
297
+ # Ancestry Database Column
298
+
299
+ ## Collation Indexes
300
+
301
+ Sorry, using collation or index operator classes makes this a little complicated. The
302
+ root of the issue is that in order to use indexes, the ancestry column needs to
303
+ compare strings using ascii rules.
304
+
305
+ It is well known that `LIKE '/1/2/%'` will use an index because the wildchard (i.e.: `%`)
306
+ is on the right hand side of the `LIKE`. While that is true for ascii strings, it is not
307
+ necessarily true for unicode. Since ancestry only uses ascii characters, telling the database
308
+ this constraint will optimize the `LIKE` statemens.
309
+
310
+ ## Collation Sorting
311
+
312
+ As of 2018, standard unicode collation ignores punctuation for sorting. This ignores
313
+ the ancestry delimiter (i.e.: `/`) and returns data in the wrong order. The exception
314
+ being Postgres on a mac, which ignores proper unicode collation and instead uses
315
+ ISO-8859-1 ordering (read: ascii sorting).
316
+
317
+ Using the proper column storage and indexes will ensure that data is returned from the
318
+ database in the correct order. It will also ensure that developers on Mac or Windows will
319
+ get the same results as linux production servers, if that is your setup.
320
+
321
+ ## Migrating Collation
322
+
323
+ If you are reading this and want to alter your table to add collation to an existing column,
324
+ remember to drop existing indexes on the `ancestry` column and recreate them.
325
+
326
+ ## ancestry_format materialized_path and nulls
327
+
328
+ If you are using the legacy `ancestry_format` of `:materialized_path`, then you need to the
329
+ collum to allow `nulls`. Change the column create accordingly: `null: true`.
330
+
331
+ Chances are, you can ignore this section as you most likely want to use `:materialized_path2`.
332
+
333
+ ## Postgres Storage Options
334
+
335
+ ### ascii field collation
336
+
337
+ The currently suggested way to create a postgres field is using `'C'` collation:
338
+
339
+ ```ruby
340
+ t.string "ancestry", collation: 'C', null: false
341
+ t.index "ancestry"
342
+ ```
343
+
344
+ ### ascii index
345
+
346
+ If you need to use a standard collation (e.g.: `en_US`), then use an ascii index:
347
+
348
+ ```ruby
349
+ t.string "ancestry", null: false
350
+ t.index "ancestry", opclass: :varchar_pattern_ops
351
+ ```
352
+
353
+ This option is mostly there for users who have an existing ancestry column and are more
354
+ comfortable tweaking indexes rather than altering the ancestry column.
355
+
356
+ ### binary column
357
+
358
+ When the column is binary, the database doesn't convert strings using locales.
359
+ Rails will convert the strings and send byte arrays to the database.
360
+ At this time, this option is not suggested. The sql is not as readable, and currently
361
+ this does not support the `:sql` update_strategy.
362
+
363
+ ```ruby
364
+ t.binary "ancestry", limit: 3000, null: false
365
+ t.index "ancestry"
366
+ ```
367
+ You may be able to alter the database to gain some readability:
368
+
369
+ ```SQL
370
+ ALTER DATABASE dbname SET bytea_output to 'escape';
371
+ ```
372
+
373
+ ## Mysql Storage options
374
+
375
+ ### ascii field collation
376
+
377
+ The currently suggested way to create a postgres field is using `'C'` collation:
378
+
379
+ ```ruby
380
+ t.string "ancestry", collation: 'utf8mb4_bin', null: false
381
+ t.index "ancestry"
382
+ ```
383
+
384
+ ### binary collation
385
+
386
+ Collation of `binary` acts much the same way as the `binary` column:
387
+
388
+ ```ruby
389
+ t.string "ancestry", collate: 'binary', limit: 3000, null: false
390
+ t.index "ancestry"
391
+ ```
392
+
393
+ ### binary column
394
+
395
+ ```ruby
396
+ t.binary "ancestry", limit: 3000, null: false
397
+ t.index "ancestry"
398
+ ```
399
+
400
+ ### ascii character set
401
+
402
+ Mysql supports per column character sets. Using a character set of `ascii` will
403
+ set this up.
404
+
405
+ ```SQL
406
+ ALTER TABLE table
407
+ ADD COLUMN ancestry VARCHAR(2700) CHARACTER SET ascii;
408
+ ```
409
+
410
+ # Ancestry Formats
411
+
412
+ You can choose from 2 ancestry formats:
413
+
414
+ - `:materialized_path` - legacy format (currently the default for backwards compatibility reasons)
415
+ - `:materialized_path2` - newer format. Use this if it is a new column
416
+
417
+ ```
418
+ :materialized_path 1/2/3, root nodes ancestry=nil
419
+ descendants SQL: ancestry LIKE '1/2/3/%' OR ancestry = '1/2/3'
420
+ :materialized_path2 /1/2/3/, root nodes ancestry=/
421
+ descendants SQL: ancestry LIKE '/1/2/3/%'
422
+ ```
423
+
424
+ If you are unsure, choose `:materialized_path2`. It allows a not NULL column,
425
+ faster descenant queries, has one less `OR` statement in the queries, and
426
+ the path can be formed easily in a database query for added benefits.
427
+
428
+ There is more discussion in [Internals](#internals) or [Migrating ancestry format](#migrate-ancestry-format)
429
+ For migrating from `materialized_path` to `materialized_path2` see [Ancestry Column](#ancestry-column)
430
+
431
+ ## Migrating Ancestry Format
432
+
433
+ To migrate from `materialized_path` to `materialized_path2`:
434
+
435
+ ```ruby
436
+ klass = YourModel
437
+ # set all child nodes
438
+ klass.where.not(klass.arel_table[klass.ancestry_column].eq(nil)).update_all("#{klass.ancestry_column} = CONCAT('#{klass.ancestry_delimiter}', #{klass.ancestry_column}, '#{klass.ancestry_delimiter}')")
439
+ # set all root nodes
440
+ klass.where(klass.arel_table[klass.ancestry_column].eq(nil)).update_all("#{klass.ancestry_column} = '#{klass.ancestry_root}'")
441
+
442
+ change_column_null klass.table_name, klass.ancestry_column, false
443
+ ```
444
+
261
445
  # Migrating from plugin that uses parent_id column
262
446
 
263
- Most current tree plugins use a parent_id column (has_ancestry,
264
- awesome_nested_set, better_nested_set, acts_as_nested_set). With Ancestry it is
265
- easy to migrate from any of these plugins. To do so, use the
266
- `build_ancestry_from_parent_ids!` method on your ancestry model.
447
+ It should be relatively simple to migrating from a plugin that uses a `parent_id`
448
+ column, (e.g.: `awesome_nested_set`, `better_nested_set`, `acts_as_nested_set`).
267
449
 
268
- <details>
269
- <summary>Details</summary>
450
+ When running the installation steps, also remove the old gem from your `Gemfile`,
451
+ and remove the old gem's macros from the model.
270
452
 
271
- 1. Add ancestry column to your table
272
- * Create migration: **rails g migration [add_ancestry_to_](table)
273
- ancestry:string**
274
- * Add index to migration: **add_index [table], :ancestry** (UP) /
275
- **remove_index [table], :ancestry** (DOWN)
276
- * Migrate your database: **rake db:migrate**
453
+ Then populate the `ancestry` column from rails console:
454
+
455
+ ```ruby
456
+ Model.build_ancestry_from_parent_ids!
457
+ # Model.rebuild_depth_cache!
458
+ Model.check_ancestry_integrity!
459
+ ```
277
460
 
461
+ It is time to run your code. Most tree methods should work fine with ancestry
462
+ and hopefully your tests only require a few minor tweaks to get up and runnnig.
278
463
 
279
- 2. Remove old tree gem and add in Ancestry to Gemfile
280
- * See 'Installation' for more info on installing and configuring gems
464
+ Once you are happy with how your app is running, remove the old `parent_id` column:
281
465
 
466
+ ```bash
467
+ $ rails g migration remove_parent_id_from_[table]
468
+ ```
282
469
 
283
- 3. Change your model
284
- * Remove any macros required by old plugin/gem from
285
- `[app/models/](model).rb`
286
- * Add to `[app/models/](model).rb`: `has_ancestry`
470
+ ```ruby
471
+ class RemoveParentIdFromToTable < ActiveRecord::Migration[6.1]
472
+ def change
473
+ remove_column "table", "parent_id", type: :integer
474
+ end
475
+ end
476
+ ```
287
477
 
478
+ ```bash
479
+ $ rake db:migrate
480
+ ```
288
481
 
289
- 4. Generate ancestry columns
290
- * In rails console: **[model].build_ancestry_from_parent_ids!**
291
- * Make sure it worked ok: **[model].check_ancestry_integrity!**
482
+ # Depth cache
292
483
 
484
+ ## Depth Cache Migration
293
485
 
294
- 5. Change your code
295
- * Most tree calls will probably work fine with ancestry
296
- * Others must be changed or proxied
297
- * Check if all your data is intact and all tests pass
486
+ To add depth_caching to an existing model:
298
487
 
488
+ ## Add column
299
489
 
300
- 6. Drop parent_id column:
301
- * Create migration: `rails g migration [remove_parent_id_from_](table)`
302
- * Add to migration: `remove_column [table], :parent_id`
303
- * Migrate your database: `rake db:migrate`
304
- </details>
490
+ ```ruby
491
+ class AddDepthCachToTable < ActiveRecord::Migration[6.1]
492
+ def change
493
+ change_table(:table) do |t|
494
+ t.integer "ancestry_depth", default: 0
495
+ end
496
+ end
497
+ end
498
+ ```
499
+
500
+ ## Add ancestry to your model
501
+
502
+ ```ruby
503
+ # app/models/[model.rb]
504
+
505
+ class [Model] < ActiveRecord::Base
506
+ has_ancestry depth_cache: true
507
+ end
508
+ ```
509
+
510
+ ## Update existing values
511
+
512
+ Add a custom script or run from rails console.
513
+ Some use migrations, but that can make the migration suite fragile. The command of interest is:
514
+
515
+ ```ruby
516
+ Model.rebuild_depth_cache!
517
+ ```
305
518
 
306
519
  # Running Tests
307
520
 
@@ -317,26 +530,6 @@ appraisal rake test
317
530
  appraisal sqlite3-ar-50 rake test
318
531
  ```
319
532
 
320
- # Internals
321
-
322
- Ancestry stores a path from the root to the parent for every node.
323
- This is a variation on the materialised path database pattern.
324
- It allows Ancestry to fetch any relation (siblings,
325
- descendants, etc.) in a single SQL query without the complicated algorithms
326
- and incomprehensibility associated with left and right values. Additionally,
327
- any inserts, deletes and updates only affect nodes within the affected node's
328
- own subtree.
329
-
330
- In the example above, the `ancestry` column is created as a `string`. This puts a
331
- limitation on the depth of the tree of about 40 or 50 levels. To increase the
332
- maximum depth of the tree, increase the size of the `string` or use `text` to
333
- remove the limitation entirely. Changing it to a text will however decrease
334
- performance because an index cannot be put on the column in that case.
335
-
336
- The materialised path pattern requires Ancestry to use a 'like' condition in
337
- order to fetch descendants. The wild character (`%`) is on the right of the
338
- query, so indexes should be used.
339
-
340
533
  # Contributing and license
341
534
 
342
535
  Question? Bug report? Faulty/incomplete documentation? Feature request? Please
@@ -0,0 +1,27 @@
1
+ module Ancestry
2
+ class ArrayPatternValidator < ActiveModel::EachValidator
3
+ def initialize(options)
4
+ raise ArgumentError, "Pattern unspecified, Specify using :pattern" unless options[:pattern]
5
+
6
+ options[:pattern] = /\A#{options[:pattern].to_s}\Z/ unless options[:pattern].to_s.include?('\A')
7
+ options[:id] = true unless options.key?(:id)
8
+ options[:integer] = true unless options.key?(:integer)
9
+
10
+ super
11
+ end
12
+
13
+ def validate_each(record, attribute, value)
14
+ if options[:id] && value.include?(record.id)
15
+ record.errors.add(attribute, I18n.t("ancestry.exclude_self", {:class_name => self.class.name.humanize}))
16
+ end
17
+
18
+ if value.any? { |v| v.to_s !~ options[:pattern] }
19
+ record.errors.add(attribute, "illegal characters")
20
+ end
21
+
22
+ if options[:integer] && value.any? { |v| v < 1 }
23
+ record.errors.add(attribute, "non positive ancestor id")
24
+ end
25
+ end
26
+ end
27
+ end