Skip to content

Projection/read-view @filter: column-to-column comparator (eqField) — extend the shipped #207 filter for join-diamond views #222

Description

@dmealing

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/…:

{ "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 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions