Fix workspace migration failure (#36911)

Max Brunsfeld created

This fixes a regression on nightly introduced in
https://github.com/zed-industries/zed/pull/36714

Release Notes:

- N/A

Change summary

crates/command_palette/src/persistence.rs           |  18 
crates/db/src/db.rs                                 | 118 --
crates/db/src/kvp.rs                                |  30 
crates/editor/src/persistence.rs                    |  27 
crates/image_viewer/src/image_viewer.rs             |  19 
crates/onboarding/src/onboarding.rs                 |  21 
crates/onboarding/src/welcome.rs                    |  21 
crates/settings_ui/src/keybindings.rs               |  15 
crates/sqlez/src/domain.rs                          |  14 
crates/sqlez/src/migrations.rs                      |  64 +
crates/sqlez/src/thread_safe_connection.rs          |  18 
crates/terminal_view/src/persistence.rs             |  18 
crates/vim/src/state.rs                             |  18 
crates/workspace/src/path_list.rs                   |  14 
crates/workspace/src/persistence.rs                 | 655 +++++++-------
crates/zed/src/zed/component_preview/persistence.rs |  19 
16 files changed, 588 insertions(+), 501 deletions(-)

Detailed changes

crates/command_palette/src/persistence.rs 🔗

@@ -1,7 +1,10 @@
 use anyhow::Result;
 use db::{
-    define_connection, query,
-    sqlez::{bindable::Column, statement::Statement},
+    query,
+    sqlez::{
+        bindable::Column, domain::Domain, statement::Statement,
+        thread_safe_connection::ThreadSafeConnection,
+    },
     sqlez_macros::sql,
 };
 use serde::{Deserialize, Serialize};
@@ -50,8 +53,11 @@ impl Column for SerializedCommandInvocation {
     }
 }
 
-define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> =
-    &[sql!(
+pub struct CommandPaletteDB(ThreadSafeConnection);
+
+impl Domain for CommandPaletteDB {
+    const NAME: &str = stringify!(CommandPaletteDB);
+    const MIGRATIONS: &[&str] = &[sql!(
         CREATE TABLE IF NOT EXISTS command_invocations(
             id INTEGER PRIMARY KEY AUTOINCREMENT,
             command_name TEXT NOT NULL,
@@ -59,7 +65,9 @@ define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()>
             last_invoked INTEGER DEFAULT (unixepoch())  NOT NULL
         ) STRICT;
     )];
-);
+}
+
+db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []);
 
 impl CommandPaletteDB {
     pub async fn write_command_invocation(

crates/db/src/db.rs 🔗

@@ -110,11 +110,14 @@ pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection {
 }
 
 /// Implements a basic DB wrapper for a given domain
+///
+/// Arguments:
+/// - static variable name for connection
+/// - type of connection wrapper
+/// - dependencies, whose migrations should be run prior to this domain's migrations
 #[macro_export]
-macro_rules! define_connection {
-    (pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => {
-        pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
-
+macro_rules! static_connection {
+    ($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => {
         impl ::std::ops::Deref for $t {
             type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
 
@@ -123,16 +126,6 @@ macro_rules! define_connection {
             }
         }
 
-        impl $crate::sqlez::domain::Domain for $t {
-            fn name() -> &'static str {
-                stringify!($t)
-            }
-
-            fn migrations() -> &'static [&'static str] {
-                $migrations
-            }
-        }
-
         impl $t {
             #[cfg(any(test, feature = "test-support"))]
             pub async fn open_test_db(name: &'static str) -> Self {
@@ -142,44 +135,8 @@ macro_rules! define_connection {
 
         #[cfg(any(test, feature = "test-support"))]
         pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
-            $t($crate::smol::block_on($crate::open_test_db::<$t>(stringify!($id))))
-        });
-
-        #[cfg(not(any(test, feature = "test-support")))]
-        pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
-            let db_dir = $crate::database_dir();
-            let scope = if false $(|| stringify!($global) == "global")? {
-                "global"
-            } else {
-                $crate::RELEASE_CHANNEL.dev_name()
-            };
-            $t($crate::smol::block_on($crate::open_db::<$t>(db_dir, scope)))
-        });
-    };
-    (pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr; $($global:ident)?) => {
-        pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
-
-        impl ::std::ops::Deref for $t {
-            type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
-
-            fn deref(&self) -> &Self::Target {
-                &self.0
-            }
-        }
-
-        impl $crate::sqlez::domain::Domain for $t {
-            fn name() -> &'static str {
-                stringify!($t)
-            }
-
-            fn migrations() -> &'static [&'static str] {
-                $migrations
-            }
-        }
-
-        #[cfg(any(test, feature = "test-support"))]
-        pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
-            $t($crate::smol::block_on($crate::open_test_db::<($($d),+, $t)>(stringify!($id))))
+            #[allow(unused_parens)]
+            $t($crate::smol::block_on($crate::open_test_db::<($($d,)* $t)>(stringify!($id))))
         });
 
         #[cfg(not(any(test, feature = "test-support")))]
@@ -190,9 +147,10 @@ macro_rules! define_connection {
             } else {
                 $crate::RELEASE_CHANNEL.dev_name()
             };
-            $t($crate::smol::block_on($crate::open_db::<($($d),+, $t)>(db_dir, scope)))
+            #[allow(unused_parens)]
+            $t($crate::smol::block_on($crate::open_db::<($($d,)* $t)>(db_dir, scope)))
         });
-    };
+    }
 }
 
 pub fn write_and_log<F>(cx: &App, db_write: impl FnOnce() -> F + Send + 'static)
@@ -219,17 +177,12 @@ mod tests {
         enum BadDB {}
 
         impl Domain for BadDB {
-            fn name() -> &'static str {
-                "db_tests"
-            }
-
-            fn migrations() -> &'static [&'static str] {
-                &[
-                    sql!(CREATE TABLE test(value);),
-                    // failure because test already exists
-                    sql!(CREATE TABLE test(value);),
-                ]
-            }
+            const NAME: &str = "db_tests";
+            const MIGRATIONS: &[&str] = &[
+                sql!(CREATE TABLE test(value);),
+                // failure because test already exists
+                sql!(CREATE TABLE test(value);),
+            ];
         }
 
         let tempdir = tempfile::Builder::new()
@@ -251,25 +204,15 @@ mod tests {
         enum CorruptedDB {}
 
         impl Domain for CorruptedDB {
-            fn name() -> &'static str {
-                "db_tests"
-            }
-
-            fn migrations() -> &'static [&'static str] {
-                &[sql!(CREATE TABLE test(value);)]
-            }
+            const NAME: &str = "db_tests";
+            const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
         }
 
         enum GoodDB {}
 
         impl Domain for GoodDB {
-            fn name() -> &'static str {
-                "db_tests" //Notice same name
-            }
-
-            fn migrations() -> &'static [&'static str] {
-                &[sql!(CREATE TABLE test2(value);)] //But different migration
-            }
+            const NAME: &str = "db_tests"; //Notice same name
+            const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)];
         }
 
         let tempdir = tempfile::Builder::new()
@@ -305,25 +248,16 @@ mod tests {
         enum CorruptedDB {}
 
         impl Domain for CorruptedDB {
-            fn name() -> &'static str {
-                "db_tests"
-            }
+            const NAME: &str = "db_tests";
 
-            fn migrations() -> &'static [&'static str] {
-                &[sql!(CREATE TABLE test(value);)]
-            }
+            const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
         }
 
         enum GoodDB {}
 
         impl Domain for GoodDB {
-            fn name() -> &'static str {
-                "db_tests" //Notice same name
-            }
-
-            fn migrations() -> &'static [&'static str] {
-                &[sql!(CREATE TABLE test2(value);)] //But different migration
-            }
+            const NAME: &str = "db_tests"; //Notice same name
+            const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration
         }
 
         let tempdir = tempfile::Builder::new()

crates/db/src/kvp.rs 🔗

@@ -2,16 +2,26 @@ use gpui::App;
 use sqlez_macros::sql;
 use util::ResultExt as _;
 
-use crate::{define_connection, query, write_and_log};
+use crate::{
+    query,
+    sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
+    write_and_log,
+};
 
-define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
-    &[sql!(
+pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection);
+
+impl Domain for KeyValueStore {
+    const NAME: &str = stringify!(KeyValueStore);
+
+    const MIGRATIONS: &[&str] = &[sql!(
         CREATE TABLE IF NOT EXISTS kv_store(
             key TEXT PRIMARY KEY,
             value TEXT NOT NULL
         ) STRICT;
     )];
-);
+}
+
+crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []);
 
 pub trait Dismissable {
     const KEY: &'static str;
@@ -91,15 +101,19 @@ mod tests {
     }
 }
 
-define_connection!(pub static ref GLOBAL_KEY_VALUE_STORE: GlobalKeyValueStore<()> =
-    &[sql!(
+pub struct GlobalKeyValueStore(ThreadSafeConnection);
+
+impl Domain for GlobalKeyValueStore {
+    const NAME: &str = stringify!(GlobalKeyValueStore);
+    const MIGRATIONS: &[&str] = &[sql!(
         CREATE TABLE IF NOT EXISTS kv_store(
             key TEXT PRIMARY KEY,
             value TEXT NOT NULL
         ) STRICT;
     )];
-    global
-);
+}
+
+crate::static_connection!(GLOBAL_KEY_VALUE_STORE, GlobalKeyValueStore, [], global);
 
 impl GlobalKeyValueStore {
     query! {

crates/editor/src/persistence.rs 🔗

@@ -1,13 +1,17 @@
 use anyhow::Result;
-use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
-use db::sqlez::statement::Statement;
+use db::{
+    query,
+    sqlez::{
+        bindable::{Bind, Column, StaticColumnCount},
+        domain::Domain,
+        statement::Statement,
+    },
+    sqlez_macros::sql,
+};
 use fs::MTime;
 use itertools::Itertools as _;
 use std::path::PathBuf;
 
-use db::sqlez_macros::sql;
-use db::{define_connection, query};
-
 use workspace::{ItemId, WorkspaceDb, WorkspaceId};
 
 #[derive(Clone, Debug, PartialEq, Default)]
@@ -83,7 +87,11 @@ impl Column for SerializedEditor {
     }
 }
 
-define_connection!(
+pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection);
+
+impl Domain for EditorDb {
+    const NAME: &str = stringify!(EditorDb);
+
     // Current schema shape using pseudo-rust syntax:
     // editors(
     //   item_id: usize,
@@ -113,7 +121,8 @@ define_connection!(
     //   start: usize,
     //   end: usize,
     // )
-    pub static ref DB: EditorDb<WorkspaceDb> = &[
+
+    const MIGRATIONS: &[&str] = &[
         sql! (
             CREATE TABLE editors(
                 item_id INTEGER NOT NULL,
@@ -189,7 +198,9 @@ define_connection!(
             ) STRICT;
         ),
     ];
-);
+}
+
+db::static_connection!(DB, EditorDb, [WorkspaceDb]);
 
 // https://www.sqlite.org/limits.html
 // > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,

crates/image_viewer/src/image_viewer.rs 🔗

@@ -401,12 +401,19 @@ pub fn init(cx: &mut App) {
 mod persistence {
     use std::path::PathBuf;
 
-    use db::{define_connection, query, sqlez_macros::sql};
+    use db::{
+        query,
+        sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
+        sqlez_macros::sql,
+    };
     use workspace::{ItemId, WorkspaceDb, WorkspaceId};
 
-    define_connection! {
-        pub static ref IMAGE_VIEWER: ImageViewerDb<WorkspaceDb> =
-            &[sql!(
+    pub struct ImageViewerDb(ThreadSafeConnection);
+
+    impl Domain for ImageViewerDb {
+        const NAME: &str = stringify!(ImageViewerDb);
+
+        const MIGRATIONS: &[&str] = &[sql!(
                 CREATE TABLE image_viewers (
                     workspace_id INTEGER,
                     item_id INTEGER UNIQUE,
@@ -417,9 +424,11 @@ mod persistence {
                     FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
                     ON DELETE CASCADE
                 ) STRICT;
-            )];
+        )];
     }
 
+    db::static_connection!(IMAGE_VIEWER, ImageViewerDb, [WorkspaceDb]);
+
     impl ImageViewerDb {
         query! {
             pub async fn save_image_path(

crates/onboarding/src/onboarding.rs 🔗

@@ -850,13 +850,19 @@ impl workspace::SerializableItem for Onboarding {
 }
 
 mod persistence {
-    use db::{define_connection, query, sqlez_macros::sql};
+    use db::{
+        query,
+        sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
+        sqlez_macros::sql,
+    };
     use workspace::WorkspaceDb;
 
-    define_connection! {
-        pub static ref ONBOARDING_PAGES: OnboardingPagesDb<WorkspaceDb> =
-            &[
-                sql!(
+    pub struct OnboardingPagesDb(ThreadSafeConnection);
+
+    impl Domain for OnboardingPagesDb {
+        const NAME: &str = stringify!(OnboardingPagesDb);
+
+        const MIGRATIONS: &[&str] = &[sql!(
                     CREATE TABLE onboarding_pages (
                         workspace_id INTEGER,
                         item_id INTEGER UNIQUE,
@@ -866,10 +872,11 @@ mod persistence {
                         FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
                         ON DELETE CASCADE
                     ) STRICT;
-                ),
-            ];
+        )];
     }
 
+    db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]);
+
     impl OnboardingPagesDb {
         query! {
             pub async fn save_onboarding_page(

crates/onboarding/src/welcome.rs 🔗

@@ -414,13 +414,19 @@ impl workspace::SerializableItem for WelcomePage {
 }
 
 mod persistence {
-    use db::{define_connection, query, sqlez_macros::sql};
+    use db::{
+        query,
+        sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
+        sqlez_macros::sql,
+    };
     use workspace::WorkspaceDb;
 
-    define_connection! {
-        pub static ref WELCOME_PAGES: WelcomePagesDb<WorkspaceDb> =
-            &[
-                sql!(
+    pub struct WelcomePagesDb(ThreadSafeConnection);
+
+    impl Domain for WelcomePagesDb {
+        const NAME: &str = stringify!(WelcomePagesDb);
+
+        const MIGRATIONS: &[&str] = (&[sql!(
                     CREATE TABLE welcome_pages (
                         workspace_id INTEGER,
                         item_id INTEGER UNIQUE,
@@ -430,10 +436,11 @@ mod persistence {
                         FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
                         ON DELETE CASCADE
                     ) STRICT;
-                ),
-            ];
+        )]);
     }
 
+    db::static_connection!(WELCOME_PAGES, WelcomePagesDb, [WorkspaceDb]);
+
     impl WelcomePagesDb {
         query! {
             pub async fn save_welcome_page(

crates/settings_ui/src/keybindings.rs 🔗

@@ -3348,12 +3348,15 @@ impl SerializableItem for KeymapEditor {
 }
 
 mod persistence {
-    use db::{define_connection, query, sqlez_macros::sql};
+    use db::{query, sqlez::domain::Domain, sqlez_macros::sql};
     use workspace::WorkspaceDb;
 
-    define_connection! {
-        pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
-            &[sql!(
+    pub struct KeybindingEditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection);
+
+    impl Domain for KeybindingEditorDb {
+        const NAME: &str = stringify!(KeybindingEditorDb);
+
+        const MIGRATIONS: &[&str] = &[sql!(
                 CREATE TABLE keybinding_editors (
                     workspace_id INTEGER,
                     item_id INTEGER UNIQUE,
@@ -3362,9 +3365,11 @@ mod persistence {
                     FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
                     ON DELETE CASCADE
                 ) STRICT;
-            )];
+        )];
     }
 
+    db::static_connection!(KEYBINDING_EDITORS, KeybindingEditorDb, [WorkspaceDb]);
+
     impl KeybindingEditorDb {
         query! {
             pub async fn save_keybinding_editor(

crates/sqlez/src/domain.rs 🔗

@@ -1,8 +1,12 @@
 use crate::connection::Connection;
 
 pub trait Domain: 'static {
-    fn name() -> &'static str;
-    fn migrations() -> &'static [&'static str];
+    const NAME: &str;
+    const MIGRATIONS: &[&str];
+
+    fn should_allow_migration_change(_index: usize, _old: &str, _new: &str) -> bool {
+        false
+    }
 }
 
 pub trait Migrator: 'static {
@@ -17,7 +21,11 @@ impl Migrator for () {
 
 impl<D: Domain> Migrator for D {
     fn migrate(connection: &Connection) -> anyhow::Result<()> {
-        connection.migrate(Self::name(), Self::migrations())
+        connection.migrate(
+            Self::NAME,
+            Self::MIGRATIONS,
+            Self::should_allow_migration_change,
+        )
     }
 }
 

crates/sqlez/src/migrations.rs 🔗

@@ -34,7 +34,12 @@ impl Connection {
     /// Note: Unlike everything else in SQLez, migrations are run eagerly, without first
     /// preparing the SQL statements. This makes it possible to do multi-statement schema
     /// updates in a single string without running into prepare errors.
-    pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> {
+    pub fn migrate(
+        &self,
+        domain: &'static str,
+        migrations: &[&'static str],
+        mut should_allow_migration_change: impl FnMut(usize, &str, &str) -> bool,
+    ) -> Result<()> {
         self.with_savepoint("migrating", || {
             // Setup the migrations table unconditionally
             self.exec(indoc! {"
@@ -65,9 +70,14 @@ impl Connection {
                         &sqlformat::QueryParams::None,
                         Default::default(),
                     );
-                    if completed_migration == migration {
+                    if completed_migration == migration
+                        || migration.trim().starts_with("-- ALLOW_MIGRATION_CHANGE")
+                    {
                         // Migration already run. Continue
                         continue;
+                    } else if should_allow_migration_change(index, &completed_migration, &migration)
+                    {
+                        continue;
                     } else {
                         anyhow::bail!(formatdoc! {"
                             Migration changed for {domain} at step {index}
@@ -108,6 +118,7 @@ mod test {
                     a TEXT,
                     b TEXT
                 )"}],
+                disallow_migration_change,
             )
             .unwrap();
 
@@ -136,6 +147,7 @@ mod test {
                         d TEXT
                     )"},
                 ],
+                disallow_migration_change,
             )
             .unwrap();
 
@@ -214,7 +226,11 @@ mod test {
 
         // Run the migration verifying that the row got dropped
         connection
-            .migrate("test", &["DELETE FROM test_table"])
+            .migrate(
+                "test",
+                &["DELETE FROM test_table"],
+                disallow_migration_change,
+            )
             .unwrap();
         assert_eq!(
             connection
@@ -232,7 +248,11 @@ mod test {
 
         // Run the same migration again and verify that the table was left unchanged
         connection
-            .migrate("test", &["DELETE FROM test_table"])
+            .migrate(
+                "test",
+                &["DELETE FROM test_table"],
+                disallow_migration_change,
+            )
             .unwrap();
         assert_eq!(
             connection
@@ -252,27 +272,28 @@ mod test {
             .migrate(
                 "test migration",
                 &[
-                    indoc! {"
-                CREATE TABLE test (
-                    col INTEGER
-                )"},
-                    indoc! {"
-                    INSERT INTO test (col) VALUES (1)"},
+                    "CREATE TABLE test (col INTEGER)",
+                    "INSERT INTO test (col) VALUES (1)",
                 ],
+                disallow_migration_change,
             )
             .unwrap();
 
+        let mut migration_changed = false;
+
         // Create another migration with the same domain but different steps
         let second_migration_result = connection.migrate(
             "test migration",
             &[
-                indoc! {"
-                CREATE TABLE test (
-                    color INTEGER
-                )"},
-                indoc! {"
-                INSERT INTO test (color) VALUES (1)"},
+                "CREATE TABLE test (color INTEGER )",
+                "INSERT INTO test (color) VALUES (1)",
             ],
+            |_, old, new| {
+                assert_eq!(old, "CREATE TABLE test (col INTEGER)");
+                assert_eq!(new, "CREATE TABLE test (color INTEGER)");
+                migration_changed = true;
+                false
+            },
         );
 
         // Verify new migration returns error when run
@@ -284,7 +305,11 @@ mod test {
         let connection = Connection::open_memory(Some("test_create_alter_drop"));
 
         connection
-            .migrate("first_migration", &["CREATE TABLE table1(a TEXT) STRICT;"])
+            .migrate(
+                "first_migration",
+                &["CREATE TABLE table1(a TEXT) STRICT;"],
+                disallow_migration_change,
+            )
             .unwrap();
 
         connection
@@ -305,6 +330,7 @@ mod test {
 
                     ALTER TABLE table2 RENAME TO table1;
                 "}],
+                disallow_migration_change,
             )
             .unwrap();
 
@@ -312,4 +338,8 @@ mod test {
 
         assert_eq!(res, "test text");
     }
+
+    fn disallow_migration_change(_: usize, _: &str, _: &str) -> bool {
+        false
+    }
 }

crates/sqlez/src/thread_safe_connection.rs 🔗

@@ -278,12 +278,8 @@ mod test {
 
         enum TestDomain {}
         impl Domain for TestDomain {
-            fn name() -> &'static str {
-                "test"
-            }
-            fn migrations() -> &'static [&'static str] {
-                &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"]
-            }
+            const NAME: &str = "test";
+            const MIGRATIONS: &[&str] = &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"];
         }
 
         for _ in 0..100 {
@@ -312,12 +308,9 @@ mod test {
     fn wild_zed_lost_failure() {
         enum TestWorkspace {}
         impl Domain for TestWorkspace {
-            fn name() -> &'static str {
-                "workspace"
-            }
+            const NAME: &str = "workspace";
 
-            fn migrations() -> &'static [&'static str] {
-                &["
+            const MIGRATIONS: &[&str] = &["
                     CREATE TABLE workspaces(
                         workspace_id INTEGER PRIMARY KEY,
                         dock_visible INTEGER, -- Boolean
@@ -336,8 +329,7 @@ mod test {
                             ON DELETE CASCADE
                             ON UPDATE CASCADE
                     ) STRICT;
-                "]
-            }
+                "];
         }
 
         let builder =

crates/terminal_view/src/persistence.rs 🔗

@@ -9,7 +9,11 @@ use std::path::{Path, PathBuf};
 use ui::{App, Context, Pixels, Window};
 use util::ResultExt as _;
 
-use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql};
+use db::{
+    query,
+    sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection},
+    sqlez_macros::sql,
+};
 use workspace::{
     ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace,
     WorkspaceDb, WorkspaceId,
@@ -375,9 +379,13 @@ impl<'de> Deserialize<'de> for SerializedAxis {
     }
 }
 
-define_connection! {
-    pub static ref TERMINAL_DB: TerminalDb<WorkspaceDb> =
-        &[sql!(
+pub struct TerminalDb(ThreadSafeConnection);
+
+impl Domain for TerminalDb {
+    const NAME: &str = stringify!(TerminalDb);
+
+    const MIGRATIONS: &[&str] = &[
+        sql!(
             CREATE TABLE terminals (
                 workspace_id INTEGER,
                 item_id INTEGER UNIQUE,
@@ -414,6 +422,8 @@ define_connection! {
     ];
 }
 
+db::static_connection!(TERMINAL_DB, TerminalDb, [WorkspaceDb]);
+
 impl TerminalDb {
     query! {
        pub async fn update_workspace_id(

crates/vim/src/state.rs 🔗

@@ -7,8 +7,10 @@ use crate::{motion::Motion, object::Object};
 use anyhow::Result;
 use collections::HashMap;
 use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
-use db::define_connection;
-use db::sqlez_macros::sql;
+use db::{
+    sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
+    sqlez_macros::sql,
+};
 use editor::display_map::{is_invisible, replacement};
 use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer, ToPoint as EditorToPoint};
 use gpui::{
@@ -1668,8 +1670,12 @@ impl MarksView {
     }
 }
 
-define_connection! (
-    pub static ref DB: VimDb<WorkspaceDb> = &[
+pub struct VimDb(ThreadSafeConnection);
+
+impl Domain for VimDb {
+    const NAME: &str = stringify!(VimDb);
+
+    const MIGRATIONS: &[&str] = &[
         sql! (
             CREATE TABLE vim_marks (
               workspace_id INTEGER,
@@ -1689,7 +1695,9 @@ define_connection! (
             ON vim_global_marks_paths(workspace_id, mark_name);
         ),
     ];
-);
+}
+
+db::static_connection!(DB, VimDb, [WorkspaceDb]);
 
 struct SerializedMark {
     path: Arc<Path>,

crates/workspace/src/path_list.rs 🔗

@@ -58,11 +58,7 @@ impl PathList {
         let mut paths: Vec<PathBuf> = if serialized.paths.is_empty() {
             Vec::new()
         } else {
-            serde_json::from_str::<Vec<PathBuf>>(&serialized.paths)
-                .unwrap_or(Vec::new())
-                .into_iter()
-                .map(|s| SanitizedPath::from(s).into())
-                .collect()
+            serialized.paths.split('\n').map(PathBuf::from).collect()
         };
 
         let mut order: Vec<usize> = serialized
@@ -85,7 +81,13 @@ impl PathList {
     pub fn serialize(&self) -> SerializedPathList {
         use std::fmt::Write as _;
 
-        let paths = serde_json::to_string(&self.paths).unwrap_or_default();
+        let mut paths = String::new();
+        for path in self.paths.iter() {
+            if !paths.is_empty() {
+                paths.push('\n');
+            }
+            paths.push_str(&path.to_string_lossy());
+        }
 
         let mut order = String::new();
         for ix in self.order.iter() {

crates/workspace/src/persistence.rs 🔗

@@ -10,7 +10,11 @@ use std::{
 
 use anyhow::{Context as _, Result, bail};
 use collections::HashMap;
-use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
+use db::{
+    query,
+    sqlez::{connection::Connection, domain::Domain},
+    sqlez_macros::sql,
+};
 use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size};
 use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint};
 
@@ -275,186 +279,189 @@ impl sqlez::bindable::Bind for SerializedPixels {
     }
 }
 
-define_connection! {
-    pub static ref DB: WorkspaceDb<()> =
-    &[
-    sql!(
-        CREATE TABLE workspaces(
-            workspace_id INTEGER PRIMARY KEY,
-            workspace_location BLOB UNIQUE,
-            dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
-            dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
-            dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
-            left_sidebar_open INTEGER, // Boolean
-            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
-            FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
-        ) STRICT;
-
-        CREATE TABLE pane_groups(
-            group_id INTEGER PRIMARY KEY,
-            workspace_id INTEGER NOT NULL,
-            parent_group_id INTEGER, // NULL indicates that this is a root node
-            position INTEGER, // NULL indicates that this is a root node
-            axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
-            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
-            ON DELETE CASCADE
-            ON UPDATE CASCADE,
-            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
-        ) STRICT;
-
-        CREATE TABLE panes(
-            pane_id INTEGER PRIMARY KEY,
-            workspace_id INTEGER NOT NULL,
-            active INTEGER NOT NULL, // Boolean
-            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
-            ON DELETE CASCADE
-            ON UPDATE CASCADE
-        ) STRICT;
-
-        CREATE TABLE center_panes(
-            pane_id INTEGER PRIMARY KEY,
-            parent_group_id INTEGER, // NULL means that this is a root pane
-            position INTEGER, // NULL means that this is a root pane
-            FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
-            ON DELETE CASCADE,
-            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
-        ) STRICT;
-
-        CREATE TABLE items(
-            item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
-            workspace_id INTEGER NOT NULL,
-            pane_id INTEGER NOT NULL,
-            kind TEXT NOT NULL,
-            position INTEGER NOT NULL,
-            active INTEGER NOT NULL,
-            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
-            ON DELETE CASCADE
-            ON UPDATE CASCADE,
-            FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
-            ON DELETE CASCADE,
-            PRIMARY KEY(item_id, workspace_id)
-        ) STRICT;
-    ),
-    sql!(
-        ALTER TABLE workspaces ADD COLUMN window_state TEXT;
-        ALTER TABLE workspaces ADD COLUMN window_x REAL;
-        ALTER TABLE workspaces ADD COLUMN window_y REAL;
-        ALTER TABLE workspaces ADD COLUMN window_width REAL;
-        ALTER TABLE workspaces ADD COLUMN window_height REAL;
-        ALTER TABLE workspaces ADD COLUMN display BLOB;
-    ),
-    // Drop foreign key constraint from workspaces.dock_pane to panes table.
-    sql!(
-        CREATE TABLE workspaces_2(
-            workspace_id INTEGER PRIMARY KEY,
-            workspace_location BLOB UNIQUE,
-            dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
-            dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
-            dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
-            left_sidebar_open INTEGER, // Boolean
-            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
-            window_state TEXT,
-            window_x REAL,
-            window_y REAL,
-            window_width REAL,
-            window_height REAL,
-            display BLOB
-        ) STRICT;
-        INSERT INTO workspaces_2 SELECT * FROM workspaces;
-        DROP TABLE workspaces;
-        ALTER TABLE workspaces_2 RENAME TO workspaces;
-    ),
-    // Add panels related information
-    sql!(
-        ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
-        ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
-        ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
-        ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
-        ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
-        ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
-    ),
-    // Add panel zoom persistence
-    sql!(
-        ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
-        ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
-        ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
-    ),
-    // Add pane group flex data
-    sql!(
-        ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
-    ),
-    // Add fullscreen field to workspace
-    // Deprecated, `WindowBounds` holds the fullscreen state now.
-    // Preserving so users can downgrade Zed.
-    sql!(
-        ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
-    ),
-    // Add preview field to items
-    sql!(
-        ALTER TABLE items ADD COLUMN preview INTEGER; //bool
-    ),
-    // Add centered_layout field to workspace
-    sql!(
-        ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
-    ),
-    sql!(
-        CREATE TABLE remote_projects (
-            remote_project_id INTEGER NOT NULL UNIQUE,
-            path TEXT,
-            dev_server_name TEXT
-        );
-        ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
-        ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
-    ),
-    sql!(
-        DROP TABLE remote_projects;
-        CREATE TABLE dev_server_projects (
-            id INTEGER NOT NULL UNIQUE,
-            path TEXT,
-            dev_server_name TEXT
-        );
-        ALTER TABLE workspaces DROP COLUMN remote_project_id;
-        ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
-    ),
-    sql!(
-        ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
-    ),
-    sql!(
-        ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
-    ),
-    sql!(
-        ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
-    ),
-    sql!(
-        ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
-    ),
-    sql!(
-        CREATE TABLE ssh_projects (
-            id INTEGER PRIMARY KEY,
-            host TEXT NOT NULL,
-            port INTEGER,
-            path TEXT NOT NULL,
-            user TEXT
-        );
-        ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
-    ),
-    sql!(
-        ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
-    ),
-    sql!(
-        CREATE TABLE toolchains (
-            workspace_id INTEGER,
-            worktree_id INTEGER,
-            language_name TEXT NOT NULL,
-            name TEXT NOT NULL,
-            path TEXT NOT NULL,
-            PRIMARY KEY (workspace_id, worktree_id, language_name)
-        );
-    ),
-    sql!(
-        ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}";
-    ),
-    sql!(
+pub struct WorkspaceDb(ThreadSafeConnection);
+
+impl Domain for WorkspaceDb {
+    const NAME: &str = stringify!(WorkspaceDb);
+
+    const MIGRATIONS: &[&str] = &[
+        sql!(
+            CREATE TABLE workspaces(
+                workspace_id INTEGER PRIMARY KEY,
+                workspace_location BLOB UNIQUE,
+                dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
+                dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
+                dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
+                left_sidebar_open INTEGER, // Boolean
+                timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
+                FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
+            ) STRICT;
+
+            CREATE TABLE pane_groups(
+                group_id INTEGER PRIMARY KEY,
+                workspace_id INTEGER NOT NULL,
+                parent_group_id INTEGER, // NULL indicates that this is a root node
+                position INTEGER, // NULL indicates that this is a root node
+                axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
+                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+                ON DELETE CASCADE
+                ON UPDATE CASCADE,
+                FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
+            ) STRICT;
+
+            CREATE TABLE panes(
+                pane_id INTEGER PRIMARY KEY,
+                workspace_id INTEGER NOT NULL,
+                active INTEGER NOT NULL, // Boolean
+                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+                ON DELETE CASCADE
+                ON UPDATE CASCADE
+            ) STRICT;
+
+            CREATE TABLE center_panes(
+                pane_id INTEGER PRIMARY KEY,
+                parent_group_id INTEGER, // NULL means that this is a root pane
+                position INTEGER, // NULL means that this is a root pane
+                FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
+                ON DELETE CASCADE,
+                FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
+            ) STRICT;
+
+            CREATE TABLE items(
+                item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
+                workspace_id INTEGER NOT NULL,
+                pane_id INTEGER NOT NULL,
+                kind TEXT NOT NULL,
+                position INTEGER NOT NULL,
+                active INTEGER NOT NULL,
+                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+                ON DELETE CASCADE
+                ON UPDATE CASCADE,
+                FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
+                ON DELETE CASCADE,
+                PRIMARY KEY(item_id, workspace_id)
+            ) STRICT;
+        ),
+        sql!(
+            ALTER TABLE workspaces ADD COLUMN window_state TEXT;
+            ALTER TABLE workspaces ADD COLUMN window_x REAL;
+            ALTER TABLE workspaces ADD COLUMN window_y REAL;
+            ALTER TABLE workspaces ADD COLUMN window_width REAL;
+            ALTER TABLE workspaces ADD COLUMN window_height REAL;
+            ALTER TABLE workspaces ADD COLUMN display BLOB;
+        ),
+        // Drop foreign key constraint from workspaces.dock_pane to panes table.
+        sql!(
+            CREATE TABLE workspaces_2(
+                workspace_id INTEGER PRIMARY KEY,
+                workspace_location BLOB UNIQUE,
+                dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
+                dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
+                dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
+                left_sidebar_open INTEGER, // Boolean
+                timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
+                window_state TEXT,
+                window_x REAL,
+                window_y REAL,
+                window_width REAL,
+                window_height REAL,
+                display BLOB
+            ) STRICT;
+            INSERT INTO workspaces_2 SELECT * FROM workspaces;
+            DROP TABLE workspaces;
+            ALTER TABLE workspaces_2 RENAME TO workspaces;
+        ),
+        // Add panels related information
+        sql!(
+            ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
+            ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
+            ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
+            ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
+            ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
+            ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
+        ),
+        // Add panel zoom persistence
+        sql!(
+            ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
+            ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
+            ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
+        ),
+        // Add pane group flex data
+        sql!(
+            ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
+        ),
+        // Add fullscreen field to workspace
+        // Deprecated, `WindowBounds` holds the fullscreen state now.
+        // Preserving so users can downgrade Zed.
+        sql!(
+            ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
+        ),
+        // Add preview field to items
+        sql!(
+            ALTER TABLE items ADD COLUMN preview INTEGER; //bool
+        ),
+        // Add centered_layout field to workspace
+        sql!(
+            ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
+        ),
+        sql!(
+            CREATE TABLE remote_projects (
+                remote_project_id INTEGER NOT NULL UNIQUE,
+                path TEXT,
+                dev_server_name TEXT
+            );
+            ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
+            ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
+        ),
+        sql!(
+            DROP TABLE remote_projects;
+            CREATE TABLE dev_server_projects (
+                id INTEGER NOT NULL UNIQUE,
+                path TEXT,
+                dev_server_name TEXT
+            );
+            ALTER TABLE workspaces DROP COLUMN remote_project_id;
+            ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
+        ),
+        sql!(
+            ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
+        ),
+        sql!(
+            ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
+        ),
+        sql!(
+            ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
+        ),
+        sql!(
+            ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
+        ),
+        sql!(
+            CREATE TABLE ssh_projects (
+                id INTEGER PRIMARY KEY,
+                host TEXT NOT NULL,
+                port INTEGER,
+                path TEXT NOT NULL,
+                user TEXT
+            );
+            ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
+        ),
+        sql!(
+            ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
+        ),
+        sql!(
+            CREATE TABLE toolchains (
+                workspace_id INTEGER,
+                worktree_id INTEGER,
+                language_name TEXT NOT NULL,
+                name TEXT NOT NULL,
+                path TEXT NOT NULL,
+                PRIMARY KEY (workspace_id, worktree_id, language_name)
+            );
+        ),
+        sql!(
+            ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}";
+        ),
+        sql!(
             CREATE TABLE breakpoints (
                 workspace_id INTEGER NOT NULL,
                 path TEXT NOT NULL,
@@ -466,141 +473,165 @@ define_connection! {
                 ON UPDATE CASCADE
             );
         ),
-    sql!(
-        ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT;
-        CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array);
-        ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT;
-    ),
-    sql!(
-        ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL
-    ),
-    sql!(
-        ALTER TABLE breakpoints DROP COLUMN kind
-    ),
-    sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL),
-    sql!(
-        ALTER TABLE breakpoints ADD COLUMN condition TEXT;
-        ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT;
-    ),
-    sql!(CREATE TABLE toolchains2 (
-        workspace_id INTEGER,
-        worktree_id INTEGER,
-        language_name TEXT NOT NULL,
-        name TEXT NOT NULL,
-        path TEXT NOT NULL,
-        raw_json TEXT NOT NULL,
-        relative_worktree_path TEXT NOT NULL,
-        PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT;
-        INSERT INTO toolchains2
-            SELECT * FROM toolchains;
-        DROP TABLE toolchains;
-        ALTER TABLE toolchains2 RENAME TO toolchains;
-    ),
-    sql!(
-        CREATE TABLE ssh_connections (
-            id INTEGER PRIMARY KEY,
-            host TEXT NOT NULL,
-            port INTEGER,
-            user TEXT
-        );
+        sql!(
+            ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT;
+            CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array);
+            ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT;
+        ),
+        sql!(
+            ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL
+        ),
+        sql!(
+            ALTER TABLE breakpoints DROP COLUMN kind
+        ),
+        sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL),
+        sql!(
+            ALTER TABLE breakpoints ADD COLUMN condition TEXT;
+            ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT;
+        ),
+        sql!(CREATE TABLE toolchains2 (
+            workspace_id INTEGER,
+            worktree_id INTEGER,
+            language_name TEXT NOT NULL,
+            name TEXT NOT NULL,
+            path TEXT NOT NULL,
+            raw_json TEXT NOT NULL,
+            relative_worktree_path TEXT NOT NULL,
+            PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT;
+            INSERT INTO toolchains2
+                SELECT * FROM toolchains;
+            DROP TABLE toolchains;
+            ALTER TABLE toolchains2 RENAME TO toolchains;
+        ),
+        sql!(
+            CREATE TABLE ssh_connections (
+                id INTEGER PRIMARY KEY,
+                host TEXT NOT NULL,
+                port INTEGER,
+                user TEXT
+            );
 
-        INSERT INTO ssh_connections (host, port, user)
-        SELECT DISTINCT host, port, user
-        FROM ssh_projects;
-
-        CREATE TABLE workspaces_2(
-            workspace_id INTEGER PRIMARY KEY,
-            paths TEXT,
-            paths_order TEXT,
-            ssh_connection_id INTEGER REFERENCES ssh_connections(id),
-            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
-            window_state TEXT,
-            window_x REAL,
-            window_y REAL,
-            window_width REAL,
-            window_height REAL,
-            display BLOB,
-            left_dock_visible INTEGER,
-            left_dock_active_panel TEXT,
-            right_dock_visible INTEGER,
-            right_dock_active_panel TEXT,
-            bottom_dock_visible INTEGER,
-            bottom_dock_active_panel TEXT,
-            left_dock_zoom INTEGER,
-            right_dock_zoom INTEGER,
-            bottom_dock_zoom INTEGER,
-            fullscreen INTEGER,
-            centered_layout INTEGER,
-            session_id TEXT,
-            window_id INTEGER
-        ) STRICT;
-
-        INSERT
-        INTO workspaces_2
-        SELECT
-            workspaces.workspace_id,
-            CASE
-                WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths
+            INSERT INTO ssh_connections (host, port, user)
+            SELECT DISTINCT host, port, user
+            FROM ssh_projects;
+
+            CREATE TABLE workspaces_2(
+                workspace_id INTEGER PRIMARY KEY,
+                paths TEXT,
+                paths_order TEXT,
+                ssh_connection_id INTEGER REFERENCES ssh_connections(id),
+                timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
+                window_state TEXT,
+                window_x REAL,
+                window_y REAL,
+                window_width REAL,
+                window_height REAL,
+                display BLOB,
+                left_dock_visible INTEGER,
+                left_dock_active_panel TEXT,
+                right_dock_visible INTEGER,
+                right_dock_active_panel TEXT,
+                bottom_dock_visible INTEGER,
+                bottom_dock_active_panel TEXT,
+                left_dock_zoom INTEGER,
+                right_dock_zoom INTEGER,
+                bottom_dock_zoom INTEGER,
+                fullscreen INTEGER,
+                centered_layout INTEGER,
+                session_id TEXT,
+                window_id INTEGER
+            ) STRICT;
+
+            INSERT
+            INTO workspaces_2
+            SELECT
+                workspaces.workspace_id,
+                CASE
+                    WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths
+                    ELSE
+                        CASE
+                            WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN
+                                NULL
+                            ELSE
+                                replace(workspaces.local_paths_array, ',', "\n")
+                        END
+                END as paths,
+
+                CASE
+                    WHEN ssh_projects.id IS NOT NULL THEN ""
+                    ELSE workspaces.local_paths_order_array
+                END as paths_order,
+
+                CASE
+                    WHEN ssh_projects.id IS NOT NULL THEN (
+                        SELECT ssh_connections.id
+                        FROM ssh_connections
+                        WHERE
+                            ssh_connections.host IS ssh_projects.host AND
+                            ssh_connections.port IS ssh_projects.port AND
+                            ssh_connections.user IS ssh_projects.user
+                    )
+                    ELSE NULL
+                END as ssh_connection_id,
+
+                workspaces.timestamp,
+                workspaces.window_state,
+                workspaces.window_x,
+                workspaces.window_y,
+                workspaces.window_width,
+                workspaces.window_height,
+                workspaces.display,
+                workspaces.left_dock_visible,
+                workspaces.left_dock_active_panel,
+                workspaces.right_dock_visible,
+                workspaces.right_dock_active_panel,
+                workspaces.bottom_dock_visible,
+                workspaces.bottom_dock_active_panel,
+                workspaces.left_dock_zoom,
+                workspaces.right_dock_zoom,
+                workspaces.bottom_dock_zoom,
+                workspaces.fullscreen,
+                workspaces.centered_layout,
+                workspaces.session_id,
+                workspaces.window_id
+            FROM
+                workspaces LEFT JOIN
+                ssh_projects ON
+                workspaces.ssh_project_id = ssh_projects.id;
+
+            DROP TABLE ssh_projects;
+            DROP TABLE workspaces;
+            ALTER TABLE workspaces_2 RENAME TO workspaces;
+
+            CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths);
+        ),
+        // Fix any data from when workspaces.paths were briefly encoded as JSON arrays
+        sql!(
+            UPDATE workspaces
+            SET paths = CASE
+                WHEN substr(paths, 1, 2) = '[' || '"' AND substr(paths, -2, 2) = '"' || ']' THEN
+                    replace(
+                        substr(paths, 3, length(paths) - 4),
+                        '"' || ',' || '"',
+                        CHAR(10)
+                    )
                 ELSE
-                    CASE
-                        WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN
-                            NULL
-                        ELSE
-                            json('[' || '"' || replace(workspaces.local_paths_array, ',', '"' || "," || '"') || '"' || ']')
-                    END
-            END as paths,
-
-            CASE
-                WHEN ssh_projects.id IS NOT NULL THEN ""
-                ELSE workspaces.local_paths_order_array
-            END as paths_order,
-
-            CASE
-                WHEN ssh_projects.id IS NOT NULL THEN (
-                    SELECT ssh_connections.id
-                    FROM ssh_connections
-                    WHERE
-                        ssh_connections.host IS ssh_projects.host AND
-                        ssh_connections.port IS ssh_projects.port AND
-                        ssh_connections.user IS ssh_projects.user
-                )
-                ELSE NULL
-            END as ssh_connection_id,
-
-            workspaces.timestamp,
-            workspaces.window_state,
-            workspaces.window_x,
-            workspaces.window_y,
-            workspaces.window_width,
-            workspaces.window_height,
-            workspaces.display,
-            workspaces.left_dock_visible,
-            workspaces.left_dock_active_panel,
-            workspaces.right_dock_visible,
-            workspaces.right_dock_active_panel,
-            workspaces.bottom_dock_visible,
-            workspaces.bottom_dock_active_panel,
-            workspaces.left_dock_zoom,
-            workspaces.right_dock_zoom,
-            workspaces.bottom_dock_zoom,
-            workspaces.fullscreen,
-            workspaces.centered_layout,
-            workspaces.session_id,
-            workspaces.window_id
-        FROM
-            workspaces LEFT JOIN
-            ssh_projects ON
-            workspaces.ssh_project_id = ssh_projects.id;
-
-        DROP TABLE ssh_projects;
-        DROP TABLE workspaces;
-        ALTER TABLE workspaces_2 RENAME TO workspaces;
-
-        CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths);
-    ),
+                    replace(paths, ',', CHAR(10))
+            END
+            WHERE paths IS NOT NULL
+        ),
     ];
+
+    // Allow recovering from bad migration that was initially shipped to nightly
+    // when introducing the ssh_connections table.
+    fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool {
+        old.starts_with("CREATE TABLE ssh_connections")
+            && new.starts_with("CREATE TABLE ssh_connections")
+    }
 }
 
+db::static_connection!(DB, WorkspaceDb, []);
+
 impl WorkspaceDb {
     /// Returns a serialized workspace for the given worktree_roots. If the passed array
     /// is empty, the most recent workspace is returned instead. If no workspace for the
@@ -1803,6 +1834,7 @@ mod tests {
                         ON DELETE CASCADE
                     ) STRICT;
                 )],
+                |_, _, _| false,
             )
             .unwrap();
         })
@@ -1851,6 +1883,7 @@ mod tests {
                                 REFERENCES workspaces(workspace_id)
                             ON DELETE CASCADE
                         ) STRICT;)],
+                |_, _, _| false,
             )
         })
         .await

crates/zed/src/zed/component_preview/persistence.rs 🔗

@@ -1,10 +1,17 @@
 use anyhow::Result;
-use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql};
+use db::{
+    query,
+    sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection},
+    sqlez_macros::sql,
+};
 use workspace::{ItemId, WorkspaceDb, WorkspaceId};
 
-define_connection! {
-    pub static ref COMPONENT_PREVIEW_DB: ComponentPreviewDb<WorkspaceDb> =
-        &[sql!(
+pub struct ComponentPreviewDb(ThreadSafeConnection);
+
+impl Domain for ComponentPreviewDb {
+    const NAME: &str = stringify!(ComponentPreviewDb);
+
+    const MIGRATIONS: &[&str] = &[sql!(
             CREATE TABLE component_previews (
                 workspace_id INTEGER,
                 item_id INTEGER UNIQUE,
@@ -13,9 +20,11 @@ define_connection! {
                 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
                 ON DELETE CASCADE
             ) STRICT;
-        )];
+    )];
 }
 
+db::static_connection!(COMPONENT_PREVIEW_DB, ComponentPreviewDb, [WorkspaceDb]);
+
 impl ComponentPreviewDb {
     pub async fn save_active_page(
         &self,