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