Trellis.Migration (Trellis v0.1.0-alpha)

Copy Markdown View Source

Trellis statements in an Ecto migration, so a transform is defined, and dropped, the way the tables it reads are created.

defmodule MyApp.Repo.Migrations.DefineWidgetPrices do
  use Ecto.Migration
  use Trellis.Migration

  # Required: see "Transactions" below.
  @disable_ddl_transaction true

  def up do
    create table(:widgets) do
      add :price, :integer
    end

    define "TRANSFORM widget_prices FROM widgets SELECT price AS price"
  end

  def down do
    apply "PAUSE TRANSFORM widget_prices"
    apply "DROP TRANSFORM widget_prices"
    drop table(:widgets)
  end
end

use Trellis.Migration imports three helpers, each a thin call through to the function of the same name on Trellis:

  • define/1: a TRANSFORM statement, and nothing else (Trellis.define/2).
  • apply/1: any statement of the grammar (Trellis.apply/2), such as the PAUSE TRANSFORM and DROP TRANSFORM a down/0 undoes a define with. (DROP refuses a transform that isn't paused, and drops its target table.)
  • status/1: the status of the transform writing a target, or nil (Trellis.status/2).

define/1 and apply/1 are queued like Ecto's own commands, so they run in order with them: a transform can read a table created earlier in the same migration. status/1 runs when it's called, since a migration reads it to decide what to queue. Each raises the Trellis.Error if the call fails, which fails the migration.

Transactions

Trellis doesn't join the migration's transaction. It runs every call on connections of its own, and each call commits before it returns. If the migration's transaction rolled back after a define, the host's changes would be undone and the definition and its target table would not.

So a migration that uses these helpers has to turn its transaction off with @disable_ddl_transaction true. define/1 and apply/1 check, as they run, that the migration's repo has no transaction open, and raise an Ecto.MigrationError before applying anything if it does. In a migration that forgot the attribute, the migration's transaction then rolls back with nothing applied on either side.

Without the transaction, a migration that fails partway leaves whatever ran before the failure in place, on both sides. Give a migration that defines transforms little else to do.

up/0 and down/0, not change/0

Ecto can reverse create table on its own, but not a Trellis statement: the reverse of a define is a pause and a drop of a target that only the statement names. So a module that uses Trellis.Migration must define up/0 and down/0, and defining change/0 is a compile error, rather than a rollback that fails partway.

Which Trellis

Ecto runs migrations without starting the application, so the application's supervised Trellis isn't running. Each helper connects a handle of its own instead, from the :trellis key of the repo's configuration, runs Trellis.migrate/1 on it (creating Trellis's own tables the first time), makes its one call, and shuts the handle down:

# config/runtime.exs
config :my_app, MyApp.Repo,
  url: database_url,
  trellis: [url: database_url]

The options are Trellis.connect/1's. A migration's handle never runs background work: :staging and :drain_threads are always the defaults, whatever the configuration says.

Running a migration once

A define isn't idempotent: a second define of a target that exists fails with a :conflict error. Ecto's schema_migrations already records which migrations have run, so a migration's define runs once per database, and these helpers add no guard of their own.

A migration that has to tolerate a transform defined some other way (by hand, or by a release task) can guard the define itself:

def up do
  if status("widget_prices") == nil do
    define "TRANSFORM widget_prices FROM widgets SELECT price AS price"
  end
end

The guard checks the target's name, not its definition: a changed statement for a target that exists is an ALTER TRANSFORM.

Summary

Functions

Queues any statement of Trellis's grammar, to run in order with the migration's other commands. It is Trellis.apply/2.

Queues a TRANSFORM statement, to run in order with the migration's other commands. It is Trellis.define/2: any other statement form is refused before anything is applied.

The status of the transform that writes target_table, or nil if none does. It is Trellis.status!/2, run as it's called rather than queued.

Functions

apply(statement)

@spec apply(String.t()) :: :ok

Queues any statement of Trellis's grammar, to run in order with the migration's other commands. It is Trellis.apply/2.

define(statement)

@spec define(String.t()) :: :ok

Queues a TRANSFORM statement, to run in order with the migration's other commands. It is Trellis.define/2: any other statement form is refused before anything is applied.

status(target_table)

@spec status(String.t()) :: Trellis.Status.t() | nil

The status of the transform that writes target_table, or nil if none does. It is Trellis.status!/2, run as it's called rather than queued.