UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

362 lines (361 loc) 14.1 kB
/** * CodeEntityRepository — AST 代码实体的仓储实现 * * 从 CodeEntityGraph 和 PanoramaScanner 提取的数据操作, * 使用 Drizzle 类型安全 API。 */ import { and, count, eq, inArray, isNotNull, like, ne, sql } from 'drizzle-orm'; import { codeEntities } from '../../infrastructure/database/drizzle/schema.js'; import { unixNow } from '../../shared/utils/common.js'; import { RepositoryBase } from '../base/RepositoryBase.js'; /* ═══ Repository 实现 ═══ */ export class CodeEntityRepositoryImpl extends RepositoryBase { constructor(drizzle) { super(drizzle, codeEntities); } /* ─── CRUD ─── */ async findById(id) { const rows = this.drizzle.select().from(this.table).where(eq(this.table.id, id)).limit(1).all(); return rows.length > 0 ? this.#mapRow(rows[0]) : null; } async create(data) { return this.upsert(data); } async delete(id) { const result = this.drizzle.delete(this.table).where(eq(this.table.id, id)).run(); return result.changes > 0; } /* ─── 核心操作 ─── */ /** UPSERT — 按 (entityId, entityType, projectRoot) 唯一约束 */ async upsert(entity) { const now = unixNow(); const protocolsJson = JSON.stringify(entity.protocols ?? []); const metaJson = JSON.stringify(entity.metadata ?? {}); this.drizzle .insert(this.table) .values({ entityId: entity.entityId, entityType: entity.entityType, projectRoot: entity.projectRoot, name: entity.name, filePath: entity.filePath ?? null, lineNumber: entity.lineNumber ?? null, superclass: entity.superclass ?? null, protocols: protocolsJson, metadataJson: metaJson, createdAt: now, updatedAt: now, }) .onConflictDoUpdate({ target: [this.table.entityId, this.table.entityType, this.table.projectRoot], set: { name: entity.name, filePath: sql `${entity.filePath ?? null}`, lineNumber: sql `${entity.lineNumber ?? null}`, superclass: sql `${entity.superclass ?? null}`, protocols: protocolsJson, metadataJson: metaJson, updatedAt: now, }, }) .run(); // 返回 upserted 行 const rows = this.drizzle .select() .from(this.table) .where(and(eq(this.table.entityId, entity.entityId), eq(this.table.entityType, entity.entityType), eq(this.table.projectRoot, entity.projectRoot))) .limit(1) .all(); return this.#mapRow(rows[0]); } /** 批量 UPSERT */ async batchUpsert(entities) { if (entities.length === 0) { return 0; } let upserted = 0; this.transaction((tx) => { const now = unixNow(); for (const entity of entities) { tx.insert(this.table) .values({ entityId: entity.entityId, entityType: entity.entityType, projectRoot: entity.projectRoot, name: entity.name, filePath: entity.filePath ?? null, lineNumber: entity.lineNumber ?? null, superclass: entity.superclass ?? null, protocols: JSON.stringify(entity.protocols ?? []), metadataJson: JSON.stringify(entity.metadata ?? {}), createdAt: now, updatedAt: now, }) .onConflictDoUpdate({ target: [this.table.entityId, this.table.entityType, this.table.projectRoot], set: { name: entity.name, filePath: sql `${entity.filePath ?? null}`, lineNumber: sql `${entity.lineNumber ?? null}`, superclass: sql `${entity.superclass ?? null}`, protocols: JSON.stringify(entity.protocols ?? []), metadataJson: JSON.stringify(entity.metadata ?? {}), updatedAt: now, }, }) .run(); upserted++; } }); return upserted; } /* ─── 查询 ─── */ /** 按文件路径查询 */ async findByFile(filePath, projectRoot) { const rows = this.drizzle .select() .from(this.table) .where(and(eq(this.table.filePath, filePath), eq(this.table.projectRoot, projectRoot))) .all(); return rows.map((r) => this.#mapRow(r)); } /** 按实体类型列表 */ async listByType(entityType, projectRoot, limit = 100) { const rows = this.drizzle .select() .from(this.table) .where(and(eq(this.table.entityType, entityType), eq(this.table.projectRoot, projectRoot))) .orderBy(this.table.name) .limit(limit) .all(); return rows.map((r) => this.#mapRow(r)); } /** 按名称搜索 */ async searchByName(query, projectRoot, options = {}) { const { entityType, limit = 50 } = options; const conditions = [ eq(this.table.projectRoot, projectRoot), like(this.table.name, `%${query}%`), ]; if (entityType) { conditions.push(eq(this.table.entityType, entityType)); } const rows = this.drizzle .select() .from(this.table) .where(and(...conditions)) .orderBy(this.table.name) .limit(limit) .all(); return rows.map((r) => this.#mapRow(r)); } /** 按 entityId + entityType + projectRoot 精确查找 */ async findByEntityId(entityId, entityType, projectRoot) { const rows = this.drizzle .select() .from(this.table) .where(and(eq(this.table.entityId, entityId), eq(this.table.entityType, entityType), eq(this.table.projectRoot, projectRoot))) .limit(1) .all(); return rows.length > 0 ? this.#mapRow(rows[0]) : null; } /** 删除指定项目的所有实体 */ async clearProject(projectRoot) { const result = this.drizzle .delete(this.table) .where(eq(this.table.projectRoot, projectRoot)) .run(); return result.changes; } /** 删除指定文件的实体(用于增量更新调用图) */ async deleteByFile(filePath, projectRoot) { const result = this.drizzle .delete(this.table) .where(and(eq(this.table.filePath, filePath), eq(this.table.projectRoot, projectRoot))) .run(); return result.changes; } /** 获取实体总数 */ async getEntityCount(projectRoot) { const condition = projectRoot ? eq(this.table.projectRoot, projectRoot) : undefined; const [row] = this.drizzle.select({ cnt: count() }).from(this.table).where(condition).all(); return row?.cnt ?? 0; } /** 按类型统计实体数 */ async countByType(projectRoot) { const rows = this.drizzle .select({ entityType: this.table.entityType, cnt: count(), }) .from(this.table) .where(eq(this.table.projectRoot, projectRoot)) .groupBy(this.table.entityType) .all(); const result = {}; for (const row of rows) { result[row.entityType] = row.cnt; } return result; } /** 按文件路径和实体类型删除 */ async deleteByFileAndType(filePath, entityType, projectRoot) { const result = this.drizzle .delete(this.table) .where(and(eq(this.table.filePath, filePath), eq(this.table.entityType, entityType), eq(this.table.projectRoot, projectRoot))) .run(); return result.changes; } /** 按 entityId + projectRoot 查找(不限 entityType) */ async findByEntityIdOnly(entityId, projectRoot) { const rows = this.drizzle .select() .from(this.table) .where(and(eq(this.table.entityId, entityId), eq(this.table.projectRoot, projectRoot))) .limit(1) .all(); return rows.length > 0 ? this.#mapRow(rows[0]) : null; } /* ─── Panorama 域查询 (Phase 5e) ─── */ /** 查询非 module 实体的 (entityId, filePath) 去重列表 */ async findDistinctEntityIdsWithFilePath(projectRoot) { const rows = this.drizzle .selectDistinct({ entityId: this.table.entityId, filePath: this.table.filePath, }) .from(this.table) .where(and(eq(this.table.projectRoot, projectRoot), isNotNull(this.table.filePath), ne(this.table.entityType, 'module'))) .all(); return rows.filter((r) => r.filePath !== null); } /** 查询本地模块 (排除 external/host nodeType) */ async findLocalModules(projectRoot) { const rows = this.drizzle .selectDistinct({ entityId: this.table.entityId, name: this.table.name, }) .from(this.table) .where(and(eq(this.table.entityType, 'module'), eq(this.table.projectRoot, projectRoot), sql `COALESCE(json_extract(${this.table.metadataJson}, '$.nodeType'), 'local') NOT IN ('external', 'host')`)) .all(); return rows; } /** 查询指定 nodeType 的模块实体 */ async findModulesByNodeTypes(projectRoot, nodeTypes) { if (nodeTypes.length === 0) { return []; } const placeholders = nodeTypes.map((t) => `'${t.replace(/'/g, "''")}'`).join(', '); const rows = this.drizzle .select() .from(this.table) .where(and(eq(this.table.entityType, 'module'), eq(this.table.projectRoot, projectRoot), sql `json_extract(${this.table.metadataJson}, '$.nodeType') IN (${sql.raw(placeholders)})`)) .all(); return rows.map((r) => this.#mapRow(r)); } /** 统计指定 nodeType 的模块数量 */ async countModulesByNodeType(projectRoot, nodeType) { const [row] = this.drizzle .select({ cnt: count() }) .from(this.table) .where(and(eq(this.table.entityType, 'module'), eq(this.table.projectRoot, projectRoot), sql `json_extract(${this.table.metadataJson}, '$.nodeType') = ${nodeType}`)) .all(); return row?.cnt ?? 0; } /** 按 projectRoot + filePaths 批量查询实体 */ async findByProjectAndFilePaths(projectRoot, filePaths) { if (filePaths.length === 0) { return []; } const rows = this.drizzle .select() .from(this.table) .where(and(eq(this.table.projectRoot, projectRoot), inArray(this.table.filePath, filePaths))) .all(); return rows.map((r) => this.#mapRow(r)); } /** 查询非 module 实体的去重文件路径列表 */ async findDistinctFilePaths(projectRoot, limit = 2000) { const rows = this.drizzle .selectDistinct({ filePath: this.table.filePath }) .from(this.table) .where(and(eq(this.table.projectRoot, projectRoot), isNotNull(this.table.filePath), ne(this.table.entityType, 'module'))) .limit(limit) .all(); return rows.filter((r) => r.filePath !== null).map((r) => r.filePath); } /** 批量 INSERT OR IGNORE (不更新已存在的行) */ async batchInsertIgnore(entities) { if (entities.length === 0) { return 0; } let inserted = 0; const now = unixNow(); this.transaction((tx) => { for (const entity of entities) { tx.insert(this.table) .values({ entityId: entity.entityId, entityType: entity.entityType, projectRoot: entity.projectRoot, name: entity.name, filePath: entity.filePath ?? null, lineNumber: entity.lineNumber ?? null, superclass: entity.superclass ?? null, protocols: JSON.stringify(entity.protocols ?? []), metadataJson: JSON.stringify(entity.metadata ?? {}), createdAt: now, updatedAt: now, }) .onConflictDoNothing() .run(); inserted++; } }); return inserted; } /** * 符号名是否存在 (ReverseGuard.#symbolExists) */ existsByName(name) { const row = this.drizzle .select({ name: this.table.name }) .from(this.table) .where(eq(this.table.name, name)) .limit(1) .get(); return row != null; } /* ─── 内部辅助 ─── */ #mapRow(row) { let protocols = []; let metadata = {}; try { protocols = JSON.parse(row.protocols ?? '[]'); } catch { /* ignore */ } try { metadata = JSON.parse(row.metadataJson ?? '{}'); } catch { /* ignore */ } return { id: row.id, entityId: row.entityId, entityType: row.entityType, projectRoot: row.projectRoot, name: row.name, filePath: row.filePath, lineNumber: row.lineNumber, superclass: row.superclass, protocols, metadata, createdAt: row.createdAt, updatedAt: row.updatedAt, }; } }