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(), }