Sudhakar Rayavaram
Problem solver (And maker),
Inquisitive (Root to most of my problems),
Software craftsman (Don't ask for estimates)

Works at TarkaLabs

Tech guy behind SportIndia.in

CSV export of Ecto models
22 Nov 2016

This post is about converting Ecto model to a csv using this CSV library

Ecto is a database DSL for Elixir and is part of the Phoenix framework. Recently we had a requirement to export one of the tables as CSV (you can never escape such export requirement as long as there is excel right?)

Here is an ecto model we wanted to export

1
2
3
4
5
6
7
8
9
10
  defmodule Message do
    use MyApp.Web, :model

    schema "messages" do
      field :message, :string
      field :status, :string
      field :uuid, Ecto.UUID
      timestamps()
    end
  end

It is a pretty simple model backed by table called “messages” which has the columns message, status and uuid

Get the data to be exported from the database

1
2
3
...
messages = Message order_by(:inserted_at) |> Repo.all
...

And trying to convert it to CSV

1
2
3
messages
  |> CSV.encode
  |> Enum.to_list

does not work because the CSV encoder does not know how to deal with the Message type. So, define an encoder function for the Message type

1
2
3
4
5
6
7
8
9
10
defmodule Message do
  ...
  defimpl CSV.Encode, for: Message do
    def encode(cm, env \\ []) do
      [cm.message, cm.status, cm.uuid]
        |> Enum.map(fn(v) -> CSV.Encode.encode(v, env) end)
        |> Enum.join(",")
    end
  end  
end

We are creating an array of all the needed column values and encoding them individually. Then, joining them to form the CSV for the given message struct

But this is not enough. CSV.encode/2 expects a stream of data in a tabular format, and encodes it to RFC 4180 compliant CSV lines. By that it means the data should be in the format

1
2
3
4
[
  ["row1-col1-data","row1-col2-data","row1-col3-data"],
  ["row2-col1-data","row2-col2-data","row2-col3-data"]
]

So, wrap the array of Ecto Message objects into an array of array having a message object per row

1
2
3
4
[
  [message1],
  [message2]  
]

Do that and pass it to the csv encoder

1
2
3
4
5
6
7
...
messages = Message order_by(:inserted_at) |> Repo.all
  |> Enum.map(fn(m) -> [m] end)
messages
  |> CSV.encode
  |> Enum.to_list
...

The resulting string will be the CSV of Ecto message instances