···1+description: Add annotations column to layers table for caching layer-level annotations (e.g. in-toto predicate types)
2+query: |
3+ ALTER TABLE layers ADD COLUMN annotations TEXT;
+15-5
pkg/appview/db/models.go
···2930// Layer represents a layer in a manifest
31type Layer struct {
32- ManifestID int64
33- Digest string
34- Size int64
35- MediaType string
36- LayerIndex int
037}
3839// ManifestReference represents a reference to a manifest in a manifest list/index
···149 Pending bool // Whether health check is still in progress
150 // Note: ArtifactType is available via embedded Manifest struct
151}
000000000
···2930// Layer represents a layer in a manifest
31type Layer struct {
32+ ManifestID int64
33+ Digest string
34+ Size int64
35+ MediaType string
36+ LayerIndex int
37+ Annotations map[string]string // JSON-encoded layer annotations (e.g. in-toto predicate type)
38}
3940// ManifestReference represents a reference to a manifest in a manifest list/index
···150 Pending bool // Whether health check is still in progress
151 // Note: ArtifactType is available via embedded Manifest struct
152}
153+154+// AttestationDetail represents an attestation manifest and its layers
155+type AttestationDetail struct {
156+ Digest string
157+ MediaType string // attestation manifest media type
158+ Size int64
159+ HoldEndpoint string // hold DID/URL where blobs are stored
160+ Layers []Layer
161+}
+100-6
pkg/appview/db/queries.go
···23import (
4 "database/sql"
05 "fmt"
6 "strings"
7 "time"
···576 return id, nil
577}
578579-// InsertLayer inserts a new layer record
0580func InsertLayer(db DBTX, layer *Layer) error {
000000000581 _, err := db.Exec(`
582- INSERT INTO layers (manifest_id, digest, size, media_type, layer_index)
583- VALUES (?, ?, ?, ?, ?)
584- `, layer.ManifestID, layer.Digest, layer.Size, layer.MediaType, layer.LayerIndex)
00000585 return err
586}
587···820// GetLayersForManifest fetches all layers for a manifest
821func GetLayersForManifest(db DBTX, manifestID int64) ([]Layer, error) {
822 rows, err := db.Query(`
823- SELECT manifest_id, digest, size, media_type, layer_index
824 FROM layers
825 WHERE manifest_id = ?
826 ORDER BY layer_index
···834 var layers []Layer
835 for rows.Next() {
836 var l Layer
837- if err := rows.Scan(&l.ManifestID, &l.Digest, &l.Size, &l.MediaType, &l.LayerIndex); err != nil {
0838 return nil, err
00000839 }
840 layers = append(layers, l)
841 }
···1207 }
12081209 return tags, nil
0000000000000000000000000000000000000000000000000000000000000000000000001210}
12111212// BackfillState represents the backfill progress
···23import (
4 "database/sql"
5+ "encoding/json"
6 "fmt"
7 "strings"
8 "time"
···577 return id, nil
578}
579580+// InsertLayer inserts or updates a layer record.
581+// Uses upsert so backfill re-processing populates new columns (e.g. annotations).
582func InsertLayer(db DBTX, layer *Layer) error {
583+ var annotationsJSON *string
584+ if len(layer.Annotations) > 0 {
585+ b, err := json.Marshal(layer.Annotations)
586+ if err != nil {
587+ return fmt.Errorf("failed to marshal layer annotations: %w", err)
588+ }
589+ s := string(b)
590+ annotationsJSON = &s
591+ }
592 _, err := db.Exec(`
593+ INSERT INTO layers (manifest_id, digest, size, media_type, layer_index, annotations)
594+ VALUES (?, ?, ?, ?, ?, ?)
595+ ON CONFLICT(manifest_id, layer_index) DO UPDATE SET
596+ digest = excluded.digest,
597+ size = excluded.size,
598+ media_type = excluded.media_type,
599+ annotations = excluded.annotations
600+ `, layer.ManifestID, layer.Digest, layer.Size, layer.MediaType, layer.LayerIndex, annotationsJSON)
601 return err
602}
603···836// GetLayersForManifest fetches all layers for a manifest
837func GetLayersForManifest(db DBTX, manifestID int64) ([]Layer, error) {
838 rows, err := db.Query(`
839+ SELECT manifest_id, digest, size, media_type, layer_index, annotations
840 FROM layers
841 WHERE manifest_id = ?
842 ORDER BY layer_index
···850 var layers []Layer
851 for rows.Next() {
852 var l Layer
853+ var annotationsJSON sql.NullString
854+ if err := rows.Scan(&l.ManifestID, &l.Digest, &l.Size, &l.MediaType, &l.LayerIndex, &annotationsJSON); err != nil {
855 return nil, err
856+ }
857+ if annotationsJSON.Valid && annotationsJSON.String != "" {
858+ if err := json.Unmarshal([]byte(annotationsJSON.String), &l.Annotations); err != nil {
859+ return nil, fmt.Errorf("failed to unmarshal layer annotations: %w", err)
860+ }
861 }
862 layers = append(layers, l)
863 }
···1229 }
12301231 return tags, nil
1232+}
1233+1234+// GetAttestationDetails returns attestation manifests and their layers for a manifest list.
1235+// Joins manifest_references (is_attestation=true) → manifests → layers.
1236+func GetAttestationDetails(db DBTX, did, repository, manifestListDigest string) ([]AttestationDetail, error) {
1237+ // Step 1: Get the manifest list ID and hold endpoint
1238+ var manifestListID int64
1239+ var parentHoldEndpoint string
1240+ err := db.QueryRow(`
1241+ SELECT id, hold_endpoint FROM manifests
1242+ WHERE did = ? AND repository = ? AND digest = ?
1243+ `, did, repository, manifestListDigest).Scan(&manifestListID, &parentHoldEndpoint)
1244+ if err != nil {
1245+ return nil, err
1246+ }
1247+1248+ // Step 2: Get attestation references and join to their manifest records
1249+ rows, err := db.Query(`
1250+ SELECT mr.digest, mr.media_type, mr.size, m.id
1251+ FROM manifest_references mr
1252+ LEFT JOIN manifests m ON m.digest = mr.digest AND m.did = ? AND m.repository = ?
1253+ WHERE mr.manifest_id = ? AND mr.is_attestation = 1
1254+ ORDER BY mr.reference_index
1255+ `, did, repository, manifestListID)
1256+ if err != nil {
1257+ return nil, err
1258+ }
1259+ defer rows.Close()
1260+1261+ type refRow struct {
1262+ digest string
1263+ mediaType string
1264+ size int64
1265+ manifestID *int64 // may be NULL if attestation manifest not indexed yet
1266+ }
1267+ var refs []refRow
1268+ for rows.Next() {
1269+ var r refRow
1270+ var mid sql.NullInt64
1271+ if err := rows.Scan(&r.digest, &r.mediaType, &r.size, &mid); err != nil {
1272+ return nil, err
1273+ }
1274+ if mid.Valid {
1275+ r.manifestID = &mid.Int64
1276+ }
1277+ refs = append(refs, r)
1278+ }
1279+ if err := rows.Err(); err != nil {
1280+ return nil, err
1281+ }
1282+1283+ // Step 3: For each attestation manifest, fetch its layers
1284+ // Use the parent manifest list's hold endpoint — attestation blobs are in the same hold
1285+ details := make([]AttestationDetail, 0, len(refs))
1286+ for _, ref := range refs {
1287+ detail := AttestationDetail{
1288+ Digest: ref.digest,
1289+ MediaType: ref.mediaType,
1290+ Size: ref.size,
1291+ HoldEndpoint: parentHoldEndpoint,
1292+ }
1293+ if ref.manifestID != nil {
1294+ layers, err := GetLayersForManifest(db, *ref.manifestID)
1295+ if err != nil {
1296+ return nil, err
1297+ }
1298+ detail.Layers = layers
1299+ }
1300+ details = append(details, detail)
1301+ }
1302+1303+ return details, nil
1304}
13051306// BackfillState represents the backfill progress
+1
pkg/appview/db/schema.sql
···55 size INTEGER NOT NULL,
56 media_type TEXT NOT NULL,
57 layer_index INTEGER NOT NULL,
058 PRIMARY KEY(manifest_id, layer_index),
59 FOREIGN KEY(manifest_id) REFERENCES manifests(id) ON DELETE CASCADE
60);
···55 size INTEGER NOT NULL,
56 media_type TEXT NOT NULL,
57 layer_index INTEGER NOT NULL,
58+ annotations TEXT,
59 PRIMARY KEY(manifest_id, layer_index),
60 FOREIGN KEY(manifest_id) REFERENCES manifests(id) ON DELETE CASCADE
61);