sql-code-graph 1.34.3__py3-none-any.whl → 1.35.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sql-code-graph
3
- Version: 1.34.3
3
+ Version: 1.35.0
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,10 +1,10 @@
1
- sqlcg/__init__.py,sha256=rQsWa6I-2VM8CvG89ZxmHPH-4x9R0fhD8py7UiimDNs,116
1
+ sqlcg/__init__.py,sha256=2bXiRwcsTAFodsSZ65K7ditQHczrjRuDewuv8ncWvio,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=tlFvl-l9hWfZBP9dO2svZXvLDbh7T5731CFyctuVqDI,2485
5
+ sqlcg/cli/main.py,sha256=K7CDIJ1tz7kxySGLeuKGZoTinucUrhINCxs_zieMmsg,2526
6
6
  sqlcg/cli/commands/__init__.py,sha256=oSHtr6VD-jNubOjuCQyZj2tBppjMEpQDh-IGQ8of9eA,30
7
- sqlcg/cli/commands/analyze.py,sha256=XMax3uHyNEe8SIMEhiLBdtbQb8QUwncoQKTB2LIT3Vg,30000
7
+ sqlcg/cli/commands/analyze.py,sha256=lC7QeBKljWVpKQ-x8BOF5Ok6y4jd329U1aBGrp159OA,31046
8
8
  sqlcg/cli/commands/catalog.py,sha256=xCIWp_9xcDPHIMUB0xrZR3-G0WMvKbfq4yMwe2sDT2g,12670
9
9
  sqlcg/cli/commands/db.py,sha256=QEDPs0boeFe8ETE8BWe7I2JLSXROUI571SHXMTUqxh8,7982
10
10
  sqlcg/cli/commands/find.py,sha256=uMdG08bU-AvB9RqsTKPfdVnJet7b3Ebj0xbsjqTclYA,2678
@@ -16,17 +16,18 @@ 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=QxhIiF_let_ablMa-WttGbcAlCCDIpT4lR4tUVymIEI,19091
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
25
26
  sqlcg/core/jobs.py,sha256=Je-fCdSKRgiSsv1W8SgNAlp36a7t7-pJZ-qKPbka9OE,3298
26
27
  sqlcg/core/noise_match.py,sha256=8jxMBum0HB7wnVyS8hHaklO279QOSNKDmPtv-0RujD8,4781
27
28
  sqlcg/core/queries.cypher,sha256=cvPOVe5GUOzJN4bxUvDxNI--xIIP8gm42TR-gUnea4U,4685
28
- sqlcg/core/queries.py,sha256=mxpG_zBENG7kcoz3YolMvHv7s_HagWluakTUw3nYSts,3631
29
- sqlcg/core/queries.sql,sha256=gT8vl7CgOUZXjElpMZrHvuE_bs2ZJ4FLFA83EKaeRWM,15601
29
+ sqlcg/core/queries.py,sha256=QwQsjI2YJPipfw69k9iSu1jpeiOIPPtkegO4TVmAVaY,3703
30
+ sqlcg/core/queries.sql,sha256=-ZoMnNt-i2kojMHLAUHvQhCZURIgI9zkfh8J8QJo1iE,16863
30
31
  sqlcg/core/schema.cypher,sha256=StaJqIzOLwF2X9HYvfHSdGiYGFb_YkiHubPrTu-Qpmg,2898
31
32
  sqlcg/core/schema.py,sha256=pVGzf__czAddmz-fsXgXqq2DTxN9EKom5RvIHLctIPU,1519
32
33
  sqlcg/indexer/__init__.py,sha256=Wh20Unz2OHs1oIyWLrpurPAasF0BET2g4iXtNk7mh2U,56
@@ -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
- sql_code_graph-1.34.3.dist-info/METADATA,sha256=v9KwyFFICF79HrcBi4rTSlyRJdVMw1U0R3rZCdMmwDk,17791
69
- sql_code_graph-1.34.3.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
70
- sql_code_graph-1.34.3.dist-info/entry_points.txt,sha256=Wfe49sVzV9p4eVFGo5RxcV-frr3HOP0yzzst8JBxQLQ,46
71
- sql_code_graph-1.34.3.dist-info/RECORD,,
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.0.dist-info/METADATA,sha256=U3Aw3b4ufs83tQ2ApR2IH3VKLT5otMEWtFFrs5AXc9A,17791
76
+ sql_code_graph-1.35.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
77
+ sql_code_graph-1.35.0.dist-info/entry_points.txt,sha256=Wfe49sVzV9p4eVFGo5RxcV-frr3HOP0yzzst8JBxQLQ,46
78
+ sql_code_graph-1.35.0.dist-info/RECORD,,
sqlcg/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """SQL Code Graph - SQL lineage and dependency analysis tool."""
2
2
 
3
- __version__ = "1.34.3"
3
+ __version__ = "1.35.0"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -8,7 +8,10 @@ import typer
8
8
  from rich.console import Console
9
9
  from rich.table import Table
10
10
 
11
- from sqlcg.core.queries import GET_TABLE_EXTERNAL_CONSUMERS_QUERY
11
+ from sqlcg.core.queries import (
12
+ GET_TABLE_EXTERNAL_CONSUMERS_QUERY,
13
+ IMPACT_CONSUMERS_VIA_LINEAGE_QUERY,
14
+ )
12
15
  from sqlcg.server.read_client import resolved_repo_root, run_read_routed
13
16
 
14
17
  if TYPE_CHECKING:
@@ -312,8 +315,16 @@ def impact( # noqa: B008
312
315
  table: str = typer.Argument(..., help="Table name to analyze"), # noqa: B008
313
316
  raw: bool = typer.Option(False, "--raw", help="Disable noise filtering on results"), # noqa: B008
314
317
  ) -> None:
315
- """Show all queries impacted by a table."""
316
- results = run_read_routed(
318
+ """Show all queries impacted by a table.
319
+
320
+ One-hop "who reads this table" — the union of direct SELECTS_FROM consumers
321
+ and consumers that read the table only via COLUMN_LINEAGE / STAR_SOURCE
322
+ (CTE-wrapped / promoted-intermediate reads, e.g. the live DWH node
323
+ ``ba.all_rows_in_selection``). Bug #6 fix: brings ``impact`` to the same
324
+ one-hop consumer completeness ``find_table_usages`` already has. Stays
325
+ one-hop (NOT transitive — that is ``downstream``'s job).
326
+ """
327
+ direct = run_read_routed(
317
328
  "SELECT DISTINCT q.id AS id, q.kind AS kind, q.target_table AS target"
318
329
  ' FROM "SqlTable" t'
319
330
  ' JOIN "SELECTS_FROM" sf ON sf.dst_key = t.qualified'
@@ -321,6 +332,17 @@ def impact( # noqa: B008
321
332
  " WHERE t.qualified = ? LIMIT 100",
322
333
  {"t": table},
323
334
  )
335
+ # Consumers reachable only via COLUMN_LINEAGE / STAR_SOURCE. Two "?" both
336
+ # bound to the qualified table name: _params_list returns list(values()) in
337
+ # insertion order, so the ordered dict {"t":..., "t2":...} binds correctly.
338
+ via = run_read_routed(
339
+ IMPACT_CONSUMERS_VIA_LINEAGE_QUERY,
340
+ {"t": table, "t2": table},
341
+ )
342
+ seen = {r["id"] for r in direct}
343
+ results = direct + [r for r in via if r["id"] not in seen]
344
+ # Preserve the existing 100-row bound on the merged consumer set.
345
+ results = results[:100]
324
346
  if not raw:
325
347
  from sqlcg.server.noise_filter import NoiseFilter
326
348
 
@@ -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/core/queries.py CHANGED
@@ -72,3 +72,4 @@ GET_TABLES_EXTERNAL_CONSUMERS_BATCH_QUERY = _Q["GET_TABLES_EXTERNAL_CONSUMERS_BA
72
72
  COUNT_EXTERNAL_CONSUMERS_QUERY = _Q["COUNT_EXTERNAL_CONSUMERS"]
73
73
  GET_TABLE_READS_ADJACENCY_QUERY = _Q["GET_TABLE_READS_ADJACENCY"]
74
74
  GET_PRODUCER_TABLES_QUERY = _Q["GET_PRODUCER_TABLES"]
75
+ IMPACT_CONSUMERS_VIA_LINEAGE_QUERY = _Q["IMPACT_CONSUMERS_VIA_LINEAGE"]
sqlcg/core/queries.sql CHANGED
@@ -413,3 +413,26 @@ WHERE t.name = ?
413
413
  SELECT DISTINCT target_table AS producer_table
414
414
  FROM "SqlQuery"
415
415
  WHERE target_table <> '' AND target_table IS NOT NULL
416
+
417
+ -- IMPACT_CONSUMERS_VIA_LINEAGE
418
+ -- Bug #6: consumers that read a table only via COLUMN_LINEAGE or STAR_SOURCE
419
+ -- (CTE-wrapped / promoted-intermediate reads that emit no top-level SELECTS_FROM
420
+ -- edge, e.g. the live DWH cross-file-promoted node ba.all_rows_in_selection:
421
+ -- kind='table', non-namespaced, 0 SELECTS_FROM in-edges, 55 COLUMN_LINEAGE
422
+ -- out-edges). Returns the same (id, kind, target) shape as the SELECTS_FROM-only
423
+ -- impact query in analyze.py:impact() so the two result sets merge + dedup on q.id.
424
+ -- Both branches key on the QUALIFIED table name (impact() keys on t.qualified):
425
+ -- COLUMN_LINEAGE branch -> src.table_qualified; STAR_SOURCE branch -> ss.dst_key.
426
+ -- The STAR_SOURCE branch closes the STAR-only-consumer gap (FIND_TABLE_USAGES
427
+ -- itself still lacks it — out of scope, separate follow-up).
428
+ -- params: [table_qualified, table_qualified] (same value bound to BOTH ?)
429
+ SELECT DISTINCT q.id AS id, q.kind AS kind, q.target_table AS target
430
+ FROM "SqlColumn" src
431
+ JOIN "COLUMN_LINEAGE" cl ON cl.src_key = src.id
432
+ JOIN "SqlQuery" q ON q.id = cl.query_id
433
+ WHERE src.table_qualified = ?
434
+ UNION
435
+ SELECT DISTINCT q.id AS id, q.kind AS kind, q.target_table AS target
436
+ FROM "STAR_SOURCE" ss
437
+ JOIN "SqlQuery" q ON q.id = ss.src_key
438
+ WHERE ss.dst_key = ?
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
+ """