closure_tree 8.0.0 → 9.0.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 +13 -0
- data/README.md +111 -38
- data/bin/rails +15 -0
- data/bin/rake +7 -7
- data/closure_tree.gemspec +11 -17
- data/lib/closure_tree/active_record_support.rb +4 -1
- data/lib/closure_tree/adapter_support.rb +11 -0
- data/lib/closure_tree/arel_helpers.rb +83 -0
- data/lib/closure_tree/configuration.rb +2 -0
- data/lib/closure_tree/deterministic_ordering.rb +2 -0
- data/lib/closure_tree/digraphs.rb +6 -4
- data/lib/closure_tree/finders.rb +103 -54
- data/lib/closure_tree/has_closure_tree.rb +5 -2
- data/lib/closure_tree/has_closure_tree_root.rb +12 -17
- data/lib/closure_tree/hash_tree.rb +2 -1
- data/lib/closure_tree/hash_tree_support.rb +38 -13
- data/lib/closure_tree/hierarchy_maintenance.rb +19 -26
- data/lib/closure_tree/model.rb +29 -29
- data/lib/closure_tree/numeric_deterministic_ordering.rb +90 -55
- data/lib/closure_tree/numeric_order_support.rb +20 -18
- data/lib/closure_tree/support.rb +29 -32
- 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 +22 -2
- data/lib/generators/closure_tree/config_generator.rb +3 -1
- data/lib/generators/closure_tree/migration_generator.rb +6 -4
- data/lib/generators/closure_tree/templates/config.rb +2 -0
- metadata +12 -104
- data/.github/workflows/ci.yml +0 -72
- data/.github/workflows/ci_jruby.yml +0 -68
- data/.github/workflows/ci_truffleruby.yml +0 -71
- data/.github/workflows/release.yml +0 -17
- data/.gitignore +0 -17
- data/.release-please-manifest.json +0 -1
- data/.rspec +0 -1
- data/.tool-versions +0 -1
- data/.yardopts +0 -3
- data/Appraisals +0 -61
- data/Gemfile +0 -6
- data/Rakefile +0 -32
- data/bin/appraisal +0 -29
- data/bin/rspec +0 -29
- data/mktree.rb +0 -38
- data/release-please-config.json +0 -4
- data/test/closure_tree/cache_invalidation_test.rb +0 -36
- data/test/closure_tree/cuisine_type_test.rb +0 -42
- data/test/closure_tree/generator_test.rb +0 -49
- data/test/closure_tree/has_closure_tree_root_test.rb +0 -80
- data/test/closure_tree/hierarchy_maintenance_test.rb +0 -56
- data/test/closure_tree/label_test.rb +0 -674
- data/test/closure_tree/metal_test.rb +0 -59
- data/test/closure_tree/model_test.rb +0 -9
- data/test/closure_tree/namespace_type_test.rb +0 -13
- data/test/closure_tree/parallel_test.rb +0 -162
- data/test/closure_tree/pool_test.rb +0 -33
- data/test/closure_tree/support_test.rb +0 -18
- data/test/closure_tree/tag_test.rb +0 -8
- data/test/closure_tree/user_test.rb +0 -175
- data/test/closure_tree/uuid_tag_test.rb +0 -8
- data/test/support/query_counter.rb +0 -25
- data/test/support/tag_examples.rb +0 -923
- data/test/test_helper.rb +0 -99
@@ -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,10 +12,10 @@ module ClosureTree
|
|
10
12
|
end
|
11
13
|
|
12
14
|
def _ct_reorder_prior_siblings_if_parent_changed
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
return unless saved_change_to_attribute?(_ct.parent_column_name) && !@was_new_record
|
16
|
+
|
17
|
+
was_parent_id = attribute_before_last_save(_ct.parent_column_name)
|
18
|
+
_ct.reorder_with_parent_id(was_parent_id)
|
17
19
|
end
|
18
20
|
|
19
21
|
def _ct_reorder_siblings(minimum_sort_order_value = nil)
|
@@ -27,60 +29,94 @@ module ClosureTree
|
|
27
29
|
|
28
30
|
def self_and_descendants_preordered
|
29
31
|
# TODO: raise NotImplementedError if sort_order is not numeric and not null?
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
+
)
|
38
55
|
|
39
56
|
self_and_descendants
|
40
|
-
.joins(
|
57
|
+
.joins(join_depths.join_sources)
|
41
58
|
.group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}")
|
42
59
|
.reorder(self.class._ct_sum_order_by(self))
|
43
60
|
end
|
44
61
|
|
45
62
|
class_methods do
|
46
|
-
|
47
63
|
# If node is nil, order the whole tree.
|
48
64
|
def _ct_sum_order_by(node = nil)
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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)
|
57
77
|
|
58
78
|
depth_column = node ? 'depths.generations' : 'depths.max_depth'
|
59
79
|
|
60
|
-
node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * "
|
61
|
-
|
80
|
+
node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " \
|
81
|
+
"power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - #{depth_column})"
|
62
82
|
|
63
83
|
# We want the NULLs to be first in case we are not ordering roots and they have NULL order.
|
64
84
|
Arel.sql("SUM(#{node_score}) IS NULL DESC, SUM(#{node_score})")
|
65
85
|
end
|
66
86
|
|
67
87
|
def roots_and_descendants_preordered
|
68
|
-
if _ct.dont_order_roots
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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)
|
84
120
|
.group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}")
|
85
121
|
.reorder(_ct_sum_order_by)
|
86
122
|
end
|
@@ -111,10 +147,10 @@ module ClosureTree
|
|
111
147
|
end
|
112
148
|
|
113
149
|
def add_sibling(sibling, add_after = true)
|
114
|
-
|
150
|
+
raise "can't add self as sibling" if self == sibling
|
115
151
|
|
116
152
|
if _ct.dont_order_roots && parent.nil?
|
117
|
-
raise ClosureTree::RootOrderingDisabledError
|
153
|
+
raise ClosureTree::RootOrderingDisabledError, 'Root ordering is disabled on this model'
|
118
154
|
end
|
119
155
|
|
120
156
|
# Make sure self isn't dirty, because we're going to call reload:
|
@@ -122,31 +158,30 @@ module ClosureTree
|
|
122
158
|
|
123
159
|
_ct.with_advisory_lock do
|
124
160
|
prior_sibling_parent = sibling.parent
|
125
|
-
reorder_from_value = if prior_sibling_parent ==
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
sibling.order_value =
|
132
|
-
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
|
133
169
|
sibling._ct_skip_sort_order_maintenance!
|
134
170
|
sibling.save # may be a no-op
|
135
171
|
|
136
172
|
_ct_reorder_siblings(reorder_from_value)
|
137
173
|
|
138
174
|
# The sort order should be correct now except for self and sibling, which may need to flip:
|
139
|
-
sibling_is_after =
|
175
|
+
sibling_is_after = reload.order_value < sibling.reload.order_value
|
140
176
|
if add_after != sibling_is_after
|
141
177
|
# We need to flip the sort orders:
|
142
|
-
self_ov
|
178
|
+
self_ov = order_value
|
179
|
+
sib_ov = sibling.order_value
|
143
180
|
update_order_value(sib_ov)
|
144
181
|
sibling.update_order_value(self_ov)
|
145
182
|
end
|
146
183
|
|
147
|
-
if prior_sibling_parent !=
|
148
|
-
prior_sibling_parent.try(:_ct_reorder_children)
|
149
|
-
end
|
184
|
+
prior_sibling_parent.try(:_ct_reorder_children) if prior_sibling_parent != parent
|
150
185
|
sibling
|
151
186
|
end
|
152
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,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'closure_tree/support_flags'
|
2
4
|
require 'closure_tree/support_attributes'
|
3
5
|
require 'closure_tree/numeric_order_support'
|
4
6
|
require 'closure_tree/active_record_support'
|
5
7
|
require 'closure_tree/hash_tree_support'
|
6
|
-
require '
|
8
|
+
require 'closure_tree/arel_helpers'
|
7
9
|
|
8
10
|
# This class and mixins are an effort to reduce the namespace pollution to models that act_as_tree.
|
9
11
|
module ClosureTree
|
@@ -12,42 +14,40 @@ module ClosureTree
|
|
12
14
|
include ClosureTree::SupportAttributes
|
13
15
|
include ClosureTree::ActiveRecordSupport
|
14
16
|
include ClosureTree::HashTreeSupport
|
17
|
+
include ClosureTree::ArelHelpers
|
15
18
|
|
16
|
-
attr_reader :model_class
|
17
|
-
attr_reader :options
|
19
|
+
attr_reader :model_class, :options
|
18
20
|
|
19
21
|
def initialize(model_class, options)
|
20
22
|
@model_class = model_class
|
23
|
+
|
21
24
|
@options = {
|
22
|
-
:
|
23
|
-
:
|
24
|
-
:
|
25
|
-
:
|
26
|
-
:
|
25
|
+
parent_column_name: 'parent_id',
|
26
|
+
dependent: :nullify, # or :destroy or :delete_all -- see the README
|
27
|
+
name_column: 'name',
|
28
|
+
with_advisory_lock: true, # This will be overridden by adapter support
|
29
|
+
numeric_order: false
|
27
30
|
}.merge(options)
|
28
31
|
raise ArgumentError, "name_column can't be 'path'" if options[:name_column] == 'path'
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
+
|
33
|
+
return unless order_is_numeric?
|
34
|
+
|
35
|
+
extend NumericOrderSupport.adapter_for_connection(connection)
|
32
36
|
end
|
33
37
|
|
34
38
|
def hierarchy_class_for_model
|
35
39
|
parent_class = model_class.module_parent
|
36
40
|
hierarchy_class = parent_class.const_set(short_hierarchy_class_name, Class.new(model_class.superclass))
|
37
|
-
use_attr_accessible = use_attr_accessible?
|
38
|
-
include_forbidden_attributes_protection = include_forbidden_attributes_protection?
|
39
41
|
model_class_name = model_class.to_s
|
40
42
|
hierarchy_class.class_eval do
|
41
|
-
include ActiveModel::ForbiddenAttributesProtection if include_forbidden_attributes_protection
|
42
43
|
belongs_to :ancestor, class_name: model_class_name
|
43
44
|
belongs_to :descendant, class_name: model_class_name
|
44
|
-
attr_accessible :ancestor, :descendant, :generations if use_attr_accessible
|
45
45
|
def ==(other)
|
46
46
|
self.class == other.class && ancestor_id == other.ancestor_id && descendant_id == other.descendant_id
|
47
47
|
end
|
48
48
|
alias :eql? :==
|
49
49
|
def hash
|
50
|
-
ancestor_id.hash << 31 ^ descendant_id.hash
|
50
|
+
(ancestor_id.hash << 31) ^ descendant_id.hash
|
51
51
|
end
|
52
52
|
end
|
53
53
|
hierarchy_class.table_name = hierarchy_table_name
|
@@ -59,21 +59,19 @@ module ClosureTree
|
|
59
59
|
# because they may have overridden the table name, which is what we want to be consistent with
|
60
60
|
# in order for the schema to make sense.
|
61
61
|
tablename = options[:hierarchy_table_name] ||
|
62
|
-
|
62
|
+
"#{remove_prefix_and_suffix(table_name, model_class).singularize}_hierarchies"
|
63
63
|
|
64
64
|
[model_class.table_name_prefix, tablename, model_class.table_name_suffix].join
|
65
65
|
end
|
66
66
|
|
67
67
|
def with_order_option(opts)
|
68
|
-
if order_option?
|
69
|
-
opts[:order] = [opts[:order], order_by].compact.join(",")
|
70
|
-
end
|
68
|
+
opts[:order] = [opts[:order], order_by].compact.join(',') if order_option?
|
71
69
|
opts
|
72
70
|
end
|
73
71
|
|
74
72
|
def scope_with_order(scope, additional_order_by = nil)
|
75
73
|
if order_option?
|
76
|
-
scope.order(*
|
74
|
+
scope.order(*[additional_order_by, order_by].compact)
|
77
75
|
else
|
78
76
|
additional_order_by ? scope.order(additional_order_by) : scope
|
79
77
|
end
|
@@ -81,10 +79,10 @@ module ClosureTree
|
|
81
79
|
|
82
80
|
# lambda-ize the order, but don't apply the default order_option
|
83
81
|
def has_many_order_without_option(order_by_opt)
|
84
|
-
[
|
82
|
+
[-> { order(order_by_opt.call) }]
|
85
83
|
end
|
86
84
|
|
87
|
-
def has_many_order_with_option(order_by_opt=nil)
|
85
|
+
def has_many_order_with_option(order_by_opt = nil)
|
88
86
|
order_options = [order_by_opt, order_by].compact
|
89
87
|
[lambda {
|
90
88
|
order_options = order_options.map { |o| o.is_a?(Proc) ? o.call : o }
|
@@ -105,9 +103,9 @@ module ClosureTree
|
|
105
103
|
end
|
106
104
|
|
107
105
|
def with_advisory_lock(&block)
|
108
|
-
if options[:with_advisory_lock]
|
106
|
+
if options[:with_advisory_lock] && connection.supports_advisory_locks? && model_class.respond_to?(:with_advisory_lock)
|
109
107
|
model_class.with_advisory_lock(advisory_lock_name) do
|
110
|
-
transaction
|
108
|
+
transaction(&block)
|
111
109
|
end
|
112
110
|
else
|
113
111
|
yield
|
@@ -119,7 +117,7 @@ module ClosureTree
|
|
119
117
|
unless path.first.is_a?(Hash)
|
120
118
|
if subclass? && has_inheritance_column?
|
121
119
|
attributes = attributes.with_indifferent_access
|
122
|
-
attributes[inheritance_column] ||=
|
120
|
+
attributes[inheritance_column] ||= sti_name
|
123
121
|
end
|
124
122
|
path = path.map { |ea| attributes.merge(name_column => ea) }
|
125
123
|
end
|
@@ -127,11 +125,9 @@ module ClosureTree
|
|
127
125
|
end
|
128
126
|
|
129
127
|
def scoped_attributes(scope, attributes, target_table = model_class.table_name)
|
130
|
-
table_prefixed_attributes =
|
131
|
-
|
132
|
-
|
133
|
-
end
|
134
|
-
]
|
128
|
+
table_prefixed_attributes = attributes.transform_keys do |column_name|
|
129
|
+
"#{target_table}.#{column_name}"
|
130
|
+
end
|
135
131
|
scope.where(table_prefixed_attributes)
|
136
132
|
end
|
137
133
|
|
@@ -146,6 +142,7 @@ module ClosureTree
|
|
146
142
|
path.in_groups(max_join_tables, false).each do |subpath|
|
147
143
|
child = model_class.find_by_path(subpath, attributes, next_parent_id)
|
148
144
|
return nil if child.nil?
|
145
|
+
|
149
146
|
next_parent_id = child._ct_id
|
150
147
|
end
|
151
148
|
child
|
@@ -164,7 +161,7 @@ module ClosureTree
|
|
164
161
|
end
|
165
162
|
|
166
163
|
def create!(model_class, attributes)
|
167
|
-
create(model_class, attributes).tap
|
164
|
+
create(model_class, attributes).tap(&:save!)
|
168
165
|
end
|
169
166
|
end
|
170
167
|
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,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_record'
|
2
4
|
|
3
5
|
module ClosureTree
|
@@ -14,6 +16,7 @@ module ClosureTree
|
|
14
16
|
autoload :DeterministicOrdering
|
15
17
|
autoload :NumericDeterministicOrdering
|
16
18
|
autoload :Configuration
|
19
|
+
autoload :AdapterSupport
|
17
20
|
|
18
21
|
def self.configure
|
19
22
|
yield configuration
|
@@ -25,6 +28,23 @@ module ClosureTree
|
|
25
28
|
end
|
26
29
|
|
27
30
|
ActiveSupport.on_load :active_record do
|
28
|
-
ActiveRecord::Base.
|
29
|
-
ActiveRecord::Base.
|
31
|
+
ActiveRecord::Base.extend ClosureTree::HasClosureTree
|
32
|
+
ActiveRecord::Base.extend ClosureTree::HasClosureTreeRoot
|
33
|
+
end
|
34
|
+
|
35
|
+
# Adapter injection for different database types
|
36
|
+
ActiveSupport.on_load :active_record_postgresqladapter do
|
37
|
+
prepend ClosureTree::AdapterSupport
|
38
|
+
end
|
39
|
+
|
40
|
+
ActiveSupport.on_load :active_record_mysql2adapter do
|
41
|
+
prepend ClosureTree::AdapterSupport
|
42
|
+
end
|
43
|
+
|
44
|
+
ActiveSupport.on_load :active_record_trilogyadapter do
|
45
|
+
prepend ClosureTree::AdapterSupport
|
46
|
+
end
|
47
|
+
|
48
|
+
ActiveSupport.on_load :active_record_sqlite3adapter do
|
49
|
+
prepend ClosureTree::AdapterSupport
|
30
50
|
end
|
@@ -1,7 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ClosureTree
|
2
4
|
module Generators # :nodoc:
|
3
5
|
class ConfigGenerator < Rails::Generators::Base # :nodoc:
|
4
|
-
source_root File.expand_path('
|
6
|
+
source_root File.expand_path('templates', __dir__)
|
5
7
|
desc 'Install closure tree config.'
|
6
8
|
|
7
9
|
def config
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'closure_tree/active_record_support'
|
2
4
|
require 'forwardable'
|
3
5
|
require 'rails/generators'
|
@@ -36,10 +38,10 @@ module ClosureTree
|
|
36
38
|
|
37
39
|
def ct
|
38
40
|
@ct ||= if target_class.respond_to?(:_ct)
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
41
|
+
target_class._ct
|
42
|
+
else
|
43
|
+
raise "Please RTFM and add the `has_closure_tree` (or `acts_as_tree`) annotation to #{class_name} before creating the migration."
|
44
|
+
end
|
43
45
|
end
|
44
46
|
|
45
47
|
def migration_version
|