ancestry 4.1.0 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 236bf763182abf156d175085f58ec757d196523e4b6f388cfbd4830768194adb
4
- data.tar.gz: b69ba3c642ee2ea03f46c62c23e7f45207d366f57ccfce2e77d0040fe3286a00
3
+ metadata.gz: 82bcd1895093ab9b569806ef05fc307fc2ca9ab53ffeb09199bd66fe3672ecfb
4
+ data.tar.gz: fe7d0d356641658be2953309c27dd28a42e81ed569690cc5e26b2721b1aa6a37
5
5
  SHA512:
6
- metadata.gz: 0a29628a71c79f4d2889620833f91c25266bf491e0925edf8cdf38a74f0ee813f186ffc9dd1beb4d134ea0bcdb091a619f00fafcd557305a797468f4601cd75c
7
- data.tar.gz: 9cbe99bad12c8b2a6a8a6f09b003d5ce17530e668f2dcf17be9502053606d26bf4f9b7ee7493a13a7b4d9fc44df8cd5d450477604fdbfa1aafbd2877193dc894
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 [HEAD] <sub><sup>now</sub></sup>
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.1.0...HEAD
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
- [![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:
@@ -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
- 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
@@ -97,7 +102,7 @@ module Ancestry
97
102
 
98
103
  private
99
104
 
100
- def derive_ancestry_pattern(primary_key_format, delimiter = '/')
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 id: parent_id
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(nil))
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}/%", nil, true))
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
- where(descendant_conditions(object))
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
- where(descendant_conditions(node).or(t[primary_key].eq(node.id)))
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).present?
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.join(ANCESTRY_DELIMITER)) : write_attribute(col, nil)
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 unless ancestry_was.present?
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}/#{id}"
132
+ path_was.blank? ? id.to_s : "#{path_was}#{ANCESTRY_DELIMITER}#{id}"
128
133
  end
129
134
 
130
- private
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
@@ -1,3 +1,3 @@
1
1
  module Ancestry
2
- VERSION = "4.1.0"
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: 4.1.0
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: 2021-06-28 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
@@ -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.2.16
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