Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion linux/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ A window holds one `Session`. Every use of its connection goes through the `Oper

The application tells each session what its connection cannot see. When `GNetworkMonitor` reports a change, and when a window adopts a session, it asks whether that session's server, or its first SSH hop, is reachable; a file, a socket and a loopback server are never asked about. A session that loses a server the network cannot reach waits offline, checking every 60 s, and reconnects as soon as the server is reachable again. While a session is open, a 5 s timer compares `CLOCK_BOOTTIME` with `CLOCK_MONOTONIC`, and after a suspend each server is checked again and a session that slept more than 30 s probes its connection. A session parked on an SSH host key keeps the error that parked it, and its banner's Review Key shows the fingerprint. Trusting it retries with ssh's question about that one key answered yes, so ssh still checks the key and writes known_hosts; a changed key is first removed with `ssh-keygen -R`, since ssh never accepts one. When the review cannot accept the key, because ssh's configuration takes keys only from known_hosts or the key is revoked, its dialog offers Retry for after the user fixes the key outside TablePro.

The session's `SchemaCatalog` holds what the window knows about the database: the schemas, each scope's objects and each table's columns. Callers that ask for the same thing at once share one fetch, a fetch runs as its own task so a view that closes does not stop it, and a failed refresh keeps the list it had. A reconnect commits the catalog the new connection read and marks the rest stale; `invalidate_table` does the same for one table after a schema change. The sidebar shows every schema's tables and views in one `GtkListView` that recycles its rows, a section per schema with the default one first and the engine's own last, and one menu whose items carry the table they act on. Its rows come from a `SidebarIndex`, sorted by collation key and given its search keys on a blocking thread, behind a `GListModel` and `GtkSectionModel` that makes a row's object only when the list view asks for it. It follows every schema's list through a watch the window drops before the session closes, and reads that list again when a reconnect commits the default schema's; a refresh that fails keeps the list and says so in a toast. The editor's completion reads the catalog when it opens: the default schema's objects, a named schema's objects after `schema.`, and the columns of the tables the statement names. Its candidates are built with that read on the runtime, and its `ProposalList` ranks them for the typed word and makes a proposal only for a row GtkSourceView shows. Either list thus costs the GTK thread the rows on screen, not the catalog, which the large-catalog case checks with 50,000 tables ([testing.md](docs/testing.md#large-catalog-case)).
The session's `SchemaCatalog` holds what the window knows about the database: the schemas, each scope's objects and each table's columns. Callers that ask for the same thing at once share one fetch, a fetch runs as its own task so a view that closes does not stop it, and a failed refresh keeps the list it had. A reconnect commits the catalog the new connection read and marks the rest stale; `invalidate_table` does the same for one table after a schema change. The sidebar shows every schema's tables and views in one `GtkListView` that recycles its rows, a section per schema with the default one first and the engine's own last, and one menu whose items carry the table they act on. Its rows come from a `SidebarIndex`, sorted by collation key and given its search keys on a blocking thread, behind a `GListModel` and `GtkSectionModel` that makes a row's object only when the list view asks for it. Each search, and each new list, picks the rows it keeps on a blocking thread too, and the list takes them in one change unless a newer search or list overtook them. It follows every schema's list through a watch the window drops before the session closes, and reads that list again when a reconnect commits the default schema's; a refresh that fails keeps the list and says so in a toast. The editor's completion reads the catalog when it opens: the default schema's objects, a named schema's objects after `schema.`, and the columns of the tables the statement names. Its candidates are built with that read on the runtime, and its `ProposalList` ranks them for the typed word and makes a proposal only for a row GtkSourceView shows. Either list thus costs the GTK thread the rows on screen, not the catalog, which the large-catalog case checks with 50,000 tables ([testing.md](docs/testing.md#large-catalog-case)).

## Workspace tab system

Expand Down
2 changes: 1 addition & 1 deletion linux/crates/app/src/ui/workspace/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ impl Workspace {
let persistence = workspace_persistence::WorkspacePersistence::new(storage.workspaces().clone());
let exports = workspace.export_ledger(&window, &services, &context, &input);
let imports = import_ledger::ImportLedger::new(context.hold.clone(), window_inhibitor(&window));
let sidebar = sidebar::build(&workspace, &input, sidebar::SidebarAffordances::for_session(&session));
let sidebar = sidebar::build(&workspace, &input, &session, &tasks);
let WorkspaceTabParts {
tab_view,
index,
Expand Down
10 changes: 8 additions & 2 deletions linux/crates/app/src/ui/workspace/sidebar/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,14 @@ use sidebar_event::SidebarEvent;

/// Build the sidebar into the workspace's template, sending what the
/// user asks of it to `input`.
pub(super) fn build(workspace: &Workspace, input: &WorkspaceSender, affordances: SidebarAffordances) -> SidebarView {
pub(super) fn build(workspace: &Workspace, input: &WorkspaceSender, session: &Session, tasks: &Tasks) -> SidebarView {
let imp = workspace.imp();
let events = input.clone();
let view = SidebarView::new(move |event| events.deliver(workspace_input(event)), affordances);
let view = SidebarView::new(
move |event| events.deliver(workspace_input(event)),
SidebarAffordances::for_session(session),
Some(tasks.clone()),
);
imp.sidebar_slot.set_child(Some(view.widget()));
view.follow_search(&imp.table_search);
imp.table_search_bar.connect_entry(&*imp.table_search);
Expand Down Expand Up @@ -207,6 +211,8 @@ impl WorkspaceModel {

pub(super) fn on_sidebar_read(&mut self, read: u64, listing: SidebarListing) {
if read != self.sidebar_read {
// A catalog's index frees a few strings per table.
drop(self.context.tasks.spawn_blocking_task(move || drop(listing)));
return;
}
if let Some(schemas) = listing.schemas {
Expand Down
95 changes: 57 additions & 38 deletions linux/crates/app/src/ui/workspace/sidebar/sidebar_list.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::rc::Rc;
use std::sync::Arc;

use gtk4 as gtk;
use gtk4::prelude::*;
Expand All @@ -17,32 +17,35 @@ glib::wrapper! {
@implements gio::ListModel, gtk::SectionModel;
}

/// The entries of an index a search keeps, in list order, and each
/// section's rows among them. Worked out away from the GTK thread: a
/// search reads every table's key.
#[derive(Debug)]
pub(crate) struct SidebarRows {
index: Arc<SidebarIndex>,
shown: Vec<u32>,
runs: Vec<(u32, u32)>,
}

impl SidebarRows {
/// `index`'s entries matching `query` as `search_key` gave it, every
/// one when it is empty.
pub(crate) fn new(index: Arc<SidebarIndex>, query: &str) -> Self {
let shown = matching(&index, query);
let runs = runs(&index, &shown);
Self { index, shown, runs }
}
}

impl SidebarList {
pub(crate) fn new() -> Self {
glib::Object::new()
}

pub(crate) fn index(&self) -> Rc<SidebarIndex> {
pub(crate) fn index(&self) -> Arc<SidebarIndex> {
self.imp().index.borrow().clone()
}

/// Show `index`, its entries matching `query` as `search_key` gave
/// it, every one when it is empty.
pub(crate) fn replace(&self, index: SidebarIndex, query: &str) {
let index = Rc::new(index);
let shown = matching(&index, query);
let imp = self.imp();
imp.objects.borrow_mut().clear();
imp.index.replace(index);
self.show(shown, true);
}

/// Show the entries matching `query`.
pub(crate) fn filter(&self, query: &str) {
let shown = matching(&self.index(), query);
self.show(shown, false);
}

/// How many rows were made since the index was last replaced.
#[cfg(test)]
pub(crate) fn made(&self) -> usize {
Expand All @@ -58,18 +61,23 @@ impl SidebarList {
.and_then(|position| u32::try_from(position).ok())
}

/// Take every row out, then put the new ones in. GtkListView looks
/// for the rows it tracks, its scroll anchor among them, through
/// every row a change adds in place of them, which would make an
/// object for each; a change that only removes or only adds has
/// nothing to look through. The list opens at its first row, as a
/// new list or a new search should.
fn show(&self, shown: Vec<u32>, replaced: bool) {
/// Show `rows`, taking every row out, then putting the new ones in.
/// GtkListView looks for the rows it tracks, its scroll anchor among
/// them, through every row a change adds in place of them, which
/// would make an object for each; a change that only removes or only
/// adds has nothing to look through. The list opens at its first row,
/// as a new list or a new search should.
pub(crate) fn show(&self, rows: SidebarRows) {
let SidebarRows { index, shown, runs } = rows;
let imp = self.imp();
let replaced = !Arc::ptr_eq(&imp.index.borrow(), &index);
if !replaced && *imp.shown.borrow() == shown {
return;
}
let runs = runs(&self.index(), &shown);
if replaced {
imp.objects.borrow_mut().clear();
imp.index.replace(index);
}
let removed = u32::try_from(imp.shown.take().len()).unwrap_or(u32::MAX);
imp.runs.take();
if removed > 0 {
Expand Down Expand Up @@ -141,6 +149,14 @@ mod tests {
SidebarIndex::build(&objects, &schemas)
}

fn replace(list: &SidebarList, index: SidebarIndex, query: &str) {
list.show(SidebarRows::new(Arc::new(index), query));
}

fn filter(list: &SidebarList, query: &str) {
list.show(SidebarRows::new(list.index(), query));
}

fn sections(list: &SidebarList) -> Vec<(u32, u32)> {
let mut found = Vec::new();
let mut position = 0;
Expand All @@ -164,7 +180,8 @@ mod tests {
#[gtk4::test]
fn sections_are_the_runs_of_one_schema() {
let list = SidebarList::new();
list.replace(
replace(
&list,
index(&[
("sales", "orders"),
("public", "users"),
Expand All @@ -187,29 +204,30 @@ mod tests {
#[gtk4::test]
fn a_row_keeps_its_object_until_the_index_is_replaced() {
let list = SidebarList::new();
list.replace(index(&[("public", "users"), ("public", "orders")]), "");
replace(&list, index(&[("public", "users"), ("public", "orders")]), "");
let first = list.item(0);

list.filter(&search_key("o"));
list.filter("");
filter(&list, &search_key("o"));
filter(&list, "");

assert_eq!(list.item(0), first, "a search made the row anew");
list.replace(index(&[("public", "users"), ("public", "orders")]), "");
replace(&list, index(&[("public", "users"), ("public", "orders")]), "");
assert_ne!(list.item(0), first, "a new index kept an old row");
}

#[gtk4::test]
fn a_search_is_one_change_and_none_when_the_rows_stay() {
let list = SidebarList::new();
list.replace(
replace(
&list,
index(&[("public", "users"), ("public", "orders"), ("sales", "user_log")]),
"",
);
let seen = changes(&list);

list.filter(&search_key("user"));
list.filter(&search_key("user"));
list.filter(&search_key("users"));
filter(&list, &search_key("user"));
filter(&list, &search_key("user"));
filter(&list, &search_key("users"));

assert_eq!(*seen.borrow(), [(0, 3, 0), (0, 0, 2), (0, 2, 0), (0, 0, 1)]);
assert_eq!(sections(&list), [(0, 1)]);
Expand All @@ -222,7 +240,7 @@ mod tests {
let list = SidebarList::new();
let seen = changes(&list);

list.replace(index(&[]), "");
replace(&list, index(&[]), "");

assert!(seen.borrow().is_empty());
assert_eq!(list.section(0), (0, u32::MAX));
Expand All @@ -233,7 +251,8 @@ mod tests {
let list = SidebarList::new();
let query = search_key("ord");

list.replace(
replace(
&list,
index(&[("public", "users"), ("public", "orders"), ("public", "carts")]),
&query,
);
Expand Down
4 changes: 2 additions & 2 deletions linux/crates/app/src/ui/workspace/sidebar/sidebar_list_imp.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;

use gtk4 as gtk;
use gtk4::prelude::*;
Expand All @@ -12,7 +12,7 @@ use super::sidebar_object::SidebarObject;

#[derive(Default)]
pub struct SidebarList {
pub(super) index: RefCell<Rc<SidebarIndex>>,
pub(super) index: RefCell<Arc<SidebarIndex>>,
/// The entries the search keeps, in list order.
pub(super) shown: RefCell<Vec<u32>>,
/// Each section's rows in `shown`, from the first to past the last.
Expand Down
Loading
Loading