Files
dbx/crates/dbx-web/src/routes/query.rs
T

1047 lines
36 KiB
Rust

use std::sync::Arc;
use axum::extract::State;
use axum::http::HeaderMap;
use axum::Json;
use serde::Deserialize;
use crate::error::AppError;
use crate::state::WebState;
use dbx_core::query_cancel::RunningTaskMetadata;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecuteQueryRequest {
pub connection_id: String,
pub database: String,
pub sql: String,
pub schema: Option<String>,
pub catalog: Option<String>,
pub execution_id: Option<String>,
pub max_rows: Option<usize>,
pub fetch_size: Option<usize>,
pub page_size: Option<usize>,
pub result_session_id: Option<String>,
pub client_session_id: Option<String>,
pub timeout_secs: Option<u64>,
pub use_transaction: Option<bool>,
pub continue_on_error: Option<bool>,
pub execution_mode: Option<dbx_core::query::QueryExecutionMode>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CancelRequest {
pub execution_id: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CloseSessionRequest {
pub connection_id: String,
pub database: String,
pub session_id: String,
pub catalog: Option<String>,
pub client_session_id: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CloseClientConnectionSessionRequest {
pub connection_id: String,
pub database: String,
pub catalog: Option<String>,
pub client_session_id: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecuteBatchRequest {
pub connection_id: String,
pub database: String,
pub statements: Vec<String>,
pub schema: Option<String>,
pub catalog: Option<String>,
pub timeout_secs: Option<u64>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AnalyzeSqlReferencesRequest {
pub sql: String,
pub dialect: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AnalyzeEditableQueryRequest {
pub sql: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FindStatementAtCursorRequest {
pub sql: String,
pub cursor_pos: usize,
pub database_type: Option<dbx_core::models::connection::DatabaseType>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PrepareQueryPaginationExecutionPlanRequest {
pub options: dbx_core::query_result_sql::QueryPaginationExecutionPlanOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildSortedQuerySqlRequest {
pub options: dbx_core::query_result_sql::SortedQuerySqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildExplainSqlRequest {
pub options: dbx_core::query_execution_sql::ExplainSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildDroppedFilePreviewSqlRequest {
pub options: dbx_core::query_execution_sql::DroppedFilePreviewSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildTableSelectSqlRequest {
pub options: dbx_core::sql_dialect::TableDataSelectSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildDatabaseSearchSqlRequest {
pub options: dbx_core::database_search_sql::DatabaseSearchSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildSearchResultWhereRequest {
pub options: dbx_core::database_search_sql::SearchResultWhereOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildRenameObjectSqlRequest {
pub options: dbx_core::db_admin_sql::RenameObjectSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildCreateDatabaseSqlRequest {
pub options: dbx_core::db_admin_sql::CreateDatabaseSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg(feature = "duckdb-sidecar")]
pub struct BuildDuckDbAttachDatabaseSqlRequest {
pub options: dbx_core::db_admin_sql::DuckDbAttachDatabaseSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildSqliteAttachDatabaseSqlRequest {
pub options: dbx_core::db_admin_sql::SqliteAttachDatabaseSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildDropObjectSqlRequest {
pub options: dbx_core::db_admin_sql::DropObjectSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildTableAdminSqlRequest {
pub options: dbx_core::db_admin_sql::TableAdminSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildDropTableChildObjectSqlRequest {
pub options: dbx_core::db_admin_sql::DropTableChildObjectSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildDatabaseNameSqlRequest {
pub options: dbx_core::db_admin_sql::DatabaseNameSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildSchemaNameSqlRequest {
pub options: dbx_core::db_admin_sql::SchemaNameSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildDatabasePropertyEditSqlRequest {
pub options: dbx_core::db_admin_sql::DatabasePropertyEditSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildDuplicateTableStructureSqlRequest {
pub options: dbx_core::db_admin_sql::DuplicateTableStructureSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildCopyTableDataSqlRequest {
pub options: dbx_core::db_admin_sql::CopyTableDataSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildExecutableObjectSourceRequest {
pub input: dbx_core::object_source_sql::EditableObjectSourceSqlInput,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildRoutineRenameObjectSourceRequest {
pub input: dbx_core::object_source_sql::RoutineRenameObjectSourceInput,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildViewDdlRequest {
pub input: dbx_core::object_source_sql::BuildViewDdlInput,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildTableStructureSqlRequest {
pub options: dbx_core::table_structure_sql::TableStructureSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PreviewSqliteTableStructureChangeRequest {
pub connection_id: String,
pub database: String,
pub options: dbx_core::table_structure_sql::TableStructureSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApplySqliteTableStructureChangeRequest {
pub connection_id: String,
pub database: String,
pub options: dbx_core::table_structure_sql::TableStructureSqlOptions,
pub schema_revision: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildSingleColumnAlterSqlRequest {
pub options: dbx_core::table_structure_sql::SingleColumnAlterSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PrepareDataGridSaveRequest {
pub options: dbx_core::data_grid_sql::DataGridSaveStatementOptions,
}
#[derive(Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExtractDataGridSelectionRequest {
pub request: dbx_core::data_grid_extractors::DataGridExtractRequest,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildDataGridCopyUpdateStatementsRequest {
pub options: dbx_core::data_grid_sql::DataGridCopyUpdateStatementOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildDataGridCopyInsertStatementRequest {
pub options: dbx_core::data_grid_sql::DataGridCopyInsertStatementOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildDataGridContextFilterConditionRequest {
pub options: dbx_core::data_grid_sql::DataGridContextFilterConditionOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildDataGridColumnValueFilterConditionRequest {
pub options: dbx_core::data_grid_sql::DataGridColumnValueFilterConditionOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildDataGridColumnValuesFilterConditionRequest {
pub options: dbx_core::data_grid_sql::DataGridColumnValuesFilterConditionOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildDataGridColumnDistinctValuesSqlRequest {
pub options: dbx_core::data_grid_sql::DataGridColumnDistinctValuesSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildDataGridCountSqlRequest {
pub options: dbx_core::data_grid_sql::DataGridCountSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildHiveTablePropertiesSqlRequest {
pub options: dbx_core::data_grid_sql::HiveTablePropertiesSqlOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildExportInsertStatementsRequest {
pub options: dbx_core::database_export::BuildExportInsertStatementsOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildExportSqlInsertRequest {
pub options: dbx_core::database_export::BuildExportSqlInsertOptions,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildDatabaseSqlExportRequest {
pub options: dbx_core::database_export::BuildDatabaseSqlExportOptions,
}
pub async fn execute_query(
State(state): State<Arc<WebState>>,
headers: HeaderMap,
Json(req): Json<ExecuteQueryRequest>,
) -> Result<Json<dbx_core::db::QueryResult>, AppError> {
let allow_database_switch = req.client_session_id.as_deref().is_some_and(|id| !id.trim().is_empty());
super::mcp_policy::ensure_sql(&state, &headers, &req.connection_id, &req.database, &req.sql, allow_database_switch)
.await?;
let execution_id = req.execution_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
let registered = state.app.running_queries.register_task(
execution_id.clone(),
RunningTaskMetadata::query(req.connection_id.clone(), req.database.clone(), req.client_session_id.clone()),
);
let cancel_token = registered.token();
tracing::debug!(connection_id = %req.connection_id, "execute_query");
let result = dbx_core::query::execute_sql_statement_with_options_typed(
&state.app,
&req.connection_id,
&req.database,
&req.sql,
req.schema.as_deref(),
Some(cancel_token),
dbx_core::query::QueryExecutionOptions {
max_rows: req.max_rows,
fetch_size: req.fetch_size,
page_size: req.page_size,
catalog: req.catalog,
result_session_id: req.result_session_id,
client_session_id: req.client_session_id,
timeout_secs: req.timeout_secs,
execution_id: Some(execution_id),
use_transaction: req.use_transaction,
execution_mode: req.execution_mode.unwrap_or_default(),
..Default::default()
},
)
.await
.map_err(|error| AppError::from(error.into_backend_error()))?;
drop(registered);
Ok(Json(result))
}
pub async fn execute_multi(
State(state): State<Arc<WebState>>,
headers: HeaderMap,
Json(req): Json<ExecuteQueryRequest>,
) -> Result<Json<Vec<dbx_core::query::ExecuteMultiResult>>, AppError> {
let allow_database_switch = req.client_session_id.as_deref().is_some_and(|id| !id.trim().is_empty());
super::mcp_policy::ensure_sql(&state, &headers, &req.connection_id, &req.database, &req.sql, allow_database_switch)
.await?;
let execution_id = req.execution_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
let registered = state.app.running_queries.register_task(
execution_id.clone(),
RunningTaskMetadata::query(req.connection_id.clone(), req.database.clone(), req.client_session_id.clone()),
);
let cancel_token = registered.token();
tracing::debug!(connection_id = %req.connection_id, "execute_multi");
let result = dbx_core::query::execute_multi_core_with_options_for_client_typed(
&state.app,
&req.connection_id,
&req.database,
&req.sql,
req.schema.as_deref(),
Some(cancel_token),
dbx_core::query::QueryExecutionOptions {
max_rows: req.max_rows,
fetch_size: req.fetch_size,
page_size: req.page_size,
catalog: req.catalog,
result_session_id: req.result_session_id,
client_session_id: req.client_session_id,
timeout_secs: req.timeout_secs,
execution_id: Some(execution_id),
use_transaction: req.use_transaction,
continue_on_error: req.continue_on_error.unwrap_or(false),
execution_mode: req.execution_mode.unwrap_or_default(),
},
)
.await
.map_err(|error| AppError::from(error.into_backend_error()))?;
drop(registered);
Ok(execute_multi_response(result))
}
fn execute_multi_response(
result: Vec<dbx_core::query::ExecuteMultiResult>,
) -> Json<Vec<dbx_core::query::ExecuteMultiResult>> {
Json(result)
}
pub async fn execute_batch(
State(state): State<Arc<WebState>>,
headers: HeaderMap,
Json(req): Json<ExecuteBatchRequest>,
) -> Result<Json<dbx_core::db::QueryResult>, AppError> {
for statement in &req.statements {
super::mcp_policy::ensure_sql(&state, &headers, &req.connection_id, &req.database, statement, false).await?;
}
tracing::debug!(connection_id = %req.connection_id, "execute_batch");
let result = dbx_core::query::execute_statements(
&state.app,
&req.connection_id,
&req.database,
&req.statements,
req.schema.as_deref(),
req.timeout_secs,
)
.await
.map_err(AppError::from)?;
Ok(Json(result))
}
pub async fn cancel_query(
State(state): State<Arc<WebState>>,
Json(req): Json<CancelRequest>,
) -> Json<serde_json::Value> {
let cancelled = state.app.running_queries.cancel(&req.execution_id);
Json(serde_json::json!({ "cancelled": cancelled }))
}
pub async fn close_query_session(
State(state): State<Arc<WebState>>,
Json(req): Json<CloseSessionRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
let closed = dbx_core::query::close_query_session(
&state.app,
&req.connection_id,
&req.database,
&req.session_id,
req.client_session_id.as_deref(),
req.catalog.as_deref(),
)
.await
.map_err(AppError::from)?;
Ok(Json(serde_json::json!(closed)))
}
pub async fn close_client_connection_session(
State(state): State<Arc<WebState>>,
Json(req): Json<CloseClientConnectionSessionRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
let database = query_session_database(&req.database, req.catalog.as_deref());
let closed = state
.app
.close_client_session_pool(&req.connection_id, database, &req.client_session_id)
.await
.map_err(AppError::from)?;
Ok(Json(serde_json::json!(closed)))
}
fn query_session_database<'a>(database: &'a str, catalog: Option<&str>) -> Option<&'a str> {
if database.trim().is_empty() || catalog.is_some() {
None
} else {
Some(database)
}
}
pub async fn execute_script(
State(state): State<Arc<WebState>>,
Json(req): Json<ExecuteQueryRequest>,
) -> Result<Json<dbx_core::db::QueryResult>, AppError> {
tracing::debug!(connection_id = %req.connection_id, "execute_script");
let db_type = {
let configs = state.app.configs.read().await;
configs.get(&req.connection_id).map(|config| config.db_type)
};
let statements = db_type
.map(|db_type| dbx_core::sql::split_sql_statements_for_database(&req.sql, db_type))
.unwrap_or_else(|| dbx_core::sql::split_sql_statements(&req.sql));
let result = dbx_core::query::execute_statements(
&state.app,
&req.connection_id,
&req.database,
&statements,
req.schema.as_deref(),
None,
)
.await
.map_err(AppError::from)?;
Ok(Json(result))
}
pub async fn execute_in_transaction(
State(state): State<Arc<WebState>>,
Json(req): Json<ExecuteBatchRequest>,
) -> Result<Json<dbx_core::db::QueryResult>, AppError> {
tracing::debug!(connection_id = %req.connection_id, "execute_in_transaction");
let result = dbx_core::query::execute_statements_in_transaction(
&state.app,
&req.connection_id,
&req.database,
&req.statements,
req.schema.as_deref(),
req.catalog.as_deref(),
)
.await
.map_err(AppError::from)?;
Ok(Json(result))
}
pub async fn execute_script_with_2pc(
State(state): State<Arc<WebState>>,
Json(req): Json<ExecuteBatchRequest>,
) -> Result<Json<dbx_core::query::SchemaDiffDeployResult>, AppError> {
tracing::debug!(connection_id = %req.connection_id, "execute_script_with_2pc");
// Single-connection real transaction (not per-statement auto-commit 2PC).
let result = dbx_core::query::execute_schema_diff_deploy(
&state.app,
&req.connection_id,
&req.database,
&req.statements,
req.schema.as_deref(),
)
.await;
Ok(Json(result))
}
pub async fn analyze_sql_references(
Json(req): Json<AnalyzeSqlReferencesRequest>,
) -> Result<Json<dbx_core::sql_analysis::SqlReferenceAnalysis>, AppError> {
dbx_core::sql_analysis::analyze_sql_references(&req.sql, req.dialect.as_deref()).map(Json).map_err(AppError::from)
}
pub async fn find_statement_at_cursor(Json(req): Json<FindStatementAtCursorRequest>) -> Json<String> {
Json(
req.database_type
.map(|db_type| dbx_core::sql::find_statement_at_cursor_for_database(&req.sql, req.cursor_pos, db_type))
.unwrap_or_else(|| dbx_core::sql::find_statement_at_cursor(&req.sql, req.cursor_pos)),
)
}
pub async fn prepare_query_pagination_execution_plan(
Json(req): Json<PrepareQueryPaginationExecutionPlanRequest>,
) -> Json<dbx_core::query_result_sql::QueryPaginationExecutionPlan> {
Json(dbx_core::query_result_sql::build_query_pagination_execution_plan(req.options))
}
pub async fn build_sorted_query_sql(
Json(req): Json<BuildSortedQuerySqlRequest>,
) -> Json<dbx_core::query_result_sql::QuerySqlBuildResult> {
Json(dbx_core::query_result_sql::build_sorted_query_sql(req.options))
}
pub async fn build_explain_sql(
Json(req): Json<BuildExplainSqlRequest>,
) -> Json<dbx_core::query_execution_sql::ExplainSqlBuildResult> {
Json(dbx_core::query_execution_sql::build_explain_sql(req.options))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetExplainInfoRequest {
pub connection_id: String,
pub database: Option<String>,
pub schema: Option<String>,
pub sql: String,
pub mode: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildCreateUserSqlRequest {
pub username: String,
pub password: String,
pub tablespace: String,
}
pub async fn get_explain_info(
State(state): State<Arc<WebState>>,
Json(req): Json<GetExplainInfoRequest>,
) -> Result<Json<String>, AppError> {
let plan = dbx_core::agent_explain::get_agent_explain_info_core(
&state.app,
&req.connection_id,
req.database.as_deref(),
req.schema.as_deref(),
&req.sql,
req.mode.as_deref(),
)
.await
.map_err(AppError::from)?;
Ok(Json(plan))
}
pub async fn build_create_user_sql(Json(req): Json<BuildCreateUserSqlRequest>) -> Result<Json<String>, AppError> {
Ok(Json(dbx_core::db_admin_sql::build_create_user_sql(&req.username, &req.password, &req.tablespace)))
}
pub async fn build_dropped_file_preview_sql(
Json(req): Json<BuildDroppedFilePreviewSqlRequest>,
) -> Json<Option<String>> {
Json(dbx_core::query_execution_sql::build_dropped_file_preview_sql(req.options))
}
pub async fn build_table_select_sql(Json(req): Json<BuildTableSelectSqlRequest>) -> Json<String> {
Json(dbx_core::sql_dialect::build_table_data_select_sql(req.options))
}
pub async fn build_database_search_sql(
Json(req): Json<BuildDatabaseSearchSqlRequest>,
) -> Json<Option<dbx_core::database_search_sql::DatabaseSearchSql>> {
Json(dbx_core::database_search_sql::build_database_search_sql(req.options))
}
pub async fn build_search_result_where(Json(req): Json<BuildSearchResultWhereRequest>) -> Json<String> {
Json(dbx_core::database_search_sql::build_search_result_where(req.options))
}
pub async fn build_rename_object_sql(Json(req): Json<BuildRenameObjectSqlRequest>) -> Result<Json<String>, AppError> {
dbx_core::db_admin_sql::build_rename_object_sql(req.options).map(Json).map_err(AppError::from)
}
pub async fn build_create_database_sql(
Json(req): Json<BuildCreateDatabaseSqlRequest>,
) -> Result<Json<String>, AppError> {
dbx_core::db_admin_sql::build_create_database_sql(req.options).map(Json).map_err(AppError::from)
}
#[cfg(feature = "duckdb-sidecar")]
pub async fn build_duckdb_attach_database_sql(Json(req): Json<BuildDuckDbAttachDatabaseSqlRequest>) -> Json<String> {
Json(dbx_core::db_admin_sql::build_duckdb_attach_database_sql(req.options))
}
pub async fn build_sqlite_attach_database_sql(Json(req): Json<BuildSqliteAttachDatabaseSqlRequest>) -> Json<String> {
Json(dbx_core::db_admin_sql::build_sqlite_attach_database_sql(req.options))
}
pub async fn build_drop_object_sql(Json(req): Json<BuildDropObjectSqlRequest>) -> Json<String> {
Json(dbx_core::db_admin_sql::build_drop_object_sql(req.options))
}
pub async fn build_drop_table_sql(Json(req): Json<BuildTableAdminSqlRequest>) -> Json<String> {
Json(dbx_core::db_admin_sql::build_drop_table_sql(req.options))
}
pub async fn build_drop_table_child_object_sql(
Json(req): Json<BuildDropTableChildObjectSqlRequest>,
) -> Result<Json<String>, AppError> {
dbx_core::db_admin_sql::build_drop_table_child_object_sql(req.options).map(Json).map_err(AppError::from)
}
pub async fn build_empty_table_sql(Json(req): Json<BuildTableAdminSqlRequest>) -> Json<String> {
Json(dbx_core::db_admin_sql::build_empty_table_sql(req.options))
}
pub async fn build_truncate_table_sql(Json(req): Json<BuildTableAdminSqlRequest>) -> Json<String> {
Json(dbx_core::db_admin_sql::build_truncate_table_sql(req.options))
}
pub async fn build_drop_database_sql(Json(req): Json<BuildDatabaseNameSqlRequest>) -> Json<String> {
Json(dbx_core::db_admin_sql::build_drop_database_sql(req.options))
}
pub async fn build_create_schema_sql(Json(req): Json<BuildSchemaNameSqlRequest>) -> Result<Json<String>, AppError> {
dbx_core::db_admin_sql::build_create_schema_sql(req.options).map(Json).map_err(AppError::from)
}
pub async fn build_update_database_properties_sql(
Json(req): Json<BuildDatabasePropertyEditSqlRequest>,
) -> Result<Json<String>, AppError> {
dbx_core::db_admin_sql::build_update_database_properties_sql(req.options).map(Json).map_err(AppError::from)
}
pub async fn build_drop_schema_sql(Json(req): Json<BuildSchemaNameSqlRequest>) -> Json<String> {
Json(dbx_core::db_admin_sql::build_drop_schema_sql(req.options))
}
pub async fn build_duplicate_table_structure_sql(
Json(req): Json<BuildDuplicateTableStructureSqlRequest>,
) -> Json<String> {
Json(dbx_core::db_admin_sql::build_duplicate_table_structure_sql(req.options))
}
pub async fn build_copy_table_data_sql(Json(req): Json<BuildCopyTableDataSqlRequest>) -> Json<String> {
Json(dbx_core::db_admin_sql::build_copy_table_data_sql(req.options))
}
pub async fn build_executable_object_source_statements(
Json(req): Json<BuildExecutableObjectSourceRequest>,
) -> Result<Json<Vec<String>>, AppError> {
dbx_core::object_source_sql::build_executable_object_source_statements(req.input).map(Json).map_err(AppError::from)
}
pub async fn build_executable_object_source_sql(
Json(req): Json<BuildExecutableObjectSourceRequest>,
) -> Result<Json<String>, AppError> {
dbx_core::object_source_sql::build_executable_object_source_sql(req.input).map(Json).map_err(AppError::from)
}
pub async fn build_editable_object_source(Json(req): Json<BuildExecutableObjectSourceRequest>) -> Json<String> {
Json(dbx_core::object_source_sql::build_editable_object_source(req.input))
}
pub async fn build_routine_rename_object_source_statements(
Json(req): Json<BuildRoutineRenameObjectSourceRequest>,
) -> Result<Json<Vec<String>>, AppError> {
dbx_core::object_source_sql::build_routine_rename_object_source_statements(req.input)
.map(Json)
.map_err(AppError::from)
}
pub async fn build_view_ddl_sql(Json(req): Json<BuildViewDdlRequest>) -> Json<String> {
Json(dbx_core::object_source_sql::build_view_ddl_sql(req.input))
}
pub async fn build_table_structure_change_sql(
Json(req): Json<BuildTableStructureSqlRequest>,
) -> Json<dbx_core::table_structure_sql::TableStructureSqlResult> {
Json(dbx_core::table_structure_sql::build_table_structure_change_sql(req.options))
}
pub async fn preview_sqlite_table_structure_change(
State(state): State<Arc<WebState>>,
Json(req): Json<PreviewSqliteTableStructureChangeRequest>,
) -> Result<Json<dbx_core::table_structure_sql::SqliteTableStructurePreview>, AppError> {
dbx_core::table_structure_sql::preview_sqlite_table_structure_change(
&state.app,
&req.connection_id,
&req.database,
req.options,
)
.await
.map(Json)
.map_err(AppError::from)
}
pub async fn apply_sqlite_table_structure_change(
State(state): State<Arc<WebState>>,
Json(req): Json<ApplySqliteTableStructureChangeRequest>,
) -> Result<Json<dbx_core::db::QueryResult>, AppError> {
dbx_core::table_structure_sql::apply_sqlite_table_structure_change(
&state.app,
&req.connection_id,
&req.database,
req.options,
&req.schema_revision,
)
.await
.map(Json)
.map_err(AppError::from)
}
pub async fn build_create_table_sql(
Json(req): Json<BuildTableStructureSqlRequest>,
) -> Json<dbx_core::table_structure_sql::TableStructureSqlResult> {
Json(dbx_core::table_structure_sql::build_create_table_sql(req.options))
}
pub async fn build_single_column_alter_sql(
Json(req): Json<BuildSingleColumnAlterSqlRequest>,
) -> Json<dbx_core::table_structure_sql::TableStructureSqlResult> {
Json(dbx_core::table_structure_sql::build_single_column_alter_sql(req.options))
}
pub async fn analyze_editable_query_editability(
Json(req): Json<AnalyzeEditableQueryRequest>,
) -> Json<dbx_core::sql_editability::QueryEditability> {
Json(dbx_core::sql_editability::analyze_editable_query_editability(&req.sql))
}
pub async fn prepare_data_grid_save(
Json(req): Json<PrepareDataGridSaveRequest>,
) -> Json<dbx_core::data_grid_sql::DataGridSavePreparation> {
Json(dbx_core::data_grid_sql::prepare_data_grid_save(req.options))
}
#[utoipa::path(
post,
path = "/api/query/extract-data-grid-selection",
request_body = ExtractDataGridSelectionRequest,
responses(
(status = 200, description = "Selection extracted successfully", body = dbx_core::data_grid_extractors::DataGridExtractResult),
(status = 400, description = "Invalid selection or extractor configuration", body = dbx_core::data_grid_extractors::DataGridExtractError),
(status = 413, description = "Request body exceeds the extractor upload limit"),
(status = 422, description = "Request body does not match the extractor contract"),
(status = 500, description = "Extractor worker failed", body = dbx_core::data_grid_extractors::DataGridExtractError)
),
tag = "data-grid"
)]
pub async fn extract_data_grid_selection(
Json(req): Json<ExtractDataGridSelectionRequest>,
) -> Result<
Json<dbx_core::data_grid_extractors::DataGridExtractResult>,
(axum::http::StatusCode, Json<dbx_core::data_grid_extractors::DataGridExtractError>),
> {
tokio::task::spawn_blocking(move || dbx_core::data_grid_extractors::extract_data_grid_selection(req.request))
.await
.map_err(|error| {
let error = dbx_core::data_grid_extractors::DataGridExtractError::new(
dbx_core::data_grid_extractors::DataGridExtractErrorCode::ExecutionFailed,
format!("Data grid extractor worker failed: {error}"),
);
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, Json(error))
})?
.map(Json)
.map_err(|error| (axum::http::StatusCode::BAD_REQUEST, Json(error)))
}
pub async fn build_data_grid_copy_update_statements(
Json(req): Json<BuildDataGridCopyUpdateStatementsRequest>,
) -> Json<Vec<String>> {
Json(dbx_core::data_grid_sql::build_data_grid_copy_update_statements(req.options))
}
pub async fn build_data_grid_copy_insert_statement(
Json(req): Json<BuildDataGridCopyInsertStatementRequest>,
) -> Json<Option<String>> {
Json(dbx_core::data_grid_sql::build_data_grid_copy_insert_statement(req.options))
}
pub async fn build_data_grid_context_filter_condition(
Json(req): Json<BuildDataGridContextFilterConditionRequest>,
) -> Json<Option<String>> {
Json(dbx_core::data_grid_sql::build_data_grid_context_filter_condition(req.options))
}
pub async fn build_data_grid_column_value_filter_condition(
Json(req): Json<BuildDataGridColumnValueFilterConditionRequest>,
) -> Json<Option<String>> {
Json(dbx_core::data_grid_sql::build_data_grid_column_value_filter_condition(req.options))
}
pub async fn build_data_grid_column_values_filter_condition(
Json(req): Json<BuildDataGridColumnValuesFilterConditionRequest>,
) -> Json<Option<String>> {
Json(dbx_core::data_grid_sql::build_data_grid_column_values_filter_condition(req.options))
}
pub async fn build_data_grid_column_distinct_values_sql(
Json(req): Json<BuildDataGridColumnDistinctValuesSqlRequest>,
) -> Json<String> {
Json(dbx_core::data_grid_sql::build_data_grid_column_distinct_values_sql(req.options))
}
pub async fn build_data_grid_count_sql(Json(req): Json<BuildDataGridCountSqlRequest>) -> Json<String> {
Json(dbx_core::data_grid_sql::build_data_grid_count_sql(req.options))
}
pub async fn build_hive_table_properties_sql(Json(req): Json<BuildHiveTablePropertiesSqlRequest>) -> Json<String> {
Json(dbx_core::data_grid_sql::build_hive_table_properties_sql(req.options))
}
pub async fn build_export_insert_statements(
Json(req): Json<BuildExportInsertStatementsRequest>,
) -> Result<Json<Vec<String>>, AppError> {
dbx_core::database_export::build_export_insert_statements(req.options).map(Json).map_err(AppError::from)
}
pub async fn build_export_sql_insert(Json(req): Json<BuildExportSqlInsertRequest>) -> Result<Json<String>, AppError> {
dbx_core::database_export::build_export_sql_insert(req.options).map(Json).map_err(AppError::from)
}
pub async fn build_database_sql_export(
State(state): State<Arc<WebState>>,
Json(req): Json<BuildDatabaseSqlExportRequest>,
) -> Result<Json<String>, AppError> {
let mut options = req.options;
// Sort tables by FK dependency when connection info is available.
if let (Some(ref conn_id), Some(ref database), Some(ref schema)) =
(&options.connection_id, &options.database, &options.schema)
{
if options.tables.len() > 1 {
let table_names: Vec<String> = options.tables.iter().filter_map(|t| t.table_name.clone()).collect();
if table_names.len() > 1 {
if let Ok(sorted_names) = dbx_core::transfer::sort_tables_by_fk_dependency(
&state.app,
conn_id,
database,
schema,
&table_names,
true,
)
.await
{
options.tables.sort_by_key(|t| {
sorted_names
.iter()
.position(|n| Some(n.as_str()) == t.table_name.as_deref())
.unwrap_or(usize::MAX)
});
}
}
}
}
dbx_core::database_export::build_database_sql_export(options).map(Json).map_err(AppError::from)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::WebState;
use axum::extract::State as AxumState;
use dbx_core::connection::AppState;
use dbx_core::storage::Storage;
async fn test_web_state() -> (Arc<WebState>, std::path::PathBuf) {
let dir = std::env::temp_dir().join(format!("dbx-web-query-test-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&dir).unwrap();
let storage = Storage::open(&dir.join("storage.db")).await.unwrap();
let app = Arc::new(AppState::new_with_plugin_dir(storage, dir.join("plugins")));
let state = Arc::new(WebState::for_tests(app, dir.clone()));
(state, dir)
}
#[tokio::test]
async fn execute_script_with_2pc_returns_structured_result() {
let (state, _dir) = test_web_state().await;
let req = ExecuteBatchRequest {
connection_id: "conn-1".to_string(),
database: "testdb".to_string(),
statements: vec!["SELECT 1".to_string()],
schema: None,
catalog: None,
timeout_secs: None,
};
let result = execute_script_with_2pc(AxumState(state), Json(req))
.await
.expect("execute_script_with_2pc should return Ok(Json(...))");
let log = result.0;
assert!(!log.transaction_id.is_empty());
assert!(!log.participants.is_empty());
assert_eq!(log.status, "rolled_back");
assert!(log.error.as_ref().is_some_and(|e| !e.is_empty()));
assert_eq!(log.statement_count, 1);
assert_eq!(log.executed_count, 0);
}
#[tokio::test]
async fn execute_script_with_2pc_empty_statements_succeeds() {
let (state, _dir) = test_web_state().await;
let req = ExecuteBatchRequest {
connection_id: "conn-empty".to_string(),
database: "testdb".to_string(),
statements: vec![],
schema: None,
catalog: None,
timeout_secs: None,
};
let result = execute_script_with_2pc(AxumState(state), Json(req)).await.expect("empty deploy should succeed");
let log = result.0;
assert_eq!(log.status, "committed");
assert_eq!(log.statement_count, 0);
assert_eq!(log.executed_count, 0);
assert!(log.error.is_none());
assert_eq!(log.participants.len(), 1);
}
#[tokio::test]
async fn execute_script_with_2pc_propagates_structured_failure_fields() {
let (state, _dir) = test_web_state().await;
let req = ExecuteBatchRequest {
connection_id: "missing-conn".to_string(),
database: "testdb".to_string(),
statements: vec!["CREATE TABLE t1 (id INT)".to_string(), "CREATE TABLE t2 (id INT)".to_string()],
schema: None,
catalog: None,
timeout_secs: None,
};
let result = execute_script_with_2pc(AxumState(state), Json(req))
.await
.expect("deploy endpoint should return structured JSON even on failure");
let log = result.0;
assert!(log.status == "rolled_back" || log.status == "mixed", "status={}", log.status);
assert_eq!(log.statement_count, 2);
assert!(log.error.as_ref().is_some_and(|e| !e.is_empty()));
// Missing connection cannot have applied statements.
assert_eq!(log.executed_count, 0);
}
#[test]
fn execute_multi_response_preserves_nested_original_error_detail() {
let result = dbx_core::query::ExecuteMultiResult {
result: dbx_core::db::QueryResult {
columns: vec!["Error".to_string()],
column_types: vec![],
column_sortables: vec![],
spatial_columns: vec![],
spatial_values: vec![],
rows: vec![vec![serde_json::json!("relation customer_orders does not exist")]],
affected_rows: 0,
execution_time_ms: 0,
truncated: false,
session_id: None,
has_more: false,
elasticsearch_raw_body: None,
messages: Vec::new(),
},
execution_error: true,
statement_index: Some(1),
error: Some(dbx_core::backend_error::BackendError::from_sql_detail(
"relation customer_orders does not exist",
)),
server_message: false,
};
let payload = serde_json::to_value(execute_multi_response(vec![result]).0).unwrap();
assert_eq!(payload[0]["statement_index"], 1);
assert_eq!(payload[0]["error"]["code"], "DBX-JDBC-4001");
assert_eq!(payload[0]["error"]["detail"], "relation customer_orders does not exist");
}
}