elasticctl_api/search/
esql.rs1use elasticctl_core::{Error, ErrorKind, Result, Transport};
4use serde_json::{Map, Value};
5use std::time::Duration;
6
7#[derive(Debug, Clone, PartialEq)]
8pub struct EsqlColumn {
9 pub name: String,
10 pub r#type: String,
11}
12
13#[derive(Debug, Clone, PartialEq)]
16pub struct EsqlResponse {
17 pub columns: Vec<EsqlColumn>,
18 pub values: Vec<Vec<Value>>,
19 pub is_partial: bool,
20}
21
22fn parse_columns(obj: &Map<String, Value>) -> Result<Vec<EsqlColumn>> {
23 let columns = obj
24 .get("columns")
25 .and_then(Value::as_array)
26 .ok_or_else(|| Error::new(ErrorKind::Http, "decoding esql response field `columns`"))?;
27 let mut out = Vec::with_capacity(columns.len());
28 for (i, col) in columns.iter().enumerate() {
29 let name = col.get("name").and_then(Value::as_str).ok_or_else(|| {
30 Error::new(
31 ErrorKind::Http,
32 format!("decoding esql response column {i} field `name`"),
33 )
34 })?;
35 let ty = col.get("type").and_then(Value::as_str).ok_or_else(|| {
36 Error::new(
37 ErrorKind::Http,
38 format!("decoding esql response column {i} field `type`"),
39 )
40 })?;
41 out.push(EsqlColumn {
42 name: name.to_string(),
43 r#type: ty.to_string(),
44 });
45 }
46 Ok(out)
47}
48
49fn is_partial(obj: &Map<String, Value>) -> bool {
50 obj.get("is_partial")
51 .and_then(Value::as_bool)
52 .unwrap_or(false)
53}
54
55pub fn decode(value: &Value) -> Result<EsqlResponse> {
59 let obj = value.as_object().ok_or_else(|| {
60 Error::new(
61 ErrorKind::Http,
62 "decoding esql response: expected an object",
63 )
64 })?;
65 let columns = parse_columns(obj)?;
66
67 let values = obj
68 .get("values")
69 .and_then(Value::as_array)
70 .ok_or_else(|| Error::new(ErrorKind::Http, "decoding esql response field `values`"))?
71 .iter()
72 .map(|row| {
73 let cells = row.as_array().ok_or_else(|| {
74 Error::new(
75 ErrorKind::Http,
76 "decoding esql response: `values` rows must be arrays",
77 )
78 })?;
79 if cells.len() != columns.len() {
80 return Err(Error::new(
81 ErrorKind::Http,
82 "decoding esql response: `values` row width does not match `columns`",
83 ));
84 }
85 Ok(cells.clone())
86 })
87 .collect::<Result<Vec<_>>>()?;
88
89 Ok(EsqlResponse {
90 columns,
91 values,
92 is_partial: is_partial(obj),
93 })
94}
95
96pub fn decode_columnar(value: &Value) -> Result<EsqlResponse> {
100 let obj = value.as_object().ok_or_else(|| {
101 Error::new(
102 ErrorKind::Http,
103 "decoding esql response: expected an object",
104 )
105 })?;
106 let columns = parse_columns(obj)?;
107
108 let cols = obj
109 .get("values")
110 .and_then(Value::as_array)
111 .ok_or_else(|| Error::new(ErrorKind::Http, "decoding esql response field `values`"))?;
112 if cols.is_empty() {
116 return Ok(EsqlResponse {
117 columns,
118 values: Vec::new(),
119 is_partial: is_partial(obj),
120 });
121 }
122 if cols.len() != columns.len() {
123 return Err(Error::new(
124 ErrorKind::Http,
125 "decoding esql response: `values` column count does not match `columns`",
126 ));
127 }
128 let arrays = cols
129 .iter()
130 .enumerate()
131 .map(|(i, col)| {
132 col.as_array().ok_or_else(|| {
133 Error::new(
134 ErrorKind::Http,
135 format!("decoding esql response: `values` column {i} must be an array"),
136 )
137 })
138 })
139 .collect::<Result<Vec<_>>>()?;
140 let row_count = arrays.first().map(|col| col.len()).unwrap_or(0);
141 if arrays.iter().any(|col| col.len() != row_count) {
142 return Err(Error::new(
143 ErrorKind::Http,
144 "decoding esql response: `values` columns have unequal lengths",
145 ));
146 }
147
148 let mut values = Vec::with_capacity(row_count);
149 for r in 0..row_count {
150 let mut row = Vec::with_capacity(columns.len());
151 for col in &arrays {
152 row.push(col[r].clone());
153 }
154 values.push(row);
155 }
156
157 Ok(EsqlResponse {
158 columns,
159 values,
160 is_partial: is_partial(obj),
161 })
162}
163
164pub async fn run_sync(t: &Transport, query: &str) -> Result<EsqlResponse> {
166 let body = serde_json::json!({ "query": query });
167 let response = t.post_absolute_es("/_query", &body).await?;
168 decode(&response)
169}
170
171pub async fn run_async(t: &Transport, query: &str) -> Result<EsqlResponse> {
175 let start = t
176 .post_absolute_es(
177 "/_query/async",
178 &serde_json::json!({ "query": query, "wait_for_completion_timeout": "1ms", "columnar": true }),
179 )
180 .await?;
181 let id = match start.get("id").and_then(Value::as_str) {
184 Some(id) => id.to_string(),
185 None => return decode_columnar(&start),
186 };
187 if start.get("is_running").and_then(Value::as_bool) == Some(false) {
190 let _ = t.delete_absolute_es(&format!("/_query/async/{id}")).await;
191 return decode_columnar(&start);
192 }
193
194 poll_until_complete(t, &id, 300, Duration::from_secs(1)).await
197}
198
199pub async fn poll_until_complete(
203 t: &Transport,
204 id: &str,
205 max_polls: usize,
206 interval: Duration,
207) -> Result<EsqlResponse> {
208 for _ in 0..max_polls {
209 let resp = match t.get_absolute_es(&format!("/_query/async/{id}")).await {
210 Ok(resp) => resp,
211 Err(err) => {
212 let _ = t.delete_absolute_es(&format!("/_query/async/{id}")).await;
213 return Err(err);
214 }
215 };
216 if resp.get("is_running").and_then(Value::as_bool) == Some(false) {
217 let _ = t.delete_absolute_es(&format!("/_query/async/{id}")).await;
218 return decode_columnar(&resp);
219 }
220 tokio::time::sleep(interval).await;
221 }
222 let _ = t.delete_absolute_es(&format!("/_query/async/{id}")).await;
223 Err(Error::new(
224 ErrorKind::Timeout,
225 format!("async query {id} still running after {max_polls} polls"),
226 ))
227}