closure_tree 7.3.0 → 9.1.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 +31 -0
- data/README.md +125 -39
- data/bin/rails +15 -0
- data/bin/rake +7 -7
- data/closure_tree.gemspec +21 -19
- data/lib/closure_tree/active_record_support.rb +6 -14
- data/lib/closure_tree/arel_helpers.rb +83 -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 -4
- 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 +30 -44
- 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 +14 -22
- data/lib/generators/closure_tree/migration_generator.rb +7 -8
- metadata +30 -81
- data/.github/workflows/ci.yml +0 -96
- data/.gitignore +0 -17
- data/.rspec +0 -1
- data/.yardopts +0 -3
- data/Appraisals +0 -105
- data/Gemfile +0 -3
- data/Rakefile +0 -28
- data/_config.yml +0 -1
- data/bin/appraisal +0 -29
- data/bin/rspec +0 -29
- data/lib/closure_tree/configuration.rb +0 -9
- data/lib/generators/closure_tree/config_generator.rb +0 -12
- data/lib/generators/closure_tree/templates/config.rb +0 -5
- 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: 9f55c2391c1a377f24383abdebcd716868debd94f7c0d9070e7c6c98f20fca7c
|
4
|
+
data.tar.gz: fbdbae702b5bec7a40f310e16a6a4d3eeb1c30287765ca4261e908421ec5d20a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 265cd31b5c7f5c1f3b7e90a242fb8683145a4ec67ae6f8ff0c6ea30573138e2a2cbcd962262aa7e351ebf2dd8a3a19d86ddf204d27b6c4115ded0e8f7b2a9039
|
7
|
+
data.tar.gz: 67483089bc889a8a335279bfe8239dd8ca7c9cfe06dfc646ec6da71c27b092c91bdf1fd35f14cc7a11c8d1ea6e635478c64d243e7b1af6c9477399513828f7f5
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,36 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [9.1.0](https://github.com/ClosureTree/closure_tree/compare/closure_tree/v9.0.0...closure_tree/v9.1.0) (2025-07-23)
|
4
|
+
|
5
|
+
|
6
|
+
### Features
|
7
|
+
|
8
|
+
* migrate from ActiveSupport::Autoload to Zeitwerk ([#457](https://github.com/ClosureTree/closure_tree/issues/457)) ([d18e80c](https://github.com/ClosureTree/closure_tree/commit/d18e80cdbd4f3510377363bc7b5166f0cc1b0a6f))
|
9
|
+
|
10
|
+
## [9.0.0](https://github.com/ClosureTree/closure_tree/compare/closure_tree-v8.0.0...closure_tree/v9.0.0) (2025-07-21)
|
11
|
+
|
12
|
+
|
13
|
+
### Features
|
14
|
+
|
15
|
+
* 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))
|
16
|
+
* rewrite with clean api ([#451](https://github.com/ClosureTree/closure_tree/issues/451)) ([f56f2e1](https://github.com/ClosureTree/closure_tree/commit/f56f2e1a3490bb8a099cea8f80b676945fce1c2e))
|
17
|
+
|
18
|
+
|
19
|
+
### Bug Fixes
|
20
|
+
|
21
|
+
* 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))
|
22
|
+
|
23
|
+
### 8.0.0
|
24
|
+
|
25
|
+
- Drop support to EOL ruby and rails
|
26
|
+
- Reference ancestor_hierarchies in depth instead of ancestors to avoid n+1
|
27
|
+
|
28
|
+
## [7.4.0](https://github.com/ClosureTree/closure_tree/tree/7.4.0)
|
29
|
+
|
30
|
+
[Full Changelog](https://github.com/ClosureTree/closure_tree/compare/v7.3.0...7.4.0)
|
31
|
+
|
32
|
+
- fix: hierarchy model with namespace should inherit from the superclass of basic model [\#384](https://github.com/ClosureTree/closure_tree/pull/384) ([shawndodo](https://github.com/shawndodo))
|
33
|
+
- Add with\_descendant to readme [\#381](https://github.com/ClosureTree/closure_tree/pull/381) ([mattvague](https://github.com/mattvague))
|
3
34
|
|
4
35
|
### 7.3.0
|
5
36
|
- Ruby 3.0 support
|
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
|
@@ -337,7 +337,8 @@ When you include ```has_closure_tree``` in your model, you can provide a hash to
|
|
337
337
|
* ```Tag.find_by_path(path, attributes)``` returns the node whose name path is ```path```. See (#find_or_create_by_path).
|
338
338
|
* ```Tag.find_or_create_by_path(path, attributes)``` returns the node whose name path is ```path```, and will create the node if it doesn't exist already.See (#find_or_create_by_path).
|
339
339
|
* ```Tag.find_all_by_generation(generation_level)``` returns the descendant nodes who are ```generation_level``` away from a root. ```Tag.find_all_by_generation(0)``` is equivalent to ```Tag.roots```.
|
340
|
-
* ```Tag.with_ancestor(ancestors)``` scopes to all descendants whose
|
340
|
+
* ```Tag.with_ancestor(ancestors)``` scopes to all descendants whose ancestors(s) is/are in the given list.
|
341
|
+
* ```Tag.with_descendant(ancestors)``` scopes to all ancestors whose descendant(s) is/are in the given list.
|
341
342
|
* ```Tag.lowest_common_ancestor(descendants)``` finds the lowest common ancestor of the descendants.
|
342
343
|
### Instance methods
|
343
344
|
|
@@ -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,45 +722,38 @@ 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
|
662
739
|
|
663
|
-
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
|
664
741
|
|
665
|
-
* Ruby
|
666
|
-
* ActiveRecord
|
742
|
+
* Ruby 3.3+
|
743
|
+
* ActiveRecord 7.2+
|
667
744
|
* PostgreSQL, MySQL, and SQLite. Concurrency tests are only run with MySQL and PostgreSQL.
|
668
745
|
|
669
|
-
|
670
|
-
|
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
|
+
```
|
671
757
|
|
672
758
|
## Change log
|
673
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,35 @@ 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'
|
30
|
+
gem.add_dependency 'zeitwerk', '~> 2.7'
|
25
31
|
|
26
|
-
gem.add_development_dependency 'appraisal'
|
27
32
|
gem.add_development_dependency 'database_cleaner'
|
28
|
-
gem.add_development_dependency '
|
33
|
+
gem.add_development_dependency 'minitest'
|
34
|
+
gem.add_development_dependency 'minitest-reporters'
|
29
35
|
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
36
|
gem.add_development_dependency 'simplecov'
|
35
37
|
gem.add_development_dependency 'timecop'
|
36
38
|
# 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,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"
|