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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7326f71d858362cf125f0db24593f22441bcfb1c1fc81124036d8d4cdaa7110f
4
- data.tar.gz: 0b14cd7c237b4b48e7f04777c213dcaed3cfe526eb74d26672f1736ee5bf89ec
3
+ metadata.gz: 85721d6e16c245ba1eeb1d4a513464df79e33ec64cf91f6252084ac2c4d49d55
4
+ data.tar.gz: aafd7ee3e237653140f31e35d299ad3d9afc21006708c6e2425df7909222ae0b
5
5
  SHA512:
6
- metadata.gz: 53ae09bebff724ee6540d5b274330ef489726dfd690a164774a140382bc631ee0e10a8f4ad522e04f8a997714567bb2151d514b203aa6b14bc24785507a3c660
7
- data.tar.gz: 14cc8606e54f4adf1de9331edc0833e7e4521a3b8a5fbb0bf9a99dad2fe619c69e6f22362f549ee44c36b21785ec14a0804a0ed3dd83976ad4350e839a6aba61
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-specific locks
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.5.0'
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'
@@ -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
- alias_table = Arel::Table.new(table_name).alias(alias_name)
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
@@ -162,7 +162,7 @@ module ClosureTree
162
162
  # Make sure self isn't dirty, because we're going to call reload:
163
163
  save
164
164
 
165
- _ct.with_advisory_lock do
165
+ _ct.with_advisory_lock(self) do
166
166
  prior_sibling_parent = sibling.parent
167
167
 
168
168
  sibling.order_value = order_value
@@ -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
- model_class.public_send(lock_method, advisory_lock_name, advisory_lock_options) do
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.in_groups(max_join_tables, false).each do |subpath|
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
- # Allow dynamic generation via proc
17
- options[:advisory_lock_name].call(base_class)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClosureTree
4
- VERSION = Gem::Version.new('9.6.2')
4
+ VERSION = Gem::Version.new('9.8.0')
5
5
  end
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.6.2
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.5.0
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.5.0
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.6.2
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: 3.6.9
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: []