1use std::collections::HashMap;
11
12use serde::{Deserialize, Serialize};
13use serde_json::{Map, Value};
14
15use crate::error::Error;
16use crate::spec::{Column, Data, FieldType, Spec};
17
18pub type Row = Map<String, Value>;
19
20#[derive(Debug, Deserialize)]
23#[serde(untagged)]
24pub enum DataDoc {
25 Envelope {
26 columns: Vec<EnvColumn>,
27 rows: Vec<Vec<Value>>,
28 #[serde(default)]
29 truncated: Option<bool>,
30 #[serde(default)]
31 total_rows: Option<u64>,
32 },
33 Rows(Vec<Row>),
34}
35
36#[derive(Debug, Deserialize)]
38pub struct EnvColumn {
39 pub name: String,
40 #[serde(default, rename = "type")]
41 pub ty: Option<String>,
42}
43
44impl From<&Column> for EnvColumn {
45 fn from(c: &Column) -> Self {
46 EnvColumn {
47 name: c.name.clone(),
48 ty: c.ty.clone(),
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
54#[serde(rename_all = "snake_case")]
55pub enum DataSource {
56 InlineValues,
57 InlineColumns,
58 StdinValues,
59 StdinColumns,
60}
61
62#[derive(Debug, Serialize)]
63pub struct DataProvenance {
64 pub source: DataSource,
65 pub truncated: Option<bool>,
66 pub total_rows: Option<u64>,
67}
68
69#[derive(Debug)]
72pub struct Table {
73 pub rows: Vec<Row>,
74 pub declared: HashMap<String, FieldType>,
75 pub provenance: DataProvenance,
76}
77
78pub fn parse_data_doc(s: &str) -> Result<DataDoc, Error> {
81 serde_json::from_str(s).map_err(|e| {
82 Error::Data(format!(
83 "cannot parse stdin as a data document; expected \
84 {{\"columns\":[{{\"name\",\"type\"?}}...],\"rows\":[[...]...]}} or a JSON array \
85 of row objects: {e}"
86 ))
87 })
88}
89
90pub fn declared_field_type(t: &str) -> FieldType {
99 match t.to_ascii_uppercase().as_str() {
100 "INT64" | "INTEGER" | "INT" | "SMALLINT" | "BIGINT" | "FLOAT64" | "FLOAT" | "DOUBLE"
101 | "NUMERIC" | "BIGNUMERIC" | "DECIMAL" | "REAL" => FieldType::Quantitative,
102 "DATE" | "DATETIME" | "TIMESTAMP" | "TIME" => FieldType::Temporal,
103 _ => FieldType::Nominal,
104 }
105}
106
107pub fn resolve(spec: &Spec, stdin: Option<DataDoc>) -> Result<Table, Error> {
110 match (&spec.data, stdin) {
111 (Some(_), Some(_)) => Err(Error::Spec(
112 "data provided twice: the spec has inline `data` and a data document \
113 arrived on stdin; remove one"
114 .into(),
115 )),
116 (Some(data), None) => resolve_inline(data),
117 (None, Some(doc)) => resolve_stdin(doc),
118 (None, None) => Err(Error::Spec(
119 "no data: the spec has no `data` and nothing arrived on stdin; add \
120 data.values or data.columns+rows to the spec, or pipe a data document"
121 .into(),
122 )),
123 }
124}
125
126fn resolve_inline(data: &Data) -> Result<Table, Error> {
131 match (&data.values, &data.columns, &data.rows) {
132 (Some(values), None, None) => finish(
134 values.clone(),
135 HashMap::new(),
136 DataSource::InlineValues,
137 None,
138 None,
139 ),
140 (None, Some(columns), Some(rows)) => {
141 let env: Vec<EnvColumn> = columns.iter().map(EnvColumn::from).collect();
144 let (rows, declared) = columnar_to_rows(&env, rows)?;
145 finish(rows, declared, DataSource::InlineColumns, None, None)
146 }
147 _ => Err(Error::Spec(
148 "data must contain either `values`, or `columns` and `rows`".into(),
149 )),
150 }
151}
152
153fn resolve_stdin(doc: DataDoc) -> Result<Table, Error> {
155 match doc {
156 DataDoc::Envelope {
157 columns,
158 rows,
159 truncated,
160 total_rows,
161 } => {
162 let (rows, declared) = columnar_to_rows(&columns, &rows)?;
163 finish(
164 rows,
165 declared,
166 DataSource::StdinColumns,
167 truncated,
168 total_rows,
169 )
170 }
171 DataDoc::Rows(rows) => finish(rows, HashMap::new(), DataSource::StdinValues, None, None),
172 }
173}
174
175fn columnar_to_rows(
178 columns: &[EnvColumn],
179 rows: &[Vec<Value>],
180) -> Result<(Vec<Row>, HashMap<String, FieldType>), Error> {
181 let mut declared = HashMap::new();
182 let mut seen = std::collections::HashSet::new();
183 for col in columns {
184 if !seen.insert(col.name.as_str()) {
185 return Err(Error::Data(format!(
186 "duplicate column name \"{}\"",
187 col.name
188 )));
189 }
190 if let Some(ty) = &col.ty {
191 declared.insert(col.name.clone(), declared_field_type(ty));
192 }
193 }
194
195 let want = columns.len();
196 let mut out = Vec::with_capacity(rows.len());
197 for (i, row) in rows.iter().enumerate() {
198 if row.len() != want {
199 return Err(Error::Data(format!(
200 "row {i} has {} values but {want} columns are declared",
201 row.len()
202 )));
203 }
204 let mut obj = Row::new();
205 for (col, val) in columns.iter().zip(row) {
206 obj.insert(col.name.clone(), val.clone());
207 }
208 out.push(obj);
209 }
210 Ok((out, declared))
211}
212
213fn finish(
215 rows: Vec<Row>,
216 declared: HashMap<String, FieldType>,
217 source: DataSource,
218 truncated: Option<bool>,
219 total_rows: Option<u64>,
220) -> Result<Table, Error> {
221 if rows.is_empty() {
222 return Err(Error::Data(
223 "data has no rows; provide at least one row".into(),
224 ));
225 }
226 Ok(Table {
227 rows,
228 declared,
229 provenance: DataProvenance {
230 source,
231 truncated,
232 total_rows,
233 },
234 })
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use serde_json::json;
241
242 fn spec_with_values() -> Spec {
243 serde_json::from_str(
244 r#"{"data":{"values":[{"x":1}]},"mark":"bar",
245 "encoding":{"x":{"field":"x"},"y":{"field":"y"}}}"#,
246 )
247 .expect("fixture spec parses")
248 }
249
250 fn spec_with_empty_values() -> Spec {
251 serde_json::from_str(
252 r#"{"data":{"values":[]},"mark":"bar",
253 "encoding":{"x":{"field":"x"},"y":{"field":"y"}}}"#,
254 )
255 .expect("fixture spec parses")
256 }
257
258 #[test]
259 fn parses_bare_row_array() {
260 let doc = parse_data_doc(r#"[{"a": 1}, {"a": 2}]"#).expect("bare array parses");
261 match doc {
262 DataDoc::Rows(rows) => assert_eq!(rows.len(), 2),
263 other => panic!("expected Rows, got {other:?}"),
264 }
265 }
266
267 #[test]
268 fn envelope_tolerates_unknown_keys() {
269 let doc = parse_data_doc(
271 r#"{"columns":[{"name":"day","type":"STRING"},{"name":"n","type":"INT64"}],
272 "rows":[["mon",32]],
273 "query":{"job_id":"abc","note":"ignored"}}"#,
274 )
275 .expect("envelope with extra keys parses");
276 match doc {
277 DataDoc::Envelope { columns, rows, .. } => {
278 assert_eq!(columns.len(), 2);
279 assert_eq!(rows.len(), 1);
280 }
281 other => panic!("expected Envelope, got {other:?}"),
282 }
283 }
284
285 #[test]
286 fn envelope_truncated_and_total_rows_reach_provenance() {
287 let doc = parse_data_doc(
288 r#"{"columns":[{"name":"n"}],"rows":[[1]],"truncated":true,"total_rows":123}"#,
289 )
290 .expect("envelope parses");
291 let table = resolve_stdin(doc).expect("resolves");
292 assert_eq!(table.provenance.source, DataSource::StdinColumns);
293 assert_eq!(table.provenance.truncated, Some(true));
294 assert_eq!(table.provenance.total_rows, Some(123));
295 }
296
297 #[test]
298 fn bare_rows_provenance_has_no_envelope_fields() {
299 let doc = parse_data_doc(r#"[{"a":1}]"#).expect("parses");
300 let table = resolve_stdin(doc).expect("resolves");
301 assert_eq!(table.provenance.source, DataSource::StdinValues);
302 assert_eq!(table.provenance.truncated, None);
303 assert_eq!(table.provenance.total_rows, None);
304 assert!(table.declared.is_empty());
305 }
306
307 #[test]
308 fn columnar_zips_rows_in_column_order() {
309 let doc = parse_data_doc(
310 r#"{"columns":[{"name":"day","type":"STRING"},{"name":"n","type":"INT64"}],
311 "rows":[["mon",32],["tue",78]]}"#,
312 )
313 .expect("parses");
314 let table = resolve_stdin(doc).expect("resolves");
315 assert_eq!(table.rows.len(), 2);
316 assert_eq!(table.rows[0].get("day"), Some(&json!("mon")));
317 assert_eq!(table.rows[0].get("n"), Some(&json!(32)));
318 assert_eq!(table.rows[1].get("day"), Some(&json!("tue")));
319 assert_eq!(table.rows[1].get("n"), Some(&json!(78)));
320 let keys: Vec<&String> = table.rows[0].keys().collect();
322 assert_eq!(keys, vec!["day", "n"]);
323 assert_eq!(table.declared.get("day"), Some(&FieldType::Nominal));
324 assert_eq!(table.declared.get("n"), Some(&FieldType::Quantitative));
325 }
326
327 #[test]
328 fn duplicate_column_name_errors() {
329 let doc = parse_data_doc(r#"{"columns":[{"name":"a"},{"name":"a"}],"rows":[[1,2]]}"#)
330 .expect("parses");
331 let err = resolve_stdin(doc).expect_err("duplicate must error");
332 insta::assert_snapshot!(err.to_string(), @r###"duplicate column name "a""###);
333 }
334
335 #[test]
336 fn row_length_mismatch_errors() {
337 let doc =
338 parse_data_doc(r#"{"columns":[{"name":"a"},{"name":"b"}],"rows":[[1,2],[1,2,3]]}"#)
339 .expect("parses");
340 let err = resolve_stdin(doc).expect_err("length mismatch must error");
341 insta::assert_snapshot!(
342 err.to_string(),
343 @"row 1 has 3 values but 2 columns are declared"
344 );
345 }
346
347 #[test]
348 fn empty_rows_errors_columnar() {
349 let doc = parse_data_doc(r#"{"columns":[{"name":"a"}],"rows":[]}"#).expect("parses");
350 let err = resolve_stdin(doc).expect_err("empty rows must error");
351 insta::assert_snapshot!(err.to_string(), @"data has no rows; provide at least one row");
352 }
353
354 #[test]
355 fn empty_rows_errors_bare_array() {
356 let doc = parse_data_doc(r#"[]"#).expect("parses");
357 let err = resolve_stdin(doc).expect_err("empty rows must error");
358 insta::assert_snapshot!(err.to_string(), @"data has no rows; provide at least one row");
359 }
360
361 #[test]
362 fn empty_rows_errors_inline_values() {
363 let err = resolve(&spec_with_empty_values(), None).expect_err("empty inline must error");
364 insta::assert_snapshot!(err.to_string(), @"data has no rows; provide at least one row");
365 }
366
367 #[test]
368 fn inline_values_resolve_to_inline_provenance() {
369 let table = resolve(&spec_with_values(), None).expect("resolves");
370 assert_eq!(table.provenance.source, DataSource::InlineValues);
371 assert_eq!(table.provenance.truncated, None);
372 assert_eq!(table.provenance.total_rows, None);
373 assert!(table.declared.is_empty());
374 assert_eq!(table.rows.len(), 1);
375 }
376
377 #[test]
378 fn data_provided_twice_errors() {
379 let doc = parse_data_doc(r#"[{"a":1}]"#).expect("parses");
380 let err = resolve(&spec_with_values(), Some(doc)).expect_err("data twice must error");
381 insta::assert_snapshot!(
382 err.to_string(),
383 @"data provided twice: the spec has inline `data` and a data document arrived on stdin; remove one"
384 );
385 }
386
387 #[test]
388 fn inline_columnar_resolves_with_declared_types() {
389 let spec: Spec = serde_json::from_str(
390 r#"{"data":{"columns":[{"name":"day","type":"STRING"},{"name":"n","type":"INT64"}],
391 "rows":[["mon",32],["tue",78]]},
392 "mark":"bar","encoding":{"x":{"field":"day"},"y":{"field":"n"}}}"#,
393 )
394 .expect("inline columnar spec parses");
395 let table = resolve(&spec, None).expect("resolves");
396 assert_eq!(table.provenance.source, DataSource::InlineColumns);
397 assert_eq!(table.rows.len(), 2);
398 assert_eq!(table.rows[0].get("day"), Some(&json!("mon")));
399 assert_eq!(table.rows[0].get("n"), Some(&json!(32)));
400 assert_eq!(table.declared.get("day"), Some(&FieldType::Nominal));
401 assert_eq!(table.declared.get("n"), Some(&FieldType::Quantitative));
402 }
403
404 #[test]
405 fn inline_data_both_forms_errors() {
406 let spec: Spec = serde_json::from_str(
407 r#"{"data":{"values":[{"a":1}],"columns":[{"name":"a"}],"rows":[[1]]},
408 "mark":"bar","encoding":{"x":{"field":"a"},"y":{"field":"a"}}}"#,
409 )
410 .expect("spec parses");
411 let err = resolve(&spec, None).expect_err("both forms must error");
412 insta::assert_snapshot!(
413 err.to_string(),
414 @"data must contain either `values`, or `columns` and `rows`"
415 );
416 }
417
418 #[test]
419 fn no_data_anywhere_errors() {
420 let spec: Spec = serde_json::from_str(
421 r#"{"mark":"bar","encoding":{"x":{"field":"a"},"y":{"field":"b"}}}"#,
422 )
423 .expect("spec without data parses");
424 let err = resolve(&spec, None).expect_err("no data must error");
425 insta::assert_snapshot!(
426 err.to_string(),
427 @"no data: the spec has no `data` and nothing arrived on stdin; add data.values or data.columns+rows to the spec, or pipe a data document"
428 );
429 }
430
431 #[test]
432 fn declared_field_type_mapping() {
433 for t in [
435 "INT64",
436 "INTEGER",
437 "INT",
438 "SMALLINT",
439 "BIGINT",
440 "FLOAT64",
441 "FLOAT",
442 "DOUBLE",
443 "NUMERIC",
444 "BIGNUMERIC",
445 "DECIMAL",
446 "REAL",
447 ] {
448 assert_eq!(declared_field_type(t), FieldType::Quantitative, "{t}");
449 }
450 for t in ["DATE", "DATETIME", "TIMESTAMP", "TIME"] {
452 assert_eq!(declared_field_type(t), FieldType::Temporal, "{t}");
453 }
454 assert_eq!(declared_field_type("STRING"), FieldType::Nominal);
456 assert_eq!(declared_field_type("BOOL"), FieldType::Nominal);
457 assert_eq!(declared_field_type("whatever"), FieldType::Nominal);
458 assert_eq!(declared_field_type("int64"), FieldType::Quantitative);
460 assert_eq!(declared_field_type("Date"), FieldType::Temporal);
461 assert_eq!(declared_field_type("String"), FieldType::Nominal);
462 }
463}