Skip to content

#447 — intra-model column edges (pass-through/rename) as SpanRole::Column - #460

Merged
cmbays merged 2 commits into
mainfrom
domain-447-column-edges
Jun 22, 2026
Merged

cmbays merged 2 commits into
mainfrom
domain-447-column-edges

Conversation

@cmbays

@cmbays cmbays commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Closes #447

Summary

CLL-2 of the source-map-spine epic (#442): the projection-provenance pass. The CTE engine now walks every body's select.projection on the SAME single parse (the cute-dbt#40 retain-don't-recompute pattern — no second parse) and writes POD column-lineage facts back to the domain CteGraph. B (downstream impact) and C (intra-model upstream trace) fall out as traversals of the one edge set.

What landed

  • PODs (verbatim, src/domain/cte.rs) — ColumnScope (Intra/Cross), ColumnRef, ColumnEdgeKind (Recce 5-way: pass_through/renamed/derived/source/join_key, #[non_exhaustive]), ColumnEdgeConfidence (resolved/ambiguous/opaque), ColumnEdge. Additive CteGraph::column_edges + column_spans with builders/accessors (skip_serializing_if = Vec::is_empty → byte-stable when empty).
  • SpanRole::Column { node_id, column } — the v2-reserved slot landed in src/domain/source_map.rs; ColumnSpan facts fold into SpanRole::Column entries, each a sub-range of the owning CteBody (contains_range-verified), projected through CodeMapPayload.column_spans.
  • The pass (src/adapters/cte_engine.rs) — Tier-1 MVP = pass-through (c.email AS email) + rename (c.email AS contact_email) only (the Identifier / aliased CompoundIdentifier cases the existing walker reaches). Confidence tracks SQL explicitness: Resolved (qualified / single-source via sole_relation_leaf), Ambiguous (unqualified multi-source → fan out to every candidate, never dropped), Opaque (SELECT * / q.* → virtual *→* edge). coalesce(...) emits NO Derived edge — honest absence, deferred to CLL-3.
  • Render — column_lineage.edges populated from graph.column_edges() (additive, skip-if-empty).

The AST walk stays in the adapter (sqlparser forbidden in domain/ by tests/domain_clean_arch.rs); it writes POD facts back.

TDD-first

10 engine tests (pass-through, rename, single-source unqualified, ambiguous fan-out, opaque star, derived-absence, downstream reverse index, upstream trace dead-ends at the ref boundary, column-span sub-range, canonical vocab) + domain serde round-trips + source-map Column-fold tests — written before the implementation.

Gates (all green)

cargo fmt --check · cargo clippy --all-targets --locked -D warnings · cargo nextest run (2438 pass) · cargo test --test bdd · RUSTDOCFLAGS=-D warnings cargo doc --no-deps --locked · cargo deny check · headless_zero_egress -- --ignored (12 pass, real Chromium file://) · headless_toggle -- --ignored (125 pass) · domain hexagonal-purity · source-map-completeness (509 node checks, 0 violations).

Golden impact (consequence)

Report goldens (jaffle-shop, playground, diff-showcase, macro-heavy?, seed-showcase, prdiff-minidag, comments-showcase) + explore tests.html pages regenerated — additive column_lineage.edges + code_map.column_spans. The diff-showcase-findings.json envelope is unchanged (findings unaffected). Both arms byte-verified against the renderer; the committed jaffle-shop/playground goldens now visibly carry the column edges (dogfood).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added column-level lineage tracking to visualize how data columns flow, transform, and propagate across models through direct references, pass-throughs, and renames.
    • Enhanced report metadata with detailed column source spans and lineage edges to provide comprehensive data provenance visibility.
  • Documentation

    • Updated example reports with expanded model metadata reflecting improved lineage tracking capabilities.

…s SpanRole::Column

CLL-2: the projection-provenance pass. The CTE engine now walks every
body's `select.projection` on the SAME single parse (the #40
retain-don't-recompute pattern — no second parse) and writes POD
column-lineage facts back to the domain `CteGraph`:

- `ColumnEdge { from_col, to_col, kind, confidence }` (+ `ColumnScope`,
  `ColumnRef`, `ColumnEdgeKind` (Recce 5-way), `ColumnEdgeConfidence`),
  additive `CteGraph::column_edges` with builder + accessor.
- `ColumnSpan` facts folded into the v2-reserved `SpanRole::Column
  { node_id, column }` source-map entries — each a sub-range of the
  owning `CteBody` entry (contains_range-verified), projected through
  `CodeMapPayload.column_spans`.

Tier-1 MVP = pass-through (`c.email AS email`) + rename
(`c.email AS contact_email`) only — the Identifier / aliased
CompoundIdentifier cases the existing walker reaches. Confidence tracks
SQL explicitness (never-a-false-claim): Resolved (qualified /
single-source via `sole_relation_leaf`), Ambiguous (unqualified
multi-source → fan out to every candidate, never dropped), Opaque
(`SELECT *` / `q.*` → a virtual `*→*` edge, badged). Expression
provenance (`coalesce(...)`) emits NO Derived edge — honest absence,
deferred to CLL-3.

Downstream impact (B) and intra-model upstream trace (C) fall out as
traversals of the one edge set. The render layer projects the edges into
`column_lineage.edges` (additive, skip-if-empty so older payloads stay
byte-stable). Report + explore goldens regenerated (additive
`column_lineage.edges` + `code_map.column_spans`); source-map-completeness
gate green; zero-egress + headless E2E green against the regen.

Closes #447

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199AmBCec5kyVEEF1Qd7TSF
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 22, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cmbays, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 30 minutes and 12 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3b92908d-93f2-4d1b-ad78-01ad608e9d74

📥 Commits

Reviewing files that changed from the base of the PR and between 59cb1e2 and 5f9ef7b.

⛔ Files ignored due to path filters (1)
  • tests/snapshots/render_integration__rendered_chrome_jaffle_shop.snap is excluded by !**/*.snap
📒 Files selected for processing (8)
  • examples/comments-showcase-report.html
  • examples/diff-showcase-report.html
  • examples/explore-macro/tests.html
  • examples/explore/tests.html
  • examples/jaffle-shop-report.html
  • examples/playground-report.html
  • examples/prdiff-minidag-report.html
  • src/adapters/cte_engine.rs
📝 Walkthrough

Walkthrough

This PR implements CLL-2 (Tier-1 intra-model column lineage): new domain PODs (ColumnEdge, ColumnSpan, ColumnRef, ColumnScope, ColumnEdgeKind, ColumnEdgeConfidence) are added to CteGraph; a projection-provenance pass is added to the CTE engine; SpanRole::Column is activated in SourceMap; the renderer emits column_spans and edges in the JSON payload; and example HTML golden files are regenerated.

Changes

CLL-2 Intra-model column lineage

Layer / File(s) Summary
Column lineage domain PODs and CteGraph fields
src/domain/cte.rs, src/domain/mod.rs
Defines ColumnScope, ColumnRef, ColumnEdgeKind, ColumnEdgeConfidence, ColumnEdge, and ColumnSpan PODs; extends CteGraph with column_edges/column_spans fields, builder/getter methods, constructor initialization, serde omit-when-empty config; re-exports all new types from the domain module.
SourceMap: SpanRole::Column and column_spans accessor
src/domain/source_map.rs
Activates SpanRole::Column { node_id, column } variant; extends SourceMap::from_cte_graph to fold graph.column_spans() into SpanRole::Column entries; adds SourceMap::column_spans() returning a BTreeMap<(node_id, column), SourceSpan>; adds tests for sub-range containment and empty-span behavior.
CTE engine: projection-provenance pass
src/adapters/cte_engine.rs
Wires collect_column_lineage into build_graph and attaches edges/spans to CteGraph; implements collect_body_column_lineage, resolve_projection_item, direct-ref helpers, confidence-fan-out (push_edges), Opaque star emission (push_star_edge), and span materialization (push_span); adds tests for pass-through/rename, unqualified resolution, ambiguous fan-out, Opaque star, span containment, no-Derived-from-expression, and canonical JSON vocabulary.
Renderer: column_spans and edges serialization
src/adapters/render.rs
Adds column_spans to CodeMapPayload (string-keyed BTreeMap, skipped when empty) populated from sm.column_spans(); adds edges to ColumnLineagePayload populated from graph.column_edges(); updates build_model_payload so column_lineage is None only when both context and edges are empty.
Example HTML golden files
examples/comments-showcase-report.html, examples/explore-seed/tests.html, examples/jaffle-shop-report.html, examples/prdiff-minidag-report.html, examples/seed-showcase-report.html
Regenerated embedded cute-dbt-data JSON payloads to include the new column_spans and column_lineage.edges fields produced by the updated renderer.

Sequence Diagram(s)

sequenceDiagram
  participant build_graph as build_graph (cte_engine.rs)
  participant collect_column_lineage
  participant collect_body_column_lineage
  participant CteGraph
  participant SourceMap
  participant CodeMapPayload as render.rs

  build_graph->>collect_column_lineage: compiled_sql, ctes, terminal query
  loop per CTE body + terminal body
    collect_column_lineage->>collect_body_column_lineage: body, node_id
    collect_body_column_lineage-->>collect_column_lineage: ColumnEdge[] + ColumnSpan[]
  end
  collect_column_lineage-->>build_graph: ColumnLineage { edges, spans }
  build_graph->>CteGraph: .with_column_edges(edges).with_column_spans(spans)
  CteGraph->>SourceMap: from_cte_graph → SpanRole::Column entries
  SourceMap->>CodeMapPayload: column_spans() → BTreeMap<node∫column, SourceSpan>
  CodeMapPayload-->>render.rs: column_spans field (JSON, skipped when empty)
  CteGraph-->>render.rs: column_edges() → ColumnLineagePayload.edges (JSON)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #447 (Intra-model column edges as SpanRole::Column): This PR directly implements all acceptance criteria from #447: ColumnScope/ColumnRef/ColumnEdge PODs verbatim, CteGraph.column_edges additive field, pass-through/rename/single-source/ambiguous/Opaque-star/no-Derived rules, SpanRole::Column sub-range containment, and canonical serde vocabulary for goldens.

Possibly related PRs

  • breezy-bays-labs/cute-dbt#38: This PR extends the renderer payload types (CodeMapPayload, ColumnLineagePayload) that were originally introduced in the referenced PR.
  • breezy-bays-labs/cute-dbt#453: This PR builds directly on the SourceMap/CodeMapPayload foundation from the referenced PR by adding SpanRole::Column entries and SourceMap::column_spans() into the same spine.

Poem

🐰 Hop, hop, a column knows its way,
From source CTE to (final select) today.
PassThrough, Renamed — each edge tagged with care,
Opaque stars sparkle in the lineage air.
The rabbit computes what no SELECT can hide,
With spans sub-ranged and confidence supplied! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly matches the main change: implementing intra-model column edges as SpanRole::Column entries, which is the core objective of the PR.
Linked Issues check ✅ Passed All acceptance criteria from issue #447 are satisfied: POD types defined verbatim, CteGraph.column_edges field added with builder/accessor pattern, pass-through/rename at Resolved confidence, single-source unqualified via sole_relation_leaf, ambiguous fan-out to all candidates, opaque edges for SELECT *, honest absence of Derived edges, SpanRole::Column entries as sub-ranges, and comprehensive TDD test coverage with 10 engine tests.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to #447 objectives: CTE engine projection-provenance pass, domain POD definitions, source map integration, and rendering payload updates. Example file updates (HTML reports) regenerate golden data with new additive fields, all within scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch domain-447-column-edges

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

📄 Rendered report preview

All golden examples regenerated cleanly.

🟡 Golden examples

Committed to examples/ and byte-identity gated — the canonical reports contributors and consumers browse. Stable across PRs.

Report View Download
playground-report.html ▶ Open ↗ ⬇ Download
prdiff-minidag-report.html ▶ Open ↗ ⬇ Download
diff-showcase-report.html ▶ Open ↗ ⬇ Download
jaffle-shop-report.html ▶ Open ↗ ⬇ Download
macro-heavy-report.html ▶ Open ↗ ⬇ Download
seed-showcase-report.html ▶ Open ↗ ⬇ Download

🐶 Live dogfood preview

This PR doesn't touch dbt-project/, so there's no live dogfood preview.

🧭 Explore preview

The two-page cute-dbt explore explorer — dag.html (model lineage) + tests.html (unit-test viewer). Same golden/live split as the report.

🟡 Golden explore

The committed examples/explore/ playground golden (the full synthetic playground manifest). Byte-identity gated in Example report check. Stable across PRs.

Page View Download
explore/dag.html ▶ Open ↗ ⬇ Download
explore/tests.html ▶ Open ↗ ⬇ Download

🐶 Live explore

This PR doesn't touch dbt-project/, so there's no live explore preview.

▶ Open ↗ opens the report or explorer in your browser in one
click — published to this repo's GitHub Pages under
/pr-460/.
⬇ Download fetches the same self-contained HTML as a workflow
artifact (auth-gated; works fully offline). Either way the report
makes zero external resource requests.

The Pages preview may take ~1 min to update after this comment
posts. On PRs from forks the Open link is unavailable (read-only
token) — use Download.

Alternative: GitHub CLI
# gh CLI >= 2.63 extracts into ./report-preview-playground/.
gh run download 27938668114 -R breezy-bays-labs/cute-dbt -n report-preview-playground
open report-preview-playground/playground-report.html

Posted by report-preview.yml for 5f9ef7b6fba28a92ba152c63277646d63dc6bdc6. Affordance only — never blocks merge.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements Tier-1 intra-model column lineage (CLL-2) in the CTE engine. It introduces new domain models for column edges, scopes, and spans, and implements a projection-provenance pass to statically resolve pass-through and renamed columns during the single-parse AST walk. These column edges and spans are folded into the source map and rendered in the output payload, enabling column-level tracking and UI anchors. Comprehensive unit tests have been added to verify the lineage resolution, serialization, and span containment. There are no review comments provided, and the implementation appears solid, so I have no additional feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/jaffle-shop-report.html`:
- Line 2583: The baseline field in the cute-dbt-data JSON embedded in the script
tag contains a repo-relative path "tests/fixtures/jaffle-shop-baseline.json"
instead of a simple filename. Change the baseline value to use only the simple
filename "jaffle-shop-baseline.json" by removing the directory path prefix, so
the JSON object at the start of the cute-dbt-data script contains just the
filename without any repo-relative path references.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3792b15f-162f-4aa6-9fbb-915ac10a6c3c

📥 Commits

Reviewing files that changed from the base of the PR and between 914f185 and 59cb1e2.

⛔ Files ignored due to path filters (1)
  • tests/snapshots/render_integration__rendered_chrome_jaffle_shop.snap is excluded by !**/*.snap
📒 Files selected for processing (14)
  • examples/comments-showcase-report.html
  • examples/diff-showcase-report.html
  • examples/explore-macro/tests.html
  • examples/explore-seed/tests.html
  • examples/explore/tests.html
  • examples/jaffle-shop-report.html
  • examples/playground-report.html
  • examples/prdiff-minidag-report.html
  • examples/seed-showcase-report.html
  • src/adapters/cte_engine.rs
  • src/adapters/render.rs
  • src/domain/cte.rs
  • src/domain/mod.rs
  • src/domain/source_map.rs

Comment thread examples/jaffle-shop-report.html Outdated
… edges (never-a-false-claim); reserve Opaque for unknown-external + push_span degrade test + strip insta artifact

A star over a KNOWN intra-model CTE (e.g. jaffle-shop's
`… renamed as (select id as customer_id, …) select * from renamed`)
was marked `confidence: opaque` with a virtual `*→*` edge — a FALSE
CLAIM baked into the committed goldens: `renamed`'s output columns ARE
resolvable. The design (column-lineage-feasibility §3 Tier-1) explicitly
scopes intra-model star EXPANSION into CLL-2.

- cte_engine: thread a per-CTE resolved-output-columns map through the
  projection pass (declaration order); a `select * from <known_cte>` /
  `q.*` now EXPANDS into one `PassThrough`/`Resolved` edge per upstream
  output column. `Opaque` + the virtual `*→*` edge is now reserved
  STRICTLY for a star over an UNKNOWN external relation (or a CTE whose
  own projection is itself non-enumerable) — honest degrade, never a
  fabricated column list. Corrected the comment that mis-scoped
  intra-CTE star expansion to CLL-4.
- TDD: `column_lineage_star_over_known_cte_expands_not_opaque` (+ the
  `q.*` and the still-Opaque honest-gap cases); kept the existing
  `column_lineage_opaque_star_over_unknown_external`.
- Kill the `push_span` boundary-guard surviving mutant: split the guard
  into `span_to_source_span` (testable with synthetic spans) and add
  `degraded_spans_are_dropped_not_fabricated` (line-0 sentinel,
  inverted range, and a control ending exactly at sql.len()).
- Strip the transient `assertion_line: 227` insta header from the
  jaffle-shop snapshot.
- Regenerate the report + explore goldens: the `renamed.* → opaque`
  edge is GONE, replaced by per-column Resolved edges; the
  `raw_customers.* → source.* opaque` edge stays (correct external).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199AmBCec5kyVEEF1Qd7TSF
@cmbays
cmbays merged commit 6cc5aa6 into main Jun 22, 2026
45 checks passed
@cmbays

cmbays commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

✅ Merged CLL-2 (#447) — intra-model column edges. 5/5 source-map-spine MVP slices complete.

What landed: the projection-provenance pass — for each CTE/terminal body, every output column resolves to its input(s): pass-through + rename as Resolved edges; a SELECT * over a known intra-model CTE expands into per-column Resolved edges; Opaque is reserved strictly for stars over unknown-external sources; expressions (coalesce/CASE) get a span but no edge (honest absence — Derived deferred to CLL-3); unqualified multi-source fans out Ambiguous, never dropped. The v2-reserved SpanRole::Column variant is now live; edges/spans ship as additive column_lineage.edges + code_map.column_spans.

Quality: council review (cao/ceng/cpo clean, cqo concerns). The CQO lens + synthesizer caught a never-a-false-claim violation — a fully-resolvable select * from <known_cte> was marked opaque in a committed golden — that the build, the verify, three chiefs, gemini, and CodeRabbit all passed. cute-dbt's core differentiator is not lying, so this was the most important catch of the run; fixed via the design-scoped star expansion (the jaffle renamed.* → opaque edge is gone, replaced by honest per-column Resolved edges; the genuinely-unknown raw_customers.* → opaque correctly remains). Both golden arms regenerated + byte-verified.

Follow-ups: #461 (pre-existing baseline-path), #462 (Source-kind disambiguation for CLL-4), #463 (badge-WHY copy + showcase).

Merged autonomously under the overnight build mandate. The deterministic source-map-spine MVP is complete (S0–S2, S3+CLL-1, CLL-2). Next: the raw-zone build (the final core slice), then the PR #440 refresh + Claude Design handoff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Intra-model column edges (pass-through/rename) as SpanRole::Column

1 participant