diff --git a/lib/closure_tree/support.rb b/lib/closure_tree/support.rb index 1245dab..f7cc5df 100644 --- a/lib/closure_tree/support.rb +++ b/lib/closure_tree/support.rb @@ -35,9 +35,25 @@ def initialize(model_class, options) extend NumericOrderSupport.adapter_for_connection(connection) end + # Find the abstract base class for database connection + # This ensures hierarchy class uses the same database but doesn't inherit + # validations/callbacks from STI parent classes (issue #392) + def abstract_base_class + klass = model_class + while klass.superclass != ActiveRecord::Base + parent = klass.superclass + # Stop at abstract class (ApplicationRecord, MysqlRecord, etc.) + return parent if parent.abstract_class? + # Stop at connection boundary (handles non-abstract parents with custom connections) + return parent if parent.connection_specification_name != parent.superclass.connection_specification_name + klass = parent + end + ActiveRecord::Base + end + def hierarchy_class_for_model parent_class = model_class.module_parent - hierarchy_class = parent_class.const_set(short_hierarchy_class_name, Class.new(model_class.superclass)) + hierarchy_class = parent_class.const_set(short_hierarchy_class_name, Class.new(abstract_base_class)) model_class_name = model_class.to_s hierarchy_class.class_eval do # Rails 8.1+ requires an implicit_order_column for models without a primary key diff --git a/test/closure_tree/hierarchy_inheritance_test.rb b/test/closure_tree/hierarchy_inheritance_test.rb new file mode 100644 index 0000000..7de02ac --- /dev/null +++ b/test/closure_tree/hierarchy_inheritance_test.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'test_helper' + +class HierarchyInheritanceTest < ActiveSupport::TestCase + # Test for issue #392: Hierarchy class should inherit from same abstract base as model + # but NOT from STI parent (to avoid inheriting validations/callbacks) + test 'MetalHierarchy inherits from same connection class as Metal' do + # Force MetalHierarchy to be loaded + Metal._ct + + # Metal < ApplicationRecord (abstract connection class) + # Adamantium < Metal (STI child) + # MetalHierarchy should inherit from ApplicationRecord, NOT Metal + assert_equal Metal.superclass, MetalHierarchy.superclass, + "MetalHierarchy should inherit from same abstract base as Metal (#{Metal.superclass})" + + # Verify it's the abstract class, not the STI parent + assert MetalHierarchy.superclass.abstract_class?, + "MetalHierarchy should inherit from abstract class" + + # The hierarchy class should NOT inherit validations from Metal + assert_not_equal Metal.validators.size, MetalHierarchy.validators.size, + "MetalHierarchy should not inherit validations from Metal" + end + + test 'Adamantium inherits has_closure_tree and uses same hierarchy as Metal' do + # Adamantium < Metal (STI - should inherit has_closure_tree) + + # Verify Adamantium inherited has_closure_tree + assert_respond_to Adamantium, :_ct, "Adamantium should inherit has_closure_tree from Metal" + + # Both should use the same hierarchy class (MetalHierarchy) + assert_equal Metal.hierarchy_class, Adamantium.hierarchy_class, + "Adamantium should use same hierarchy class as Metal (STI)" + + # The hierarchy class should inherit from ApplicationRecord, NOT Metal + assert_equal ApplicationRecord, MetalHierarchy.superclass, + "MetalHierarchy should inherit from ApplicationRecord, not Metal" + end +end