1use super::*;
2
3pub fn list_workspaces() -> Result<Vec<WorkspaceRecord>> {
4 list_workspaces_from(&database_path())
5}
6
7pub(super) struct DbPaneSize(PaneSize);
8
9impl rusqlite::types::ToSql for DbPaneSize {
10 fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
11 Ok(match self.0 {
12 PaneSize::Minimized => "minimized",
13 PaneSize::Standard => "standard",
14 PaneSize::Maximized => "maximized",
15 }
16 .into())
17 }
18}
19
20impl rusqlite::types::FromSql for DbPaneSize {
21 fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
22 match value.as_str()? {
23 "minimized" => Ok(Self(PaneSize::Minimized)),
24 "standard" => Ok(Self(PaneSize::Standard)),
25 "maximized" => Ok(Self(PaneSize::Maximized)),
26 other => Err(rusqlite::types::FromSqlError::Other(
27 format!("unknown pane size {other:?}").into(),
28 )),
29 }
30 }
31}
32
33pub fn load_workspace_pane_sizes(workspace_id: &str) -> Result<PaneSizes> {
34 load_workspace_pane_sizes_from(&database_path(), workspace_id)
35}
36
37pub fn load_workspace_pane_sizes_from(path: &Path, workspace_id: &str) -> Result<PaneSizes> {
38 let connection = open_reader(path)?;
39 let sizes = connection
40 .query_row(
41 "SELECT coalesce(p.sessions, 'standard'), coalesce(p.targets, 'standard'),
42 coalesce(p.quota, 'standard')
43 FROM workspaces w LEFT JOIN workspace_pane_sizes p USING(workspace_id)
44 WHERE w.workspace_id = ?1",
45 [workspace_id],
46 |row| {
47 Ok(PaneSizes {
48 sessions: row.get::<_, DbPaneSize>(0)?.0,
49 targets: row.get::<_, DbPaneSize>(1)?.0,
50 quota: row.get::<_, DbPaneSize>(2)?.0,
51 })
52 },
53 )
54 .optional()?
55 .with_context(|| format!("unknown workspace {workspace_id:?}"))?;
56 sizes.validate()?;
57 Ok(sizes)
58}
59
60pub fn save_workspace_pane_sizes(workspace_id: &str, sizes: PaneSizes) -> Result<()> {
61 let workspace_id = workspace_id.to_owned();
62 submit_database_write("save_workspace_pane_sizes", move |_| {
63 save_workspace_pane_sizes_to(&database_path(), &workspace_id, sizes)
64 })
65}
66
67pub fn save_workspace_pane_sizes_to(
68 path: &Path,
69 workspace_id: &str,
70 sizes: PaneSizes,
71) -> Result<()> {
72 sizes.validate()?;
73 let connection = open(path)?;
74 connection
75 .execute(
76 "INSERT INTO workspace_pane_sizes(workspace_id, sessions, targets, quota)
77 VALUES (?1, ?2, ?3, ?4)
78 ON CONFLICT(workspace_id) DO UPDATE SET
79 sessions = excluded.sessions, targets = excluded.targets, quota = excluded.quota",
80 params![
81 workspace_id,
82 DbPaneSize(sizes.sessions),
83 DbPaneSize(sizes.targets),
84 DbPaneSize(sizes.quota)
85 ],
86 )
87 .with_context(|| format!("save pane sizes for workspace {workspace_id:?}"))?;
88 Ok(())
89}
90
91pub fn list_workspaces_from(path: &Path) -> Result<Vec<WorkspaceRecord>> {
92 let connection = open_reader(path)?;
93 let mut statement = connection.prepare(
94 "SELECT w.workspace_id, w.name, w.created_at, w.last_opened_at,
95 count(s.session_id) FILTER (
96 WHERE s.state NOT IN ('stopped', 'lost', 'destroyed-with-data-loss')
97 )
98 FROM workspaces w
99 LEFT JOIN session_contexts c USING(workspace_id)
100 LEFT JOIN sessions s USING(session_id)
101 GROUP BY w.workspace_id
102 HAVING w.workspace_id != 'default' OR count(s.session_id) FILTER (
103 WHERE s.state NOT IN ('stopped', 'lost', 'destroyed-with-data-loss')
104 ) > 0
105 ORDER BY w.last_opened_at DESC, w.created_at DESC, w.workspace_id",
106 )?;
107 let rows = statement.query_map([], |row| {
108 Ok(WorkspaceRecord {
109 id: row.get(0)?,
110 name: row.get(1)?,
111 created_at: row.get(2)?,
112 last_opened_at: row.get(3)?,
113 session_count: row.get(4)?,
114 })
115 })?;
116 rows.collect::<rusqlite::Result<_>>().map_err(Into::into)
117}
118
119pub fn create_workspace(name: &str) -> Result<WorkspaceRecord> {
120 let name = name.to_owned();
121 submit_database_write("create_workspace", move |_| {
122 create_workspace_at(&database_path(), &name)
123 })
124}
125
126pub fn create_or_get_workspace(name: &str) -> Result<WorkspaceRecord> {
133 let name = name.to_owned();
134 submit_database_write("create_or_get_workspace", move |_| {
135 create_or_get_workspace_at(&database_path(), &name)
136 })
137}
138
139pub fn create_or_get_workspace_at(path: &Path, name: &str) -> Result<WorkspaceRecord> {
140 let (name, name_key) = normalize_workspace_name(name)?;
141 let id = new_workspace_id()?;
142 let now = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
143 let mut connection = open(path)?;
144 let transaction = connection.transaction()?;
145 transaction
146 .execute(
147 "INSERT INTO workspaces(workspace_id, name, name_key, created_at, last_opened_at)
148 VALUES (?1, ?2, ?3, ?4, ?4)
149 ON CONFLICT(name_key) DO NOTHING",
150 params![id, name, name_key, now],
151 )
152 .with_context(|| format!("create or find workspace {name:?}"))?;
153 let workspace = transaction.query_row(
154 "SELECT w.workspace_id, w.name, w.created_at, w.last_opened_at,
155 count(s.session_id) FILTER (
156 WHERE s.state NOT IN ('stopped', 'lost', 'destroyed-with-data-loss')
157 )
158 FROM workspaces w
159 LEFT JOIN session_contexts c USING(workspace_id)
160 LEFT JOIN sessions s USING(session_id)
161 WHERE w.name_key = ?1
162 GROUP BY w.workspace_id",
163 params![name_key],
164 |row| {
165 Ok(WorkspaceRecord {
166 id: row.get(0)?,
167 name: row.get(1)?,
168 created_at: row.get(2)?,
169 last_opened_at: row.get(3)?,
170 session_count: row.get(4)?,
171 })
172 },
173 )?;
174 transaction.commit()?;
175 Ok(workspace)
176}
177
178pub fn create_workspace_at(path: &Path, name: &str) -> Result<WorkspaceRecord> {
179 let (name, name_key) = normalize_workspace_name(name)?;
180 let id = new_workspace_id()?;
181 let now = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
182 let connection = open(path)?;
183 connection
184 .execute(
185 "INSERT INTO workspaces(workspace_id, name, name_key, created_at, last_opened_at)
186 VALUES (?1, ?2, ?3, ?4, ?4)",
187 params![id, name, name_key, now],
188 )
189 .with_context(|| format!("create workspace {name:?}"))?;
190 Ok(WorkspaceRecord {
191 id,
192 name,
193 created_at: now.clone(),
194 last_opened_at: now,
195 session_count: 0,
196 })
197}
198
199pub fn rename_workspace(workspace_id: &str, name: &str) -> Result<()> {
200 let workspace_id = workspace_id.to_owned();
201 let name = name.to_owned();
202 submit_database_write("rename_workspace", move |_| {
203 rename_workspace_at(&database_path(), &workspace_id, &name)
204 })
205}
206
207pub fn rename_workspace_at(path: &Path, workspace_id: &str, name: &str) -> Result<()> {
208 let (name, name_key) = normalize_workspace_name(name)?;
209 let connection = open(path)?;
210 let changed = connection
211 .execute(
212 "UPDATE workspaces SET name = ?2, name_key = ?3 WHERE workspace_id = ?1",
213 params![workspace_id, name, name_key],
214 )
215 .with_context(|| format!("rename workspace to {name:?}"))?;
216 ensure!(changed == 1, "unknown workspace {workspace_id:?}");
217 Ok(())
218}
219
220pub fn touch_workspace(workspace_id: &str) -> Result<()> {
221 let workspace_id = workspace_id.to_owned();
222 submit_database_write("touch_workspace", move |_| {
223 touch_workspace_at(&database_path(), &workspace_id)
224 })
225}
226
227pub fn touch_workspace_at(path: &Path, workspace_id: &str) -> Result<()> {
228 let connection = open(path)?;
229 let now = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
230 let changed = connection.execute(
231 "UPDATE workspaces SET last_opened_at = ?2 WHERE workspace_id = ?1",
232 params![workspace_id, now],
233 )?;
234 ensure!(changed == 1, "unknown workspace {workspace_id:?}");
235 Ok(())
236}
237
238pub fn delete_workspace(workspace_id: &str) -> Result<()> {
243 let workspace_id = workspace_id.to_owned();
244 submit_database_write("delete_workspace", move |_| {
245 delete_workspace_at(&database_path(), &workspace_id)
246 })
247}
248
249pub fn delete_workspace_at(path: &Path, workspace_id: &str) -> Result<()> {
250 let mut connection = open(path)?;
251 let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
252 let active_count = {
253 let mut statement = tx.prepare(
254 "SELECT s.state
255 FROM session_contexts c
256 JOIN sessions s USING(session_id)
257 WHERE c.workspace_id = ?1",
258 )?;
259 let states = statement.query_map([workspace_id], |row| row.get::<_, String>(0))?;
260 states
261 .collect::<rusqlite::Result<Vec<_>>>()?
262 .into_iter()
263 .filter(|state| stored_session_state(state).is_active())
264 .count()
265 };
266 let draft_count: u64 = tx.query_row(
267 "SELECT count(*) FROM detached_drafts WHERE workspace_id = ?1",
268 [workspace_id],
269 |row| row.get(0),
270 )?;
271 ensure!(
272 active_count == 0 && draft_count == 0,
273 "workspace is not empty ({active_count} active sessions, {draft_count} drafts)"
274 );
275 let changed = tx.execute(
276 "DELETE FROM workspaces WHERE workspace_id = ?1",
277 [workspace_id],
278 )?;
279 ensure!(changed == 1, "unknown workspace {workspace_id:?}");
280 tx.commit()?;
281 Ok(())
282}
283
284pub fn force_delete_workspace(workspace_id: &str) -> Result<()> {
292 let workspace_id = workspace_id.to_owned();
293 submit_database_write("force_delete_workspace", move |_| {
294 force_delete_workspace_at(&database_path(), &workspace_id)
295 })
296}
297
298pub fn force_delete_workspace_at(path: &Path, workspace_id: &str) -> Result<()> {
299 let mut connection = open(path)?;
300 let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
301 let active_count = {
302 let mut statement = tx.prepare(
303 "SELECT s.state
304 FROM session_contexts c
305 JOIN sessions s USING(session_id)
306 WHERE c.workspace_id = ?1",
307 )?;
308 let states = statement.query_map([workspace_id], |row| row.get::<_, String>(0))?;
309 states
310 .collect::<rusqlite::Result<Vec<_>>>()?
311 .into_iter()
312 .filter(|state| stored_session_state(state).is_active())
313 .count()
314 };
315 ensure!(
316 active_count == 0,
317 "workspace is not empty ({active_count} active sessions remain)"
318 );
319 tx.execute(
320 "DELETE FROM detached_drafts WHERE workspace_id = ?1",
321 [workspace_id],
322 )?;
323 let changed = tx.execute(
324 "DELETE FROM workspaces WHERE workspace_id = ?1",
325 [workspace_id],
326 )?;
327 ensure!(changed == 1, "unknown workspace {workspace_id:?}");
328 tx.commit()?;
329 Ok(())
330}
331
332pub fn reassign_resumable_session_workspace(session_id: &str, workspace_id: &str) -> Result<()> {
338 let session_id = session_id.to_owned();
339 let workspace_id = workspace_id.to_owned();
340 submit_database_write("reassign_resumable_session_workspace", move |_| {
341 reassign_resumable_session_workspace_at(&database_path(), &session_id, &workspace_id)
342 })
343}
344
345pub fn reassign_resumable_session_workspace_at(
346 path: &Path,
347 session_id: &str,
348 workspace_id: &str,
349) -> Result<()> {
350 let mut connection = open(path)?;
351 let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
352 let (current_workspace, state): (String, String) = tx
353 .query_row(
354 "SELECT c.workspace_id, s.state
355 FROM session_contexts c
356 JOIN sessions s USING(session_id)
357 WHERE c.session_id = ?1",
358 [session_id],
359 |row| Ok((row.get(0)?, row.get(1)?)),
360 )
361 .with_context(|| format!("find resumable session {session_id:?}"))?;
362 ensure!(
363 matches!(
364 stored_session_state(&state),
365 SessionState::Stopped | SessionState::Lost | SessionState::Error
366 ),
367 "session {session_id} is not resumable"
368 );
369 let destination_exists: bool = tx.query_row(
370 "SELECT EXISTS(SELECT 1 FROM workspaces WHERE workspace_id = ?1)",
371 [workspace_id],
372 |row| row.get(0),
373 )?;
374 ensure!(destination_exists, "unknown workspace {workspace_id:?}");
375 if current_workspace != workspace_id {
376 tx.execute(
377 "UPDATE session_contexts SET workspace_id = ?2 WHERE session_id = ?1",
378 params![session_id, workspace_id],
379 )?;
380 }
381 tx.commit()?;
382 Ok(())
383}
384
385pub fn workspace_for_session_at(path: &Path, session_id: &str) -> Result<Option<String>> {
386 open_reader(path)?
387 .query_row(
388 "SELECT workspace_id FROM session_contexts WHERE session_id = ?1",
389 [session_id],
390 |row| row.get(0),
391 )
392 .optional()
393 .map_err(Into::into)
394}
395
396pub fn session_ids_for_workspace(workspace_id: &str) -> Result<Vec<String>> {
397 session_ids_for_workspace_at(&database_path(), workspace_id)
398}
399
400pub fn session_ids_for_workspace_at(path: &Path, workspace_id: &str) -> Result<Vec<String>> {
403 let connection = open_reader(path)?;
404 let mut statement = connection.prepare(
405 "SELECT c.session_id
406 FROM session_contexts c
407 JOIN sessions s USING(session_id)
408 WHERE c.workspace_id = ?1
409 ORDER BY c.created_at, c.session_id",
410 )?;
411 let rows = statement.query_map([workspace_id], |row| row.get(0))?;
412 rows.collect::<rusqlite::Result<_>>().map_err(Into::into)
413}
414
415pub fn assign_new_session_workspace(session_id: &str, workspace_id: &str) -> Result<()> {
418 let session_id = session_id.to_owned();
419 let workspace_id = workspace_id.to_owned();
420 submit_database_write("assign_new_session_workspace", move |_| {
421 assign_new_session_workspace_at(&database_path(), &session_id, &workspace_id)
422 })
423}
424
425pub fn assign_new_session_workspace_at(
426 path: &Path,
427 session_id: &str,
428 workspace_id: &str,
429) -> Result<()> {
430 let connection = open(path)?;
431 let current: String = connection
432 .query_row(
433 "SELECT workspace_id FROM session_contexts WHERE session_id = ?1",
434 [session_id],
435 |row| row.get(0),
436 )
437 .with_context(|| format!("find session context {session_id:?}"))?;
438 if current == workspace_id {
439 return Ok(());
440 }
441 ensure!(
442 current == DEFAULT_WORKSPACE_ID,
443 "session {session_id} already belongs to workspace {current}"
444 );
445 let exists: bool = connection.query_row(
446 "SELECT EXISTS(SELECT 1 FROM workspaces WHERE workspace_id = ?1)",
447 [workspace_id],
448 |row| row.get(0),
449 )?;
450 ensure!(exists, "unknown workspace {workspace_id:?}");
451 connection.execute(
452 "UPDATE session_contexts SET workspace_id = ?2 WHERE session_id = ?1",
453 params![session_id, workspace_id],
454 )?;
455 Ok(())
456}