forked from tangled.org/core
this repo has no description
1package db 2 3import ( 4 "context" 5 "database/sql" 6 "fmt" 7 "log" 8 9 _ "github.com/mattn/go-sqlite3" 10) 11 12type DB struct { 13 *sql.DB 14} 15 16type Execer interface { 17 Query(query string, args ...any) (*sql.Rows, error) 18 QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) 19 QueryRow(query string, args ...any) *sql.Row 20 QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row 21 Exec(query string, args ...any) (sql.Result, error) 22 ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) 23 Prepare(query string) (*sql.Stmt, error) 24 PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) 25} 26 27func Make(dbPath string) (*DB, error) { 28 db, err := sql.Open("sqlite3", dbPath) 29 if err != nil { 30 return nil, err 31 } 32 _, err = db.Exec(` 33 pragma journal_mode = WAL; 34 pragma synchronous = normal; 35 pragma foreign_keys = on; 36 pragma temp_store = memory; 37 pragma mmap_size = 30000000000; 38 pragma page_size = 32768; 39 pragma auto_vacuum = incremental; 40 pragma busy_timeout = 5000; 41 42 create table if not exists registrations ( 43 id integer primary key autoincrement, 44 domain text not null unique, 45 did text not null, 46 secret text not null, 47 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 48 registered text 49 ); 50 create table if not exists public_keys ( 51 id integer primary key autoincrement, 52 did text not null, 53 name text not null, 54 key text not null, 55 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 56 unique(did, name, key) 57 ); 58 create table if not exists repos ( 59 id integer primary key autoincrement, 60 did text not null, 61 name text not null, 62 knot text not null, 63 rkey text not null, 64 at_uri text not null unique, 65 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 66 unique(did, name, knot, rkey) 67 ); 68 create table if not exists collaborators ( 69 id integer primary key autoincrement, 70 did text not null, 71 repo integer not null, 72 foreign key (repo) references repos(id) on delete cascade 73 ); 74 create table if not exists follows ( 75 user_did text not null, 76 subject_did text not null, 77 rkey text not null, 78 followed_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 79 primary key (user_did, subject_did), 80 check (user_did <> subject_did) 81 ); 82 create table if not exists issues ( 83 id integer primary key autoincrement, 84 owner_did text not null, 85 repo_at text not null, 86 issue_id integer not null, 87 title text not null, 88 body text not null, 89 open integer not null default 1, 90 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 91 issue_at text, 92 unique(repo_at, issue_id), 93 foreign key (repo_at) references repos(at_uri) on delete cascade 94 ); 95 create table if not exists comments ( 96 id integer primary key autoincrement, 97 owner_did text not null, 98 issue_id integer not null, 99 repo_at text not null, 100 comment_id integer not null, 101 comment_at text not null, 102 body text not null, 103 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 104 unique(issue_id, comment_id), 105 foreign key (repo_at, issue_id) references issues(repo_at, issue_id) on delete cascade 106 ); 107 create table if not exists pulls ( 108 -- identifiers 109 id integer primary key autoincrement, 110 pull_id integer not null, 111 112 -- at identifiers 113 repo_at text not null, 114 owner_did text not null, 115 rkey text not null, 116 pull_at text, 117 118 -- content 119 title text not null, 120 body text not null, 121 target_branch text not null, 122 state integer not null default 0 check (state in (0, 1, 2)), -- open, merged, closed 123 124 -- meta 125 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 126 127 -- constraints 128 unique(repo_at, pull_id), 129 foreign key (repo_at) references repos(at_uri) on delete cascade 130 ); 131 132 -- every pull must have atleast 1 submission: the initial submission 133 create table if not exists pull_submissions ( 134 -- identifiers 135 id integer primary key autoincrement, 136 pull_id integer not null, 137 138 -- at identifiers 139 repo_at text not null, 140 141 -- content, these are immutable, and require a resubmission to update 142 round_number integer not null default 0, 143 patch text, 144 145 -- meta 146 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 147 148 -- constraints 149 unique(repo_at, pull_id, round_number), 150 foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade 151 ); 152 153 create table if not exists pull_comments ( 154 -- identifiers 155 id integer primary key autoincrement, 156 pull_id integer not null, 157 submission_id integer not null, 158 159 -- at identifiers 160 repo_at text not null, 161 owner_did text not null, 162 comment_at text not null, 163 164 -- content 165 body text not null, 166 167 -- meta 168 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 169 170 -- constraints 171 foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade, 172 foreign key (submission_id) references pull_submissions(id) on delete cascade 173 ); 174 175 create table if not exists _jetstream ( 176 id integer primary key autoincrement, 177 last_time_us integer not null 178 ); 179 180 create table if not exists repo_issue_seqs ( 181 repo_at text primary key, 182 next_issue_id integer not null default 1 183 ); 184 185 create table if not exists repo_pull_seqs ( 186 repo_at text primary key, 187 next_pull_id integer not null default 1 188 ); 189 190 create table if not exists stars ( 191 id integer primary key autoincrement, 192 starred_by_did text not null, 193 repo_at text not null, 194 rkey text not null, 195 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 196 foreign key (repo_at) references repos(at_uri) on delete cascade, 197 unique(starred_by_did, repo_at) 198 ); 199 200 create table if not exists emails ( 201 id integer primary key autoincrement, 202 did text not null, 203 email text not null, 204 verified integer not null default 0, 205 verification_code text not null, 206 last_sent text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 207 is_primary integer not null default 0, 208 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 209 unique(did, email) 210 ); 211 212 create table if not exists artifacts ( 213 -- id 214 id integer primary key autoincrement, 215 did text not null, 216 rkey text not null, 217 218 -- meta 219 repo_at text not null, 220 tag binary(20) not null, 221 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 222 223 -- data 224 blob_cid text not null, 225 name text not null, 226 size integer not null default 0, 227 mimetype string not null default "*/*", 228 229 -- constraints 230 unique(did, rkey), -- record must be unique 231 unique(repo_at, tag, name), -- for a given tag object, each file must be unique 232 foreign key (repo_at) references repos(at_uri) on delete cascade 233 ); 234 235 create table if not exists profile ( 236 -- id 237 id integer primary key autoincrement, 238 did text not null, 239 240 -- data 241 description text not null, 242 include_bluesky integer not null default 0, 243 location text, 244 245 -- constraints 246 unique(did) 247 ); 248 create table if not exists profile_links ( 249 -- id 250 id integer primary key autoincrement, 251 did text not null, 252 253 -- data 254 link text not null, 255 256 -- constraints 257 foreign key (did) references profile(did) on delete cascade 258 ); 259 create table if not exists profile_stats ( 260 -- id 261 id integer primary key autoincrement, 262 did text not null, 263 264 -- data 265 kind text not null check (kind in ( 266 "merged-pull-request-count", 267 "closed-pull-request-count", 268 "open-pull-request-count", 269 "open-issue-count", 270 "closed-issue-count", 271 "repository-count" 272 )), 273 274 -- constraints 275 foreign key (did) references profile(did) on delete cascade 276 ); 277 create table if not exists profile_pinned_repositories ( 278 -- id 279 id integer primary key autoincrement, 280 did text not null, 281 282 -- data 283 at_uri text not null, 284 285 -- constraints 286 unique(did, at_uri), 287 foreign key (did) references profile(did) on delete cascade, 288 foreign key (at_uri) references repos(at_uri) on delete cascade 289 ); 290 291 create table if not exists migrations ( 292 id integer primary key autoincrement, 293 name text unique 294 ) 295 `) 296 if err != nil { 297 return nil, err 298 } 299 300 // run migrations 301 runMigration(db, "add-description-to-repos", func(tx *sql.Tx) error { 302 tx.Exec(` 303 alter table repos add column description text check (length(description) <= 200); 304 `) 305 return nil 306 }) 307 308 runMigration(db, "add-rkey-to-pubkeys", func(tx *sql.Tx) error { 309 // add unconstrained column 310 _, err := tx.Exec(` 311 alter table public_keys 312 add column rkey text; 313 `) 314 if err != nil { 315 return err 316 } 317 318 // backfill 319 _, err = tx.Exec(` 320 update public_keys 321 set rkey = '' 322 where rkey is null; 323 `) 324 if err != nil { 325 return err 326 } 327 328 return nil 329 }) 330 331 runMigration(db, "add-rkey-to-comments", func(tx *sql.Tx) error { 332 _, err := tx.Exec(` 333 alter table comments drop column comment_at; 334 alter table comments add column rkey text; 335 `) 336 return err 337 }) 338 339 runMigration(db, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error { 340 _, err := tx.Exec(` 341 alter table comments add column deleted text; -- timestamp 342 alter table comments add column edited text; -- timestamp 343 `) 344 return err 345 }) 346 347 runMigration(db, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error { 348 _, err := tx.Exec(` 349 alter table pulls add column source_branch text; 350 alter table pulls add column source_repo_at text; 351 alter table pull_submissions add column source_rev text; 352 `) 353 return err 354 }) 355 356 runMigration(db, "add-source-to-repos", func(tx *sql.Tx) error { 357 _, err := tx.Exec(` 358 alter table repos add column source text; 359 `) 360 return err 361 }) 362 363 return &DB{db}, nil 364} 365 366type migrationFn = func(*sql.Tx) error 367 368func runMigration(d *sql.DB, name string, migrationFn migrationFn) error { 369 tx, err := d.Begin() 370 if err != nil { 371 return err 372 } 373 defer tx.Rollback() 374 375 var exists bool 376 err = tx.QueryRow("select exists (select 1 from migrations where name = ?)", name).Scan(&exists) 377 if err != nil { 378 return err 379 } 380 381 if !exists { 382 // run migration 383 err = migrationFn(tx) 384 if err != nil { 385 log.Printf("Failed to run migration %s: %v", name, err) 386 return err 387 } 388 389 // mark migration as complete 390 _, err = tx.Exec("insert into migrations (name) values (?)", name) 391 if err != nil { 392 log.Printf("Failed to mark migration %s as complete: %v", name, err) 393 return err 394 } 395 396 // commit the transaction 397 if err := tx.Commit(); err != nil { 398 return err 399 } 400 401 log.Printf("migration %s applied successfully", name) 402 } else { 403 log.Printf("skipped migration %s, already applied", name) 404 } 405 406 return nil 407} 408 409type filter struct { 410 key string 411 arg any 412} 413 414func Filter(key string, arg any) filter { 415 return filter{ 416 key: key, 417 arg: arg, 418 } 419} 420 421func (f filter) Condition() string { 422 return fmt.Sprintf("%s = ?", f.key) 423}