closure_tree 9.6.2 → 9.8.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 +22 -0
- data/README.md +23 -6
- data/closure_tree.gemspec +1 -1
- data/lib/closure_tree/finders.rb +1 -1
- data/lib/closure_tree/hierarchy_maintenance.rb +6 -4
- data/lib/closure_tree/numeric_deterministic_ordering.rb +1 -1
- data/lib/closure_tree/support.rb +14 -3
- data/lib/closure_tree/support_attributes.rb +3 -9
- data/lib/closure_tree/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85721d6e16c245ba1eeb1d4a513464df79e33ec64cf91f6252084ac2c4d49d55
|
|
4
|
+
data.tar.gz: aafd7ee3e237653140f31e35d299ad3d9afc21006708c6e2425df7909222ae0b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 755dc5dedfca297b47841f9bebd4fdd2ca7cf84f57828850290c7981a764a127a2acbf65175e8ed0be583f2e6620112fdf37f0690b9e174c93e26ee8265d051b
|
|
7
|
+
data.tar.gz: 5369312577a8c88ea1430258340171772dd40c6efabe69d5eafdf304cea4f678865a982cc153a07bdfdc234edcf63f7607825758e265b5f9c4483dd781d6938c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [9.8.0](https://github.com/ClosureTree/closure_tree/compare/closure_tree/v9.7.0...closure_tree/v9.8.0) (2026-08-05)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* require with_advisory_lock 7.6.0 ([27d9f5e](https://github.com/ClosureTree/closure_tree/commit/27d9f5e268afb266e53fddac21100bfc67ab1d83))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* batch find_by_large_path in groups of max_join_tables elements ([7e05aa1](https://github.com/ClosureTree/closure_tree/commit/7e05aa1cd1771278f920a3dce917da36db456c1f)), closes [#347](https://github.com/ClosureTree/closure_tree/issues/347)
|
|
14
|
+
* materialize transaction before taking transaction-level advisory lock ([a2ea522](https://github.com/ClosureTree/closure_tree/commit/a2ea5223ba69195add5f6361c6fa3af2a76d37b8))
|
|
15
|
+
* use model arel_table in cleanup! for Rails 8.2 compatibility ([f46abc7](https://github.com/ClosureTree/closure_tree/commit/f46abc7e540a3cc53ea17b7ffa2a9abd7724ecdd))
|
|
16
|
+
* use transaction-level advisory locks inside open transactions on PostgreSQL ([b4eb3df](https://github.com/ClosureTree/closure_tree/commit/b4eb3df89dd4f7a4151a7f6204842661b219b241))
|
|
17
|
+
|
|
18
|
+
## [9.7.0](https://github.com/ClosureTree/closure_tree/compare/closure_tree/v9.6.2...closure_tree/v9.7.0) (2026-05-06)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* allow advisory_lock_name proc to receive instance for per-tenant lock granularity ([#491](https://github.com/ClosureTree/closure_tree/issues/491)) ([a7a554c](https://github.com/ClosureTree/closure_tree/commit/a7a554c004fb865ab4ee721f929a8a00bb5cdc46))
|
|
24
|
+
|
|
3
25
|
## [9.6.2](https://github.com/ClosureTree/closure_tree/compare/closure_tree/v9.6.1...closure_tree/v9.6.2) (2026-04-07)
|
|
4
26
|
|
|
5
27
|
|
data/README.md
CHANGED
|
@@ -109,9 +109,6 @@ Note that closure_tree only supports ActiveRecord 7.2 and later, and has test co
|
|
|
109
109
|
|
|
110
110
|
If you're starting from scratch you don't need to call `rebuild!`.
|
|
111
111
|
|
|
112
|
-
NOTE: Run `rails g closure_tree:config` to create an initializer with extra
|
|
113
|
-
configurations. (Optional)
|
|
114
|
-
|
|
115
112
|
## Warning
|
|
116
113
|
|
|
117
114
|
As stated above, using multiple hierarchy gems (like `ancestry` or `nested set`) on the same model
|
|
@@ -560,7 +557,7 @@ class Tag < ApplicationRecord
|
|
|
560
557
|
has_closure_tree advisory_lock_name: 'custom_tag_lock'
|
|
561
558
|
end
|
|
562
559
|
|
|
563
|
-
# Dynamic via Proc
|
|
560
|
+
# Dynamic via Proc (1-arity: receives model class only)
|
|
564
561
|
class Tag < ApplicationRecord
|
|
565
562
|
has_closure_tree advisory_lock_name: ->(model_class) { "#{Rails.env}_#{model_class.name.underscore}" }
|
|
566
563
|
end
|
|
@@ -568,16 +565,36 @@ end
|
|
|
568
565
|
# Delegate to model method
|
|
569
566
|
class Tag < ApplicationRecord
|
|
570
567
|
has_closure_tree advisory_lock_name: :custom_lock_name
|
|
571
|
-
|
|
568
|
+
|
|
572
569
|
def self.custom_lock_name
|
|
573
570
|
"tag_lock_#{current_tenant_id}"
|
|
574
571
|
end
|
|
575
572
|
end
|
|
576
573
|
```
|
|
577
574
|
|
|
575
|
+
#### Per-instance lock names (multi-tenant / scoped models)
|
|
576
|
+
|
|
577
|
+
Pass a 2-arity proc to receive both the model class and the current record instance.
|
|
578
|
+
This is the recommended approach for scoped models where each tenant should have its own lock,
|
|
579
|
+
avoiding unnecessary serialization across tenants.
|
|
580
|
+
|
|
581
|
+
```ruby
|
|
582
|
+
class Node < ApplicationRecord
|
|
583
|
+
has_closure_tree scope: :company_id,
|
|
584
|
+
advisory_lock_name: ->(klass, instance) {
|
|
585
|
+
company = instance&.company_id
|
|
586
|
+
company ? "ct_#{klass.name}_#{company}" : "ct_#{klass.name}"
|
|
587
|
+
}
|
|
588
|
+
end
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
When `instance` is `nil` (class-level operations like `Node.rebuild!`), the proc should
|
|
592
|
+
fall back to a model-wide name. Instance-level operations (`save`, `destroy`, `add_sibling`,
|
|
593
|
+
`find_or_create_by_path`) pass the record itself so the lock is scoped to that tenant.
|
|
594
|
+
|
|
578
595
|
This is particularly useful when:
|
|
579
596
|
* You need environment-specific lock names
|
|
580
|
-
* You're using multi-tenancy and need tenant-
|
|
597
|
+
* You're using multi-tenancy and need per-tenant locks (avoiding cross-tenant contention)
|
|
581
598
|
* You want to avoid lock name collisions between similar model names
|
|
582
599
|
|
|
583
600
|
## Multi-Database Support
|
data/closure_tree.gemspec
CHANGED
|
@@ -26,7 +26,7 @@ Gem::Specification.new do |gem|
|
|
|
26
26
|
gem.required_ruby_version = '>= 3.3.0'
|
|
27
27
|
|
|
28
28
|
gem.add_dependency 'activerecord', '>= 7.2.0'
|
|
29
|
-
gem.add_dependency 'with_advisory_lock', '>= 7.
|
|
29
|
+
gem.add_dependency 'with_advisory_lock', '>= 7.6.0'
|
|
30
30
|
gem.add_dependency 'zeitwerk', '~> 2.7'
|
|
31
31
|
|
|
32
32
|
gem.add_development_dependency 'database_cleaner'
|
data/lib/closure_tree/finders.rb
CHANGED
|
@@ -20,7 +20,7 @@ module ClosureTree
|
|
|
20
20
|
return found if found
|
|
21
21
|
|
|
22
22
|
attrs = subpath.shift
|
|
23
|
-
_ct.with_advisory_lock do
|
|
23
|
+
_ct.with_advisory_lock(self) do
|
|
24
24
|
# shenanigans because children.create is bound to the superclass
|
|
25
25
|
# (in the case of polymorphism):
|
|
26
26
|
child = children.where(attrs).first || begin
|
|
@@ -64,7 +64,7 @@ module ClosureTree
|
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def _ct_before_destroy
|
|
67
|
-
_ct.with_advisory_lock do
|
|
67
|
+
_ct.with_advisory_lock(self) do
|
|
68
68
|
_ct_adopt_children_to_grandparent if _ct.options[:dependent] == :adopt
|
|
69
69
|
delete_hierarchy_references
|
|
70
70
|
self.class.find(id).children.find_each(&:rebuild!) if _ct.options[:dependent] == :nullify
|
|
@@ -86,7 +86,7 @@ module ClosureTree
|
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def rebuild!(called_by_rebuild = false)
|
|
89
|
-
_ct.with_advisory_lock do
|
|
89
|
+
_ct.with_advisory_lock(self) do
|
|
90
90
|
delete_hierarchy_references unless (defined? @was_new_record) && @was_new_record
|
|
91
91
|
hierarchy_class.create!(ancestor: self, descendant: self, generations: 0)
|
|
92
92
|
unless root?
|
|
@@ -112,7 +112,7 @@ module ClosureTree
|
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
def delete_hierarchy_references
|
|
115
|
-
_ct.with_advisory_lock do
|
|
115
|
+
_ct.with_advisory_lock(self) do
|
|
116
116
|
# The crazy double-wrapped sub-subselect works around MySQL's limitation of subselects on the same table that is being mutated.
|
|
117
117
|
# It shouldn't affect performance of postgresql.
|
|
118
118
|
# See http://dev.mysql.com/doc/refman/5.0/en/subquery-errors.html
|
|
@@ -140,7 +140,9 @@ module ClosureTree
|
|
|
140
140
|
|
|
141
141
|
%i[descendant_id ancestor_id].each do |foreign_key|
|
|
142
142
|
alias_name = "#{foreign_key.to_s.split('_').first}s"
|
|
143
|
-
|
|
143
|
+
# Rails 8.2 made Arel::Table.new keyword-only; the model's own arel_table
|
|
144
|
+
# is equivalent and works across all supported Rails versions.
|
|
145
|
+
alias_table = arel_table.alias(alias_name)
|
|
144
146
|
arel_join = hierarchy_table.join(alias_table, Arel::Nodes::OuterJoin)
|
|
145
147
|
.on(alias_table[primary_key].eq(hierarchy_table[foreign_key]))
|
|
146
148
|
.join_sources
|
data/lib/closure_tree/support.rb
CHANGED
|
@@ -157,10 +157,15 @@ module ClosureTree
|
|
|
157
157
|
" AND #{conditions.join(' AND ')}"
|
|
158
158
|
end
|
|
159
159
|
|
|
160
|
-
def with_advisory_lock(&block)
|
|
160
|
+
def with_advisory_lock(instance = nil, &block)
|
|
161
161
|
lock_method = options[:advisory_lock_timeout_seconds].present? ? :with_advisory_lock! : :with_advisory_lock
|
|
162
162
|
if options[:with_advisory_lock] && connection.supports_advisory_locks? && model_class.respond_to?(lock_method)
|
|
163
|
-
|
|
163
|
+
lock_options = advisory_lock_options
|
|
164
|
+
if use_transaction_level_lock?
|
|
165
|
+
connection.materialize_transactions
|
|
166
|
+
lock_options = lock_options.merge(transaction: true)
|
|
167
|
+
end
|
|
168
|
+
model_class.public_send(lock_method, advisory_lock_name(instance), lock_options) do
|
|
164
169
|
transaction(&block)
|
|
165
170
|
end
|
|
166
171
|
else
|
|
@@ -168,6 +173,12 @@ module ClosureTree
|
|
|
168
173
|
end
|
|
169
174
|
end
|
|
170
175
|
|
|
176
|
+
private def use_transaction_level_lock?
|
|
177
|
+
return false unless connection.adapter_name.match?(/postg/i)
|
|
178
|
+
|
|
179
|
+
connection.open_transactions == 1 && connection.current_transaction.joinable?
|
|
180
|
+
end
|
|
181
|
+
|
|
171
182
|
def build_ancestry_attr_path(path, attributes)
|
|
172
183
|
path = path.is_a?(Array) ? path.dup : [path]
|
|
173
184
|
unless path.first.is_a?(Hash)
|
|
@@ -195,7 +206,7 @@ module ClosureTree
|
|
|
195
206
|
def find_by_large_path(path, attributes = {}, parent_id = nil)
|
|
196
207
|
next_parent_id = parent_id
|
|
197
208
|
child = nil
|
|
198
|
-
path.
|
|
209
|
+
path.in_groups_of(max_join_tables, false).each do |subpath|
|
|
199
210
|
child = model_class.find_by_path(subpath, attributes, next_parent_id)
|
|
200
211
|
return nil if child.nil?
|
|
201
212
|
|
|
@@ -8,28 +8,22 @@ module ClosureTree
|
|
|
8
8
|
extend Forwardable
|
|
9
9
|
def_delegators :model_class, :connection, :transaction, :table_name, :base_class, :inheritance_column, :column_names
|
|
10
10
|
|
|
11
|
-
def advisory_lock_name
|
|
12
|
-
# Allow customization via options or instance method
|
|
11
|
+
def advisory_lock_name(instance = nil)
|
|
13
12
|
if options[:advisory_lock_name]
|
|
14
13
|
case options[:advisory_lock_name]
|
|
15
14
|
when Proc
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
proc = options[:advisory_lock_name]
|
|
16
|
+
proc.arity == 1 ? proc.call(base_class) : proc.call(base_class, instance)
|
|
18
17
|
when Symbol
|
|
19
|
-
# Allow delegation to a model method
|
|
20
18
|
if model_class.respond_to?(options[:advisory_lock_name])
|
|
21
19
|
model_class.send(options[:advisory_lock_name])
|
|
22
20
|
else
|
|
23
21
|
raise ArgumentError, "Model #{model_class} does not respond to #{options[:advisory_lock_name]}"
|
|
24
22
|
end
|
|
25
23
|
else
|
|
26
|
-
# Use static string value
|
|
27
24
|
options[:advisory_lock_name].to_s
|
|
28
25
|
end
|
|
29
26
|
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
27
|
"ct_#{Zlib.crc32(base_class.name.to_s).to_s(16)}"
|
|
34
28
|
end
|
|
35
29
|
end
|
data/lib/closure_tree/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: closure_tree
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 9.
|
|
4
|
+
version: 9.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matthew McEachen
|
|
@@ -30,14 +30,14 @@ dependencies:
|
|
|
30
30
|
requirements:
|
|
31
31
|
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 7.
|
|
33
|
+
version: 7.6.0
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 7.
|
|
40
|
+
version: 7.6.0
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: zeitwerk
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -178,7 +178,7 @@ licenses:
|
|
|
178
178
|
metadata:
|
|
179
179
|
bug_tracker_uri: https://github.com/ClosureTree/closure_tree/issues
|
|
180
180
|
changelog_uri: https://github.com/ClosureTree/closure_tree/blob/master/CHANGELOG.md
|
|
181
|
-
documentation_uri: https://www.rubydoc.info/gems/closure_tree/9.
|
|
181
|
+
documentation_uri: https://www.rubydoc.info/gems/closure_tree/9.8.0
|
|
182
182
|
homepage_uri: https://closuretree.github.io/closure_tree/
|
|
183
183
|
source_code_uri: https://github.com/ClosureTree/closure_tree
|
|
184
184
|
rubygems_mfa_required: 'true'
|
|
@@ -196,7 +196,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
196
196
|
- !ruby/object:Gem::Version
|
|
197
197
|
version: '0'
|
|
198
198
|
requirements: []
|
|
199
|
-
rubygems_version:
|
|
199
|
+
rubygems_version: 4.0.10
|
|
200
200
|
specification_version: 4
|
|
201
201
|
summary: Easily and efficiently make your ActiveRecord model support hierarchies
|
|
202
202
|
test_files: []
|