Definition
Data migration is the process of transferring data between storage systems, formats, or database schemas while preserving data integrity, consistency, and completeness. Migrations encompass schema migrations (changing database table structures), data transformations (converting data formats or enriching records), storage migrations (moving data between backend technologies), and bulk imports (loading data from external sources). Effective migration strategies are versioned, reversible, idempotent, and validated at every stage.
In the Elixir ecosystem, Ecto migrations provide a first-class mechanism for database schema evolution, offering timestamped versioning, up/down reversibility, and SQL generation from Elixir DSL expressions.
Technical Deep Dive
Data migrations in production systems must handle several challenges simultaneously.
| Challenge | Mitigation | Prismatic Approach |
|---|---|---|
| Downtime | Rolling migrations, blue-green deploys | Ecto migrations + Fly.io rolling deploys |
| Data Loss | Reversible migrations, backup-first | change/0 callbacks with automatic reverse |
| Inconsistency | Transactional migrations, checksums | Ecto transactions + content hash validation |
| Performance | Batched operations, concurrent indexes | CREATE INDEX CONCURRENTLY, batched inserts |
| Ordering | Timestamped migration files | Ecto timestamp-based ordering |
| Team Conflicts | Sequential version numbers | Ecto timestamps prevent conflicts |
Usage in Prismatic Platform
The Prismatic Platform manages migrations across multiple Ecto repos (main, DD pipeline, Perimeter) with coordinated schema evolution.
defmodule Prismatic.Repo.Migrations.CreateDdEntities do
@moduledoc """
Creates the core DD pipeline entity storage table with
JSONB attributes, content hashing for diff detection,
and proper indexing for efficient queries.
"""
use Ecto.Migration
def change do
create table(:dd_entities, primary_key: false) do
add :id, :binary_id, primary_key: true
add :source_slug, :string, null: false
add :external_id, :string, null: false
add :entity_type, :string, null: false
add :name, :string, null: false
add :attributes, :map, default: %{}
add :content_hash, :string, null: false
add :fetch_record_id, references(:dd_fetch_records, type: :binary_id)
timestamps(type: :utc_datetime_usec)
end
create unique_index(:dd_entities, [:source_slug, :external_id])
create index(:dd_entities, [:entity_type])
create index(:dd_entities, [:content_hash])
create index(:dd_entities, [:inserted_at])
end
endCode Examples
defmodule Prismatic.DataMigrator do
@moduledoc """
Cross-backend data migration utility leveraging the adapter pattern
to transfer data between storage backends with validation and
progress tracking.
"""
@type migration_opts :: [
batch_size: pos_integer(),
validate: boolean(),
on_conflict: :skip | :replace | :error
]
@spec migrate(module(), module(), migration_opts()) ::
{:ok, %{migrated: non_neg_integer(), skipped: non_neg_integer()}} |
{:error, term()}
def migrate(source_adapter, target_adapter, opts \\ []) do
batch_size = Keyword.get(opts, :batch_size, 500)
validate? = Keyword.get(opts, :validate, true)
with {:ok, items} <- source_adapter.list([]) do
result =
items
|> Enum.chunk_every(batch_size)
|> Enum.reduce(%{migrated: 0, skipped: 0}, fn batch, acc ->
case process_batch(batch, target_adapter, validate?, opts) do
{:ok, count} -> %{acc | migrated: acc.migrated + count}
{:skip, count} -> %{acc | skipped: acc.skipped + count}
end
end)
{:ok, result}
end
end
defp process_batch(batch, target_adapter, validate?, opts) do
validated =
if validate? do
Enum.filter(batch, &valid_record?/1)
else
batch
end
pairs = Enum.map(validated, fn record -> {record.id, record} end)
case target_adapter.bulk_put(pairs, opts) do
{:ok, count} -> {:ok, count}
{:error, _} -> {:skip, length(pairs)}
end
end
defp valid_record?(%{id: id, content_hash: hash}) when is_binary(id) and is_binary(hash), do: true
defp valid_record?(_), do: false
endBest Practices
- Always write reversible migrations -- use
change/0instead ofup/0+down/0when possible; Ecto auto-generates reverse operations. - Create indexes concurrently in production --
CREATE INDEX CONCURRENTLYavoids table locks on large tables. - Batch large data migrations -- processing millions of records in a single transaction will exhaust memory and lock tables.
- Validate data after migration -- row counts, checksums, and sample verification ensure completeness.
- Never mix schema and data migrations -- keep structural changes separate from data transformations for clear rollback paths.
- Test migrations against production-like data -- edge cases in real data frequently break migrations that pass on test fixtures.
Related Terms
- Ecto -- Database wrapper providing the migration DSL
- Data Quality -- Validation ensuring migration correctness
- ETL -- Extract-Transform-Load pattern used in complex migrations
- Data Pipeline -- Automated data processing workflows including migrations
- Deployment -- Release process incorporating database migrations
See Also
- Technologies -- Database technologies and migration tools
- Architecture -- Platform data architecture
- Apps -- Umbrella applications with migration requirements
Connect & Contribute
Created by Tomas Korcak (korczis) | Open Source under GHL