Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
b77db21
feat(linux-editor): name the tab's database and schema in the editor bar
sedax90 Oct 30, 2026
f5c2d42
docs(linux-editor): add the changelog entry
sedax90 Oct 30, 2026
48a216d
feat(linux-editor): switch a query tab to another MySQL or MariaDB da…
sedax90 Oct 30, 2026
7dd1bc7
docs(linux-editor): add the changelog entry
sedax90 Oct 30, 2026
35be6ed
feat(linux-editor): switch a query tab's database on SQL Server, Clic…
sedax90 Oct 30, 2026
c47fc28
docs(linux-editor): add the changelog entry
sedax90 Oct 30, 2026
d4cc649
fix(linux-editor): refuse a MongoDB database name a use statement can…
sedax90 Oct 30, 2026
d2549cb
feat(linux-editor): move a PostgreSQL query tab to another database b…
sedax90 Oct 30, 2026
b56bb37
docs(linux-editor): add the changelog entry
sedax90 Oct 30, 2026
03ba56d
feat(linux-session): open a connection's session in another of the se…
sedax90 Oct 30, 2026
0f86941
docs(linux): propose a session per open database of a connection
sedax90 Oct 30, 2026
84dfe86
Merge remote-tracking branch 'origin/main' into feat/linux-editor-dat…
datlechin Oct 30, 2026
fb4e698
wip: saved-in key
sedax90 Oct 30, 2026
30e2e27
Merge branch 'feat/linux-editor-database-switch-reconnect' into feat/…
sedax90 Oct 30, 2026
9f6c1d7
feat(linux-workspace): open another of a connection's databases besid…
sedax90 Oct 30, 2026
e08f518
docs(linux-workspace): add the changelog entry
sedax90 Oct 30, 2026
0c8f034
fix(linux-editor): name the database a tab's session is in, and only …
datlechin Oct 30, 2026
5bc735b
Merge branch 'feat/linux-editor-database-picker' into feat/linux-edit…
datlechin Oct 30, 2026
a771366
fix(linux-editor): move a tab's own session with the driver, and comp…
datlechin Oct 30, 2026
c86f693
fix(linux-editor): tell the workspace when a USE moves the tab
datlechin Oct 30, 2026
f1ff043
Merge branch 'feat/linux-editor-database-switch' into feat/linux-edit…
datlechin Oct 30, 2026
0c0e7c2
fix(linux-editor): move SQL Server, ClickHouse, Cassandra and MongoDB…
datlechin Oct 30, 2026
b51d85e
Merge branch 'feat/linux-editor-database-switch-more' into feat/linux…
datlechin Oct 30, 2026
2bb98f9
fix(linux-editor): open a PostgreSQL tab's session in its database th…
datlechin Oct 30, 2026
ef2d68c
fix(linux-session): open a sibling session at the connection's Safe M…
datlechin Oct 30, 2026
a283924
docs(linux): record the design the database switch is built on
datlechin Oct 30, 2026
b2dc979
docs(linux): name the ADR's decision in the index
datlechin Oct 30, 2026
642c553
Merge branch 'feat/linux-editor-database-switch-reconnect' into feat/…
datlechin Oct 30, 2026
d9cbcd6
Merge branch 'feat/linux-session-per-database' into feat/linux-worksp…
datlechin Oct 30, 2026
22d04f0
Merge branch 'docs/linux-adr-database-sessions' into feat/linux-works…
datlechin Oct 30, 2026
b59e7c6
fix(linux-workspace): close a database that opened after its connecti…
datlechin Oct 30, 2026
cbe0a11
fix(linux-editor): end a session whose database after a USE the serve…
datlechin Oct 30, 2026
3738fe2
fix(linux-editor): end a run only once the tab knows the database it …
datlechin Oct 30, 2026
abad14b
Merge branch 'feat/linux-editor-database-picker' into feat/linux-edit…
datlechin Oct 30, 2026
755a489
fix(linux-editor): report only the keyspace Cassandra confirmed, and …
datlechin Oct 30, 2026
d85b302
Merge branch 'feat/linux-editor-database-switch-reconnect' into feat/…
datlechin Oct 30, 2026
91baad7
Merge branch 'feat/linux-editor-database-switch-more' into feat/linux…
datlechin Oct 30, 2026
a24efc1
Merge branch 'feat/linux-editor-database-switch' into feat/linux-edit…
datlechin Oct 30, 2026
a250df6
fix(linux-workspace): close a connection's databases with it under on…
datlechin Oct 30, 2026
b35a1b0
test(linux-session): count the session's own hold on its credentials
datlechin Oct 30, 2026
db94834
Merge branch 'feat/linux-session-per-database' into feat/linux-worksp…
datlechin Oct 30, 2026
cb30659
Merge branch 'feat/linux-editor-database-switch-more' into feat/linux…
datlechin Oct 30, 2026
ad372fe
test(linux-mssql): pin the bracket quoting a move's USE uses
datlechin Oct 30, 2026
74646c7
Merge branch 'feat/linux-editor-database-switch-reconnect' into feat/…
datlechin Oct 30, 2026
93ababb
fix(linux-editor): offer the databases of a connection opened without…
datlechin Oct 30, 2026
615302c
test(linux-editor): a connection without a database name names the se…
datlechin Oct 30, 2026
1e57089
Merge branch 'feat/linux-editor-database-switch-more' into feat/linux…
datlechin Oct 30, 2026
b77db8f
Merge branch 'feat/linux-editor-database-switch' into feat/linux-edit…
datlechin Oct 30, 2026
d7ef12e
fix(linux-editor): keep a PostgreSQL tab's session until one opens in…
datlechin Oct 30, 2026
52a8d0f
Merge branch 'feat/linux-editor-database-switch-reconnect' into feat/…
datlechin Oct 30, 2026
3d592b9
fix(linux-workspace): keep another database's table state apart, and …
datlechin Oct 30, 2026
139e75f
test(linux-clickhouse): read the collected rows without a binding
datlechin Oct 30, 2026
a124560
Merge branch 'feat/linux-editor-database-switch-more' into feat/linux…
datlechin Oct 30, 2026
0f333fb
Merge branch 'feat/linux-editor-database-switch-reconnect' into feat/…
datlechin Oct 30, 2026
2b50233
fix(linux-editor): show a database name's underscores in the menu
datlechin Oct 30, 2026
de81995
Merge branch 'feat/linux-editor-database-switch' into feat/linux-edit…
datlechin Oct 30, 2026
8c79b3c
Merge branch 'feat/linux-editor-database-switch-more' into feat/linux…
datlechin Oct 30, 2026
a8c10e9
Merge branch 'feat/linux-editor-database-switch-reconnect' into feat/…
datlechin Oct 30, 2026
cfd96f7
refactor(linux-editor): reuse the menu label helper for database names
datlechin Oct 30, 2026
1c53758
Merge branch 'feat/linux-editor-database-switch' into feat/linux-edit…
datlechin Oct 30, 2026
d10a3c8
Merge branch 'feat/linux-editor-database-switch-more' into feat/linux…
datlechin Oct 30, 2026
7ca7123
Merge branch 'feat/linux-editor-database-switch-reconnect' into feat/…
datlechin Oct 30, 2026
eb2be50
test(linux-workspace): ask the tear-down question over a list of work…
datlechin Oct 30, 2026
cde4cfe
test(linux-workspace): an open connection's menu offers Open Database
datlechin Oct 30, 2026
4f70da8
Merge origin/main after #3896 landed
datlechin Oct 30, 2026
7fa1d4f
test(linux-workspace): name the file names without a closure
datlechin Oct 30, 2026
f223b92
Merge origin/main after #3896 landed
datlechin Oct 30, 2026
0396ea4
Merge origin/main after #3896 landed
datlechin Oct 30, 2026
8b911bd
fix(linux-workspace): close a database that joined during its connect…
datlechin Oct 30, 2026
e50f175
fix(linux-editor): export, describe and complete a moved tab in its o…
datlechin Oct 30, 2026
ff46d66
Merge branch 'feat/linux-editor-database-switch' into feat/linux-edit…
datlechin Oct 30, 2026
5e2dd74
Merge branch 'feat/linux-editor-database-switch-more' into feat/linux…
datlechin Oct 30, 2026
495864f
Merge branch 'feat/linux-editor-database-switch-reconnect' into feat/…
datlechin Oct 30, 2026
94cea80
test(linux-mssql): create the test's database outside a transaction
datlechin Oct 30, 2026
ff5ef22
Merge branch 'feat/linux-editor-database-switch-more' into feat/linux…
datlechin Oct 30, 2026
8a29583
Merge branch 'feat/linux-editor-database-switch-reconnect' into feat/…
datlechin Oct 30, 2026
f942f90
test(linux-editor): a menu for the one database a connection in none …
datlechin Oct 30, 2026
0125ee8
Merge branch 'feat/linux-editor-database-switch' into feat/linux-edit…
datlechin Oct 30, 2026
e07e3b5
Merge branch 'feat/linux-editor-database-switch-more' into feat/linux…
datlechin Oct 30, 2026
8f9d571
Merge branch 'feat/linux-editor-database-switch-reconnect' into feat/…
datlechin Oct 30, 2026
8a67ff5
Merge remote-tracking branch 'origin/main' into feat/linux-editor-dat…
datlechin Oct 30, 2026
0ff2aa8
Merge remote-tracking branch 'origin/main' into feat/linux-editor-dat…
datlechin Oct 30, 2026
e5840c7
Merge remote-tracking branch 'origin/main' into feat/linux-workspace-…
datlechin Oct 30, 2026
75edb70
Merge origin/main after #3897 landed
datlechin Oct 30, 2026
68553f0
Merge branch 'feat/linux-editor-database-switch-more' into feat/linux…
datlechin Oct 30, 2026
ed2f163
Merge branch 'feat/linux-editor-database-switch-reconnect' into feat/…
datlechin Oct 30, 2026
162e292
Rebuild on main: the connection's open session owns its other databases
datlechin Oct 31, 2026
7513997
fix(linux-workspace): offer Open Database only where a connection's c…
datlechin Oct 31, 2026
e955989
fix(linux-workspace): give a database that joins the level its connec…
datlechin Oct 31, 2026
8807595
test(linux-workspace): name whether a collection's menu opens databases
datlechin Oct 31, 2026
435a239
Merge origin/main after #3904 landed
datlechin Oct 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions linux/changelog.d/3902.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Open Database… on a connection's row opens another of the server's databases beside it in the window, with its own tables and tabs.
4 changes: 3 additions & 1 deletion linux/crates/app/src/application/mcp_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ fn answer(app: &TableProApplication, request: McpRequest) {
.into_iter()
.filter_map(|(key, _)| match key {
SessionKey::Saved(id) => Some(id),
SessionKey::File(_) => None,
// Another database of an open connection is no other
// connection.
SessionKey::SavedIn { .. } | SessionKey::File(_) => None,
})
.collect();
let _ = reply.send(open);
Expand Down
59 changes: 59 additions & 0 deletions linux/crates/app/src/services/connection_deletion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ impl ConnectionDeletion {
if let Err(error) = self.workspaces.purge(&SessionKey::Saved(id)).await {
report.purge_failures.push(PurgeFailure::Workspace(error));
}
if let Err(error) = self.workspaces.purge_databases_of(id).await {
report.purge_failures.push(PurgeFailure::Workspace(error));
}
let Some(history) = self.history.clone() else {
let unavailable = || "the query history did not open".to_owned();
report.purge_failures.push(PurgeFailure::SavedQueries(unavailable()));
Expand Down Expand Up @@ -357,6 +360,62 @@ mod tests {
}
}

/// Another database opened beside the connection keeps its tabs and
/// drafts under a name of its own, which goes with the connection.
#[tokio::test]
async fn the_tabs_of_its_other_databases_go_with_the_connection() {
let fixture = fixture().await;
let (database, other_database) = (
SessionKey::SavedIn {
id: fixture.id,
database: "analytics".to_owned(),
},
SessionKey::SavedIn {
id: fixture.other,
database: "analytics".to_owned(),
},
);
let names = [&database, &other_database].map(WorkspaceFileName::for_key);
for (key, name) in [&database, &other_database].into_iter().zip(&names) {
let draft = DraftId::new();
fixture
.drafts
.write_blocking(&name.draft_scope(), draft, "SELECT 1")
.expect("a draft");
fixture
.workspaces
.schedule(
key,
WorkspaceRecord {
active_tab: Some(0),
tabs: vec![TabRecord::Editor {
draft,
file: None,
database: None,
}],
},
)
.expect("scheduled");
fixture.workspaces.flush(key).await.expect("the saved tabs");
}
let workspaces_dir = fixture.paths.workspaces_dir();
let drafts_dir = |name: &WorkspaceFileName| fixture.drafts.root().join(name.draft_scope().as_str());

fixture
.deletion()
.run(fixture.id, DeletionOptions { delete_history: false })
.await
.expect("the delete");

assert!(!workspaces_dir.join(names[0].file_name()).exists(), "its tabs stayed");
assert!(!drafts_dir(&names[0]).exists(), "its drafts stayed");
assert!(
workspaces_dir.join(names[1].file_name()).exists(),
"another connection's went"
);
assert!(drafts_dir(&names[1]).exists());
}

#[tokio::test]
async fn secret_failure_keeps_entry_and_skips_purge() {
let fixture = fixture().await;
Expand Down
2 changes: 1 addition & 1 deletion linux/crates/app/src/services/file_resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ pub fn resolve_file_blocking(
Some(connection) => FileTarget::Saved(Box::new(connection)),
None => FileTarget::File(identity),
},
SessionKey::File(_) => FileTarget::File(identity),
SessionKey::SavedIn { .. } | SessionKey::File(_) => FileTarget::File(identity),
}
});
ResolvedFile { uri, target }
Expand Down
16 changes: 15 additions & 1 deletion linux/crates/app/src/ui/session_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ use tablepro_session::SessionKey;
use uuid::Uuid;

const SAVED: &str = "saved:";
const SAVED_IN: &str = "saved-in:";
const FILE: &str = "file:";

/// `saved:<uuid>` or `file:<uri>`.
/// `saved:<uuid>`, `saved-in:<uuid>/<database>` or `file:<uri>`. A uuid
/// holds no `/`, so the database after it may hold any.
pub(crate) fn to_variant(key: &SessionKey) -> glib::Variant {
match key {
SessionKey::Saved(id) => format!("{SAVED}{id}"),
SessionKey::SavedIn { id, database } => format!("{SAVED_IN}{id}/{database}"),
SessionKey::File(uri) => format!("{FILE}{uri}"),
}
.to_variant()
Expand All @@ -24,6 +27,13 @@ pub(crate) fn from_variant(value: &glib::Variant) -> Option<SessionKey> {
if let Some(id) = text.strip_prefix(SAVED) {
return Uuid::parse_str(id).ok().map(SessionKey::Saved);
}
if let Some(rest) = text.strip_prefix(SAVED_IN) {
let (id, database) = rest.split_once('/')?;
return Uuid::parse_str(id).ok().map(|id| SessionKey::SavedIn {
id,
database: database.to_owned(),
});
}
text.strip_prefix(FILE)
.filter(|uri| glib::Uri::peek_scheme(uri).is_some())
.map(|uri| SessionKey::File(uri.to_owned()))
Expand All @@ -37,6 +47,10 @@ mod tests {
fn a_key_round_trips() {
let keys = [
SessionKey::Saved(Uuid::new_v4()),
SessionKey::SavedIn {
id: Uuid::new_v4(),
database: "sales/2026".to_owned(),
},
SessionKey::File("file:///home/u/R%26D/sales.db".to_owned()),
SessionKey::File("file:///srv/caf%C3%A9.sqlite3".to_owned()),
];
Expand Down
76 changes: 51 additions & 25 deletions linux/crates/app/src/ui/session_window/connections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,19 +481,27 @@ impl SessionWindow {
/// connection opened again, so the window reconnects once the tabs
/// may go; until then the saved level waits for the next connect.
pub(super) fn on_set_safe_mode(&self, key: &SessionKey, level: SafeModeLevel) {
let Some(session) = self
.imp()
.sessions
.borrow()
.iter()
.find(|open| open.claim.key() == key)
.map(|open| open.session.clone())
else {
// The level is the connection's, which its other databases took
// when they opened (0030).
let key = match key {
SessionKey::SavedIn { id, .. } => SessionKey::Saved(*id),
SessionKey::Saved(_) | SessionKey::File(_) => key.clone(),
};
let Some(sessions) = self.find_open(|open| {
(open.claim.key() == &key).then(|| {
open.with_databases()
.map(|open| (open.claim.key().clone(), open.session.clone()))
.collect::<Vec<_>>()
})
}) else {
return;
};
let Some((_, session)) = sessions.first() else {
return;
};
let saved = match key {
let saved = match &key {
SessionKey::Saved(id) => self.stored_connection(*id),
SessionKey::File(_) => None,
SessionKey::SavedIn { .. } | SessionKey::File(_) => None,
};
// The saved level can differ from the session's after a reconnect
// was declined, and a level still being written can land after
Expand All @@ -504,7 +512,9 @@ impl SessionWindow {
let differs =
session.safe_mode() != level || writing || saved.as_ref().is_some_and(|saved| saved.safe_mode != level);
if differs {
let applied = session.safe_mode() == level || session.set_safe_mode(level);
let applied = sessions
.iter()
.all(|(_, session)| session.safe_mode() == level || session.set_safe_mode(level));
match (saved.map(|saved| saved.id), applied) {
(Some(id), _) => self.save_safe_mode(LevelSave {
id,
Expand All @@ -517,7 +527,9 @@ impl SessionWindow {
)),
}
if applied {
self.deliver_to(key, WorkspaceInput::SafeModeChanged);
for (key, _) in &sessions {
self.deliver_to(key, WorkspaceInput::SafeModeChanged);
}
}
}
self.show_safe_mode();
Expand Down Expand Up @@ -571,20 +583,20 @@ impl SessionWindow {

/// Ask the tabs, as Disconnect does, then connect again.
fn reopen_after_teardown(&self, id: Uuid) {
let Some(workspace) = self.session_workspace(&SessionKey::Saved(id)) else {
return;
};
let (Some(handle), Some(guards), Some(input)) = (workspace.prompt_handle(), self.close_guards(), self.input())
else {
let (Some(handles), Some(guards), Some(input)) = (
self.teardown_handles(&SessionKey::Saved(id)),
self.close_guards(),
self.input(),
) else {
return;
};
guards.begin_teardown();
if guards.may_tear_down(&handle) {
if guards.may_tear_down(&handles) {
input.deliver(SessionWindowInput::ReopenApproved(id));
return;
}
let window = self.downgrade();
guards.ask_tear_down(self.upcast_ref(), &handle, move |decision| match decision {
guards.ask_tear_down(self.upcast_ref(), &handles, move |decision| match decision {
CloseDecision::Approved => input.deliver(SessionWindowInput::ReopenApproved(id)),
_ => {
if let Some(window) = window.upgrade() {
Expand All @@ -595,17 +607,31 @@ impl SessionWindow {
}

/// Tear the session down and connect it again through the window's
/// one connect path, at the level just saved. The window's other
/// sessions and connects go on.
/// one connect path, at the level just saved, with the other
/// databases it had open. The window's other sessions and connects
/// go on.
pub(super) fn on_reopen_approved(&self, id: Uuid) {
let key = SessionKey::Saved(id);
let stored = self.stored_connection(id);
if !self.tear_down_session(&SessionKey::Saved(id)) {
let databases = self
.find_open(|open| (open.claim.key() == &key).then(|| open.databases.names()))
.unwrap_or_default();
if !self.tear_down_session(&key) {
return;
}
match stored {
Some(saved) => self.connect(ConnectSource::Saved(Box::new(saved)), CredentialDemand::default()),
None => self.show_toast(&crate::i18n::gettext("The connection was deleted.")),
let Some(saved) = stored else {
self.show_toast(&crate::i18n::gettext("The connection was deleted."));
return;
};
let source = ConnectSource::Saved(Box::new(saved));
let Some(claim) = self.claim_for(&key) else {
return;
};
if let Some(mut pending) = self.spawn_connect(claim, source, None, CredentialDemand::default()) {
pending.databases = databases;
self.imp().connects.borrow_mut().push(pending);
}
self.connects_changed();
}

/// Tick the level of the session in front in the main menu.
Expand Down
Loading
Loading