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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/README.md +123 -38
  4. data/bin/rails +15 -0
  5. data/bin/rake +7 -7
  6. data/closure_tree.gemspec +20 -19
  7. data/lib/closure_tree/active_record_support.rb +6 -14
  8. data/lib/closure_tree/adapter_support.rb +11 -0
  9. data/lib/closure_tree/arel_helpers.rb +83 -0
  10. data/lib/closure_tree/configuration.rb +2 -0
  11. data/lib/closure_tree/deterministic_ordering.rb +2 -0
  12. data/lib/closure_tree/digraphs.rb +7 -5
  13. data/lib/closure_tree/finders.rb +104 -55
  14. data/lib/closure_tree/has_closure_tree.rb +5 -2
  15. data/lib/closure_tree/has_closure_tree_root.rb +12 -17
  16. data/lib/closure_tree/hash_tree.rb +3 -2
  17. data/lib/closure_tree/hash_tree_support.rb +38 -13
  18. data/lib/closure_tree/hierarchy_maintenance.rb +20 -30
  19. data/lib/closure_tree/model.rb +31 -31
  20. data/lib/closure_tree/numeric_deterministic_ordering.rb +90 -60
  21. data/lib/closure_tree/numeric_order_support.rb +20 -18
  22. data/lib/closure_tree/support.rb +31 -38
  23. data/lib/closure_tree/support_attributes.rb +31 -5
  24. data/lib/closure_tree/support_flags.rb +2 -12
  25. data/lib/closure_tree/test/matcher.rb +10 -12
  26. data/lib/closure_tree/version.rb +3 -1
  27. data/lib/closure_tree.rb +22 -2
  28. data/lib/generators/closure_tree/config_generator.rb +3 -1
  29. data/lib/generators/closure_tree/migration_generator.rb +7 -8
  30. data/lib/generators/closure_tree/templates/config.rb +2 -0
  31. metadata +25 -86
  32. data/.github/workflows/ci.yml +0 -98
  33. data/.gitignore +0 -17
  34. data/.rspec +0 -1
  35. data/.yardopts +0 -3
  36. data/Appraisals +0 -105
  37. data/Gemfile +0 -7
  38. data/Rakefile +0 -37
  39. data/_config.yml +0 -1
  40. data/bin/appraisal +0 -29
  41. data/bin/rspec +0 -29
  42. data/mktree.rb +0 -38
  43. data/tests.sh +0 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4ac0bb16bca6ba730b542b35639b067ad113e2c7010265cae6e0192425e1b82
4
- data.tar.gz: 03004e7033f76268a1e96b3ee4a0c5d9066a4c716caab80c6082ddb60404b598
3
+ metadata.gz: 7da1033357e368c9a4d2a36b95a8e75899b61b63e872c25167dd5f8862afb454
4
+ data.tar.gz: '08df010ac42f946d8171152ad5ef4a8e6c1b94263cf39e5d297e4cbf805dca81'
5
5
  SHA512:
6
- metadata.gz: 5eac3ad625f051cc1e5fa62165076119d1cda689b619aeb6e83c9a32bbd33f343817c867cfd97abc26d32c56346eb833d39e747ec1d60ae9abf018e79a762442
7
- data.tar.gz: 6f01c8b3b6ef3382340d0743a87e34243e9283bb64d3cefec1a79a7fb23c670f8da02e0125f7497e8afa834077aa75c60830988f14b2c7e095a6f7fe3ac8e02b
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
- [![Join the chat at https://gitter.im/closure_tree/Lobby](https://badges.gitter.im/closure_tree/Lobby.svg)](https://gitter.im/closure_tree/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
9
- [![Build Status](https://api.travis-ci.org/ClosureTree/closure_tree.svg?branch=master)](http://travis-ci.org/ClosureTree/closure_tree)
8
+ [![CI](https://github.com/ClosureTree/closure_tree/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/ClosureTree/closure_tree/actions/workflows/ci.yml)
10
9
  [![Gem Version](https://badge.fury.io/rb/closure_tree.svg)](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 4.2, 5.0, 5.1, 5.2 and 6.0 with Ruby 2.5 and 2.6__
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 4.2 and later, and has test coverage for MySQL, PostgreSQL, and SQLite.
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 < ActiveRecord::Base
65
+ class Tag < ApplicationRecord
66
66
  has_closure_tree
67
67
  end
68
68
 
69
- class AnotherTag < ActiveRecord::Base
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 < ActiveRecord::Base
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 < ActiveRecord::Base
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 < ActiveRecord::Base
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 some RSpec2/3 matchers which you may use for your tests:
725
+ Closure tree comes with test matchers which you may use in your tests:
634
726
 
635
727
  ```ruby
636
- require 'spec_helper'
728
+ require 'test_helper'
637
729
  require 'closure_tree/test/matcher'
638
730
 
639
- describe Category do
640
- # Should syntax
641
- it { should be_a_closure_tree }
642
- # Expect syntax
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](http://travis-ci.org/#!/ClosureTree/closure_tree) of
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 2.5, 2.6 and 2.7
667
- * ActiveRecord 4.2, 5.x and 6.0
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
- Assuming you're using [rbenv](https://github.com/sstephenson/rbenv), you can use ```tests.sh``` to
671
- run the test matrix locally.
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 "pathname"
12
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
- Pathname.new(__FILE__).realpath)
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
14
 
15
- bundle_binstub = File.expand_path("../bundle", __FILE__)
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 "rubygems"
27
- require "bundler/setup"
26
+ require 'rubygems'
27
+ require 'bundler/setup'
28
28
 
29
- load Gem.bin_path("rake", "rake")
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 = ::ClosureTree::VERSION
8
- gem.authors = ['Matthew McEachen']
9
- gem.email = ['matthew-github@mceachen.org']
10
- gem.homepage = 'http://mceachen.github.io/closure_tree/'
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 = %q(Easily and efficiently make your ActiveRecord model support hierarchies)
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.files = `git ls-files`.split($/).reject do |f|
17
- f.match(%r{^(spec|img|gemfiles)})
18
- end
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.test_files = gem.files.grep(%r{^spec/})
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.add_runtime_dependency 'activerecord', '>= 4.2.10'
24
- gem.add_runtime_dependency 'with_advisory_lock', '>= 4.0.0'
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 'generator_spec'
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 quote(field)
5
- connection.quote(field)
6
- end
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
  class Configuration # :nodoc:
3
5
  attr_accessor :database_less
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ClosureTree
2
4
  module DeterministicOrdering
3
5
  def order_value
@@ -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
- module ClassMethods
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.reduce({}) { |h, ea| h[ea.id] = ea; h }
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"