closure_tree 9.5.0 → 9.6.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: 6ab4fbc86c1ae73dc78bb959fa0038e88dfa60e91b1f7322707c76bc5bd8583a
4
- data.tar.gz: '08824b7349988634bc2d0c4ab3e3ad7757d4491e1617c68b44359e42aafacd51'
3
+ metadata.gz: 8e3f99ea82d4fd195ea034a9f6fcfdd0ce1ec69e74fa2b62241b68eb74070676
4
+ data.tar.gz: b59d11e3fe4e6236c457776b5b5e7cc7133c77759789fbe816b8185db44855e8
5
5
  SHA512:
6
- metadata.gz: 31d6affbeb9376696c84d6424a55df7dbbab839f37f245da53185ad488a21bbad1871c1ac0f42cddd912388a172af4b7341e3a92287b47622a957b80c9d59061
7
- data.tar.gz: 6ead47a3741c1a6d859771b532391dd8aa29f64554884eb042aca67964643c0cfc009662ad5d2939f090ce1b83fa1dff12d004c5ecaee22d5bf538e783b722e3
6
+ metadata.gz: 2a1d3b69d8397b4f5d8b8ad7e81df74e9577c77dcbd10851b5fdc00a17f2b0b5caee0446fe71b3aaec28d0b79c7a4b8d97a89edcd3ac4599bbbeb4b0d6ddeb21
7
+ data.tar.gz: 88d92d332638b6c38e6575ae14ebfd8ec060c2ef0137f7335b72bda4f84537d7db43032623ac5138d31424753cbf83208e5f7f4de0ce1953fcd03a0d8314fbeb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [9.6.0](https://github.com/ClosureTree/closure_tree/compare/closure_tree/v9.5.0...closure_tree/v9.6.0) (2026-02-16)
4
+
5
+
6
+ ### Features
7
+
8
+ * raise error when advisory lock cannot be acquired within configured timeout ([#480](https://github.com/ClosureTree/closure_tree/issues/480)) ([c030385](https://github.com/ClosureTree/closure_tree/commit/c030385297a9d3042d43354676a794b1c5757d2a))
9
+ * sibling reordering when node changes parent or scope ([#484](https://github.com/ClosureTree/closure_tree/issues/484)) ([254ba36](https://github.com/ClosureTree/closure_tree/commit/254ba360638bf21717d214b7fd328db8ffa167e0))
10
+
3
11
  ## [9.5.0](https://github.com/ClosureTree/closure_tree/compare/closure_tree-v9.3.0...closure_tree/v9.5.0) (2026-01-21)
4
12
 
5
13
 
data/README.md CHANGED
@@ -322,6 +322,7 @@ When you include ```has_closure_tree``` in your model, you can provide a hash to
322
322
  * ```:order``` used to set up [deterministic ordering](#deterministic-ordering)
323
323
  * ```:scope``` restricts root nodes and sibling ordering to specific columns. Can be a single symbol or an array of symbols. Example: ```scope: :user_id``` or ```scope: [:user_id, :group_id]```. This ensures that root nodes and siblings are scoped correctly when reordering. See [Ordering Roots](#ordering-roots) for more details.
324
324
  * ```:touch``` delegates to the `belongs_to` annotation for the parent, so `touch`ing cascades to all children (the performance of this for deep trees isn't currently optimal).
325
+ * ```:advisory_lock_timeout_seconds``` When set, the advisory lock will raise ```WithAdvisoryLock::FailedToAcquireLock``` if the lock cannot be acquired within the timeout period. This helps callers handle timeout scenarios (e.g. retry or fail fast). If the option is not specified, the lock is waited for indefinitely until it is acquired. See [Lock wait timeouts](https://github.com/ClosureTree/with_advisory_lock?tab=readme-ov-file#lock-wait-timeouts) in the with_advisory_lock gem for details.
325
326
 
326
327
  ## Accessing Data
327
328
 
@@ -15,6 +15,7 @@ module ClosureTree
15
15
  :touch,
16
16
  :with_advisory_lock,
17
17
  :advisory_lock_name,
18
+ :advisory_lock_timeout_seconds,
18
19
  :scope
19
20
  )
20
21
 
@@ -18,7 +18,10 @@ module ClosureTree
18
18
  end
19
19
 
20
20
  def _ct_skip_sort_order_maintenance!
21
- @_ct_skip_sort_order_maintenance = true
21
+ ActiveSupport::Deprecation.new.warn(
22
+ '_ct_skip_sort_order_maintenance! is deprecated and will be removed in the next major version. ' \
23
+ 'Sort order maintenance is now handled automatically.'
24
+ )
22
25
  end
23
26
 
24
27
  def _ct_validate
@@ -38,7 +41,17 @@ module ClosureTree
38
41
  end
39
42
 
40
43
  def _ct_after_save
41
- rebuild! if saved_changes[_ct.parent_column_name] || @was_new_record
44
+ scope_changed = _ct.order_is_numeric? && _ct.scope_changed?(self)
45
+
46
+ if saved_changes[_ct.parent_column_name] || @was_new_record
47
+ rebuild!
48
+ elsif scope_changed
49
+ # Scope changed without parent change - reorder old scope's siblings
50
+ _ct_reorder_prior_siblings_if_parent_changed
51
+ _ct_reorder_siblings
52
+ elsif _ct.order_option? && saved_changes[_ct.order_column_sym]
53
+ _ct_reorder_siblings(saved_changes[_ct.order_column_sym].min)
54
+ end
42
55
  if saved_changes[_ct.parent_column_name] && !@was_new_record
43
56
  # Resetting the ancestral collections addresses
44
57
  # https://github.com/mceachen/closure_tree/issues/68
@@ -46,7 +59,6 @@ module ClosureTree
46
59
  self_and_ancestors.reload
47
60
  end
48
61
  @was_new_record = false # we aren't new anymore.
49
- @_ct_skip_sort_order_maintenance = false # only skip once.
50
62
  true # don't cancel anything.
51
63
  end
52
64
 
@@ -86,7 +98,7 @@ module ClosureTree
86
98
  SQL
87
99
  end
88
100
 
89
- if _ct.order_is_numeric? && !@_ct_skip_sort_order_maintenance
101
+ if _ct.order_is_numeric?
90
102
  _ct_reorder_prior_siblings_if_parent_changed
91
103
  # Prevent double-reordering of siblings:
92
104
  _ct_reorder_siblings unless called_by_rebuild
@@ -12,11 +12,16 @@ module ClosureTree
12
12
  end
13
13
 
14
14
  def _ct_reorder_prior_siblings_if_parent_changed
15
- return unless saved_change_to_attribute?(_ct.parent_column_name) && !@was_new_record
15
+ return if @was_new_record
16
+
17
+ parent_changed = saved_change_to_attribute?(_ct.parent_column_name)
18
+ scope_changed = _ct.scope_changed?(self)
19
+
20
+ return unless parent_changed || scope_changed
16
21
 
17
22
  was_parent_id = attribute_before_last_save(_ct.parent_column_name)
18
- scope_conditions = _ct.scope_values_from_instance(self)
19
- _ct.reorder_with_parent_id(was_parent_id, nil, scope_conditions)
23
+ previous_scope_conditions = _ct.previous_scope_values_from_instance(self)
24
+ _ct.reorder_with_parent_id(was_parent_id, nil, previous_scope_conditions)
20
25
  end
21
26
 
22
27
  def _ct_reorder_siblings(minimum_sort_order_value = nil)
@@ -132,9 +137,7 @@ module ClosureTree
132
137
  def prepend_child(child_node)
133
138
  child_node.order_value = -1
134
139
  child_node.parent = self
135
- child_node._ct_skip_sort_order_maintenance!
136
140
  if child_node.save
137
- _ct_reorder_children
138
141
  child_node.reload
139
142
  else
140
143
  child_node
@@ -161,19 +164,11 @@ module ClosureTree
161
164
 
162
165
  _ct.with_advisory_lock do
163
166
  prior_sibling_parent = sibling.parent
164
- reorder_from_value = if prior_sibling_parent == parent
165
- [order_value, sibling.order_value].compact.min
166
- else
167
- order_value
168
- end
169
167
 
170
168
  sibling.order_value = order_value
171
169
  sibling.parent = parent
172
- sibling._ct_skip_sort_order_maintenance!
173
170
  sibling.save # may be a no-op
174
171
 
175
- _ct_reorder_siblings(reorder_from_value)
176
-
177
172
  # The sort order should be correct now except for self and sibling, which may need to flip:
178
173
  sibling_is_after = reload.order_value < sibling.reload.order_value
179
174
  if add_after != sibling_is_after
@@ -19,6 +19,7 @@ module ClosureTree
19
19
  dependent: :nullify, # or :destroy, :delete_all, or :adopt -- see the README
20
20
  name_column: 'name',
21
21
  with_advisory_lock: true, # This will be overridden by adapter support
22
+ advisory_lock_timeout_seconds: nil,
22
23
  numeric_order: false
23
24
  }.merge(options)
24
25
  raise ArgumentError, "name_column can't be 'path'" if options[:name_column] == 'path'
@@ -30,6 +31,10 @@ module ClosureTree
30
31
  end
31
32
  end
32
33
 
34
+ if !@options[:with_advisory_lock] && @options[:advisory_lock_timeout_seconds].present?
35
+ raise ArgumentError, "advisory_lock_timeout_seconds can't be specified when advisory_lock is disabled"
36
+ end
37
+
33
38
  return unless order_is_numeric?
34
39
 
35
40
  extend NumericOrderSupport.adapter_for_connection(connection)
@@ -153,8 +158,9 @@ module ClosureTree
153
158
  end
154
159
 
155
160
  def with_advisory_lock(&block)
156
- if options[:with_advisory_lock] && connection.supports_advisory_locks? && model_class.respond_to?(:with_advisory_lock)
157
- model_class.with_advisory_lock(advisory_lock_name) do
161
+ lock_method = options[:advisory_lock_timeout_seconds].present? ? :with_advisory_lock! : :with_advisory_lock
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
158
164
  transaction(&block)
159
165
  end
160
166
  else
@@ -251,6 +257,45 @@ module ClosureTree
251
257
  scope_hash
252
258
  end
253
259
 
260
+ def previous_scope_values_from_instance(instance)
261
+ return {} unless options[:scope] && instance
262
+
263
+ scope_option = options[:scope]
264
+ scope_hash = {}
265
+
266
+ case scope_option
267
+ when Symbol
268
+ value = instance.attribute_before_last_save(scope_option)
269
+ scope_hash[scope_option] = value
270
+ when Array
271
+ scope_option.each do |item|
272
+ if item.is_a?(Symbol)
273
+ value = instance.attribute_before_last_save(item)
274
+ scope_hash[item] = value
275
+ end
276
+ end
277
+ end
278
+
279
+ scope_hash
280
+ end
281
+
282
+ def scope_changed?(instance)
283
+ return false unless options[:scope] && instance
284
+
285
+ scope_option = options[:scope]
286
+
287
+ case scope_option
288
+ when Symbol
289
+ instance.saved_change_to_attribute?(scope_option)
290
+ when Array
291
+ scope_option.any? do |item|
292
+ item.is_a?(Symbol) && instance.saved_change_to_attribute?(item)
293
+ end
294
+ else
295
+ false
296
+ end
297
+ end
298
+
254
299
  def apply_scope_conditions(scope, instance = nil)
255
300
  return scope unless options[:scope] && instance
256
301
 
@@ -34,6 +34,10 @@ module ClosureTree
34
34
  end
35
35
  end
36
36
 
37
+ def advisory_lock_options
38
+ { timeout_seconds: options[:advisory_lock_timeout_seconds] }.compact
39
+ end
40
+
37
41
  def quoted_table_name
38
42
  connection.quote_table_name(table_name)
39
43
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClosureTree
4
- VERSION = Gem::Version.new('9.5.0')
4
+ VERSION = Gem::Version.new('9.6.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.5.0
4
+ version: 9.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew McEachen
@@ -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.5.0
181
+ documentation_uri: https://www.rubydoc.info/gems/closure_tree/9.6.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'