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.
- {sql_code_graph-1.36.2.dist-info → sql_code_graph-1.37.1.dist-info}/METADATA +1 -1
- {sql_code_graph-1.36.2.dist-info → sql_code_graph-1.37.1.dist-info}/RECORD +6 -6
- sqlcg/__init__.py +1 -1
- sqlcg/viz/assets/template.html +120 -14
- {sql_code_graph-1.36.2.dist-info → sql_code_graph-1.37.1.dist-info}/WHEEL +0 -0
- {sql_code_graph-1.36.2.dist-info → sql_code_graph-1.37.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.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=
|
|
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=
|
|
76
|
-
sql_code_graph-1.
|
|
77
|
-
sql_code_graph-1.
|
|
78
|
-
sql_code_graph-1.
|
|
79
|
-
sql_code_graph-1.
|
|
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
sqlcg/viz/assets/template.html
CHANGED
|
@@ -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="
|
|
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
|
|
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'
|
|
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
|
-
|
|
150
|
-
|
|
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 &&
|
|
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
|
-
|
|
259
|
-
|
|
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']) {
|
|
File without changes
|
|
File without changes
|