persistence.rs

  1use anyhow::Result;
  2use db::sqlez::statement::Statement;
  3use std::path::PathBuf;
  4
  5use db::sqlez_macros::sql;
  6use db::{define_connection, query};
  7
  8use workspace::{ItemId, WorkspaceDb, WorkspaceId};
  9
 10define_connection!(
 11    // Current schema shape using pseudo-rust syntax:
 12    // editors(
 13    //   item_id: usize,
 14    //   workspace_id: usize,
 15    //   path: Option<PathBuf>,
 16    //   scroll_top_row: usize,
 17    //   scroll_vertical_offset: f32,
 18    //   scroll_horizontal_offset: f32,
 19    //   content: Option<String>,
 20    //   language: Option<String>,
 21    // )
 22    pub static ref DB: EditorDb<WorkspaceDb> =
 23        &[sql! (
 24            CREATE TABLE editors(
 25                item_id INTEGER NOT NULL,
 26                workspace_id INTEGER NOT NULL,
 27                path BLOB NOT NULL,
 28                PRIMARY KEY(item_id, workspace_id),
 29                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 30                ON DELETE CASCADE
 31                ON UPDATE CASCADE
 32            ) STRICT;
 33        ),
 34        sql! (
 35            ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
 36            ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
 37            ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
 38        ),
 39        sql! (
 40            // Since sqlite3 doesn't support ALTER COLUMN, we create a new
 41            // table, move the data over, drop the old table, rename new table.
 42            CREATE TABLE new_editors_tmp (
 43                item_id INTEGER NOT NULL,
 44                workspace_id INTEGER NOT NULL,
 45                path BLOB, // <-- No longer "NOT NULL"
 46                scroll_top_row INTEGER NOT NULL DEFAULT 0,
 47                scroll_horizontal_offset REAL NOT NULL DEFAULT 0,
 48                scroll_vertical_offset REAL NOT NULL DEFAULT 0,
 49                contents TEXT, // New
 50                language TEXT, // New
 51                PRIMARY KEY(item_id, workspace_id),
 52                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 53                ON DELETE CASCADE
 54                ON UPDATE CASCADE
 55            ) STRICT;
 56
 57            INSERT INTO new_editors_tmp(item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset)
 58            SELECT item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
 59            FROM editors;
 60
 61            DROP TABLE editors;
 62
 63            ALTER TABLE new_editors_tmp RENAME TO editors;
 64        )];
 65);
 66
 67impl EditorDb {
 68    query! {
 69        pub fn get_path_and_contents(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(Option<PathBuf>, Option<String>, Option<String>)>> {
 70            SELECT path, contents, language FROM editors
 71            WHERE item_id = ? AND workspace_id = ?
 72        }
 73    }
 74
 75    query! {
 76        pub async fn save_path(item_id: ItemId, workspace_id: WorkspaceId, path: PathBuf) -> Result<()> {
 77            INSERT INTO editors
 78                (item_id, workspace_id, path)
 79            VALUES
 80                (?1, ?2, ?3)
 81            ON CONFLICT DO UPDATE SET
 82                item_id = ?1,
 83                workspace_id = ?2,
 84                path = ?3
 85        }
 86    }
 87
 88    query! {
 89        pub async fn save_contents(item_id: ItemId, workspace: WorkspaceId, contents: Option<String>, language: Option<String>) -> Result<()> {
 90            INSERT INTO editors
 91                (item_id, workspace_id, contents, language)
 92            VALUES
 93                (?1, ?2, ?3, ?4)
 94            ON CONFLICT DO UPDATE SET
 95                item_id = ?1,
 96                workspace_id = ?2,
 97                contents = ?3,
 98                language = ?4
 99        }
100    }
101
102    // Returns the scroll top row, and offset
103    query! {
104        pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
105            SELECT scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
106            FROM editors
107            WHERE item_id = ? AND workspace_id = ?
108        }
109    }
110
111    query! {
112        pub async fn save_scroll_position(
113            item_id: ItemId,
114            workspace_id: WorkspaceId,
115            top_row: u32,
116            vertical_offset: f32,
117            horizontal_offset: f32
118        ) -> Result<()> {
119            UPDATE OR IGNORE editors
120            SET
121                scroll_top_row = ?3,
122                scroll_horizontal_offset = ?4,
123                scroll_vertical_offset = ?5
124            WHERE item_id = ?1 AND workspace_id = ?2
125        }
126    }
127
128    pub async fn delete_unloaded_items(
129        &self,
130        workspace: WorkspaceId,
131        alive_items: Vec<ItemId>,
132    ) -> Result<()> {
133        let placeholders = alive_items
134            .iter()
135            .map(|_| "?")
136            .collect::<Vec<&str>>()
137            .join(", ");
138
139        let query = format!(
140            "DELETE FROM editors WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
141        );
142
143        self.write(move |conn| {
144            let mut statement = Statement::prepare(conn, query)?;
145            let mut next_index = statement.bind(&workspace, 1)?;
146            for id in alive_items {
147                next_index = statement.bind(&id, next_index)?;
148            }
149            statement.exec()
150        })
151        .await
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use gpui;
159
160    #[gpui::test]
161    async fn test_saving_content() {
162        env_logger::try_init().ok();
163
164        let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
165
166        // Sanity check: make sure there is no row in the `editors` table
167        assert_eq!(DB.get_path_and_contents(1234, workspace_id).unwrap(), None);
168
169        // Save content/language
170        DB.save_contents(
171            1234,
172            workspace_id,
173            Some("testing".into()),
174            Some("Go".into()),
175        )
176        .await
177        .unwrap();
178
179        // Check that it can be read from DB
180        let path_and_contents = DB.get_path_and_contents(1234, workspace_id).unwrap();
181        let (path, contents, language) = path_and_contents.unwrap();
182        assert!(path.is_none());
183        assert_eq!(contents, Some("testing".to_owned()));
184        assert_eq!(language, Some("Go".to_owned()));
185
186        // Update it with NULL
187        DB.save_contents(1234, workspace_id, None, None)
188            .await
189            .unwrap();
190
191        // Check that it worked
192        let path_and_contents = DB.get_path_and_contents(1234, workspace_id).unwrap();
193        let (path, contents, language) = path_and_contents.unwrap();
194        assert!(path.is_none());
195        assert!(contents.is_none());
196        assert!(language.is_none());
197    }
198}