I’m trying to find a database agnostic way of comparing dates with active record queries. I’ve the following query:
UserRole.where("(effective_end_date - effective_start_date) > ?", 900.seconds)
This works fine on MySQL but produces an error on PG as the sql it generates doesn’t contain the ‘interval’ syntax. From the console:
←[1m←[36mUserRole Load (2.0ms)←[0m ←[1mSELECT "user_roles".* FROM "user_roles" WHERE "user_roles"."effective_end_date" IS NULL AND ((effective_end_d
ate - effective_start_date) > '--- 900
...
')←[0m
ActiveRecord::StatementInvalid: PG::Error: ERROR: invalid input syntax for type interval: "--- 900
When I run this with the to_sql I option I get:
irb(main):001:0> UserRole.where("effective_end_date - effective_start_date) > ?", 900.seconds).to_sql
=> "SELECT \"user_roles\".* FROM \"user_roles\" WHERE \"user_roles\".\"effective_end_date\" IS NULL AND (effective_end_date - effective_start_date) >
'--- 900\n...\n')"
All help appreciated.
If your
effective_end_dateandeffective_start_datecolumns really are dates then your query is pointless because dates have a minimum resolution of one day and 900s is quite a bit smaller than 86400s (AKA25*60*60or 1 day). So I’ll assume that your “date” columns are actually datetime (AKA timestamp) columns; if this is true then you might want to rename the columns to avoid confusion during maintenance,effectively_starts_atandeffectively_ends_atwould probably be good matches for the usual Rails conventions. If this assumption is invalid then you should change your column types or stop using 900s.Back to the real problem. ActiveRecord converts Ruby values to SQL values using the
ActiveRecord::ConnectionAdapters::Quoting#quotemethod:So if you try to use something as a value for a placeholder and there isn’t any specific handling built in for that type, then you get YAML (a bizarre choice of defaults IMO). Also,
900.secondsis anActiveSupport::Durationobject (despite what900.seconds.classsays) and thecase valuehas no branch forActiveSupport::Durationso900.secondswill get YAMLified.The PostgreSQL adapter provides its own
quoteinActiveRecord::ConnectionAdapters::PostgreSQLAdapter#quotebut that doesn’t know aboutActiveSupport::Durationeither. The MySQL adapter’squoteis also ignorant ofActiveSupport::Duration. You could monkey patch some sense into thesequotemethods. Something like this in an initializer:With that patch in place, you get intervals that PostgreSQL understands when you use an
ActiveSupport::Duration:If you add a similar patch to the MySQL adapter’s
quote(which is left as an exercise for the reader), then things like:will do The Right Thing in both PostgreSQL and MySQL and your code won’t have to worry about it.
That said, developing and deploying on different databases is a really bad idea that will make Santa Claus cry and go looking for some coal (possibly laced with arsenic, possibly radioactive) for your stocking. So don’t do that.
If on the other hand you’re trying to build database-agnostic software, then you’re in for some happy fun times! Database portability is largely a myth and database-agnostic software always means writing your own portability layer on top of the ORM and database interfaces that your platform provides. You will have to exhaustively test everything on each database you plan to support, everyone pays lip service to the SQL Standard but no one seems to fully support it and everyone has their own extensions and quirks to worry about. You will end up writing your own portability layer that will consist of a mixture of utility methods and monkey patches.