package db import ( "database/sql" "errors" "fmt" ) // GetOrCreateRepository returns the repository ID for the given name, // creating it if it does not exist (implicit creation on first push). func (d *DB) GetOrCreateRepository(name string) (int64, error) { var id int64 err := d.QueryRow(`SELECT id FROM repositories WHERE name = ?`, name).Scan(&id) if err == nil { return id, nil } if !errors.Is(err, sql.ErrNoRows) { return 0, fmt.Errorf("db: get repository: %w", err) } result, err := d.Exec(`INSERT INTO repositories (name) VALUES (?)`, name) if err != nil { return 0, fmt.Errorf("db: create repository: %w", err) } id, err = result.LastInsertId() if err != nil { return 0, fmt.Errorf("db: repository last insert id: %w", err) } return id, nil } // BlobExists checks whether a blob with the given digest exists in the blobs table. func (d *DB) BlobExists(digest string) (bool, error) { var count int err := d.QueryRow(`SELECT COUNT(*) FROM blobs WHERE digest = ?`, digest).Scan(&count) if err != nil { return false, fmt.Errorf("db: blob exists: %w", err) } return count > 0, nil } // InsertBlob inserts a blob row if it does not already exist. // Returns without error if the blob already exists (content-addressed dedup). func (d *DB) InsertBlob(digest string, size int64) error { _, err := d.Exec( `INSERT OR IGNORE INTO blobs (digest, size) VALUES (?, ?)`, digest, size, ) if err != nil { return fmt.Errorf("db: insert blob: %w", err) } return nil } // PushManifestParams holds the parameters for a manifest push operation. type PushManifestParams struct { RepoName string Digest string MediaType string Content []byte Size int64 Tag string // empty if push-by-digest BlobDigests []string // referenced blob digests } // PushManifest executes the full manifest push in a single transaction per // ARCHITECTURE.md ยง5. It creates the repository if needed, inserts/updates // the manifest, populates manifest_blobs, and updates the tag if provided. func (d *DB) PushManifest(p PushManifestParams) error { tx, err := d.Begin() if err != nil { return fmt.Errorf("db: begin push manifest: %w", err) } // Step a: create repository if not exists. var repoID int64 err = tx.QueryRow(`SELECT id FROM repositories WHERE name = ?`, p.RepoName).Scan(&repoID) if errors.Is(err, sql.ErrNoRows) { result, insertErr := tx.Exec(`INSERT INTO repositories (name) VALUES (?)`, p.RepoName) if insertErr != nil { _ = tx.Rollback() return fmt.Errorf("db: create repository: %w", insertErr) } repoID, err = result.LastInsertId() if err != nil { _ = tx.Rollback() return fmt.Errorf("db: repository last insert id: %w", err) } } else if err != nil { _ = tx.Rollback() return fmt.Errorf("db: get repository: %w", err) } // Step b: insert or update manifest. // Use INSERT OR REPLACE on the UNIQUE(repository_id, digest) constraint. result, err := tx.Exec( `INSERT INTO manifests (repository_id, digest, media_type, content, size) VALUES (?, ?, ?, ?, ?) ON CONFLICT(repository_id, digest) DO UPDATE SET media_type = excluded.media_type, content = excluded.content, size = excluded.size`, repoID, p.Digest, p.MediaType, p.Content, p.Size, ) if err != nil { _ = tx.Rollback() return fmt.Errorf("db: insert manifest: %w", err) } manifestID, err := result.LastInsertId() if err != nil { _ = tx.Rollback() return fmt.Errorf("db: manifest last insert id: %w", err) } // Step c: populate manifest_blobs join table. // Delete existing entries for this manifest first (in case of re-push). _, err = tx.Exec(`DELETE FROM manifest_blobs WHERE manifest_id = ?`, manifestID) if err != nil { _ = tx.Rollback() return fmt.Errorf("db: clear manifest_blobs: %w", err) } for _, blobDigest := range p.BlobDigests { _, err = tx.Exec( `INSERT INTO manifest_blobs (manifest_id, blob_id) SELECT ?, id FROM blobs WHERE digest = ?`, manifestID, blobDigest, ) if err != nil { _ = tx.Rollback() return fmt.Errorf("db: insert manifest_blob: %w", err) } } // Step d: if reference is a tag, insert or update tag row. if p.Tag != "" { _, err = tx.Exec( `INSERT INTO tags (repository_id, name, manifest_id) VALUES (?, ?, ?) ON CONFLICT(repository_id, name) DO UPDATE SET manifest_id = excluded.manifest_id, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ','now')`, repoID, p.Tag, manifestID, ) if err != nil { _ = tx.Rollback() return fmt.Errorf("db: upsert tag: %w", err) } } if err := tx.Commit(); err != nil { return fmt.Errorf("db: commit push manifest: %w", err) } return nil }