Models & Migrations
Models are how your bot talks to its database. Grace models are SQLModel classes with an ActiveRecord-style API layered on top by grace.model.Model.
Generating a Model
Generate your first model, in column_name:Type pairs:
grace generate model Task name:String description:String done:Boolean
This command automatically:
- Creates a new database migration in
db/alembic/versions/ - Generates a new model class in
bot/models/task.py - Registers the model for use with the built-in ORM utilities
Example output:
[2025-10-18 21:02:21] development generate model_generator INFO Generating model 'Task'
[2025-10-18 21:02:21] development __init__ migration INFO Context impl SQLiteImpl.
[2025-10-18 21:02:21] development __init__ migration INFO Will assume non-transactional DDL.
[2025-10-18 21:02:21] development _compare_tables compare INFO Detected added table 'task'
Generating /db/alembic/versions/99e6d0cf0aec_create_task.py ... done
Note
Column types are SQLAlchemy-style names, not Python types: String, Text, Integer, Float, Boolean. Each is mapped to its corresponding Python annotation (str, str, int, float, bool) in the generated model. Any other type name raises a ValidationError.
Note
grace generate model requires a database — if your project was created with --no-database, it warns and does nothing until you run grace generate database (see Database Management).
The generated bot/models/task.py:
from grace.model import Field, Model
class Task(Model):
id: int | None = Field(default=None, primary_key=True)
name: str
description: str
done: bool
You don't need to pass table=True yourself — Grace's model metaclass sets it automatically for every Model subclass.
Database Migrations
Grace uses Alembic to manage schema migrations. When you generate a model (or run grace generate migration), Grace autogenerates a migration by diffing your models against the current database schema, writing the result to db/alembic/versions/.
Example migration file:
"""Create Task
Revision ID: 99e6d0cf0aec
Revises:
Create Date: 2025-10-18 21:02:21.978903
"""
import sqlmodel
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = '99e6d0cf0aec'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table('task',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('done', sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
def downgrade() -> None:
op.drop_table('task')
Directory layout:
db/
├── alembic/
│ ├── versions/
│ │ └── 99e6d0cf0aec_create_task.py
│ ├── env.py
│ └── script.py.mako
Writing a Migration Manually
If you change a model by hand (add a column, rename a field, etc.) without regenerating it, create a migration for just that change:
grace generate migration "Add priority to Task"
Running Migrations
Apply pending migrations to your database:
grace db up
[2025-10-18 21:05:42] development db_up INFO Applying migration 99e6d0cf0aec_create_task.py
[2025-10-18 21:05:42] development db_up INFO Upgrade successful.
Roll back with grace db down. Both commands accept an optional revision (default head) — see Database Management.
Querying Models
Every Model subclass gets a fluent, chainable Query API directly on the class:
from bot.models.task import Task
# Create
task = Task.create(name="Write docs", description="Finish the guide", done=False)
# Find
Task.find(1) # by primary key
Task.find_by(name="Write docs") # first match by equality
# Query, filter, and order
Task.where(Task.done == False).order_by(Task.id.desc()).limit(10).all()
Task.not_(done=True).all()
Task.with_("comments").where(Task.done == True).all()
# Update / delete / reload
task.update(done=True)
task.reload()
task.delete()
# Aggregate
Task.count()
See the Model reference for the full list of query and persistence methods.
Next Steps
Now let's expose these tasks to Discord users with Extensions.