Fix workspace migration failure (#36911)

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

Release Notes:

- N/A
This commit is contained in:
Max Brunsfeld 2025-08-25 17:27:52 -07:00 committed by GitHub
parent f8667a8379
commit d43df9e841
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 582 additions and 495 deletions

View file

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

View file

@ -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 /// 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_export]
macro_rules! define_connection { macro_rules! static_connection {
(pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => { ($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => {
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
impl ::std::ops::Deref for $t { impl ::std::ops::Deref for $t {
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection; 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 { impl $t {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub async fn open_test_db(name: &'static str) -> Self { pub async fn open_test_db(name: &'static str) -> Self {
@ -142,7 +135,8 @@ macro_rules! define_connection {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
$t($crate::smol::block_on($crate::open_test_db::<$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")))] #[cfg(not(any(test, feature = "test-support")))]
@ -153,46 +147,10 @@ macro_rules! define_connection {
} else { } else {
$crate::RELEASE_CHANNEL.dev_name() $crate::RELEASE_CHANNEL.dev_name()
}; };
$t($crate::smol::block_on($crate::open_db::<$t>(db_dir, scope))) #[allow(unused_parens)]
$t($crate::smol::block_on($crate::open_db::<($($d,)* $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))))
});
#[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::<($($d),+, $t)>(db_dir, scope)))
});
};
} }
pub fn write_and_log<F>(cx: &App, db_write: impl FnOnce() -> F + Send + 'static) pub fn write_and_log<F>(cx: &App, db_write: impl FnOnce() -> F + Send + 'static)
@ -219,17 +177,12 @@ mod tests {
enum BadDB {} enum BadDB {}
impl Domain for BadDB { impl Domain for BadDB {
fn name() -> &'static str { const NAME: &str = "db_tests";
"db_tests" const MIGRATIONS: &[&str] = &[
} sql!(CREATE TABLE test(value);),
// failure because test already exists
fn migrations() -> &'static [&'static str] { sql!(CREATE TABLE test(value);),
&[ ];
sql!(CREATE TABLE test(value);),
// failure because test already exists
sql!(CREATE TABLE test(value);),
]
}
} }
let tempdir = tempfile::Builder::new() let tempdir = tempfile::Builder::new()
@ -251,25 +204,15 @@ mod tests {
enum CorruptedDB {} enum CorruptedDB {}
impl Domain for CorruptedDB { impl Domain for CorruptedDB {
fn name() -> &'static str { const NAME: &str = "db_tests";
"db_tests" const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test(value);)]
}
} }
enum GoodDB {} enum GoodDB {}
impl Domain for GoodDB { impl Domain for GoodDB {
fn name() -> &'static str { const NAME: &str = "db_tests"; //Notice same name
"db_tests" //Notice same name const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)];
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test2(value);)] //But different migration
}
} }
let tempdir = tempfile::Builder::new() let tempdir = tempfile::Builder::new()
@ -305,25 +248,16 @@ mod tests {
enum CorruptedDB {} enum CorruptedDB {}
impl Domain for CorruptedDB { impl Domain for CorruptedDB {
fn name() -> &'static str { const NAME: &str = "db_tests";
"db_tests"
}
fn migrations() -> &'static [&'static str] { const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
&[sql!(CREATE TABLE test(value);)]
}
} }
enum GoodDB {} enum GoodDB {}
impl Domain for GoodDB { impl Domain for GoodDB {
fn name() -> &'static str { const NAME: &str = "db_tests"; //Notice same name
"db_tests" //Notice same name const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test2(value);)] //But different migration
}
} }
let tempdir = tempfile::Builder::new() let tempdir = tempfile::Builder::new()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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