1use std::sync::LazyLock;
5
6pub const SCHEMA: &str = "\
10CREATE TABLE IF NOT EXISTS users (
11 id TEXT PRIMARY KEY,
12 nickname TEXT NOT NULL UNIQUE,
13 api_key TEXT NOT NULL UNIQUE,
14 is_admin BOOLEAN NOT NULL DEFAULT 0,
15 created_at TEXT NOT NULL DEFAULT (datetime('now'))
16);
17CREATE TABLE IF NOT EXISTS teams (
18 id TEXT PRIMARY KEY,
19 name TEXT NOT NULL,
20 description TEXT,
21 is_public BOOLEAN NOT NULL DEFAULT 0,
22 created_by TEXT NOT NULL,
23 created_at TEXT NOT NULL DEFAULT (datetime('now'))
24);
25CREATE TABLE IF NOT EXISTS team_members (
26 team_id TEXT NOT NULL,
27 user_id TEXT NOT NULL,
28 role TEXT NOT NULL DEFAULT 'member',
29 joined_at TEXT NOT NULL DEFAULT (datetime('now')),
30 PRIMARY KEY (team_id, user_id)
31);
32CREATE TABLE IF NOT EXISTS sessions (
33 id TEXT PRIMARY KEY,
34 user_id TEXT,
35 team_id TEXT NOT NULL,
36 tool TEXT NOT NULL,
37 agent_provider TEXT,
38 agent_model TEXT,
39 title TEXT,
40 description TEXT,
41 tags TEXT,
42 created_at TEXT NOT NULL,
43 uploaded_at TEXT NOT NULL DEFAULT (datetime('now')),
44 message_count INTEGER DEFAULT 0,
45 task_count INTEGER DEFAULT 0,
46 event_count INTEGER DEFAULT 0,
47 duration_seconds INTEGER DEFAULT 0,
48 total_input_tokens INTEGER DEFAULT 0,
49 total_output_tokens INTEGER DEFAULT 0,
50 body_storage_key TEXT NOT NULL
51);
52";
53
54pub const SESSION_LINKS_SCHEMA: &str = "\
56CREATE TABLE IF NOT EXISTS session_links (
57 session_id TEXT NOT NULL,
58 linked_session_id TEXT NOT NULL,
59 link_type TEXT NOT NULL DEFAULT 'handoff',
60 created_at TEXT NOT NULL DEFAULT (datetime('now')),
61 PRIMARY KEY (session_id, linked_session_id)
62);
63CREATE INDEX IF NOT EXISTS idx_session_links_linked ON session_links(linked_session_id);
64";
65
66pub const LOCAL_SCHEMA: &str = "\
68CREATE TABLE IF NOT EXISTS local_sessions (
69 id TEXT PRIMARY KEY,
70 source_path TEXT,
71 sync_status TEXT NOT NULL DEFAULT 'local_only',
72 last_synced_at TEXT,
73 user_id TEXT,
74 nickname TEXT,
75 team_id TEXT,
76 tool TEXT NOT NULL,
77 agent_provider TEXT,
78 agent_model TEXT,
79 title TEXT,
80 description TEXT,
81 tags TEXT,
82 created_at TEXT NOT NULL,
83 uploaded_at TEXT,
84 message_count INTEGER DEFAULT 0,
85 task_count INTEGER DEFAULT 0,
86 event_count INTEGER DEFAULT 0,
87 duration_seconds INTEGER DEFAULT 0,
88 total_input_tokens INTEGER DEFAULT 0,
89 total_output_tokens INTEGER DEFAULT 0,
90 git_remote TEXT,
91 git_branch TEXT,
92 git_commit TEXT,
93 git_repo_name TEXT,
94 pr_number INTEGER,
95 pr_url TEXT,
96 working_directory TEXT
97);
98CREATE TABLE IF NOT EXISTS sync_cursors (
99 team_id TEXT NOT NULL,
100 cursor TEXT NOT NULL,
101 updated_at TEXT NOT NULL DEFAULT (datetime('now')),
102 PRIMARY KEY (team_id)
103);
104CREATE TABLE IF NOT EXISTS body_cache (
105 session_id TEXT PRIMARY KEY,
106 body BLOB,
107 cached_at TEXT NOT NULL DEFAULT (datetime('now'))
108);
109";
110
111pub const SESSION_COLUMNS: &str = "\
115s.id, s.user_id, u.nickname, s.team_id, s.tool, \
116s.agent_provider, s.agent_model, s.title, s.description, s.tags, \
117s.created_at, s.uploaded_at, s.message_count, s.task_count, \
118s.event_count, s.duration_seconds, \
119s.total_input_tokens, s.total_output_tokens";
120
121pub const TEAM_COLUMNS: &str = "\
123t.id, t.name, t.description, t.is_public, t.created_by, t.created_at";
124
125pub const MEMBER_COLUMNS: &str = "\
127tm.user_id, u.nickname, tm.role, tm.joined_at";
128
129pub static SESSION_LIST_BASE: LazyLock<String> = LazyLock::new(|| {
133 format!(
134 "SELECT {SESSION_COLUMNS} \
135 FROM sessions s LEFT JOIN users u ON u.id = s.user_id"
136 )
137});
138
139pub static SESSION_GET: LazyLock<String> = LazyLock::new(|| {
141 format!(
142 "SELECT {SESSION_COLUMNS} \
143 FROM sessions s LEFT JOIN users u ON u.id = s.user_id \
144 WHERE s.id = ?1"
145 )
146});
147
148pub const SESSION_INSERT: &str = "\
150INSERT INTO sessions \
151(id, user_id, team_id, tool, agent_provider, agent_model, \
152 title, description, tags, created_at, \
153 message_count, task_count, event_count, duration_seconds, \
154 total_input_tokens, total_output_tokens, body_storage_key) \
155VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17)";
156
157pub static TEAM_LIST_MY: LazyLock<String> = LazyLock::new(|| {
159 format!(
160 "SELECT {TEAM_COLUMNS} FROM teams t \
161 INNER JOIN team_members tm ON tm.team_id = t.id \
162 WHERE tm.user_id = ?1 ORDER BY t.created_at DESC"
163 )
164});
165
166pub static TEAM_GET: LazyLock<String> =
168 LazyLock::new(|| format!("SELECT {TEAM_COLUMNS} FROM teams t WHERE t.id = ?1"));
169
170pub const TEAM_INSERT: &str = "\
172INSERT INTO teams (id, name, description, is_public, created_by) \
173VALUES (?1, ?2, ?3, ?4, ?5)";
174
175pub const MEMBER_INSERT: &str = "\
177INSERT INTO team_members (team_id, user_id, role) VALUES (?1, ?2, ?3)";
178
179pub const MEMBER_DELETE: &str = "\
181DELETE FROM team_members WHERE team_id = ?1 AND user_id = ?2";
182
183pub static MEMBER_LIST: LazyLock<String> = LazyLock::new(|| {
185 format!(
186 "SELECT {MEMBER_COLUMNS} \
187 FROM team_members tm \
188 INNER JOIN users u ON u.id = tm.user_id \
189 WHERE tm.team_id = ?1 \
190 ORDER BY tm.joined_at ASC"
191 )
192});
193
194pub const SESSION_LINK_INSERT: &str = "\
198INSERT OR IGNORE INTO session_links (session_id, linked_session_id, link_type) \
199VALUES (?1, ?2, ?3)";
200
201pub const SESSION_LINKS_BY_SESSION: &str = "\
203SELECT session_id, linked_session_id, link_type, created_at \
204FROM session_links \
205WHERE session_id = ?1 OR linked_session_id = ?1";
206
207pub const USER_BY_API_KEY: &str = "\
211SELECT id, nickname, is_admin, email FROM users WHERE api_key = ?1";
212
213pub const USER_BY_ID: &str = "\
215SELECT id, nickname, is_admin, email FROM users WHERE id = ?1";
216
217pub const USER_BY_EMAIL_FOR_LOGIN: &str = "\
219SELECT id, nickname, password_hash, password_salt FROM users WHERE email = ?1";
220
221pub const USER_EMAIL_EXISTS: &str = "\
223SELECT COUNT(*) > 0 FROM users WHERE email = ?1";
224
225pub const USER_COUNT: &str = "SELECT COUNT(*) FROM users";
227
228pub const USER_INSERT_WITH_EMAIL: &str = "\
230INSERT INTO users (id, nickname, api_key, is_admin, email, password_hash, password_salt) \
231VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)";
232
233pub const USER_INSERT: &str = "\
235INSERT INTO users (id, nickname, api_key, is_admin) VALUES (?1, ?2, ?3, ?4)";
236
237pub const REFRESH_TOKEN_INSERT: &str = "\
239INSERT INTO refresh_tokens (id, user_id, token_hash, expires_at) VALUES (?1, ?2, ?3, ?4)";
240
241pub const REFRESH_TOKEN_LOOKUP: &str = "\
243SELECT rt.id, rt.user_id, rt.expires_at, u.nickname \
244FROM refresh_tokens rt JOIN users u ON u.id = rt.user_id \
245WHERE rt.token_hash = ?1";
246
247pub const REFRESH_TOKEN_DELETE: &str = "\
249DELETE FROM refresh_tokens WHERE token_hash = ?1";
250
251pub const USER_PASSWORD_FIELDS: &str = "\
253SELECT password_hash, password_salt FROM users WHERE id = ?1";
254
255pub const USER_UPDATE_PASSWORD: &str = "\
257UPDATE users SET password_hash = ?1, password_salt = ?2 WHERE id = ?3";
258
259pub const USER_UPDATE_API_KEY: &str = "\
261UPDATE users SET api_key = ?1 WHERE id = ?2";
262
263pub const USER_SETTINGS_FIELDS: &str = "\
265SELECT api_key, created_at FROM users WHERE id = ?1";
266
267pub const USER_EMAIL_AVATAR: &str = "\
269SELECT email, \
270(SELECT avatar_url FROM oauth_identities WHERE user_id = ?1 LIMIT 1) \
271FROM users WHERE id = ?1";
272
273pub const OAUTH_IDENTITIES_SCHEMA: &str = "\
277CREATE TABLE IF NOT EXISTS oauth_identities (
278 user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
279 provider TEXT NOT NULL,
280 provider_user_id TEXT NOT NULL,
281 provider_username TEXT,
282 avatar_url TEXT,
283 instance_url TEXT,
284 created_at TEXT NOT NULL DEFAULT (datetime('now')),
285 PRIMARY KEY (provider, provider_user_id),
286 UNIQUE (user_id, provider)
287);
288";
289
290pub const OAUTH_STATES_SCHEMA: &str = "\
292CREATE TABLE IF NOT EXISTS oauth_states (
293 state TEXT PRIMARY KEY,
294 provider TEXT NOT NULL,
295 created_at TEXT NOT NULL DEFAULT (datetime('now')),
296 expires_at TEXT NOT NULL,
297 user_id TEXT
298);
299";
300
301pub const REFRESH_TOKENS_SCHEMA: &str = "\
303CREATE TABLE IF NOT EXISTS refresh_tokens (
304 id TEXT PRIMARY KEY,
305 user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
306 token_hash TEXT NOT NULL UNIQUE,
307 expires_at TEXT NOT NULL,
308 created_at TEXT NOT NULL DEFAULT (datetime('now'))
309);
310";
311
312pub const OAUTH_IDENTITY_UPSERT: &str = "\
314INSERT INTO oauth_identities (user_id, provider, provider_user_id, provider_username, avatar_url, instance_url) \
315VALUES (?1, ?2, ?3, ?4, ?5, ?6) \
316ON CONFLICT (provider, provider_user_id) DO UPDATE SET \
317 provider_username = excluded.provider_username, \
318 avatar_url = excluded.avatar_url";
319
320pub const OAUTH_IDENTITY_FIND_BY_PROVIDER: &str = "\
322SELECT user_id, provider, provider_user_id, provider_username, avatar_url, instance_url \
323FROM oauth_identities WHERE provider = ?1 AND provider_user_id = ?2";
324
325pub const OAUTH_IDENTITY_FIND_BY_USER: &str = "\
327SELECT user_id, provider, provider_user_id, provider_username, avatar_url, instance_url \
328FROM oauth_identities WHERE user_id = ?1";
329
330pub const OAUTH_STATE_INSERT: &str = "\
332INSERT INTO oauth_states (state, provider, expires_at, user_id) VALUES (?1, ?2, ?3, ?4)";
333
334pub const OAUTH_STATE_VALIDATE: &str = "\
336SELECT state, provider, expires_at, user_id FROM oauth_states WHERE state = ?1";
337
338pub const OAUTH_STATE_DELETE: &str = "\
340DELETE FROM oauth_states WHERE state = ?1";
341
342pub const TEAM_MEMBER_EXISTS: &str = "\
346SELECT COUNT(*) FROM team_members WHERE team_id = ?1 AND user_id = ?2";
347
348pub const TEAM_MEMBER_ROLE: &str = "\
350SELECT role FROM team_members WHERE team_id = ?1 AND user_id = ?2";
351
352pub const TEAM_EXISTS: &str = "\
354SELECT COUNT(*) FROM teams WHERE id = ?1";
355
356pub const TEAM_MEMBER_COUNT: &str = "\
358SELECT COUNT(*) FROM team_members WHERE team_id = ?1";
359
360pub const USER_BY_NICKNAME: &str = "\
362SELECT id FROM users WHERE nickname = ?1";
363
364pub const TEAM_CREATED_AT: &str = "\
366SELECT created_at FROM teams WHERE id = ?1";
367
368pub const MEMBER_JOINED_AT: &str = "\
370SELECT joined_at FROM team_members WHERE team_id = ?1 AND user_id = ?2";
371
372pub const TEAM_NAME_BY_ID: &str = "\
374SELECT name FROM teams WHERE id = ?1";
375
376pub const TEAM_UPDATE_NAME: &str = "\
378UPDATE teams SET name = ?1 WHERE id = ?2";
379
380pub const TEAM_UPDATE_DESCRIPTION: &str = "\
382UPDATE teams SET description = ?1 WHERE id = ?2";
383
384pub const TEAM_UPDATE_VISIBILITY: &str = "\
386UPDATE teams SET is_public = ?1 WHERE id = ?2";
387
388pub const INVITATIONS_SCHEMA: &str = "\
392CREATE TABLE IF NOT EXISTS team_invitations (
393 id TEXT PRIMARY KEY,
394 team_id TEXT NOT NULL REFERENCES teams(id),
395 email TEXT,
396 oauth_provider TEXT,
397 oauth_provider_username TEXT,
398 invited_by TEXT NOT NULL REFERENCES users(id),
399 role TEXT NOT NULL DEFAULT 'member',
400 status TEXT NOT NULL DEFAULT 'pending',
401 created_at TEXT NOT NULL DEFAULT (datetime('now')),
402 expires_at TEXT NOT NULL
403);
404";
405
406pub const INVITATION_COLUMNS: &str = "\
408i.id, i.team_id, t.name AS team_name, i.email, \
409i.oauth_provider, i.oauth_provider_username, \
410u.nickname AS invited_by_nickname, i.role, i.status, i.created_at, i.expires_at";
411
412pub const INVITATION_INSERT: &str = "\
414INSERT INTO team_invitations \
415(id, team_id, email, oauth_provider, oauth_provider_username, invited_by, role, expires_at) \
416VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)";
417
418pub static INVITATION_LIST_MY: LazyLock<String> = LazyLock::new(|| {
421 format!(
422 "SELECT {INVITATION_COLUMNS} \
423 FROM team_invitations i \
424 INNER JOIN teams t ON t.id = i.team_id \
425 INNER JOIN users u ON u.id = i.invited_by \
426 WHERE i.status = 'pending' \
427 AND i.expires_at > datetime('now') \
428 AND ((i.email IS NOT NULL AND i.email = ?1) \
429 OR (i.oauth_provider IS NOT NULL AND i.oauth_provider_username IS NOT NULL \
430 AND EXISTS (SELECT 1 FROM oauth_identities oi \
431 WHERE oi.user_id = ?2 \
432 AND oi.provider = i.oauth_provider \
433 AND lower(oi.provider_username) = lower(i.oauth_provider_username)))) \
434 ORDER BY i.created_at DESC"
435 )
436});
437
438pub const INVITATION_LOOKUP: &str = "\
440SELECT id, team_id, email, oauth_provider, oauth_provider_username, role, status, expires_at \
441FROM team_invitations WHERE id = ?1";
442
443pub const INVITATION_UPDATE_STATUS: &str = "\
445UPDATE team_invitations SET status = ?1 WHERE id = ?2";
446
447pub const INVITATION_DUP_CHECK_EMAIL: &str = "\
449SELECT COUNT(*) FROM team_invitations \
450WHERE team_id = ?1 AND email = ?2 AND status = 'pending'";
451
452pub const INVITATION_DUP_CHECK_OAUTH: &str = "\
454SELECT COUNT(*) FROM team_invitations \
455WHERE team_id = ?1 AND oauth_provider = ?2 AND oauth_provider_username = ?3 AND status = 'pending'";
456
457pub const OAUTH_IDENTITY_MATCH: &str = "\
459SELECT COUNT(*) FROM oauth_identities \
460WHERE user_id = ?1 AND provider = ?2 AND lower(provider_username) = lower(?3)";