Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Incorrect infinity and NaN conversion to Ruby values in postgresql adapter #13334

Closed
wants to merge 1 commit into from

5 participants

@gregolsen

Active Record successfully stores Ruby Infinity value in a Postgresql DB column of double precision type.
However it fails to convert it back to Ruby Float::INFINITY when the record is fetched from DB.
The issue can be illustrated with the code below:
While the value is correctly stored as 'Infinity' when it is accessed through AR instance - it's converted to 0.

It looks like the problem is in the type cast which is simply to_f for the float values.
Thus - in case 'Infinity' or 'NaN' in database it converts it to 0.

update - all below is fixed
The naive fix in this PR is rather to start the discussion and clarify is this really an issue or I'm just missing something.
For example after applying this fix Point.create starts throwing an exception Infinity (FloatDomainError) - that's because OID::Float#type_cast is called with Float::INFINITY as an argument.

require 'active_record'
require 'pg'

ActiveRecord::Base.establish_connection(
  database: 'infinity',
  adapter: 'postgresql',
  username: 'postgres'
)

ActiveRecord::Base.connection.instance_eval do
  create_table :points, force: true do |t|
    t.column :value, :float
  end
end

class Point < ActiveRecord::Base; end
infinity = 1.0/0
Point.create(value: infinity)
point = Point.first
p Point.connection.execute("select value from points limit 1").to_a #=> [{"value"=>"Infinity"}]
p point.read_attribute_before_type_cast('value') #=> "Infinity"
p point.value #=> 0.0 when it should be Float::INFINITY
@pftg

Please add changelog entry into this PR.

...b/active_record/connection_adapters/postgresql/oid.rb
@@ -211,8 +211,13 @@ def type_cast(value)
class Float < Type
def type_cast(value)
return if value.nil?
-
- value.to_f
+ case value
+ when 'Infinity' then ::Float::INFINITY

This is likely a hotspot area. Perhaps extract these strings into constants to avoid extra object allocation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@gregolsen

Changelog entry added, commits squashed

@senny
Owner

This looks reasonable. Can you push a rebased version?

@gregolsen

@senny I've rebased this PR and fixed the specs. However one spec MemCacheStoreTest#test_deserializes_unloaded_class has failed due to memcache server timeout which has nothing to do with the changes introduced by this PR I hope :-)

@senny
Owner

@gregolsen yea that happens from time to time, will restart the job for you.

@senny senny commented on the diff
...b/active_record/connection_adapters/postgresql/oid.rb
@@ -255,8 +255,15 @@ def type; :float end
def type_cast(value)
return if value.nil?
+ return value unless ::String === value
@senny Owner
senny added a note

what call prompted this guard? Do we have a test to make sure we don't get regressions?

Tests were failing previously but now it looks fine without this guard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
activerecord/CHANGELOG.md
@@ -1,3 +1,18 @@
+* Fix `PostgreSQLAdapter::OID::Float#type_cast` to convert Infinity and
+ NaN PostgreSQL values into a native Ruby `Float::INFINITY` and `Float::NAN`
+
+ Example:
+
+ # Before
+ Point.create(value: 1.0/0)
+ p Point.last.value #=> 0.0
@senny Owner
senny added a note

no need to have p in front of the expression. The comment # => signals that we are talking about the evaluated value of the expression on the left. Nitpick: can you include a space after # so that it looks like # =>?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
activerecord/CHANGELOG.md
@@ -1,3 +1,18 @@
+* Fix `PostgreSQLAdapter::OID::Float#type_cast` to convert Infinity and
+ NaN PostgreSQL values into a native Ruby `Float::INFINITY` and `Float::NAN`
+
+ Example:
+
+ # Before
+ Point.create(value: 1.0/0)
+ p Point.last.value #=> 0.0
+
+ # After
+ Point.create(value: 1.0/0)
+ p Point.last.value #=> Infinity
@senny Owner
senny added a note

same as above

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@senny senny commented on the diff
...b/active_record/connection_adapters/postgresql/oid.rb
((5 lines not shown))
- value.to_f
+ case value
+ when 'Infinity' then ::Float::INFINITY
+ when '-Infinity' then -::Float::INFINITY
+ when 'NaN' then ::Float::NAN
+ else
+ value.to_f
+ end
@senny Owner
senny added a note

talked this over with @jeremy . He suggested to create a type map to perform the conversion:

TYPE_CASTS.fetch(value) { value.to_f };  TYPE_CASTS = { nil => nil, ‘Infinity’ => Float::INFINITY, … }
@senny Owner
senny added a note

@gregolsen after further discussion we think folding the guard into the case statement is better looking than the type map. Please strike my previous comment and include the guard in the case. Sorry for the roundtrip :blush:

no problem :smiley:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@senny senny closed this in 4320b77
@senny
Owner

@gregolsen Thanks :yellow_heart: . I applied a rebased and slightly modified version of the patch.

@gregolsen

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Apr 2, 2014
  1. @gregolsen

    Fix PostgreSQL Infinity and NaN values conversion into native Ruby Fl…

    gregolsen authored
    …oat::INFINITY and Float::NAN
This page is out of date. Refresh to see the latest.
View
15 activerecord/CHANGELOG.md
@@ -1,3 +1,18 @@
+* Fix `PostgreSQLAdapter::OID::Float#type_cast` to convert Infinity and
+ NaN PostgreSQL values into a native Ruby `Float::INFINITY` and `Float::NAN`
+
+ Example:
+
+ # Before
+ Point.create(value: 1.0/0)
+ Point.last.value # => 0.0
+
+ # After
+ Point.create(value: 1.0/0)
+ Point.last.value # => Infinity
+
+ *Innokenty Mikhailov*
+
* The PostgreSQL adapter supports custom domains. Fixes #14305.
*Yves Senn*
View
9 activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -255,8 +255,15 @@ def type; :float end
def type_cast(value)
return if value.nil?
+ return value unless ::String === value
@senny Owner
senny added a note

what call prompted this guard? Do we have a test to make sure we don't get regressions?

Tests were failing previously but now it looks fine without this guard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
- value.to_f
+ case value
+ when 'Infinity' then ::Float::INFINITY
+ when '-Infinity' then -::Float::INFINITY
+ when 'NaN' then ::Float::NAN
+ else
+ value.to_f
+ end
@senny Owner
senny added a note

talked this over with @jeremy . He suggested to create a type map to perform the conversion:

TYPE_CASTS.fetch(value) { value.to_f };  TYPE_CASTS = { nil => nil, ‘Infinity’ => Float::INFINITY, … }
@senny Owner
senny added a note

@gregolsen after further discussion we think folding the guard into the case statement is better looking than the type map. Please strike my previous comment and include the guard in the case. Sorry for the roundtrip :blush:

no problem :smiley:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
end
end
View
7 activerecord/test/cases/adapters/postgresql/datatype_test.rb
@@ -50,7 +50,11 @@ def setup
@second_money = PostgresqlMoney.find(2)
@connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (1, 123.456, 123456.789)")
+ @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (2, '-Infinity', 'Infinity')")
+ @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (3, 123.456, 'NaN')")
@first_number = PostgresqlNumber.find(1)
+ @second_number = PostgresqlNumber.find(2)
+ @third_number = PostgresqlNumber.find(3)
@connection.execute("INSERT INTO postgresql_times (id, time_interval, scaled_time_interval) VALUES (1, '1 year 2 days ago', '3 weeks ago')")
@first_time = PostgresqlTime.find(1)
@@ -154,6 +158,9 @@ def test_update_tsvector
def test_number_values
assert_equal 123.456, @first_number.single
assert_equal 123456.789, @first_number.double
+ assert_equal -::Float::INFINITY, @second_number.single
+ assert_equal ::Float::INFINITY, @second_number.double
+ assert_same ::Float::NAN, @third_number.double
end
def test_time_values
Something went wrong with that request. Please try again.