diff --git a/influxdb3/tests/server/query.rs b/influxdb3/tests/server/query.rs index 7081f1803d..116342fd98 100644 --- a/influxdb3/tests/server/query.rs +++ b/influxdb3/tests/server/query.rs @@ -3,6 +3,7 @@ use futures::StreamExt; use influxdb3_client::Precision; use pretty_assertions::assert_eq; use serde_json::{json, Value}; +use test_helpers::assert_contains; #[tokio::test] async fn api_v3_query_sql() { @@ -50,6 +51,125 @@ async fn api_v3_query_sql() { ); } +#[tokio::test] +async fn api_v3_query_sql_params() { + let server = TestServer::spawn().await; + + server + .write_lp_to_db( + "foo", + "cpu,host=a,region=us-east usage=0.9 1 + cpu,host=b,region=us-east usage=0.50 1 + cpu,host=a,region=us-east usage=0.80 2 + cpu,host=b,region=us-east usage=0.60 2 + cpu,host=a,region=us-east usage=0.70 3 + cpu,host=b,region=us-east usage=0.70 3 + cpu,host=a,region=us-east usage=0.50 4 + cpu,host=b,region=us-east usage=0.80 4", + Precision::Second, + ) + .await + .unwrap(); + + let client = reqwest::Client::new(); + let url = format!("{base}/api/v3/query_sql", base = server.client_addr()); + + // Use a POST request + { + let resp = client + .post(&url) + .json(&json!({ + "db": "foo", + "q": "SELECT * FROM cpu WHERE host = $host AND usage > $usage", + "params": { + "host": "b", + "usage": 0.60, + }, + "format": "pretty", + })) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + assert_eq!( + "+------+---------+---------------------+-------+\n\ + | host | region | time | usage |\n\ + +------+---------+---------------------+-------+\n\ + | b | us-east | 1970-01-01T00:00:03 | 0.7 |\n\ + | b | us-east | 1970-01-01T00:00:04 | 0.8 |\n\ + +------+---------+---------------------+-------+", + resp + ); + } + + // Use a GET request + { + let params = serde_json::to_string(&json!({ + "host": "b", + "usage": 0.60, + })) + .unwrap(); + let resp = client + .get(&url) + .query(&[ + ("db", "foo"), + ( + "q", + "SELECT * FROM cpu WHERE host = $host AND usage > $usage", + ), + ("format", "pretty"), + ("params", params.as_str()), + ]) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + assert_eq!( + "+------+---------+---------------------+-------+\n\ + | host | region | time | usage |\n\ + +------+---------+---------------------+-------+\n\ + | b | us-east | 1970-01-01T00:00:03 | 0.7 |\n\ + | b | us-east | 1970-01-01T00:00:04 | 0.8 |\n\ + +------+---------+---------------------+-------+", + resp + ); + } + + // Check for errors + { + let resp = client + .post(&url) + .json(&json!({ + "db": "foo", + "q": "SELECT * FROM cpu WHERE host = $host", + "params": { + "not_host": "a" + }, + "format": "pretty", + })) + .send() + .await + .unwrap(); + let status = resp.status(); + let body = resp.text().await.unwrap(); + + // TODO - it would be nice if this was a 4xx error, because this is really + // a user error; however, the underlying error that occurs when Logical + // planning is DatafusionError::Internal, and is not so convenient to deal + // with. This may get addressed in: + // + // https://github.com/apache/arrow-datafusion/issues/9738 + assert!(status.is_server_error()); + assert_contains!(body, "No value found for placeholder with name $host"); + } +} + #[tokio::test] async fn api_v3_query_influxql() { let server = TestServer::spawn().await; @@ -270,6 +390,129 @@ async fn api_v3_query_influxql() { } } +#[tokio::test] +async fn api_v3_query_influxql_params() { + let server = TestServer::spawn().await; + + server + .write_lp_to_db( + "foo", + "cpu,host=a,region=us-east usage=0.9 1 + cpu,host=b,region=us-east usage=0.50 1 + cpu,host=a,region=us-east usage=0.80 2 + cpu,host=b,region=us-east usage=0.60 2 + cpu,host=a,region=us-east usage=0.70 3 + cpu,host=b,region=us-east usage=0.70 3 + cpu,host=a,region=us-east usage=0.50 4 + cpu,host=b,region=us-east usage=0.80 4", + Precision::Second, + ) + .await + .unwrap(); + + let client = reqwest::Client::new(); + let url = format!("{base}/api/v3/query_influxql", base = server.client_addr()); + + // Use a POST request + { + let resp = client + .post(&url) + .json(&json!({ + "db": "foo", + "q": "SELECT * FROM cpu WHERE host = $host AND usage > $usage", + "params": { + "host": "b", + "usage": 0.60, + }, + "format": "pretty", + })) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + assert_eq!( + "+------------------+---------------------+------+---------+-------+\n\ + | iox::measurement | time | host | region | usage |\n\ + +------------------+---------------------+------+---------+-------+\n\ + | cpu | 1970-01-01T00:00:03 | b | us-east | 0.7 |\n\ + | cpu | 1970-01-01T00:00:04 | b | us-east | 0.8 |\n\ + +------------------+---------------------+------+---------+-------+", + resp + ); + } + + // Use a GET request + { + let params = serde_json::to_string(&json!({ + "host": "b", + "usage": 0.60, + })) + .unwrap(); + let resp = client + .get(&url) + .query(&[ + ("db", "foo"), + ( + "q", + "SELECT * FROM cpu WHERE host = $host AND usage > $usage", + ), + ("format", "pretty"), + ("params", params.as_str()), + ]) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + assert_eq!( + "+------------------+---------------------+------+---------+-------+\n\ + | iox::measurement | time | host | region | usage |\n\ + +------------------+---------------------+------+---------+-------+\n\ + | cpu | 1970-01-01T00:00:03 | b | us-east | 0.7 |\n\ + | cpu | 1970-01-01T00:00:04 | b | us-east | 0.8 |\n\ + +------------------+---------------------+------+---------+-------+", + resp + ); + } + + // Check for errors + { + let resp = client + .post(&url) + .json(&json!({ + "db": "foo", + "q": "SELECT * FROM cpu WHERE host = $host", + "params": { + "not_host": "a" + }, + "format": "pretty", + })) + .send() + .await + .unwrap(); + let status = resp.status(); + let body = resp.text().await.unwrap(); + + // TODO - it would be nice if this was a 4xx error, because this is really + // a user error; however, the underlying error that occurs when Logical + // planning is DatafusionError::Internal, and is not so convenient to deal + // with. This may get addressed in: + // + // https://github.com/apache/arrow-datafusion/issues/9738 + assert!(status.is_server_error()); + assert_contains!( + body, + "Bind parameter '$host' was referenced in the InfluxQL \ + statement but its value is undefined" + ); + } +} + #[tokio::test] async fn api_v1_query() { let server = TestServer::spawn().await; diff --git a/influxdb3_server/src/http.rs b/influxdb3_server/src/http.rs index 055920f98d..c88cfc809f 100644 --- a/influxdb3_server/src/http.rs +++ b/influxdb3_server/src/http.rs @@ -17,6 +17,7 @@ use hyper::header::AUTHORIZATION; use hyper::header::CONTENT_ENCODING; use hyper::header::CONTENT_TYPE; use hyper::http::HeaderValue; +use hyper::HeaderMap; use hyper::{Body, Method, Request, Response, StatusCode}; use influxdb3_write::catalog::Error as CatalogError; use influxdb3_write::persister::TrackedMemoryArrowWriter; @@ -25,6 +26,7 @@ use influxdb3_write::BufferedWriteRequest; use influxdb3_write::Precision; use influxdb3_write::WriteBuffer; use iox_query_influxql_rewrite as rewrite; +use iox_query_params::StatementParams; use iox_time::TimeProvider; use observability_deps::tracing::{debug, error, info}; use serde::de::DeserializeOwned; @@ -93,6 +95,10 @@ pub enum Error { #[error("access denied")] Forbidden, + /// The HTTP request method is not supported for this resource + #[error("unsupported method")] + UnsupportedMethod, + /// PProf support is not compiled #[error("pprof support is not compiled")] PProfIsNotCompiled, @@ -265,6 +271,18 @@ impl Error { .body(body) .unwrap() } + Self::UnsupportedMethod => { + let err: ErrorMessage<()> = ErrorMessage { + error: self.to_string(), + data: None, + }; + let serialized = serde_json::to_string(&err).unwrap(); + let body = Body::from(serialized); + Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .body(body) + .unwrap() + } _ => { let body = Body::from(self.to_string()); Response::builder() @@ -347,17 +365,18 @@ where } async fn query_sql(&self, req: Request
) -> Result