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) — containsuse Ash.Resource, thepostgresblock, 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
endThe 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
end2. 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
end3. 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
--fragmentssplits generated schema details into a separateModelfragment module, keeping your resource file safe from regeneration--no-migrationsprevents migration generation and addsmigrate? falseto thepostgresblock- 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-tablesto exclude tables,--tablesto scope to specific schemas (e.g.,accounts.), and--extendto apply extensions to generated resources