Skip to content

View DDL: derive INNER vs LEFT OUTER join from reference optionality instead of hardcoding LEFT OUTER #209

Description

@dmealing

Summary

renderJoin (view-ddl-emit.ts:230) hardcodes LEFT OUTER JOIN for every join in a synthesized projection / entity-read-view. There is no way to emit an INNER JOIN, and no join-type attr anywhere. The cardinality (view-spec.ts:12) and referenceHolder (view-spec.ts:18) flags only feed aggregate-inflation warnings and flip the ON-clause direction — neither selects the join type.

Why this is a correctness gap (not just cosmetics)

A join over a required (NOT NULL) reference is semantically INNER. Lowering it to LEFT OUTER silently returns extra rows: base rows whose match is absent appear with NULL joined columns, where an INNER would exclude them. When a projection/read-view stands in for — or reproduces — an existing INNER-join view, the generated view returns a different result set. This is invisible to verify --db (the view body still "matches" what the tool emits) and only surfaces as wrong data downstream.

-- required FK: every order has a customer → the legacy view is INNER
SELECT o.*, c.name FROM orders o JOIN customers c ON o.customer_id = c.id;
-- today's emitter produces LEFT OUTER → orders with a dangling customer_id leak in with NULL name

Proposal (model-first: the join type falls out of the model)

Derive the join type from the reference's optionality, which the metadata already knows:

  • a required identity.reference / non-null FK ⇒ INNER JOIN
  • a nullable reference ⇒ LEFT OUTER JOIN (today's behavior)

Plus an optional explicit override (@join: inner|left) on the origin.*/@via for the rare case the derivation is wrong. The default should be principled, not a blanket LEFT OUTER.

Backend-agnostic

The inner/outer distinction maps cleanly: Mongo $lookup + $unwind preserveNullAndEmptyArrays (false = inner, true = outer); an inner-vs-outer denormalization choice for a search index. It is a lowering detail of one semantic (a modeled reference), consistent with the #195 backend-agnostic lens.

Scope

JoinNode carries a resolved join-type (computed from reference optionality, overridable); renderJoin honors it; golden-DDL fixtures for the required (INNER) and nullable (LEFT OUTER) cases. Small, self-contained, and removes a silent result-set change when adopting existing INNER-join views.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions