From f00ce451aca73b92dfd1072dc774c137ca496a04 Mon Sep 17 00:00:00 2001 From: arookieofc <2128194521@qq.com> Date: Wed, 4 Nov 2026 00:03:38 +0800 Subject: [PATCH] fix(remote): serve the database commands from the remote server (#1029) A remote window sends a command to openidea-server only when the server registers it; none of the db_* commands were registered, so they ran locally, where a remote window has no project ("no project is open in this window"). The command bodies move from src-tauri into ide_host::ops::db, the Tauri commands become thin wrappers, and the remote dispatcher registers the same ops, with cancellation for execute / table page / explain. db_export_file stays local because exports are written on the user's machine. Fixes #1019 Co-authored-by: a-rookie-of-C-language <186315048+a-rookie-of-C-language@users.noreply.github.com> --- crates/ide-host/src/ops/db.rs | 422 +++++++++++++++++++++++++++++++++ crates/ide-host/src/ops/mod.rs | 2 + crates/ide-host/tests/rpc.rs | 27 +++ src-tauri/src/commands/db.rs | 215 +++++------------ src/bindings.ts | 38 +-- 5 files changed, 520 insertions(+), 184 deletions(-) create mode 100644 crates/ide-host/src/ops/db.rs diff --git a/crates/ide-host/src/ops/db.rs b/crates/ide-host/src/ops/db.rs new file mode 100644 index 00000000..63c8419a --- /dev/null +++ b/crates/ide-host/src/ops/db.rs @@ -0,0 +1,422 @@ +//! Database commands shared by the desktop shell's IPC commands and the +//! remote server ([`crate::rpc`]). `db_export_file` stays in the desktop +//! shell: its path comes from a local save dialog, so the export must land +//! on the user's machine, not the side the project runs on. +use crate::Host; +use crate::rpc::Dispatcher; +use ide_core::{IdeError, IdeResult, IpcPath, RequestId}; +use ide_db::split::SqlStatement; +use ide_db::{ + CompletionModel, DataSource, Databases, DbObjectKind, DbRowEdit, HistoryEntry, PlanNode, QueryResult, + SchemaObjects, SortKey, TableDetails, TableInfo, TablePage, +}; +use serde::{Deserialize, Serialize}; +use specta::Type; + +/// One statement of a console run. +#[derive(Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +#[serde(rename = "DbStatementOutcome")] +pub struct StatementOutcome { + pub statement: SqlStatement, + pub result: Option, + pub error: Option, +} + +pub(crate) fn register(d: &mut Dispatcher) { + d.add("db_sources", |c| async move { sources(&c.host, &c.window) }); + d.add("db_save_sources", |c| async move { + let sources = c.arg("sources")?; + save_sources(&c.host, &c.window, sources).await + }); + d.add("db_set_password", |c| async move { + let (id, password, save) = + (c.arg::("id")?, c.arg::>("password")?, c.arg::("save")?); + set_password(&c.host, &c.window, &id, password, save).await + }); + d.add("db_test", |c| async move { + let (source, password, ssh_secret) = ( + c.arg::("source")?, + c.arg::>("password")?, + c.arg::>("sshSecret")?, + ); + test(&c.host, &c.window, source, password, ssh_secret).await + }); + d.add("db_set_ssh_secret", |c| async move { + let (id, secret, save) = (c.arg::("id")?, c.arg::>("secret")?, c.arg::("save")?); + set_ssh_secret(&c.host, &c.window, &id, secret, save).await + }); + d.add("db_trust_ssh_host", |c| async move { + let id = c.arg::("id")?; + trust_ssh_host(&c.host, &c.window, &id).await + }); + d.add("db_connected", |c| async move { connected(&c.host, &c.window) }); + d.add("db_connect", |c| async move { + let id = c.arg::("id")?; + connect(&c.host, &c.window, &id).await + }); + d.add("db_set_manual_commit", |c| async move { + let (id, manual) = (c.arg::("id")?, c.arg::("manual")?); + set_manual_commit(&c.host, &c.window, &id, manual).await + }); + d.add("db_finish_transaction", |c| async move { + let (id, commit) = (c.arg::("id")?, c.arg::("commit")?); + finish_transaction(&c.host, &c.window, &id, commit).await + }); + d.add("db_disconnect", |c| async move { + let id = c.arg::("id")?; + disconnect(&c.host, &c.window, &id).await + }); + d.add("db_databases", |c| async move { + let id = c.arg::("id")?; + databases(&c.host, &c.window, &id).await + }); + d.add("db_schemas", |c| async move { + let id = c.arg::("id")?; + schemas(&c.host, &c.window, &id).await + }); + d.add("db_tables", |c| async move { + let (id, schema) = (c.arg::("id")?, c.arg::("schema")?); + tables(&c.host, &c.window, &id, &schema).await + }); + d.add("db_table_details", |c| async move { + let (id, schema, table) = (c.arg::("id")?, c.arg::("schema")?, c.arg::("table")?); + table_details(&c.host, &c.window, &id, &schema, &table).await + }); + d.add("db_ddl", |c| async move { + let (id, schema, table) = (c.arg::("id")?, c.arg::("schema")?, c.arg::("table")?); + ddl(&c.host, &c.window, &id, &schema, &table).await + }); + d.add("db_schema_objects", |c| async move { + let (id, schema) = (c.arg::("id")?, c.arg::("schema")?); + schema_objects(&c.host, &c.window, &id, &schema).await + }); + d.add("db_object_ddl", |c| async move { + let (id, schema, kind, name) = ( + c.arg::("id")?, + c.arg::("schema")?, + c.arg::("kind")?, + c.arg::("name")?, + ); + object_ddl(&c.host, &c.window, &id, &schema, kind, &name).await + }); + d.add("db_explain", |c| async move { + let (request, id, statement) = + (c.arg::("request")?, c.arg::("id")?, c.arg::("statement")?); + explain(&c.host, &c.window, request, &id, &statement).await + }); + d.add("db_history", |c| async move { + let id = c.arg::("id")?; + history(&c.host, &c.window, &id).await + }); + d.add("db_remove_history", |c| async move { + let (id, texts) = (c.arg::("id")?, c.arg::>>("texts")?); + remove_history(&c.host, &c.window, &id, texts).await + }); + d.add("db_completion", |c| async move { + let id = c.arg::("id")?; + completion(&c.host, &c.window, &id).await + }); + d.add("db_statements", |c| async move { + let (id, text, from, to) = + (c.arg::("id")?, c.arg::("text")?, c.arg::("from")?, c.arg::("to")?); + statements(&c.host, &c.window, &id, &text, from, to) + }); + d.add("db_execute", |c| async move { + let (request, id, statements, max_rows) = ( + c.arg::("request")?, + c.arg::("id")?, + c.arg::>("statements")?, + c.arg::("maxRows")?, + ); + execute(&c.host, &c.window, request, &id, statements, max_rows).await + }); + d.add("db_table_page", |c| async move { + let (request, id, schema, table) = ( + c.arg::("request")?, + c.arg::("id")?, + c.arg::("schema")?, + c.arg::("table")?, + ); + let (filter, sort, offset, limit) = ( + c.arg::>("filter")?, + c.arg::>("sort")?, + c.arg::("offset")?, + c.arg::("limit")?, + ); + table_page(&c.host, &c.window, request, &id, &schema, &table, filter, sort, offset, limit).await + }); + d.add("db_apply_edits", |c| async move { + let (id, schema, table, edits) = ( + c.arg::("id")?, + c.arg::("schema")?, + c.arg::("table")?, + c.arg::>("edits")?, + ); + apply_edits(&c.host, &c.window, &id, &schema, &table, edits).await + }); + d.add("db_console_path", |c| async move { console_path(&c.host, &c.arg::("id")?) }); +} + +/// The data sources configured for the project (the tree's roots). +pub fn sources(host: &Host, window: &str) -> IdeResult> { + host.project(window)?.db.sources() +} + +/// Replaces the project's data sources with `sources`. +pub async fn save_sources(host: &Host, window: &str, sources: Vec) -> IdeResult<()> { + let db = host.project(window)?.db.clone(); + db.save_sources(sources).await +} + +/// Remembers the password of data source `id` for the session, and in the OS +/// saved passwords when `save` (`None` forgets it). +pub async fn set_password(host: &Host, window: &str, id: &str, password: Option, save: bool) -> IdeResult<()> { + let db = host.project(window)?.db.clone(); + db.set_password(id, password, save).await +} + +/// Test Connection: connects with unsaved settings and returns the server version. +pub async fn test( + host: &Host, + window: &str, + source: DataSource, + password: Option, + ssh_secret: Option, +) -> IdeResult { + let db = host.project(window)?.db.clone(); + db.test(&source, password, ssh_secret).await +} + +/// Remembers the SSH password / key passphrase of a data source (see `set_password`). +pub async fn set_ssh_secret(host: &Host, window: &str, id: &str, secret: Option, save: bool) -> IdeResult<()> { + let db = host.project(window)?.db.clone(); + db.set_ssh_secret(id, secret, save).await +} + +/// Trusts the SSH host key that made the last connection of `id` fail with +/// "Unknown SSH host key" (records it in `~/.ssh/known_hosts`). +pub async fn trust_ssh_host(host: &Host, window: &str, id: &str) -> IdeResult<()> { + let db = host.project(window)?.db.clone(); + db.trust_ssh_host(id).await +} + +/// The data sources with an open connection. +pub fn connected(host: &Host, window: &str) -> IdeResult> { + Ok(host.project(window)?.db.connected()) +} + +/// Connects (fails with `permissionDenied` when the password is missing or wrong). +pub async fn connect(host: &Host, window: &str, id: &str) -> IdeResult<()> { + let db = host.project(window)?.db.clone(); + db.pool(id).await.map(|_| ()) +} + +/// Switches a data source between auto-commit and manual commit (leaving +/// manual mode rolls back what was not committed). +pub async fn set_manual_commit(host: &Host, window: &str, id: &str, manual: bool) -> IdeResult<()> { + let db = host.project(window)?.db.clone(); + db.set_manual_commit(id, manual).await; + Ok(()) +} + +/// Commits or rolls back the open transaction of a manual-commit data source. +pub async fn finish_transaction(host: &Host, window: &str, id: &str, commit: bool) -> IdeResult<()> { + let db = host.project(window)?.db.clone(); + db.finish_transaction(id, commit).await +} + +/// Closes the connection of data source `id`. +pub async fn disconnect(host: &Host, window: &str, id: &str) -> IdeResult<()> { + let db = host.project(window)?.db.clone(); + db.disconnect(id).await; + Ok(()) +} + +/// Databases of the server of data source `id` (PostgreSQL); connect to one as `:`. +pub async fn databases(host: &Host, window: &str, id: &str) -> IdeResult { + let db = host.project(window)?.db.clone(); + db.databases(id).await +} + +/// The schemas of data source `id`. +pub async fn schemas(host: &Host, window: &str, id: &str) -> IdeResult> { + let db = host.project(window)?.db.clone(); + db.schemas(id).await +} + +/// The tables and views of `schema`. +pub async fn tables(host: &Host, window: &str, id: &str, schema: &str) -> IdeResult> { + let db = host.project(window)?.db.clone(); + db.tables(id, schema).await +} + +/// Columns, keys and indexes of `table`. +pub async fn table_details(host: &Host, window: &str, id: &str, schema: &str, table: &str) -> IdeResult { + let db = host.project(window)?.db.clone(); + db.table_details(id, schema, table).await +} + +/// The DDL of table or view `table` (Go to DDL, Generate DDL). +pub async fn ddl(host: &Host, window: &str, id: &str, schema: &str, table: &str) -> IdeResult { + let db = host.project(window)?.db.clone(); + db.ddl(id, schema, table).await +} + +/// The routines and sequences of `schema` (the tree's `routines` / `sequences`). +pub async fn schema_objects(host: &Host, window: &str, id: &str, schema: &str) -> IdeResult { + let db = host.project(window)?.db.clone(); + db.schema_objects(id, schema).await +} + +/// The definition of a routine, trigger or sequence (Go to DDL, Quick Documentation). +pub async fn object_ddl( + host: &Host, + window: &str, + id: &str, + schema: &str, + kind: DbObjectKind, + name: &str, +) -> IdeResult { + let db = host.project(window)?.db.clone(); + db.object_ddl(id, schema, kind, name).await +} + +/// Explain Plan: the execution plan of `statement` on `id`, without running it. +pub async fn explain( + host: &Host, + window: &str, + request: RequestId, + id: &str, + statement: &str, +) -> IdeResult> { + let db = host.project(window)?.db.clone(); + let cancel = host.begin_request(request); + let result = db.explain(id, statement, &cancel).await; + host.end_request(request); + result +} + +/// The statements data source `id` ran, newest first (Browse Query History). +pub async fn history(host: &Host, window: &str, id: &str) -> IdeResult> { + let db = host.project(window)?.db.clone(); + db.history(id).await +} + +/// Removes statements from the query history of `id` (`None`: all of them). +pub async fn remove_history(host: &Host, window: &str, id: &str, texts: Option>) -> IdeResult<()> { + let db = host.project(window)?.db.clone(); + db.remove_history(id, texts).await +} + +/// Tables, columns and foreign keys for SQL completion. +pub async fn completion(host: &Host, window: &str, id: &str) -> IdeResult { + let db = host.project(window)?.db.clone(); + db.completion(id).await +} + +/// Statements a console runs: the selection's (`from < to`) or the one at the caret (`from`). +pub fn statements(host: &Host, window: &str, id: &str, text: &str, from: u32, to: u32) -> IdeResult> { + let driver = host.project(window)?.db.source(id)?.driver; + Ok(if from < to { + ide_db::split::statements_in(text, from, to, driver) + } else { + ide_db::split::statement_at(text, from, driver).into_iter().collect() + }) +} + +/// Runs `statements` in order on data source `id`, stopping at the first +/// error. Cancel with `cancel_request(request)`. +pub async fn execute( + host: &Host, + window: &str, + request: RequestId, + id: &str, + statements: Vec, + max_rows: u32, +) -> IdeResult> { + let db = host.project(window)?.db.clone(); + let cancel = host.begin_request(request); + let mut out = Vec::new(); + for statement in statements { + match db.execute(id, &statement.text, max_rows.max(1) as usize, &cancel).await { + Ok(result) => out.push(StatementOutcome { statement, result: Some(result), error: None }), + Err(e) if e.kind == ide_core::error::ErrorKind::Cancelled => { + host.end_request(request); + return Err(e); + } + Err(e) if e.kind == ide_core::error::ErrorKind::PermissionDenied && out.is_empty() => { + // Not connected for lack of a password: the UI asks and retries. + host.end_request(request); + return Err(e); + } + Err(e) => { + out.push(StatementOutcome { statement, result: None, error: Some(e.message) }); + break; + } + } + } + host.end_request(request); + Ok(out) +} + +/// One page of the rows of `table` (the table editor). +#[allow(clippy::too_many_arguments)] +pub async fn table_page( + host: &Host, + window: &str, + request: RequestId, + id: &str, + schema: &str, + table: &str, + filter: Option, + sort: Vec, + offset: u32, + limit: u32, +) -> IdeResult { + let db = host.project(window)?.db.clone(); + let cancel = host.begin_request(request); + let result = + db.table_page(id, schema, table, filter.as_deref(), &sort, offset.into(), limit.max(1).into(), &cancel).await; + host.end_request(request); + result +} + +/// Submits the table editor's changes in one transaction; returns the rows changed. +pub async fn apply_edits( + host: &Host, + window: &str, + id: &str, + schema: &str, + table: &str, + edits: Vec, +) -> IdeResult { + let db = host.project(window)?.db.clone(); + Ok(db.apply_edits(id, schema, table, &edits).await? as f64) +} + +/// The query console file of data source `id` (`/consoles/db//console.sql`), +/// created on first use. Consoles are files of the project, so a remote +/// window resolves it on the host the project runs on. +pub fn console_path(host: &Host, id: &str) -> IdeResult { + let (source, database) = ide_db::split_target(id); + if source.is_empty() || !source.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') { + return Err(IdeError::invalid("invalid data source id")); + } + // One console per database: `` for the default one, `.` for the others. + let name = match database { + Some(db) => { + let safe: String = + db.chars().map(|c| if c.is_alphanumeric() || c == '-' || c == '_' { c } else { '_' }).collect(); + format!("{source}.{safe}") + } + None => source.to_owned(), + }; + let dir = host.data_dir.join("consoles").join("db").join(&name); + std::fs::create_dir_all(&dir)?; + let file = dir.join("console.sql"); + if !file.exists() { + std::fs::write(&file, "")?; + } + Ok(file.into()) +} diff --git a/crates/ide-host/src/ops/mod.rs b/crates/ide-host/src/ops/mod.rs index c29dc4e9..9c020130 100644 --- a/crates/ide-host/src/ops/mod.rs +++ b/crates/ide-host/src/ops/mod.rs @@ -7,6 +7,7 @@ pub mod analysis; pub mod build; pub mod bytes; pub mod coverage; +pub mod db; pub mod debug; pub mod dependencies; pub mod deploy; @@ -86,6 +87,7 @@ pub fn register_all(d: &mut crate::rpc::Dispatcher) { run::register(d); env::register(d); deploy::register(d); + db::register(d); } impl Host { diff --git a/crates/ide-host/tests/rpc.rs b/crates/ide-host/tests/rpc.rs index a0ec5004..f0158ac3 100644 --- a/crates/ide-host/tests/rpc.rs +++ b/crates/ide-host/tests/rpc.rs @@ -321,6 +321,33 @@ async fn local_deployment_runs_on_the_project_host() { "remote_delete", "remote_mkdir", "remote_rename", + "db_sources", + "db_save_sources", + "db_set_password", + "db_test", + "db_set_ssh_secret", + "db_trust_ssh_host", + "db_connected", + "db_connect", + "db_set_manual_commit", + "db_finish_transaction", + "db_disconnect", + "db_databases", + "db_schemas", + "db_tables", + "db_table_details", + "db_ddl", + "db_schema_objects", + "db_object_ddl", + "db_explain", + "db_history", + "db_remove_history", + "db_completion", + "db_statements", + "db_execute", + "db_table_page", + "db_apply_edits", + "db_console_path", ] { assert!(dispatcher.commands().contains(&cmd), "missing host command {cmd}"); } diff --git a/src-tauri/src/commands/db.rs b/src-tauri/src/commands/db.rs index 93f20ff6..0b9781c5 100644 --- a/src-tauri/src/commands/db.rs +++ b/src-tauri/src/commands/db.rs @@ -1,29 +1,25 @@ +//! IPC commands of `ide_host::ops::db` (the shared implementation). + use crate::state::AppState; -use ide_core::{IdeError, IdeResult, IpcPath, RequestId}; +use ide_core::{IdeResult, IpcPath, RequestId}; use ide_db::split::SqlStatement; -use ide_db::{ - CompletionModel, DataSource, DbObjectKind, DbRowEdit, HistoryEntry, PlanNode, QueryResult, SchemaObjects, SortKey, - TableDetails, TableInfo, TablePage, -}; -use serde::{Deserialize, Serialize}; -use specta::Type; +use ide_db::{DataSource, DbObjectKind, DbRowEdit, SortKey}; +#[allow(unused_imports)] +pub use ide_host::ops::db::*; use tauri::{State, Window}; #[tauri::command] #[specta::specta] pub fn db_sources(window: Window, state: State<'_, AppState>) -> IdeResult> { - state.project(window.label())?.db.sources() + ide_host::ops::db::sources(&state.host, window.label()) } #[tauri::command] #[specta::specta] pub async fn db_save_sources(window: Window, state: State<'_, AppState>, sources: Vec) -> IdeResult<()> { - let db = state.project(window.label())?.db.clone(); - db.save_sources(sources).await + ide_host::ops::db::save_sources(&state.host, window.label(), sources).await } -/// Remembers the password of data source `id` for the session, and in the OS -/// saved passwords when `save` (`None` forgets it). #[tauri::command] #[specta::specta] pub async fn db_set_password( @@ -33,11 +29,9 @@ pub async fn db_set_password( password: Option, save: bool, ) -> IdeResult<()> { - let db = state.project(window.label())?.db.clone(); - db.set_password(&id, password, save).await + ide_host::ops::db::set_password(&state.host, window.label(), &id, password, save).await } -/// Test Connection: connects with unsaved settings and returns the server version. #[tauri::command] #[specta::specta] pub async fn db_test( @@ -47,11 +41,9 @@ pub async fn db_test( password: Option, ssh_secret: Option, ) -> IdeResult { - let db = state.project(window.label())?.db.clone(); - db.test(&source, password, ssh_secret).await + ide_host::ops::db::test(&state.host, window.label(), source, password, ssh_secret).await } -/// Remembers the SSH password / key passphrase of a data source (see `db_set_password`). #[tauri::command] #[specta::specta] pub async fn db_set_ssh_secret( @@ -61,35 +53,27 @@ pub async fn db_set_ssh_secret( secret: Option, save: bool, ) -> IdeResult<()> { - let db = state.project(window.label())?.db.clone(); - db.set_ssh_secret(&id, secret, save).await + ide_host::ops::db::set_ssh_secret(&state.host, window.label(), &id, secret, save).await } -/// Trusts the SSH host key that made the last connection of `id` fail with -/// "Unknown SSH host key" (records it in `~/.ssh/known_hosts`). #[tauri::command] #[specta::specta] pub async fn db_trust_ssh_host(window: Window, state: State<'_, AppState>, id: String) -> IdeResult<()> { - let db = state.project(window.label())?.db.clone(); - db.trust_ssh_host(&id).await + ide_host::ops::db::trust_ssh_host(&state.host, window.label(), &id).await } #[tauri::command] #[specta::specta] pub fn db_connected(window: Window, state: State<'_, AppState>) -> IdeResult> { - Ok(state.project(window.label())?.db.connected()) + ide_host::ops::db::connected(&state.host, window.label()) } -/// Connects (fails with `permissionDenied` when the password is missing or wrong). #[tauri::command] #[specta::specta] pub async fn db_connect(window: Window, state: State<'_, AppState>, id: String) -> IdeResult<()> { - let db = state.project(window.label())?.db.clone(); - db.pool(&id).await.map(|_| ()) + ide_host::ops::db::connect(&state.host, window.label(), &id).await } -/// Switches a data source between auto-commit and manual commit (leaving -/// manual mode rolls back what was not committed). #[tauri::command] #[specta::specta] pub async fn db_set_manual_commit( @@ -98,12 +82,9 @@ pub async fn db_set_manual_commit( id: String, manual: bool, ) -> IdeResult<()> { - let db = state.project(window.label())?.db.clone(); - db.set_manual_commit(&id, manual).await; - Ok(()) + ide_host::ops::db::set_manual_commit(&state.host, window.label(), &id, manual).await } -/// Commits or rolls back the open transaction of a manual-commit data source. #[tauri::command] #[specta::specta] pub async fn db_finish_transaction( @@ -112,31 +93,25 @@ pub async fn db_finish_transaction( id: String, commit: bool, ) -> IdeResult<()> { - let db = state.project(window.label())?.db.clone(); - db.finish_transaction(&id, commit).await + ide_host::ops::db::finish_transaction(&state.host, window.label(), &id, commit).await } #[tauri::command] #[specta::specta] pub async fn db_disconnect(window: Window, state: State<'_, AppState>, id: String) -> IdeResult<()> { - let db = state.project(window.label())?.db.clone(); - db.disconnect(&id).await; - Ok(()) + ide_host::ops::db::disconnect(&state.host, window.label(), &id).await } -/// Databases of the server of data source `id` (PostgreSQL); connect to one as `:`. #[tauri::command] #[specta::specta] pub async fn db_databases(window: Window, state: State<'_, AppState>, id: String) -> IdeResult { - let db = state.project(window.label())?.db.clone(); - db.databases(&id).await + ide_host::ops::db::databases(&state.host, window.label(), &id).await } #[tauri::command] #[specta::specta] pub async fn db_schemas(window: Window, state: State<'_, AppState>, id: String) -> IdeResult> { - let db = state.project(window.label())?.db.clone(); - db.schemas(&id).await + ide_host::ops::db::schemas(&state.host, window.label(), &id).await } #[tauri::command] @@ -146,9 +121,8 @@ pub async fn db_tables( state: State<'_, AppState>, id: String, schema: String, -) -> IdeResult> { - let db = state.project(window.label())?.db.clone(); - db.tables(&id, &schema).await +) -> IdeResult> { + ide_host::ops::db::tables(&state.host, window.label(), &id, &schema).await } #[tauri::command] @@ -159,12 +133,10 @@ pub async fn db_table_details( id: String, schema: String, table: String, -) -> IdeResult { - let db = state.project(window.label())?.db.clone(); - db.table_details(&id, &schema, &table).await +) -> IdeResult { + ide_host::ops::db::table_details(&state.host, window.label(), &id, &schema, &table).await } -/// The DDL of table or view `table` (Go to DDL, Generate DDL). #[tauri::command] #[specta::specta] pub async fn db_ddl( @@ -174,11 +146,9 @@ pub async fn db_ddl( schema: String, table: String, ) -> IdeResult { - let db = state.project(window.label())?.db.clone(); - db.ddl(&id, &schema, &table).await + ide_host::ops::db::ddl(&state.host, window.label(), &id, &schema, &table).await } -/// The routines and sequences of `schema` (the tree's `routines` / `sequences`). #[tauri::command] #[specta::specta] pub async fn db_schema_objects( @@ -186,12 +156,10 @@ pub async fn db_schema_objects( state: State<'_, AppState>, id: String, schema: String, -) -> IdeResult { - let db = state.project(window.label())?.db.clone(); - db.schema_objects(&id, &schema).await +) -> IdeResult { + ide_host::ops::db::schema_objects(&state.host, window.label(), &id, &schema).await } -/// The definition of a routine, trigger or sequence (Go to DDL, Quick Documentation). #[tauri::command] #[specta::specta] pub async fn db_object_ddl( @@ -202,11 +170,9 @@ pub async fn db_object_ddl( kind: DbObjectKind, name: String, ) -> IdeResult { - let db = state.project(window.label())?.db.clone(); - db.object_ddl(&id, &schema, kind, &name).await + ide_host::ops::db::object_ddl(&state.host, window.label(), &id, &schema, kind, &name).await } -/// Explain Plan: the execution plan of `statement` on `id`, without running it. #[tauri::command] #[specta::specta] pub async fn db_explain( @@ -215,23 +181,20 @@ pub async fn db_explain( request: RequestId, id: String, statement: String, -) -> IdeResult> { - let db = state.project(window.label())?.db.clone(); - let cancel = state.begin_request(request); - let result = db.explain(&id, &statement, &cancel).await; - state.end_request(request); - result +) -> IdeResult> { + ide_host::ops::db::explain(&state.host, window.label(), request, &id, &statement).await } -/// The statements data source `id` ran, newest first (Browse Query History). #[tauri::command] #[specta::specta] -pub async fn db_history(window: Window, state: State<'_, AppState>, id: String) -> IdeResult> { - let db = state.project(window.label())?.db.clone(); - db.history(&id).await +pub async fn db_history( + window: Window, + state: State<'_, AppState>, + id: String, +) -> IdeResult> { + ide_host::ops::db::history(&state.host, window.label(), &id).await } -/// Removes statements from the query history of `id` (`None`: all of them). #[tauri::command] #[specta::specta] pub async fn db_remove_history( @@ -240,11 +203,12 @@ pub async fn db_remove_history( id: String, texts: Option>, ) -> IdeResult<()> { - let db = state.project(window.label())?.db.clone(); - db.remove_history(&id, texts).await + ide_host::ops::db::remove_history(&state.host, window.label(), &id, texts).await } -/// Writes exported result data (CSV, JSON, SQL…) to `path`. +/// Writes exported result data (CSV, JSON, SQL…) to `path`. Stays in the +/// desktop shell: the path comes from a local save dialog, and a remote +/// window exports through its local fs commands instead. #[tauri::command] #[specta::specta] pub async fn db_export_file(path: std::path::PathBuf, text: String) -> IdeResult<()> { @@ -255,24 +219,16 @@ pub async fn db_export_file(path: std::path::PathBuf, text: String) -> IdeResult .await } -/// Tables, columns and foreign keys for SQL completion. #[tauri::command] #[specta::specta] -pub async fn db_completion(window: Window, state: State<'_, AppState>, id: String) -> IdeResult { - let db = state.project(window.label())?.db.clone(); - db.completion(&id).await +pub async fn db_completion( + window: Window, + state: State<'_, AppState>, + id: String, +) -> IdeResult { + ide_host::ops::db::completion(&state.host, window.label(), &id).await } -#[derive(Clone, Serialize, Deserialize, Type)] -#[serde(rename_all = "camelCase")] -#[serde(rename = "DbStatementOutcome")] -pub struct StatementOutcome { - pub statement: SqlStatement, - pub result: Option, - pub error: Option, -} - -/// Statements a console runs: the selection's (`from < to`) or the one at the caret (`from`). #[tauri::command] #[specta::specta] pub fn db_statements( @@ -283,16 +239,9 @@ pub fn db_statements( from: u32, to: u32, ) -> IdeResult> { - let driver = state.project(window.label())?.db.source(&id)?.driver; - Ok(if from < to { - ide_db::split::statements_in(&text, from, to, driver) - } else { - ide_db::split::statement_at(&text, from, driver).into_iter().collect() - }) + ide_host::ops::db::statements(&state.host, window.label(), &id, &text, from, to) } -/// Runs `statements` in order on data source `id`, stopping at the first -/// error. Cancel with `cancel_request(request)`. #[tauri::command] #[specta::specta] pub async fn db_execute( @@ -303,29 +252,7 @@ pub async fn db_execute( statements: Vec, max_rows: u32, ) -> IdeResult> { - let db = state.project(window.label())?.db.clone(); - let cancel = state.begin_request(request); - let mut out = Vec::new(); - for statement in statements { - match db.execute(&id, &statement.text, max_rows.max(1) as usize, &cancel).await { - Ok(result) => out.push(StatementOutcome { statement, result: Some(result), error: None }), - Err(e) if e.kind == ide_core::error::ErrorKind::Cancelled => { - state.end_request(request); - return Err(e); - } - Err(e) if e.kind == ide_core::error::ErrorKind::PermissionDenied && out.is_empty() => { - // Not connected for lack of a password: the UI asks and retries. - state.end_request(request); - return Err(e); - } - Err(e) => { - out.push(StatementOutcome { statement, result: None, error: Some(e.message) }); - break; - } - } - } - state.end_request(request); - Ok(out) + ide_host::ops::db::execute(&state.host, window.label(), request, &id, statements, max_rows).await } #[tauri::command] @@ -342,17 +269,22 @@ pub async fn db_table_page( sort: Vec, offset: u32, limit: u32, -) -> IdeResult { - let db = state.project(window.label())?.db.clone(); - let cancel = state.begin_request(request); - let result = db - .table_page(&id, &schema, &table, filter.as_deref(), &sort, offset.into(), limit.max(1).into(), &cancel) - .await; - state.end_request(request); - result +) -> IdeResult { + ide_host::ops::db::table_page( + &state.host, + window.label(), + request, + &id, + &schema, + &table, + filter, + sort, + offset, + limit, + ) + .await } -/// Submits the table editor's changes in one transaction; returns the rows changed. #[tauri::command] #[specta::specta] pub async fn db_apply_edits( @@ -363,32 +295,11 @@ pub async fn db_apply_edits( table: String, edits: Vec, ) -> IdeResult { - let db = state.project(window.label())?.db.clone(); - Ok(db.apply_edits(&id, &schema, &table, &edits).await? as f64) + ide_host::ops::db::apply_edits(&state.host, window.label(), &id, &schema, &table, edits).await } -/// The query console file of data source `id` (`/consoles/db//console.sql`), created on first use. #[tauri::command] #[specta::specta] pub fn db_console_path(state: State<'_, AppState>, id: String) -> IdeResult { - let (source, database) = ide_db::split_target(&id); - if source.is_empty() || !source.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') { - return Err(IdeError::invalid("invalid data source id")); - } - // One console per database: `` for the default one, `.` for the others. - let name = match database { - Some(db) => { - let safe: String = - db.chars().map(|c| if c.is_alphanumeric() || c == '-' || c == '_' { c } else { '_' }).collect(); - format!("{source}.{safe}") - } - None => source.to_owned(), - }; - let dir = state.data_dir.join("consoles").join("db").join(&name); - std::fs::create_dir_all(&dir)?; - let file = dir.join("console.sql"); - if !file.exists() { - std::fs::write(&file, "")?; - } - Ok(file.into()) + ide_host::ops::db::console_path(&state.host, &id) } diff --git a/src/bindings.ts b/src/bindings.ts index fd9ac8c3..02fc09f7 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -973,17 +973,8 @@ export const commands = { proxyCheckConnection: (proxy: ProxySettings, url: string) => typedError(__TAURI_INVOKE("proxy_check_connection", { proxy, url })), dbSources: () => typedError(__TAURI_INVOKE("db_sources")), dbSaveSources: (sources: DataSource[]) => typedError(__TAURI_INVOKE("db_save_sources", { sources })), - /** - * Remembers the password of data source `id` for the session, and in the OS - * saved passwords when `save` (`None` forgets it). - */ dbSetPassword: (id: string, password: string | null, save: boolean) => typedError(__TAURI_INVOKE("db_set_password", { id, password, save })), - /** Remembers the SSH password / key passphrase of a data source (see `db_set_password`). */ dbSetSshSecret: (id: string, secret: string | null, save: boolean) => typedError(__TAURI_INVOKE("db_set_ssh_secret", { id, secret, save })), - /** - * Trusts the SSH host key that made the last connection of `id` fail with - * "Unknown SSH host key" (records it in `~/.ssh/known_hosts`). - */ dbTrustSshHost: (id: string) => typedError(__TAURI_INVOKE("db_trust_ssh_host", { id })), /** Test Connection: connects with `config` as edited (not necessarily saved). */ sshTest: (config: SshConfiguration, secret: string | null) => typedError(__TAURI_INVOKE("ssh_test", { config, secret })), @@ -1054,51 +1045,33 @@ export const commands = { * (and the project's `remoteEnv` on a remote host); running ones keep theirs. */ envReload: () => typedError(__TAURI_INVOKE("env_reload")), - /** Test Connection: connects with unsaved settings and returns the server version. */ dbTest: (source: DataSource, password: string | null, sshSecret: string | null) => typedError(__TAURI_INVOKE("db_test", { source, password, sshSecret })), dbConnected: () => typedError(__TAURI_INVOKE("db_connected")), - /** Connects (fails with `permissionDenied` when the password is missing or wrong). */ dbConnect: (id: string) => typedError(__TAURI_INVOKE("db_connect", { id })), dbDisconnect: (id: string) => typedError(__TAURI_INVOKE("db_disconnect", { id })), - /** Databases of the server of data source `id` (PostgreSQL); connect to one as `:`. */ dbDatabases: (id: string) => typedError(__TAURI_INVOKE("db_databases", { id })), dbSchemas: (id: string) => typedError(__TAURI_INVOKE("db_schemas", { id })), dbTables: (id: string, schema: string) => typedError(__TAURI_INVOKE("db_tables", { id, schema })), dbTableDetails: (id: string, schema: string, table: string) => typedError(__TAURI_INVOKE("db_table_details", { id, schema, table })), - /** The DDL of table or view `table` (Go to DDL, Generate DDL). */ dbDdl: (id: string, schema: string, table: string) => typedError(__TAURI_INVOKE("db_ddl", { id, schema, table })), - /** The routines and sequences of `schema` (the tree's `routines` / `sequences`). */ dbSchemaObjects: (id: string, schema: string) => typedError(__TAURI_INVOKE("db_schema_objects", { id, schema })), - /** The definition of a routine, trigger or sequence (Go to DDL, Quick Documentation). */ dbObjectDdl: (id: string, schema: string, kind: DbObjectKind, name: string) => typedError(__TAURI_INVOKE("db_object_ddl", { id, schema, kind, name })), - /** Explain Plan: the execution plan of `statement` on `id`, without running it. */ dbExplain: (request: RequestId, id: string, statement: string) => typedError(__TAURI_INVOKE("db_explain", { request, id, statement })), - /** The statements data source `id` ran, newest first (Browse Query History). */ dbHistory: (id: string) => typedError(__TAURI_INVOKE("db_history", { id })), - /** Removes statements from the query history of `id` (`None`: all of them). */ dbRemoveHistory: (id: string, texts: string[] | null) => typedError(__TAURI_INVOKE("db_remove_history", { id, texts })), - /** Tables, columns and foreign keys for SQL completion. */ dbCompletion: (id: string) => typedError(__TAURI_INVOKE("db_completion", { id })), - /** Writes exported result data (CSV, JSON, SQL…) to `path`. */ + /** + * Writes exported result data (CSV, JSON, SQL…) to `path`. Stays in the + * desktop shell: the path comes from a local save dialog, and a remote + * window exports through its local fs commands instead. + */ dbExportFile: (path: string, text: string) => typedError(__TAURI_INVOKE("db_export_file", { path, text })), - /** - * Switches a data source between auto-commit and manual commit (leaving - * manual mode rolls back what was not committed). - */ dbSetManualCommit: (id: string, manual: boolean) => typedError(__TAURI_INVOKE("db_set_manual_commit", { id, manual })), - /** Commits or rolls back the open transaction of a manual-commit data source. */ dbFinishTransaction: (id: string, commit: boolean) => typedError(__TAURI_INVOKE("db_finish_transaction", { id, commit })), - /** Statements a console runs: the selection's (`from < to`) or the one at the caret (`from`). */ dbStatements: (id: string, text: string, from: number, to: number) => typedError(__TAURI_INVOKE("db_statements", { id, text, from, to })), - /** - * Runs `statements` in order on data source `id`, stopping at the first - * error. Cancel with `cancel_request(request)`. - */ dbExecute: (request: RequestId, id: string, statements: SqlStatement[], maxRows: number) => typedError(__TAURI_INVOKE("db_execute", { request, id, statements, maxRows })), dbTablePage: (request: RequestId, id: string, schema: string, table: string, filter: string | null, sort: DbSortKey[], offset: number, limit: number) => typedError(__TAURI_INVOKE("db_table_page", { request, id, schema, table, filter, sort, offset, limit })), - /** Submits the table editor's changes in one transaction; returns the rows changed. */ dbApplyEdits: (id: string, schema: string, table: string, edits: DbRowEdit[]) => typedError(__TAURI_INVOKE("db_apply_edits", { id, schema, table, edits })), - /** The query console file of data source `id` (`/consoles/db//console.sql`), created on first use. */ dbConsolePath: (id: string) => typedError(__TAURI_INVOKE("db_console_path", { id })), }; @@ -2403,6 +2376,7 @@ export type DbSslMode = "disable" | "prefer" | "require" | /** [`DbSslMode::VerifyCa`], and check that the certificate names the host. */ "verifyFull"; +/** One statement of a console run. */ export type DbStatementOutcome = { statement: SqlStatement, result: DbQueryResult | null,