Table
Relation Paths & Dot Notation
Column names support deep dot-notation for relations, aggregates, pivots, and morphs. The Core Relation AST parser automatically determines JOINs, eager loads, and subqueries.
On this page
Column names support deep dot-notation for relations, aggregates, pivots, and morphs. The Core Relation AST parser automatically determines JOINs, eager loads, and subqueries.
Simple Relation
TextColumn::make('author.name') // belongsTo or hasOneTextColumn::make('category.title')
Nested Relations
TextColumn::make('author.country.name') // 3 levels deepTextColumn::make('order.customer.company.name') // 4 levels deep
Aggregates
TextColumn::make('orders.count') // withCountTextColumn::make('items.sum.amount') // withSumTextColumn::make('ratings.avg.score') // withAvgTextColumn::make('bids.min.amount') // withMinTextColumn::make('bids.max.amount') // withMax
Pivot Data
TextColumn::make('tags.pivot.sort_order')TextColumn::make('roles.pivot.assigned_at')->dateTime()
Morph Relations
TextColumn::make('commentable.title') // polymorphic
Sorting & Filtering a Singular Relation
A singular relation — belongsTo, hasOne, or hasOneThrough — is resolved with
a LEFT JOIN, so both sorting and filtering happen in SQL against the joined
table: no whereHas subquery, no in-memory filtering. Nested singular chains join
segment by segment (company.country.name → two chained joins), and a
hasOneThrough expands to two joins itself (base → intermediate → far).
// Sorting: mark the relation column sortable.TextColumn::make('company.name')->sortable(); // Filtering: SelectFilter supports the dot path directly in its name.SelectFilter::make('company.name') ->options(['Acme' => 'Acme', 'Globex' => 'Globex']); // Equivalent, if you'd rather the filter's state key stay flat ('company')// while still targeting the relation column:SelectFilter::make('company') ->column('company.name') ->options(['Acme' => 'Acme', 'Globex' => 'Globex']);
Both compile to the same join:
select "users".*from "users"left join "companies" as "users_company" on "users"."company_id" = "users_company"."id"where "users_company"."name" = ? -- filterorder by "users_company"."name" asc -- sort
Only singular relations (belongsTo, hasOne, hasOneThrough) are joined this
way. To-many relations — hasMany, belongsToMany, hasManyThrough, morphMany
— and morph targets are eager-loaded for display and are not sortable/filterable
through the join (a join would multiply parent rows).
Scopes & relation constraints (incl. soft deletes)
The joined side matches what Eloquent's own relation query returns. A relation
that carries any constraint — the model's global scopes (SoftDeletes,
tenancy, a published/active flag, anything from addGlobalScope()) or
constraints declared on the relation method itself
(belongsTo(...)->where('active', true)) — is joined as a scoped subquery:
left join ( select * from "companies" where "companies"."deleted_at" is null -- SoftDeletes global scope and "companies"."active" = ? -- ->where(...) on the relation) as "users_company" on "users"."company_id" = "users_company"."id"
The LEFT JOIN stays a LEFT JOIN: a parent whose related row is scoped away
still appears, with the related value treated as absent (sorts/filters as NULL).
For a hasOneThrough, both the intermediate and far models are scoped. A relation
with no scopes or constraints keeps a plain direct-table join.
Limits: relation-method constraints are honoured for
belongsTo/hasOnebut not for ahasOneThrough(only its models' global scopes apply). A constraint must be self-contained — one that correlates to the parent row (whereColumn('companies.x', 'users.y')) can't be expressed as a subquery.morphOneis not join-scoped (it is eager-loaded for display).
How It Works
RelationPath::parse('author.country.name')produces[RelationSegment('author'), RelationSegment('country'), ColumnSegment('name')]QueryPlannerbuilds aRelationGraphdetermining optimal access strategy- Singular
belongsTo/hasOne/hasOneThroughrelations → LEFT JOIN (enables sorting and filtering;hasOneThroughuses two joins via its intermediate table) - HasMany/belongsToMany/hasManyThrough/morphMany → eager load (display only)
- Aggregates →
withCount()/withSum()subqueries - Pivot → intermediate table JOIN