ancestry 5.0.0 → 5.1.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: 84b6f22caf8e4b43a3807fe036cdb098c8e141c0cd75efefb6c3fb5994155557
4
- data.tar.gz: d12e54734dca578ca1860dc6ad90f385e55741049dfee37f32f52daaf2fea419
3
+ metadata.gz: fa438f6ed3ae400ec1783712fb569500f53ce4d419d3c3baa186bdad75307fd8
4
+ data.tar.gz: 61d26886919b74f482e96f9d59cc0fa4131068d61b3316747523c0b315ab57c4
5
5
  SHA512:
6
- metadata.gz: 4d001a3bfe2418e202534ff6b143a74f22382da63a7cdc61cc68da8330fa7b9b8adadc267bcc96dc2aad30d698f46b74ebcf6c31de839a53a62da14fa5fd8e4b
7
- data.tar.gz: 4db5a4ebc441a9f2c267eb1854c51e4015ae60bc6f4fca168b3e97264e184e1b93b7cf002ce508b259ef17649c078644a34223586147eab3504e8d0d736b3c87
6
+ metadata.gz: a6e238d9fb765a56d60c4bfe9f9ba06c926ffe0bc76155858024914a94a71034827877c3ef17258121e43c85b6a49aea61df8a44021f96b65341ef48fab62426
7
+ data.tar.gz: 7b1ca54b59347311d55375b5703c56134e8d11243a64ade0d5d94071a65311b683f593c8a8daa1aa5386bf2969e3588f3f7876ee2b5520440aff388fdac906bb
data/CHANGELOG.md CHANGED
@@ -3,7 +3,28 @@
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 [5.0.0] <sub><sup>2026-02-08</sub></sup>
6
+ ## Version [5.1.0] <sub><sup>2026-03-10</sup></sub>
7
+
8
+ * `update_strategy: :sql` now works on all databases (SQLite, MySQL, PostgreSQL) [#715](https://github.com/stefankroes/ancestry/pull/715)
9
+ * Introduce depth constraint validation for subtree moves [#713](https://github.com/stefankroes/ancestry/pull/713) (thx @wilburhimself)
10
+ * Fix: CHANGELOG HTML entity fix [#712](https://github.com/stefankroes/ancestry/pull/712) (thx @biow0lf)
11
+ * Testing Ruby 4.0 support [#709](https://github.com/stefankroes/ancestry/pull/709) (thx @fkmy)
12
+ * Tested against Ruby 2.7–4.0 and Rails 6.0–8.1 [#716](https://github.com/stefankroes/ancestry/pull/716)
13
+
14
+ ### Deprecations
15
+
16
+ These class-level accessors still work but are being phased out.
17
+ We are moving configuration into baked method bodies and away from class variables.
18
+ If you depend on reading these at runtime, now is a good time to find alternatives:
19
+
20
+ * `ancestry_column`, `ancestry_delimiter`, `depth_cache_column`, `counter_cache_column`, `touch_ancestors`
21
+
22
+ ### Breaking Changes
23
+
24
+ * `sort_by_ancestry` and `check_ancestry_integrity!` now delegate to `_sort_by_ancestry` and
25
+ `_check_ancestry_integrity!` with an explicit column parameter. The public API is unchanged.
26
+
27
+ ## Version [5.0.0] <sub><sup>2026-02-08</sup></sub>
7
28
 
8
29
  * Fix: `siblings` now excludes self [#710](https://github.com/stefankroes/ancestry/pull/710) (thx @chikamichi)
9
30
  * Introduce `orphan_strategy: :none` [#658](https://github.com/stefankroes/ancestry/pull/658)
@@ -23,20 +44,20 @@ a nice looking [Changelog](http://keepachangelog.com).
23
44
  * Ruby 3.4 support
24
45
  * Rails 8.0 support
25
46
 
26
- #### Notable features
47
+ ### Notable features
27
48
 
28
49
  Depth scopes now work without `cache_depth`. But please only use this in background
29
50
  jobs. If you need to do this in the ui, please use `cache_depth`.
30
51
 
31
52
  `build_cache_depth_sql!` is quicker than `build_cache_depth!` (1 query instead of N+1 queries).
32
53
 
33
- #### Deprecations
54
+ ### Deprecations
34
55
 
35
- - Option `:depth_cache_column` is going away.
56
+ * Option `:depth_cache_column` is going away.
36
57
  Please use a single key instead: `cache_depth: :depth_cach_column_name`.
37
58
  `cache_depth: true` still defaults to `ancestry_depth`.
38
59
 
39
- #### Breaking Changes
60
+ ### Breaking Changes
40
61
 
41
62
  * `siblings` no longer returns self. This is a bug fix, but does change behavior.
42
63
  * Dropped support for Rails < 6.1
@@ -45,34 +66,34 @@ jobs. If you need to do this in the ui, please use `cache_depth`.
45
66
  * Options are no longer set via class methods. Using `has_ancestry` is now the only way to enable these features.
46
67
  These are all not part of the public API.
47
68
  * These are now class level read only accessors
48
- - `ancestry_base_class` (introduced 1.1, removed by #633)
49
- - `ancestry_column` (introduced 1.2, removed by #633)
50
- - `ancestry_delimiter` (introduced 4.3.0, removed by #633)
51
- - `depth_cache_column` (introduced 4.3.0, removed by #654)
69
+ * `ancestry_base_class` (introduced 1.1, removed by #633)
70
+ * `ancestry_column` (introduced 1.2, removed by #633)
71
+ * `ancestry_delimiter` (introduced 4.3.0, removed by #633)
72
+ * `depth_cache_column` (introduced 4.3.0, removed by #654)
52
73
  * These no longer have any accessors
53
- - `ancestry_format` (introduced 4.3.0, removed by #654)
54
- - `orphan_strategy` (introduced 1.1, removed by #617)
55
- - `ancestry_primary_key_format` (introduced 4.3.0, removed by #649)
56
- - `touch_ancestors` (introduced 2.1, removed by TODO)
74
+ * `ancestry_format` (introduced 4.3.0, removed by #654)
75
+ * `orphan_strategy` (introduced 1.1, removed by #617)
76
+ * `ancestry_primary_key_format` (introduced 4.3.0, removed by #649)
77
+ * `touch_ancestors` (introduced 2.1, removed by TODO)
57
78
  * These are seen as internal and may go away:
58
- - `apply_orphan_strategy` Please use `orphan_strategy: :none` and a custom `before_destory` instead.
79
+ * `apply_orphan_strategy` Please use `orphan_strategy: :none` and a custom `before_destory` instead.
59
80
 
60
- ## Version [4.3.3] <sub><sup>2023-04-01</sub></sup>
81
+ ## Version [4.3.3] <sub><sup>2023-04-01</sup></sub>
61
82
 
62
83
  * Fix: sort_by_ancesty with custom ancestry_column [#656](https://github.com/stefankroes/ancestry/pull/656) (thx @mitsuru)
63
84
 
64
- ## Version [4.3.2] <sub><sup>2023-03-25</sub></sup>
85
+ ## Version [4.3.2] <sub><sup>2023-03-25</sup></sub>
65
86
 
66
87
  * Fix: added back fields that were removed in #589 [#647](https://github.com/stefankroes/ancestry/pull/647) (thx @rastamhadi)
67
- - path_ids_in_database
88
+ * path_ids_in_database
68
89
 
69
- ## Version [4.3.1] <sub><sup>2023-03-19</sub></sup>
90
+ ## Version [4.3.1] <sub><sup>2023-03-19</sup></sub>
70
91
 
71
92
  * Fix: added back fields that were removed in #589 [#637](https://github.com/stefankroes/ancestry/pull/637) (thx @znz)
72
- - ancestor_ids_in_database
73
- - parent_id_in_database
93
+ * ancestor_ids_in_database
94
+ * parent_id_in_database
74
95
 
75
- ## Version [4.3.0] <sub><sup>2023-03-09</sub></sup>
96
+ ## Version [4.3.0] <sub><sup>2023-03-09</sup></sub>
76
97
 
77
98
  * Fix: materialized_path2 strategy [#597](https://github.com/stefankroes/ancestry/pull/597) (thx @kshnurov)
78
99
  * Fix: descendants ancestry is now updated in after_update callbacks [#589](https://github.com/stefankroes/ancestry/pull/589) (thx @kshnurov)
@@ -84,7 +105,7 @@ jobs. If you need to do this in the ui, please use `cache_depth`.
84
105
  * ruby 3.2 support [#596](https://github.com/stefankroes/ancestry/pull/596) (thx @petergoldstein)
85
106
  * Reduce memory for sort_by_ancestry [#415](https://github.com/stefankroes/ancestry/pull/415)
86
107
 
87
- #### Notable features
108
+ ### Notable features
88
109
 
89
110
  Default configuration values are provided for a few options: `update_strategy`, `ancestry_format`, and `primary_key_format`.
90
111
  These can be set in an initializer via `Ancestry.default_{ancestry_format} = value`
@@ -94,12 +115,12 @@ It shows promise to make the `ancestry` field more sql friendly.
94
115
 
95
116
  Both of these are better documented in [the readme](/README.md).
96
117
 
97
- #### Breaking changes
118
+ ### Breaking changes
98
119
 
99
- - `ancestry_primary_key_format` is now specified or a single key not the whole regular expression.
120
+ * `ancestry_primary_key_format` is now specified or a single key not the whole regular expression.
100
121
  We used to accept `/\A[0-9]+(/[0-9]+)*` or `'[0-9]'`, but now we only accept `'[0-9]'`.
101
122
 
102
- ## Version [4.2.0] <sub><sup>2022-06-09</sub></sup>
123
+ ## Version [4.2.0] <sub><sup>2022-06-09</sup></sub>
103
124
 
104
125
  * added strategy: materialized_path2 [#571](https://github.com/stefankroes/ancestry/pull/571)
105
126
  * Added tree_view method [#561](https://github.com/stefankroes/ancestry/pull/561) (thx @bizcho)
@@ -108,7 +129,7 @@ Both of these are better documented in [the readme](/README.md).
108
129
  * rails 7.0 support (thx @chenillen, @petergoldstein)
109
130
  * Documentation fixes (thx @benkoshy, @mijoharas)
110
131
 
111
- ## Version [4.1.0] <sub><sup>2021-06-25</sub></sup>
132
+ ## Version [4.1.0] <sub><sup>2021-06-25</sup></sub>
112
133
 
113
134
  * `parent` with an invalid id now returns nil (thx @vanboom)
114
135
  * `root` returns self if ancestry is invalid (thx @vanboom)
@@ -116,29 +137,29 @@ Both of these are better documented in [the readme](/README.md).
116
137
  * oracleenhanced uses nulls first for sorting (thx @lual)
117
138
  * fix counter cache and STI (thx @mattvague)
118
139
 
119
- ## Version [4.0.0] <sub><sup>2021-04-12</sub></sup>
140
+ ## Version [4.0.0] <sub><sup>2021-04-12</sup></sub>
120
141
 
121
142
  * dropped support for rails 4.2 and 5.0 (thx @d-m-u)
122
143
  * better documentation counter cache option (thx @pustomytnyk)
123
144
  * clean up code (thx @amatsuda @d-m-u)
124
- * fixed rails 6.1 support (thx @cmr119 @d-staehler @danini-the-panini )
145
+ * fixed rails 6.1 support (thx @cmr119 @d-staehler @danini-the-panini)
125
146
  * phasing out `parent_id?`, `ancestors?` and using `has_parent?` instead
126
147
  * fixed postgres order bug on rails 6.2 and higher (thx @smoyt)
127
148
 
128
- ## Version [3.2.1] <sub><sup>2020-09-23</sub></sup>
149
+ ## Version [3.2.1] <sub><sup>2020-09-23</sup></sub>
129
150
 
130
151
  * fixed gemspec to include locales and pg (thx @HectorMF)
131
152
 
132
- ## Version [3.2.0] <sub><sup>2020-09-23</sub></sup>
153
+ ## Version [3.2.0] <sub><sup>2020-09-23</sup></sub>
133
154
 
134
155
  * introduce i18n
135
156
  * pg sql optimization for ancestry changes (thx @suonlight and @geis)
136
157
  * pg sql optimization for sorting (thx @brendon and @d-m-u)
137
- * fix to humanise model name (thx @mkllnk)
158
+ * fix to humanize model name (thx @mkllnk)
138
159
  * able to convert to ancestry from a parent_id column with a different name
139
160
  * documentation fixes for better diagrams and grammar (thx @dtamais, @d-m-u, and @CamilleDrapier)
140
161
 
141
- ## Version [3.1.0] <sub><sup>2020-08-03</sub></sup>
162
+ ## Version [3.1.0] <sub><sup>2020-08-03</sup></sub>
142
163
 
143
164
  * `:primary_key_format` method lets you change syntax. good for uuids.
144
165
  * changed code from being `ancestry` string to `ancestry_ids` focused. May break monkey patches.
@@ -147,40 +168,40 @@ Both of these are better documented in [the readme](/README.md).
147
168
  * Better documentation for relationships (thnx @dtamai and @d-m-u)
148
169
  * Fix creating children in `after_*` callbacks (thx @jstirk)
149
170
 
150
- ## Version [3.0.7] <sub><sup>2018-11-06</sub></sup>
171
+ ## Version [3.0.7] <sub><sup>2018-11-06</sup></sub>
151
172
 
152
173
  * Fixed rails 5.1 change detection (thx @jrafanie)
153
174
  * Introduce counter cache (thx @hw676018683)
154
175
 
155
- ## Version [3.0.6] <sub><sup>2018-11-06</sub></sup>
176
+ ## Version [3.0.6] <sub><sup>2018-11-06</sup></sub>
156
177
 
157
178
  * Fixed rails 4.1 version check (thx @myxoh)
158
179
 
159
- ## Version [3.0.5] <sub><sup>2018-11-06</sub></sup>
180
+ ## Version [3.0.5] <sub><sup>2018-11-06</sup></sub>
160
181
 
161
- ## Changed
182
+ ### Changed
162
183
 
163
184
  * Added indirect children support (thx @tilo)
164
185
  * Fixed test sorting for pg on mac osx
165
186
 
166
- ## Fixes
187
+ ### Fixes
167
188
 
168
189
  * Reduced memory footprint of parsing ancestry column (thx @NickLaMuro)
169
190
 
170
- ## Version [3.0.4] <sub><sup>2018-10-27</sub></sup>
191
+ ## Version [3.0.4] <sub><sup>2018-10-27</sup></sub>
171
192
 
172
- ## Fixes
193
+ ### Fixes
173
194
 
174
195
  * Properly detects non-integer columns (thx @adam101)
175
196
  * Arrange no longer drops nodes due to missing parents (thx @trafium)
176
197
 
177
- ## Version [3.0.3] <sub><sup>2018-10-23</sub></sup>
198
+ ## Version [3.0.3] <sub><sup>2018-10-23</sup></sub>
178
199
 
179
200
  This branch (3.x) should still be compatible with rails 3 and 4.
180
201
  Rails 5.1 and 5.2 support were introduced in this version, but ongoing support
181
202
  has been moved to ancestry 4.0
182
203
 
183
- ## Fixes
204
+ ### Fixes
184
205
 
185
206
  * Reduce object allocation (thx @NickLaMuro)
186
207
  * Rails 5.1 fixes (thx @ctrombley)
@@ -190,9 +211,9 @@ has been moved to ancestry 4.0
190
211
  * Dropped builds for ruby 1.9.3, 2.0, 2.1, and 2.2
191
212
  * Dropped builds for Rails 3.x and 4.x (will use Active Record `or` syntax)
192
213
 
193
- ## Version [3.0.2] <sub><sup>2018-04-24</sub></sup>
214
+ ## Version [3.0.2] <sub><sup>2018-04-24</sup></sub>
194
215
 
195
- ## Fixes
216
+ ### Fixes
196
217
 
197
218
  * fixed `order_by_ancestry` bug
198
219
  * fixed order tests for postgres on mac (it uses a different collation)
@@ -200,9 +221,9 @@ has been moved to ancestry 4.0
200
221
  * added missing `Ancestry::version`
201
222
  * added Rails 5.2 support (thx @jjuliano)
202
223
 
203
- ## Version [3.0.1] <sub><sup>2017-07-05</sub></sup>
224
+ ## Version [3.0.1] <sub><sup>2017-07-05</sup></sub>
204
225
 
205
- ## Fixes
226
+ ### Fixes
206
227
 
207
228
  * added gem metadata
208
229
  * fixed keep a changelog link (thx @mattbrictson)
@@ -211,14 +232,14 @@ has been moved to ancestry 4.0
211
232
  * fixed tests on mysql 5.7 and rails 3.2
212
233
  * Dropped 3.1 scope changes
213
234
 
214
- ## Version [3.0.0] <sub><sup>2017-05-18</sub></sup>
235
+ ## Version [3.0.0] <sub><sup>2017-05-18</sup></sub>
215
236
 
216
- ## Changed
237
+ ### Changed
217
238
 
218
239
  * Dropping Rails 3.0, and 3.1. Added Rails 5.1 support (thx @ledermann)
219
240
  * Dropping Rails 4.0, 4.1 for build reasons. Since 4.2 is supported, all 4.x should still work.
220
241
 
221
- ## Fixes
242
+ ### Fixes
222
243
 
223
244
  * Performance: Use `pluck` vs `map` for ids (thx @njakobsen and @culturecode)
224
245
  * Fixed acts_as_tree compatibility (thx @crazymykl)
@@ -227,7 +248,7 @@ has been moved to ancestry 4.0
227
248
  * Properly touches parents when different class for STI (thx @samtgarson)
228
249
  * Fixed issues with parent_id (only present on master) (thx @domcleal)
229
250
 
230
- ## Version [2.2.2] <sub><sup>2016-11-01</sub></sup>
251
+ ## Version [2.2.2] <sub><sup>2016-11-01</sup></sub>
231
252
 
232
253
  ### Changed
233
254
 
@@ -235,22 +256,25 @@ has been moved to ancestry 4.0
235
256
  * Fixed bug with explicit order clauses (introduced in 2.2.0)
236
257
  * No longer load schema on `has_ancestry` load (thx @ledermann)
237
258
 
238
- ## Version [2.2.1] <sub><sup>2016-10-25</sub></sup>
259
+ ## Version [2.2.1] <sub><sup>2016-10-25</sup></sub>
239
260
 
240
261
  Sorry for blip, local master got out of sync with upstream master.
241
262
  Missed 2 commits (which are feature adds)
242
263
 
243
264
  ### Added
265
+
244
266
  * Use like (vs ilike) for rails 5.0 (performance enhancement)
245
267
  * Use `COALESCE` for sorting on pg, mysql, and sqlite vs `CASE`
246
268
 
247
- ## Version [2.2.0] <sub><sup>2016-10-25</sub></sup>
269
+ ## Version [2.2.0] <sub><sup>2016-10-25</sup></sub>
248
270
 
249
271
  ### Added
272
+
250
273
  * Predicates for scopes: e.g.: `ancestor_of?`, `parent_of?` (thx @neglectedvalue)
251
274
  * Scope `path_of`
252
275
 
253
276
  ### Changed
277
+
254
278
  * `arrange` now accepts blocks (thx @mastfish)
255
279
  * Performance tuning `arrange_node` (thx @fryguy)
256
280
  * In orphan strategy, set `ancestry` to `nil` for no parents (thx @haslinger)
@@ -259,7 +283,8 @@ Missed 2 commits (which are feature adds)
259
283
  * Upgrading tests for ruby versions (thx @brocktimus, @fryguy, @yui-knk)
260
284
  * Fix non-default ancestry not getting used properly (thx @javiyu)
261
285
 
262
- ## Version [2.1.0] <sub><sup>2014-04-16</sub></sup>
286
+ ## Version [2.1.0] <sub><sup>2014-04-16</sup></sub>
287
+
263
288
  * Added arrange_serializable (thx @krishandley, @chicagogrrl)
264
289
  * Add the :touch to update ancestors on save (thx @adammck)
265
290
  * Change conditions into arel (thx @mlitwiniuk)
@@ -268,7 +293,8 @@ Missed 2 commits (which are feature adds)
268
293
  * Performance tweak (thx @mjc)
269
294
  * Improvements to organization (thx @xsuchy, @ryakh)
270
295
 
271
- ## Version [2.0.0] <sub><sup>2013-05-17</sub></sup>
296
+ ## Version [2.0.0] <sub><sup>2013-05-17</sup></sub>
297
+
272
298
  * Removed rails 2 compatibility
273
299
  * Added table name to condition constructing methods (thx @aflatter)
274
300
  * Fix depth_cache not being updated when moving up to ancestors (thx @scottatron)
@@ -280,34 +306,40 @@ Missed 2 commits (which are feature adds)
280
306
  * New adopt strategy (thx unknown)
281
307
  * Many more improvements
282
308
 
283
- ## Version [1.3.0] <sub><sup>2012-05-04</sub></sup>
309
+ ## Version [1.3.0] <sub><sup>2012-05-04</sup></sub>
310
+
284
311
  * Ancestry now ignores default scopes when moving or destroying nodes, ensuring tree consistency
285
312
  * Changed ActiveRecord dependency to 2.3.14
286
313
 
287
- ## Version [1.2.5] <sub><sup>2012-03-15</sub></sup>
314
+ ## Version [1.2.5] <sub><sup>2012-03-15</sup></sub>
315
+
288
316
  * Fixed warnings: "parenthesize argument(s) for future version"
289
317
  * Fixed a bug in the restore_ancestry_integrity! method (thx Arthur Holstvoogd)
290
318
 
291
- ## Version [1.2.4] <sub><sup>2011-04-22</sub></sup>
319
+ ## Version [1.2.4] <sub><sup>2011-04-22</sup></sub>
320
+
292
321
  * Prepended table names to column names in queries (thx @raelik)
293
322
  * Better check to see if acts_as_tree can be overloaded (thx @jims)
294
- * Performance inprovements (thx @kueda)
323
+ * Performance improvements (thx @kueda)
324
+
325
+ ## Version [1.2.3] <sub><sup>2010-10-28</sup></sub>
295
326
 
296
- ## Version [1.2.3] <sub><sup>2010-10-28</sub></sup>
297
327
  * Fixed error with determining ActiveRecord version
298
328
  * Added option to specify :primary_key_format (thx @rolftimmermans)
299
329
 
300
- ## Version [1.2.2] <sub><sup>2010-10-24</sub></sup>
330
+ ## Version [1.2.2] <sub><sup>2010-10-24</sup></sub>
331
+
301
332
  * Fixed all deprecation warnings for rails 3.0.X
302
333
  * Added `:report` option to `check_ancestry_integrity!`
303
334
  * Changed ActiveRecord dependency to 2.2.2
304
335
  * Tested and fixed for ruby 1.8.7 and 1.9.2
305
336
  * Changed usage of `update_attributes` to `update_attribute` to allow ancestry column protection
306
337
 
307
- ## Version [1.2.0] <sub><sup>2009-11-07</sub></sup>
338
+ ## Version [1.2.0] <sub><sup>2009-11-07</sup></sub>
339
+
308
340
  * Removed some duplication in has_ancestry
309
341
  * Cleaned up plugin pattern according to http://yehudakatz.com/2009/11/12/better-ruby-idioms/
310
- * Moved parts of ancestry into seperate files
342
+ * Moved parts of ancestry into separate files
311
343
  * Made it possible to pass options into the arrange method
312
344
  * Renamed acts_as_tree to has_ancestry
313
345
  * Aliased has_ancestry as acts_as_tree if acts_as_tree is available
@@ -315,46 +347,52 @@ Missed 2 commits (which are feature adds)
315
347
  * Updated ordered_by_ancestry scope to support Microsoft SQL Server
316
348
  * Added empty hash as parameter to exists? calls for older ActiveRecord versions
317
349
 
318
- ## Version [1.1.4] <sub><sup>2009-11-07</sub></sup>
350
+ ## Version [1.1.4] <sub><sup>2009-11-07</sup></sub>
351
+
319
352
  * Thanks to a patch from tom taylor, Ancestry now works with different primary keys
320
353
 
321
- ## Version [1.1.3] <sub><sup>2009-11-01</sub></sup>
354
+ ## Version [1.1.3] <sub><sup>2009-11-01</sup></sub>
355
+
322
356
  * Fixed a pretty bad bug where several operations took far too many queries
323
357
 
324
- ## Version [1.1.2] <sub><sup>2009-10-29</sub></sup>
358
+ ## Version [1.1.2] <sub><sup>2009-10-29</sup></sub>
359
+
325
360
  * Added validation for depth cache column
326
361
  * Added STI support (reported broken)
327
362
 
328
- ## Version [1.1.1] <sub><sup>2009-10-28</sub></sup>
363
+ ## Version [1.1.1] <sub><sup>2009-10-28</sup></sub>
364
+
329
365
  * Fixed some parentheses warnings that where reported
330
366
  * Fixed a reported issue with arrangement
331
367
  * Fixed issues with ancestors and path order on postgres
332
368
  * Added ordered_by_ancestry scope (needed to fix issues)
333
369
 
334
- ## Version [1.1.0] <sub><sup>2009-10-22</sub></sup>
370
+ ## Version [1.1.0] <sub><sup>2009-10-22</sup></sub>
371
+
335
372
  * Depth caching (and cache rebuilding)
336
373
  * Depth method for nodes
337
374
  * Named scopes for selecting by depth
338
- * Relative depth options for tree navigation methods:
339
- * ancestors
340
- * path
341
- * descendants
342
- * descendant_ids
343
- * subtree
344
- * subtree_ids
375
+ * Relative depth options for tree navigation methods:
376
+ * ancestors
377
+ * path
378
+ * descendants
379
+ * descendant_ids
380
+ * subtree
381
+ * subtree_ids
345
382
  * Updated README
346
383
  * Easy migration from existing plugins/gems
347
384
  * acts_as_tree checks unknown options
348
385
  * acts_as_tree checks that options are hash
349
386
  * Added a bang (!) to the integrity functions
350
- * Since these functions should only be used from ./script/console and not
351
- from your application, this change is not considered as breaking backwards
352
- compatibility and the major version wasn't bumped.
387
+ * Since these functions should only be used from ./script/console and not
388
+ from your application, this change is not considered as breaking backwards
389
+ compatibility and the major version wasn't bumped.
353
390
  * Updated install script to point to documentation
354
391
  * Removed rails specific init
355
392
  * Removed uninstall script
356
393
 
357
- ## Version 1.0.0 <sub><sup>2009-10-16</sub></sup>
394
+ ## Version 1.0.0 <sub><sup>2009-10-16</sup></sub>
395
+
358
396
  * Initial version
359
397
  * Tree building
360
398
  * Tree navigation
@@ -365,8 +403,8 @@ Missed 2 commits (which are feature adds)
365
403
  * Named scopes
366
404
  * Validations
367
405
 
368
-
369
- [HEAD]: https://github.com/stefankroes/ancestry/compare/v5.0.0...HEAD
406
+ [HEAD]: https://github.com/stefankroes/ancestry/compare/v5.1.0...HEAD
407
+ [5.1.0]: https://github.com/stefankroes/ancestry/compare/v5.0.0...v5.1.0
370
408
  [5.0.0]: https://github.com/stefankroes/ancestry/compare/v4.3.3...v5.0.0
371
409
  [4.3.3]: https://github.com/stefankroes/ancestry/compare/v4.3.2...v4.3.3
372
410
  [4.3.2]: https://github.com/stefankroes/ancestry/compare/v4.3.1...v4.3.2
data/README.md CHANGED
@@ -519,6 +519,19 @@ Some use migrations, but that can make the migration suite fragile. The command
519
519
  Model.rebuild_depth_cache!
520
520
  ```
521
521
 
522
+ ## Depth Constraints
523
+
524
+ You can use standard Rails validations on your depth cache column to restrict the depth of your tree. For example, to ensure no nodes are deeper than 2:
525
+
526
+ ```ruby
527
+ class TreeNode < ActiveRecord::Base
528
+ has_ancestry cache_depth: true
529
+ validates :ancestry_depth, numericality: { less_than_or_equal_to: 2 }
530
+ end
531
+ ```
532
+
533
+ Ancestry will automatically validate not just the node itself, but also ensure that moving a subtree does not cause any of its descendants to exceed the maximum depth.
534
+
522
535
  # Running Tests
523
536
 
524
537
  ```bash
@@ -108,13 +108,17 @@ module Ancestry
108
108
 
109
109
  # Pseudo-preordered array of nodes. Children will always follow parents,
110
110
  # This is deterministic unless the parents are missing *and* a sort block is specified
111
- def sort_by_ancestry(nodes)
111
+ def sort_by_ancestry(nodes, &block)
112
+ _sort_by_ancestry(nodes, ancestry_column, &block)
113
+ end
114
+
115
+ def _sort_by_ancestry(nodes, column, &block)
112
116
  arranged = nodes if nodes.is_a?(Hash)
113
117
 
114
118
  unless arranged
115
119
  presorted_nodes = nodes.sort do |a, b|
116
- rank = (a.public_send(ancestry_column) || ' ') <=> (b.public_send(ancestry_column) || ' ')
117
- rank = yield(a, b) if rank == 0 && block_given?
120
+ rank = (a.public_send(column) || ' ') <=> (b.public_send(column) || ' ')
121
+ rank = block.call(a, b) if rank == 0 && block
118
122
  rank
119
123
  end
120
124
 
@@ -129,6 +133,10 @@ module Ancestry
129
133
  # just in case, raise an AncestryIntegrityException if issues are detected
130
134
  # specify :report => :list to return an array of exceptions or :report => :echo to echo any error messages
131
135
  def check_ancestry_integrity!(options = {})
136
+ _check_ancestry_integrity!(ancestry_column, options)
137
+ end
138
+
139
+ def _check_ancestry_integrity!(column, options = {})
132
140
  parents = {}
133
141
  exceptions = [] if options[:report] == :list
134
142
 
@@ -139,7 +147,7 @@ module Ancestry
139
147
  if !node.sane_ancestor_ids?
140
148
  raise Ancestry::AncestryIntegrityException, I18n.t("ancestry.invalid_ancestry_column",
141
149
  :node_id => node.id,
142
- :ancestry_column => node.read_attribute(node.class.ancestry_column))
150
+ :ancestry_column => node.read_attribute(column))
143
151
  end
144
152
  # ... check that all ancestors exist
145
153
  node.ancestor_ids.each do |ancestor_id|
@@ -50,13 +50,20 @@ module Ancestry
50
50
  validates ancestry_column, ancestry_validation_options(primary_key_format)
51
51
 
52
52
  update_strategy = options[:update_strategy] || Ancestry.default_update_strategy
53
- include Ancestry::MaterializedPathPg if update_strategy == :sql
53
+ include Ancestry::MaterializedPathPg
54
54
 
55
55
  # Validate that the ancestor ids don't include own id
56
56
  validate :ancestry_exclude_self
57
57
 
58
+ # Validate descendants' depths don't exceed max depth when moving them
59
+ validate :ancestry_depth_of_descendants, if: :ancestry_changed?
60
+
58
61
  # Update descendants with new ancestry after update
59
- after_update :update_descendants_with_new_ancestry, if: :ancestry_changed?
62
+ if update_strategy == :sql
63
+ after_update :update_descendants_with_new_ancestry_sql, if: :ancestry_changed?
64
+ else
65
+ after_update :update_descendants_with_new_ancestry, if: :ancestry_changed?
66
+ end
60
67
 
61
68
  # Apply orphan strategy before destroy
62
69
  orphan_strategy_helper = "apply_orphan_strategy_#{orphan_strategy}"
@@ -7,6 +7,32 @@ module Ancestry
7
7
  errors.add(:base, I18n.t("ancestry.exclude_self", class_name: self.class.model_name.human)) if ancestor_ids.include?(id)
8
8
  end
9
9
 
10
+ # Validate that descendants' depths don't exceed max depth when moving them
11
+ def ancestry_depth_of_descendants
12
+ return if new_record? || (respond_to?(:previously_new_record?) && previously_new_record?)
13
+ return unless self.class.respond_to?(:depth_cache_column) && self.class.depth_cache_column
14
+
15
+ column = self.class.depth_cache_column
16
+ validator = self.class.validators_on(column).find do |v|
17
+ v.is_a?(ActiveModel::Validations::NumericalityValidator) &&
18
+ (v.options[:less_than_or_equal_to] || v.options[:less_than])
19
+ end
20
+ return unless validator
21
+
22
+ max_depth = validator.options[:less_than_or_equal_to] || (validator.options[:less_than] - 1)
23
+
24
+ old_value = attribute_in_database(self.class.ancestry_column)
25
+ new_value = read_attribute(self.class.ancestry_column)
26
+ depth_change = self.class.ancestry_depth_change(old_value, new_value)
27
+
28
+ if depth_change > 0
29
+ max_descendant_depth = unscoped_descendants.maximum(column) || attribute_in_database(column) || 0
30
+ if max_descendant_depth + depth_change > max_depth
31
+ errors.add(column, :less_than_or_equal_to, count: max_depth)
32
+ end
33
+ end
34
+ end
35
+
10
36
  # Update descendants with new ancestry (after update)
11
37
  def update_descendants_with_new_ancestry
12
38
  # If enabled and the new ancestry is sane ...
@@ -30,16 +30,13 @@ module Ancestry
30
30
  end
31
31
 
32
32
  def children_of(object)
33
- t = arel_table
34
33
  node = to_node(object)
35
- where(t[ancestry_column].eq(node.child_ancestry))
34
+ where(arel_table[ancestry_column].eq(node.child_ancestry))
36
35
  end
37
36
 
38
- # indirect = anyone who is a descendant, but not a child
39
37
  def indirects_of(object)
40
- t = arel_table
41
38
  node = to_node(object)
42
- where(t[ancestry_column].matches("#{node.child_ancestry}#{ancestry_delimiter}%", nil, true))
39
+ where(MaterializedPath.indirects_condition(arel_table[ancestry_column], node.child_ancestry, ancestry_delimiter))
43
40
  end
44
41
 
45
42
  def descendants_of(object)
@@ -47,8 +44,7 @@ module Ancestry
47
44
  end
48
45
 
49
46
  def descendants_by_ancestry(ancestry)
50
- t = arel_table
51
- t[ancestry_column].matches("#{ancestry}#{ancestry_delimiter}%", nil, true).or(t[ancestry_column].eq(ancestry))
47
+ MaterializedPath.descendants_condition(arel_table[ancestry_column], ancestry, ancestry_delimiter)
52
48
  end
53
49
 
54
50
  def descendant_conditions(object)
@@ -62,15 +58,13 @@ module Ancestry
62
58
  end
63
59
 
64
60
  def subtree_of(object)
65
- t = arel_table
66
61
  node = to_node(object)
67
- descendants_of(node).or(where(t[primary_key].eq(node.id)))
62
+ descendants_of(node).or(where(arel_table[primary_key].eq(node.id)))
68
63
  end
69
64
 
70
65
  def siblings_of(object)
71
- t = arel_table
72
66
  node = to_node(object)
73
- where(t[ancestry_column].eq(node[ancestry_column].presence))
67
+ where(arel_table[ancestry_column].eq(node[ancestry_column].presence))
74
68
  end
75
69
 
76
70
  def ordered_by_ancestry(order = nil)
@@ -95,11 +89,7 @@ module Ancestry
95
89
  end
96
90
 
97
91
  def child_ancestry_sql
98
- %{
99
- CASE WHEN #{table_name}.#{ancestry_column} IS NULL THEN #{concat("#{table_name}.#{primary_key}")}
100
- ELSE #{concat("#{table_name}.#{ancestry_column}", "'#{ancestry_delimiter}'", "#{table_name}.#{primary_key}")}
101
- END
102
- }
92
+ MaterializedPath.child_ancestry_sql(table_name, ancestry_column, primary_key, ancestry_delimiter, connection.adapter_name.downcase)
103
93
  end
104
94
 
105
95
  def ancestry_depth_sql
@@ -107,53 +97,85 @@ module Ancestry
107
97
  end
108
98
 
109
99
  def generate_ancestry(ancestor_ids)
100
+ MaterializedPath.generate(ancestor_ids, ancestry_delimiter, ancestry_root)
101
+ end
102
+
103
+ def parse_ancestry_column(obj)
104
+ MaterializedPath.parse(obj, ancestry_root, ancestry_delimiter, primary_key_is_an_integer?)
105
+ end
106
+
107
+ def ancestry_depth_change(old_value, new_value)
108
+ parse_ancestry_column(new_value).size - parse_ancestry_column(old_value).size
109
+ end
110
+
111
+ def self.generate(ancestor_ids, delimiter, root)
110
112
  if ancestor_ids.present? && ancestor_ids.any?
111
- ancestor_ids.join(ancestry_delimiter)
113
+ ancestor_ids.join(delimiter)
112
114
  else
113
- ancestry_root
115
+ root
114
116
  end
115
117
  end
116
118
 
117
- def parse_ancestry_column(obj)
118
- return [] if obj.nil? || obj == ancestry_root
119
+ def self.parse(obj, root, delimiter, integer_pk)
120
+ return [] if obj.nil? || obj == root
119
121
 
120
- obj_ids = obj.split(ancestry_delimiter).delete_if(&:blank?)
121
- primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
122
+ obj_ids = obj.split(delimiter).delete_if(&:blank?)
123
+ integer_pk ? obj_ids.map!(&:to_i) : obj_ids
122
124
  end
123
125
 
124
- def ancestry_depth_change(old_value, new_value)
125
- parse_ancestry_column(new_value).size - parse_ancestry_column(old_value).size
126
+ def self.child_ancestry_value(ancestry_value, id, delimiter)
127
+ [ancestry_value, id].compact.join(delimiter)
128
+ end
129
+
130
+ # Arel condition: descendants have ancestry matching child_ancestry or starting with child_ancestry/
131
+ def self.descendants_condition(attr, child_ancestry, delimiter)
132
+ attr.matches("#{child_ancestry}#{delimiter}%", nil, true).or(attr.eq(child_ancestry))
133
+ end
134
+
135
+ # Arel condition: indirects have ancestry matching child_ancestry/*/
136
+ def self.indirects_condition(attr, child_ancestry, delimiter)
137
+ attr.matches("#{child_ancestry}#{delimiter}%", nil, true)
126
138
  end
127
139
 
128
140
  def concat(*args)
129
- if %w(sqlite sqlite3).include?(connection.adapter_name.downcase)
141
+ MaterializedPath.concat(connection.adapter_name.downcase, *args)
142
+ end
143
+
144
+ def self.concat(adapter, *args)
145
+ if %w(sqlite sqlite3).include?(adapter)
130
146
  args.join('||')
131
147
  else
132
148
  %{CONCAT(#{args.join(', ')})}
133
149
  end
134
150
  end
135
151
 
152
+ def self.child_ancestry_sql(table_name, ancestry_column, primary_key, delimiter, adapter)
153
+ pk_sql = concat(adapter, "#{table_name}.#{primary_key}")
154
+ full_sql = concat(adapter, "#{table_name}.#{ancestry_column}", "'#{delimiter}'", "#{table_name}.#{primary_key}")
155
+ %{
156
+ CASE WHEN #{table_name}.#{ancestry_column} IS NULL THEN #{pk_sql}
157
+ ELSE #{full_sql}
158
+ END
159
+ }
160
+ end
161
+
136
162
  def self.construct_depth_sql(table_name, ancestry_column, ancestry_delimiter)
137
163
  tmp = %{(LENGTH(#{table_name}.#{ancestry_column}) - LENGTH(REPLACE(#{table_name}.#{ancestry_column},'#{ancestry_delimiter}','')))}
138
164
  tmp += "/#{ancestry_delimiter.size}" if ancestry_delimiter.size > 1
139
165
  "(CASE WHEN #{table_name}.#{ancestry_column} IS NULL THEN 0 ELSE 1 + #{tmp} END)"
140
166
  end
141
167
 
142
- private
143
-
144
- def ancestry_validation_options(ancestry_primary_key_format)
168
+ def self.validation_options(primary_key_format, delimiter)
145
169
  {
146
- format: {with: ancestry_format_regexp(ancestry_primary_key_format)},
147
- allow_nil: ancestry_nil_allowed?
170
+ format: {with: /\A#{primary_key_format}(#{Regexp.escape(delimiter)}#{primary_key_format})*\z/.freeze},
171
+ allow_nil: true
148
172
  }
149
173
  end
150
174
 
151
- def ancestry_nil_allowed?
152
- true
153
- end
175
+ private
154
176
 
155
- def ancestry_format_regexp(primary_key_format)
156
- /\A#{primary_key_format}(#{Regexp.escape(ancestry_delimiter)}#{primary_key_format})*\z/.freeze
177
+ def ancestry_validation_options(ancestry_primary_key_format)
178
+ MaterializedPath.validation_options(ancestry_primary_key_format, ancestry_delimiter)
157
179
  end
158
180
 
159
181
  module InstanceMethods
@@ -168,23 +190,23 @@ module Ancestry
168
190
  end
169
191
 
170
192
  def ancestor_ids
171
- self.class.parse_ancestry_column(read_attribute(self.class.ancestry_column))
193
+ MaterializedPath.parse(read_attribute(self.class.ancestry_column), self.class.ancestry_root, self.class.ancestry_delimiter, self.class.primary_key_is_an_integer?)
172
194
  end
173
195
 
174
196
  def ancestor_ids_in_database
175
- self.class.parse_ancestry_column(attribute_in_database(self.class.ancestry_column))
197
+ MaterializedPath.parse(attribute_in_database(self.class.ancestry_column), self.class.ancestry_root, self.class.ancestry_delimiter, self.class.primary_key_is_an_integer?)
176
198
  end
177
199
 
178
200
  def ancestor_ids_before_last_save
179
- self.class.parse_ancestry_column(attribute_before_last_save(self.class.ancestry_column))
201
+ MaterializedPath.parse(attribute_before_last_save(self.class.ancestry_column), self.class.ancestry_root, self.class.ancestry_delimiter, self.class.primary_key_is_an_integer?)
180
202
  end
181
203
 
182
204
  def parent_id_in_database
183
- self.class.parse_ancestry_column(attribute_in_database(self.class.ancestry_column)).last
205
+ MaterializedPath.parse(attribute_in_database(self.class.ancestry_column), self.class.ancestry_root, self.class.ancestry_delimiter, self.class.primary_key_is_an_integer?).last
184
206
  end
185
207
 
186
208
  def parent_id_before_last_save
187
- self.class.parse_ancestry_column(attribute_before_last_save(self.class.ancestry_column)).last
209
+ MaterializedPath.parse(attribute_before_last_save(self.class.ancestry_column), self.class.ancestry_root, self.class.ancestry_delimiter, self.class.primary_key_is_an_integer?).last
188
210
  end
189
211
 
190
212
  # optimization - better to go directly to column and avoid parsing
@@ -200,7 +222,7 @@ module Ancestry
200
222
  def child_ancestry
201
223
  raise(Ancestry::AncestryException, I18n.t("ancestry.no_child_for_new_record")) if new_record?
202
224
 
203
- [attribute_in_database(self.class.ancestry_column), id].compact.join(self.class.ancestry_delimiter)
225
+ MaterializedPath.child_ancestry_value(attribute_in_database(self.class.ancestry_column), id, self.class.ancestry_delimiter)
204
226
  end
205
227
 
206
228
  # The ancestry value for this record's old children
@@ -212,7 +234,7 @@ module Ancestry
212
234
  raise Ancestry::AncestryException, I18n.t("ancestry.no_child_for_new_record")
213
235
  end
214
236
 
215
- [attribute_before_last_save(self.class.ancestry_column), id].compact.join(self.class.ancestry_delimiter)
237
+ MaterializedPath.child_ancestry_value(attribute_before_last_save(self.class.ancestry_column), id, self.class.ancestry_delimiter)
216
238
  end
217
239
  end
218
240
  end
@@ -13,9 +13,8 @@ module Ancestry
13
13
  end
14
14
 
15
15
  def indirects_of(object)
16
- t = arel_table
17
16
  node = to_node(object)
18
- where(t[ancestry_column].matches("#{node.child_ancestry}%#{ancestry_delimiter}%", nil, true))
17
+ where(MaterializedPath2.indirects_condition(arel_table[ancestry_column], node.child_ancestry, ancestry_delimiter))
19
18
  end
20
19
 
21
20
  def ordered_by_ancestry(order = nil)
@@ -23,7 +22,7 @@ module Ancestry
23
22
  end
24
23
 
25
24
  def descendants_by_ancestry(ancestry)
26
- arel_table[ancestry_column].matches("#{ancestry}%", nil, true)
25
+ MaterializedPath2.descendants_condition(arel_table[ancestry_column], ancestry, ancestry_delimiter)
27
26
  end
28
27
 
29
28
  def ancestry_root
@@ -31,7 +30,7 @@ module Ancestry
31
30
  end
32
31
 
33
32
  def child_ancestry_sql
34
- concat("#{table_name}.#{ancestry_column}", "#{table_name}.#{primary_key}", "'#{ancestry_delimiter}'")
33
+ MaterializedPath2.child_ancestry_sql(table_name, ancestry_column, primary_key, ancestry_delimiter, connection.adapter_name.downcase)
35
34
  end
36
35
 
37
36
  def ancestry_depth_sql
@@ -39,13 +38,35 @@ module Ancestry
39
38
  end
40
39
 
41
40
  def generate_ancestry(ancestor_ids)
41
+ MaterializedPath2.generate(ancestor_ids, ancestry_delimiter, ancestry_root)
42
+ end
43
+
44
+ def self.generate(ancestor_ids, delimiter, root)
42
45
  if ancestor_ids.present? && ancestor_ids.any?
43
- "#{ancestry_delimiter}#{ancestor_ids.join(ancestry_delimiter)}#{ancestry_delimiter}"
46
+ "#{delimiter}#{ancestor_ids.join(delimiter)}#{delimiter}"
44
47
  else
45
- ancestry_root
48
+ root
46
49
  end
47
50
  end
48
51
 
52
+ def self.child_ancestry_value(ancestry_value, id, delimiter)
53
+ "#{ancestry_value}#{id}#{delimiter}"
54
+ end
55
+
56
+ def self.child_ancestry_sql(table_name, ancestry_column, primary_key, delimiter, adapter)
57
+ MaterializedPath.concat(adapter, "#{table_name}.#{ancestry_column}", "#{table_name}.#{primary_key}", "'#{delimiter}'")
58
+ end
59
+
60
+ # mp2: descendants just use LIKE (trailing delimiter prevents false prefix matches)
61
+ def self.descendants_condition(attr, child_ancestry, _delimiter)
62
+ attr.matches("#{child_ancestry}%", nil, true)
63
+ end
64
+
65
+ # mp2: indirects match child_ancestry + at least one more segment
66
+ def self.indirects_condition(attr, child_ancestry, delimiter)
67
+ attr.matches("#{child_ancestry}%#{delimiter}%", nil, true)
68
+ end
69
+
49
70
  # module method
50
71
  def self.construct_depth_sql(table_name, ancestry_column, ancestry_delimiter)
51
72
  tmp = %{(LENGTH(#{table_name}.#{ancestry_column}) - LENGTH(REPLACE(#{table_name}.#{ancestry_column},'#{ancestry_delimiter}','')))}
@@ -53,14 +74,17 @@ module Ancestry
53
74
  "(#{tmp} -1)"
54
75
  end
55
76
 
56
- private
57
-
58
- def ancestry_nil_allowed?
59
- false
77
+ def self.validation_options(primary_key_format, delimiter)
78
+ {
79
+ format: {with: /\A#{Regexp.escape(delimiter)}(#{primary_key_format}#{Regexp.escape(delimiter)})*\z/.freeze},
80
+ allow_nil: false
81
+ }
60
82
  end
61
83
 
62
- def ancestry_format_regexp(primary_key_format)
63
- /\A#{Regexp.escape(ancestry_delimiter)}(#{primary_key_format}#{Regexp.escape(ancestry_delimiter)})*\z/.freeze
84
+ private
85
+
86
+ def ancestry_validation_options(ancestry_primary_key_format)
87
+ MaterializedPath2.validation_options(ancestry_primary_key_format, ancestry_delimiter)
64
88
  end
65
89
 
66
90
  module InstanceMethods
@@ -68,7 +92,7 @@ module Ancestry
68
92
  def child_ancestry
69
93
  raise(Ancestry::AncestryException, I18n.t("ancestry.no_child_for_new_record")) if new_record?
70
94
 
71
- "#{attribute_in_database(self.class.ancestry_column)}#{id}#{self.class.ancestry_delimiter}"
95
+ MaterializedPath2.child_ancestry_value(attribute_in_database(self.class.ancestry_column), id, self.class.ancestry_delimiter)
72
96
  end
73
97
 
74
98
  # Please see notes for MaterializedPath#child_ancestry_before_last_save
@@ -77,7 +101,7 @@ module Ancestry
77
101
  raise(Ancestry::AncestryException, I18n.t("ancestry.no_child_for_new_record"))
78
102
  end
79
103
 
80
- "#{attribute_before_last_save(self.class.ancestry_column)}#{id}#{self.class.ancestry_delimiter}"
104
+ MaterializedPath2.child_ancestry_value(attribute_before_last_save(self.class.ancestry_column), id, self.class.ancestry_delimiter)
81
105
  end
82
106
  end
83
107
  end
@@ -2,15 +2,19 @@
2
2
 
3
3
  module Ancestry
4
4
  module MaterializedPathPg
5
- # Update descendants with new ancestry (after update)
6
- def update_descendants_with_new_ancestry
5
+ # Update descendants with new ancestry using a single SQL statement (after update)
6
+ def update_descendants_with_new_ancestry_sql
7
7
  # If enabled and node is existing and ancestry was updated and the new ancestry is sane ...
8
8
  # The only way the ancestry could be bad is via `update_attribute` with a bad value
9
9
  if !ancestry_callbacks_disabled? && sane_ancestor_ids?
10
10
  old_ancestry = self.class.generate_ancestry(path_ids_before_last_save)
11
11
  new_ancestry = self.class.generate_ancestry(path_ids)
12
+ # Replace old ancestry prefix with new ancestry:
13
+ # CONCAT(new_ancestry, SUBSTRING(column, LENGTH(old_ancestry) + 1))
14
+ column = self.class.ancestry_column
15
+ replace_sql = self.class.concat("'#{new_ancestry}'", "SUBSTRING(#{column}, #{old_ancestry.length + 1})")
12
16
  update_clause = {
13
- self.class.ancestry_column => Arel.sql("regexp_replace(#{self.class.ancestry_column}, '^#{Regexp.escape(old_ancestry)}', '#{new_ancestry}')")
17
+ column => Arel.sql(replace_sql)
14
18
  }
15
19
 
16
20
  current_time = current_time_from_proper_timezone
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ancestry
4
- VERSION = '5.0.0'
4
+ VERSION = '5.1.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ancestry
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.0
4
+ version: 5.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Kroes