Skip to main content

mongreldb_kit/
tsv.rs

1//! TSV import/export for kit tables.
2//!
3//! Format (matches the engine's TSV convention): a header row of column names,
4//! tab-separated cells, `NULL` encoded as an empty field, and `\t \n \r \\`
5//! backslash-escaped. Numbers/bools render as their literal text; arrays and
6//! objects (json/embedding/sparse columns) render as escaped JSON.
7//!
8//! Because `NULL` is the empty field, an empty *string* value round-trips as
9//! `null` — the documented limitation of this format.
10
11use crate::error::Result;
12use crate::schema::Row;
13use mongreldb_kit_core::schema::{ColumnType, Table as KitTable};
14use serde_json::{Map, Value};
15
16fn escape(s: &str) -> String {
17    let mut o = String::with_capacity(s.len());
18    for c in s.chars() {
19        match c {
20            '\\' => o.push_str("\\\\"),
21            '\t' => o.push_str("\\t"),
22            '\n' => o.push_str("\\n"),
23            '\r' => o.push_str("\\r"),
24            _ => o.push(c),
25        }
26    }
27    o
28}
29
30fn unescape(s: &str) -> String {
31    let mut o = String::with_capacity(s.len());
32    let mut it = s.chars();
33    while let Some(c) = it.next() {
34        if c == '\\' {
35            match it.next() {
36                Some('t') => o.push('\t'),
37                Some('n') => o.push('\n'),
38                Some('r') => o.push('\r'),
39                Some('\\') => o.push('\\'),
40                Some(other) => {
41                    o.push('\\');
42                    o.push(other);
43                }
44                None => o.push('\\'),
45            }
46        } else {
47            o.push(c);
48        }
49    }
50    o
51}
52
53fn cell_to_tsv(v: &Value) -> String {
54    match v {
55        Value::Null => String::new(),
56        Value::String(s) => escape(s),
57        Value::Bool(b) => b.to_string(),
58        Value::Number(n) => n.to_string(),
59        other => escape(&serde_json::to_string(other).unwrap_or_default()),
60    }
61}
62
63/// Serialize `rows` (in schema column order) to a TSV document.
64pub fn rows_to_tsv(table: &KitTable, rows: &[Row]) -> String {
65    let cols: Vec<&str> = table.columns.iter().map(|c| c.name.as_str()).collect();
66    let mut out = String::new();
67    out.push_str(&cols.join("\t"));
68    out.push('\n');
69    for row in rows {
70        let line: Vec<String> = cols
71            .iter()
72            .map(|name| cell_to_tsv(row.values.get(*name).unwrap_or(&Value::Null)))
73            .collect();
74        out.push_str(&line.join("\t"));
75        out.push('\n');
76    }
77    out
78}
79
80fn parse_cell(raw: &str, ty: ColumnType) -> Result<Value> {
81    if raw.is_empty() {
82        return Ok(Value::Null);
83    }
84    let text = unescape(raw);
85    let v = match ty {
86        ColumnType::Bool => Value::Bool(text == "true"),
87        ColumnType::Int8
88        | ColumnType::Int16
89        | ColumnType::Int32
90        | ColumnType::Int64
91        | ColumnType::TimestampNanos => match text.parse::<i64>() {
92            Ok(n) => Value::Number(n.into()),
93            Err(_) => Value::String(text),
94        },
95        ColumnType::Float32 | ColumnType::Float64 => match text.parse::<f64>() {
96            Ok(f) => serde_json::Number::from_f64(f)
97                .map(Value::Number)
98                .unwrap_or(Value::Null),
99            Err(_) => Value::String(text),
100        },
101        ColumnType::Text
102        | ColumnType::Date
103        | ColumnType::DateTime
104        | ColumnType::Date64
105        | ColumnType::Time64
106        | ColumnType::Interval
107        | ColumnType::Decimal128
108        | ColumnType::Uuid
109        | ColumnType::JsonNative
110        | ColumnType::Array => Value::String(text),
111        ColumnType::Bytes | ColumnType::Json | ColumnType::Embedding | ColumnType::Sparse => {
112            serde_json::from_str(&text).unwrap_or(Value::String(text))
113        }
114    };
115    Ok(v)
116}
117
118/// Parse a TSV document into rows (maps keyed by the header column names). Only
119/// columns named in the header are set; unknown header columns are ignored.
120pub fn tsv_to_rows(table: &KitTable, text: &str) -> Result<Vec<Map<String, Value>>> {
121    let mut lines = text.split('\n');
122    let header = match lines.next() {
123        Some(h) if !h.is_empty() => h,
124        _ => return Ok(Vec::new()),
125    };
126    let names: Vec<String> = header.split('\t').map(|s| s.to_string()).collect();
127    let types: Vec<Option<ColumnType>> = names
128        .iter()
129        .map(|n| {
130            table
131                .columns
132                .iter()
133                .find(|c| &c.name == n)
134                .map(|c| c.application_type)
135        })
136        .collect();
137
138    let mut rows = Vec::new();
139    for line in lines {
140        if line.is_empty() {
141            continue;
142        }
143        let mut map = Map::new();
144        for (i, field) in line.split('\t').enumerate() {
145            let Some(name) = names.get(i) else { continue };
146            let Some(Some(ty)) = types.get(i) else {
147                continue; // header column not in the schema
148            };
149            map.insert(name.clone(), parse_cell(field, *ty)?);
150        }
151        rows.push(map);
152    }
153    Ok(rows)
154}