persistence.rs

  1#![allow(dead_code)]
  2
  3pub mod model;
  4
  5use std::path::Path;
  6
  7use anyhow::{anyhow, bail, Context, Result};
  8use db2::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
  9use gpui2::WindowBounds;
 10
 11use util::{unzip_option, ResultExt};
 12use uuid::Uuid;
 13
 14use crate::{Axis, WorkspaceId};
 15
 16use model::{
 17    GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
 18    WorkspaceLocation,
 19};
 20
 21use self::model::DockStructure;
 22
 23define_connection! {
 24    // Current schema shape using pseudo-rust syntax:
 25    //
 26    // workspaces(
 27    //   workspace_id: usize, // Primary key for workspaces
 28    //   workspace_location: Bincode<Vec<PathBuf>>,
 29    //   dock_visible: bool, // Deprecated
 30    //   dock_anchor: DockAnchor, // Deprecated
 31    //   dock_pane: Option<usize>, // Deprecated
 32    //   left_sidebar_open: boolean,
 33    //   timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
 34    //   window_state: String, // WindowBounds Discriminant
 35    //   window_x: Option<f32>, // WindowBounds::Fixed RectF x
 36    //   window_y: Option<f32>, // WindowBounds::Fixed RectF y
 37    //   window_width: Option<f32>, // WindowBounds::Fixed RectF width
 38    //   window_height: Option<f32>, // WindowBounds::Fixed RectF height
 39    //   display: Option<Uuid>, // Display id
 40    // )
 41    //
 42    // pane_groups(
 43    //   group_id: usize, // Primary key for pane_groups
 44    //   workspace_id: usize, // References workspaces table
 45    //   parent_group_id: Option<usize>, // None indicates that this is the root node
 46    //   position: Optiopn<usize>, // None indicates that this is the root node
 47    //   axis: Option<Axis>, // 'Vertical', 'Horizontal'
 48    //   flexes: Option<Vec<f32>>, // A JSON array of floats
 49    // )
 50    //
 51    // panes(
 52    //     pane_id: usize, // Primary key for panes
 53    //     workspace_id: usize, // References workspaces table
 54    //     active: bool,
 55    // )
 56    //
 57    // center_panes(
 58    //     pane_id: usize, // Primary key for center_panes
 59    //     parent_group_id: Option<usize>, // References pane_groups. If none, this is the root
 60    //     position: Option<usize>, // None indicates this is the root
 61    // )
 62    //
 63    // CREATE TABLE items(
 64    //     item_id: usize, // This is the item's view id, so this is not unique
 65    //     workspace_id: usize, // References workspaces table
 66    //     pane_id: usize, // References panes table
 67    //     kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global
 68    //     position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column
 69    //     active: bool, // Indicates if this item is the active one in the pane
 70    // )
 71    pub static ref DB: WorkspaceDb<()> =
 72    &[sql!(
 73        CREATE TABLE workspaces(
 74            workspace_id INTEGER PRIMARY KEY,
 75            workspace_location BLOB UNIQUE,
 76            dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
 77            dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
 78            dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
 79            left_sidebar_open INTEGER, // Boolean
 80            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 81            FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
 82        ) STRICT;
 83
 84        CREATE TABLE pane_groups(
 85            group_id INTEGER PRIMARY KEY,
 86            workspace_id INTEGER NOT NULL,
 87            parent_group_id INTEGER, // NULL indicates that this is a root node
 88            position INTEGER, // NULL indicates that this is a root node
 89            axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
 90            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 91            ON DELETE CASCADE
 92            ON UPDATE CASCADE,
 93            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 94        ) STRICT;
 95
 96        CREATE TABLE panes(
 97            pane_id INTEGER PRIMARY KEY,
 98            workspace_id INTEGER NOT NULL,
 99            active INTEGER NOT NULL, // Boolean
100            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
101            ON DELETE CASCADE
102            ON UPDATE CASCADE
103        ) STRICT;
104
105        CREATE TABLE center_panes(
106            pane_id INTEGER PRIMARY KEY,
107            parent_group_id INTEGER, // NULL means that this is a root pane
108            position INTEGER, // NULL means that this is a root pane
109            FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
110            ON DELETE CASCADE,
111            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
112        ) STRICT;
113
114        CREATE TABLE items(
115            item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
116            workspace_id INTEGER NOT NULL,
117            pane_id INTEGER NOT NULL,
118            kind TEXT NOT NULL,
119            position INTEGER NOT NULL,
120            active INTEGER NOT NULL,
121            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
122            ON DELETE CASCADE
123            ON UPDATE CASCADE,
124            FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
125            ON DELETE CASCADE,
126            PRIMARY KEY(item_id, workspace_id)
127        ) STRICT;
128    ),
129    sql!(
130        ALTER TABLE workspaces ADD COLUMN window_state TEXT;
131        ALTER TABLE workspaces ADD COLUMN window_x REAL;
132        ALTER TABLE workspaces ADD COLUMN window_y REAL;
133        ALTER TABLE workspaces ADD COLUMN window_width REAL;
134        ALTER TABLE workspaces ADD COLUMN window_height REAL;
135        ALTER TABLE workspaces ADD COLUMN display BLOB;
136    ),
137    // Drop foreign key constraint from workspaces.dock_pane to panes table.
138    sql!(
139        CREATE TABLE workspaces_2(
140            workspace_id INTEGER PRIMARY KEY,
141            workspace_location BLOB UNIQUE,
142            dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
143            dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
144            dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
145            left_sidebar_open INTEGER, // Boolean
146            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
147            window_state TEXT,
148            window_x REAL,
149            window_y REAL,
150            window_width REAL,
151            window_height REAL,
152            display BLOB
153        ) STRICT;
154        INSERT INTO workspaces_2 SELECT * FROM workspaces;
155        DROP TABLE workspaces;
156        ALTER TABLE workspaces_2 RENAME TO workspaces;
157    ),
158    // Add panels related information
159    sql!(
160        ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
161        ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
162        ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
163        ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
164        ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
165        ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
166    ),
167    // Add panel zoom persistence
168    sql!(
169        ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
170        ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
171        ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
172    ),
173    // Add pane group flex data
174    sql!(
175        ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
176    )
177    ];
178}
179
180impl WorkspaceDb {
181    /// Returns a serialized workspace for the given worktree_roots. If the passed array
182    /// is empty, the most recent workspace is returned instead. If no workspace for the
183    /// passed roots is stored, returns none.
184    pub fn workspace_for_roots<P: AsRef<Path>>(
185        &self,
186        worktree_roots: &[P],
187    ) -> Option<SerializedWorkspace> {
188        let workspace_location: WorkspaceLocation = worktree_roots.into();
189
190        // Note that we re-assign the workspace_id here in case it's empty
191        // and we've grabbed the most recent workspace
192        let (workspace_id, workspace_location, bounds, display, docks): (
193            WorkspaceId,
194            WorkspaceLocation,
195            Option<WindowBounds>,
196            Option<Uuid>,
197            DockStructure,
198        ) = self
199            .select_row_bound(sql! {
200                SELECT
201                    workspace_id,
202                    workspace_location,
203                    window_state,
204                    window_x,
205                    window_y,
206                    window_width,
207                    window_height,
208                    display,
209                    left_dock_visible,
210                    left_dock_active_panel,
211                    left_dock_zoom,
212                    right_dock_visible,
213                    right_dock_active_panel,
214                    right_dock_zoom,
215                    bottom_dock_visible,
216                    bottom_dock_active_panel,
217                    bottom_dock_zoom
218                FROM workspaces
219                WHERE workspace_location = ?
220            })
221            .and_then(|mut prepared_statement| (prepared_statement)(&workspace_location))
222            .context("No workspaces found")
223            .warn_on_err()
224            .flatten()?;
225
226        Some(SerializedWorkspace {
227            id: workspace_id,
228            location: workspace_location.clone(),
229            center_group: self
230                .get_center_pane_group(workspace_id)
231                .context("Getting center group")
232                .log_err()?,
233            bounds,
234            display,
235            docks,
236        })
237    }
238
239    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
240    /// that used this workspace previously
241    pub async fn save_workspace(&self, workspace: SerializedWorkspace) {
242        self.write(move |conn| {
243            conn.with_savepoint("update_worktrees", || {
244                // Clear out panes and pane_groups
245                conn.exec_bound(sql!(
246                    DELETE FROM pane_groups WHERE workspace_id = ?1;
247                    DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
248                .expect("Clearing old panes");
249
250                conn.exec_bound(sql!(
251                    DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?
252                ))?((&workspace.location, workspace.id.clone()))
253                .context("clearing out old locations")?;
254
255                // Upsert
256                conn.exec_bound(sql!(
257                    INSERT INTO workspaces(
258                        workspace_id,
259                        workspace_location,
260                        left_dock_visible,
261                        left_dock_active_panel,
262                        left_dock_zoom,
263                        right_dock_visible,
264                        right_dock_active_panel,
265                        right_dock_zoom,
266                        bottom_dock_visible,
267                        bottom_dock_active_panel,
268                        bottom_dock_zoom,
269                        timestamp
270                    )
271                    VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
272                    ON CONFLICT DO
273                    UPDATE SET
274                        workspace_location = ?2,
275                        left_dock_visible = ?3,
276                        left_dock_active_panel = ?4,
277                        left_dock_zoom = ?5,
278                        right_dock_visible = ?6,
279                        right_dock_active_panel = ?7,
280                        right_dock_zoom = ?8,
281                        bottom_dock_visible = ?9,
282                        bottom_dock_active_panel = ?10,
283                        bottom_dock_zoom = ?11,
284                        timestamp = CURRENT_TIMESTAMP
285                ))?((workspace.id, &workspace.location, workspace.docks))
286                .context("Updating workspace")?;
287
288                // Save center pane group
289                Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
290                    .context("save pane group in save workspace")?;
291
292                Ok(())
293            })
294            .log_err();
295        })
296        .await;
297    }
298
299    query! {
300        pub async fn next_id() -> Result<WorkspaceId> {
301            INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
302        }
303    }
304
305    query! {
306        fn recent_workspaces() -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
307            SELECT workspace_id, workspace_location
308            FROM workspaces
309            WHERE workspace_location IS NOT NULL
310            ORDER BY timestamp DESC
311        }
312    }
313
314    query! {
315        async fn delete_stale_workspace(id: WorkspaceId) -> Result<()> {
316            DELETE FROM workspaces
317            WHERE workspace_id IS ?
318        }
319    }
320
321    // Returns the recent locations which are still valid on disk and deletes ones which no longer
322    // exist.
323    pub async fn recent_workspaces_on_disk(&self) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
324        let mut result = Vec::new();
325        let mut delete_tasks = Vec::new();
326        for (id, location) in self.recent_workspaces()? {
327            if location.paths().iter().all(|path| path.exists())
328                && location.paths().iter().any(|path| path.is_dir())
329            {
330                result.push((id, location));
331            } else {
332                delete_tasks.push(self.delete_stale_workspace(id));
333            }
334        }
335
336        futures::future::join_all(delete_tasks).await;
337        Ok(result)
338    }
339
340    pub async fn last_workspace(&self) -> Result<Option<WorkspaceLocation>> {
341        Ok(self
342            .recent_workspaces_on_disk()
343            .await?
344            .into_iter()
345            .next()
346            .map(|(_, location)| location))
347    }
348
349    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
350        Ok(self
351            .get_pane_group(workspace_id, None)?
352            .into_iter()
353            .next()
354            .unwrap_or_else(|| {
355                SerializedPaneGroup::Pane(SerializedPane {
356                    active: true,
357                    children: vec![],
358                })
359            }))
360    }
361
362    fn get_pane_group(
363        &self,
364        workspace_id: WorkspaceId,
365        group_id: Option<GroupId>,
366    ) -> Result<Vec<SerializedPaneGroup>> {
367        type GroupKey = (Option<GroupId>, WorkspaceId);
368        type GroupOrPane = (
369            Option<GroupId>,
370            Option<Axis>,
371            Option<PaneId>,
372            Option<bool>,
373            Option<String>,
374        );
375        self.select_bound::<GroupKey, GroupOrPane>(sql!(
376            SELECT group_id, axis, pane_id, active, flexes
377                FROM (SELECT
378                        group_id,
379                        axis,
380                        NULL as pane_id,
381                        NULL as active,
382                        position,
383                        parent_group_id,
384                        workspace_id,
385                        flexes
386                      FROM pane_groups
387                    UNION
388                      SELECT
389                        NULL,
390                        NULL,
391                        center_panes.pane_id,
392                        panes.active as active,
393                        position,
394                        parent_group_id,
395                        panes.workspace_id as workspace_id,
396                        NULL
397                      FROM center_panes
398                      JOIN panes ON center_panes.pane_id = panes.pane_id)
399                WHERE parent_group_id IS ? AND workspace_id = ?
400                ORDER BY position
401        ))?((group_id, workspace_id))?
402        .into_iter()
403        .map(|(group_id, axis, pane_id, active, flexes)| {
404            if let Some((group_id, axis)) = group_id.zip(axis) {
405                let flexes = flexes
406                    .map(|flexes| serde_json::from_str::<Vec<f32>>(&flexes))
407                    .transpose()?;
408
409                Ok(SerializedPaneGroup::Group {
410                    axis,
411                    children: self.get_pane_group(workspace_id, Some(group_id))?,
412                    flexes,
413                })
414            } else if let Some((pane_id, active)) = pane_id.zip(active) {
415                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
416                    self.get_items(pane_id)?,
417                    active,
418                )))
419            } else {
420                bail!("Pane Group Child was neither a pane group or a pane");
421            }
422        })
423        // Filter out panes and pane groups which don't have any children or items
424        .filter(|pane_group| match pane_group {
425            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
426            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
427            _ => true,
428        })
429        .collect::<Result<_>>()
430    }
431
432    fn save_pane_group(
433        conn: &Connection,
434        workspace_id: WorkspaceId,
435        pane_group: &SerializedPaneGroup,
436        parent: Option<(GroupId, usize)>,
437    ) -> Result<()> {
438        match pane_group {
439            SerializedPaneGroup::Group {
440                axis,
441                children,
442                flexes,
443            } => {
444                let (parent_id, position) = unzip_option(parent);
445
446                let flex_string = flexes
447                    .as_ref()
448                    .map(|flexes| serde_json::json!(flexes).to_string());
449
450                let group_id = conn.select_row_bound::<_, i64>(sql!(
451                    INSERT INTO pane_groups(
452                        workspace_id,
453                        parent_group_id,
454                        position,
455                        axis,
456                        flexes
457                    )
458                    VALUES (?, ?, ?, ?, ?)
459                    RETURNING group_id
460                ))?((
461                    workspace_id,
462                    parent_id,
463                    position,
464                    *axis,
465                    flex_string,
466                ))?
467                .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
468
469                for (position, group) in children.iter().enumerate() {
470                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
471                }
472
473                Ok(())
474            }
475            SerializedPaneGroup::Pane(pane) => {
476                Self::save_pane(conn, workspace_id, &pane, parent)?;
477                Ok(())
478            }
479        }
480    }
481
482    fn save_pane(
483        conn: &Connection,
484        workspace_id: WorkspaceId,
485        pane: &SerializedPane,
486        parent: Option<(GroupId, usize)>,
487    ) -> Result<PaneId> {
488        let pane_id = conn.select_row_bound::<_, i64>(sql!(
489            INSERT INTO panes(workspace_id, active)
490            VALUES (?, ?)
491            RETURNING pane_id
492        ))?((workspace_id, pane.active))?
493        .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
494
495        let (parent_id, order) = unzip_option(parent);
496        conn.exec_bound(sql!(
497            INSERT INTO center_panes(pane_id, parent_group_id, position)
498            VALUES (?, ?, ?)
499        ))?((pane_id, parent_id, order))?;
500
501        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
502
503        Ok(pane_id)
504    }
505
506    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
507        Ok(self.select_bound(sql!(
508            SELECT kind, item_id, active FROM items
509            WHERE pane_id = ?
510                ORDER BY position
511        ))?(pane_id)?)
512    }
513
514    fn save_items(
515        conn: &Connection,
516        workspace_id: WorkspaceId,
517        pane_id: PaneId,
518        items: &[SerializedItem],
519    ) -> Result<()> {
520        let mut insert = conn.exec_bound(sql!(
521            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?)
522        )).context("Preparing insertion")?;
523        for (position, item) in items.iter().enumerate() {
524            insert((workspace_id, pane_id, position, item))?;
525        }
526
527        Ok(())
528    }
529
530    query! {
531        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
532            UPDATE workspaces
533            SET timestamp = CURRENT_TIMESTAMP
534            WHERE workspace_id = ?
535        }
536    }
537
538    query! {
539        pub async fn set_window_bounds(workspace_id: WorkspaceId, bounds: WindowBounds, display: Uuid) -> Result<()> {
540            UPDATE workspaces
541            SET window_state = ?2,
542                window_x = ?3,
543                window_y = ?4,
544                window_width = ?5,
545                window_height = ?6,
546                display = ?7
547            WHERE workspace_id = ?1
548        }
549    }
550}
551
552// todo!()
553// #[cfg(test)]
554// mod tests {
555//     use super::*;
556//     use db::open_test_db;
557
558//     #[gpui::test]
559//     async fn test_next_id_stability() {
560//         env_logger::try_init().ok();
561
562//         let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
563
564//         db.write(|conn| {
565//             conn.migrate(
566//                 "test_table",
567//                 &[sql!(
568//                     CREATE TABLE test_table(
569//                         text TEXT,
570//                         workspace_id INTEGER,
571//                         FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
572//                         ON DELETE CASCADE
573//                     ) STRICT;
574//                 )],
575//             )
576//             .unwrap();
577//         })
578//         .await;
579
580//         let id = db.next_id().await.unwrap();
581//         // Assert the empty row got inserted
582//         assert_eq!(
583//             Some(id),
584//             db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
585//                 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
586//             ))
587//             .unwrap()(id)
588//             .unwrap()
589//         );
590
591//         db.write(move |conn| {
592//             conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
593//                 .unwrap()(("test-text-1", id))
594//             .unwrap()
595//         })
596//         .await;
597
598//         let test_text_1 = db
599//             .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
600//             .unwrap()(1)
601//         .unwrap()
602//         .unwrap();
603//         assert_eq!(test_text_1, "test-text-1");
604//     }
605
606//     #[gpui::test]
607//     async fn test_workspace_id_stability() {
608//         env_logger::try_init().ok();
609
610//         let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
611
612//         db.write(|conn| {
613//             conn.migrate(
614//                 "test_table",
615//                 &[sql!(
616//                     CREATE TABLE test_table(
617//                         text TEXT,
618//                         workspace_id INTEGER,
619//                         FOREIGN KEY(workspace_id)
620//                             REFERENCES workspaces(workspace_id)
621//                         ON DELETE CASCADE
622//                     ) STRICT;)],
623//             )
624//         })
625//         .await
626//         .unwrap();
627
628//         let mut workspace_1 = SerializedWorkspace {
629//             id: 1,
630//             location: (["/tmp", "/tmp2"]).into(),
631//             center_group: Default::default(),
632//             bounds: Default::default(),
633//             display: Default::default(),
634//             docks: Default::default(),
635//         };
636
637//         let workspace_2 = SerializedWorkspace {
638//             id: 2,
639//             location: (["/tmp"]).into(),
640//             center_group: Default::default(),
641//             bounds: Default::default(),
642//             display: Default::default(),
643//             docks: Default::default(),
644//         };
645
646//         db.save_workspace(workspace_1.clone()).await;
647
648//         db.write(|conn| {
649//             conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
650//                 .unwrap()(("test-text-1", 1))
651//             .unwrap();
652//         })
653//         .await;
654
655//         db.save_workspace(workspace_2.clone()).await;
656
657//         db.write(|conn| {
658//             conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
659//                 .unwrap()(("test-text-2", 2))
660//             .unwrap();
661//         })
662//         .await;
663
664//         workspace_1.location = (["/tmp", "/tmp3"]).into();
665//         db.save_workspace(workspace_1.clone()).await;
666//         db.save_workspace(workspace_1).await;
667//         db.save_workspace(workspace_2).await;
668
669//         let test_text_2 = db
670//             .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
671//             .unwrap()(2)
672//         .unwrap()
673//         .unwrap();
674//         assert_eq!(test_text_2, "test-text-2");
675
676//         let test_text_1 = db
677//             .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
678//             .unwrap()(1)
679//         .unwrap()
680//         .unwrap();
681//         assert_eq!(test_text_1, "test-text-1");
682//     }
683
684//     fn group(axis: gpui::Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
685//         SerializedPaneGroup::Group {
686//             axis,
687//             flexes: None,
688//             children,
689//         }
690//     }
691
692//     #[gpui::test]
693//     async fn test_full_workspace_serialization() {
694//         env_logger::try_init().ok();
695
696//         let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
697
698//         //  -----------------
699//         //  | 1,2   | 5,6   |
700//         //  | - - - |       |
701//         //  | 3,4   |       |
702//         //  -----------------
703//         let center_group = group(
704//             gpui::Axis::Horizontal,
705//             vec![
706//                 group(
707//                     gpui::Axis::Vertical,
708//                     vec![
709//                         SerializedPaneGroup::Pane(SerializedPane::new(
710//                             vec![
711//                                 SerializedItem::new("Terminal", 5, false),
712//                                 SerializedItem::new("Terminal", 6, true),
713//                             ],
714//                             false,
715//                         )),
716//                         SerializedPaneGroup::Pane(SerializedPane::new(
717//                             vec![
718//                                 SerializedItem::new("Terminal", 7, true),
719//                                 SerializedItem::new("Terminal", 8, false),
720//                             ],
721//                             false,
722//                         )),
723//                     ],
724//                 ),
725//                 SerializedPaneGroup::Pane(SerializedPane::new(
726//                     vec![
727//                         SerializedItem::new("Terminal", 9, false),
728//                         SerializedItem::new("Terminal", 10, true),
729//                     ],
730//                     false,
731//                 )),
732//             ],
733//         );
734
735//         let workspace = SerializedWorkspace {
736//             id: 5,
737//             location: (["/tmp", "/tmp2"]).into(),
738//             center_group,
739//             bounds: Default::default(),
740//             display: Default::default(),
741//             docks: Default::default(),
742//         };
743
744//         db.save_workspace(workspace.clone()).await;
745//         let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
746
747//         assert_eq!(workspace, round_trip_workspace.unwrap());
748
749//         // Test guaranteed duplicate IDs
750//         db.save_workspace(workspace.clone()).await;
751//         db.save_workspace(workspace.clone()).await;
752
753//         let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
754//         assert_eq!(workspace, round_trip_workspace.unwrap());
755//     }
756
757//     #[gpui::test]
758//     async fn test_workspace_assignment() {
759//         env_logger::try_init().ok();
760
761//         let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
762
763//         let workspace_1 = SerializedWorkspace {
764//             id: 1,
765//             location: (["/tmp", "/tmp2"]).into(),
766//             center_group: Default::default(),
767//             bounds: Default::default(),
768//             display: Default::default(),
769//             docks: Default::default(),
770//         };
771
772//         let mut workspace_2 = SerializedWorkspace {
773//             id: 2,
774//             location: (["/tmp"]).into(),
775//             center_group: Default::default(),
776//             bounds: Default::default(),
777//             display: Default::default(),
778//             docks: Default::default(),
779//         };
780
781//         db.save_workspace(workspace_1.clone()).await;
782//         db.save_workspace(workspace_2.clone()).await;
783
784//         // Test that paths are treated as a set
785//         assert_eq!(
786//             db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
787//             workspace_1
788//         );
789//         assert_eq!(
790//             db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
791//             workspace_1
792//         );
793
794//         // Make sure that other keys work
795//         assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
796//         assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
797
798//         // Test 'mutate' case of updating a pre-existing id
799//         workspace_2.location = (["/tmp", "/tmp2"]).into();
800
801//         db.save_workspace(workspace_2.clone()).await;
802//         assert_eq!(
803//             db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
804//             workspace_2
805//         );
806
807//         // Test other mechanism for mutating
808//         let mut workspace_3 = SerializedWorkspace {
809//             id: 3,
810//             location: (&["/tmp", "/tmp2"]).into(),
811//             center_group: Default::default(),
812//             bounds: Default::default(),
813//             display: Default::default(),
814//             docks: Default::default(),
815//         };
816
817//         db.save_workspace(workspace_3.clone()).await;
818//         assert_eq!(
819//             db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
820//             workspace_3
821//         );
822
823//         // Make sure that updating paths differently also works
824//         workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
825//         db.save_workspace(workspace_3.clone()).await;
826//         assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
827//         assert_eq!(
828//             db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
829//                 .unwrap(),
830//             workspace_3
831//         );
832//     }
833
834//     use crate::persistence::model::SerializedWorkspace;
835//     use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
836
837//     fn default_workspace<P: AsRef<Path>>(
838//         workspace_id: &[P],
839//         center_group: &SerializedPaneGroup,
840//     ) -> SerializedWorkspace {
841//         SerializedWorkspace {
842//             id: 4,
843//             location: workspace_id.into(),
844//             center_group: center_group.clone(),
845//             bounds: Default::default(),
846//             display: Default::default(),
847//             docks: Default::default(),
848//         }
849//     }
850
851//     #[gpui::test]
852//     async fn test_simple_split() {
853//         env_logger::try_init().ok();
854
855//         let db = WorkspaceDb(open_test_db("simple_split").await);
856
857//         //  -----------------
858//         //  | 1,2   | 5,6   |
859//         //  | - - - |       |
860//         //  | 3,4   |       |
861//         //  -----------------
862//         let center_pane = group(
863//             gpui::Axis::Horizontal,
864//             vec![
865//                 group(
866//                     gpui::Axis::Vertical,
867//                     vec![
868//                         SerializedPaneGroup::Pane(SerializedPane::new(
869//                             vec![
870//                                 SerializedItem::new("Terminal", 1, false),
871//                                 SerializedItem::new("Terminal", 2, true),
872//                             ],
873//                             false,
874//                         )),
875//                         SerializedPaneGroup::Pane(SerializedPane::new(
876//                             vec![
877//                                 SerializedItem::new("Terminal", 4, false),
878//                                 SerializedItem::new("Terminal", 3, true),
879//                             ],
880//                             true,
881//                         )),
882//                     ],
883//                 ),
884//                 SerializedPaneGroup::Pane(SerializedPane::new(
885//                     vec![
886//                         SerializedItem::new("Terminal", 5, true),
887//                         SerializedItem::new("Terminal", 6, false),
888//                     ],
889//                     false,
890//                 )),
891//             ],
892//         );
893
894//         let workspace = default_workspace(&["/tmp"], &center_pane);
895
896//         db.save_workspace(workspace.clone()).await;
897
898//         let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
899
900//         assert_eq!(workspace.center_group, new_workspace.center_group);
901//     }
902
903//     #[gpui::test]
904//     async fn test_cleanup_panes() {
905//         env_logger::try_init().ok();
906
907//         let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
908
909//         let center_pane = group(
910//             gpui::Axis::Horizontal,
911//             vec![
912//                 group(
913//                     gpui::Axis::Vertical,
914//                     vec![
915//                         SerializedPaneGroup::Pane(SerializedPane::new(
916//                             vec![
917//                                 SerializedItem::new("Terminal", 1, false),
918//                                 SerializedItem::new("Terminal", 2, true),
919//                             ],
920//                             false,
921//                         )),
922//                         SerializedPaneGroup::Pane(SerializedPane::new(
923//                             vec![
924//                                 SerializedItem::new("Terminal", 4, false),
925//                                 SerializedItem::new("Terminal", 3, true),
926//                             ],
927//                             true,
928//                         )),
929//                     ],
930//                 ),
931//                 SerializedPaneGroup::Pane(SerializedPane::new(
932//                     vec![
933//                         SerializedItem::new("Terminal", 5, false),
934//                         SerializedItem::new("Terminal", 6, true),
935//                     ],
936//                     false,
937//                 )),
938//             ],
939//         );
940
941//         let id = &["/tmp"];
942
943//         let mut workspace = default_workspace(id, &center_pane);
944
945//         db.save_workspace(workspace.clone()).await;
946
947//         workspace.center_group = group(
948//             gpui::Axis::Vertical,
949//             vec![
950//                 SerializedPaneGroup::Pane(SerializedPane::new(
951//                     vec![
952//                         SerializedItem::new("Terminal", 1, false),
953//                         SerializedItem::new("Terminal", 2, true),
954//                     ],
955//                     false,
956//                 )),
957//                 SerializedPaneGroup::Pane(SerializedPane::new(
958//                     vec![
959//                         SerializedItem::new("Terminal", 4, true),
960//                         SerializedItem::new("Terminal", 3, false),
961//                     ],
962//                     true,
963//                 )),
964//             ],
965//         );
966
967//         db.save_workspace(workspace.clone()).await;
968
969//         let new_workspace = db.workspace_for_roots(id).unwrap();
970
971//         assert_eq!(workspace.center_group, new_workspace.center_group);
972//     }
973// }