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,48 +147,12 @@ 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)
where where
F: Future<Output = anyhow::Result<()>> + Send, F: Future<Output = anyhow::Result<()>> + Send,
@ -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] = &[
}
fn migrations() -> &'static [&'static str] {
&[
sql!(CREATE TABLE test(value);), sql!(CREATE TABLE test(value);),
// failure because test already exists // failure because test already exists
sql!(CREATE TABLE test(value);), 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,
@ -420,6 +427,8 @@ mod persistence {
)]; )];
} }
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,
@ -3365,6 +3368,8 @@ mod persistence {
)]; )];
} }
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,9 +279,12 @@ impl sqlez::bindable::Bind for SerializedPixels {
} }
} }
define_connection! { pub struct WorkspaceDb(ThreadSafeConnection);
pub static ref DB: WorkspaceDb<()> =
&[ impl Domain for WorkspaceDb {
const NAME: &str = stringify!(WorkspaceDb);
const MIGRATIONS: &[&str] = &[
sql!( sql!(
CREATE TABLE workspaces( CREATE TABLE workspaces(
workspace_id INTEGER PRIMARY KEY, workspace_id INTEGER PRIMARY KEY,
@ -546,7 +553,7 @@ define_connection! {
WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN
NULL NULL
ELSE ELSE
json('[' || '"' || replace(workspaces.local_paths_array, ',', '"' || "," || '"') || '"' || ']') replace(workspaces.local_paths_array, ',', "\n")
END END
END as paths, END as paths,
@ -598,8 +605,32 @@ define_connection! {
CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths); 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
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 { 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
@ -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,
@ -16,6 +23,8 @@ define_connection! {
)]; )];
} }
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,