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 +4 -4
- data/CHANGELOG.md +14 -1
- data/README.md +332 -139
- data/lib/ancestry/array_pattern_validator.rb +27 -0
- data/lib/ancestry/class_methods.rb +21 -15
- data/lib/ancestry/has_ancestry.rb +22 -28
- data/lib/ancestry/instance_methods.rb +13 -7
- data/lib/ancestry/locales/en.yml +1 -0
- data/lib/ancestry/materialized_path.rb +53 -29
- data/lib/ancestry/materialized_path2.rb +36 -27
- data/lib/ancestry/materialized_path_pg.rb +6 -6
- data/lib/ancestry/materialized_path_string.rb +46 -0
- data/lib/ancestry/materialized_path_string2.rb +46 -0
- data/lib/ancestry/version.rb +1 -1
- data/lib/ancestry.rb +38 -1
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 19e9d786304fcb6d41f135572b0475167d378bafd24f1e084bab1aa87b759dc5
|
4
|
+
data.tar.gz: 34d82c19057c6036e0e05080fa84569cf4343b8a2ebb19f718ea5093e398e4bf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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.
|
50
|
+
- Ancestry 4.x only supports rails 5.2 and higher
|
20
51
|
|
21
52
|
# Installation
|
22
53
|
|
23
|
-
Follow these
|
54
|
+
Follow these steps to apply Ancestry to any ActiveRecord model:
|
24
55
|
|
25
|
-
##
|
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
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
65
|
-
|
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
|
-
|
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
|
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
|
95
|
-
|
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
|
-
|
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
|
-
#
|
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
|
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
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
:primary_key_format
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
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
|
-
`
|
272
|
+
## `arrange_serializable`
|
242
273
|
|
243
|
-
|
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
|
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
|
-
|
264
|
-
|
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
|
-
|
269
|
-
|
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
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
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
|
-
|
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
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
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
|