sql-code-graph 1.2.2__py3-none-any.whl → 1.3.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.
- {sql_code_graph-1.2.2.dist-info → sql_code_graph-1.3.0.dist-info}/METADATA +1 -1
- {sql_code_graph-1.2.2.dist-info → sql_code_graph-1.3.0.dist-info}/RECORD +15 -14
- sqlcg/__init__.py +1 -1
- sqlcg/cli/commands/db.py +23 -0
- sqlcg/cli/commands/git.py +11 -4
- sqlcg/cli/commands/index.py +167 -4
- sqlcg/cli/commands/mcp.py +70 -3
- sqlcg/cli/commands/reindex.py +146 -76
- sqlcg/core/kuzu_backend.py +10 -6
- sqlcg/metrics/store.py +48 -0
- sqlcg/server/server.py +165 -70
- sqlcg/server/tools.py +155 -14
- sqlcg/server/writer.py +634 -0
- {sql_code_graph-1.2.2.dist-info → sql_code_graph-1.3.0.dist-info}/WHEEL +0 -0
- {sql_code_graph-1.2.2.dist-info → sql_code_graph-1.3.0.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.3.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,17 +1,17 @@
|
|
|
1
|
-
sqlcg/__init__.py,sha256=
|
|
1
|
+
sqlcg/__init__.py,sha256=Zwww9eU1OV0KsLzksvKQE65bNgEFPPwTUb0XCSW4JIE,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
6
|
sqlcg/cli/commands/analyze.py,sha256=xr3RHmO4eFTP4VKZn4DAx3BJzSi60_DIZmdE-QLfsHI,13601
|
|
7
|
-
sqlcg/cli/commands/db.py,sha256=
|
|
7
|
+
sqlcg/cli/commands/db.py,sha256=paW096LE8fMpxPNvoW9zHmZ9xjb-dEbwVmfHR1bcg7U,8676
|
|
8
8
|
sqlcg/cli/commands/find.py,sha256=zTYN9goILalYq4R9x6lIR6MmNcydDbR17UXkx1gPRsI,2913
|
|
9
9
|
sqlcg/cli/commands/gain.py,sha256=Kws76u1na2XxmbWN_YWrPaYHYmYBLC6DDDf7xqnltqc,9126
|
|
10
|
-
sqlcg/cli/commands/git.py,sha256=
|
|
11
|
-
sqlcg/cli/commands/index.py,sha256=
|
|
10
|
+
sqlcg/cli/commands/git.py,sha256=9a8T2FVxcAHq1H6Cslaq34t10w9fBGf4T2reiLk33t8,6135
|
|
11
|
+
sqlcg/cli/commands/index.py,sha256=1ElHRPkn-CPprIz869A__98aSyf-P5E56PVA0lq7xBw,17462
|
|
12
12
|
sqlcg/cli/commands/install.py,sha256=KNABvrLbamPyYnmnVdCaM_MNezbDc-pr6IkignCWI8k,9186
|
|
13
|
-
sqlcg/cli/commands/mcp.py,sha256=
|
|
14
|
-
sqlcg/cli/commands/reindex.py,sha256=
|
|
13
|
+
sqlcg/cli/commands/mcp.py,sha256=QYaupf69lLpYzIoqsPJoCPiAggLVkYBzwpuOLRzxJDU,9140
|
|
14
|
+
sqlcg/cli/commands/reindex.py,sha256=Ki5BHbI_nuM6ML0-w7qnVZqSAhELSpsFaUo6BVqzhRo,13812
|
|
15
15
|
sqlcg/cli/commands/report.py,sha256=JU0qjyMxwOukE7bN3XvvIzOI7zMg_Gsnvk_8F6pKNpA,4915
|
|
16
16
|
sqlcg/cli/commands/uninstall.py,sha256=IYwQaqnMmmzW0Nlls40wD-L3tVkMgKIMRXUkcXPMUc4,9398
|
|
17
17
|
sqlcg/cli/commands/watch.py,sha256=7N6c-QuvxAEGHzDZ0C3CU2BkHSraZW9YtgoFnz7SaQo,2373
|
|
@@ -20,7 +20,7 @@ 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=PHW7VqI7oCLKsHnm4OoBoNnE2XT19ohxUpQMMIJnjlY,17038
|
|
24
24
|
sqlcg/core/neo4j_backend.py,sha256=AM1TncP9GBGph-rSHwalZPmGUV2kFILzaJP-PSB0UYw,8437
|
|
25
25
|
sqlcg/core/queries.cypher,sha256=cvPOVe5GUOzJN4bxUvDxNI--xIIP8gm42TR-gUnea4U,4685
|
|
26
26
|
sqlcg/core/queries.py,sha256=gkl4bhkZM8FsvbSA-IaK17sRFcO3hB5YlVCemkCXgWM,2064
|
|
@@ -38,7 +38,7 @@ sqlcg/lineage/__init__.py,sha256=Da1DlYwtK13WHv_RnHjAtNkHTOuFbhxqCjT1Le7DsWM,46
|
|
|
38
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
|
-
sqlcg/metrics/store.py,sha256=
|
|
41
|
+
sqlcg/metrics/store.py,sha256=O1UoBu4dIZYIgNBqLWIyL3vLAnSgWrJinOgSLhQigHM,10596
|
|
42
42
|
sqlcg/parsers/__init__.py,sha256=AamA8wBbDZV9_zEtZCI4Hyen5UAVKHmBwjTghTt2PZE,785
|
|
43
43
|
sqlcg/parsers/ansi_parser.py,sha256=mGZvijMOMQ4i1BybpwU29a8jnIGViefhy9fxzkSpsRM,17193
|
|
44
44
|
sqlcg/parsers/base.py,sha256=IiOkVsm6jz9-48RqDCXiW-UXAraNxQ4pKXvSA7aolnA,49907
|
|
@@ -53,14 +53,15 @@ sqlcg/server/exceptions.py,sha256=EONw34icOByCTpppSQrvQBW6asc4hfqaGDCAFjv96II,46
|
|
|
53
53
|
sqlcg/server/models.py,sha256=l7ORy6sbtzBW1y3qVaeLwEukbyAgBkz9S5VIm2q4b24,19378
|
|
54
54
|
sqlcg/server/noise_filter.py,sha256=idSBGgdKWWccJdpOo9qgbM2350Oew-2l5W6Yc9GYQqY,6337
|
|
55
55
|
sqlcg/server/read_client.py,sha256=ncoJK7UckGhWtN9bv1CgViNMNtac96zBUE7RPYQ8_WI,7783
|
|
56
|
-
sqlcg/server/server.py,sha256=
|
|
56
|
+
sqlcg/server/server.py,sha256=9SilAu18cHTZUQvSo8S8e9CxSM6CUlG8rX2OnHBUh1Y,24178
|
|
57
57
|
sqlcg/server/skill.py,sha256=GE8eeimk6yiGGJ74erGypqYAviur5peSR6_2a4QQWVM,12828
|
|
58
|
-
sqlcg/server/tools.py,sha256=
|
|
58
|
+
sqlcg/server/tools.py,sha256=DTaDwZQmL6jzNF8vgJeNhVMGRoIrrpcxNiXOMpWvx-A,72401
|
|
59
|
+
sqlcg/server/writer.py,sha256=gagS6EVG8A4OKpf0GAb--MUielnaiIULwVVik58pT6k,24693
|
|
59
60
|
sqlcg/utils/__init__.py,sha256=--iqt5ThTXmT8Wz7da8hs3n0zDfYPl8P-z5OgRJ_77E,154
|
|
60
61
|
sqlcg/utils/hashing.py,sha256=H25-sYfxHKb3_IERFnHyAIYNiXN470Oqo5sJT_D3YOA,438
|
|
61
62
|
sqlcg/utils/ignore.py,sha256=wJjwa0mjnQ_xJExOUxk25y00g065XmmzJapqV3ifD5o,1151
|
|
62
63
|
sqlcg/utils/logging.py,sha256=u0fCmYsLj9o81vawm3xZTHaw68GQYVm7JxG-gP81u8A,840
|
|
63
|
-
sql_code_graph-1.
|
|
64
|
-
sql_code_graph-1.
|
|
65
|
-
sql_code_graph-1.
|
|
66
|
-
sql_code_graph-1.
|
|
64
|
+
sql_code_graph-1.3.0.dist-info/METADATA,sha256=2QJqn9Q606zlPKDgeYrNzq0d1QW35jIRCxmvDCvvNIE,14148
|
|
65
|
+
sql_code_graph-1.3.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
66
|
+
sql_code_graph-1.3.0.dist-info/entry_points.txt,sha256=Wfe49sVzV9p4eVFGo5RxcV-frr3HOP0yzzst8JBxQLQ,46
|
|
67
|
+
sql_code_graph-1.3.0.dist-info/RECORD,,
|
sqlcg/__init__.py
CHANGED
sqlcg/cli/commands/db.py
CHANGED
|
@@ -45,6 +45,29 @@ def db_reset( # noqa: B008
|
|
|
45
45
|
repo: str | None = typer.Option(None, "--repo", help="Reset only this repo path"), # noqa: B008
|
|
46
46
|
) -> None:
|
|
47
47
|
"""Wipe the database or a single repo's subgraph."""
|
|
48
|
+
import socket as _socket
|
|
49
|
+
|
|
50
|
+
from sqlcg.server.control import sock_path
|
|
51
|
+
|
|
52
|
+
# Step 3.4 (OD-3 / W2): refuse cleanly when a server is live — both the
|
|
53
|
+
# full reset and the --repo partial reset open the RW backend directly and
|
|
54
|
+
# would fight the server's lock. Guard runs BEFORE either destructive branch.
|
|
55
|
+
sp = sock_path()
|
|
56
|
+
if sp.exists():
|
|
57
|
+
try:
|
|
58
|
+
with _socket.socket(_socket.AF_UNIX, _socket.SOCK_STREAM) as s:
|
|
59
|
+
s.settimeout(1)
|
|
60
|
+
s.connect(str(sp))
|
|
61
|
+
# Connection succeeded — a server is live.
|
|
62
|
+
console.print(
|
|
63
|
+
"[red]A server is running on this database; stop it first "
|
|
64
|
+
"('sqlcg mcp stop') before resetting the database.[/red]"
|
|
65
|
+
)
|
|
66
|
+
raise typer.Exit(1)
|
|
67
|
+
except (FileNotFoundError, ConnectionRefusedError, OSError):
|
|
68
|
+
# No live server — fall through to destructive action.
|
|
69
|
+
pass
|
|
70
|
+
|
|
48
71
|
if repo:
|
|
49
72
|
# Delete all nodes for this repo (use run_write for mutation)
|
|
50
73
|
with get_backend() as backend:
|
sqlcg/cli/commands/git.py
CHANGED
|
@@ -33,7 +33,7 @@ _HOOKS: list[_HookSpec] = [
|
|
|
33
33
|
'[ "$3" = "1" ] || exit 0\n'
|
|
34
34
|
'{sqlcg_bin} reindex --from "$1" --to "$2"'
|
|
35
35
|
' "$(git rev-parse --show-toplevel)" --dialect auto --quiet --notify'
|
|
36
|
-
' || echo "sqlcg: graph not updated (
|
|
36
|
+
' || echo "sqlcg: graph not updated (reindex failed)'
|
|
37
37
|
" -- run 'sqlcg mcp status'\" >&2\n"
|
|
38
38
|
),
|
|
39
39
|
),
|
|
@@ -50,10 +50,10 @@ PREV=$(git rev-parse --verify --quiet ORIG_HEAD)
|
|
|
50
50
|
TOP=$(git rev-parse --show-toplevel)
|
|
51
51
|
if [ -n "$PREV" ]; then
|
|
52
52
|
{sqlcg_bin} reindex --from "$PREV" --to HEAD "$TOP" --dialect auto --quiet --notify \\
|
|
53
|
-
|| echo "sqlcg: graph not updated (
|
|
53
|
+
|| echo "sqlcg: graph not updated (reindex failed) -- run 'sqlcg mcp status'" >&2
|
|
54
54
|
else
|
|
55
55
|
{sqlcg_bin} reindex "$TOP" --dialect auto --quiet --notify \\
|
|
56
|
-
|| echo "sqlcg: graph not updated (
|
|
56
|
+
|| echo "sqlcg: graph not updated (reindex failed) -- run 'sqlcg mcp status'" >&2
|
|
57
57
|
fi
|
|
58
58
|
""",
|
|
59
59
|
),
|
|
@@ -101,7 +101,14 @@ def _install_single_hook(hooks_dir: Path, spec: _HookSpec, sqlcg_bin: str) -> No
|
|
|
101
101
|
if hook_path.exists():
|
|
102
102
|
existing_content = hook_path.read_text()
|
|
103
103
|
if spec.sentinel in existing_content:
|
|
104
|
-
|
|
104
|
+
if existing_content == script:
|
|
105
|
+
# Byte-identical current template — true idempotency, silent skip.
|
|
106
|
+
return
|
|
107
|
+
# Sentinel present but content differs: sqlcg-owned but stale hook.
|
|
108
|
+
# Overwrite with the current rendered template and report the upgrade.
|
|
109
|
+
hook_path.write_text(script)
|
|
110
|
+
hook_path.chmod(0o755)
|
|
111
|
+
console.print(f"[green]Upgraded git hook:[/green] .git/hooks/{spec.filename}")
|
|
105
112
|
return
|
|
106
113
|
else:
|
|
107
114
|
# Foreign hook without sqlcg sentinel
|
sqlcg/cli/commands/index.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Index command for scanning and indexing SQL files."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import os
|
|
5
|
+
import socket as _socket
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
|
|
6
8
|
import typer
|
|
@@ -19,6 +21,10 @@ from sqlcg.indexer.indexer import Indexer
|
|
|
19
21
|
|
|
20
22
|
console = Console()
|
|
21
23
|
|
|
24
|
+
# Socket timeout for the index-via-server path.
|
|
25
|
+
# Generous budget: full index of a large repo can take several minutes.
|
|
26
|
+
_INDEX_SOCKET_TIMEOUT_S = 600
|
|
27
|
+
|
|
22
28
|
|
|
23
29
|
def index_cmd( # noqa: B008
|
|
24
30
|
path: Path = typer.Argument(..., help="Directory to index"), # noqa: B008
|
|
@@ -71,9 +77,24 @@ def index_cmd( # noqa: B008
|
|
|
71
77
|
"Marks freshness as 'indexed with working-tree changes'."
|
|
72
78
|
),
|
|
73
79
|
),
|
|
80
|
+
detach: bool = typer.Option( # noqa: B008
|
|
81
|
+
False,
|
|
82
|
+
"--detach",
|
|
83
|
+
help=(
|
|
84
|
+
"When routing through a live server, return immediately after enqueueing "
|
|
85
|
+
"(fire-and-forget). Default is to wait for the index to complete."
|
|
86
|
+
),
|
|
87
|
+
),
|
|
74
88
|
) -> None:
|
|
75
89
|
"""Index SQL files in a directory.
|
|
76
90
|
|
|
91
|
+
When a server is live on this DB, the index is routed through the server's
|
|
92
|
+
control socket so the DB is never opened directly (avoids lock contention).
|
|
93
|
+
Use --detach to enqueue and return immediately (fire-and-forget).
|
|
94
|
+
|
|
95
|
+
With no server live, falls back to the direct-write path unchanged
|
|
96
|
+
(zero-config small-repo invariant).
|
|
97
|
+
|
|
77
98
|
Schema aliases (staging schema → canonical schema) can be configured in
|
|
78
99
|
.sqlcg.toml under sqlcg.schema_aliases, e.g. da_tmp = "da".
|
|
79
100
|
"""
|
|
@@ -85,6 +106,26 @@ def index_cmd( # noqa: B008
|
|
|
85
106
|
logging.getLogger("sqlcg").setLevel(level)
|
|
86
107
|
logging.getLogger("sqlglot").setLevel(level)
|
|
87
108
|
|
|
109
|
+
# Resolve path early so socket routing uses the absolute path.
|
|
110
|
+
path = path.resolve()
|
|
111
|
+
|
|
112
|
+
# Resolve dialect before routing so the WriterRequest always carries a concrete
|
|
113
|
+
# dialect (never the literal sentinel "auto"). Bug A: the route call was before
|
|
114
|
+
# this resolution, causing the server to receive "auto" and fail with
|
|
115
|
+
# "Unknown dialect 'auto'" on every server-routed index.
|
|
116
|
+
if dialect == "auto":
|
|
117
|
+
dialect = get_dialect(path)
|
|
118
|
+
|
|
119
|
+
# Step 3.2 — probe for a live server and route through the socket if present.
|
|
120
|
+
_routed = _try_route_index_via_server(
|
|
121
|
+
path=path,
|
|
122
|
+
dialect=dialect,
|
|
123
|
+
wait=not detach,
|
|
124
|
+
quiet=quiet,
|
|
125
|
+
)
|
|
126
|
+
if _routed:
|
|
127
|
+
return
|
|
128
|
+
|
|
88
129
|
# Route parse warnings to stderr (--verbose) or to the configured log file.
|
|
89
130
|
sqlcg_log = logging.getLogger("sqlcg")
|
|
90
131
|
|
|
@@ -117,10 +158,6 @@ def index_cmd( # noqa: B008
|
|
|
117
158
|
if buffer_pool_size > 0:
|
|
118
159
|
os.environ["SQLCG_BUFFER_POOL_MB"] = str(buffer_pool_size)
|
|
119
160
|
|
|
120
|
-
# Resolve dialect: 'auto' reads from .sqlcg.toml, otherwise use provided value
|
|
121
|
-
if dialect == "auto":
|
|
122
|
-
dialect = get_dialect(path)
|
|
123
|
-
|
|
124
161
|
if not quiet and not config_file_present(path):
|
|
125
162
|
console.print(
|
|
126
163
|
f"[yellow]No .sqlcg.toml found at {path}/.sqlcg.toml — "
|
|
@@ -172,6 +209,132 @@ def index_cmd( # noqa: B008
|
|
|
172
209
|
)
|
|
173
210
|
|
|
174
211
|
|
|
212
|
+
def _try_route_index_via_server(
|
|
213
|
+
*,
|
|
214
|
+
path: Path,
|
|
215
|
+
dialect: str | None,
|
|
216
|
+
wait: bool,
|
|
217
|
+
quiet: bool,
|
|
218
|
+
) -> bool:
|
|
219
|
+
"""Probe for a live server and route the index through the socket if found.
|
|
220
|
+
|
|
221
|
+
Returns True if the index was handled via the server (caller should return).
|
|
222
|
+
Returns False if no server is live (caller should fall through to direct path).
|
|
223
|
+
"""
|
|
224
|
+
from sqlcg.server.control import sock_path
|
|
225
|
+
|
|
226
|
+
sp = sock_path()
|
|
227
|
+
if not sp.exists():
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
payload = {
|
|
231
|
+
"op": "index",
|
|
232
|
+
"root": str(path),
|
|
233
|
+
"dialect": dialect,
|
|
234
|
+
"wait": wait,
|
|
235
|
+
"requested_by": "cli",
|
|
236
|
+
}
|
|
237
|
+
payload_bytes = json.dumps(payload).encode()
|
|
238
|
+
frame = f"{len(payload_bytes)}\n".encode() + payload_bytes
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
with _socket.socket(_socket.AF_UNIX, _socket.SOCK_STREAM) as s:
|
|
242
|
+
s.settimeout(_INDEX_SOCKET_TIMEOUT_S)
|
|
243
|
+
s.connect(str(sp))
|
|
244
|
+
s.sendall(frame)
|
|
245
|
+
|
|
246
|
+
if not wait:
|
|
247
|
+
# Fire-and-forget: read one framed acknowledgement frame.
|
|
248
|
+
f = s.makefile("rb")
|
|
249
|
+
length_line = f.readline()
|
|
250
|
+
if length_line:
|
|
251
|
+
try:
|
|
252
|
+
body_len = int(length_line.strip())
|
|
253
|
+
resp_bytes = f.read(body_len)
|
|
254
|
+
resp = json.loads(resp_bytes)
|
|
255
|
+
if "error" in resp:
|
|
256
|
+
err = resp["error"]
|
|
257
|
+
if "SQLCG_DB_PATH" in err or "write lock" in err:
|
|
258
|
+
console.print(f"[red]{err}[/red]")
|
|
259
|
+
else:
|
|
260
|
+
console.print(f"[red]Server error: {err}[/red]")
|
|
261
|
+
raise typer.Exit(1)
|
|
262
|
+
if not quiet:
|
|
263
|
+
pos = resp.get("position", "?")
|
|
264
|
+
console.print(f"[green]Queued via server[/green] (position {pos})")
|
|
265
|
+
except (ValueError, json.JSONDecodeError):
|
|
266
|
+
pass
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
# wait=True: stream framed frames until done:true.
|
|
270
|
+
f = s.makefile("rb")
|
|
271
|
+
with Progress(
|
|
272
|
+
SpinnerColumn(),
|
|
273
|
+
TextColumn("[progress.description]{task.description}"),
|
|
274
|
+
BarColumn(),
|
|
275
|
+
MofNCompleteColumn(),
|
|
276
|
+
TimeRemainingColumn(),
|
|
277
|
+
console=console,
|
|
278
|
+
redirect_stderr=True,
|
|
279
|
+
) as progress:
|
|
280
|
+
task = progress.add_task("Indexing via server", total=None)
|
|
281
|
+
|
|
282
|
+
while True:
|
|
283
|
+
length_line = f.readline()
|
|
284
|
+
if not length_line:
|
|
285
|
+
break
|
|
286
|
+
try:
|
|
287
|
+
body_len = int(length_line.strip())
|
|
288
|
+
except ValueError:
|
|
289
|
+
break
|
|
290
|
+
frame_bytes = f.read(body_len)
|
|
291
|
+
frame_resp = json.loads(frame_bytes)
|
|
292
|
+
|
|
293
|
+
if frame_resp.get("done"):
|
|
294
|
+
if not frame_resp.get("ok"):
|
|
295
|
+
err = frame_resp.get("error", "unknown error")
|
|
296
|
+
if "SQLCG_DB_PATH" in err or "write lock" in err:
|
|
297
|
+
console.print(f"[red]{err}[/red]")
|
|
298
|
+
else:
|
|
299
|
+
console.print(f"[red]Server index error: {err}[/red]")
|
|
300
|
+
raise typer.Exit(1)
|
|
301
|
+
srv_summary = frame_resp.get("summary", {})
|
|
302
|
+
if not quiet:
|
|
303
|
+
console.print(
|
|
304
|
+
f"[green]Indexed via server[/green] "
|
|
305
|
+
f"{srv_summary.get('files_parsed', '?')} files — "
|
|
306
|
+
f"{srv_summary.get('tables_found', '?')} tables, "
|
|
307
|
+
f"{srv_summary.get('lineage_edges_created', '?')} edges"
|
|
308
|
+
)
|
|
309
|
+
break
|
|
310
|
+
# Progress frame
|
|
311
|
+
files_done = frame_resp.get("files_done", 0)
|
|
312
|
+
files_total = frame_resp.get("files_total")
|
|
313
|
+
if files_total:
|
|
314
|
+
progress.update(task, completed=files_done, total=files_total)
|
|
315
|
+
|
|
316
|
+
return True
|
|
317
|
+
|
|
318
|
+
except TimeoutError:
|
|
319
|
+
import sys as _sys
|
|
320
|
+
|
|
321
|
+
print(
|
|
322
|
+
f"Server is still applying the index (timed out waiting after "
|
|
323
|
+
f"{_INDEX_SOCKET_TIMEOUT_S}s); the graph will update when it finishes "
|
|
324
|
+
"— check 'sqlcg mcp status'.",
|
|
325
|
+
file=_sys.stderr,
|
|
326
|
+
)
|
|
327
|
+
raise typer.Exit(0) from None
|
|
328
|
+
except (FileNotFoundError, ConnectionRefusedError, OSError):
|
|
329
|
+
# No live server — fall through to direct path.
|
|
330
|
+
return False
|
|
331
|
+
except typer.Exit:
|
|
332
|
+
raise
|
|
333
|
+
except Exception as exc:
|
|
334
|
+
console.print(f"[red]Socket routing failed: {exc}[/red]")
|
|
335
|
+
raise typer.Exit(1) from exc
|
|
336
|
+
|
|
337
|
+
|
|
175
338
|
def _run_index(
|
|
176
339
|
*,
|
|
177
340
|
path: Path,
|
sqlcg/cli/commands/mcp.py
CHANGED
|
@@ -78,7 +78,12 @@ def mcp_status() -> None:
|
|
|
78
78
|
"""Print server status JSON (connects to control socket).
|
|
79
79
|
|
|
80
80
|
Returns JSON with fields: running, pid, db_path, indexed_sha, head_sha,
|
|
81
|
-
stale_by_commits, connected_clients, uptime when a server
|
|
81
|
+
stale_by_commits, connected_clients, uptime, writer_queue when a server
|
|
82
|
+
is live.
|
|
83
|
+
|
|
84
|
+
The status response is length-prefixed framed (v1.3.0, B3) so large
|
|
85
|
+
writer_queue payloads are received in full — the client uses the
|
|
86
|
+
recv-exactly makefile+readline+read(n) pattern, NOT a single recv(4096).
|
|
82
87
|
|
|
83
88
|
When no server is found: {"running": false}.
|
|
84
89
|
When the PID file exists with a live process but the socket is unavailable:
|
|
@@ -89,6 +94,7 @@ def mcp_status() -> None:
|
|
|
89
94
|
to the PID-file probe — never hangs or errors on a dead socket.
|
|
90
95
|
"""
|
|
91
96
|
import socket as _socket
|
|
97
|
+
from datetime import datetime
|
|
92
98
|
|
|
93
99
|
from sqlcg.server.control import is_pid_alive, read_pid, sock_path
|
|
94
100
|
|
|
@@ -98,8 +104,69 @@ def mcp_status() -> None:
|
|
|
98
104
|
s.settimeout(2)
|
|
99
105
|
s.connect(str(sp))
|
|
100
106
|
s.sendall(json.dumps({"op": "status"}).encode() + b"\n")
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
# Framed recv-exactly (B3 / OD-4): read length line then exactly that many bytes.
|
|
108
|
+
# This replaces the old s.recv(4096) which would truncate large writer_queue payloads.
|
|
109
|
+
f = s.makefile("rb")
|
|
110
|
+
length_line = f.readline()
|
|
111
|
+
if length_line:
|
|
112
|
+
try:
|
|
113
|
+
body_len = int(length_line.strip())
|
|
114
|
+
data = f.read(body_len)
|
|
115
|
+
except (ValueError, OSError):
|
|
116
|
+
data = length_line # fallback: treat first line as body
|
|
117
|
+
else:
|
|
118
|
+
data = b""
|
|
119
|
+
|
|
120
|
+
status = json.loads(data.decode())
|
|
121
|
+
|
|
122
|
+
# Pretty-print the base fields.
|
|
123
|
+
console.print_json(json.dumps({k: v for k, v in status.items() if k != "writer_queue"}))
|
|
124
|
+
|
|
125
|
+
# Render the writer_queue block separately for readability.
|
|
126
|
+
wq = status.get("writer_queue")
|
|
127
|
+
if wq:
|
|
128
|
+
console.print("\n[bold]writer_queue[/bold]")
|
|
129
|
+
active = wq.get("active")
|
|
130
|
+
if active:
|
|
131
|
+
console.print(f" active: op={active.get('op')!r} root={active.get('root')!r}")
|
|
132
|
+
prog = wq.get("active_progress", {})
|
|
133
|
+
if prog.get("state") == "running":
|
|
134
|
+
files_done = prog.get("files_done", 0)
|
|
135
|
+
files_total = prog.get("files_total")
|
|
136
|
+
if files_total:
|
|
137
|
+
console.print(f" progress: {files_done}/{files_total} files")
|
|
138
|
+
else:
|
|
139
|
+
console.print(" active: none")
|
|
140
|
+
|
|
141
|
+
pending = wq.get("pending", [])
|
|
142
|
+
console.print(f" pending: {len(pending)}")
|
|
143
|
+
|
|
144
|
+
total_coalesced = wq.get("coalesced_since_start", 0)
|
|
145
|
+
by_reason = wq.get("coalesced_by_reason", {})
|
|
146
|
+
if total_coalesced:
|
|
147
|
+
from sqlcg.server.writer import (
|
|
148
|
+
COALESCE_COLLAPSED_INTO_PENDING_REINDEX,
|
|
149
|
+
COALESCE_REINDEX_DROPPED_INDEX_PENDING,
|
|
150
|
+
COALESCE_SUPERSEDED_BY_INDEX,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
n_sup = by_reason.get(COALESCE_SUPERSEDED_BY_INDEX, 0)
|
|
154
|
+
n_col = by_reason.get(COALESCE_COLLAPSED_INTO_PENDING_REINDEX, 0)
|
|
155
|
+
n_drop = by_reason.get(COALESCE_REINDEX_DROPPED_INDEX_PENDING, 0)
|
|
156
|
+
console.print(
|
|
157
|
+
f" coalesced: {total_coalesced} "
|
|
158
|
+
f"(superseded_by_index={n_sup}, "
|
|
159
|
+
f"collapsed_into_pending_reindex={n_col}, "
|
|
160
|
+
f"reindex_dropped_index_pending={n_drop})"
|
|
161
|
+
)
|
|
162
|
+
last_at = wq.get("last_coalesce_at")
|
|
163
|
+
last_reason = wq.get("last_coalesce_reason")
|
|
164
|
+
if last_at and last_reason:
|
|
165
|
+
last_human = datetime.fromtimestamp(last_at).strftime("%Y-%m-%d %H:%M:%S")
|
|
166
|
+
console.print(f" last coalesce: {last_reason} at {last_human}")
|
|
167
|
+
else:
|
|
168
|
+
console.print(" coalesced: 0")
|
|
169
|
+
|
|
103
170
|
except (FileNotFoundError, ConnectionRefusedError, OSError):
|
|
104
171
|
# Socket unavailable — probe via PID file (R3: stale-socket fall-through)
|
|
105
172
|
rec = read_pid()
|