I have a table of events, where some of the rows pertain to devices. I’m looking to select/delete the first/last N events per device in a single statement. The device ID is not itself a column of the table, but computed from one of the other columns.
So, first of all I managed to do what I want in SQLite using ROW_NUMBER(). These two queries should be more or less equivalent, and do what I expect:
Using nested queries
SELECT *
FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY device_id ORDER BY emitted_at) AS rn
FROM (
SELECT *, COALESCE(JSON_EXTRACT(arguments, '$.context.device_id'), JSON_EXTRACT(arguments, '$.arguments.device_id')) AS device_id
FROM events
)
);
Using WITH
WITH
events_with_device_id AS (
SELECT *, COALESCE(JSON_EXTRACT(arguments, '$.context.device_id'), JSON_EXTRACT(arguments, '$.arguments.device_id')) AS device_id
FROM events
),
events_with_device_id_and_row_number AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY device_id ORDER BY emitted_at) AS rn
FROM events_with_device_id
)
SELECT * FROM events_with_device_id_and_row_number;
Now I’m trying and failing to accomplish the same thing with Ecto.
First I tried the most straightforward option, putting everything in a single from/2:
from e in Event,
windows: [device_id: [partition_by: e.device_id, order_by: :emitted_at]],
select_merge: %{
device_id: device_id_column(e),
rn: row_number() |> over(:device_id)
}
(device_id_column/1 is the COALESCE(...) you see in SQL)
This failed as I'd expected
[debug] QUERY ERROR source="events" db=0.0ms queue=0.1ms idle=1036.2ms
SELECT e0."id", e0."type", e0."arguments", e0."emitted_at", e0."inserted_at", e0."updated_at", coalesce(json_extract(e0."arguments", '$.context.device_id'), json_extract(e0."arguments", '$.arguments.device_id')), row_number() OVER "device_id" FROM "events" AS e0 WINDOW "device_id" AS (PARTITION BY e0."device_id" ORDER BY e0."emitted_at") LIMIT 1 []
↳ :elixir.eval_external_handler/3, at: src/elixir.erl:371
** (Exqlite.Error) no such column: e0.device_id
SELECT e0."id", e0."type", e0."arguments", e0."emitted_at", e0."inserted_at", e0."updated_at", coalesce(json_extract(e0."arguments", '$.context.device_id'), json_extract(e0."arguments", '$.arguments.device_id')), row_number() OVER "device_id" FROM "events" AS e0 WINDOW "device_id" AS (PARTITION BY e0."device_id" ORDER BY e0."emitted_at") LIMIT 1
(ecto_sql 3.13.4) lib/ecto/adapters/sql.ex:1113: Ecto.Adapters.SQL.raise_sql_call_error/1
(ecto_sql 3.13.4) lib/ecto/adapters/sql.ex:1011: Ecto.Adapters.SQL.execute/6
(ecto 3.13.5) lib/ecto/repo/queryable.ex:241: Ecto.Repo.Queryable.execute/4
(ecto 3.13.5) lib/ecto/repo/queryable.ex:19: Ecto.Repo.Queryable.all/3
iex:36: (file)
Then I tried the second most straightforward option:
events_with_device_id = from e in Event,
select_merge: %{device_id: device_id_column(e)}
from e in events_with_device_id,
windows: [device_id: [partition_by: e.device_id, order_by: :emitted_at]],
select_merge: %{rn: row_number() |> over(:device_id)}
This also failed; it's how Ecto queries are composed after all
[debug] QUERY ERROR source="events" db=0.0ms idle=1531.7ms
SELECT e0."id", e0."type", e0."arguments", e0."emitted_at", e0."inserted_at", e0."updated_at", coalesce(json_extract(e0."arguments", '$.context.device_id'), json_extract(e0."arguments", '$.arguments.device_id')), row_number() OVER "device_id" FROM "events" AS e0 WINDOW "device_id" AS (PARTITION BY e0."device_id" ORDER BY e0."emitted_at") LIMIT 1 []
↳ :elixir.eval_external_handler/3, at: src/elixir.erl:371
** (Exqlite.Error) no such column: e0.device_id
SELECT e0."id", e0."type", e0."arguments", e0."emitted_at", e0."inserted_at", e0."updated_at", coalesce(json_extract(e0."arguments", '$.context.device_id'), json_extract(e0."arguments", '$.arguments.device_id')), row_number() OVER "device_id" FROM "events" AS e0 WINDOW "device_id" AS (PARTITION BY e0."device_id" ORDER BY e0."emitted_at") LIMIT 1
(ecto_sql 3.13.4) lib/ecto/adapters/sql.ex:1113: Ecto.Adapters.SQL.raise_sql_call_error/1
(ecto_sql 3.13.4) lib/ecto/adapters/sql.ex:1011: Ecto.Adapters.SQL.execute/6
(ecto 3.13.5) lib/ecto/repo/queryable.ex:241: Ecto.Repo.Queryable.execute/4
(ecto 3.13.5) lib/ecto/repo/queryable.ex:19: Ecto.Repo.Queryable.all/3
iex:35: (file)
So I turned to with_cte/3:
events_with_device_id =
from e in Event,
select_merge: %{device_id: device_id_column(e)}
{"events_with_device_id", Event}
|> with_cte("events_with_device_id", as: ^events_with_device_id)
|> windows(device_id_window: [partition_by: :device_id, order_by: :emitted_at])
|> select_merge(%{rn: row_number() |> over(:device_id_window)})
And of course it also failed, otherwise I wouldn't be here ^^
[debug] QUERY OK source="events_with_device_id" db=0.9ms idle=1269.0ms
WITH "events_with_device_id" AS (SELECT se0."id" AS "id", se0."type" AS "type", se0."arguments" AS "arguments", se0."emitted_at" AS "emitted_at", se0."inserted_at" AS "inserted_at", se0."updated_at" AS "updated_at", coalesce(json_extract(se0."arguments", '$.context.device_id'), json_extract(se0."arguments", '$.arguments.device_id')) AS "device_id" FROM "events" AS se0) SELECT e0."id", e0."type", e0."arguments", e0."emitted_at", e0."inserted_at", e0."updated_at", row_number() OVER "device_id_window" FROM "events_with_device_id" AS e0 WINDOW "device_id_window" AS (PARTITION BY e0."device_id" ORDER BY e0."inserted_at") LIMIT 1 []
↳ :elixir.eval_external_handler/3, at: src/elixir.erl:371
** (ArgumentError) struct Event does not have the key :rn
(ecto 3.13.5) lib/ecto/repo/queryable.ex:327: anonymous fn/4 in Ecto.Repo.Queryable.process/4
(stdlib 7.2) maps.erl:894: :maps.fold_1/4
(ecto 3.13.5) lib/ecto/repo/queryable.ex:326: Ecto.Repo.Queryable.process/4
(ecto 3.13.5) lib/ecto/repo/queryable.ex:278: Ecto.Repo.Queryable.preprocess/4
(elixir 1.19.5) lib/enum.ex:1688: Enum."-map/2-lists^map/1-1-"/2
(ecto 3.13.5) lib/ecto/repo/queryable.ex:246: Ecto.Repo.Queryable.execute/4
(ecto 3.13.5) lib/ecto/repo/queryable.ex:19: Ecto.Repo.Queryable.all/3
iex:43: (file)
From the generated query I can see that there’s no AS rn next to the ROW_NUMBER() ... result expression, which could explain the error. Why is it missing, though?! Am I holding it wrong?
Thanks in advance!






















