Skip to content

Commit 02fb62c

Browse files
committed
dummy test file
1 parent e820041 commit 02fb62c

2 files changed

Lines changed: 351 additions & 0 deletions

File tree

dummy_change.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def dummy():
2+
return "incremental test"
3+
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
"""SQLite-backed knowledge store."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import sqlite3
7+
import threading
8+
from dataclasses import asdict
9+
from pathlib import Path
10+
from typing import Any, Optional
11+
12+
from knowcode.data_models import Entity, EntityKind, Location, Relationship, RelationshipKind
13+
from knowcode.storage.knowledge_store import KnowledgeStore
14+
from knowcode.utils.entity_identity import ensure_entity_content_hash
15+
16+
17+
class SqliteKnowledgeStore:
18+
"""SQLite-backed knowledge store with recursive query support."""
19+
20+
SCHEMA_VERSION = 1
21+
22+
def __init__(self, db_path: str | Path) -> None:
23+
"""Initialize SQLite knowledge store."""
24+
self._db_path = Path(db_path)
25+
self._db_path.parent.mkdir(parents=True, exist_ok=True)
26+
self._lock = threading.Lock()
27+
28+
self._conn = sqlite3.connect(
29+
str(self._db_path),
30+
check_same_thread=False,
31+
isolation_level=None, # auto-commit
32+
)
33+
self._conn.row_factory = sqlite3.Row
34+
self._conn.execute("PRAGMA journal_mode=WAL")
35+
self._conn.execute("PRAGMA synchronous=NORMAL")
36+
self._init_schema()
37+
38+
def _init_schema(self) -> None:
39+
"""Initialize database schema."""
40+
with self._lock:
41+
# We use isolation_level=None to manually control transactions when needed,
42+
# but for init we can just execute
43+
self._conn.execute("BEGIN")
44+
try:
45+
self._conn.execute(
46+
"""
47+
CREATE TABLE IF NOT EXISTS entities (
48+
entity_id TEXT UNIQUE NOT NULL,
49+
kind TEXT NOT NULL,
50+
name TEXT NOT NULL,
51+
qualified_name TEXT NOT NULL,
52+
file_path TEXT NOT NULL,
53+
line_start INT NOT NULL,
54+
line_end INT NOT NULL,
55+
docstring TEXT,
56+
signature TEXT,
57+
source_code TEXT,
58+
metadata_json TEXT,
59+
content_hash TEXT
60+
)
61+
"""
62+
)
63+
self._conn.execute(
64+
"""
65+
CREATE TABLE IF NOT EXISTS relationships (
66+
source_id TEXT NOT NULL,
67+
target_id TEXT NOT NULL,
68+
kind TEXT NOT NULL,
69+
metadata_json TEXT
70+
)
71+
"""
72+
)
73+
# Indexes
74+
self._conn.execute("CREATE INDEX IF NOT EXISTS idx_entities_kind ON entities(kind)")
75+
self._conn.execute("CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name)")
76+
self._conn.execute("CREATE INDEX IF NOT EXISTS idx_entities_qualified_name ON entities(qualified_name)")
77+
78+
self._conn.execute("CREATE INDEX IF NOT EXISTS idx_relationships_source_kind ON relationships(source_id, kind)")
79+
self._conn.execute("CREATE INDEX IF NOT EXISTS idx_relationships_target_kind ON relationships(target_id, kind)")
80+
self._conn.execute("COMMIT")
81+
except Exception:
82+
self._conn.execute("ROLLBACK")
83+
raise
84+
85+
def close(self) -> None:
86+
"""Close database connection."""
87+
self._conn.close()
88+
89+
def add_entity(self, entity: Entity) -> None:
90+
"""Add an entity to the store."""
91+
ensure_entity_content_hash(entity)
92+
metadata_json = json.dumps(entity.metadata) if entity.metadata else "{}"
93+
94+
with self._lock:
95+
self._conn.execute(
96+
"""
97+
INSERT OR REPLACE INTO entities (
98+
entity_id, kind, name, qualified_name, file_path,
99+
line_start, line_end, docstring, signature,
100+
source_code, metadata_json, content_hash
101+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
102+
""",
103+
(
104+
entity.id,
105+
entity.kind.value,
106+
entity.name,
107+
entity.qualified_name,
108+
entity.location.file_path,
109+
entity.location.line_start,
110+
entity.location.line_end,
111+
entity.docstring,
112+
entity.signature,
113+
entity.source_code,
114+
metadata_json,
115+
entity.metadata.get("content_hash"),
116+
),
117+
)
118+
119+
def add_relationship(self, rel: Relationship) -> None:
120+
"""Add a relationship to the store."""
121+
metadata_json = json.dumps(rel.metadata) if rel.metadata else "{}"
122+
123+
with self._lock:
124+
self._conn.execute(
125+
"""
126+
INSERT INTO relationships (source_id, target_id, kind, metadata_json)
127+
VALUES (?, ?, ?, ?)
128+
""",
129+
(rel.source_id, rel.target_id, rel.kind.value, metadata_json),
130+
)
131+
132+
def _row_to_entity(self, row: sqlite3.Row) -> Entity:
133+
"""Convert a database row to an Entity."""
134+
metadata = json.loads(row["metadata_json"]) if row["metadata_json"] else {}
135+
if row["content_hash"]:
136+
metadata["content_hash"] = row["content_hash"]
137+
138+
location = Location(
139+
file_path=row["file_path"],
140+
line_start=row["line_start"],
141+
line_end=row["line_end"],
142+
)
143+
return Entity(
144+
id=row["entity_id"],
145+
kind=EntityKind(row["kind"]),
146+
name=row["name"],
147+
qualified_name=row["qualified_name"],
148+
location=location,
149+
docstring=row["docstring"],
150+
signature=row["signature"],
151+
source_code=row["source_code"],
152+
metadata=metadata,
153+
)
154+
155+
def _row_to_relationship(self, row: sqlite3.Row) -> Relationship:
156+
"""Convert a database row to a Relationship."""
157+
metadata = json.loads(row["metadata_json"]) if row["metadata_json"] else {}
158+
return Relationship(
159+
source_id=row["source_id"],
160+
target_id=row["target_id"],
161+
kind=RelationshipKind(row["kind"]),
162+
metadata=metadata,
163+
)
164+
165+
def get_entity(self, entity_id: str) -> Optional[Entity]:
166+
"""Fetch an entity by ID."""
167+
cursor = self._conn.execute(
168+
"SELECT * FROM entities WHERE entity_id = ?", (entity_id,)
169+
)
170+
row = cursor.fetchone()
171+
if row:
172+
return self._row_to_entity(row)
173+
return None
174+
175+
def search(self, pattern: str) -> list[Entity]:
176+
"""Search entities by name or qualified name pattern."""
177+
like_pattern = f"%{pattern}%"
178+
cursor = self._conn.execute(
179+
"""
180+
SELECT * FROM entities
181+
WHERE name LIKE ? OR qualified_name LIKE ?
182+
""",
183+
(like_pattern, like_pattern),
184+
)
185+
return [self._row_to_entity(row) for row in cursor]
186+
187+
def get_callers(self, entity_id: str) -> list[Entity]:
188+
"""Return caller entities."""
189+
cursor = self._conn.execute(
190+
"""
191+
SELECT e.* FROM entities e
192+
JOIN relationships r ON e.entity_id = r.source_id
193+
WHERE r.target_id = ? AND r.kind = ?
194+
""",
195+
(entity_id, RelationshipKind.CALLS.value),
196+
)
197+
return [self._row_to_entity(row) for row in cursor]
198+
199+
def get_callees(self, entity_id: str) -> list[Entity]:
200+
"""Return callee entities."""
201+
cursor = self._conn.execute(
202+
"""
203+
SELECT e.* FROM entities e
204+
JOIN relationships r ON e.entity_id = r.target_id
205+
WHERE r.source_id = ? AND r.kind = ?
206+
""",
207+
(entity_id, RelationshipKind.CALLS.value),
208+
)
209+
return [self._row_to_entity(row) for row in cursor]
210+
211+
def get_dependencies(self, entity_id: str) -> list[Entity]:
212+
"""Return entities this entity depends on."""
213+
cursor = self._conn.execute(
214+
"""
215+
SELECT DISTINCT e.* FROM entities e
216+
JOIN relationships r ON e.entity_id = r.target_id
217+
WHERE r.source_id = ? AND r.kind IN (?, ?)
218+
""",
219+
(entity_id, RelationshipKind.CALLS.value, RelationshipKind.IMPORTS.value),
220+
)
221+
return [self._row_to_entity(row) for row in cursor]
222+
223+
def get_dependents(self, entity_id: str) -> list[Entity]:
224+
"""Return entities depending on this entity."""
225+
cursor = self._conn.execute(
226+
"""
227+
SELECT DISTINCT e.* FROM entities e
228+
JOIN relationships r ON e.entity_id = r.source_id
229+
WHERE r.target_id = ? AND r.kind IN (?, ?)
230+
""",
231+
(entity_id, RelationshipKind.CALLS.value, RelationshipKind.IMPORTS.value),
232+
)
233+
return [self._row_to_entity(row) for row in cursor]
234+
235+
def trace_calls(
236+
self,
237+
entity_id: str,
238+
direction: str = "callees",
239+
depth: int = 1,
240+
max_results: int = 50,
241+
) -> list[dict[str, Any]]:
242+
"""Multi-hop call graph traversal using Recursive CTE."""
243+
if direction not in ("callers", "callees"):
244+
raise ValueError(f"direction must be 'callers' or 'callees', got {direction}")
245+
246+
if direction == "callees":
247+
start_col, next_col = "source_id", "target_id"
248+
else:
249+
start_col, next_col = "target_id", "source_id"
250+
251+
query = f"""
252+
WITH RECURSIVE call_chain(entity_id, depth) AS (
253+
SELECT {next_col}, 1 FROM relationships
254+
WHERE {start_col} = ? AND kind = ?
255+
UNION ALL
256+
SELECT r.{next_col}, cc.depth + 1
257+
FROM relationships r
258+
JOIN call_chain cc ON r.{start_col} = cc.entity_id
259+
WHERE r.kind = ? AND cc.depth < ?
260+
)
261+
SELECT DISTINCT e.entity_id, e.name, e.qualified_name, e.kind, e.file_path, e.line_start, MIN(cc.depth) as call_depth
262+
FROM call_chain cc
263+
JOIN entities e ON e.entity_id = cc.entity_id
264+
GROUP BY e.entity_id
265+
ORDER BY call_depth, e.name
266+
LIMIT ?;
267+
"""
268+
269+
cursor = self._conn.execute(
270+
query,
271+
(entity_id, RelationshipKind.CALLS.value, RelationshipKind.CALLS.value, depth, max_results),
272+
)
273+
274+
results = []
275+
for row in cursor:
276+
results.append({
277+
"entity_id": row["entity_id"],
278+
"name": row["name"],
279+
"qualified_name": row["qualified_name"],
280+
"kind": row["kind"],
281+
"file": row["file_path"],
282+
"line": row["line_start"],
283+
"call_depth": row["call_depth"],
284+
})
285+
return results
286+
287+
def get_impact(self, entity_id: str, max_depth: int = 3) -> dict[str, Any]:
288+
"""Analyze the impact of modifying or deleting an entity."""
289+
entity = self.get_entity(entity_id)
290+
if not entity:
291+
return {
292+
"entity_id": entity_id,
293+
"direct_dependents": [],
294+
"transitive_dependents": [],
295+
"risk_score": 0.0,
296+
"error": "Entity not found",
297+
}
298+
299+
direct = self.trace_calls(entity_id, direction="callers", depth=1, max_results=100)
300+
transitive = self.trace_calls(entity_id, direction="callers", depth=max_depth, max_results=100)
301+
302+
direct_ids = {d["entity_id"] for d in direct}
303+
transitive_only = [t for t in transitive if t["entity_id"] not in direct_ids]
304+
305+
direct_count = len(direct)
306+
transitive_count = len(transitive_only)
307+
308+
breadth_score = min(1.0, (direct_count + transitive_count * 0.5) / 20)
309+
310+
affected_files = {d.get("file") for d in direct + transitive_only}
311+
file_score = min(1.0, len(affected_files) / 5)
312+
313+
type_score = 0.3
314+
if entity.kind == EntityKind.CLASS:
315+
type_score = 0.6
316+
elif entity.kind == EntityKind.MODULE:
317+
type_score = 0.8
318+
319+
risk_score = round((breadth_score + file_score + type_score) / 3, 2)
320+
321+
return {
322+
"entity_id": entity_id,
323+
"entity_name": entity.qualified_name,
324+
"direct_dependents": direct,
325+
"transitive_dependents": transitive_only,
326+
"total_affected": direct_count + transitive_count,
327+
"affected_files": list(affected_files),
328+
"risk_score": min(1.0, risk_score),
329+
}
330+
331+
@classmethod
332+
def from_json(cls, json_path: str | Path, db_path: str | Path) -> "SqliteKnowledgeStore":
333+
"""Migrate from a JSON knowledge store to SQLite."""
334+
store = KnowledgeStore.load(json_path)
335+
sqlite_store = cls(db_path)
336+
337+
sqlite_store._conn.execute("BEGIN")
338+
try:
339+
for entity in store.entities.values():
340+
sqlite_store.add_entity(entity)
341+
for rel in store.relationships:
342+
sqlite_store.add_relationship(rel)
343+
sqlite_store._conn.execute("COMMIT")
344+
except Exception:
345+
sqlite_store._conn.execute("ROLLBACK")
346+
raise
347+
348+
return sqlite_store

0 commit comments

Comments
 (0)