closure_tree 9.2.0 → 9.5.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 +30 -0
- data/README.md +24 -1
- data/closure_tree.gemspec +2 -2
- data/lib/closure_tree/arel_helpers.rb +8 -0
- data/lib/closure_tree/association_setup.rb +2 -2
- data/lib/closure_tree/has_closure_tree.rb +2 -1
- data/lib/closure_tree/hierarchy_maintenance.rb +15 -1
- data/lib/closure_tree/model.rb +3 -1
- data/lib/closure_tree/numeric_deterministic_ordering.rb +6 -3
- data/lib/closure_tree/numeric_order_support.rb +12 -5
- data/lib/closure_tree/support.rb +89 -2
- data/lib/closure_tree/version.rb +1 -1
- metadata +8 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ab4fbc86c1ae73dc78bb959fa0038e88dfa60e91b1f7322707c76bc5bd8583a
|
|
4
|
+
data.tar.gz: '08824b7349988634bc2d0c4ab3e3ad7757d4491e1617c68b44359e42aafacd51'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 31d6affbeb9376696c84d6424a55df7dbbab839f37f245da53185ad488a21bbad1871c1ac0f42cddd912388a172af4b7341e3a92287b47622a957b80c9d59061
|
|
7
|
+
data.tar.gz: 6ead47a3741c1a6d859771b532391dd8aa29f64554884eb042aca67964643c0cfc009662ad5d2939f090ce1b83fa1dff12d004c5ecaee22d5bf538e783b722e3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [9.5.0](https://github.com/ClosureTree/closure_tree/compare/closure_tree-v9.3.0...closure_tree/v9.5.0) (2026-01-21)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* Add `dependent: :adopt` option for `has_closure_tree` ([#471](https://github.com/ClosureTree/closure_tree/issues/471)) ([d47d7c9](https://github.com/ClosureTree/closure_tree/commit/d47d7c93b59327112a68a7f8454a278877b6ba77))
|
|
9
|
+
* add PostgreSQL schema-qualified table name support ([#462](https://github.com/ClosureTree/closure_tree/issues/462)) ([5f9006c](https://github.com/ClosureTree/closure_tree/commit/5f9006cece95a76f665cb50c2615317e3fa48586))
|
|
10
|
+
* Add runtime advisory lock name customization and multi-database documentation ([#454](https://github.com/ClosureTree/closure_tree/issues/454)) ([d6ffd73](https://github.com/ClosureTree/closure_tree/commit/d6ffd7381e25a28f7a4742bfa2d9c893f0115395))
|
|
11
|
+
* migrate from ActiveSupport::Autoload to Zeitwerk ([#457](https://github.com/ClosureTree/closure_tree/issues/457)) ([d18e80c](https://github.com/ClosureTree/closure_tree/commit/d18e80cdbd4f3510377363bc7b5166f0cc1b0a6f))
|
|
12
|
+
* rewrite with clean api ([#451](https://github.com/ClosureTree/closure_tree/issues/451)) ([f56f2e1](https://github.com/ClosureTree/closure_tree/commit/f56f2e1a3490bb8a099cea8f80b676945fce1c2e))
|
|
13
|
+
* use with_advisory_lock that support rails 8.2 ([#479](https://github.com/ClosureTree/closure_tree/issues/479)) ([bca6231](https://github.com/ClosureTree/closure_tree/commit/bca623168d3255f57e186a3bdcd39e00d38e2a7c))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* add implicit_order_column for Rails 8.1+ compatibility ([#464](https://github.com/ClosureTree/closure_tree/issues/464)) ([f384303](https://github.com/ClosureTree/closure_tree/commit/f38430334a79ea15d236f9212118dcb5e4530746))
|
|
19
|
+
* configure release-please to recognize v8.0.0 release ([#455](https://github.com/ClosureTree/closure_tree/issues/455)) ([fc34f21](https://github.com/ClosureTree/closure_tree/commit/fc34f2148570afd83608b07a3f5282e5fd475783))
|
|
20
|
+
* eager loading of hierarchies by defining the primary key ([#465](https://github.com/ClosureTree/closure_tree/issues/465)) ([8c7e490](https://github.com/ClosureTree/closure_tree/commit/8c7e490ff89239e44aee71577288bff0e177a8d6))
|
|
21
|
+
* hierarchy class inheritance to avoid STI validations ([#392](https://github.com/ClosureTree/closure_tree/issues/392)) ([#472](https://github.com/ClosureTree/closure_tree/issues/472)) ([73b07a6](https://github.com/ClosureTree/closure_tree/commit/73b07a6cfeec226af16f51ce7c9c6c1d6d921b3d))
|
|
22
|
+
* improve api usage ([#475](https://github.com/ClosureTree/closure_tree/issues/475)) ([cde4d29](https://github.com/ClosureTree/closure_tree/commit/cde4d292236b4267ac4c0be2e2b21092a8b6298c))
|
|
23
|
+
* nil scope values being excluded from scope filtering ([#476](https://github.com/ClosureTree/closure_tree/issues/476)) ([f45880e](https://github.com/ClosureTree/closure_tree/commit/f45880e9765bfa3c1f171b7386bf5a5584208ad7))
|
|
24
|
+
* restore proper deprecation for database_less configuration ([#459](https://github.com/ClosureTree/closure_tree/issues/459)) ([de8b402](https://github.com/ClosureTree/closure_tree/commit/de8b40233d3de5243afce7ec9de9ad26c2eee181))
|
|
25
|
+
|
|
26
|
+
## [9.3.0](https://github.com/ClosureTree/closure_tree/compare/closure_tree/v9.2.0...closure_tree/v9.3.0) (2025-11-19)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Features
|
|
30
|
+
|
|
31
|
+
* eager loading of hierarchies by defining the primary key ([#465](https://github.com/ClosureTree/closure_tree/issues/465)) ([8c7e490](https://github.com/ClosureTree/closure_tree/commit/8c7e490ff89239e44aee71577288bff0e177a8d6))
|
|
32
|
+
|
|
3
33
|
## [9.2.0](https://github.com/ClosureTree/closure_tree/compare/closure_tree/v9.1.1...closure_tree/v9.2.0) (2025-10-17)
|
|
4
34
|
|
|
5
35
|
|
data/README.md
CHANGED
|
@@ -314,11 +314,13 @@ When you include ```has_closure_tree``` in your model, you can provide a hash to
|
|
|
314
314
|
* ```:hierarchy_table_name``` to override the hierarchy table name. This defaults to the singular name of the model + "_hierarchies", like ```tag_hierarchies```.
|
|
315
315
|
* ```:dependent``` determines what happens when a node is destroyed. Defaults to ```nullify```.
|
|
316
316
|
* ```:nullify``` will simply set the parent column to null. Each child node will be considered a "root" node. This is the default.
|
|
317
|
+
* ```:adopt``` will move children to their grandparent (parent's parent). If there is no grandparent, children become root nodes. This is useful for maintaining tree structure when removing intermediate nodes.
|
|
317
318
|
* ```:delete_all``` will delete all descendant nodes (which circumvents the destroy hooks)
|
|
318
319
|
* ```:destroy``` will destroy all descendant nodes (which runs the destroy hooks on each child node)
|
|
319
320
|
* ```nil``` does nothing with descendant nodes
|
|
320
321
|
* ```:name_column``` used by #```find_or_create_by_path```, #```find_by_path```, and ```ancestry_path``` instance methods. This is primarily useful if the model only has one required field (like a "tag").
|
|
321
322
|
* ```:order``` used to set up [deterministic ordering](#deterministic-ordering)
|
|
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.
|
|
322
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).
|
|
323
325
|
|
|
324
326
|
## Accessing Data
|
|
@@ -491,9 +493,30 @@ table. So for instance if you have 5 nodes with no parent, they will be ordered
|
|
|
491
493
|
If your model represents many separate trees and you have a lot of records, this can cause performance
|
|
492
494
|
problems, and doesn't really make much sense.
|
|
493
495
|
|
|
494
|
-
You can
|
|
496
|
+
You can scope root nodes and sibling ordering by passing the `scope` option:
|
|
495
497
|
|
|
498
|
+
```ruby
|
|
499
|
+
class Block < ApplicationRecord
|
|
500
|
+
has_closure_tree order: 'sort_order', numeric_order: true, scope: :user_id
|
|
501
|
+
end
|
|
496
502
|
```
|
|
503
|
+
|
|
504
|
+
This ensures that:
|
|
505
|
+
* Root nodes are scoped by the specified columns. You can filter roots like: ```Block.roots.where(user_id: 123)```
|
|
506
|
+
* Sibling reordering only affects nodes with the same scope values
|
|
507
|
+
* Children reordering respects the parent's scope values
|
|
508
|
+
|
|
509
|
+
You can also scope by multiple columns:
|
|
510
|
+
|
|
511
|
+
```ruby
|
|
512
|
+
class Block < ApplicationRecord
|
|
513
|
+
has_closure_tree order: 'sort_order', numeric_order: true, scope: [:user_id, :group_id]
|
|
514
|
+
end
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Alternatively, you can disable root ordering entirely by passing `dont_order_roots: true`:
|
|
518
|
+
|
|
519
|
+
```ruby
|
|
497
520
|
has_closure_tree order: 'sort_order', numeric_order: true, dont_order_roots: true
|
|
498
521
|
```
|
|
499
522
|
|
data/closure_tree.gemspec
CHANGED
|
@@ -26,11 +26,11 @@ 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.5.0'
|
|
30
30
|
gem.add_dependency 'zeitwerk', '~> 2.7'
|
|
31
31
|
|
|
32
32
|
gem.add_development_dependency 'database_cleaner'
|
|
33
|
-
gem.add_development_dependency 'minitest'
|
|
33
|
+
gem.add_development_dependency 'minitest', '~> 5.0'
|
|
34
34
|
gem.add_development_dependency 'minitest-reporters'
|
|
35
35
|
gem.add_development_dependency 'parallel'
|
|
36
36
|
gem.add_development_dependency 'simplecov'
|
|
@@ -79,5 +79,13 @@ module ClosureTree
|
|
|
79
79
|
|
|
80
80
|
delete_manager
|
|
81
81
|
end
|
|
82
|
+
|
|
83
|
+
# Convert an Arel AST to SQL using the correct connection's visitor
|
|
84
|
+
# This ensures proper quoting for the specific database adapter (MySQL uses backticks, PostgreSQL uses double quotes)
|
|
85
|
+
def to_sql_with_connection(arel_manager)
|
|
86
|
+
collector = Arel::Collectors::SQLString.new
|
|
87
|
+
visitor = connection.visitor
|
|
88
|
+
visitor.accept(arel_manager.ast, collector).value
|
|
89
|
+
end
|
|
82
90
|
end
|
|
83
91
|
end
|
|
@@ -20,7 +20,7 @@ module ClosureTree
|
|
|
20
20
|
|
|
21
21
|
has_many :children, *_ct.has_many_order_with_option, class_name: _ct.model_class.to_s,
|
|
22
22
|
foreign_key: _ct.parent_column_name,
|
|
23
|
-
dependent: _ct.options[:dependent],
|
|
23
|
+
dependent: _ct.options[:dependent] == :adopt ? :nullify : _ct.options[:dependent],
|
|
24
24
|
inverse_of: :parent do
|
|
25
25
|
# We have to redefine hash_tree because the activerecord relation is already scoped to parent_id.
|
|
26
26
|
def hash_tree(options = {})
|
|
@@ -47,4 +47,4 @@ module ClosureTree
|
|
|
47
47
|
source: :descendant
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
|
-
end
|
|
50
|
+
end
|
|
@@ -52,12 +52,26 @@ module ClosureTree
|
|
|
52
52
|
|
|
53
53
|
def _ct_before_destroy
|
|
54
54
|
_ct.with_advisory_lock do
|
|
55
|
+
_ct_adopt_children_to_grandparent if _ct.options[:dependent] == :adopt
|
|
55
56
|
delete_hierarchy_references
|
|
56
57
|
self.class.find(id).children.find_each(&:rebuild!) if _ct.options[:dependent] == :nullify
|
|
57
58
|
end
|
|
58
59
|
true # don't prevent destruction
|
|
59
60
|
end
|
|
60
61
|
|
|
62
|
+
private def _ct_adopt_children_to_grandparent
|
|
63
|
+
grandparent_id = read_attribute(_ct.parent_column_name)
|
|
64
|
+
children_ids = self.class.where(_ct.parent_column_name => id).pluck(:id)
|
|
65
|
+
|
|
66
|
+
return if children_ids.empty?
|
|
67
|
+
|
|
68
|
+
# Update all children's parent_id in a single query
|
|
69
|
+
self.class.where(id: children_ids).update_all(_ct.parent_column_name => grandparent_id)
|
|
70
|
+
|
|
71
|
+
# Rebuild hierarchy for each child
|
|
72
|
+
self.class.where(id: children_ids).find_each(&:rebuild!)
|
|
73
|
+
end
|
|
74
|
+
|
|
61
75
|
def rebuild!(called_by_rebuild = false)
|
|
62
76
|
_ct.with_advisory_lock do
|
|
63
77
|
delete_hierarchy_references unless (defined? @was_new_record) && @was_new_record
|
|
@@ -93,7 +107,7 @@ module ClosureTree
|
|
|
93
107
|
|
|
94
108
|
hierarchy_table = hierarchy_class.arel_table
|
|
95
109
|
delete_query = _ct.build_hierarchy_delete_query(hierarchy_table, id)
|
|
96
|
-
_ct.connection.execute(delete_query
|
|
110
|
+
_ct.connection.execute(_ct.to_sql_with_connection(delete_query))
|
|
97
111
|
end
|
|
98
112
|
end
|
|
99
113
|
|
data/lib/closure_tree/model.rb
CHANGED
|
@@ -80,7 +80,9 @@ module ClosureTree
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def self_and_siblings
|
|
83
|
-
_ct.
|
|
83
|
+
scope = _ct.base_class.where(_ct.parent_column_sym => _ct_parent_id)
|
|
84
|
+
scope = _ct.apply_scope_conditions(scope, self)
|
|
85
|
+
_ct.scope_with_order(scope)
|
|
84
86
|
end
|
|
85
87
|
|
|
86
88
|
def siblings
|
|
@@ -15,16 +15,19 @@ module ClosureTree
|
|
|
15
15
|
return unless saved_change_to_attribute?(_ct.parent_column_name) && !@was_new_record
|
|
16
16
|
|
|
17
17
|
was_parent_id = attribute_before_last_save(_ct.parent_column_name)
|
|
18
|
-
_ct.
|
|
18
|
+
scope_conditions = _ct.scope_values_from_instance(self)
|
|
19
|
+
_ct.reorder_with_parent_id(was_parent_id, nil, scope_conditions)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def _ct_reorder_siblings(minimum_sort_order_value = nil)
|
|
22
|
-
_ct.
|
|
23
|
+
scope_conditions = _ct.scope_values_from_instance(self)
|
|
24
|
+
_ct.reorder_with_parent_id(_ct_parent_id, minimum_sort_order_value, scope_conditions)
|
|
23
25
|
reload unless destroyed?
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def _ct_reorder_children(minimum_sort_order_value = nil)
|
|
27
|
-
_ct.
|
|
29
|
+
scope_conditions = _ct.scope_values_from_instance(self)
|
|
30
|
+
_ct.reorder_with_parent_id(_ct_id, minimum_sort_order_value, scope_conditions)
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
def self_and_descendants_preordered
|
|
@@ -14,7 +14,7 @@ module ClosureTree
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
module MysqlAdapter
|
|
17
|
-
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
|
|
17
|
+
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil, scope_conditions = {})
|
|
18
18
|
return if parent_id.nil? && dont_order_roots
|
|
19
19
|
|
|
20
20
|
min_where = if minimum_sort_order_value
|
|
@@ -22,18 +22,21 @@ module ClosureTree
|
|
|
22
22
|
else
|
|
23
23
|
''
|
|
24
24
|
end
|
|
25
|
+
|
|
26
|
+
scope_where = build_scope_where_clause(scope_conditions)
|
|
27
|
+
|
|
25
28
|
connection.execute 'SET @i = 0'
|
|
26
29
|
connection.execute <<-SQL.squish
|
|
27
30
|
UPDATE #{quoted_table_name}
|
|
28
31
|
SET #{quoted_order_column} = (@i := @i + 1) + #{minimum_sort_order_value.to_i - 1}
|
|
29
|
-
WHERE #{where_eq(parent_column_name, parent_id)} #{min_where}
|
|
32
|
+
WHERE #{where_eq(parent_column_name, parent_id)} #{min_where}#{scope_where}
|
|
30
33
|
ORDER BY #{nulls_last_order_by}
|
|
31
34
|
SQL
|
|
32
35
|
end
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
module PostgreSQLAdapter
|
|
36
|
-
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
|
|
39
|
+
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil, scope_conditions = {})
|
|
37
40
|
return if parent_id.nil? && dont_order_roots
|
|
38
41
|
|
|
39
42
|
min_where = if minimum_sort_order_value
|
|
@@ -41,13 +44,16 @@ module ClosureTree
|
|
|
41
44
|
else
|
|
42
45
|
''
|
|
43
46
|
end
|
|
47
|
+
|
|
48
|
+
scope_where = build_scope_where_clause(scope_conditions)
|
|
49
|
+
|
|
44
50
|
connection.execute <<-SQL.squish
|
|
45
51
|
UPDATE #{quoted_table_name}
|
|
46
52
|
SET #{quoted_order_column(false)} = t.seq + #{minimum_sort_order_value.to_i - 1}
|
|
47
53
|
FROM (
|
|
48
54
|
SELECT #{quoted_id_column_name} AS id, row_number() OVER(ORDER BY #{order_by}) AS seq
|
|
49
55
|
FROM #{quoted_table_name}
|
|
50
|
-
WHERE #{where_eq(parent_column_name, parent_id)} #{min_where}
|
|
56
|
+
WHERE #{where_eq(parent_column_name, parent_id)} #{min_where}#{scope_where}
|
|
51
57
|
) AS t
|
|
52
58
|
WHERE #{quoted_table_name}.#{quoted_id_column_name} = t.id and
|
|
53
59
|
#{quoted_table_name}.#{quoted_order_column(false)} is distinct from t.seq + #{minimum_sort_order_value.to_i - 1}
|
|
@@ -60,12 +66,13 @@ module ClosureTree
|
|
|
60
66
|
end
|
|
61
67
|
|
|
62
68
|
module GenericAdapter
|
|
63
|
-
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
|
|
69
|
+
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil, scope_conditions = {})
|
|
64
70
|
return if parent_id.nil? && dont_order_roots
|
|
65
71
|
|
|
66
72
|
scope = model_class
|
|
67
73
|
.where(parent_column_sym => parent_id)
|
|
68
74
|
.order(nulls_last_order_by)
|
|
75
|
+
scope = scope.where(scope_conditions) if scope_conditions.any?
|
|
69
76
|
scope = scope.where("#{quoted_order_column} >= #{minimum_sort_order_value}") if minimum_sort_order_value
|
|
70
77
|
scope.each_with_index do |ea, idx|
|
|
71
78
|
ea.update_order_value(idx + minimum_sort_order_value.to_i)
|
data/lib/closure_tree/support.rb
CHANGED
|
@@ -16,26 +16,53 @@ module ClosureTree
|
|
|
16
16
|
|
|
17
17
|
@options = {
|
|
18
18
|
parent_column_name: 'parent_id',
|
|
19
|
-
dependent: :nullify, # or :destroy or :
|
|
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
22
|
numeric_order: false
|
|
23
23
|
}.merge(options)
|
|
24
24
|
raise ArgumentError, "name_column can't be 'path'" if options[:name_column] == 'path'
|
|
25
25
|
|
|
26
|
+
if options[:scope]
|
|
27
|
+
scope_option = options[:scope]
|
|
28
|
+
unless scope_option.is_a?(Symbol) || (scope_option.is_a?(Array) && scope_option.all? { |item| item.is_a?(Symbol) })
|
|
29
|
+
raise ArgumentError, "scope option must be a Symbol or an Array of Symbols (e.g., :user_id or [:user_id, :group_id])"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
26
33
|
return unless order_is_numeric?
|
|
27
34
|
|
|
28
35
|
extend NumericOrderSupport.adapter_for_connection(connection)
|
|
29
36
|
end
|
|
30
37
|
|
|
38
|
+
# Find the abstract base class for database connection
|
|
39
|
+
# This ensures hierarchy class uses the same database but doesn't inherit
|
|
40
|
+
# validations/callbacks from STI parent classes (issue #392)
|
|
41
|
+
def abstract_base_class
|
|
42
|
+
klass = model_class
|
|
43
|
+
while klass.superclass != ActiveRecord::Base
|
|
44
|
+
parent = klass.superclass
|
|
45
|
+
# Stop at abstract class (ApplicationRecord, SecondaryRecord, etc.)
|
|
46
|
+
return parent if parent.abstract_class?
|
|
47
|
+
# Stop at connection boundary (handles non-abstract parents with custom connections)
|
|
48
|
+
return parent if parent.connection_specification_name != parent.superclass.connection_specification_name
|
|
49
|
+
klass = parent
|
|
50
|
+
end
|
|
51
|
+
ActiveRecord::Base
|
|
52
|
+
end
|
|
53
|
+
|
|
31
54
|
def hierarchy_class_for_model
|
|
32
55
|
parent_class = model_class.module_parent
|
|
33
|
-
hierarchy_class = parent_class.const_set(short_hierarchy_class_name, Class.new(
|
|
56
|
+
hierarchy_class = parent_class.const_set(short_hierarchy_class_name, Class.new(abstract_base_class))
|
|
34
57
|
model_class_name = model_class.to_s
|
|
35
58
|
hierarchy_class.class_eval do
|
|
36
59
|
# Rails 8.1+ requires an implicit_order_column for models without a primary key
|
|
37
60
|
self.implicit_order_column = 'ancestor_id'
|
|
38
61
|
|
|
62
|
+
# Rails uses the primary key to correctly match associations when using a join to preload (e.g. via `eager_load`).
|
|
63
|
+
# The migration generator adds a unique index across these three columns so this is safe.
|
|
64
|
+
self.primary_key = [:ancestor_id, :descendant_id, :generations]
|
|
65
|
+
|
|
39
66
|
belongs_to :ancestor, class_name: model_class_name
|
|
40
67
|
belongs_to :descendant, class_name: model_class_name
|
|
41
68
|
def ==(other)
|
|
@@ -109,6 +136,22 @@ module ClosureTree
|
|
|
109
136
|
end
|
|
110
137
|
end
|
|
111
138
|
|
|
139
|
+
# Builds SQL WHERE conditions for scope columns
|
|
140
|
+
# Returns a string that can be appended to a WHERE clause
|
|
141
|
+
def build_scope_where_clause(scope_conditions)
|
|
142
|
+
return '' unless scope_conditions.is_a?(Hash) && scope_conditions.any?
|
|
143
|
+
|
|
144
|
+
conditions = scope_conditions.map do |column, value|
|
|
145
|
+
if value.nil?
|
|
146
|
+
"#{connection.quote_column_name(column.to_s)} IS NULL"
|
|
147
|
+
else
|
|
148
|
+
"#{connection.quote_column_name(column.to_s)} = #{quoted_value(value)}"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
" AND #{conditions.join(' AND ')}"
|
|
153
|
+
end
|
|
154
|
+
|
|
112
155
|
def with_advisory_lock(&block)
|
|
113
156
|
if options[:with_advisory_lock] && connection.supports_advisory_locks? && model_class.respond_to?(:with_advisory_lock)
|
|
114
157
|
model_class.with_advisory_lock(advisory_lock_name) do
|
|
@@ -170,5 +213,49 @@ module ClosureTree
|
|
|
170
213
|
def create!(model_class, attributes)
|
|
171
214
|
create(model_class, attributes).tap(&:save!)
|
|
172
215
|
end
|
|
216
|
+
|
|
217
|
+
def scope_columns
|
|
218
|
+
return [] unless options[:scope]
|
|
219
|
+
|
|
220
|
+
scope_option = options[:scope]
|
|
221
|
+
|
|
222
|
+
case scope_option
|
|
223
|
+
when Symbol
|
|
224
|
+
[scope_option]
|
|
225
|
+
when Array
|
|
226
|
+
scope_option.select { |item| item.is_a?(Symbol) }
|
|
227
|
+
else
|
|
228
|
+
[]
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def scope_values_from_instance(instance)
|
|
233
|
+
return {} unless options[:scope] && instance
|
|
234
|
+
|
|
235
|
+
scope_option = options[:scope]
|
|
236
|
+
scope_hash = {}
|
|
237
|
+
|
|
238
|
+
case scope_option
|
|
239
|
+
when Symbol
|
|
240
|
+
value = instance.read_attribute(scope_option)
|
|
241
|
+
scope_hash[scope_option] = value
|
|
242
|
+
when Array
|
|
243
|
+
scope_option.each do |item|
|
|
244
|
+
if item.is_a?(Symbol)
|
|
245
|
+
value = instance.read_attribute(item)
|
|
246
|
+
scope_hash[item] = value
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
scope_hash
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def apply_scope_conditions(scope, instance = nil)
|
|
255
|
+
return scope unless options[:scope] && instance
|
|
256
|
+
|
|
257
|
+
scope_values = scope_values_from_instance(instance)
|
|
258
|
+
scope_values.any? ? scope.where(scope_values) : scope
|
|
259
|
+
end
|
|
173
260
|
end
|
|
174
261
|
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.5.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.5.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.5.0
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: zeitwerk
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -70,16 +70,16 @@ dependencies:
|
|
|
70
70
|
name: minitest
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
72
72
|
requirements:
|
|
73
|
-
- - "
|
|
73
|
+
- - "~>"
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '0'
|
|
75
|
+
version: '5.0'
|
|
76
76
|
type: :development
|
|
77
77
|
prerelease: false
|
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
80
|
-
- - "
|
|
80
|
+
- - "~>"
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '0'
|
|
82
|
+
version: '5.0'
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
84
84
|
name: minitest-reporters
|
|
85
85
|
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.5.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'
|