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
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions postgres-types/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Added

* Added support for `jiff` 0.1 via the `with-jiff-01` feature.

## v0.2.7 - 2024-07-21

### Added
Expand Down
2 changes: 2 additions & 0 deletions postgres-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ with-eui48-0_4 = ["eui48-04"]
with-eui48-1 = ["eui48-1"]
with-geo-types-0_6 = ["geo-types-06"]
with-geo-types-0_7 = ["geo-types-0_7"]
with-jiff-0_1 = ["jiff-01"]
with-serde_json-1 = ["serde-1", "serde_json-1"]
with-smol_str-01 = ["smol_str-01"]
with-uuid-0_8 = ["uuid-08"]
Expand All @@ -46,6 +47,7 @@ eui48-04 = { version = "0.4", package = "eui48", optional = true }
eui48-1 = { version = "1.0", package = "eui48", optional = true, default-features = false }
geo-types-06 = { version = "0.6", package = "geo-types", optional = true }
geo-types-0_7 = { version = "0.7", package = "geo-types", optional = true }
jiff-01 = { version = "0.1", package = "jiff", optional = true }
serde-1 = { version = "1.0", package = "serde", optional = true }
serde_json-1 = { version = "1.0", package = "serde_json", optional = true }
uuid-08 = { version = "0.8", package = "uuid", optional = true }
Expand Down
118 changes: 118 additions & 0 deletions postgres-types/src/jiff_01.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use bytes::BytesMut;
use jiff_01::{
civil::{Date, DateTime, Time},
tz::TimeZone,
Span, Timestamp as JiffTimestamp, Zoned,
};
use postgres_protocol::types;
use std::error::Error;

use crate::{FromSql, IsNull, ToSql, Type};

const fn base() -> DateTime {
DateTime::constant(2000, 1, 1, 0, 0, 0, 0)
}

impl<'a> FromSql<'a> for DateTime {
fn from_sql(_: &Type, raw: &[u8]) -> Result<DateTime, Box<dyn Error + Sync + Send>> {
let t = types::timestamp_from_sql(raw)?;
Ok(base().checked_add(Span::new().microseconds(t))?)
}

accepts!(TIMESTAMP);
}

impl ToSql for DateTime {
fn to_sql(&self, _: &Type, w: &mut BytesMut) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
types::timestamp_to_sql(self.since(base())?.get_microseconds(), w);
Ok(IsNull::No)
}

accepts!(TIMESTAMP);
to_sql_checked!();
}

impl<'a> FromSql<'a> for JiffTimestamp {
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

}

accepts!(TIMESTAMPTZ);
}

impl ToSql for JiffTimestamp {
fn to_sql(&self, _: &Type, w: &mut BytesMut) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
types::timestamp_to_sql(
self.since(base().to_zoned(TimeZone::UTC)?)?
.get_microseconds(),
w,
);
Ok(IsNull::No)
}

accepts!(TIMESTAMPTZ);
to_sql_checked!();
}

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.

}

accepts!(TIMESTAMPTZ);
}

impl ToSql for Zoned {
fn to_sql(
&self,
type_: &Type,
w: &mut BytesMut,
) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
self.timestamp().to_sql(type_, w)
}

accepts!(TIMESTAMPTZ);
to_sql_checked!();
}

impl<'a> FromSql<'a> for Date {
fn from_sql(_: &Type, raw: &[u8]) -> Result<Date, Box<dyn Error + Sync + Send>> {
let jd = types::date_from_sql(raw)?;
Ok(base().date().checked_add(Span::new().days(jd))?)
}

accepts!(DATE);
}

impl ToSql for Date {
fn to_sql(&self, _: &Type, w: &mut BytesMut) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
let jd = self.since(base().date())?.get_days();
types::date_to_sql(jd, w);
Ok(IsNull::No)
}

accepts!(DATE);
to_sql_checked!();
}

impl<'a> FromSql<'a> for Time {
fn from_sql(_: &Type, raw: &[u8]) -> Result<Time, Box<dyn Error + Sync + Send>> {
let usec = types::time_from_sql(raw)?;
Ok(Time::midnight() + Span::new().microseconds(usec))
}

accepts!(TIME);
}

impl ToSql for Time {
fn to_sql(&self, _: &Type, w: &mut BytesMut) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
let delta = self.since(Time::midnight())?;
types::time_to_sql(delta.get_microseconds(), w);
Ok(IsNull::No)
}

accepts!(TIME);
to_sql_checked!();
}
7 changes: 7 additions & 0 deletions postgres-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ mod eui48_1;
mod geo_types_06;
#[cfg(feature = "with-geo-types-0_7")]
mod geo_types_07;
#[cfg(feature = "with-jiff-0_1")]
mod jiff_01;
#[cfg(feature = "with-serde_json-1")]
mod serde_json_1;
#[cfg(feature = "with-smol_str-01")]
Expand Down Expand Up @@ -491,6 +493,11 @@ impl WrongType {
/// | `time::OffsetDateTime` | TIMESTAMP WITH TIME ZONE |
/// | `time::Date` | DATE |
/// | `time::Time` | TIME |
/// | `jiff::civil::DateTime` | TIMESTAMP |
/// | `jiff::Timestamp` | TIMESTAMP WITH TIME ZONE |
/// | `jiff::Zoned` | TIMESTAMP WITH TIME ZONE |
/// | `jiff::civil::Date` | DATE |
/// | `jiff::civil::Time` | TIME |
/// | `eui48::MacAddress` | MACADDR |
/// | `geo_types::Point<f64>` | POINT |
/// | `geo_types::Rect<f64>` | BOX |
Expand Down
6 changes: 6 additions & 0 deletions postgres/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

## Unreleased

### Added

* Added support for `jiff` 0.1 via the `with-jiff-01` feature.

## v0.19.8 - 2024-07-21

### Added
Expand Down
1 change: 1 addition & 0 deletions postgres/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ with-eui48-0_4 = ["tokio-postgres/with-eui48-0_4"]
with-eui48-1 = ["tokio-postgres/with-eui48-1"]
with-geo-types-0_6 = ["tokio-postgres/with-geo-types-0_6"]
with-geo-types-0_7 = ["tokio-postgres/with-geo-types-0_7"]
with-jiff-0_1 = ["tokio-postgres/with-jiff-0_1"]
with-serde_json-1 = ["tokio-postgres/with-serde_json-1"]
with-smol_str-01 = ["tokio-postgres/with-smol_str-01"]
with-uuid-0_8 = ["tokio-postgres/with-uuid-0_8"]
Expand Down
4 changes: 4 additions & 0 deletions tokio-postgres/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Added

* Added support for `jiff` 0.1 via the `with-jiff-01` feature.

## v0.7.11 - 2024-07-21

### Fixed
Expand Down
2 changes: 2 additions & 0 deletions tokio-postgres/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ with-eui48-0_4 = ["postgres-types/with-eui48-0_4"]
with-eui48-1 = ["postgres-types/with-eui48-1"]
with-geo-types-0_6 = ["postgres-types/with-geo-types-0_6"]
with-geo-types-0_7 = ["postgres-types/with-geo-types-0_7"]
with-jiff-0_1 = ["postgres-types/with-jiff-0_1"]
with-serde_json-1 = ["postgres-types/with-serde_json-1"]
with-smol_str-01 = ["postgres-types/with-smol_str-01"]
with-uuid-0_8 = ["postgres-types/with-uuid-0_8"]
Expand Down Expand Up @@ -81,6 +82,7 @@ chrono-04 = { version = "0.4", package = "chrono", default-features = false }
eui48-1 = { version = "1.0", package = "eui48", default-features = false }
geo-types-06 = { version = "0.6", package = "geo-types" }
geo-types-07 = { version = "0.7", package = "geo-types" }
jiff-01 = { version = "0.1", package = "jiff" }
serde-1 = { version = "1.0", package = "serde" }
serde_json-1 = { version = "1.0", package = "serde_json" }
smol_str-01 = { version = "0.1", package = "smol_str" }
Expand Down
1 change: 1 addition & 0 deletions tokio-postgres/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
//! | `with-eui48-1` | Enable support for the 1.0 version of the `eui48` crate. | [eui48](https://crates.io/crates/eui48) 1.0 | no |
//! | `with-geo-types-0_6` | Enable support for the 0.6 version of the `geo-types` crate. | [geo-types](https://crates.io/crates/geo-types/0.6.0) 0.6 | no |
//! | `with-geo-types-0_7` | Enable support for the 0.7 version of the `geo-types` crate. | [geo-types](https://crates.io/crates/geo-types/0.7.0) 0.7 | no |
//! | `with-jiff-0_1` | Enable support for the 0.1 version of the `jiff` crate. | [jiff](https://crates.io/crates/jiff/0.1.0) 0.1 | no |
//! | `with-serde_json-1` | Enable support for the `serde_json` crate. | [serde_json](https://crates.io/crates/serde_json) 1.0 | no |
//! | `with-uuid-0_8` | Enable support for the `uuid` crate. | [uuid](https://crates.io/crates/uuid) 0.8 | no |
//! | `with-uuid-1` | Enable support for the `uuid` crate. | [uuid](https://crates.io/crates/uuid) 1.0 | no |
Expand Down