sql-code-graph 1.36.2__py3-none-any.whl → 1.37.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sql-code-graph
3
- Version: 1.36.2
3
+ Version: 1.37.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,4 +1,4 @@
1
- sqlcg/__init__.py,sha256=lDqeTiiKtYlwFCXOBJv--TA4e7oisecgTT3NeETH6eY,116
1
+ sqlcg/__init__.py,sha256=bfwekIDaFpChaS-yg8BLF_HhMid1W5BaoDHobS3Ryyc,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
@@ -72,8 +72,8 @@ sqlcg/viz/data.py,sha256=deLWOZBgewM1x-kk1NhUnt0mswyt0nzqSevHzBCOHHc,5899
72
72
  sqlcg/viz/render.py,sha256=BINkGbJbbb_iqhrkN795RaQsdg8nqCiJtsEFF1yo22Y,2737
73
73
  sqlcg/viz/tags.py,sha256=6zRnGlHjuGmEeB6yN1uhzm8rqL7ZGoyL1Ki7jI5oM6A,5368
74
74
  sqlcg/viz/assets/force-graph.min.js,sha256=jNdYdDdrYiUdUlElxRkolPBt30rstQk2q15Q32VVdzc,177272
75
- sqlcg/viz/assets/template.html,sha256=9_j-mvo1ZxwgiJPDdVrNmca37dTrTjjYVd3977u-DxE,12294
76
- sql_code_graph-1.36.2.dist-info/METADATA,sha256=if10KHC8utK49r0BY4N9DmsK98-Uz1mUlauVqTiFEaE,19208
77
- sql_code_graph-1.36.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
78
- sql_code_graph-1.36.2.dist-info/entry_points.txt,sha256=Wfe49sVzV9p4eVFGo5RxcV-frr3HOP0yzzst8JBxQLQ,46
79
- sql_code_graph-1.36.2.dist-info/RECORD,,
75
+ sqlcg/viz/assets/template.html,sha256=48ACoI2EjhITfvxDpSo_Js4OVG3L2BGq56eiwJfBupA,17404
76
+ sql_code_graph-1.37.1.dist-info/METADATA,sha256=5iPu-AfnvE_myVt6yjW2ix66uwp_5ZJay0dFgcJieAA,19208
77
+ sql_code_graph-1.37.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
78
+ sql_code_graph-1.37.1.dist-info/entry_points.txt,sha256=Wfe49sVzV9p4eVFGo5RxcV-frr3HOP0yzzst8JBxQLQ,46
79
+ sql_code_graph-1.37.1.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.36.2"
3
+ __version__ = "1.37.1"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -29,14 +29,14 @@
29
29
  <div class="facet" id="kind-facet"><b>kind</b></div>
30
30
  <div class="facet" id="tag-facet"><b>tag</b></div>
31
31
  <select id="jobsel"><option value="">all jobs</option></select>
32
- <label>min deg <input id="mindeg" type="number" value="0" min="0" style="width:3.2em"></label>
32
+ <label>min deg <input id="mindeg" type="number" value="1" min="0" style="width:3.2em"></label>
33
33
  <label><input id="egomode" type="checkbox"> ego</label>
34
34
  <label><input id="labelmode" type="checkbox" checked> labels</label>
35
35
  <label><input id="tagcolor" type="checkbox"> color by tag</label>
36
36
  <span id="meta"></span>
37
37
  </div>
38
38
  <div id="graph"></div>
39
- <div id="info">click a node · scroll to zoom · drag to pan · click a chip to toggle, alt/right-click to solo</div>
39
+ <div id="info">hover/click a node to trace its 1-hop lineage (Esc or background clears) · scroll to zoom · drag to pan · click a chip to toggle, alt/right-click to solo · set min deg 0 to show isolated tables</div>
40
40
  <script>
41
41
  const DATA = /*__DATA__*/;
42
42
  const TAGS = /*__TAGS__*/;
@@ -57,6 +57,22 @@ for (const l of DATA.links) {
57
57
  adj.get(s).add(t); adj.get(t).add(s);
58
58
  }
59
59
 
60
+ // ── Neighborhood highlight (headline) ───────────────────────────────────────
61
+ // Pure, side-effect-free: given the adjacency map and a focus id, return the
62
+ // 1-hop highlight set = focus ∪ direct neighbors. Returns an EMPTY Set when id
63
+ // is null (no highlight active). Factored out as a standalone fn so it can be
64
+ // asserted directly in a Node harness (no DOM, no force-graph) — this is the
65
+ // AC2 binding guard that the highlight is wired to the baked adjacency, not an
66
+ // orphaned reference. (Amendment 1.)
67
+ function computeHighlightSet(adjMap, id) {
68
+ const out = new Set();
69
+ if (id == null) return out;
70
+ out.add(id);
71
+ const nbrs = adjMap.get(id);
72
+ if (nbrs) for (const n of nbrs) out.add(n);
73
+ return out;
74
+ }
75
+
60
76
  // ── Schema palette + colors ──────────────────────────────────────────────
61
77
  const SCHEMA_PALETTE = SCHEMA_CONFIG.palette;
62
78
  const SCHEMA_OTHER_COLOR = SCHEMA_CONFIG.other;
@@ -69,10 +85,12 @@ curatedSchemas.forEach((s, i) => schemaColour.set(s, SCHEMA_PALETTE[i % SCHEMA_P
69
85
  dataSchemas.forEach(s => { if (!schemaColour.has(s)) schemaColour.set(s, SCHEMA_OTHER_COLOR); });
70
86
  function schemaFill(n) { return schemaColour.get(n.schema) || SCHEMA_OTHER_COLOR; }
71
87
 
72
- // ── Kind vocabulary (B2): table/view/temp default ON; cte/derived default OFF.
73
- // "external" is never emitted. Unknown kinds bucket into "table". ─────────
88
+ // ── Kind vocabulary: table/view default ON; temp/cte/derived default OFF.
89
+ // "external" is never emitted. Unknown kinds bucket into "table".
90
+ // temp stays in KNOWN_KINDS so it remains a toggleable chip (804 temp nodes
91
+ // at live scale are clutter by default; one chip click reveals them). ──────
74
92
  const KNOWN_KINDS = ['table', 'view', 'temp', 'cte', 'derived'];
75
- const KIND_DEFAULT_ON = new Set(['table', 'view', 'temp']);
93
+ const KIND_DEFAULT_ON = new Set(['table', 'view']);
76
94
  function kindBucket(k) { return KNOWN_KINDS.includes(k) ? k : 'table'; }
77
95
 
78
96
  // ── Facet selection state. Empty selected-set for a facet = "do not filter". ─
@@ -83,8 +101,20 @@ const sel = {
83
101
  };
84
102
  const soloBackup = { schema: null, kind: null, tag: null };
85
103
 
86
- let focusNode = null;
87
- let hoveredNode = null;
104
+ let focusNode = null; // ego-mode focus (orthogonal to highlight)
105
+ let hoveredNode = null; // transient hover target
106
+ let pinnedNode = null; // click-pinned highlight focus (survives cursor leave)
107
+
108
+ // highlightId: pin wins over hover. highlightSet: focus ∪ 1-hop neighbors,
109
+ // recomputed only when the focus changes (cheap; not per frame). These are read
110
+ // by the styling accessors below — repaint is styling-only, NEVER a graphData()
111
+ // rebuild (Amendment 2: a rebuild re-seeds the force layout → the graph jumps).
112
+ let highlightId = null;
113
+ let highlightSet = new Set();
114
+ function setHighlight(id) {
115
+ highlightId = id;
116
+ highlightSet = computeHighlightSet(adj, id);
117
+ }
88
118
 
89
119
  function buildFacet(facetId, key, items, colorOf, defaultOn) {
90
120
  const root = document.getElementById(facetId);
@@ -145,9 +175,15 @@ buildFacet('schema-facet', 'schema', curatedSchemas,
145
175
  s => schemaColour.get(s) || SCHEMA_OTHER_COLOR, () => false);
146
176
  // kind facet
147
177
  buildFacet('kind-facet', 'kind', KNOWN_KINDS, null, k => KIND_DEFAULT_ON.has(k));
148
- // tag facet (legend swatches double as filter controls)
149
- buildFacet('tag-facet', 'tag', TAGS.labels || [],
150
- t => (TAGS.colors || {})[t] || SCHEMA_OTHER_COLOR, () => false);
178
+ // tag facet (legend swatches double as filter controls). When no --tags were
179
+ // passed the label list is empty; hide the whole facet container so a lonely
180
+ // "TAG" header is not shown (Phase 4 empty-facet polish). No JS error either way.
181
+ if ((TAGS.labels || []).length) {
182
+ buildFacet('tag-facet', 'tag', TAGS.labels,
183
+ t => (TAGS.colors || {})[t] || SCHEMA_OTHER_COLOR, () => false);
184
+ } else {
185
+ document.getElementById('tag-facet').style.display = 'none';
186
+ }
151
187
 
152
188
  // ── Job facet → dropdown (filter only). ──────────────────────────────────
153
189
  const jobSel = document.getElementById('jobsel');
@@ -227,24 +263,67 @@ function tagRing(n) {
227
263
 
228
264
  function drawNodeCanvas(node, ctx, globalScale) {
229
265
  const r = Math.max(1, Math.sqrt(node.deg || 1));
266
+ // Highlight-aware alpha: when a highlight is active, nodes outside the 1-hop
267
+ // set dim; nodes inside paint full. No highlight → everything full.
268
+ const inSet = highlightSet.has(node.id);
269
+ const dim = highlightId !== null && !inSet;
270
+ ctx.globalAlpha = dim ? 0.12 : 1;
230
271
  ctx.beginPath();
231
272
  ctx.arc(node.x, node.y, r, 0, 2 * Math.PI);
232
273
  ctx.fillStyle = nodeFill(node);
233
274
  ctx.fill();
234
275
  const ring = tagRing(node);
235
- if (ring && !node._hit) {
276
+ if (ring && !node._hit && !dim) {
236
277
  ctx.lineWidth = 1.5 / globalScale;
237
278
  ctx.strokeStyle = ring;
238
279
  ctx.beginPath();
239
280
  ctx.arc(node.x, node.y, r + 1.5 / globalScale, 0, 2 * Math.PI);
240
281
  ctx.stroke();
241
282
  }
283
+ // Focus ring on the highlightId node itself (white, distinct from tag ring).
284
+ if (node.id === highlightId) {
285
+ ctx.lineWidth = 2 / globalScale;
286
+ ctx.strokeStyle = '#ffffff';
287
+ ctx.beginPath();
288
+ ctx.arc(node.x, node.y, r + 3 / globalScale, 0, 2 * Math.PI);
289
+ ctx.stroke();
290
+ }
242
291
  const showLabels = document.getElementById('labelmode').checked;
243
- if (showLabels && (r * globalScale > 6 || node._hit || node.id === hoveredNode)) {
292
+ if (showLabels && !dim
293
+ && (r * globalScale > 6 || node._hit || inSet || node.id === hoveredNode)) {
244
294
  ctx.font = `${10 / globalScale}px system-ui`;
245
295
  ctx.fillStyle = '#c9d1d9';
246
296
  ctx.fillText(node.id, node.x + r + 1, node.y + 3 / globalScale);
247
297
  }
298
+ ctx.globalAlpha = 1;
299
+ }
300
+
301
+ // ── Edge styling (Item 2): legible base edges on #0d1117; dim/bright tiers and
302
+ // directional arrows driven by highlightId/highlightSet. A link is "incident"
303
+ // to the highlight iff BOTH endpoints are in highlightSet (highlightSet is
304
+ // focus + its neighbors, so this is exactly the focus's own edges). ─────────
305
+ function linkEnds(l) {
306
+ return [l.source.id ?? l.source, l.target.id ?? l.target];
307
+ }
308
+ function linkIncident(l) {
309
+ if (highlightId === null) return false;
310
+ const [s, t] = linkEnds(l);
311
+ return highlightSet.has(s) && highlightSet.has(t);
312
+ }
313
+ function linkColor(l) {
314
+ if (highlightId === null) return '#3b4350'; // legible base on dark bg
315
+ if (linkIncident(l)) return '#79c0ff'; // bright (traced lineage)
316
+ return 'rgba(59,67,80,0.08)'; // dimmed away from focus
317
+ }
318
+ function linkWidth(l) {
319
+ if (highlightId !== null && linkIncident(l)) return 2;
320
+ return 1;
321
+ }
322
+ // Directional arrows on HIGHLIGHTED edges ONLY (D3): base state stays clean.
323
+ // DATA.links carry source->target = producer->consumer (data.py), so the arrow
324
+ // points producer→consumer with no data change.
325
+ function linkArrowLen(l) {
326
+ return (highlightId !== null && linkIncident(l)) ? 4 : 0;
248
327
  }
249
328
 
250
329
  // ── Graph init ────────────────────────────────────────────────────────────
@@ -252,12 +331,39 @@ const Graph = ForceGraph()(document.getElementById('graph'))
252
331
  .graphData(currentData())
253
332
  .nodeId('id')
254
333
  .nodeCanvasObject(drawNodeCanvas)
334
+ .linkColor(linkColor)
335
+ .linkWidth(linkWidth)
336
+ .linkDirectionalArrowLength(linkArrowLen)
337
+ .linkDirectionalArrowRelPos(1)
255
338
  .nodeLabel(n => `${n.id}\nschema: ${n.schema} · kind: ${n.kind} · degree: ${n.deg}`
256
339
  + ((n.tags || []).length ? `\ntags: ${n.tags.join(', ')}` : '')
257
340
  + ((n.jobs || []).length ? `\njobs: ${n.jobs.join(', ')}` : ''))
258
- .onNodeClick(n => { focusNode = n.id; refresh(); })
259
- .onNodeHover(n => { hoveredNode = n ? n.id : null; });
341
+ // Highlight handlers mutate ONLY hover/pin state, then recompute highlightSet.
342
+ // They DO NOT call refresh()/graphData() that would re-seed the force layout
343
+ // and make the graph jump at 3069/6671 scale (Amendment 2). force-graph
344
+ // repaints every animation frame, re-reading the closure-visible highlightId/
345
+ // highlightSet, so updating state is enough; ego-mode keeps focusNode separate.
346
+ .onNodeClick(n => {
347
+ pinnedNode = (pinnedNode === n.id) ? null : n.id; // toggle pin
348
+ setHighlight(pinnedNode ?? hoveredNode);
349
+ focusNode = n.id; // ego-mode focus (orthogonal; only used when ego is on)
350
+ })
351
+ .onNodeHover(n => {
352
+ hoveredNode = n ? n.id : null;
353
+ if (pinnedNode === null) setHighlight(hoveredNode); // pin wins over hover
354
+ })
355
+ .onBackgroundClick(() => { // click empty space clears the pin
356
+ pinnedNode = null;
357
+ setHighlight(hoveredNode);
358
+ });
359
+
360
+ // Escape clears the pin (transient hover clears on its own via onNodeHover).
361
+ document.addEventListener('keydown', e => {
362
+ if (e.key === 'Escape') { pinnedNode = null; setHighlight(hoveredNode); }
363
+ });
260
364
 
365
+ // refresh() rebuilds graphData → re-seeds layout. Reserved for FILTER changes
366
+ // (facets / mindeg / search / ego) ONLY, never for highlight (Amendment 2).
261
367
  function refresh() { Graph.graphData(currentData()); }
262
368
 
263
369
  for (const id of ['search', 'mindeg', 'egomode', 'labelmode', 'tagcolor']) {