feat(health): remove per-issue detail API, add tag tables and persistence
- Drop fetchIssueDetail — issue list response already contains all needed fields - Add tags/issue_tags/pull_tags tables with indexes - Persist PR and Issue tags during savePull/saveIssue - Fix issue aggregate field names (total_count/opened_count/closed_count) - Document tag tables and queries in queries.md
This commit is contained in:
parent
0da45def2b
commit
0a578bbab0
|
|
@ -60,21 +60,6 @@ func fetchIssueListPage(ctx *common.RuntimeContext, owner, repo, state string, p
|
|||
return issues, data
|
||||
}
|
||||
|
||||
func fetchIssueDetail(ctx *common.RuntimeContext, owner, repo string, issueID int) (map[string]interface{}, error) {
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/issues/%d", v1RepoPath(owner, repo), issueID), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !env.OK {
|
||||
return nil, fmt.Errorf("API error fetching issue %d", issueID)
|
||||
}
|
||||
data, ok := env.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected response format for issue %d", issueID)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func sleep() {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,56 @@ func getOrCreateRepo(db *sql.DB, repoName, owner string) (int, error) {
|
|||
return int(lastID), nil
|
||||
}
|
||||
|
||||
func getOrCreateTag(db *sql.DB, repoID int, tagName string) (int, error) {
|
||||
var id int
|
||||
err := db.QueryRow("SELECT id FROM tags WHERE repo_id = ? AND name = ?", repoID, tagName).Scan(&id)
|
||||
if err == nil {
|
||||
return id, nil
|
||||
}
|
||||
res, err := db.Exec("INSERT INTO tags (repo_id, name) VALUES (?, ?)", repoID, tagName)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("insert tag %q for repo %d: %w", tagName, repoID, err)
|
||||
}
|
||||
lastID, _ := res.LastInsertId()
|
||||
return int(lastID), nil
|
||||
}
|
||||
|
||||
func savePullTags(db *sql.DB, pullID int, tagIDs []int) {
|
||||
if len(tagIDs) == 0 {
|
||||
return
|
||||
}
|
||||
for _, tagID := range tagIDs {
|
||||
db.Exec("INSERT OR IGNORE INTO pull_tags (pull_id, tag_id) VALUES (?, ?)", pullID, tagID)
|
||||
}
|
||||
}
|
||||
|
||||
func saveIssueTags(db *sql.DB, issueID int, tagIDs []int) {
|
||||
if len(tagIDs) == 0 {
|
||||
return
|
||||
}
|
||||
for _, tagID := range tagIDs {
|
||||
db.Exec("INSERT OR IGNORE INTO issue_tags (issue_id, tag_id) VALUES (?, ?)", issueID, tagID)
|
||||
}
|
||||
}
|
||||
|
||||
func extractTagNames(data map[string]interface{}, key string) []string {
|
||||
rawTags, ok := data[key].([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var names []string
|
||||
for _, item := range rawTags {
|
||||
tag, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if name, ok := tag["name"].(string); ok && name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func savePull(db *sql.DB, repoID int, pr map[string]interface{}) {
|
||||
// id: pull_request_id preferred, fallback to id
|
||||
var prID float64
|
||||
|
|
@ -128,6 +178,16 @@ func savePull(db *sql.DB, repoID int, pr map[string]interface{}) {
|
|||
db.Exec(`INSERT OR REPLACE INTO pulls (id, repo_id, number, creater_id, status, processor_id, create_time, close_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
int(prID), repoID, int(prNumber), createrID, status, processorID, createTime, nil)
|
||||
|
||||
// Save tags
|
||||
tagNames := extractTagNames(pr, "issue_tags")
|
||||
var tagIDs []int
|
||||
for _, name := range tagNames {
|
||||
if tid, err := getOrCreateTag(db, repoID, name); err == nil {
|
||||
tagIDs = append(tagIDs, tid)
|
||||
}
|
||||
}
|
||||
savePullTags(db, int(prID), tagIDs)
|
||||
}
|
||||
|
||||
func saveIssue(db *sql.DB, repoID int, issue map[string]interface{}, issueNumber int, listUpdatedAt string) {
|
||||
|
|
@ -165,4 +225,14 @@ func saveIssue(db *sql.DB, repoID int, issue map[string]interface{}, issueNumber
|
|||
db.Exec(`INSERT OR REPLACE INTO issues (id, repo_id, number, creater_id, processor_id, create_time, close_time, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
int(issueID), repoID, issueNumber, createrID, processorID, createTime, closeTime, status)
|
||||
|
||||
// Save tags
|
||||
tagNames := extractTagNames(issue, "tags")
|
||||
var tagIDs []int
|
||||
for _, name := range tagNames {
|
||||
if tid, err := getOrCreateTag(db, repoID, name); err == nil {
|
||||
tagIDs = append(tagIDs, tid)
|
||||
}
|
||||
}
|
||||
saveIssueTags(db, int(issueID), tagIDs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,13 +134,8 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
issueNumber = int(v)
|
||||
}
|
||||
|
||||
detail, err := fetchIssueDetail(ctx, ctx.Owner, ctx.Repo, int(issueID))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, " Error fetching issue %d detail: %v\n", int(issueID), err)
|
||||
continue
|
||||
}
|
||||
listUpdatedAt, _ := issue["updated_at"].(string)
|
||||
saveIssue(db, repoID, detail, issueNumber, listUpdatedAt)
|
||||
saveIssue(db, repoID, issue, issueNumber, listUpdatedAt)
|
||||
}
|
||||
if len(issues) < 20 {
|
||||
break
|
||||
|
|
@ -153,7 +148,7 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
fmt.Fprintf(os.Stderr, " Total Issues: %d\n", len(seenIssueIDs))
|
||||
if issueAgg != nil {
|
||||
fmt.Fprintf(os.Stderr, " API aggregates: total=%v, open=%v, closed=%v\n",
|
||||
issueAgg["all_count"], issueAgg["open_count"], issueAgg["close_count"])
|
||||
issueAgg["total_count"], issueAgg["opened_count"], issueAgg["closed_count"])
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "\nData saved to %s\n", dbPath)
|
||||
|
|
|
|||
|
|
@ -49,3 +49,35 @@ CREATE INDEX IF NOT EXISTS idx_pulls_repo_id ON pulls(repo_id);
|
|||
CREATE INDEX IF NOT EXISTS idx_pulls_status ON pulls(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_pulls_create_time ON pulls(create_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_pulls_creater_id ON pulls(creater_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
repo_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
FOREIGN KEY (repo_id) REFERENCES repos(id),
|
||||
UNIQUE(repo_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tags_repo_id ON tags(repo_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS issue_tags (
|
||||
issue_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (issue_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_issue_tags_issue_id ON issue_tags(issue_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_issue_tags_tag_id ON issue_tags(tag_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pull_tags (
|
||||
pull_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (pull_id) REFERENCES pulls(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (pull_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pull_tags_pull_id ON pull_tags(pull_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pull_tags_tag_id ON pull_tags(tag_id);
|
||||
|
|
|
|||
|
|
@ -67,6 +67,48 @@ CREATE TABLE pulls (
|
|||
|
||||
API 字段映射:`status` ← `pull_request_status`(0=open, 1=merged, 2=closed),`creater_id` ← `author_login`。
|
||||
|
||||
### tags
|
||||
|
||||
```sql
|
||||
CREATE TABLE tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
repo_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
FOREIGN KEY (repo_id) REFERENCES repos(id),
|
||||
UNIQUE(repo_id, name)
|
||||
);
|
||||
```
|
||||
|
||||
API 字段映射:`name` ← `name`,先调用 `GET /:owner/:repo/tags` 批量写入,后续 PR/Issue 保存时只做关联查询。
|
||||
|
||||
### issue_tags
|
||||
|
||||
```sql
|
||||
CREATE TABLE issue_tags (
|
||||
issue_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (issue_id, tag_id)
|
||||
);
|
||||
```
|
||||
|
||||
关系表:一个 Issue 可关联多个 tag。写入来源:Issue detail 中的 `tags` 数组。
|
||||
|
||||
### pull_tags
|
||||
|
||||
```sql
|
||||
CREATE TABLE pull_tags (
|
||||
pull_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (pull_id) REFERENCES pulls(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (pull_id, tag_id)
|
||||
);
|
||||
```
|
||||
|
||||
关系表:一个 PR 可关联多个 tag。写入来源:PR list 中的 `issue_tags` 数组。
|
||||
|
||||
## 1. 确定目标仓库
|
||||
|
||||
数据库可能包含多个仓库的数据,查询前需先获取目标仓库的 `repo_id`:
|
||||
|
|
@ -334,6 +376,54 @@ FROM (
|
|||
);
|
||||
```
|
||||
|
||||
### 标签列表
|
||||
|
||||
```sql
|
||||
SELECT name FROM tags WHERE repo_id = <repo_id> ORDER BY name;
|
||||
```
|
||||
|
||||
### 按标签统计 Issue 数量
|
||||
|
||||
```sql
|
||||
SELECT t.name, COUNT(*) as issue_count
|
||||
FROM tags t
|
||||
JOIN issue_tags it ON t.id = it.tag_id
|
||||
JOIN issues i ON it.issue_id = i.id
|
||||
WHERE t.repo_id = <repo_id>
|
||||
GROUP BY t.id
|
||||
ORDER BY issue_count DESC;
|
||||
```
|
||||
|
||||
### 按标签统计 PR 数量
|
||||
|
||||
```sql
|
||||
SELECT t.name, COUNT(*) as pr_count
|
||||
FROM tags t
|
||||
JOIN pull_tags pt ON t.id = pt.tag_id
|
||||
JOIN pulls p ON pt.pull_id = p.id
|
||||
WHERE t.repo_id = <repo_id>
|
||||
GROUP BY t.id
|
||||
ORDER BY pr_count DESC;
|
||||
```
|
||||
|
||||
### 查询某 Issue 的所有标签
|
||||
|
||||
```sql
|
||||
SELECT t.name
|
||||
FROM tags t
|
||||
JOIN issue_tags it ON t.id = it.tag_id
|
||||
WHERE it.issue_id = <issue_id>;
|
||||
```
|
||||
|
||||
### 查询某 PR 的所有标签
|
||||
|
||||
```sql
|
||||
SELECT t.name
|
||||
FROM tags t
|
||||
JOIN pull_tags pt ON t.id = pt.tag_id
|
||||
WHERE pt.pull_id = <pull_id>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 报告组装清单
|
||||
|
|
|
|||
Loading…
Reference in New Issue