Skip to content

Ransack treats integer 1 as truthy in scope parameters #1654

@phbrugnolo

Description

@phbrugnolo

Environment

  • Ruby version: 3.4.6
  • Rails version: 8.1.1
  • Ransack version: 4.1.1

Description

When using Ransack with a custom scope that expects an integer argument, passing 1 causes the scope to receive no arguments (treated as boolean true), while other integers like 2, 3, etc. work correctly.

Steps to Reproduce

1. Create the models

# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class

  def self.ransackable_attributes(auth_object = nil)
    column_names + _ransackers.keys
  end
end

# app/models/family.rb
class Family < ApplicationRecord
  has_many :family_members
end

# app/models/person.rb
class Person < ApplicationRecord
  has_many :family_members
  has_many :families, through: :family_members
  has_many :personal_bonds

  scope :family_bonds, ->(family_id) {
    puts "Received argument: #{family_id.inspect} (class: #{family_id.class})"

    family_members_subquery = FamilyMember.active.where(family_id: family_id).select(:person_id)
    bonds_from = PersonalBond.where(person_id: family_members_subquery).select(:bonded_person_id)
    bonds_to = PersonalBond.where(bonded_person_id: family_members_subquery).select(:person_id)
    where(id: family_members_subquery).or(where(id: bonds_from)).or(where(id: bonds_to)).distinct
  }

  def self.ransackable_scopes(_auth_object = nil)
    %i[family_bonds]
  end
end

# app/models/family_member.rb
class FamilyMember < ApplicationRecord
  belongs_to :family
  belongs_to :person

  scope :active, (-> { active_at(Date.current) })
  scope :active_at, (->(date) { where("(#{table_name}.started_at IS NOT NULL AND #{table_name}.started_at <= ?) AND (#{table_name}.finished_at IS NULL OR #{table_name}.finished_at >= ?)", date, date) })
end

# app/models/personal_bond.rb
class PersonalBond < ApplicationRecord
  belongs_to :person
  belongs_to :bonded_person, class_name: 'Person'
end

2. Create the database schema

# db/migrate/xxx_create_test_schema.rb
class CreateTestSchema < ActiveRecord::Migration[8.1]
  def change
    create_table :families do |t|
      t.string :name
      t.timestamps
    end

    create_table :people do |t|
      t.string :name
      t.timestamps
    end

    create_table :family_members do |t|
      t.references :family, null: false, foreign_key: true
      t.references :person, null: false, foreign_key: true
      t.date :started_at
      t.date :finished_at
      t.timestamps
    end

    create_table :personal_bonds do |t|
      t.references :person, null: false, foreign_key: true
      t.references :bonded_person, null: false, foreign_key: { to_table: :people }
      t.timestamps
    end
  end
end

3. Run the test script

# test.rb
# Reset database
ActiveRecord::Base.connection.tables.each do |table|
  next if table == 'schema_migrations' || table == 'ar_internal_metadata'
  ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS #{table}")
end
load Rails.root.join('db', 'schema.rb')


# Create test data
puts "=" * 80
puts "CREATING TEST DATA"
puts "=" * 80

family1 = Family.create!(name: 'Family 1')
family2 = Family.create!(name: 'Family 2')

person1 = Person.create!(name: 'Person 1')
person2 = Person.create!(name: 'Person 2')
person3 = Person.create!(name: 'Person 3')

FamilyMember.create!(family: family1, person: person1, started_at: Date.today)
FamilyMember.create!(family: family1, person: person2, started_at: Date.today)
FamilyMember.create!(family: family2, person: person3, started_at: Date.today)

puts "\nData created:"
puts "- Family 1 (id: 1) with Person 1 and Person 2"
puts "- Family 2 (id: 2) with Person 3"
puts ""

# ============================================================================
# TEST 1: Direct scope call (works correctly)
# ============================================================================
puts "=" * 80
puts "TEST 1: Direct scope call Person.family_bonds(1)"
puts "=" * 80
puts "Expected: Scope receives argument 1 correctly"
puts ""

begin
  result = Person.family_bonds(1)
  puts "✓ SUCCESS: Returned #{result.count} people"
  puts "  People: #{result.pluck(:name).join(', ')}"
rescue => e
  puts "✗ FAILED: #{e.class} - #{e.message}"
end

puts ""

# ============================================================================
# TEST 2: Ransack with value 1 (BUG - treats the scope as truthy)
# ============================================================================
puts "=" * 80
puts "TEST 2: Ransack with value 1 - Person.ransack!(family_bonds: 1)"
puts "=" * 80
puts "Expected: Scope receives argument 1"
puts "Actual: Scope receives NO arguments (treated as truthy)"
puts ""

begin
  search = Person.ransack!(family_bonds: 1)
  result = search.result
  puts "✓ SUCCESS: Returned #{result.count} people"
  puts "  People: #{result.pluck(:name).join(', ')}"
rescue ArgumentError => e
  puts "✗ BUG: #{e.class} - #{e.message}"
  puts "  Ransack is treating 1 as truthy and not passing argument to scope!"
rescue => e
  puts "✗ UNEXPECTED ERROR: #{e.class} - #{e.message}"
end

puts ""

# ============================================================================
# TEST 3: Ransack with value 2 (works correctly)
# ============================================================================
puts "=" * 80
puts "TEST 3: Ransack with value 2 - Person.ransack!(family_bonds: 2)"
puts "=" * 80
puts "Expected: Scope receives argument 2 correctly"
puts ""

begin
  search = Person.ransack!(family_bonds: 2)
  result = search.result
  puts "✓ SUCCESS: Returned #{result.count} people"
  puts "  People: #{result.pluck(:name).join(', ')}"
rescue => e
  puts "✗ FAILED: #{e.class} - #{e.message}"
end

puts ""

# ============================================================================
# TEST 4: Ransack with value 3 (works correctly)
# ============================================================================
puts "=" * 80
puts "TEST 4: Ransack with value 3 - Person.ransack!(family_bonds: 3)"
puts "=" * 80
puts "Expected: Scope receives argument 3 correctly"
puts ""

begin
  search = Person.ransack!(family_bonds: 3)
  result = search.result
  puts "✓ SUCCESS: Returned #{result.count} people"
  puts "  People: #{result.pluck(:name).join(', ')}"
rescue => e
  puts "✗ FAILED: #{e.class} - #{e.message}"
end

puts ""

# ============================================================================
# TEST 5: Ransack with string "1" 
# ============================================================================
puts "=" * 80
puts "TEST 5: Ransack with string '1' - Person.ransack!(family_bonds: '1')"
puts "=" * 80
puts "Testing if passing as string avoids the bug"
puts ""

begin
  search = Person.ransack!(family_bonds: '1')
  result = search.result
  puts "✓ SUCCESS: Returned #{result.count} people"
  puts "  People: #{result.pluck(:name).join(', ')}"
rescue => e
  puts "✗ FAILED: #{e.class} - #{e.message}"
end

puts ""

# ============================================================================
# SUMMARY
# ============================================================================
puts "=" * 80
puts "SUMMARY"
puts "=" * 80
puts "The bug is confirmed when:"
puts "- Person.family_bonds(1) works ✓"
puts "- Person.ransack!(family_bonds: 2) works ✓"
puts "- Person.ransack!(family_bonds: 1) FAILS ✗"
puts ""
puts "Cause: Ransack treats 1 as boolean true"
puts "=" * 80

Run with: rails runner test.rb

Expected Behavior

Person.ransack!(family_bonds: 1) should pass the integer 1 as an argument to the family_bonds scope, just like it does with 2, 3, or any other integer.

Actual Behavior

When passing 1 as the value, Ransack appears to treat it as a truthy and calls the scope without arguments, causing:

ArgumentError: wrong number of arguments (given 0, expected 1)

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