package db import ( "context" "database/sql" "errors" ) type Release struct { ID int64 RepoID int64 TagName *string Name string Notes *string IncludeSourceCode bool CreatedAt string } type ReleaseAsset struct { ID int64 ReleaseID int64 Filename string Size int64 CreatedAt string } // NewAsset describes a file already written to disk that still needs a row. type NewAsset struct { Filename string Size int64 } const releaseColumns = `id, repo_id, tag_name, name, notes, include_source_code, created_at` func scanRelease(s rowScanner) (*Release, error) { var r Release err := s.Scan(&r.ID, &r.RepoID, &r.TagName, &r.Name, &r.Notes, &r.IncludeSourceCode, &r.CreatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, err } return &r, nil } func (d *DB) CountReleases(ctx context.Context, repoID int64) (int, error) { var n int err := d.QueryRowContext(ctx, `SELECT COUNT(*) FROM releases WHERE repo_id = ?`, repoID).Scan(&n) return n, err } func (d *DB) ListReleases(ctx context.Context, repoID int64, limit, offset int) ([]Release, error) { rows, err := d.QueryContext(ctx, `SELECT `+releaseColumns+` FROM releases WHERE repo_id = ? ORDER BY id DESC LIMIT ? OFFSET ?`, repoID, limit, offset) if err != nil { return nil, err } defer rows.Close() var out []Release for rows.Next() { r, err := scanRelease(rows) if err != nil { return nil, err } out = append(out, *r) } return out, rows.Err() } func (d *DB) ReleaseInRepo(ctx context.Context, repoID, id int64) (*Release, error) { return scanRelease(d.QueryRowContext(ctx, `SELECT `+releaseColumns+` FROM releases WHERE repo_id = ? AND id = ?`, repoID, id)) } // AssetCounts returns the number of assets per release for a list view. func (d *DB) AssetCounts(ctx context.Context, releaseIDs []int64) (map[int64]int, error) { out := map[int64]int{} if len(releaseIDs) == 0 { return out, nil } rows, err := d.QueryContext(ctx, `SELECT release_id, COUNT(*) FROM release_assets WHERE release_id IN (`+placeholders(len(releaseIDs))+`) GROUP BY release_id`, int64Args(releaseIDs)...) if err != nil { return nil, err } defer rows.Close() for rows.Next() { var id int64 var n int if err := rows.Scan(&id, &n); err != nil { return nil, err } out[id] = n } return out, rows.Err() } func (d *DB) ListReleaseAssets(ctx context.Context, releaseID int64) ([]ReleaseAsset, error) { rows, err := d.QueryContext(ctx, `SELECT id, release_id, filename, size, created_at FROM release_assets WHERE release_id = ? ORDER BY id ASC`, releaseID) if err != nil { return nil, err } defer rows.Close() var out []ReleaseAsset for rows.Next() { var a ReleaseAsset if err := rows.Scan(&a.ID, &a.ReleaseID, &a.Filename, &a.Size, &a.CreatedAt); err != nil { return nil, err } out = append(out, a) } return out, rows.Err() } func (d *DB) ReleaseAssetByFilename(ctx context.Context, releaseID int64, filename string) (*ReleaseAsset, error) { var a ReleaseAsset err := d.QueryRowContext(ctx, `SELECT id, release_id, filename, size, created_at FROM release_assets WHERE release_id = ? AND filename = ?`, releaseID, filename).Scan(&a.ID, &a.ReleaseID, &a.Filename, &a.Size, &a.CreatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, err } return &a, nil } // CreateRelease inserts the release row and returns its id. The caller writes // the release directory afterwards and deletes the row when that fails. File // work must not run inside a transaction: SQLite here has one connection, so // a long write would block every other request. func (d *DB) CreateRelease(ctx context.Context, repoID int64, tagName *string, name string, notes *string, includeSource bool, now string, ) (int64, error) { res, err := d.ExecContext(ctx, `INSERT INTO releases (repo_id, tag_name, name, notes, include_source_code, created_at) VALUES (?, ?, ?, ?, ?, ?)`, repoID, tagName, name, notes, includeSource, now) if err != nil { return 0, err } return res.LastInsertId() } // AddReleaseAssets records the files the caller already wrote to disk. func (d *DB) AddReleaseAssets(ctx context.Context, releaseID int64, assets []NewAsset, now string) error { if len(assets) == 0 { return nil } tx, err := d.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() for _, a := range assets { if _, err := tx.ExecContext(ctx, `INSERT INTO release_assets (release_id, filename, size, created_at) VALUES (?, ?, ?, ?)`, releaseID, a.Filename, a.Size, now); err != nil { return err } } return tx.Commit() } // ReleaseExistsForTag reports whether this repository already has a release // for the given git tag. func (d *DB) ReleaseExistsForTag(ctx context.Context, repoID int64, tagName string) (bool, error) { var n int err := d.QueryRowContext(ctx, `SELECT COUNT(*) FROM releases WHERE repo_id = ? AND tag_name = ?`, repoID, tagName).Scan(&n) return n > 0, err } func (d *DB) DeleteRelease(ctx context.Context, id int64) error { _, err := d.ExecContext(ctx, `DELETE FROM releases WHERE id = ?`, id) return err } func (d *DB) DeleteReleases(ctx context.Context, ids []int64) error { if len(ids) == 0 { return nil } _, err := d.ExecContext(ctx, `DELETE FROM releases WHERE id IN (`+placeholders(len(ids))+`)`, int64Args(ids)...) return err } // ReleaseTag maps a git tag to its release, for the tag list view. type ReleaseTag struct { ID int64 TagName string } func (d *DB) ReleaseTags(ctx context.Context, repoID int64) ([]ReleaseTag, error) { rows, err := d.QueryContext(ctx, `SELECT id, tag_name FROM releases WHERE repo_id = ? AND tag_name IS NOT NULL`, repoID) if err != nil { return nil, err } defer rows.Close() var out []ReleaseTag for rows.Next() { var t ReleaseTag if err := rows.Scan(&t.ID, &t.TagName); err != nil { return nil, err } out = append(out, t) } return out, rows.Err() } func (d *DB) AllReleaseIDs(ctx context.Context) ([]int64, error) { return d.releaseIDs(ctx, `SELECT id FROM releases`) } // ReleaseIDsWithDirectory lists releases that must have an on-disk directory: // they carry source archives or at least one asset. func (d *DB) ReleaseIDsWithDirectory(ctx context.Context) ([]int64, error) { return d.releaseIDs(ctx, `SELECT releases.id FROM releases WHERE releases.include_source_code = 1 OR EXISTS (SELECT 1 FROM release_assets WHERE release_assets.release_id = releases.id)`) } func (d *DB) releaseIDs(ctx context.Context, query string) ([]int64, error) { rows, err := d.QueryContext(ctx, query) if err != nil { return nil, err } defer rows.Close() var out []int64 for rows.Next() { var id int64 if err := rows.Scan(&id); err != nil { return nil, err } out = append(out, id) } return out, rows.Err() }