Computed Fields
Computed fields automatically calculate their values from other fields.
Basic Computed Field
Section titled “Basic Computed Field”from fullfinity.engine.base import *
class Order(Model): quantity = Integer(default=1) unit_price = Monetary(default=0.0)
# Computed field total = Monetary( calculate="_compute_total", store=True )
@Model.calculate("quantity", "unit_price") async def _compute_total(self): self.total = self.quantity * self.unit_priceThe @calculate Decorator
Section titled “The @calculate Decorator”Declares which fields trigger recomputation:
@Model.calculate("quantity", "unit_price")async def _compute_total(self): self.total = self.quantity * self.unit_priceWhen quantity or unit_price changes, _compute_total is called automatically.
Stored vs Non-Stored
Section titled “Stored vs Non-Stored”Stored (store=True)
Section titled “Stored (store=True)”- Saved in database
- Computed on save
- Can be filtered/sorted
- Better for frequently accessed data
total = Monetary(calculate="_compute_total", store=True)Non-Stored (store=False)
Section titled “Non-Stored (store=False)”- Computed on-the-fly
- Not in database
- Cannot be filtered
- Better for derived display values
- Only hydrated on root-level records by default
display_name = Char( max_length=255, calculate="get_display_name", store=False):::warning Hydration Depth
Non-stored computed fields are only automatically hydrated on root-level query results. For nested records (via prefetch_related), you must explicitly request them using with_fields().
# Root level: margin is hydrated ✓products = await Product.filter().all()print(product.margin) # Works
# Nested: margin is NOT hydrated ✗order = await Order.filter(id=1).prefetch_related("lines__product").first()print(order.lines[0].product.margin) # Raises AttributeError!
# Fix: Use with_fields to explicitly request nested calculated fieldsorder = await Order.filter(id=1).with_fields("lines__product__margin").first()print(order.lines[0].product.margin) # Works ✓See Querying Data - Select Specific Fields for more details. :::
Chained Dependencies
Section titled “Chained Dependencies”Computed fields can depend on other computed fields:
class Order(Model): quantity = Integer(default=1) unit_price = Monetary(default=0.0) discount_percent = Float(default=0.0) tax_rate = Float(default=10.0)
# Subtotal depends on quantity and price subtotal = Monetary(calculate="_compute_subtotal", store=True)
# Discount depends on subtotal discount_amount = Monetary(calculate="_compute_discount", store=True)
# Total depends on subtotal, discount, and tax total = Monetary(calculate="_compute_total", store=True)
@Model.calculate("quantity", "unit_price") async def _compute_subtotal(self): self.subtotal = self.quantity * self.unit_price
@Model.calculate("subtotal", "discount_percent") async def _compute_discount(self): self.discount_amount = self.subtotal * (self.discount_percent / 100)
@Model.calculate("subtotal", "discount_amount", "tax_rate") async def _compute_total(self): after_discount = self.subtotal - self.discount_amount self.total = after_discount * (1 + self.tax_rate / 100)Depending on Relations
Section titled “Depending on Relations”Compute from related records:
class Invoice(Model): customer = ManyToOne("Contact", related_name="invoices")
# Depends on lines (OneToMany) total = Monetary(calculate="_compute_total", store=True)
@Model.calculate("lines", "lines__amount") async def _compute_total(self): await self.fetch_related("lines") self.total = sum(line.amount for line in self.lines)
class InvoiceLine(Model): invoice = ManyToOne("Invoice", related_name="lines") quantity = Integer(default=1) unit_price = Monetary() amount = Monetary(calculate="_compute_amount", store=True)
@Model.calculate("quantity", "unit_price") async def _compute_amount(self): self.amount = self.quantity * self.unit_priceRelated Fields
Section titled “Related Fields”Simpler alternative for single-value lookups:
class Contact(Model): company = ManyToOne("Company", related_name="contacts")
# Auto-computed from relation company_name = Char( max_length=255, related_field="company__name", store=False )
country_code = Char( max_length=10, related_field="company__country__code", store=False )Display Name
Section titled “Display Name”Common pattern for record display:
class Contact(Model): first_name = Char(max_length=100) last_name = Char(max_length=100) email = Char(max_length=255)
async def get_display_name(self): if self.first_name and self.last_name: self.display_name = f"{self.first_name} {self.last_name}" elif self.email: self.display_name = self.email else: self.display_name = f"Contact #{self.id}"Count/Aggregate Fields
Section titled “Count/Aggregate Fields”class TaskList(Model): name = Char(max_length=255)
task_count = Integer(calculate="_compute_task_count", store=False) completed_count = Integer(calculate="_compute_task_count", store=False) progress = Float(calculate="_compute_task_count", store=False)
@Model.calculate("tasks", "tasks__status") async def _compute_task_count(self): await self.fetch_related("tasks") self.task_count = len(self.tasks) if self.tasks else 0 self.completed_count = sum( 1 for t in self.tasks if t.status == "Done" ) if self.tasks else 0 self.progress = ( self.completed_count / self.task_count * 100 if self.task_count > 0 else 0 )Conditional Computation
Section titled “Conditional Computation”class Product(Model): type = Selection(choices=["Physical", "Digital"], default="Physical") weight = Float(default=0.0) file_size = Integer(default=0)
shipping_required = Boolean(calculate="_compute_shipping", store=True)
@Model.calculate("type") async def _compute_shipping(self): self.shipping_required = self.type == "Physical"Best Practices
Section titled “Best Practices”1. Always Use the @Model.calculate Decorator
Section titled “1. Always Use the @Model.calculate Decorator”Every computed field method must have the @Model.calculate decorator. Without it, the system won’t know to call your method:
# Good - decorator present@Model.calculate("quantity", "unit_price")async def compute_total(self): self.total = self.quantity * self.unit_price
# Bad - missing decorator (raises error at startup!)async def compute_total(self): self.total = self.quantity * self.unit_priceFor fields with no dependencies (computed from external sources), use empty parentheses:
@Model.calculate()async def compute_is_current(self): # Query another model Website = get_model("Website") website = await Website.filter(theme__id__eq=self.id).first() self.is_current = website is not None2. Declare All Dependencies
Section titled “2. Declare All Dependencies”# Good@Model.calculate("quantity", "unit_price", "discount")async def _compute_total(self): self.total = (self.quantity * self.unit_price) - self.discount
# Bad - missing dependency@Model.calculate("quantity", "unit_price") # Missing "discount"!async def _compute_total(self): self.total = (self.quantity * self.unit_price) - self.discount3. Keep Computations Fast
Section titled “3. Keep Computations Fast”# Good - simple calculation@Model.calculate("lines")async def _compute_line_count(self): await self.fetch_related("lines") self.line_count = len(self.lines)
# Bad - expensive operation in computed field@Model.calculate("lines")async def _compute_statistics(self): # Don't do complex operations here await self.fetch_related("lines__product__category__parent") # ... complex aggregation4. Use store=False for Display-Only
Section titled “4. Use store=False for Display-Only”# Good - not stored, just for displaydisplay_name = Char(calculate="get_display_name", store=False)
# Consider storing if you need to filter/sortsearchable_name = Char(calculate="get_searchable_name", store=True, index=True)5. Handle None Values
Section titled “5. Handle None Values”@Model.calculate("quantity", "unit_price")async def _compute_total(self): qty = self.quantity or 0 price = self.unit_price or 0 self.total = qty * priceNext Steps
Section titled “Next Steps”- Querying Data - Filter and search records
- Defining Models - Model structure