sql-glider 0.1.20__py3-none-any.whl → 0.1.21__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sql_glider-0.1.20.dist-info → sql_glider-0.1.21.dist-info}/METADATA +41 -1
- {sql_glider-0.1.20.dist-info → sql_glider-0.1.21.dist-info}/RECORD +8 -7
- sqlglider/_version.py +2 -2
- sqlglider/cli.py +101 -4
- sqlglider/graph/diagram_formatters.py +330 -0
- {sql_glider-0.1.20.dist-info → sql_glider-0.1.21.dist-info}/WHEEL +0 -0
- {sql_glider-0.1.20.dist-info → sql_glider-0.1.21.dist-info}/entry_points.txt +0 -0
- {sql_glider-0.1.20.dist-info → sql_glider-0.1.21.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sql-glider
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.21
|
|
4
4
|
Summary: SQL Utility Toolkit for better understanding, use, and governance of your queries in a native environment.
|
|
5
5
|
Project-URL: Homepage, https://github.com/rycowhi/sql-glider/
|
|
6
6
|
Project-URL: Repository, https://github.com/rycowhi/sql-glider/
|
|
@@ -32,8 +32,12 @@ Description-Content-Type: text/markdown
|
|
|
32
32
|
|
|
33
33
|
# SQL Glider
|
|
34
34
|
|
|
35
|
+

|
|
36
|
+
|
|
35
37
|
SQL Utility Toolkit for better understanding, use, and governance of your queries in a native environment.
|
|
36
38
|
|
|
39
|
+
**[Read the docs](https://rycowhi.github.io/sql-glider/)**
|
|
40
|
+
|
|
37
41
|
## Overview
|
|
38
42
|
|
|
39
43
|
SQL Glider provides powerful column-level and table-level lineage analysis for SQL queries using SQLGlot. It operates on standalone SQL files without requiring a full project setup, making it perfect for ad-hoc analysis, data governance, and understanding query dependencies.
|
|
@@ -409,6 +413,17 @@ uv run sqlglider graph query graph.json --upstream orders.customer_id
|
|
|
409
413
|
|
|
410
414
|
# Query downstream dependencies (find all columns affected by a source)
|
|
411
415
|
uv run sqlglider graph query graph.json --downstream customers.id
|
|
416
|
+
|
|
417
|
+
# Query with Mermaid diagram output
|
|
418
|
+
uv run sqlglider graph query graph.json --upstream orders.customer_id -f mermaid
|
|
419
|
+
|
|
420
|
+
# Query with DOT (Graphviz) diagram output
|
|
421
|
+
uv run sqlglider graph query graph.json --downstream customers.id -f dot
|
|
422
|
+
|
|
423
|
+
# Visualize entire graph as a diagram
|
|
424
|
+
uv run sqlglider graph visualize graph.json # Mermaid (default)
|
|
425
|
+
uv run sqlglider graph visualize graph.json -f dot # DOT/Graphviz
|
|
426
|
+
uv run sqlglider graph visualize graph.json -o lineage.mmd # Save to file
|
|
412
427
|
```
|
|
413
428
|
|
|
414
429
|
**Example Upstream Query Output:**
|
|
@@ -796,6 +811,31 @@ src/sqlglider/
|
|
|
796
811
|
└── file_utils.py # File I/O utilities
|
|
797
812
|
```
|
|
798
813
|
|
|
814
|
+
## Documentation
|
|
815
|
+
|
|
816
|
+
Project documentation is built with [Zensical](https://zensical.org/) and lives in the `docs/` directory:
|
|
817
|
+
|
|
818
|
+
```
|
|
819
|
+
docs/
|
|
820
|
+
├── zensical.toml # Site configuration (name, theme, features)
|
|
821
|
+
└── docs/ # Markdown content
|
|
822
|
+
└── index.md # Landing page
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
To preview docs locally:
|
|
826
|
+
|
|
827
|
+
```bash
|
|
828
|
+
cd docs && uv run zensical serve
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
To build the static site:
|
|
832
|
+
|
|
833
|
+
```bash
|
|
834
|
+
cd docs && uv run zensical build
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
The built site outputs to `docs/site/` (git-ignored). Documentation is automatically deployed to GitHub Pages on pushes to `main` via the `docs.yml` workflow.
|
|
838
|
+
|
|
799
839
|
## Publishing
|
|
800
840
|
|
|
801
841
|
SQL Glider is configured for publishing to both TestPyPI and PyPI using `uv`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
sqlglider/__init__.py,sha256=gDf7s52dMcX7JuCZ1SLawcB1vb3U0yJCohu9RQAATBY,125
|
|
2
|
-
sqlglider/_version.py,sha256=
|
|
3
|
-
sqlglider/cli.py,sha256=
|
|
2
|
+
sqlglider/_version.py,sha256=QwRysHnO-cJwIFSvXReHDj93DEPhlibOTtnnb25J9-c,706
|
|
3
|
+
sqlglider/cli.py,sha256=jQisnNRvRR5AeBnGF7POtcndMR-7cen7yfp7WDShgO4,68523
|
|
4
4
|
sqlglider/global_models.py,sha256=2vyJXAuXOsXQpE-D3F0ejj7eR9z0nDWFjTkielhzM8k,356
|
|
5
5
|
sqlglider/catalog/__init__.py,sha256=2PqFPyzFXJ14FpSUcBmVK2L-a_ypWQHAbHFHxLDk_LE,814
|
|
6
6
|
sqlglider/catalog/base.py,sha256=R7htHC43InpH4uRjYk33dMYYji6oylHns7Ye_mgfjJE,3116
|
|
@@ -12,6 +12,7 @@ sqlglider/dissection/formatters.py,sha256=M7gsmTNljRIeLIRv4D0vHvqJVrTqWSpsg7vem8
|
|
|
12
12
|
sqlglider/dissection/models.py,sha256=RRD3RIteqbUBY6e-74skKDvMH3qeAUaqA2sFcrjP5GQ,3618
|
|
13
13
|
sqlglider/graph/__init__.py,sha256=4DDdrPM75CmeQWt7wHdBsjCm1s70BHGLYdijIbaUEKY,871
|
|
14
14
|
sqlglider/graph/builder.py,sha256=VNBdsDlkiaId3JGvr2G4h6OIFek_9zPsGMIYL9GpJlk,15796
|
|
15
|
+
sqlglider/graph/diagram_formatters.py,sha256=loCpT343Awuj1mue8pv2fNTfwrLfK8n9adlfpPI_OQw,10825
|
|
15
16
|
sqlglider/graph/formatters.py,sha256=p85-WN9oPmEETsAtWSo1sIQELF36w85QoFEJyfBZGoM,4800
|
|
16
17
|
sqlglider/graph/merge.py,sha256=uUZlm4BN3S9gRL66Cc2mzhbtuh4SVAv2n4cN4eUEQBU,4077
|
|
17
18
|
sqlglider/graph/models.py,sha256=EYmjv_WzDSNp_WfhJ6H-qBIOkAcoNKS7GRUryfKrHuY,9330
|
|
@@ -31,8 +32,8 @@ sqlglider/utils/__init__.py,sha256=KGp9-UzKz_OFBOTFoSy-g-NXDZsvyWXG_9-1zcC6ePE,2
|
|
|
31
32
|
sqlglider/utils/config.py,sha256=qx5zE9pjLCCzHQDFVPLVd7LgJ-lghxUa2x-aZOAHByY,4962
|
|
32
33
|
sqlglider/utils/file_utils.py,sha256=5_ff28E0r1R7emZzsOnRuHd-7zIX6873eyr1SuPEr4E,1093
|
|
33
34
|
sqlglider/utils/schema.py,sha256=LiWrYDunXKJdoSlpKmIaIQ2hLSaIN1iQHqkXjMpGzRE,1883
|
|
34
|
-
sql_glider-0.1.
|
|
35
|
-
sql_glider-0.1.
|
|
36
|
-
sql_glider-0.1.
|
|
37
|
-
sql_glider-0.1.
|
|
38
|
-
sql_glider-0.1.
|
|
35
|
+
sql_glider-0.1.21.dist-info/METADATA,sha256=tceZxbt-9yvvLlQn52RcmixUiPs62HhswbJFiGMksmA,29695
|
|
36
|
+
sql_glider-0.1.21.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
37
|
+
sql_glider-0.1.21.dist-info/entry_points.txt,sha256=HDuakHqHS5C0HFKsMIxMYmDU7-BLBGrnIJcYaVRu-s0,251
|
|
38
|
+
sql_glider-0.1.21.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
39
|
+
sql_glider-0.1.21.dist-info/RECORD,,
|
sqlglider/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
31
|
+
__version__ = version = '0.1.21'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 21)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
sqlglider/cli.py
CHANGED
|
@@ -1642,7 +1642,7 @@ def graph_query(
|
|
|
1642
1642
|
"text",
|
|
1643
1643
|
"--output-format",
|
|
1644
1644
|
"-f",
|
|
1645
|
-
help="Output format: 'text', 'json', or '
|
|
1645
|
+
help="Output format: 'text', 'json', 'csv', 'mermaid', 'mermaid-markdown', or 'dot'",
|
|
1646
1646
|
),
|
|
1647
1647
|
) -> None:
|
|
1648
1648
|
"""
|
|
@@ -1678,10 +1678,10 @@ def graph_query(
|
|
|
1678
1678
|
)
|
|
1679
1679
|
raise typer.Exit(1)
|
|
1680
1680
|
|
|
1681
|
-
if output_format not in ["text", "json", "csv"]:
|
|
1681
|
+
if output_format not in ["text", "json", "csv", "mermaid", "mermaid-markdown", "dot"]:
|
|
1682
1682
|
err_console.print(
|
|
1683
1683
|
f"[red]Error:[/red] Invalid output format '{output_format}'. "
|
|
1684
|
-
"Use 'text', 'json', or '
|
|
1684
|
+
"Use 'text', 'json', 'csv', 'mermaid', 'mermaid-markdown', or 'dot'."
|
|
1685
1685
|
)
|
|
1686
1686
|
raise typer.Exit(1)
|
|
1687
1687
|
|
|
@@ -1699,8 +1699,20 @@ def graph_query(
|
|
|
1699
1699
|
_format_query_result_text(result)
|
|
1700
1700
|
elif output_format == "json":
|
|
1701
1701
|
_format_query_result_json(result)
|
|
1702
|
-
|
|
1702
|
+
elif output_format == "csv":
|
|
1703
1703
|
_format_query_result_csv(result)
|
|
1704
|
+
elif output_format == "mermaid":
|
|
1705
|
+
from sqlglider.graph.diagram_formatters import MermaidFormatter
|
|
1706
|
+
|
|
1707
|
+
print(MermaidFormatter.format_query_result(result))
|
|
1708
|
+
elif output_format == "mermaid-markdown":
|
|
1709
|
+
from sqlglider.graph.diagram_formatters import MermaidMarkdownFormatter
|
|
1710
|
+
|
|
1711
|
+
print(MermaidMarkdownFormatter.format_query_result(result))
|
|
1712
|
+
elif output_format == "dot":
|
|
1713
|
+
from sqlglider.graph.diagram_formatters import DotFormatter
|
|
1714
|
+
|
|
1715
|
+
print(DotFormatter.format_query_result(result))
|
|
1704
1716
|
|
|
1705
1717
|
except FileNotFoundError as e:
|
|
1706
1718
|
err_console.print(f"[red]Error:[/red] {e}")
|
|
@@ -1795,6 +1807,91 @@ def _format_query_result_csv(result) -> None:
|
|
|
1795
1807
|
)
|
|
1796
1808
|
|
|
1797
1809
|
|
|
1810
|
+
@graph_app.command("visualize")
|
|
1811
|
+
def graph_visualize(
|
|
1812
|
+
graph_file: Path = typer.Argument(
|
|
1813
|
+
...,
|
|
1814
|
+
exists=True,
|
|
1815
|
+
help="Path to graph JSON file",
|
|
1816
|
+
),
|
|
1817
|
+
output_format: str = typer.Option(
|
|
1818
|
+
"mermaid",
|
|
1819
|
+
"--output-format",
|
|
1820
|
+
"-f",
|
|
1821
|
+
help="Diagram format: 'mermaid', 'mermaid-markdown', or 'dot'",
|
|
1822
|
+
),
|
|
1823
|
+
output_file: Optional[Path] = typer.Option(
|
|
1824
|
+
None,
|
|
1825
|
+
"--output-file",
|
|
1826
|
+
"-o",
|
|
1827
|
+
help="Write diagram to file instead of stdout",
|
|
1828
|
+
),
|
|
1829
|
+
) -> None:
|
|
1830
|
+
"""
|
|
1831
|
+
Visualize the entire lineage graph as a diagram.
|
|
1832
|
+
|
|
1833
|
+
Generates Mermaid or DOT (Graphviz) diagrams showing all nodes and edges
|
|
1834
|
+
in the graph for visualization tools.
|
|
1835
|
+
|
|
1836
|
+
Examples:
|
|
1837
|
+
|
|
1838
|
+
# Generate Mermaid diagram
|
|
1839
|
+
sqlglider graph visualize graph.json
|
|
1840
|
+
|
|
1841
|
+
# Generate DOT diagram for Graphviz
|
|
1842
|
+
sqlglider graph visualize graph.json -f dot
|
|
1843
|
+
|
|
1844
|
+
# Save to file
|
|
1845
|
+
sqlglider graph visualize graph.json -o lineage.mmd
|
|
1846
|
+
|
|
1847
|
+
# Render DOT to PNG with Graphviz
|
|
1848
|
+
sqlglider graph visualize graph.json -f dot -o lineage.dot
|
|
1849
|
+
"""
|
|
1850
|
+
from sqlglider.graph.diagram_formatters import (
|
|
1851
|
+
DotFormatter,
|
|
1852
|
+
MermaidFormatter,
|
|
1853
|
+
MermaidMarkdownFormatter,
|
|
1854
|
+
)
|
|
1855
|
+
from sqlglider.graph.serialization import load_graph
|
|
1856
|
+
|
|
1857
|
+
if output_format not in ["mermaid", "mermaid-markdown", "dot"]:
|
|
1858
|
+
err_console.print(
|
|
1859
|
+
f"[red]Error:[/red] Invalid output format '{output_format}'. "
|
|
1860
|
+
"Use 'mermaid', 'mermaid-markdown', or 'dot'."
|
|
1861
|
+
)
|
|
1862
|
+
raise typer.Exit(1)
|
|
1863
|
+
|
|
1864
|
+
try:
|
|
1865
|
+
graph = load_graph(graph_file)
|
|
1866
|
+
|
|
1867
|
+
if output_format == "mermaid":
|
|
1868
|
+
diagram = MermaidFormatter.format_full_graph(graph)
|
|
1869
|
+
elif output_format == "mermaid-markdown":
|
|
1870
|
+
diagram = MermaidMarkdownFormatter.format_full_graph(graph)
|
|
1871
|
+
else:
|
|
1872
|
+
diagram = DotFormatter.format_full_graph(graph)
|
|
1873
|
+
|
|
1874
|
+
if output_file:
|
|
1875
|
+
output_file.write_text(diagram, encoding="utf-8")
|
|
1876
|
+
console.print(
|
|
1877
|
+
f"[green]Success:[/green] Diagram written to {output_file}"
|
|
1878
|
+
)
|
|
1879
|
+
else:
|
|
1880
|
+
print(diagram)
|
|
1881
|
+
|
|
1882
|
+
except FileNotFoundError as e:
|
|
1883
|
+
err_console.print(f"[red]Error:[/red] {e}")
|
|
1884
|
+
raise typer.Exit(1)
|
|
1885
|
+
|
|
1886
|
+
except ValueError as e:
|
|
1887
|
+
err_console.print(f"[red]Error:[/red] {e}")
|
|
1888
|
+
raise typer.Exit(1)
|
|
1889
|
+
|
|
1890
|
+
except Exception as e:
|
|
1891
|
+
err_console.print(f"[red]Error:[/red] Unexpected error: {e}")
|
|
1892
|
+
raise typer.Exit(1)
|
|
1893
|
+
|
|
1894
|
+
|
|
1798
1895
|
@app.command()
|
|
1799
1896
|
def dissect(
|
|
1800
1897
|
sql_file: Annotated[
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Diagram formatters for lineage graphs (Mermaid and DOT/Graphviz)."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Set
|
|
5
|
+
|
|
6
|
+
from sqlglider.graph.models import LineageGraph
|
|
7
|
+
from sqlglider.graph.query import LineageQueryResult
|
|
8
|
+
|
|
9
|
+
# Color palette (muted jewel tones for light/dark mode compatibility)
|
|
10
|
+
QUERIED_FILL = "#e6a843"
|
|
11
|
+
QUERIED_STROKE = "#b8860b"
|
|
12
|
+
ROOT_FILL = "#4ecdc4"
|
|
13
|
+
ROOT_STROKE = "#2b9e96"
|
|
14
|
+
LEAF_FILL = "#c084fc"
|
|
15
|
+
LEAF_STROKE = "#7c3aed"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _sanitize_mermaid_id(identifier: str) -> str:
|
|
19
|
+
"""Sanitize an identifier for use as a Mermaid node ID.
|
|
20
|
+
|
|
21
|
+
Replaces non-alphanumeric characters with underscores.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
identifier: Raw node identifier (e.g., "schema.table.column")
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Sanitized ID safe for Mermaid syntax
|
|
28
|
+
"""
|
|
29
|
+
return re.sub(r"[^a-zA-Z0-9_]", "_", identifier)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _quote_dot_id(identifier: str) -> str:
|
|
33
|
+
"""Quote an identifier for use in DOT syntax.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
identifier: Raw node identifier
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Double-quoted identifier with internal quotes escaped
|
|
40
|
+
"""
|
|
41
|
+
escaped = identifier.replace("\\", "\\\\").replace('"', '\\"')
|
|
42
|
+
return f'"{escaped}"'
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _collect_query_edges(result: LineageQueryResult) -> Set[tuple[str, str]]:
|
|
46
|
+
"""Extract unique directed edges from all paths in a query result.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
result: Query result containing paths
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Set of (source, target) identifier pairs
|
|
53
|
+
"""
|
|
54
|
+
edges: Set[tuple[str, str]] = set()
|
|
55
|
+
for node in result.related_columns:
|
|
56
|
+
for path in node.paths:
|
|
57
|
+
for i in range(len(path.nodes) - 1):
|
|
58
|
+
edges.add((path.nodes[i], path.nodes[i + 1]))
|
|
59
|
+
return edges
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _collect_query_nodes(result: LineageQueryResult) -> Set[str]:
|
|
63
|
+
"""Extract all unique node identifiers from query result paths.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
result: Query result containing paths
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Set of node identifiers
|
|
70
|
+
"""
|
|
71
|
+
nodes: Set[str] = set()
|
|
72
|
+
for node in result.related_columns:
|
|
73
|
+
for path in node.paths:
|
|
74
|
+
nodes.update(path.nodes)
|
|
75
|
+
# Always include the queried column itself
|
|
76
|
+
nodes.add(result.query_column)
|
|
77
|
+
return nodes
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class MermaidFormatter:
|
|
81
|
+
"""Format lineage graphs and query results as Mermaid diagrams."""
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def format_full_graph(graph: LineageGraph) -> str:
|
|
85
|
+
"""Format complete lineage graph as a Mermaid flowchart.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
graph: LineageGraph with all nodes and edges
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Mermaid diagram string (flowchart TD syntax)
|
|
92
|
+
"""
|
|
93
|
+
lines = ["flowchart TD"]
|
|
94
|
+
|
|
95
|
+
if not graph.nodes and not graph.edges:
|
|
96
|
+
return "\n".join(lines)
|
|
97
|
+
|
|
98
|
+
# Declare nodes with labels
|
|
99
|
+
for node in graph.nodes:
|
|
100
|
+
node_id = _sanitize_mermaid_id(node.identifier)
|
|
101
|
+
lines.append(f' {node_id}["{node.identifier}"]')
|
|
102
|
+
|
|
103
|
+
# Add edges
|
|
104
|
+
for edge in graph.edges:
|
|
105
|
+
src = _sanitize_mermaid_id(edge.source_node)
|
|
106
|
+
tgt = _sanitize_mermaid_id(edge.target_node)
|
|
107
|
+
lines.append(f" {src} --> {tgt}")
|
|
108
|
+
|
|
109
|
+
return "\n".join(lines)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def format_query_result(result: LineageQueryResult) -> str:
|
|
113
|
+
"""Format query result as a Mermaid flowchart with styling.
|
|
114
|
+
|
|
115
|
+
The queried column is highlighted in amber, root nodes in teal,
|
|
116
|
+
and leaf nodes in violet. A legend subgraph is included.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
result: LineageQueryResult from upstream/downstream query
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Mermaid diagram string with style directives and legend
|
|
123
|
+
"""
|
|
124
|
+
lines = ["flowchart TD"]
|
|
125
|
+
|
|
126
|
+
if not result.related_columns:
|
|
127
|
+
# Show just the queried node
|
|
128
|
+
node_id = _sanitize_mermaid_id(result.query_column)
|
|
129
|
+
lines.append(f' {node_id}["{result.query_column}"]')
|
|
130
|
+
return "\n".join(lines)
|
|
131
|
+
|
|
132
|
+
all_nodes = _collect_query_nodes(result)
|
|
133
|
+
edges = _collect_query_edges(result)
|
|
134
|
+
|
|
135
|
+
# Declare nodes
|
|
136
|
+
for identifier in sorted(all_nodes):
|
|
137
|
+
node_id = _sanitize_mermaid_id(identifier)
|
|
138
|
+
lines.append(f' {node_id}["{identifier}"]')
|
|
139
|
+
|
|
140
|
+
# Add edges
|
|
141
|
+
for src, tgt in sorted(edges):
|
|
142
|
+
lines.append(
|
|
143
|
+
f" {_sanitize_mermaid_id(src)} --> {_sanitize_mermaid_id(tgt)}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Style directives
|
|
147
|
+
queried_id = _sanitize_mermaid_id(result.query_column)
|
|
148
|
+
lines.append(
|
|
149
|
+
f" style {queried_id} fill:{QUERIED_FILL},stroke:{QUERIED_STROKE},stroke-width:3px"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
root_ids = set()
|
|
153
|
+
leaf_ids = set()
|
|
154
|
+
for node in result.related_columns:
|
|
155
|
+
if node.is_root:
|
|
156
|
+
root_ids.add(_sanitize_mermaid_id(node.identifier))
|
|
157
|
+
if node.is_leaf:
|
|
158
|
+
leaf_ids.add(_sanitize_mermaid_id(node.identifier))
|
|
159
|
+
|
|
160
|
+
for rid in sorted(root_ids):
|
|
161
|
+
if rid != queried_id:
|
|
162
|
+
lines.append(f" style {rid} fill:{ROOT_FILL},stroke:{ROOT_STROKE}")
|
|
163
|
+
|
|
164
|
+
for lid in sorted(leaf_ids):
|
|
165
|
+
if lid != queried_id and lid not in root_ids:
|
|
166
|
+
lines.append(f" style {lid} fill:{LEAF_FILL},stroke:{LEAF_STROKE}")
|
|
167
|
+
|
|
168
|
+
# Legend
|
|
169
|
+
lines.append("")
|
|
170
|
+
lines.append(" subgraph Legend")
|
|
171
|
+
lines.append(' legend_queried["Queried Column"]')
|
|
172
|
+
lines.append(' legend_root["Root (no upstream)"]')
|
|
173
|
+
lines.append(' legend_leaf["Leaf (no downstream)"]')
|
|
174
|
+
lines.append(" end")
|
|
175
|
+
lines.append(
|
|
176
|
+
f" style legend_queried fill:{QUERIED_FILL},stroke:{QUERIED_STROKE},stroke-width:3px"
|
|
177
|
+
)
|
|
178
|
+
lines.append(f" style legend_root fill:{ROOT_FILL},stroke:{ROOT_STROKE}")
|
|
179
|
+
lines.append(f" style legend_leaf fill:{LEAF_FILL},stroke:{LEAF_STROKE}")
|
|
180
|
+
|
|
181
|
+
return "\n".join(lines)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class MermaidMarkdownFormatter:
|
|
185
|
+
"""Format lineage graphs and query results as Mermaid diagrams wrapped in markdown code fences."""
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def format_full_graph(graph: LineageGraph) -> str:
|
|
189
|
+
"""Format complete lineage graph as a Mermaid diagram in a markdown code block.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
graph: LineageGraph with all nodes and edges
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Markdown string with fenced Mermaid diagram
|
|
196
|
+
"""
|
|
197
|
+
mermaid = MermaidFormatter.format_full_graph(graph)
|
|
198
|
+
return f"```mermaid\n{mermaid}\n```"
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def format_query_result(result: LineageQueryResult) -> str:
|
|
202
|
+
"""Format query result as a Mermaid diagram in a markdown code block.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
result: LineageQueryResult from upstream/downstream query
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Markdown string with fenced Mermaid diagram
|
|
209
|
+
"""
|
|
210
|
+
mermaid = MermaidFormatter.format_query_result(result)
|
|
211
|
+
return f"```mermaid\n{mermaid}\n```"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class DotFormatter:
|
|
215
|
+
"""Format lineage graphs and query results as DOT (Graphviz) diagrams."""
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def format_full_graph(graph: LineageGraph) -> str:
|
|
219
|
+
"""Format complete lineage graph as a DOT digraph.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
graph: LineageGraph with all nodes and edges
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
DOT diagram string
|
|
226
|
+
"""
|
|
227
|
+
lines = [
|
|
228
|
+
"digraph lineage {",
|
|
229
|
+
" rankdir=LR;",
|
|
230
|
+
" node [shape=box, style=rounded];",
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
if not graph.nodes and not graph.edges:
|
|
234
|
+
lines.append("}")
|
|
235
|
+
return "\n".join(lines)
|
|
236
|
+
|
|
237
|
+
# Declare nodes
|
|
238
|
+
for node in graph.nodes:
|
|
239
|
+
lines.append(f" {_quote_dot_id(node.identifier)};")
|
|
240
|
+
|
|
241
|
+
# Add edges
|
|
242
|
+
for edge in graph.edges:
|
|
243
|
+
src = _quote_dot_id(edge.source_node)
|
|
244
|
+
tgt = _quote_dot_id(edge.target_node)
|
|
245
|
+
lines.append(f" {src} -> {tgt};")
|
|
246
|
+
|
|
247
|
+
lines.append("}")
|
|
248
|
+
return "\n".join(lines)
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def format_query_result(result: LineageQueryResult) -> str:
|
|
252
|
+
"""Format query result as a DOT digraph with styling.
|
|
253
|
+
|
|
254
|
+
The queried column is highlighted in amber, root nodes in teal,
|
|
255
|
+
and leaf nodes in violet. A legend subgraph is included.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
result: LineageQueryResult from upstream/downstream query
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
DOT diagram string with node attributes and legend
|
|
262
|
+
"""
|
|
263
|
+
lines = [
|
|
264
|
+
"digraph lineage {",
|
|
265
|
+
" rankdir=LR;",
|
|
266
|
+
" node [shape=box, style=rounded];",
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
if not result.related_columns:
|
|
270
|
+
qid = _quote_dot_id(result.query_column)
|
|
271
|
+
lines.append(
|
|
272
|
+
f' {qid} [style="rounded,filled", fillcolor="{QUERIED_FILL}"];'
|
|
273
|
+
)
|
|
274
|
+
lines.append("}")
|
|
275
|
+
return "\n".join(lines)
|
|
276
|
+
|
|
277
|
+
all_nodes = _collect_query_nodes(result)
|
|
278
|
+
edges = _collect_query_edges(result)
|
|
279
|
+
|
|
280
|
+
# Build styling lookup
|
|
281
|
+
root_ids: Set[str] = set()
|
|
282
|
+
leaf_ids: Set[str] = set()
|
|
283
|
+
for node in result.related_columns:
|
|
284
|
+
if node.is_root:
|
|
285
|
+
root_ids.add(node.identifier)
|
|
286
|
+
if node.is_leaf:
|
|
287
|
+
leaf_ids.add(node.identifier)
|
|
288
|
+
|
|
289
|
+
# Declare nodes with styling
|
|
290
|
+
for identifier in sorted(all_nodes):
|
|
291
|
+
qid = _quote_dot_id(identifier)
|
|
292
|
+
if identifier == result.query_column:
|
|
293
|
+
lines.append(
|
|
294
|
+
f' {qid} [style="rounded,filled", fillcolor="{QUERIED_FILL}"];'
|
|
295
|
+
)
|
|
296
|
+
elif identifier in root_ids:
|
|
297
|
+
lines.append(
|
|
298
|
+
f' {qid} [style="rounded,filled", fillcolor="{ROOT_FILL}"];'
|
|
299
|
+
)
|
|
300
|
+
elif identifier in leaf_ids:
|
|
301
|
+
lines.append(
|
|
302
|
+
f' {qid} [style="rounded,filled", fillcolor="{LEAF_FILL}"];'
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
lines.append(f" {qid};")
|
|
306
|
+
|
|
307
|
+
# Add edges
|
|
308
|
+
for src, tgt in sorted(edges):
|
|
309
|
+
lines.append(f" {_quote_dot_id(src)} -> {_quote_dot_id(tgt)};")
|
|
310
|
+
|
|
311
|
+
# Legend
|
|
312
|
+
lines.append("")
|
|
313
|
+
lines.append(" subgraph cluster_legend {")
|
|
314
|
+
lines.append(' label="Legend";')
|
|
315
|
+
lines.append(" style=dashed;")
|
|
316
|
+
lines.append(
|
|
317
|
+
f' legend_queried [label="Queried Column", style="rounded,filled", fillcolor="{QUERIED_FILL}"];'
|
|
318
|
+
)
|
|
319
|
+
lines.append(
|
|
320
|
+
f' legend_root [label="Root (no upstream)", style="rounded,filled", fillcolor="{ROOT_FILL}"];'
|
|
321
|
+
)
|
|
322
|
+
lines.append(
|
|
323
|
+
f' legend_leaf [label="Leaf (no downstream)", style="rounded,filled", fillcolor="{LEAF_FILL}"];'
|
|
324
|
+
)
|
|
325
|
+
lines.append(" legend_queried -> legend_root [style=invis];")
|
|
326
|
+
lines.append(" legend_root -> legend_leaf [style=invis];")
|
|
327
|
+
lines.append(" }")
|
|
328
|
+
|
|
329
|
+
lines.append("}")
|
|
330
|
+
return "\n".join(lines)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|