ash-project / ash-project/ash_csv
`update` and `destroy` write the CSV file with rows in reverse order
- Dominant language
- Elixir
- Stars
- 16
- Forks
- 11
- PR merge metrics
- No merged PRs in 30d
Description
### Code of Conduct
- [x] I agree to follow this project's Code of Conduct
### AI Policy
- [x] I agree to follow this project's AI Policy, or I agree that AI was not used while creating this issue.
### Versions
elixir 1.20.0-otp-29
erlang 29.0.1
### Operating system
linux
### Current Behavior
### Describe the bug
`AshCsv.DataLayer.update/2` and `destroy/2` rewrite the whole file, but the rows come out in reverse order. Every update or destroy flips the file.
In `do_update/3` (and in `dump/6`, which it calls), rows are gathered with `Enum.reduce_while` by prepending (`[row | results]` / `[result | results]`). The list is passed straight to `dump_to_iodata/1` without `Enum.reverse/1`. `do_destroy/3` (via `cast/6`) does the same.
Reproduced on 0.9.8; the same code is on `main` and in 0.9.9.
### Reproduction
### To reproduce
```elixir
defmodule MyApp.Thing do
use Ash.Resource, domain: MyApp.Domain, data_layer: AshCsv.DataLayer
csv do
file "things.csv"
columns [:id, :value]
header? true
end
actions do
defaults [:read, :destroy, update: [:value]]
end
attributes do
attribute :id, :integer, primary_key?: true, allow_nil?: false, public?: true
attribute :value, :string, public?: true
end
end
```
`things.csv`:
```
id,value
1,a
2,b
3,c
```
```elixir
MyApp.Thing |> Ash.get!(2) |> Ash.Changeset.for_update(:update, %{value: "x"}) |> Ash.update!()
```
### Expected
```
id,value
1,a
2,x
3,c
```
### Actual
```
id,value
3,c
2,x
1,a
### Expected Behavior
### Suggested fix
Add `Enum.reverse/1` to the `{:ok, rows}` branch of both `do_update/3` and `do_destroy/3`, before `dump_to_iodata/1`.
### Related observation
The header that gets written back comes from `columns` (`header/1`), not from the header already in the file. If the file's header differs from the column names (e.g. `JB_id,M_id,Dato` mapped to `columns [:id, :original_ejer_id, :uddeling_dato]`), the first update replaces it. Reads skip the header row, so it's easy to assume the original header is preserved. It may be intended, but it's surprising for files shared with other tools.
Also, the rewrite uses `File.write(path, iodata, [:write])`, which truncates in place. A crash mid-write can leave a partial file. Writing to a temp file in the same directory and then calling `File.rename/2` would avoid that.
Contributor guide
Research direction
Start in do_update/3 and do_destroy/3, tracing their calls through dump/6 and cast/6 to dump_to_iodata/1. Reproduce the provided update and destroy scenarios, then verify that rows retain their original order after each rewrite while the updated or removed row is reflected correctly.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- elixir
- Domain
- database
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100