Skip to main content

netget/client/influxdb/
query.rs

1//! Queries: Flux at `POST /api/v2/query` (annotated CSV back) and InfluxQL at the
2//! v1-compatible `GET /query` (v1 JSON back), read into structured tables for the model.
3//!
4//! The model never sees CSV. A Flux answer is split into its annotated blocks, each row into
5//! fields (RFC 4180 quoting), each field converted by its `#datatype` annotation and `#default`
6//! — so a `long` is a JSON integer, a `boolean` a JSON boolean, and an empty cell is the
7//! column's default or `null`. Rows are grouped into tables by the `table` column, which is
8//! how a Flux result says which table a row belongs to. An InfluxQL answer is already JSON,
9//! and is bounded and passed through series by series.
10//!
11//! An event carries at most [`MAX_REPORTED_ROWS`] rows in at most [`MAX_REPORTED_TABLES`]
12//! tables, and says how many it left out. The Flux reader holds no more than that while it
13//! reads: rows and tables past the bounds are validated and counted, never stored, each
14//! header's column descriptors are built once and shared by its rows, and a header or
15//! annotation row is refused past [`MAX_HEADER_BYTES`]. Error text from the server reaches
16//! the model cut to [`MAX_ERROR_TEXT_BYTES`].
17use super::transport::{query as percent, Origin};
18use anyhow::{bail, ensure, Context, Result};
19use bytes::Bytes;
20use http_body_util::{BodyExt, Full, Limited};
21use hyper::{
22    client::conn::http1,
23    header::{ACCEPT, AUTHORIZATION, CONNECTION, CONTENT_ENCODING, CONTENT_TYPE, HOST},
24    Request,
25};
26use hyper_util::rt::TokioIo;
27use serde::{Deserialize, Serialize};
28use serde_json::{json, Map, Value};
29use std::sync::Arc;
30use std::time::Duration;
31
32/// A whole query exchange, connect to last byte.
33pub const QUERY_TIMEOUT: Duration = Duration::from_secs(30);
34/// The answer body, either dialect.
35pub const MAX_QUERY_RESPONSE_BYTES: usize = 4 * 1024 * 1024;
36/// Longest query text sent.
37pub const MAX_QUERY_BYTES: usize = 16 * 1024;
38/// Rows an event reports, all tables or series together.
39pub const MAX_REPORTED_ROWS: usize = 1000;
40/// Tables (Flux) or series (InfluxQL) an event reports.
41pub const MAX_REPORTED_TABLES: usize = 64;
42/// Columns in one table.
43pub const MAX_COLUMNS: usize = 256;
44/// Bytes in one Flux header row's column names, and in one annotation row's fields. Column
45/// descriptors and `#default` values are copied into what an event carries, so their size is
46/// bounded where they are read.
47pub const MAX_HEADER_BYTES: usize = 16 * 1024;
48/// Server-supplied error text, as the model sees it: a v1/v2 error body's message, an
49/// in-band Flux error, or why a 2xx answer could not be read.
50pub const MAX_ERROR_TEXT_BYTES: usize = 1024;
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "lowercase")]
54pub enum Language {
55    Flux,
56    Influxql,
57}
58
59/// A query as the model asked it.
60#[derive(Clone, Debug, Serialize, Deserialize)]
61pub struct QuerySpec {
62    pub language: Language,
63    pub query: String,
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub org: Option<String>,
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub db: Option<String>,
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub rp: Option<String>,
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub epoch: Option<String>,
72}
73
74impl QuerySpec {
75    pub fn validate(&self) -> Result<()> {
76        ensure!(
77            !self.query.trim().is_empty() && self.query.len() <= MAX_QUERY_BYTES,
78            "query must be 1 to {MAX_QUERY_BYTES} bytes"
79        );
80        match self.language {
81            Language::Flux => {
82                let org = self.org.as_deref().context("a Flux query needs 'org'")?;
83                ensure!(
84                    !org.is_empty() && org.len() <= 1024,
85                    "org must be 1 to 1024 bytes"
86                );
87            }
88            Language::Influxql => {
89                for value in [&self.db, &self.rp].into_iter().flatten() {
90                    ensure!(value.len() <= 1024, "db and rp are at most 1024 bytes");
91                }
92                if let Some(e) = &self.epoch {
93                    ensure!(
94                        ["ns", "u", "ms", "s", "m", "h"].contains(&e.as_str()),
95                        "epoch must be ns, u, ms, s, m or h"
96                    );
97                }
98            }
99        }
100        Ok(())
101    }
102
103    fn request(&self, origin: &Origin, token: &Option<String>) -> Result<Request<Full<Bytes>>> {
104        let mut builder = Request::builder()
105            .header(HOST, &origin.authority)
106            .header(CONNECTION, "close");
107        if let Some(token) = token {
108            builder = builder.header(AUTHORIZATION, format!("Token {token}"));
109        }
110        Ok(match self.language {
111            Language::Flux => builder
112                .method("POST")
113                .uri(format!(
114                    "/api/v2/query?org={}",
115                    percent(self.org.as_deref().unwrap_or_default())
116                ))
117                .header(CONTENT_TYPE, "application/json")
118                .header(ACCEPT, "application/csv")
119                .body(Full::new(Bytes::from(serde_json::to_vec(&json!({
120                    "query": self.query,
121                    "type": "flux",
122                    "dialect": {
123                        "header": true,
124                        "delimiter": ",",
125                        "annotations": ["datatype", "group", "default"],
126                        "commentPrefix": "#",
127                        "dateTimeFormat": "RFC3339",
128                    },
129                }))?)))?,
130            Language::Influxql => {
131                let mut target = format!("/query?q={}", percent(&self.query));
132                for (key, value) in [("db", &self.db), ("rp", &self.rp), ("epoch", &self.epoch)] {
133                    if let Some(value) = value {
134                        target.push_str(&format!("&{key}={}", percent(value)));
135                    }
136                }
137                builder
138                    .method("GET")
139                    .uri(target)
140                    .header(ACCEPT, "application/json")
141                    .body(Full::new(Bytes::new()))?
142            }
143        })
144    }
145}
146
147/// What a query exchange reports to the model.
148#[derive(Clone, Debug, Serialize)]
149pub struct QueryResponse {
150    pub language: Language,
151    pub query: String,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub org: Option<String>,
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub db: Option<String>,
156    pub status: u16,
157    /// Flux: the result tables.
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub tables: Option<Vec<Value>>,
160    /// InfluxQL: one entry per statement, `{statement_id, series}` or `{statement_id, error}`.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub statements: Option<Vec<Value>>,
163    /// Rows in the answer, and how many of them this event carries.
164    pub row_count: usize,
165    pub reported_rows: usize,
166    pub truncated: bool,
167    /// Non-2xx answers: the server's own message, and its code where the dialect has one.
168    pub error: Option<Value>,
169}
170
171pub async fn query(
172    origin: Origin,
173    token: Option<String>,
174    spec: QuerySpec,
175) -> Result<QueryResponse> {
176    let request = spec.request(&origin, &token)?;
177    let (status, content_type, body) = tokio::time::timeout(QUERY_TIMEOUT, async move {
178        let socket = tokio::net::TcpStream::connect(&origin.connect_addr).await?;
179        let (mut sender, connection) = http1::Builder::new()
180            .max_headers(64)
181            .max_buf_size(32 * 1024)
182            .handshake(TokioIo::new(socket))
183            .await?;
184        let exchange = async {
185            let response = sender.send_request(request).await?;
186            let (parts, body) = response.into_parts();
187            ensure!(
188                parts
189                    .headers
190                    .get(CONTENT_ENCODING)
191                    .map(|v| v.as_bytes() == b"identity")
192                    .unwrap_or(true),
193                "compressed HTTP responses unsupported"
194            );
195            let content_type = parts
196                .headers
197                .get(CONTENT_TYPE)
198                .and_then(|v| v.to_str().ok())
199                .and_then(|v| v.split(';').next())
200                .map(|v| v.trim().to_ascii_lowercase())
201                .unwrap_or_default();
202            let bytes = Limited::new(body, MAX_QUERY_RESPONSE_BYTES)
203                .collect()
204                .await
205                .map_err(|_| {
206                    anyhow::anyhow!(
207                        "query answer passes {MAX_QUERY_RESPONSE_BYTES} bytes or is cut short"
208                    )
209                })?
210                .to_bytes();
211            Ok::<_, anyhow::Error>((parts.status.as_u16(), content_type, bytes))
212        };
213        tokio::pin!(connection);
214        tokio::pin!(exchange);
215        tokio::select! {
216            result = &mut exchange => result,
217            result = &mut connection => {
218                result.context("HTTP driver failed")?;
219                exchange.await
220            }
221        }
222    })
223    .await
224    .context("InfluxDB query deadline exceeded")??;
225    let mut response = QueryResponse {
226        language: spec.language,
227        query: spec.query,
228        org: spec.org,
229        db: spec.db,
230        status,
231        tables: None,
232        statements: None,
233        row_count: 0,
234        reported_rows: 0,
235        truncated: false,
236        error: None,
237    };
238    if !(200..300).contains(&status) {
239        response.error = Some(error_body(&body));
240        return Ok(response);
241    }
242    // A 2xx answer the client cannot read — or a Flux failure reported in-band, after the
243    // status line was already sent — is reported to the model as an error, not as tables,
244    // and the session carries on.
245    if let Err(e) = read_answer(&mut response, spec.language, &content_type, &body) {
246        response.error = Some(json!({
247            "code": "unreadable_answer",
248            "message": crate::utils::truncate_for_log(&e.to_string(), MAX_ERROR_TEXT_BYTES),
249        }));
250    }
251    Ok(response)
252}
253
254fn read_answer(
255    response: &mut QueryResponse,
256    language: Language,
257    content_type: &str,
258    body: &[u8],
259) -> Result<()> {
260    let text = std::str::from_utf8(body).context("the answer is not UTF-8")?;
261    let mut budget = Budget::default();
262    match language {
263        Language::Flux => {
264            ensure!(
265                content_type.is_empty()
266                    || content_type == "text/csv"
267                    || content_type == "application/csv",
268                "a Flux answer must be CSV, not {content_type}"
269            );
270            let answer = read_flux(text)?;
271            response.tables = Some(budget.flux(answer));
272        }
273        Language::Influxql => {
274            let value: Value =
275                serde_json::from_str(text).context("the InfluxQL answer is not JSON")?;
276            response.statements = Some(budget.statements(&value)?);
277        }
278    }
279    budget.finish(response);
280    Ok(())
281}
282
283/// An error body as the model sees it: v2's `{code, message}`, v1's `{error}` as
284/// `{message}`, or the text itself when it is neither — each cut to [`MAX_ERROR_TEXT_BYTES`].
285fn error_body(body: &[u8]) -> Value {
286    let text = String::from_utf8_lossy(body);
287    match serde_json::from_str::<Value>(&text) {
288        Ok(v) if v.get("message").is_some() => json!({
289            "code": v.get("code").map_or(Value::Null, bounded),
290            "message": bounded(&v["message"]),
291        }),
292        Ok(v) if v.get("error").is_some() => {
293            json!({"code": Value::Null, "message": bounded(&v["error"])})
294        }
295        _ => json!({
296            "code": Value::Null,
297            "message": crate::utils::truncate_for_log(text.trim(), MAX_ERROR_TEXT_BYTES),
298        }),
299    }
300}
301
302/// A server-supplied JSON value bounded for the model: a string cut on a char boundary to
303/// [`MAX_ERROR_TEXT_BYTES`]; an array or object whose JSON text passes that becomes its
304/// text, cut the same way.
305fn bounded(value: &Value) -> Value {
306    match value {
307        Value::String(s) => Value::String(crate::utils::truncate_for_log(s, MAX_ERROR_TEXT_BYTES)),
308        Value::Array(_) | Value::Object(_) => {
309            let text = value.to_string();
310            if text.len() <= MAX_ERROR_TEXT_BYTES {
311                value.clone()
312            } else {
313                Value::String(crate::utils::truncate_for_log(&text, MAX_ERROR_TEXT_BYTES))
314            }
315        }
316        _ => value.clone(),
317    }
318}
319
320#[derive(Default)]
321struct Budget {
322    rows: usize,
323    reported: usize,
324    tables: usize,
325    truncated: bool,
326}
327
328impl Budget {
329    fn take(&mut self, rows: Vec<Value>) -> Vec<Value> {
330        self.rows += rows.len();
331        let room = MAX_REPORTED_ROWS - self.reported;
332        if rows.len() > room {
333            self.truncated = true;
334        }
335        let kept: Vec<Value> = rows.into_iter().take(room).collect();
336        self.reported += kept.len();
337        kept
338    }
339
340    fn table_room(&mut self) -> bool {
341        self.tables += 1;
342        if self.tables > MAX_REPORTED_TABLES {
343            self.truncated = true;
344            return false;
345        }
346        true
347    }
348
349    /// The tables `read_flux` kept, with the counts of everything it did not.
350    fn flux(&mut self, answer: FluxAnswer) -> Vec<Value> {
351        let mut out = Vec::new();
352        for t in answer.tables {
353            if !self.table_room() {
354                self.rows += t.rows.len();
355                continue;
356            }
357            let rows = self.take(t.rows);
358            out.push(json!({
359                "result": t.result,
360                "table": t.table,
361                "columns": t.columns,
362                "rows": rows,
363            }));
364        }
365        // `take` counted only the rows the reader kept; the reader counted them all.
366        self.rows = answer.row_count;
367        if answer.table_count > MAX_REPORTED_TABLES || answer.row_count > self.reported {
368            self.truncated = true;
369        }
370        out
371    }
372
373    fn statements(&mut self, value: &Value) -> Result<Vec<Value>> {
374        let results = value
375            .get("results")
376            .and_then(|r| r.as_array())
377            .context("InfluxQL answer has no 'results'")?;
378        let mut out = Vec::new();
379        for r in results {
380            let id = r.get("statement_id").cloned().unwrap_or(Value::Null);
381            if let Some(e) = r.get("error") {
382                out.push(json!({"statement_id": id, "error": bounded(e)}));
383                continue;
384            }
385            let mut series = Vec::new();
386            for s in r
387                .get("series")
388                .and_then(|s| s.as_array())
389                .into_iter()
390                .flatten()
391            {
392                let values = s
393                    .get("values")
394                    .and_then(|v| v.as_array())
395                    .cloned()
396                    .unwrap_or_default();
397                if !self.table_room() {
398                    self.rows += values.len();
399                    continue;
400                }
401                let columns = s
402                    .get("columns")
403                    .and_then(|c| c.as_array())
404                    .context("a series has no columns")?;
405                ensure!(
406                    columns.len() <= MAX_COLUMNS,
407                    "a series has more than {MAX_COLUMNS} columns"
408                );
409                let mut entry = Map::new();
410                entry.insert("name".into(), s.get("name").cloned().unwrap_or(Value::Null));
411                if let Some(tags) = s.get("tags") {
412                    entry.insert("tags".into(), tags.clone());
413                }
414                entry.insert("columns".into(), Value::Array(columns.clone()));
415                entry.insert("values".into(), Value::Array(self.take(values)));
416                series.push(Value::Object(entry));
417            }
418            out.push(json!({"statement_id": id, "series": series}));
419        }
420        Ok(out)
421    }
422
423    fn finish(self, response: &mut QueryResponse) {
424        response.row_count = self.rows;
425        response.reported_rows = self.reported;
426        response.truncated = self.truncated;
427    }
428}
429
430/// One Flux table: its columns (name, datatype, whether in the group key) and typed rows.
431#[derive(Debug)]
432pub struct FluxTable {
433    pub result: String,
434    pub table: i64,
435    pub columns: Vec<Value>,
436    pub rows: Vec<Value>,
437}
438
439/// A Flux answer as read: the first [`MAX_REPORTED_TABLES`] tables, holding the first
440/// [`MAX_REPORTED_ROWS`] rows between them, and how many of each the answer had in all.
441#[derive(Debug)]
442pub struct FluxAnswer {
443    pub tables: Vec<FluxTable>,
444    /// Tables in the answer, kept or not.
445    pub table_count: usize,
446    /// Rows in the answer, kept or not.
447    pub row_count: usize,
448}
449
450/// Split a CSV document into records, RFC 4180: quoted fields may hold commas, doubled quotes
451/// and line breaks; a record ends at CRLF or LF; an empty line is an empty record.
452pub fn csv_records(text: &str) -> Result<Vec<Vec<String>>> {
453    CsvRecords::new(text, usize::MAX).collect()
454}
455
456/// [`csv_records`] one record at a time, so a reader holds one record rather than the whole
457/// document. A record with more than `max_fields` fields is an error, raised at the comma that
458/// passes the bound rather than after the record is held whole.
459pub struct CsvRecords<'a> {
460    chars: std::iter::Peekable<std::str::Chars<'a>>,
461    max_fields: usize,
462    done: bool,
463}
464
465impl<'a> CsvRecords<'a> {
466    pub fn new(text: &'a str, max_fields: usize) -> Self {
467        CsvRecords {
468            chars: text.chars().peekable(),
469            max_fields,
470            done: false,
471        }
472    }
473}
474
475impl Iterator for CsvRecords<'_> {
476    type Item = Result<Vec<String>>;
477
478    fn next(&mut self) -> Option<Self::Item> {
479        if self.done {
480            return None;
481        }
482        let mut record = Vec::new();
483        let mut field = String::new();
484        let mut quoted = false;
485        let mut empty_line = true;
486        while let Some(c) = self.chars.next() {
487            if quoted {
488                if c == '"' {
489                    if self.chars.peek() == Some(&'"') {
490                        field.push('"');
491                        self.chars.next();
492                    } else {
493                        quoted = false;
494                    }
495                } else {
496                    field.push(c);
497                }
498                continue;
499            }
500            match c {
501                '"' => {
502                    quoted = true;
503                    empty_line = false;
504                }
505                ',' => {
506                    record.push(std::mem::take(&mut field));
507                    empty_line = false;
508                    // At least one more field follows this comma.
509                    if record.len() >= self.max_fields {
510                        self.done = true;
511                        return Some(Err(anyhow::anyhow!(
512                            "a CSV record has more than {} fields",
513                            self.max_fields
514                        )));
515                    }
516                }
517                '\r' if self.chars.peek() == Some(&'\n') => {}
518                '\n' => {
519                    if empty_line && record.is_empty() && field.is_empty() {
520                        return Some(Ok(Vec::new()));
521                    }
522                    record.push(field);
523                    return Some(Ok(record));
524                }
525                c => {
526                    field.push(c);
527                    empty_line = false;
528                }
529            }
530        }
531        self.done = true;
532        if quoted {
533            return Some(Err(anyhow::anyhow!("unterminated quoted CSV field")));
534        }
535        if !empty_line || !record.is_empty() || !field.is_empty() {
536            record.push(field);
537            return Some(Ok(record));
538        }
539        None
540    }
541}
542
543fn typed(text: &str, datatype: &str) -> Result<Value> {
544    Ok(match datatype {
545        "long" => json!(text
546            .parse::<i64>()
547            .with_context(|| format!("{text:?} is not a long"))?),
548        "unsignedLong" => json!(text
549            .parse::<u64>()
550            .with_context(|| format!("{text:?} is not an unsignedLong"))?),
551        "double" => match text {
552            "+Inf" | "Inf" => json!("+Inf"),
553            "-Inf" => json!("-Inf"),
554            "NaN" => json!("NaN"),
555            _ => json!(text
556                .parse::<f64>()
557                .with_context(|| format!("{text:?} is not a double"))?),
558        },
559        "boolean" => match text {
560            "true" => json!(true),
561            "false" => json!(false),
562            _ => bail!("{text:?} is not a boolean"),
563        },
564        // string, dateTime:RFC3339, duration, base64Binary: the text is the value.
565        _ => json!(text),
566    })
567}
568
569/// The header row of the current block, read once.
570struct Header {
571    names: Vec<String>,
572    /// The descriptor of every column but `result` and `table`, built once per header and
573    /// shared by every table it starts.
574    columns: Arc<Vec<Value>>,
575    /// Which header this is: rows under the same header have the same columns.
576    generation: usize,
577    /// A header with an `error` column and no `table` column is Flux reporting a failure.
578    error_at: Option<usize>,
579}
580
581impl Header {
582    fn new(names: Vec<String>, datatypes: &[String], groups: &[String], generation: usize) -> Self {
583        let error_at = if names.iter().any(|n| n == "table") {
584            None
585        } else {
586            names.iter().position(|n| n == "error")
587        };
588        let columns = Arc::new(Self::columns(&names, datatypes, groups));
589        Header {
590            names,
591            columns,
592            generation,
593            error_at,
594        }
595    }
596
597    fn columns(names: &[String], datatypes: &[String], groups: &[String]) -> Vec<Value> {
598        names
599            .iter()
600            .enumerate()
601            .skip(1)
602            .filter(|(_, name)| *name != "result" && *name != "table")
603            .map(|(i, name)| {
604                json!({
605                    "name": name,
606                    "type": datatypes.get(i).map(String::as_str).unwrap_or("string"),
607                    "group": groups.get(i).is_some_and(|g| g == "true"),
608                })
609            })
610            .collect()
611    }
612
613    /// The last column named `name`, as a row reads it.
614    fn last(&self, name: &str) -> Option<usize> {
615        self.names
616            .iter()
617            .enumerate()
618            .skip(1)
619            .filter(|(_, n)| *n == name)
620            .map(|(i, _)| i)
621            .last()
622    }
623}
624
625/// The table the last row belonged to, whether or not it was kept.
626struct Current {
627    result: String,
628    table: i64,
629    generation: usize,
630    columns: Arc<Vec<Value>>,
631    kept: bool,
632}
633
634fn record_bytes(record: &[String]) -> usize {
635    record.iter().map(String::len).sum()
636}
637
638/// Field `i` of a row, or the column's `#default` when it is empty. Borrowed: nothing is
639/// copied until a row is kept.
640fn cell<'a>(record: &'a [String], defaults: &'a [String], i: usize) -> &'a str {
641    if record[i].is_empty() {
642        defaults.get(i).map_or("", String::as_str)
643    } else {
644        &record[i]
645    }
646}
647
648/// Read Flux's annotated CSV into tables. A block is a run of annotation rows, a header, and
649/// data rows, ending at an empty line or the next annotation. A block whose header has an
650/// `error` column is Flux reporting a failure mid-stream, and is returned as an error.
651///
652/// Every row is checked against its header and datatypes, but only the first
653/// [`MAX_REPORTED_TABLES`] tables and the first [`MAX_REPORTED_ROWS`] rows are stored; the
654/// rest are counted. A header or annotation row past [`MAX_HEADER_BYTES`] is an error.
655pub fn read_flux(text: &str) -> Result<FluxAnswer> {
656    let mut answer = FluxAnswer {
657        tables: Vec::new(),
658        table_count: 0,
659        row_count: 0,
660    };
661    let mut kept_rows = 0usize;
662    let mut datatypes: Vec<String> = Vec::new();
663    let mut groups: Vec<String> = Vec::new();
664    let mut defaults: Vec<String> = Vec::new();
665    let mut header: Option<Header> = None;
666    let mut generation = 0usize;
667    let mut current: Option<Current> = None;
668    for record in CsvRecords::new(text, MAX_COLUMNS + 1) {
669        let record = record?;
670        if record.is_empty() || record.iter().all(|f| f.is_empty()) {
671            header = None;
672            continue;
673        }
674        if matches!(record[0].as_str(), "#datatype" | "#group" | "#default") {
675            ensure!(
676                record_bytes(&record) <= MAX_HEADER_BYTES,
677                "a Flux annotation row passes {MAX_HEADER_BYTES} bytes"
678            );
679        }
680        match record[0].as_str() {
681            "#datatype" => {
682                header = None;
683                datatypes = record;
684                continue;
685            }
686            "#group" => {
687                groups = record;
688                // A #group row after the header changes the columns of the rows after it.
689                if let Some(h) = header.as_mut() {
690                    generation += 1;
691                    h.columns = Arc::new(Header::columns(&h.names, &datatypes, &groups));
692                    h.generation = generation;
693                }
694                continue;
695            }
696            "#default" => {
697                defaults = record;
698                continue;
699            }
700            _ => {}
701        }
702        let Some(h) = &header else {
703            ensure!(
704                record.len() <= MAX_COLUMNS + 1,
705                "more than {MAX_COLUMNS} columns"
706            );
707            ensure!(
708                record_bytes(&record) <= MAX_HEADER_BYTES,
709                "a Flux header passes {MAX_HEADER_BYTES} bytes of column names"
710            );
711            let reports_error =
712                record.iter().any(|n| n == "error") && !record.iter().any(|n| n == "table");
713            if !reports_error {
714                ensure!(
715                    datatypes.len() == record.len(),
716                    "a Flux header without a matching #datatype annotation"
717                );
718            }
719            generation += 1;
720            header = Some(Header::new(record, &datatypes, &groups, generation));
721            continue;
722        };
723        ensure!(
724            record.len() == h.names.len(),
725            "a Flux row has {} fields for {} columns",
726            record.len(),
727            h.names.len()
728        );
729        if let Some(at) = h.error_at {
730            bail!(
731                "the query failed: {}",
732                crate::utils::truncate_for_log(&record[at], MAX_ERROR_TEXT_BYTES)
733            );
734        }
735        let result = h
736            .last("result")
737            .map_or("_result", |i| cell(&record, &defaults, i));
738        let table: i64 = match h.last("table") {
739            Some(i) => cell(&record, &defaults, i)
740                .parse()
741                .context("the table column is not a long")?,
742            None => 0,
743        };
744
745        let same = current.as_ref().is_some_and(|c| {
746            c.table == table
747                && c.result == result
748                && (c.generation == h.generation || c.columns == h.columns)
749        });
750        if same {
751            if let Some(c) = current.as_mut() {
752                c.generation = h.generation;
753            }
754        } else {
755            answer.table_count += 1;
756            let kept = answer.tables.len() < MAX_REPORTED_TABLES;
757            if kept {
758                answer.tables.push(FluxTable {
759                    result: result.to_string(),
760                    table,
761                    columns: h.columns.as_ref().clone(),
762                    rows: Vec::new(),
763                });
764            }
765            current = Some(Current {
766                result: result.to_string(),
767                table,
768                generation: h.generation,
769                columns: h.columns.clone(),
770                kept,
771            });
772        }
773        answer.row_count += 1;
774        let keep = current.as_ref().is_some_and(|c| c.kept) && kept_rows < MAX_REPORTED_ROWS;
775
776        let mut row = Vec::new();
777        for (i, name) in h.names.iter().enumerate().skip(1) {
778            if name == "result" || name == "table" {
779                continue;
780            }
781            let datatype = datatypes.get(i).map(String::as_str).unwrap_or("string");
782            let text = cell(&record, &defaults, i);
783            let value = if text.is_empty() && datatype != "string" {
784                Value::Null
785            } else {
786                typed(text, datatype)?
787            };
788            if keep {
789                row.push(value);
790            }
791        }
792        if keep {
793            if let Some(t) = answer.tables.last_mut() {
794                t.rows.push(Value::Array(row));
795                kept_rows += 1;
796            }
797        }
798    }
799    Ok(answer)
800}