releases.go
| 1 | package db |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "database/sql" |
| 6 | "errors" |
| 7 | ) |
| 8 | |
| 9 | type Release struct { |
| 10 | ID int64 |
| 11 | RepoID int64 |
| 12 | TagName *string |
| 13 | Name string |
| 14 | Notes *string |
| 15 | IncludeSourceCode bool |
| 16 | CreatedAt string |
| 17 | } |
| 18 | |
| 19 | type ReleaseAsset struct { |
| 20 | ID int64 |
| 21 | ReleaseID int64 |
| 22 | Filename string |
| 23 | Size int64 |
| 24 | CreatedAt string |
| 25 | } |
| 26 | |
| 27 | // NewAsset describes a file already written to disk that still needs a row. |
| 28 | type NewAsset struct { |
| 29 | Filename string |
| 30 | Size int64 |
| 31 | } |
| 32 | |
| 33 | const releaseColumns = `id, repo_id, tag_name, name, notes, include_source_code, created_at` |
| 34 | |
| 35 | func scanRelease(s rowScanner) (*Release, error) { |
| 36 | var r Release |
| 37 | err := s.Scan(&r.ID, &r.RepoID, &r.TagName, &r.Name, &r.Notes, &r.IncludeSourceCode, &r.CreatedAt) |
| 38 | if errors.Is(err, sql.ErrNoRows) { |
| 39 | return nil, nil |
| 40 | } |
| 41 | if err != nil { |
| 42 | return nil, err |
| 43 | } |
| 44 | return &r, nil |
| 45 | } |
| 46 | |
| 47 | func (d *DB) CountReleases(ctx context.Context, repoID int64) (int, error) { |
| 48 | var n int |
| 49 | err := d.QueryRowContext(ctx, `SELECT COUNT(*) FROM releases WHERE repo_id = ?`, repoID).Scan(&n) |
| 50 | return n, err |
| 51 | } |
| 52 | |
| 53 | func (d *DB) ListReleases(ctx context.Context, repoID int64, limit, offset int) ([]Release, error) { |
| 54 | rows, err := d.QueryContext(ctx, |
| 55 | `SELECT `+releaseColumns+` FROM releases WHERE repo_id = ? ORDER BY id DESC LIMIT ? OFFSET ?`, |
| 56 | repoID, limit, offset) |
| 57 | if err != nil { |
| 58 | return nil, err |
| 59 | } |
| 60 | defer rows.Close() |
| 61 | var out []Release |
| 62 | for rows.Next() { |
| 63 | r, err := scanRelease(rows) |
| 64 | if err != nil { |
| 65 | return nil, err |
| 66 | } |
| 67 | out = append(out, *r) |
| 68 | } |
| 69 | return out, rows.Err() |
| 70 | } |
| 71 | |
| 72 | func (d *DB) ReleaseInRepo(ctx context.Context, repoID, id int64) (*Release, error) { |
| 73 | return scanRelease(d.QueryRowContext(ctx, |
| 74 | `SELECT `+releaseColumns+` FROM releases WHERE repo_id = ? AND id = ?`, repoID, id)) |
| 75 | } |
| 76 | |
| 77 | // AssetCounts returns the number of assets per release for a list view. |
| 78 | func (d *DB) AssetCounts(ctx context.Context, releaseIDs []int64) (map[int64]int, error) { |
| 79 | out := map[int64]int{} |
| 80 | if len(releaseIDs) == 0 { |
| 81 | return out, nil |
| 82 | } |
| 83 | rows, err := d.QueryContext(ctx, |
| 84 | `SELECT release_id, COUNT(*) FROM release_assets |
| 85 | WHERE release_id IN (`+placeholders(len(releaseIDs))+`) GROUP BY release_id`, |
| 86 | int64Args(releaseIDs)...) |
| 87 | if err != nil { |
| 88 | return nil, err |
| 89 | } |
| 90 | defer rows.Close() |
| 91 | for rows.Next() { |
| 92 | var id int64 |
| 93 | var n int |
| 94 | if err := rows.Scan(&id, &n); err != nil { |
| 95 | return nil, err |
| 96 | } |
| 97 | out[id] = n |
| 98 | } |
| 99 | return out, rows.Err() |
| 100 | } |
| 101 | |
| 102 | func (d *DB) ListReleaseAssets(ctx context.Context, releaseID int64) ([]ReleaseAsset, error) { |
| 103 | rows, err := d.QueryContext(ctx, |
| 104 | `SELECT id, release_id, filename, size, created_at FROM release_assets |
| 105 | WHERE release_id = ? ORDER BY id ASC`, releaseID) |
| 106 | if err != nil { |
| 107 | return nil, err |
| 108 | } |
| 109 | defer rows.Close() |
| 110 | var out []ReleaseAsset |
| 111 | for rows.Next() { |
| 112 | var a ReleaseAsset |
| 113 | if err := rows.Scan(&a.ID, &a.ReleaseID, &a.Filename, &a.Size, &a.CreatedAt); err != nil { |
| 114 | return nil, err |
| 115 | } |
| 116 | out = append(out, a) |
| 117 | } |
| 118 | return out, rows.Err() |
| 119 | } |
| 120 | |
| 121 | func (d *DB) ReleaseAssetByFilename(ctx context.Context, releaseID int64, filename string) (*ReleaseAsset, error) { |
| 122 | var a ReleaseAsset |
| 123 | err := d.QueryRowContext(ctx, |
| 124 | `SELECT id, release_id, filename, size, created_at FROM release_assets |
| 125 | WHERE release_id = ? AND filename = ?`, |
| 126 | releaseID, filename).Scan(&a.ID, &a.ReleaseID, &a.Filename, &a.Size, &a.CreatedAt) |
| 127 | if errors.Is(err, sql.ErrNoRows) { |
| 128 | return nil, nil |
| 129 | } |
| 130 | if err != nil { |
| 131 | return nil, err |
| 132 | } |
| 133 | return &a, nil |
| 134 | } |
| 135 | |
| 136 | // CreateRelease inserts the release row and returns its id. The caller writes |
| 137 | // the release directory afterwards and deletes the row when that fails. File |
| 138 | // work must not run inside a transaction: SQLite here has one connection, so |
| 139 | // a long write would block every other request. |
| 140 | func (d *DB) CreateRelease(ctx context.Context, repoID int64, tagName *string, name string, |
| 141 | notes *string, includeSource bool, now string, |
| 142 | ) (int64, error) { |
| 143 | res, err := d.ExecContext(ctx, |
| 144 | `INSERT INTO releases (repo_id, tag_name, name, notes, include_source_code, created_at) |
| 145 | VALUES (?, ?, ?, ?, ?, ?)`, |
| 146 | repoID, tagName, name, notes, includeSource, now) |
| 147 | if err != nil { |
| 148 | return 0, err |
| 149 | } |
| 150 | return res.LastInsertId() |
| 151 | } |
| 152 | |
| 153 | // AddReleaseAssets records the files the caller already wrote to disk. |
| 154 | func (d *DB) AddReleaseAssets(ctx context.Context, releaseID int64, assets []NewAsset, now string) error { |
| 155 | if len(assets) == 0 { |
| 156 | return nil |
| 157 | } |
| 158 | tx, err := d.BeginTx(ctx, nil) |
| 159 | if err != nil { |
| 160 | return err |
| 161 | } |
| 162 | defer tx.Rollback() |
| 163 | for _, a := range assets { |
| 164 | if _, err := tx.ExecContext(ctx, |
| 165 | `INSERT INTO release_assets (release_id, filename, size, created_at) VALUES (?, ?, ?, ?)`, |
| 166 | releaseID, a.Filename, a.Size, now); err != nil { |
| 167 | return err |
| 168 | } |
| 169 | } |
| 170 | return tx.Commit() |
| 171 | } |
| 172 | |
| 173 | // ReleaseExistsForTag reports whether this repository already has a release |
| 174 | // for the given git tag. |
| 175 | func (d *DB) ReleaseExistsForTag(ctx context.Context, repoID int64, tagName string) (bool, error) { |
| 176 | var n int |
| 177 | err := d.QueryRowContext(ctx, |
| 178 | `SELECT COUNT(*) FROM releases WHERE repo_id = ? AND tag_name = ?`, repoID, tagName).Scan(&n) |
| 179 | return n > 0, err |
| 180 | } |
| 181 | |
| 182 | func (d *DB) DeleteRelease(ctx context.Context, id int64) error { |
| 183 | _, err := d.ExecContext(ctx, `DELETE FROM releases WHERE id = ?`, id) |
| 184 | return err |
| 185 | } |
| 186 | |
| 187 | func (d *DB) DeleteReleases(ctx context.Context, ids []int64) error { |
| 188 | if len(ids) == 0 { |
| 189 | return nil |
| 190 | } |
| 191 | _, err := d.ExecContext(ctx, |
| 192 | `DELETE FROM releases WHERE id IN (`+placeholders(len(ids))+`)`, int64Args(ids)...) |
| 193 | return err |
| 194 | } |
| 195 | |
| 196 | // ReleaseTag maps a git tag to its release, for the tag list view. |
| 197 | type ReleaseTag struct { |
| 198 | ID int64 |
| 199 | TagName string |
| 200 | } |
| 201 | |
| 202 | func (d *DB) ReleaseTags(ctx context.Context, repoID int64) ([]ReleaseTag, error) { |
| 203 | rows, err := d.QueryContext(ctx, |
| 204 | `SELECT id, tag_name FROM releases WHERE repo_id = ? AND tag_name IS NOT NULL`, repoID) |
| 205 | if err != nil { |
| 206 | return nil, err |
| 207 | } |
| 208 | defer rows.Close() |
| 209 | var out []ReleaseTag |
| 210 | for rows.Next() { |
| 211 | var t ReleaseTag |
| 212 | if err := rows.Scan(&t.ID, &t.TagName); err != nil { |
| 213 | return nil, err |
| 214 | } |
| 215 | out = append(out, t) |
| 216 | } |
| 217 | return out, rows.Err() |
| 218 | } |
| 219 | |
| 220 | func (d *DB) AllReleaseIDs(ctx context.Context) ([]int64, error) { |
| 221 | return d.releaseIDs(ctx, `SELECT id FROM releases`) |
| 222 | } |
| 223 | |
| 224 | // ReleaseIDsWithDirectory lists releases that must have an on-disk directory: |
| 225 | // they carry source archives or at least one asset. |
| 226 | func (d *DB) ReleaseIDsWithDirectory(ctx context.Context) ([]int64, error) { |
| 227 | return d.releaseIDs(ctx, |
| 228 | `SELECT releases.id FROM releases |
| 229 | WHERE releases.include_source_code = 1 |
| 230 | OR EXISTS (SELECT 1 FROM release_assets WHERE release_assets.release_id = releases.id)`) |
| 231 | } |
| 232 | |
| 233 | func (d *DB) releaseIDs(ctx context.Context, query string) ([]int64, error) { |
| 234 | rows, err := d.QueryContext(ctx, query) |
| 235 | if err != nil { |
| 236 | return nil, err |
| 237 | } |
| 238 | defer rows.Close() |
| 239 | var out []int64 |
| 240 | for rows.Next() { |
| 241 | var id int64 |
| 242 | if err := rows.Scan(&id); err != nil { |
| 243 | return nil, err |
| 244 | } |
| 245 | out = append(out, id) |
| 246 | } |
| 247 | return out, rows.Err() |
| 248 | } |
| 249 |