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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +111 -38
  4. data/bin/rails +15 -0
  5. data/bin/rake +7 -7
  6. data/closure_tree.gemspec +11 -17
  7. data/lib/closure_tree/active_record_support.rb +4 -1
  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 +6 -4
  13. data/lib/closure_tree/finders.rb +103 -54
  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 +2 -1
  17. data/lib/closure_tree/hash_tree_support.rb +38 -13
  18. data/lib/closure_tree/hierarchy_maintenance.rb +19 -26
  19. data/lib/closure_tree/model.rb +29 -29
  20. data/lib/closure_tree/numeric_deterministic_ordering.rb +90 -55
  21. data/lib/closure_tree/numeric_order_support.rb +20 -18
  22. data/lib/closure_tree/support.rb +29 -32
  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 +6 -4
  30. data/lib/generators/closure_tree/templates/config.rb +2 -0
  31. metadata +12 -104
  32. data/.github/workflows/ci.yml +0 -72
  33. data/.github/workflows/ci_jruby.yml +0 -68
  34. data/.github/workflows/ci_truffleruby.yml +0 -71
  35. data/.github/workflows/release.yml +0 -17
  36. data/.gitignore +0 -17
  37. data/.release-please-manifest.json +0 -1
  38. data/.rspec +0 -1
  39. data/.tool-versions +0 -1
  40. data/.yardopts +0 -3
  41. data/Appraisals +0 -61
  42. data/Gemfile +0 -6
  43. data/Rakefile +0 -32
  44. data/bin/appraisal +0 -29
  45. data/bin/rspec +0 -29
  46. data/mktree.rb +0 -38
  47. data/release-please-config.json +0 -4
  48. data/test/closure_tree/cache_invalidation_test.rb +0 -36
  49. data/test/closure_tree/cuisine_type_test.rb +0 -42
  50. data/test/closure_tree/generator_test.rb +0 -49
  51. data/test/closure_tree/has_closure_tree_root_test.rb +0 -80
  52. data/test/closure_tree/hierarchy_maintenance_test.rb +0 -56
  53. data/test/closure_tree/label_test.rb +0 -674
  54. data/test/closure_tree/metal_test.rb +0 -59
  55. data/test/closure_tree/model_test.rb +0 -9
  56. data/test/closure_tree/namespace_type_test.rb +0 -13
  57. data/test/closure_tree/parallel_test.rb +0 -162
  58. data/test/closure_tree/pool_test.rb +0 -33
  59. data/test/closure_tree/support_test.rb +0 -18
  60. data/test/closure_tree/tag_test.rb +0 -8
  61. data/test/closure_tree/user_test.rb +0 -175
  62. data/test/closure_tree/uuid_tag_test.rb +0 -8
  63. data/test/support/query_counter.rb +0 -25
  64. data/test/support/tag_examples.rb +0 -923
  65. data/test/test_helper.rb +0 -99
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1833e9f0cef7b45722dc67fcea625de8ee04bd820de6567da77f5f7bf3a97e65
4
- data.tar.gz: 6c0c050d6b081b612b11d76c2e79b1f84f56dade7ad3b6f2b445f9bfa3d90d1d
3
+ metadata.gz: 7da1033357e368c9a4d2a36b95a8e75899b61b63e872c25167dd5f8862afb454
4
+ data.tar.gz: '08df010ac42f946d8171152ad5ef4a8e6c1b94263cf39e5d297e4cbf805dca81'
5
5
  SHA512:
6
- metadata.gz: 3d61b4e41fd71552f54aba6bfec350fc75d5a923797c580fe2d7e48df6e7f3917477f7992c6bd9d59b614730f9978c77acd100ee4d848d737afb42e3e629dcf6
7
- data.tar.gz: ed181080ce63b08712cbd8981cfa057cb4c432ed228cff8dac22a3770f9d41d7a81adff3b3b251bffb710e030952d6fe570e767707c835d72cbb0915ec328008
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.1+ with Ruby 3.3+__
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.1 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.
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 < ActiveRecord::Base
65
+ class Tag < ApplicationRecord
65
66
  has_closure_tree
66
67
  end
67
68
 
68
- class AnotherTag < ActiveRecord::Base
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 < ActiveRecord::Base
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 < ActiveRecord::Base
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 < ActiveRecord::Base
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 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:
633
726
 
634
727
  ```ruby
635
- require 'spec_helper'
728
+ require 'test_helper'
636
729
  require 'closure_tree/test/matcher'
637
730
 
638
- describe Category do
639
- # Should syntax
640
- it { should be_a_closure_tree }
641
- # Expect syntax
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.1+
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
- $ appraisal bundle # this will install the matrix of dependencies
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 appraisal activerecord-7.0 rake test
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 "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,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 = ::ClosureTree::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 = %q(Easily and efficiently make your ActiveRecord model support hierarchies)
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' => "https://github.com/ClosureTree/closure_tree/issues",
17
- 'changelog_uri' => "https://github.com/ClosureTree/closure_tree/blob/master/CHANGELOG.md",
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' => "https://closuretree.github.io/closure_tree/",
20
- 'source_code_uri' => "https://github.com/ClosureTree/closure_tree",
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 = `git ls-files`.split($/).reject do |f|
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.add_runtime_dependency 'activerecord', '>= 7.1.0'
31
- gem.add_runtime_dependency 'with_advisory_lock', '>= 5.0.0', '< 6.0.0'
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 'rspec-instafail'
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, suff = model.table_name_prefix, model.table_name_suffix
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
  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
@@ -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.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"