gutomotta
Ecto: how to insert into a Schema with one field being a subquery
Hey everyone, just joined this forum. I’m new to Elixir and have been working on a side project to learn it. Happy to be here.
I have a question related to Ecto, and I’m not sure if what I’m trying to do makes sense.
I want to insert rows in a SQL table from an Ecto.Changeset that will contain some fields provided by the user, and I want one of the fields not to be provided by the user, but to be the result of a query instead.
Some Background
I’m writing a Phoenix application. It has a SQL database (I’m using SQLite), and there are two related Schemas: a “Parent” has_many “Children” (so “Child” belongs_to “Parent”, it has a parent_id FK).
I want the children table to have a column named parent_scoped_row_id, which would store the row number of that child scoped by parent_id. Here’s an example of what I mean:
| id | parent_id | inserted_on | parent_scoped_row_id |
|---|---|---|---|
| 1 | 1 | 2023-12-01 | 1 |
| 2 | 1 | 2023-12-02 | 2 |
| 3 | 1 | 2023-12-03 | 3 |
| 4 | 2 | 2023-12-01 | 1 |
| 5 | 2 | 2023-12-02 | 2 |
| 6 | 3 | 2023-12-01 | 1 |
I want parent_scoped_row_id to not be provided by users, but auto-generated instead.
SQL solution
The following works on SQLite:
create table parents(id primary key);
create table children(
id primary key,
parent_id integer not null constraint "child_parent_fk" references parents(id),
inserted_on text,
parent_scoped_row_id integer
);
insert into parents values (1), (2), (3);
insert into children values (
1,
1,
"2023-12-01",
(select 1 + coalesce(max(parent_scoped_row_id), 0) from children where parent_id = 1)
);
insert into children values (
2,
2,
"2023-12-01",
(select 1 + coalesce(max(parent_scoped_row_id), 0) from children where parent_id = 2)
);
-- and so on...
Problem is: how can I reproduce it with Ecto?
What I’ve tried
# child.ex
defmodule Myapp.Example.Child do
use Ecto.Schema
import Ecto.Changeset
alias Myapp.Example.Parent
schema "children" do
field :inserted_on, :date
field :parent_scoped_row_id, :integer
belongs_to :parent, Parent
end
@doc false
def changeset(child, attrs) do
child
|> cast(attrs, [:inserted_on, :parent_id])
|> validate_required([:inserted_on, :parent_id])
end
end
# application code
defmodule Example
def create_child(attrs \\ %{}) do
changeset =
%Child{},
|> Child.changeset(attrs)
parent_id = get_field(changeset, :parent_id)
subquery =
from(
c in Child,
where: c.parent_id == ^parent_id,
select: 1 + coalesce(max(c.parent_scoped_row_id), 0)
)
changeset
|> put_change(:parent_scoped_row_id, subquery)
|> Repo.insert()
end
end
But when I run create_child(), I get this error:
** (Ecto.ChangeError) value `#Ecto.Query<from c0 in Myapp.Example.Child, where: c0.parent_id == ^1, select: 1 + coalesce(max(c0.parent_scoped_row_id), 0)>` for `Myapp.Example.Child.parent_scoped_row_id` in `insert` does not match type :integer
I also tried using other types for :parent_scoped_row_id, and also not declaring it altogether but none of those worked.
I could probably solve this by getting the field and then putting it in the changeset before inserting, all inside a transaction. Not sure this would work, but I really wanted to try solving it with a single SQL statement before going this path.
What now?
I’m not sure where to go from here. I suppose I could try to build a Custom Field Type instead of :integer for my field, or try Ecto’s autogenerate, or something else. But as I said, I’m new to all of this and I don’t know how autogenerate and Custom Field Types work. I also don’t know if those are the right solution for the problem.
Does anyone have any insights that could help me?
Thank you for your patience reading all this ![]()
Marked As Solved
joey_the_snake
It seems like the SQLite adapter does not handle query values at the moment. It is turning it into a query parameter. Take a look at the difference between the Postgres and SQLite adapter for this function:
SQLite: ecto_sqlite3/lib/ecto/adapters/sqlite3/connection.ex at main · elixir-sqlite/ecto_sqlite3 · GitHub
Postgres: ecto_sql/lib/ecto/adapters/postgres/connection.ex at master · elixir-ecto/ecto_sql · GitHub
My advice would be to open an issue for the sqlite adapter and see if it’s possible for them to support it.
Also Liked
joey_the_snake
insert_all will allow fields to be populated by a subquery
joey_the_snake
No problem. The issue with plain insert is that it expects values so it can perform validation prior to persisting to the database.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance










