Blink uses an adapter pattern to support different database bulk insert implementations. While Blink ships with a PostgreSQL adapter (Blink.Adapter.Postgres), you can create custom adapters for other databases (e.g., MySQL).
The Adapter Behavior
All adapters must implement the Blink.Adapter behaviour, which requires a single callback:
@callback call(
rows :: Enumerable.t(),
table_name :: String.t(),
repo :: Ecto.Repo.t(),
opts :: Keyword.t()
) :: :okThe call/4 function receives:
rows- An enumerable (list or stream) of maps to bulk inserttable_name- Target table name (always a string; atom table names are converted first)repo- Ecto repository moduleopts- Adapter-specific options. Every option passed torun/3orcopy_to_table/4that Blink does not consume itself arrives here, so the adapter owns its option vocabulary and should validate it —Keyword.validate!/2rejects unknown keys, and invalid values should raiseArgumentErrortoo.
Example:
defmodule MyApp.Adapters.MySQL do
@behaviour Blink.Adapter
@impl true
def call(rows, table_name, repo, opts \\ []) do
# Your bulk insert implementation
:ok
end
endThe adapter should:
- Perform bulk insertion using database-specific commands
- Return
:okon success - Raise an exception on failure
Supporting :atomic
Seeds are all-or-nothing by default. When atomic: true, run/3 opens a
transaction on the calling process's connection and relies on the adapter to
perform the whole copy in the calling process so it enrols in that
transaction. An adapter that hands batches to other processes — each checking
out its own connection — silently voids the guarantee: a failed seed would
report a rollback while parts of it stayed committed.
Support :atomic only by copying in the calling process. If your adapter
cannot, reject the option rather than ignore it:
@impl true
def call(rows, table_name, repo, opts) do
case Keyword.get(opts, :atomic, true) do
true -> raise ArgumentError, "MyApp.Adapters.MySQL does not support atomic: true"
false -> bulk_insert(rows, table_name, repo, opts)
end
endUsing a Custom Adapter
Pass your adapter when calling run/3:
defmodule MyApp.Seeder do
use Blink
def call do
new()
|> with_table("users")
|> run(MyApp.Repo, adapter: MyApp.Adapters.MySQL)
end
def table(_seeder, "users") do
[
%{id: 1, name: "Alice"},
%{id: 2, name: "Bob"}
]
end
endOr use it directly with copy_to_table/4:
Blink.copy_to_table(items, "users", MyApp.Repo, adapter: MyApp.Adapters.MySQL)Overriding the Run Function
For complete control over the insertion process, you can override the run/3 callback in your Blink module:
defmodule MyApp.Seeder do
use Blink
@impl true
def run(%Blink.Seeder{} = seeder, repo, opts \\ []) do
# Implement custom transaction logic, error handling, etc.
# Tip: Blink.copy_to_table/4 is available for inserting data into individual tables
end
enduse Blink imports Blink.Seeder rather than aliasing it, so match on the
struct by its full name — a bare %Seeder{} does not resolve.
This allows you to customize transaction behavior, error handling, or set a default adapter for all operations. Note that overriding run/3 replaces Blink's transaction handling, so atomic: true no longer does anything by itself — your implementation is responsible for opening the transaction.
Reference
See Blink.Adapter.Postgres for a complete reference implementation using PostgreSQL's COPY FROM STDIN command.