Repository: BurntSushi/jiff (language: rust)

Issue:
add ecosystem integration to jiff? (e.g., optional dependencies on `icu`, `diesel`, `sqlx`, etc)
In #4 and in other places, I've resisted adding "ecosystem integration" into Jiff. Not necessarily because I expected others to instead depend on Jiff necessarily right away, but that _eventually_, as folks started using Jiff (hopefully), it would make sense for them to add a Jiff option.

But I think I should probably flip this script. Getting Jiff adopted is going to be difficult, and I don't know if it has any broadly applicable "killer feature" that will convince folks to jump over from other datetime libraries. (I think Jiff's `Span` is really amazing, but I haven't figured out the best way of communicating that to a broad audience yet.) I think this means that the best path to Jiff adoption is to reduce friction as much as possible.

I get the sense that folks rely heavily on other crates providing integration points for their chosen datetime types of choice. In particular, it is often the case that converting from one type to another is actually quite tricky. Even tricky enough that [I myself get tripped up](https://github.com/sfackler/rust-postgres/pull/1164). So expecting users to just hand-roll these sorts of integrations is probably pretty lofty.

So... I think one way to reduce friction is for Jiff to provide these integrations. One of my initial reservations was introducing public dependencies on crates that are "less stable/mature." While Jiff is currently brand new and I expect to do some breaking change releases, I do hope that in about ~1 year's time, we'll have a Jiff 1.0 and that I'll commit to that for the foreseeable future with no breaking changes. However, I actually don't think this is a huge deal. We can just include the major/minor version of the crate in the feature name itself, [just like `rust-postgres` does](https://github.com/sfackler/rust-postgres/blob/d2634a46b5bc1de5176f0766626e7a9f6757f362/postgres-types/Cargo.toml#L53-L54).

I have two concerns with this approach. That is, if I start this way by introducing dependencies on crates like `diesel`, then:

1. Does this mean it will become very difficult, if not impossible, to coordinate a switch where instead of Jiff providing the integration points, the higher level libraries do it instead?
2. A datetime library ought to appear "low" in the dependency tree. It is intended to be a building block for other systems. So if Jiff starts depending on "bigger" systems like `diesel` (`diesel` is just an example, this applies to most crates I think), then what happens if those bigger systems actually need to depend on a datetime library? Recursive dependencies are a no-go.

Should I pursue this course? Should I limit it to Jiff 0.x versions and then, once 1.0 is out, insist that the integration points get flipped? I'm not sure. That will make 3 datetime choices for a lot of crates, and it's a tough sell.
add ecosystem integration to jiff? (e.g., optional dependencies on `icu`, `diesel`, `sqlx`, etc)
In #4 and in other places, I've resisted adding "ecosystem integration" into Jiff. Not necessarily because I expected others to instead depend on Jiff necessarily right away, but that _eventually_, as folks started using Jiff (hopefully), it would make sense for them to add a Jiff option.

But I think I should probably flip this script. Getting Jiff adopted is going to be difficult, and I don't know if it has any broadly applicable "killer feature" that will convince folks to jump over from other datetime libraries. (I think Jiff's `Span` is really amazing, but I haven't figured out the best way of communicating that to a broad audience yet.) I think this means that the best path to Jiff adoption is to reduce friction as much as possible.

I get the sense that folks rely heavily on other crates providing integration points for their chosen datetime types of choice. In particular, it is often the case that converting from one type to another is actually quite tricky. Even tricky enough that [I myself get tripped up](https://github.com/sfackler/rust-postgres/pull/1164). So expecting users to just hand-roll these sorts of integrations is probably pretty lofty.

So... I think one way to reduce friction is for Jiff to provide these integrations. One of my initial reservations was introducing public dependencies on crates that are "less stable/mature." While Jiff is currently brand new and I expect to do some breaking change releases, I do hope that in about ~1 year's time, we'll have a Jiff 1.0 and that I'll commit to that for the foreseeable future with no breaking changes. However, I actually don't think this is a huge deal. We can just include the major/minor version of the crate in the feature name itself, [just like `rust-postgres` does](https://github.com/sfackler/rust-postgres/blob/d2634a46b5bc1de5176f0766626e7a9f6757f362/postgres-types/Cargo.toml#L53-L54).

I have two concerns with this approach. That is, if I start this way by introducing dependencies on crates like `diesel`, then:

1. Does this mean it will become very difficult, if not impossible, to coordinate a switch where instead of Jiff providing the integration points, the higher level libraries do it instead?
2. A datetime library ought to appear "low" in the dependency tree. It is intended to be a building block for other systems. So if Jiff starts depending on "bigger" systems like `diesel` (`diesel` is just an example, this applies to most crates I think), then what happens if those bigger systems actually need to depend on a datetime library? Recursive dependencies are a no-go.

Should I pursue this course? Should I limit it to Jiff 0.x versions and then, once 1.0 is out, insist that the integration points get flipped? I'm not sure. That will make 3 datetime choices for a lot of crates, and it's a tough sell.


Discussion / hints:
Can you make those integrations a separate crate?
Not easily. Or at least, not without a degraded user experience. Because of the orphan rule. That is, I expect most "integrations" to be implementing traits for Jiff data types. That means the integration needs to be:

1. In the place where the trait is defined. (So, e.g., in diesel.)
2. In the place where the type is defined. That is, in `jiff`.
3. In some other crate with a newtype wrapper. Then anyone using this crate has to manage the newtype wrapper. This is "doable," but there's friction involved. It can be lessened using various tricks, but I don't think it can be zero friction.
for (3) maybe a nice macro could minimize the friction

this will make dependency-conscious ppl more likely to integrate jiff
I don't think `jiff` should have even *optional* dependencies on crates like `sqlx`; having a low-level data type even *optionally* depend on a database in order to implement the database's trait seems a little too absurd, as well as (as you noted) making it difficult for those libraries to (directly or indirectly) depend on `jiff`.

One of these days, I hope we figure out a way to relax the orphan rule enough that a `sqlx-jiff` crate can implement integration between `sqlx` traits and `jiff` types. But we don't have that yet, and still need a solution.

Ideally, some of the crates will be willing to accept PRs early on, in the hopes that they'll treat a new low-level data type crate favorably. If they don't want to integrate it because it's pre-1.0, you might be able to get confirmation that they'd be willing to integrate it once it's 1.0.

If that doesn't work, then for something like `sqlx`, for now, I think it'd be *entirely* reasonable to have a `sqlx-jiff` crate that provides 1) a newtype wrapper, 2) bidirectional `From` impls, 3) a `Deref` impl, 4) a `to_jiff()` method on the newtype, and 5) an extension trait with a `to_sqlx()` method on jiff types, to avoid having to turbofish the `From` impl for disambiguation (because `.into()` is ambiguous when dealing with an "accept anything implementing a trait" crate like `sqlx`). That's not zero-friction, but it's low-friction.
I like that plan @joshtriplett. I think that's probably best way to go. I think this will be my next focus, because I feel like I hear "jiff doesn't have as much ecosystem integration" as a big friction point. The newtype approach when an integration point downstream is perhaps not yet desirable does seem better than doing it directly from Jiff. Definitely not ideal, but I think there is enough "meat and potatoes" when converting between datetime types that it is actually worth doing.
Hi, 

If you finished the postgres support for jiff, can you please do the same for DuckDB?  It has exactly the same data types as postgres, so I hope it should be trivial.  The relevant crate is `duckdb-rs`.

Thank you very much!  I am growing my Rust usage and slowly shedding the chrono dependency.

Tony
 
I haven't done anything yet. I'll likely prioritize the crates that folks use the most first. But I'll keep `duckdb-rs` in mind!
> I think it'd be entirely reasonable to have a sqlx-jiff crate that provides

Here is a snippet I used in my private project that proves this way works - https://github.com/launchbadge/sqlx/issues/3487#issuecomment-2366739587 (wrap Timestamp to bridge from/to PG's TIMESTAMPTZ)

I'm not quite familiar with the wrapper way. If one can provide a code snippet of how we can implement "2) bidirectional From impls, 3) a Deref impl" in a decent way, I may be able to spend some time trying out a sqlx-jiff crate.

It will define feature flags like "postgres", "mysql" and "sqlite" for different backends. Note that some sqlx integration like `PgTimeTz` is defined at upstream and we cannot extend it with jiff support due to the orphan rule.
I think I would prefer having the integration crates live in this repository. Some of the integrations (like PostgreSQL, as you've discovered) are pretty subtle, and I think it's important to have them under the umbrella of either the Jiff project or the project that's being integrated with.

I don't have a ton of time unfortunately to do abstract mentoring. With that said, if you want to get started by submitting a PR to this repo with a new `jiff-sqlx` crate, I can probably do a review.
Could implementing `From` and `Into` the chrono types give a first simple solution to this problem? 

Obviously it would not allow the user to drop chrono as a dependency, but it could allow the user to work with jiff's API until they have to hand it off to a library that doesn't support jiff yet. The ecosystem already has support for chrono, so it could be a general solution until the ecosystem provides support for jiff directly?
I don't think it makes sense in really any circumstance to add a dependency on `chrono` from `jiff`. Even an optional one. I think the route to go there is something like a `jiff-chrono` (or `chrono-jiff`) crate that facilitates conversions to-and-from types between the crates.

It's also not especially straight-forward to just provide `From` impls. It works in some cases. For example, `chrono::DateTime<Utc>` and `Timestamp` make sense. But what about a `Zoned`? You might need `chrono-tz` for that. Honestly, it's all very thorny and I haven't thought through all the intricacies of conversions yet. And if you aren't careful, the conversion step could easily lose information along the way.

It's also not totally clear to me that `From` impls necessarily make anything automatic. I think you still need to insert some kind of explicit conversion code when working with crates that, say, only support Chrono and not Jiff. The `From` impls provide value by having an expert (probably me) implement them, but those can be in a `jiff-chrono` crate (or something).

Ecosystem integration is definitely on my list of things I want to do soon. I can definitely see that it is a blocker for some folks. Or at least, is causing folks to bounce off of Jiff. ("I tried using Jiff, but sqlx didn't support it and I didn't want to dig into it so I just dropped it for now." is something I heard recently.) I'm considering doing a `jiff 0.2` release ~soon, and then that might be a good time to publish some ecosystem integration crates to bootstrap things.

My hope is that once I get Jiff 1.0 out (I'm still planning on doing that in summer of this year, 2025), folks will be more willing to add integration points with it upstream. But we'll see.
> It's also not totally clear to me that From impls necessarily make anything automatic. I think you still need to insert some kind of explicit conversion code when working with crates that, say, only support Chrono and not Jiff.

Yes, the explicit conversion at the boundaries with ecosystem crates was how I imagined it working. To me at least it wouldn't sound too bad to require an explicit conversion for the time being whenever you convert to/from e.g. diesel models.
Ah okay, yeah, I think we're on the same page then. I think that falls into a `jiff-chrono` crate (and probably also `jiff-time`). I think those crates are good ideas and I do plan to make them.
> Does this mean it will become very difficult, if not impossible, to coordinate a switch where instead of Jiff providing the integration points, the higher level libraries do it instead?

I think that the example of `rkyv` suggests that if you offer integrations as features in the `jiff` crate itself, it might indeed be quite difficult to smoothly and gracefully stop offering them in the future.

https://github.com/rkyv/rkyv/blob/49262dc189d8d7236c6dfba0c1a19896c2a6e771/rkyv/Cargo.toml#L26-L45

They write,

> Support for various common crates. These are primarily to get users off the ground and build some momentum.
>
> These are NOT PLANNED to remain in rkyv for the 1.0 release. Much like serde, these implementations should be moved into their respective crates over time. Before adding support for another crate, please consider getting rkyv support in the crate instead.

Now, I haven’t been watching *that* closely, but I haven’t noticed any of the “ecosystem” crates actually adding support for `rkyv`. It’s not obvious that any progress will be made on migrating these integrations out of `rkyv` before a 1.0 release eventually comes out and drops them all.
Yeah I suspect you're right. `chrono` does notably have an `rkyv` integration, but I agree it is in general uncommon. `rkyv` is also tricky in general since, in the ideal case of an `Archive` impl, your internal representation becomes part of the public API.
Important up front: for better or worse, I'd start with `chrono` - large parts of the ecosystem adopted it as the default time crate. Once that's done, interested people could write PRs to other crates to add support for `jiff` without your input.

----

I'm very sympathetic to what you're trying to achieve, and would *love* to use `jiff`. But with no way to do conversion to and from anything that I could store in the database (Postgres in my case, using raw `tokio-postrgres`), it's just not viable to use.

----

Another point is fallibility: I know that it's not always possible to convert between two types even though both are supposed to represent the same thing. How would that be handled?

As an example: [Protobuf timestamps](https://docs.rs/prost-types/latest/prost_types/struct.Timestamp.html) are not always convertible. Heck, even converting `jiff::Timestamp` to `prost_types::Timestamp` would be fallible, since Protobuf constrains the dates to ones that can be represented as RFC 3339 strings (only allowing years in the range 0-9999 inclusive).

On that note: I could probably write the `prost_types` conversion myself, since the two timestamp types are so close, except Google doesn't use leap seconds (using smear seconds instead) and I don't have a clue how that plays into everything.

----

I would also not put too much stock in releasing version 1.0 being a big blocker for people. For better or worse, Cargo and the Rust ecosystem provide stronger guarantees than the semantic versioning spec, and there's a lot of quite big and popular crates that are still pre-1.0. `rand`, `num`, `sqlx`, `tracing`, to name a few.
@jaskij 

> Postgres in my case, using raw tokio-postrgres

FYI, tokio-postgres supports jiff types from 0.7.12: https://github.com/sfackler/rust-postgres/commit/6de0fceebe3c1800a5e812f35449e464fbc33f55
Doesn't `tokio-postgres` have Jiff support? https://github.com/sfackler/rust-postgres/blob/07b6878d5c72e20ff47be6b618cc2b88c43ff661/tokio-postgres/Cargo.toml#L37

> As an example: [Protobuf timestamps](https://docs.rs/prost-types/latest/prost_types/struct.Timestamp.html) are not always convertible. Heck, even converting jiff::Timestamp to prost_types::Timestamp would be fallible, since Protobuf constrains the dates to ones that can be represented as RFC 3339 strings (only allowing years in the range 0-9999 inclusive).

I think you just treat it as an error like any other right? You're going to have the same problem here with pretty much any other datetime library right?

> On that note: I could probably write the prost_types conversion myself, since the two timestamp types are so close, except Google doesn't use leap seconds (using smear seconds instead) and I don't have a clue how that plays into everything.

Neither does Jiff. So you're good.

> I would also not put too much stock in releasing version 1.0 being a big blocker for people. For better or worse, Cargo and the Rust ecosystem provide stronger guarantees than the semantic versioning spec, and there's a lot of quite big and popular crates that are still pre-1.0. rand, num, sqlx, tracing, to name a few.

It's not about 1.0 per se. It's about getting to a version and committing to it. I want to send a "this is a stable foundation to build upon" message.

We don't really need to debate the meaning of 1.0 here. I've done my fair share of 1.0 releases in my ten years building crates for Rust. (`regex`, `bstr`, `aho-corasick`, `csv`, `walkdir` and probably more that I'm forgetting.)
Thanks for the info about `jiff` support in `postgres-types`, that's a major blocker crossed out for me. Turns out their documentation got updated for `FromSql` but not `ToSql`, and I was looking at the latter.

----

When it comes to fallibility - I just wanted to point it out, since I'd be surprised if there wasn't some naive crate somewhere that treats this conversions as infallible. But as long as it's not widespread, that's not an issue for you.

----

Re: 1.0, again, just wanted to make a point, but you have it all well thought it and it's not my intention to force you to change your view.
Can you make those integrations a separate crate?
Not easily. Or at least, not without a degraded user experience. Because of the orphan rule. That is, I expect most "integrations" to be implementing traits for Jiff data types. That means the integration needs to be:

1. In the place where the trait is defined. (So, e.g., in diesel.)
2. In the place where the type is defined. That is, in `jiff`.
3. In some other crate with a newtype wrapper. Then anyone using this crate has to manage the newtype wrapper. This is "doable," but there's friction involved. It can be lessened using various tricks, but I don't think it can be zero friction.
for (3) maybe a nice macro could minimize the friction

this will make dependency-conscious ppl more likely to integrate jiff
I don't think `jiff` should have even *optional* dependencies on crates like `sqlx`; having a low-level data type even *optionally* depend on a database in order to implement the database's trait seems a little too absurd, as well as (as you noted) making it difficult for those libraries to (directly or indirectly) depend on `jiff`.

One of these days, I hope we figure out a way to relax the orphan rule enough that a `sqlx-jiff` crate can implement integration between `sqlx` traits and `jiff` types. But we don't have that yet, and still need a solution.

Ideally, some of the crates will be willing to accept PRs early on, in the hopes that they'll treat a new low-level data type crate favorably. If they don't want to integrate it because it's pre-1.0, you might be able to get confirmation that they'd be willing to integrate it once it's 1.0.

If that doesn't work, then for something like `sqlx`, for now, I think it'd be *entirely* reasonable to have a `sqlx-jiff` crate that provides 1) a newtype wrapper, 2) bidirectional `From` impls, 3) a `Deref` impl, 4) a `to_jiff()` method on the newtype, and 5) an extension trait with a `to_sqlx()` method on jiff types, to avoid having to turbofish the `From` impl for disambiguation (because `.into()` is ambiguous when dealing with an "accept anything implementing a trait" crate like `sqlx`). That's not zero-friction, but it's low-friction.
I like that plan @joshtriplett. I think that's probably best way to go. I think this will be my next focus, because I feel like I hear "jiff doesn't have as much ecosystem integration" as a big friction point. The newtype approach when an integration point downstream is perhaps not yet desirable does seem better than doing it directly from Jiff. Definitely not ideal, but I think there is enough "meat and potatoes" when converting between datetime types that it is actually worth doing.
Hi, 

If you finished the postgres support for jiff, can you please do the same for DuckDB?  It has exactly the same data types as postgres, so I hope it should be trivial.  The relevant crate is `duckdb-rs`.

Thank you very much!  I am growing my Rust usage and slowly shedding the chrono dependency.

Tony
 
I haven't done anything yet. I'll likely prioritize the crates that folks use the most first. But I'll keep `duckdb-rs` in mind!
> I think it'd be entirely reasonable to have a sqlx-jiff crate that provides

Here is a snippet I used in my private project that proves this way works - https://github.com/launchbadge/sqlx/issues/3487#issuecomment-2366739587 (wrap Timestamp to bridge from/to PG's TIMESTAMPTZ)

I'm not quite familiar with the wrapper way. If one can provide a code snippet of how we can implement "2) bidirectional From impls, 3) a Deref impl" in a decent way, I may be able to spend some time trying out a sqlx-jiff crate.

It will define feature flags like "postgres", "mysql" and "sqlite" for different backends. Note that some sqlx integration like `PgTimeTz` is defined at upstream and we cannot extend it with jiff support due to the orphan rule.
I think I would prefer having the integration crates live in this repository. Some of the integrations (like PostgreSQL, as you've discovered) are pretty subtle, and I think it's important to have them under the umbrella of either the Jiff project or the project that's being integrated with.

I don't have a ton of time unfortunately to do abstract mentoring. With that said, if you want to get started by submitting a PR to this repo with a new `jiff-sqlx` crate, I can probably do a review.
Could implementing `From` and `Into` the chrono types give a first simple solution to this problem? 

Obviously it would not allow the user to drop chrono as a dependency, but it could allow the user to work with jiff's API until they have to hand it off to a library that doesn't support jiff yet. The ecosystem already has support for chrono, so it could be a general solution until the ecosystem provides support for jiff directly?
I don't think it makes sense in really any circumstance to add a dependency on `chrono` from `jiff`. Even an optional one. I think the route to go there is something like a `jiff-chrono` (or `chrono-jiff`) crate that facilitates conversions to-and-from types between the crates.

It's also not especially straight-forward to just provide `From` impls. It works in some cases. For example, `chrono::DateTime<Utc>` and `Timestamp` make sense. But what about a `Zoned`? You might need `chrono-tz` for that. Honestly, it's all very thorny and I haven't thought through all the intricacies of conversions yet. And if you aren't careful, the conversion step could easily lose information along the way.

It's also not totally clear to me that `From` impls necessarily make anything automatic. I think you still need to insert some kind of explicit conversion code when working with crates that, say, only support Chrono and not Jiff. The `From` impls provide value by having an expert (probably me) implement them, but those can be in a `jiff-chrono` crate (or something).

Ecosystem integration is definitely on my list of things I want to do soon. I can definitely see that it is a blocker for some folks. Or at least, is causing folks to bounce off of Jiff. ("I tried using Jiff, but sqlx didn't support it and I didn't want to dig into it so I just dropped it for now." is something I heard recently.) I'm considering doing a `jiff 0.2` release ~soon, and then that might be a good time to publish some ecosystem integration crates to bootstrap things.

My hope is that once I get Jiff 1.0 out (I'm still planning on doing that in summer of this year, 2025), folks will be more willing to add integration points with it upstream. But we'll see.
> It's also not totally clear to me that From impls necessarily make anything automatic. I think you still need to insert some kind of explicit conversion code when working with crates that, say, only support Chrono and not Jiff.

Yes, the explicit conversion at the boundaries with ecosystem crates was how I imagined it working. To me at least it wouldn't sound too bad to require an explicit conversion for the time being whenever you convert to/from e.g. diesel models.
Ah okay, yeah, I think we're on the same page then. I think that falls into a `jiff-chrono` crate (and probably also `jiff-time`). I think those crates are good ideas and I do plan to make them.
> Does this mean it will become very difficult, if not impossible, to coordinate a switch where instead of Jiff providing the integration points, the higher level libraries do it instead?

I think that the example of `rkyv` suggests that if you offer integrations as features in the `jiff` crate itself, it might indeed be quite difficult to smoothly and gracefully stop offering them in the future.

https://github.com/rkyv/rkyv/blob/49262dc189d8d7236c6dfba0c1a19896c2a6e771/rkyv/Cargo.toml#L26-L45

They write,

> Support for various common crates. These are primarily to get users off the ground and build some momentum.
>
> These are NOT PLANNED to remain in rkyv for the 1.0 release. Much like serde, these implementations should be moved into their respective crates over time. Before adding support for another crate, please consider getting rkyv support in the crate instead.

Now, I haven’t been watching *that* closely, but I haven’t noticed any of the “ecosystem” crates actually adding support for `rkyv`. It’s not obvious that any progress will be made on migrating these integrations out of `rkyv` before a 1.0 release eventually comes out and drops them all.
Yeah I suspect you're right. `chrono` does notably have an `rkyv` integration, but I agree it is in general uncommon. `rkyv` is also tricky in general since, in the ideal case of an `Archive` impl, your internal representation becomes part of the public API.
Important up front: for better or worse, I'd start with `chrono` - large parts of the ecosystem adopted it as the default time crate. Once that's done, interested people could write PRs to other crates to add support for `jiff` without your input.

----

I'm very sympathetic to what you're trying to achieve, and would *love* to use `jiff`. But with no way to do conversion to and from anything that I could store in the database (Postgres in my case, using raw `tokio-postrgres`), it's just not viable to use.

----

Another point is fallibility: I know that it's not always possible to convert between two types even though both are supposed to represent the same thing. How would that be handled?

As an example: [Protobuf timestamps](https://docs.rs/prost-types/latest/prost_types/struct.Timestamp.html) are not always convertible. Heck, even converting `jiff::Timestamp` to `prost_types::Timestamp` would be fallible, since Protobuf constrains the dates to ones that can be represented as RFC 3339 strings (only allowing years in the range 0-9999 inclusive).

On that note: I could probably write the `prost_types` conversion myself, since the two timestamp types are so close, except Google doesn't use leap seconds (using smear seconds instead) and I don't have a clue how that plays into everything.

----

I would also not put too much stock in releasing version 1.0 being a big blocker for people. For better or worse, Cargo and the Rust ecosystem provide stronger guarantees than the semantic versioning spec, and there's a lot of quite big and popular crates that are still pre-1.0. `rand`, `num`, `sqlx`, `tracing`, to name a few.
@jaskij 

> Postgres in my case, using raw tokio-postrgres

FYI, tokio-postgres supports jiff types from 0.7.12: https://github.com/sfackler/rust-postgres/commit/6de0fceebe3c1800a5e812f35449e464fbc33f55
Doesn't `tokio-postgres` have Jiff support? https://github.com/sfackler/rust-postgres/blob/07b6878d5c72e20ff47be6b618cc2b88c43ff661/tokio-postgres/Cargo.toml#L37

> As an example: [Protobuf timestamps](https://docs.rs/prost-types/latest/prost_types/struct.Timestamp.html) are not always convertible. Heck, even converting jiff::Timestamp to prost_types::Timestamp would be fallible, since Protobuf constrains the dates to ones that can be represented as RFC 3339 strings (only allowing years in the range 0-9999 inclusive).

I think you just treat it as an error like any other right? You're going to have the same problem here with pretty much any other datetime library right?

> On that note: I could probably write the prost_types conversion myself, since the two timestamp types are so close, except Google doesn't use leap seconds (using smear seconds instead) and I don't have a clue how that plays into everything.

Neither does Jiff. So you're good.

> I would also not put too much stock in releasing version 1.0 being a big blocker for people. For better or worse, Cargo and the Rust ecosystem provide stronger guarantees than the semantic versioning spec, and there's a lot of quite big and popular crates that are still pre-1.0. rand, num, sqlx, tracing, to name a few.

It's not about 1.0 per se. It's about getting to a version and committing to it. I want to send a "this is a stable foundation to build upon" message.

We don't really need to debate the meaning of 1.0 here. I've done my fair share of 1.0 releases in my ten years building crates for Rust. (`regex`, `bstr`, `aho-corasick`, `csv`, `walkdir` and probably more that I'm forgetting.)
Thanks for the info about `jiff` support in `postgres-types`, that's a major blocker crossed out for me. Turns out their documentation got updated for `FromSql` but not `ToSql`, and I was looking at the latter.

----

When it comes to fallibility - I just wanted to point it out, since I'd be surprised if there wasn't some naive crate somewhere that treats this conversions as infallible. But as long as it's not widespread, that's not an issue for you.

----

Re: 1.0, again, just wanted to make a point, but you have it all well thought it and it's not my intention to force you to change your view.

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b2f513e5..56e6b2a3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -141,6 +141,12 @@ jobs:
       run: cargo build --manifest-path ./examples/sqlx-postgres/Cargo.toml
     - name: Run sqlx-sqlite example
       run: cargo run --manifest-path ./examples/sqlx-sqlite/Cargo.toml
+    - name: Build diesel-mysql example
+      run: cargo build --manifest-path ./examples/diesel-mysql/Cargo.toml
+    - name: Build diesel-postgres example
+      run: cargo build --manifest-path ./examples/diesel-postgres/Cargo.toml
+    - name: Run diesel-sqlite example
+      run: cargo build --manifest-path ./examples/diesel-sqlite/Cargo.toml
 
   # Generic testing for most cross targets. Some get special treatment in
   # other jobs.
diff --git a/.gitignore b/.gitignore
index b96d35ca..01b9451d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
 /Cargo.lock
+/examples/**/Cargo.lock
 /target
+/examples/**/target
 /tmp
 /ecma-test262
 /BREADCRUMBS
diff --git a/.vim/coc-settings.json b/.vim/coc-settings.json
index 21b7e80b..d8bbea5f 100644
--- a/.vim/coc-settings.json
+++ b/.vim/coc-settings.json
@@ -1,6 +1,9 @@
 {
   "rust-analyzer.linkedProjects": [
     "bench/Cargo.toml",
+    "examples/diesel-mysql/Cargo.toml",
+    "examples/diesel-postgres/Cargo.toml",
+    "examples/diesel-sqlite/Cargo.toml",
     "examples/sqlx-postgres/Cargo.toml",
     "examples/sqlx-sqlite/Cargo.toml",
     "Cargo.toml"
diff --git a/Cargo.toml b/Cargo.toml
index 894bea38..e345d537 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,13 +31,20 @@ include = [
 [workspace]
 members = [
   "crates/jiff-cli",
+  "crates/jiff-diesel",
   "crates/jiff-icu",
   "crates/jiff-sqlx",
   "crates/jiff-tzdb",
   "crates/jiff-tzdb-platform",
   "examples/*",
 ]
-exclude = ["examples/sqlx-postgres", "examples/sqlx-sqlite"]
+exclude = [
+  "examples/diesel-mysql",
+  "examples/diesel-postgres",
+  "examples/diesel-sqlite",
+  "examples/sqlx-postgres",
+  "examples/sqlx-sqlite",
+]
 
 # Features are documented in the "Crate features" section of the crate docs:
 # https://docs.rs/jiff/*/#crate-features
diff --git a/crates/jiff-diesel/COPYING b/crates/jiff-diesel/COPYING
new file mode 100644
index 00000000..bb9c20a0
--- /dev/null
+++ b/crates/jiff-diesel/COPYING
@@ -0,0 +1,3 @@
+This project is dual-licensed under the Unlicense and MIT licenses.
+
+You may use this code under the terms of either license.
diff --git a/crates/jiff-diesel/Cargo.toml b/crates/jiff-diesel/Cargo.toml
new file mode 100644
index 00000000..41eec6bb
--- /dev/null
+++ b/crates/jiff-diesel/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "jiff-diesel"
+version = "0.0.1"  #:version
+authors = ["Andrew Gallant <jamslam@gmail.com>"]
+license = "Unlicense OR MIT"
+homepage = "https://github.com/BurntSushi/jiff/tree/master/crates/jiff-diesel"
+repository = "https://github.com/BurntSushi/jiff"
+documentation = "https://docs.rs/jiff-diesel"
+description = "Integration for Jiff in Diesel."
+categories = ["date-and-time"]
+keywords = ["date", "time", "jiff", "diesel", "zone"]
+workspace = "../.."
+edition = "2021"
+rust-version = "1.70"
+include = ["/*.rs", "COPYING", "LICENSE-MIT", "UNLICENSE"]
+
+[lib]
+name = "jiff_diesel"
+bench = false
+path = "src/lib.rs"
+
+[features]
+default = []
+mysql = ["diesel/mysql"]
+postgres = ["diesel/postgres"]
+sqlite = ["diesel/sqlite"]
+
+[dependencies]
+jiff = { path = "../..", default-features = false }
+diesel = { version = "2.0.0", default-features = false }
+
+[dev-dependencies]
+jiff = { path = "../..", default-features = true }
diff --git a/crates/jiff-diesel/LICENSE-MIT b/crates/jiff-diesel/LICENSE-MIT
new file mode 100644
index 00000000..3b0a5dc0
--- /dev/null
+++ b/crates/jiff-diesel/LICENSE-MIT
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Andrew Gallant
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/crates/jiff-diesel/README.md b/crates/jiff-diesel/README.md
new file mode 100644
index 00000000..20dd0fad
--- /dev/null
+++ b/crates/jiff-diesel/README.md
@@ -0,0 +1,7 @@
+jiff-icu
+========
+WIP
+
+### Documentation
+
+https://docs.rs/jiff-icu
diff --git a/crates/jiff-diesel/UNLICENSE b/crates/jiff-diesel/UNLICENSE
new file mode 100644
index 00000000..68a49daa
--- /dev/null
+++ b/crates/jiff-diesel/UNLICENSE
@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <http://unlicense.org/>
diff --git a/crates/jiff-diesel/src/lib.rs b/crates/jiff-diesel/src/lib.rs
new file mode 100644
index 00000000..28699554
--- /dev/null
+++ b/crates/jiff-diesel/src/lib.rs
@@ -0,0 +1,70 @@
+/*!
+This crate provides integration points for [Jiff](jiff) and [Diesel](diesel).
+
+Examples can be found in the
+[examples directory of the Jiff repository][examples].
+
+# Organization
+
+This crates defines several types that wrap corresponding types in
+Jiff. Each wrapper type provides implementations of traits found in
+Diesel. For most types, these are the [`diesel::deserialize::Queryable`],
+[`diesel::deserialize::FromSql`] and [`diesel::serialize::ToSql`] traits.
+
+The intended workflow is to use these wrapper types within your wire types for
+encoding and decoding data from databases such as PostgreSQL. The wrapper types
+own the logic for encoding and decoding the data in database specific formats.
+
+In order to the minimize the annoyance of wrapper types, the following
+conveniences are afforded:
+
+* A [`ToDiesel`] trait is provided. Several Jiff types implement this trait.
+The trait provides easy conversion to the corresponding wrapper type in this
+crate.
+* A concrete `to_jiff` method is provided on each wrapper type. For example,
+[`Timestamp::to_jiff`]. This method is the reverse of `ToDiesel`. This converts
+from the wrapper type to the corresponding Jiff type.
+* There are `From` trait implementations from the wrapper type to the
+corresponding Jiff type, and vice versa.
+
+# Database support
+
+At present, MySQL, PostgreSQL and SQLite are supported.
+
+# Future
+
+This crate exists because there are generally only three ways to implement
+the necessary traits in Diesel:
+
+1. Make Jiff depend on Diesel, and implement the corresponding traits where
+Jiff's types are defined.
+2. Make Diesel depend on Jiff, and implement the corresponding traits where the
+traits are defined.
+3. Make a crate like this one with types that wrap Jiff's types, and implements
+the corresponding traits for the wrapper types.
+
+This was done because it seems inappropriate for a "lower level" crate like
+Jiff to depend on Diesel. And while it might be appropriate for Diesel to
+optionally depend on Jiff (like it does for [`chrono`] or [`time`]), at time of
+writing, Jiff is still early in its life. It's totally reasonable to wait for
+it to mature. Plus, the thought of three different datetime integrations is,
+admittedly, tough to stomach.
+
+In the future, it may be prudent for this crate to be upstreamed into Diesel
+itself.
+
+[examples]: https://github.com/BurntSushi/jiff/tree/master/examples
+[`chrono`]: https://docs.rs/chrono
+[`time`]: https://docs.rs/time
+*/
+#![deny(missing_docs)]
+
+pub use self::wrappers::{Date, DateTime, Span, Time, Timestamp, ToDiesel};
+
+#[cfg(feature = "mysql")]
+mod mysql;
+#[cfg(feature = "postgres")]
+mod postgres;
+#[cfg(feature = "sqlite")]
+mod sqlite;
+mod wrappers;
diff --git a/crates/jiff-diesel/src/mysql.rs b/crates/jiff-diesel/src/mysql.rs
new file mode 100644
index 00000000..401d9111
--- /dev/null
+++ b/crates/jiff-diesel/src/mysql.rs
@@ -0,0 +1,235 @@
+use diesel::{
+    deserialize::{self, FromSql, Queryable},
+    mysql::{
+        data_types::{MysqlTime, MysqlTimestampType},
+        Mysql, MysqlValue,
+    },
+    serialize::{self, Output, ToSql},
+    sql_types,
+};
+use jiff::{civil, tz};
+
+use crate::{Date, DateTime, Time, Timestamp, ToDiesel};
+
+static UTC: tz::TimeZone = tz::TimeZone::UTC;
+
+impl Queryable<sql_types::Datetime, Mysql> for Timestamp {
+    type Row = Timestamp;
+
+    fn build(row: Timestamp) -> deserialize::Result<Timestamp> {
+        Ok(row)
+    }
+}
+
+impl ToSql<sql_types::Datetime, Mysql> for Timestamp {
+    fn to_sql<'b>(
+        &'b self,
+        out: &mut Output<'b, '_, Mysql>,
+    ) -> serialize::Result {
+        let mut dt = UTC.to_datetime(self.to_jiff());
+        if dt.nanosecond() != 0 {
+            dt = dt.round(jiff::Unit::Microsecond)?;
+        }
+        let mysql_time = MysqlTime::new(
+            dt.year().try_into()?,
+            dt.month().unsigned_abs().into(),
+            dt.day().unsigned_abs().into(),
+            dt.hour().unsigned_abs().into(),
+            dt.minute().unsigned_abs().into(),
+            dt.second().unsigned_abs().into(),
+            (dt.subsec_nanosecond().unsigned_abs() / 1_000).into(),
+            false,
+            MysqlTimestampType::MYSQL_TIMESTAMP_DATETIME_TZ,
+            0,
+        );
+        <MysqlTime as ToSql<sql_types::Datetime, Mysql>>::to_sql(
+            &mysql_time,
+            &mut out.reborrow(),
+        )
+    }
+}
+
+impl FromSql<sql_types::Datetime, Mysql> for Timestamp {
+    fn from_sql(bytes: MysqlValue<'_>) -> deserialize::Result<Timestamp> {
+        let mysql_time =
+            <MysqlTime as FromSql<sql_types::Datetime, Mysql>>::from_sql(
+                bytes,
+            )?;
+        let nanos =
+            mysql_time.second_part.checked_mul(1_000).ok_or_else(|| {
+                format!("converting second part to nanoseconds overflowed")
+            })?;
+        let dt = civil::DateTime::new(
+            mysql_time.year.try_into()?,
+            mysql_time.month.try_into()?,
+            mysql_time.day.try_into()?,
+            mysql_time.hour.try_into()?,
+            mysql_time.minute.try_into()?,
+            mysql_time.second.try_into()?,
+            nanos.try_into()?,
+        )?;
+        let offset = tz::Offset::from_seconds(
+            mysql_time.time_zone_displacement.try_into()?,
+        )?;
+        Ok(offset.to_timestamp(dt)?.to_diesel())
+    }
+}
+
+impl Queryable<sql_types::Timestamp, Mysql> for DateTime {
+    type Row = DateTime;
+
+    fn build(row: DateTime) -> deserialize::Result<DateTime> {
+        Ok(row)
+    }
+}
+
+impl ToSql<sql_types::Timestamp, Mysql> for DateTime {
+    fn to_sql<'b>(
+        &'b self,
+        out: &mut Output<'b, '_, Mysql>,
+    ) -> serialize::Result {
+        let mut dt = self.to_jiff();
+        if dt.nanosecond() != 0 {
+            dt = dt.round(jiff::Unit::Microsecond)?;
+        }
+        let mysql_time = MysqlTime::new(
+            dt.year().try_into()?,
+            dt.month().unsigned_abs().into(),
+            dt.day().unsigned_abs().into(),
+            dt.hour().unsigned_abs().into(),
+            dt.minute().unsigned_abs().into(),
+            dt.second().unsigned_abs().into(),
+            (dt.subsec_nanosecond().unsigned_abs() / 1_000).into(),
+            false,
+            MysqlTimestampType::MYSQL_TIMESTAMP_DATETIME,
+            0,
+        );
+        <MysqlTime as ToSql<sql_types::Timestamp, Mysql>>::to_sql(
+            &mysql_time,
+            &mut out.reborrow(),
+        )
+    }
+}
+
+impl FromSql<sql_types::Timestamp, Mysql> for DateTime {
+    fn from_sql(bytes: MysqlValue<'_>) -> deserialize::Result<DateTime> {
+        let mysql_time = <MysqlTime as FromSql<
+            sql_types::Timestamp,
+            Mysql,
+        >>::from_sql(bytes)?;
+        let nanos =
+            mysql_time.second_part.checked_mul(1_000).ok_or_else(|| {
+                format!("converting second part to nanoseconds overflowed")
+            })?;
+        let dt = civil::DateTime::new(
+            mysql_time.year.try_into()?,
+            mysql_time.month.try_into()?,
+            mysql_time.day.try_into()?,
+            mysql_time.hour.try_into()?,
+            mysql_time.minute.try_into()?,
+            mysql_time.second.try_into()?,
+            nanos.try_into()?,
+        )?;
+        Ok(dt.to_diesel())
+    }
+}
+
+impl Queryable<sql_types::Date, Mysql> for Date {
+    type Row = Date;
+
+    fn build(row: Date) -> deserialize::Result<Date> {
+        Ok(row)
+    }
+}
+
+impl ToSql<sql_types::Date, Mysql> for Date {
+    fn to_sql<'b>(
+        &'b self,
+        out: &mut Output<'b, '_, Mysql>,
+    ) -> serialize::Result {
+        let date = self.to_jiff();
+        let mysql_time = MysqlTime::new(
+            date.year().try_into()?,
+            date.month().unsigned_abs().into(),
+            date.day().unsigned_abs().into(),
+            0,
+            0,
+            0,
+            0,
+            false,
+            MysqlTimestampType::MYSQL_TIMESTAMP_DATE,
+            0,
+        );
+        <MysqlTime as ToSql<sql_types::Date, Mysql>>::to_sql(
+            &mysql_time,
+            &mut out.reborrow(),
+        )
+    }
+}
+
+impl FromSql<sql_types::Date, Mysql> for Date {
+    fn from_sql(bytes: MysqlValue<'_>) -> deserialize::Result<Date> {
+        let mysql_time =
+            <MysqlTime as FromSql<sql_types::Date, Mysql>>::from_sql(bytes)?;
+        let date = civil::Date::new(
+            mysql_time.year.try_into()?,
+            mysql_time.month.try_into()?,
+            mysql_time.day.try_into()?,
+        )?;
+        Ok(date.to_diesel())
+    }
+}
+
+impl Queryable<sql_types::Time, Mysql> for Time {
+    type Row = Time;
+
+    fn build(row: Time) -> deserialize::Result<Time> {
+        Ok(row)
+    }
+}
+
+impl ToSql<sql_types::Time, Mysql> for Time {
+    fn to_sql<'b>(
+        &'b self,
+        out: &mut Output<'b, '_, Mysql>,
+    ) -> serialize::Result {
+        let mut time = self.to_jiff();
+        if time.nanosecond() != 0 {
+            time = time.round(jiff::Unit::Microsecond)?;
+        }
+        let mysql_time = MysqlTime::new(
+            0,
+            0,
+            0,
+            time.hour().unsigned_abs().into(),
+            time.minute().unsigned_abs().into(),
+            time.second().unsigned_abs().into(),
+            (time.subsec_nanosecond().unsigned_abs() / 1_000).into(),
+            false,
+            MysqlTimestampType::MYSQL_TIMESTAMP_TIME,
+            0,
+        );
+        <MysqlTime as ToSql<sql_types::Time, Mysql>>::to_sql(
+            &mysql_time,
+            &mut out.reborrow(),
+        )
+    }
+}
+
+impl FromSql<sql_types::Time, Mysql> for Time {
+    fn from_sql(bytes: MysqlValue<'_>) -> deserialize::Result<Time> {
+        let mysql_time =
+            <MysqlTime as FromSql<sql_types::Time, Mysql>>::from_sql(bytes)?;
+        let nanos =
+            mysql_time.second_part.checked_mul(1_000).ok_or_else(|| {
+                format!("converting second part to nanoseconds overflowed")
+            })?;
+        let time = civil::Time::new(
+            mysql_time.hour.try_into()?,
+            mysql_time.minute.try_into()?,
+            mysql_time.second.try_into()?,
+            nanos.try_into()?,
+        )?;
+        Ok(time.to_diesel())
+    }
+}
diff --git a/crates/jiff-diesel/src/postgres.rs b/crates/jiff-diesel/src/postgres.rs
new file mode 100644
index 00000000..0a854744
--- /dev/null
+++ b/crates/jiff-diesel/src/postgres.rs
@@ -0,0 +1,182 @@
+use diesel::{
+    deserialize::{self, FromSql, Queryable},
+    pg::{
+        data_types::{PgDate, PgInterval, PgTime, PgTimestamp},
+        Pg, PgValue,
+    },
+    serialize::{self, Output, ToSql},
+    sql_types,
+};
+use jiff::{civil, tz};
+
+use crate::{Date, DateTime, Span, Time, Timestamp, ToDiesel};
+
+/// Apprently the actual format of values on the wire is not
+/// a documented guarantee of PostgreSQL.[1] Instead, I just `sqlx`'s
+/// source code for `chrono` to figure out what the type of the source
+/// data is. (And this is fine for Diesel integration too.)
+///
+/// [1]: https://www.postgresql.org/docs/current/protocol-overview.html#PROTOCOL-FORMAT-CODES
+static POSTGRES_EPOCH_DATE: civil::Date = civil::date(2000, 1, 1);
+static POSTGRES_EPOCH_DATETIME: civil::DateTime =
+    civil::date(2000, 1, 1).at(0, 0, 0, 0);
+static POSTGRES_EPOCH_TIMESTAMP: i64 = 946684800;
+static MIDNIGHT: civil::Time = civil::Time::midnight();
+static UTC: tz::TimeZone = tz::TimeZone::UTC;
+
+impl Queryable<sql_types::Timestamptz, Pg> for Timestamp {
+    type Row = Timestamp;
+
+    fn build(row: Timestamp) -> deserialize::Result<Timestamp> {
+        Ok(row)
+    }
+}
+
+impl ToSql<sql_types::Timestamptz, Pg> for Timestamp {
+    fn to_sql<'b>(
+        &'b self,
+        out: &mut Output<'b, '_, Pg>,
+    ) -> serialize::Result {
+        // I guess the encoding here, based on Diesel, is the same as civil
+        // time. But the assumption is that the civil time is in UTC.
+        let dt = UTC.to_datetime(self.to_jiff()).to_diesel();
+        ToSql::<sql_types::Timestamp, Pg>::to_sql(&dt, &mut out.reborrow())
+    }
+}
+
+impl FromSql<sql_types::Timestamptz, Pg> for Timestamp {
+    fn from_sql(bytes: PgValue<'_>) -> deserialize::Result<Timestamp> {
+        // The encoding is the number of *microseconds* since
+        // POSTGRES_EPOCH_DATETIME.
+        let PgTimestamp(micros) =
+            FromSql::<sql_types::Timestamptz, Pg>::from_sql(bytes)?;
+        let micros = jiff::SignedDuration::from_micros(micros);
+        // OK because the timestamp is known to be valid and in range.
+        let epoch =
+            jiff::Timestamp::from_second(POSTGRES_EPOCH_TIMESTAMP).unwrap();
+        Ok(epoch.checked_add(micros)?.to_diesel())
+    }
+}
+
+impl Queryable<sql_types::Timestamp, Pg> for DateTime {
+    type Row = DateTime;
+
+    fn build(row: DateTime) -> deserialize::Result<DateTime> {
+        Ok(row)
+    }
+}
+
+impl ToSql<sql_types::Timestamp, Pg> for DateTime {
+    fn to_sql<'b>(
+        &'b self,
+        out: &mut Output<'b, '_, Pg>,
+    ) -> serialize::Result {
+        // The encoding is the number of *microseconds* since
+        // POSTGRES_EPOCH_DATETIME.
+        let micros =
+            self.to_jiff().duration_since(POSTGRES_EPOCH_DATETIME).as_micros();
+        // OK because the maximum duration between two Jiff civil datetimes
+        // is 631,107,417,599,999,999, which is less than i64::MAX.
+        let micros = i64::try_from(micros).unwrap();
+        ToSql::<sql_types::Timestamp, Pg>::to_sql(
+            &PgTimestamp(micros),
+            &mut out.reborrow(),
+        )
+    }
+}
+
+impl FromSql<sql_types::Timestamp, Pg> for DateTime {
+    fn from_sql(bytes: PgValue<'_>) -> deserialize::Result<DateTime> {
+        // The encoding is the number of *microseconds* since
+        // POSTGRES_EPOCH_DATETIME.
+        let PgTimestamp(micros) =
+            FromSql::<sql_types::Timestamp, Pg>::from_sql(bytes)?;
+        let micros = jiff::SignedDuration::from_micros(micros);
+        Ok(POSTGRES_EPOCH_DATETIME.checked_add(micros)?.to_diesel())
+    }
+}
+
+impl Queryable<sql_types::Date, Pg> for Date {
+    type Row = Date;
+
+    fn build(row: Date) -> deserialize::Result<Date> {
+        Ok(row)
+    }
+}
+
+impl ToSql<sql_types::Date, Pg> for Date {
+    fn to_sql<'b>(
+        &'b self,
+        out: &mut Output<'b, '_, Pg>,
+    ) -> serialize::Result {
+        // The encoding is the number of days since
+        // POSTGRES_EPOCH_DATE.
+        let days = (self.to_jiff() - POSTGRES_EPOCH_DATE).get_days();
+        ToSql::<sql_types::Date, Pg>::to_sql(
+            &PgDate(days),
+            &mut out.reborrow(),
+        )
+    }
+}
+
+impl FromSql<sql_types::Date, Pg> for Date {
+    fn from_sql(bytes: PgValue<'_>) -> deserialize::Result<Date> {
+        let PgDate(days) = FromSql::<sql_types::Date, Pg>::from_sql(bytes)?;
+        let span = jiff::Span::new().try_days(days)?;
+        Ok(POSTGRES_EPOCH_DATE.checked_add(span)?.to_diesel())
+    }
+}
+
+impl Queryable<sql_types::Time, Pg> for Time {
+    type Row = Time;
+
+    fn build(row: Time) -> deserialize::Result<Time> {
+        Ok(row)
+    }
+}
+
+impl ToSql<sql_types::Time, Pg> for Time {
+    fn to_sql<'b>(
+        &'b self,
+        out: &mut Output<'b, '_, Pg>,
+    ) -> serialize::Result {
+        // The encoding is the number of *microseconds* since midnight.
+        let micros = self.to_jiff().duration_since(MIDNIGHT).as_micros();
+        // OK since the max number of microseconds here is
+        // 86399999999, which always fits into an `i64`.
+        let micros = i64::try_from(micros).unwrap();
+        ToSql::<sql_types::Time, Pg>::to_sql(
+            &PgTime(micros),
+            &mut out.reborrow(),
+        )
+    }
+}
+
+impl FromSql<sql_types::Time, Pg> for Time {
+    fn from_sql(bytes: PgValue<'_>) -> deserialize::Result<Time> {
+        // The encoding is the number of *microseconds* since midnight.
+        let PgTime(micros) = FromSql::<sql_types::Time, Pg>::from_sql(bytes)?;
+        let micros = jiff::SignedDuration::from_micros(micros);
+        Ok(MIDNIGHT.checked_add(micros)?.to_diesel())
+    }
+}
+
+impl Queryable<sql_types::Interval, Pg> for Span {
+    type Row = Span;
+
+    fn build(row: Span) -> deserialize::Result<Span> {
+        Ok(row)
+    }
+}
+
+impl FromSql<sql_types::Interval, Pg> for Span {
+    fn from_sql(bytes: PgValue<'_>) -> deserialize::Result<Span> {
+        let interval: PgInterval =
+            FromSql::<sql_types::Interval, Pg>::from_sql(bytes)?;
+        let span = jiff::Span::new()
+            .try_months(interval.months)?
+            .try_days(interval.days)?
+            .try_microseconds(interval.microseconds)?;
+        Ok(span.to_diesel())
+    }
+}
diff --git a/crates/jiff-diesel/src/sqlite.rs b/crates/jiff-diesel/src/sqlite.rs
new file mode 100644
index 00000000..d12ee14d
--- /dev/null
+++ b/crates/jiff-diesel/src/sqlite.rs
@@ -0,0 +1,170 @@
+use diesel::{
+    deserialize::{self, FromSql, Queryable},
+    serialize::{self, IsNull, Output, ToSql},
+    sql_types,
+    sqlite::{Sqlite, SqliteValue},
+};
+use jiff::fmt::temporal::DateTimeParser;
+
+use crate::{Date, DateTime, Time, Timestamp, ToDiesel};
+
+static PARSER: DateTimeParser = DateTimeParser::new();
+
+impl Queryable<sql_types::TimestamptzSqlite, Sqlite> for Timestamp {
+    type Row = Timestamp;
+
+    fn build(row: Timestamp) -> deserialize::Result<Timestamp> {
+        Ok(row)
+    }
+}
+
+impl ToSql<sql_types::TimestamptzSqlite, Sqlite> for Timestamp {
+    fn to_sql<'b>(
+        &'b self,
+        out: &mut Output<'b, '_, Sqlite>,
+    ) -> serialize::Result {
+        out.set_value(self.to_jiff().to_string());
+        Ok(IsNull::No)
+    }
+}
+
+impl FromSql<sql_types::TimestamptzSqlite, Sqlite> for Timestamp {
+    fn from_sql(
+        value: SqliteValue<'_, '_, '_>,
+    ) -> deserialize::Result<Timestamp> {
+        // Apparently the Diesel internal implementations get to use
+        // `RawValue::parse_string`, which avoids creating a new alloc.
+        // But I guess we have to get a full alloc here? Pretty lame if
+        // you ask me. Ideally we could get a `&str` and parse straight
+        // from that. Jiff can parse from `&[u8]`, but we also use
+        // std's float parsing, which only works on `&str`.
+        let text: String =
+            FromSql::<sql_types::Timestamp, Sqlite>::from_sql(value)?;
+        // If there's a `:` somewhere, then it must be a textual timestamp.
+        // Moreover, a textual timestamp requires that a `:` be present
+        // somewhere for Jiff to parse it. (SQLite might not strictly require
+        // this though...)
+        if text.contains(':') {
+            let date = PARSER.parse_timestamp(text)?;
+            return Ok(date.to_diesel());
+        }
+        let julian_days = text.parse::<f64>()?;
+        julian_days_to_timestamp(julian_days).map(jiff::Timestamp::to_diesel)
+    }
+}
+
+impl Queryable<sql_types::Timestamp, Sqlite> for DateTime {
+    type Row = DateTime;
+
+    fn build(row: DateTime) -> deserialize::Result<DateTime> {
+        Ok(row)
+    }
+}
+
+impl ToSql<sql_types::Timestamp, Sqlite> for DateTime {
+    fn to_sql<'b>(
+        &'b self,
+        out: &mut Output<'b, '_, Sqlite>,
+    ) -> serialize::Result {
+        out.set_value(self.to_jiff().to_string());
+        Ok(IsNull::No)
+    }
+}
+
+impl FromSql<sql_types::Timestamp, Sqlite> for DateTime {
+    fn from_sql(
+        value: SqliteValue<'_, '_, '_>,
+    ) -> deserialize::Result<DateTime> {
+        // Apparently the Diesel internal implementations get to use
+        // `RawValue::parse_string`, which avoids creating a new alloc.
+        // But I guess we have to get a full alloc here? Pretty lame
+        // if you ask me. Ideally we could get a `&[u8]` and parse
+        // straight from that (Jiff doesn't need a `&str`).
+        let text: String =
+            FromSql::<sql_types::Timestamp, Sqlite>::from_sql(value)?;
+        let dt = PARSER.parse_datetime(text)?;
+        Ok(dt.to_diesel())
+    }
+}
+
+impl Queryable<sql_types::Date, Sqlite> for Date {
+    type Row = Date;
+
+    fn build(row: Date) -> deserialize::Result<Date> {
+        Ok(row)
+    }
+}
+
+impl ToSql<sql_types::Date, Sqlite> for Date {
+    fn to_sql<'b>(
+        &'b self,
+        out: &mut Output<'b, '_, Sqlite>,
+    ) -> serialize::Result {
+        out.set_value(self.to_jiff().to_string());
+        Ok(IsNull::No)
+    }
+}
+
+impl FromSql<sql_types::Date, Sqlite> for Date {
+    fn from_sql(value: SqliteValue<'_, '_, '_>) -> deserialize::Result<Date> {
+        // Apparently the Diesel internal implementations get to use
+        // `RawValue::parse_string`, which avoids creating a new alloc.
+        // But I guess we have to get a full alloc here? Pretty lame
+        // if you ask me. Ideally we could get a `&[u8]` and parse
+        // straight from that (Jiff doesn't need a `&str`).
+        let text: String =
+            FromSql::<sql_types::Date, Sqlite>::from_sql(value)?;
+        let date = PARSER.parse_date(text)?;
+        Ok(date.to_diesel())
+    }
+}
+
+impl Queryable<sql_types::Time, Sqlite> for Time {
+    type Row = Time;
+
+    fn build(row: Time) -> deserialize::Result<Time> {
+        Ok(row)
+    }
+}
+
+impl ToSql<sql_types::Time, Sqlite> for Time {
+    fn to_sql<'b>(
+        &'b self,
+        out: &mut Output<'b, '_, Sqlite>,
+    ) -> serialize::Result {
+        out.set_value(self.to_jiff().to_string());
+        Ok(IsNull::No)
+    }
+}
+
+impl FromSql<sql_types::Time, Sqlite> for Time {
+    fn from_sql(value: SqliteValue<'_, '_, '_>) -> deserialize::Result<Time> {
+        // Apparently the Diesel internal implementations get to use
+        // `RawValue::parse_string`, which avoids creating a new alloc.
+        // But I guess we have to get a full alloc here? Pretty lame
+        // if you ask me. Ideally we could get a `&[u8]` and parse
+        // straight from that (Jiff doesn't need a `&str`).
+        let text: String =
+            FromSql::<sql_types::Time, Sqlite>::from_sql(value)?;
+        let time = PARSER.parse_time(text)?;
+        Ok(time.to_diesel())
+    }
+}
+
+fn julian_days_to_timestamp(
+    days: f64,
+) -> deserialize::Result<jiff::Timestamp> {
+    // The Unix epoch in terms of SQLite julian days:
+    //
+    //     sqlite> select julianday('1970-01-01T00:00:00Z');
+    //     julianday('1970-01-01T00:00:00Z')
+    //     ---------------------------------
+    //     2440587.5
+    static UNIX_EPOCH_AS_JULIAN_DAYS: f64 = 2440587.5;
+    // SQLite assumes 24 hours in every day.
+    static SECONDS_PER_DAY: f64 = 86400.0;
+
+    let timestamp = (days - UNIX_EPOCH_AS_JULIAN_DAYS) * SECONDS_PER_DAY;
+    let sdur = jiff::SignedDuration::try_from_secs_f64(timestamp)?;
+    Ok(jiff::Timestamp::from_duration(sdur)?)
+}
diff --git a/crates/jiff-diesel/src/wrappers.rs b/crates/jiff-diesel/src/wrappers.rs
new file mode 100644
index 00000000..f89648f1
--- /dev/null
+++ b/crates/jiff-diesel/src/wrappers.rs
@@ -0,0 +1,225 @@
+/// A trait for convenient conversions from Jiff types to Diesel types.
+///
+/// # Example
+///
+/// This shows how to convert a [`jiff::Timestamp`] to a [`Timestamp`]:
+///
+/// ```
+/// use jiff_diesel::ToDiesel;
+///
+/// let ts: jiff::Timestamp = "2025-02-20T17:00-05".parse()?;
+/// let wrapper = ts.to_diesel();
+/// assert_eq!(format!("{wrapper:?}"), "Timestamp(2025-02-20T22:00:00Z)");
+///
+/// # Ok::<(), Box<dyn std::error::Error>>(())
+/// ```
+pub trait ToDiesel {
+    /// The wrapper type to convert to.
+    type Target;
+
+    /// A conversion method that converts a Jiff type to a Diesel wrapper type.
+    fn to_diesel(self) -> Self::Target;
+}
+
+/// A wrapper type for [`jiff::Timestamp`].
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
+#[cfg_attr(
+    any(feature = "mysql", feature = "postgres", feature = "sqlite"),
+    derive(diesel::expression::AsExpression)
+)]
+#[cfg_attr(feature = "mysql", diesel(sql_type = diesel::sql_types::Datetime))]
+#[cfg_attr(feature = "postgres", diesel(sql_type = diesel::sql_types::Timestamptz))]
+#[cfg_attr(feature = "sqlite", diesel(sql_type = diesel::sql_types::TimestamptzSqlite))]
+pub struct Timestamp(jiff::Timestamp);
+
+impl Timestamp {
+    /// Converts this wrapper to a [`jiff::Timestamp`].
+    pub fn to_jiff(self) -> jiff::Timestamp {
+        self.0
+    }
+}
+
+impl ToDiesel for jiff::Timestamp {
+    type Target = Timestamp;
+
+    fn to_diesel(self) -> Timestamp {
+        Timestamp(self)
+    }
+}
+
+impl From<jiff::Timestamp> for Timestamp {
+    fn from(x: jiff::Timestamp) -> Timestamp {
+        Timestamp(x)
+    }
+}
+
+impl From<Timestamp> for jiff::Timestamp {
+    fn from(x: Timestamp) -> jiff::Timestamp {
+        x.0
+    }
+}
+
+/// A wrapper type for [`jiff::civil::DateTime`].
+#[derive(
+    Clone,
+    Copy,
+    Debug,
+    Eq,
+    Hash,
+    PartialEq,
+    PartialOrd,
+    Ord,
+    diesel::expression::AsExpression,
+)]
+#[diesel(sql_type = diesel::sql_types::Timestamp)]
+pub struct DateTime(jiff::civil::DateTime);
+
+impl DateTime {
+    /// Converts this wrapper to a [`jiff::civil::DateTime`].
+    pub fn to_jiff(self) -> jiff::civil::DateTime {
+        self.0
+    }
+}
+
+impl ToDiesel for jiff::civil::DateTime {
+    type Target = DateTime;
+
+    fn to_diesel(self) -> DateTime {
+        DateTime(self)
+    }
+}
+
+impl From<jiff::civil::DateTime> for DateTime {
+    fn from(x: jiff::civil::DateTime) -> DateTime {
+        DateTime(x)
+    }
+}
+
+impl From<DateTime> for jiff::civil::DateTime {
+    fn from(x: DateTime) -> jiff::civil::DateTime {
+        x.0
+    }
+}
+
+/// A wrapper type for [`jiff::civil::Date`].
+#[derive(
+    Clone,
+    Copy,
+    Debug,
+    Eq,
+    Hash,
+    PartialEq,
+    PartialOrd,
+    Ord,
+    diesel::expression::AsExpression,
+)]
+#[diesel(sql_type = diesel::sql_types::Date)]
+pub struct Date(jiff::civil::Date);
+
+impl Date {
+    /// Converts this wrapper to a [`jiff::civil::Date`].
+    pub fn to_jiff(self) -> jiff::civil::Date {
+        self.0
+    }
+}
+
+impl ToDiesel for jiff::civil::Date {
+    type Target = Date;
+
+    fn to_diesel(self) -> Date {
+        Date(self)
+    }
+}
+
+impl From<jiff::civil::Date> for Date {
+    fn from(x: jiff::civil::Date) -> Date {
+        Date(x)
+    }
+}
+
+impl From<Date> for jiff::civil::Date {
+    fn from(x: Date) -> jiff::civil::Date {
+        x.0
+    }
+}
+
+/// A wrapper type for [`jiff::civil::Time`].
+#[derive(
+    Clone,
+    Copy,
+    Debug,
+    Eq,
+    Hash,
+    PartialEq,
+    PartialOrd,
+    Ord,
+    diesel::expression::AsExpression,
+)]
+#[diesel(sql_type = diesel::sql_types::Time)]
+pub struct Time(jiff::civil::Time);
+
+impl Time {
+    /// Converts this wrapper to a [`jiff::civil::Time`].
+    pub fn to_jiff(self) -> jiff::civil::Time {
+        self.0
+    }
+}
+
+impl ToDiesel for jiff::civil::Time {
+    type Target = Time;
+
+    fn to_diesel(self) -> Time {
+        Time(self)
+    }
+}
+
+impl From<jiff::civil::Time> for Time {
+    fn from(x: jiff::civil::Time) -> Time {
+        Time(x)
+    }
+}
+
+impl From<Time> for jiff::civil::Time {
+    fn from(x: Time) -> jiff::civil::Time {
+        x.0
+    }
+}
+
+/// A wrapper type for [`jiff::Span`].
+///
+/// # PostgreSQL: Limited support
+///
+/// This type _only_ has a [`diesel::deserialize::FromSql`] trait implementation
+/// for PostgreSQL. The reason for this is that encoding an arbitrary
+/// `Span` into a PostgreSQL interval requires a relative datetime.
+/// Therefore, users wanting to store a `Span` will need to explicitly use a
+/// [`diesel::pg::data_types::PgInterval`] at least at encoding time.
+#[derive(Clone, Copy, Debug)]
+pub struct Span(jiff::Span);
+
+impl Span {
+    /// Converts this wrapper to a [`jiff::Span`].
+    pub fn to_jiff(self) -> jiff::Span {
+        self.0
+    }
+}
+
+impl ToDiesel for jiff::Span {
+    type Target = Span;
+
+    fn to_diesel(self) -> Span {
+        Span(self)
+    }
+}
+
+impl From<jiff::Span> for Span {
+    fn from(x: jiff::Span) -> Span {
+        Span(x)
+    }
+}
+
+impl From<Span> for jiff::Span {
+    fn from(x: Span) -> jiff::Span {
+        x.0
+    }
+}
diff --git a/crates/jiff-sqlx/Cargo.toml b/crates/jiff-sqlx/Cargo.toml
index 23b995cd..2fd13895 100644
--- a/crates/jiff-sqlx/Cargo.toml
+++ b/crates/jiff-sqlx/Cargo.toml
@@ -6,7 +6,7 @@ license = "Unlicense OR MIT"
 homepage = "https://github.com/BurntSushi/jiff/tree/master/crates/jiff-sqlx"
 repository = "https://github.com/BurntSushi/jiff"
 documentation = "https://docs.rs/jiff-sqlx"
-description = "Integration for Jiff in sqlx."
+description = "Integration for Jiff in SQLx."
 categories = ["date-and-time"]
 keywords = ["date", "time", "jiff", "sqlx", "zone"]
 workspace = "../.."
diff --git a/crates/jiff-sqlx/src/sqlite.rs b/crates/jiff-sqlx/src/sqlite.rs
index dc498989..c34e3b47 100644
--- a/crates/jiff-sqlx/src/sqlite.rs
+++ b/crates/jiff-sqlx/src/sqlite.rs
@@ -37,7 +37,7 @@ impl<'r> Decode<'r, Sqlite> for Timestamp {
     fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
         // We use a `&str` here because we might need to parse an `f64` from
         // it. std doesn't support parsing `f64` from `&[u8]` AND it seems like
-        // we can pass `value` by reference or clone it, so we are limited
+        // we can't pass `value` by reference or clone it, so we are limited
         // to exactly one decode. WTF.
         let text = <&str as Decode<Sqlite>>::decode(value)?;
         // If there's a `:` somewhere, then it must be a textual timestamp.
@@ -116,8 +116,6 @@ impl Encode<'_, Sqlite> for Time {
 
 impl<'r> Decode<'r, Sqlite> for Time {
     fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
-        static PARSER: DateTimeParser = DateTimeParser::new();
-
         let text = <&[u8] as Decode<Sqlite>>::decode(value)?;
         let date = PARSER.parse_time(text)?;
         Ok(date.to_sqlx())
diff --git a/examples/diesel-mysql/Cargo.toml b/examples/diesel-mysql/Cargo.toml
new file mode 100644
index 00000000..17e957e0
--- /dev/null
+++ b/examples/diesel-mysql/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+publish = false
+name = "diesel-mysql-test"
+version = "0.0.1"
+edition = "2021"
+
+# This example has very fat dependencies. And running it requires a PostgreSQL
+# dataabase connection. This makes it so this example won't get compiled or run
+# with `cargo test --all`.
+[workspace]
+
+[dependencies]
+anyhow = "1.0.95"
+diesel = { version = "2.2.0", features = ["mysql"] }
+jiff = { path = "../.." }
+jiff-diesel = { path = "../../crates/jiff-diesel", features = ["mysql"] }
+
+[[bin]]
+name = "diesel-mysql-test"
+path = "main.rs"
diff --git a/examples/diesel-mysql/main.rs b/examples/diesel-mysql/main.rs
new file mode 100644
index 00000000..d9d32f97
--- /dev/null
+++ b/examples/diesel-mysql/main.rs
@@ -0,0 +1,89 @@
+use diesel::{
+    connection::Connection, dsl::sql, mysql::MysqlConnection,
+    query_dsl::RunQueryDsl, select, sql_types, ExpressionMethods,
+};
+use jiff::civil;
+use jiff_diesel::ToDiesel;
+
+fn main() -> anyhow::Result<()> {
+    // To set this up, I ran
+    //
+    //     $ sudo mariadb
+    //     MariaDB [(none)]> CREATE USER 'demo'@'localhost' IDENTIFIED BY 'password';
+    //     MariaDB [(none)]> CREATE DATABASE jiff_diesel_example;
+    //     MariaDB [(none)]> GRANT ALL PRIVILEGES ON jiff_diesel_example.* TO 'demo'@'localhost';
+    //
+    // And then I was able to connect using the code below.
+    let mut conn = MysqlConnection::establish(
+        "mysql://demo:password@localhost/jiff_diesel_example",
+    )?;
+
+    example_datetime_decode(&mut conn)?;
+    example_datetime_encode(&mut conn)?;
+
+    Ok(())
+}
+
+/// Performs a decoding with all of Jiff's datetime types.
+fn example_datetime_decode(conn: &mut MysqlConnection) -> anyhow::Result<()> {
+    // Decoding jiff::Timestamp
+    let query = select(sql::<sql_types::Datetime>("'1970-01-01 00:00:00Z'"));
+    let got: jiff_diesel::Timestamp = query.get_result(conn)?;
+    let expected =
+        "1970-01-01T00:00:00Z".parse::<jiff::Timestamp>()?.to_diesel();
+    assert_eq!(expected, got);
+
+    // Decoding jiff::civil::DateTime
+    let query = select(sql::<sql_types::Timestamp>("'2025-07-20 00:00:00'"));
+    let got: jiff_diesel::DateTime = query.get_result(conn)?;
+    let expected = civil::date(2025, 7, 20).at(0, 0, 0, 0).to_diesel();
+    assert_eq!(expected, got);
+
+    // Decoding jiff::civil::Date
+    let query = select(sql::<sql_types::Date>("'1999-01-08'"));
+    let got: jiff_diesel::Date = query.get_result(conn)?;
+    let expected = civil::date(1999, 1, 8).to_diesel();
+    assert_eq!(expected, got);
+
+    // Decoding with jiff::civil::Time
+    let query = select(sql::<sql_types::Time>("'23:59:59.999999'"));
+    let got: jiff_diesel::Time = query.get_result(conn)?;
+    let expected = civil::time(23, 59, 59, 999_999_000).to_diesel();
+    assert_eq!(expected, got);
+
+    Ok(())
+}
+
+/// Performs a encoding with all of Jiff's datetime types.
+fn example_datetime_encode(conn: &mut MysqlConnection) -> anyhow::Result<()> {
+    // Encoding jiff::Timestamp
+    let ts = "1970-01-01T00:00:01Z".parse::<jiff::Timestamp>()?.to_diesel();
+    let query = select(
+        sql::<sql_types::Datetime>("CAST('1970-01-01 00:00:01' AS DATETIME)")
+            .eq(ts),
+    );
+    assert!(query.get_result::<bool>(conn)?);
+
+    // Encoding jiff::civil::DateTime
+    let dt = civil::date(2025, 7, 20).at(0, 0, 0, 0).to_diesel();
+    let query = select(
+        sql::<sql_types::Timestamp>("CAST('2025-07-20 00:00:00' AS DATETIME)")
+            .eq(dt),
+    );
+    assert!(query.get_result::<bool>(conn)?);
+
+    // Encoding jiff::civil::Date
+    let date = civil::date(1999, 1, 8).to_diesel();
+    let query =
+        select(sql::<sql_types::Date>("CAST('1999-01-08' AS DATE)").eq(date));
+    assert!(query.get_result::<bool>(conn)?);
+
+    // Encoding jiff::civil::Time
+    let time = civil::time(23, 59, 59, 999_999_000).to_diesel();
+    let query = select(
+        sql::<sql_types::Time>("CAST('23:59:59.999999' AS TIME(6))").eq(time),
+    );
+    assert!(query.get_result::<bool>(conn)?);
+
+    Ok(())
+}
diff --git a/examples/diesel-postgres/Cargo.toml b/examples/diesel-postgres/Cargo.toml
new file mode 100644
index 00000000..1ce90b4e
--- /dev/null
+++ b/examples/diesel-postgres/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+publish = false
+name = "diesel-postgres-test"
+version = "0.0.1"
+edition = "2021"
+
+# This example has very fat dependencies. And running it requires a PostgreSQL
+# dataabase connection. This makes it so this example won't get compiled or run
+# with `cargo test --all`.
+[workspace]
+
+[dependencies]
+anyhow = "1.0.95"
+diesel = { version = "2.2.0", features = ["postgres"] }
+jiff = { path = "../.." }
+jiff-diesel = { path = "../../crates/jiff-diesel", features = ["postgres"] }
+
+[[bin]]
+name = "diesel-postgres-test"
+path = "main.rs"
diff --git a/examples/diesel-postgres/main.rs b/examples/diesel-postgres/main.rs
new file mode 100644
index 00000000..8498f501
--- /dev/null
+++ b/examples/diesel-postgres/main.rs
@@ -0,0 +1,108 @@
+use diesel::{
+    connection::Connection, dsl::sql, pg::PgConnection,
+    query_dsl::RunQueryDsl, select, sql_query, sql_types,
+};
+use jiff::civil;
+use jiff_diesel::ToDiesel;
+
+fn main() -> anyhow::Result<()> {
+    let mut conn = PgConnection::establish(
+        "postgres://postgres:password@localhost/test",
+    )?;
+
+    example_datetime_roundtrip(&mut conn)?;
+    example_span_decode(&mut conn)?;
+    example_time_zone_setting(&mut conn)?;
+
+    Ok(())
+}
+
+/// Performs a round-trip with all of Jiff's datetime types.
+fn example_datetime_roundtrip(conn: &mut PgConnection) -> anyhow::Result<()> {
+    // Integration with jiff::Timestamp
+    let ts = "1970-01-01T00:00:00Z".parse::<jiff::Timestamp>()?.to_diesel();
+    let query = select(sql::<sql_types::Timestamptz>(
+        "'1970-01-01 00:00:00Z'::timestamp with time zone",
+    ));
+    let got: jiff_diesel::Timestamp = query.get_result(conn)?;
+    assert_eq!(ts, got);
+
+    // Integration with jiff::civil::DateTime
+    let dt = civil::date(2025, 7, 20).at(0, 0, 0, 0).to_diesel();
+    let query = select(sql::<sql_types::Timestamp>(
+        "'2025-07-20 00:00:00'::timestamp without time zone",
+    ));
+    let got: jiff_diesel::DateTime = query.get_result(conn)?;
+    assert_eq!(dt, got);
+
+    // Integration with jiff::civil::Date
+    let date = civil::date(1999, 1, 8).to_diesel();
+    let query = select(sql::<sql_types::Date>("'1999-01-08'::date"));
+    let got: jiff_diesel::Date = query.get_result(conn)?;
+    assert_eq!(date, got);
+
+    // Integration with jiff::civil::Time
+    let time = civil::time(23, 59, 59, 999_999_000).to_diesel();
+    let query = select(sql::<sql_types::Time>("'23:59:59.999999'::time"));
+    let got: jiff_diesel::Time = query.get_result(conn)?;
+    assert_eq!(time, got);
+
+    Ok(())
+}
+
+/// Shows how to decode a PostgreSQL interval into a Jiff `Span`.
+///
+/// Jiff doesn't support encoding a `Span` as a PostgreSQL interval, but
+/// Jiff can decode a PostgreSQL interval into a `Span`. The reason for this
+/// is that encoding an arbitrary `Span` into a PostgreSQL interval requires
+/// a relative datetime. Therefore, users wanting to store a `Span` will
+/// need to explicitly use a `sqlx::postgres::types::PgInterval` at least at
+/// encoding time.
+fn example_span_decode(conn: &mut PgConnection) -> anyhow::Result<()> {
+    let query = select(sql::<sql_types::Interval>(
+        "'2 years 15 months 100 weeks 99 hours 123456789 milliseconds'::interval",
+    ));
+    let got: jiff_diesel::Span = query.get_result(conn)?;
+
+    // The reason the span is in months/days/micros is because this
+    // is how the interval is transmitted from PostgreSQL. Years and
+    // months collapse into months, weeks and days collapses into days
+    // and everything else collapses into microseconds. If you need to
+    // preserve a `Span` as-is, then you'll need to encode it using Jiff's
+    // "friendly" duration format.
+    let span =
+        jiff::Span::new().months(39).days(700).microseconds(479856789000i64);
+    assert_eq!(span.fieldwise(), got.to_jiff());
+
+    Ok(())
+}
+
+/// Demonstrates that PostgreSQL's time zone session setting doesn't fuck up
+/// decoding.
+///
+/// This doesn't seem to trigger the same "text" decoding path that happens
+/// in SQLx. No clue why. And the Diesel impls for datetime types don't seem
+/// to have handling for text decoding, so maybe Diesel is doing something
+/// that forces binary data. ¯\_(ツ)_/¯
+///
+/// Ref: https://github.com/launchbadge/sqlx/issues/703#issuecomment-939133588
+fn example_time_zone_setting(conn: &mut PgConnection) -> anyhow::Result<()> {
+    sql_query("SET TIME ZONE 'Australia/Tasmania'").execute(conn)?;
+
+    let ts = "2020-01-01T05:01:01Z".parse::<jiff::Timestamp>()?.to_diesel();
+    let query = select(sql::<sql_types::Timestamptz>(
+        "'2020-01-01 05:01:01+00'::timestamp with time zone",
+    ));
+    let got: jiff_diesel::Timestamp = query.get_result(conn)?;
+    assert_eq!(ts, got);
+
+    let dt =
+        "2020-01-01T05:01:01".parse::<jiff::civil::DateTime>()?.to_diesel();
+    let query = select(sql::<sql_types::Timestamp>(
+        "'2020-01-01 05:01:01+00'::timestamp without time zone",
+    ));
+    let got: jiff_diesel::DateTime = query.get_result(conn)?;
+    assert_eq!(dt, got);
+
+    Ok(())
+}
diff --git a/examples/diesel-sqlite/Cargo.toml b/examples/diesel-sqlite/Cargo.toml
new file mode 100644
index 00000000..710ec559
--- /dev/null
+++ b/examples/diesel-sqlite/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+publish = false
+name = "diesel-sqlite-test"
+version = "0.0.1"
+edition = "2021"
+
+# This example has very fat dependencies. This makes it so this example won't
+# get compiled or run with `cargo test --all`.
+[workspace]
+
+[dependencies]
+anyhow = "1.0.95"
+diesel = { version = "2.2.0", features = ["sqlite"] }
+jiff = { path = "../.." }
+jiff-diesel = { path = "../../crates/jiff-diesel", features = ["sqlite"] }
+tempfile = "3.16.0"
+
+[[bin]]
+name = "diesel-sqlite-test"
+path = "main.rs"
diff --git a/examples/diesel-sqlite/main.rs b/examples/diesel-sqlite/main.rs
new file mode 100644
index 00000000..fe284864
--- /dev/null
+++ b/examples/diesel-sqlite/main.rs
@@ -0,0 +1,86 @@
+use anyhow::Context;
+use diesel::{
+    connection::Connection, dsl::sql, query_dsl::RunQueryDsl, select,
+    sql_types, sqlite::SqliteConnection,
+};
+use jiff::civil;
+use jiff_diesel::ToDiesel;
+
+fn main() -> anyhow::Result<()> {
+    let tmpfile = tempfile::NamedTempFile::new()?;
+    let path = tmpfile.path().to_str().context("invalid temporary file")?;
+    let mut conn = SqliteConnection::establish(path)?;
+
+    example_datetime_roundtrip(&mut conn)?;
+    example_timestamp_julian(&mut conn)?;
+
+    Ok(())
+}
+
+/// Performs a round-trip with all of Jiff's datetime types.
+fn example_datetime_roundtrip(
+    conn: &mut SqliteConnection,
+) -> anyhow::Result<()> {
+    // Integration with jiff::Timestamp
+    let ts = "1970-01-01T00:00:00Z".parse::<jiff::Timestamp>()?.to_diesel();
+    let query =
+        select(sql::<sql_types::TimestamptzSqlite>("'1970-01-01 00:00:00Z'"));
+    let got: jiff_diesel::Timestamp = query.get_result(conn)?;
+    assert_eq!(ts, got);
+
+    // Integration with jiff::civil::DateTime
+    let dt = civil::date(2025, 7, 20).at(0, 0, 0, 0).to_diesel();
+    let query = select(sql::<sql_types::Timestamp>("'2025-07-20 00:00:00'"));
+    let got: jiff_diesel::DateTime = query.get_result(conn)?;
+    assert_eq!(dt, got);
+
+    // Integration with jiff::civil::Date
+    let date = civil::date(1999, 1, 8).to_diesel();
+    let query = select(sql::<sql_types::Date>("'1999-01-08'"));
+    let got: jiff_diesel::Date = query.get_result(conn)?;
+    assert_eq!(date, got);
+
+    // Integration with jiff::civil::Time
+    let time = civil::time(23, 59, 59, 999_999_000).to_diesel();
+    let query = select(sql::<sql_types::Time>("'23:59:59.999999'"));
+    let got: jiff_diesel::Time = query.get_result(conn)?;
+    assert_eq!(time, got);
+
+    Ok(())
+}
+
+/// Demonstrates that Jiff works with SQLite's "julian day" format.
+///
+/// Here's a sample transcript to show how SQLite itself works:
+///
+/// ```text
+/// sqlite> select julianday('2025-02-06T21:33:30-05:00');
+/// julianday('2025-02-06T21:33:30-05:00')
+/// --------------------------------------
+/// 2460713.60659722
+/// sqlite> select datetime(2460713.60659722);
+/// datetime(2460713.60659722)
+/// --------------------------
+/// 2025-02-07 02:33:30
+/// sqlite> select unixepoch(2460713.60659722);
+/// unixepoch(2460713.60659722)
+/// ---------------------------
+/// 1738895610
+/// ```
+fn example_timestamp_julian(
+    conn: &mut SqliteConnection,
+) -> anyhow::Result<()> {
+    let query = select(sql::<sql_types::TimestamptzSqlite>(
+        "julianday('2025-02-06T21:33:30-05:00')",
+    ));
+    let got: jiff_diesel::Timestamp = query.get_result(conn)?;
+
+    // Play stupid games, win stupid prizes. The loss of precision here
+    // is what you get when you use floating point to represent datetimes.
+    let expected = "2025-02-07T02:33:29.99981308Z"
+        .parse::<jiff::Timestamp>()?
+        .to_diesel();
+    assert_eq!(got, expected);
+
+    Ok(())
+}
diff --git a/examples/sqlx-postgres/.gitignore b/examples/sqlx-postgres/.gitignore
deleted file mode 100644
index 1b72444a..00000000
--- a/examples/sqlx-postgres/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-/Cargo.lock
-/target
diff --git a/examples/sqlx-postgres/Cargo.toml b/examples/sqlx-postgres/Cargo.toml
index 28f161b5..bd4e0d97 100644
--- a/examples/sqlx-postgres/Cargo.toml
+++ b/examples/sqlx-postgres/Cargo.toml
@@ -11,10 +11,9 @@ edition = "2021"
 
 [dependencies]
 anyhow = "1.0.95"
-chrono = "0.4.39"
 jiff = { path = "../.." }
 jiff-sqlx = { path = "../../crates/jiff-sqlx", features = ["postgres"] }
-sqlx = { version = "0.8.3", features = ["chrono", "postgres", "runtime-tokio"] }
+sqlx = { version = "0.8.3", features = ["postgres", "runtime-tokio"] }
 tokio = { version = "1.43.0", features = ["full"] }
 
 [[bin]]
diff --git a/examples/sqlx-sqlite/.gitignore b/examples/sqlx-sqlite/.gitignore
deleted file mode 100644
index 1b72444a..00000000
--- a/examples/sqlx-sqlite/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-/Cargo.lock
-/target
diff --git a/examples/uptime/.gitignore b/examples/uptime/.gitignore
deleted file mode 100644
index 4fffb2f8..00000000
--- a/examples/uptime/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-/target
-/Cargo.lock


Test patch: