ancestry 4.2.0 → 4.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|