ancestry 4.3.3 → 5.1.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 +161 -65
- data/README.md +56 -40
- data/lib/ancestry/class_methods.rb +105 -69
- data/lib/ancestry/exceptions.rb +3 -1
- data/lib/ancestry/has_ancestry.rb +82 -49
- data/lib/ancestry/instance_methods.rb +129 -87
- data/lib/ancestry/locales/en.yml +0 -1
- data/lib/ancestry/materialized_path.rb +117 -54
- data/lib/ancestry/materialized_path2.rb +66 -21
- data/lib/ancestry/materialized_path_pg.rb +32 -13
- data/lib/ancestry/version.rb +3 -1
- data/lib/ancestry.rb +3 -1
- metadata +18 -5
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Ancestry
|
|
2
4
|
# store ancestry as grandparent_id/parent_id
|
|
3
5
|
# root a=nil,id=1 children=id,id/% == 1, 1/%
|
|
@@ -28,16 +30,13 @@ module Ancestry
|
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
def children_of(object)
|
|
31
|
-
t = arel_table
|
|
32
33
|
node = to_node(object)
|
|
33
|
-
where(
|
|
34
|
+
where(arel_table[ancestry_column].eq(node.child_ancestry))
|
|
34
35
|
end
|
|
35
36
|
|
|
36
|
-
# indirect = anyone who is a descendant, but not a child
|
|
37
37
|
def indirects_of(object)
|
|
38
|
-
t = arel_table
|
|
39
38
|
node = to_node(object)
|
|
40
|
-
where(
|
|
39
|
+
where(MaterializedPath.indirects_condition(arel_table[ancestry_column], node.child_ancestry, ancestry_delimiter))
|
|
41
40
|
end
|
|
42
41
|
|
|
43
42
|
def descendants_of(object)
|
|
@@ -45,30 +44,27 @@ module Ancestry
|
|
|
45
44
|
end
|
|
46
45
|
|
|
47
46
|
def descendants_by_ancestry(ancestry)
|
|
48
|
-
|
|
49
|
-
t[ancestry_column].matches("#{ancestry}#{ancestry_delimiter}%", nil, true).or(t[ancestry_column].eq(ancestry))
|
|
47
|
+
MaterializedPath.descendants_condition(arel_table[ancestry_column], ancestry, ancestry_delimiter)
|
|
50
48
|
end
|
|
51
49
|
|
|
52
50
|
def descendant_conditions(object)
|
|
53
51
|
node = to_node(object)
|
|
54
|
-
descendants_by_ancestry(
|
|
52
|
+
descendants_by_ancestry(node.child_ancestry)
|
|
55
53
|
end
|
|
56
54
|
|
|
57
|
-
def
|
|
55
|
+
def descendant_before_last_save_conditions(object)
|
|
58
56
|
node = to_node(object)
|
|
59
|
-
descendants_by_ancestry(
|
|
57
|
+
descendants_by_ancestry(node.child_ancestry_before_last_save)
|
|
60
58
|
end
|
|
61
59
|
|
|
62
60
|
def subtree_of(object)
|
|
63
|
-
t = arel_table
|
|
64
61
|
node = to_node(object)
|
|
65
|
-
descendants_of(node).or(where(
|
|
62
|
+
descendants_of(node).or(where(arel_table[primary_key].eq(node.id)))
|
|
66
63
|
end
|
|
67
64
|
|
|
68
65
|
def siblings_of(object)
|
|
69
|
-
t = arel_table
|
|
70
66
|
node = to_node(object)
|
|
71
|
-
where(
|
|
67
|
+
where(arel_table[ancestry_column].eq(node[ancestry_column].presence))
|
|
72
68
|
end
|
|
73
69
|
|
|
74
70
|
def ordered_by_ancestry(order = nil)
|
|
@@ -92,86 +88,153 @@ module Ancestry
|
|
|
92
88
|
nil
|
|
93
89
|
end
|
|
94
90
|
|
|
95
|
-
|
|
91
|
+
def child_ancestry_sql
|
|
92
|
+
MaterializedPath.child_ancestry_sql(table_name, ancestry_column, primary_key, ancestry_delimiter, connection.adapter_name.downcase)
|
|
93
|
+
end
|
|
96
94
|
|
|
97
|
-
def
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
def ancestry_depth_sql
|
|
96
|
+
@ancestry_depth_sql ||= MaterializedPath.construct_depth_sql(table_name, ancestry_column, ancestry_delimiter)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def generate_ancestry(ancestor_ids)
|
|
100
|
+
MaterializedPath.generate(ancestor_ids, ancestry_delimiter, ancestry_root)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def parse_ancestry_column(obj)
|
|
104
|
+
MaterializedPath.parse(obj, ancestry_root, ancestry_delimiter, primary_key_is_an_integer?)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def ancestry_depth_change(old_value, new_value)
|
|
108
|
+
parse_ancestry_column(new_value).size - parse_ancestry_column(old_value).size
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.generate(ancestor_ids, delimiter, root)
|
|
112
|
+
if ancestor_ids.present? && ancestor_ids.any?
|
|
113
|
+
ancestor_ids.join(delimiter)
|
|
114
|
+
else
|
|
115
|
+
root
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def self.parse(obj, root, delimiter, integer_pk)
|
|
120
|
+
return [] if obj.nil? || obj == root
|
|
121
|
+
|
|
122
|
+
obj_ids = obj.split(delimiter).delete_if(&:blank?)
|
|
123
|
+
integer_pk ? obj_ids.map!(&:to_i) : obj_ids
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.child_ancestry_value(ancestry_value, id, delimiter)
|
|
127
|
+
[ancestry_value, id].compact.join(delimiter)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Arel condition: descendants have ancestry matching child_ancestry or starting with child_ancestry/
|
|
131
|
+
def self.descendants_condition(attr, child_ancestry, delimiter)
|
|
132
|
+
attr.matches("#{child_ancestry}#{delimiter}%", nil, true).or(attr.eq(child_ancestry))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Arel condition: indirects have ancestry matching child_ancestry/*/
|
|
136
|
+
def self.indirects_condition(attr, child_ancestry, delimiter)
|
|
137
|
+
attr.matches("#{child_ancestry}#{delimiter}%", nil, true)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def concat(*args)
|
|
141
|
+
MaterializedPath.concat(connection.adapter_name.downcase, *args)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.concat(adapter, *args)
|
|
145
|
+
if %w(sqlite sqlite3).include?(adapter)
|
|
146
|
+
args.join('||')
|
|
147
|
+
else
|
|
148
|
+
%{CONCAT(#{args.join(', ')})}
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def self.child_ancestry_sql(table_name, ancestry_column, primary_key, delimiter, adapter)
|
|
153
|
+
pk_sql = concat(adapter, "#{table_name}.#{primary_key}")
|
|
154
|
+
full_sql = concat(adapter, "#{table_name}.#{ancestry_column}", "'#{delimiter}'", "#{table_name}.#{primary_key}")
|
|
155
|
+
%{
|
|
156
|
+
CASE WHEN #{table_name}.#{ancestry_column} IS NULL THEN #{pk_sql}
|
|
157
|
+
ELSE #{full_sql}
|
|
158
|
+
END
|
|
101
159
|
}
|
|
102
160
|
end
|
|
103
161
|
|
|
104
|
-
def
|
|
105
|
-
|
|
162
|
+
def self.construct_depth_sql(table_name, ancestry_column, ancestry_delimiter)
|
|
163
|
+
tmp = %{(LENGTH(#{table_name}.#{ancestry_column}) - LENGTH(REPLACE(#{table_name}.#{ancestry_column},'#{ancestry_delimiter}','')))}
|
|
164
|
+
tmp += "/#{ancestry_delimiter.size}" if ancestry_delimiter.size > 1
|
|
165
|
+
"(CASE WHEN #{table_name}.#{ancestry_column} IS NULL THEN 0 ELSE 1 + #{tmp} END)"
|
|
106
166
|
end
|
|
107
167
|
|
|
108
|
-
def
|
|
109
|
-
|
|
168
|
+
def self.validation_options(primary_key_format, delimiter)
|
|
169
|
+
{
|
|
170
|
+
format: {with: /\A#{primary_key_format}(#{Regexp.escape(delimiter)}#{primary_key_format})*\z/.freeze},
|
|
171
|
+
allow_nil: true
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
def ancestry_validation_options(ancestry_primary_key_format)
|
|
178
|
+
MaterializedPath.validation_options(ancestry_primary_key_format, ancestry_delimiter)
|
|
110
179
|
end
|
|
111
180
|
|
|
112
181
|
module InstanceMethods
|
|
113
182
|
# optimization - better to go directly to column and avoid parsing
|
|
114
183
|
def ancestors?
|
|
115
|
-
read_attribute(self.
|
|
184
|
+
read_attribute(self.class.ancestry_column) != self.class.ancestry_root
|
|
116
185
|
end
|
|
117
|
-
alias
|
|
186
|
+
alias has_parent? ancestors?
|
|
118
187
|
|
|
119
188
|
def ancestor_ids=(value)
|
|
120
|
-
write_attribute(self.
|
|
189
|
+
write_attribute(self.class.ancestry_column, self.class.generate_ancestry(value))
|
|
121
190
|
end
|
|
122
191
|
|
|
123
192
|
def ancestor_ids
|
|
124
|
-
|
|
193
|
+
MaterializedPath.parse(read_attribute(self.class.ancestry_column), self.class.ancestry_root, self.class.ancestry_delimiter, self.class.primary_key_is_an_integer?)
|
|
125
194
|
end
|
|
126
195
|
|
|
127
196
|
def ancestor_ids_in_database
|
|
128
|
-
|
|
197
|
+
MaterializedPath.parse(attribute_in_database(self.class.ancestry_column), self.class.ancestry_root, self.class.ancestry_delimiter, self.class.primary_key_is_an_integer?)
|
|
129
198
|
end
|
|
130
199
|
|
|
131
200
|
def ancestor_ids_before_last_save
|
|
132
|
-
|
|
201
|
+
MaterializedPath.parse(attribute_before_last_save(self.class.ancestry_column), self.class.ancestry_root, self.class.ancestry_delimiter, self.class.primary_key_is_an_integer?)
|
|
133
202
|
end
|
|
134
203
|
|
|
135
204
|
def parent_id_in_database
|
|
136
|
-
|
|
205
|
+
MaterializedPath.parse(attribute_in_database(self.class.ancestry_column), self.class.ancestry_root, self.class.ancestry_delimiter, self.class.primary_key_is_an_integer?).last
|
|
137
206
|
end
|
|
138
207
|
|
|
139
208
|
def parent_id_before_last_save
|
|
140
|
-
|
|
209
|
+
MaterializedPath.parse(attribute_before_last_save(self.class.ancestry_column), self.class.ancestry_root, self.class.ancestry_delimiter, self.class.primary_key_is_an_integer?).last
|
|
141
210
|
end
|
|
142
211
|
|
|
143
212
|
# optimization - better to go directly to column and avoid parsing
|
|
144
213
|
def sibling_of?(node)
|
|
145
|
-
|
|
214
|
+
read_attribute(self.class.ancestry_column) == node.read_attribute(node.class.ancestry_column)
|
|
146
215
|
end
|
|
147
216
|
|
|
148
|
-
#
|
|
149
|
-
#
|
|
150
|
-
#
|
|
217
|
+
# The ancestry value for this record's children
|
|
218
|
+
# This can also be thought of as the ancestry value for the path
|
|
219
|
+
# If this is a new record, it has no id, and it is not valid.
|
|
220
|
+
# NOTE: This could have been called child_ancestry_in_database
|
|
221
|
+
# the child records were created from the version in the database
|
|
151
222
|
def child_ancestry
|
|
152
|
-
|
|
153
|
-
raise Ancestry::AncestryException.new(I18n.t("ancestry.no_child_for_new_record")) if new_record?
|
|
154
|
-
[attribute_in_database(self.ancestry_base_class.ancestry_column), id].compact.join(self.ancestry_base_class.ancestry_delimiter)
|
|
155
|
-
end
|
|
223
|
+
raise(Ancestry::AncestryException, I18n.t("ancestry.no_child_for_new_record")) if new_record?
|
|
156
224
|
|
|
157
|
-
|
|
158
|
-
# New records cannot have children
|
|
159
|
-
raise Ancestry::AncestryException.new(I18n.t("ancestry.no_child_for_new_record")) if new_record?
|
|
160
|
-
[attribute_before_last_save(self.ancestry_base_class.ancestry_column), id].compact.join(self.ancestry_base_class.ancestry_delimiter)
|
|
225
|
+
MaterializedPath.child_ancestry_value(attribute_in_database(self.class.ancestry_column), id, self.class.ancestry_delimiter)
|
|
161
226
|
end
|
|
162
227
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if ancestor_ids.present? && ancestor_ids.any?
|
|
171
|
-
ancestor_ids.join(self.ancestry_base_class.ancestry_delimiter)
|
|
172
|
-
else
|
|
173
|
-
self.ancestry_base_class.ancestry_root
|
|
228
|
+
# The ancestry value for this record's old children
|
|
229
|
+
# Currently used in an after_update via unscoped_descendants_before_last_save
|
|
230
|
+
# to find the old children and bring them along (or to )
|
|
231
|
+
# This is not valid in a new record's after_save.
|
|
232
|
+
def child_ancestry_before_last_save
|
|
233
|
+
if new_record? || (respond_to?(:previously_new_record?) && previously_new_record?)
|
|
234
|
+
raise Ancestry::AncestryException, I18n.t("ancestry.no_child_for_new_record")
|
|
174
235
|
end
|
|
236
|
+
|
|
237
|
+
MaterializedPath.child_ancestry_value(attribute_before_last_save(self.class.ancestry_column), id, self.class.ancestry_delimiter)
|
|
175
238
|
end
|
|
176
239
|
end
|
|
177
240
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Ancestry
|
|
2
4
|
# store ancestry as /grandparent_id/parent_id/
|
|
3
5
|
# root: a=/,id=1 children=#{a}#{id}/% == /1/%
|
|
@@ -11,9 +13,8 @@ module Ancestry
|
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def indirects_of(object)
|
|
14
|
-
t = arel_table
|
|
15
16
|
node = to_node(object)
|
|
16
|
-
where(
|
|
17
|
+
where(MaterializedPath2.indirects_condition(arel_table[ancestry_column], node.child_ancestry, ancestry_delimiter))
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def ordered_by_ancestry(order = nil)
|
|
@@ -21,42 +22,86 @@ module Ancestry
|
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def descendants_by_ancestry(ancestry)
|
|
24
|
-
arel_table[ancestry_column]
|
|
25
|
+
MaterializedPath2.descendants_condition(arel_table[ancestry_column], ancestry, ancestry_delimiter)
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def ancestry_root
|
|
28
29
|
ancestry_delimiter
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
def child_ancestry_sql
|
|
33
|
+
MaterializedPath2.child_ancestry_sql(table_name, ancestry_column, primary_key, ancestry_delimiter, connection.adapter_name.downcase)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ancestry_depth_sql
|
|
37
|
+
@ancestry_depth_sql ||= MaterializedPath2.construct_depth_sql(table_name, ancestry_column, ancestry_delimiter)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def generate_ancestry(ancestor_ids)
|
|
41
|
+
MaterializedPath2.generate(ancestor_ids, ancestry_delimiter, ancestry_root)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.generate(ancestor_ids, delimiter, root)
|
|
45
|
+
if ancestor_ids.present? && ancestor_ids.any?
|
|
46
|
+
"#{delimiter}#{ancestor_ids.join(delimiter)}#{delimiter}"
|
|
47
|
+
else
|
|
48
|
+
root
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.child_ancestry_value(ancestry_value, id, delimiter)
|
|
53
|
+
"#{ancestry_value}#{id}#{delimiter}"
|
|
54
|
+
end
|
|
32
55
|
|
|
33
|
-
def
|
|
34
|
-
|
|
56
|
+
def self.child_ancestry_sql(table_name, ancestry_column, primary_key, delimiter, adapter)
|
|
57
|
+
MaterializedPath.concat(adapter, "#{table_name}.#{ancestry_column}", "#{table_name}.#{primary_key}", "'#{delimiter}'")
|
|
35
58
|
end
|
|
36
59
|
|
|
37
|
-
|
|
38
|
-
|
|
60
|
+
# mp2: descendants just use LIKE (trailing delimiter prevents false prefix matches)
|
|
61
|
+
def self.descendants_condition(attr, child_ancestry, _delimiter)
|
|
62
|
+
attr.matches("#{child_ancestry}%", nil, true)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# mp2: indirects match child_ancestry + at least one more segment
|
|
66
|
+
def self.indirects_condition(attr, child_ancestry, delimiter)
|
|
67
|
+
attr.matches("#{child_ancestry}%#{delimiter}%", nil, true)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# module method
|
|
71
|
+
def self.construct_depth_sql(table_name, ancestry_column, ancestry_delimiter)
|
|
72
|
+
tmp = %{(LENGTH(#{table_name}.#{ancestry_column}) - LENGTH(REPLACE(#{table_name}.#{ancestry_column},'#{ancestry_delimiter}','')))}
|
|
73
|
+
tmp += "/#{ancestry_delimiter.size}" if ancestry_delimiter.size > 1
|
|
74
|
+
"(#{tmp} -1)"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.validation_options(primary_key_format, delimiter)
|
|
78
|
+
{
|
|
79
|
+
format: {with: /\A#{Regexp.escape(delimiter)}(#{primary_key_format}#{Regexp.escape(delimiter)})*\z/.freeze},
|
|
80
|
+
allow_nil: false
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def ancestry_validation_options(ancestry_primary_key_format)
|
|
87
|
+
MaterializedPath2.validation_options(ancestry_primary_key_format, ancestry_delimiter)
|
|
39
88
|
end
|
|
40
89
|
|
|
41
90
|
module InstanceMethods
|
|
91
|
+
# Please see notes for MaterializedPath#child_ancestry
|
|
42
92
|
def child_ancestry
|
|
43
|
-
|
|
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}"
|
|
46
|
-
end
|
|
93
|
+
raise(Ancestry::AncestryException, I18n.t("ancestry.no_child_for_new_record")) if new_record?
|
|
47
94
|
|
|
48
|
-
|
|
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}"
|
|
95
|
+
MaterializedPath2.child_ancestry_value(attribute_in_database(self.class.ancestry_column), id, self.class.ancestry_delimiter)
|
|
52
96
|
end
|
|
53
97
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
self.ancestry_base_class.ancestry_root
|
|
98
|
+
# Please see notes for MaterializedPath#child_ancestry_before_last_save
|
|
99
|
+
def child_ancestry_before_last_save
|
|
100
|
+
if new_record? || (respond_to?(:previously_new_record?) && previously_new_record?)
|
|
101
|
+
raise(Ancestry::AncestryException, I18n.t("ancestry.no_child_for_new_record"))
|
|
59
102
|
end
|
|
103
|
+
|
|
104
|
+
MaterializedPath2.child_ancestry_value(attribute_before_last_save(self.class.ancestry_column), id, self.class.ancestry_delimiter)
|
|
60
105
|
end
|
|
61
106
|
end
|
|
62
107
|
end
|
|
@@ -1,23 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Ancestry
|
|
2
4
|
module MaterializedPathPg
|
|
3
|
-
# Update descendants with new ancestry (after update)
|
|
4
|
-
def
|
|
5
|
+
# Update descendants with new ancestry using a single SQL statement (after update)
|
|
6
|
+
def update_descendants_with_new_ancestry_sql
|
|
5
7
|
# If enabled and node is existing and ancestry was updated and the new ancestry is sane ...
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
old_ancestry = generate_ancestry(
|
|
9
|
-
new_ancestry = generate_ancestry(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
# The only way the ancestry could be bad is via `update_attribute` with a bad value
|
|
9
|
+
if !ancestry_callbacks_disabled? && sane_ancestor_ids?
|
|
10
|
+
old_ancestry = self.class.generate_ancestry(path_ids_before_last_save)
|
|
11
|
+
new_ancestry = self.class.generate_ancestry(path_ids)
|
|
12
|
+
# Replace old ancestry prefix with new ancestry:
|
|
13
|
+
# CONCAT(new_ancestry, SUBSTRING(column, LENGTH(old_ancestry) + 1))
|
|
14
|
+
column = self.class.ancestry_column
|
|
15
|
+
replace_sql = self.class.concat("'#{new_ancestry}'", "SUBSTRING(#{column}, #{old_ancestry.length + 1})")
|
|
16
|
+
update_clause = {
|
|
17
|
+
column => Arel.sql(replace_sql)
|
|
18
|
+
}
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
update_clause
|
|
20
|
+
current_time = current_time_from_proper_timezone
|
|
21
|
+
timestamp_attributes_for_update_in_model.each do |column|
|
|
22
|
+
update_clause[column] = current_time
|
|
17
23
|
end
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
update_descendants_hook(update_clause, old_ancestry, new_ancestry)
|
|
26
|
+
unscoped_descendants_before_last_save.update_all update_clause
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def update_descendants_hook(update_clause, old_ancestry, new_ancestry)
|
|
31
|
+
if self.class.respond_to?(:depth_cache_column)
|
|
32
|
+
depth_cache_column = self.class.depth_cache_column
|
|
33
|
+
depth_change = self.class.ancestry_depth_change(old_ancestry, new_ancestry)
|
|
34
|
+
|
|
35
|
+
if depth_change != 0
|
|
36
|
+
update_clause[depth_cache_column] = Arel.sql("#{depth_cache_column} + #{depth_change}")
|
|
37
|
+
end
|
|
20
38
|
end
|
|
39
|
+
update_clause
|
|
21
40
|
end
|
|
22
41
|
end
|
|
23
42
|
end
|
data/lib/ancestry/version.rb
CHANGED
data/lib/ancestry.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative 'ancestry/version'
|
|
2
4
|
require_relative 'ancestry/class_methods'
|
|
3
5
|
require_relative 'ancestry/instance_methods'
|
|
@@ -8,7 +10,7 @@ require_relative 'ancestry/materialized_path2'
|
|
|
8
10
|
require_relative 'ancestry/materialized_path_pg'
|
|
9
11
|
|
|
10
12
|
I18n.load_path += Dir[File.join(File.expand_path(File.dirname(__FILE__)),
|
|
11
|
-
|
|
13
|
+
'ancestry', 'locales', '*.{rb,yml}').to_s]
|
|
12
14
|
|
|
13
15
|
module Ancestry
|
|
14
16
|
@@default_update_strategy = :ruby
|
metadata
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ancestry
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 5.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stefan Kroes
|
|
8
8
|
- Keenan Brock
|
|
9
|
-
autorequire:
|
|
10
9
|
bindir: bin
|
|
11
10
|
cert_chain: []
|
|
12
|
-
date:
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
13
12
|
dependencies:
|
|
14
13
|
- !ruby/object:Gem::Dependency
|
|
15
14
|
name: activerecord
|
|
@@ -25,6 +24,20 @@ dependencies:
|
|
|
25
24
|
- - ">="
|
|
26
25
|
- !ruby/object:Gem::Version
|
|
27
26
|
version: 5.2.6
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: logger
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
28
41
|
- !ruby/object:Gem::Dependency
|
|
29
42
|
name: appraisal
|
|
30
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -129,6 +142,7 @@ metadata:
|
|
|
129
142
|
changelog_uri: https://github.com/stefankroes/ancestry/blob/master/CHANGELOG.md
|
|
130
143
|
source_code_uri: https://github.com/stefankroes/ancestry/
|
|
131
144
|
bug_tracker_uri: https://github.com/stefankroes/ancestry/issues
|
|
145
|
+
rubygems_mfa_required: 'true'
|
|
132
146
|
post_install_message: Thank you for installing Ancestry. You can visit http://github.com/stefankroes/ancestry
|
|
133
147
|
to read the documentation.
|
|
134
148
|
rdoc_options: []
|
|
@@ -145,8 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
145
159
|
- !ruby/object:Gem::Version
|
|
146
160
|
version: '0'
|
|
147
161
|
requirements: []
|
|
148
|
-
rubygems_version:
|
|
149
|
-
signing_key:
|
|
162
|
+
rubygems_version: 4.0.6
|
|
150
163
|
specification_version: 4
|
|
151
164
|
summary: Organize ActiveRecord model into a tree structure
|
|
152
165
|
test_files: []
|