View Source ArangoXEcto.Migrator (ArangoX Ecto v2.0.0)

Lower level API for managing migrations.

This module provides functions to manage migrations. The tasks mentioned below are provided to cover the general use cases but it may be necessary to use the lower level API in some cases. For example, when building an Elixir release, Mix is not available, so this module provides the functions needed to migrate the system.

Large parts of this module is based on the ecto_sql Migrator module, so a lot of credit to them.

Mix tasks

The following mix tasks are available. They are similar to ecto_sql tasks but are appended with .arango to allow for both ecto_sql and arangox_ecto to be used side by side.

Example: Running an individual migration

Imagine you have this migration:

defmodule MyApp.MigrationExample do
  use ArangoXEcto.Migration

  def up do
    create collection("my_collection")
  end

  def down do
    drop collection("my_collection")
  end
end

You can execute it manually with:

ArangoXEcto.Migrator.up(Repo, 20240101120000, MyApp.MigrationExample)

Example: Running migrations in a release

Elixir v1.9 introduces mix release, which generates a self-contained directory that consists of your application code, all of its dependencies, plus the whole Erlang Virtual Machine (VM) and runtime.

When a release is assembled, Mix is no longer available inside a release and therefore none of the Mix tasks. Users may still need a mechanism to migrate their databases. This can be achieved with using this ArangoXEcto.Migrator module:

defmodule MyApp.Release do
  @app :my_app

  def migrate do
    for repo <- repos() do
      {:ok, _, _} = ArangoXEcto.Migrator.with_repo(repo, &ArangoXEcto.Migrator.run(&1, :up, all: true))
    end
  end

  def rollback(repo, version) do
    {:ok, _, _} = ArangoXEcto.Migrator.with_repo(repo, &ArangoXEcto.Migrator.run(&1, :down, to: version))
  end

  defp repos do
    Application.load(@app)
    Application.fetch_env!(@app, :ecto_repos)
  end
end

The example above uses with_repo/3 to make sure the repository is started and then runs all migrations up or a given migration down. Note you will have to replace MyApp and :my_app on the first two lines by your actual application name. Once the file above is added to your application, you can assemble a new release and invoke the commands above in the release root like this:

$ bin/my_app eval "MyApp.Release.migrate"
$ bin/my_app eval "MyApp.Release.rollback(MyApp.Repo, 20231225190000)"

Example: Running migrations on application startup

Add the following to the top of your application child spec:

{ArangoXEcto.Migrator, 
  repos: Application.fetch_env!(:my_app, :ecto_repos), 
  skip: System.get_env("SKIP_MIGRATIONS") == "true")}

This will allow you to use the environment variable SKIP_MIGRATIONS to skip running the migrations on startup.

Additionally, the other options specified in up/4 can be passed. For example, if you wanted to log the migrations and run them in a specific prefix you could do the following:

{ArangoXEcto.Migrator, 
  repos: Application.fetch_env!(:my_app, :ecto_repos), 
  log_migrator: true,
  prefix: "super_cool_user"}

Rolling back would be done normally using the mix arango.rollback mix task.

Summary

Functions

Returns a specification to start this module under a supervisor.

Runs a down migration on a given repo.

Executes a command on a repo

Gets all migrated versions.

Returns an array of the statuses of migrations for the given repo

Returns an array of the statuses of migrations for the given repo

Gets the migrations path for a repository.

Runs migrations for the given repository.

Apply migrations to a repository with a given strategy.

Runs migrations as part of your supervision tree.

Runs an up migration on a given repo.

Ensures the repo is started to perform migration operations.

Types

command()

(since 2.0.0)
@type command() :: :create | :create_if_not_exists | :drop | :drop_if_exists

log()

(since 2.0.0)
@type log() :: {Logger.level(), Logger.message(), Logger.metadata()}

subcommand()

(since 2.0.0)
@type subcommand() :: :add | :add_enum | :rename

Functions

child_spec(init_arg)

(since 2.0.0)

Returns a specification to start this module under a supervisor.

See Supervisor.

down(repo, version, module, opts \\ [])

(since 2.0.0)
@spec down(Ecto.Repo.t(), integer(), module(), Keyword.t()) :: :ok | :already_down

Runs a down migration on a given repo.

Options

  • :log - the level to use for logging of migration instructions. Defaults to :info. Can be any of Logger.level/0 values or a boolean. If false, it also avoids logging messages from the database.
  • :log_migrations - the level to use for logging of ArangoDB commands generated by migrations. Can be any of the Logger.level/0 values or a boolean. If false, logging is disabled. If true, uses the configured Repo logger level. Defaults to false
  • :log_migrator - the level to use for logging of ArangoDB commands emitted by the migrator, such as transactions, locks, etc. Can be any of the Logger.level/0 values or a boolean. If false, logging is disabled. If true, uses the configured Repo logger level. Defaults to false
  • :prefix - the prefix to run the migrations on

execute_command(meta, aql, opts)

(since 2.0.0)

Executes a command on a repo

The commands are passed from the migration module. A command essentially is the action of a migration, e.g. create, alter, rename, delete an object.

opts is passed to the database query when the command is run. Available options are dependant on the command being executed.

Available Commands

All commands are either a tuple or a string with AQL which is executed. The first element of the tuple is the action to be performed, the second is the object of the action. For some commands (such as creation of a collection) there is a third element of the tuple.

Creates (:create and :create_if_not_exists)

2nd element (Object)Third element
CollectionField commands passed to JsonSchema
Index
ViewView definition commands
Analyzer

Alter (:alter)

2nd element (Object)Third element
Existing CollectionField commands passed to JsonSchema (merged with existing schema)
Existing ViewView definition commands

Drops (:drop and :drop_if_exists)

2nd element (Object)Third element
Existing Collection
Existing Index
Existing View
Existing Analyzer

Rename (:rename)

2nd element (Object)Third element
Existing CollectionCollection with new name
Existing ViewView with new name

generate_view_definition(view, subcommands)

(since 2.0.0)

migrated_versions(repo, opts \\ [])

(since 2.0.0)
@spec migrated_versions(repo :: Ecto.Repo.t(), opts :: Keyword.t()) :: [integer()]

Gets all migrated versions.

Ensures the migrations collection exists if it doesn't exist yet.

Options

  • :prefix - the prefix to run the migrations on
  • :dynamic_repo - the name of the Repo supervisor process. See Ecto.Repo.put_dynamic_repo/1.
  • :skip_collection_creation - skips any attempt to create the migration collection Useful for situations where user needs to check migrations but has insufficient permissions to create the collection. Note that migrations commands may fail if this is set to true. Defaults to false. Accepts a boolean.

migrations(repo)

(since 2.0.0)
@spec migrations(Ecto.Repo.t()) :: [
  {:up | :down, id :: integer(), name :: String.t()}
]

Returns an array of the statuses of migrations for the given repo

Doesn't actually run the migrations.

This is a shortcut to

ArangoXEcto.Migrator.migrations(repo, [ArangoXEcto.Migrator.migrations_path(repo)])

migrations(repo, directories, opts \\ [])

(since 2.0.0)
@spec migrations(Ecto.Repo.t(), String.t() | [String.t()], Keyword.t()) :: [
  {:up | :down, id :: integer(), name :: String.t()}
]

Returns an array of the statuses of migrations for the given repo

Doesn't actually run the migrations.

migrations_path(repo, directory \\ "migrations")

(since 2.0.0)
@spec migrations_path(repo :: Ecto.Repo.t(), directory :: String.t()) :: String.t()

Gets the migrations path for a repository.

Accepts a second option allowing to specify the name of / path to the directory for the migrations.

run(repo, direction, opts)

(since 2.0.0)
@spec run(Ecto.Repo.t(), atom(), Keyword.t()) :: [integer()]

Runs migrations for the given repository.

Equivalent to:

ArangoXEcto.Migrator.run(repo, [ArangoXEcto.Migrator.migrations_path(repo)], direction, opts)

See run/4 for more information.

run(repo, migration_source, direction, opts)

(since 2.0.0)
@spec run(
  Ecto.Repo.t(),
  String.t() | [String.t()] | [{integer(), module()}],
  atom(),
  Keyword.t()
) :: [
  integer()
]

Apply migrations to a repository with a given strategy.

The second argument identifies where the migrations are sourced from. A binary representing directory (or a list of binaries representing directories) may be passed, in which case we will load all files following the "#{VERSION}_#{NAME}.exs" schema. The migration_source may also be a list of tuples that identify the version number and migration modules to be run, for example:

ArangoXEcto.Migrator.run(Repo, [{0, MyApp.Migration1}, {1, MyApp.Migration2}, ...], :up, opts)

A strategy (which is one of :all, :step, :to, or :to_exclusive) must be given as an option.

Execution model

In order to run migrations, at least two database connections are necessary. One is used to lock the "_migrations" collection and the other one to effectively run the migrations. This allows multiple nodes to run migrations at the same time, but guarantee that only one of them will effectively migrate the database.

Options

  • :all - runs all available if true

  • :step - runs the specific number of migrations

  • :to - runs all until the supplied version is reached (including the version given in :to)

  • :to_exclusive - runs all until the supplied version is reached (excluding the version given in :to_exclusive)

Plus all other options described in up/4.

start_link(opts)

(since 2.0.0)
@spec start_link(Keyword.t()) :: GenServer.on_start()

Runs migrations as part of your supervision tree.

Options

  • :repos - Required option to tell the migrator which Repo's to migrate. Example: repos: [MyApp.Repo]

  • :skip - Option to skip migrations. Defaults to false.

Plus all other options described in up/4.

See "Example: Running migrations on application startup" for more info.

up(repo, version, module, opts \\ [])

(since 2.0.0)
@spec up(Ecto.Repo.t(), integer(), module(), Keyword.t()) :: :ok | :already_up

Runs an up migration on a given repo.

Options

  • :log - the level to use for logging of migration instructions. Defaults to :info. Can be any of Logger.level/0 values or a boolean. If false, it also avoids logging messages from the database.
  • :log_migrations - the level to use for logging of ArangoDB commands generated by migrations. Can be any of the Logger.level/0 values or a boolean. If false, logging is disabled. If true, uses the configured Repo logger level. Defaults to false
  • :log_migrator - the level to use for logging of ArangoDB commands emitted by the migrator, such as transactions, locks, etc. Can be any of the Logger.level/0 values or a boolean. If false, logging is disabled. If true, uses the configured Repo logger level. Defaults to false
  • :prefix - the prefix to run the migrations on
  • :dynamic_repo - the name of the Repo supervisor process. See Ecto.Repo.put_dynamic_repo/1.
  • :strict_version_order - abort when applying a migration with old timestamp (otherwise it emits a warning)

with_repo(repo, fun, opts \\ [])

(since 2.0.0)
@spec with_repo(repo :: Ecto.Repo.t(), fun :: function(), opts :: Keyword.t()) ::
  {:ok, any(), list()} | {:error, any()}

Ensures the repo is started to perform migration operations.

All applications required to run the repo will be started before with the chosen mode. If the repo has not yet been started, it is manually started, with a :pool_size of 2 (or otherwise passed), before the given function is executed, and the repo is then terminated. If the repo was already started, then the function is directly executed, without terminating the repo afterwards.

Although this function was designed to start repositories for running migrations, it can be used by any code, Mix task, or release tooling that needs to briefly start a repository to perform a certain operation and then terminate.

The repo may also configure a :start_apps_before_migration option which is a list of applications to be started before the migration runs.

It returns {:ok, fun_return, apps}, with all apps that have been started, or {:error, term}.

Options

  • :pool_size - The pool size to start the repo for migrations. Defaults to 2.
  • :mode - The mode to start all applications. Defaults to :permanent.

Examples

{:ok, _, _} =
  ArangoXEcto.Migrator.with_repo(repo, fn repo ->
    ArangoXEcto.Migrator.run(repo, :up, all: true)
  end)