persistence.rs

  1use anyhow::Result;
  2use db::{
  3    query,
  4    sqlez::{
  5        bindable::{Bind, Column, StaticColumnCount},
  6        domain::Domain,
  7        statement::Statement,
  8    },
  9    sqlez_macros::sql,
 10};
 11use fs::MTime;
 12use itertools::Itertools as _;
 13use std::path::PathBuf;
 14
 15use workspace::{ItemId, WorkspaceDb, WorkspaceId};
 16
 17#[derive(Clone, Debug, PartialEq, Default)]
 18pub(crate) struct SerializedEditor {
 19    pub(crate) abs_path: Option<PathBuf>,
 20    pub(crate) contents: Option<String>,
 21    pub(crate) language: Option<String>,
 22    pub(crate) mtime: Option<MTime>,
 23}
 24
 25impl StaticColumnCount for SerializedEditor {
 26    fn column_count() -> usize {
 27        6
 28    }
 29}
 30
 31impl Bind for SerializedEditor {
 32    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
 33        let start_index = statement.bind(&self.abs_path, start_index)?;
 34        let start_index = statement.bind(
 35            &self
 36                .abs_path
 37                .as_ref()
 38                .map(|p| p.to_string_lossy().into_owned()),
 39            start_index,
 40        )?;
 41        let start_index = statement.bind(&self.contents, start_index)?;
 42        let start_index = statement.bind(&self.language, start_index)?;
 43
 44        let start_index = match self
 45            .mtime
 46            .and_then(|mtime| mtime.to_seconds_and_nanos_for_persistence())
 47        {
 48            Some((seconds, nanos)) => {
 49                let start_index = statement.bind(&(seconds as i64), start_index)?;
 50                statement.bind(&(nanos as i32), start_index)?
 51            }
 52            None => {
 53                let start_index = statement.bind::<Option<i64>>(&None, start_index)?;
 54                statement.bind::<Option<i32>>(&None, start_index)?
 55            }
 56        };
 57        Ok(start_index)
 58    }
 59}
 60
 61impl Column for SerializedEditor {
 62    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 63        let (abs_path, start_index): (Option<PathBuf>, i32) =
 64            Column::column(statement, start_index)?;
 65        let (_abs_path, start_index): (Option<PathBuf>, i32) =
 66            Column::column(statement, start_index)?;
 67        let (contents, start_index): (Option<String>, i32) =
 68            Column::column(statement, start_index)?;
 69        let (language, start_index): (Option<String>, i32) =
 70            Column::column(statement, start_index)?;
 71        let (mtime_seconds, start_index): (Option<i64>, i32) =
 72            Column::column(statement, start_index)?;
 73        let (mtime_nanos, start_index): (Option<i32>, i32) =
 74            Column::column(statement, start_index)?;
 75
 76        let mtime = mtime_seconds
 77            .zip(mtime_nanos)
 78            .map(|(seconds, nanos)| MTime::from_seconds_and_nanos(seconds as u64, nanos as u32));
 79
 80        let editor = Self {
 81            abs_path,
 82            contents,
 83            language,
 84            mtime,
 85        };
 86        Ok((editor, start_index))
 87    }
 88}
 89
 90pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection);
 91
 92impl Domain for EditorDb {
 93    const NAME: &str = stringify!(EditorDb);
 94
 95    // Current schema shape using pseudo-rust syntax:
 96    // editors(
 97    //   item_id: usize,
 98    //   workspace_id: usize,
 99    //   path: Option<PathBuf>,
100    //   scroll_top_row: usize,
101    //   scroll_vertical_offset: f32,
102    //   scroll_horizontal_offset: f32,
103    //   contents: Option<String>,
104    //   language: Option<String>,
105    //   mtime_seconds: Option<i64>,
106    //   mtime_nanos: Option<i32>,
107    // )
108    //
109    // editor_selections(
110    //   item_id: usize,
111    //   editor_id: usize,
112    //   workspace_id: usize,
113    //   start: usize,
114    //   end: usize,
115    // )
116    //
117    // editor_folds(
118    //   item_id: usize,
119    //   editor_id: usize,
120    //   workspace_id: usize,
121    //   start: usize,
122    //   end: usize,
123    // )
124
125    const MIGRATIONS: &[&str] = &[
126        sql! (
127            CREATE TABLE editors(
128                item_id INTEGER NOT NULL,
129                workspace_id INTEGER NOT NULL,
130                path BLOB NOT NULL,
131                PRIMARY KEY(item_id, workspace_id),
132                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
133                ON DELETE CASCADE
134                ON UPDATE CASCADE
135            ) STRICT;
136        ),
137        sql! (
138            ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
139            ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
140            ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
141        ),
142        sql! (
143            // Since sqlite3 doesn't support ALTER COLUMN, we create a new
144            // table, move the data over, drop the old table, rename new table.
145            CREATE TABLE new_editors_tmp (
146                item_id INTEGER NOT NULL,
147                workspace_id INTEGER NOT NULL,
148                path BLOB, // <-- No longer "NOT NULL"
149                scroll_top_row INTEGER NOT NULL DEFAULT 0,
150                scroll_horizontal_offset REAL NOT NULL DEFAULT 0,
151                scroll_vertical_offset REAL NOT NULL DEFAULT 0,
152                contents TEXT, // New
153                language TEXT, // New
154                PRIMARY KEY(item_id, workspace_id),
155                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
156                ON DELETE CASCADE
157                ON UPDATE CASCADE
158            ) STRICT;
159
160            INSERT INTO new_editors_tmp(item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset)
161            SELECT item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
162            FROM editors;
163
164            DROP TABLE editors;
165
166            ALTER TABLE new_editors_tmp RENAME TO editors;
167        ),
168        sql! (
169            ALTER TABLE editors ADD COLUMN mtime_seconds INTEGER DEFAULT NULL;
170            ALTER TABLE editors ADD COLUMN mtime_nanos INTEGER DEFAULT NULL;
171        ),
172        sql! (
173            CREATE TABLE editor_selections (
174                item_id INTEGER NOT NULL,
175                editor_id INTEGER NOT NULL,
176                workspace_id INTEGER NOT NULL,
177                start INTEGER NOT NULL,
178                end INTEGER NOT NULL,
179                PRIMARY KEY(item_id),
180                FOREIGN KEY(editor_id, workspace_id) REFERENCES editors(item_id, workspace_id)
181                ON DELETE CASCADE
182            ) STRICT;
183        ),
184        sql! (
185            ALTER TABLE editors ADD COLUMN buffer_path TEXT;
186            UPDATE editors SET buffer_path = CAST(path AS TEXT);
187        ),
188        sql! (
189            CREATE TABLE editor_folds (
190                item_id INTEGER NOT NULL,
191                editor_id INTEGER NOT NULL,
192                workspace_id INTEGER NOT NULL,
193                start INTEGER NOT NULL,
194                end INTEGER NOT NULL,
195                PRIMARY KEY(item_id),
196                FOREIGN KEY(editor_id, workspace_id) REFERENCES editors(item_id, workspace_id)
197                ON DELETE CASCADE
198            ) STRICT;
199        ),
200    ];
201}
202
203db::static_connection!(DB, EditorDb, [WorkspaceDb]);
204
205// https://www.sqlite.org/limits.html
206// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
207// > which defaults to <..> 32766 for SQLite versions after 3.32.0.
208const MAX_QUERY_PLACEHOLDERS: usize = 32000;
209
210impl EditorDb {
211    query! {
212        pub fn get_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<SerializedEditor>> {
213            SELECT path, buffer_path, contents, language, mtime_seconds, mtime_nanos FROM editors
214            WHERE item_id = ? AND workspace_id = ?
215        }
216    }
217
218    query! {
219        pub async fn save_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId, serialized_editor: SerializedEditor) -> Result<()> {
220            INSERT INTO editors
221                (item_id, workspace_id, path, buffer_path, contents, language, mtime_seconds, mtime_nanos)
222            VALUES
223                (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
224            ON CONFLICT DO UPDATE SET
225                item_id = ?1,
226                workspace_id = ?2,
227                path = ?3,
228                buffer_path = ?4,
229                contents = ?5,
230                language = ?6,
231                mtime_seconds = ?7,
232                mtime_nanos = ?8
233        }
234    }
235
236    // Returns the scroll top row, and offset
237    query! {
238        pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f64, f64)>> {
239            SELECT scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
240            FROM editors
241            WHERE item_id = ? AND workspace_id = ?
242        }
243    }
244
245    query! {
246        pub async fn save_scroll_position(
247            item_id: ItemId,
248            workspace_id: WorkspaceId,
249            top_row: u32,
250            vertical_offset: f64,
251            horizontal_offset: f64
252        ) -> Result<()> {
253            UPDATE OR IGNORE editors
254            SET
255                scroll_top_row = ?3,
256                scroll_horizontal_offset = ?4,
257                scroll_vertical_offset = ?5
258            WHERE item_id = ?1 AND workspace_id = ?2
259        }
260    }
261
262    query! {
263        pub fn get_editor_selections(
264            editor_id: ItemId,
265            workspace_id: WorkspaceId
266        ) -> Result<Vec<(usize, usize)>> {
267            SELECT start, end
268            FROM editor_selections
269            WHERE editor_id = ?1 AND workspace_id = ?2
270        }
271    }
272
273    query! {
274        pub fn get_editor_folds(
275            editor_id: ItemId,
276            workspace_id: WorkspaceId
277        ) -> Result<Vec<(usize, usize)>> {
278            SELECT start, end
279            FROM editor_folds
280            WHERE editor_id = ?1 AND workspace_id = ?2
281        }
282    }
283
284    pub async fn save_editor_selections(
285        &self,
286        editor_id: ItemId,
287        workspace_id: WorkspaceId,
288        selections: Vec<(usize, usize)>,
289    ) -> Result<()> {
290        log::debug!("Saving selections for editor {editor_id} in workspace {workspace_id:?}");
291        let mut first_selection;
292        let mut last_selection = 0_usize;
293        for (count, placeholders) in std::iter::once("(?1, ?2, ?, ?)")
294            .cycle()
295            .take(selections.len())
296            .chunks(MAX_QUERY_PLACEHOLDERS / 4)
297            .into_iter()
298            .map(|chunk| {
299                let mut count = 0;
300                let placeholders = chunk
301                    .inspect(|_| {
302                        count += 1;
303                    })
304                    .join(", ");
305                (count, placeholders)
306            })
307            .collect::<Vec<_>>()
308        {
309            first_selection = last_selection;
310            last_selection = last_selection + count;
311            let query = format!(
312                r#"
313DELETE FROM editor_selections WHERE editor_id = ?1 AND workspace_id = ?2;
314
315INSERT OR IGNORE INTO editor_selections (editor_id, workspace_id, start, end)
316VALUES {placeholders};
317"#
318            );
319
320            let selections = selections[first_selection..last_selection].to_vec();
321            self.write(move |conn| {
322                let mut statement = Statement::prepare(conn, query)?;
323                statement.bind(&editor_id, 1)?;
324                let mut next_index = statement.bind(&workspace_id, 2)?;
325                for (start, end) in selections {
326                    next_index = statement.bind(&start, next_index)?;
327                    next_index = statement.bind(&end, next_index)?;
328                }
329                statement.exec()
330            })
331            .await?;
332        }
333        Ok(())
334    }
335
336    pub async fn save_editor_folds(
337        &self,
338        editor_id: ItemId,
339        workspace_id: WorkspaceId,
340        folds: Vec<(usize, usize)>,
341    ) -> Result<()> {
342        log::debug!("Saving folds for editor {editor_id} in workspace {workspace_id:?}");
343        let mut first_fold;
344        let mut last_fold = 0_usize;
345        for (count, placeholders) in std::iter::once("(?1, ?2, ?, ?)")
346            .cycle()
347            .take(folds.len())
348            .chunks(MAX_QUERY_PLACEHOLDERS / 4)
349            .into_iter()
350            .map(|chunk| {
351                let mut count = 0;
352                let placeholders = chunk
353                    .inspect(|_| {
354                        count += 1;
355                    })
356                    .join(", ");
357                (count, placeholders)
358            })
359            .collect::<Vec<_>>()
360        {
361            first_fold = last_fold;
362            last_fold = last_fold + count;
363            let query = format!(
364                r#"
365DELETE FROM editor_folds WHERE editor_id = ?1 AND workspace_id = ?2;
366
367INSERT OR IGNORE INTO editor_folds (editor_id, workspace_id, start, end)
368VALUES {placeholders};
369"#
370            );
371
372            let folds = folds[first_fold..last_fold].to_vec();
373            self.write(move |conn| {
374                let mut statement = Statement::prepare(conn, query)?;
375                statement.bind(&editor_id, 1)?;
376                let mut next_index = statement.bind(&workspace_id, 2)?;
377                for (start, end) in folds {
378                    next_index = statement.bind(&start, next_index)?;
379                    next_index = statement.bind(&end, next_index)?;
380                }
381                statement.exec()
382            })
383            .await?;
384        }
385        Ok(())
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[gpui::test]
394    async fn test_save_and_get_serialized_editor() {
395        let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
396
397        let serialized_editor = SerializedEditor {
398            abs_path: Some(PathBuf::from("testing.txt")),
399            contents: None,
400            language: None,
401            mtime: None,
402        };
403
404        DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
405            .await
406            .unwrap();
407
408        let have = DB
409            .get_serialized_editor(1234, workspace_id)
410            .unwrap()
411            .unwrap();
412        assert_eq!(have, serialized_editor);
413
414        // Now update contents and language
415        let serialized_editor = SerializedEditor {
416            abs_path: Some(PathBuf::from("testing.txt")),
417            contents: Some("Test".to_owned()),
418            language: Some("Go".to_owned()),
419            mtime: None,
420        };
421
422        DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
423            .await
424            .unwrap();
425
426        let have = DB
427            .get_serialized_editor(1234, workspace_id)
428            .unwrap()
429            .unwrap();
430        assert_eq!(have, serialized_editor);
431
432        // Now set all the fields to NULL
433        let serialized_editor = SerializedEditor {
434            abs_path: None,
435            contents: None,
436            language: None,
437            mtime: None,
438        };
439
440        DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
441            .await
442            .unwrap();
443
444        let have = DB
445            .get_serialized_editor(1234, workspace_id)
446            .unwrap()
447            .unwrap();
448        assert_eq!(have, serialized_editor);
449
450        // Storing and retrieving mtime
451        let serialized_editor = SerializedEditor {
452            abs_path: None,
453            contents: None,
454            language: None,
455            mtime: Some(MTime::from_seconds_and_nanos(100, 42)),
456        };
457
458        DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
459            .await
460            .unwrap();
461
462        let have = DB
463            .get_serialized_editor(1234, workspace_id)
464            .unwrap()
465            .unwrap();
466        assert_eq!(have, serialized_editor);
467    }
468}