Add typed relation column paths - #70
Conversation
| pub(crate) fn discover(&mut self, expr: &ExprNode) { | ||
| expr.visit_paths(&mut |path| self.require(path, None)); | ||
| } |
There was a problem hiding this comment.
Outer typed paths are replanned inside correlated subqueries
discover unconditionally treats every relation path found in an expression as local to the query being compiled. For example, an outer Assignment::query().where_exists(Record::query().filter(Assignment::first.id.eq(Record::owner_id))) causes the inner Record query to add the Assignment::first join, producing an ON ... = path_assignments.first_id reference even though path_assignments is not in the inner FROM; the outer query also never discovers the path because Exists nodes are skipped. Thus typed outer-path correlations—especially the distinct sibling paths this PR is intended to support—generate invalid SQL instead of resolving against the enclosing path alias.
There was a problem hiding this comment.
Not addressing in this PR. The reported unaliased example works in PostgreSQL; the failure was reproduced with an explicitly aliased outer query using a typed relation path. This is a limitation of the new typed-path API, not a demonstrated regression of an existing main-branch API, so we are deferring it.
|
|
||
| // Keep mutation APIs accepting ordinary Column values. Deref reuses the column | ||
| // methods, while trait implementations preserve paths in functions and operators. | ||
| pub struct RelatedColumn<P, T>(Column<P, T>); |
There was a problem hiding this comment.
Relation columns cannot be used in composite row predicates
RelatedColumn is not accepted by the sealed RowColumns implementations, which only cover tuples of Column. Trait resolution does not dereference tuple elements, so a SELECT filter such as row((Record::owner.id, Record::owner.code)).in_(...) fails to compile even though both ordinary columns and relation paths are supported in filters.
There was a problem hiding this comment.
Not addressing in this PR. Relation-column tuples are not yet supported by RowColumns, but ordinary-column tuples still work and their implementations are unchanged from main. This is a new-API support gap rather than an existing API regression, so we are deferring it.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ce74378. Configure here.
| return qualifier; | ||
| } | ||
| self.relation_alias(table).unwrap_or(table.qualifier()) | ||
| } |
There was a problem hiding this comment.
Aliased self-relations misbind base columns
Medium Severity
column_qualifier matches the base table with full Table equality, including alias, then falls back to a unique relation join on the same table. On an aliased self-relation query, unaliased model columns such as Node::id or Node::label compile against the parent join alias instead of the base row. Join ON keys avoid this by matching name and schema only, so filters and projections can silently use the parent row.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit ce74378. Configure here.
There was a problem hiding this comment.
Deferring under the agreed regression-only scope. The reported parent-column binding reproduces, but on main an unaliased Node column likewise refers to the unaliased joined Node table rather than an explicitly aliased base table. There is no demonstrated loss of a previously supported base-column binding; explicitly aliased Column values identify the base row.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ce7437880a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let (_, alias) = self | ||
| .relation_aliases | ||
| .iter() | ||
| .find(|(existing, _)| existing == path) | ||
| .expect("relation columns require a SELECT query containing their path"); |
There was a problem hiding this comment.
Reject relation paths in mutation predicates before compilation
When a related expression is passed to a mutation predicate, such as Record::update().set(Record::label, "x").filter(Record::owner.enabled.eq(true)).compile() or the equivalent delete, the public API type-checks because mutation filters accept Expr<Option<bool>>, but mutation builders never plan relation aliases. Compilation therefore reaches this expect and panics. Either reject related predicates at the mutation API boundary or compile them without requiring a SELECT join plan.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Deferring under the agreed regression-only scope. Both UPDATE and DELETE compilation panic with the new relation-path predicates, so the unsupported case is real. These typed relation-path expressions did not exist on main, and there is no demonstrated regression of an existing mutation API. Supporting or statically rejecting related mutation predicates is outside this PR’s current scope.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |


BelongsTocolumn paths with automatic left joins. Reuse declared relation joins and eager-loading joins, preserve explicit inner/left semantics, and keep different relation paths distinct through nested queries and decoding.Filter through a relation and load its model with the same join:
Reuse an explicit inner join and automatically add the nested organization join:
Distinguish two relations to the same model:
.with(...)controls loading independently of filtering. Related column expressions preserve outer-join nullability, and paths also work in projections, grouping, and ordering. Customjoin_on(...)conditions retain their own joins and table references.Note
Overview Adds typed
BelongsTocolumn paths, enabling expressions such asRecord::owner.organization.labelin filters, projections, ordering, grouping, and aggregate predicates.Join planning Automatically creates and reuses
LEFT JOINs for referenced paths while coordinating explicit joins and eager loading. Nested, self-referential, same-target, custom-condition, and correlated subquery joins receive correctly scoped aliases.Type safety Preserves column value types, operator constraints, and outer-join nullability throughout relation expressions. Related columns remain read-only and cannot be passed to mutation APIs such as
.set(...).Model generation Emits relation-path metadata for derived models, avoids ambiguous inverse
#[has_many]inference when multiple foreign keys target the same model, and reserves the__dbkit_field prefix for generated internals.