ancestry 4.1.0 → 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 +4 -4
- data/CHANGELOG.md +10 -2
- data/README.md +8 -2
- data/lib/ancestry/class_methods.rb +14 -0
- data/lib/ancestry/has_ancestry.rb +19 -4
- data/lib/ancestry/instance_methods.rb +2 -1
- data/lib/ancestry/materialized_path.rb +19 -12
- data/lib/ancestry/materialized_path2.rb +54 -0
- data/lib/ancestry/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 82bcd1895093ab9b569806ef05fc307fc2ca9ab53ffeb09199bd66fe3672ecfb
|
4
|
+
data.tar.gz: fe7d0d356641658be2953309c27dd28a42e81ed569690cc5e26b2721b1aa6a37
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f33384a1114d865662be0133c532249be0e7ea438a66e469947b3aaba3172378ae50309e02c4f219c1f161281a9e060bff6087a496850c26351fd83348627e79
|
7
|
+
data.tar.gz: 2a6cb3fb28f6c9a8228b522c590c2125c4e31056a97ac6a6cda96ea4152725fe89e375b59c894091ebcd3d5be36ad18b348c6691211bc6bea2b0a49502c9f423
|
data/CHANGELOG.md
CHANGED
@@ -3,7 +3,14 @@
|
|
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 [
|
6
|
+
## Version [4.2.0] <sub><sup>2022-06-09</sub></sup>
|
7
|
+
|
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)
|
7
14
|
|
8
15
|
## Version [4.1.0] <sub><sup>2021-06-25</sub></sup>
|
9
16
|
|
@@ -263,7 +270,8 @@ Missed 2 commits (which are feature adds)
|
|
263
270
|
* Validations
|
264
271
|
|
265
272
|
|
266
|
-
[HEAD]: https://github.com/stefankroes/ancestry/compare/v4.
|
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
|
267
275
|
[4.1.0]: https://github.com/stefankroes/ancestry/compare/v4.0.0...v4.1.0
|
268
276
|
[4.0.0]: https://github.com/stefankroes/ancestry/compare/v3.2.1...v4.0.0
|
269
277
|
[3.2.1]: https://github.com/stefankroes/ancestry/compare/v3.2.0...v3.2.1
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
[](https://gitter.im/stefankroes/ancestry?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
1
|
+
[](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:
|
@@ -57,6 +59,7 @@ with `C` or `POSIX` encoding. This is a more primitive encoding and just compare
|
|
57
59
|
bytes. Since this column will just contains numbers and slashes, it works much
|
58
60
|
better. It also works better for the uuid case as well.
|
59
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.
|
60
63
|
|
61
64
|
If you opt out of this, and are trying to run tests on postgres, you may need to
|
62
65
|
set the environment variable `COLLATE_SYMBOLS=false`. Sorry to say that a discussion
|
@@ -64,6 +67,8 @@ on this topic is out of scope. The important take away is postgres sort order is
|
|
64
67
|
not consistent across operating systems but other databases do not have this same
|
65
68
|
issue.
|
66
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).
|
71
|
+
|
67
72
|
## Add ancestry to your model
|
68
73
|
* Add to app/models/[model.rb]:
|
69
74
|
|
@@ -71,7 +76,8 @@ issue.
|
|
71
76
|
# app/models/[model.rb]
|
72
77
|
|
73
78
|
class [Model] < ActiveRecord::Base
|
74
|
-
has_ancestry
|
79
|
+
has_ancestry # or alternatively as below:
|
80
|
+
# has_ancestry ancestry_column: :name ## if you've used a different column name
|
75
81
|
end
|
76
82
|
```
|
77
83
|
|
@@ -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)
|
@@ -4,7 +4,7 @@ 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
|
7
|
+
unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :touch, :counter_cache, :primary_key_format, :update_strategy, :strategy].include? key
|
8
8
|
raise Ancestry::AncestryException.new(I18n.t("ancestry.unknown_option", key: key.inspect, value: value.inspect))
|
9
9
|
end
|
10
10
|
end
|
@@ -27,8 +27,13 @@ module Ancestry
|
|
27
27
|
# Include dynamic class methods
|
28
28
|
extend Ancestry::ClassMethods
|
29
29
|
|
30
|
-
|
31
|
-
|
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
|
@@ -97,7 +102,7 @@ module Ancestry
|
|
97
102
|
|
98
103
|
private
|
99
104
|
|
100
|
-
def
|
105
|
+
def derive_materialized_pattern(primary_key_format, delimiter = '/')
|
101
106
|
primary_key_format ||= '[0-9]+'
|
102
107
|
|
103
108
|
if primary_key_format.to_s.include?('\A')
|
@@ -106,6 +111,16 @@ module Ancestry
|
|
106
111
|
/\A#{primary_key_format}(#{delimiter}#{primary_key_format})*\Z/
|
107
112
|
end
|
108
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
|
109
124
|
end
|
110
125
|
end
|
111
126
|
|
@@ -173,7 +173,7 @@ module Ancestry
|
|
173
173
|
def parent
|
174
174
|
if has_parent?
|
175
175
|
unscoped_where do |scope|
|
176
|
-
scope.find_by
|
176
|
+
scope.find_by scope.primary_key => parent_id
|
177
177
|
end
|
178
178
|
end
|
179
179
|
end
|
@@ -297,6 +297,7 @@ module Ancestry
|
|
297
297
|
def without_ancestry_callbacks
|
298
298
|
@disable_ancestry_callbacks = true
|
299
299
|
yield
|
300
|
+
ensure
|
300
301
|
@disable_ancestry_callbacks = false
|
301
302
|
end
|
302
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
6
|
BEFORE_LAST_SAVE_SUFFIX = '_before_last_save'.freeze
|
4
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(
|
20
|
+
where(arel_table[ancestry_column].eq(ROOT))
|
17
21
|
end
|
18
22
|
|
19
23
|
def ancestors_of(object)
|
@@ -38,11 +42,12 @@ module Ancestry
|
|
38
42
|
def indirects_of(object)
|
39
43
|
t = arel_table
|
40
44
|
node = to_node(object)
|
41
|
-
where(t[ancestry_column].matches("#{node.child_ancestry}
|
45
|
+
where(t[ancestry_column].matches("#{node.child_ancestry}#{ANCESTRY_DELIMITER}%", nil, true))
|
42
46
|
end
|
43
47
|
|
44
48
|
def descendants_of(object)
|
45
|
-
|
49
|
+
node = to_node(object)
|
50
|
+
indirects_of(node).or(children_of(node))
|
46
51
|
end
|
47
52
|
|
48
53
|
# deprecated
|
@@ -55,7 +60,7 @@ module Ancestry
|
|
55
60
|
def subtree_of(object)
|
56
61
|
t = arel_table
|
57
62
|
node = to_node(object)
|
58
|
-
|
63
|
+
descendants_of(node).or(where(t[primary_key].eq(node.id)))
|
59
64
|
end
|
60
65
|
|
61
66
|
def siblings_of(object)
|
@@ -84,13 +89,13 @@ module Ancestry
|
|
84
89
|
module InstanceMethods
|
85
90
|
# optimization - better to go directly to column and avoid parsing
|
86
91
|
def ancestors?
|
87
|
-
read_attribute(self.ancestry_base_class.ancestry_column)
|
92
|
+
read_attribute(self.ancestry_base_class.ancestry_column) != ROOT
|
88
93
|
end
|
89
94
|
alias :has_parent? :ancestors?
|
90
95
|
|
91
96
|
def ancestor_ids=(value)
|
92
97
|
col = self.ancestry_base_class.ancestry_column
|
93
|
-
value.present? ? write_attribute(col, value
|
98
|
+
value.present? ? write_attribute(col, generate_ancestry(value)) : write_attribute(col, ROOT)
|
94
99
|
end
|
95
100
|
|
96
101
|
def ancestor_ids
|
@@ -107,7 +112,7 @@ module Ancestry
|
|
107
112
|
|
108
113
|
def parent_id_before_last_save
|
109
114
|
ancestry_was = send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}")
|
110
|
-
return
|
115
|
+
return if ancestry_was == ROOT
|
111
116
|
|
112
117
|
parse_ancestry_column(ancestry_was).last
|
113
118
|
end
|
@@ -124,16 +129,18 @@ module Ancestry
|
|
124
129
|
# New records cannot have children
|
125
130
|
raise Ancestry::AncestryException.new(I18n.t("ancestry.no_child_for_new_record")) if new_record?
|
126
131
|
path_was = self.send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}")
|
127
|
-
path_was.blank? ? id.to_s : "#{path_was}
|
132
|
+
path_was.blank? ? id.to_s : "#{path_was}#{ANCESTRY_DELIMITER}#{id}"
|
128
133
|
end
|
129
134
|
|
130
|
-
|
131
|
-
|
132
|
-
def parse_ancestry_column obj
|
133
|
-
return [] unless obj
|
135
|
+
def parse_ancestry_column(obj)
|
136
|
+
return [] if obj == ROOT
|
134
137
|
obj_ids = obj.split(ANCESTRY_DELIMITER)
|
135
138
|
self.class.primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
|
136
139
|
end
|
140
|
+
|
141
|
+
def generate_ancestry(ancestor_ids)
|
142
|
+
ancestor_ids.join(ANCESTRY_DELIMITER)
|
143
|
+
end
|
137
144
|
end
|
138
145
|
end
|
139
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
|
data/lib/ancestry/version.rb
CHANGED
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: 4.
|
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:
|
12
|
+
date: 2022-06-10 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -118,6 +118,7 @@ files:
|
|
118
118
|
- lib/ancestry/instance_methods.rb
|
119
119
|
- lib/ancestry/locales/en.yml
|
120
120
|
- lib/ancestry/materialized_path.rb
|
121
|
+
- lib/ancestry/materialized_path2.rb
|
121
122
|
- lib/ancestry/materialized_path_pg.rb
|
122
123
|
- lib/ancestry/version.rb
|
123
124
|
homepage: https://github.com/stefankroes/ancestry
|
@@ -144,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
144
145
|
- !ruby/object:Gem::Version
|
145
146
|
version: '0'
|
146
147
|
requirements: []
|
147
|
-
rubygems_version: 3.
|
148
|
+
rubygems_version: 3.3.7
|
148
149
|
signing_key:
|
149
150
|
specification_version: 4
|
150
151
|
summary: Organize ActiveRecord model into a tree structure
|