persistence.rs

  1use anyhow::Result;
  2use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
  3use db::sqlez::statement::Statement;
  4use fs::MTime;
  5use std::path::PathBuf;
  6
  7use db::sqlez_macros::sql;
  8use db::{define_connection, query};
  9
 10use workspace::{ItemId, WorkspaceDb, WorkspaceId};
 11
 12#[derive(Clone, Debug, PartialEq, Default)]
 13pub(crate) struct SerializedEditor {
 14    pub(crate) abs_path: Option<PathBuf>,
 15    pub(crate) contents: Option<String>,
 16    pub(crate) language: Option<String>,
 17    pub(crate) mtime: Option<MTime>,
 18}
 19
 20impl StaticColumnCount for SerializedEditor {
 21    fn column_count() -> usize {
 22        5
 23    }
 24}
 25
 26impl Bind for SerializedEditor {
 27    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
 28        let start_index = statement.bind(&self.abs_path, start_index)?;
 29        let start_index = statement.bind(&self.contents, start_index)?;
 30        let start_index = statement.bind(&self.language, start_index)?;
 31
 32        let start_index = match self
 33            .mtime
 34            .and_then(|mtime| mtime.to_seconds_and_nanos_for_persistence())
 35        {
 36            Some((seconds, nanos)) => {
 37                let start_index = statement.bind(&(seconds as i64), start_index)?;
 38                statement.bind(&(nanos as i32), start_index)?
 39            }
 40            None => {
 41                let start_index = statement.bind::<Option<i64>>(&None, start_index)?;
 42                statement.bind::<Option<i32>>(&None, start_index)?
 43            }
 44        };
 45        Ok(start_index)
 46    }
 47}
 48
 49impl Column for SerializedEditor {
 50    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 51        let (abs_path, start_index): (Option<PathBuf>, i32) =
 52            Column::column(statement, start_index)?;
 53        let (contents, start_index): (Option<String>, i32) =
 54            Column::column(statement, start_index)?;
 55        let (language, start_index): (Option<String>, i32) =
 56            Column::column(statement, start_index)?;
 57        let (mtime_seconds, start_index): (Option<i64>, i32) =
 58            Column::column(statement, start_index)?;
 59        let (mtime_nanos, start_index): (Option<i32>, i32) =
 60            Column::column(statement, start_index)?;
 61
 62        let mtime = mtime_seconds
 63            .zip(mtime_nanos)
 64            .map(|(seconds, nanos)| MTime::from_seconds_and_nanos(seconds as u64, nanos as u32));
 65
 66        let editor = Self {
 67            abs_path,
 68            contents,
 69            language,
 70            mtime,
 71        };
 72        Ok((editor, start_index))
 73    }
 74}
 75
 76define_connection!(
 77    // Current schema shape using pseudo-rust syntax:
 78    // editors(
 79    //   item_id: usize,
 80    //   workspace_id: usize,
 81    //   path: Option<PathBuf>,
 82    //   scroll_top_row: usize,
 83    //   scroll_vertical_offset: f32,
 84    //   scroll_horizontal_offset: f32,
 85    //   content: Option<String>,
 86    //   language: Option<String>,
 87    //   mtime_seconds: Option<i64>,
 88    //   mtime_nanos: Option<i32>,
 89    // )
 90    pub static ref DB: EditorDb<WorkspaceDb> = &[
 91        sql! (
 92            CREATE TABLE editors(
 93                item_id INTEGER NOT NULL,
 94                workspace_id INTEGER NOT NULL,
 95                path BLOB NOT NULL,
 96                PRIMARY KEY(item_id, workspace_id),
 97                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 98                ON DELETE CASCADE
 99                ON UPDATE CASCADE
100            ) STRICT;
101        ),
102        sql! (
103            ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
104            ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
105            ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
106        ),
107        sql! (
108            // Since sqlite3 doesn't support ALTER COLUMN, we create a new
109            // table, move the data over, drop the old table, rename new table.
110            CREATE TABLE new_editors_tmp (
111                item_id INTEGER NOT NULL,
112                workspace_id INTEGER NOT NULL,
113                path BLOB, // <-- No longer "NOT NULL"
114                scroll_top_row INTEGER NOT NULL DEFAULT 0,
115                scroll_horizontal_offset REAL NOT NULL DEFAULT 0,
116                scroll_vertical_offset REAL NOT NULL DEFAULT 0,
117                contents TEXT, // New
118                language TEXT, // New
119                PRIMARY KEY(item_id, workspace_id),
120                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
121                ON DELETE CASCADE
122                ON UPDATE CASCADE
123            ) STRICT;
124
125            INSERT INTO new_editors_tmp(item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset)
126            SELECT item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
127            FROM editors;
128
129            DROP TABLE editors;
130
131            ALTER TABLE new_editors_tmp RENAME TO editors;
132        ),
133        sql! (
134            ALTER TABLE editors ADD COLUMN mtime_seconds INTEGER DEFAULT NULL;
135            ALTER TABLE editors ADD COLUMN mtime_nanos INTEGER DEFAULT NULL;
136        ),
137    ];
138);
139
140impl EditorDb {
141    query! {
142        pub fn get_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<SerializedEditor>> {
143            SELECT path, contents, language, mtime_seconds, mtime_nanos FROM editors
144            WHERE item_id = ? AND workspace_id = ?
145        }
146    }
147
148    query! {
149        pub async fn save_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId, serialized_editor: SerializedEditor) -> Result<()> {
150            INSERT INTO editors
151                (item_id, workspace_id, path, contents, language, mtime_seconds, mtime_nanos)
152            VALUES
153                (?1, ?2, ?3, ?4, ?5, ?6, ?7)
154            ON CONFLICT DO UPDATE SET
155                item_id = ?1,
156                workspace_id = ?2,
157                path = ?3,
158                contents = ?4,
159                language = ?5,
160                mtime_seconds = ?6,
161                mtime_nanos = ?7
162        }
163    }
164
165    // Returns the scroll top row, and offset
166    query! {
167        pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
168            SELECT scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
169            FROM editors
170            WHERE item_id = ? AND workspace_id = ?
171        }
172    }
173
174    query! {
175        pub async fn save_scroll_position(
176            item_id: ItemId,
177            workspace_id: WorkspaceId,
178            top_row: u32,
179            vertical_offset: f32,
180            horizontal_offset: f32
181        ) -> Result<()> {
182            UPDATE OR IGNORE editors
183            SET
184                scroll_top_row = ?3,
185                scroll_horizontal_offset = ?4,
186                scroll_vertical_offset = ?5
187            WHERE item_id = ?1 AND workspace_id = ?2
188        }
189    }
190
191    pub async fn delete_unloaded_items(
192        &self,
193        workspace: WorkspaceId,
194        alive_items: Vec<ItemId>,
195    ) -> Result<()> {
196        let placeholders = alive_items
197            .iter()
198            .map(|_| "?")
199            .collect::<Vec<&str>>()
200            .join(", ");
201
202        let query = format!(
203            "DELETE FROM editors WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
204        );
205
206        self.write(move |conn| {
207            let mut statement = Statement::prepare(conn, query)?;
208            let mut next_index = statement.bind(&workspace, 1)?;
209            for id in alive_items {
210                next_index = statement.bind(&id, next_index)?;
211            }
212            statement.exec()
213        })
214        .await
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[gpui::test]
223    async fn test_save_and_get_serialized_editor() {
224        let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
225
226        let serialized_editor = SerializedEditor {
227            abs_path: Some(PathBuf::from("testing.txt")),
228            contents: None,
229            language: None,
230            mtime: None,
231        };
232
233        DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
234            .await
235            .unwrap();
236
237        let have = DB
238            .get_serialized_editor(1234, workspace_id)
239            .unwrap()
240            .unwrap();
241        assert_eq!(have, serialized_editor);
242
243        // Now update contents and language
244        let serialized_editor = SerializedEditor {
245            abs_path: Some(PathBuf::from("testing.txt")),
246            contents: Some("Test".to_owned()),
247            language: Some("Go".to_owned()),
248            mtime: None,
249        };
250
251        DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
252            .await
253            .unwrap();
254
255        let have = DB
256            .get_serialized_editor(1234, workspace_id)
257            .unwrap()
258            .unwrap();
259        assert_eq!(have, serialized_editor);
260
261        // Now set all the fields to NULL
262        let serialized_editor = SerializedEditor {
263            abs_path: None,
264            contents: None,
265            language: None,
266            mtime: None,
267        };
268
269        DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
270            .await
271            .unwrap();
272
273        let have = DB
274            .get_serialized_editor(1234, workspace_id)
275            .unwrap()
276            .unwrap();
277        assert_eq!(have, serialized_editor);
278
279        // Storing and retrieving mtime
280        let serialized_editor = SerializedEditor {
281            abs_path: None,
282            contents: None,
283            language: None,
284            mtime: Some(MTime::from_seconds_and_nanos(100, 42)),
285        };
286
287        DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
288            .await
289            .unwrap();
290
291        let have = DB
292            .get_serialized_editor(1234, workspace_id)
293            .unwrap()
294            .unwrap();
295        assert_eq!(have, serialized_editor);
296    }
297}