-
Notifications
You must be signed in to change notification settings - Fork 11.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[12.x] fix: update postgres grammar date format and time precision #51363
Conversation
72d3ec3
to
567bc3f
Compare
Can we make this change on a patch release? Seems breaking? |
Postgres only converts the datetime to UTC when the field contains a timezone (e.g.,
Thus, the conversion should only affect fields with timezones, which if people are using then they would already have to be manually converting the Carbon instances to UTC or their datetimes would be incorrect. Also, if the timestamp precision is not 6, then Postgres will round up the microseconds (per typical rounding rules) to the appropriate precision:
This should be fine in Production, however, this might cause some tests to fail by a second difference if they're, say, comparing a I suppose to be on the safe side it would be better to merge to 12.x. |
I also think the best is to target this at 12.x |
567bc3f
to
e5373fb
Compare
Updated target to 12.x |
Make sure to mark this PR as ready if you need another review. |
I encountered the issue before too but changing it by default resulted in bc breaks in edge cases. As microseconds and timezones are not used often, I am not sure whether we should complicate it. Especially as MySQL and PostgreSQL will then from now on (with default settings) behave differently. I solved this in my extended PostgreSQL driver by using traits which is an easy thing to implement. So anyone departing from the default can change the model. But I personally would like to not add more behavioural differences between using MySQL and PostgreSQL. |
I understand your concern, but I believe that the benefits far outweigh the cons, improve the developer experience, and reduce the potential for developer mistakes. Current ProblemAs it currently stands, passing a string to Eloquent / QueryBuilder works exactly as expected, but passing a Carbon instance truncates the microseconds and timezone information causing serious data integrity issues. The Carbon instance contains all the information needed to fully define that timestamp---I should not lose information or wind up with an incorrectly serialized string when I pass a Carbon instance to Eloquent / QueryBuilder. I also should not experience different behaviour between passing a Carbon instance and passing an equivalent timestamp string. Postgres Rounding Issue
You don't elaborate on what these edge cases are, but I suspect that you are referring to Postgres rounding the microseconds when using a non-default precision. I just updated our enterprise application to override the Postgres Grammar Automatic Timezone ConversionThe main driver behind this push was to ensure that all timestamps are consistently handled and converted to UTC by the framework/database (and not by the developer) before being persisted or queried. Keep in mind that the Postgres Before this, we had simply defaulted to Moving all timestamps to Additional Thoughts
That may be so, but they are used and it is worth the effort to ensure that they are handled correctly and consistently. I firmly believe that when using Postgres all timestamps should be
I took a look at your traits, and you only override the Model
Postgres and MySQL are different, and we should leverage these differences to their strengths rather than trying to find a poor middle ground. ConclusionBy unilaterally adopting
The proposed change:
Timestamp Conversion QueryFor anyone who is interested, here is the query I used to convert all timestamps across the board: Note that we only use the <?php
$columns = DB::select(<<<'SQL'
SELECT table_name, column_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND data_type IN ('timestamp without time zone', 'timestamp with time zone');
SQL);
collect($columns)
->groupBy('table_name')
->each(function ($columns, $table): void {
$alterStatement = collect($columns)
->map(fn ($column) => <<<SQL
ALTER COLUMN {$column->column_name} SET DATA TYPE timestamp(6) with time zone
SQL)
->join(', ');
DB::statement(<<<SQL
ALTER TABLE public.{$table}
{$alterStatement};
SQL);
}); |
Ah nice, we encountered this issue as well. We have to explicitly define casts for those fields, because of this missing. This is crucial for cases when your application works in UTC, but because of legacy reasons your database connection needs to be in a specific non UTC timezone. |
e5373fb
to
cbc56aa
Compare
Hello!
Current Problem
As it currently stands, passing a string to Eloquent / QueryBuilder works exactly as expected, but passing a Carbon instance truncates the microseconds and timezone information causing serious data integrity issues. The Carbon instance contains all the information needed to fully define that timestamp---I should not lose information or wind up with an incorrectly serialized string when I pass a Carbon instance to Eloquent / QueryBuilder. I also should not experience different behaviour between passing a Carbon instance and passing an equivalent timestamp string.
PostgreSQL supports
time[stamp] with time zone
fields with a resolution of 1 microsecond. However, the default grammar date format isY-m-d H:i:s
which truncatesDateTimeInterface
objects that are passed to queries.This results in invalid queries and records being saved to / returned from the database. For example:
This PR updates the PostgresGrammar date format to prevent the lose of information:
and it updates the default timestamp precision for Postgres to 6 (the Postgres default). This means that the following is equivalent in Postgres:
Postgres Rounding Issue
There is a rounding issue when the database has a precision that's less than the serialized datetime. This is not a bug in Postgres---it follows the general rounding rules---however, it can catch the developer off guard if they're not aware of this. For example, a serialized date of
2024-05-09 23:59:59.999999+00
(could be given by Carbon'snow()->endOfDay()
for instance) would actually be rounded up to2024-05-10 00:00:00
(when using a precision of 0) because it follows the prescribed rounding rules.To avoid this rounding confusion, I converted all timestamps in our database to
timestamp with timezone
using the default precision of 6 (microseconds). All Postgres timestamps are stored using 8 bytes regardless of the precision or if it's with/without timezone, as detailed in the Postgres docs. This means that there is no storage benefit to using a lower precision, and using the default precision (6) completely negates the rounding issue.Automatic Timezone Conversion
The main driver behind this push is to ensure that all timestamps are consistently handled and converted to UTC by the framework/database (and not by the developer) before being persisted or queried. Keep in mind that the Postgres
timestamp with timezone
does not actually store the timezone in the database, it just means that Postgres will automatically convert a timestamp to UTC before persisting / querying. When usingwithout timezone
the timestamp is used as-is without conversion, so if you pass a timestamp in a different timezone than your database (which should be in UTC), it will be stored as-is and not converted (which is very bad!).Before this, we had simply defaulted to
timestamp(0) without timezone
, however, this meant that the developer had to manually convert every Carbon instance to UTC before passing it to Eloquent / QueryBuilder. This was a great source of bugs which resulted in incorrect timestamps being persisted to the database and incorrect results being retrieved from the database if the developer forgot to manually convert the Carbon instance to UTC.Moving all timestamps to
timestamp with timezone
greatly improved the developer experience, cleaned up code, and eliminated an entire class of bugs because now timezone conversion is handled by the framework/database instead of the developer.Conclusion
I firmly believe that when using Postgres all timestamps should be
timestamp with timezone
with the default precision (6), I don't see any valid reason to deviate from this. This solves a lot of timezone conversion headaches and negates any rounding issues. I strongly recommend that the Laravel documentation should be updated to reflect this best practice when using Postgres.By unilaterally adopting
timestamptz
/timestamp with timezone
/timestamp(6) with timezone
(all the same) for all of our timestamps, we:The proposed change:
Timestamp Conversion Query
For anyone who is interested, here is the query I used to convert all timestamps across the board:
Note that we only use the
public
schema but this can easily be modified to work with multiple schemas.Thanks!