coen.bakker

coen.bakker

Blink - Fast bulk seeding for Ecto/PostgreSQL with clean, declarative syntax

Blink is a library for fast bulk data insertion into PostgreSQL databases using the COPY command. It provides a clean, declarative syntax for defining seeders.

Features:

  • Uses PostgreSQL’s COPY for fast bulk inserts
  • Tables inserted in declaration order to respect foreign key constraints
  • Access data from previously defined tables when building subsequent tables
  • Store auxiliary context data that won’t be inserted into the database
  • Load data from CSV/JSON files with Blink.from_csv/2 and Blink.from_json/2
  • :transform option for type conversion when loading from files
  • Integrates with ExMachina nicely
  • Rollback on errors
  • Adapter pattern for supporting other databases

Example:

defmodule MyApp.Seeder do
  use Blink

  def call do
    new()
    |> add_table(:users)
    |> add_table(:posts)
    |> insert(MyApp.Repo)
  end

  def table(_store, :users) do
    [
      %{id: 1, name: "Alice", email: "alice@example.com"},
      %{id: 2, name: "Bob", email: "bob@example.com"}
    ]
  end

  def table(store, :posts) do
    users = store.tables.users
    # Build posts referencing users...
  end
end

Links:

https://github.com/nerds-and-company/blink

Most Liked

Asd

Asd

Good library.

I’ve read the code and found a couple of fairly obvious bugs (like non-escaped strings in generated CSV) and limitations (like reading everything in memory), so I made a PR with fixes.

I am also providing fairly cheap consultancy services if you want to have this kind of review and contribution in your private projects.

coen.bakker

coen.bakker

v0.5.0 Released

Version 0.5.0 is now available. This release marks a big step toward 1.0.0 — it covers all the major changes I had planned. Now the focus shifts to gathering feedback, fixing bugs, and addressing any remaining breaking changes before 1.0.0 (though I don’t have any in mind).

The headline feature is stream support, which enables memory-efficient seeding of large datasets.

Both table/2 clauses return streams in the example below, but returning lists still works as before.

defmodule Blog.Seeder do
  use Blink

  def call do
    new()
    |> with_table("users")
    |> with_table("posts")
    |> run(Blog.Repo, timeout: :infinity)
  end

  def table(_seeder, "users") do
    Stream.map(1..200_000, fn i ->
      %{
        id: i,
        name: "User #{i}",
        email: "user#{i}@example.com",
        ...
        inserted_at: ~U[2024-01-01 00:00:00Z],
        updated_at: ~U[2024-01-01 00:00:00Z]
      }
    end)
  end
  
  def table(seeder, "posts") do
    users_stream = seeder.tables["users"]

    Stream.flat_map(users_stream, fn user ->
      for i <- 1..20 do
        %{
          id: (user.id - 1) * 20 + i,
          title: "Post #{i} by #{user.name}",
          body: "This is the content of post #{i}",
          user_id: user.id,
          ...
          inserted_at: ~U[2024-01-01 00:00:00Z],
          updated_at: ~U[2024-01-01 00:00:00Z]
        }
      end
    end)
  end
end

Other highlights

  • JSONB support — nested maps are automatically JSON-encoded during insertion
  • Configurable timeout — :timeout option for long-running transactions
  • Configurable batch size — :batch_size option controls stream chunking (default: 10,000 rows)
  • Performance improvement — CSV encoding executes significantly faster
  • Bug fix — CSV escaping now correctly handles pipes, quotes, newlines, and backslashes

Breaking changes

  • Blink.Store → Blink.Seeder
  • insert/3 → run/3
  • add_table/2 → with_table/2
  • add_context/2 → with_context/2
  • Return values simplified to :ok (raises on failure)
  • Adapter call/4 callback now receives table_name as a string

Full changelog: v0.5.0 release

Asd

Asd

You missed a couple of other important things from my PR:

  1. Doing

    try do
      adapter.call(...)
    rescue
      UndefinedFunctionError ->
        raise "Module #{inspect adapter} must implement call/4"
    end
    

    is a strange approach. Removing the try completely would result in the more readable and meaningful exception.

    Plus, it is a buggy approach. Take for example a situation then the call function itself calls an undefined function. This try clause would hide this error, making debugging a nightmare

  2. You new approach opens and parses a CSV file twice in stream mode. First one to get the headers and second one to stream the data. This is not an issue when there is a one huge file, but it is an issue when there are a lot of small files. Opening a file is an operation which is more expensive than reading from a file

Last Post!

coen.bakker

coen.bakker

v0.6.0 Released

Version 0.6.0 is now available. This version brings parallel COPY operations, enabling significantly faster bulk inserts when seeding data.

defmodule Blog.Seeder do
  use Blink

  def call do
    new()
    |> with_table("categories")
    |> with_table("events")
    |> run(Blog.Repo, max_concurrency: 8)
  end

  def table(_seeder, "categories"), do: # ...
  def table(_seeder, "events"), do: # ...
end

Highlights:

  • Parallel COPY operations — the new :max_concurrency option (default: 6) allows batches to be inserted using multiple database connections in parallel
  • Per-table options — configure :batch_size and :max_concurrency per table via with_table/4
  • New guide — added Configuring Options guide

Full changelog: CHANGELOG.md

Where Next?

Popular in Announcing Top

OvermindDL1
Been making an MLElixir thing (not released yet…) for fun in spare time in the past day. I’m just trying to see how much I can get an ML...
132 14347 106
New
Crowdhailer
Raxx is an alternative to Plug and is inspired by projects such as Rack(Ruby) and Ring(Clojure). 1.0-rc.1 is now available. To use it re...
New
devonestes
Introducing assertions, the library that helps you write really great test assertions! GitHub: GitHub - devonestes/assertions: Helpful a...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 44532 311
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
nikokozak
Hello all, I’ve been working on Svonix - a library for quickly integrating Svelte components into Phoenix views. It’s a much-needed succ...
New
zachdaniel
Ash Framework What is Ash? Ash Framework is a declarative, resource-oriented application development framework for Elixir. A resource can...
New

Other popular topics Top

KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36654 110
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New

We're in Beta

About us Mission Statement