265 lines
8.9 KiB
Go
265 lines
8.9 KiB
Go
package models
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
type CrashSignature struct {
|
|
ID string `json:"id"`
|
|
RepositoryID string `json:"repository_id"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
SampleTrace *string `json:"sample_trace,omitempty"`
|
|
FirstSeenAt time.Time `json:"first_seen_at"`
|
|
LastSeenAt time.Time `json:"last_seen_at"`
|
|
OccurrenceCount int `json:"occurrence_count"`
|
|
}
|
|
|
|
type CrashGroup struct {
|
|
ID string `json:"id"`
|
|
CrashSignatureID string `json:"crash_signature_id"`
|
|
RepositoryID string `json:"repository_id"`
|
|
Title string `json:"title"`
|
|
Status string `json:"status"`
|
|
ForgejoIssueID *int `json:"forgejo_issue_id,omitempty"`
|
|
ForgejoIssueURL *string `json:"forgejo_issue_url,omitempty"`
|
|
FirstSeenAt time.Time `json:"first_seen_at"`
|
|
LastSeenAt time.Time `json:"last_seen_at"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
|
|
// Joined fields
|
|
RepoName string `json:"repo_name,omitempty"`
|
|
Fingerprint string `json:"fingerprint,omitempty"`
|
|
OccurrenceCount int `json:"occurrence_count,omitempty"`
|
|
}
|
|
|
|
// GetOrCreateSignature upserts a crash signature, incrementing occurrence count.
|
|
func GetOrCreateSignature(ctx context.Context, pool *pgxpool.Pool, repoID, fingerprint string, sampleTrace *string) (*CrashSignature, bool, error) {
|
|
sig := &CrashSignature{}
|
|
var created bool
|
|
|
|
// Try insert first.
|
|
err := pool.QueryRow(ctx, `
|
|
INSERT INTO crash_signatures (repository_id, fingerprint, sample_trace)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (repository_id, fingerprint)
|
|
DO UPDATE SET
|
|
last_seen_at = NOW(),
|
|
occurrence_count = crash_signatures.occurrence_count + 1
|
|
RETURNING id, repository_id, fingerprint, sample_trace, first_seen_at, last_seen_at, occurrence_count
|
|
`, repoID, fingerprint, sampleTrace).Scan(
|
|
&sig.ID, &sig.RepositoryID, &sig.Fingerprint, &sig.SampleTrace,
|
|
&sig.FirstSeenAt, &sig.LastSeenAt, &sig.OccurrenceCount,
|
|
)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("get or create signature: %w", err)
|
|
}
|
|
|
|
// If occurrence count is 1, this is a new signature.
|
|
created = sig.OccurrenceCount == 1
|
|
return sig, created, nil
|
|
}
|
|
|
|
// CreateCrashGroup creates a crash group for a new signature.
|
|
func CreateCrashGroup(ctx context.Context, pool *pgxpool.Pool, sigID, repoID, title string) (*CrashGroup, error) {
|
|
cg := &CrashGroup{}
|
|
err := pool.QueryRow(ctx, `
|
|
INSERT INTO crash_groups (crash_signature_id, repository_id, title)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING id, crash_signature_id, repository_id, title, status,
|
|
forgejo_issue_id, forgejo_issue_url, first_seen_at, last_seen_at, created_at, updated_at
|
|
`, sigID, repoID, title).Scan(
|
|
&cg.ID, &cg.CrashSignatureID, &cg.RepositoryID, &cg.Title, &cg.Status,
|
|
&cg.ForgejoIssueID, &cg.ForgejoIssueURL, &cg.FirstSeenAt, &cg.LastSeenAt,
|
|
&cg.CreatedAt, &cg.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating crash group: %w", err)
|
|
}
|
|
return cg, nil
|
|
}
|
|
|
|
// ListCrashGroups returns crash groups with joined data.
|
|
func ListCrashGroups(ctx context.Context, pool *pgxpool.Pool, repoID, status string, limit, offset int) ([]CrashGroup, int, error) {
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
|
|
baseQuery := `
|
|
FROM crash_groups cg
|
|
JOIN crash_signatures cs ON cs.id = cg.crash_signature_id
|
|
JOIN repositories r ON r.id = cg.repository_id
|
|
WHERE 1=1
|
|
`
|
|
args := []any{}
|
|
argN := 1
|
|
|
|
if repoID != "" {
|
|
baseQuery += fmt.Sprintf(" AND cg.repository_id = $%d", argN)
|
|
args = append(args, repoID)
|
|
argN++
|
|
}
|
|
if status != "" {
|
|
baseQuery += fmt.Sprintf(" AND cg.status = $%d", argN)
|
|
args = append(args, status)
|
|
argN++
|
|
}
|
|
|
|
var total int
|
|
err := pool.QueryRow(ctx, "SELECT COUNT(*) "+baseQuery, args...).Scan(&total)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("counting crash groups: %w", err)
|
|
}
|
|
|
|
selectQuery := fmt.Sprintf(`
|
|
SELECT cg.id, cg.crash_signature_id, cg.repository_id, cg.title, cg.status,
|
|
cg.forgejo_issue_id, cg.forgejo_issue_url, cg.first_seen_at, cg.last_seen_at,
|
|
cg.created_at, cg.updated_at,
|
|
r.name, cs.fingerprint, cs.occurrence_count
|
|
%s
|
|
ORDER BY cg.last_seen_at DESC
|
|
LIMIT $%d OFFSET $%d
|
|
`, baseQuery, argN, argN+1)
|
|
args = append(args, limit, offset)
|
|
|
|
rows, err := pool.Query(ctx, selectQuery, args...)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("listing crash groups: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var groups []CrashGroup
|
|
for rows.Next() {
|
|
var cg CrashGroup
|
|
if err := rows.Scan(
|
|
&cg.ID, &cg.CrashSignatureID, &cg.RepositoryID, &cg.Title, &cg.Status,
|
|
&cg.ForgejoIssueID, &cg.ForgejoIssueURL, &cg.FirstSeenAt, &cg.LastSeenAt,
|
|
&cg.CreatedAt, &cg.UpdatedAt,
|
|
&cg.RepoName, &cg.Fingerprint, &cg.OccurrenceCount,
|
|
); err != nil {
|
|
return nil, 0, fmt.Errorf("scanning crash group: %w", err)
|
|
}
|
|
groups = append(groups, cg)
|
|
}
|
|
return groups, total, nil
|
|
}
|
|
|
|
// GetCrashGroup returns a single crash group by ID.
|
|
func GetCrashGroup(ctx context.Context, pool *pgxpool.Pool, id string) (*CrashGroup, error) {
|
|
cg := &CrashGroup{}
|
|
err := pool.QueryRow(ctx, `
|
|
SELECT cg.id, cg.crash_signature_id, cg.repository_id, cg.title, cg.status,
|
|
cg.forgejo_issue_id, cg.forgejo_issue_url, cg.first_seen_at, cg.last_seen_at,
|
|
cg.created_at, cg.updated_at,
|
|
r.name, cs.fingerprint, cs.occurrence_count
|
|
FROM crash_groups cg
|
|
JOIN crash_signatures cs ON cs.id = cg.crash_signature_id
|
|
JOIN repositories r ON r.id = cg.repository_id
|
|
WHERE cg.id = $1
|
|
`, id).Scan(
|
|
&cg.ID, &cg.CrashSignatureID, &cg.RepositoryID, &cg.Title, &cg.Status,
|
|
&cg.ForgejoIssueID, &cg.ForgejoIssueURL, &cg.FirstSeenAt, &cg.LastSeenAt,
|
|
&cg.CreatedAt, &cg.UpdatedAt,
|
|
&cg.RepoName, &cg.Fingerprint, &cg.OccurrenceCount,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting crash group: %w", err)
|
|
}
|
|
return cg, nil
|
|
}
|
|
|
|
// UpdateCrashGroupIssue links a crash group to a Forgejo issue.
|
|
func UpdateCrashGroupIssue(ctx context.Context, pool *pgxpool.Pool, groupID string, issueNumber int, issueURL string) error {
|
|
_, err := pool.Exec(ctx, `
|
|
UPDATE crash_groups SET forgejo_issue_id = $1, forgejo_issue_url = $2, updated_at = NOW() WHERE id = $3
|
|
`, issueNumber, issueURL, groupID)
|
|
if err != nil {
|
|
return fmt.Errorf("updating crash group issue: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ResolveCrashGroupByIssue marks a crash group as resolved when its Forgejo issue is closed.
|
|
func ResolveCrashGroupByIssue(ctx context.Context, pool *pgxpool.Pool, issueNumber int) error {
|
|
_, err := pool.Exec(ctx, `
|
|
UPDATE crash_groups SET status = 'resolved', updated_at = NOW() WHERE forgejo_issue_id = $1
|
|
`, issueNumber)
|
|
if err != nil {
|
|
return fmt.Errorf("resolving crash group by issue: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReopenCrashGroupByIssue reopens a crash group when its Forgejo issue is reopened.
|
|
func ReopenCrashGroupByIssue(ctx context.Context, pool *pgxpool.Pool, issueNumber int) error {
|
|
_, err := pool.Exec(ctx, `
|
|
UPDATE crash_groups SET status = 'open', updated_at = NOW() WHERE forgejo_issue_id = $1
|
|
`, issueNumber)
|
|
if err != nil {
|
|
return fmt.Errorf("reopening crash group by issue: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateArtifactSignature links an artifact to a signature.
|
|
func UpdateArtifactSignature(ctx context.Context, pool *pgxpool.Pool, artifactID, signatureID, fingerprint string) error {
|
|
_, err := pool.Exec(ctx, `
|
|
UPDATE artifacts SET signature_id = $1, fingerprint = $2 WHERE id = $3
|
|
`, signatureID, fingerprint, artifactID)
|
|
if err != nil {
|
|
return fmt.Errorf("updating artifact signature: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SearchArtifacts performs full-text search on artifacts.
|
|
func SearchArtifacts(ctx context.Context, pool *pgxpool.Pool, query string, limit, offset int) ([]Artifact, int, error) {
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
|
|
var total int
|
|
err := pool.QueryRow(ctx, `
|
|
SELECT COUNT(*)
|
|
FROM artifacts a
|
|
WHERE a.search_vector @@ plainto_tsquery('english', $1)
|
|
`, query).Scan(&total)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("counting search results: %w", err)
|
|
}
|
|
|
|
rows, err := pool.Query(ctx, `
|
|
SELECT a.id, a.repository_id, a.commit_id, a.build_id, a.type, a.blob_key, a.blob_size,
|
|
a.crash_message, a.stack_trace, a.tags, a.metadata, a.created_at,
|
|
r.name, c.sha
|
|
FROM artifacts a
|
|
JOIN repositories r ON r.id = a.repository_id
|
|
JOIN commits c ON c.id = a.commit_id
|
|
WHERE a.search_vector @@ plainto_tsquery('english', $1)
|
|
ORDER BY ts_rank(a.search_vector, plainto_tsquery('english', $1)) DESC
|
|
LIMIT $2 OFFSET $3
|
|
`, query, limit, offset)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("searching artifacts: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var artifacts []Artifact
|
|
for rows.Next() {
|
|
var a Artifact
|
|
if err := rows.Scan(
|
|
&a.ID, &a.RepositoryID, &a.CommitID, &a.BuildID, &a.Type, &a.BlobKey, &a.BlobSize,
|
|
&a.CrashMessage, &a.StackTrace, &a.Tags, &a.Metadata, &a.CreatedAt,
|
|
&a.RepoName, &a.CommitSHA,
|
|
); err != nil {
|
|
return nil, 0, fmt.Errorf("scanning search result: %w", err)
|
|
}
|
|
artifacts = append(artifacts, a)
|
|
}
|
|
return artifacts, total, nil
|
|
}
|