sql-code-graph 1.34.4__py3-none-any.whl → 1.35.1__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_code_graph-1.34.4.dist-info → sql_code_graph-1.35.1.dist-info}/METADATA +1 -1
- {sql_code_graph-1.34.4.dist-info → sql_code_graph-1.35.1.dist-info}/RECORD +15 -8
- sqlcg/__init__.py +1 -1
- sqlcg/cli/commands/viz.py +102 -0
- sqlcg/cli/main.py +2 -0
- sqlcg/core/config.py +38 -0
- sqlcg/parsers/ansi_parser.py +35 -16
- sqlcg/viz/__init__.py +10 -0
- sqlcg/viz/assets/force-graph.min.js +5 -0
- sqlcg/viz/assets/template.html +274 -0
- sqlcg/viz/data.py +141 -0
- sqlcg/viz/render.py +79 -0
- sqlcg/viz/tags.py +149 -0
- {sql_code_graph-1.34.4.dist-info → sql_code_graph-1.35.1.dist-info}/WHEEL +0 -0
- {sql_code_graph-1.34.4.dist-info → sql_code_graph-1.35.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sql-code-graph
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.35.1
|
|
4
4
|
Summary: SQL code graph analyzer and lineage tracer
|
|
5
5
|
Project-URL: Homepage, https://github.com/Warhorze/sql-code-graph
|
|
6
6
|
Project-URL: Repository, https://github.com/Warhorze/sql-code-graph
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
sqlcg/__init__.py,sha256=
|
|
1
|
+
sqlcg/__init__.py,sha256=VahDFz44BfVysbx4bUBilcYdqMPT5hNDCMqoIkgonw0,116
|
|
2
2
|
sqlcg/__main__.py,sha256=1YoFLcqEgTwYq1J3TbUwpkdG0zeeLIf2fJvwWI-CLFU,109
|
|
3
3
|
sqlcg/cli/__init__.py,sha256=W8fD0LpMq2xm_5WKGNMvJh2WBL1ho5E8hUeAqXQYT1g,28
|
|
4
4
|
sqlcg/cli/coverage.py,sha256=Xm9ITzZDHv2mJ70Q5jCacVuhDStVrE3gq12_-Ypvtd8,43823
|
|
5
|
-
sqlcg/cli/main.py,sha256=
|
|
5
|
+
sqlcg/cli/main.py,sha256=K7CDIJ1tz7kxySGLeuKGZoTinucUrhINCxs_zieMmsg,2526
|
|
6
6
|
sqlcg/cli/commands/__init__.py,sha256=oSHtr6VD-jNubOjuCQyZj2tBppjMEpQDh-IGQ8of9eA,30
|
|
7
7
|
sqlcg/cli/commands/analyze.py,sha256=lC7QeBKljWVpKQ-x8BOF5Ok6y4jd329U1aBGrp159OA,31046
|
|
8
8
|
sqlcg/cli/commands/catalog.py,sha256=xCIWp_9xcDPHIMUB0xrZR3-G0WMvKbfq4yMwe2sDT2g,12670
|
|
@@ -16,9 +16,10 @@ sqlcg/cli/commands/mcp.py,sha256=drh5udcroZFddmJAG3q9JWxekDqTO9H4xC-oIqiis-Y,148
|
|
|
16
16
|
sqlcg/cli/commands/reindex.py,sha256=6j1QL3TjrsnWaFSIKvpGJ3LF8CoCjAbue0UFtmk9FS0,22704
|
|
17
17
|
sqlcg/cli/commands/report.py,sha256=JU0qjyMxwOukE7bN3XvvIzOI7zMg_Gsnvk_8F6pKNpA,4915
|
|
18
18
|
sqlcg/cli/commands/uninstall.py,sha256=WrA1FnINxnd6mmE4-_QBK0aHBnibstJeAT8swnQKG4M,8962
|
|
19
|
+
sqlcg/cli/commands/viz.py,sha256=2GAP6YSNuFXmWM8D8IohkaO5GJ3nj2KLdA0kHLx137M,3779
|
|
19
20
|
sqlcg/cli/commands/watch.py,sha256=_OvUQ4m_Pp2lAzI_f9UX2Gwb56zbsaSC4Tj8h51uciA,2806
|
|
20
21
|
sqlcg/core/__init__.py,sha256=dXvLWpbQ72f5CM6sKSvBDnEGqGuIZaN5MmHyD8Vf1aA,154
|
|
21
|
-
sqlcg/core/config.py,sha256=
|
|
22
|
+
sqlcg/core/config.py,sha256=dOQUQCsMGgT8Vp8gIfLdmMasB2IozGvHpAsjftCyDXw,20552
|
|
22
23
|
sqlcg/core/duckdb_backend.py,sha256=zPsxiO2_qcE6axVZu8P3gUNHnJ1cgCCFv1XXy_t_LEw,42298
|
|
23
24
|
sqlcg/core/freshness.py,sha256=Cm-w7jACPrt8TIstZpCzDJpYsHCiBo63ni5js8wCGbg,6248
|
|
24
25
|
sqlcg/core/graph_db.py,sha256=3iZ1J32cEn7_cVhNA1tLiNuJHCyatN4F40HXLLfmwHY,10815
|
|
@@ -43,7 +44,7 @@ sqlcg/lineage/schema_resolver.py,sha256=iXt6LYF6UVWsGUpcfbmjmGn9wCgXl721lTGf_8Aa
|
|
|
43
44
|
sqlcg/metrics/__init__.py,sha256=hLJ6wm4St8qqYwKh3o9QG7lcEt1BEYM31ccqO9tGpIg,133
|
|
44
45
|
sqlcg/metrics/store.py,sha256=KuDtxvyAgug9_KtiSCpvgKM2VZM7VSaI3D11uMLjJJk,10604
|
|
45
46
|
sqlcg/parsers/__init__.py,sha256=AamA8wBbDZV9_zEtZCI4Hyen5UAVKHmBwjTghTt2PZE,785
|
|
46
|
-
sqlcg/parsers/ansi_parser.py,sha256=
|
|
47
|
+
sqlcg/parsers/ansi_parser.py,sha256=RX6eVj7gt1qmsHNJLAF_a4jyW3RCI5W2oF4rd53cKNg,39336
|
|
47
48
|
sqlcg/parsers/base.py,sha256=Q2X_qN7DmGFU2oA6Z9fcxyjpkeb6lOsia4BcwPvwNLo,97806
|
|
48
49
|
sqlcg/parsers/bigquery_parser.py,sha256=mOnWTfXB_Dp4JwFE1PVYOB6CDPf5nYE0Dea8kJCl9uQ,2827
|
|
49
50
|
sqlcg/parsers/postgres_parser.py,sha256=lYfUpQY6j4Qm7ndXBtXbgPoGzYqYddWt5YeFnWKdA6I,946
|
|
@@ -65,7 +66,13 @@ sqlcg/utils/__init__.py,sha256=--iqt5ThTXmT8Wz7da8hs3n0zDfYPl8P-z5OgRJ_77E,154
|
|
|
65
66
|
sqlcg/utils/hashing.py,sha256=H25-sYfxHKb3_IERFnHyAIYNiXN470Oqo5sJT_D3YOA,438
|
|
66
67
|
sqlcg/utils/ignore.py,sha256=6__DfiYgynI2b8nH52O2BzI1EZcZ_LRazhqEOJVGGcE,1955
|
|
67
68
|
sqlcg/utils/logging.py,sha256=u0fCmYsLj9o81vawm3xZTHaw68GQYVm7JxG-gP81u8A,840
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
sqlcg/viz/__init__.py,sha256=ha-RMeqhhOoAsDQOfOjCcO0mnDKOU8MgHlK_Yxs4O8k,455
|
|
70
|
+
sqlcg/viz/data.py,sha256=deLWOZBgewM1x-kk1NhUnt0mswyt0nzqSevHzBCOHHc,5899
|
|
71
|
+
sqlcg/viz/render.py,sha256=BINkGbJbbb_iqhrkN795RaQsdg8nqCiJtsEFF1yo22Y,2737
|
|
72
|
+
sqlcg/viz/tags.py,sha256=6zRnGlHjuGmEeB6yN1uhzm8rqL7ZGoyL1Ki7jI5oM6A,5368
|
|
73
|
+
sqlcg/viz/assets/force-graph.min.js,sha256=jNdYdDdrYiUdUlElxRkolPBt30rstQk2q15Q32VVdzc,177272
|
|
74
|
+
sqlcg/viz/assets/template.html,sha256=9_j-mvo1ZxwgiJPDdVrNmca37dTrTjjYVd3977u-DxE,12294
|
|
75
|
+
sql_code_graph-1.35.1.dist-info/METADATA,sha256=YOc2OzDrdadgZdpmY3QhwueUwScKj39svzKO4dgd9Q0,17791
|
|
76
|
+
sql_code_graph-1.35.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
77
|
+
sql_code_graph-1.35.1.dist-info/entry_points.txt,sha256=Wfe49sVzV9p4eVFGo5RxcV-frr3HOP0yzzst8JBxQLQ,46
|
|
78
|
+
sql_code_graph-1.35.1.dist-info/RECORD,,
|
sqlcg/__init__.py
CHANGED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""The sqlcg viz command — generate a self-contained graph-explorer HTML.
|
|
2
|
+
|
|
3
|
+
Plan: plan/sprints/feature_graph_viz.md §"CLI signature".
|
|
4
|
+
|
|
5
|
+
Queries the live graph (via the server-aware ``run_read_routed`` seam),
|
|
6
|
+
attaches per-table label facets (tags/jobs) from BYO CSVs, and writes ONE
|
|
7
|
+
self-contained ``.html`` with the force-graph library inlined — openable on
|
|
8
|
+
Windows via ``file://``. Touches no parser/indexer/lineage code.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
|
|
18
|
+
from sqlcg.core.config import get_viz_schemas
|
|
19
|
+
from sqlcg.utils.logging import getLogger
|
|
20
|
+
from sqlcg.viz.data import build_viz_data
|
|
21
|
+
from sqlcg.viz.render import DEFAULT_VIZ_OUT, render_html
|
|
22
|
+
from sqlcg.viz.tags import FacetMap, load_facet, resolve_labels
|
|
23
|
+
|
|
24
|
+
logger = getLogger(__name__)
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _facet_payload(facet: FacetMap) -> dict:
|
|
29
|
+
"""Shape a FacetMap for baking: ``{labels, colors}``."""
|
|
30
|
+
return {"labels": facet.labels, "colors": facet.colors}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def viz_cmd(
|
|
34
|
+
tags: Path | None = typer.Option( # noqa: B008
|
|
35
|
+
None,
|
|
36
|
+
"--tags",
|
|
37
|
+
help=(
|
|
38
|
+
"BYO tags CSV (pattern,label[,color]; header required). The 'tag' "
|
|
39
|
+
"facet drives the legend swatches (color + filter). Patterns are "
|
|
40
|
+
"fnmatch globs (*, ?, [seq]) matched case-insensitively against the "
|
|
41
|
+
"qualified table name. Many-to-many. Omit to hide the tag legend."
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
jobs: Path | None = typer.Option( # noqa: B008
|
|
45
|
+
None,
|
|
46
|
+
"--jobs",
|
|
47
|
+
help=(
|
|
48
|
+
"BYO jobs CSV, same shape as --tags. The 'job' facet drives the job "
|
|
49
|
+
"dropdown (filter only). Omit to leave the dropdown empty."
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
out: Path | None = typer.Option( # noqa: B008
|
|
53
|
+
None,
|
|
54
|
+
"--out",
|
|
55
|
+
help=f"Output HTML path (default: {DEFAULT_VIZ_OUT} in the current directory).",
|
|
56
|
+
),
|
|
57
|
+
config_dir: Path = typer.Option( # noqa: B008
|
|
58
|
+
Path("."),
|
|
59
|
+
"--config-dir",
|
|
60
|
+
help="Directory searched for .sqlcg.toml ([sqlcg.viz] schemas).",
|
|
61
|
+
),
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Generate a self-contained graph-explorer HTML from the live graph.
|
|
64
|
+
|
|
65
|
+
Reads the graph via the server-aware routed-read path (never a direct DB
|
|
66
|
+
open). The emitted file inlines the force-graph library plus the baked node/
|
|
67
|
+
edge/adjacency data and the tag/job facets — no external resources — so it
|
|
68
|
+
opens by double-click on Windows.
|
|
69
|
+
|
|
70
|
+
The client-side filter composes schema ∩ kind ∩ tag (multi-select per facet,
|
|
71
|
+
union within a facet, intersection across) plus a job dropdown; an edge
|
|
72
|
+
renders only when both endpoints are visible. Node kinds table/view/temp are
|
|
73
|
+
ON by default, cte/derived OFF (switchable).
|
|
74
|
+
"""
|
|
75
|
+
out_path = out if out is not None else Path(DEFAULT_VIZ_OUT)
|
|
76
|
+
|
|
77
|
+
schemas = get_viz_schemas(config_dir)
|
|
78
|
+
|
|
79
|
+
data = build_viz_data()
|
|
80
|
+
|
|
81
|
+
tag_facet = load_facet(tags, "tag") if tags is not None else FacetMap(name="tag")
|
|
82
|
+
job_facet = load_facet(jobs, "job") if jobs is not None else FacetMap(name="job")
|
|
83
|
+
|
|
84
|
+
# Attach resolved facet labels to every node (m2m glob resolution).
|
|
85
|
+
for node in data["nodes"]:
|
|
86
|
+
node["tags"] = resolve_labels(node["id"], tag_facet)
|
|
87
|
+
node["jobs"] = resolve_labels(node["id"], job_facet)
|
|
88
|
+
|
|
89
|
+
html = render_html(
|
|
90
|
+
data,
|
|
91
|
+
_facet_payload(tag_facet),
|
|
92
|
+
_facet_payload(job_facet),
|
|
93
|
+
schemas,
|
|
94
|
+
)
|
|
95
|
+
out_path.write_text(html, encoding="utf-8")
|
|
96
|
+
|
|
97
|
+
schema_count = len(schemas) if schemas else len({n["schema"] for n in data["nodes"]})
|
|
98
|
+
console.print(
|
|
99
|
+
f"Wrote {out_path} — {len(data['nodes'])} nodes, "
|
|
100
|
+
f"{len(data['links'])} edges, {len(tag_facet.labels)} tags, "
|
|
101
|
+
f"{len(job_facet.labels)} jobs, {schema_count} schemas."
|
|
102
|
+
)
|
sqlcg/cli/main.py
CHANGED
|
@@ -16,6 +16,7 @@ from sqlcg.cli.commands import (
|
|
|
16
16
|
reindex,
|
|
17
17
|
report,
|
|
18
18
|
uninstall,
|
|
19
|
+
viz,
|
|
19
20
|
watch,
|
|
20
21
|
)
|
|
21
22
|
|
|
@@ -84,6 +85,7 @@ app.command("reindex")(reindex.reindex_cmd)
|
|
|
84
85
|
app.command("watch")(watch.watch_cmd)
|
|
85
86
|
app.command("gain")(gain.gain_cmd)
|
|
86
87
|
app.command("report")(report.report_cmd)
|
|
88
|
+
app.command("viz")(viz.viz_cmd)
|
|
87
89
|
app.command("install")(install.install_cmd)
|
|
88
90
|
app.command("uninstall")(uninstall.uninstall_cmd)
|
|
89
91
|
|
sqlcg/core/config.py
CHANGED
|
@@ -151,6 +151,44 @@ def get_noise_filter_patterns(path: Path) -> list[str]:
|
|
|
151
151
|
return default_patterns
|
|
152
152
|
|
|
153
153
|
|
|
154
|
+
def get_viz_schemas(path: Path) -> list[str]:
|
|
155
|
+
"""Get the curated viz schema list from .sqlcg.toml.
|
|
156
|
+
|
|
157
|
+
Reads [sqlcg.viz] -> schemas (a list of schema-name strings) from
|
|
158
|
+
.sqlcg.toml. Used by ``sqlcg viz`` to decide which schemas get a distinct
|
|
159
|
+
hue / appear as multi-select facets. When the block/key is absent or not a
|
|
160
|
+
list, returns ``[]`` and the generator falls back to **data-derived**
|
|
161
|
+
schemas (every distinct schema present in the graph), preserving the
|
|
162
|
+
zero-config small-repo path::
|
|
163
|
+
|
|
164
|
+
[sqlcg.viz]
|
|
165
|
+
schemas = ["ba", "da", "ia_analytics"]
|
|
166
|
+
|
|
167
|
+
This reader mirrors ``get_schema_aliases`` / ``get_noise_filter_patterns``
|
|
168
|
+
exactly (same .sqlcg.toml open, same ``except Exception: pass`` guard, same
|
|
169
|
+
fallback shape). It is presentation-only config; it never touches indexing
|
|
170
|
+
or lineage.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
path: Root directory to search for .sqlcg.toml
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
List of configured schema names. ``[]`` when the key is absent or
|
|
177
|
+
malformed (caller derives schemas from the data).
|
|
178
|
+
"""
|
|
179
|
+
config_file = Path(path) / ".sqlcg.toml"
|
|
180
|
+
if config_file.exists():
|
|
181
|
+
try:
|
|
182
|
+
with open(config_file, "rb") as f:
|
|
183
|
+
config = tomllib.load(f)
|
|
184
|
+
raw = config.get("sqlcg", {}).get("viz", {}).get("schemas")
|
|
185
|
+
if isinstance(raw, list):
|
|
186
|
+
return [s for s in raw if isinstance(s, str)]
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
|
|
154
192
|
def get_ignored_tables(path: Path) -> list[str]:
|
|
155
193
|
"""Get explicitly-ignored qualified table names from .sqlcg.toml.
|
|
156
194
|
|
sqlcg/parsers/ansi_parser.py
CHANGED
|
@@ -551,25 +551,44 @@ class AnsiParser(SqlParser):
|
|
|
551
551
|
# measurement artifact (96.1%/4,544 of 4,727 on the DWH corpus).
|
|
552
552
|
can_have_lineage = self._can_have_column_lineage(stmt)
|
|
553
553
|
|
|
554
|
+
# Phantom source-node gate (#161): a non-table/non-view CREATE
|
|
555
|
+
# (SEQUENCE / STAGE / FILE FORMAT / SCHEMA / INDEX / PROCEDURE / ...) has
|
|
556
|
+
# target=None and no data-flow source. Its only Table node is its own
|
|
557
|
+
# object name, which _fallback_table_scan below would otherwise scoop into
|
|
558
|
+
# `sources`, leaking a phantom SqlTable SOURCE node + SELECTS_FROM edge
|
|
559
|
+
# into the graph. #159 fixed the kind LABEL (-> OTHER); this closes the
|
|
560
|
+
# source-node half. Keying off stmt.kind (the sqlglot AST value, e.g.
|
|
561
|
+
# 'SEQUENCE') keeps the gate independent of the _classify/QueryKind path.
|
|
562
|
+
# CREATE TABLE/VIEW ... AS SELECT keep kind in (TABLE, VIEW) and so retain
|
|
563
|
+
# their real sources. Real procedure lineage comes from the spliced inner
|
|
564
|
+
# DML (a separate node the gate never touches), so gating PROCEDURE is
|
|
565
|
+
# edge-neutral. This single gate feeds all three
|
|
566
|
+
# `referenced_tables.extend(query_node.sources)` sites
|
|
567
|
+
# (ansi_parser.py:246, snowflake_parser.py:784 / :944).
|
|
568
|
+
is_non_table_create = isinstance(stmt, exp.Create) and stmt.kind not in ("TABLE", "VIEW")
|
|
569
|
+
|
|
554
570
|
# Try to extract table references using scope analysis
|
|
555
571
|
try:
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
if root_scope is None:
|
|
559
|
-
from sqlglot.optimizer.scope import build_scope
|
|
560
|
-
|
|
561
|
-
root_scope = build_scope(stmt)
|
|
562
|
-
|
|
563
|
-
if root_scope:
|
|
564
|
-
sources = self._real_tables(root_scope)
|
|
572
|
+
if is_non_table_create:
|
|
573
|
+
sources = []
|
|
565
574
|
else:
|
|
566
|
-
#
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
575
|
+
# Use pre-built scope if provided, otherwise build it here (fallback)
|
|
576
|
+
root_scope = scope
|
|
577
|
+
if root_scope is None:
|
|
578
|
+
from sqlglot.optimizer.scope import build_scope
|
|
579
|
+
|
|
580
|
+
root_scope = build_scope(stmt)
|
|
581
|
+
|
|
582
|
+
if root_scope:
|
|
583
|
+
sources = self._real_tables(root_scope)
|
|
584
|
+
else:
|
|
585
|
+
# Fallback to basic table extraction
|
|
586
|
+
sources = [
|
|
587
|
+
r
|
|
588
|
+
for s in self._fallback_table_scan(stmt)
|
|
589
|
+
if (r := self._apply_table_alias(s)) is not None
|
|
590
|
+
]
|
|
591
|
+
parse_failed = can_have_lineage
|
|
573
592
|
except Exception as exc:
|
|
574
593
|
logger.debug("Failed to build scope for statement %d in %s: %s", stmt_index, path, exc)
|
|
575
594
|
sources = [
|
sqlcg/viz/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Self-contained graph-visualization generator for ``sqlcg viz``.
|
|
2
|
+
|
|
3
|
+
Plan: plan/sprints/feature_graph_viz.md
|
|
4
|
+
|
|
5
|
+
A pure read + render feature: queries the live DuckDB graph (via the
|
|
6
|
+
server-aware ``run_read_routed`` seam), resolves query-anchored edges to
|
|
7
|
+
table->table links, attaches per-table label facets (tags/jobs) from a BYO
|
|
8
|
+
CSV, and emits ONE self-contained HTML file with the force-graph library
|
|
9
|
+
inlined. No parser/indexer/lineage code is touched.
|
|
10
|
+
"""
|