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.
Summary
renderJoin(view-ddl-emit.ts:230) hardcodesLEFT OUTER JOINfor every join in a synthesized projection / entity-read-view. There is no way to emit anINNER JOIN, and no join-type attr anywhere. Thecardinality(view-spec.ts:12) andreferenceHolder(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 OUTERsilently 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 toverify --db(the view body still "matches" what the tool emits) and only surfaces as wrong data downstream.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:
identity.reference/ non-null FK ⇒INNER JOINLEFT OUTER JOIN(today's behavior)Plus an optional explicit override (
@join: inner|left) on theorigin.*/@viafor the rare case the derivation is wrong. The default should be principled, not a blanketLEFT OUTER.Backend-agnostic
The inner/outer distinction maps cleanly: Mongo
$lookup+$unwindpreserveNullAndEmptyArrays(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
JoinNodecarries a resolved join-type (computed from reference optionality, overridable);renderJoinhonors 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.