codegraph-nav 0.1.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.
- codegraph_nav/__init__.py +194 -0
- codegraph_nav/ast_grep_analyzer.py +448 -0
- codegraph_nav/cli.py +223 -0
- codegraph_nav/code_navigator.py +1328 -0
- codegraph_nav/code_search.py +1009 -0
- codegraph_nav/colors.py +209 -0
- codegraph_nav/completions.py +354 -0
- codegraph_nav/dart_analyzer.py +301 -0
- codegraph_nav/dependency_graph.py +814 -0
- codegraph_nav/domain/__init__.py +20 -0
- codegraph_nav/domain/routes.py +337 -0
- codegraph_nav/domain/schemas.py +229 -0
- codegraph_nav/domain/tags.py +87 -0
- codegraph_nav/exporters.py +563 -0
- codegraph_nav/go_analyzer.py +273 -0
- codegraph_nav/graph/__init__.py +72 -0
- codegraph_nav/graph/builder.py +409 -0
- codegraph_nav/graph/communities.py +402 -0
- codegraph_nav/graph/flows.py +311 -0
- codegraph_nav/graph/query.py +380 -0
- codegraph_nav/graph/schema.py +266 -0
- codegraph_nav/graph/search.py +257 -0
- codegraph_nav/graph/store.py +517 -0
- codegraph_nav/hints.py +195 -0
- codegraph_nav/import_resolver.py +891 -0
- codegraph_nav/js_ts_analyzer.py +564 -0
- codegraph_nav/line_reader.py +664 -0
- codegraph_nav/mcp/__init__.py +39 -0
- codegraph_nav/mcp/__main__.py +5 -0
- codegraph_nav/mcp/server.py +2228 -0
- codegraph_nav/py.typed +2 -0
- codegraph_nav/ruby_analyzer.py +259 -0
- codegraph_nav/rust_analyzer.py +379 -0
- codegraph_nav/token_efficient_renderer.py +743 -0
- codegraph_nav/watcher.py +382 -0
- codegraph_nav-0.1.0.dist-info/METADATA +487 -0
- codegraph_nav-0.1.0.dist-info/RECORD +41 -0
- codegraph_nav-0.1.0.dist-info/WHEEL +5 -0
- codegraph_nav-0.1.0.dist-info/entry_points.txt +4 -0
- codegraph_nav-0.1.0.dist-info/licenses/LICENSE +21 -0
- codegraph_nav-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
"""GraphStore — SQLite-backed graph storage with WAL mode.
|
|
2
|
+
|
|
3
|
+
Provides CRUD operations for nodes, edges, and flows. All queries use
|
|
4
|
+
parameterized SQL to prevent injection. Batch operations respect the
|
|
5
|
+
SQLite 999-variable limit.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sqlite3
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import cast
|
|
15
|
+
|
|
16
|
+
from .schema import BATCH_SIZE, ensure_schema, is_fts5_available
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GraphStore:
|
|
20
|
+
"""SQLite graph store with WAL mode for concurrent access."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, db_path: str | Path):
|
|
23
|
+
self.db_path = Path(db_path)
|
|
24
|
+
self._conn = sqlite3.connect(str(self.db_path), timeout=30, check_same_thread=False)
|
|
25
|
+
self._conn.row_factory = sqlite3.Row
|
|
26
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
27
|
+
self._conn.execute("PRAGMA busy_timeout=5000")
|
|
28
|
+
self._schema_version = ensure_schema(self._conn)
|
|
29
|
+
self._fts_available: bool | None = None
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def conn(self) -> sqlite3.Connection:
|
|
33
|
+
return self._conn
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def fts_available(self) -> bool:
|
|
37
|
+
if self._fts_available is None:
|
|
38
|
+
self._fts_available = is_fts5_available(self._conn)
|
|
39
|
+
return self._fts_available
|
|
40
|
+
|
|
41
|
+
def close(self):
|
|
42
|
+
if self._conn:
|
|
43
|
+
self._conn.close()
|
|
44
|
+
self._conn = None
|
|
45
|
+
|
|
46
|
+
# ------------------------------------------------------------------
|
|
47
|
+
# Node CRUD
|
|
48
|
+
# ------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def upsert_node(
|
|
51
|
+
self,
|
|
52
|
+
kind: str,
|
|
53
|
+
name: str,
|
|
54
|
+
qualified_name: str,
|
|
55
|
+
file_path: str,
|
|
56
|
+
line_start: int | None = None,
|
|
57
|
+
line_end: int | None = None,
|
|
58
|
+
language: str | None = None,
|
|
59
|
+
parent_name: str | None = None,
|
|
60
|
+
signature: str | None = None,
|
|
61
|
+
is_test: bool = False,
|
|
62
|
+
file_hash: str | None = None,
|
|
63
|
+
extra: dict | None = None,
|
|
64
|
+
) -> int:
|
|
65
|
+
"""Insert or replace a node. Returns the node ID."""
|
|
66
|
+
now = time.time()
|
|
67
|
+
extra_json = json.dumps(extra) if extra else "{}"
|
|
68
|
+
cur = self._conn.execute(
|
|
69
|
+
"""INSERT OR REPLACE INTO nodes
|
|
70
|
+
(kind, name, qualified_name, file_path, line_start, line_end,
|
|
71
|
+
language, parent_name, signature, is_test, file_hash, extra, updated_at)
|
|
72
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
73
|
+
(
|
|
74
|
+
kind,
|
|
75
|
+
name,
|
|
76
|
+
qualified_name,
|
|
77
|
+
file_path,
|
|
78
|
+
line_start,
|
|
79
|
+
line_end,
|
|
80
|
+
language,
|
|
81
|
+
parent_name,
|
|
82
|
+
signature,
|
|
83
|
+
int(is_test),
|
|
84
|
+
file_hash,
|
|
85
|
+
extra_json,
|
|
86
|
+
now,
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
self._conn.commit()
|
|
90
|
+
assert cur.lastrowid is not None
|
|
91
|
+
return cur.lastrowid
|
|
92
|
+
|
|
93
|
+
def batch_upsert_nodes(self, nodes: list[dict]):
|
|
94
|
+
"""Bulk insert/replace nodes."""
|
|
95
|
+
now = time.time()
|
|
96
|
+
rows = []
|
|
97
|
+
for n in nodes:
|
|
98
|
+
extra_json = json.dumps(n.get("extra", {})) if n.get("extra") else "{}"
|
|
99
|
+
rows.append(
|
|
100
|
+
(
|
|
101
|
+
n["kind"],
|
|
102
|
+
n["name"],
|
|
103
|
+
n["qualified_name"],
|
|
104
|
+
n["file_path"],
|
|
105
|
+
n.get("line_start"),
|
|
106
|
+
n.get("line_end"),
|
|
107
|
+
n.get("language"),
|
|
108
|
+
n.get("parent_name"),
|
|
109
|
+
n.get("signature"),
|
|
110
|
+
int(n.get("is_test", False)),
|
|
111
|
+
n.get("file_hash"),
|
|
112
|
+
extra_json,
|
|
113
|
+
now,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
self._conn.executemany(
|
|
117
|
+
"""INSERT OR REPLACE INTO nodes
|
|
118
|
+
(kind, name, qualified_name, file_path, line_start, line_end,
|
|
119
|
+
language, parent_name, signature, is_test, file_hash, extra, updated_at)
|
|
120
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
121
|
+
rows,
|
|
122
|
+
)
|
|
123
|
+
self._conn.commit()
|
|
124
|
+
|
|
125
|
+
def get_node(self, qualified_name: str) -> sqlite3.Row | None:
|
|
126
|
+
return cast(
|
|
127
|
+
"sqlite3.Row | None",
|
|
128
|
+
self._conn.execute(
|
|
129
|
+
"SELECT * FROM nodes WHERE qualified_name = ?", (qualified_name,)
|
|
130
|
+
).fetchone(),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def get_node_by_id(self, node_id: int) -> sqlite3.Row | None:
|
|
134
|
+
return cast(
|
|
135
|
+
"sqlite3.Row | None",
|
|
136
|
+
self._conn.execute("SELECT * FROM nodes WHERE id = ?", (node_id,)).fetchone(),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def get_nodes_by_file(self, file_path: str) -> list[sqlite3.Row]:
|
|
140
|
+
return self._conn.execute(
|
|
141
|
+
"SELECT * FROM nodes WHERE file_path = ?", (file_path,)
|
|
142
|
+
).fetchall()
|
|
143
|
+
|
|
144
|
+
def get_nodes_by_name(self, name: str) -> list[sqlite3.Row]:
|
|
145
|
+
return self._conn.execute("SELECT * FROM nodes WHERE name = ?", (name,)).fetchall()
|
|
146
|
+
|
|
147
|
+
def get_all_nodes(self, kind: str | None = None) -> list[sqlite3.Row]:
|
|
148
|
+
if kind:
|
|
149
|
+
return self._conn.execute("SELECT * FROM nodes WHERE kind = ?", (kind,)).fetchall()
|
|
150
|
+
return self._conn.execute("SELECT * FROM nodes").fetchall()
|
|
151
|
+
|
|
152
|
+
def delete_file_nodes(self, file_path: str):
|
|
153
|
+
"""Delete all nodes and their edges for a file."""
|
|
154
|
+
# Get qualified names for edge cleanup
|
|
155
|
+
qns = [
|
|
156
|
+
row[0]
|
|
157
|
+
for row in self._conn.execute(
|
|
158
|
+
"SELECT qualified_name FROM nodes WHERE file_path = ?", (file_path,)
|
|
159
|
+
).fetchall()
|
|
160
|
+
]
|
|
161
|
+
if qns:
|
|
162
|
+
for i in range(0, len(qns), BATCH_SIZE):
|
|
163
|
+
batch = qns[i : i + BATCH_SIZE]
|
|
164
|
+
placeholders = ",".join("?" for _ in batch)
|
|
165
|
+
self._conn.execute(
|
|
166
|
+
f"DELETE FROM edges WHERE source_qualified IN ({placeholders})",
|
|
167
|
+
batch,
|
|
168
|
+
)
|
|
169
|
+
self._conn.execute(
|
|
170
|
+
f"DELETE FROM edges WHERE target_qualified IN ({placeholders})",
|
|
171
|
+
batch,
|
|
172
|
+
)
|
|
173
|
+
self._conn.execute("DELETE FROM nodes WHERE file_path = ?", (file_path,))
|
|
174
|
+
# Also delete edges originating from this file
|
|
175
|
+
self._conn.execute("DELETE FROM edges WHERE file_path = ?", (file_path,))
|
|
176
|
+
self._conn.commit()
|
|
177
|
+
|
|
178
|
+
def get_file_hash(self, file_path: str) -> str | None:
|
|
179
|
+
"""Get stored hash for a File node."""
|
|
180
|
+
row = self._conn.execute(
|
|
181
|
+
"SELECT file_hash FROM nodes WHERE file_path = ? AND kind = 'File'",
|
|
182
|
+
(file_path,),
|
|
183
|
+
).fetchone()
|
|
184
|
+
return row[0] if row else None
|
|
185
|
+
|
|
186
|
+
# ------------------------------------------------------------------
|
|
187
|
+
# Edge CRUD
|
|
188
|
+
# ------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
def upsert_edge(
|
|
191
|
+
self,
|
|
192
|
+
kind: str,
|
|
193
|
+
source_qualified: str,
|
|
194
|
+
target_qualified: str,
|
|
195
|
+
file_path: str,
|
|
196
|
+
extra: dict | None = None,
|
|
197
|
+
) -> int:
|
|
198
|
+
"""Insert an edge (ignores duplicates)."""
|
|
199
|
+
now = time.time()
|
|
200
|
+
extra_json = json.dumps(extra) if extra else "{}"
|
|
201
|
+
cur = self._conn.execute(
|
|
202
|
+
"""INSERT OR IGNORE INTO edges
|
|
203
|
+
(kind, source_qualified, target_qualified, file_path, extra, updated_at)
|
|
204
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
205
|
+
(kind, source_qualified, target_qualified, file_path, extra_json, now),
|
|
206
|
+
)
|
|
207
|
+
self._conn.commit()
|
|
208
|
+
assert cur.lastrowid is not None
|
|
209
|
+
return cur.lastrowid
|
|
210
|
+
|
|
211
|
+
def batch_upsert_edges(self, edges: list[dict]):
|
|
212
|
+
"""Bulk insert edges (ignores duplicates)."""
|
|
213
|
+
now = time.time()
|
|
214
|
+
rows = []
|
|
215
|
+
for e in edges:
|
|
216
|
+
extra_json = json.dumps(e.get("extra", {})) if e.get("extra") else "{}"
|
|
217
|
+
rows.append(
|
|
218
|
+
(
|
|
219
|
+
e["kind"],
|
|
220
|
+
e["source_qualified"],
|
|
221
|
+
e["target_qualified"],
|
|
222
|
+
e["file_path"],
|
|
223
|
+
extra_json,
|
|
224
|
+
now,
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
self._conn.executemany(
|
|
228
|
+
"""INSERT OR IGNORE INTO edges
|
|
229
|
+
(kind, source_qualified, target_qualified, file_path, extra, updated_at)
|
|
230
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
231
|
+
rows,
|
|
232
|
+
)
|
|
233
|
+
self._conn.commit()
|
|
234
|
+
|
|
235
|
+
def get_edges_from(self, source_qualified: str, kind: str | None = None) -> list[sqlite3.Row]:
|
|
236
|
+
if kind:
|
|
237
|
+
return self._conn.execute(
|
|
238
|
+
"SELECT * FROM edges WHERE source_qualified = ? AND kind = ?",
|
|
239
|
+
(source_qualified, kind),
|
|
240
|
+
).fetchall()
|
|
241
|
+
return self._conn.execute(
|
|
242
|
+
"SELECT * FROM edges WHERE source_qualified = ?", (source_qualified,)
|
|
243
|
+
).fetchall()
|
|
244
|
+
|
|
245
|
+
def get_edges_to(self, target_qualified: str, kind: str | None = None) -> list[sqlite3.Row]:
|
|
246
|
+
if kind:
|
|
247
|
+
return self._conn.execute(
|
|
248
|
+
"SELECT * FROM edges WHERE target_qualified = ? AND kind = ?",
|
|
249
|
+
(target_qualified, kind),
|
|
250
|
+
).fetchall()
|
|
251
|
+
return self._conn.execute(
|
|
252
|
+
"SELECT * FROM edges WHERE target_qualified = ?", (target_qualified,)
|
|
253
|
+
).fetchall()
|
|
254
|
+
|
|
255
|
+
# ------------------------------------------------------------------
|
|
256
|
+
# Flow operations
|
|
257
|
+
# ------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
def insert_flow(
|
|
260
|
+
self,
|
|
261
|
+
name: str,
|
|
262
|
+
entry_point_id: int,
|
|
263
|
+
depth: int,
|
|
264
|
+
node_count: int,
|
|
265
|
+
file_count: int,
|
|
266
|
+
criticality: float,
|
|
267
|
+
path_ids: list[int],
|
|
268
|
+
) -> int:
|
|
269
|
+
"""Insert a flow and its memberships."""
|
|
270
|
+
now = time.time()
|
|
271
|
+
cur = self._conn.execute(
|
|
272
|
+
"""INSERT INTO flows
|
|
273
|
+
(name, entry_point_id, depth, node_count, file_count, criticality,
|
|
274
|
+
path_json, updated_at)
|
|
275
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
276
|
+
(
|
|
277
|
+
name,
|
|
278
|
+
entry_point_id,
|
|
279
|
+
depth,
|
|
280
|
+
node_count,
|
|
281
|
+
file_count,
|
|
282
|
+
criticality,
|
|
283
|
+
json.dumps(path_ids),
|
|
284
|
+
now,
|
|
285
|
+
),
|
|
286
|
+
)
|
|
287
|
+
flow_id = cur.lastrowid
|
|
288
|
+
assert flow_id is not None
|
|
289
|
+
memberships = [(flow_id, node_id, pos) for pos, node_id in enumerate(path_ids)]
|
|
290
|
+
self._conn.executemany(
|
|
291
|
+
"INSERT OR IGNORE INTO flow_memberships (flow_id, node_id, position) VALUES (?, ?, ?)",
|
|
292
|
+
memberships,
|
|
293
|
+
)
|
|
294
|
+
self._conn.commit()
|
|
295
|
+
return flow_id
|
|
296
|
+
|
|
297
|
+
def get_flows(self, sort_by: str = "criticality", limit: int = 20) -> list[sqlite3.Row]:
|
|
298
|
+
order = "criticality DESC" if sort_by == "criticality" else "name ASC"
|
|
299
|
+
return self._conn.execute(
|
|
300
|
+
f"SELECT * FROM flows ORDER BY {order} LIMIT ?", (limit,)
|
|
301
|
+
).fetchall()
|
|
302
|
+
|
|
303
|
+
def get_flow_memberships(self, node_id: int) -> list[sqlite3.Row]:
|
|
304
|
+
return self._conn.execute(
|
|
305
|
+
"SELECT * FROM flow_memberships WHERE node_id = ?", (node_id,)
|
|
306
|
+
).fetchall()
|
|
307
|
+
|
|
308
|
+
def count_flow_memberships(self, node_id: int) -> int:
|
|
309
|
+
row = self._conn.execute(
|
|
310
|
+
"SELECT COUNT(*) FROM flow_memberships WHERE node_id = ?", (node_id,)
|
|
311
|
+
).fetchone()
|
|
312
|
+
return row[0] if row else 0
|
|
313
|
+
|
|
314
|
+
def clear_flows(self):
|
|
315
|
+
"""Delete all flows and memberships."""
|
|
316
|
+
self._conn.execute("DELETE FROM flow_memberships")
|
|
317
|
+
self._conn.execute("DELETE FROM flows")
|
|
318
|
+
self._conn.commit()
|
|
319
|
+
|
|
320
|
+
# ------------------------------------------------------------------
|
|
321
|
+
# Community operations
|
|
322
|
+
# ------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
def insert_community(
|
|
325
|
+
self,
|
|
326
|
+
name: str,
|
|
327
|
+
cohesion: float,
|
|
328
|
+
node_count: int = 0,
|
|
329
|
+
file_prefix: str | None = None,
|
|
330
|
+
keywords: list[str] | None = None,
|
|
331
|
+
) -> int:
|
|
332
|
+
"""Insert a community. Returns community ID."""
|
|
333
|
+
now = time.time()
|
|
334
|
+
kw_json = json.dumps(keywords or [])
|
|
335
|
+
cur = self._conn.execute(
|
|
336
|
+
"""INSERT INTO communities (name, node_count, cohesion, file_prefix, keywords, updated_at)
|
|
337
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
338
|
+
(name, node_count, cohesion, file_prefix, kw_json, now),
|
|
339
|
+
)
|
|
340
|
+
self._conn.commit()
|
|
341
|
+
assert cur.lastrowid is not None
|
|
342
|
+
return cur.lastrowid
|
|
343
|
+
|
|
344
|
+
def add_community_members(self, community_id: int, node_ids: list[int]):
|
|
345
|
+
"""Add nodes to a community."""
|
|
346
|
+
rows = [(community_id, nid) for nid in node_ids]
|
|
347
|
+
self._conn.executemany(
|
|
348
|
+
"INSERT OR IGNORE INTO community_members (community_id, node_id) VALUES (?, ?)",
|
|
349
|
+
rows,
|
|
350
|
+
)
|
|
351
|
+
self._conn.commit()
|
|
352
|
+
|
|
353
|
+
def get_communities(self, sort_by: str = "node_count", limit: int = 20) -> list[sqlite3.Row]:
|
|
354
|
+
order = {"node_count": "node_count DESC", "cohesion": "cohesion DESC", "name": "name ASC"}
|
|
355
|
+
order_sql = order.get(sort_by, "node_count DESC")
|
|
356
|
+
return self._conn.execute(
|
|
357
|
+
f"SELECT * FROM communities ORDER BY {order_sql} LIMIT ?", (limit,)
|
|
358
|
+
).fetchall()
|
|
359
|
+
|
|
360
|
+
def get_community_members(self, community_id: int) -> list[sqlite3.Row]:
|
|
361
|
+
return self._conn.execute(
|
|
362
|
+
"""SELECT n.* FROM nodes n
|
|
363
|
+
JOIN community_members cm ON cm.node_id = n.id
|
|
364
|
+
WHERE cm.community_id = ?""",
|
|
365
|
+
(community_id,),
|
|
366
|
+
).fetchall()
|
|
367
|
+
|
|
368
|
+
def get_node_community(self, node_id: int) -> sqlite3.Row | None:
|
|
369
|
+
row = self._conn.execute(
|
|
370
|
+
"""SELECT c.* FROM communities c
|
|
371
|
+
JOIN community_members cm ON cm.community_id = c.id
|
|
372
|
+
WHERE cm.node_id = ?""",
|
|
373
|
+
(node_id,),
|
|
374
|
+
).fetchone()
|
|
375
|
+
return cast("sqlite3.Row | None", row)
|
|
376
|
+
|
|
377
|
+
def clear_communities(self):
|
|
378
|
+
"""Delete all communities and memberships."""
|
|
379
|
+
self._conn.execute("DELETE FROM community_members")
|
|
380
|
+
self._conn.execute("DELETE FROM communities")
|
|
381
|
+
self._conn.commit()
|
|
382
|
+
|
|
383
|
+
# ------------------------------------------------------------------
|
|
384
|
+
# Route operations
|
|
385
|
+
# ------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
def insert_route(
|
|
388
|
+
self,
|
|
389
|
+
method: str,
|
|
390
|
+
path: str,
|
|
391
|
+
file_path: str,
|
|
392
|
+
handler_name: str | None = None,
|
|
393
|
+
framework: str | None = None,
|
|
394
|
+
tags: list[str] | None = None,
|
|
395
|
+
confidence: str = "high",
|
|
396
|
+
) -> int:
|
|
397
|
+
now = time.time()
|
|
398
|
+
tags_json = json.dumps(tags or [])
|
|
399
|
+
cur = self._conn.execute(
|
|
400
|
+
"""INSERT INTO routes (method, path, file_path, handler_name, framework, tags, confidence, updated_at)
|
|
401
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
402
|
+
(method, path, file_path, handler_name, framework, tags_json, confidence, now),
|
|
403
|
+
)
|
|
404
|
+
self._conn.commit()
|
|
405
|
+
assert cur.lastrowid is not None
|
|
406
|
+
return cur.lastrowid
|
|
407
|
+
|
|
408
|
+
def get_routes(
|
|
409
|
+
self, framework: str | None = None, file_path: str | None = None
|
|
410
|
+
) -> list[sqlite3.Row]:
|
|
411
|
+
if framework and file_path:
|
|
412
|
+
return self._conn.execute(
|
|
413
|
+
"SELECT * FROM routes WHERE framework = ? AND file_path = ?",
|
|
414
|
+
(framework, file_path),
|
|
415
|
+
).fetchall()
|
|
416
|
+
if framework:
|
|
417
|
+
return self._conn.execute(
|
|
418
|
+
"SELECT * FROM routes WHERE framework = ?", (framework,)
|
|
419
|
+
).fetchall()
|
|
420
|
+
if file_path:
|
|
421
|
+
return self._conn.execute(
|
|
422
|
+
"SELECT * FROM routes WHERE file_path = ?", (file_path,)
|
|
423
|
+
).fetchall()
|
|
424
|
+
return self._conn.execute("SELECT * FROM routes").fetchall()
|
|
425
|
+
|
|
426
|
+
def clear_routes(self):
|
|
427
|
+
self._conn.execute("DELETE FROM routes")
|
|
428
|
+
self._conn.commit()
|
|
429
|
+
|
|
430
|
+
# ------------------------------------------------------------------
|
|
431
|
+
# Schema/ORM operations
|
|
432
|
+
# ------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
def insert_schema(
|
|
435
|
+
self,
|
|
436
|
+
name: str,
|
|
437
|
+
file_path: str,
|
|
438
|
+
orm: str | None = None,
|
|
439
|
+
fields: list[dict] | None = None,
|
|
440
|
+
relations: list[str] | None = None,
|
|
441
|
+
) -> int:
|
|
442
|
+
now = time.time()
|
|
443
|
+
fields_json = json.dumps(fields or [])
|
|
444
|
+
relations_json = json.dumps(relations or [])
|
|
445
|
+
cur = self._conn.execute(
|
|
446
|
+
"""INSERT INTO schemas (name, file_path, orm, fields, relations, updated_at)
|
|
447
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
448
|
+
(name, file_path, orm, fields_json, relations_json, now),
|
|
449
|
+
)
|
|
450
|
+
self._conn.commit()
|
|
451
|
+
assert cur.lastrowid is not None
|
|
452
|
+
return cur.lastrowid
|
|
453
|
+
|
|
454
|
+
def get_schemas(self, orm: str | None = None) -> list[sqlite3.Row]:
|
|
455
|
+
if orm:
|
|
456
|
+
return self._conn.execute("SELECT * FROM schemas WHERE orm = ?", (orm,)).fetchall()
|
|
457
|
+
return self._conn.execute("SELECT * FROM schemas").fetchall()
|
|
458
|
+
|
|
459
|
+
def clear_schemas(self):
|
|
460
|
+
self._conn.execute("DELETE FROM schemas")
|
|
461
|
+
self._conn.commit()
|
|
462
|
+
|
|
463
|
+
# ------------------------------------------------------------------
|
|
464
|
+
# Stats
|
|
465
|
+
# ------------------------------------------------------------------
|
|
466
|
+
|
|
467
|
+
def get_stats(self) -> dict:
|
|
468
|
+
"""Get graph statistics."""
|
|
469
|
+
node_count = self._conn.execute("SELECT COUNT(*) FROM nodes").fetchone()[0]
|
|
470
|
+
edge_count = self._conn.execute("SELECT COUNT(*) FROM edges").fetchone()[0]
|
|
471
|
+
file_count = self._conn.execute(
|
|
472
|
+
"SELECT COUNT(*) FROM nodes WHERE kind = 'File'"
|
|
473
|
+
).fetchone()[0]
|
|
474
|
+
flow_count = self._conn.execute("SELECT COUNT(*) FROM flows").fetchone()[0]
|
|
475
|
+
community_count = self._conn.execute("SELECT COUNT(*) FROM communities").fetchone()[0]
|
|
476
|
+
route_count = self._conn.execute("SELECT COUNT(*) FROM routes").fetchone()[0]
|
|
477
|
+
schema_count = self._conn.execute("SELECT COUNT(*) FROM schemas").fetchone()[0]
|
|
478
|
+
|
|
479
|
+
kind_counts = {}
|
|
480
|
+
for row in self._conn.execute("SELECT kind, COUNT(*) FROM nodes GROUP BY kind").fetchall():
|
|
481
|
+
kind_counts[row[0]] = row[1]
|
|
482
|
+
|
|
483
|
+
edge_kind_counts = {}
|
|
484
|
+
for row in self._conn.execute("SELECT kind, COUNT(*) FROM edges GROUP BY kind").fetchall():
|
|
485
|
+
edge_kind_counts[row[0]] = row[1]
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
"nodes": node_count,
|
|
489
|
+
"edges": edge_count,
|
|
490
|
+
"files": file_count,
|
|
491
|
+
"flows": flow_count,
|
|
492
|
+
"communities": community_count,
|
|
493
|
+
"routes": route_count,
|
|
494
|
+
"schemas": schema_count,
|
|
495
|
+
"nodes_by_kind": kind_counts,
|
|
496
|
+
"edges_by_kind": edge_kind_counts,
|
|
497
|
+
"fts_available": self.fts_available,
|
|
498
|
+
"schema_version": self._schema_version,
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
# ------------------------------------------------------------------
|
|
502
|
+
# Batch helpers
|
|
503
|
+
# ------------------------------------------------------------------
|
|
504
|
+
|
|
505
|
+
def batch_get_nodes(self, qualified_names: set[str]) -> list[sqlite3.Row]:
|
|
506
|
+
"""Fetch nodes by qualified names in batches."""
|
|
507
|
+
results = []
|
|
508
|
+
qns = list(qualified_names)
|
|
509
|
+
for i in range(0, len(qns), BATCH_SIZE):
|
|
510
|
+
batch = qns[i : i + BATCH_SIZE]
|
|
511
|
+
placeholders = ",".join("?" for _ in batch)
|
|
512
|
+
rows = self._conn.execute(
|
|
513
|
+
f"SELECT * FROM nodes WHERE qualified_name IN ({placeholders})",
|
|
514
|
+
batch,
|
|
515
|
+
).fetchall()
|
|
516
|
+
results.extend(rows)
|
|
517
|
+
return results
|