Skip to content

Commit feca190

Browse files
authored
improvement: --dev codegen flag (#154)
1 parent 1ce17b8 commit feca190

File tree

12 files changed

+396
-29
lines changed

12 files changed

+396
-29
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ test_snapshots_path
2828
test/test.db
2929
test/test.db-shm
3030
test/test.db-wal
31+
test/dev_test.db
32+
test/dev_test.db-shm
33+
test/dev_test.db-wal

config/config.exs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,15 @@ if Mix.env() == :test do
2828
pool: Ecto.Adapters.SQL.Sandbox,
2929
migration_primary_key: [name: :id, type: :binary_id]
3030

31+
config :ash_sqlite, AshSqlite.DevTestRepo,
32+
database: Path.join(__DIR__, "../test/dev_test.db"),
33+
pool_size: 1,
34+
migration_lock: false,
35+
pool: Ecto.Adapters.SQL.Sandbox,
36+
migration_primary_key: [name: :id, type: :binary_id]
37+
3138
config :ash_sqlite,
32-
ecto_repos: [AshSqlite.TestRepo],
39+
ecto_repos: [AshSqlite.TestRepo, AshSqlite.DevTestRepo],
3340
ash_domains: [
3441
AshSqlite.Test.Domain
3542
]

documentation/topics/development/migrations-and-tasks.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,30 @@ Ash comes with its own tasks, and AshSqlite exposes lower level tasks that you c
66

77
## Basic Workflow
88

9-
- Make resource changes
10-
- Run `mix ash.codegen --name add_a_combobulator` to generate migrations and resource snapshots
11-
- Run `mix ash.migrate` to run those migrations
9+
### Development Workflow (Recommended)
1210

13-
For more information on generating migrations, run `mix help ash_sqlite.generate_migrations` (the underlying task that is called by `mix ash.migrate`)
11+
For development iterations, use the dev workflow to avoid naming migrations prematurely:
12+
13+
1. Make resource changes
14+
2. Run `mix ash.codegen --dev` to generate dev migrations
15+
3. Review the migrations and run `mix ash.migrate` to run them
16+
4. Continue making changes and running `mix ash.codegen --dev` as needed
17+
5. When your feature is complete, run `mix ash.codegen add_feature_name` to generate final named migrations (this will remove dev migrations and squash them)
18+
6. Review the migrations and run `mix ash.migrate` to run them
19+
20+
### Traditional Migration Generation
21+
22+
For single-step changes or when you know the final feature name:
23+
24+
1. Make resource changes
25+
2. Run `mix ash.codegen --name add_a_combobulator` to generate migrations and resource snapshots
26+
3. Run `mix ash.migrate` to run those migrations
27+
28+
> **Tip**: The dev workflow (`--dev` flag) is preferred during development as it allows you to iterate without thinking of migration names and provides better development ergonomics.
29+
30+
> **Warning**: Always review migrations before applying them to ensure they are correct and safe.
31+
32+
For more information on generating migrations, run `mix help ash_sqlite.generate_migrations` (the underlying task that is called by `mix ash.codegen`)
1433

1534
### Regenerating Migrations
1635

documentation/tutorials/getting-started-with-ash-sqlite.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ Then we will generate database migrations. This is one of the many ways that Ash
170170
mix ash_sqlite.generate_migrations --name add_tickets_and_representatives
171171
```
172172

173+
> **Development Tip**: For iterative development, you can use `mix ash_sqlite.generate_migrations --dev` to create dev migrations without needing to name them immediately. When you're ready to finalize your changes, run the command with a proper name to consolidate all dev migrations into a single, well-named migration.
174+
173175
If you are unfamiliar with database migrations, it is a good idea to get a rough idea of what they are and how they work. See the links at the bottom of this guide for more. A rough overview of how migrations work is that each time you need to make changes to your database, they are saved as small, reproducible scripts that can be applied in order. This is necessary both for clean deploys as well as working with multiple developers making changes to the structure of a single database.
174176

175177
Typically, you need to write these by hand. AshSqlite, however, will store snapshots each time you run the command to generate migrations and will figure out what migrations need to be created.

lib/migration_generator/migration_generator.ex

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ defmodule AshSqlite.MigrationGenerator do
1717
format: true,
1818
dry_run: false,
1919
check: false,
20+
dev: false,
21+
auto_name: false,
2022
drop_columns: false
2123

2224
def generate(domains, opts \\ []) do
@@ -213,7 +215,7 @@ defmodule AshSqlite.MigrationGenerator do
213215
migration_file =
214216
opts
215217
|> migration_path(repo)
216-
|> Path.join(migration_name <> ".exs")
218+
|> Path.join(migration_name <> "#{if opts.dev, do: "_dev"}.exs")
217219

218220
sanitized_module =
219221
module
@@ -332,6 +334,22 @@ defmodule AshSqlite.MigrationGenerator do
332334
:ok
333335

334336
operations ->
337+
dev_migrations = get_dev_migrations(opts, repo)
338+
339+
if !opts.dev and dev_migrations != [] do
340+
if opts.check do
341+
Mix.shell().error("""
342+
Generated migrations are from dev mode.
343+
344+
Generate migrations without `--dev` flag.
345+
""")
346+
347+
exit({:shutdown, 1})
348+
else
349+
remove_dev_migrations_and_snapshots(dev_migrations, repo, opts, snapshots)
350+
end
351+
end
352+
335353
if opts.check do
336354
IO.puts("""
337355
Migrations would have been generated, but the --check flag was provided.
@@ -353,6 +371,46 @@ defmodule AshSqlite.MigrationGenerator do
353371
end)
354372
end
355373

374+
defp get_dev_migrations(opts, repo) do
375+
opts
376+
|> migration_path(repo)
377+
|> File.ls()
378+
|> case do
379+
{:error, _error} -> []
380+
{:ok, migrations} -> Enum.filter(migrations, &String.contains?(&1, "_dev.exs"))
381+
end
382+
end
383+
384+
defp remove_dev_migrations_and_snapshots(dev_migrations, repo, opts, snapshots) do
385+
# Remove dev migration files
386+
Enum.each(dev_migrations, fn migration_name ->
387+
opts
388+
|> migration_path(repo)
389+
|> Path.join(migration_name)
390+
|> File.rm!()
391+
end)
392+
393+
# Remove dev snapshots
394+
Enum.each(snapshots, fn snapshot ->
395+
snapshot_folder =
396+
opts
397+
|> snapshot_path(snapshot.repo)
398+
|> Path.join(repo_name(snapshot.repo))
399+
|> Path.join(snapshot.table)
400+
401+
if File.exists?(snapshot_folder) do
402+
snapshot_folder
403+
|> File.ls!()
404+
|> Enum.filter(&String.contains?(&1, "_dev.json"))
405+
|> Enum.each(fn snapshot_name ->
406+
snapshot_folder
407+
|> Path.join(snapshot_name)
408+
|> File.rm!()
409+
end)
410+
end
411+
end)
412+
end
413+
356414
defp add_order_to_operations({snapshot, operations}) do
357415
operations_with_order = Enum.map(operations, &add_order_to_operation(&1, snapshot.attributes))
358416

@@ -712,6 +770,8 @@ defmodule AshSqlite.MigrationGenerator do
712770
defp write_migration!({up, down}, repo, opts) do
713771
migration_path = migration_path(opts, repo)
714772

773+
require_name!(opts)
774+
715775
{migration_name, last_part} =
716776
if opts.name do
717777
{"#{timestamp(true)}_#{opts.name}", "#{opts.name}"}
@@ -742,7 +802,7 @@ defmodule AshSqlite.MigrationGenerator do
742802

743803
migration_file =
744804
migration_path
745-
|> Path.join(migration_name <> ".exs")
805+
|> Path.join(migration_name <> "#{if opts.dev, do: "_dev"}.exs")
746806

747807
module_name =
748808
Module.concat([repo, Migrations, Macro.camelize(last_part)])
@@ -815,6 +875,20 @@ defmodule AshSqlite.MigrationGenerator do
815875
end
816876
end
817877

878+
defp require_name!(opts) do
879+
if !opts.name && !opts.dry_run && !opts.check && !opts.dev && !opts.auto_name do
880+
raise """
881+
Name must be provided when generating migrations, unless `--dry-run` or `--check` or `--dev` is also provided.
882+
883+
Please provide a name. for example:
884+
885+
mix ash_sqlite.generate_migrations <name> ...args
886+
"""
887+
end
888+
889+
:ok
890+
end
891+
818892
defp add_line_numbers(contents) do
819893
lines = String.split(contents, "\n")
820894

@@ -837,15 +911,16 @@ defmodule AshSqlite.MigrationGenerator do
837911
|> snapshot_path(snapshot.repo)
838912
|> Path.join(repo_name)
839913

840-
snapshot_file = Path.join(snapshot_folder, "#{snapshot.table}/#{timestamp()}.json")
914+
dev = if opts.dev, do: "_dev"
915+
snapshot_file = Path.join(snapshot_folder, "#{snapshot.table}/#{timestamp()}#{dev}.json")
841916

842917
File.mkdir_p(Path.dirname(snapshot_file))
843918
File.write!(snapshot_file, snapshot_binary, [])
844919

845-
old_snapshot_folder = Path.join(snapshot_folder, "#{snapshot.table}.json")
920+
old_snapshot_folder = Path.join(snapshot_folder, "#{snapshot.table}#{dev}.json")
846921

847922
if File.exists?(old_snapshot_folder) do
848-
new_snapshot_folder = Path.join(snapshot_folder, "#{snapshot.table}/initial.json")
923+
new_snapshot_folder = Path.join(snapshot_folder, "#{snapshot.table}/initial#{dev}.json")
849924
File.rename(old_snapshot_folder, new_snapshot_folder)
850925
end
851926
end)

lib/mix/tasks/ash_sqlite.generate_migrations.ex

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ defmodule Mix.Tasks.AshSqlite.GenerateMigrations do
2020
* `no-format` - files that are created will not be formatted with the code formatter
2121
* `dry-run` - no files are created, instead the new migration is printed
2222
* `check` - no files are created, returns an exit(1) code if the current snapshots and resources don't fit
23+
* `dev` - dev files are created (see Development Workflow section below)
2324
2425
#### Snapshots
2526
@@ -58,6 +59,25 @@ defmodule Mix.Tasks.AshSqlite.GenerateMigrations do
5859
Non-function default values will be dumped to their native type and inspected. This may not work for some types,
5960
and may require manual intervention/patches to the migration generator code.
6061
62+
#### Development Workflow
63+
64+
The `--dev` flag enables a development-focused migration workflow that allows you to iterate
65+
on resource changes without committing to migration names prematurely:
66+
67+
1. Make resource changes
68+
2. Run `mix ash_sqlite.generate_migrations --dev` to generate dev migrations
69+
- Creates migration files with `_dev.exs` suffix
70+
- Creates snapshot files with `_dev.json` suffix
71+
- No migration name required
72+
3. Continue making changes and running `--dev` as needed
73+
4. When ready, run `mix ash_sqlite.generate_migrations my_feature_name` to:
74+
- Remove all dev migrations and snapshots
75+
- Generate final named migrations that consolidate all changes
76+
- Create clean snapshots
77+
78+
This workflow prevents migration history pollution during development while maintaining
79+
the ability to generate clean, well-named migrations for production.
80+
6181
#### Identities
6282
6383
Identities will cause the migration generator to generate unique constraints. If multiple
@@ -79,6 +99,8 @@ defmodule Mix.Tasks.AshSqlite.GenerateMigrations do
7999
no_format: :boolean,
80100
dry_run: :boolean,
81101
check: :boolean,
102+
dev: :boolean,
103+
auto_name: :boolean,
82104
drop_columns: :boolean
83105
]
84106
)

priv/dev_test_repo/migrations/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)