suitable-loop 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.
@@ -0,0 +1,60 @@
1
+ """Configuration management for Suitable Loop."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+
9
+
10
+ @dataclass
11
+ class AnalysisConfig:
12
+ max_file_size_kb: int = 500
13
+ exclude_patterns: list[str] = field(default_factory=lambda: [
14
+ "**/venv/**",
15
+ "**/.venv/**",
16
+ "**/node_modules/**",
17
+ "**/__pycache__/**",
18
+ "**/migrations/**",
19
+ "**/.git/**",
20
+ ])
21
+ complexity_threshold: int = 10
22
+
23
+
24
+ @dataclass
25
+ class GitConfig:
26
+ default_commit_depth: int = 50
27
+ risk_weights: dict[str, float] = field(default_factory=lambda: {
28
+ "complexity": 0.30,
29
+ "blast_radius": 0.25,
30
+ "churn": 0.20,
31
+ "lines": 0.15,
32
+ "files": 0.10,
33
+ })
34
+
35
+
36
+ @dataclass
37
+ class LogConfig:
38
+ auto_detect_format: bool = True
39
+ max_entries_per_ingest: int = 50_000
40
+
41
+
42
+ @dataclass
43
+ class SuitableLoopConfig:
44
+ db_path: Path = field(default_factory=lambda: Path(
45
+ os.environ.get("SUITABLE_LOOP_DB_PATH", "~/.suitable-loop/suitable-loop.db")
46
+ ).expanduser())
47
+ log_level: str = "INFO"
48
+ analysis: AnalysisConfig = field(default_factory=AnalysisConfig)
49
+ git: GitConfig = field(default_factory=GitConfig)
50
+ logging: LogConfig = field(default_factory=LogConfig)
51
+
52
+
53
+ def load_config() -> SuitableLoopConfig:
54
+ """Load configuration from environment variables."""
55
+ config = SuitableLoopConfig()
56
+ if db_path := os.environ.get("SUITABLE_LOOP_DB_PATH"):
57
+ config.db_path = Path(db_path).expanduser()
58
+ if log_level := os.environ.get("SUITABLE_LOOP_LOG_LEVEL"):
59
+ config.log_level = log_level
60
+ return config
suitable_loop/db.py ADDED
@@ -0,0 +1,497 @@
1
+ """SQLite database layer for CodeZero."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sqlite3
7
+ from pathlib import Path
8
+
9
+ from .config import SuitableLoopConfig
10
+ from .models import (
11
+ CallEdge,
12
+ ClassEntity,
13
+ CommitFile,
14
+ CommitInfo,
15
+ ErrorCodeLink,
16
+ ErrorGroup,
17
+ FileDependency,
18
+ FileEntity,
19
+ FunctionEntity,
20
+ ImportEntity,
21
+ LogEntry,
22
+ )
23
+
24
+ SCHEMA_SQL = """
25
+ CREATE TABLE IF NOT EXISTS files (
26
+ id INTEGER PRIMARY KEY,
27
+ path TEXT UNIQUE NOT NULL,
28
+ project_root TEXT NOT NULL,
29
+ size_bytes INTEGER,
30
+ last_modified REAL,
31
+ last_indexed REAL,
32
+ line_count INTEGER,
33
+ hash TEXT
34
+ );
35
+
36
+ CREATE TABLE IF NOT EXISTS functions (
37
+ id INTEGER PRIMARY KEY,
38
+ file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
39
+ name TEXT NOT NULL,
40
+ qualified_name TEXT NOT NULL,
41
+ class_name TEXT,
42
+ line_start INTEGER,
43
+ line_end INTEGER,
44
+ signature TEXT,
45
+ docstring TEXT,
46
+ complexity INTEGER,
47
+ is_method BOOLEAN DEFAULT FALSE,
48
+ is_async BOOLEAN DEFAULT FALSE
49
+ );
50
+
51
+ CREATE TABLE IF NOT EXISTS classes (
52
+ id INTEGER PRIMARY KEY,
53
+ file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
54
+ name TEXT NOT NULL,
55
+ qualified_name TEXT NOT NULL,
56
+ line_start INTEGER,
57
+ line_end INTEGER,
58
+ bases TEXT,
59
+ docstring TEXT
60
+ );
61
+
62
+ CREATE TABLE IF NOT EXISTS imports (
63
+ id INTEGER PRIMARY KEY,
64
+ file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
65
+ module TEXT NOT NULL,
66
+ alias TEXT,
67
+ is_internal BOOLEAN,
68
+ resolved_file_id INTEGER REFERENCES files(id)
69
+ );
70
+
71
+ CREATE TABLE IF NOT EXISTS call_edges (
72
+ id INTEGER PRIMARY KEY,
73
+ caller_id INTEGER REFERENCES functions(id) ON DELETE CASCADE,
74
+ callee_id INTEGER REFERENCES functions(id) ON DELETE CASCADE,
75
+ file_id INTEGER REFERENCES files(id),
76
+ line_number INTEGER,
77
+ UNIQUE(caller_id, callee_id, line_number)
78
+ );
79
+
80
+ CREATE TABLE IF NOT EXISTS file_dependencies (
81
+ id INTEGER PRIMARY KEY,
82
+ source_file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
83
+ target_file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
84
+ UNIQUE(source_file_id, target_file_id)
85
+ );
86
+
87
+ CREATE TABLE IF NOT EXISTS commits (
88
+ id INTEGER PRIMARY KEY,
89
+ repo_path TEXT NOT NULL,
90
+ sha TEXT NOT NULL,
91
+ author TEXT,
92
+ timestamp REAL,
93
+ message TEXT,
94
+ files_changed INTEGER,
95
+ insertions INTEGER,
96
+ deletions INTEGER,
97
+ risk_score REAL,
98
+ UNIQUE(repo_path, sha)
99
+ );
100
+
101
+ CREATE TABLE IF NOT EXISTS commit_files (
102
+ id INTEGER PRIMARY KEY,
103
+ commit_id INTEGER REFERENCES commits(id) ON DELETE CASCADE,
104
+ file_path TEXT NOT NULL,
105
+ change_type TEXT,
106
+ insertions INTEGER,
107
+ deletions INTEGER,
108
+ complexity_before INTEGER,
109
+ complexity_after INTEGER
110
+ );
111
+
112
+ CREATE TABLE IF NOT EXISTS log_entries (
113
+ id INTEGER PRIMARY KEY,
114
+ source_file TEXT NOT NULL,
115
+ timestamp REAL,
116
+ level TEXT,
117
+ logger_name TEXT,
118
+ message TEXT,
119
+ raw_line TEXT,
120
+ error_group_id INTEGER REFERENCES error_groups(id)
121
+ );
122
+
123
+ CREATE TABLE IF NOT EXISTS error_groups (
124
+ id INTEGER PRIMARY KEY,
125
+ signature TEXT UNIQUE NOT NULL,
126
+ exception_type TEXT,
127
+ exception_message TEXT,
128
+ traceback TEXT,
129
+ first_seen REAL,
130
+ last_seen REAL,
131
+ occurrence_count INTEGER DEFAULT 1
132
+ );
133
+
134
+ CREATE TABLE IF NOT EXISTS error_code_links (
135
+ id INTEGER PRIMARY KEY,
136
+ error_group_id INTEGER REFERENCES error_groups(id) ON DELETE CASCADE,
137
+ function_id INTEGER REFERENCES functions(id),
138
+ file_id INTEGER REFERENCES files(id),
139
+ line_number INTEGER,
140
+ frame_position INTEGER
141
+ );
142
+
143
+ CREATE INDEX IF NOT EXISTS idx_functions_file ON functions(file_id);
144
+ CREATE INDEX IF NOT EXISTS idx_functions_qname ON functions(qualified_name);
145
+ CREATE INDEX IF NOT EXISTS idx_classes_file ON classes(file_id);
146
+ CREATE INDEX IF NOT EXISTS idx_imports_file ON imports(file_id);
147
+ CREATE INDEX IF NOT EXISTS idx_call_edges_caller ON call_edges(caller_id);
148
+ CREATE INDEX IF NOT EXISTS idx_call_edges_callee ON call_edges(callee_id);
149
+ CREATE INDEX IF NOT EXISTS idx_commits_repo ON commits(repo_path);
150
+ CREATE INDEX IF NOT EXISTS idx_log_entries_group ON log_entries(error_group_id);
151
+ CREATE INDEX IF NOT EXISTS idx_error_code_links_group ON error_code_links(error_group_id);
152
+ """
153
+
154
+
155
+ class Database:
156
+ def __init__(self, config: SuitableLoopConfig):
157
+ self.db_path = config.db_path
158
+ self._conn: sqlite3.Connection | None = None
159
+
160
+ def connect(self) -> sqlite3.Connection:
161
+ if self._conn is not None:
162
+ return self._conn
163
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
164
+ self._conn = sqlite3.connect(str(self.db_path))
165
+ self._conn.execute("PRAGMA journal_mode=WAL")
166
+ self._conn.execute("PRAGMA foreign_keys=ON")
167
+ self._conn.row_factory = sqlite3.Row
168
+ self._init_schema()
169
+ return self._conn
170
+
171
+ def _init_schema(self):
172
+ self._conn.executescript(SCHEMA_SQL)
173
+ self._conn.commit()
174
+
175
+ def close(self):
176
+ if self._conn:
177
+ self._conn.close()
178
+ self._conn = None
179
+
180
+ @property
181
+ def conn(self) -> sqlite3.Connection:
182
+ return self.connect()
183
+
184
+ # --- File operations ---
185
+
186
+ def upsert_file(self, f: FileEntity) -> int:
187
+ cur = self.conn.execute(
188
+ """INSERT INTO files (path, project_root, size_bytes, last_modified, last_indexed, line_count, hash)
189
+ VALUES (?, ?, ?, ?, ?, ?, ?)
190
+ ON CONFLICT(path) DO UPDATE SET
191
+ size_bytes=excluded.size_bytes, last_modified=excluded.last_modified,
192
+ last_indexed=excluded.last_indexed, line_count=excluded.line_count, hash=excluded.hash
193
+ RETURNING id""",
194
+ (f.path, f.project_root, f.size_bytes, f.last_modified, f.last_indexed, f.line_count, f.hash),
195
+ )
196
+ row = cur.fetchone()
197
+ self.conn.commit()
198
+ return row[0]
199
+
200
+ def get_file_by_path(self, path: str) -> FileEntity | None:
201
+ row = self.conn.execute("SELECT * FROM files WHERE path = ?", (path,)).fetchone()
202
+ if not row:
203
+ return None
204
+ return FileEntity(**dict(row))
205
+
206
+ def get_file_by_id(self, file_id: int) -> FileEntity | None:
207
+ row = self.conn.execute("SELECT * FROM files WHERE id = ?", (file_id,)).fetchone()
208
+ if not row:
209
+ return None
210
+ return FileEntity(**dict(row))
211
+
212
+ def find_file_by_suffix(self, suffix: str) -> FileEntity | None:
213
+ """Find a file whose path ends with *suffix* (e.g. ``db.py``)."""
214
+ row = self.conn.execute(
215
+ "SELECT * FROM files WHERE path LIKE ? LIMIT 1", (f"%{suffix}",)
216
+ ).fetchone()
217
+ if not row:
218
+ return None
219
+ return FileEntity(**dict(row))
220
+
221
+ def get_all_files(self, project_root: str) -> list[FileEntity]:
222
+ rows = self.conn.execute("SELECT * FROM files WHERE project_root = ?", (project_root,)).fetchall()
223
+ return [FileEntity(**dict(r)) for r in rows]
224
+
225
+ def delete_file(self, file_id: int):
226
+ self.conn.execute("DELETE FROM files WHERE id = ?", (file_id,))
227
+ self.conn.commit()
228
+
229
+ # --- Function operations ---
230
+
231
+ def insert_function(self, f: FunctionEntity) -> int:
232
+ cur = self.conn.execute(
233
+ """INSERT INTO functions (file_id, name, qualified_name, class_name, line_start, line_end,
234
+ signature, docstring, complexity, is_method, is_async)
235
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id""",
236
+ (f.file_id, f.name, f.qualified_name, f.class_name, f.line_start, f.line_end,
237
+ f.signature, f.docstring, f.complexity, f.is_method, f.is_async),
238
+ )
239
+ row = cur.fetchone()
240
+ return row[0]
241
+
242
+ def get_functions_by_file(self, file_id: int) -> list[FunctionEntity]:
243
+ rows = self.conn.execute("SELECT * FROM functions WHERE file_id = ?", (file_id,)).fetchall()
244
+ return [FunctionEntity(**dict(r)) for r in rows]
245
+
246
+ def get_function_by_qualified_name(self, qname: str) -> FunctionEntity | None:
247
+ row = self.conn.execute("SELECT * FROM functions WHERE qualified_name = ?", (qname,)).fetchone()
248
+ if not row:
249
+ return None
250
+ return FunctionEntity(**dict(row))
251
+
252
+ def get_function_by_id(self, func_id: int) -> FunctionEntity | None:
253
+ row = self.conn.execute("SELECT * FROM functions WHERE id = ?", (func_id,)).fetchone()
254
+ if not row:
255
+ return None
256
+ return FunctionEntity(**dict(row))
257
+
258
+ def search_functions(self, query: str, limit: int = 20) -> list[FunctionEntity]:
259
+ rows = self.conn.execute(
260
+ "SELECT * FROM functions WHERE qualified_name LIKE ? OR name LIKE ? LIMIT ?",
261
+ (f"%{query}%", f"%{query}%", limit),
262
+ ).fetchall()
263
+ return [FunctionEntity(**dict(r)) for r in rows]
264
+
265
+ def delete_functions_by_file(self, file_id: int):
266
+ self.conn.execute("DELETE FROM functions WHERE file_id = ?", (file_id,))
267
+
268
+ # --- Class operations ---
269
+
270
+ def insert_class(self, c: ClassEntity) -> int:
271
+ cur = self.conn.execute(
272
+ """INSERT INTO classes (file_id, name, qualified_name, line_start, line_end, bases, docstring)
273
+ VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id""",
274
+ (c.file_id, c.name, c.qualified_name, c.line_start, c.line_end, json.dumps(c.bases), c.docstring),
275
+ )
276
+ row = cur.fetchone()
277
+ return row[0]
278
+
279
+ def get_classes_by_file(self, file_id: int) -> list[ClassEntity]:
280
+ rows = self.conn.execute("SELECT * FROM classes WHERE file_id = ?", (file_id,)).fetchall()
281
+ result = []
282
+ for r in rows:
283
+ d = dict(r)
284
+ d["bases"] = json.loads(d["bases"]) if d["bases"] else []
285
+ result.append(ClassEntity(**d))
286
+ return result
287
+
288
+ def delete_classes_by_file(self, file_id: int):
289
+ self.conn.execute("DELETE FROM classes WHERE file_id = ?", (file_id,))
290
+
291
+ # --- Import operations ---
292
+
293
+ def insert_import(self, imp: ImportEntity) -> int:
294
+ cur = self.conn.execute(
295
+ """INSERT INTO imports (file_id, module, alias, is_internal, resolved_file_id)
296
+ VALUES (?, ?, ?, ?, ?) RETURNING id""",
297
+ (imp.file_id, imp.module, imp.alias, imp.is_internal, imp.resolved_file_id),
298
+ )
299
+ row = cur.fetchone()
300
+ return row[0]
301
+
302
+ def get_imports_by_file(self, file_id: int) -> list[ImportEntity]:
303
+ rows = self.conn.execute("SELECT * FROM imports WHERE file_id = ?", (file_id,)).fetchall()
304
+ return [ImportEntity(**dict(r)) for r in rows]
305
+
306
+ def delete_imports_by_file(self, file_id: int):
307
+ self.conn.execute("DELETE FROM imports WHERE file_id = ?", (file_id,))
308
+
309
+ # --- Call edge operations ---
310
+
311
+ def insert_call_edge(self, edge: CallEdge) -> int | None:
312
+ try:
313
+ cur = self.conn.execute(
314
+ """INSERT INTO call_edges (caller_id, callee_id, file_id, line_number)
315
+ VALUES (?, ?, ?, ?) RETURNING id""",
316
+ (edge.caller_id, edge.callee_id, edge.file_id, edge.line_number),
317
+ )
318
+ row = cur.fetchone()
319
+ return row[0]
320
+ except sqlite3.IntegrityError:
321
+ return None
322
+
323
+ def get_callers(self, function_id: int) -> list[FunctionEntity]:
324
+ rows = self.conn.execute(
325
+ """SELECT f.* FROM functions f
326
+ JOIN call_edges ce ON f.id = ce.caller_id
327
+ WHERE ce.callee_id = ?""",
328
+ (function_id,),
329
+ ).fetchall()
330
+ return [FunctionEntity(**dict(r)) for r in rows]
331
+
332
+ def get_callees(self, function_id: int) -> list[FunctionEntity]:
333
+ rows = self.conn.execute(
334
+ """SELECT f.* FROM functions f
335
+ JOIN call_edges ce ON f.id = ce.callee_id
336
+ WHERE ce.caller_id = ?""",
337
+ (function_id,),
338
+ ).fetchall()
339
+ return [FunctionEntity(**dict(r)) for r in rows]
340
+
341
+ def delete_call_edges_by_file(self, file_id: int):
342
+ self.conn.execute("DELETE FROM call_edges WHERE file_id = ?", (file_id,))
343
+
344
+ # --- File dependency operations ---
345
+
346
+ def insert_file_dependency(self, dep: FileDependency) -> int | None:
347
+ try:
348
+ cur = self.conn.execute(
349
+ """INSERT INTO file_dependencies (source_file_id, target_file_id)
350
+ VALUES (?, ?) RETURNING id""",
351
+ (dep.source_file_id, dep.target_file_id),
352
+ )
353
+ row = cur.fetchone()
354
+ return row[0]
355
+ except sqlite3.IntegrityError:
356
+ return None
357
+
358
+ def get_file_dependents(self, file_id: int) -> list[FileEntity]:
359
+ rows = self.conn.execute(
360
+ """SELECT f.* FROM files f
361
+ JOIN file_dependencies fd ON f.id = fd.source_file_id
362
+ WHERE fd.target_file_id = ?""",
363
+ (file_id,),
364
+ ).fetchall()
365
+ return [FileEntity(**dict(r)) for r in rows]
366
+
367
+ def get_file_dependencies(self, file_id: int) -> list[FileEntity]:
368
+ rows = self.conn.execute(
369
+ """SELECT f.* FROM files f
370
+ JOIN file_dependencies fd ON f.id = fd.target_file_id
371
+ WHERE fd.source_file_id = ?""",
372
+ (file_id,),
373
+ ).fetchall()
374
+ return [FileEntity(**dict(r)) for r in rows]
375
+
376
+ def delete_file_dependencies_by_source(self, file_id: int):
377
+ self.conn.execute("DELETE FROM file_dependencies WHERE source_file_id = ?", (file_id,))
378
+
379
+ # --- Commit operations ---
380
+
381
+ def upsert_commit(self, c: CommitInfo) -> int:
382
+ cur = self.conn.execute(
383
+ """INSERT INTO commits (repo_path, sha, author, timestamp, message, files_changed,
384
+ insertions, deletions, risk_score)
385
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
386
+ ON CONFLICT(repo_path, sha) DO UPDATE SET risk_score=excluded.risk_score
387
+ RETURNING id""",
388
+ (c.repo_path, c.sha, c.author, c.timestamp, c.message, c.files_changed,
389
+ c.insertions, c.deletions, c.risk_score),
390
+ )
391
+ row = cur.fetchone()
392
+ self.conn.commit()
393
+ return row[0]
394
+
395
+ def insert_commit_file(self, cf: CommitFile) -> int:
396
+ cur = self.conn.execute(
397
+ """INSERT INTO commit_files (commit_id, file_path, change_type, insertions, deletions,
398
+ complexity_before, complexity_after)
399
+ VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id""",
400
+ (cf.commit_id, cf.file_path, cf.change_type, cf.insertions, cf.deletions,
401
+ cf.complexity_before, cf.complexity_after),
402
+ )
403
+ row = cur.fetchone()
404
+ return row[0]
405
+
406
+ def get_commits(self, repo_path: str, limit: int = 50) -> list[CommitInfo]:
407
+ rows = self.conn.execute(
408
+ "SELECT * FROM commits WHERE repo_path = ? ORDER BY timestamp DESC LIMIT ?",
409
+ (repo_path, limit),
410
+ ).fetchall()
411
+ return [CommitInfo(**dict(r)) for r in rows]
412
+
413
+ # --- Error group operations ---
414
+
415
+ def upsert_error_group(self, eg: ErrorGroup) -> int:
416
+ cur = self.conn.execute(
417
+ """INSERT INTO error_groups (signature, exception_type, exception_message, traceback,
418
+ first_seen, last_seen, occurrence_count)
419
+ VALUES (?, ?, ?, ?, ?, ?, ?)
420
+ ON CONFLICT(signature) DO UPDATE SET
421
+ last_seen=excluded.last_seen,
422
+ occurrence_count=error_groups.occurrence_count + excluded.occurrence_count
423
+ RETURNING id""",
424
+ (eg.signature, eg.exception_type, eg.exception_message, eg.traceback,
425
+ eg.first_seen, eg.last_seen, eg.occurrence_count),
426
+ )
427
+ row = cur.fetchone()
428
+ self.conn.commit()
429
+ return row[0]
430
+
431
+ def get_error_groups(self, limit: int = 20) -> list[ErrorGroup]:
432
+ rows = self.conn.execute(
433
+ "SELECT * FROM error_groups ORDER BY occurrence_count DESC LIMIT ?", (limit,)
434
+ ).fetchall()
435
+ return [ErrorGroup(**dict(r)) for r in rows]
436
+
437
+ def get_error_group_by_id(self, group_id: int) -> ErrorGroup | None:
438
+ row = self.conn.execute("SELECT * FROM error_groups WHERE id = ?", (group_id,)).fetchone()
439
+ if not row:
440
+ return None
441
+ return ErrorGroup(**dict(row))
442
+
443
+ # --- Log entry operations ---
444
+
445
+ def insert_log_entry(self, le: LogEntry) -> int:
446
+ cur = self.conn.execute(
447
+ """INSERT INTO log_entries (source_file, timestamp, level, logger_name, message, raw_line, error_group_id)
448
+ VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id""",
449
+ (le.source_file, le.timestamp, le.level, le.logger_name, le.message, le.raw_line, le.error_group_id),
450
+ )
451
+ row = cur.fetchone()
452
+ return row[0]
453
+
454
+ # --- Error code link operations ---
455
+
456
+ def insert_error_code_link(self, link: ErrorCodeLink) -> int:
457
+ cur = self.conn.execute(
458
+ """INSERT INTO error_code_links (error_group_id, function_id, file_id, line_number, frame_position)
459
+ VALUES (?, ?, ?, ?, ?) RETURNING id""",
460
+ (link.error_group_id, link.function_id, link.file_id, link.line_number, link.frame_position),
461
+ )
462
+ row = cur.fetchone()
463
+ return row[0]
464
+
465
+ def get_error_code_links(self, error_group_id: int) -> list[dict]:
466
+ rows = self.conn.execute(
467
+ """SELECT ecl.*, f.qualified_name as function_name, fi.path as file_path
468
+ FROM error_code_links ecl
469
+ LEFT JOIN functions f ON ecl.function_id = f.id
470
+ LEFT JOIN files fi ON ecl.file_id = fi.id
471
+ WHERE ecl.error_group_id = ?
472
+ ORDER BY ecl.frame_position""",
473
+ (error_group_id,),
474
+ ).fetchall()
475
+ return [dict(r) for r in rows]
476
+
477
+ # --- Utility ---
478
+
479
+ def get_stats(self) -> dict:
480
+ stats = {}
481
+ for table in ["files", "functions", "classes", "imports", "call_edges",
482
+ "file_dependencies", "commits", "log_entries", "error_groups"]:
483
+ row = self.conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone() # noqa: S608
484
+ stats[table] = row[0]
485
+ stats["db_size_bytes"] = self.db_path.stat().st_size if self.db_path.exists() else 0
486
+ return stats
487
+
488
+ def clear_file_data(self, file_id: int):
489
+ """Remove all data associated with a file for re-indexing."""
490
+ self.delete_call_edges_by_file(file_id)
491
+ self.delete_functions_by_file(file_id)
492
+ self.delete_classes_by_file(file_id)
493
+ self.delete_imports_by_file(file_id)
494
+ self.delete_file_dependencies_by_source(file_id)
495
+
496
+ def commit(self):
497
+ self.conn.commit()
@@ -0,0 +1 @@
1
+ """Suitable Loop graph engine."""