-
-
Notifications
You must be signed in to change notification settings - Fork 829
Closed
Description
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'
end2. 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
end3. 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 "=" * 80Run 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
Labels
No labels