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 self.require_packed_capability("metric_packed_import").await
103 }
104
105 pub async fn require_packed_export(&self) -> Result<()> {
107 self.require_packed_capability("metric_packed_export").await
108 }
109
110 async fn require_packed_capability(&self, capability: &str) -> Result<()> {
111 let url = format!("http://{}/v1/capabilities", self.addr);
112 let mut builder = reqwest::Client::builder().timeout(self.timeout);
113 if let Some(proxy) = self.proxy.clone() {
114 builder = builder.proxy(proxy);
115 }
116 if self.no_proxy {
117 builder = builder.no_proxy();
118 }
119 let client = builder.build().context(BuildClientSnafu)?;
120 let mut request = client.get(&url);
121 if let Some(auth) = &self.auth_header {
122 request = request.header("Authorization", auth);
123 }
124 let response = request.send().await.with_context(|_| HttpQuerySqlSnafu {
125 reason: "packed snapshot capability request failed",
126 })?;
127 if response.status() == reqwest::StatusCode::NOT_FOUND {
128 return crate::error::InvalidArgumentsSnafu {
129 msg: format!("server does not support {capability}"),
130 }
131 .fail();
132 }
133 let response = response
134 .error_for_status()
135 .with_context(|_| HttpQuerySqlSnafu {
136 reason: "packed snapshot capability request rejected",
137 })?;
138 let body = response.text().await.with_context(|_| HttpQuerySqlSnafu {
139 reason: "cannot read capability response",
140 })?;
141 let value: Value = serde_json::from_str(&body).context(SerdeJsonSnafu)?;
142 if !value.is_object() {
143 return crate::error::UnexpectedSnafu {
144 msg: "invalid capability response: expected JSON object",
145 }
146 .fail();
147 }
148 if value.get(capability).and_then(Value::as_u64) != Some(1) {
149 return crate::error::InvalidArgumentsSnafu {
150 msg: format!("server does not support {capability} version 1"),
151 }
152 .fail();
153 }
154 Ok(())
155 }
156
157 pub async fn sql(&self, sql: &str, schema: &str) -> Result<Option<Vec<Vec<Value>>>> {
159 let body = self.sql_response(sql, schema).await?;
160 Ok(body.output().first().and_then(|output| match output {
161 GreptimeQueryOutput::Records(records) => Some(records.rows().clone()),
162 GreptimeQueryOutput::AffectedRows(_) => None,
163 }))
164 }
165
166 pub(crate) async fn sql_response(
167 &self,
168 sql: &str,
169 schema: &str,
170 ) -> Result<GreptimedbV1Response> {
171 let url = format!("http://{}/v1/sql", self.addr);
172 let params = [
173 ("db", format!("{}-{}", self.catalog, schema)),
174 ("sql", sql.to_string()),
175 ];
176 let mut builder = reqwest::Client::builder();
177 if let Some(proxy) = self.proxy.clone() {
178 builder = builder.proxy(proxy);
179 }
180 if self.no_proxy {
181 builder = builder.no_proxy();
182 }
183 let client = builder.build().context(BuildClientSnafu)?;
184 let mut request = client
185 .post(&url)
186 .form(¶ms)
187 .header("Content-Type", "application/x-www-form-urlencoded");
188 if let Some(ref auth) = self.auth_header {
189 request = request.header("Authorization", auth);
190 }
191
192 request = request.header(
193 GREPTIME_DB_HEADER_TIMEOUT,
194 format_duration(self.timeout).to_string(),
195 );
196
197 let response = request.send().await.with_context(|_| HttpQuerySqlSnafu {
198 reason: format!("bad url: {}", url),
199 })?;
200 let response = response
201 .error_for_status()
202 .with_context(|_| HttpQuerySqlSnafu {
203 reason: format!("query failed: {}", sql),
204 })?;
205
206 let text = response.text().await.with_context(|_| HttpQuerySqlSnafu {
207 reason: "cannot get response text".to_string(),
208 })?;
209
210 serde_json::from_str::<GreptimedbV1Response>(&text).context(SerdeJsonSnafu)
211 }
212}
213
214pub(crate) fn split_database(database: &str) -> Result<(String, Option<String>)> {
216 let (catalog, schema) = match database.split_once('-') {
217 Some((catalog, schema)) => (catalog, schema),
218 None => (DEFAULT_CATALOG_NAME, database),
219 };
220
221 if schema == "*" {
222 Ok((catalog.to_string(), None))
223 } else {
224 Ok((catalog.to_string(), Some(schema.to_string())))
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 #[tokio::test]
231 async fn packed_capability_probe_checks_auth_and_protocol_version() {
232 use tokio::io::{AsyncReadExt, AsyncWriteExt};
233 for export in [false, true] {
234 for (status, body, supported) in [
235 (200, r#"{"metric_packed_import":1,"future":2}"#, true),
236 (200, "{}", false),
237 (200, r#"{"metric_packed_import":2}"#, false),
238 (404, "{}", false),
239 (401, "{}", false),
240 (403, "{}", false),
241 (200, "[]", false),
242 (200, "invalid-json", false),
243 ] {
244 let body = if export {
245 body.replace("metric_packed_import", "metric_packed_export")
246 } else {
247 body.to_string()
248 };
249 let response_body = body.clone();
250 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
251 let address = listener.local_addr().unwrap();
252 let server = tokio::spawn(async move {
253 let body = response_body;
254 let (mut socket, _) = listener.accept().await.unwrap();
255 let mut request = vec![0; 4096];
256 let n = socket.read(&mut request).await.unwrap();
257 let request = String::from_utf8_lossy(&request[..n]).to_lowercase();
258 assert!(request.starts_with("get /v1/capabilities "));
259 assert!(request.contains("authorization: basic dxnlcjpwyxnzd29yza=="));
260 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();
261 });
262 let client = super::DatabaseClient::new(
263 address.to_string(),
264 "greptime".into(),
265 Some("user:password".into()),
266 std::time::Duration::from_secs(5),
267 None,
268 true,
269 );
270 assert_eq!(
271 if export {
272 client.require_packed_export().await.is_ok()
273 } else {
274 client.require_packed_import().await.is_ok()
275 },
276 supported,
277 "{status}: {body}"
278 );
279 server.await.unwrap();
280 }
281 }
282 }
283
284 use super::*;
285
286 #[test]
287 fn test_split_database() {
288 let result = split_database("catalog-schema").unwrap();
289 assert_eq!(result, ("catalog".to_string(), Some("schema".to_string())));
290
291 let result = split_database("schema").unwrap();
292 assert_eq!(result, ("greptime".to_string(), Some("schema".to_string())));
293
294 let result = split_database("catalog-*").unwrap();
295 assert_eq!(result, ("catalog".to_string(), None));
296
297 let result = split_database("*").unwrap();
298 assert_eq!(result, ("greptime".to_string(), None));
299 }
300}