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>
This commit is contained in:
arookieofc
2026-11-04 00:03:38 +08:00
committed by GitHub
co-authored by a-rookie-of-C-language
parent 8e4c9f6f61
commit f00ce451ac
5 changed files with 520 additions and 184 deletions
+422
View File
@@ -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<QueryResult>,
pub error: Option<String>,
}
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::<String>("id")?, c.arg::<Option<String>>("password")?, c.arg::<bool>("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::<DataSource>("source")?,
c.arg::<Option<String>>("password")?,
c.arg::<Option<String>>("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::<String>("id")?, c.arg::<Option<String>>("secret")?, c.arg::<bool>("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::<String>("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::<String>("id")?;
connect(&c.host, &c.window, &id).await
});
d.add("db_set_manual_commit", |c| async move {
let (id, manual) = (c.arg::<String>("id")?, c.arg::<bool>("manual")?);
set_manual_commit(&c.host, &c.window, &id, manual).await
});
d.add("db_finish_transaction", |c| async move {
let (id, commit) = (c.arg::<String>("id")?, c.arg::<bool>("commit")?);
finish_transaction(&c.host, &c.window, &id, commit).await
});
d.add("db_disconnect", |c| async move {
let id = c.arg::<String>("id")?;
disconnect(&c.host, &c.window, &id).await
});
d.add("db_databases", |c| async move {
let id = c.arg::<String>("id")?;
databases(&c.host, &c.window, &id).await
});
d.add("db_schemas", |c| async move {
let id = c.arg::<String>("id")?;
schemas(&c.host, &c.window, &id).await
});
d.add("db_tables", |c| async move {
let (id, schema) = (c.arg::<String>("id")?, c.arg::<String>("schema")?);
tables(&c.host, &c.window, &id, &schema).await
});
d.add("db_table_details", |c| async move {
let (id, schema, table) = (c.arg::<String>("id")?, c.arg::<String>("schema")?, c.arg::<String>("table")?);
table_details(&c.host, &c.window, &id, &schema, &table).await
});
d.add("db_ddl", |c| async move {
let (id, schema, table) = (c.arg::<String>("id")?, c.arg::<String>("schema")?, c.arg::<String>("table")?);
ddl(&c.host, &c.window, &id, &schema, &table).await
});
d.add("db_schema_objects", |c| async move {
let (id, schema) = (c.arg::<String>("id")?, c.arg::<String>("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::<String>("id")?,
c.arg::<String>("schema")?,
c.arg::<DbObjectKind>("kind")?,
c.arg::<String>("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::<RequestId>("request")?, c.arg::<String>("id")?, c.arg::<String>("statement")?);
explain(&c.host, &c.window, request, &id, &statement).await
});
d.add("db_history", |c| async move {
let id = c.arg::<String>("id")?;
history(&c.host, &c.window, &id).await
});
d.add("db_remove_history", |c| async move {
let (id, texts) = (c.arg::<String>("id")?, c.arg::<Option<Vec<String>>>("texts")?);
remove_history(&c.host, &c.window, &id, texts).await
});
d.add("db_completion", |c| async move {
let id = c.arg::<String>("id")?;
completion(&c.host, &c.window, &id).await
});
d.add("db_statements", |c| async move {
let (id, text, from, to) =
(c.arg::<String>("id")?, c.arg::<String>("text")?, c.arg::<u32>("from")?, c.arg::<u32>("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::<RequestId>("request")?,
c.arg::<String>("id")?,
c.arg::<Vec<SqlStatement>>("statements")?,
c.arg::<u32>("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::<RequestId>("request")?,
c.arg::<String>("id")?,
c.arg::<String>("schema")?,
c.arg::<String>("table")?,
);
let (filter, sort, offset, limit) = (
c.arg::<Option<String>>("filter")?,
c.arg::<Vec<SortKey>>("sort")?,
c.arg::<u32>("offset")?,
c.arg::<u32>("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::<String>("id")?,
c.arg::<String>("schema")?,
c.arg::<String>("table")?,
c.arg::<Vec<DbRowEdit>>("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::<String>("id")?) });
}
/// The data sources configured for the project (the tree's roots).
pub fn sources(host: &Host, window: &str) -> IdeResult<Vec<DataSource>> {
host.project(window)?.db.sources()
}
/// Replaces the project's data sources with `sources`.
pub async fn save_sources(host: &Host, window: &str, sources: Vec<DataSource>) -> 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<String>, 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<String>,
ssh_secret: Option<String>,
) -> IdeResult<String> {
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<String>, 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<Vec<String>> {
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 `<id>:<database>`.
pub async fn databases(host: &Host, window: &str, id: &str) -> IdeResult<Databases> {
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<Vec<String>> {
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<Vec<TableInfo>> {
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<TableDetails> {
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<String> {
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<SchemaObjects> {
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<String> {
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<Vec<PlanNode>> {
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<Vec<HistoryEntry>> {
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<Vec<String>>) -> 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<CompletionModel> {
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<Vec<SqlStatement>> {
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<SqlStatement>,
max_rows: u32,
) -> IdeResult<Vec<StatementOutcome>> {
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<String>,
sort: Vec<SortKey>,
offset: u32,
limit: u32,
) -> IdeResult<TablePage> {
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<DbRowEdit>,
) -> IdeResult<f64> {
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` (`<dataDir>/consoles/db/<id>/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<IpcPath> {
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: `<id>` for the default one, `<id>.<database>` 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())
}
+2
View File
@@ -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 {
+27
View File
@@ -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}");
}
+63 -152
View File
@@ -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<Vec<DataSource>> {
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<DataSource>) -> 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<String>,
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<String>,
ssh_secret: Option<String>,
) -> IdeResult<String> {
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<String>,
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<Vec<String>> {
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 `<id>:<database>`.
#[tauri::command]
#[specta::specta]
pub async fn db_databases(window: Window, state: State<'_, AppState>, id: String) -> IdeResult<ide_db::Databases> {
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<Vec<String>> {
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<Vec<TableInfo>> {
let db = state.project(window.label())?.db.clone();
db.tables(&id, &schema).await
) -> IdeResult<Vec<ide_db::TableInfo>> {
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<TableDetails> {
let db = state.project(window.label())?.db.clone();
db.table_details(&id, &schema, &table).await
) -> IdeResult<ide_db::TableDetails> {
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<String> {
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<SchemaObjects> {
let db = state.project(window.label())?.db.clone();
db.schema_objects(&id, &schema).await
) -> IdeResult<ide_db::SchemaObjects> {
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<String> {
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<Vec<PlanNode>> {
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<Vec<ide_db::PlanNode>> {
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<Vec<HistoryEntry>> {
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<Vec<ide_db::HistoryEntry>> {
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<Vec<String>>,
) -> 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<CompletionModel> {
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_db::CompletionModel> {
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<QueryResult>,
pub error: Option<String>,
}
/// 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<Vec<SqlStatement>> {
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<SqlStatement>,
max_rows: u32,
) -> IdeResult<Vec<StatementOutcome>> {
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<SortKey>,
offset: u32,
limit: u32,
) -> IdeResult<TablePage> {
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_db::TablePage> {
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<DbRowEdit>,
) -> IdeResult<f64> {
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` (`<appData>/consoles/db/<id>/console.sql`), created on first use.
#[tauri::command]
#[specta::specta]
pub fn db_console_path(state: State<'_, AppState>, id: String) -> IdeResult<IpcPath> {
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: `<id>` for the default one, `<id>.<database>` 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)
}
+6 -32
View File
@@ -973,17 +973,8 @@ export const commands = {
proxyCheckConnection: (proxy: ProxySettings, url: string) => typedError<string, IdeError>(__TAURI_INVOKE("proxy_check_connection", { proxy, url })),
dbSources: () => typedError<DataSource[], IdeError>(__TAURI_INVOKE("db_sources")),
dbSaveSources: (sources: DataSource[]) => typedError<null, IdeError>(__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<null, IdeError>(__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<null, IdeError>(__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<null, IdeError>(__TAURI_INVOKE("db_trust_ssh_host", { id })),
/** Test Connection: connects with `config` as edited (not necessarily saved). */
sshTest: (config: SshConfiguration, secret: string | null) => typedError<string, IdeError>(__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<EnvReloaded, IdeError>(__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<string, IdeError>(__TAURI_INVOKE("db_test", { source, password, sshSecret })),
dbConnected: () => typedError<string[], IdeError>(__TAURI_INVOKE("db_connected")),
/** Connects (fails with `permissionDenied` when the password is missing or wrong). */
dbConnect: (id: string) => typedError<null, IdeError>(__TAURI_INVOKE("db_connect", { id })),
dbDisconnect: (id: string) => typedError<null, IdeError>(__TAURI_INVOKE("db_disconnect", { id })),
/** Databases of the server of data source `id` (PostgreSQL); connect to one as `<id>:<database>`. */
dbDatabases: (id: string) => typedError<DbDatabases, IdeError>(__TAURI_INVOKE("db_databases", { id })),
dbSchemas: (id: string) => typedError<string[], IdeError>(__TAURI_INVOKE("db_schemas", { id })),
dbTables: (id: string, schema: string) => typedError<DbTableInfo[], IdeError>(__TAURI_INVOKE("db_tables", { id, schema })),
dbTableDetails: (id: string, schema: string, table: string) => typedError<DbTableDetails, IdeError>(__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<string, IdeError>(__TAURI_INVOKE("db_ddl", { id, schema, table })),
/** The routines and sequences of `schema` (the tree's `routines` / `sequences`). */
dbSchemaObjects: (id: string, schema: string) => typedError<DbSchemaObjects, IdeError>(__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<string, IdeError>(__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<DbPlanNode[], IdeError>(__TAURI_INVOKE("db_explain", { request, id, statement })),
/** The statements data source `id` ran, newest first (Browse Query History). */
dbHistory: (id: string) => typedError<DbHistoryEntry[], IdeError>(__TAURI_INVOKE("db_history", { id })),
/** Removes statements from the query history of `id` (`None`: all of them). */
dbRemoveHistory: (id: string, texts: string[] | null) => typedError<null, IdeError>(__TAURI_INVOKE("db_remove_history", { id, texts })),
/** Tables, columns and foreign keys for SQL completion. */
dbCompletion: (id: string) => typedError<DbCompletion, IdeError>(__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<null, IdeError>(__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<null, IdeError>(__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<null, IdeError>(__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<SqlStatement[], IdeError>(__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<DbStatementOutcome[], IdeError>(__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<DbTablePage, IdeError>(__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<number | null, IdeError>(__TAURI_INVOKE("db_apply_edits", { id, schema, table, edits })),
/** The query console file of data source `id` (`<appData>/consoles/db/<id>/console.sql`), created on first use. */
dbConsolePath: (id: string) => typedError<IpcPath, IdeError>(__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,