| | |
| | | |
| | | // Store SQLite 存储层 |
| | | type Store struct { |
| | | db *sql.DB |
| | | db *sql.DB |
| | | dbPath string |
| | | } |
| | | |
| | | // Open 打开或创建数据库 |
| | |
| | | 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 |
| | | } |
| | | |
| | |
| | | |
| | | // ClearData 清空数据(重建前调用) |
| | | func (s *Store) ClearData() error { |
| | | _, err := s.db.Exec("DELETE FROM edges; DELETE FROM nodes;") |
| | | _, err := s.db.Exec("DELETE FROM edges; DELETE FROM nodes; DELETE FROM unresolved_links;") |
| | | 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 |
| | | } |
| | |
| | | // 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 |
| | | } |
| | | |
| | | // InsertUnresolved 悬空链接入表(幂等:同 from_node+link_text 不重复插) |
| | | func (s *Store) InsertUnresolved(fromNode int64, linkText, nameTail string) error { |
| | | _, err := s.db.Exec(` |
| | | INSERT INTO unresolved_links (from_node, link_text, name_tail, status) |
| | | SELECT ?, ?, ?, 'pending' |
| | | WHERE NOT EXISTS ( |
| | | SELECT 1 FROM unresolved_links WHERE from_node = ? AND link_text = ?)`, |
| | | fromNode, linkText, nameTail, fromNode, linkText) |
| | | 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 |
| | | } |
| | | |
| | |
| | | return links, nil |
| | | } |
| | | |
| | | // GetUnresolvedLinks 某文档的悬空链接文本列表 |
| | | func (s *Store) GetUnresolvedLinks(path string) ([]string, error) { |
| | | rows, err := s.db.Query(` |
| | | SELECT u.link_text FROM unresolved_links u |
| | | JOIN nodes n ON n.id = u.from_node WHERE n.path = ?`, path) |
| | | if err != nil { |
| | | return nil, err |
| | | } |
| | | defer rows.Close() |
| | | var links []string |
| | | for rows.Next() { |
| | | var l string |
| | | rows.Scan(&l) |
| | | links = append(links, l) |
| | | } |
| | | return links, rows.Err() |
| | | } |
| | | |
| | | // NodeInfo 节点基本信息(用于 GC) |
| | | type NodeInfo struct { |
| | | ID int64 |