Skip to main content

netget/server/influxdb/
query.rs

1//! Queries: Flux at `POST /api/v2/query` and InfluxQL at the v1-compatible `/query`.
2//!
3//! The server holds no time series — the project's rule is that protocols implement no
4//! storage — so a query reaches the model (or a configured handler) as an `influx_query` event
5//! and the answer comes back as structured tables. This module reads the request in each
6//! dialect's own shape and renders the model's tables in each dialect's own wire format:
7//!
8//! - **Flux** answers in annotated CSV (`#datatype`, `#group`, `#default`, then a header and
9//!   rows), the format every official client parses; each table is its own annotated block.
10//! - **InfluxQL** answers in the v1 JSON shape, `{"results": [{"statement_id", "series": [{"name",
11//!   "tags", "columns", "values"}]}]}`, with times as RFC 3339 or, when the client sends
12//!   `epoch`, integers in that unit.
13//!
14//! Every value is checked against its column's declared type before anything is written, so a
15//! model that says `long` and writes `0.5` is refused rather than putting a lie on the wire.
16
17use anyhow::{bail, ensure, Context, Result};
18use serde::{Deserialize, Serialize};
19use serde_json::{json, Value};
20use std::collections::BTreeMap;
21
22/// Longest query text accepted, Flux or InfluxQL.
23pub const MAX_QUERY_BYTES: usize = 16 * 1024;
24/// Statements in one InfluxQL request.
25pub const MAX_STATEMENTS: usize = 16;
26/// Tables in one answer, all statements together.
27pub const MAX_RESULT_TABLES: usize = 64;
28/// Columns in one table, and tags on one table (Flux renders each tag as a column too).
29pub const MAX_RESULT_COLUMNS: usize = 64;
30/// Rows in one answer, all tables together.
31pub const MAX_RESULT_ROWS: usize = 10_000;
32/// Longest string a cell, tag or name may hold.
33pub const MAX_CELL_BYTES: usize = 64 * 1024;
34/// The rendered answer, whichever dialect.
35pub const MAX_RESPONSE_BYTES: usize = 4 * 1024 * 1024;
36
37/// The query dialect.
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "lowercase")]
40pub enum Language {
41    Flux,
42    Influxql,
43}
44
45/// A query as the event reports it.
46#[derive(Clone, Debug, Serialize)]
47pub struct QueryRequest {
48    pub language: Language,
49    pub query: String,
50    /// InfluxQL only: the statements, split on `;` outside quotes.
51    #[serde(skip_serializing_if = "Vec::is_empty")]
52    pub statements: Vec<String>,
53    /// Flux: the org; InfluxQL: absent.
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub org: Option<String>,
56    /// InfluxQL: the database (`db`), and retention policy (`rp`).
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub db: Option<String>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub rp: Option<String>,
61    /// InfluxQL: the unit the client asked times in, `ns`/`u`/`ms`/`s`/`m`/`h`.
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub epoch: Option<String>,
64    /// The credential, never reported: compared against the configured token.
65    #[serde(skip)]
66    pub v1_password: Option<String>,
67}
68
69fn decode_pairs(text: &str) -> Result<BTreeMap<String, String>> {
70    let mut out = BTreeMap::new();
71    for field in text.split('&').filter(|f| !f.is_empty()) {
72        let (k, v) = field.split_once('=').unwrap_or((field, ""));
73        let k = super::query_piece(k)?;
74        let v = super::query_piece(v)?;
75        ensure!(out.insert(k, v).is_none(), "duplicate query parameter");
76    }
77    Ok(out)
78}
79
80fn bounded(text: String, what: &str) -> Result<String> {
81    ensure!(
82        text.len() <= MAX_QUERY_BYTES,
83        "{what} is {} bytes; at most {MAX_QUERY_BYTES}",
84        text.len()
85    );
86    ensure!(!text.trim().is_empty(), "{what} is empty");
87    Ok(text)
88}
89
90/// `POST /api/v2/query?org=…`: a JSON body `{"query": …, "type": "flux"}` (what every official
91/// client sends) or a raw `application/vnd.flux` body.
92pub fn parse_flux(
93    uri_query: Option<&str>,
94    content_type: Option<&str>,
95    body: &[u8],
96) -> Result<QueryRequest> {
97    let params = decode_pairs(uri_query.unwrap_or(""))?;
98    let org = params.get("org").or_else(|| params.get("orgID")).cloned();
99    let mime = content_type
100        .and_then(|c| c.split(';').next())
101        .map(|c| c.trim().to_ascii_lowercase())
102        .unwrap_or_else(|| "application/json".into());
103    let query = match mime.as_str() {
104        "application/vnd.flux" => String::from_utf8(body.to_vec()).context("Flux is not UTF-8")?,
105        "application/json" => {
106            let v: Value = serde_json::from_slice(body).context("query body is not JSON")?;
107            if let Some(t) = v.get("type").and_then(|t| t.as_str()) {
108                ensure!(
109                    t == "flux",
110                    "only Flux is served at /api/v2/query, not {t:?}"
111                );
112            }
113            v.get("query")
114                .and_then(|q| q.as_str())
115                .context("the JSON body needs a 'query'")?
116                .to_string()
117        }
118        other => bail!("unsupported query content type {other}"),
119    };
120    Ok(QueryRequest {
121        language: Language::Flux,
122        query: bounded(query, "the Flux query")?,
123        statements: Vec::new(),
124        org,
125        db: None,
126        rp: None,
127        epoch: None,
128        v1_password: None,
129    })
130}
131
132/// Split InfluxQL on `;` outside single and double quotes.
133pub fn split_statements(q: &str) -> Vec<String> {
134    let mut out = Vec::new();
135    let mut current = String::new();
136    let mut quote: Option<char> = None;
137    let mut escaped = false;
138    for c in q.chars() {
139        if escaped {
140            current.push(c);
141            escaped = false;
142            continue;
143        }
144        match (c, quote) {
145            ('\\', Some(_)) => {
146                escaped = true;
147                current.push(c);
148            }
149            ('\'' | '"', None) => {
150                quote = Some(c);
151                current.push(c);
152            }
153            (c2, Some(q2)) if c2 == q2 => {
154                quote = None;
155                current.push(c);
156            }
157            (';', None) => {
158                if !current.trim().is_empty() {
159                    out.push(current.trim().to_string());
160                }
161                current.clear();
162            }
163            _ => current.push(c),
164        }
165    }
166    if !current.trim().is_empty() {
167        out.push(current.trim().to_string());
168    }
169    out
170}
171
172/// `GET|POST /query?db=…&q=…`: InfluxQL, its parameters in the query string or, for a POST, in
173/// a form-encoded body (both v1 client libraries do one or the other).
174pub fn parse_influxql(
175    uri_query: Option<&str>,
176    content_type: Option<&str>,
177    body: &[u8],
178) -> Result<QueryRequest> {
179    let mut params = decode_pairs(uri_query.unwrap_or(""))?;
180    let form = content_type
181        .and_then(|c| c.split(';').next())
182        .is_some_and(|c| {
183            c.trim()
184                .eq_ignore_ascii_case("application/x-www-form-urlencoded")
185        });
186    if form && !body.is_empty() {
187        for (k, v) in decode_pairs(std::str::from_utf8(body).context("form body is not UTF-8")?)? {
188            ensure!(params.insert(k, v).is_none(), "duplicate query parameter");
189        }
190    }
191    if params.get("chunked").is_some_and(|c| c == "true") {
192        bail!("chunked responses are not served");
193    }
194    let query = bounded(
195        params
196            .remove("q")
197            .context("q (the InfluxQL query) is required")?,
198        "q",
199    )?;
200    let statements = split_statements(&query);
201    ensure!(!statements.is_empty(), "q holds no statement");
202    ensure!(
203        statements.len() <= MAX_STATEMENTS,
204        "q holds {} statements; at most {MAX_STATEMENTS}",
205        statements.len()
206    );
207    // An empty `epoch` is RFC 3339, as InfluxDB reads it: node-influx sends `epoch=` whenever
208    // no precision was asked for.
209    let epoch = params.remove("epoch").filter(|e| !e.is_empty());
210    if let Some(e) = &epoch {
211        ensure!(
212            ["ns", "n", "u", "µ", "ms", "s", "m", "h"].contains(&e.as_str()),
213            "epoch must be ns, u, ms, s, m or h"
214        );
215    }
216    Ok(QueryRequest {
217        language: Language::Influxql,
218        query,
219        statements,
220        org: None,
221        db: params.remove("db"),
222        rp: params.remove("rp"),
223        epoch,
224        v1_password: params.remove("p"),
225    })
226}
227
228/// A column's type, in Flux's annotation vocabulary.
229#[derive(Clone, Copy, Debug, PartialEq, Eq)]
230pub enum ColumnType {
231    String,
232    Long,
233    UnsignedLong,
234    Double,
235    Boolean,
236    DateTime,
237}
238
239impl ColumnType {
240    fn parse(s: &str) -> Result<ColumnType> {
241        Ok(match s {
242            "string" => ColumnType::String,
243            "long" => ColumnType::Long,
244            "unsignedLong" => ColumnType::UnsignedLong,
245            "double" => ColumnType::Double,
246            "boolean" => ColumnType::Boolean,
247            "dateTime:RFC3339" | "dateTime" | "time" => ColumnType::DateTime,
248            other => bail!(
249                "column type {other:?} is not one of string, long, unsignedLong, double, \
250                 boolean, dateTime:RFC3339"
251            ),
252        })
253    }
254
255    fn annotation(self) -> &'static str {
256        match self {
257            ColumnType::String => "string",
258            ColumnType::Long => "long",
259            ColumnType::UnsignedLong => "unsignedLong",
260            ColumnType::Double => "double",
261            ColumnType::Boolean => "boolean",
262            ColumnType::DateTime => "dateTime:RFC3339",
263        }
264    }
265}
266
267#[derive(Clone, Debug)]
268pub struct Column {
269    pub name: String,
270    pub kind: ColumnType,
271    pub group: bool,
272}
273
274/// A cell, checked against its column's type.
275#[derive(Clone, Debug)]
276pub enum Cell {
277    Null,
278    Text(String),
279    Long(i64),
280    Unsigned(u64),
281    Double(f64),
282    Bool(bool),
283    /// Nanoseconds since the epoch, and the text the model wrote.
284    Time(i64, String),
285}
286
287#[derive(Clone, Debug)]
288pub struct Table {
289    pub measurement: Option<String>,
290    pub tags: BTreeMap<String, String>,
291    pub columns: Vec<Column>,
292    pub rows: Vec<Vec<Cell>>,
293}
294
295/// One InfluxQL statement's answer.
296#[derive(Clone, Debug)]
297pub enum StatementResult {
298    Tables(Vec<Table>),
299    Error(String),
300}
301
302fn short(text: &str, what: &str) -> Result<String> {
303    ensure!(
304        text.len() <= MAX_CELL_BYTES,
305        "{what} is {} bytes; at most {MAX_CELL_BYTES}",
306        text.len()
307    );
308    Ok(text.to_string())
309}
310
311fn parse_time(text: &str) -> Result<i64> {
312    let t = chrono::DateTime::parse_from_rfc3339(text)
313        .with_context(|| format!("{text:?} is not an RFC 3339 time"))?;
314    t.timestamp_nanos_opt()
315        .with_context(|| format!("{text:?} is outside the nanosecond range"))
316}
317
318fn cell(v: &Value, kind: ColumnType, where_: &str) -> Result<Cell> {
319    if v.is_null() {
320        return Ok(Cell::Null);
321    }
322    Ok(match kind {
323        ColumnType::String => Cell::Text(short(
324            v.as_str()
325                .with_context(|| format!("{where_}: a string column holds {v}"))?,
326            where_,
327        )?),
328        ColumnType::Long => Cell::Long(
329            v.as_i64()
330                .with_context(|| format!("{where_}: a long column holds {v}"))?,
331        ),
332        ColumnType::UnsignedLong => Cell::Unsigned(
333            v.as_u64()
334                .with_context(|| format!("{where_}: an unsignedLong column holds {v}"))?,
335        ),
336        ColumnType::Double => {
337            let f = v
338                .as_f64()
339                .with_context(|| format!("{where_}: a double column holds {v}"))?;
340            ensure!(f.is_finite(), "{where_}: a double must be finite");
341            Cell::Double(f)
342        }
343        ColumnType::Boolean => Cell::Bool(
344            v.as_bool()
345                .with_context(|| format!("{where_}: a boolean column holds {v}"))?,
346        ),
347        ColumnType::DateTime => {
348            let text = v
349                .as_str()
350                .with_context(|| format!("{where_}: a dateTime column holds {v}"))?;
351            Cell::Time(parse_time(text)?, text.to_string())
352        }
353    })
354}
355
356/// Read the model's tables, checking every bound and every cell's type.
357pub fn tables_from_json(
358    v: &Value,
359    rows_so_far: &mut usize,
360    tables_so_far: &mut usize,
361) -> Result<Vec<Table>> {
362    let items = v.as_array().context("'tables' must be an array")?;
363    let mut out = Vec::with_capacity(items.len());
364    for (ti, t) in items.iter().enumerate() {
365        *tables_so_far += 1;
366        ensure!(
367            *tables_so_far <= MAX_RESULT_TABLES,
368            "more than {MAX_RESULT_TABLES} tables"
369        );
370        let measurement = t
371            .get("measurement")
372            .and_then(|m| m.as_str())
373            .map(|m| short(m, "measurement"))
374            .transpose()?;
375        let mut tags = BTreeMap::new();
376        if let Some(obj) = t.get("tags").and_then(|t| t.as_object()) {
377            ensure!(
378                obj.len() <= MAX_RESULT_COLUMNS,
379                "tables[{ti}] has {} tags; at most {MAX_RESULT_COLUMNS}",
380                obj.len()
381            );
382            for (k, v) in obj {
383                let v = v
384                    .as_str()
385                    .with_context(|| format!("tables[{ti}].tags.{k} must be a string"))?;
386                tags.insert(short(k, "tag key")?, short(v, "tag value")?);
387            }
388        }
389        let cols = t
390            .get("columns")
391            .and_then(|c| c.as_array())
392            .with_context(|| format!("tables[{ti}] needs 'columns'"))?;
393        ensure!(
394            !cols.is_empty() && cols.len() <= MAX_RESULT_COLUMNS,
395            "tables[{ti}] has {} columns; 1 to {MAX_RESULT_COLUMNS}",
396            cols.len()
397        );
398        let columns = cols
399            .iter()
400            .enumerate()
401            .map(|(ci, c)| {
402                let name = c
403                    .get("name")
404                    .and_then(|n| n.as_str())
405                    .with_context(|| format!("tables[{ti}].columns[{ci}] needs 'name'"))?;
406                let kind =
407                    ColumnType::parse(c.get("type").and_then(|k| k.as_str()).unwrap_or("string"))?;
408                Ok(Column {
409                    name: short(name, "column name")?,
410                    kind,
411                    group: c.get("group").and_then(|g| g.as_bool()).unwrap_or(false),
412                })
413            })
414            .collect::<Result<Vec<_>>>()?;
415        let rows_json = t
416            .get("rows")
417            .and_then(|r| r.as_array())
418            .with_context(|| format!("tables[{ti}] needs 'rows', an array of arrays"))?;
419        let mut rows = Vec::with_capacity(rows_json.len());
420        for (ri, r) in rows_json.iter().enumerate() {
421            *rows_so_far += 1;
422            ensure!(
423                *rows_so_far <= MAX_RESULT_ROWS,
424                "more than {MAX_RESULT_ROWS} rows"
425            );
426            let r = r
427                .as_array()
428                .with_context(|| format!("tables[{ti}].rows[{ri}] must be an array"))?;
429            ensure!(
430                r.len() == columns.len(),
431                "tables[{ti}].rows[{ri}] has {} values for {} columns",
432                r.len(),
433                columns.len()
434            );
435            rows.push(
436                r.iter()
437                    .zip(&columns)
438                    .map(|(v, c)| cell(v, c.kind, &format!("tables[{ti}].rows[{ri}].{}", c.name)))
439                    .collect::<Result<Vec<_>>>()?,
440            );
441        }
442        out.push(Table {
443            measurement,
444            tags,
445            columns,
446            rows,
447        });
448    }
449    Ok(out)
450}
451
452/// The InfluxQL statements' answers from a `send_influx_query_result` action: `statements`
453/// when given, else `tables` as the one statement's answer.
454pub fn statements_from_json(action: &Value) -> Result<Vec<StatementResult>> {
455    let mut rows = 0;
456    let mut tables = 0;
457    if let Some(list) = action.get("statements").and_then(|s| s.as_array()) {
458        ensure!(
459            list.len() <= MAX_STATEMENTS,
460            "more than {MAX_STATEMENTS} statements"
461        );
462        return list
463            .iter()
464            .map(|s| {
465                if let Some(err) = s.get("error").and_then(|e| e.as_str()) {
466                    Ok(StatementResult::Error(short(err, "error")?))
467                } else {
468                    Ok(StatementResult::Tables(tables_from_json(
469                        s.get("tables").unwrap_or(&json!([])),
470                        &mut rows,
471                        &mut tables,
472                    )?))
473                }
474            })
475            .collect();
476    }
477    Ok(vec![StatementResult::Tables(tables_from_json(
478        action
479            .get("tables")
480            .context("needs 'tables' (or 'statements')")?,
481        &mut rows,
482        &mut tables,
483    )?)])
484}
485
486fn csv_field(s: &str) -> String {
487    if s.contains([',', '"', '\n', '\r']) {
488        format!("\"{}\"", s.replace('"', "\"\""))
489    } else {
490        s.to_string()
491    }
492}
493
494fn cell_text(c: &Cell) -> String {
495    match c {
496        Cell::Null => String::new(),
497        Cell::Text(s) => s.clone(),
498        Cell::Long(n) => n.to_string(),
499        Cell::Unsigned(n) => n.to_string(),
500        Cell::Double(f) => {
501            let s = f.to_string();
502            if s.contains(['.', 'e', 'E']) || s.contains("inf") || s.contains("NaN") {
503                s
504            } else {
505                format!("{s}.0")
506            }
507        }
508        Cell::Bool(b) => b.to_string(),
509        Cell::Time(_, text) => text.clone(),
510    }
511}
512
513/// Flux's annotated CSV (RFC 4180 rows under `#datatype`, `#group`, `#default`). Each table
514/// is a block of its own, separated by an empty line, with its index in the `table` column.
515/// Tags become string columns in the group key, and the measurement `_measurement`.
516///
517/// Every line is measured before it is appended, so the answer is refused at the line that
518/// would take it past [`MAX_RESPONSE_BYTES`] and never grows beyond it.
519pub fn render_flux(tables: &[Table]) -> Result<String> {
520    let mut out = String::new();
521    let line = |out: &mut String, first: &str, a: &str, b: &str, rest: Vec<String>| {
522        let len = first.len()
523            + a.len()
524            + b.len()
525            + 2
526            + rest.iter().map(|r| r.len() + 1).sum::<usize>()
527            + 2;
528        ensure!(
529            out.len() + len <= MAX_RESPONSE_BYTES,
530            "the answer passes {MAX_RESPONSE_BYTES} bytes"
531        );
532        out.push_str(first);
533        out.push(',');
534        out.push_str(a);
535        out.push(',');
536        out.push_str(b);
537        for r in rest {
538            out.push(',');
539            out.push_str(&r);
540        }
541        out.push_str("\r\n");
542        Ok(())
543    };
544    for (index, t) in tables.iter().enumerate() {
545        let mut columns = t.columns.clone();
546        let mut constants: Vec<String> = Vec::new();
547        if let Some(m) = &t.measurement {
548            columns.push(Column {
549                name: "_measurement".into(),
550                kind: ColumnType::String,
551                group: true,
552            });
553            constants.push(csv_field(m));
554        }
555        for (k, v) in &t.tags {
556            columns.push(Column {
557                name: k.clone(),
558                kind: ColumnType::String,
559                group: true,
560            });
561            constants.push(csv_field(v));
562        }
563        if index > 0 {
564            ensure!(
565                out.len() + 2 <= MAX_RESPONSE_BYTES,
566                "the answer passes {MAX_RESPONSE_BYTES} bytes"
567            );
568            out.push_str("\r\n");
569        }
570        line(
571            &mut out,
572            "#datatype",
573            "string",
574            "long",
575            columns
576                .iter()
577                .map(|c| c.kind.annotation().to_string())
578                .collect(),
579        )?;
580        line(
581            &mut out,
582            "#group",
583            "false",
584            "false",
585            columns.iter().map(|c| c.group.to_string()).collect(),
586        )?;
587        line(
588            &mut out,
589            "#default",
590            "_result",
591            "",
592            columns.iter().map(|_| String::new()).collect(),
593        )?;
594        line(
595            &mut out,
596            "",
597            "result",
598            "table",
599            columns.iter().map(|c| csv_field(&c.name)).collect(),
600        )?;
601        let table = index.to_string();
602        for row in &t.rows {
603            let mut cells: Vec<String> = row.iter().map(|c| csv_field(&cell_text(c))).collect();
604            cells.extend(constants.iter().cloned());
605            line(&mut out, "", "", &table, cells)?;
606        }
607    }
608    if out.is_empty() {
609        // No tables: an empty result is an empty body with the CSV content type.
610        out.push_str("\r\n");
611    }
612    Ok(out)
613}
614
615fn epoch_value(ns: i64, epoch: &str) -> i64 {
616    match epoch {
617        "u" | "µ" => ns / 1_000,
618        "ms" => ns / 1_000_000,
619        "s" => ns / 1_000_000_000,
620        "m" => ns / 60_000_000_000,
621        "h" => ns / 3_600_000_000_000,
622        _ => ns,
623    }
624}
625
626fn cell_json(c: &Cell, epoch: Option<&str>) -> Value {
627    match c {
628        Cell::Null => Value::Null,
629        Cell::Text(s) => json!(s),
630        Cell::Long(n) => json!(n),
631        Cell::Unsigned(n) => json!(n),
632        Cell::Double(f) => json!(f),
633        Cell::Bool(b) => json!(b),
634        Cell::Time(ns, text) => match epoch {
635            Some(e) => json!(epoch_value(*ns, e)),
636            None => json!(text),
637        },
638    }
639}
640
641/// InfluxQL's v1 JSON: one result per statement, one series per table.
642pub fn render_influxql(results: &[StatementResult], epoch: Option<&str>) -> Result<Vec<u8>> {
643    let results: Vec<Value> = results
644        .iter()
645        .enumerate()
646        .map(|(i, r)| match r {
647            StatementResult::Error(e) => json!({"statement_id": i, "error": e}),
648            StatementResult::Tables(tables) if tables.is_empty() => json!({"statement_id": i}),
649            StatementResult::Tables(tables) => {
650                let series: Vec<Value> = tables
651                    .iter()
652                    .map(|t| {
653                        let mut s = json!({
654                            "name": t.measurement.clone().unwrap_or_else(|| "result".into()),
655                            "columns": t.columns.iter().map(|c| c.name.clone()).collect::<Vec<_>>(),
656                            "values": t.rows.iter().map(|row| row.iter().map(|c| cell_json(c, epoch)).collect::<Vec<_>>()).collect::<Vec<_>>(),
657                        });
658                        if !t.tags.is_empty() {
659                            s["tags"] = json!(t.tags);
660                        }
661                        s
662                    })
663                    .collect();
664                json!({"statement_id": i, "series": series})
665            }
666        })
667        .collect();
668    let body = serde_json::to_vec(&json!({ "results": results }))?;
669    ensure!(
670        body.len() <= MAX_RESPONSE_BYTES,
671        "the answer passes {MAX_RESPONSE_BYTES} bytes"
672    );
673    Ok(body)
674}