sql-code-graph 1.1.3__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.3.dist-info → sql_code_graph-1.2.2.dist-info}/METADATA +11 -1
- {sql_code_graph-1.1.3.dist-info → sql_code_graph-1.2.2.dist-info}/RECORD +15 -14
- {sql_code_graph-1.1.3.dist-info → sql_code_graph-1.2.2.dist-info}/WHEEL +1 -1
- sqlcg/__init__.py +1 -1
- sqlcg/cli/commands/analyze.py +151 -149
- 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 +15 -13
- sqlcg/indexer/indexer.py +91 -11
- sqlcg/lineage/aggregator.py +17 -45
- sqlcg/parsers/ansi_parser.py +2 -2
- sqlcg/server/read_client.py +192 -0
- sqlcg/server/server.py +97 -18
- {sql_code_graph-1.1.3.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,7 +16,7 @@ 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
|
|
@@ -30,17 +30,17 @@ 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=
|
|
43
|
+
sqlcg/parsers/ansi_parser.py,sha256=mGZvijMOMQ4i1BybpwU29a8jnIGViefhy9fxzkSpsRM,17193
|
|
44
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
|
|
@@ -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,26 +19,36 @@ app = typer.Typer(help="Lineage analysis")
|
|
|
19
19
|
console = Console()
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def _kind_filter(
|
|
23
|
-
"""Build the
|
|
22
|
+
def _kind_filter(node_alias: str, *, include_intermediate: bool) -> str:
|
|
23
|
+
"""Build the SqlTable kind-filter clause for upstream/downstream queries.
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
node is
|
|
28
|
-
re-
|
|
29
|
-
``
|
|
30
|
-
node
|
|
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).
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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.
|
|
36
46
|
"""
|
|
37
47
|
if include_intermediate:
|
|
38
48
|
return ""
|
|
39
49
|
return (
|
|
40
|
-
f"OPTIONAL MATCH (t:SqlTable {{qualified: {
|
|
41
|
-
f"WITH
|
|
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'] "
|
|
42
52
|
)
|
|
43
53
|
|
|
44
54
|
|
|
@@ -57,49 +67,46 @@ def upstream( # noqa: B008
|
|
|
57
67
|
console.print("[red]Error: --depth must be between 1 and 100[/red]")
|
|
58
68
|
raise typer.Exit(1)
|
|
59
69
|
|
|
60
|
-
# By default, filter out CTE/derived intermediate nodes; --include-intermediate restores them
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
# By default, filter out CTE/derived intermediate nodes; --include-intermediate restores them
|
|
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},
|
|
82
|
+
)
|
|
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}})"
|
|
70
87
|
f"<-[:{RelType.COLUMN_LINEAGE}*1..{depth}]-(src:{NodeLabel.COLUMN}) "
|
|
71
|
-
f"{
|
|
72
|
-
f"OPTIONAL MATCH (src)-[
|
|
73
|
-
"OPTIONAL MATCH (q:SqlQuery {id:
|
|
74
|
-
"
|
|
75
|
-
|
|
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},
|
|
76
94
|
)
|
|
77
|
-
if
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
f"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"OPTIONAL MATCH (q:SqlQuery {id: direct.query_id}) "
|
|
85
|
-
"RETURN src.id AS id, q.file_path AS file, q.start_line AS line LIMIT 100",
|
|
86
|
-
{"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."
|
|
87
102
|
)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
96
|
-
results = fallback_results
|
|
97
|
-
if not raw:
|
|
98
|
-
from sqlcg.server.noise_filter import NoiseFilter
|
|
99
|
-
|
|
100
|
-
nf = NoiseFilter.from_config() # repo_root=None → falls back to Path.cwd()
|
|
101
|
-
results = _filter_column_results(results, nf)
|
|
102
|
-
_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"])
|
|
103
110
|
|
|
104
111
|
|
|
105
112
|
@app.command("downstream")
|
|
@@ -117,72 +124,70 @@ def downstream( # noqa: B008
|
|
|
117
124
|
console.print("[red]Error: --depth must be between 1 and 100[/red]")
|
|
118
125
|
raise typer.Exit(1)
|
|
119
126
|
|
|
120
|
-
# By default, filter out CTE/derived intermediate nodes; --include-intermediate restores them
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
# By default, filter out CTE/derived intermediate nodes; --include-intermediate restores them
|
|
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},
|
|
139
|
+
)
|
|
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}})"
|
|
129
144
|
f"-[:{RelType.COLUMN_LINEAGE}*1..{depth}]->(dst:{NodeLabel.COLUMN}) "
|
|
130
|
-
f"{
|
|
131
|
-
f"OPTIONAL MATCH (
|
|
132
|
-
"OPTIONAL MATCH (q:SqlQuery {id:
|
|
133
|
-
"
|
|
134
|
-
|
|
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},
|
|
135
151
|
)
|
|
136
|
-
if
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
f"
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
"OPTIONAL MATCH (q:SqlQuery {id: direct.query_id}) "
|
|
144
|
-
"RETURN dst.id AS id, q.file_path AS file, q.start_line AS line LIMIT 100",
|
|
145
|
-
{"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."
|
|
146
159
|
)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
terminal_tables
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
for tbl in sorted(terminal_tables):
|
|
176
|
-
rows_ec = backend.run_read(
|
|
177
|
-
GET_TABLE_EXTERNAL_CONSUMERS_QUERY,
|
|
178
|
-
{"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": ""}
|
|
179
188
|
)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
{"id": f"[external] {ec['name']} ({ec['consumer_type']})", "file:line": ""}
|
|
183
|
-
)
|
|
184
|
-
if consumer_rows:
|
|
185
|
-
_print_table(consumer_rows, ["id", "file:line"])
|
|
189
|
+
if consumer_rows:
|
|
190
|
+
_print_table(consumer_rows, ["id", "file:line"])
|
|
186
191
|
|
|
187
192
|
|
|
188
193
|
@app.command("impact")
|
|
@@ -191,19 +196,18 @@ def impact( # noqa: B008
|
|
|
191
196
|
raw: bool = typer.Option(False, "--raw", help="Disable noise filtering on results"), # noqa: B008
|
|
192
197
|
) -> None:
|
|
193
198
|
"""Show all queries impacted by a table."""
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
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
|
|
203
207
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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"])
|
|
207
211
|
|
|
208
212
|
|
|
209
213
|
@app.command("failures")
|
|
@@ -227,15 +231,14 @@ def failures(
|
|
|
227
231
|
with 'sqlcg db reset && sqlcg index <path>' if the graph was built with
|
|
228
232
|
an earlier version.
|
|
229
233
|
"""
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
_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"])
|
|
239
242
|
|
|
240
243
|
|
|
241
244
|
@app.command("unused")
|
|
@@ -244,18 +247,17 @@ def unused(
|
|
|
244
247
|
raw: bool = typer.Option(False, "--raw", help="Disable noise filtering on results"), # noqa: B008
|
|
245
248
|
) -> None:
|
|
246
249
|
"""Find tables with no query references."""
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
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
|
|
255
257
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
258
|
+
nf = NoiseFilter.from_config()
|
|
259
|
+
results = [r for r in results if not nf.is_noise(r["qualified"])]
|
|
260
|
+
_print_table(results, ["qualified"])
|
|
259
261
|
|
|
260
262
|
|
|
261
263
|
def _bare_ref(ref: str) -> str:
|