Models¶
Active Record models live in app/models/ and inherit IModel. Class annotations are the source of truth for columns — migrations and seeders are generated from them.
Reads and writes are awaitable: find / all / save / delete / update / Query.get / Query.first. where / order_by / limit only build the query.
Define a model¶
future make:model Stock
from future.interfaces.IModel import IModel
class Stock(IModel):
# __table__ = "stocks" # optional; default is tableized class name (Stock → stocks)
# __connection__ = "default" # name from DATABASES / Database()
id: str
name: str
symbol: str
instrument_id: str
price: float | None
Register connections in Configuration / Database before using the model at runtime or in CLI.
Per-model connection¶
__connection__ selects which entry in DATABASES the model uses. Default is "default", which resolves to the name in DATABASES["default"] (e.g. "sqlite"). Set it to any other registered key to put a model on a different store:
from future.interfaces.IModel import IModel
class Stock(IModel):
__connection__ = "default" # → DATABASES["default"] → e.g. sqlite
class Trade(IModel):
__connection__ = "mysql" # → DATABASES["mysql"]
class Event(IModel):
__connection__ = "postgres" # → DATABASES["postgres"]
future make:migration copies the model’s __connection__ onto the migration class. future migrate / rollback apply each migration on that connection (migration history is tracked per connection). Seeders call the model’s own save() / queries, so they follow the model connection automatically.
Register every named connection in DATABASES before use — see Database.
Annotations drive generators¶
After annotations are set:
future make:migration Stock # → database/migrations/…_create_stocks.py from annotations
future make:seed Stock # → database/seeds/StockSeeder.py from annotations
# or every annotated model under app/models/:
future make:migrations
future make:seeds # skips seed files that already exist
The first migration generated for a model is a create snapshot. After changing the model annotations, run make:migration again: Future detects the earlier snapshot and generates an update migration. Its up() contains the new model shape and its down() contains the preceding shape, so both migrate and rollback remain model-driven.
When adding a required column to a table that already contains rows, Future backfills it from the model class default. If the model has no default, it uses the type's zero value ("", 0, 0.0, False, or the Unix epoch). Optional fields are backfilled with NULL.
Then apply / run:
future migrate
future seed # all seeders
future seed StockSeeder # one class name
Prefer either make:migration Stock or make:migrations for the same model in one session — not both (duplicate files).
Generated migration (from the Stock annotations above):
from future.migrations import Migration, Schema
class CreateStocks(Migration):
__connection__ = "default"
async def up(self):
async with Schema.create("stocks") as table:
table.id()
table.string("name")
table.string("symbol")
table.string("instrument_id")
table.float("price").nullable()
async def down(self):
await Schema.drop("stocks")
Generated seeder uses the model fields (Faker stubs):
from faker import Faker
from future.seeder import Seeder
from app.models.Stock import Stock
class StockSeeder(Seeder):
async def run(self):
fake = Faker()
for _ in range(10):
await Stock(
id=fake.uuid4(),
name=fake.company(),
symbol=fake.unique.lexify(text="????").upper(),
instrument_id=str(fake.random_int(10000, 99999)),
price=float(fake.pyfloat(min_value=1, max_value=100)),
).save()
Edit generated files if you need indexes, extras, or richer seed data. Re-running generators does not replace hand-edited migrations; seeds skip existing files on make:seeds.
For example, adding market: str | None to Stock and generating again produces:
class UpdateStocks(Migration):
__connection__ = "default"
async def up(self):
async with Schema.update("stocks") as table:
table.id()
table.string("name")
table.string("symbol")
table.string("instrument_id")
table.float("price").nullable()
table.string("market").nullable()
async def down(self):
async with Schema.update("stocks") as table:
table.id()
table.string("name")
table.string("symbol")
table.string("instrument_id")
table.float("price").nullable()
CRUD¶
stock = Stock(id="1", name="Equinor", symbol="EQNR", instrument_id="16105067", price=250.0)
await stock.save()
stock = await Stock.find("1")
stocks = await Stock.all()
stock.price = 251.0
await stock.save()
await stock.delete()
Query¶
where / order_by / limit return a Query — await .get() or .first() to hit the DB:
await Stock.where("symbol", "EQNR").first()
await Stock.where("price", ">", 100).order_by("price", "desc").get()
await Stock.where("name", "like", "%Equinor%").limit(20).get()
In a controller:
async def get_stocks(self) -> Response:
ticker = self.request.query.get("ticker")
if ticker:
stocks = await Stock.where("symbol", ticker).get()
else:
stocks = await Stock.all()
return self.response.json([s.to_dict() for s in stocks], status=200)
See CLI for the full command list and Database for drivers and connection registration.