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