Migrations, Seeders & Factories

Craft provides database schema version control (Migrations) and database seeding tools (Seeders and Factories) to set up and manage database states across different environments.


Migrations

Migrations are stored in database/migrations/ and contain two functions: up() to create/modify tables, and down() to reverse the change.

Example Migration

from craft.migrations import Migration, Schema

def up():
    Schema.create_table("posts", lambda t: (
        t.id(),
        t.string("title").unique(),
        t.text("body"),
        t.boolean("published").default(False),
        t.foreign_id("user_id").constrained().cascade_on_delete(),
        t.timestamps(),
    ))

def down():
    Schema.drop_table("posts")

Table Column Types

The table builder object (t) supports the following data type definitions:

Running Migrations

Manage database schemas incrementally using the dev.py CLI:

# Run all pending forward migrations
python dev.py migrate

# Preview queries without modifying the database
python dev.py migrate --pretend

# View migration history and applied batches
python dev.py migrate:status

Data persistence: Rollback and reset operations are refused in every environment. All schema evolution must be forward-only. See Database Safety.


Seeders

Seeders populate your database with initial reference records and system data. Seeders should be written idempotently so they can run safely without purging or duplicating data. They inherit from craft.seeding.Seeder and define a run method:

Example Seeder

from craft.seeding import Seeder
from app.Models.User import User

class DatabaseSeeder(Seeder):
    def run(self):
        # 1. Idempotent check or creation
        if not User.query().where("email", "[email protected]").exists():
            User.create({
                "name": "Admin User",
                "email": "[email protected]",
                "password": "hashed-secret-password"
            })

Execute seeders by running:

python dev.py db seed

Seeding once (SEED_ONCE)

A seeder that checks for a row before inserting it is idempotent only while that row stays as it was seeded. Once someone edits the row, the check no longer finds it and the next run inserts a second copy. A seeder that must never run twice declares SEED_ONCE:

from craft.seeding import Seeder


class PlatformCatalogSeeder(Seeder):
    SEED_ONCE = "platform_catalog"   # a stable key; or True to key on the module and class name

    def run(self):
        ...

db seed, release:sync and Seeder.call() look the key up in the seed_ledger table before running the seeder, skip it when the key is there, and record the key after a successful run. The ledger is append-only.


Factories

Factories define the default attribute layout for a model. definition() returns a plain dict — there is no bundled fake-data library, so use the standard library, or add faker to your own project and call it yourself.

Example Factory

import random
import uuid

from craft.factories import Factory
from app.Models.Post import Post

class PostFactory(Factory):
    model = Post

    def definition(self):
        return {
            "title": f"Post {uuid.uuid4().hex[:8]}",
            "body": " ".join(random.choices(WORDS, k=30)),
            "published": True,
            "user_id": 1,
        }

Every value must be unique where the schema demands it — a factory that returns a constant for a unique column fails on the second record.

Use factories within seeders or unit tests to generate multiple records quickly:

# Create and save a single post to the database
post = PostFactory.new().create()

# Generate a list of 5 unsaved post model instances
posts = PostFactory.new().count(5).make()

PostgreSQL schema features

Partial, expression, GIN/GiST/HNSW and CONCURRENTLY indexes, CHECK and EXCLUDE constraints, declarative partitioning, managed extensions and row-level security policies are covered in PostgreSQL.

Materialized views that block a migration (PostgreSQL only)

PostgreSQL refuses to alter a column a materialized view depends on, so a database whose views were created outside the migrations cannot migrate. Opt in to have the migrator step around them:

MIGRATION_SUSPEND_MATERIALIZED_VIEWS=true

With the flag on (framework.MIGRATION_SUSPEND_MATERIALIZED_VIEWS, default false), migrate drops every materialized view before the pending batch and recreates each one afterwards with its indexes, retrying in passes so a view that depends on another comes back in the right order. The views are restored even when a migration in the batch fails; migrate --pretend suspends nothing.