Skip to content

Commit

Permalink
Improve postgres interval overflow protection
Browse files Browse the repository at this point in the history
> Sometimes, operations on Times returns just float numbers of seconds, so, we
need to handle that.
> Example: Time.current - (Time.current + 1.hour) # => -3600.000001776 (Float)

When this happens, it's possible to endup with a pretty large number of seconds,
making the ISO 8601 formatted duration to trigger an `interval field value out
range error`.

PostgreSQL 15 has already improved overflow detection when casting values to
interval

However, to further reduce the likelihood of such issues and ensure
better-formatted duration types, it now implements the construction of
`ActiveSupport::Duration` during the serialization step.

How to reproduce (at least in versions prior to PG 15+):

``` ruby

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  gem "rails", github: "rails/rails", branch: "main"
  gem "pg"
end

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

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

ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :projects, force: true do |t|
    t.interval :duration
  end
end

class Project < ActiveRecord::Base; end

class IntervalBugTest < Minitest::Test
  def test_duration
    project = Project.create(duration: 70.years)
    assert_equal 70.years, project.duration
  end

  def test_duration_error
    duration = 70.years.ago - Time.now()

    assert_raises ActiveRecord::StatementInvalid do
      Project.create(duration: duration)
    end
  end
end
```
  • Loading branch information
EduardoGHdez committed Nov 24, 2023
1 parent 04f29cb commit c528b34
Show file tree
Hide file tree
Showing 2 changed files with 8 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def serialize(value)
when ::Numeric
# Sometimes operations on Times returns just float number of seconds so we need to handle that.
# Example: Time.current - (Time.current + 1.hour) # => -3600.000001776 (Float)
value.seconds.iso8601(precision: self.precision)
ActiveSupport::Duration.build(value).iso8601(precision: self.precision)
else
super
end
Expand Down
7 changes: 7 additions & 0 deletions activerecord/test/cases/adapters/postgresql/datatype_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ def test_time_values
assert_equal (-21.day), @first_time.scaled_time_interval
end

def test_update_large_time_in_seconds
@first_time.scaled_time_interval = 70.years.to_f
assert @first_time.save
assert @first_time.reload
assert_equal 70.years, @first_time.scaled_time_interval
end

def test_oid_values
assert_equal 1234, @first_oid.obj_id
end
Expand Down

0 comments on commit c528b34

Please sign in to comment.