Skip to main content

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