ancestry 3.2.1 → 4.2.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: 2f1ae94346d7ad0aa07830437239dc7f13dda73408c61a118e60f53ef21e6282
4
- data.tar.gz: 1eb3e39a50b257dddc13559d52b03955c8174682f990a723614ca9e2e3dc6945
3
+ metadata.gz: 82bcd1895093ab9b569806ef05fc307fc2ca9ab53ffeb09199bd66fe3672ecfb
4
+ data.tar.gz: fe7d0d356641658be2953309c27dd28a42e81ed569690cc5e26b2721b1aa6a37
5
5
  SHA512:
6
- metadata.gz: 87a3c689530416fd20fa4064bb7c610e8e9e3b013324776e5a7c96ad7cb0a8f1a136dfbe71ada9e53b5116ef41602dab7d2d7664e2fc047e8a31f7a6598e2238
7
- data.tar.gz: f420ad732aca06c42116a97db60898db8712163aae660d13b8f0d32b806eaa6e81edac4ba2eb167986456995859cfd52da829abbbdf7d3377177b9f9049b73d5
6
+ metadata.gz: f33384a1114d865662be0133c532249be0e7ea438a66e469947b3aaba3172378ae50309e02c4f219c1f161281a9e060bff6087a496850c26351fd83348627e79
7
+ data.tar.gz: 2a6cb3fb28f6c9a8228b522c590c2125c4e31056a97ac6a6cda96ea4152725fe89e375b59c894091ebcd3d5be36ad18b348c6691211bc6bea2b0a49502c9f423
data/CHANGELOG.md CHANGED
@@ -3,15 +3,37 @@
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 [HEAD] <small>now</small>
6
+ ## Version [4.2.0] <sub><sup>2022-06-09</sub></sup>
7
7
 
8
- * dropped support for rails 4.2 and 5.0
8
+ * added strategy: materialized_path2 [#571](https://github.com/stefankroes/ancestry/pull/571)
9
+ * Added tree_view method [#561](https://github.com/stefankroes/ancestry/pull/561) (thx @bizcho)
10
+ * Fixed bug when errors would not undo callbacks [#566](https://github.com/stefankroes/ancestry/pull/566) (thx @daniloisr)
11
+ * ruby 3.0 support
12
+ * rails 7.0 support (thx @chenillen, @petergoldstein)
13
+ * Documentation fixes (thx @benkoshy, @mijoharas)
9
14
 
10
- ## Versions [3.2.1] <small>2020-09-23</small>
15
+ ## Version [4.1.0] <sub><sup>2021-06-25</sub></sup>
16
+
17
+ * `parent` with an invalid id now returns nil (thx @vanboom)
18
+ * `root` returns self if ancestry is invalid (thx @vanboom)
19
+ * fix case where invalid object prevented ancestry updates (thx @d-m-u)
20
+ * oracleenhanced uses nulls first for sorting (thx @lual)
21
+ * fix counter cache and STI (thx @mattvague)
22
+
23
+ ## Version [4.0.0] <sub><sup>2021-04-12</sub></sup>
24
+
25
+ * dropped support for rails 4.2 and 5.0 (thx @d-m-u)
26
+ * better documentation counter cache option (thx @pustomytnyk)
27
+ * clean up code (thx @amatsuda @d-m-u)
28
+ * fixed rails 6.1 support (thx @cmr119 @d-staehler @danini-the-panini )
29
+ * phasing out `parent_id?`, `ancestors?` and using `has_parent?` instead
30
+ * fixed postgres order bug on rails 6.2 and higher (thx @smoyt)
31
+
32
+ ## Version [3.2.1] <sub><sup>2020-09-23</sub></sup>
11
33
 
12
34
  * fixed gemspec to include locales and pg (thx @HectorMF)
13
35
 
14
- ## Versions [3.2.0] <small>2020-09-23</small>
36
+ ## Version [3.2.0] <sub><sup>2020-09-23</sub></sup>
15
37
 
16
38
  * introduce i18n
17
39
  * pg sql optimization for ancestry changes (thx @suonlight and @geis)
@@ -20,7 +42,7 @@ a nice looking [Changelog](http://keepachangelog.com).
20
42
  * able to convert to ancestry from a parent_id column with a different name
21
43
  * documentation fixes for better diagrams and grammar (thx @dtamais, @d-m-u, and @CamilleDrapier)
22
44
 
23
- ## Versions [3.1.0] <small>2020-08-03</small>
45
+ ## Version [3.1.0] <sub><sup>2020-08-03</sub></sup>
24
46
 
25
47
  * `:primary_key_format` method lets you change syntax. good for uuids.
26
48
  * changed code from being `ancestry` string to `ancestry_ids` focused. May break monkey patches.
@@ -29,16 +51,16 @@ a nice looking [Changelog](http://keepachangelog.com).
29
51
  * Better documentation for relationships (thnx @dtamai and @d-m-u)
30
52
  * Fix creating children in `after_*` callbacks (thx @jstirk)
31
53
 
32
- ## Version [3.0.7] <small>2018-11-06</small>
54
+ ## Version [3.0.7] <sub><sup>2018-11-06</sub></sup>
33
55
 
34
56
  * Fixed rails 5.1 change detection (thx @jrafanie)
35
57
  * Introduce counter cache (thx @hw676018683)
36
58
 
37
- ## Version [3.0.6] <small>2018-11-06</small>
59
+ ## Version [3.0.6] <sub><sup>2018-11-06</sub></sup>
38
60
 
39
61
  * Fixed rails 4.1 version check (thx @myxoh)
40
62
 
41
- ## Version [3.0.5] <small>2018-11-06</small>
63
+ ## Version [3.0.5] <sub><sup>2018-11-06</sub></sup>
42
64
 
43
65
  ## Changed
44
66
 
@@ -49,14 +71,14 @@ a nice looking [Changelog](http://keepachangelog.com).
49
71
 
50
72
  * Reduced memory footprint of parsing ancestry column (thx @NickLaMuro)
51
73
 
52
- ## Version [3.0.4] <small>2018-10-27</small>
74
+ ## Version [3.0.4] <sub><sup>2018-10-27</sub></sup>
53
75
 
54
76
  ## Fixes
55
77
 
56
78
  * Properly detects non-integer columns (thx @adam101)
57
79
  * Arrange no longer drops nodes due to missing parents (thx @trafium)
58
80
 
59
- ## Version [3.0.3] <small>2018-10-23</small>
81
+ ## Version [3.0.3] <sub><sup>2018-10-23</sub></sup>
60
82
 
61
83
  This branch (3.x) should still be compatible with rails 3 and 4.
62
84
  Rails 5.1 and 5.2 support were introduced in this version, but ongoing support
@@ -72,7 +94,7 @@ has been moved to ancestry 4.0
72
94
  * Dropped builds for ruby 1.9.3, 2.0, 2.1, and 2.2
73
95
  * Dropped builds for Rails 3.x and 4.x (will use Active Record `or` syntax)
74
96
 
75
- ## Version [3.0.2] <small>2018-04-24</small>
97
+ ## Version [3.0.2] <sub><sup>2018-04-24</sub></sup>
76
98
 
77
99
  ## Fixes
78
100
 
@@ -82,7 +104,7 @@ has been moved to ancestry 4.0
82
104
  * added missing `Ancestry::version`
83
105
  * added Rails 5.2 support (thx @jjuliano)
84
106
 
85
- ## Version [3.0.1] <small>2017-07-05</small>
107
+ ## Version [3.0.1] <sub><sup>2017-07-05</sub></sup>
86
108
 
87
109
  ## Fixes
88
110
 
@@ -93,7 +115,7 @@ has been moved to ancestry 4.0
93
115
  * fixed tests on mysql 5.7 and rails 3.2
94
116
  * Dropped 3.1 scope changes
95
117
 
96
- ## Version [3.0.0] <small>2017-05-18</small>
118
+ ## Version [3.0.0] <sub><sup>2017-05-18</sub></sup>
97
119
 
98
120
  ## Changed
99
121
 
@@ -109,7 +131,7 @@ has been moved to ancestry 4.0
109
131
  * Properly touches parents when different class for STI (thx @samtgarson)
110
132
  * Fixed issues with parent_id (only present on master) (thx @domcleal)
111
133
 
112
- ## Version [2.2.2] <small>2016-11-01</small>
134
+ ## Version [2.2.2] <sub><sup>2016-11-01</sub></sup>
113
135
 
114
136
  ### Changed
115
137
 
@@ -117,7 +139,7 @@ has been moved to ancestry 4.0
117
139
  * Fixed bug with explicit order clauses (introduced in 2.2.0)
118
140
  * No longer load schema on `has_ancestry` load (thx @ledermann)
119
141
 
120
- ## Version [2.2.1] <small>2016-10-25</small>
142
+ ## Version [2.2.1] <sub><sup>2016-10-25</sub></sup>
121
143
 
122
144
  Sorry for blip, local master got out of sync with upstream master.
123
145
  Missed 2 commits (which are feature adds)
@@ -126,7 +148,7 @@ Missed 2 commits (which are feature adds)
126
148
  * Use like (vs ilike) for rails 5.0 (performance enhancement)
127
149
  * Use `COALESCE` for sorting on pg, mysql, and sqlite vs `CASE`
128
150
 
129
- ## Version [2.2.0] <small>2016-10-25</small>
151
+ ## Version [2.2.0] <sub><sup>2016-10-25</sub></sup>
130
152
 
131
153
  ### Added
132
154
  * Predicates for scopes: e.g.: `ancestor_of?`, `parent_of?` (thx @neglectedvalue)
@@ -141,7 +163,7 @@ Missed 2 commits (which are feature adds)
141
163
  * Upgrading tests for ruby versions (thx @brocktimus, @fryguy, @yui-knk)
142
164
  * Fix non-default ancestry not getting used properly (thx @javiyu)
143
165
 
144
- ## Version [2.1.0] <small>2014-04-16</small>
166
+ ## Version [2.1.0] <sub><sup>2014-04-16</sub></sup>
145
167
  * Added arrange_serializable (thx @krishandley, @chicagogrrl)
146
168
  * Add the :touch to update ancestors on save (thx @adammck)
147
169
  * Change conditions into arel (thx @mlitwiniuk)
@@ -150,7 +172,7 @@ Missed 2 commits (which are feature adds)
150
172
  * Performance tweak (thx @mjc)
151
173
  * Improvements to organization (thx @xsuchy, @ryakh)
152
174
 
153
- ## Version [2.0.0] <small>2013-05-17</small>
175
+ ## Version [2.0.0] <sub><sup>2013-05-17</sub></sup>
154
176
  * Removed rails 2 compatibility
155
177
  * Added table name to condition constructing methods (thx @aflatter)
156
178
  * Fix depth_cache not being updated when moving up to ancestors (thx @scottatron)
@@ -162,31 +184,31 @@ Missed 2 commits (which are feature adds)
162
184
  * New adopt strategy (thx unknown)
163
185
  * Many more improvements
164
186
 
165
- ## Version [1.3.0] <small>2012-05-04</small>
187
+ ## Version [1.3.0] <sub><sup>2012-05-04</sub></sup>
166
188
  * Ancestry now ignores default scopes when moving or destroying nodes, ensuring tree consistency
167
189
  * Changed ActiveRecord dependency to 2.3.14
168
190
 
169
- ## Version [1.2.5] <small>2012-03-15</small>
191
+ ## Version [1.2.5] <sub><sup>2012-03-15</sub></sup>
170
192
  * Fixed warnings: "parenthesize argument(s) for future version"
171
193
  * Fixed a bug in the restore_ancestry_integrity! method (thx Arthur Holstvoogd)
172
194
 
173
- ## Version [1.2.4] <small>2011-04-22</small>
195
+ ## Version [1.2.4] <sub><sup>2011-04-22</sub></sup>
174
196
  * Prepended table names to column names in queries (thx @raelik)
175
197
  * Better check to see if acts_as_tree can be overloaded (thx @jims)
176
198
  * Performance inprovements (thx @kueda)
177
199
 
178
- ## Version [1.2.3] <small>2010-10-28</small>
200
+ ## Version [1.2.3] <sub><sup>2010-10-28</sub></sup>
179
201
  * Fixed error with determining ActiveRecord version
180
202
  * Added option to specify :primary_key_format (thx @rolftimmermans)
181
203
 
182
- ## Version [1.2.2] <small>2010-10-24</small>
204
+ ## Version [1.2.2] <sub><sup>2010-10-24</sub></sup>
183
205
  * Fixed all deprecation warnings for rails 3.0.X
184
206
  * Added `:report` option to `check_ancestry_integrity!`
185
207
  * Changed ActiveRecord dependency to 2.2.2
186
208
  * Tested and fixed for ruby 1.8.7 and 1.9.2
187
209
  * Changed usage of `update_attributes` to `update_attribute` to allow ancestry column protection
188
210
 
189
- ## Version [1.2.0] <small>2009-11-07</small>
211
+ ## Version [1.2.0] <sub><sup>2009-11-07</sub></sup>
190
212
  * Removed some duplication in has_ancestry
191
213
  * Cleaned up plugin pattern according to http://yehudakatz.com/2009/11/12/better-ruby-idioms/
192
214
  * Moved parts of ancestry into seperate files
@@ -197,23 +219,23 @@ Missed 2 commits (which are feature adds)
197
219
  * Updated ordered_by_ancestry scope to support Microsoft SQL Server
198
220
  * Added empty hash as parameter to exists? calls for older ActiveRecord versions
199
221
 
200
- ## Version [1.1.4] <small>2009-11-07</small>
222
+ ## Version [1.1.4] <sub><sup>2009-11-07</sub></sup>
201
223
  * Thanks to a patch from tom taylor, Ancestry now works with different primary keys
202
224
 
203
- ## Version [1.1.3] <small>2009-11-01</small>
225
+ ## Version [1.1.3] <sub><sup>2009-11-01</sub></sup>
204
226
  * Fixed a pretty bad bug where several operations took far too many queries
205
227
 
206
- ## Version [1.1.2] <small>2009-10-29</small>
228
+ ## Version [1.1.2] <sub><sup>2009-10-29</sub></sup>
207
229
  * Added validation for depth cache column
208
230
  * Added STI support (reported broken)
209
231
 
210
- ## Version [1.1.1] <small>2009-10-28</small>
232
+ ## Version [1.1.1] <sub><sup>2009-10-28</sub></sup>
211
233
  * Fixed some parentheses warnings that where reported
212
234
  * Fixed a reported issue with arrangement
213
235
  * Fixed issues with ancestors and path order on postgres
214
236
  * Added ordered_by_ancestry scope (needed to fix issues)
215
237
 
216
- ## Version [1.1.0] <small>2009-10-22</small>
238
+ ## Version [1.1.0] <sub><sup>2009-10-22</sub></sup>
217
239
  * Depth caching (and cache rebuilding)
218
240
  * Depth method for nodes
219
241
  * Named scopes for selecting by depth
@@ -236,7 +258,7 @@ Missed 2 commits (which are feature adds)
236
258
  * Removed rails specific init
237
259
  * Removed uninstall script
238
260
 
239
- ## Version 1.0.0 <small>2009-10-16</small>
261
+ ## Version 1.0.0 <sub><sup>2009-10-16</sub></sup>
240
262
  * Initial version
241
263
  * Tree building
242
264
  * Tree navigation
@@ -248,8 +270,12 @@ Missed 2 commits (which are feature adds)
248
270
  * Validations
249
271
 
250
272
 
251
- [HEAD]: https://github.com/stefankroes/ancestry/compare/v3.2.0...HEAD
252
- [3.1.0]: https://github.com/stefankroes/ancestry/compare/v3.1.0...v3.2.0
273
+ [HEAD]: https://github.com/stefankroes/ancestry/compare/v4.2.0...HEAD
274
+ [4.2.0]: https://github.com/stefankroes/ancestry/compare/v4.1.0...v4.2.0
275
+ [4.1.0]: https://github.com/stefankroes/ancestry/compare/v4.0.0...v4.1.0
276
+ [4.0.0]: https://github.com/stefankroes/ancestry/compare/v3.2.1...v4.0.0
277
+ [3.2.1]: https://github.com/stefankroes/ancestry/compare/v3.2.0...v3.2.1
278
+ [3.2.0]: https://github.com/stefankroes/ancestry/compare/v3.1.0...v3.2.0
253
279
  [3.1.0]: https://github.com/stefankroes/ancestry/compare/v3.0.7...v3.1.0
254
280
  [3.0.7]: https://github.com/stefankroes/ancestry/compare/v3.0.6...v3.0.7
255
281
  [3.0.6]: https://github.com/stefankroes/ancestry/compare/v3.0.5...v3.0.6
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Build Status](https://travis-ci.org/stefankroes/ancestry.svg?branch=master)](https://travis-ci.org/stefankroes/ancestry) [![Coverage Status](https://coveralls.io/repos/stefankroes/ancestry/badge.svg)](https://coveralls.io/r/stefankroes/ancestry) [![Gitter](https://badges.gitter.im/Join+Chat.svg)](https://gitter.im/stefankroes/ancestry?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Security](https://hakiri.io/github/stefankroes/ancestry/master.svg)](https://hakiri.io/github/stefankroes/ancestry/master)
1
+ [![Gitter](https://badges.gitter.im/Join+Chat.svg)](https://gitter.im/stefankroes/ancestry?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
2
2
 
3
3
  # Ancestry
4
4
 
@@ -44,6 +44,8 @@ $ bundle install
44
44
 
45
45
  ```bash
46
46
  $ rails g migration add_ancestry_to_[table] ancestry:string:index
47
+ # or use different column name of your choosing. e.g. name:
48
+ # rails g migration add_name_to_[people] name:string:index
47
49
  ```
48
50
 
49
51
  * Migrate your database:
@@ -52,6 +54,20 @@ $ rails g migration add_ancestry_to_[table] ancestry:string:index
52
54
  $ rake db:migrate
53
55
  ```
54
56
 
57
+ Depending upon your comfort with databases, you may want to create the column
58
+ with `C` or `POSIX` encoding. This is a more primitive encoding and just compares
59
+ bytes. Since this column will just contains numbers and slashes, it works much
60
+ better. It also works better for the uuid case as well.
61
+
62
+ Alternatively, if you create a [`text_pattern_ops`](https://www.postgresql.org/docs/current/indexes-opclass.html) index for your postgresql column, subtree selection will use an efficient index for you regardless of whether you created the column with `POSIX` encoding.
63
+
64
+ If you opt out of this, and are trying to run tests on postgres, you may need to
65
+ set the environment variable `COLLATE_SYMBOLS=false`. Sorry to say that a discussion
66
+ on this topic is out of scope. The important take away is postgres sort order is
67
+ not consistent across operating systems but other databases do not have this same
68
+ issue.
69
+
70
+ NOTE: A Btree index (as is recommended) has a limitaton of 2704 characters for the ancestry column. This means you can't have an tree with a depth that is too great (~> 900 items at most).
55
71
 
56
72
  ## Add ancestry to your model
57
73
  * Add to app/models/[model.rb]:
@@ -60,7 +76,8 @@ $ rake db:migrate
60
76
  # app/models/[model.rb]
61
77
 
62
78
  class [Model] < ActiveRecord::Base
63
- has_ancestry
79
+ has_ancestry # or alternatively as below:
80
+ # has_ancestry ancestry_column: :name ## if you've used a different column name
64
81
  end
65
82
  ```
66
83
 
@@ -132,6 +149,9 @@ The has_ancestry method supports the following options:
132
149
  By default, primary keys only match integers ([0-9]+)
133
150
  :touch Instruct Ancestry to touch the ancestors of a node when it changes, to
134
151
  invalidate nested key-based caches. (default: false)
152
+ :counter_cache Boolean whether to create counter cache column accessor.
153
+ Default column name is `children_count`.
154
+ Pass symbol to use different column name (default: false)
135
155
 
136
156
  # (Named) Scopes
137
157
 
@@ -16,7 +16,7 @@ module Ancestry
16
16
  if [:before_depth, :to_depth, :at_depth, :from_depth, :after_depth].include? scope_name
17
17
  scope.send scope_name, depth + relative_depth
18
18
  else
19
- raise Ancestry::AncestryException.new(I18n.t("ancestry.unknown_depth_option", {:scope_name => scope_name}))
19
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.unknown_depth_option", scope_name: scope_name))
20
20
  end
21
21
  end
22
22
  end
@@ -74,6 +74,20 @@ module Ancestry
74
74
  end
75
75
  end
76
76
 
77
+ def tree_view(column, data = nil)
78
+ data = arrange unless data
79
+ data.each do |parent, children|
80
+ if parent.depth == 0
81
+ puts parent[column]
82
+ else
83
+ num = parent.depth - 1
84
+ indent = " "*num
85
+ puts " #{"|" if parent.depth > 1}#{indent}|_ #{parent[column]}"
86
+ end
87
+ tree_view(column, children) if children
88
+ end
89
+ end
90
+
77
91
  # Pseudo-preordered array of nodes. Children will always follow parents,
78
92
  def sort_by_ancestry(nodes, &block)
79
93
  arranged = nodes if nodes.is_a?(Hash)
@@ -217,31 +231,15 @@ module Ancestry
217
231
  end
218
232
 
219
233
  def unscoped_where
220
- if ActiveRecord::VERSION::MAJOR < 4
221
- self.ancestry_base_class.unscoped do
222
- yield self.ancestry_base_class
223
- end
224
- else
225
- yield self.ancestry_base_class.unscope(:where)
226
- end
234
+ yield self.ancestry_base_class.default_scoped.unscope(:where)
227
235
  end
228
236
 
229
237
  ANCESTRY_UNCAST_TYPES = [:string, :uuid, :text].freeze
230
- if ActiveSupport::VERSION::STRING < "4.2"
231
- def primary_key_is_an_integer?
232
- if defined?(@primary_key_is_an_integer)
233
- @primary_key_is_an_integer
234
- else
235
- @primary_key_is_an_integer = !ANCESTRY_UNCAST_TYPES.include?(columns_hash[primary_key.to_s].type)
236
- end
237
- end
238
- else
239
- def primary_key_is_an_integer?
240
- if defined?(@primary_key_is_an_integer)
241
- @primary_key_is_an_integer
242
- else
243
- @primary_key_is_an_integer = !ANCESTRY_UNCAST_TYPES.include?(type_for_attribute(primary_key).type)
244
- end
238
+ def primary_key_is_an_integer?
239
+ if defined?(@primary_key_is_an_integer)
240
+ @primary_key_is_an_integer
241
+ else
242
+ @primary_key_is_an_integer = !ANCESTRY_UNCAST_TYPES.include?(type_for_attribute(primary_key).type)
245
243
  end
246
244
  end
247
245
  end
@@ -4,8 +4,8 @@ module Ancestry
4
4
  # Check options
5
5
  raise Ancestry::AncestryException.new(I18n.t("ancestry.option_must_be_hash")) unless options.is_a? Hash
6
6
  options.each do |key, value|
7
- unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :touch, :counter_cache, :primary_key_format, :update_strategy].include? key
8
- raise Ancestry::AncestryException.new(I18n.t("ancestry.unknown_option", {:key => key.inspect, :value => value.inspect}))
7
+ unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :touch, :counter_cache, :primary_key_format, :update_strategy, :strategy].include? key
8
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.unknown_option", key: key.inspect, value: value.inspect))
9
9
  end
10
10
  end
11
11
 
@@ -27,8 +27,13 @@ module Ancestry
27
27
  # Include dynamic class methods
28
28
  extend Ancestry::ClassMethods
29
29
 
30
- validates_format_of self.ancestry_column, :with => derive_ancestry_pattern(options[:primary_key_format]), :allow_nil => true
31
- extend Ancestry::MaterializedPath
30
+ if options[:strategy] == :materialized_path2
31
+ validates_format_of self.ancestry_column, :with => derive_materialized2_pattern(options[:primary_key_format]), :allow_nil => false
32
+ extend Ancestry::MaterializedPath2
33
+ else
34
+ validates_format_of self.ancestry_column, :with => derive_materialized_pattern(options[:primary_key_format]), :allow_nil => true
35
+ extend Ancestry::MaterializedPath
36
+ end
32
37
 
33
38
  update_strategy = options[:update_strategy] || Ancestry.default_update_strategy
34
39
  include Ancestry::MaterializedPathPg if update_strategy == :sql
@@ -87,12 +92,7 @@ module Ancestry
87
92
 
88
93
  after_touch :touch_ancestors_callback
89
94
  after_destroy :touch_ancestors_callback
90
-
91
- if ActiveRecord::VERSION::STRING >= '5.1.0'
92
- after_save :touch_ancestors_callback, if: :saved_changes?
93
- else
94
- after_save :touch_ancestors_callback, if: :changed?
95
- end
95
+ after_save :touch_ancestors_callback, if: :saved_changes?
96
96
  end
97
97
 
98
98
  def acts_as_tree(*args)
@@ -102,7 +102,7 @@ module Ancestry
102
102
 
103
103
  private
104
104
 
105
- def derive_ancestry_pattern(primary_key_format, delimiter = '/')
105
+ def derive_materialized_pattern(primary_key_format, delimiter = '/')
106
106
  primary_key_format ||= '[0-9]+'
107
107
 
108
108
  if primary_key_format.to_s.include?('\A')
@@ -111,9 +111,19 @@ module Ancestry
111
111
  /\A#{primary_key_format}(#{delimiter}#{primary_key_format})*\Z/
112
112
  end
113
113
  end
114
+
115
+ def derive_materialized2_pattern(primary_key_format, delimiter = '/')
116
+ primary_key_format ||= '[0-9]+'
117
+
118
+ if primary_key_format.to_s.include?('\A')
119
+ primary_key_format
120
+ else
121
+ /\A#{delimiter}(#{primary_key_format}#{delimiter})*\Z/
122
+ end
123
+ end
114
124
  end
115
125
  end
116
126
 
117
127
  ActiveSupport.on_load :active_record do
118
- send :extend, Ancestry::HasAncestry
128
+ extend Ancestry::HasAncestry
119
129
  end
@@ -2,13 +2,13 @@ module Ancestry
2
2
  module InstanceMethods
3
3
  # Validate that the ancestors don't include itself
4
4
  def ancestry_exclude_self
5
- errors.add(:base, I18n.t("ancestry.exclude_self", {:class_name => self.class.name.humanize})) if ancestor_ids.include? self.id
5
+ errors.add(:base, I18n.t("ancestry.exclude_self", class_name: self.class.name.humanize)) if ancestor_ids.include? self.id
6
6
  end
7
7
 
8
8
  # Update descendants with new ancestry (before save)
9
9
  def update_descendants_with_new_ancestry
10
10
  # If enabled and node is existing and ancestry was updated and the new ancestry is sane ...
11
- if !ancestry_callbacks_disabled? && !new_record? && ancestry_changed? && sane_ancestry?
11
+ if !ancestry_callbacks_disabled? && !new_record? && ancestry_changed? && sane_ancestor_ids?
12
12
  # ... for each descendant ...
13
13
  unscoped_descendants.each do |descendant|
14
14
  # ... replace old ancestry with new ancestry
@@ -39,7 +39,7 @@ module Ancestry
39
39
  when :adopt # make child elements of this node, child of its parent
40
40
  descendants.each do |descendant|
41
41
  descendant.without_ancestry_callbacks do
42
- descendant.update_attribute :ancestor_ids, descendant.ancestor_ids.delete_if { |x| x == self.id }
42
+ descendant.update_attribute :ancestor_ids, (descendant.ancestor_ids.delete_if { |x| x == self.id })
43
43
  end
44
44
  end
45
45
  when :restrict # throw an exception if it has children
@@ -62,7 +62,7 @@ module Ancestry
62
62
 
63
63
  # Counter Cache
64
64
  def increase_parent_counter_cache
65
- self.class.increment_counter _counter_cache_column, parent_id
65
+ self.ancestry_base_class.increment_counter _counter_cache_column, parent_id
66
66
  end
67
67
 
68
68
  def decrease_parent_counter_cache
@@ -74,24 +74,19 @@ module Ancestry
74
74
  return if defined?(@_trigger_destroy_callback) && !@_trigger_destroy_callback
75
75
  return if ancestry_callbacks_disabled?
76
76
 
77
- self.class.decrement_counter _counter_cache_column, parent_id
77
+ self.ancestry_base_class.decrement_counter _counter_cache_column, parent_id
78
78
  end
79
79
 
80
80
  def update_parent_counter_cache
81
- changed =
82
- if ActiveRecord::VERSION::STRING >= '5.1.0'
83
- saved_change_to_attribute?(self.ancestry_base_class.ancestry_column)
84
- else
85
- ancestry_changed?
86
- end
81
+ changed = saved_change_to_attribute?(self.ancestry_base_class.ancestry_column)
87
82
 
88
83
  return unless changed
89
84
 
90
85
  if parent_id_was = parent_id_before_last_save
91
- self.class.decrement_counter _counter_cache_column, parent_id_was
86
+ self.ancestry_base_class.decrement_counter _counter_cache_column, parent_id_was
92
87
  end
93
88
 
94
- parent_id && self.class.increment_counter(_counter_cache_column, parent_id)
89
+ parent_id && self.ancestry_base_class.increment_counter(_counter_cache_column, parent_id)
95
90
  end
96
91
 
97
92
  def _counter_cache_column
@@ -100,28 +95,37 @@ module Ancestry
100
95
 
101
96
  # Ancestors
102
97
 
103
- def ancestors?
98
+ def has_parent?
104
99
  ancestor_ids.present?
105
100
  end
106
- alias :has_parent? :ancestors?
101
+ alias :ancestors? :has_parent?
107
102
 
108
103
  def ancestry_changed?
109
104
  column = self.ancestry_base_class.ancestry_column.to_s
110
- if ActiveRecord::VERSION::STRING >= '5.1.0'
111
105
  # These methods return nil if there are no changes.
112
106
  # This was fixed in a refactoring in rails 6.0: https://github.com/rails/rails/pull/35933
113
107
  !!(will_save_change_to_attribute?(column) || saved_change_to_attribute?(column))
114
- else
115
- changed.include?(column)
116
- end
117
108
  end
118
109
 
119
110
  def sane_ancestor_ids?
120
- valid? || errors[self.ancestry_base_class.ancestry_column].blank?
111
+ current_context, self.validation_context = validation_context, nil
112
+ errors.clear
113
+
114
+ attribute = ancestry_base_class.ancestry_column
115
+ ancestry_value = send(attribute)
116
+ return true unless ancestry_value
117
+
118
+ self.class.validators_on(attribute).each do |validator|
119
+ validator.validate_each(self, attribute, ancestry_value)
120
+ end
121
+ ancestry_exclude_self
122
+ errors.none?
123
+ ensure
124
+ self.validation_context = current_context
121
125
  end
122
126
 
123
127
  def ancestors depth_options = {}
124
- return self.ancestry_base_class.none unless ancestors?
128
+ return self.ancestry_base_class.none unless has_parent?
125
129
  self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.ancestors_of(self)
126
130
  end
127
131
 
@@ -162,12 +166,16 @@ module Ancestry
162
166
  end
163
167
 
164
168
  def parent_id
165
- ancestor_ids.last if ancestors?
169
+ ancestor_ids.last if has_parent?
166
170
  end
167
171
  alias :parent_id? :ancestors?
168
172
 
169
173
  def parent
170
- unscoped_find(parent_id) if ancestors?
174
+ if has_parent?
175
+ unscoped_where do |scope|
176
+ scope.find_by scope.primary_key => parent_id
177
+ end
178
+ end
171
179
  end
172
180
 
173
181
  def parent_of?(node)
@@ -177,15 +185,19 @@ module Ancestry
177
185
  # Root
178
186
 
179
187
  def root_id
180
- ancestors? ? ancestor_ids.first : id
188
+ has_parent? ? ancestor_ids.first : id
181
189
  end
182
190
 
183
191
  def root
184
- ancestors? ? unscoped_find(root_id) : self
192
+ if has_parent?
193
+ unscoped_where { |scope| scope.find_by(id: root_id) } || self
194
+ else
195
+ self
196
+ end
185
197
  end
186
198
 
187
199
  def is_root?
188
- !ancestors?
200
+ !has_parent?
189
201
  end
190
202
  alias :root? :is_root?
191
203
 
@@ -285,6 +297,7 @@ module Ancestry
285
297
  def without_ancestry_callbacks
286
298
  @disable_ancestry_callbacks = true
287
299
  yield
300
+ ensure
288
301
  @disable_ancestry_callbacks = false
289
302
  end
290
303
 
@@ -1,8 +1,12 @@
1
1
  module Ancestry
2
+ # store ancestry as grandparent_id/parent_id
3
+ # root a=nil,id=1 children=id,id/% == 1, 1/%
4
+ # 3: a=1/2,id=3 children=a/id,a/id/% == 1/2/3, 1/2/3/%
2
5
  module MaterializedPath
3
- BEFORE_LAST_SAVE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_before_last_save'.freeze : '_was'.freeze
4
- IN_DATABASE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_in_database'.freeze : '_was'.freeze
6
+ BEFORE_LAST_SAVE_SUFFIX = '_before_last_save'.freeze
7
+ IN_DATABASE_SUFFIX = '_in_database'.freeze
5
8
  ANCESTRY_DELIMITER='/'.freeze
9
+ ROOT=nil
6
10
 
7
11
  def self.extended(base)
8
12
  base.send(:include, InstanceMethods)
@@ -13,7 +17,7 @@ module Ancestry
13
17
  end
14
18
 
15
19
  def roots
16
- where(arel_table[ancestry_column].eq(nil))
20
+ where(arel_table[ancestry_column].eq(ROOT))
17
21
  end
18
22
 
19
23
  def ancestors_of(object)
@@ -38,34 +42,25 @@ module Ancestry
38
42
  def indirects_of(object)
39
43
  t = arel_table
40
44
  node = to_node(object)
41
- # rails has case sensitive matching.
42
- if ActiveRecord::VERSION::MAJOR >= 5
43
- where(t[ancestry_column].matches("#{node.child_ancestry}/%", nil, true))
44
- else
45
- where(t[ancestry_column].matches("#{node.child_ancestry}/%"))
46
- end
45
+ where(t[ancestry_column].matches("#{node.child_ancestry}#{ANCESTRY_DELIMITER}%", nil, true))
47
46
  end
48
47
 
49
48
  def descendants_of(object)
50
- where(descendant_conditions(object))
49
+ node = to_node(object)
50
+ indirects_of(node).or(children_of(node))
51
51
  end
52
52
 
53
53
  # deprecated
54
54
  def descendant_conditions(object)
55
55
  t = arel_table
56
56
  node = to_node(object)
57
- # rails has case sensitive matching.
58
- if ActiveRecord::VERSION::MAJOR >= 5
59
- t[ancestry_column].matches("#{node.child_ancestry}/%", nil, true).or(t[ancestry_column].eq(node.child_ancestry))
60
- else
61
- t[ancestry_column].matches("#{node.child_ancestry}/%").or(t[ancestry_column].eq(node.child_ancestry))
62
- end
57
+ t[ancestry_column].matches("#{node.child_ancestry}/%", nil, true).or(t[ancestry_column].eq(node.child_ancestry))
63
58
  end
64
59
 
65
60
  def subtree_of(object)
66
61
  t = arel_table
67
62
  node = to_node(object)
68
- where(descendant_conditions(node).or(t[primary_key].eq(node.id)))
63
+ descendants_of(node).or(where(t[primary_key].eq(node.id)))
69
64
  end
70
65
 
71
66
  def siblings_of(object)
@@ -77,8 +72,8 @@ module Ancestry
77
72
  def ordered_by_ancestry(order = nil)
78
73
  if %w(mysql mysql2 sqlite sqlite3).include?(connection.adapter_name.downcase)
79
74
  reorder(arel_table[ancestry_column], order)
80
- elsif %w(postgresql).include?(connection.adapter_name.downcase) && ActiveRecord::VERSION::STRING >= "6.1"
81
- reorder(Arel::Nodes::Ascending.new(arel_table[ancestry_column]).nulls_first)
75
+ elsif %w(postgresql oracleenhanced).include?(connection.adapter_name.downcase) && ActiveRecord::VERSION::STRING >= "6.1"
76
+ reorder(Arel::Nodes::Ascending.new(arel_table[ancestry_column]).nulls_first, order)
82
77
  else
83
78
  reorder(
84
79
  Arel::Nodes::Ascending.new(Arel::Nodes::NamedFunction.new('COALESCE', [arel_table[ancestry_column], Arel.sql("''")])),
@@ -92,22 +87,15 @@ module Ancestry
92
87
  end
93
88
 
94
89
  module InstanceMethods
95
-
96
- # Validates the ancestry, but can also be applied if validation is bypassed to determine if children should be affected
97
- def sane_ancestry?
98
- ancestry_value = read_attribute(self.ancestry_base_class.ancestry_column)
99
- (ancestry_value.nil? || !ancestor_ids.include?(self.id)) && valid?
100
- end
101
-
102
90
  # optimization - better to go directly to column and avoid parsing
103
91
  def ancestors?
104
- read_attribute(self.ancestry_base_class.ancestry_column).present?
92
+ read_attribute(self.ancestry_base_class.ancestry_column) != ROOT
105
93
  end
106
94
  alias :has_parent? :ancestors?
107
95
 
108
96
  def ancestor_ids=(value)
109
97
  col = self.ancestry_base_class.ancestry_column
110
- value.present? ? write_attribute(col, value.join(ANCESTRY_DELIMITER)) : write_attribute(col, nil)
98
+ value.present? ? write_attribute(col, generate_ancestry(value)) : write_attribute(col, ROOT)
111
99
  end
112
100
 
113
101
  def ancestor_ids
@@ -124,7 +112,7 @@ module Ancestry
124
112
 
125
113
  def parent_id_before_last_save
126
114
  ancestry_was = send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}")
127
- return unless ancestry_was.present?
115
+ return if ancestry_was == ROOT
128
116
 
129
117
  parse_ancestry_column(ancestry_was).last
130
118
  end
@@ -141,16 +129,18 @@ module Ancestry
141
129
  # New records cannot have children
142
130
  raise Ancestry::AncestryException.new(I18n.t("ancestry.no_child_for_new_record")) if new_record?
143
131
  path_was = self.send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}")
144
- path_was.blank? ? id.to_s : "#{path_was}/#{id}"
132
+ path_was.blank? ? id.to_s : "#{path_was}#{ANCESTRY_DELIMITER}#{id}"
145
133
  end
146
134
 
147
- private
148
-
149
- def parse_ancestry_column obj
150
- return [] unless obj
135
+ def parse_ancestry_column(obj)
136
+ return [] if obj == ROOT
151
137
  obj_ids = obj.split(ANCESTRY_DELIMITER)
152
138
  self.class.primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
153
139
  end
140
+
141
+ def generate_ancestry(ancestor_ids)
142
+ ancestor_ids.join(ANCESTRY_DELIMITER)
143
+ end
154
144
  end
155
145
  end
156
146
  end
@@ -0,0 +1,54 @@
1
+ module Ancestry
2
+ # store ancestry as /grandparent_id/parent_id/
3
+ # root: a=/,id=1 children=a.id/% == /1/%
4
+ # 3: a=/1/2/,id=3 children=a.id/% == /1/2/3/%
5
+ module MaterializedPath2 < MaterializedPath
6
+ def indirects_of(object)
7
+ t = arel_table
8
+ node = to_node(object)
9
+ where(t[ancestry_column].matches("#{node.child_ancestry}%#{ANCESTRY_DELIMITER}%", nil, true))
10
+ end
11
+
12
+ def subtree_of(object)
13
+ t = arel_table
14
+ node = to_node(object)
15
+ where(descendant_conditions(node).or(t[primary_key].eq(node.id)))
16
+ end
17
+
18
+ def siblings_of(object)
19
+ t = arel_table
20
+ node = to_node(object)
21
+ where(t[ancestry_column].eq(node[ancestry_column]))
22
+ end
23
+
24
+ def ordered_by_ancestry(order = nil)
25
+ reorder(Arel::Nodes::Ascending.new(arel_table[ancestry_column]), order)
26
+ end
27
+
28
+ # deprecated
29
+ def descendant_conditions(object)
30
+ t = arel_table
31
+ node = to_node(object)
32
+ t[ancestry_column].matches("#{node.child_ancestry}%", nil, true)
33
+ end
34
+
35
+ module InstanceMethods
36
+ def child_ancestry
37
+ # New records cannot have children
38
+ raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?
39
+ path_was = self.send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}")
40
+ "#{path_was}#{id}#{ANCESTRY_DELIMITER}"
41
+ end
42
+
43
+ def parse_ancestry_column(obj)
44
+ return [] if obj == ROOT
45
+ obj_ids = obj.split(ANCESTRY_DELIMITER).delete_if(&:blank?)
46
+ self.class.primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
47
+ end
48
+
49
+ def generate_ancestry(ancestor_ids)
50
+ "#{ANCESTRY_DELIMITER}#{ancestor_ids.join(ANCESTRY_DELIMITER)}#{ANCESTRY_DELIMITER}"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,22 +1,22 @@
1
1
  module Ancestry
2
2
  module MaterializedPathPg
3
- # Update descendants with new ancestry (before save)
4
- def update_descendants_with_new_ancestry
5
- # If enabled and node is existing and ancestry was updated and the new ancestry is sane ...
6
- if !ancestry_callbacks_disabled? && !new_record? && ancestry_changed? && sane_ancestry?
7
- ancestry_column = ancestry_base_class.ancestry_column
8
- old_ancestry = path_ids_in_database.join(Ancestry::MaterializedPath::ANCESTRY_DELIMITER)
9
- new_ancestry = path_ids.join(Ancestry::MaterializedPath::ANCESTRY_DELIMITER)
10
- update_clause = [
11
- "#{ancestry_column} = regexp_replace(#{ancestry_column}, '^#{old_ancestry}', '#{new_ancestry}')"
12
- ]
3
+ # Update descendants with new ancestry (before save)
4
+ def update_descendants_with_new_ancestry
5
+ # If enabled and node is existing and ancestry was updated and the new ancestry is sane ...
6
+ if !ancestry_callbacks_disabled? && !new_record? && ancestry_changed? && sane_ancestor_ids?
7
+ ancestry_column = ancestry_base_class.ancestry_column
8
+ old_ancestry = path_ids_in_database.join(Ancestry::MaterializedPath::ANCESTRY_DELIMITER)
9
+ new_ancestry = path_ids.join(Ancestry::MaterializedPath::ANCESTRY_DELIMITER)
10
+ update_clause = [
11
+ "#{ancestry_column} = regexp_replace(#{ancestry_column}, '^#{old_ancestry}', '#{new_ancestry}')"
12
+ ]
13
13
 
14
- if ancestry_base_class.respond_to?(:depth_cache_column) && respond_to?(ancestry_base_class.depth_cache_column)
15
- depth_cache_column = ancestry_base_class.depth_cache_column.to_s
16
- update_clause << "#{depth_cache_column} = length(regexp_replace(regexp_replace(ancestry, '^#{old_ancestry}', '#{new_ancestry}'), '\\d', '', 'g')) + 1"
17
- end
14
+ if ancestry_base_class.respond_to?(:depth_cache_column) && respond_to?(ancestry_base_class.depth_cache_column)
15
+ depth_cache_column = ancestry_base_class.depth_cache_column.to_s
16
+ update_clause << "#{depth_cache_column} = length(regexp_replace(regexp_replace(ancestry, '^#{old_ancestry}', '#{new_ancestry}'), '\\d', '', 'g')) + 1"
17
+ end
18
18
 
19
- unscoped_descendants.update_all update_clause.join(', ')
19
+ unscoped_descendants.update_all update_clause.join(', ')
20
20
  end
21
21
  end
22
22
  end
@@ -1,3 +1,3 @@
1
1
  module Ancestry
2
- VERSION = "3.2.1"
2
+ VERSION = '4.2.0'
3
3
  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: 3.2.1
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Kroes
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-09-23 00:00:00.000000000 Z
12
+ date: 2022-06-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -17,14 +17,14 @@ dependencies:
17
17
  requirements:
18
18
  - - ">="
19
19
  - !ruby/object:Gem::Version
20
- version: 4.2.0
20
+ version: 5.2.6
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
- version: 4.2.0
27
+ version: 5.2.6
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: appraisal
30
30
  requirement: !ruby/object:Gem::Requirement
@@ -67,6 +67,20 @@ dependencies:
67
67
  - - "~>"
68
68
  - !ruby/object:Gem::Version
69
69
  version: '13.0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: simplecov
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
70
84
  - !ruby/object:Gem::Dependency
71
85
  name: yard
72
86
  requirement: !ruby/object:Gem::Requirement
@@ -104,6 +118,7 @@ files:
104
118
  - lib/ancestry/instance_methods.rb
105
119
  - lib/ancestry/locales/en.yml
106
120
  - lib/ancestry/materialized_path.rb
121
+ - lib/ancestry/materialized_path2.rb
107
122
  - lib/ancestry/materialized_path_pg.rb
108
123
  - lib/ancestry/version.rb
109
124
  homepage: https://github.com/stefankroes/ancestry
@@ -123,15 +138,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
123
138
  requirements:
124
139
  - - ">="
125
140
  - !ruby/object:Gem::Version
126
- version: 2.0.0
141
+ version: '2.5'
127
142
  required_rubygems_version: !ruby/object:Gem::Requirement
128
143
  requirements:
129
144
  - - ">="
130
145
  - !ruby/object:Gem::Version
131
146
  version: '0'
132
147
  requirements: []
133
- rubyforge_project:
134
- rubygems_version: 2.7.6.2
148
+ rubygems_version: 3.3.7
135
149
  signing_key:
136
150
  specification_version: 4
137
151
  summary: Organize ActiveRecord model into a tree structure