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    //   start_fingerprint: Option<String>,
124    //   end_fingerprint: Option<String>,
125    // )
126
127    const MIGRATIONS: &[&str] = &[
128        sql! (
129            CREATE TABLE editors(
130                item_id INTEGER NOT NULL,
131                workspace_id INTEGER NOT NULL,
132                path BLOB NOT NULL,
133                PRIMARY KEY(item_id, workspace_id),
134                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
135                ON DELETE CASCADE
136                ON UPDATE CASCADE
137            ) STRICT;
138        ),
139        sql! (
140            ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
141            ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
142            ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
143        ),
144        sql! (
145            // Since sqlite3 doesn't support ALTER COLUMN, we create a new
146            // table, move the data over, drop the old table, rename new table.
147            CREATE TABLE new_editors_tmp (
148                item_id INTEGER NOT NULL,
149                workspace_id INTEGER NOT NULL,
150                path BLOB, // <-- No longer "NOT NULL"
151                scroll_top_row INTEGER NOT NULL DEFAULT 0,
152                scroll_horizontal_offset REAL NOT NULL DEFAULT 0,
153                scroll_vertical_offset REAL NOT NULL DEFAULT 0,
154                contents TEXT, // New
155                language TEXT, // New
156                PRIMARY KEY(item_id, workspace_id),
157                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
158                ON DELETE CASCADE
159                ON UPDATE CASCADE
160            ) STRICT;
161
162            INSERT INTO new_editors_tmp(item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset)
163            SELECT item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
164            FROM editors;
165
166            DROP TABLE editors;
167
168            ALTER TABLE new_editors_tmp RENAME TO editors;
169        ),
170        sql! (
171            ALTER TABLE editors ADD COLUMN mtime_seconds INTEGER DEFAULT NULL;
172            ALTER TABLE editors ADD COLUMN mtime_nanos INTEGER DEFAULT NULL;
173        ),
174        sql! (
175            CREATE TABLE editor_selections (
176                item_id INTEGER NOT NULL,
177                editor_id INTEGER NOT NULL,
178                workspace_id INTEGER NOT NULL,
179                start INTEGER NOT NULL,
180                end INTEGER NOT NULL,
181                PRIMARY KEY(item_id),
182                FOREIGN KEY(editor_id, workspace_id) REFERENCES editors(item_id, workspace_id)
183                ON DELETE CASCADE
184            ) STRICT;
185        ),
186        sql! (
187            ALTER TABLE editors ADD COLUMN buffer_path TEXT;
188            UPDATE editors SET buffer_path = CAST(path AS TEXT);
189        ),
190        sql! (
191            CREATE TABLE editor_folds (
192                item_id INTEGER NOT NULL,
193                editor_id INTEGER NOT NULL,
194                workspace_id INTEGER NOT NULL,
195                start INTEGER NOT NULL,
196                end INTEGER NOT NULL,
197                PRIMARY KEY(item_id),
198                FOREIGN KEY(editor_id, workspace_id) REFERENCES editors(item_id, workspace_id)
199                ON DELETE CASCADE
200            ) STRICT;
201        ),
202        sql! (
203            ALTER TABLE editor_folds ADD COLUMN start_fingerprint TEXT;
204            ALTER TABLE editor_folds ADD COLUMN end_fingerprint TEXT;
205        ),
206    ];
207}
208
209db::static_connection!(DB, EditorDb, [WorkspaceDb]);
210
211// https://www.sqlite.org/limits.html
212// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
213// > which defaults to <..> 32766 for SQLite versions after 3.32.0.
214const MAX_QUERY_PLACEHOLDERS: usize = 32000;
215
216impl EditorDb {
217    query! {
218        pub fn get_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<SerializedEditor>> {
219            SELECT path, buffer_path, contents, language, mtime_seconds, mtime_nanos FROM editors
220            WHERE item_id = ? AND workspace_id = ?
221        }
222    }
223
224    query! {
225        pub async fn save_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId, serialized_editor: SerializedEditor) -> Result<()> {
226            INSERT INTO editors
227                (item_id, workspace_id, path, buffer_path, contents, language, mtime_seconds, mtime_nanos)
228            VALUES
229                (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
230            ON CONFLICT DO UPDATE SET
231                item_id = ?1,
232                workspace_id = ?2,
233                path = ?3,
234                buffer_path = ?4,
235                contents = ?5,
236                language = ?6,
237                mtime_seconds = ?7,
238                mtime_nanos = ?8
239        }
240    }
241
242    // Returns the scroll top row, and offset
243    query! {
244        pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f64, f64)>> {
245            SELECT scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
246            FROM editors
247            WHERE item_id = ? AND workspace_id = ?
248        }
249    }
250
251    query! {
252        pub async fn save_scroll_position(
253            item_id: ItemId,
254            workspace_id: WorkspaceId,
255            top_row: u32,
256            vertical_offset: f64,
257            horizontal_offset: f64
258        ) -> Result<()> {
259            UPDATE OR IGNORE editors
260            SET
261                scroll_top_row = ?3,
262                scroll_horizontal_offset = ?4,
263                scroll_vertical_offset = ?5
264            WHERE item_id = ?1 AND workspace_id = ?2
265        }
266    }
267
268    query! {
269        pub fn get_editor_selections(
270            editor_id: ItemId,
271            workspace_id: WorkspaceId
272        ) -> Result<Vec<(usize, usize)>> {
273            SELECT start, end
274            FROM editor_selections
275            WHERE editor_id = ?1 AND workspace_id = ?2
276        }
277    }
278
279    query! {
280        pub fn get_editor_folds(
281            editor_id: ItemId,
282            workspace_id: WorkspaceId
283        ) -> Result<Vec<(usize, usize, Option<String>, Option<String>)>> {
284            SELECT start, end, start_fingerprint, end_fingerprint
285            FROM editor_folds
286            WHERE editor_id = ?1 AND workspace_id = ?2
287        }
288    }
289
290    // Migrate folds from an old editor_id to a new one.
291    // This is needed because entity IDs change between sessions, but workspace
292    // cleanup deletes old editor rows (cascading to folds) before the new
293    // editor has a chance to re-save its folds.
294    //
295    // We temporarily disable FK checks because the new editor row doesn't exist
296    // yet (it gets created during workspace serialization, which runs later).
297    pub async fn migrate_editor_folds(
298        &self,
299        old_editor_id: ItemId,
300        new_editor_id: ItemId,
301        workspace_id: WorkspaceId,
302    ) -> Result<()> {
303        self.write(move |conn| {
304            let _ = conn.exec("PRAGMA foreign_keys = OFF");
305            let mut statement = Statement::prepare(
306                conn,
307                "UPDATE editor_folds SET editor_id = ?2 WHERE editor_id = ?1 AND workspace_id = ?3",
308            )?;
309            statement.bind(&old_editor_id, 1)?;
310            statement.bind(&new_editor_id, 2)?;
311            statement.bind(&workspace_id, 3)?;
312            let result = statement.exec();
313            let _ = conn.exec("PRAGMA foreign_keys = ON");
314            result
315        })
316        .await
317    }
318
319    pub async fn save_editor_selections(
320        &self,
321        editor_id: ItemId,
322        workspace_id: WorkspaceId,
323        selections: Vec<(usize, usize)>,
324    ) -> Result<()> {
325        log::debug!("Saving selections for editor {editor_id} in workspace {workspace_id:?}");
326        let mut first_selection;
327        let mut last_selection = 0_usize;
328        for (count, placeholders) in std::iter::once("(?1, ?2, ?, ?)")
329            .cycle()
330            .take(selections.len())
331            .chunks(MAX_QUERY_PLACEHOLDERS / 4)
332            .into_iter()
333            .map(|chunk| {
334                let mut count = 0;
335                let placeholders = chunk
336                    .inspect(|_| {
337                        count += 1;
338                    })
339                    .join(", ");
340                (count, placeholders)
341            })
342            .collect::<Vec<_>>()
343        {
344            first_selection = last_selection;
345            last_selection = last_selection + count;
346            let query = format!(
347                r#"
348DELETE FROM editor_selections WHERE editor_id = ?1 AND workspace_id = ?2;
349
350INSERT OR IGNORE INTO editor_selections (editor_id, workspace_id, start, end)
351VALUES {placeholders};
352"#
353            );
354
355            let selections = selections[first_selection..last_selection].to_vec();
356            self.write(move |conn| {
357                let mut statement = Statement::prepare(conn, query)?;
358                statement.bind(&editor_id, 1)?;
359                let mut next_index = statement.bind(&workspace_id, 2)?;
360                for (start, end) in selections {
361                    next_index = statement.bind(&start, next_index)?;
362                    next_index = statement.bind(&end, next_index)?;
363                }
364                statement.exec()
365            })
366            .await?;
367        }
368        Ok(())
369    }
370
371    pub async fn save_editor_folds(
372        &self,
373        editor_id: ItemId,
374        workspace_id: WorkspaceId,
375        folds: Vec<(usize, usize, String, String)>,
376    ) -> Result<()> {
377        log::debug!("Saving folds for editor {editor_id} in workspace {workspace_id:?}");
378        let mut first_fold;
379        let mut last_fold = 0_usize;
380        for (count, placeholders) in std::iter::once("(?1, ?2, ?, ?, ?, ?)")
381            .cycle()
382            .take(folds.len())
383            .chunks(MAX_QUERY_PLACEHOLDERS / 6)
384            .into_iter()
385            .map(|chunk| {
386                let mut count = 0;
387                let placeholders = chunk
388                    .inspect(|_| {
389                        count += 1;
390                    })
391                    .join(", ");
392                (count, placeholders)
393            })
394            .collect::<Vec<_>>()
395        {
396            first_fold = last_fold;
397            last_fold = last_fold + count;
398            let query = format!(
399                r#"
400DELETE FROM editor_folds WHERE editor_id = ?1 AND workspace_id = ?2;
401
402INSERT OR IGNORE INTO editor_folds (editor_id, workspace_id, start, end, start_fingerprint, end_fingerprint)
403VALUES {placeholders};
404"#
405            );
406
407            let folds = folds[first_fold..last_fold].to_vec();
408            self.write(move |conn| {
409                let mut statement = Statement::prepare(conn, query)?;
410                statement.bind(&editor_id, 1)?;
411                let mut next_index = statement.bind(&workspace_id, 2)?;
412                for (start, end, start_fp, end_fp) in folds {
413                    next_index = statement.bind(&start, next_index)?;
414                    next_index = statement.bind(&end, next_index)?;
415                    next_index = statement.bind(&start_fp, next_index)?;
416                    next_index = statement.bind(&end_fp, next_index)?;
417                }
418                statement.exec()
419            })
420            .await?;
421        }
422        Ok(())
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429
430    #[gpui::test]
431    async fn test_save_and_get_serialized_editor() {
432        let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
433
434        let serialized_editor = SerializedEditor {
435            abs_path: Some(PathBuf::from("testing.txt")),
436            contents: None,
437            language: None,
438            mtime: None,
439        };
440
441        DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
442            .await
443            .unwrap();
444
445        let have = DB
446            .get_serialized_editor(1234, workspace_id)
447            .unwrap()
448            .unwrap();
449        assert_eq!(have, serialized_editor);
450
451        // Now update contents and language
452        let serialized_editor = SerializedEditor {
453            abs_path: Some(PathBuf::from("testing.txt")),
454            contents: Some("Test".to_owned()),
455            language: Some("Go".to_owned()),
456            mtime: None,
457        };
458
459        DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
460            .await
461            .unwrap();
462
463        let have = DB
464            .get_serialized_editor(1234, workspace_id)
465            .unwrap()
466            .unwrap();
467        assert_eq!(have, serialized_editor);
468
469        // Now set all the fields to NULL
470        let serialized_editor = SerializedEditor {
471            abs_path: None,
472            contents: None,
473            language: None,
474            mtime: None,
475        };
476
477        DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
478            .await
479            .unwrap();
480
481        let have = DB
482            .get_serialized_editor(1234, workspace_id)
483            .unwrap()
484            .unwrap();
485        assert_eq!(have, serialized_editor);
486
487        // Storing and retrieving mtime
488        let serialized_editor = SerializedEditor {
489            abs_path: None,
490            contents: None,
491            language: None,
492            mtime: Some(MTime::from_seconds_and_nanos(100, 42)),
493        };
494
495        DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
496            .await
497            .unwrap();
498
499        let have = DB
500            .get_serialized_editor(1234, workspace_id)
501            .unwrap()
502            .unwrap();
503        assert_eq!(have, serialized_editor);
504    }
505
506    #[gpui::test]
507    async fn test_save_and_get_editor_folds_with_fingerprints() {
508        let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
509
510        // First create an editor entry (folds have FK to editors)
511        let serialized_editor = SerializedEditor {
512            abs_path: Some(PathBuf::from("test_folds.txt")),
513            contents: None,
514            language: None,
515            mtime: None,
516        };
517        DB.save_serialized_editor(5678, workspace_id, serialized_editor)
518            .await
519            .unwrap();
520
521        // Save folds with fingerprints (32-byte content samples at fold boundaries)
522        let folds = vec![
523            (
524                100,
525                200,
526                "fn main() {".to_string(),
527                "} // end main".to_string(),
528            ),
529            (
530                300,
531                400,
532                "struct Foo {".to_string(),
533                "} // end Foo".to_string(),
534            ),
535        ];
536        DB.save_editor_folds(5678, workspace_id, folds.clone())
537            .await
538            .unwrap();
539
540        // Retrieve and verify fingerprints are preserved
541        let retrieved = DB.get_editor_folds(5678, workspace_id).unwrap();
542        assert_eq!(retrieved.len(), 2);
543        assert_eq!(
544            retrieved[0],
545            (
546                100,
547                200,
548                Some("fn main() {".to_string()),
549                Some("} // end main".to_string())
550            )
551        );
552        assert_eq!(
553            retrieved[1],
554            (
555                300,
556                400,
557                Some("struct Foo {".to_string()),
558                Some("} // end Foo".to_string())
559            )
560        );
561
562        // Test overwrite: saving new folds replaces old ones
563        let new_folds = vec![(
564            500,
565            600,
566            "impl Bar {".to_string(),
567            "} // end impl".to_string(),
568        )];
569        DB.save_editor_folds(5678, workspace_id, new_folds)
570            .await
571            .unwrap();
572
573        let retrieved = DB.get_editor_folds(5678, workspace_id).unwrap();
574        assert_eq!(retrieved.len(), 1);
575        assert_eq!(
576            retrieved[0],
577            (
578                500,
579                600,
580                Some("impl Bar {".to_string()),
581                Some("} // end impl".to_string())
582            )
583        );
584    }
585
586    // NOTE: The fingerprint search logic (finding content at new offsets when file
587    // is modified externally) is in editor.rs:restore_from_db and requires a full
588    // Editor context to test. Manual testing procedure:
589    // 1. Open a file, fold some sections, close Zed
590    // 2. Add text at the START of the file externally (shifts all offsets)
591    // 3. Reopen Zed - folds should be restored at their NEW correct positions
592    // The search uses contains_str_at() to find fingerprints in the buffer.
593}