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    pub static ref DB: EditorDb<WorkspaceDb> = &[
101        sql! (
102            CREATE TABLE editors(
103                item_id INTEGER NOT NULL,
104                workspace_id INTEGER NOT NULL,
105                path BLOB NOT NULL,
106                PRIMARY KEY(item_id, workspace_id),
107                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
108                ON DELETE CASCADE
109                ON UPDATE CASCADE
110            ) STRICT;
111        ),
112        sql! (
113            ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
114            ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
115            ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
116        ),
117        sql! (
118            // Since sqlite3 doesn't support ALTER COLUMN, we create a new
119            // table, move the data over, drop the old table, rename new table.
120            CREATE TABLE new_editors_tmp (
121                item_id INTEGER NOT NULL,
122                workspace_id INTEGER NOT NULL,
123                path BLOB, // <-- No longer "NOT NULL"
124                scroll_top_row INTEGER NOT NULL DEFAULT 0,
125                scroll_horizontal_offset REAL NOT NULL DEFAULT 0,
126                scroll_vertical_offset REAL NOT NULL DEFAULT 0,
127                contents TEXT, // New
128                language TEXT, // New
129                PRIMARY KEY(item_id, workspace_id),
130                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
131                ON DELETE CASCADE
132                ON UPDATE CASCADE
133            ) STRICT;
134
135            INSERT INTO new_editors_tmp(item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset)
136            SELECT item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
137            FROM editors;
138
139            DROP TABLE editors;
140
141            ALTER TABLE new_editors_tmp RENAME TO editors;
142        ),
143        sql! (
144            ALTER TABLE editors ADD COLUMN mtime_seconds INTEGER DEFAULT NULL;
145            ALTER TABLE editors ADD COLUMN mtime_nanos INTEGER DEFAULT NULL;
146        ),
147        sql! (
148            CREATE TABLE editor_selections (
149                item_id INTEGER NOT NULL,
150                editor_id INTEGER NOT NULL,
151                workspace_id INTEGER NOT NULL,
152                start INTEGER NOT NULL,
153                end INTEGER NOT NULL,
154                PRIMARY KEY(item_id),
155                FOREIGN KEY(editor_id, workspace_id) REFERENCES editors(item_id, workspace_id)
156                ON DELETE CASCADE
157            ) STRICT;
158        ),
159        sql! (
160            ALTER TABLE editors ADD COLUMN buffer_path TEXT;
161            UPDATE editors SET buffer_path = CAST(path AS TEXT);
162        ),
163    ];
164);
165
166// https://www.sqlite.org/limits.html
167// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
168// > which defaults to <..> 32766 for SQLite versions after 3.32.0.
169const MAX_QUERY_PLACEHOLDERS: usize = 32000;
170
171impl EditorDb {
172    query! {
173        pub fn get_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<SerializedEditor>> {
174            SELECT path, buffer_path, contents, language, mtime_seconds, mtime_nanos FROM editors
175            WHERE item_id = ? AND workspace_id = ?
176        }
177    }
178
179    query! {
180        pub async fn save_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId, serialized_editor: SerializedEditor) -> Result<()> {
181            INSERT INTO editors
182                (item_id, workspace_id, path, buffer_path, contents, language, mtime_seconds, mtime_nanos)
183            VALUES
184                (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
185            ON CONFLICT DO UPDATE SET
186                item_id = ?1,
187                workspace_id = ?2,
188                path = ?3,
189                buffer_path = ?4,
190                contents = ?5,
191                language = ?6,
192                mtime_seconds = ?7,
193                mtime_nanos = ?8
194        }
195    }
196
197    // Returns the scroll top row, and offset
198    query! {
199        pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
200            SELECT scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
201            FROM editors
202            WHERE item_id = ? AND workspace_id = ?
203        }
204    }
205
206    query! {
207        pub async fn save_scroll_position(
208            item_id: ItemId,
209            workspace_id: WorkspaceId,
210            top_row: u32,
211            vertical_offset: f32,
212            horizontal_offset: f32
213        ) -> Result<()> {
214            UPDATE OR IGNORE editors
215            SET
216                scroll_top_row = ?3,
217                scroll_horizontal_offset = ?4,
218                scroll_vertical_offset = ?5
219            WHERE item_id = ?1 AND workspace_id = ?2
220        }
221    }
222
223    query! {
224        pub fn get_editor_selections(
225            editor_id: ItemId,
226            workspace_id: WorkspaceId
227        ) -> Result<Vec<(usize, usize)>> {
228            SELECT start, end
229            FROM editor_selections
230            WHERE editor_id = ?1 AND workspace_id = ?2
231        }
232    }
233
234    pub async fn save_editor_selections(
235        &self,
236        editor_id: ItemId,
237        workspace_id: WorkspaceId,
238        selections: Vec<(usize, usize)>,
239    ) -> Result<()> {
240        let mut first_selection;
241        let mut last_selection = 0_usize;
242        for (count, placeholders) in std::iter::once("(?1, ?2, ?, ?)")
243            .cycle()
244            .take(selections.len())
245            .chunks(MAX_QUERY_PLACEHOLDERS / 4)
246            .into_iter()
247            .map(|chunk| {
248                let mut count = 0;
249                let placeholders = chunk
250                    .inspect(|_| {
251                        count += 1;
252                    })
253                    .join(", ");
254                (count, placeholders)
255            })
256            .collect::<Vec<_>>()
257        {
258            first_selection = last_selection;
259            last_selection = last_selection + count;
260            let query = format!(
261                r#"
262DELETE FROM editor_selections WHERE editor_id = ?1 AND workspace_id = ?2;
263
264INSERT OR IGNORE INTO editor_selections (editor_id, workspace_id, start, end)
265VALUES {placeholders};
266"#
267            );
268
269            let selections = selections[first_selection..last_selection].to_vec();
270            self.write(move |conn| {
271                let mut statement = Statement::prepare(conn, query)?;
272                statement.bind(&editor_id, 1)?;
273                let mut next_index = statement.bind(&workspace_id, 2)?;
274                for (start, end) in selections {
275                    next_index = statement.bind(&start, next_index)?;
276                    next_index = statement.bind(&end, next_index)?;
277                }
278                statement.exec()
279            })
280            .await?;
281        }
282        Ok(())
283    }
284
285    pub async fn delete_unloaded_items(
286        &self,
287        workspace: WorkspaceId,
288        alive_items: Vec<ItemId>,
289    ) -> Result<()> {
290        let placeholders = alive_items
291            .iter()
292            .map(|_| "?")
293            .collect::<Vec<&str>>()
294            .join(", ");
295
296        let query = format!(
297            "DELETE FROM editors WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
298        );
299
300        self.write(move |conn| {
301            let mut statement = Statement::prepare(conn, query)?;
302            let mut next_index = statement.bind(&workspace, 1)?;
303            for id in alive_items {
304                next_index = statement.bind(&id, next_index)?;
305            }
306            statement.exec()
307        })
308        .await
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    #[gpui::test]
317    async fn test_save_and_get_serialized_editor() {
318        let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
319
320        let serialized_editor = SerializedEditor {
321            abs_path: Some(PathBuf::from("testing.txt")),
322            contents: None,
323            language: None,
324            mtime: None,
325        };
326
327        DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
328            .await
329            .unwrap();
330
331        let have = DB
332            .get_serialized_editor(1234, workspace_id)
333            .unwrap()
334            .unwrap();
335        assert_eq!(have, serialized_editor);
336
337        // Now update contents and language
338        let serialized_editor = SerializedEditor {
339            abs_path: Some(PathBuf::from("testing.txt")),
340            contents: Some("Test".to_owned()),
341            language: Some("Go".to_owned()),
342            mtime: None,
343        };
344
345        DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
346            .await
347            .unwrap();
348
349        let have = DB
350            .get_serialized_editor(1234, workspace_id)
351            .unwrap()
352            .unwrap();
353        assert_eq!(have, serialized_editor);
354
355        // Now set all the fields to NULL
356        let serialized_editor = SerializedEditor {
357            abs_path: None,
358            contents: None,
359            language: None,
360            mtime: None,
361        };
362
363        DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
364            .await
365            .unwrap();
366
367        let have = DB
368            .get_serialized_editor(1234, workspace_id)
369            .unwrap()
370            .unwrap();
371        assert_eq!(have, serialized_editor);
372
373        // Storing and retrieving mtime
374        let serialized_editor = SerializedEditor {
375            abs_path: None,
376            contents: None,
377            language: None,
378            mtime: Some(MTime::from_seconds_and_nanos(100, 42)),
379        };
380
381        DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
382            .await
383            .unwrap();
384
385        let have = DB
386            .get_serialized_editor(1234, workspace_id)
387            .unwrap()
388            .unwrap();
389        assert_eq!(have, serialized_editor);
390    }
391}