package artifacts import ( "context" "database/sql" "errors" "fmt" "sort" "strings" "git.wntrmute.dev/kyle/exo/core" ) // GetTag returns the tag ID for a given tag string. Returns empty string if // the tag doesn't exist. func GetTag(ctx context.Context, tx *sql.Tx, tag string) (string, error) { var id string row := tx.QueryRowContext(ctx, `SELECT id FROM tags WHERE tag=?`, tag) if err := row.Scan(&id); err != nil { if errors.Is(err, sql.ErrNoRows) { return "", nil } return "", fmt.Errorf("artifacts: failed to look up tag %q: %w", tag, err) } return id, nil } // CreateTag idempotently creates a tag. If the tag already exists, this is a no-op. func CreateTag(ctx context.Context, tx *sql.Tx, tag string) error { id, err := GetTag(ctx, tx, tag) if err != nil { return fmt.Errorf("artifacts: creating tag failed: %w", err) } if id != "" { return nil } id = core.NewUUID() _, err = tx.ExecContext(ctx, `INSERT INTO tags (id, tag) VALUES (?, ?)`, id, tag) if err != nil { return fmt.Errorf("artifacts: creating tag %q failed: %w", tag, err) } return nil } // GetAllTags returns all tag strings, sorted alphabetically. func GetAllTags(ctx context.Context, tx *sql.Tx) ([]string, error) { rows, err := tx.QueryContext(ctx, `SELECT tag FROM tags`) if err != nil { return nil, fmt.Errorf("artifacts: failed to get all tags: %w", err) } defer func() { _ = rows.Close() }() var tags []string for rows.Next() { var tag string if err := rows.Scan(&tag); err != nil { return nil, fmt.Errorf("artifacts: failed to scan tag: %w", err) } tags = append(tags, tag) } sort.Strings(tags) return tags, rows.Err() } // GetCategory returns the category ID for a given category string. // Returns empty string if the category doesn't exist. func GetCategory(ctx context.Context, tx *sql.Tx, category string) (string, error) { var id string row := tx.QueryRowContext(ctx, `SELECT id FROM categories WHERE category=?`, category) if err := row.Scan(&id); err != nil { if errors.Is(err, sql.ErrNoRows) { return "", nil } return "", fmt.Errorf("artifacts: failed to look up category %q: %w", category, err) } return id, nil } // CreateCategory idempotently creates a category. func CreateCategory(ctx context.Context, tx *sql.Tx, category string) error { id, err := GetCategory(ctx, tx, category) if err != nil { return fmt.Errorf("artifacts: creating category failed: %w", err) } if id != "" { return nil } id = core.NewUUID() _, err = tx.ExecContext(ctx, `INSERT INTO categories (id, category) VALUES (?, ?)`, id, category) if err != nil { return fmt.Errorf("artifacts: creating category %q failed: %w", category, err) } return nil } // GetAllCategories returns all category strings, sorted alphabetically. func GetAllCategories(ctx context.Context, tx *sql.Tx) ([]string, error) { rows, err := tx.QueryContext(ctx, `SELECT category FROM categories`) if err != nil { return nil, fmt.Errorf("artifacts: failed to get all categories: %w", err) } defer func() { _ = rows.Close() }() var categories []string for rows.Next() { var category string if err := rows.Scan(&category); err != nil { return nil, fmt.Errorf("artifacts: failed to scan category: %w", err) } categories = append(categories, category) } sort.Strings(categories) return categories, rows.Err() } // tagsFromTagIDs resolves a list of tag UUIDs to their string values. func tagsFromTagIDs(ctx context.Context, tx *sql.Tx, idList []string) (map[string]bool, error) { if len(idList) == 0 { return map[string]bool{}, nil } placeholders := make([]string, len(idList)) args := make([]any, len(idList)) for i, id := range idList { placeholders[i] = "?" args[i] = id } query := `SELECT tag FROM tags WHERE id IN (` + strings.Join(placeholders, ",") + `)` //nolint:gosec // placeholders are literal "?" strings, not user input rows, err := tx.QueryContext(ctx, query, args...) if err != nil { return nil, fmt.Errorf("artifacts: failed to resolve tag IDs: %w", err) } defer func() { _ = rows.Close() }() tags := map[string]bool{} for rows.Next() { var tag string if err := rows.Scan(&tag); err != nil { return nil, err } tags[tag] = true } return tags, rows.Err() } // categoriesFromCategoryIDs resolves a list of category UUIDs to their string values. func categoriesFromCategoryIDs(ctx context.Context, tx *sql.Tx, idList []string) (map[string]bool, error) { if len(idList) == 0 { return map[string]bool{}, nil } placeholders := make([]string, len(idList)) args := make([]any, len(idList)) for i, id := range idList { placeholders[i] = "?" args[i] = id } query := `SELECT category FROM categories WHERE id IN (` + strings.Join(placeholders, ",") + `)` //nolint:gosec // placeholders are literal "?" strings, not user input rows, err := tx.QueryContext(ctx, query, args...) if err != nil { return nil, fmt.Errorf("artifacts: failed to resolve category IDs: %w", err) } defer func() { _ = rows.Close() }() categories := map[string]bool{} for rows.Next() { var category string if err := rows.Scan(&category); err != nil { return nil, err } categories[category] = true } return categories, rows.Err() } // GetArtifactIDsForTag returns artifact IDs that have the given tag. func GetArtifactIDsForTag(ctx context.Context, tx *sql.Tx, tag string) ([]string, error) { tagID, err := GetTag(ctx, tx, tag) if err != nil { return nil, fmt.Errorf("artifacts: failed to look up tag ID: %w", err) } if tagID == "" { return nil, nil } rows, err := tx.QueryContext(ctx, `SELECT artifact_id FROM artifact_tags WHERE tag_id=?`, tagID) if err != nil { return nil, fmt.Errorf("artifacts: failed to get artifact IDs for tag: %w", err) } defer func() { _ = rows.Close() }() var ids []string for rows.Next() { var id string if err := rows.Scan(&id); err != nil { return nil, fmt.Errorf("artifacts: failed to scan artifact ID: %w", err) } ids = append(ids, id) } return ids, rows.Err() }