From bea32df1a62f92782c0e001d04609012581cc79e Mon Sep 17 00:00:00 2001 From: Christopher Bays Date: Sun, 31 May 2026 13:17:40 -0400 Subject: [PATCH 1/5] feat: inline SQL diff for changed models in PR-review report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the #96 line-diff substrate to a changed model's raw SQL (raw_code), rendered in the Model SQL section with a Raw↔Diff toggle. report-mode, diff-sourced (--pr-diff), no base manifest. - domain/pr_diff: rename YamlBlockDiff -> BlockDiff (content-agnostic; JSON field `yaml_diff` kept so #96 wire/JS/snapshots don't move). Widen block_aligns_with_hunks + reconstruct_one to (raw, start, end). Add reconstruct_model_sql_diffs over manifest raw_code (whole-file span; strip exactly one trailing \n for git's line frame — handles the dbt-core vs dbt-fusion trailing-newline divergence). N7b drift guard reused; stale/untouched/whitespace-only -> plain SQL view. - Whitespace ignored as standard, per line-pair (ws_equal, git --ignore-all-space semantics) across both the YAML (#96) and SQL (#111) diffs. N7b stays whitespace-exact. - render: ModelPayload.sql_diff (skip_serializing_if, mirrors TestPayload.yaml_diff); threaded through build_payload/render_report. PrDiff-arm-gated in cli; Baseline => empty map (digest snapshot unmoved). - template: renderYamlDiff -> renderBlockDiff(diff, highlightFn) with highlightSql/highlightYaml; highlight-bypass preserved. Model SQL Raw↔Diff toggle (default Diff when sql_diff present). - BDD: 4 scenarios appended to pr_diff_scoping.feature (count stays 10); headless SQL-diff toggle tests; book docs (raw_code source, whitespace standard + caveat, engine trailing-newline note). Closes #111 Co-Authored-By: Claude Opus 4.8 (1M context) --- book/src/how-it-works.md | 41 + book/src/recipes/github-actions-pr-review.md | 11 +- examples/jaffle-shop-report.html | 79 +- examples/playground-report.html | 79 +- features/pr_diff_scoping.feature | 54 ++ src/adapters/render.rs | 133 ++- src/cli/mod.rs | 27 +- src/domain/mod.rs | 4 +- src/domain/pr_diff.rs | 891 +++++++++++++++--- templates/report.html | 79 +- tests/golden_report.rs | 1 + tests/headless_toggle.rs | 229 ++++- tests/render_integration.rs | 1 + ...egration__rendered_chrome_jaffle_shop.snap | 79 +- tests/steps/builders.rs | 33 + tests/steps/pr_diff_scoping.rs | 182 +++- tests/steps/world.rs | 34 + 17 files changed, 1760 insertions(+), 197 deletions(-) diff --git a/book/src/how-it-works.md b/book/src/how-it-works.md index f627b2a0..28de3a35 100644 --- a/book/src/how-it-works.md +++ b/book/src/how-it-works.md @@ -213,6 +213,47 @@ page lays out as: The inline diff is **`--pr-diff`-only**: baseline mode computes `changed` from a structural manifest diff with no hunks, so it has no inline YAML diff and the drawer always shows the authored YAML. +6. **Model SQL section** — the model's **raw Jinja source** (`raw_code` + from the manifest — the diffable layer; compiled SQL is generated and + un-diffable). When the PR diff changed the model's `.sql`, this section + adds a **Raw ↔ Diff** toggle and opens on the **Diff** view, showing + the inline diff of the model's SQL (cute-dbt#111). The diff reuses the + exact line-diff substrate the Authoring YAML drawer uses — same + change-pair rendering, same intra-line emphasis, same N7b drift guard. + `raw_code` is read straight from the manifest (no `--project-root` + filesystem read needed, unlike the YAML drawer), so the SQL diff fires + on any changed model `.sql`. When there's no SQL diff — baseline mode, + the model is in scope only via a changed *test*, the diff is stale, or + the change was whitespace-only — the section shows the plain raw SQL + exactly as before. dbt engines differ in one detail cute-dbt + normalizes away: dbt-core ships `raw_code` with the file's trailing + newline stripped, dbt-fusion ships it byte-identical but keeping the + trailing newline; cute-dbt strips a single trailing newline so the SQL + diff renders identically regardless of which engine compiled the + manifest. + +### Whitespace-only changes are ignored (standard) + +Both inline diffs (the YAML drawer and the Model SQL diff) **ignore +whitespace-only differences** as standard behavior — no flag, no opt-in. +A re-indentation, a trailing-whitespace edit, or a blank-line churn that +leaves the substantive content unchanged renders as **context**, not as a +change (`git --ignore-all-space` semantics, compared per line-pair). If +*every* change in a block is whitespace-only, cute-dbt emits **no inline +diff** and shows the plain view. The drift guard upstream stays +whitespace-exact (a whitespace divergence between the diff and the +working tree is genuine revision drift, not a no-op), so whitespace +insensitivity applies only to the change/no-change decision, never to the +staleness check. + +This is a best-effort filter over git's pre-computed `--unified=0` hunks, +not a re-diff: cute-dbt cannot re-pair lines git already split. The +dominant cases (re-indentation, trailing whitespace, blank-line churn) +are handled; for a multi-line replacement that mixes a re-indent with a +real edit, the removed line may render at the hunk's top rather than +paired to its own offset — the change-set stays correct, only the line +placement is approximate. For YAML, an indentation change that alters +*structure* still surfaces via the accompanying value changes. The description banner started life at the top of the test-selection section. It moved to between the CTE DAG and the inspect/expected diff --git a/book/src/recipes/github-actions-pr-review.md b/book/src/recipes/github-actions-pr-review.md index b6e06968..e24dfbe4 100644 --- a/book/src/recipes/github-actions-pr-review.md +++ b/book/src/recipes/github-actions-pr-review.md @@ -93,9 +93,14 @@ cute-dbt \ "read from this file"). cute-dbt parses the diff's `+++ b/` headers to pick the in-scope set and its `@@ … @@` hunks to flag which tests the PR actually edited (block-precise updated detection + the inline YAML diff -drawer key off the hunk spans). `--project-root dbt_project` rewrites the -repo-relative diff paths so they match the manifest's project-relative -`original_file_path`s (drop it if your dbt project is at the repo root). +drawer key off the hunk spans). When a hunk changes a model's `.sql`, the +Model SQL section also shows an inline **SQL diff** of the model's raw +Jinja with a Raw ↔ Diff toggle — same diff engine, keyed off the same +hunks. Both inline diffs ignore whitespace-only edits as standard (a pure +re-indent shows the plain view, not a noisy diff). `--project-root +dbt_project` rewrites the repo-relative diff paths so they match the +manifest's project-relative `original_file_path`s (drop it if your dbt +project is at the repo root). cute-dbt parses the diff you hand it — it never shells out to `git` or reads `GITHUB_EVENT_PATH`, so the workflow stays in control of *how* the diff is produced. diff --git a/examples/jaffle-shop-report.html b/examples/jaffle-shop-report.html index af20ee1c..0bd115bd 100644 --- a/examples/jaffle-shop-report.html +++ b/examples/jaffle-shop-report.html @@ -4369,11 +4369,17 @@

Expected

} } - // cute-dbt#47 — populate (or hide) the per-model Model SQL section. - // The askama template emits one static
; JS - // rebinds its body and Copy handler each time the user selects a + // cute-dbt#47 / cute-dbt#111 — populate (or hide) the per-model Model SQL + // section. The askama template emits one static
; + // JS rebinds its body and Copy handler each time the user selects a // different model. When `raw_sql` is missing, hide the whole section // (defensive — dbt 1.8+ populates raw_code on every node). + // + // cute-dbt#111: when the selected model carries an inline SQL diff + // (m.sql_diff — PR-diff mode, the model's .sql changed), the section shows + // a Raw↔Diff toggle defaulting to Diff (rendered via renderBlockDiff with + // the SQL highlighter on context lines only). Absent a diff it renders the + // plain raw SQL exactly as cute-dbt#47 did. Copy always copies the raw SQL. function renderModelSql(m) { var $section = $(".model-sql"); var raw = m && m.raw_sql; @@ -4382,13 +4388,47 @@

Expected

return; } $section.show(); - $section.find(".model-sql-code").html(highlightSql(raw)); - var $copy = $section.find(".model-sql-copy"); - $copy.off("click.cuteDbtModelSql").on("click.cuteDbtModelSql", function (e) { + var hasDiff = m.sql_diff && m.sql_diff.lines && m.sql_diff.lines.length; + $section.find(".model-sql-summary-hint").text(hasDiff ? "diff" : "raw, with Jinja"); + var $wrap = $section.find(".model-sql-wrap"); + // Rebuild the wrap body each model switch so a prior model's toggle/diff + // never leaks; the Copy button is re-created here too. + $wrap.empty(); + var $copy = $("