Working With Existing Databases

Copy Markdown View Source

When you're building an Ash application against a database you don't own or control — such as a shared company database, a legacy system, or a third-party service's database — you need a workflow that lets you iterate on your Ash resources without generating migrations. The --fragments and --no-migrations options to mix ash_postgres.gen.resources are designed for exactly this.

The Problem

Normally, Ash resources are the source of truth for your database schema, and migrations are generated from them. But when the database is managed externally:

  • You don't want Ash generating migrations for a schema you don't control
  • The upstream schema may change, and you need to regenerate your resources to match
  • You still want to customize your resources with actions, calculations, validations, and other Ash features — without losing those customizations on regeneration

The Workflow

1. Generate resources with --fragments and --no-migrations

mix ash_postgres.gen.resources MyApp.ExternalDb \
  --tables users,orders,products \
  --no-migrations \
  --fragments

This creates two files per table:

  • The resource file (e.g., lib/my_app/external_db/user.ex) — contains use Ash.Resource, the postgres block, and any actions. This is your file to customize.
  • The fragment file (e.g., lib/my_app/external_db/user/model.ex) — contains the attributes, relationships, and identities introspected from the database. This file is regenerated by the tool.

The resource file will include migrate? false in its postgres block (from --no-migrations), telling Ash not to generate migrations for it:

defmodule MyApp.ExternalDb.User do
  use Ash.Resource,
    domain: MyApp.ExternalDb,
    data_layer: AshPostgres.DataLayer,
    fragments: [MyApp.ExternalDb.User.Model]

  postgres do
    table "users"
    repo MyApp.Repo
    migrate? false
  end
end

The fragment file contains the schema details:

defmodule MyApp.ExternalDb.User.Model do
  use Spark.Dsl.Fragment,
    of: Ash.Resource

  attributes do
    uuid_primary_key :id
    attribute :email, :string, public?: true
    attribute :name, :string, public?: true
    # ...
  end

  relationships do
    has_many :orders, MyApp.ExternalDb.Order
    # ...
  end

  identities do
    identity :unique_email, [:email]
  end
end

2. Customize your resources

Add actions, calculations, validations, changes, and anything else to the resource file. This is your space:

defmodule MyApp.ExternalDb.User do
  use Ash.Resource,
    domain: MyApp.ExternalDb,
    data_layer: AshPostgres.DataLayer,
    fragments: [MyApp.ExternalDb.User.Model]

  actions do
    defaults [:read]

    read :by_email do
      argument :email, :string, allow_nil?: false
      filter expr(email == ^arg(:email))
    end
  end

  calculations do
    calculate :display_name, :string, expr(name || email)
  end

  postgres do
    table "users"
    repo MyApp.Repo
    migrate? false
  end
end

3. Regenerate fragments when the schema changes

When the upstream database schema changes (new columns, new tables, changed relationships), re-run the same command:

mix ash_postgres.gen.resources MyApp.ExternalDb \
  --tables users,orders,products \
  --no-migrations \
  --fragments

Because the resource files already exist, only the fragment files are regenerated. Your customizations in the resource files are untouched.

4. Review the diff

After regeneration, review the changes with git diff to see what changed in the schema. New columns will appear as new attributes, altered relationships will be updated, and so on.

Key Points

  • --fragments splits generated schema details into a separate Model fragment module, keeping your resource file safe from regeneration
  • --no-migrations prevents migration generation and adds migrate? false to the postgres block
  • Fragment files are disposable — they are regenerated from the database each time. Don't put custom code in them.
  • Resource files are yours — once created on the first run, they won't be overwritten by subsequent runs
  • You can also use --skip-tables to exclude tables, --tables to scope to specific schemas (e.g., accounts.), and --extend to apply extensions to generated resources