Skip to main content

elasticctl_api/search/
mod.rs

1//! Ad hoc data search: ES|QL and Query DSL.
2
3pub mod dataview;
4pub mod dsl;
5pub mod esql;
6
7use elasticctl_core::{Result, Transport};
8
9pub struct SearchRequest {
10    pub index: Option<String>,
11    pub data_view: Option<String>,
12    pub limit: Option<usize>,
13}
14
15/// Return `query` with leading whitespace and ES|QL comments (`// …` to end of
16/// line, `/* … */` blocks) stripped, so classification reads the first real
17/// token rather than a comment.
18fn skip_leading_comments(query: &str) -> &str {
19    let mut rest = query;
20    loop {
21        rest = rest.trim_start();
22        if rest.starts_with("//") {
23            rest = &rest[rest.find('\n').map_or(rest.len(), |end| end + 1)..];
24        } else if rest.starts_with("/*") {
25            let Some(end) = rest.find("*/") else {
26                return "";
27            };
28            rest = &rest[end + 2..];
29        } else {
30            return rest;
31        }
32    }
33}
34
35pub fn has_source(query: &str) -> bool {
36    let rest = skip_leading_comments(query);
37    let word = &rest[..rest.find(char::is_whitespace).unwrap_or(rest.len())];
38    matches!(
39        word.to_ascii_lowercase().as_str(),
40        "from" | "row" | "show" | "metrics"
41    )
42}
43
44pub fn prepend_from(query: &str, pattern: &str) -> String {
45    if has_source(query) {
46        query.to_string()
47    } else {
48        let q = query.trim_start();
49        let sep = if q.starts_with('|') { "" } else { "| " };
50        format!("FROM {pattern} {sep}{q}")
51    }
52}
53
54/// Rewrite an ES|QL query's source to `pattern`, used when `--index` or
55/// `--data-view` is given. A leading `FROM <source>` becomes `FROM <pattern>`
56/// with any `| …` pipeline kept; `ROW`/`SHOW`/`METRICS` pass through untouched;
57/// a query with no source command gets `FROM <pattern>` prepended.
58pub fn rewrite_from(query: &str, pattern: &str) -> String {
59    let rest = skip_leading_comments(query);
60    let leading = &query[..query.len() - rest.len()];
61    let word_end = rest.find(char::is_whitespace).unwrap_or(rest.len());
62    let word = &rest[..word_end];
63    if word.eq_ignore_ascii_case("from") {
64        let after = &rest[word_end..];
65        let source_end = after.find('|').unwrap_or(after.len());
66        let pipeline = &after[source_end..];
67        if pipeline.is_empty() {
68            format!("{leading}FROM {pattern}")
69        } else {
70            format!("{leading}FROM {pattern} {pipeline}")
71        }
72    } else if word.eq_ignore_ascii_case("row")
73        || word.eq_ignore_ascii_case("show")
74        || word.eq_ignore_ascii_case("metrics")
75    {
76        query.to_string()
77    } else {
78        prepend_from(query, pattern)
79    }
80}
81
82/// Resolve an ES|QL query's source: `--index` wins, then `--data-view`, then
83/// the query's own source, then the space's default alerts index.
84pub async fn resolve_esql_query(t: &Transport, query: &str, req: &SearchRequest) -> Result<String> {
85    match (&req.index, &req.data_view) {
86        (Some(i), _) => Ok(rewrite_from(query, i)),
87        (_, Some(dv)) => Ok(rewrite_from(query, &dataview::resolve(t, dv).await?)),
88        _ => {
89            if has_source(query) {
90                Ok(query.to_string())
91            } else {
92                Ok(prepend_from(
93                    query,
94                    &dataview::default_alerts_index(t).await?,
95                ))
96            }
97        }
98    }
99}
100
101/// Resolve a DSL search's index: `--index` wins, then `--data-view`, then the
102/// space's default alerts index.
103pub async fn resolve_dsl_index(t: &Transport, req: &SearchRequest) -> Result<String> {
104    match (&req.index, &req.data_view) {
105        (Some(i), _) => Ok(i.clone()),
106        (_, Some(dv)) => dataview::resolve(t, dv).await,
107        _ => dataview::default_alerts_index(t).await,
108    }
109}