Relates to: #207 (projection/read-view row-scope @filter, shipped in 0.17.0 / 7.9.0) · Kind: feature
Summary
A projection's object-level @filter (#207) can compare a declared column to a value
({ status: { eq: "active" } }), but not to another declared column. Add a column-to-column
comparator — e.g. { colA: { eqField: "colB" } } — that lowers to WHERE <aliasA>.<colA> = <aliasB>.<colB>.
Scope it (like the rest of #207) to the projection's own output columns.
Why (concrete adopter need)
Adopting a legacy schema onto declarative origin.* views, two legacy views are a join diamond: one
entity is reached by two paths, and the second path is expressed as an equality between two already-joined
columns (not a join edge). The origin join tree is a trie — it cannot represent the diamond as a join — but
both sides of the equality are already SELECTed as output columns, so an outer-WHERE column-to-column
compare expresses the constraint exactly, with no new join.
-- current_league_season_view (paraphrased): league + its CURRENT season
FROM league L JOIN season S ON S.league_id = L.id
WHERE L.current_season_id = S.id -- both L.current_season_id and S.id are output columns
-- league_member_account_view: rank rows scoped to the league's current season
... WHERE L.current_season_id = R.season_id -- both output columns
Today these are the only two of ~25 views we cannot model declaratively purely because of this one gap; every
other predicate is a value compare (eq/in/isNull) that #207 already handles. We keep them on the @sql
escape (ADR-0043) meanwhile.
Proposed shape
Extend the attr.filter field-clause grammar with a column reference operator alongside eq/ne/gt/…:
eqField (and optionally neField/gtField/…) takes the name of another declared projection field.
- Resolution mirrors
resolveViewFilter (extract-view-spec.ts): look the referenced field up in
selectSpec.columns, emit <sourceAlias>.<sourceColumn> for it — the same machinery that already resolves
the left-hand side. The loader gate (validateProjectionFilter) validates the referenced field exists and is
a passthrough/computed column (not an aggregate), exactly as it does for the filtered field.
- Lowering (
view-ddl-emit.ts renderFilterCond): a cmp clause whose value is a column ref renders
lhs = rhs instead of lhs = <literal>.
Non-goals / notes
- Not a join. It is a residual WHERE predicate over columns the view already projects.
- Does not help genuinely un-projected filter columns (a separate, already-noted ask) or composite/multi-column
join edges (those stay @sql).
Impact
Converts the two diamond views above exactly (row-set identical to the legacy @sql), taking a real-world
25-view adoption from 5 @sql down to 3 (the remaining 3 being a true row-multiplying reverse has-many ×2 and
a 3-column composite join — legitimate @sql territory).
Relates to: #207 (projection/read-view row-scope
@filter, shipped in 0.17.0 / 7.9.0) · Kind: featureSummary
A projection's object-level
@filter(#207) can compare a declared column to a value(
{ status: { eq: "active" } }), but not to another declared column. Add a column-to-columncomparator — e.g.
{ colA: { eqField: "colB" } }— that lowers toWHERE <aliasA>.<colA> = <aliasB>.<colB>.Scope it (like the rest of #207) to the projection's own output columns.
Why (concrete adopter need)
Adopting a legacy schema onto declarative
origin.*views, two legacy views are a join diamond: oneentity is reached by two paths, and the second path is expressed as an equality between two already-joined
columns (not a join edge). The origin join tree is a trie — it cannot represent the diamond as a join — but
both sides of the equality are already SELECTed as output columns, so an outer-
WHEREcolumn-to-columncompare expresses the constraint exactly, with no new join.
Today these are the only two of ~25 views we cannot model declaratively purely because of this one gap; every
other predicate is a value compare (
eq/in/isNull) that #207 already handles. We keep them on the@sqlescape (ADR-0043) meanwhile.
Proposed shape
Extend the
attr.filterfield-clause grammar with a column reference operator alongsideeq/ne/gt/…:{ "object.projection": { "name": "CurrentLeagueSeason", "@filter": { "current_season_id": { "eqField": "season_id" } }, // WHERE l.current_season_id = s.id "children": [ /* … origin.passthrough columns incl. current_season_id and season_id … */ ] } }eqField(and optionallyneField/gtField/…) takes the name of another declared projection field.resolveViewFilter(extract-view-spec.ts): look the referenced field up inselectSpec.columns, emit<sourceAlias>.<sourceColumn>for it — the same machinery that already resolvesthe left-hand side. The loader gate (
validateProjectionFilter) validates the referenced field exists and isa passthrough/computed column (not an aggregate), exactly as it does for the filtered field.
view-ddl-emit.tsrenderFilterCond): acmpclause whose value is a column ref renderslhs = rhsinstead oflhs = <literal>.Non-goals / notes
join edges (those stay
@sql).Impact
Converts the two diamond views above exactly (row-set identical to the legacy
@sql), taking a real-world25-view adoption from 5
@sqldown to 3 (the remaining 3 being a true row-multiplying reverse has-many ×2 anda 3-column composite join — legitimate
@sqlterritory).