sql-code-graph 1.1.0__py3-none-any.whl → 1.2.2__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.1.0.dist-info → sql_code_graph-1.2.2.dist-info}/METADATA +11 -1
- {sql_code_graph-1.1.0.dist-info → sql_code_graph-1.2.2.dist-info}/RECORD +19 -18
- {sql_code_graph-1.1.0.dist-info → sql_code_graph-1.2.2.dist-info}/WHEEL +1 -1
- sqlcg/__init__.py +1 -1
- sqlcg/cli/commands/analyze.py +156 -134
- sqlcg/cli/commands/db.py +92 -86
- sqlcg/cli/commands/find.py +30 -33
- sqlcg/cli/commands/gain.py +13 -11
- sqlcg/core/config.py +35 -5
- sqlcg/core/kuzu_backend.py +4 -1
- sqlcg/core/queries.cypher +0 -6
- sqlcg/core/queries.py +0 -1
- sqlcg/indexer/indexer.py +109 -11
- sqlcg/lineage/aggregator.py +17 -45
- sqlcg/parsers/ansi_parser.py +2 -2
- sqlcg/parsers/base.py +7 -1
- sqlcg/server/read_client.py +192 -0
- sqlcg/server/server.py +97 -18
- {sql_code_graph-1.1.0.dist-info → sql_code_graph-1.2.2.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.2.2
|
|
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
|
|
@@ -267,6 +267,16 @@ sqlcg mcp restart # stop the server (client must respawn it
|
|
|
267
267
|
sqlcg version # show installed version
|
|
268
268
|
```
|
|
269
269
|
|
|
270
|
+
### Reads while the server is running (v1.2.0)
|
|
271
|
+
|
|
272
|
+
KuzuDB allows a single writer, so while the MCP server is live it holds the
|
|
273
|
+
database lock. CLI **read** commands (`find`, `analyze`, `db info`, `list-repos`,
|
|
274
|
+
`gain`) automatically route their query through the running server over its
|
|
275
|
+
control socket and return rows as usual — no flag, no config. When no server is
|
|
276
|
+
running they open the database directly, exactly as before. If the server is
|
|
277
|
+
mid-reindex the read waits for it to finish rather than failing with
|
|
278
|
+
"Database is locked".
|
|
279
|
+
|
|
270
280
|
## Supported dialects
|
|
271
281
|
|
|
272
282
|
sqlcg is built on [sqlglot](https://github.com/tobymao/sqlglot), so other dialects
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
sqlcg/__init__.py,sha256=
|
|
1
|
+
sqlcg/__init__.py,sha256=6jzbRxs0WG3Yq2KYIDG9bagxRZvhbhp6agviO4_nXwM,115
|
|
2
2
|
sqlcg/__main__.py,sha256=1YoFLcqEgTwYq1J3TbUwpkdG0zeeLIf2fJvwWI-CLFU,109
|
|
3
3
|
sqlcg/cli/__init__.py,sha256=W8fD0LpMq2xm_5WKGNMvJh2WBL1ho5E8hUeAqXQYT1g,28
|
|
4
4
|
sqlcg/cli/main.py,sha256=WmdTjsOlz1ozi2Y3Aq4ezR_FCRl-Lc1YOKw3_d48dlY,1650
|
|
5
5
|
sqlcg/cli/commands/__init__.py,sha256=oSHtr6VD-jNubOjuCQyZj2tBppjMEpQDh-IGQ8of9eA,30
|
|
6
|
-
sqlcg/cli/commands/analyze.py,sha256=
|
|
7
|
-
sqlcg/cli/commands/db.py,sha256=
|
|
8
|
-
sqlcg/cli/commands/find.py,sha256=
|
|
9
|
-
sqlcg/cli/commands/gain.py,sha256=
|
|
6
|
+
sqlcg/cli/commands/analyze.py,sha256=xr3RHmO4eFTP4VKZn4DAx3BJzSi60_DIZmdE-QLfsHI,13601
|
|
7
|
+
sqlcg/cli/commands/db.py,sha256=iUPnSxkSjt2EZzj_zmXiIyM39NTTCpazD4Y8Q9iPZEc,7725
|
|
8
|
+
sqlcg/cli/commands/find.py,sha256=zTYN9goILalYq4R9x6lIR6MmNcydDbR17UXkx1gPRsI,2913
|
|
9
|
+
sqlcg/cli/commands/gain.py,sha256=Kws76u1na2XxmbWN_YWrPaYHYmYBLC6DDDf7xqnltqc,9126
|
|
10
10
|
sqlcg/cli/commands/git.py,sha256=yMgWOuoTCTBr2P1QgmghRi5ikmUYHuxDUVyBDYerErw,5728
|
|
11
11
|
sqlcg/cli/commands/index.py,sha256=xMnxKDiUt5LH_3lKAotoRctL4VSOvcw7Gq--idLPtm0,11091
|
|
12
12
|
sqlcg/cli/commands/install.py,sha256=KNABvrLbamPyYnmnVdCaM_MNezbDc-pr6IkignCWI8k,9186
|
|
@@ -16,32 +16,32 @@ sqlcg/cli/commands/report.py,sha256=JU0qjyMxwOukE7bN3XvvIzOI7zMg_Gsnvk_8F6pKNpA,
|
|
|
16
16
|
sqlcg/cli/commands/uninstall.py,sha256=IYwQaqnMmmzW0Nlls40wD-L3tVkMgKIMRXUkcXPMUc4,9398
|
|
17
17
|
sqlcg/cli/commands/watch.py,sha256=7N6c-QuvxAEGHzDZ0C3CU2BkHSraZW9YtgoFnz7SaQo,2373
|
|
18
18
|
sqlcg/core/__init__.py,sha256=uNsJCrCMVWVT80sHPtI_f39BYqIf5N0i6LSq8x8HsyI,283
|
|
19
|
-
sqlcg/core/config.py,sha256=
|
|
19
|
+
sqlcg/core/config.py,sha256=qNR-yXkfYfS8Y8WX4Qo6Zkq8PPP_ZiTrvX0DLmEZkGY,14821
|
|
20
20
|
sqlcg/core/freshness.py,sha256=gRb8pRPw5SdIUxAYkMXIJ00DTdQ6CegRZPAvWnv0rU0,4575
|
|
21
21
|
sqlcg/core/graph_db.py,sha256=Aa85wPFg26H-Ud9SrZyxCHH-99iitAI5S3X9T_62Yyw,7957
|
|
22
22
|
sqlcg/core/jobs.py,sha256=Je-fCdSKRgiSsv1W8SgNAlp36a7t7-pJZ-qKPbka9OE,3298
|
|
23
|
-
sqlcg/core/kuzu_backend.py,sha256=
|
|
23
|
+
sqlcg/core/kuzu_backend.py,sha256=3kL8bGEQm70fuxYUdt1p7fsY12lCLQ07x01NYg6FOGA,16821
|
|
24
24
|
sqlcg/core/neo4j_backend.py,sha256=AM1TncP9GBGph-rSHwalZPmGUV2kFILzaJP-PSB0UYw,8437
|
|
25
|
-
sqlcg/core/queries.cypher,sha256=
|
|
26
|
-
sqlcg/core/queries.py,sha256=
|
|
25
|
+
sqlcg/core/queries.cypher,sha256=cvPOVe5GUOzJN4bxUvDxNI--xIIP8gm42TR-gUnea4U,4685
|
|
26
|
+
sqlcg/core/queries.py,sha256=gkl4bhkZM8FsvbSA-IaK17sRFcO3hB5YlVCemkCXgWM,2064
|
|
27
27
|
sqlcg/core/schema.cypher,sha256=rK5QMhSrzZhuj73NeNXGX6oM-rPPPvxFjex0fEyUvkQ,2859
|
|
28
28
|
sqlcg/core/schema.py,sha256=JO5rkspYKjL9AEl5mt0VIJKn-IPOH3kJV_fVmAMuFCI,1467
|
|
29
29
|
sqlcg/indexer/__init__.py,sha256=Wh20Unz2OHs1oIyWLrpurPAasF0BET2g4iXtNk7mh2U,56
|
|
30
30
|
sqlcg/indexer/dbt_adapter.py,sha256=EB5x1WU5Z9d-I97ADDj88S_hG1C4z4nbrv8JUCzXfy8,686
|
|
31
31
|
sqlcg/indexer/error_classify.py,sha256=MYjPVprwT-ARPjBCyCzu2F9DSrZfnTVtVIoBgm8s4H8,5329
|
|
32
32
|
sqlcg/indexer/git_delta.py,sha256=P-QM4vnVURT2KLiE6u3cQynRUF-mTH13cbB4I20YHPQ,4468
|
|
33
|
-
sqlcg/indexer/indexer.py,sha256=
|
|
33
|
+
sqlcg/indexer/indexer.py,sha256=7TCgBLl3ml3mF8Z2q4YHZJ6HdxSuLH-rPJTibnUJUe4,66658
|
|
34
34
|
sqlcg/indexer/pool.py,sha256=BTYx-pBe6zwUG89MHh0X7nzGNVlsHN-GjovYKanVI1s,18553
|
|
35
35
|
sqlcg/indexer/walker.py,sha256=umNaqDbuerr75VYG1TEOv0ATsbI40O3SIw35f7XJcDE,1931
|
|
36
36
|
sqlcg/indexer/watcher.py,sha256=mJQq1LASRLKKwhz0WhCUWPLLqyPR2_-FD_8efYU6gE8,8442
|
|
37
37
|
sqlcg/lineage/__init__.py,sha256=Da1DlYwtK13WHv_RnHjAtNkHTOuFbhxqCjT1Le7DsWM,46
|
|
38
|
-
sqlcg/lineage/aggregator.py,sha256=
|
|
38
|
+
sqlcg/lineage/aggregator.py,sha256=LVyNcmvLBHWbh8SrDsJJBKd7sLg3-2NhEWwEndG7Jbc,4144
|
|
39
39
|
sqlcg/lineage/schema_resolver.py,sha256=iXt6LYF6UVWsGUpcfbmjmGn9wCgXl721lTGf_8AaWcc,7320
|
|
40
40
|
sqlcg/metrics/__init__.py,sha256=hLJ6wm4St8qqYwKh3o9QG7lcEt1BEYM31ccqO9tGpIg,133
|
|
41
41
|
sqlcg/metrics/store.py,sha256=BaMf7QYTmYMlX_Jzi1GNU8R2sMVkWdn07f-ZSndtcNk,8879
|
|
42
42
|
sqlcg/parsers/__init__.py,sha256=AamA8wBbDZV9_zEtZCI4Hyen5UAVKHmBwjTghTt2PZE,785
|
|
43
|
-
sqlcg/parsers/ansi_parser.py,sha256=
|
|
44
|
-
sqlcg/parsers/base.py,sha256=
|
|
43
|
+
sqlcg/parsers/ansi_parser.py,sha256=mGZvijMOMQ4i1BybpwU29a8jnIGViefhy9fxzkSpsRM,17193
|
|
44
|
+
sqlcg/parsers/base.py,sha256=IiOkVsm6jz9-48RqDCXiW-UXAraNxQ4pKXvSA7aolnA,49907
|
|
45
45
|
sqlcg/parsers/bigquery_parser.py,sha256=mOnWTfXB_Dp4JwFE1PVYOB6CDPf5nYE0Dea8kJCl9uQ,2827
|
|
46
46
|
sqlcg/parsers/postgres_parser.py,sha256=lYfUpQY6j4Qm7ndXBtXbgPoGzYqYddWt5YeFnWKdA6I,946
|
|
47
47
|
sqlcg/parsers/registry.py,sha256=LXy1F6rqQI6VdxpRvZg_tNpoEucW3mXZHYBMlMONbX4,1496
|
|
@@ -52,14 +52,15 @@ sqlcg/server/control.py,sha256=v-r21npODiHlHnJHuo_6KWrKclQKq_E1QyrzIWjqgtY,4508
|
|
|
52
52
|
sqlcg/server/exceptions.py,sha256=EONw34icOByCTpppSQrvQBW6asc4hfqaGDCAFjv96II,469
|
|
53
53
|
sqlcg/server/models.py,sha256=l7ORy6sbtzBW1y3qVaeLwEukbyAgBkz9S5VIm2q4b24,19378
|
|
54
54
|
sqlcg/server/noise_filter.py,sha256=idSBGgdKWWccJdpOo9qgbM2350Oew-2l5W6Yc9GYQqY,6337
|
|
55
|
-
sqlcg/server/
|
|
55
|
+
sqlcg/server/read_client.py,sha256=ncoJK7UckGhWtN9bv1CgViNMNtac96zBUE7RPYQ8_WI,7783
|
|
56
|
+
sqlcg/server/server.py,sha256=QBb7N4lc5o7KBh1Ik6MAiOvBV8SW81ahtkM1ZOVuyXE,18983
|
|
56
57
|
sqlcg/server/skill.py,sha256=GE8eeimk6yiGGJ74erGypqYAviur5peSR6_2a4QQWVM,12828
|
|
57
58
|
sqlcg/server/tools.py,sha256=JvijDC0h5uHjZyZUIZq9sztNG3W5sr-Yy5rHwOVuJec,66642
|
|
58
59
|
sqlcg/utils/__init__.py,sha256=--iqt5ThTXmT8Wz7da8hs3n0zDfYPl8P-z5OgRJ_77E,154
|
|
59
60
|
sqlcg/utils/hashing.py,sha256=H25-sYfxHKb3_IERFnHyAIYNiXN470Oqo5sJT_D3YOA,438
|
|
60
61
|
sqlcg/utils/ignore.py,sha256=wJjwa0mjnQ_xJExOUxk25y00g065XmmzJapqV3ifD5o,1151
|
|
61
62
|
sqlcg/utils/logging.py,sha256=u0fCmYsLj9o81vawm3xZTHaw68GQYVm7JxG-gP81u8A,840
|
|
62
|
-
sql_code_graph-1.
|
|
63
|
-
sql_code_graph-1.
|
|
64
|
-
sql_code_graph-1.
|
|
65
|
-
sql_code_graph-1.
|
|
63
|
+
sql_code_graph-1.2.2.dist-info/METADATA,sha256=e4llIRyH4QYoHbIznQIpZ_wreqsuscvDzuLBVQBv33M,14148
|
|
64
|
+
sql_code_graph-1.2.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
65
|
+
sql_code_graph-1.2.2.dist-info/entry_points.txt,sha256=Wfe49sVzV9p4eVFGo5RxcV-frr3HOP0yzzst8JBxQLQ,46
|
|
66
|
+
sql_code_graph-1.2.2.dist-info/RECORD,,
|
sqlcg/__init__.py
CHANGED
sqlcg/cli/commands/analyze.py
CHANGED
|
@@ -8,9 +8,9 @@ import typer
|
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
from rich.table import Table
|
|
10
10
|
|
|
11
|
-
from sqlcg.core.config import get_backend
|
|
12
11
|
from sqlcg.core.queries import GET_TABLE_EXTERNAL_CONSUMERS_QUERY
|
|
13
12
|
from sqlcg.core.schema import NodeLabel, RelType
|
|
13
|
+
from sqlcg.server.read_client import run_read_routed
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
from sqlcg.server.noise_filter import NoiseFilter
|
|
@@ -19,6 +19,39 @@ app = typer.Typer(help="Lineage analysis")
|
|
|
19
19
|
console = Console()
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def _kind_filter(node_alias: str, *, include_intermediate: bool) -> str:
|
|
23
|
+
"""Build the SqlTable kind-filter clause for upstream/downstream queries.
|
|
24
|
+
|
|
25
|
+
Uses an OPTIONAL MATCH + explicit ``WITH … WHERE t.kind IS NULL OR …`` form
|
|
26
|
+
(Half B of the #38/#39 fix) so that a node-less physical source — one whose
|
|
27
|
+
SqlTable node is absent because it was only seen inside a CTE body before
|
|
28
|
+
re-index — is KEPT, not dropped. CTE/derived intermediates carry
|
|
29
|
+
``kind='cte'`` or ``kind='derived'`` and are excluded by the
|
|
30
|
+
``kind IN [...]`` guard; ``IS NULL`` matches the absent-node case (t = NULL
|
|
31
|
+
→ t.kind = NULL → IS NULL is TRUE).
|
|
32
|
+
|
|
33
|
+
The ``WITH {node_alias}, t WHERE`` clause is required: an OPTIONAL MATCH
|
|
34
|
+
WHERE clause in KuzuDB applies to the match attempt and does not filter the
|
|
35
|
+
surrounding row. The subsequent WITH … WHERE is the actual row filter.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
node_alias: The Cypher alias for the column node whose table is filtered
|
|
39
|
+
(``"src"`` for upstream, ``"dst"`` for downstream).
|
|
40
|
+
include_intermediate: When True, return an empty string (no filtering);
|
|
41
|
+
all intermediates including CTE nodes are kept.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
A Cypher fragment string (with trailing space) to embed directly in the
|
|
45
|
+
query, or an empty string when include_intermediate is True.
|
|
46
|
+
"""
|
|
47
|
+
if include_intermediate:
|
|
48
|
+
return ""
|
|
49
|
+
return (
|
|
50
|
+
f"OPTIONAL MATCH (t:SqlTable {{qualified: {node_alias}.table_qualified}}) "
|
|
51
|
+
f"WITH {node_alias}, t WHERE t.kind IS NULL OR t.kind IN ['table', 'external'] "
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
22
55
|
@app.command("upstream")
|
|
23
56
|
def upstream( # noqa: B008
|
|
24
57
|
ref: str = typer.Argument(..., help="Column reference"), # noqa: B008
|
|
@@ -35,49 +68,45 @@ def upstream( # noqa: B008
|
|
|
35
68
|
raise typer.Exit(1)
|
|
36
69
|
|
|
37
70
|
# By default, filter out CTE/derived intermediate nodes; --include-intermediate restores them
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
71
|
+
kf = _kind_filter("src", include_intermediate=include_intermediate)
|
|
72
|
+
|
|
73
|
+
results = run_read_routed(
|
|
74
|
+
f"MATCH (c:{NodeLabel.COLUMN} {{id: $ref}})"
|
|
75
|
+
f"<-[:{RelType.COLUMN_LINEAGE}*1..{depth}]-(src:{NodeLabel.COLUMN}) "
|
|
76
|
+
f"{kf}"
|
|
77
|
+
f"OPTIONAL MATCH (src)-[srcedge:{RelType.COLUMN_LINEAGE}]->() "
|
|
78
|
+
"OPTIONAL MATCH (q:SqlQuery {id: srcedge.query_id}) "
|
|
79
|
+
"WITH src, min(q.start_line) AS line, min(q.file_path) AS file "
|
|
80
|
+
"RETURN src.id AS id, file AS file, line AS line LIMIT 100",
|
|
81
|
+
{"ref": ref},
|
|
43
82
|
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
f"MATCH (c:{NodeLabel.COLUMN} {{id: $
|
|
83
|
+
if not results and len(ref.split(".")) >= 3:
|
|
84
|
+
bare = _bare_ref(ref)
|
|
85
|
+
fallback_results = run_read_routed(
|
|
86
|
+
f"MATCH (c:{NodeLabel.COLUMN} {{id: $bare}})"
|
|
48
87
|
f"<-[:{RelType.COLUMN_LINEAGE}*1..{depth}]-(src:{NodeLabel.COLUMN}) "
|
|
49
|
-
f"{
|
|
50
|
-
f"OPTIONAL MATCH (src)-[
|
|
51
|
-
"OPTIONAL MATCH (q:SqlQuery {id:
|
|
52
|
-
"
|
|
53
|
-
|
|
88
|
+
f"{kf}"
|
|
89
|
+
f"OPTIONAL MATCH (src)-[srcedge:{RelType.COLUMN_LINEAGE}]->() "
|
|
90
|
+
"OPTIONAL MATCH (q:SqlQuery {id: srcedge.query_id}) "
|
|
91
|
+
"WITH src, min(q.start_line) AS line, min(q.file_path) AS file "
|
|
92
|
+
"RETURN src.id AS id, file AS file, line AS line LIMIT 100",
|
|
93
|
+
{"bare": bare},
|
|
54
94
|
)
|
|
55
|
-
if
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
f"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"OPTIONAL MATCH (q:SqlQuery {id: direct.query_id}) "
|
|
63
|
-
"RETURN src.id AS id, q.file_path AS file, q.start_line AS line LIMIT 100",
|
|
64
|
-
{"bare": bare},
|
|
95
|
+
if fallback_results:
|
|
96
|
+
console.print(
|
|
97
|
+
f"[yellow]Hint:[/yellow] No results for '{ref}'. "
|
|
98
|
+
f"Found {len(fallback_results)} edge(s) under bare name '{bare}'. "
|
|
99
|
+
"The INSERT target may have been indexed without a schema prefix. "
|
|
100
|
+
"Multiple tables with the same unqualified name in different schemas "
|
|
101
|
+
"would all match — re-index with an explicit schema for precise results."
|
|
65
102
|
)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
74
|
-
results = fallback_results
|
|
75
|
-
if not raw:
|
|
76
|
-
from sqlcg.server.noise_filter import NoiseFilter
|
|
77
|
-
|
|
78
|
-
nf = NoiseFilter.from_config() # repo_root=None → falls back to Path.cwd()
|
|
79
|
-
results = _filter_column_results(results, nf)
|
|
80
|
-
_print_table(_add_file_line_col(results), ["id", "file:line"])
|
|
103
|
+
results = fallback_results
|
|
104
|
+
if not raw:
|
|
105
|
+
from sqlcg.server.noise_filter import NoiseFilter
|
|
106
|
+
|
|
107
|
+
nf = NoiseFilter.from_config() # repo_root=None → falls back to Path.cwd()
|
|
108
|
+
results = _filter_column_results(results, nf)
|
|
109
|
+
_print_table(_add_file_line_col(results), ["id", "file:line"])
|
|
81
110
|
|
|
82
111
|
|
|
83
112
|
@app.command("downstream")
|
|
@@ -96,73 +125,69 @@ def downstream( # noqa: B008
|
|
|
96
125
|
raise typer.Exit(1)
|
|
97
126
|
|
|
98
127
|
# By default, filter out CTE/derived intermediate nodes; --include-intermediate restores them
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"
|
|
128
|
+
kf = _kind_filter("dst", include_intermediate=include_intermediate)
|
|
129
|
+
|
|
130
|
+
results = run_read_routed(
|
|
131
|
+
f"MATCH (c:{NodeLabel.COLUMN} {{id: $ref}})"
|
|
132
|
+
f"-[:{RelType.COLUMN_LINEAGE}*1..{depth}]->(dst:{NodeLabel.COLUMN}) "
|
|
133
|
+
f"{kf}"
|
|
134
|
+
f"OPTIONAL MATCH ()-[dstedge:{RelType.COLUMN_LINEAGE}]->(dst) "
|
|
135
|
+
"OPTIONAL MATCH (q:SqlQuery {id: dstedge.query_id}) "
|
|
136
|
+
"WITH dst, min(q.start_line) AS line, min(q.file_path) AS file "
|
|
137
|
+
"RETURN dst.id AS id, file AS file, line AS line LIMIT 100",
|
|
138
|
+
{"ref": ref},
|
|
104
139
|
)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
f"MATCH (c:{NodeLabel.COLUMN} {{id: $
|
|
140
|
+
if not results and len(ref.split(".")) >= 3:
|
|
141
|
+
bare = _bare_ref(ref)
|
|
142
|
+
fallback_results = run_read_routed(
|
|
143
|
+
f"MATCH (c:{NodeLabel.COLUMN} {{id: $bare}})"
|
|
109
144
|
f"-[:{RelType.COLUMN_LINEAGE}*1..{depth}]->(dst:{NodeLabel.COLUMN}) "
|
|
110
|
-
f"{
|
|
111
|
-
f"OPTIONAL MATCH (
|
|
112
|
-
"OPTIONAL MATCH (q:SqlQuery {id:
|
|
113
|
-
"
|
|
114
|
-
|
|
145
|
+
f"{kf}"
|
|
146
|
+
f"OPTIONAL MATCH ()-[dstedge:{RelType.COLUMN_LINEAGE}]->(dst) "
|
|
147
|
+
"OPTIONAL MATCH (q:SqlQuery {id: dstedge.query_id}) "
|
|
148
|
+
"WITH dst, min(q.start_line) AS line, min(q.file_path) AS file "
|
|
149
|
+
"RETURN dst.id AS id, file AS file, line AS line LIMIT 100",
|
|
150
|
+
{"bare": bare},
|
|
115
151
|
)
|
|
116
|
-
if
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
f"
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
"OPTIONAL MATCH (q:SqlQuery {id: direct.query_id}) "
|
|
124
|
-
"RETURN dst.id AS id, q.file_path AS file, q.start_line AS line LIMIT 100",
|
|
125
|
-
{"bare": bare},
|
|
152
|
+
if fallback_results:
|
|
153
|
+
console.print(
|
|
154
|
+
f"[yellow]Hint:[/yellow] No results for '{ref}'. "
|
|
155
|
+
f"Found {len(fallback_results)} edge(s) under bare name '{bare}'. "
|
|
156
|
+
"The INSERT target may have been indexed without a schema prefix. "
|
|
157
|
+
"Multiple tables with the same unqualified name in different schemas "
|
|
158
|
+
"would all match — re-index with an explicit schema for precise results."
|
|
126
159
|
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
terminal_tables
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
for tbl in sorted(terminal_tables):
|
|
156
|
-
rows_ec = backend.run_read(
|
|
157
|
-
GET_TABLE_EXTERNAL_CONSUMERS_QUERY,
|
|
158
|
-
{"table_qualified": tbl},
|
|
160
|
+
results = fallback_results
|
|
161
|
+
if not raw:
|
|
162
|
+
from sqlcg.server.noise_filter import NoiseFilter
|
|
163
|
+
|
|
164
|
+
nf = NoiseFilter.from_config() # repo_root=None → falls back to Path.cwd()
|
|
165
|
+
results = _filter_column_results(results, nf)
|
|
166
|
+
_print_table(_add_file_line_col(results), ["id", "file:line"])
|
|
167
|
+
|
|
168
|
+
# Append external consumer rows for terminal tables (scalar query, one per terminal).
|
|
169
|
+
# Resolve terminal tables from the column results; fall back to the root column's table.
|
|
170
|
+
terminal_tables: set[str] = set()
|
|
171
|
+
for r in results:
|
|
172
|
+
tbl = _col_id_to_table(r["id"])
|
|
173
|
+
if tbl:
|
|
174
|
+
terminal_tables.add(tbl)
|
|
175
|
+
# Also check the root column's table (in case no downstream columns were found).
|
|
176
|
+
root_parts = ref.rsplit(".", 1)
|
|
177
|
+
if len(root_parts) == 2:
|
|
178
|
+
terminal_tables.add(root_parts[0])
|
|
179
|
+
consumer_rows: list[dict] = []
|
|
180
|
+
for tbl in sorted(terminal_tables):
|
|
181
|
+
rows_ec = run_read_routed(
|
|
182
|
+
GET_TABLE_EXTERNAL_CONSUMERS_QUERY,
|
|
183
|
+
{"table_qualified": tbl},
|
|
184
|
+
)
|
|
185
|
+
for ec in rows_ec:
|
|
186
|
+
consumer_rows.append(
|
|
187
|
+
{"id": f"[external] {ec['name']} ({ec['consumer_type']})", "file:line": ""}
|
|
159
188
|
)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
{"id": f"[external] {ec['name']} ({ec['consumer_type']})", "file:line": ""}
|
|
163
|
-
)
|
|
164
|
-
if consumer_rows:
|
|
165
|
-
_print_table(consumer_rows, ["id", "file:line"])
|
|
189
|
+
if consumer_rows:
|
|
190
|
+
_print_table(consumer_rows, ["id", "file:line"])
|
|
166
191
|
|
|
167
192
|
|
|
168
193
|
@app.command("impact")
|
|
@@ -171,19 +196,18 @@ def impact( # noqa: B008
|
|
|
171
196
|
raw: bool = typer.Option(False, "--raw", help="Disable noise filtering on results"), # noqa: B008
|
|
172
197
|
) -> None:
|
|
173
198
|
"""Show all queries impacted by a table."""
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
from sqlcg.server.noise_filter import NoiseFilter
|
|
199
|
+
results = run_read_routed(
|
|
200
|
+
f"MATCH (t:{NodeLabel.TABLE} {{qualified: $t}})"
|
|
201
|
+
f"<-[:{RelType.SELECTS_FROM}]-(q:{NodeLabel.QUERY}) "
|
|
202
|
+
"RETURN DISTINCT q.id AS id, q.kind AS kind, q.target_table AS target LIMIT 100",
|
|
203
|
+
{"t": table},
|
|
204
|
+
)
|
|
205
|
+
if not raw:
|
|
206
|
+
from sqlcg.server.noise_filter import NoiseFilter
|
|
183
207
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
208
|
+
nf = NoiseFilter.from_config()
|
|
209
|
+
results = [r for r in results if not nf.is_noise(r.get("target", ""))]
|
|
210
|
+
_print_table(results, ["id", "kind"])
|
|
187
211
|
|
|
188
212
|
|
|
189
213
|
@app.command("failures")
|
|
@@ -207,15 +231,14 @@ def failures(
|
|
|
207
231
|
with 'sqlcg db reset && sqlcg index <path>' if the graph was built with
|
|
208
232
|
an earlier version.
|
|
209
233
|
"""
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
_print_table(rows, ["path", "cause"])
|
|
234
|
+
cypher = (
|
|
235
|
+
f"MATCH (f:{NodeLabel.FILE}) WHERE f.parse_failed = true "
|
|
236
|
+
"AND ($cause IS NULL OR f.parse_cause = $cause) "
|
|
237
|
+
"RETURN f.path AS path, f.parse_cause AS cause "
|
|
238
|
+
f"ORDER BY f.parse_cause LIMIT {limit}"
|
|
239
|
+
)
|
|
240
|
+
rows = run_read_routed(cypher, {"cause": cause})
|
|
241
|
+
_print_table(rows, ["path", "cause"])
|
|
219
242
|
|
|
220
243
|
|
|
221
244
|
@app.command("unused")
|
|
@@ -224,18 +247,17 @@ def unused(
|
|
|
224
247
|
raw: bool = typer.Option(False, "--raw", help="Disable noise filtering on results"), # noqa: B008
|
|
225
248
|
) -> None:
|
|
226
249
|
"""Find tables with no query references."""
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
from sqlcg.server.noise_filter import NoiseFilter
|
|
250
|
+
results = run_read_routed(
|
|
251
|
+
f"MATCH (t:{NodeLabel.TABLE}) WHERE NOT (t)<-[:{RelType.SELECTS_FROM}]-() "
|
|
252
|
+
"RETURN DISTINCT t.qualified AS qualified LIMIT 100",
|
|
253
|
+
{},
|
|
254
|
+
)
|
|
255
|
+
if not raw:
|
|
256
|
+
from sqlcg.server.noise_filter import NoiseFilter
|
|
235
257
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
258
|
+
nf = NoiseFilter.from_config()
|
|
259
|
+
results = [r for r in results if not nf.is_noise(r["qualified"])]
|
|
260
|
+
_print_table(results, ["qualified"])
|
|
239
261
|
|
|
240
262
|
|
|
241
263
|
def _bare_ref(ref: str) -> str:
|