Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unexpected behavior when calling Model.find_by_id with true or false #51727

Open
olepalm opened this issue May 3, 2024 · 5 comments
Open

Unexpected behavior when calling Model.find_by_id with true or false #51727

olepalm opened this issue May 3, 2024 · 5 comments

Comments

@olepalm
Copy link

olepalm commented May 3, 2024

Steps to reproduce

Assuming a model with id 1 exists in the database

Model.find_by_id(true) 
# => #\<Model:0x0000012323 id: 1 ... >

Expected behavior

Empty result (nil), as no Model has true as id.

More or less the same result as one would get if no records with id 1 exists.

These changes should also be reflected in Model.find(true) and similar find methods. (Model.find_by(:id => true))

Alternatively, if this behavior is intended to support databases without explicit boolean data types (like MySQL), maybe the documentation on https://api.rubyonrails.org/classes/ActiveModel/Type/Integer.html should be updated.

Specifically

Values are cast using their to_i method, except for blank strings, which are cast to nil. If a to_i method is not defined or raises an error, the value will be cast to nil.

Which is not the behavior observed.

Actual behavior

true is converted to 1, which results in query like this:

Model Load (1.2ms) SELECT "models".* FROM "models" WHERE "models"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]

Which is somewhat surprising, as instead of returning nil you may get an instance of Model with id == 1, which is usually very likely to exist.

System configuration

Rails version: 7.1.3.2

Ruby version: 3.2.0

@maniSHarma7575
Copy link
Contributor

maniSHarma7575 commented May 5, 2024

@olepalm I believe this behavior is expected since there are test cases for it in ActiveModel Integer, where we serialize boolean values to integers.

test "casting booleans for database" do
type = Type::Integer.new
assert_equal 1, type.serialize(true)
assert_equal 0, type.serialize(false)
end

We need to await comments from the Rails core team regarding whether this behavior is expected or necessitates a fix. If a fix is indeed required, then we'll need to update the serialize method accordingly.

I'll submit a pull request once we receive confirmation from the Rails maintenance team.

def serialize(value)
return if value.is_a?(::String) && non_numeric_string?(value)
ensure_in_range(super)
end

to

  def serialize(value)
    return if value.is_a?(::TrueClass) || value.is_a?(::FalseClass)
    return if value.is_a?(::String) && non_numeric_string?(value)
    ensure_in_range(super)
  end

Reproduce Script:

# frozen_string_literal: true

require "bundler/inline"
require "debug"
gemfile(true) do
  source "https://rubygems.org"

  # If you want to test against edge Rails replace the previous line with this:
  gem "rails", github: "rails/rails", branch: "main"

  gem "sqlite3", "~> 1.4"
end

require "active_record"
require "minitest/autorun"
require "logger"

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

ActiveRecord::Schema.define do
  create_table :posts do |t|
    t.string :title
    t.timestamps
  end
end

class Post < ActiveRecord::Base
end

class BugTest < Minitest::Test
  def test_find_methods
    post = Post.new(title: "Example Post")
    post.save
    post = Post.last
    assert_nil Post.find_by(id: true)
    assert_nil Post.find_by_id(true)
    assert_raises do
      Post.find(true)
    end
  end
end

@maniSHarma7575
Copy link
Contributor

@byroot / @fatkodima Does this need to be fixed, or is it the desired behavior?

@byroot
Copy link
Member

byroot commented May 6, 2024

I'd say ideally it should be fixed, but I fear this may break apps, and I'm not convinced this is a big enough problem to take that risk.

If you have time it would be worth to use git blame to find the commit that introduced this behavior to see if there was any historical reason to do so.

@rafaelfranca
Copy link
Member

Some databases didn't had boolean, so you needed to transform them in integer. That is the reason for this behavior.

SQLite is one of them

2.1. Boolean Datatype
SQLite does not have a separate Boolean storage class. Instead, Boolean values are stored as integers 0 (false) and 1 (true).

SQLite recognizes the keywords "TRUE" and "FALSE", as of version 3.23.0 (2018-04-02) but those keywords are really just alternative spellings for the integer literals 1 and 0 respectively.

https://www.sqlite.org/datatype3.html

@maniSHarma7575
Copy link
Contributor

maniSHarma7575 commented May 7, 2024

Some databases didn't had boolean, so you needed to transform them in integer. That is the reason for this behavior.

@rafaelfranca, I believe the issue lies in how the find, find_by_id, and find_by(id: ...) methods filter based on the primary key id, typically an integer.

The complication arises with databases like SQLite, lacking a distinct Boolean storage class. Consequently, we're converting Boolean values to integers.

When passing a Boolean to these methods, incorrect results occur:

True returns records with id equal to 1.
False returns records with id equal to 0.

Indeed, the desired behavior would be for the methods to return nil when passed a Boolean value. However, currently, they incorrectly return records with id equal to 1 for True and 0 for False

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants