closure_tree 7.4.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 +18 -0
- data/README.md +123 -38
- data/bin/rails +15 -0
- data/bin/rake +7 -7
- data/closure_tree.gemspec +20 -19
- data/lib/closure_tree/active_record_support.rb +6 -14
- 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 +7 -5
- data/lib/closure_tree/finders.rb +104 -55
- 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 +3 -2
- data/lib/closure_tree/hash_tree_support.rb +38 -13
- data/lib/closure_tree/hierarchy_maintenance.rb +20 -30
- data/lib/closure_tree/model.rb +31 -31
- data/lib/closure_tree/numeric_deterministic_ordering.rb +90 -60
- data/lib/closure_tree/numeric_order_support.rb +20 -18
- data/lib/closure_tree/support.rb +31 -38
- 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 +7 -8
- data/lib/generators/closure_tree/templates/config.rb +2 -0
- metadata +25 -86
- data/.github/workflows/ci.yml +0 -98
- data/.gitignore +0 -17
- data/.rspec +0 -1
- data/.yardopts +0 -3
- data/Appraisals +0 -105
- data/Gemfile +0 -7
- data/Rakefile +0 -37
- data/_config.yml +0 -1
- data/bin/appraisal +0 -29
- data/bin/rspec +0 -29
- data/mktree.rb +0 -38
- data/tests.sh +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7da1033357e368c9a4d2a36b95a8e75899b61b63e872c25167dd5f8862afb454
|
4
|
+
data.tar.gz: '08df010ac42f946d8171152ad5ef4a8e6c1b94263cf39e5d297e4cbf805dca81'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 34ef107ba1737f7e4cec6ff581149ce66b4daed0dee6b0e4bc266688a748c182e8c4e1082d38846de22671a23b2a640d80f753715f8e93fdc7889d134cd14c97
|
7
|
+
data.tar.gz: 300a2a3b499a8428e23f89235e409dce6cbd50378ee6406f2ab03d03b97f63d20b5365ff9adabbfaff47ce9b50a54fd5a2e80576dd5839b7d23debce98528074
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,23 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [9.0.0](https://github.com/ClosureTree/closure_tree/compare/closure_tree-v8.0.0...closure_tree/v9.0.0) (2025-07-21)
|
4
|
+
|
5
|
+
|
6
|
+
### Features
|
7
|
+
|
8
|
+
* 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))
|
9
|
+
* rewrite with clean api ([#451](https://github.com/ClosureTree/closure_tree/issues/451)) ([f56f2e1](https://github.com/ClosureTree/closure_tree/commit/f56f2e1a3490bb8a099cea8f80b676945fce1c2e))
|
10
|
+
|
11
|
+
|
12
|
+
### Bug Fixes
|
13
|
+
|
14
|
+
* 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))
|
15
|
+
|
16
|
+
### 8.0.0
|
17
|
+
|
18
|
+
- Drop support to EOL ruby and rails
|
19
|
+
- Reference ancestor_hierarchies in depth instead of ancestors to avoid n+1
|
20
|
+
|
3
21
|
## [7.4.0](https://github.com/ClosureTree/closure_tree/tree/7.4.0)
|
4
22
|
|
5
23
|
[Full Changelog](https://github.com/ClosureTree/closure_tree/compare/v7.3.0...7.4.0)
|
data/README.md
CHANGED
@@ -5,8 +5,7 @@
|
|
5
5
|
Common applications include modeling hierarchical data, like tags, threaded comments, page graphs in CMSes,
|
6
6
|
and tracking user referrals.
|
7
7
|
|
8
|
-
[](http://travis-ci.org/ClosureTree/closure_tree)
|
8
|
+
[](https://github.com/ClosureTree/closure_tree/actions/workflows/ci.yml)
|
10
9
|
[](https://badge.fury.io/rb/closure_tree)
|
11
10
|
|
12
11
|
Dramatically more performant than
|
@@ -25,7 +24,7 @@ closure_tree has some great features:
|
|
25
24
|
* 2 SQL INSERTs on node creation
|
26
25
|
* 3 SQL INSERT/UPDATEs on node reparenting
|
27
26
|
* __Support for [concurrency](#concurrency)__ (using [with_advisory_lock](https://github.com/ClosureTree/with_advisory_lock))
|
28
|
-
* __Tested against ActiveRecord
|
27
|
+
* __Tested against ActiveRecord 7.2+ with Ruby 3.3+__
|
29
28
|
* Support for reparenting children (and all their descendants)
|
30
29
|
* Support for [single-table inheritance (STI)](#sti) within the hierarchy
|
31
30
|
* ```find_or_create_by_path``` for [building out heterogeneous hierarchies quickly and conveniently](#find_or_create_by_path)
|
@@ -47,13 +46,14 @@ for a description of different tree storage algorithms.
|
|
47
46
|
- [Polymorphic hierarchies with STI](#polymorphic-hierarchies-with-sti)
|
48
47
|
- [Deterministic ordering](#deterministic-ordering)
|
49
48
|
- [Concurrency](#concurrency)
|
49
|
+
- [Multi-Database Support](#multi-database-support)
|
50
50
|
- [FAQ](#faq)
|
51
51
|
- [Testing](#testing)
|
52
52
|
- [Change log](#change-log)
|
53
53
|
|
54
54
|
## Installation
|
55
55
|
|
56
|
-
Note that closure_tree only supports ActiveRecord
|
56
|
+
Note that closure_tree only supports ActiveRecord 7.2 and later, and has test coverage for MySQL, PostgreSQL, and SQLite.
|
57
57
|
|
58
58
|
1. Add `gem 'closure_tree'` to your Gemfile
|
59
59
|
|
@@ -62,11 +62,11 @@ Note that closure_tree only supports ActiveRecord 4.2 and later, and has test co
|
|
62
62
|
3. Add `has_closure_tree` (or `acts_as_tree`, which is an alias of the same method) to your hierarchical model:
|
63
63
|
|
64
64
|
```ruby
|
65
|
-
class Tag <
|
65
|
+
class Tag < ApplicationRecord
|
66
66
|
has_closure_tree
|
67
67
|
end
|
68
68
|
|
69
|
-
class AnotherTag <
|
69
|
+
class AnotherTag < ApplicationRecord
|
70
70
|
acts_as_tree
|
71
71
|
end
|
72
72
|
```
|
@@ -83,7 +83,7 @@ Note that closure_tree only supports ActiveRecord 4.2 and later, and has test co
|
|
83
83
|
You may want to also [add a column for deterministic ordering of children](#deterministic-ordering), but that's optional.
|
84
84
|
|
85
85
|
```ruby
|
86
|
-
class AddParentIdToTag < ActiveRecord::Migration
|
86
|
+
class AddParentIdToTag < ActiveRecord::Migration[7.2]
|
87
87
|
def change
|
88
88
|
add_column :tags, :parent_id, :integer
|
89
89
|
end
|
@@ -385,7 +385,7 @@ Polymorphic models using single table inheritance (STI) are supported:
|
|
385
385
|
2. Subclass the model class. You only need to add ```has_closure_tree``` to your base class:
|
386
386
|
|
387
387
|
```ruby
|
388
|
-
class Tag <
|
388
|
+
class Tag < ApplicationRecord
|
389
389
|
has_closure_tree
|
390
390
|
end
|
391
391
|
class WhenTag < Tag ; end
|
@@ -412,7 +412,7 @@ By default, children will be ordered by your database engine, which may not be w
|
|
412
412
|
If you want to order children alphabetically, and your model has a ```name``` column, you'd do this:
|
413
413
|
|
414
414
|
```ruby
|
415
|
-
class Tag <
|
415
|
+
class Tag < ApplicationRecord
|
416
416
|
has_closure_tree order: 'name'
|
417
417
|
end
|
418
418
|
```
|
@@ -426,7 +426,7 @@ t.integer :sort_order
|
|
426
426
|
and in your model:
|
427
427
|
|
428
428
|
```ruby
|
429
|
-
class OrderedTag <
|
429
|
+
class OrderedTag < ApplicationRecord
|
430
430
|
has_closure_tree order: 'sort_order', numeric_order: true
|
431
431
|
end
|
432
432
|
```
|
@@ -526,7 +526,7 @@ If you are already managing concurrency elsewhere in your application, and want
|
|
526
526
|
of with_advisory_lock, pass ```with_advisory_lock: false``` in the options hash:
|
527
527
|
|
528
528
|
```ruby
|
529
|
-
class Tag
|
529
|
+
class Tag < ApplicationRecord
|
530
530
|
has_closure_tree with_advisory_lock: false
|
531
531
|
end
|
532
532
|
```
|
@@ -534,6 +534,98 @@ end
|
|
534
534
|
Note that you *will eventually have data corruption* if you disable advisory locks, write to your
|
535
535
|
database with multiple threads, and don't provide an alternative mutex.
|
536
536
|
|
537
|
+
### Customizing Advisory Lock Names
|
538
|
+
|
539
|
+
By default, closure_tree generates advisory lock names based on the model class name. You can customize
|
540
|
+
this behavior in several ways:
|
541
|
+
|
542
|
+
```ruby
|
543
|
+
# Static string
|
544
|
+
class Tag < ApplicationRecord
|
545
|
+
has_closure_tree advisory_lock_name: 'custom_tag_lock'
|
546
|
+
end
|
547
|
+
|
548
|
+
# Dynamic via Proc
|
549
|
+
class Tag < ApplicationRecord
|
550
|
+
has_closure_tree advisory_lock_name: ->(model_class) { "#{Rails.env}_#{model_class.name.underscore}" }
|
551
|
+
end
|
552
|
+
|
553
|
+
# Delegate to model method
|
554
|
+
class Tag < ApplicationRecord
|
555
|
+
has_closure_tree advisory_lock_name: :custom_lock_name
|
556
|
+
|
557
|
+
def self.custom_lock_name
|
558
|
+
"tag_lock_#{current_tenant_id}"
|
559
|
+
end
|
560
|
+
end
|
561
|
+
```
|
562
|
+
|
563
|
+
This is particularly useful when:
|
564
|
+
* You need environment-specific lock names
|
565
|
+
* You're using multi-tenancy and need tenant-specific locks
|
566
|
+
* You want to avoid lock name collisions between similar model names
|
567
|
+
|
568
|
+
## Multi-Database Support
|
569
|
+
|
570
|
+
Closure Tree fully supports running with multiple databases simultaneously, including mixing different database engines (PostgreSQL, MySQL, SQLite) in the same application. This is particularly useful for:
|
571
|
+
|
572
|
+
* Applications with read replicas
|
573
|
+
* Sharding strategies
|
574
|
+
* Testing with different database engines
|
575
|
+
* Gradual database migrations
|
576
|
+
|
577
|
+
### Database-Specific Behaviors
|
578
|
+
|
579
|
+
#### PostgreSQL
|
580
|
+
* Full support for advisory locks via `with_advisory_lock`
|
581
|
+
* Excellent concurrency support with row-level locking
|
582
|
+
* Best overall performance for tree operations
|
583
|
+
|
584
|
+
#### MySQL
|
585
|
+
* Advisory locks supported via `with_advisory_lock`
|
586
|
+
* Note: MySQL's row-level locking may incorrectly report deadlocks in some cases
|
587
|
+
* Requires MySQL 5.7.12+ to avoid hierarchy maintenance errors
|
588
|
+
|
589
|
+
#### SQLite
|
590
|
+
* **No advisory lock support** - always returns false from `with_advisory_lock`
|
591
|
+
* Falls back to file-based locking for tests
|
592
|
+
* Suitable for development and testing, but not recommended for production with concurrent writes
|
593
|
+
|
594
|
+
### Configuration
|
595
|
+
|
596
|
+
When using multiple databases, closure_tree automatically detects the correct adapter for each connection:
|
597
|
+
|
598
|
+
```ruby
|
599
|
+
class Tag < ApplicationRecord
|
600
|
+
connects_to database: { writing: :primary, reading: :replica }
|
601
|
+
has_closure_tree
|
602
|
+
end
|
603
|
+
|
604
|
+
class Category < ApplicationRecord
|
605
|
+
connects_to database: { writing: :sqlite_db }
|
606
|
+
has_closure_tree
|
607
|
+
end
|
608
|
+
```
|
609
|
+
|
610
|
+
Each model will use the appropriate database-specific SQL syntax and features based on its connection adapter.
|
611
|
+
|
612
|
+
### Testing with Multiple Databases
|
613
|
+
|
614
|
+
You can run the test suite against different databases:
|
615
|
+
|
616
|
+
```bash
|
617
|
+
# Run with PostgreSQL
|
618
|
+
DATABASE_URL=postgres://localhost/closure_tree_test rake test
|
619
|
+
|
620
|
+
# Run with MySQL
|
621
|
+
DATABASE_URL=mysql2://localhost/closure_tree_test rake test
|
622
|
+
|
623
|
+
# Run with SQLite (default)
|
624
|
+
rake test
|
625
|
+
```
|
626
|
+
|
627
|
+
For simultaneous multi-database testing, the test suite automatically sets up connections to all three database types when available.
|
628
|
+
|
537
629
|
## I18n
|
538
630
|
|
539
631
|
You can customize error messages using [I18n](http://guides.rubyonrails.org/i18n.html):
|
@@ -630,45 +722,38 @@ Upgrade to MySQL 5.7.12 or later if you see [this issue](https://github.com/Clos
|
|
630
722
|
|
631
723
|
## Testing with Closure Tree
|
632
724
|
|
633
|
-
Closure tree comes with
|
725
|
+
Closure tree comes with test matchers which you may use in your tests:
|
634
726
|
|
635
727
|
```ruby
|
636
|
-
require '
|
728
|
+
require 'test_helper'
|
637
729
|
require 'closure_tree/test/matcher'
|
638
730
|
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
it { is_expected.to be_a_closure_tree }
|
644
|
-
end
|
645
|
-
|
646
|
-
describe Label do
|
647
|
-
# Should syntax
|
648
|
-
it { should be_a_closure_tree.ordered }
|
649
|
-
# Expect syntax
|
650
|
-
it { is_expected.to be_a_closure_tree.ordered }
|
651
|
-
end
|
652
|
-
|
653
|
-
describe TodoList::Item do
|
654
|
-
# Should syntax
|
655
|
-
it { should be_a_closure_tree.ordered(:priority_order) }
|
656
|
-
# Expect syntax
|
657
|
-
it { is_expected.to be_a_closure_tree.ordered(:priority_order) }
|
731
|
+
class CategoryTest < ActiveSupport::TestCase
|
732
|
+
test "should be a closure tree" do
|
733
|
+
assert Category.new.is_a?(ClosureTree::Model)
|
734
|
+
end
|
658
735
|
end
|
659
|
-
|
660
736
|
```
|
661
737
|
|
662
738
|
## Testing
|
663
739
|
|
664
|
-
Closure tree is [tested under every valid combination](
|
740
|
+
Closure tree is [tested under every valid combination](https://github.com/ClosureTree/closure_tree/blob/master/.github/workflows/ci.yml) of
|
665
741
|
|
666
|
-
* Ruby
|
667
|
-
* ActiveRecord
|
742
|
+
* Ruby 3.3+
|
743
|
+
* ActiveRecord 7.2+
|
668
744
|
* PostgreSQL, MySQL, and SQLite. Concurrency tests are only run with MySQL and PostgreSQL.
|
669
745
|
|
670
|
-
|
671
|
-
|
746
|
+
```shell
|
747
|
+
$ bundle
|
748
|
+
$ rake test # this will run the tests
|
749
|
+
```
|
750
|
+
|
751
|
+
By default the test are run with sqlite3 only.
|
752
|
+
You run test with other databases by passing the database url as environment variable:
|
753
|
+
|
754
|
+
```shell
|
755
|
+
$ DATABASE_URL=postgres://localhost/my_database rake test
|
756
|
+
```
|
672
757
|
|
673
758
|
## Change log
|
674
759
|
|
data/bin/rails
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# This command will automatically be run when you run "rails" with Rails gems
|
5
|
+
# installed from the root of your application.
|
6
|
+
|
7
|
+
ENGINE_ROOT = File.expand_path('..', __dir__)
|
8
|
+
APP_PATH = File.expand_path('../test/dummy/config/application', __dir__)
|
9
|
+
|
10
|
+
# Set up gems listed in the Gemfile.
|
11
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
12
|
+
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
|
13
|
+
|
14
|
+
require 'rails/all'
|
15
|
+
require 'rails/engine/commands'
|
data/bin/rake
CHANGED
@@ -8,11 +8,11 @@
|
|
8
8
|
# this file is here to facilitate running it.
|
9
9
|
#
|
10
10
|
|
11
|
-
require
|
12
|
-
ENV[
|
13
|
-
|
11
|
+
require 'pathname'
|
12
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
14
|
|
15
|
-
bundle_binstub = File.expand_path(
|
15
|
+
bundle_binstub = File.expand_path('bundle', __dir__)
|
16
16
|
|
17
17
|
if File.file?(bundle_binstub)
|
18
18
|
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
@@ -23,7 +23,7 @@ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
require
|
27
|
-
require
|
26
|
+
require 'rubygems'
|
27
|
+
require 'bundler/setup'
|
28
28
|
|
29
|
-
load Gem.bin_path(
|
29
|
+
load Gem.bin_path('rake', 'rake')
|
data/closure_tree.gemspec
CHANGED
@@ -4,33 +4,34 @@ require_relative 'lib/closure_tree/version'
|
|
4
4
|
|
5
5
|
Gem::Specification.new do |gem|
|
6
6
|
gem.name = 'closure_tree'
|
7
|
-
gem.version =
|
8
|
-
gem.authors = ['Matthew McEachen']
|
9
|
-
gem.email = [
|
10
|
-
gem.homepage = '
|
7
|
+
gem.version = ClosureTree::VERSION
|
8
|
+
gem.authors = ['Matthew McEachen', 'Abdelkader Boudih']
|
9
|
+
gem.email = %w[matthew+github@mceachen.org terminale@gmail.com]
|
10
|
+
gem.homepage = 'https://github.com/ClosureTree/closure_tree/'
|
11
11
|
|
12
|
-
gem.summary =
|
13
|
-
gem.description = gem.summary
|
12
|
+
gem.summary = 'Easily and efficiently make your ActiveRecord model support hierarchies'
|
14
13
|
gem.license = 'MIT'
|
15
14
|
|
16
|
-
gem.
|
17
|
-
|
18
|
-
|
15
|
+
gem.metadata = {
|
16
|
+
'bug_tracker_uri' => 'https://github.com/ClosureTree/closure_tree/issues',
|
17
|
+
'changelog_uri' => 'https://github.com/ClosureTree/closure_tree/blob/master/CHANGELOG.md',
|
18
|
+
'documentation_uri' => "https://www.rubydoc.info/gems/closure_tree/#{gem.version}",
|
19
|
+
'homepage_uri' => 'https://closuretree.github.io/closure_tree/',
|
20
|
+
'source_code_uri' => 'https://github.com/ClosureTree/closure_tree',
|
21
|
+
'rubygems_mfa_required' => 'true'
|
22
|
+
}
|
19
23
|
|
20
|
-
gem.
|
21
|
-
gem.required_ruby_version = '>= 2.0.0'
|
24
|
+
gem.files = Dir.glob('{lib}/**/*') + Dir.glob('bin/*') + %w[README.md CHANGELOG.md MIT-LICENSE closure_tree.gemspec]
|
22
25
|
|
23
|
-
gem.
|
24
|
-
|
26
|
+
gem.required_ruby_version = '>= 3.3.0'
|
27
|
+
|
28
|
+
gem.add_dependency 'activerecord', '>= 7.2.0'
|
29
|
+
gem.add_dependency 'with_advisory_lock', '>= 7.0.0'
|
25
30
|
|
26
|
-
gem.add_development_dependency 'appraisal'
|
27
31
|
gem.add_development_dependency 'database_cleaner'
|
28
|
-
gem.add_development_dependency '
|
32
|
+
gem.add_development_dependency 'minitest'
|
33
|
+
gem.add_development_dependency 'minitest-reporters'
|
29
34
|
gem.add_development_dependency 'parallel'
|
30
|
-
gem.add_development_dependency 'pg'
|
31
|
-
gem.add_development_dependency 'rspec-instafail'
|
32
|
-
gem.add_development_dependency 'rspec-rails'
|
33
|
-
gem.add_development_dependency 'sqlite3'
|
34
35
|
gem.add_development_dependency 'simplecov'
|
35
36
|
gem.add_development_dependency 'timecop'
|
36
37
|
# gem.add_development_dependency 'byebug'
|
@@ -1,20 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ClosureTree
|
2
4
|
module ActiveRecordSupport
|
5
|
+
delegate :quote, to: :connection
|
3
6
|
|
4
|
-
def
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
def ensure_fixed_table_name(table_name)
|
9
|
-
[
|
10
|
-
ActiveRecord::Base.table_name_prefix,
|
11
|
-
remove_prefix_and_suffix(table_name),
|
12
|
-
ActiveRecord::Base.table_name_suffix
|
13
|
-
].compact.join
|
14
|
-
end
|
15
|
-
|
16
|
-
def remove_prefix_and_suffix(table_name)
|
17
|
-
pre, suff = ActiveRecord::Base.table_name_prefix, ActiveRecord::Base.table_name_suffix
|
7
|
+
def remove_prefix_and_suffix(table_name, model = ActiveRecord::Base)
|
8
|
+
pre = model.table_name_prefix
|
9
|
+
suff = model.table_name_suffix
|
18
10
|
if table_name.start_with?(pre) && table_name.end_with?(suff)
|
19
11
|
table_name[pre.size..-(suff.size + 1)]
|
20
12
|
else
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClosureTree
|
4
|
+
module AdapterSupport
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# This module is now only used to ensure the adapter has been loaded
|
8
|
+
# The actual advisory lock functionality is handled through the model's
|
9
|
+
# with_advisory_lock method from the with_advisory_lock gem
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClosureTree
|
4
|
+
module ArelHelpers
|
5
|
+
# Get model's arel table
|
6
|
+
def model_table
|
7
|
+
@model_table ||= model_class.arel_table
|
8
|
+
end
|
9
|
+
|
10
|
+
# Get hierarchy table from a model class
|
11
|
+
# This method should be called from instance methods where hierarchy_class is available
|
12
|
+
def hierarchy_table_for(model)
|
13
|
+
if model.respond_to?(:hierarchy_class)
|
14
|
+
model.hierarchy_class.arel_table
|
15
|
+
elsif model.class.respond_to?(:hierarchy_class)
|
16
|
+
model.class.hierarchy_class.arel_table
|
17
|
+
else
|
18
|
+
raise ArgumentError, "Cannot find hierarchy_class for #{model}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Get hierarchy table using the model_class
|
23
|
+
# This is for Support class methods
|
24
|
+
def hierarchy_table
|
25
|
+
@hierarchy_table ||= begin
|
26
|
+
hierarchy_class_name = options[:hierarchy_class_name] || "#{model_class}Hierarchy"
|
27
|
+
hierarchy_class_name.constantize.arel_table
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Helper to create an Arel node for a table with an alias
|
32
|
+
def aliased_table(table, alias_name)
|
33
|
+
table.alias(alias_name)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Build Arel queries for hierarchy operations
|
37
|
+
def build_hierarchy_insert_query(hierarchy_table, node_id, parent_id)
|
38
|
+
x = aliased_table(hierarchy_table, 'x')
|
39
|
+
|
40
|
+
# Build the SELECT subquery - use SelectManager
|
41
|
+
select_query = Arel::SelectManager.new(x)
|
42
|
+
select_query.project(
|
43
|
+
x[:ancestor_id],
|
44
|
+
Arel.sql(quote(node_id)),
|
45
|
+
x[:generations] + 1
|
46
|
+
)
|
47
|
+
select_query.where(x[:descendant_id].eq(parent_id))
|
48
|
+
|
49
|
+
# Build the INSERT statement
|
50
|
+
insert_manager = Arel::InsertManager.new
|
51
|
+
insert_manager.into(hierarchy_table)
|
52
|
+
insert_manager.columns << hierarchy_table[:ancestor_id]
|
53
|
+
insert_manager.columns << hierarchy_table[:descendant_id]
|
54
|
+
insert_manager.columns << hierarchy_table[:generations]
|
55
|
+
insert_manager.select(select_query)
|
56
|
+
|
57
|
+
insert_manager
|
58
|
+
end
|
59
|
+
|
60
|
+
def build_hierarchy_delete_query(hierarchy_table, id)
|
61
|
+
# Build the innermost subquery
|
62
|
+
inner_subquery_manager = Arel::SelectManager.new(hierarchy_table)
|
63
|
+
inner_subquery_manager.project(hierarchy_table[:descendant_id])
|
64
|
+
inner_subquery_manager.where(
|
65
|
+
hierarchy_table[:ancestor_id].eq(id)
|
66
|
+
.or(hierarchy_table[:descendant_id].eq(id))
|
67
|
+
)
|
68
|
+
inner_subquery = inner_subquery_manager.as('x')
|
69
|
+
|
70
|
+
# Build the middle subquery with DISTINCT
|
71
|
+
middle_subquery = Arel::SelectManager.new
|
72
|
+
middle_subquery.from(inner_subquery)
|
73
|
+
middle_subquery.project(inner_subquery[:descendant_id]).distinct
|
74
|
+
|
75
|
+
# Build the DELETE statement
|
76
|
+
delete_manager = Arel::DeleteManager.new
|
77
|
+
delete_manager.from(hierarchy_table)
|
78
|
+
delete_manager.where(hierarchy_table[:descendant_id].in(middle_subquery))
|
79
|
+
|
80
|
+
delete_manager
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ClosureTree
|
2
4
|
module Digraphs
|
3
5
|
extend ActiveSupport::Concern
|
@@ -11,16 +13,16 @@ module ClosureTree
|
|
11
13
|
_ct.has_name? ? read_attribute(_ct.name_column) : to_s
|
12
14
|
end
|
13
15
|
|
14
|
-
|
16
|
+
class_methods do
|
15
17
|
# Renders the given scope as a DOT digraph, suitable for rendering by Graphviz
|
16
18
|
def to_dot_digraph(tree_scope)
|
17
|
-
id_to_instance = tree_scope.
|
19
|
+
id_to_instance = tree_scope.each_with_object({}) do |ea, h|
|
20
|
+
h[ea.id] = ea
|
21
|
+
end
|
18
22
|
output = StringIO.new
|
19
23
|
output << "digraph G {\n"
|
20
24
|
tree_scope.each do |ea|
|
21
|
-
if id_to_instance.key? ea._ct_parent_id
|
22
|
-
output << " \"#{ea._ct_parent_id}\" -> \"#{ea._ct_id}\"\n"
|
23
|
-
end
|
25
|
+
output << " \"#{ea._ct_parent_id}\" -> \"#{ea._ct_id}\"\n" if id_to_instance.key? ea._ct_parent_id
|
24
26
|
output << " \"#{ea._ct_id}\" [label=\"#{ea.to_digraph_label}\"]\n"
|
25
27
|
end
|
26
28
|
output << "}\n"
|