Do you build types for Ecto schemas assuming a hydrated value from the database or do you allow `nil` ids, etc?

When you are building out a @type for an Ecto schema do you express the type as a hydrated value from the Repo, making an assumption that fields like id and updated_at at non-nil values or do you keep the type more flexible with possible nil values for id and update_at?

I’ve historically preferred to make the assumption that the @type represents a hydrated value from the Repo, but this has burned me a bit since I’ll often need to build a change_noun/2 or changeset/2 type of function and for those the common pattern is to accept an empty struct value like %Noun{}. When I try to @spec the changeset/2 function saying it accepts the struct (not the type) I’ll get yelled at because credo does not recommend using Structs in @spec decorations: <> I could make the type more flexible but not sure I like that.

Does anyone have thoughts about this? Curious about what other people are doing.

I would imagine that using a type where the primary key is potentially nil is inconvenient since in most of the cases it will be there and to make the type checker happy, you’d need to litter the codebase with the required checks. Maybe a better way would be to have multiple types since that would reflect the reality better: there’s a type that represents the struct that is read from the database (some fields are not nil and there are some other constraints that the database schema enforces) and there’s a type that represent the data that you’re trying to put into the database (the result of calling apply on your changeset). But then there are cases like you mentioned or upserts:

Because we used on_conflict: :nothing , instead of getting an error, we got {:ok, struct} . However the returned struct does not reflect the data in the database. One possible mechanism to detect if an insert or nothing happened in case of on_conflict: :nothing is by checking the id field. id will be nil if the field is autogenerated by the database and no insert happened.

Because of those kind of issues I personally don’t use typespecs. I know the code works and I don’t like adding more characters to make the compiler/typechecker agree with me - I’d rather add more automated tests to cover that.

1 Like

Thanks for the insight and specifically for the suggestion above. It had not occurred to me as an option, but yeah makes a lot of sense. Thanks!