-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathsqlite.go
More file actions
325 lines (306 loc) · 11.5 KB
/
Copy pathsqlite.go
File metadata and controls
325 lines (306 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
package patchbin
import (
"fmt"
"log/slog"
"github.com/jmoiron/sqlx"
_ "modernc.org/sqlite"
)
var sqliteSchema = `
CREATE TABLE IF NOT EXISTS app_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pubkey TEXT NOT NULL UNIQUE,
name TEXT NOT NULL UNIQUE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS acl (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pubkey string,
ip_address string,
permission string NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS patch_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
repo_name TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL,
text TEXT NOT NULL,
status TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL,
last_activity DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT pr_user_id_fk
FOREIGN KEY(user_id) REFERENCES app_users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
CREATE TABLE IF NOT EXISTS patchsets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
patch_request_id INTEGER NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT patchset_user_id_fk
FOREIGN KEY(user_id) REFERENCES app_users(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT patchset_patch_request_id_fk
FOREIGN KEY(patch_request_id) REFERENCES patch_requests(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
CREATE TABLE IF NOT EXISTS patches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
patchset_id INTEGER NOT NULL,
author_name TEXT NOT NULL,
author_email TEXT NOT NULL,
author_date DATETIME NOT NULL,
title TEXT NOT NULL,
body TEXT NOT NULL,
body_appendix TEXT NOT NULL,
commit_sha TEXT NOT NULL,
content_sha TEXT NOT NULL,
raw_text TEXT NOT NULL,
base_commit_sha TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT patches_user_id_fk
FOREIGN KEY(user_id) REFERENCES app_users(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT patches_patchset_id_fk
FOREIGN KEY(patchset_id) REFERENCES patchsets(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
CREATE TABLE IF NOT EXISTS event_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
patch_request_id INTEGER,
patchset_id INTEGER,
event TEXT NOT NULL,
data TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT event_logs_pr_id_fk
FOREIGN KEY(patch_request_id) REFERENCES patch_requests(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT event_logs_patchset_id_fk
FOREIGN KEY(patchset_id) REFERENCES patchsets(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT event_logs_user_id_fk
FOREIGN KEY(user_id) REFERENCES app_users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_patch_requests_last_activity ON patch_requests(last_activity);
`
var sqliteMigrations = []string{
"", // migration #0 is reserved for schema initialization
"ALTER TABLE patches ADD COLUMN base_commit_sha TEXT",
// added this by accident
"",
// create repos table
`CREATE TABLE IF NOT EXISTS repos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (user_id, name),
CONSTRAINT repo_user_id_fk
FOREIGN KEY(user_id) REFERENCES app_users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);`,
// migrate existing repo info from patch_requests
`INSERT INTO repos (user_id, name) SELECT user_id, repo_id from patch_requests group by repo_id;`,
// convert patch_requests.repo_id to integer with FK constraint
`CREATE TABLE IF NOT EXISTS tmp_patch_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
repo_id INTEGER NOT NULL,
name TEXT NOT NULL,
text TEXT NOT NULL,
status TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL,
CONSTRAINT pr_user_id_fk
FOREIGN KEY(user_id) REFERENCES app_users(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT pr_repo_id_fk
FOREIGN KEY(repo_id) REFERENCES repos(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
INSERT INTO tmp_patch_requests (user_id, repo_id, name, text, status, created_at, updated_at)
SELECT pr.user_id, repos.id, pr.name, pr.text, pr.status, pr.created_at, pr.updated_at
FROM patch_requests AS pr
INNER JOIN repos ON repos.name = pr.repo_id;
DROP TABLE patch_requests;
ALTER TABLE tmp_patch_requests RENAME TO patch_requests;`,
// convert event_logs.repo_id to integer with FK constraint
`CREATE TABLE IF NOT EXISTS tmp_event_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
repo_id INTEGER,
patch_request_id INTEGER,
patchset_id INTEGER,
event TEXT NOT NULL,
data TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT event_logs_pr_id_fk
FOREIGN KEY(patch_request_id) REFERENCES patch_requests(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT event_logs_patchset_id_fk
FOREIGN KEY(patchset_id) REFERENCES patchsets(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT event_logs_user_id_fk
FOREIGN KEY(user_id) REFERENCES app_users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
CONSTRAINT event_logs_repo_id_fk
FOREIGN KEY(repo_id) REFERENCES repos(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
INSERT INTO tmp_event_logs (user_id, repo_id, patch_request_id, patchset_id, event, data, created_at)
SELECT ev.user_id, repos.id, ev.patch_request_id, ev.patchset_id, ev.event, ev.data, ev.created_at
FROM event_logs AS ev
LEFT JOIN repos ON repos.name = ev.repo_id;
DROP TABLE event_logs;
ALTER TABLE tmp_event_logs RENAME TO event_logs;`,
// Phase 1: Add repo_name column to patch_requests
`ALTER TABLE patch_requests ADD COLUMN repo_name TEXT`,
// Phase 1: Populate repo_name from existing repos table
`UPDATE patch_requests SET repo_name = (SELECT name FROM repos WHERE id = patch_requests.repo_id)`,
// Phase 1: Remove patch_requests whose repo no longer exists. These are
// orphans from repo deletion, since ON DELETE CASCADE never fired
// because PRAGMA foreign_keys was never enabled.
`DELETE FROM event_logs WHERE patch_request_id IN (SELECT id FROM patch_requests WHERE repo_name IS NULL);
DELETE FROM patches WHERE patchset_id IN (SELECT id FROM patchsets WHERE patch_request_id IN (SELECT id FROM patch_requests WHERE repo_name IS NULL));
DELETE FROM patchsets WHERE patch_request_id IN (SELECT id FROM patch_requests WHERE repo_name IS NULL);
DELETE FROM patch_requests WHERE repo_name IS NULL;`,
// Phase 1: Add last_activity column to patch_requests
`ALTER TABLE patch_requests ADD COLUMN last_activity DATETIME`,
// Phase 1: Set initial last_activity values from event_logs
`UPDATE patch_requests SET last_activity = (SELECT MAX(created_at) FROM event_logs WHERE patch_request_id = patch_requests.id) WHERE last_activity IS NULL`,
// Phase 1: Set last_activity to created_at for PRs with no events
`UPDATE patch_requests SET last_activity = created_at WHERE last_activity IS NULL`,
// Phase 1: Create index on last_activity for fast filtering
`CREATE INDEX IF NOT EXISTS idx_patch_requests_last_activity ON patch_requests(last_activity)`,
// Phase 2: Drop repos table (no longer needed, repo_name is stored directly)
`DROP TABLE IF EXISTS repos`,
// Phase 2: Rebuild patch_requests without repo_id (SQLite can't drop a
// column that's part of a foreign key constraint via ALTER TABLE).
`CREATE TABLE tmp_patch_requests_v2 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
repo_name TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL,
text TEXT NOT NULL,
status TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL,
last_activity DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT pr_user_id_fk
FOREIGN KEY(user_id) REFERENCES app_users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
INSERT INTO tmp_patch_requests_v2 (id, user_id, repo_name, name, text, status, created_at, updated_at, last_activity)
SELECT id, user_id, repo_name, name, text, status, created_at, updated_at, last_activity
FROM patch_requests;
DROP TABLE patch_requests;
ALTER TABLE tmp_patch_requests_v2 RENAME TO patch_requests;
CREATE INDEX IF NOT EXISTS idx_patch_requests_last_activity ON patch_requests(last_activity);`,
// Phase 2: Rebuild event_logs without repo_id, same reasoning as above.
`CREATE TABLE tmp_event_logs_v2 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
patch_request_id INTEGER,
patchset_id INTEGER,
event TEXT NOT NULL,
data TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT event_logs_pr_id_fk
FOREIGN KEY(patch_request_id) REFERENCES patch_requests(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT event_logs_patchset_id_fk
FOREIGN KEY(patchset_id) REFERENCES patchsets(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT event_logs_user_id_fk
FOREIGN KEY(user_id) REFERENCES app_users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
INSERT INTO tmp_event_logs_v2 (id, user_id, patch_request_id, patchset_id, event, data, created_at)
SELECT id, user_id, patch_request_id, patchset_id, event, data, created_at
FROM event_logs;
DROP TABLE event_logs;
ALTER TABLE tmp_event_logs_v2 RENAME TO event_logs;`,
// Phase 2: Collapse legacy statuses (closed, accepted, reviewed) into
// open, since the new model only has draft and open.
`UPDATE patch_requests SET status = 'open' WHERE status NOT IN ('draft', 'open')`,
// Delete patch requests with an empty title. These come from patchsets
// whose first patch had no subject line and are unusable in the UI.
`DELETE FROM event_logs WHERE patch_request_id IN (SELECT id FROM patch_requests WHERE trim(name) = '');
DELETE FROM patches WHERE patchset_id IN (SELECT id FROM patchsets WHERE patch_request_id IN (SELECT id FROM patch_requests WHERE trim(name) = ''));
DELETE FROM patchsets WHERE patch_request_id IN (SELECT id FROM patch_requests WHERE trim(name) = '');
DELETE FROM patch_requests WHERE trim(name) = '';`,
}
// Open opens a database connection.
func SqliteOpen(dsn string, logger *slog.Logger) (*sqlx.DB, error) {
logger.Info("opening db file", "dsn", dsn)
db, err := sqlx.Connect("sqlite", dsn)
if err != nil {
return nil, err
}
err = sqliteUpgrade(db)
if err != nil {
_ = db.Close()
return nil, err
}
return db, nil
}
func sqliteUpgrade(db *sqlx.DB) error {
var version int
if err := db.QueryRow("PRAGMA user_version").Scan(&version); err != nil {
return fmt.Errorf("failed to query schema version: %v", err)
}
if version == len(sqliteMigrations) {
return nil
} else if version > len(sqliteMigrations) {
return fmt.Errorf("patchbin (version %d) older than schema (version %d)", len(sqliteMigrations), version)
}
tx, err := db.Beginx()
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
if version == 0 {
if _, err := tx.Exec(sqliteSchema); err != nil {
return fmt.Errorf("failed to initialize schema: %v", err)
}
} else {
for i := version; i < len(sqliteMigrations); i++ {
if _, err := tx.Exec(sqliteMigrations[i]); err != nil {
return fmt.Errorf("failed to execute migration #%v: %v", i, err)
}
}
}
// For some reason prepared statements don't work here
_, err = tx.Exec(fmt.Sprintf("PRAGMA user_version = %d", len(sqliteMigrations)))
if err != nil {
return fmt.Errorf("failed to bump schema version: %v", err)
}
return tx.Commit()
}