package db import ( "strings" "time" ) // GetRepositoryAnnotations retrieves all annotations for a repository func GetRepositoryAnnotations(db DBTX, did, repository string) (map[string]string, error) { rows, err := db.Query(` SELECT key, value FROM repository_annotations WHERE did = ? AND repository = ? `, did, repository) if err != nil { return nil, err } defer rows.Close() annotations := make(map[string]string) for rows.Next() { var key, value string if err := rows.Scan(&key, &value); err != nil { return nil, err } annotations[key] = value } return annotations, rows.Err() } // GetRepositoryAnnotationsByDID retrieves all annotations for every // repository owned by a DID, grouped as map[repository]map[key]value. // Used by bulk-fetch paths to avoid issuing one query per repository. func GetRepositoryAnnotationsByDID(db DBTX, did string) (map[string]map[string]string, error) { rows, err := db.Query(` SELECT repository, key, value FROM repository_annotations WHERE did = ? `, did) if err != nil { return nil, err } defer rows.Close() out := make(map[string]map[string]string) for rows.Next() { var repo, key, value string if err := rows.Scan(&repo, &key, &value); err != nil { return nil, err } m, ok := out[repo] if !ok { m = make(map[string]string) out[repo] = m } m[key] = value } return out, rows.Err() } // UpsertRepositoryAnnotations upserts annotations for a repository. // Stale keys not present in the new map are deleted. // Unchanged values are skipped to avoid unnecessary writes. // Only called when manifest has at least one non-empty annotation. // Atomicity is provided by the caller's transaction when used during backfill. func UpsertRepositoryAnnotations(db DBTX, did, repository string, annotations map[string]string) error { // Delete keys that are no longer in the annotation set if len(annotations) == 0 { _, err := db.Exec(` DELETE FROM repository_annotations WHERE did = ? AND repository = ? `, did, repository) return err } // Build placeholders for the NOT IN clause placeholders := make([]string, 0, len(annotations)) args := []any{did, repository} for key := range annotations { placeholders = append(placeholders, "?") args = append(args, key) } _, err := db.Exec(` DELETE FROM repository_annotations WHERE did = ? AND repository = ? AND key NOT IN (`+strings.Join(placeholders, ",")+`) `, args...) if err != nil { return err } // Upsert each annotation, only writing when value changed stmt, err := db.Prepare(` INSERT INTO repository_annotations (did, repository, key, value, updated_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(did, repository, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at WHERE excluded.value != repository_annotations.value `) if err != nil { return err } defer stmt.Close() now := time.Now() for key, value := range annotations { _, err = stmt.Exec(did, repository, key, value, now) if err != nil { return err } } return nil } // DeleteRepositoryAnnotations removes all annotations for a repository func DeleteRepositoryAnnotations(db DBTX, did, repository string) error { _, err := db.Exec(` DELETE FROM repository_annotations WHERE did = ? AND repository = ? `, did, repository) return err }