closure_tree 8.0.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 +13 -0
- data/README.md +111 -38
- data/bin/rails +15 -0
- data/bin/rake +7 -7
- data/closure_tree.gemspec +11 -17
- data/lib/closure_tree/active_record_support.rb +4 -1
- 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 +6 -4
- data/lib/closure_tree/finders.rb +103 -54
- 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 +2 -1
- data/lib/closure_tree/hash_tree_support.rb +38 -13
- data/lib/closure_tree/hierarchy_maintenance.rb +19 -26
- data/lib/closure_tree/model.rb +29 -29
- data/lib/closure_tree/numeric_deterministic_ordering.rb +90 -55
- data/lib/closure_tree/numeric_order_support.rb +20 -18
- data/lib/closure_tree/support.rb +29 -32
- 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 +6 -4
- data/lib/generators/closure_tree/templates/config.rb +2 -0
- metadata +12 -104
- data/.github/workflows/ci.yml +0 -72
- data/.github/workflows/ci_jruby.yml +0 -68
- data/.github/workflows/ci_truffleruby.yml +0 -71
- data/.github/workflows/release.yml +0 -17
- data/.gitignore +0 -17
- data/.release-please-manifest.json +0 -1
- data/.rspec +0 -1
- data/.tool-versions +0 -1
- data/.yardopts +0 -3
- data/Appraisals +0 -61
- data/Gemfile +0 -6
- data/Rakefile +0 -32
- data/bin/appraisal +0 -29
- data/bin/rspec +0 -29
- data/mktree.rb +0 -38
- data/release-please-config.json +0 -4
- data/test/closure_tree/cache_invalidation_test.rb +0 -36
- data/test/closure_tree/cuisine_type_test.rb +0 -42
- data/test/closure_tree/generator_test.rb +0 -49
- data/test/closure_tree/has_closure_tree_root_test.rb +0 -80
- data/test/closure_tree/hierarchy_maintenance_test.rb +0 -56
- data/test/closure_tree/label_test.rb +0 -674
- data/test/closure_tree/metal_test.rb +0 -59
- data/test/closure_tree/model_test.rb +0 -9
- data/test/closure_tree/namespace_type_test.rb +0 -13
- data/test/closure_tree/parallel_test.rb +0 -162
- data/test/closure_tree/pool_test.rb +0 -33
- data/test/closure_tree/support_test.rb +0 -18
- data/test/closure_tree/tag_test.rb +0 -8
- data/test/closure_tree/user_test.rb +0 -175
- data/test/closure_tree/uuid_tag_test.rb +0 -8
- data/test/support/query_counter.rb +0 -25
- data/test/support/tag_examples.rb +0 -923
- data/test/test_helper.rb +0 -99
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,18 @@
|
|
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
|
+
|
3
16
|
### 8.0.0
|
4
17
|
|
5
18
|
- Drop support to EOL ruby and rails
|
data/README.md
CHANGED
@@ -24,7 +24,7 @@ closure_tree has some great features:
|
|
24
24
|
* 2 SQL INSERTs on node creation
|
25
25
|
* 3 SQL INSERT/UPDATEs on node reparenting
|
26
26
|
* __Support for [concurrency](#concurrency)__ (using [with_advisory_lock](https://github.com/ClosureTree/with_advisory_lock))
|
27
|
-
* __Tested against ActiveRecord 7.
|
27
|
+
* __Tested against ActiveRecord 7.2+ with Ruby 3.3+__
|
28
28
|
* Support for reparenting children (and all their descendants)
|
29
29
|
* Support for [single-table inheritance (STI)](#sti) within the hierarchy
|
30
30
|
* ```find_or_create_by_path``` for [building out heterogeneous hierarchies quickly and conveniently](#find_or_create_by_path)
|
@@ -46,13 +46,14 @@ for a description of different tree storage algorithms.
|
|
46
46
|
- [Polymorphic hierarchies with STI](#polymorphic-hierarchies-with-sti)
|
47
47
|
- [Deterministic ordering](#deterministic-ordering)
|
48
48
|
- [Concurrency](#concurrency)
|
49
|
+
- [Multi-Database Support](#multi-database-support)
|
49
50
|
- [FAQ](#faq)
|
50
51
|
- [Testing](#testing)
|
51
52
|
- [Change log](#change-log)
|
52
53
|
|
53
54
|
## Installation
|
54
55
|
|
55
|
-
Note that closure_tree only supports ActiveRecord 7.
|
56
|
+
Note that closure_tree only supports ActiveRecord 7.2 and later, and has test coverage for MySQL, PostgreSQL, and SQLite.
|
56
57
|
|
57
58
|
1. Add `gem 'closure_tree'` to your Gemfile
|
58
59
|
|
@@ -61,11 +62,11 @@ Note that closure_tree only supports ActiveRecord 7.1 and later, and has test co
|
|
61
62
|
3. Add `has_closure_tree` (or `acts_as_tree`, which is an alias of the same method) to your hierarchical model:
|
62
63
|
|
63
64
|
```ruby
|
64
|
-
class Tag <
|
65
|
+
class Tag < ApplicationRecord
|
65
66
|
has_closure_tree
|
66
67
|
end
|
67
68
|
|
68
|
-
class AnotherTag <
|
69
|
+
class AnotherTag < ApplicationRecord
|
69
70
|
acts_as_tree
|
70
71
|
end
|
71
72
|
```
|
@@ -82,7 +83,7 @@ Note that closure_tree only supports ActiveRecord 7.1 and later, and has test co
|
|
82
83
|
You may want to also [add a column for deterministic ordering of children](#deterministic-ordering), but that's optional.
|
83
84
|
|
84
85
|
```ruby
|
85
|
-
class AddParentIdToTag < ActiveRecord::Migration
|
86
|
+
class AddParentIdToTag < ActiveRecord::Migration[7.2]
|
86
87
|
def change
|
87
88
|
add_column :tags, :parent_id, :integer
|
88
89
|
end
|
@@ -384,7 +385,7 @@ Polymorphic models using single table inheritance (STI) are supported:
|
|
384
385
|
2. Subclass the model class. You only need to add ```has_closure_tree``` to your base class:
|
385
386
|
|
386
387
|
```ruby
|
387
|
-
class Tag <
|
388
|
+
class Tag < ApplicationRecord
|
388
389
|
has_closure_tree
|
389
390
|
end
|
390
391
|
class WhenTag < Tag ; end
|
@@ -411,7 +412,7 @@ By default, children will be ordered by your database engine, which may not be w
|
|
411
412
|
If you want to order children alphabetically, and your model has a ```name``` column, you'd do this:
|
412
413
|
|
413
414
|
```ruby
|
414
|
-
class Tag <
|
415
|
+
class Tag < ApplicationRecord
|
415
416
|
has_closure_tree order: 'name'
|
416
417
|
end
|
417
418
|
```
|
@@ -425,7 +426,7 @@ t.integer :sort_order
|
|
425
426
|
and in your model:
|
426
427
|
|
427
428
|
```ruby
|
428
|
-
class OrderedTag <
|
429
|
+
class OrderedTag < ApplicationRecord
|
429
430
|
has_closure_tree order: 'sort_order', numeric_order: true
|
430
431
|
end
|
431
432
|
```
|
@@ -525,7 +526,7 @@ If you are already managing concurrency elsewhere in your application, and want
|
|
525
526
|
of with_advisory_lock, pass ```with_advisory_lock: false``` in the options hash:
|
526
527
|
|
527
528
|
```ruby
|
528
|
-
class Tag
|
529
|
+
class Tag < ApplicationRecord
|
529
530
|
has_closure_tree with_advisory_lock: false
|
530
531
|
end
|
531
532
|
```
|
@@ -533,6 +534,98 @@ end
|
|
533
534
|
Note that you *will eventually have data corruption* if you disable advisory locks, write to your
|
534
535
|
database with multiple threads, and don't provide an alternative mutex.
|
535
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
|
+
|
536
629
|
## I18n
|
537
630
|
|
538
631
|
You can customize error messages using [I18n](http://guides.rubyonrails.org/i18n.html):
|
@@ -629,33 +722,17 @@ Upgrade to MySQL 5.7.12 or later if you see [this issue](https://github.com/Clos
|
|
629
722
|
|
630
723
|
## Testing with Closure Tree
|
631
724
|
|
632
|
-
Closure tree comes with
|
725
|
+
Closure tree comes with test matchers which you may use in your tests:
|
633
726
|
|
634
727
|
```ruby
|
635
|
-
require '
|
728
|
+
require 'test_helper'
|
636
729
|
require 'closure_tree/test/matcher'
|
637
730
|
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
it { is_expected.to be_a_closure_tree }
|
643
|
-
end
|
644
|
-
|
645
|
-
describe Label do
|
646
|
-
# Should syntax
|
647
|
-
it { should be_a_closure_tree.ordered }
|
648
|
-
# Expect syntax
|
649
|
-
it { is_expected.to be_a_closure_tree.ordered }
|
650
|
-
end
|
651
|
-
|
652
|
-
describe TodoList::Item do
|
653
|
-
# Should syntax
|
654
|
-
it { should be_a_closure_tree.ordered(:priority_order) }
|
655
|
-
# Expect syntax
|
656
|
-
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
|
657
735
|
end
|
658
|
-
|
659
736
|
```
|
660
737
|
|
661
738
|
## Testing
|
@@ -663,23 +740,19 @@ end
|
|
663
740
|
Closure tree is [tested under every valid combination](https://github.com/ClosureTree/closure_tree/blob/master/.github/workflows/ci.yml) of
|
664
741
|
|
665
742
|
* Ruby 3.3+
|
666
|
-
* ActiveRecord 7.
|
743
|
+
* ActiveRecord 7.2+
|
667
744
|
* PostgreSQL, MySQL, and SQLite. Concurrency tests are only run with MySQL and PostgreSQL.
|
668
745
|
|
669
746
|
```shell
|
670
747
|
$ bundle
|
671
|
-
$
|
672
|
-
$ appraisal rake # this will run the tests in all combinations
|
673
|
-
$ appraisal activerecord-7.0 rake # this will run the tests in AR 7.0 only
|
674
|
-
$ appraisal activerecord-7.0 rake spec # this will run rspec in AR 7.0 only
|
675
|
-
$ appraisal activerecord-7.0 rake test # this will run minitest in AR 7.0 only
|
748
|
+
$ rake test # this will run the tests
|
676
749
|
```
|
677
750
|
|
678
751
|
By default the test are run with sqlite3 only.
|
679
752
|
You run test with other databases by passing the database url as environment variable:
|
680
753
|
|
681
754
|
```shell
|
682
|
-
$ DATABASE_URL=postgres://localhost/my_database
|
755
|
+
$ DATABASE_URL=postgres://localhost/my_database rake test
|
683
756
|
```
|
684
757
|
|
685
758
|
## Change log
|
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,40 +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 =
|
7
|
+
gem.version = ClosureTree::VERSION
|
8
8
|
gem.authors = ['Matthew McEachen', 'Abdelkader Boudih']
|
9
9
|
gem.email = %w[matthew+github@mceachen.org terminale@gmail.com]
|
10
10
|
gem.homepage = 'https://github.com/ClosureTree/closure_tree/'
|
11
11
|
|
12
|
-
gem.summary =
|
12
|
+
gem.summary = 'Easily and efficiently make your ActiveRecord model support hierarchies'
|
13
13
|
gem.license = 'MIT'
|
14
14
|
|
15
15
|
gem.metadata = {
|
16
|
-
'bug_tracker_uri'
|
17
|
-
'changelog_uri'
|
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
18
|
'documentation_uri' => "https://www.rubydoc.info/gems/closure_tree/#{gem.version}",
|
19
|
-
'homepage_uri'
|
20
|
-
'source_code_uri'
|
19
|
+
'homepage_uri' => 'https://closuretree.github.io/closure_tree/',
|
20
|
+
'source_code_uri' => 'https://github.com/ClosureTree/closure_tree',
|
21
|
+
'rubygems_mfa_required' => 'true'
|
21
22
|
}
|
22
23
|
|
23
|
-
gem.files
|
24
|
-
f.match(%r{^(spec|img|gemfiles)})
|
25
|
-
end
|
24
|
+
gem.files = Dir.glob('{lib}/**/*') + Dir.glob('bin/*') + %w[README.md CHANGELOG.md MIT-LICENSE closure_tree.gemspec]
|
26
25
|
|
27
|
-
gem.test_files = gem.files.grep(%r{^spec/})
|
28
26
|
gem.required_ruby_version = '>= 3.3.0'
|
29
27
|
|
30
|
-
gem.
|
31
|
-
gem.
|
28
|
+
gem.add_dependency 'activerecord', '>= 7.2.0'
|
29
|
+
gem.add_dependency 'with_advisory_lock', '>= 7.0.0'
|
32
30
|
|
33
|
-
gem.add_development_dependency 'appraisal'
|
34
31
|
gem.add_development_dependency 'database_cleaner'
|
35
|
-
gem.add_development_dependency 'generator_spec'
|
36
|
-
gem.add_development_dependency 'parallel'
|
37
32
|
gem.add_development_dependency 'minitest'
|
38
33
|
gem.add_development_dependency 'minitest-reporters'
|
39
|
-
gem.add_development_dependency '
|
40
|
-
gem.add_development_dependency 'rspec-rails'
|
34
|
+
gem.add_development_dependency 'parallel'
|
41
35
|
gem.add_development_dependency 'simplecov'
|
42
36
|
gem.add_development_dependency 'timecop'
|
43
37
|
# gem.add_development_dependency 'byebug'
|
@@ -1,9 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ClosureTree
|
2
4
|
module ActiveRecordSupport
|
3
5
|
delegate :quote, to: :connection
|
4
6
|
|
5
7
|
def remove_prefix_and_suffix(table_name, model = ActiveRecord::Base)
|
6
|
-
pre
|
8
|
+
pre = model.table_name_prefix
|
9
|
+
suff = model.table_name_suffix
|
7
10
|
if table_name.start_with?(pre) && table_name.end_with?(suff)
|
8
11
|
table_name[pre.size..-(suff.size + 1)]
|
9
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
|
@@ -14,13 +16,13 @@ module ClosureTree
|
|
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"
|