ancestry 4.2.0 → 4.3.0

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.
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