internal/index/sqlite.go
@@ -15,7 +15,8 @@
// Store SQLite 存储层
type Store struct {
   db *sql.DB
   db     *sql.DB
   dbPath string
}
// Open 打开或创建数据库
@@ -31,11 +32,15 @@
      return nil, fmt.Errorf("打开数据库失败: %w", err)
   }
   s := &Store{db: db}
   s := &Store{db: db, dbPath: dbPath}
   if err := s.initTables(); err != nil {
      db.Close()
      return nil, err
   }
   if err := s.migrate(); err != nil {
      db.Close()
      return nil, err
   }
   return s, nil
}
@@ -92,17 +97,20 @@
   return err
}
// InsertNode 插入节点
// InsertNode 插入节点(全量重建路径用)
func (s *Store) InsertNode(n *graph.Node) (int64, error) {
   tagsJSON, _ := json.Marshal(n.Tags)
   entitiesJSON, _ := json.Marshal(n.Entities)
   wikilinksJSON, _ := json.Marshal(n.Wikilinks)
   aliasesJSON, _ := json.Marshal(n.Aliases)
   result, err := s.db.Exec(`
      INSERT INTO nodes (path, title, section, tags, entities, wikilinks, content_fts)
      VALUES (?, ?, ?, ?, ?, ?, ?)
      INSERT INTO nodes (path, title, section, tags, entities, wikilinks, aliases, status,
                     content_fts, size, mtime, content_hash)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
   `, n.Path, n.Title, n.Section, string(tagsJSON), string(entitiesJSON),
      string(wikilinksJSON), n.Content)
      string(wikilinksJSON), string(aliasesJSON), n.Status, n.Content,
      n.Size, n.Mtime, n.ContentHash)
   if err != nil {
      return 0, err
   }
@@ -112,9 +120,97 @@
// InsertEdge 插入边
func (s *Store) InsertEdge(e *graph.Edge) error {
   _, err := s.db.Exec(`
      INSERT OR IGNORE INTO edges (from_node, to_node, relation, label)
      VALUES (?, ?, ?, ?)
   `, e.FromNode, e.ToNode, e.Relation, e.Label)
      INSERT OR IGNORE INTO edges (from_node, to_node, relation, label, provenance)
      VALUES (?, ?, ?, ?, ?)
   `, e.FromNode, e.ToNode, e.Relation, e.Label, e.Provenance)
   return err
}
// FileStat 索引中的文件指纹
type FileStat struct {
   Path        string
   Size        int64
   Mtime       int64
   ContentHash string
}
// GetFileStats 返回所有已索引文件的指纹
func (s *Store) GetFileStats() (map[string]FileStat, error) {
   rows, err := s.db.Query(`SELECT path, size, mtime, content_hash FROM nodes`)
   if err != nil {
      return nil, err
   }
   defer rows.Close()
   m := make(map[string]FileStat)
   for rows.Next() {
      var st FileStat
      var hash sql.NullString
      if err := rows.Scan(&st.Path, &st.Size, &st.Mtime, &hash); err != nil {
         return nil, err
      }
      st.ContentHash = hash.String
      m[st.Path] = st
   }
   return m, rows.Err()
}
// UpsertNode 按 path 插入或更新节点(触发器自动维护 FTS)
func (s *Store) UpsertNode(n *graph.Node, size, mtime int64, contentHash string) error {
   tagsJSON, _ := json.Marshal(n.Tags)
   entitiesJSON, _ := json.Marshal(n.Entities)
   wikilinksJSON, _ := json.Marshal(n.Wikilinks)
   aliasesJSON, _ := json.Marshal(n.Aliases)
   _, err := s.db.Exec(`
      INSERT INTO nodes (path, title, section, tags, entities, wikilinks, aliases, status,
                     content_fts, size, mtime, content_hash)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      ON CONFLICT(path) DO UPDATE SET
         title = excluded.title,
         section = excluded.section,
         tags = excluded.tags,
         entities = excluded.entities,
         wikilinks = excluded.wikilinks,
         aliases = excluded.aliases,
         status = excluded.status,
         content_fts = excluded.content_fts,
         size = excluded.size,
         mtime = excluded.mtime,
         content_hash = excluded.content_hash,
         updated_at = datetime('now')`,
      n.Path, n.Title, n.Section, string(tagsJSON), string(entitiesJSON),
      string(wikilinksJSON), string(aliasesJSON), n.Status, n.Content, size, mtime, contentHash)
   return err
}
// DeleteNode 按 path 删节点(级联删边),同时清该节点相关的 unresolved_links
func (s *Store) DeleteNode(path string) error {
   tx, err := s.db.Begin()
   if err != nil {
      return err
   }
   var id int64
   if err := tx.QueryRow(`SELECT id FROM nodes WHERE path = ?`, path).Scan(&id); err != nil {
      tx.Rollback()
      return nil // 不存在,视为成功
   }
   if _, err := tx.Exec(`DELETE FROM edges WHERE from_node = ? OR to_node = ?`, id, id); err != nil {
      tx.Rollback()
      return err
   }
   if _, err := tx.Exec(`DELETE FROM unresolved_links WHERE from_node = ?`, id); err != nil {
      tx.Rollback()
      return err
   }
   if _, err := tx.Exec(`DELETE FROM nodes WHERE id = ?`, id); err != nil {
      tx.Rollback()
      return err
   }
   return tx.Commit()
}
// DeleteNodeEdges 只删某节点的边(保留节点行,用于"修改"场景重建边)
func (s *Store) DeleteNodeEdges(nodeID int64) error {
   _, err := s.db.Exec(`DELETE FROM edges WHERE from_node = ? OR to_node = ?`, nodeID, nodeID)
   return err
}