Skip to content

#155 — fix spurious DAG cycle when a CTE shares the model's name (stable node identity) - #156

Merged
cmbays merged 3 commits into
mainfrom
adapters-155-dag-node-identity
Jun 9, 2026
Merged

cmbays merged 3 commits into
mainfrom
adapters-155-dag-node-identity

Conversation

@cmbays

@cmbays cmbays commented Jun 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes the spurious circular reference in the lineage DAG (cute-dbt#155): a
model whose CTE shares the model's name — the idiomatic with orders as (...)
on the orders model — collapsed the import CTE and the model's final-select
node into one Mermaid node, showing a phantom orders ↔ final cycle and
clobbering the import node's compiled-SQL panel with the terminal's
select * from final.

Root cause

The terminal node's id was set to the model's bare name, colliding with
the same-named import CTE. Three render-layer sites keyed on that id then
collapsed: the Mermaid node id, the edge endpoints, and the compiled_sql
map — the last is why the IMPORT-badged node displayed the terminal's SQL.
The CTE engine itself was correct (it emits distinct, acyclic nodes); the
collapse was purely in the renderer's name-as-identity keying.

Fix

Key node identity by the engine's stable node name everywhere; the
model's file name rides as a display-only label on the terminal node. CTE
names are unique within a WITH, so terminal-vs-CTE was the only collision
vector — now closed in general (stable identity, not name-derived).

Per Christopher's suggestion, the terminal node displays <model>.sql —
semantically "this node is the model's compiled output", and unmistakable
from a same-named import CTE. In the collision case the DAG now reads
orders (import) → final → orders.sql (the model).

⚠️ Report-wide change — bump is your call

The terminal label change applies to every report, not just collision
cases: every terminal now reads <model>.sql instead of <model>. The
byte-gated examples reflect it. Tagged fix → patch (it's a bug fix and
the .sql label is the disambiguation mechanism), but flagging it so you can
consciously pick patch vs. minor at squash given the v0.x output-shape rule.

Verification

  • Render-layer unit regression: unique node ids, no self-cycle, the
    import node keeps its own SQL, terminal label orders.sql.
  • Real-Chromium headless regression reproducing the exact reported
    symptom: click the import node → its panel shows raw_orders, not
    from final; plus three distinct nodes + the a11y aria-label match.
  • a11y: the terminal node's aria-label was announcing the internal id
    ((final select)) instead of the visible <model>.sql — fixed + asserted.
  • Full sweep green: fmt, clippy --locked, nextest (731), bdd (85), headless
    --ignored (toggle 25 + zero-egress), cargo doc -D warnings, deny,
    crap4rs (the touched fns are CRAP 1–5, far under render.rs's strict 15), all
    three examples byte-identical on re-render.

Dogfood note (honest gap)

No committed golden reproduces the collision — all three golden fixtures
(jaffle-shop / playground / diff-showcase) are real-compile-provenance'd
(sha256 + origin_url) and can't be hand-edited to add a self-named-CTE model
without breaking that provenance. The committed headless regression is the
durable guard; the live dbt-project/ orders model (which surfaced this)
renders correctly now and will show on the next dbt-project-touching PR's
preview.

Closes #155

🤖 Generated with Claude Code


Open in Stage

Summary by CodeRabbit

  • New Features

    • DAG visualization now displays human-readable node labels instead of internal identifiers for enhanced clarity and usability.
  • Accessibility

    • Improved screen reader support with descriptive labels for DAG node inspection and navigation.
  • Bug Fixes

    • Fixed terminal node identification to prevent collisions with models that have self-named import CTEs.

github-actions Bot and others added 2 commits June 8, 2026 21:59
A model whose CTE shares the model's name — the idiomatic `with orders as
(...)` on the `orders` model — rendered a spurious `orders ↔ final` cycle in
the lineage DAG. The terminal node's id was set to the model's bare name,
colliding with the same-named import CTE, so the two distinct engine nodes
collapsed into one Mermaid node and the import node's compiled-SQL panel was
clobbered with the terminal's `select * from final`.

Key node identity by the engine's stable node name everywhere (Mermaid node
id, edge endpoints, the compiled_sql map); the model's file name rides as a
display-only `label` on the terminal node. CTE names are unique within a
WITH, so terminal-vs-CTE was the only collision vector — now closed in general
via stable-index identity rather than name-derived identity.

Per Christopher's suggestion, the terminal node now displays `<model>.sql`
(the model's file name) in every report — semantically "this node is the
model's compiled output", and visually unmistakable from a same-named import
CTE. The byte-gated example reports reflect the new terminal label.

Tested: render-layer unit regression (unique ids, no self-cycle, import keeps
its own SQL, terminal label `orders.sql`) + a real-Chromium headless
regression reproducing the exact reported symptom (click the import node →
its panel shows `raw_orders`, not `from final`).

Closes #155

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-label

`bindDagNodeClicks` built the node aria-label from the internal node id, so
after the identity fix a screen reader announced the terminal as
`(final select)` while it visibly reads `<model>.sql`. Thread the display
label (`label || id`) into the aria-label so the announced and visible names
match again. Headless regression asserts the terminal node's aria-label
carries its `.sql` label. Examples + snapshot regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 9, 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 51 minutes and 47 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ 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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bcb67539-90a6-474e-a632-76354e8bccb4

📥 Commits

Reviewing files that changed from the base of the PR and between b8a5194 and e737edf.

📒 Files selected for processing (1)
  • dbt-project/models/orders.sql
📝 Walkthrough

Walkthrough

This PR fixes a DAG rendering bug (#155) where a model with a CTE sharing the model's name creates a spurious cycle and displays wrong compiled SQL. The fix decouples node stable identity from display label, uses stable IDs for DAG structure/compiled-SQL keying, and propagates label-based rendering through the template and example fixtures.

Changes

Self-Named CTE DAG Collision Fix

Layer / File(s) Summary
Terminal node payload contract
src/adapters/render.rs, tests/
NodePayload adds optional label field (serialized only when present); terminal node id is now the stable engine name (final select), with display name moved to label. Unit tests updated to assert the new contract.
Node payload and compiled SQL generation
src/adapters/render.rs
build_model_payload, build_node_payloads, and build_compiled_sql now use stable engine node names (not model-name substitution) for DAG edges, node IDs, and compiled-SQL keys. Terminal node label is set to <model>.sql while preserving stable id identity.
Compiled SQL and regression tests
src/adapters/render.rs, tests/
Update existing tests to verify stable keying; add comprehensive regression test for self-named import CTE, verifying distinct node IDs, no cycles, correct roles, and that import-CTE SQL is not overwritten by terminal SQL.
Template label-based DAG rendering
templates/report.html
Update Mermaid node display text, safeToLabel mapping, aria-label accessibility labels, and node detail header to prefer visible label over internal id.
Headless test helper and regression
tests/headless_toggle.rs
Add model_node_with_compiled helper; introduce ignored integration test for self-named CTE collision scenario, verifying distinct DAG nodes, correct labels, and import-CTE compiled SQL is preserved.
Example HTML golden fixtures
examples/diff-showcase-report.html, examples/jaffle-shop-report.html, examples/playground-report.html
Update embedded JSON fixtures to reflect new terminal id/label structure and update inline JavaScript to render node labels in Mermaid DAG, accessibility labels, and node detail headers.

Sequence Diagram(s)

The PR does not introduce new component interactions warranting a sequence diagram. The changes are structural refinements to node identity representation and display rendering, without new async workflows, API calls, or multi-component request/response flows.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • breezy-bays-labs/cute-dbt#38: Introduces the Askama v0.1 report renderer and template foundations that this PR extends by refining the DAG node payload contract and label-based rendering logic in src/adapters/render.rs and templates/report.html.

Poem

🐰 Two nodes had the same name, alas—
Their cycle was spurious, a graph-breaking pass.
Now labels and IDs are finally apart,
And CTEs bloom with a stable heart. ✨
The terminal shines with (final select) bright,
While label displays the model name right!

🚥 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 title clearly and specifically describes the main change: fixing a spurious DAG cycle when a CTE shares the model's name by establishing stable node identity.
Linked Issues check ✅ Passed The PR implementation comprehensively addresses all acceptance criteria from issue #155: fixes the spurious DAG cycle, corrects compiled-SQL display, preserves node roles/tints, uses stable id-based identity while maintaining label-for-display, ensures import-CTE binding still resolves correctly, and adds a synthetic headless regression fixture asserting no self-cycle and correct import SQL.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #155: render-layer identity refactoring (render.rs), template/JS accessibility/UI updates (report.html, example HTMLs), headless regression test (headless_toggle.rs), and expected golden-file regeneration.
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 adapters-155-dag-node-identity

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.

@ghost

ghost commented Jun 9, 2026 •

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 6 individual chapters for you:

Title
1 Update NodePayload schema for stable identity
2 Implement stable node identity in renderer
3 Update HTML template for label support
4 Add regression tests for DAG collisions
5 Refresh example reports with new labels
6 Other changes
Open in Stage

Chapters generated by Stage for commit e737edf on Jun 9, 2026 2:15am UTC.

@github-actions

github-actions Bot commented Jun 9, 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
jaffle-shop-report.html ▶ Open ↗ ⬇ Download
diff-showcase-report.html ▶ Open ↗ ⬇ Download
playground-report.html ▶ Open ↗ ⬇ Download

🐶 Live dogfood preview

This PR's own dbt-project/ diff, freshly compiled by fusion into an ephemeral manifest (never committed) and rendered with --pr-diff. Regenerated every PR — the live self-dogfood, not a committed example.

Report View Download
dbt-project-report.html ▶ Open ↗ ⬇ Download

▶ Open ↗ opens the report in your browser in one click —
published to this repo's GitHub Pages under /pr-156/.
⬇ 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 27179609085 -R breezy-bays-labs/cute-dbt -n report-preview-playground
open report-preview-playground/playground-report.html

Posted by report-preview.yml for e737edfe6e8f39bb4f200ba0135f55b75fb435c2. 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 resolves an issue (cute-dbt#155) where a self-named import CTE (e.g., with orders as (...) in an orders model) would collapse into the terminal node in the DAG. It achieves this by decoupling the stable node ID from its display label: the terminal node's ID is now a stable engine name, while its display label is formatted as <model_name>.sql. The changes span the Rust adapter, HTML templates, and integration tests. Feedback was provided to update a doc comment in src/adapters/render.rs to accurately reflect that the terminal node's label is formatted with the .sql suffix rather than just being the bare model name.

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.

Comment thread src/adapters/render.rs
…ers model

Touch the `orders` model (whose import CTE is named `orders`, the collision
case) so the PR's report-preview renders its DAG — surfacing the fix live:
`orders` (import) → `final` → `orders.sql` (terminal), three distinct nodes
with no spurious self-cycle. The comment is purely documentary; the model's
compiled SQL and output are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cmbays
cmbays merged commit ff7d677 into main Jun 9, 2026
31 checks passed
@cmbays
cmbays deleted the adapters-155-dag-node-identity branch June 9, 2026 02:48
github-actions Bot added a commit that referenced this pull request Jun 9, 2026
cmbays added a commit that referenced this pull request Jun 10, 2026
…olumn tooltips (#188)

* feat(adapters): design integration PR-2 - engine merge (settings panel, unified/split diffs) + port-forward

Replace the template's ~2100-line inline engine with two first-party
include_str! assets: templates/interaction.js (the merged redesign
interaction engine) and templates/theme.js (the appearance engine), wired
through asset_embed::{INTERACTION_JS,THEME_JS} with banner-pin +
end-of-file-sentinel tests (the REPORT_CSS pattern).

Engine merge (handoff -> shipped, our behaviors win every conflict):
- Settings panel: static Appearance markup (style/theme/accent/density/
  diff-style/diff-layout) in the template, wired by theme.js; persists
  under cute-dbt.appearance.v1; DataTables dark syncs via html.dark. The
  cog + panel are now universal (themes apply to baseline reports too);
  the #139 context-lines/normalize rows stay PR-diff-only.
- Diff renderers: GitHub-grade two-column line-number gutters, shared
  diffBody/diffNumbers helpers, hunk folds keep the #132/#136 contract,
  and a NEW split (side-by-side) renderer - diffViewsHtml emits BOTH
  layouts over the same BlockDiff lines verbatim (kind/text/emphasis; no
  Rust diff-engine change); CSS shows one per html[data-difflayout] with
  a responsive fallback.
- Code cards: file-path headers (defined_in on the YAML drawer,
  <model>.sql on the Model-SQL card), single-gutter line numbers on
  plain blocks (highlightLines*), incremental badge relocated into the
  Model-SQL card header (static span removed per handoff README section 3).
- DAG: theme-aware edge palette (JOIN_COLORS_LIGHT/_DARK + dagEdges),
  __cuteRerenderDag re-tint hook; Cytoscape surface deliberately
  excluded (Bucket 2).

Port-forward audit (the fork predates these; shipped behavior kept):
- #156 stable DAG node identity: already present in the handoff fork's
  JS; the Rust side never forked. Verified, no regression.
- #158 minmax(0,1fr) stacked-panel fix: PR-1 chassis kept it; the 375px
  headless regression stays green.
- #161 strategy-correct expect-semantics tooltip: kept OUR wording (the
  handoff carried the pre-#159 merge/insert copy - rejected).
- #166 column tooltips: kept OUR payload shape + focusable-button/CSS-
  bubble renderer (the handoff's body-appended th.has-col-meta renderer
  targets the not-yet-existing ModelPayload.column_meta - rejected;
  belongs to handoff PR-3).

Also: report.css drops its PR-1 bridge layer per its own contract and
gains the .settings-row-stack chrome; the edge-vocab-completeness gate +
BDD legend steps re-point at templates/interaction.js and now check both
palettes; headless guards added for the appearance toggles, the split
renderer, the gutters and the relocated badge; goldens + insta
snapshots regenerated (byte-identity re-verified CI-style).

Closes #178

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(adapters): column-header tooltips to the handoff spec (th-trigger, accent keys, value chips) + gemini fixes

Christopher's review override on PR #188: the #166-tooltip deferral is
reversed — the column-header tooltips must match the handoff spec in
this PR, adapted to OUR shipped per-table column_meta payload (NOT
ModelPayload.column_meta, which stays PR-3).

Spec match (handoff engine/interaction.js + base.css, README section 2.2):
- The WHOLE header cell is the hover/focus trigger — the info icon
  (buildColTooltip's focusable button + per-th bubble) is gone.
  decorateColHeader marks a metadata-bearing th with .has-col-meta +
  data-col-name/desc/tests; showColTip fills + positions the single
  body-appended #col-tooltip bubble with the handoff's positioning math
  verbatim (position:fixed, horizontal clamp, flip-above on bottom
  overflow — never clipped by the .table-fit scroller).
- Bubble content per spec: .ct-desc (description, larger) ->
  "Data tests" label -> per-test .ct-test rows with .ct-key (accent
  color), .ct-vals > .ct-val chips for accepted_values args, .ct-detail
  muted mono for relationships/range detail. CSS ported from the
  handoff base.css (comment bodies kept free of close-sequences per the
  README porting-bug warning).
- Render-layer POD (adapter, not domain): ColumnMetaPayload.tests is
  now Vec<ColumnTestPayload {name, values, detail}> built by
  column_test_payload, the section-2.2 display mapping: unique /
  not_null -> bare prose names; accepted_values -> "accepted values" +
  values chips; relationships -> detail "model.field" (ref()/source()
  unwrapped to the last quoted arg); accepted_range (any namespace) ->
  detail "min-max" / ">= min" / "<= max"; any other test keeps its
  package-qualified raw identifier with no values/detail.
- A11y (the #166 contract carried onto the th): tabindex="0" so
  keyboard users reach the tip via focusin; aria-label on the trigger
  summarizes description + tests; the singleton bubble is aria-hidden;
  hover AND focus both reveal; decorated headers SHED the native title
  (undecorated headers keep theirs); :focus-visible outline added.

Guards updated:
- headless: column_header_tooltips_th_trigger_hover_focus_and_skip_
  bare_columns — th-trigger semantics (no icon/button in any th),
  tabindex/aria/title assertions, focus AND hover reveal + hide,
  .ct-key/.ct-val/.ct-detail content over an accepted_values +
  relationships fixture, given-table input-model resolution.
- BDD: the report_generation scenario matches the structured entries'
  display `name` ("not_null" -> "not null"); payload contract otherwise
  unchanged.
- render.rs unit tests pin the full section-2.2 mapping table.

Gemini review threads (PR #188, both applied): theme.js gains an
ES5-safe qsaForEach helper replacing NodeList.prototype.forEach at
every control-wiring site, and reflowTables binds jQuery locally.

Goldens + the chrome insta snapshot regenerated; byte-identity
re-verified CI-style; visual check against the handoff reference.html
confirms the tooltip treatment matches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

bug: lineage DAG shows a spurious cycle + wrong compiled-SQL when a CTE shares the model's name

1 participant