1use anyhow::{bail, ensure, Context, Result};
18use serde::{Deserialize, Serialize};
19use serde_json::{json, Value};
20use std::collections::BTreeMap;
21
22pub const MAX_QUERY_BYTES: usize = 16 * 1024;
24pub const MAX_STATEMENTS: usize = 16;
26pub const MAX_RESULT_TABLES: usize = 64;
28pub const MAX_RESULT_COLUMNS: usize = 64;
30pub const MAX_RESULT_ROWS: usize = 10_000;
32pub const MAX_CELL_BYTES: usize = 64 * 1024;
34pub const MAX_RESPONSE_BYTES: usize = 4 * 1024 * 1024;
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "lowercase")]
40pub enum Language {
41 Flux,
42 Influxql,
43}
44
45#[derive(Clone, Debug, Serialize)]
47pub struct QueryRequest {
48 pub language: Language,
49 pub query: String,
50 #[serde(skip_serializing_if = "Vec::is_empty")]
52 pub statements: Vec<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub org: Option<String>,
56 #[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 #[serde(skip_serializing_if = "Option::is_none")]
63 pub epoch: Option<String>,
64 #[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
90pub 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
132pub 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
172pub 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 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#[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#[derive(Clone, Debug)]
276pub enum Cell {
277 Null,
278 Text(String),
279 Long(i64),
280 Unsigned(u64),
281 Double(f64),
282 Bool(bool),
283 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#[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
356pub 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
452pub 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
513pub 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 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
641pub 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}