releases.go
⎇
Raw
1package db
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7)
8
9type 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
19type 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.
28type NewAsset struct {
29 Filename string
30 Size int64
31}
32
33const releaseColumns = `id, repo_id, tag_name, name, notes, include_source_code, created_at`
34
35func 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
47func (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
53func (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
72func (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.
78func (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
102func (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
121func (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.
140func (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.
154func (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.
175func (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
182func (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
187func (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.
197type ReleaseTag struct {
198 ID int64
199 TagName string
200}
201
202func (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
220func (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.
226func (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
233func (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