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