When you generate a Phoenix project, there is a priv/repo/seeds.exs file that you can use to seed the database. Let’s see how to seed it from a CSV file.

We’ll be working with the transactions.csv file from Mint, and the project name is Minty. This assumes phoenix.gen.html has been used to create a model and the resources line has been added to web/router.ex.

$ mix phoenix.gen.html Transaction transactions date:string description:string original_description:string amount:string transaction_type:string category:string account_name:string labels:string notes:string

First, consult the documentation for the CSV library and add the dependency to mix.exs:

  defp deps do
    [ ...
     {:csv, "~> 1.2.3"}

Then in priv/repo/seeds.exs we’ll use the CSV library to generate a Map for each row of the CSV file with keys that EXACTLY match our Ecto model.

# Script for populating the database. You can run it as:
#     mix run priv/repo/seeds.exs
# Inside the script, you can read and write to any of your
# repositories directly:
#     Minty.Repo.insert!(%Minty.SomeModel{})
# We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong.

alias Minty.Transaction
alias Minty.Repo

defmodule Minty.Seeds do

  def store_it(row) do
    changeset = Transaction.changeset(%Transaction{}, row)


  |> Stream.drop(1)
  |> CSV.decode(headers: [:date, :description, :original_description, :amount, :transaction_type, :category, :account_name, :labels, :notes])
  |> Enum.each(&Minty.Seeds.store_it/1)

Note that we are dropping the first line of the CSV file. It contains headers, but we need to define different ones that match the Ecto model to make insertion easier.

As the comments indicate, you can run this with mix run priv/repo/seeds.exs.

This works, but it assumes everything is a string. To match up the types we need to do a bit more work.


The date column in the Mint.com CSV file is unfortunately in M[M]/DD/YYYY format. Additionally, the days are zero-padded, but not the months. (?!!) Ecto.Date can work with several formats, but that is not one of them. Let’s flip this around to be YYYY-MM-DD instead.

  def fix_date(%{date: <<m0,"/",d1,d0,"/",y3,y2,y1,y0>>} = row) do
    date = <<y3,y2,y1,y0,"-","0",m0,"-",d1,d0>>
    Map.update!(row,:date,fn _ -> date end)

  def fix_date(%{date: <<m1,m0,"/",d1,d0,"/",y3,y2,y1,y0>>} = row) do
    date = <<y3,y2,y1,y0,"-",m1,m0,"-",d1,d0>>
    Map.update!(row,:date,fn _ -> date end)

This pattern matches on the one- and two-digit months and then rearranges the bits with -’s in between, also zero-padding the one-digit months.

An earlier version of this used String.split and String.rjust, but I think I like it better with the pattern matching.

Now the migration can have add :date, :date and the model can have field :date, Ecto.Date.


All of the amounts in the CSV are positive numbers, and there is a separate transaction_type field that contains either “debit” or “credit”. Mint doesn’t really do double-entry bookeeping so I have no idea why they use this concept. Let’s keep income as positive numbers and change all the expenses (“debits”) to negative.

We’re still working with Strings at this point, so all we need to do is prepend a dash to indicate that it’s negative.

  def fix_amount(%{transaction_type: "debit"} = row) do

  def fix_amount(%{transaction_type: "credit"} = row) do

This pattern matches on a Map containing a key of transaction_type, and either updates the value under the :amount key, (or not.)

Now the amount field can be :float in both the migration and the model.


Drop the database, re-create it, and run the (edited) migration again:

$ mix ecto.drop
$ mix ecto.create
$ mix ecto.migrate
$ mix run "priv/repo/seeds.exs"

Now you should have data with a proper date field and positive/negative numbers so that math will work.


Now that the data is loaded, let’s try some queries. I have this in priv/repo/queries.exs

defmodule Minty.Queries do
  import Ecto.Query

  alias Minty.Repo
  alias Minty.Transaction

  def bignum do
      from txn in Transaction,
      where: txn.amount > 1000

  def summary do
      from txn in Transaction,
      group_by: txn.category,
      select: [txn.category, sum(txn.amount)]

Play with it in iex -S mix:

> c("priv/repo/queries.exs")
# large transactions
> Minty.Queries.bignum

# summarized by category, sorted by category name
> Minty.Queries.summary |> Enum.sort |> Enum.each(&(IO.inspect &1))

# summarized by category, sorted by amount
> Minty.Queries.summary |> Enum.sort(&(Enum.at(&1,1)>Enum.at(&2,1))) |> Enum.each(&(IO.puts("#{Enum.at(&1,0)} #{Float.to_string(Enum.at(&1,1),[decimals: 2])}")))


We’ve seen how to seed the database from a CSV file in a Phoenix project including date and float type fields, and some simple queries.

The code for this example is available at https://github.com/wsmoak/minty and is MIT licensed.

Copyright 2016 Wendy Smoak - This post first appeared on http://wsmoak.github.io and is CC BY-NC licensed.