Model Annotations
Every Model instance can have an $annotations field containing computed columns, aggregations, or joined fields that aren't part of the Model definition.
How Annotations Work
Use the annotate() method to add non-model columns to your query results:
// annotate(sqlFunction, column, alias)
const user = await User.query()
.annotate("count", "id", "total")
.first();
console.log(user.$annotations.total); // number
Annotation Signatures
// SQL function with column
.annotate("count", "id", "userCount") // COUNT(id) AS userCount
.annotate("max", "age", "maxAge") // MAX(age) AS maxAge
.annotate("avg", "salary", "avgSalary") // AVG(salary) AS avgSalary
// Column alias only
.annotate("id", "userId") // id AS userId
Combining with Select
When using annotate alone without select, model fields are omitted:
// Only annotation - model fields not included
const r1 = await User.query()
.annotate('count', '*', 'total')
.first();
// Result: { $annotations: { total: number } }
// With select('*') - includes model fields + annotation
const r2 = await User.query()
.select('*')
.annotate('count', '*', 'total')
.first();
// Result: User & { $annotations: { total: number } }
Removing Annotations
Remove the $annotations property from results:
const user = await User.query()
.removeAnnotations()
.first();
// Result: User (no $annotations)
Type Safety
Annotations are type-safe in ModelQueryBuilder:
const user = await User.query()
.annotate("count", "id", "total")
.first();
// TypeScript knows $annotations.total exists
console.log(user.$annotations.total);
note
selectRaw can add columns but they won't be typed. Always use annotate for type-safe computed columns.
Next: Model Hooks