Timestamp can't be saved - postgresql - ruby-on-rails

I have the below code snippet:
class Foo
include DataMapper::Resource
property :id, Serial
property :timestamp, DateTime
end
I just want to convert the current time to ms:
class Time
def to_ms
(self.to_f * 1000.0).to_i
end
end
def current_time
time = Time.now
return time.to_ms
end
time = current_time # => 1352633569151
but when I am going to save the Foo with above timestamp, then it can't be saved to the database and I'm not getting any error message.
foo = Foo.new
foo.timestamp = time
foo.save
Any idea?

are you using a correct format for your :datetime property?
should be like:
DateTime.now.to_s
=> "2012-11-11T14:04:02+02:00"
or a "native" DateTime object, without any conversions.
DataMapper will carry to convert it to according values based on adapter used.
also, to have exceptions raised when saving items:
DataMapper::Model.raise_on_save_failure = true
that's a global setting, i.e. all models will raise exceptions.
to make only some model to behave like this:
YourModel.raise_on_save_failure = true
http://datamapper.org/docs/create_and_destroy.html
See "Raising an exception when save fails" chapter
btw, to see what's wrong with your item before saving it, use and item.valid? and item.errors
foo = Foo.new
foo.timestamp = time
if foo.valid?
foo.save
else
p foo.errors
end
I replicated your code and got following error:
#errors={:timestamp=>["Timestamp must be of type DateTime"]}
See live demo here

The PostgreSQL data types would be timestamp or timestamp with time zone. But that contradicts what you are doing. You take the epoch value and multiply by 1000. You'd have to save that as integer or some numeric type.
More about the handling of timestamps in Postgres in this related answer.
I would save the value as timestamp with time zone as is (no multiplication). You can always extract ms out of it if need should be.
If you need to translate the Unix epoch value back to a timestamp, use:
SELECT to_timestamp(1352633569.151);
--> timestamptz 2012-11-11 12:32:49.151+01
Just save "now"
If you actually want to save "now", i.e. the current point in time, then let Postgres do it for you. Just make sure the database server has a reliable local time - install ntp. This is generally more reliable, accurate and simple.
Set the DEFAULT of the timestamp column to now() or CURRENT_TIMESTAMP.
If you want timestamp instead of timestamptz you can still use now(), which is translated to "local" time according to the servers timezone setting. Or, to get the time for a given time zone:
now() AT ZIME ZONE 'Europe/Vienna' -- your time zone here
Or, in your particular case, since you seem to want only three fractional digits: now()::timestamp(3) or CURRENT_TIMESTAMP(3) or CURRENT_TIMESTAMP(3) AT ZIME ZONE 'Europe/Vienna'.
Or, if you define the type of the column as timestamp(3), all timestamp values are coerced to the type and rounded to 3 fractional decimal digits automatically.
So this would be all you need:
CREATE TABLE tbl (
-- other columns
,ts_column timestamp(3) DEFAULT now()
);
The value is set automatically on INSERT, you don't even have to mention the column.
If you want to update it ON UPDATE, add a TRIGGER like this:
Trigger function:
CREATE OR REPLACE FUNCTION trg_up_ts()
RETURNS trigger AS
$BODY$
BEGIN
NEW.ts_column := now();
RETURN NEW;
END
$BODY$ LANGUAGE plpgsql VOLATILE
Trigger:
CREATE TRIGGER log_up_ts
BEFORE UPDATE ON tbl
FOR EACH ROW EXECUTE PROCEDURE trg_up_ts();
Now, everything works automatically.
If that's not what you are after, #slivu's answer seems to cover the Ruby side just nicely.

I'm not familiar with PostgreSQL, but why are you assigning a Fixnum (time) to timestamp (which is a DateTime)? Your model must be failing to convert time to a proper DateTime value before generating the SQL.
Try foo.save!. I'm pretty sure you'll see an error, either reported from PostgreSQL, saying 1352633569151 is not a valid value for the table column, or your model will say it can't parse 1352633569151 to a valid DateTime.
foo.timestamp = Time.now or foo.timestamp = '2012-11-11 00:00:00' is something that'll work.

Related

Rails 5 and PostgreSQL 'Interval' data type

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?
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
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.
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
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 :/

'Time' being changed to a 'Datetime' after being added to a hash

My controller creates something called start_time. When I print start_time's value before it's added to a LittleClassSession hash, here's what I get:
22:45:00
Okay, it looks like a value with the type time. After it's added to the hash, I ask the controller what the :start_time value is.
#little_class_session = LittleClassSession.new({
...
:start_time => start_time
})
puts #little_class_session.start_time
Here's what it puts:
2000-01-01 22:45:00 UTC
It appears to be formatted like a datetime, but asking what the .class of the start_time attribute is returns:
Time
The LittleClassSession start_time column is a time in the table (I can verify this by checking the type in the Rails console) but was a datetime when the model was created.
What could be causing this?
While your database may support a "time" column (meaning just a time with no date information), Rails by default does not (largely because neither does Ruby's standard library -- even a Time contains date information). As such, when you assign it to your model, Rails is coercing it into the type it knows how to deal with, DateTime. So, you have a few options:
Ignore the date part of the time when you use it.
Use a gem like tod to deal with your time-only types, and follow the guidelines in the README for hooking it up to Rails.
Store start_time_hour and start_time_minutes in two separate columns, and work with them as needed (e.g, Date.current + start_time_hour.hours + start_time_minutes.minutes).
Hope that helps!

Find_or_initialize_by Issue

I have a DB Table for a Model entitled TradeDailyAverage. It has a date (DateTime) & averageprice (decimal) column
When I run this, I can't get the averageprice attribute to update:
newaverage = TradeDailyAverage.find_or_initialize_by_date(date)
newaverage.update_attributes :averageprice => dailyaverage
Further, when I run this, the date will show up, but the averageprice will not show up in rails console. It only shows up as blank:
newaverage = TradeDailyAverage.find_or_initialize_by_date(date)
puts newaverage.averageprice
puts newaverage.date
Is there anything special that I need to do to averageprice before I save it?
Here is all of the entire Rake Task for your reference:
averages = Trade.where('date >= ?', 7.days.ago).average(:price, :group => "DATE_TRUNC('day', date - INTERVAL '1 hour')")
# Loops through each daily average produced above
averages.each do |date, avg|
# Converts the BigDecimal to Floating Point(?)
averagefloat = avg.to_f
# Rounds the Daily Average to only two decimal points
dailyaverage = number_with_precision(averagefloat, :precision => 2)
newaverage = TradeDailyAverage.find_or_initialize_by_date(date)
newaverage.update_attributes :averageprice => dailyaverage
If you want to use find_or_initialize_by you need to think carefully about the implications. Lets take your first example:
newaverage = TradeDailyAverage.find_or_initialize_by_date(date)
newaverage.update_attributes :averageprice => dailyaverage
This should work, when the TradeDailyAverage for the given date is already in the database. It should not work however, when you get a new record back. The reason is simply because a new record is not persisted to the database. There is no way for update_attributes to update a non persisted record. You have two options here:
1.) Do not use update_attributes but assign the value and call save. This works for both, new and created records:
newaverage = TradeDailyAverage.find_or_initialize_by_date(date)
newaverage.averageprice = dailyaverage
newaverage.save
2.) Do not use find_or_initialize_by but find_or_create_by. This way if the record does not exist, a new one is directly written to the database. Now update_attributes should work because you always get persisted records back. Note that this approach has the drawback that you save records without an averageprice to the database. I would not recommend that.
The explanation above should also explain your second example:
newaverage = TradeDailyAverage.find_or_initialize_by_date(date)
puts newaverage.averageprice
puts newaverage.date
This should output the averageprice and the date for persisted records. If you get a newly initialized record back though, it will only display the date. Since you only initialized the record with a date object, there is no way that the averageprice is already set.
My issue was simply that upon saving to my database, PostgreSQL was changing the hourly time, possibly due to a timezone issue. Thus, all of my instances above were new, and I couldn't update attributes of an existing model. So, I converted my datetime data to dates, changed my date db column to date instead of datetime, and now everything is working smoothly. Yves gives some great info above though which helped me later on.

Best way to store ISO8601 in datetime column in Rails?

I'm passing my Rails controller a parameter that has a value of:
time = "2011-11-14T23:53:14.000Z"
For my app, I have a a database called "Reminders" with a remind_at column, which is a datetime column just like created_at and modified_at.
However I can't figure out how I should save this. I imagine it would be something like:
r = Reminder.create(:remind_at => time)
r.save
But this does not save it properly. Am I supposde to do some conversion of some sort first? Any thoughts?
Yes, you need to convert it into a Time object:
time = Time.parse("2011-11-14T23:53:14.000Z")

Inserting time into a time column with Ruby on Rails

I have run with a problem which i believe is Active Records fault. I am parsing an XML file which contains jobs. This xml file contains nodes which indicate walltime in the time format 00:00:00. I also have a model which will accept these jobs. However, when the time is larger than an actual 24H time, Active record inserts it as NULL. Examples below:
INSERT INTO `jobs` (`jobid`, `walltime`) VALUES('71413', 'NULL')
INSERT INTO `jobs` (`jobid`, `walltime`) VALUES('71413', '15:24:10')
Any ideas? Thank you!
The standard SQL time and datetime data types aren't intended to store a duration. Probably in agreement with those standards, ActiveRecord's time attribute assignment logic uses the time parsing rules of the native Ruby Time class to reject invalid time of day.
The way to store durations, as you intend, is either:
Store the duration as an integer (e.g. "number of seconds"), or
Store two (date)times, a start and an end, and use date arithmetic on them.
class Thing < ActiveRecord::Base
...
def duration
return start - end
end
def duration=(length)
start = Time.now
end = start + length
end
...
end

Resources