Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/adr/033-async-seeder-stub.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 033. Generated seeders have an async run

Date: 2026-10-21

## Problem

The `seed` command writes table seeders from `create_seed.stub`. The stub has a synchronous `def run(self)` and docstrings. `Seeder.call`, `run_database_seed` and `run_specific_seed` all `await ...run()`, so every generated seeder fails when it is seeded:

```
TypeError: 'NoneType' object can't be awaited
```

## Alternatives

1. Make `Seeder` accept both forms by awaiting the result only when it is awaitable. This would keep blocking seeders working, but seeders write through the async ORM, so a synchronous `run` cannot await queries anyway.
2. Make the stub match the async seeder API.

## Decision

Option 2. The stub generates `async def run(self) -> None` with no docstrings. The import, `from fastapi_startkit.masoniteorm.seeders import Seeder`, is already correct and stays. `Seeder` is unchanged.

## Validation

- A new test generates `PostTableSeeder` with `MakeSeedCommand`, checks the import, the `async def run` and that there are no docstrings, and imports it through `Seeder.run_specific_seed`, which awaits `run()`.
- The test fails on the old stub.
- Running the old stub's output through `run_specific_seed` raises the `TypeError` above.
1 change: 1 addition & 0 deletions docs/adr/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ Read this index first, then open the records relevant to the change. Before impl
| [025](025-model-lifecycle-events.md) | 2026-10-20 | Model lifecycle events, automatic timestamps and datetime casts | `save()` fires `saving → creating/updating → query → created/updated → saved`. Observer changes are persisted, and `updating` fires only when the model is dirty. A new `Model.delete()` fires `deleting → DELETE → deleted`. `observe_events` is async and awaits async handlers. Mass query-builder writes fire no events. Laravel-style auto timestamps: `created_at`/`updated_at` are filled on create unless supplied, `updated_at` moves on update, and `__timestamps__ = False` disables both. `DateCast.set` returns a UTC datetime; `Connection.prepare_binding` passes it unchanged to Postgres/MySQL, and SQLite converts it to a naive UTC string. Postgres registers an asyncpg `timestamp` codec that stores aware values as naive UTC; `timestamptz` keeps asyncpg's codec. `Annotated[T, Cast]` (a `BaseCast` subclass or instance) overrides a column's cast, and `Attribute.__casts__` is removed. |
| [026](026-schema-column-change.md) | 2026-10-20 | Altering existing columns with `.change()` | `.change()` fully redefines the column, the way Laravel does. MySQL (`MODIFY`) and SQLite (table rebuild) were already correct. Postgres now emits `TYPE ... USING` casts, correct lengths, quoted defaults or `DROP DEFAULT`, `SET`/`DROP NOT NULL`, and replaces the enum check constraint. MSSQL drops the current default constraint (looked up in `sys.default_constraints`), runs one `ALTER COLUMN` per column, then adds `[DF_<table>_<col>]`. Identifiers are bracket- or double-quote-escaped and the MSSQL drop target is built server-side with `QUOTENAME`; changing a Postgres serial column drops its `nextval` default. |
| [032](032-migrate-seed-option.md) | 2026-10-21 | db:migrate:fresh and db:migrate:refresh --seed run seeders in the same event loop | `--seed` called the unregistered `seed:run` through Cleo `call` (which also drops the first argument) inside a running event loop, where `db:seed` would call `asyncio.run` again. The seeder selection moves into async `DBSeedCommand.run_seeders`, which `db:seed`, `db:migrate:fresh` and `db:migrate:refresh` all await. `db:migrate:refresh` is now registered. |
| [033](033-async-seeder-stub.md) | 2026-10-21 | Generated seeders have an async run | `create_seed.stub` generated a synchronous `run` with docstrings, but `Seeder` awaits `run()`, so every generated table seeder raised `TypeError`. The stub now generates `async def run(self) -> None` with no docstrings; `Seeder` is unchanged. |
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
"""__SEEDER_NAME__ Seeder."""

from fastapi_startkit.masoniteorm.seeders import Seeder


class __SEEDER_NAME__(Seeder):
def run(self):
"""Run the database seeds."""
async def run(self) -> None:
pass
25 changes: 25 additions & 0 deletions fastapi_startkit/tests/masoniteorm/commands/test_make_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import importlib.util
import os
import shutil
import sys
import tempfile
import unittest

Expand All @@ -12,6 +13,7 @@
from fastapi_startkit.masoniteorm.commands.MakeObserverCommand import MakeObserverCommand
from fastapi_startkit.masoniteorm.commands.MakeSeedCommand import MakeSeedCommand
from fastapi_startkit.masoniteorm.migrations.Migration import Migration
from fastapi_startkit.masoniteorm.seeders import Seeder

from .fixtures.app import create_app, DB_PATH

Expand Down Expand Up @@ -236,6 +238,29 @@ def test_reports_when_seed_already_exists(self):
output = tester.io.fetch_output()
self.assertIn("already exists.", output)

def test_generated_seeder_runs_through_seeding_path(self):
os.makedirs("generated_seeds/seeders")
self._tester(MakeSeedCommand).execute("Post --directory generated_seeds/seeders")

content = open(os.path.join("generated_seeds/seeders", "post_table_seeder.py")).read()
self.assertIn("from fastapi_startkit.masoniteorm.seeders import Seeder", content)
self.assertIn("async def run(self) -> None:", content)
self.assertNotIn('"""', content)

sys.path.insert(0, self._tmp_dir)
self.addCleanup(sys.path.remove, self._tmp_dir)
self.addCleanup(self._forget_modules, "generated_seeds")

seeder = Seeder(seed_path="generated_seeds/seeders")
asyncio.run(seeder.run_specific_seed("post_table_seeder.PostTableSeeder"))

self.assertEqual([seed.__name__ for seed in seeder.ran_seeds], ["PostTableSeeder"])

@staticmethod
def _forget_modules(package):
for name in [name for name in sys.modules if name == package or name.startswith(f"{package}.")]:
del sys.modules[name]


class TestMakeObserverCommand(_TempCwdTestCase):
# make:observer emits a plain observer class with no schema side effects, so
Expand Down
Loading