1use std::time::Duration;
16
17use base64::Engine;
18use base64::engine::general_purpose;
19use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
20use common_error::ext::BoxedError;
21use humantime::format_duration;
22use serde_json::Value;
23use servers::http::GreptimeQueryOutput;
24use servers::http::header::constants::GREPTIME_DB_HEADER_TIMEOUT;
25use servers::http::result::greptime_result_v1::GreptimedbV1Response;
26use snafu::ResultExt;
27
28use crate::error::{
29 BuildClientSnafu, HttpQuerySqlSnafu, ParseProxyOptsSnafu, Result, SerdeJsonSnafu,
30};
31
32#[derive(Debug, Clone)]
33pub struct DatabaseClient {
34 addr: String,
35 catalog: String,
36 auth_header: Option<String>,
37 timeout: Duration,
38 proxy: Option<reqwest::Proxy>,
39 no_proxy: bool,
40}
41
42pub fn parse_proxy_opts(
43 proxy: Option<String>,
44 no_proxy: bool,
45) -> std::result::Result<Option<reqwest::Proxy>, BoxedError> {
46 if no_proxy {
47 return Ok(None);
48 }
49 proxy
50 .map(|proxy| {
51 reqwest::Proxy::all(proxy)
52 .context(ParseProxyOptsSnafu)
53 .map_err(BoxedError::new)
54 })
55 .transpose()
56}
57
58impl DatabaseClient {
59 pub fn new(
60 addr: String,
61 catalog: String,
62 auth_basic: Option<String>,
63 timeout: Duration,
64 proxy: Option<reqwest::Proxy>,
65 no_proxy: bool,
66 ) -> Self {
67 let auth_header = if let Some(basic) = auth_basic {
68 let encoded = general_purpose::STANDARD.encode(basic);
69 Some(format!("basic {}", encoded))
70 } else {
71 None
72 };
73
74 if no_proxy {
75 common_telemetry::info!("Proxy disabled");
76 } else if let Some(ref proxy) = proxy {
77 common_telemetry::info!("Using proxy: {:?}", proxy);
78 } else {
79 common_telemetry::info!("Using system proxy(if any)");
80 }
81
82 Self {
83 addr,
84 catalog,
85 auth_header,
86 timeout,
87 proxy,
88 no_proxy,
89 }
90 }
91
92 pub fn addr(&self) -> &str {
93 &self.addr
94 }
95
96 pub async fn sql_in_public(&self, sql: &str) -> Result<Option<Vec<Vec<Value>>>> {
97 self.sql(sql, DEFAULT_SCHEMA_NAME).await
98 }
99
100 pub async fn require_packed_import(&self) -> Result<()> {
102 let url = format!("http://{}/v1/capabilities", self.addr);
103 let mut builder = reqwest::Client::builder().timeout(self.timeout);
104 if let Some(proxy) = self.proxy.clone() {
105 builder = builder.proxy(proxy);
106 }
107 if self.no_proxy {
108 builder = builder.no_proxy();
109 }
110 let client = builder.build().context(BuildClientSnafu)?;
111 let mut request = client.get(&url);
112 if let Some(auth) = &self.auth_header {
113 request = request.header("Authorization", auth);
114 }
115 let response = request.send().await.with_context(|_| HttpQuerySqlSnafu {
116 reason: "packed import capability request failed",
117 })?;
118 if response.status() == reqwest::StatusCode::NOT_FOUND {
119 return crate::error::InvalidArgumentsSnafu {
120 msg: "target does not support packed import",
121 }
122 .fail();
123 }
124 let response = response
125 .error_for_status()
126 .with_context(|_| HttpQuerySqlSnafu {
127 reason: "packed import capability request rejected",
128 })?;
129 let body = response.text().await.with_context(|_| HttpQuerySqlSnafu {
130 reason: "cannot read capability response",
131 })?;
132 let value: Value = serde_json::from_str(&body).context(SerdeJsonSnafu)?;
133 if !value.is_object() {
134 return crate::error::UnexpectedSnafu {
135 msg: "invalid capability response: expected JSON object",
136 }
137 .fail();
138 }
139 if value.get("metric_packed_import").and_then(Value::as_u64) != Some(1) {
140 return crate::error::InvalidArgumentsSnafu {
141 msg: "target does not support packed import version 1",
142 }
143 .fail();
144 }
145 Ok(())
146 }
147
148 pub async fn sql(&self, sql: &str, schema: &str) -> Result<Option<Vec<Vec<Value>>>> {
150 let body = self.sql_response(sql, schema).await?;
151 Ok(body.output().first().and_then(|output| match output {
152 GreptimeQueryOutput::Records(records) => Some(records.rows().clone()),
153 GreptimeQueryOutput::AffectedRows(_) => None,
154 }))
155 }
156
157 pub(crate) async fn sql_response(
158 &self,
159 sql: &str,
160 schema: &str,
161 ) -> Result<GreptimedbV1Response> {
162 let url = format!("http://{}/v1/sql", self.addr);
163 let params = [
164 ("db", format!("{}-{}", self.catalog, schema)),
165 ("sql", sql.to_string()),
166 ];
167 let mut builder = reqwest::Client::builder();
168 if let Some(proxy) = self.proxy.clone() {
169 builder = builder.proxy(proxy);
170 }
171 if self.no_proxy {
172 builder = builder.no_proxy();
173 }
174 let client = builder.build().context(BuildClientSnafu)?;
175 let mut request = client
176 .post(&url)
177 .form(¶ms)
178 .header("Content-Type", "application/x-www-form-urlencoded");
179 if let Some(ref auth) = self.auth_header {
180 request = request.header("Authorization", auth);
181 }
182
183 request = request.header(
184 GREPTIME_DB_HEADER_TIMEOUT,
185 format_duration(self.timeout).to_string(),
186 );
187
188 let response = request.send().await.with_context(|_| HttpQuerySqlSnafu {
189 reason: format!("bad url: {}", url),
190 })?;
191 let response = response
192 .error_for_status()
193 .with_context(|_| HttpQuerySqlSnafu {
194 reason: format!("query failed: {}", sql),
195 })?;
196
197 let text = response.text().await.with_context(|_| HttpQuerySqlSnafu {
198 reason: "cannot get response text".to_string(),
199 })?;
200
201 serde_json::from_str::<GreptimedbV1Response>(&text).context(SerdeJsonSnafu)
202 }
203}
204
205pub(crate) fn split_database(database: &str) -> Result<(String, Option<String>)> {
207 let (catalog, schema) = match database.split_once('-') {
208 Some((catalog, schema)) => (catalog, schema),
209 None => (DEFAULT_CATALOG_NAME, database),
210 };
211
212 if schema == "*" {
213 Ok((catalog.to_string(), None))
214 } else {
215 Ok((catalog.to_string(), Some(schema.to_string())))
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 #[tokio::test]
222 async fn packed_capability_probe_checks_auth_and_protocol_version() {
223 use tokio::io::{AsyncReadExt, AsyncWriteExt};
224 for (status, body, supported) in [
225 (200, r#"{"metric_packed_import":1,"future":2}"#, true),
226 (200, "{}", false),
227 (200, r#"{"metric_packed_import":2}"#, false),
228 (404, "{}", false),
229 (401, "{}", false),
230 (403, "{}", false),
231 (200, "[]", false),
232 (200, "invalid-json", false),
233 ] {
234 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
235 let address = listener.local_addr().unwrap();
236 let server = tokio::spawn(async move {
237 let (mut socket, _) = listener.accept().await.unwrap();
238 let mut request = vec![0; 4096];
239 let n = socket.read(&mut request).await.unwrap();
240 let request = String::from_utf8_lossy(&request[..n]).to_lowercase();
241 assert!(request.starts_with("get /v1/capabilities "));
242 assert!(request.contains("authorization: basic dxnlcjpwyxnzd29yza=="));
243 socket.write_all(format!("HTTP/1.1 {status} Response\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).as_bytes()).await.unwrap();
244 });
245 let client = super::DatabaseClient::new(
246 address.to_string(),
247 "greptime".into(),
248 Some("user:password".into()),
249 std::time::Duration::from_secs(5),
250 None,
251 true,
252 );
253 assert_eq!(
254 client.require_packed_import().await.is_ok(),
255 supported,
256 "{status}: {body}"
257 );
258 server.await.unwrap();
259 }
260 }
261
262 use super::*;
263
264 #[test]
265 fn test_split_database() {
266 let result = split_database("catalog-schema").unwrap();
267 assert_eq!(result, ("catalog".to_string(), Some("schema".to_string())));
268
269 let result = split_database("schema").unwrap();
270 assert_eq!(result, ("greptime".to_string(), Some("schema".to_string())));
271
272 let result = split_database("catalog-*").unwrap();
273 assert_eq!(result, ("catalog".to_string(), None));
274
275 let result = split_database("*").unwrap();
276 assert_eq!(result, ("greptime".to_string(), None));
277 }
278}