Differences between UUID primary keys in Ecto vs Ash
As far as I can tell, the Ash Resource special attribute
uuid_primary_key
unwraps into:
Ash.UUID.generate/0
seems to be a passthrough to Ecto.UUID.generate()
. What I found unexpected was the migration files.
Echo's migration file looks like:
Ash's migration file looks like:
I'm not entirely sure if the functionality will all be the same at the end of the day. Reading docs and source code, it seems like Postgrex treats :binary
, :binary_id
, and :uuid
similarly when ran by Ecto SQL, but I'm not sure.
The most surprising is the :default
option in Ash's implementation. It hard-codes a Postgres DB function as a default value even though :primary_key
option is set (which I think inherently makes the field non-nullable in most databases.
The reason I'm worried about it is because I'm interested in using UUIDv7 for my PKs. That function may never run if generating UUIDs in Elixir-land, but it seems off to be there.15 Replies
I’m pretty sure that using uuid and binary types under the hood will be equivalent in all ways that matter 🙂 in terms of the default, that is there because
uuid_primary_key
sets a default
value
If you want to change it, you can set default: &Some.other_generator/0
If the migration generator sees default functions that it knows about it sets a db level default. It’s a bit magical but covers the most common cases of DateTime.utc_now
and Ash.UUID.generate/0
You can configure the migration defaults for a field using the ash_postgres DSL:
You also don’t have to use uuid_primary_key
you could define a uuidv7 type and say
Which should be the equivalent of uuid_primary_key
but with your own type.Thanks for the info! I'm not quite a pro at navigating all the docs and source in Ash yet.
Yeah, I've been playing with manually defining the PK via
attribute
DSL. Something I noticed is in the documentation online, they say that using uuid_primary_key
is equivalent to what I wrote in my original post, but that might not be true. I noticed a file at resource_snapshots/repo/posts/yada-yada.json
tells a different story. It says that generated?
is false
.
Did I catch that right, or am I mistaken?🤔 Ah, yeah looks like you're right
generated?
is primarily informational, and is actually only important with using integer primary keys with postgres (the generated?
tells it to use a series)What property tells it to generate ID in Elixir-land if not that one?
the default being
&Ash.UUID.generate/0
generated?: true
means "the database may supply a value for this"
IIRC we do read_after_writes?
in ecto for things that are marked as generated though, but that hasn't seemed to impact anything. Would need to test it out by adding default: nil
to uuid_primary_key
and seeing if it comes back nil
. If so we may want to update uuid_primary_key
to have generated?: true
Okay, dropping PK scenario. What of these statements:
generated false
and default &MyMod.my_func/0
=> value generated in Elixir-land
generated true
and default fragement("plugin_func()")
=> value generated in data-layer land (Postgres)So
default
in the attribute is only concerning elixirland
If you want to set a database default you do it in the postgres
block
generated? false
means "the data layer will never supply a value for this. Its only the one we set" generated? true
means it might, and so we make sure to read it after writes and do any other required things (keep in mind Ash is data layer agnostic, so not all options make tangible differences on all data layers)I see. Okay, I think that clears it up for me. My main confusion I think is seeing that
fragment
in the migrations. Seems to show up if type is :uuid
no matter what.It should only show up if
default: &Ash.UUID.generate/0
(the default for uuid_primary_key
I swapped it for Ecto's function to see what'd it do and it still threw that on there. I'm testing writing my own to see what it does
oh, yeah it might also have a case for
Ecto.UUID.generate/0
😆
lemme check
Haha, there it is :thinkies:
Okay, I will close this post soon. I want to read over it again and comment a summary before I do.
Okay, following up here and for posterity: more 2 questions.
Question 1: your example @Zach Daniel:
The
type
(in this theoretical case "UUIDv7") concerns itself with decoding and encoding. The default
concerns itself with generating a value and encoding to put into the database in Elixir-land for create actions. In this case, generated?
should be false, right?
Question 2: for UUIDs that we generate in Elixir-land, as is the case with default: &Ash.UUID.generate/0
, what's the point of that magical ~S[fragment("uuid_generate_v4()")]
?1. Correct
generated?
only matters if you don't have a default
, and set migration_defaults [id: <a fragment>]
or for integer_primary_key
2. It keeps your data more consistent with other usage of it. I.e if someone goes in and creates a thing in the db directly they don't have to figure out how to make a valid uuidCreating a uuidv7 type and checking if the https://pgxn.org/dist/pg_uuidv7/ extension is installed (otherwise use v4) is on my todo list.
PGXN: PostgreSQL Extension Network
pg_uuidv7: Create UUIDv7 values in Postgres / PostgreSQL Extension ...
Search all indexed extensions, distributions, users, and tags on the PostgreSQL Extension Network.
TLDR;
Don't worry about it if you're okay with your UUID being version 4. The end-of-the day for most projects will result in the same fuctionality. If you want to use a different UUID version for your PKs, manually write out an
attribute
DSL block and put your UUID-generating function capture in default
.
To sum up this post, my confusion around UUIDs in Ecto vs Ash came from comparing the migration files respectively.
- Ecto uses type binary_id
, Ash uses uuid
; I have not verified this is exactly the same inside the DB
- Ash adds the null: false
bit when using uuid_primary_key
- Ash adds an Ecto fragment for generating UUIDs on the DB even though Ash generates them in Elixir-land (as in the DB doesn't do it when using Ash); I haven't varified and wouldn't know how to
- Ash's addition of the Ecto fragment only applies when when the default
in the attribute
DSL block is Ash's or Ecto's standard UUID generate function
If wanting to use a different type of UUID, you cannot use uuid_primary_key
at the time of writing. You will have to manually write out an attribute
DSL block and use default
(this applies to Elixir-land only) for generating the UUID primary key. If you'd like parity to Ash's magic fragment, you'll need to use the migration_defaults
DSL in a postgres
clause to do it.
Don't know if I got the verbiage right.