persistence.rs

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