Skip to content

feat: SQL query-plan diagrams, and a reports/ folder for generated artifacts - #50

Merged
andre-salvati merged 7 commits into
mainfrom
feat/sql-diagram
Jul 22, 2026
Merged

feat: SQL query-plan diagrams, and a reports/ folder for generated artifacts#50
andre-salvati merged 7 commits into
mainfrom
feat/sql-diagram

Conversation

@andre-salvati

Copy link
Copy Markdown
Owner

What?

Adds make sql-diagram / /sql-diagram: a sqlglot-based tool that draws a SQL query either as its execution steps (default) or as column-level lineage, writing the analysed .sql, a Mermaid .mmd and a standalone .svg. Also collects generated artifacts under a new reports/ folder — coverage_reports/ becomes reports/coverage/, cost_report/ becomes reports/cost/, diagrams land in reports/sql-diagram/ — and folds in the pending doc consolidation in specs/tooling.md / specs/workflow.md.

Why?

Reading a non-trivial query in this repo means reconstructing its shape by hand, and the queries that matter most (the system.billing cost queries) live interpolated inside f-strings, so the SQL that actually runs exists nowhere in the tree. Existing Claude skills for this either analyse an EXPLAIN you paste in or draw generic Mermaid from model inference; none derives the diagram from the query. Parsing the AST gives edges that are what the query says rather than what a model guessed.

How?

sqlglot models a multi-table join as one n-ary step, so build_plan splits it back into JOIN 1, JOIN 2, … in written order — that is what makes an under-constrained join predicate visible, which is exactly the class of bug that caused the 3.5× fan-out fixed in #47. Each join carries its side and full ON condition; --comments annotates tables with their Unity Catalog comment via the dev profile, since the MCP service principal lacks USE SCHEMA on system.billing. It is opt-in as the only network call, and a table without a comment is left unannotated rather than described from guesswork.

SVG is written by hand rather than shelled out to mmdc, which would pull a Node toolchain and headless Chromium into a Python repo — the same trade scripts/star_history.py already makes. One CSS class per box, because a multi-class cascade is not resolved by every SVG renderer.

Validation?

uv run pytest passes with coverage writing to the new path; ruff clean. Both modes exercised on the two cost queries, and the SVG rasterised and inspected at each step — which caught black-on-black boxes twice and mid-sentence truncation. Round-tripping the emitted .sql exposed non-determinism: planner.Step.dependencies is a set, so node numbering followed the hash seed and three runs over one input produced three different files, making the .mmd churn in git. Dependencies are now walked in a stable order and the round trip reproduces byte-for-byte across five runs in both modes.

Impact in prod

  • No table schema/data change — no production impact.

No medallion table, job, or pipeline is touched. The one operational change is the CI coverage artifact path in onpush.yml, updated to reports/coverage/ in the same commit as the pyproject.toml setting that produces it.

andre-salvati and others added 4 commits July 22, 2026 12:39
Add scripts/sql_lineage.py, which parses a query with sqlglot and emits a
Mermaid flowchart tracing each output column back to the source columns it
reads, grouped into one subgraph per table. Exposed as `make sql-lineage`
and driven by the /sql-lineage command.

Lineage comes from the AST rather than model inference, so the edges are
exactly what the query says. Columns that cannot be attributed to a table
are grouped under "(unqualified)" instead of guessed at, and SELECT * is
rejected outright since tracing it would need the table schemas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expand specs/tooling.md with install-layout and MCP-identity sections,
consolidate the changelog discipline and branch rules into
specs/workflow.md, and repoint CLAUDE.md at the new anchors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rework the sqlglot diagram tool from lineage-only into sql-diagram, whose
default mode draws the query's execution steps: one node per scan, one per
individual join with its side and keys, then filter, aggregate and sort. A
CTE renders as its own sub-pipeline. sqlglot models a multi-table join as a
single n-ary step, so build_plan splits it back into JOIN 1, JOIN 2, … in
written order — which is what makes an under-constrained join predicate
visible. Column lineage stays available as mode=lineage.

Diagrams carry each join's full ON condition and, with --comments, the
source table's Unity Catalog comment. Comment lookup is opt-in because it
is the only part that touches the network, and it uses the dev profile: the
MCP service principal lacks USE SCHEMA on system.billing. A table with no
comment is left unannotated rather than described from guesswork.

SVG is written by hand rather than shelled out to mmdc, which would pull a
Node toolchain and a headless Chromium into a Python repo — the trade
scripts/star_history.py already makes. Both renderers use one CSS class per
box: a multi-class cascade is not resolved by every SVG renderer.

Collect generated artifacts under reports/: coverage_reports/ becomes
reports/coverage/ and cost_report/ becomes reports/cost/, with diagrams in
reports/sql-diagram/. All three are gitignored, each keeping one committed
example. Refreshes that example cost report to 2026-07-22.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rministic

sql-diagram now writes <name>.sql alongside the .mmd and .svg. A diagram
without the query it came from cannot be checked, and the source is usually
an f-string in a .py file that no longer reads the same once its
placeholders are filled in; the emitted file is the query as parsed, so
re-running the tool on it reproduces the diagram.

Verifying that round trip exposed a real defect: planner.Step.dependencies
is a set, so iteration order followed the process hash seed and node
numbering changed on every run — three runs over one input gave three
different files. That made the .mmd churn in git, defeating the diffable
property it was documented to have. Dependencies are now walked in a stable
(name, type) order, and the round trip reproduces byte-for-byte.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
andre-salvati added a commit that referenced this pull request Jul 22, 2026
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
andre-salvati and others added 2 commits July 22, 2026 14:25
…tests

Each fix gets a test named after the defect, so a regression says what it
broke. scripts/ has had no test coverage until now, which is how these
survived.

- Plan mode no longer inherits lineage's constraints. main computed the
  lineage graph unconditionally, so a SELECT * query the plan renders fine
  died in the tracer that never ran. Each mode now builds only what it
  renders, which also drops four redundant parses of the same SQL.
- Join predicates are split on the AST, not on generated text. The old
  regex deleted any "TRUE AND" — including one written inside a real
  predicate — and text splitting tore apart string literals containing AND
  and miscounted parens around a literal ")". An OR conjunct keeps its
  parentheses: prefixed with "and", a bare OR reads as the opposite
  grouping.
- A join with no keys and no predicate is labelled CROSS, not INNER.
  Calling a cross product an inner join hides exactly the unconstrained
  join this diagram exists to expose.
- Unity Catalog comments are matched to a scan by exact table name. The
  substring match gave system.billing.usage_extra the comment belonging to
  system.billing.usage.
- The driving table is read from the FROM clause. Deducing it by
  elimination lost every join edge when it projected no columns of its own,
  and it is now drawn even then. Note that sqlglot v30 renames the arg to
  `from_`, so the previous key lookup would have returned None silently.
- CTAS and INSERT … SELECT are unwrapped and diagrammed; unparseable or
  SELECT-less input exits with one line instead of a traceback.
- Lineage refuses a query projecting one name twice rather than tracing
  both to the first match. The review suggested keying the graph by index,
  but sqlglot resolves lineage by name, so that keeps both nodes while
  attributing one to the wrong table — a confident wrong diagram is worse
  than a refusal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The eight review fixes in scripts/sql_diagram.py stay; only their
regression coverage is dropped. Recoverable from ce0fe54 if wanted back.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@andre-salvati
andre-salvati merged commit 40b62d8 into main Jul 22, 2026
1 check passed
@andre-salvati
andre-salvati deleted the feat/sql-diagram branch July 22, 2026 17:50
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.

1 participant