closure_tree 7.3.0 → 9.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 +31 -0
- data/README.md +125 -39
- data/bin/rails +15 -0
- data/bin/rake +7 -7
- data/closure_tree.gemspec +21 -19
- data/lib/closure_tree/active_record_support.rb +6 -14
- data/lib/closure_tree/arel_helpers.rb +83 -0
- data/lib/closure_tree/deterministic_ordering.rb +2 -0
- data/lib/closure_tree/digraphs.rb +7 -5
- data/lib/closure_tree/finders.rb +104 -55
- data/lib/closure_tree/has_closure_tree.rb +5 -4
- data/lib/closure_tree/has_closure_tree_root.rb +12 -17
- data/lib/closure_tree/hash_tree.rb +3 -2
- data/lib/closure_tree/hash_tree_support.rb +38 -13
- data/lib/closure_tree/hierarchy_maintenance.rb +20 -30
- data/lib/closure_tree/model.rb +31 -31
- data/lib/closure_tree/numeric_deterministic_ordering.rb +90 -60
- data/lib/closure_tree/numeric_order_support.rb +20 -18
- data/lib/closure_tree/support.rb +30 -44
- data/lib/closure_tree/support_attributes.rb +31 -5
- data/lib/closure_tree/support_flags.rb +2 -12
- data/lib/closure_tree/test/matcher.rb +10 -12
- data/lib/closure_tree/version.rb +3 -1
- data/lib/closure_tree.rb +14 -22
- data/lib/generators/closure_tree/migration_generator.rb +7 -8
- metadata +30 -81
- data/.github/workflows/ci.yml +0 -96
- data/.gitignore +0 -17
- data/.rspec +0 -1
- data/.yardopts +0 -3
- data/Appraisals +0 -105
- data/Gemfile +0 -3
- data/Rakefile +0 -28
- data/_config.yml +0 -1
- data/bin/appraisal +0 -29
- data/bin/rspec +0 -29
- data/lib/closure_tree/configuration.rb +0 -9
- data/lib/generators/closure_tree/config_generator.rb +0 -12
- data/lib/generators/closure_tree/templates/config.rb +0 -5
- data/mktree.rb +0 -38
- data/tests.sh +0 -11
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/concern'
|
2
4
|
|
3
5
|
# This module is only included if the order column is an integer.
|
@@ -10,15 +12,10 @@ module ClosureTree
|
|
10
12
|
end
|
11
13
|
|
12
14
|
def _ct_reorder_prior_siblings_if_parent_changed
|
13
|
-
|
14
|
-
change_method = as_5_1 ? :saved_change_to_attribute? : :attribute_changed?
|
15
|
-
|
16
|
-
if public_send(change_method, _ct.parent_column_name) && !@was_new_record
|
17
|
-
attribute_method = as_5_1 ? :attribute_before_last_save : :attribute_was
|
15
|
+
return unless saved_change_to_attribute?(_ct.parent_column_name) && !@was_new_record
|
18
16
|
|
19
|
-
|
20
|
-
|
21
|
-
end
|
17
|
+
was_parent_id = attribute_before_last_save(_ct.parent_column_name)
|
18
|
+
_ct.reorder_with_parent_id(was_parent_id)
|
22
19
|
end
|
23
20
|
|
24
21
|
def _ct_reorder_siblings(minimum_sort_order_value = nil)
|
@@ -32,60 +29,94 @@ module ClosureTree
|
|
32
29
|
|
33
30
|
def self_and_descendants_preordered
|
34
31
|
# TODO: raise NotImplementedError if sort_order is not numeric and not null?
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
32
|
+
hierarchy_table = self.class.hierarchy_class.arel_table
|
33
|
+
model_table = self.class.arel_table
|
34
|
+
|
35
|
+
# Create aliased tables for the joins
|
36
|
+
anc_hier = _ct.aliased_table(hierarchy_table, 'anc_hier')
|
37
|
+
anc = _ct.aliased_table(model_table, 'anc')
|
38
|
+
depths = _ct.aliased_table(hierarchy_table, 'depths')
|
39
|
+
|
40
|
+
# Build the join conditions using Arel
|
41
|
+
join_anc_hier = hierarchy_table
|
42
|
+
.join(anc_hier)
|
43
|
+
.on(anc_hier[:descendant_id].eq(hierarchy_table[:descendant_id]))
|
44
|
+
|
45
|
+
join_anc = join_anc_hier
|
46
|
+
.join(anc)
|
47
|
+
.on(anc[self.class.primary_key].eq(anc_hier[:ancestor_id]))
|
48
|
+
|
49
|
+
join_depths = join_anc
|
50
|
+
.join(depths)
|
51
|
+
.on(
|
52
|
+
depths[:ancestor_id].eq(id)
|
53
|
+
.and(depths[:descendant_id].eq(anc[self.class.primary_key]))
|
54
|
+
)
|
43
55
|
|
44
56
|
self_and_descendants
|
45
|
-
.joins(
|
57
|
+
.joins(join_depths.join_sources)
|
46
58
|
.group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}")
|
47
59
|
.reorder(self.class._ct_sum_order_by(self))
|
48
60
|
end
|
49
61
|
|
50
|
-
|
51
|
-
|
62
|
+
class_methods do
|
52
63
|
# If node is nil, order the whole tree.
|
53
64
|
def _ct_sum_order_by(node = nil)
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
65
|
+
# Build the stats query using Arel
|
66
|
+
hierarchy_table = hierarchy_class.arel_table
|
67
|
+
|
68
|
+
query = hierarchy_table
|
69
|
+
.project(
|
70
|
+
Arel.star.count.as('total_descendants'),
|
71
|
+
hierarchy_table[:generations].maximum.as('max_depth')
|
72
|
+
)
|
73
|
+
|
74
|
+
query = query.where(hierarchy_table[:ancestor_id].eq(node.id)) if node
|
75
|
+
|
76
|
+
h = _ct.connection.select_one(query.to_sql)
|
62
77
|
|
63
78
|
depth_column = node ? 'depths.generations' : 'depths.max_depth'
|
64
79
|
|
65
|
-
node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * "
|
66
|
-
|
80
|
+
node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " \
|
81
|
+
"power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - #{depth_column})"
|
67
82
|
|
68
83
|
# We want the NULLs to be first in case we are not ordering roots and they have NULL order.
|
69
84
|
Arel.sql("SUM(#{node_score}) IS NULL DESC, SUM(#{node_score})")
|
70
85
|
end
|
71
86
|
|
72
87
|
def roots_and_descendants_preordered
|
73
|
-
if _ct.dont_order_roots
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
88
|
+
raise ClosureTree::RootOrderingDisabledError, 'Root ordering is disabled on this model' if _ct.dont_order_roots
|
89
|
+
|
90
|
+
hierarchy_table = hierarchy_class.arel_table
|
91
|
+
model_table = arel_table
|
92
|
+
|
93
|
+
# Create aliased tables
|
94
|
+
anc_hier = _ct.aliased_table(hierarchy_table, 'anc_hier')
|
95
|
+
anc = _ct.aliased_table(model_table, 'anc')
|
96
|
+
|
97
|
+
# Build the subquery for depths
|
98
|
+
depths_subquery = hierarchy_table
|
99
|
+
.project(
|
100
|
+
hierarchy_table[:descendant_id],
|
101
|
+
hierarchy_table[:generations].maximum.as('max_depth')
|
102
|
+
)
|
103
|
+
.group(hierarchy_table[:descendant_id])
|
104
|
+
.as('depths')
|
105
|
+
|
106
|
+
# Build the join conditions
|
107
|
+
join_anc_hier = model_table
|
108
|
+
.join(anc_hier)
|
109
|
+
.on(anc_hier[:descendant_id].eq(model_table[primary_key]))
|
110
|
+
|
111
|
+
join_anc = join_anc_hier
|
112
|
+
.join(anc)
|
113
|
+
.on(anc[primary_key].eq(anc_hier[:ancestor_id]))
|
114
|
+
|
115
|
+
join_depths = join_anc
|
116
|
+
.join(depths_subquery)
|
117
|
+
.on(depths_subquery[:descendant_id].eq(anc[primary_key]))
|
118
|
+
|
119
|
+
joins(join_depths.join_sources)
|
89
120
|
.group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}")
|
90
121
|
.reorder(_ct_sum_order_by)
|
91
122
|
end
|
@@ -116,10 +147,10 @@ module ClosureTree
|
|
116
147
|
end
|
117
148
|
|
118
149
|
def add_sibling(sibling, add_after = true)
|
119
|
-
|
150
|
+
raise "can't add self as sibling" if self == sibling
|
120
151
|
|
121
152
|
if _ct.dont_order_roots && parent.nil?
|
122
|
-
raise ClosureTree::RootOrderingDisabledError
|
153
|
+
raise ClosureTree::RootOrderingDisabledError, 'Root ordering is disabled on this model'
|
123
154
|
end
|
124
155
|
|
125
156
|
# Make sure self isn't dirty, because we're going to call reload:
|
@@ -127,31 +158,30 @@ module ClosureTree
|
|
127
158
|
|
128
159
|
_ct.with_advisory_lock do
|
129
160
|
prior_sibling_parent = sibling.parent
|
130
|
-
reorder_from_value = if prior_sibling_parent ==
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
sibling.order_value =
|
137
|
-
sibling.parent =
|
161
|
+
reorder_from_value = if prior_sibling_parent == parent
|
162
|
+
[order_value, sibling.order_value].compact.min
|
163
|
+
else
|
164
|
+
order_value
|
165
|
+
end
|
166
|
+
|
167
|
+
sibling.order_value = order_value
|
168
|
+
sibling.parent = parent
|
138
169
|
sibling._ct_skip_sort_order_maintenance!
|
139
170
|
sibling.save # may be a no-op
|
140
171
|
|
141
172
|
_ct_reorder_siblings(reorder_from_value)
|
142
173
|
|
143
174
|
# The sort order should be correct now except for self and sibling, which may need to flip:
|
144
|
-
sibling_is_after =
|
175
|
+
sibling_is_after = reload.order_value < sibling.reload.order_value
|
145
176
|
if add_after != sibling_is_after
|
146
177
|
# We need to flip the sort orders:
|
147
|
-
self_ov
|
178
|
+
self_ov = order_value
|
179
|
+
sib_ov = sibling.order_value
|
148
180
|
update_order_value(sib_ov)
|
149
181
|
sibling.update_order_value(self_ov)
|
150
182
|
end
|
151
183
|
|
152
|
-
if prior_sibling_parent !=
|
153
|
-
prior_sibling_parent.try(:_ct_reorder_children)
|
154
|
-
end
|
184
|
+
prior_sibling_parent.try(:_ct_reorder_children) if prior_sibling_parent != parent
|
155
185
|
sibling
|
156
186
|
end
|
157
187
|
end
|
@@ -1,11 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ClosureTree
|
2
4
|
module NumericOrderSupport
|
3
|
-
|
4
5
|
def self.adapter_for_connection(connection)
|
5
|
-
|
6
|
-
if
|
6
|
+
adapter_name = connection.adapter_name.downcase
|
7
|
+
if adapter_name.include?('postgresql') || adapter_name.include?('postgis')
|
7
8
|
::ClosureTree::NumericOrderSupport::PostgreSQLAdapter
|
8
|
-
elsif
|
9
|
+
elsif adapter_name.include?('mysql') || adapter_name.include?('trilogy')
|
9
10
|
::ClosureTree::NumericOrderSupport::MysqlAdapter
|
10
11
|
else
|
11
12
|
::ClosureTree::NumericOrderSupport::GenericAdapter
|
@@ -15,11 +16,12 @@ module ClosureTree
|
|
15
16
|
module MysqlAdapter
|
16
17
|
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
|
17
18
|
return if parent_id.nil? && dont_order_roots
|
19
|
+
|
18
20
|
min_where = if minimum_sort_order_value
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
21
|
+
"AND #{quoted_order_column} >= #{minimum_sort_order_value}"
|
22
|
+
else
|
23
|
+
''
|
24
|
+
end
|
23
25
|
connection.execute 'SET @i = 0'
|
24
26
|
connection.execute <<-SQL.squish
|
25
27
|
UPDATE #{quoted_table_name}
|
@@ -33,11 +35,12 @@ module ClosureTree
|
|
33
35
|
module PostgreSQLAdapter
|
34
36
|
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
|
35
37
|
return if parent_id.nil? && dont_order_roots
|
38
|
+
|
36
39
|
min_where = if minimum_sort_order_value
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
40
|
+
"AND #{quoted_order_column} >= #{minimum_sort_order_value}"
|
41
|
+
else
|
42
|
+
''
|
43
|
+
end
|
41
44
|
connection.execute <<-SQL.squish
|
42
45
|
UPDATE #{quoted_table_name}
|
43
46
|
SET #{quoted_order_column(false)} = t.seq + #{minimum_sort_order_value.to_i - 1}
|
@@ -59,12 +62,11 @@ module ClosureTree
|
|
59
62
|
module GenericAdapter
|
60
63
|
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
|
61
64
|
return if parent_id.nil? && dont_order_roots
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
end
|
65
|
+
|
66
|
+
scope = model_class
|
67
|
+
.where(parent_column_sym => parent_id)
|
68
|
+
.order(nulls_last_order_by)
|
69
|
+
scope = scope.where("#{quoted_order_column} >= #{minimum_sort_order_value}") if minimum_sort_order_value
|
68
70
|
scope.each_with_index do |ea, idx|
|
69
71
|
ea.update_order_value(idx + minimum_sort_order_value.to_i)
|
70
72
|
end
|
data/lib/closure_tree/support.rb
CHANGED
@@ -1,9 +1,4 @@
|
|
1
|
-
|
2
|
-
require 'closure_tree/support_attributes'
|
3
|
-
require 'closure_tree/numeric_order_support'
|
4
|
-
require 'closure_tree/active_record_support'
|
5
|
-
require 'closure_tree/hash_tree_support'
|
6
|
-
require 'with_advisory_lock'
|
1
|
+
# frozen_string_literal: true
|
7
2
|
|
8
3
|
# This class and mixins are an effort to reduce the namespace pollution to models that act_as_tree.
|
9
4
|
module ClosureTree
|
@@ -12,42 +7,40 @@ module ClosureTree
|
|
12
7
|
include ClosureTree::SupportAttributes
|
13
8
|
include ClosureTree::ActiveRecordSupport
|
14
9
|
include ClosureTree::HashTreeSupport
|
10
|
+
include ClosureTree::ArelHelpers
|
15
11
|
|
16
|
-
attr_reader :model_class
|
17
|
-
attr_reader :options
|
12
|
+
attr_reader :model_class, :options
|
18
13
|
|
19
14
|
def initialize(model_class, options)
|
20
15
|
@model_class = model_class
|
16
|
+
|
21
17
|
@options = {
|
22
|
-
:
|
23
|
-
:
|
24
|
-
:
|
25
|
-
:
|
26
|
-
:
|
18
|
+
parent_column_name: 'parent_id',
|
19
|
+
dependent: :nullify, # or :destroy or :delete_all -- see the README
|
20
|
+
name_column: 'name',
|
21
|
+
with_advisory_lock: true, # This will be overridden by adapter support
|
22
|
+
numeric_order: false
|
27
23
|
}.merge(options)
|
28
24
|
raise ArgumentError, "name_column can't be 'path'" if options[:name_column] == 'path'
|
29
|
-
|
30
|
-
|
31
|
-
|
25
|
+
|
26
|
+
return unless order_is_numeric?
|
27
|
+
|
28
|
+
extend NumericOrderSupport.adapter_for_connection(connection)
|
32
29
|
end
|
33
30
|
|
34
31
|
def hierarchy_class_for_model
|
35
|
-
parent_class =
|
36
|
-
hierarchy_class = parent_class.const_set(short_hierarchy_class_name, Class.new(
|
37
|
-
use_attr_accessible = use_attr_accessible?
|
38
|
-
include_forbidden_attributes_protection = include_forbidden_attributes_protection?
|
32
|
+
parent_class = model_class.module_parent
|
33
|
+
hierarchy_class = parent_class.const_set(short_hierarchy_class_name, Class.new(model_class.superclass))
|
39
34
|
model_class_name = model_class.to_s
|
40
35
|
hierarchy_class.class_eval do
|
41
|
-
include ActiveModel::ForbiddenAttributesProtection if include_forbidden_attributes_protection
|
42
36
|
belongs_to :ancestor, class_name: model_class_name
|
43
37
|
belongs_to :descendant, class_name: model_class_name
|
44
|
-
attr_accessible :ancestor, :descendant, :generations if use_attr_accessible
|
45
38
|
def ==(other)
|
46
39
|
self.class == other.class && ancestor_id == other.ancestor_id && descendant_id == other.descendant_id
|
47
40
|
end
|
48
41
|
alias :eql? :==
|
49
42
|
def hash
|
50
|
-
ancestor_id.hash << 31 ^ descendant_id.hash
|
43
|
+
(ancestor_id.hash << 31) ^ descendant_id.hash
|
51
44
|
end
|
52
45
|
end
|
53
46
|
hierarchy_class.table_name = hierarchy_table_name
|
@@ -59,36 +52,30 @@ module ClosureTree
|
|
59
52
|
# because they may have overridden the table name, which is what we want to be consistent with
|
60
53
|
# in order for the schema to make sense.
|
61
54
|
tablename = options[:hierarchy_table_name] ||
|
62
|
-
|
55
|
+
"#{remove_prefix_and_suffix(table_name, model_class).singularize}_hierarchies"
|
63
56
|
|
64
|
-
|
57
|
+
[model_class.table_name_prefix, tablename, model_class.table_name_suffix].join
|
65
58
|
end
|
66
59
|
|
67
60
|
def with_order_option(opts)
|
68
|
-
if order_option?
|
69
|
-
opts[:order] = [opts[:order], order_by].compact.join(",")
|
70
|
-
end
|
61
|
+
opts[:order] = [opts[:order], order_by].compact.join(',') if order_option?
|
71
62
|
opts
|
72
63
|
end
|
73
64
|
|
74
65
|
def scope_with_order(scope, additional_order_by = nil)
|
75
66
|
if order_option?
|
76
|
-
scope.order(*
|
67
|
+
scope.order(*[additional_order_by, order_by].compact)
|
77
68
|
else
|
78
69
|
additional_order_by ? scope.order(additional_order_by) : scope
|
79
70
|
end
|
80
71
|
end
|
81
72
|
|
82
|
-
def belongs_to_with_optional_option(opts)
|
83
|
-
ActiveRecord::VERSION::MAJOR < 5 ? opts.except(:optional) : opts
|
84
|
-
end
|
85
|
-
|
86
73
|
# lambda-ize the order, but don't apply the default order_option
|
87
74
|
def has_many_order_without_option(order_by_opt)
|
88
|
-
[
|
75
|
+
[-> { order(order_by_opt.call) }]
|
89
76
|
end
|
90
77
|
|
91
|
-
def has_many_order_with_option(order_by_opt=nil)
|
78
|
+
def has_many_order_with_option(order_by_opt = nil)
|
92
79
|
order_options = [order_by_opt, order_by].compact
|
93
80
|
[lambda {
|
94
81
|
order_options = order_options.map { |o| o.is_a?(Proc) ? o.call : o }
|
@@ -109,9 +96,9 @@ module ClosureTree
|
|
109
96
|
end
|
110
97
|
|
111
98
|
def with_advisory_lock(&block)
|
112
|
-
if options[:with_advisory_lock]
|
99
|
+
if options[:with_advisory_lock] && connection.supports_advisory_locks? && model_class.respond_to?(:with_advisory_lock)
|
113
100
|
model_class.with_advisory_lock(advisory_lock_name) do
|
114
|
-
transaction
|
101
|
+
transaction(&block)
|
115
102
|
end
|
116
103
|
else
|
117
104
|
yield
|
@@ -123,7 +110,7 @@ module ClosureTree
|
|
123
110
|
unless path.first.is_a?(Hash)
|
124
111
|
if subclass? && has_inheritance_column?
|
125
112
|
attributes = attributes.with_indifferent_access
|
126
|
-
attributes[inheritance_column] ||=
|
113
|
+
attributes[inheritance_column] ||= sti_name
|
127
114
|
end
|
128
115
|
path = path.map { |ea| attributes.merge(name_column => ea) }
|
129
116
|
end
|
@@ -131,11 +118,9 @@ module ClosureTree
|
|
131
118
|
end
|
132
119
|
|
133
120
|
def scoped_attributes(scope, attributes, target_table = model_class.table_name)
|
134
|
-
table_prefixed_attributes =
|
135
|
-
|
136
|
-
|
137
|
-
end
|
138
|
-
]
|
121
|
+
table_prefixed_attributes = attributes.transform_keys do |column_name|
|
122
|
+
"#{target_table}.#{column_name}"
|
123
|
+
end
|
139
124
|
scope.where(table_prefixed_attributes)
|
140
125
|
end
|
141
126
|
|
@@ -150,6 +135,7 @@ module ClosureTree
|
|
150
135
|
path.in_groups(max_join_tables, false).each do |subpath|
|
151
136
|
child = model_class.find_by_path(subpath, attributes, next_parent_id)
|
152
137
|
return nil if child.nil?
|
138
|
+
|
153
139
|
next_parent_id = child._ct_id
|
154
140
|
end
|
155
141
|
child
|
@@ -168,7 +154,7 @@ module ClosureTree
|
|
168
154
|
end
|
169
155
|
|
170
156
|
def create!(model_class, attributes)
|
171
|
-
create(model_class, attributes).tap
|
157
|
+
create(model_class, attributes).tap(&:save!)
|
172
158
|
end
|
173
159
|
end
|
174
160
|
end
|
@@ -1,11 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'forwardable'
|
4
|
+
require 'zlib'
|
5
|
+
|
2
6
|
module ClosureTree
|
3
7
|
module SupportAttributes
|
4
8
|
extend Forwardable
|
5
9
|
def_delegators :model_class, :connection, :transaction, :table_name, :base_class, :inheritance_column, :column_names
|
6
10
|
|
7
11
|
def advisory_lock_name
|
8
|
-
|
12
|
+
# Allow customization via options or instance method
|
13
|
+
if options[:advisory_lock_name]
|
14
|
+
case options[:advisory_lock_name]
|
15
|
+
when Proc
|
16
|
+
# Allow dynamic generation via proc
|
17
|
+
options[:advisory_lock_name].call(base_class)
|
18
|
+
when Symbol
|
19
|
+
# Allow delegation to a model method
|
20
|
+
if model_class.respond_to?(options[:advisory_lock_name])
|
21
|
+
model_class.send(options[:advisory_lock_name])
|
22
|
+
else
|
23
|
+
raise ArgumentError, "Model #{model_class} does not respond to #{options[:advisory_lock_name]}"
|
24
|
+
end
|
25
|
+
else
|
26
|
+
# Use static string value
|
27
|
+
options[:advisory_lock_name].to_s
|
28
|
+
end
|
29
|
+
else
|
30
|
+
# Default: Use CRC32 for a shorter, consistent hash
|
31
|
+
# This gives us 8 hex characters which is plenty for uniqueness
|
32
|
+
# and leaves room for prefixes
|
33
|
+
"ct_#{Zlib.crc32(base_class.name.to_s).to_s(16)}"
|
34
|
+
end
|
9
35
|
end
|
10
36
|
|
11
37
|
def quoted_table_name
|
@@ -17,7 +43,7 @@ module ClosureTree
|
|
17
43
|
end
|
18
44
|
|
19
45
|
def hierarchy_class_name
|
20
|
-
options[:hierarchy_class_name] || model_class
|
46
|
+
options[:hierarchy_class_name] || "#{model_class}Hierarchy"
|
21
47
|
end
|
22
48
|
|
23
49
|
def primary_key_column
|
@@ -84,7 +110,7 @@ module ClosureTree
|
|
84
110
|
end
|
85
111
|
|
86
112
|
def order_by_order(reverse = false)
|
87
|
-
desc =
|
113
|
+
desc = !(order_by.to_s =~ /DESC\z/).nil?
|
88
114
|
desc = !desc if reverse
|
89
115
|
desc ? 'DESC' : 'ASC'
|
90
116
|
end
|
@@ -111,13 +137,13 @@ module ClosureTree
|
|
111
137
|
|
112
138
|
def quoted_order_column(include_table_name = true)
|
113
139
|
require_order_column
|
114
|
-
prefix = include_table_name ? "#{quoted_table_name}." :
|
140
|
+
prefix = include_table_name ? "#{quoted_table_name}." : ''
|
115
141
|
"#{prefix}#{connection.quote_column_name(order_column)}"
|
116
142
|
end
|
117
143
|
|
118
144
|
# table_name alias keyword , like "AS". When used on table name alias, Oracle Database don't support used 'AS'
|
119
145
|
def t_alias_keyword
|
120
|
-
|
146
|
+
ActiveRecord::Base.connection.adapter_name.to_sym == :OracleEnhanced ? '' : 'AS'
|
121
147
|
end
|
122
148
|
end
|
123
149
|
end
|
@@ -1,17 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ClosureTree
|
2
4
|
module SupportFlags
|
3
|
-
|
4
|
-
def use_attr_accessible?
|
5
|
-
defined?(ActiveModel::MassAssignmentSecurity) &&
|
6
|
-
model_class.respond_to?(:accessible_attributes) &&
|
7
|
-
! model_class.accessible_attributes.empty?
|
8
|
-
end
|
9
|
-
|
10
|
-
def include_forbidden_attributes_protection?
|
11
|
-
defined?(ActiveModel::ForbiddenAttributesProtection) &&
|
12
|
-
model_class.ancestors.include?(ActiveModel::ForbiddenAttributesProtection)
|
13
|
-
end
|
14
|
-
|
15
5
|
def order_option?
|
16
6
|
order_by.present?
|
17
7
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'closure_tree'
|
2
4
|
|
3
5
|
module ClosureTree
|
@@ -19,13 +21,13 @@ module ClosureTree
|
|
19
21
|
end
|
20
22
|
|
21
23
|
# Checking if hierarchy table exists (common error)
|
22
|
-
unless
|
24
|
+
unless @subject.hierarchy_class.table_exists?
|
23
25
|
@message = "expected #{@subject.name}'s hierarchy table '#{@subject.hierarchy_class.table_name}' to exist"
|
24
26
|
return false
|
25
27
|
end
|
26
28
|
|
27
29
|
if @ordered
|
28
|
-
unless
|
30
|
+
unless @subject._ct.options.include?(:order)
|
29
31
|
@message = "expected #{@subject.name} to be an ordered closure tree"
|
30
32
|
return false
|
31
33
|
end
|
@@ -36,13 +38,13 @@ module ClosureTree
|
|
36
38
|
end
|
37
39
|
|
38
40
|
if @with_advisory_lock && !@subject._ct.options[:with_advisory_lock]
|
39
|
-
|
40
|
-
|
41
|
+
@message = "expected #{@subject.name} to have advisory lock"
|
42
|
+
return false
|
41
43
|
end
|
42
44
|
|
43
45
|
if @without_advisory_lock && @subject._ct.options[:with_advisory_lock]
|
44
|
-
|
45
|
-
|
46
|
+
@message = "expected #{@subject.name} to not have advisory lock"
|
47
|
+
return false
|
46
48
|
end
|
47
49
|
|
48
50
|
return true
|
@@ -70,13 +72,13 @@ module ClosureTree
|
|
70
72
|
@message || "expected #{@subject.name} to #{description}"
|
71
73
|
end
|
72
74
|
|
73
|
-
|
75
|
+
alias failure_message_for_should failure_message
|
74
76
|
|
75
77
|
def failure_message_when_negated
|
76
78
|
"expected #{@subject.name} not be a closure tree, but it is."
|
77
79
|
end
|
78
80
|
|
79
|
-
|
81
|
+
alias failure_message_for_should_not failure_message_when_negated
|
80
82
|
|
81
83
|
def description
|
82
84
|
"be a#{@ordered} closure tree#{@with_advisory_lock}"
|
@@ -85,7 +87,3 @@ module ClosureTree
|
|
85
87
|
end
|
86
88
|
end
|
87
89
|
end
|
88
|
-
|
89
|
-
RSpec.configure do |c|
|
90
|
-
c.include ClosureTree::Test::Matcher, type: :model
|
91
|
-
end
|
data/lib/closure_tree/version.rb
CHANGED
data/lib/closure_tree.rb
CHANGED
@@ -1,30 +1,22 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require 'active_record'
|
4
|
+
require 'zeitwerk'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
autoload :HierarchyMaintenance
|
10
|
-
autoload :Model
|
11
|
-
autoload :Finders
|
12
|
-
autoload :HashTree
|
13
|
-
autoload :Digraphs
|
14
|
-
autoload :DeterministicOrdering
|
15
|
-
autoload :NumericDeterministicOrdering
|
16
|
-
autoload :Configuration
|
6
|
+
loader = Zeitwerk::Loader.for_gem
|
7
|
+
loader.ignore("#{__dir__}/generators")
|
8
|
+
loader.setup
|
17
9
|
|
10
|
+
module ClosureTree
|
18
11
|
def self.configure
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
12
|
+
ActiveSupport::Deprecation.new.warn(
|
13
|
+
'ClosureTree.configure is deprecated and will be removed in a future version. ' \
|
14
|
+
'Configuration is no longer needed.'
|
15
|
+
)
|
16
|
+
yield if block_given?
|
24
17
|
end
|
25
18
|
end
|
26
19
|
|
27
|
-
ActiveSupport.on_load
|
28
|
-
|
29
|
-
ActiveRecord::Base.send :extend, ClosureTree::HasClosureTreeRoot
|
20
|
+
ActiveSupport.on_load(:active_record) do
|
21
|
+
extend ClosureTree::HasClosureTree, ClosureTree::HasClosureTreeRoot
|
30
22
|
end
|