9

Does Rails really not properly support PostgreSQL's interval data type?

I had to use this Stack Overflow answer from 2013 to create an interval column, and now it looks like I'll need to use this piece of code from 2013 to get ActiveRecord to treat the interval as something other than a string.

Is that how it is? Am I better off just using an integer data type to represent the number of minutes instead?

3
  • 2
    The rails sources seems to mention interval in the appropriate places so it looks like Rails5 does. I'd answer but I don't have a Rails5 setup handy to verify my guesswork and I'm not sure how extensive the support is (if it really is there). Commented May 13, 2017 at 4:14
  • Thanks for this. It looks like some of the 'interval' stuff has been added fairly recently, so I'm going to upgrade to 5.1.1 and see if it works any better. Commented May 14, 2017 at 12:24
  • 2
    Looks like upgrading fixes the the first problem (allowing you to easily create the interval column), but doesn't fix the second (having activerecord interpret the column as an interval, rather than a string). I think it's probably easier to just use an integer data type. Thanks for the help. Commented May 14, 2017 at 17:09

4 Answers 4

4

From Rails 5.1, you can use postgres 'Interval' Data Type, so you can do things like this in a migration:

add_column :your_table, :new_column, :interval, default: "2 weeks"

Although ActiveRecord only treat interval as string, but if you set the IntervalStyle to iso_8601 in your postgresql database, it will display the interval in iso8601 style: 2 weeks => P14D

execute "ALTER DATABASE your_database SET IntervalStyle = 'iso_8601'"

You can then directly parse the column to a ActiveSupport::Duration

In your model.rb

def new_column
  ActiveSupport::Duration.parse self[:new_column]
end

More infomation of ISO8601 intervals can be find at https://en.wikipedia.org/wiki/ISO_8601#Time_intervals

Sign up to request clarification or add additional context in comments.

1 Comment

Helpful tip: interpolate the database name into the string for portability: execute "ALTER DATABASE \"#{connection.current_database}\" SET intervalstyle = 'iso_8601'"
2

I had a similar issue and went with defining reader method for the particular column on the ActiveRecord model. Like this:

class DivingResults < ActiveRecord::Base
  # This overrides the same method for db column, generated by rails
  def underwater_duration
    interval_from_db = super
    time_parts = interval_from_db.split(':')
    if time_parts.size > 1 # Handle formats like 17:04:41.478432
      units = %i(hours minutes seconds)
      in_seconds = time_parts
        .map.with_index { |t,i| t.to_i.public_send(units[i]) }
        .reduce(&:+) # Turn each part to seconds and then sum
      ActiveSupport::Duration.build in_seconds
    else # Handle formats in seconds
      ActiveSupport::Duration.build(interval_from_db.to_i)
    end
  end
end

This allows to use ActiveSupport::Duration instance elsewhere. Hopefully Rails will start handling the PostgreSQL interval data type automatically in near future.

1 Comment

That is not scalable and involves implicit parsing, formatting, and then explicit parsing again.
1

A more complete and integrated solution is available in Rails 6.1


The current answers suggest overriding readers and writers in the models. I took the alter database suggestion and built a gem for ISO8601 intervals, ar_interval.

It provides a simple ActiveRecord::Type that deals with the serialization and casting of ISO8601 strings for you!

The tests include examples for how to use it.

If there is interest, the additional formats Sam Soffes demonstrates could be included in the tests

Comments

0

Similar to Madis' solution, this one handles fractions of a second and ISO8601 durations:

def duration
  return nil unless (value = super)

  # Handle ISO8601 duration
  return ActiveSupport::Duration.parse(value) if value.start_with?('P')

  time_parts = value.split(':')
  if time_parts.size > 1
    # Handle formats like 17:04:41.478432
    units = %i[hours minutes seconds]
    in_seconds = time_parts.map.with_index { |t, i| t.to_f.public_send(units[i]) }.reduce(&:+)
    ActiveSupport::Duration.build in_seconds
  else
    # Handle formats in seconds
    ActiveSupport::Duration.build(value)
  end
end

def duration=(value)
  unless value.is_a?(String)
    value = ActiveSupport::Duration.build(value).iso8601
  end

  self[:duration] = value
end

This assumes you setup your database like Leo mentions in his answer. No idea why sometimes they come back from Postgres in the PT42S format and sometimes in the 00:00:42.000 format :/

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.