Skip to content

Commit a605666

Browse files
os-zhuangclaude
andcommitted
docs(adr): ADR-0071 dataset semantic-layer depth (multi-hop joins + matrix pivot)
Grounded design for the two ADR-0021 follow-ups: multi-hop relationship joins (include accepts dotted paths; compiler chains; strategy emits chained LEFT JOINs with per-hop RLS, behind a depth cap) and matrix pivoting (renderer pivots the existing rows/columns into a 2-D grid; query/spec unchanged). Phased P1 (matrix, objectui-only) → P2 (multi-hop framework) → P3 (objectui catalog/UI). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e8956b4 commit a605666

1 file changed

Lines changed: 100 additions & 0 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# ADR-0071: Dataset semantic-layer depth — multi-hop joins and matrix pivoting
2+
3+
## Status
4+
5+
Proposed
6+
7+
## Context
8+
9+
ADR-0021 established the `dataset` semantic layer: a base `object`, relationships
10+
to `include` (joins **derived** from the object graph — no hand-written `ON`
11+
clauses), and named `dimensions` / `measures` that presentations bind by name.
12+
RLS/tenant scoping is enforced per joined object at runtime (D-C, fail-closed).
13+
14+
Two capability gaps remain, both flagged as follow-ups in ADR-0021:
15+
16+
1. **Single-hop joins only.** `Dataset.include` is `string[]` of relationship
17+
**names** (lookup / master_detail fields on the *base* object), and a
18+
dimension/measure `field` uses a one-dot `relationship.field` path.
19+
`native-sql-strategy.qualifyAndRegisterJoin` splits on the **first** dot:
20+
`account.region``LEFT JOIN account` + column `"account"."region"`. A
21+
two-hop path like `account.owner.region` is parsed as alias=`account`,
22+
column=`owner.region` → invalid SQL. You cannot group/aggregate by a field
23+
two relationships away (e.g. `opportunity → account → owner → region`).
24+
25+
2. **Matrix / "across" is flattened, not pivoted.** `Report` already declares
26+
`rows` (down) and `columns` (across) for `type: 'matrix'`, and the dataset
27+
query can group by both. But the dataset report renderer shows a **flat**
28+
table (rows ∪ columns as combined groupings) — it does not pivot the
29+
`columns` dimension into a 2-D grid (ADR-0021 D2 follow-up). A true matrix
30+
(rows × distinct-column-values × measure cells) isn't rendered.
31+
32+
## Decision
33+
34+
### A. Multi-hop joins (framework: spec + compiler + strategy + RLS)
35+
36+
- **Spec.** `Dataset.include` accepts relationship **paths** — dotted chains of
37+
lookup/master_detail field names from the base object (`'account'`,
38+
`'account.owner'`). A dimension/measure `field` may reference any field
39+
reachable along a **declared** path (`account.owner.region`). A depth cap
40+
(default **3 hops**) bounds join count/perf; undeclared paths are still
41+
rejected (D-C unchanged — only declared relationships are joinable).
42+
- **Compiler** (`dataset-compiler`). Expand each `include` path into the ordered
43+
join chain; `cube.joins` is keyed by the full path alias (`account.owner`) and
44+
carries `{ parentAlias, fkField, targetObject }` so the chain is reconstructable.
45+
- **Strategy** (`native-sql-strategy.qualifyAndRegisterJoin`). For `a.b.c`,
46+
register the chain (`base → a` on alias `a`; `a → b` on alias `a.b`) and
47+
qualify the column as `"a.b"."c"`. Emit `LEFT JOIN`s in dependency order. Base
48+
columns stay qualified with the base table (the fix from objectui#…/framework
49+
joined-column work) so shared column names remain unambiguous across all hops.
50+
- **RLS (D-C, fail-closed).** Apply the tenant read-scope to **every** object in
51+
the chain, not just the first hop. The strategy already scopes per joined
52+
alias; generalize the loop to each hop's target object.
53+
- **UI** (objectui). `useDatasetFieldCatalog` lazily expands a relationship's own
54+
relationships (one more level on demand) so the field picker offers
55+
`account.owner.region`; the include editor shows/edits paths; the existing
56+
`missingRelationship` author-time validation generalizes to paths.
57+
58+
### B. Matrix pivot (mostly objectui renderer)
59+
60+
- **Query unchanged.** Group by `rows ∪ columns` — the across dimension is just
61+
another `groupBy`.
62+
- **Renderer** (`DatasetReportRenderer`). When `type === 'matrix'` and `columns`
63+
is non-empty, **pivot** the flat result into a 2-D grid: distinct `columns`
64+
values become column headers, `rows` go down the side, measure(s) fill the
65+
cells. Cap distinct column values (default **50**) with a "+N more — refine
66+
with a filter" notice to avoid column explosion.
67+
- **Spec unchanged** (`rows` / `columns` already exist on `Report`).
68+
69+
## Consequences
70+
71+
- New capability: analytics two-plus relationships deep, and true matrix reports.
72+
- Cost/risk: multi-hop joins multiply `LEFT JOIN`s (perf) and widen the RLS
73+
surface (each hop scoped) — bounded by the depth cap + declared-path allowlist.
74+
Matrix column cardinality is bounded by the column cap.
75+
- **Backward compatible.** Single-hop `include` and flat rows keep working
76+
unchanged; the depth cap and "flat unless `matrix` + `columns`" defaults
77+
preserve current behaviour.
78+
79+
## Phasing
80+
81+
- **P1 — matrix pivot (objectui-only, lowest risk).** Render the matrix grid from
82+
the existing `rows`/`columns`. No spec/query change. Ships independently.
83+
- **P2 — multi-hop (framework).** Spec path support + compiler chain + strategy
84+
chained joins + per-hop RLS, behind the depth cap. Gated by the ADR-0021
85+
reconciliation harness (old-vs-new numbers).
86+
- **P3 — objectui catalog/UI.** Multi-hop field picker + include-path editor +
87+
path-aware author-time validation.
88+
89+
## Alternatives considered
90+
91+
- **Arbitrary join predicates / raw SQL** — rejected (ADR-0021 D-C: joins are
92+
derived from the object graph, no `ON` clauses; keeps datasets reviewable and
93+
RLS-safe).
94+
- **Materialized / precomputed pivots** — out of scope; query-time pivot is
95+
sufficient at expected cardinalities (bounded by the column cap).
96+
97+
## Related
98+
99+
- ADR-0021 (analytics dataset semantic layer) — the foundation; D2 / join
100+
follow-ups this ADR addresses.

0 commit comments

Comments
 (0)