persistence.rs

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