Populating a database with known rows: the Seeder for code-defined data, the FixtureLoader for JSON and JSONL files, ordering, and making seeds re-runnable.
Two ways to put known rows into a database: in code, or in files.
from sillo.record.helpers import Seeder, FixtureLoaderSeeder
Section titled “Seeder”seeder = Seeder(database)seeder.seed(User, [ {"email": "ada@example.com", "username": "ada", "is_staff": True}, {"email": "bob@example.com", "username": "bob"},])seeder.seed(Post, [ {"title": "Hello", "body": "…", "author_id": 1},])
created = await seeder.run()seed() registers rows and returns the seeder, so calls chain. Nothing is
written until run(), which returns the number of rows created.
Order is registration order, User rows before Post rows above, which is
what lets the post reference an author.
Each row goes through create(), so model events,
casts and validation
all apply. Seeded data is real data, written the same way the application would
write it.
FixtureLoader
Section titled “FixtureLoader”For data that belongs in files rather than in Python: reference data, a demo dataset, something a non-developer maintains.
fixtures/ 01_users.json 02_posts.jsonl[ { "email": "ada@example.com", "username": "ada" }, { "email": "bob@example.com", "username": "bob" }]{"title": "Hello", "body": "…", "author_id": 1}{"title": "World", "body": "…", "author_id": 2}loader = FixtureLoader("fixtures/")count = await loader.load_all().json holds an array of objects; .jsonl holds one object per line. Use
JSONL for anything large. It streams, and a diff shows one changed row rather
than a reindented file.
Ordering
Section titled “Ordering”load_all() reads files in sorted filename order, which is why the example
numbers them. Fixtures that reference each other need the referenced rows
first, and 01_/02_ is how you say so.
Files whose suffix is not .json or .jsonl are skipped, so a README in the
directory is harmless.
Naming
Section titled “Naming”The filename stem resolves to a model through Tortoise’s registry. When the name does not match, map it:
loader = FixtureLoader("fixtures/", models={"01_users": User, "02_posts": Post})Which you will need whenever you use numeric prefixes, since 01_users is not
a model name.
One file
Section titled “One file”await loader.load("01_users")Without the extension.
Where seeding belongs
Section titled “Where seeding belongs”A console command. It runs when you ask, in the environment you ask, and
sillo puts it beside every other command:
from sillo.console import Command, Flag
class Seed(Command): name = "db:seed" help = "Load the development dataset"
arguments = [Flag("force", short="f", help="Run outside development")]
async def handle(self): from app.config import config
if config.app_env != "local" and not self.flag("force"): self.fail("Refusing to seed outside development. Use --force.")
loader = FixtureLoader("fixtures/") count = await loader.load_all() self.success(f"Loaded {count} rows.")app.add_command(Seed)sillo db:seedThe environment guard is worth the three lines. Seeding is a write, and the command that populates your development database looks exactly like the one that would overwrite production.
Not in a migration
Section titled “Not in a migration”A migration is applied once per environment, in order, and is expected to be deterministic. Demo data is none of those things: you want to re-run it, you want it in development and not in production, and you want to change it without writing a new migration.
Reference data that the application genuinely requires (currencies, country codes, a default role) is the exception, and belongs in a migration precisely because it must exist everywhere the schema does.
Making seeds re-runnable
Section titled “Making seeds re-runnable”create() raises on the second run when a unique constraint is involved. To
make a seed idempotent, upsert:
for row in rows: await User.upsert(**row, conflict_fields=["email"])Or clear first, deliberately and with the same guard as above:
await Post.all().delete()await User.all().delete()An idempotent seed is worth the effort. A development database gets reset far more often than you expect.
See also
Section titled “See also”- Factories: for test data, generated per test.
- Bulk operations: for large volumes.
- Migrations: for data that must exist everywhere.