this repo has no description
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 id integer primary key autoincrement,
108 owner_did text not null,
109 repo_at text not null,
110 pull_id integer not null,
111 title text not null,
112 patch text,
113 patch_at text not null,
114 open integer not null default 1,
115 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
116 unique(repo_at, pull_id),
117 foreign key (repo_at) references repos(at_uri) on delete cascade
118 );
119 create table if not exists pull_comments (
120 id integer primary key autoincrement,
121 owner_did text not null,
122 pull_id integer not null,
123 repo_at text not null,
124 comment_id integer not null,
125 comment_at text not null,
126 body text not null,
127 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
128 unique(pull_id, comment_id),
129 foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade
130 );
131 create table if not exists _jetstream (
132 id integer primary key autoincrement,
133 last_time_us integer not null
134 );
135
136 create table if not exists repo_issue_seqs (
137 repo_at text primary key,
138 next_issue_id integer not null default 1
139 );
140
141 create table if not exists repo_pull_seqs (
142 repo_at text primary key,
143 next_pull_id integer not null default 1
144 );
145
146 create table if not exists stars (
147 id integer primary key autoincrement,
148 starred_by_did text not null,
149 repo_at text not null,
150 rkey text not null,
151 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
152 foreign key (repo_at) references repos(at_uri) on delete cascade,
153 unique(starred_by_did, repo_at)
154 );
155
156 create table if not exists migrations (
157 id integer primary key autoincrement,
158 name text unique
159 )
160 `)
161 if err != nil {
162 return nil, err
163 }
164
165 // run migrations
166 runMigration(db, "add-description-to-repos", func(tx *sql.Tx) error {
167 tx.Exec(`
168 alter table repos add column description text check (length(description) <= 200);
169 `)
170 return nil
171 })
172
173 runMigration(db, "add-rkey-to-pubkeys", func(tx *sql.Tx) error {
174 // add unconstrained column
175 _, err := tx.Exec(`
176 alter table public_keys
177 add column rkey text;
178 `)
179 if err != nil {
180 return err
181 }
182
183 // backfill
184 _, err = tx.Exec(`
185 update public_keys
186 set rkey = ''
187 where rkey is null;
188 `)
189 if err != nil {
190 return err
191 }
192
193 return nil
194 })
195
196 return &DB{db}, nil
197}
198
199type migrationFn = func(*sql.Tx) error
200
201func runMigration(d *sql.DB, name string, migrationFn migrationFn) error {
202 tx, err := d.Begin()
203 if err != nil {
204 return err
205 }
206 defer tx.Rollback()
207
208 var exists bool
209 err = tx.QueryRow("select exists (select 1 from migrations where name = ?)", name).Scan(&exists)
210 if err != nil {
211 return err
212 }
213
214 if !exists {
215 // run migration
216 err = migrationFn(tx)
217 if err != nil {
218 log.Printf("Failed to run migration %s: %v", name, err)
219 return err
220 }
221
222 // mark migration as complete
223 _, err = tx.Exec("insert into migrations (name) values (?)", name)
224 if err != nil {
225 log.Printf("Failed to mark migration %s as complete: %v", name, err)
226 return err
227 }
228
229 // commit the transaction
230 if err := tx.Commit(); err != nil {
231 return err
232 }
233
234 log.Printf("migration %s applied successfully", name)
235 } else {
236 log.Printf("skipped migration %s, already applied", name)
237 }
238
239 return nil
240}