Skip to content

Add UUID v7 helper functions#4739

Merged
josevalim merged 6 commits into
elixir-ecto:masterfrom
drtheuns:master
Jul 7, 2026
Merged

Add UUID v7 helper functions#4739
josevalim merged 6 commits into
elixir-ecto:masterfrom
drtheuns:master

Conversation

@drtheuns

@drtheuns drtheuns commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Adds timestamp/1, datetime/1, and version/1 helpers for dealing with UUIDs.

My main use case is dealing with importing data with existing timestamps but no
UUIDv7 yet. There's currently no way to control the timestamp that's used for
the manual creation of the UUID.

Some parallels with Postgres:

  • uuid_extract_timestamp -> Ecto.UUID.timestamp
  • uuid_extract_version -> Ecto.UUID.version

Adds `timestamp/1`, `datetime/1`, and `version/1` helpers for dealing
with UUIDs.
Comment thread lib/ecto/uuid.ex Outdated
Comment thread lib/ecto/uuid.ex Outdated
Comment thread lib/ecto/uuid.ex Outdated
@greg-rychlewski

greg-rychlewski commented Jul 3, 2026

Copy link
Copy Markdown
Member

I don't use UUIDv7 but one thing I am also wondering is why Postgres only allows shifts from the current time rather than setting precise timestamps. I'm guessing it screws up the guarantees somehow and might be insecure. So I think we need to understand this before opening up the option in Ecto.

@josevalim

Copy link
Copy Markdown
Member

Let's please revert the :timestamp option because it opens up a pandora box. What means to pass it with monotonic precision? We can discuss it separately.

datetime is convenient in my opinion because it can also encode the correct precision in the field. One option is to call to_unix(precision \\ :second) to mirror datetime. But then should we call the other one to_datetime?

We also need to decide if we should raise or return ok/error tuples. Note conversion functions in Elixir raises, which would align with the rename proposed above. We should also add @specs.

@drtheuns

drtheuns commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

I found the following email on the removal of uuidv7(timestamp) from Postgres during development: https://www.postgresql.org/message-id/CAGECzQSdRW1PxqRM9F%3DDZ9daCc8D32i7HO7nHE1_ep3mOcv_1g%40mail.gmail.com

The use-case they outline is pretty much exactly my usecase:

I do agree with others however, that being able to pass in an
arbitrary timestamp for UUID generation would be very useful. For
example to be able to partition by the timestamp in the UUID and then
being able to later load data for an older timestamp and have it be
added to to the older partition

I understand why it shouldn't be a general part of this UUIDv7 implementation though, so I'll add it as a (temp) function for my migration.


I've removed the timestamp function & option. The datetime function was renamed to to_datetime & to_datetime! to match the functions in DateTime (to_iso8610, to_unix, etc).

@josevalim

Copy link
Copy Markdown
Member

Sorry, my feedback was unclear, so let me expand. We should have both to_unix and to_datetime. They raise as before. There are no bang variants for conversion functions in Elixir.

@drtheuns

drtheuns commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Hopefully I got it right this time :)

Comment thread lib/ecto/uuid.ex Outdated
@josevalim

Copy link
Copy Markdown
Member

cc @ryanwinchester

@ryanwinchester

ryanwinchester commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

I found the following email on the removal of uuidv7(timestamp) from Postgres during development: https://www.postgresql.org/message-id/CAGECzQSdRW1PxqRM9F%3DDZ9daCc8D32i7HO7nHE1_ep3mOcv_1g%40mail.gmail.com

The use-case they outline is pretty much exactly my usecase:

I do agree with others however, that being able to pass in an
arbitrary timestamp for UUID generation would be very useful. For
example to be able to partition by the timestamp in the UUID and then
being able to later load data for an older timestamp and have it be
added to to the older partition

I understand why it shouldn't be a general part of this UUIDv7 implementation though, so I'll add it as a (temp) function for my migration.

Having the "from timestamp" helper function is nice if you need to backfill records and want them with a specific timestamp (I do do this sometimes, and just make my own helper). However, I agree with the reasons they stated that it doesn't fit UUIDv7 spec and that making it UUIDv8 makes sense.

Probably deserves a discussion outside of this PR.

In my opinion, just wait for the postgres folks to make a decision and then copy that, since they have more minds dedicated to these discussions.

Comment thread lib/ecto/uuid.ex Outdated

@ryanwinchester ryanwinchester left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Works like postgres' uuid_extract_timestamp 👍

@warmwaffles

warmwaffles commented Jul 6, 2026

Copy link
Copy Markdown
Member

Having the helper function is nice if you need to backfill records and want them with a specific timestamp (I do do this sometimes, and just make my own helper).

We had convert integer PKs to UUIDv7s and I wanted to set the timestamp based on the inserted_at column that was with the record to remain faithful to the v7 spec. I resorted to my own helper.

Something like this with :uniq (for the next person who is searching for help on how to do this with a helper 😉)

  def generate_uuid(unixtime) do
    <<rand_a::12, _::6, rand_b::62>> = :crypto.strong_rand_bytes(10)

    raw = <<unixtime::biguint(48), 7::4, rand_a::12, 2::2, rand_b::62>>
    Uniq.UUID.format(raw, :default)
  end

Edit: with Ecto.UUID

  def generate_uuid(unixtime) do
    <<rand_a::12, _::6, rand_b::62>> = :crypto.strong_rand_bytes(10)

    raw = <<unixtime::biguint(48), 7::4, rand_a::12, 2::2, rand_b::62>>
    Ecto.UUID.cast!(raw)
  end

@drtheuns

drtheuns commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Having the helper function is nice if you need to backfill records and want them with a specific timestamp (I do do this sometimes, and just make my own helper).

We had convert integer PKs to UUIDv7s and I wanted to set the timestamp based on the inserted_at column that was with the record to remain faithful to the v7 spec. I resorted to my own helper.

Something like this with :uniq (for the next person who is searching for help on how to do this with a helper 😉)

  def generate_uuid(unixtime) do
    <<rand_a::12, _::6, rand_b::62>> = :crypto.strong_rand_bytes(10)

    raw = <<unixtime::biguint(48), 7::4, rand_a::12, 2::2, rand_b::62>>
    Uniq.UUID.format(raw, :default)
  end

You can also replace the Uniq.UUID.format with just Ecto.UUID.cast!(raw).

@josevalim josevalim merged commit 18c050d into elixir-ecto:master Jul 7, 2026
8 checks passed
@josevalim

Copy link
Copy Markdown
Member

💚 💙 💜 💛 ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants