ancestry 4.2.0 → 4.3.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 +14 -1
- data/README.md +332 -139
- data/lib/ancestry/array_pattern_validator.rb +27 -0
- data/lib/ancestry/class_methods.rb +21 -15
- data/lib/ancestry/has_ancestry.rb +22 -28
- data/lib/ancestry/instance_methods.rb +13 -7
- data/lib/ancestry/locales/en.yml +1 -0
- data/lib/ancestry/materialized_path.rb +53 -29
- data/lib/ancestry/materialized_path2.rb +36 -27
- data/lib/ancestry/materialized_path_pg.rb +6 -6
- data/lib/ancestry/materialized_path_string.rb +46 -0
- data/lib/ancestry/materialized_path_string2.rb +46 -0
- data/lib/ancestry/version.rb +1 -1
- data/lib/ancestry.rb +38 -1
- metadata +6 -3
@@ -46,8 +46,10 @@ module Ancestry
|
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
|
-
#
|
50
|
-
#
|
49
|
+
# arranges array of nodes to a hierarchical hash
|
50
|
+
#
|
51
|
+
# @param nodes [Array[Node]] nodes to be arranged
|
52
|
+
# @returns Hash{Node => {Node => {}, Node => {}}}
|
51
53
|
# If a node's parent is not included, the node will be included as if it is a top level node
|
52
54
|
def arrange_nodes(nodes)
|
53
55
|
node_ids = Set.new(nodes.map(&:id))
|
@@ -60,6 +62,18 @@ module Ancestry
|
|
60
62
|
end
|
61
63
|
end
|
62
64
|
|
65
|
+
# convert a hash of the form {node => children} to an array of nodes, child first
|
66
|
+
#
|
67
|
+
# @param arranged [Hash{Node => {Node => {}, Node => {}}}] arranged nodes
|
68
|
+
# @returns [Array[Node]] array of nodes with the parent before the children
|
69
|
+
def flatten_arranged_nodes(arranged, nodes = [])
|
70
|
+
arranged.each do |node, children|
|
71
|
+
nodes << node
|
72
|
+
flatten_arranged_nodes(children, nodes) unless children.empty?
|
73
|
+
end
|
74
|
+
nodes
|
75
|
+
end
|
76
|
+
|
63
77
|
# Arrangement to nested array for serialization
|
64
78
|
# You can also supply your own serialization logic using blocks
|
65
79
|
# also allows you to pass the order just as you can pass it to the arrange method
|
@@ -89,29 +103,21 @@ module Ancestry
|
|
89
103
|
end
|
90
104
|
|
91
105
|
# Pseudo-preordered array of nodes. Children will always follow parents,
|
106
|
+
# This is deterministic unless the parents are missing *and* a sort block is specified
|
92
107
|
def sort_by_ancestry(nodes, &block)
|
93
108
|
arranged = nodes if nodes.is_a?(Hash)
|
94
109
|
|
95
110
|
unless arranged
|
96
111
|
presorted_nodes = nodes.sort do |a, b|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
yield a, b
|
101
|
-
else
|
102
|
-
a_cestry <=> b_cestry
|
103
|
-
end
|
112
|
+
rank = (a.ancestry || ' ') <=> (b.ancestry || ' ')
|
113
|
+
rank = yield(a, b) if rank == 0 && block_given?
|
114
|
+
rank
|
104
115
|
end
|
105
116
|
|
106
117
|
arranged = arrange_nodes(presorted_nodes)
|
107
118
|
end
|
108
119
|
|
109
|
-
arranged
|
110
|
-
node, children = pair
|
111
|
-
sorted_nodes << node
|
112
|
-
sorted_nodes += sort_by_ancestry(children, &block) unless children.blank?
|
113
|
-
sorted_nodes
|
114
|
-
end
|
120
|
+
flatten_arranged_nodes(arranged)
|
115
121
|
end
|
116
122
|
|
117
123
|
# Integrity checking
|
@@ -4,15 +4,25 @@ 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, :
|
7
|
+
unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :touch, :counter_cache, :primary_key_format, :update_strategy, :ancestry_format].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
|
11
11
|
|
12
|
+
if options[:ancestry_format].present? && ![:materialized_path, :materialized_path2].include?( options[:ancestry_format] )
|
13
|
+
raise Ancestry::AncestryException.new(I18n.t("ancestry.unknown_format", value: options[:ancestry_format]))
|
14
|
+
end
|
15
|
+
|
12
16
|
# Create ancestry column accessor and set to option or default
|
13
17
|
cattr_accessor :ancestry_column
|
14
18
|
self.ancestry_column = options[:ancestry_column] || :ancestry
|
15
19
|
|
20
|
+
cattr_accessor :ancestry_primary_key_format
|
21
|
+
self.ancestry_primary_key_format = options[:primary_key_format].presence || Ancestry.default_primary_key_format
|
22
|
+
|
23
|
+
cattr_accessor :ancestry_delimiter
|
24
|
+
self.ancestry_delimiter = '/'
|
25
|
+
|
16
26
|
# Save self as base class (for STI)
|
17
27
|
cattr_accessor :ancestry_base_class
|
18
28
|
self.ancestry_base_class = self
|
@@ -27,14 +37,19 @@ module Ancestry
|
|
27
37
|
# Include dynamic class methods
|
28
38
|
extend Ancestry::ClassMethods
|
29
39
|
|
30
|
-
|
31
|
-
|
40
|
+
cattr_accessor :ancestry_format
|
41
|
+
self.ancestry_format = options[:ancestry_format] || Ancestry.default_ancestry_format
|
42
|
+
|
43
|
+
if ancestry_format == :materialized_path2
|
32
44
|
extend Ancestry::MaterializedPath2
|
33
45
|
else
|
34
|
-
validates_format_of self.ancestry_column, :with => derive_materialized_pattern(options[:primary_key_format]), :allow_nil => true
|
35
46
|
extend Ancestry::MaterializedPath
|
36
47
|
end
|
37
48
|
|
49
|
+
attribute self.ancestry_column, default: self.ancestry_root
|
50
|
+
|
51
|
+
validates self.ancestry_column, ancestry_validation_options
|
52
|
+
|
38
53
|
update_strategy = options[:update_strategy] || Ancestry.default_update_strategy
|
39
54
|
include Ancestry::MaterializedPathPg if update_strategy == :sql
|
40
55
|
|
@@ -45,8 +60,8 @@ module Ancestry
|
|
45
60
|
# Validate that the ancestor ids don't include own id
|
46
61
|
validate :ancestry_exclude_self
|
47
62
|
|
48
|
-
# Update descendants with new ancestry
|
49
|
-
|
63
|
+
# Update descendants with new ancestry after update
|
64
|
+
after_update :update_descendants_with_new_ancestry
|
50
65
|
|
51
66
|
# Apply orphan strategy before destroy
|
52
67
|
before_destroy :apply_orphan_strategy
|
@@ -99,31 +114,10 @@ module Ancestry
|
|
99
114
|
return super if defined?(super)
|
100
115
|
has_ancestry(*args)
|
101
116
|
end
|
102
|
-
|
103
|
-
private
|
104
|
-
|
105
|
-
def derive_materialized_pattern(primary_key_format, delimiter = '/')
|
106
|
-
primary_key_format ||= '[0-9]+'
|
107
|
-
|
108
|
-
if primary_key_format.to_s.include?('\A')
|
109
|
-
primary_key_format
|
110
|
-
else
|
111
|
-
/\A#{primary_key_format}(#{delimiter}#{primary_key_format})*\Z/
|
112
|
-
end
|
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
|
124
117
|
end
|
125
118
|
end
|
126
119
|
|
120
|
+
require 'active_support'
|
127
121
|
ActiveSupport.on_load :active_record do
|
128
122
|
extend Ancestry::HasAncestry
|
129
123
|
end
|
@@ -5,15 +5,15 @@ module Ancestry
|
|
5
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
|
-
# Update descendants with new ancestry (
|
8
|
+
# Update descendants with new ancestry (after update)
|
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
11
|
if !ancestry_callbacks_disabled? && !new_record? && ancestry_changed? && sane_ancestor_ids?
|
12
12
|
# ... for each descendant ...
|
13
|
-
|
13
|
+
unscoped_descendants_before_save.each do |descendant|
|
14
14
|
# ... replace old ancestry with new ancestry
|
15
15
|
descendant.without_ancestry_callbacks do
|
16
|
-
new_ancestor_ids = path_ids + (descendant.ancestor_ids -
|
16
|
+
new_ancestor_ids = path_ids + (descendant.ancestor_ids - path_ids_before_last_save)
|
17
17
|
descendant.update_attribute(:ancestor_ids, new_ancestor_ids)
|
18
18
|
end
|
19
19
|
end
|
@@ -133,8 +133,8 @@ module Ancestry
|
|
133
133
|
ancestor_ids + [id]
|
134
134
|
end
|
135
135
|
|
136
|
-
def
|
137
|
-
|
136
|
+
def path_ids_before_last_save
|
137
|
+
ancestor_ids_before_last_save + [id]
|
138
138
|
end
|
139
139
|
|
140
140
|
def path depth_options = {}
|
@@ -190,7 +190,7 @@ module Ancestry
|
|
190
190
|
|
191
191
|
def root
|
192
192
|
if has_parent?
|
193
|
-
unscoped_where { |scope| scope.find_by(
|
193
|
+
unscoped_where { |scope| scope.find_by(scope.primary_key => root_id) } || self
|
194
194
|
else
|
195
195
|
self
|
196
196
|
end
|
@@ -312,10 +312,16 @@ module Ancestry
|
|
312
312
|
end
|
313
313
|
end
|
314
314
|
|
315
|
+
def unscoped_descendants_before_save
|
316
|
+
unscoped_where do |scope|
|
317
|
+
scope.where self.ancestry_base_class.descendant_before_save_conditions(self)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
315
321
|
# works with after save context (hence before_last_save)
|
316
322
|
def unscoped_current_and_previous_ancestors
|
317
323
|
unscoped_where do |scope|
|
318
|
-
scope.where
|
324
|
+
scope.where scope.primary_key => (ancestor_ids + ancestor_ids_before_last_save).uniq
|
319
325
|
end
|
320
326
|
end
|
321
327
|
|
data/lib/ancestry/locales/en.yml
CHANGED
@@ -9,6 +9,7 @@ en:
|
|
9
9
|
|
10
10
|
option_must_be_hash: "Options for has_ancestry must be in a hash."
|
11
11
|
unknown_option: "Unknown option for has_ancestry: %{key} => %{value}."
|
12
|
+
unknown_format: "Unknown ancestry format: %{value}."
|
12
13
|
named_scope_depth_cache: "Named scope '%{scope_name}' is only available when depth caching is enabled."
|
13
14
|
|
14
15
|
exclude_self: "%{class_name} cannot be a descendant of itself."
|
@@ -3,11 +3,6 @@ module Ancestry
|
|
3
3
|
# root a=nil,id=1 children=id,id/% == 1, 1/%
|
4
4
|
# 3: a=1/2,id=3 children=a/id,a/id/% == 1/2/3, 1/2/3/%
|
5
5
|
module MaterializedPath
|
6
|
-
BEFORE_LAST_SAVE_SUFFIX = '_before_last_save'.freeze
|
7
|
-
IN_DATABASE_SUFFIX = '_in_database'.freeze
|
8
|
-
ANCESTRY_DELIMITER='/'.freeze
|
9
|
-
ROOT=nil
|
10
|
-
|
11
6
|
def self.extended(base)
|
12
7
|
base.send(:include, InstanceMethods)
|
13
8
|
end
|
@@ -17,7 +12,7 @@ module Ancestry
|
|
17
12
|
end
|
18
13
|
|
19
14
|
def roots
|
20
|
-
where(arel_table[ancestry_column].eq(
|
15
|
+
where(arel_table[ancestry_column].eq(ancestry_root))
|
21
16
|
end
|
22
17
|
|
23
18
|
def ancestors_of(object)
|
@@ -42,19 +37,26 @@ module Ancestry
|
|
42
37
|
def indirects_of(object)
|
43
38
|
t = arel_table
|
44
39
|
node = to_node(object)
|
45
|
-
where(t[ancestry_column].matches("#{node.child_ancestry}#{
|
40
|
+
where(t[ancestry_column].matches("#{node.child_ancestry}#{ancestry_delimiter}%", nil, true))
|
46
41
|
end
|
47
42
|
|
48
43
|
def descendants_of(object)
|
49
|
-
|
50
|
-
indirects_of(node).or(children_of(node))
|
44
|
+
where(descendant_conditions(object))
|
51
45
|
end
|
52
46
|
|
53
|
-
|
54
|
-
def descendant_conditions(object)
|
47
|
+
def descendants_by_ancestry(ancestry)
|
55
48
|
t = arel_table
|
49
|
+
t[ancestry_column].matches("#{ancestry}#{ancestry_delimiter}%", nil, true).or(t[ancestry_column].eq(ancestry))
|
50
|
+
end
|
51
|
+
|
52
|
+
def descendant_conditions(object)
|
56
53
|
node = to_node(object)
|
57
|
-
|
54
|
+
descendants_by_ancestry( node.child_ancestry )
|
55
|
+
end
|
56
|
+
|
57
|
+
def descendant_before_save_conditions(object)
|
58
|
+
node = to_node(object)
|
59
|
+
descendants_by_ancestry( node.child_ancestry_before_save )
|
58
60
|
end
|
59
61
|
|
60
62
|
def subtree_of(object)
|
@@ -86,35 +88,48 @@ module Ancestry
|
|
86
88
|
ordered_by_ancestry(order)
|
87
89
|
end
|
88
90
|
|
91
|
+
def ancestry_root
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def ancestry_validation_options
|
98
|
+
{
|
99
|
+
format: { with: ancestry_format_regexp },
|
100
|
+
allow_nil: ancestry_nil_allowed?
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
def ancestry_nil_allowed?
|
105
|
+
true
|
106
|
+
end
|
107
|
+
|
108
|
+
def ancestry_format_regexp
|
109
|
+
/\A#{ancestry_primary_key_format}(#{Regexp.escape(ancestry_delimiter)}#{ancestry_primary_key_format})*\z/.freeze
|
110
|
+
end
|
111
|
+
|
89
112
|
module InstanceMethods
|
90
113
|
# optimization - better to go directly to column and avoid parsing
|
91
114
|
def ancestors?
|
92
|
-
read_attribute(self.ancestry_base_class.ancestry_column) !=
|
115
|
+
read_attribute(self.ancestry_base_class.ancestry_column) != self.ancestry_base_class.ancestry_root
|
93
116
|
end
|
94
117
|
alias :has_parent? :ancestors?
|
95
118
|
|
96
119
|
def ancestor_ids=(value)
|
97
|
-
|
98
|
-
value.present? ? write_attribute(col, generate_ancestry(value)) : write_attribute(col, ROOT)
|
120
|
+
write_attribute(self.ancestry_base_class.ancestry_column, generate_ancestry(value))
|
99
121
|
end
|
100
122
|
|
101
123
|
def ancestor_ids
|
102
124
|
parse_ancestry_column(read_attribute(self.ancestry_base_class.ancestry_column))
|
103
125
|
end
|
104
126
|
|
105
|
-
def ancestor_ids_in_database
|
106
|
-
parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}"))
|
107
|
-
end
|
108
|
-
|
109
127
|
def ancestor_ids_before_last_save
|
110
|
-
parse_ancestry_column(
|
128
|
+
parse_ancestry_column(attribute_before_last_save(self.ancestry_base_class.ancestry_column))
|
111
129
|
end
|
112
130
|
|
113
131
|
def parent_id_before_last_save
|
114
|
-
|
115
|
-
return if ancestry_was == ROOT
|
116
|
-
|
117
|
-
parse_ancestry_column(ancestry_was).last
|
132
|
+
parse_ancestry_column(attribute_before_last_save(self.ancestry_base_class.ancestry_column)).last
|
118
133
|
end
|
119
134
|
|
120
135
|
# optimization - better to go directly to column and avoid parsing
|
@@ -128,18 +143,27 @@ module Ancestry
|
|
128
143
|
def child_ancestry
|
129
144
|
# New records cannot have children
|
130
145
|
raise Ancestry::AncestryException.new(I18n.t("ancestry.no_child_for_new_record")) if new_record?
|
131
|
-
|
132
|
-
|
146
|
+
[attribute_in_database(self.ancestry_base_class.ancestry_column), id].compact.join(self.ancestry_base_class.ancestry_delimiter)
|
147
|
+
end
|
148
|
+
|
149
|
+
def child_ancestry_before_save
|
150
|
+
# New records cannot have children
|
151
|
+
raise Ancestry::AncestryException.new(I18n.t("ancestry.no_child_for_new_record")) if new_record?
|
152
|
+
[attribute_before_last_save(self.ancestry_base_class.ancestry_column), id].compact.join(self.ancestry_base_class.ancestry_delimiter)
|
133
153
|
end
|
134
154
|
|
135
155
|
def parse_ancestry_column(obj)
|
136
|
-
return [] if obj ==
|
137
|
-
obj_ids = obj.split(
|
156
|
+
return [] if obj.nil? || obj == self.ancestry_base_class.ancestry_root
|
157
|
+
obj_ids = obj.split(self.ancestry_base_class.ancestry_delimiter).delete_if(&:blank?)
|
138
158
|
self.class.primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
|
139
159
|
end
|
140
160
|
|
141
161
|
def generate_ancestry(ancestor_ids)
|
142
|
-
ancestor_ids.
|
162
|
+
if ancestor_ids.present? && ancestor_ids.any?
|
163
|
+
ancestor_ids.join(self.ancestry_base_class.ancestry_delimiter)
|
164
|
+
else
|
165
|
+
self.ancestry_base_class.ancestry_root
|
166
|
+
end
|
143
167
|
end
|
144
168
|
end
|
145
169
|
end
|
@@ -1,53 +1,62 @@
|
|
1
1
|
module Ancestry
|
2
2
|
# store ancestry as /grandparent_id/parent_id/
|
3
|
-
# root: a=/,id=1 children
|
4
|
-
# 3: a=/1/2/,id=3 children
|
5
|
-
module MaterializedPath2
|
6
|
-
|
7
|
-
t = arel_table
|
8
|
-
node = to_node(object)
|
9
|
-
where(t[ancestry_column].matches("#{node.child_ancestry}%#{ANCESTRY_DELIMITER}%", nil, true))
|
10
|
-
end
|
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
|
6
|
+
include MaterializedPath
|
11
7
|
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
where(descendant_conditions(node).or(t[primary_key].eq(node.id)))
|
8
|
+
def self.extended(base)
|
9
|
+
base.send(:include, MaterializedPath::InstanceMethods)
|
10
|
+
base.send(:include, InstanceMethods)
|
16
11
|
end
|
17
12
|
|
18
|
-
def
|
13
|
+
def indirects_of(object)
|
19
14
|
t = arel_table
|
20
15
|
node = to_node(object)
|
21
|
-
where(t[ancestry_column].
|
16
|
+
where(t[ancestry_column].matches("#{node.child_ancestry}%#{ancestry_delimiter}%", nil, true))
|
22
17
|
end
|
23
18
|
|
24
19
|
def ordered_by_ancestry(order = nil)
|
25
20
|
reorder(Arel::Nodes::Ascending.new(arel_table[ancestry_column]), order)
|
26
21
|
end
|
27
22
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
23
|
+
def descendants_by_ancestry(ancestry)
|
24
|
+
arel_table[ancestry_column].matches("#{ancestry}%", nil, true)
|
25
|
+
end
|
26
|
+
|
27
|
+
def ancestry_root
|
28
|
+
ancestry_delimiter
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def ancestry_nil_allowed?
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
def ancestry_format_regexp
|
38
|
+
/\A#{Regexp.escape(ancestry_delimiter)}(#{ancestry_primary_key_format}#{Regexp.escape(ancestry_delimiter)})*\z/.freeze
|
33
39
|
end
|
34
40
|
|
35
41
|
module InstanceMethods
|
36
42
|
def child_ancestry
|
37
43
|
# New records cannot have children
|
38
|
-
raise Ancestry::AncestryException.new(
|
39
|
-
|
40
|
-
"#{path_was}#{id}#{ANCESTRY_DELIMITER}"
|
44
|
+
raise Ancestry::AncestryException.new(I18n.t("ancestry.no_child_for_new_record")) if new_record?
|
45
|
+
"#{attribute_in_database(self.ancestry_base_class.ancestry_column)}#{id}#{self.ancestry_base_class.ancestry_delimiter}"
|
41
46
|
end
|
42
47
|
|
43
|
-
def
|
44
|
-
|
45
|
-
|
46
|
-
self.
|
48
|
+
def child_ancestry_before_save
|
49
|
+
# New records cannot have children
|
50
|
+
raise Ancestry::AncestryException.new(I18n.t("ancestry.no_child_for_new_record")) if new_record?
|
51
|
+
"#{attribute_before_last_save(self.ancestry_base_class.ancestry_column)}#{id}#{self.ancestry_base_class.ancestry_delimiter}"
|
47
52
|
end
|
48
53
|
|
49
54
|
def generate_ancestry(ancestor_ids)
|
50
|
-
|
55
|
+
if ancestor_ids.present? && ancestor_ids.any?
|
56
|
+
"#{self.ancestry_base_class.ancestry_delimiter}#{ancestor_ids.join(self.ancestry_base_class.ancestry_delimiter)}#{self.ancestry_base_class.ancestry_delimiter}"
|
57
|
+
else
|
58
|
+
self.ancestry_base_class.ancestry_root
|
59
|
+
end
|
51
60
|
end
|
52
61
|
end
|
53
62
|
end
|
@@ -1,22 +1,22 @@
|
|
1
1
|
module Ancestry
|
2
2
|
module MaterializedPathPg
|
3
|
-
# Update descendants with new ancestry (
|
3
|
+
# Update descendants with new ancestry (after update)
|
4
4
|
def update_descendants_with_new_ancestry
|
5
5
|
# If enabled and node is existing and ancestry was updated and the new ancestry is sane ...
|
6
6
|
if !ancestry_callbacks_disabled? && !new_record? && ancestry_changed? && sane_ancestor_ids?
|
7
7
|
ancestry_column = ancestry_base_class.ancestry_column
|
8
|
-
old_ancestry =
|
9
|
-
new_ancestry = path_ids
|
8
|
+
old_ancestry = generate_ancestry( path_ids_before_last_save )
|
9
|
+
new_ancestry = generate_ancestry( path_ids )
|
10
10
|
update_clause = [
|
11
|
-
"#{ancestry_column} = regexp_replace(#{ancestry_column}, '^#{old_ancestry}', '#{new_ancestry}')"
|
11
|
+
"#{ancestry_column} = regexp_replace(#{ancestry_column}, '^#{Regexp.escape(old_ancestry)}', '#{new_ancestry}')"
|
12
12
|
]
|
13
13
|
|
14
14
|
if ancestry_base_class.respond_to?(:depth_cache_column) && respond_to?(ancestry_base_class.depth_cache_column)
|
15
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}'), '
|
16
|
+
update_clause << "#{depth_cache_column} = length(regexp_replace(regexp_replace(ancestry, '^#{Regexp.escape(old_ancestry)}', '#{new_ancestry}'), '[^#{ancestry_base_class.ancestry_delimiter}]', '', 'g')) #{ancestry_base_class.ancestry_format == :materialized_path2 ? '-' : '+'} 1"
|
17
17
|
end
|
18
18
|
|
19
|
-
|
19
|
+
unscoped_descendants_before_save.update_all update_clause.join(', ')
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Ancestry
|
2
|
+
class MaterializedPathString < ActiveRecord::Type::Value
|
3
|
+
def initialize(casting: :to_i, delimiter: '/')
|
4
|
+
@casting = casting&.to_proc
|
5
|
+
@delimiter = delimiter
|
6
|
+
end
|
7
|
+
|
8
|
+
def type
|
9
|
+
:materialized_path_string
|
10
|
+
end
|
11
|
+
|
12
|
+
# convert to database type
|
13
|
+
def serialize(value)
|
14
|
+
if value.kind_of?(Array)
|
15
|
+
value.map(&:to_s).join(@delimiter).presence
|
16
|
+
elsif value.kind_of?(Integer)
|
17
|
+
value.to_s
|
18
|
+
elsif value.nil? || value.kind_of?(String)
|
19
|
+
value
|
20
|
+
else
|
21
|
+
byebug
|
22
|
+
puts "curious type: #{value.class}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def cast(value)
|
27
|
+
cast_value(value) #unless value.nil? (want to get rid of this - fix default value)
|
28
|
+
end
|
29
|
+
|
30
|
+
# called by cast (form or setter) or deserialize (database)
|
31
|
+
def cast_value(value)
|
32
|
+
if value.kind_of?(Array)
|
33
|
+
super
|
34
|
+
elsif value.nil?
|
35
|
+
# would prefer to use default here
|
36
|
+
# but with default, it kept thinking the field had changed when it hadn't
|
37
|
+
# (that may be a rails bug though)
|
38
|
+
super([])
|
39
|
+
else
|
40
|
+
#TODO: test ancestry=1
|
41
|
+
super(value.to_s.split(@delimiter).map(&@casting))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
ActiveRecord::Type.register(:materialized_path_string, Ancestry::MaterializedPathString)
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# used for materialized path
|
2
|
+
class MaterializedPathString2 < ActiveRecord::Type::Value
|
3
|
+
def initialize(casting: :to_i, delimiter: '/')
|
4
|
+
@casting = casting&.to_proc
|
5
|
+
@delimiter = delimiter
|
6
|
+
end
|
7
|
+
|
8
|
+
def type
|
9
|
+
:materialized_path_string2
|
10
|
+
end
|
11
|
+
|
12
|
+
# convert to database type
|
13
|
+
def serialize(value)
|
14
|
+
if value.kind_of?(Array)
|
15
|
+
# TODO: check all values to ensure no extra slashes
|
16
|
+
value.empty? ? @delimiter : "#{@delimiter}#{value.map(&:to_s).join(@delimiter)}#{@delimiter}"
|
17
|
+
elsif value.kind_of?(Integer)
|
18
|
+
value.to_s
|
19
|
+
elsif value.nil? || value.kind_of?(String)
|
20
|
+
value
|
21
|
+
else
|
22
|
+
byebug
|
23
|
+
puts "curious type: #{value.class}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def cast(value)
|
28
|
+
cast_value(value) #unless value.nil? (want to get rid of this - fix default value)
|
29
|
+
end
|
30
|
+
|
31
|
+
# called by cast (form or setter) or deserialize (database)
|
32
|
+
def cast_value(value)
|
33
|
+
if value.kind_of?(Array)
|
34
|
+
super
|
35
|
+
elsif value.nil?
|
36
|
+
# would prefer to use default here
|
37
|
+
# but with default, it kept thinking the field had changed when it hadn't
|
38
|
+
super([])
|
39
|
+
else
|
40
|
+
#TODO: test ancestry=1
|
41
|
+
super(value.to_s.split(@delimiter).map(&@casting))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
ActiveRecord::Type.register(:materialized_path_string2, Ancestry::MaterializedPathString)
|
data/lib/ancestry/version.rb
CHANGED
data/lib/ancestry.rb
CHANGED
@@ -4,6 +4,7 @@ require_relative 'ancestry/instance_methods'
|
|
4
4
|
require_relative 'ancestry/exceptions'
|
5
5
|
require_relative 'ancestry/has_ancestry'
|
6
6
|
require_relative 'ancestry/materialized_path'
|
7
|
+
require_relative 'ancestry/materialized_path2'
|
7
8
|
require_relative 'ancestry/materialized_path_pg'
|
8
9
|
|
9
10
|
I18n.load_path += Dir[File.join(File.expand_path(File.dirname(__FILE__)),
|
@@ -11,6 +12,8 @@ I18n.load_path += Dir[File.join(File.expand_path(File.dirname(__FILE__)),
|
|
11
12
|
|
12
13
|
module Ancestry
|
13
14
|
@@default_update_strategy = :ruby
|
15
|
+
@@default_ancestry_format = :materialized_path
|
16
|
+
@@default_primary_key_format = '[0-9]+'
|
14
17
|
|
15
18
|
# @!default_update_strategy
|
16
19
|
# @return [Symbol] the default strategy for updating ancestry
|
@@ -26,7 +29,6 @@ module Ancestry
|
|
26
29
|
#
|
27
30
|
# Child records are updated in sql and callbacks will not get called.
|
28
31
|
# Associated records in memory will have the wrong ancestry value
|
29
|
-
|
30
32
|
def self.default_update_strategy
|
31
33
|
@@default_update_strategy
|
32
34
|
end
|
@@ -34,4 +36,39 @@ module Ancestry
|
|
34
36
|
def self.default_update_strategy=(value)
|
35
37
|
@@default_update_strategy = value
|
36
38
|
end
|
39
|
+
|
40
|
+
# @!default_ancestry_format
|
41
|
+
# @return [Symbol] the default strategy for updating ancestry
|
42
|
+
#
|
43
|
+
# The value changes the default way that ancestry is stored in the database
|
44
|
+
#
|
45
|
+
# :materialized_path (default and legacy)
|
46
|
+
#
|
47
|
+
# Ancestry is of the form null (for no ancestors) and 1/2/ for children
|
48
|
+
#
|
49
|
+
# :materialized_path2 (preferred)
|
50
|
+
#
|
51
|
+
# Ancestry is of the form '/' (for no ancestors) and '/1/2/' for children
|
52
|
+
def self.default_ancestry_format
|
53
|
+
@@default_ancestry_format
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.default_ancestry_format=(value)
|
57
|
+
@@default_ancestry_format = value
|
58
|
+
end
|
59
|
+
|
60
|
+
# @!default_primary_key_format
|
61
|
+
# @return [Symbol] the regular expression representing the primary key
|
62
|
+
#
|
63
|
+
# The value represents the way the id looks for validation
|
64
|
+
#
|
65
|
+
# '[0-9]+' (default) for integer ids
|
66
|
+
# '[-A-Fa-f0-9]{36}' for uuids (though you can find other regular expressions)
|
67
|
+
def self.default_primary_key_format
|
68
|
+
@@default_primary_key_format
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.default_primary_key_format=(value)
|
72
|
+
@@default_primary_key_format = value
|
73
|
+
end
|
37
74
|
end
|