`Ecto.Multi.insert_all` doesn't autogenerate uuid?

This will work:

Repo.insert!(
  %Vote{
    user_id: user_id,
    input_id: hd(input_ids)
  },
  prefix: Triplex.to_prefix(tenant)
)

Query generated:

INSERT INTO “localhost”.“votes” (“input_id”,“user_id”,“id”,“inserted_at”,“updated_at”) VALUES ($1,$2,$3,$4,$5)

This won’t:

new_votes =
 [
   %{
     input_id: "2c92c315-b81f-413e-8014-b7a0ab2038a6",
     user_id: "2dd65dcd-9741-4075-943c-c372c8a2ae97"
   }
 ]

Ecto.Multi.new()
|> Ecto.Multi.delete_all(:delete_votes, delete_votes_query, prefix: Triplex.to_prefix(tenant))
|> Ecto.Multi.insert_all(:insert_all, Vote, new_votes, prefix: Triplex.to_prefix(tenant))
|> Repo.transaction()

Error:

** (Postgrex.Error) ERROR 23502 (not_null_violation) null value in column “id” violates not-null constraint
table: votes
column: id

Query generated:

INSERT INTO “localhost”.“votes” (“input_id”,“user_id”) VALUES ($1,$2)

I do not understand why Ecto.Multi.insert_all doesn’t generate id and timestamp columns like Repo.insert! does.

In the Vote schema we have:
@primary_key {:id, Ecto.UUID, autogenerate: true}

deps: {:ecto_sql, "~> 3.1"}

Any help?:frowning:

I do not understand why Ecto.Multi.insert_all doesn’t generate id and timestamp columns like Repo.insert! does.

The ID thing is definitely weird, but the timestamps not being generated is documented behavior:

If the schema contains an autogenerated ID field, it will be handled either at the adapter or the storage layer. However any other autogenerated value, like timestamps, won’t be autogenerated when using insert_all/3.

There’s this thread that describes almost exactly the same issue, but what needed to change to make the problem go away isn’t clear :frowning:

IDs generation can be moved to the database level. Postgres has great uuid-ossp extension. Enable it, set the default value for id column to uuid_generate_v4() and use @primary_key {:id, Ecto.UUID, read_after_writes: true}.

4 Likes

See docs in link below mentioning that UUID are actually purposely not autogenerated:

The *_all functions in ecto deal with queries and plain values, but not schemas. Even if you supply a schema module, it’s only used for building an query (they’re Ecto.Queryable's).

Given all the timestamp and autogenerate stuff is defined for schemas, those functions don’t know about those things and therefore can’t handle them.

1 Like

Thank you. But then I still wonder what’s the reason that we don’t have a version of those *_all functions that “do more”/“are smarter” (e.g. autogenerate an UUID), in addition to the lower-level ones (allowing then to avoid using insert in an Enum.each if you want to use Schema’s configs).

Also why are specifically the *_all functions lower-level at all, as opposed to other functions? I don’t grasp the design choices.

Likely because they can’t do everything. Repo.insert can deal with assocs, which requires it to run multiple queries. All the *_all functions run just a single sql query, so they cannot support associations at all. So instead of opting for the vague middleground of supporting some schema based features and not others they simply support non of them. This is besides the fact that those functions are meant to work without needing schemas in the first place.

1 Like

It’s still hard for me to see why we can’t take into account the auto generated fields if an Ecto schema is passed as first argument. This is not related to associations. If I change my timestamp from naivedatetime to datetime, now I have to change the code using insert_all and such functions, or (less likely) if I change the type of id.

I can live with it though.

The “IF an Ecto Schema is passed…” is exactly why.

Supporting only a subset of schema based functionality of only a subset of possible arguments to *_all functions will be even more confusing than just saying: sorry all the schema stuff is not supported here. Besides the fact the implementation of the latter is likely way simpler to maintain.

Basically *_all functions are build to support Repo.insert_all("table", [%{id: 1}]) – maybe even more than Repo.insert_all(MySchema, [%{id: 1}]). Repo.insert is the other way round. It supports only schemas, but not plain queries.

1 Like

Could we mention one concrete example of a schema functionality (among the whole set of schema functionalities) that would be confusing for not to be supported in the case that support for field generation (id/timestamps) is added?

With fields on schemas you can alter the column name of a field:

schema "table" do
  field :test, source: "old_legacy_name"
end

Repo.insert_all(MySchema, [%{test: "value 1", old_legacy_name: "value 2"}]

What would this do under the assumption the function would work with schema functionality?

1 Like

Thank you, great example.

Actually I think now that insert_all should not accept Schemas, because this leads to brittle code; its usage seems limited.

Imagine you change the source of a field like your example above, or imagine you change the types of timestamps, … changes required may be forgotten or hard to find. What do you think?

It then seems that insert_all & co seem more fit for scripts.

Actually I think now that insert_all should not accept Schemas, because this leads to brittle code; its usage seems limited.

Imagine you change the source of a field like your example above, or imagine you change the types of timestamps, … changes required may be forgotten or hard to find. What do you think?

That’s exactly the reason, why Repo.insert does only accept schema structs and not plain maps.

It then seems that insert_all & co seem more fit for scripts.

Not necessarily, but it’s for sure more low level.

2 Likes