Skip to content
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

Add Jiff support #1164

Merged
merged 8 commits into from
Aug 18, 2024
Merged

Add Jiff support #1164

merged 8 commits into from
Aug 18, 2024

Conversation

allan2
Copy link
Contributor

@allan2 allan2 commented Jul 22, 2024

This PR adds support for version 0.1 of Jiff.

Jiff is inspired by Temporal, a TC39 proposal to improve datetime handling in JavaScript.

The implementation in jiff_01.rs is based on chrono_04.rs. When using FromSql and ToSql, Zoned sets the timezone to UTC.
Jiff does not support leap seconds, which is nice because Postgres doesn't either.

Closes #1163

@allan2 allan2 changed the title Add jiff support Add Jiff support Jul 22, 2024
fn from_sql(type_: &Type, raw: &[u8]) -> Result<JiffTimestamp, Box<dyn Error + Sync + Send>> {
Ok(DateTime::from_sql(type_, raw)?
.to_zoned(TimeZone::UTC)?
.timestamp())

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not intricately familiar with how PostgreSQL represents time, but it seems like you can avoid going through a DateTime here and just compute a JiffTimestamp directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in f00d208


impl<'a> FromSql<'a> for Zoned {
fn from_sql(type_: &Type, raw: &[u8]) -> Result<Zoned, Box<dyn Error + Sync + Send>> {
Ok(JiffTimestamp::from_sql(type_, raw)?.to_zoned(TimeZone::UTC))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it correct to assume UTC here? There is no time zone information in PostgreSQL?

I wonder if it makes sense to leave out the impl for Zoned and instead require that callers just use a jiff::Timestamp and do their own conversions to Zoned as needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UTC is correct. From the Postgres docs:

For timestamp with time zone, the internally stored value is always in UTC... An input value that has an explicit time zone specified is converted to UTC using the appropriate offset for that time zone. If no time zone is stated in the input string, then it is assumed to be in the time zone indicated by the system's TimeZone parameter, and is converted to UTC using the offset for the timezone zone.

Consumers will usually reach for Timestamp, but Zoned being ToSql and FromSql can occasionally be convenient. I am in favour of including them. It would also match chrono::DateTime<FixedOffset> and time::OffsetDateTime having them.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm skeptical that a Zoned implementation makes sense here personally. Think of a Zoned as "an assertion that a particular datetime is in a specific geographic region." Just because you can pick some kind of default here doesn't mean you should. And the civil datetime types are also somewhat questionable given that you're freely going to and from instants in time and back to inexact time. That also seems wrong.

I understand you're starting from a position of "this is how the Chrono integration works," but Chrono's DateTime<FixedOffset> and time's OffsetDateTime are much closer to Jiff's Timestamp than Jiff's Zoned. The closest analog Chrono has to a Jiff Zoned is DateTime<chrono_tz::Tz>.

The only trait implementation I feel 100% confident about here is for jiff::Timestamp.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have always felt weird about how Postgres handles time zones and how the timestamp with time zone type does not in fact contain the time zone information. Keeping the actual geographic region requires an additional column. What you wrote about Zoned resonates with me when it comes to correctness. I agree that jiff::Timestamp matches up with timestamptz and Zoned does not match with any current Postgres datetime type. I was concerned about conforming to Postgres and this crate's implementation details but I agree with you now on omitting impl for Zoned.

I am not sure I agree with your civil type concerns though. I feel like the Postgres types match up quite nicely for these inexact expressions of time

(Postgres also has a timetz. Can we express that in Jiff?)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RE Zoned: okay, great. Thank you.

I am not sure I agree with your civil type concerns though. I feel like the Postgres types match up quite nicely for these inexact expressions of time

I'm going based on the code in this PR. What I see is that a PostgreSQL timestamp (which is exact time) is being convert to and from a Jiff civil::DateTime, which is inexact time. Going from inexact time to exact time should require some assertion of time zone. Indeed, if you add a time zone to a civil::DateTime, then you get a Zoned. The same reason that a Zoned impl is inappropriate applies here.

AIUI, PostgreSQL does have Date and Time types, and those are inexact times, just like Jiff's civil::Date and civil::Time types.

Now that I look closer at this patch, I see that a DateTime is being constructed from a timestamp, while Date and Time are being constructed from PostgreSQL date and time types. The fact that these are different is 100% certainly wrong. I don't think PostgreSQL has a datetime type (something that combines date and time), so probably the jiff::civil::DateTime implementation should be removed.

Basically, any time you move between exact and inexact time, you should have some kind of time zone assertion involved. And usually, "just use UTC" is not the right thing to do.

(Postgres also has a timetz. Can we express that in Jiff?)

Noooooooooo. See: https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use_timetz

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timestamp in Postgres is inexact, being a combination of date and time without time zone. It can be used to record the general idea of the 2025 New Year's Countdown, taking place on Dec 31, 2024 at 11:59pm, without regard for the geographic location.

timestamptz is exact., asserting the time zone (albeit in UTC only).

Copy link

@BurntSushi BurntSushi Jul 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timestamp in Postgres is inexact, being a combination of date and time without time zone.

Whoa. OK. Let me backup and read their docs more carefully... Because if a timestamp is just inexact time and timestamptz is just inexact time with a time zone, then that does not make a timestamptz exact. It could be ambiguous. But according to this, yes, PostgreSQL is saying that it is exact. So maybe they are doing disambiguation on insert or something. And also, this seems to indeed agree that just a timestamp is actually inexact time:

timestamp (also known as timestamp without time zone) doesn't do any of that, it just stores a date and time you give it. You can think of it being a picture of a calendar and a clock rather than a point in time. Without additional information - the timezone - you don't know what time it records.

Wild.

I had assumed that timestamp was, well, just a Unix timestamp. Which is always in UTC. And timestamptz stored some extra bit of information like an offset. OK, so I think this is the mapping then:

  • date -> jiff::civil::Date
  • time -> jiff::civil::Time
  • timestamp -> jiff::civil::DateTime
  • timestamptz -> jiff::Timestamp

I think where I got confused is that in this patch, the timestamp -> jiff::civil::DateTime is implemented by using what appears to be a representation for exact time (number of seconds since an epoch). But I guess that's just a representational thing.

And yes, it looks like the above mapping lines up with what you have in this PR. OK, sorry for the noise!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had assumed that timestamp was, well, just a Unix timestamp. Which is always in UTC. And timestamptz stored some extra bit of information like an offset. OK, so I think this is the mapping then:

  • date -> jiff::civil::Date
  • time -> jiff::civil::Time
  • timestamp -> jiff::civil::DateTime
  • timestamptz -> jiff::Timestamp

I think where I got confused is that in this patch, the timestamp -> jiff::civil::DateTime is implemented by using what appears to be a representation for exact time (number of seconds since an epoch). But I guess that's just a representational thing.

It's certainly confusing.

timestamp and timestamptz both internally store microseconds since Y2K midnight UTC. The difference is that timestamptz converts the input to UTC before storing the number of microseconds. It bakes in the offset so we get the correct exact time, but the actual source time zone/offset is discarded.

For the record, DateTime and Timestamp are much better names than timestamp and timestamptz to describe what they really are.

And yes, it looks like the above mapping lines up with what you have in this PR. OK, sorry for the noise!

Comments were much appreciated :) At least we're only dealing with Terran time.

The impl now directly computes `Timestamp` rather than going through
`DateTime` and `Zoned`.
`Timestamp` already has impl and is semantically accurate for mapping to `timestamptz`, unlike `Zoned`.
End users can do their own conversions from `Timestamp` to `Zoned` if desired.
@sfackler
Copy link
Owner

Can you also add some tests? Here are chrono's for reference: https://github.com/sfackler/rust-postgres/blob/master/tokio-postgres/tests/test/types/chrono_04.rs

This sets the smallest unit to microseconds when calculating time deltas.

Previously, the number of microseconds was expressed improperly because
the rounding was not set.
This adds tests in the same fashion as the existing ones for `chrono`
and `time`.

Overflow is now handled using fallible operations.
For example, `Span:microseconds` is replaced with `Span::try_microseconds`.
Postgres infinity values are workiing as expected.

All tests are passing.
The implementation was previously removed as per jiff author comment.
@allan2
Copy link
Contributor Author

allan2 commented Aug 15, 2024

This PR is complete and ready for review. All tests are passing.

One minor note:

In the tests, I use jiff's FromStr implementations to parse strings into date types. [1]

let s = "'1970-01-01 00:00:00.010000000Z'";
let ts: Timestamp = s.trim_matches('\'').parse().unwrap();  // I did this
let ts: Timestamp = Timestamp::strptime("'%Y-%m-%d %H:%M:%S.%f %#z'", s); // I could do this if you prefer

Special thanks to @cmarkh who opened allan2/rust-postgres#1, which I unfortunately missed.

@BurntSushi
Copy link

The FromStr impl should be used if possible IMO. It's more flexible (it permits a T separator between the date and time) and probably 2x faster since it's a dedicated parser.

@sfackler sfackler merged commit 6fd0444 into sfackler:master Aug 18, 2024
4 checks passed
@sfackler
Copy link
Owner

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support for jiff datetime types
3 participants