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