Skip to content

extract_correlated_key incorrectly handles Arel::Nodes::And nodes with multiple children #1571

@riseshia

Description

@riseshia

Summary

ransack fails to build query which has more than 2 conditions on join clause, that is a quite rare case such as the relation using polymorphic and default_scope both. ;)

Reproduction

This could be reproducable via all versions, with Rails >= 7.1

# test-extract-correlated-key-bug.rb

# Run it in your console with: `ruby test-extract-correlated-key-bug.rb`

unless File.exist?('Gemfile')
  File.write('Gemfile', <<-GEMFILE)
    source 'https://rubygems.org'

    # Rails master
    gem 'rails', github: 'rails/rails', branch: 'main'

    # Rails last release
    # gem 'rails'

    gem 'sqlite3'
    gem 'ransack', github: 'activerecord-hackery/ransack'
  GEMFILE

  system 'bundle install'
end

require 'bundler'
Bundler.setup(:default)

require 'active_record'
require 'minitest/autorun'
require 'logger'
require 'ransack'

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
ActiveRecord::Base.logger = Logger.new(STDOUT)

# Display versions.
message = "Running test case with Ruby #{RUBY_VERSION}, Active Record #{
  ::ActiveRecord::VERSION::STRING}, Arel #{Arel::VERSION} and #{
  ::ActiveRecord::Base.connection.adapter_name}"
line = '=' * message.length
puts line, message, line

ActiveRecord::Schema.define do
  create_table :articles, force: true do |t|
    t.string :title
    t.text :body
    t.boolean :published, null: false, default: false
    t.timestamps null: false
  end

  create_table :notes, force: true do |t|
    t.string :note
    t.references :notable, polymorphic: true, null: false
    t.timestamps null: false
  end
end

class Article < ActiveRecord::Base
  has_many :notes, as: :notable
  has_many :recent_notes, as: :notable

  default_scope { where(published: true) }

  def self.ransackable_attributes(auth_object = nil)
    ["body", "created_at", "id", "published", "title", "updated_at"]
  end

  def self.ransackable_associations(auth_object = nil)
    ["notes", "recent_notes"]
  end
end

class Note < ActiveRecord::Base
  belongs_to :notable, polymorphic: true

  def self.ransackable_attributes(auth_object = nil)
    ["created_at", "id", "note", "notable_id", "notable_type", "updated_at"]
  end
end

class RecentNote < ActiveRecord::Base
  self.table_name = "notes"
  default_scope { where(notable_id: 1) }

  belongs_to :notable, polymorphic: true

  def self.ransackable_attributes(auth_object = nil)
    ["created_at", "id", "note", "notable_id", "notable_type", "updated_at"]
  end
end

class BugTest < Minitest::Test
  def test_extract_correlated_key_with_and_node
    # This test case demonstrates the bug where extract_correlated_key
    # returns nil for Arel::Nodes::And nodes instead of properly
    # checking all children nodes.

    search = Article.ransack(
      g: [
        {
          m: "and",
          c: [
            {
              a: ["recent_notes_note"],
              p: "not_cont_all",
              v: ["aa"],
            }
          ]
        }
      ]
    )

    expected_sql = <<~SQL.strip
      SELECT DISTINCT "articles".* FROM "articles"
      LEFT OUTER JOIN "notes" "recent_notes_articles_join"
        ON "recent_notes_articles_join"."notable_type" = 'Article'
        AND "recent_notes_articles_join"."notable_id" = "articles"."id"
        AND ("recent_notes_articles_join"."notable_id" = 1)
      WHERE "articles"."published" = 1
        AND (NOT ("recent_notes_articles_join"."note" LIKE '%aa%'))
    SQL

    # raises `NoMethodError: undefined method 'eq' for nil`
    assert_equal expected_sql, search.result.to_sql.strip
  end
end

How to fix

It seems to be enough to check all children instead of check left/right child:

--- a/lib/ransack/adapters/active_record/context.rb
+++ b/lib/ransack/adapters/active_record/context.rb
@@ -201,7 +201,10 @@ module Ransack
               nil
             end
           when Arel::Nodes::And
-            extract_correlated_key(join_root.left) || extract_correlated_key(join_root.right)
+            # And may have multiple children, so we need to check all, not via left/right
+            join_root.children.each do |child|
+              key = extract_correlated_key(child)
+              return key if key
+            end
+            nil
           else
             # eg parent was Arel::Nodes::And and the evaluated side was one of
             # Arel::Nodes::Grouping or MultiTenant::TenantEnforcementClause

Here is the patch with this #1572.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions