diff --git a/src/servers/src/http.rs b/src/servers/src/http.rs index 85c598efc4..0ed86572a2 100644 --- a/src/servers/src/http.rs +++ b/src/servers/src/http.rs @@ -1050,6 +1050,10 @@ impl HttpServer { "/sql/parse", routing::get(handler::sql_parse).post(handler::sql_parse), ) + .route( + "/sql/format", + routing::get(handler::sql_format).post(handler::sql_format), + ) .route( "/promql", routing::get(handler::promql).post(handler::promql), diff --git a/src/servers/src/http/handler.rs b/src/servers/src/http/handler.rs index 6cd977d7fa..6a2d5af73d 100644 --- a/src/servers/src/http/handler.rs +++ b/src/servers/src/http/handler.rs @@ -176,6 +176,49 @@ pub async fn sql_parse( Ok(stmts.into()) } +#[derive(Debug, Serialize, Deserialize)] +pub struct SqlFormatResponse { + pub formatted: String, +} + +/// Handler to format sql string +#[axum_macros::debug_handler] +#[tracing::instrument(skip_all, fields(protocol = "http", request_type = "sql_format"))] +pub async fn sql_format( + Query(query_params): Query, + Form(form_params): Form, +) -> axum::response::Response { + let Some(sql) = query_params.sql.or(form_params.sql) else { + let resp = ErrorResponse::from_error_message( + StatusCode::InvalidArguments, + "sql parameter is required.".to_string(), + ); + return HttpResponse::Error(resp).into_response(); + }; + + // Parse using GreptimeDB dialect then reconstruct statements via Display + let stmts = match ParserContext::create_with_dialect( + &sql, + &GreptimeDbDialect {}, + ParseOptions::default(), + ) { + Ok(v) => v, + Err(e) => return HttpResponse::Error(ErrorResponse::from_error(e)).into_response(), + }; + + let mut parts: Vec = Vec::with_capacity(stmts.len()); + for stmt in stmts { + let mut s = format!("{:#}", stmt); + if !s.trim_end().ends_with(';') { + s.push(';'); + } + parts.push(s); + } + + let formatted = parts.join("\n"); + Json(SqlFormatResponse { formatted }).into_response() +} + /// Create a response from query result pub async fn from_output( outputs: Vec>, diff --git a/tests-integration/tests/http.rs b/tests-integration/tests/http.rs index c5cf7e89bd..aefebe6939 100644 --- a/tests-integration/tests/http.rs +++ b/tests-integration/tests/http.rs @@ -548,6 +548,52 @@ pub async fn test_sql_api(store_type: StorageType) { guard.remove_all().await; } +#[tokio::test] +async fn test_sql_format_api() { + let (app, mut guard) = + setup_test_http_app_with_frontend(StorageType::File, "sql_format_api").await; + let client = TestClient::new(app).await; + + // missing sql + let res = client.get("/v1/sql/format").send().await; + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + + let body = serde_json::from_str::(&res.text().await).unwrap(); + assert_eq!(body.code(), 1004); + assert_eq!(body.error(), "sql parameter is required."); + + // with sql + let res = client + .get("/v1/sql/format?sql=select%201%20as%20x") + .send() + .await; + assert_eq!(res.status(), StatusCode::OK); + + let json: serde_json::Value = serde_json::from_str(&res.text().await).unwrap(); + let formatted = json + .get("formatted") + .and_then(|v| v.as_str()) + .unwrap_or_default(); + assert_eq!(formatted, "SELECT 1 AS x;"); + + // complex query + let complex_query = "WITH RECURSIVE slow_cte AS (SELECT 1 AS n, md5(random()) AS hash UNION ALL SELECT n + 1, md5(concat(hash, n)) FROM slow_cte WHERE n < 4500) SELECT COUNT(*) FROM slow_cte"; + let encoded_complex_query = encode(complex_query); + + let query_params = format!("/v1/sql/format?sql={encoded_complex_query}"); + let res = client.get(&query_params).send().await; + assert_eq!(res.status(), StatusCode::OK); + + let json: serde_json::Value = serde_json::from_str(&res.text().await).unwrap(); + let formatted = json + .get("formatted") + .and_then(|v| v.as_str()) + .unwrap_or_default(); + assert_eq!(formatted, "WITH RECURSIVE slow_cte AS (SELECT 1 AS n, md5(random()) AS hash UNION ALL SELECT n + 1, md5(concat(hash, n)) FROM slow_cte WHERE n < 4500) SELECT COUNT(*) FROM slow_cte;"); + + guard.remove_all().await; +} + pub async fn test_http_sql_slow_query(store_type: StorageType) { let (app, mut guard) = setup_test_http_app_with_frontend(store_type, "sql_api").await; let client = TestClient::new(app).await;