PMFlow/backend/app/services/signal/symbol_index/mixin_query_overview.py

177 lines
6.3 KiB
Python

from __future__ import annotations
from .shared import *
class SymbolIndexQueryOverviewMixin:
async def search_symbols(
self,
*,
query: str,
language: Optional[str] = None,
symbol_type: Optional[str] = None,
limit: int = 30,
) -> dict[str, Any]:
text = str(query or "").strip()
if not text:
return {"count": 0, "items": []}
like = f"%{text}%"
stmt = select(SymbolDefinition).where(
and_(
SymbolDefinition.tenant_id == self.tenant_id,
SymbolDefinition.status == "active",
or_(
SymbolDefinition.symbol_name.ilike(like),
SymbolDefinition.qualified_name.ilike(like),
SymbolDefinition.file_path.ilike(like),
),
)
)
if language:
stmt = stmt.where(SymbolDefinition.language == str(language).strip().lower()[:20])
if symbol_type:
stmt = stmt.where(SymbolDefinition.symbol_type == str(symbol_type).strip().lower()[:32])
rows = (
await self.db.execute(
stmt.order_by(SymbolDefinition.updated_at.desc(), SymbolDefinition.id.desc()).limit(max(1, min(int(limit), 200)))
)
).scalars().all()
return {
"count": len(rows),
"items": [self._serialize_symbol(row) for row in rows],
}
async def get_symbol_detail(self, *, symbol_id: int) -> dict[str, Any]:
symbol = (
await self.db.execute(
select(SymbolDefinition).where(
and_(
SymbolDefinition.tenant_id == self.tenant_id,
SymbolDefinition.id == int(symbol_id),
)
)
)
).scalar_one_or_none()
if symbol is None:
raise ValueError("symbol_not_found")
refs = (
await self.db.execute(
select(SymbolReference).where(
and_(
SymbolReference.tenant_id == self.tenant_id,
SymbolReference.source_symbol_id == int(symbol.id),
)
)
.order_by(SymbolReference.id.desc())
.limit(120)
)
).scalars().all()
inbound_refs = (
await self.db.execute(
select(SymbolReference)
.where(
and_(
SymbolReference.tenant_id == self.tenant_id,
or_(
SymbolReference.target_symbol_name == str(symbol.symbol_name),
SymbolReference.target_qualified_name == str(symbol.qualified_name or ""),
),
)
)
.order_by(SymbolReference.id.desc())
.limit(120)
)
).scalars().all()
return {
"symbol": self._serialize_symbol(symbol),
"outbound_refs": [self._serialize_ref(row) for row in refs],
"inbound_refs": [self._serialize_ref(row) for row in inbound_refs],
}
async def get_overview(
self,
*,
top_n_files: int = 10,
auto_sync_if_empty: bool = False,
base_dir: Optional[str] = None,
) -> dict[str, Any]:
total = int(
(
await self.db.execute(
select(func.count(SymbolDefinition.id)).where(SymbolDefinition.tenant_id == self.tenant_id)
)
).scalar_one()
or 0
)
if total == 0 and auto_sync_if_empty:
await self.sync_codebase_symbols(base_dir=base_dir, max_files=300, force_reindex=False)
total = int(
(
await self.db.execute(
select(func.count(SymbolDefinition.id)).where(SymbolDefinition.tenant_id == self.tenant_id)
)
).scalar_one()
or 0
)
language_groups = (
await self.db.execute(
select(SymbolDefinition.language, func.count(SymbolDefinition.id))
.where(SymbolDefinition.tenant_id == self.tenant_id)
.group_by(SymbolDefinition.language)
)
).all()
type_groups = (
await self.db.execute(
select(SymbolDefinition.symbol_type, func.count(SymbolDefinition.id))
.where(SymbolDefinition.tenant_id == self.tenant_id)
.group_by(SymbolDefinition.symbol_type)
)
).all()
file_groups = (
await self.db.execute(
select(SymbolDefinition.file_path, func.count(SymbolDefinition.id))
.where(SymbolDefinition.tenant_id == self.tenant_id)
.group_by(SymbolDefinition.file_path)
.order_by(func.count(SymbolDefinition.id).desc())
.limit(max(1, min(int(top_n_files), 50)))
)
).all()
ref_total = int(
(
await self.db.execute(
select(func.count(SymbolReference.id)).where(SymbolReference.tenant_id == self.tenant_id)
)
).scalar_one()
or 0
)
indexed_files_total = int(
(
await self.db.execute(
select(func.count(func.distinct(SymbolDefinition.file_path))).where(
SymbolDefinition.tenant_id == self.tenant_id
)
)
).scalar_one()
or 0
)
return {
"tenant_id": self.tenant_id,
"indexed_files": indexed_files_total,
"symbol_total": total,
"reference_total": ref_total,
"python_symbols": int(dict(language_groups).get("python", 0) or 0),
"typescript_symbols": int(dict(language_groups).get("typescript", 0) or 0),
"symbols_by_language": {str(k): int(v or 0) for k, v in language_groups},
"symbols_by_type": {str(k): int(v or 0) for k, v in type_groups},
"top_modules": [
{"module": str(path), "symbols": int(cnt or 0)}
for path, cnt in file_groups
],
"snapshot_at": now_cn_aware().isoformat(),
}