ash-project / ash-project/ash_csv

`update` and `destroy` write the CSV file with rows in reverse order

Closed Beginner friendly
#16 0 comments 0 reactions 0 assignees View on GitHub
bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.