1use std::fs;
17
18use crate::error::IoError;
19
20#[non_exhaustive]
24#[derive(Debug, Clone, PartialEq)]
25pub enum DataType {
26 Int32,
28 Int64,
30 Float32,
32 Float64,
34 Utf8,
36 Boolean,
38 Binary,
40 List(Box<DataType>),
42 Struct(Vec<Field>),
44}
45
46#[derive(Debug, Clone, PartialEq)]
48pub struct Field {
49 pub name: String,
51 pub data_type: DataType,
53 pub nullable: bool,
55}
56
57impl Field {
58 pub fn new(name: impl Into<String>, data_type: DataType, nullable: bool) -> Self {
60 Self {
61 name: name.into(),
62 data_type,
63 nullable,
64 }
65 }
66}
67
68#[derive(Debug, Clone, PartialEq)]
70pub struct Schema {
71 pub fields: Vec<Field>,
73}
74
75impl Schema {
76 pub fn new(fields: Vec<Field>) -> Self {
78 Self { fields }
79 }
80
81 pub fn find_field(&self, name: &str) -> Option<&Field> {
83 self.fields.iter().find(|f| f.name == name)
84 }
85
86 pub fn n_fields(&self) -> usize {
88 self.fields.len()
89 }
90}
91
92#[derive(Debug, Clone)]
96pub enum ColumnValues {
97 Int32s(Vec<Option<i32>>),
99 Int64s(Vec<Option<i64>>),
101 Float32s(Vec<Option<f32>>),
103 Float64s(Vec<Option<f64>>),
105 Utf8s(Vec<Option<String>>),
107 Booleans(Vec<Option<bool>>),
109}
110
111impl ColumnValues {
112 pub fn len(&self) -> usize {
114 match self {
115 ColumnValues::Int32s(v) => v.len(),
116 ColumnValues::Int64s(v) => v.len(),
117 ColumnValues::Float32s(v) => v.len(),
118 ColumnValues::Float64s(v) => v.len(),
119 ColumnValues::Utf8s(v) => v.len(),
120 ColumnValues::Booleans(v) => v.len(),
121 }
122 }
123
124 pub fn is_empty(&self) -> bool {
126 self.len() == 0
127 }
128
129 pub fn slice(&self, offset: usize, length: usize) -> ColumnValues {
131 let end = (offset + length).min(self.len());
132 match self {
133 ColumnValues::Int32s(v) => ColumnValues::Int32s(v[offset..end].to_vec()),
134 ColumnValues::Int64s(v) => ColumnValues::Int64s(v[offset..end].to_vec()),
135 ColumnValues::Float32s(v) => ColumnValues::Float32s(v[offset..end].to_vec()),
136 ColumnValues::Float64s(v) => ColumnValues::Float64s(v[offset..end].to_vec()),
137 ColumnValues::Utf8s(v) => ColumnValues::Utf8s(v[offset..end].to_vec()),
138 ColumnValues::Booleans(v) => ColumnValues::Booleans(v[offset..end].to_vec()),
139 }
140 }
141
142 pub fn filter_by_mask(&self, mask: &[bool]) -> ColumnValues {
144 match self {
145 ColumnValues::Int32s(v) => ColumnValues::Int32s(
146 v.iter()
147 .zip(mask)
148 .filter_map(|(val, &m)| if m { Some(val.clone()) } else { None })
149 .collect(),
150 ),
151 ColumnValues::Int64s(v) => ColumnValues::Int64s(
152 v.iter()
153 .zip(mask)
154 .filter_map(|(val, &m)| if m { Some(val.clone()) } else { None })
155 .collect(),
156 ),
157 ColumnValues::Float32s(v) => ColumnValues::Float32s(
158 v.iter()
159 .zip(mask)
160 .filter_map(|(val, &m)| if m { Some(*val) } else { None })
161 .collect(),
162 ),
163 ColumnValues::Float64s(v) => ColumnValues::Float64s(
164 v.iter()
165 .zip(mask)
166 .filter_map(|(val, &m)| if m { Some(*val) } else { None })
167 .collect(),
168 ),
169 ColumnValues::Utf8s(v) => ColumnValues::Utf8s(
170 v.iter()
171 .zip(mask)
172 .filter_map(|(val, &m)| if m { Some(val.clone()) } else { None })
173 .collect(),
174 ),
175 ColumnValues::Booleans(v) => ColumnValues::Booleans(
176 v.iter()
177 .zip(mask)
178 .filter_map(|(val, &m)| if m { Some(*val) } else { None })
179 .collect(),
180 ),
181 }
182 }
183}
184
185#[derive(Debug, Clone)]
187pub struct ColumnBatch {
188 pub name: String,
190 pub data_type: DataType,
192 pub values: ColumnValues,
194}
195
196#[derive(Debug, Clone)]
198pub struct RecordBatch {
199 pub schema: Schema,
201 pub columns: Vec<ColumnBatch>,
203 pub n_rows: usize,
205}
206
207impl RecordBatch {
208 pub fn new(schema: Schema, columns: Vec<ColumnBatch>) -> Result<Self, IoError> {
210 if columns.len() != schema.n_fields() {
212 return Err(IoError::ValidationError(format!(
213 "schema has {} fields but {} columns were provided",
214 schema.n_fields(),
215 columns.len()
216 )));
217 }
218
219 let n_rows = if columns.is_empty() {
221 0
222 } else {
223 columns[0].values.len()
224 };
225 for col in &columns {
226 if col.values.len() != n_rows {
227 return Err(IoError::ValidationError(format!(
228 "column '{}' has {} rows but expected {n_rows}",
229 col.name,
230 col.values.len()
231 )));
232 }
233 }
234
235 Ok(Self {
236 schema,
237 columns,
238 n_rows,
239 })
240 }
241
242 pub fn column(&self, name: &str) -> Option<&ColumnBatch> {
244 self.columns.iter().find(|c| c.name == name)
245 }
246
247 pub fn slice(&self, offset: usize, length: usize) -> Result<RecordBatch, IoError> {
249 if offset > self.n_rows {
250 return Err(IoError::ValidationError(format!(
251 "slice offset {offset} exceeds n_rows {}",
252 self.n_rows
253 )));
254 }
255 let actual_len = length.min(self.n_rows.saturating_sub(offset));
256 let new_columns: Vec<ColumnBatch> = self
257 .columns
258 .iter()
259 .map(|c| ColumnBatch {
260 name: c.name.clone(),
261 data_type: c.data_type.clone(),
262 values: c.values.slice(offset, actual_len),
263 })
264 .collect();
265
266 Ok(RecordBatch {
267 schema: self.schema.clone(),
268 columns: new_columns,
269 n_rows: actual_len,
270 })
271 }
272}
273
274pub trait TableProvider: Send + Sync {
281 fn schema(&self) -> &Schema;
283
284 fn scan(
289 &self,
290 projection: Option<&[usize]>,
291 limit: Option<usize>,
292 ) -> Result<Box<dyn Iterator<Item = Result<RecordBatch, IoError>>>, IoError>;
293
294 fn n_rows_estimate(&self) -> Option<usize> {
296 None
297 }
298}
299
300pub struct InMemoryTable {
304 schema: Schema,
305 batches: Vec<RecordBatch>,
306}
307
308impl InMemoryTable {
309 pub fn new(schema: Schema, batches: Vec<RecordBatch>) -> Self {
311 Self { schema, batches }
312 }
313
314 pub fn total_rows(&self) -> usize {
316 self.batches.iter().map(|b| b.n_rows).sum()
317 }
318}
319
320impl TableProvider for InMemoryTable {
321 fn schema(&self) -> &Schema {
322 &self.schema
323 }
324
325 fn scan(
326 &self,
327 projection: Option<&[usize]>,
328 limit: Option<usize>,
329 ) -> Result<Box<dyn Iterator<Item = Result<RecordBatch, IoError>>>, IoError> {
330 let proj_indices: Option<Vec<usize>> = projection.map(|p| p.to_vec());
331 let mut batches: Vec<Result<RecordBatch, IoError>> = Vec::new();
332 let mut rows_remaining = limit;
333
334 for batch in &self.batches {
335 let batch = if let Some(rem) = rows_remaining {
337 if rem == 0 {
338 break;
339 }
340 let sliced = batch.slice(0, rem)?;
341 rows_remaining = Some(rem.saturating_sub(sliced.n_rows));
342 sliced
343 } else {
344 batch.clone()
345 };
346
347 let batch = if let Some(ref indices) = proj_indices {
349 project_batch_by_indices(&batch, indices)?
350 } else {
351 batch
352 };
353
354 batches.push(Ok(batch));
355 }
356
357 Ok(Box::new(batches.into_iter()))
358 }
359
360 fn n_rows_estimate(&self) -> Option<usize> {
361 Some(self.total_rows())
362 }
363}
364
365pub struct CsvTableProvider {
372 schema: Schema,
373 batch: RecordBatch,
374}
375
376impl CsvTableProvider {
377 pub fn open(path: &str, has_header: bool) -> Result<Self, IoError> {
379 let content = fs::read_to_string(path)
380 .map_err(|e| IoError::FileNotFound(format!("cannot read CSV file {path}: {e}")))?;
381
382 let mut lines = content.lines();
383 let first_line = match lines.next() {
384 Some(l) => l,
385 None => {
386 let schema = Schema::new(vec![]);
388 let batch = RecordBatch {
389 schema: schema.clone(),
390 columns: vec![],
391 n_rows: 0,
392 };
393 return Ok(Self { schema, batch });
394 }
395 };
396
397 let header_fields: Vec<String> = split_csv_line(first_line);
398
399 let column_names: Vec<String> = if has_header {
401 header_fields.clone()
402 } else {
403 (0..header_fields.len())
404 .map(|i| format!("col_{i}"))
405 .collect()
406 };
407
408 let fields: Vec<Field> = column_names
409 .iter()
410 .map(|name| Field::new(name.clone(), DataType::Utf8, true))
411 .collect();
412 let schema = Schema::new(fields);
413
414 let mut row_data: Vec<Vec<Option<String>>> = Vec::new();
416
417 if !has_header {
419 let row: Vec<Option<String>> = header_fields
420 .into_iter()
421 .map(|v| if v.is_empty() { None } else { Some(v) })
422 .collect();
423 row_data.push(row);
424 }
425
426 for line in lines {
427 if line.trim().is_empty() {
428 continue;
429 }
430 let cols = split_csv_line(line);
431 let row: Vec<Option<String>> = cols
432 .into_iter()
433 .map(|v| if v.is_empty() { None } else { Some(v) })
434 .collect();
435 row_data.push(row);
436 }
437
438 let n_cols = schema.n_fields();
439 let n_rows = row_data.len();
440
441 let mut col_vecs: Vec<Vec<Option<String>>> = vec![Vec::with_capacity(n_rows); n_cols];
443 for row in &row_data {
444 for (ci, cell) in row.iter().enumerate() {
445 if ci < n_cols {
446 col_vecs[ci].push(cell.clone());
447 }
448 }
449 for ci in row.len()..n_cols {
451 col_vecs[ci].push(None);
452 }
453 }
454
455 let columns: Vec<ColumnBatch> = schema
456 .fields
457 .iter()
458 .zip(col_vecs)
459 .map(|(field, vals)| ColumnBatch {
460 name: field.name.clone(),
461 data_type: DataType::Utf8,
462 values: ColumnValues::Utf8s(vals),
463 })
464 .collect();
465
466 let batch = RecordBatch::new(schema.clone(), columns)?;
467 Ok(Self { schema, batch })
468 }
469}
470
471impl TableProvider for CsvTableProvider {
472 fn schema(&self) -> &Schema {
473 &self.schema
474 }
475
476 fn scan(
477 &self,
478 projection: Option<&[usize]>,
479 limit: Option<usize>,
480 ) -> Result<Box<dyn Iterator<Item = Result<RecordBatch, IoError>>>, IoError> {
481 let batch = if let Some(lim) = limit {
482 self.batch.slice(0, lim)?
483 } else {
484 self.batch.clone()
485 };
486
487 let batch = if let Some(proj) = projection {
488 project_batch_by_indices(&batch, proj)?
489 } else {
490 batch
491 };
492
493 Ok(Box::new(std::iter::once(Ok(batch))))
494 }
495
496 fn n_rows_estimate(&self) -> Option<usize> {
497 Some(self.batch.n_rows)
498 }
499}
500
501pub fn filter_batch(
507 batch: &RecordBatch,
508 column: &str,
509 predicate: impl Fn(&ColumnValues, usize) -> bool,
510) -> Result<RecordBatch, IoError> {
511 let col = batch
512 .column(column)
513 .ok_or_else(|| IoError::NotFound(format!("column '{column}' not found in batch")))?;
514
515 let mask: Vec<bool> = (0..batch.n_rows)
516 .map(|i| predicate(&col.values, i))
517 .collect();
518
519 let n_rows = mask.iter().filter(|&&m| m).count();
520
521 let new_columns: Vec<ColumnBatch> = batch
522 .columns
523 .iter()
524 .map(|c| ColumnBatch {
525 name: c.name.clone(),
526 data_type: c.data_type.clone(),
527 values: c.values.filter_by_mask(&mask),
528 })
529 .collect();
530
531 Ok(RecordBatch {
532 schema: batch.schema.clone(),
533 columns: new_columns,
534 n_rows,
535 })
536}
537
538pub fn project_batch(batch: &RecordBatch, columns: &[&str]) -> Result<RecordBatch, IoError> {
540 let mut new_fields = Vec::new();
541 let mut new_columns = Vec::new();
542
543 for &name in columns {
544 let field = batch
545 .schema
546 .find_field(name)
547 .ok_or_else(|| IoError::NotFound(format!("projection: column '{name}' not found")))?;
548 let col = batch.column(name).ok_or_else(|| {
549 IoError::NotFound(format!("projection: column data '{name}' not found"))
550 })?;
551 new_fields.push(field.clone());
552 new_columns.push(col.clone());
553 }
554
555 let new_schema = Schema::new(new_fields);
556 Ok(RecordBatch {
557 schema: new_schema,
558 columns: new_columns,
559 n_rows: batch.n_rows,
560 })
561}
562
563pub fn project_batch_by_indices(
565 batch: &RecordBatch,
566 indices: &[usize],
567) -> Result<RecordBatch, IoError> {
568 let mut new_fields = Vec::new();
569 let mut new_columns = Vec::new();
570
571 for &idx in indices {
572 let field = batch.schema.fields.get(idx).ok_or_else(|| {
573 IoError::ValidationError(format!(
574 "projection index {idx} out of range (schema has {} fields)",
575 batch.schema.n_fields()
576 ))
577 })?;
578 let col = batch
579 .columns
580 .get(idx)
581 .ok_or_else(|| IoError::ValidationError(format!("column index {idx} out of range")))?;
582 new_fields.push(field.clone());
583 new_columns.push(col.clone());
584 }
585
586 let new_schema = Schema::new(new_fields);
587 Ok(RecordBatch {
588 schema: new_schema,
589 columns: new_columns,
590 n_rows: batch.n_rows,
591 })
592}
593
594fn split_csv_line(line: &str) -> Vec<String> {
598 let mut fields = Vec::new();
599 let mut current = String::new();
600 let mut in_quotes = false;
601 let mut chars = line.chars().peekable();
602
603 while let Some(ch) = chars.next() {
604 match ch {
605 '"' if !in_quotes => {
606 in_quotes = true;
607 }
608 '"' if in_quotes => {
609 if chars.peek() == Some(&'"') {
611 current.push('"');
612 chars.next();
613 } else {
614 in_quotes = false;
615 }
616 }
617 ',' if !in_quotes => {
618 fields.push(current.trim().to_string());
619 current = String::new();
620 }
621 other => current.push(other),
622 }
623 }
624 fields.push(current.trim().to_string());
625 fields
626}
627
628#[cfg(test)]
631mod tests {
632 use super::*;
633 use std::io::Write;
634
635 fn make_schema() -> Schema {
636 Schema::new(vec![
637 Field::new("id", DataType::Int32, false),
638 Field::new("name", DataType::Utf8, true),
639 Field::new("score", DataType::Float64, true),
640 ])
641 }
642
643 fn make_batch(schema: &Schema, n: usize) -> RecordBatch {
644 let ids: Vec<Option<i32>> = (0..n as i32).map(Some).collect();
645 let names: Vec<Option<String>> = (0..n).map(|i| Some(format!("user_{i}"))).collect();
646 let scores: Vec<Option<f64>> = (0..n).map(|i| Some(i as f64 * 1.5)).collect();
647
648 RecordBatch::new(
649 schema.clone(),
650 vec![
651 ColumnBatch {
652 name: "id".to_string(),
653 data_type: DataType::Int32,
654 values: ColumnValues::Int32s(ids),
655 },
656 ColumnBatch {
657 name: "name".to_string(),
658 data_type: DataType::Utf8,
659 values: ColumnValues::Utf8s(names),
660 },
661 ColumnBatch {
662 name: "score".to_string(),
663 data_type: DataType::Float64,
664 values: ColumnValues::Float64s(scores),
665 },
666 ],
667 )
668 .expect("make_batch")
669 }
670
671 #[test]
674 fn test_schema_find_field() {
675 let schema = make_schema();
676 assert!(schema.find_field("id").is_some());
677 assert!(schema.find_field("name").is_some());
678 assert!(schema.find_field("missing").is_none());
679 assert_eq!(schema.n_fields(), 3);
680 }
681
682 #[test]
685 fn test_record_batch_column_count() {
686 let schema = make_schema();
687 let batch = make_batch(&schema, 10);
688 assert_eq!(batch.n_rows, 10);
689 assert!(batch.column("id").is_some());
690 assert!(batch.column("score").is_some());
691 assert!(batch.column("missing").is_none());
692 }
693
694 #[test]
695 fn test_record_batch_slice() {
696 let schema = make_schema();
697 let batch = make_batch(&schema, 10);
698 let sliced = batch.slice(2, 3).expect("slice");
699 assert_eq!(sliced.n_rows, 3);
700 if let ColumnValues::Int32s(ref ids) = sliced.column("id").unwrap().values {
701 assert_eq!(ids[0], Some(2));
702 assert_eq!(ids[2], Some(4));
703 } else {
704 panic!("expected Int32s");
705 }
706 }
707
708 #[test]
709 fn test_record_batch_slice_beyond_end() {
710 let schema = make_schema();
711 let batch = make_batch(&schema, 5);
712 let sliced = batch.slice(3, 100).expect("slice beyond end");
713 assert_eq!(sliced.n_rows, 2);
714 }
715
716 #[test]
717 fn test_record_batch_new_column_count_mismatch() {
718 let schema = make_schema();
719 let result = RecordBatch::new(
721 schema,
722 vec![
723 ColumnBatch {
724 name: "id".to_string(),
725 data_type: DataType::Int32,
726 values: ColumnValues::Int32s(vec![Some(1)]),
727 },
728 ColumnBatch {
729 name: "name".to_string(),
730 data_type: DataType::Utf8,
731 values: ColumnValues::Utf8s(vec![Some("a".to_string())]),
732 },
733 ],
734 );
735 assert!(result.is_err());
736 }
737
738 #[test]
741 fn test_inmemory_table_scan_all() {
742 let schema = make_schema();
743 let batch1 = make_batch(&schema, 5);
744 let batch2 = make_batch(&schema, 3);
745 let table = InMemoryTable::new(schema, vec![batch1, batch2]);
746
747 let batches: Vec<RecordBatch> = table
748 .scan(None, None)
749 .expect("scan")
750 .map(|r| r.expect("batch"))
751 .collect();
752 assert_eq!(batches.len(), 2);
753 assert_eq!(batches[0].n_rows, 5);
754 assert_eq!(batches[1].n_rows, 3);
755 assert_eq!(table.n_rows_estimate(), Some(8));
756 }
757
758 #[test]
759 fn test_inmemory_table_scan_limit() {
760 let schema = make_schema();
761 let batch = make_batch(&schema, 10);
762 let table = InMemoryTable::new(schema, vec![batch]);
763
764 let batches: Vec<RecordBatch> = table
765 .scan(None, Some(4))
766 .expect("scan")
767 .map(|r| r.expect("batch"))
768 .collect();
769 let total: usize = batches.iter().map(|b| b.n_rows).sum();
770 assert_eq!(total, 4);
771 }
772
773 #[test]
774 fn test_inmemory_table_projection() {
775 let schema = make_schema();
776 let batch = make_batch(&schema, 5);
777 let table = InMemoryTable::new(schema, vec![batch]);
778
779 let batches: Vec<RecordBatch> = table
781 .scan(Some(&[0, 2]), None)
782 .expect("scan")
783 .map(|r| r.expect("batch"))
784 .collect();
785 assert_eq!(batches[0].schema.n_fields(), 2);
786 assert!(batches[0].column("id").is_some());
787 assert!(batches[0].column("score").is_some());
788 assert!(batches[0].column("name").is_none());
789 }
790
791 #[test]
794 fn test_project_batch_by_name() {
795 let schema = make_schema();
796 let batch = make_batch(&schema, 4);
797 let projected = project_batch(&batch, &["id", "score"]).expect("project");
798 assert_eq!(projected.schema.n_fields(), 2);
799 assert_eq!(projected.n_rows, 4);
800 }
801
802 #[test]
803 fn test_project_batch_missing_column() {
804 let schema = make_schema();
805 let batch = make_batch(&schema, 4);
806 let result = project_batch(&batch, &["nonexistent"]);
807 assert!(result.is_err());
808 }
809
810 #[test]
813 fn test_filter_batch_int32() {
814 let schema = make_schema();
815 let batch = make_batch(&schema, 10);
816
817 let filtered = filter_batch(&batch, "id", |vals, row| {
819 if let ColumnValues::Int32s(ref v) = vals {
820 v[row].map_or(false, |id| id >= 5)
821 } else {
822 false
823 }
824 })
825 .expect("filter");
826
827 assert_eq!(filtered.n_rows, 5);
828 }
829
830 #[test]
831 fn test_filter_batch_missing_column() {
832 let schema = make_schema();
833 let batch = make_batch(&schema, 5);
834 let result = filter_batch(&batch, "nonexistent", |_, _| true);
835 assert!(result.is_err());
836 }
837
838 fn write_temp_csv(content: &str) -> tempfile::NamedTempFile {
841 let mut f = tempfile::NamedTempFile::new().expect("tempfile");
842 f.write_all(content.as_bytes()).expect("write csv");
843 f
844 }
845
846 #[test]
847 fn test_csv_provider_with_header() {
848 let csv = "id,name,value\n1,alice,10.5\n2,bob,20.0\n3,carol,30.3\n";
849 let tmp = write_temp_csv(csv);
850 let provider =
851 CsvTableProvider::open(tmp.path().to_str().unwrap(), true).expect("open csv");
852
853 assert_eq!(provider.schema().n_fields(), 3);
854 assert!(provider.schema().find_field("id").is_some());
855 assert!(provider.schema().find_field("name").is_some());
856 assert!(provider.schema().find_field("value").is_some());
857
858 let batches: Vec<RecordBatch> = provider
859 .scan(None, None)
860 .expect("scan")
861 .map(|r| r.expect("batch"))
862 .collect();
863 assert_eq!(batches[0].n_rows, 3);
864 }
865
866 #[test]
867 fn test_csv_provider_without_header() {
868 let csv = "1,alice\n2,bob\n";
869 let tmp = write_temp_csv(csv);
870 let provider = CsvTableProvider::open(tmp.path().to_str().unwrap(), false)
871 .expect("open csv no-header");
872
873 assert_eq!(provider.schema().n_fields(), 2);
874 assert!(provider.schema().find_field("col_0").is_some());
875 assert!(provider.schema().find_field("col_1").is_some());
876
877 let batches: Vec<RecordBatch> = provider
878 .scan(None, None)
879 .expect("scan")
880 .map(|r| r.expect("batch"))
881 .collect();
882 assert_eq!(batches[0].n_rows, 2);
883 }
884
885 #[test]
886 fn test_csv_provider_limit() {
887 let csv = "a,b\n1,2\n3,4\n5,6\n7,8\n";
888 let tmp = write_temp_csv(csv);
889 let provider =
890 CsvTableProvider::open(tmp.path().to_str().unwrap(), true).expect("open csv");
891
892 let batches: Vec<RecordBatch> = provider
893 .scan(None, Some(2))
894 .expect("scan")
895 .map(|r| r.expect("batch"))
896 .collect();
897 let total: usize = batches.iter().map(|b| b.n_rows).sum();
898 assert_eq!(total, 2);
899 }
900
901 #[test]
902 fn test_csv_provider_projection() {
903 let csv = "x,y,z\n1,2,3\n4,5,6\n";
904 let tmp = write_temp_csv(csv);
905 let provider =
906 CsvTableProvider::open(tmp.path().to_str().unwrap(), true).expect("open csv");
907
908 let batches: Vec<RecordBatch> = provider
910 .scan(Some(&[0, 2]), None)
911 .expect("scan")
912 .map(|r| r.expect("batch"))
913 .collect();
914 assert_eq!(batches[0].schema.n_fields(), 2);
915 assert!(batches[0].column("x").is_some());
916 assert!(batches[0].column("z").is_some());
917 assert!(batches[0].column("y").is_none());
918 }
919
920 #[test]
923 fn test_split_csv_quoted() {
924 let line = r#"hello,"world, with comma",42"#;
925 let fields = split_csv_line(line);
926 assert_eq!(fields, vec!["hello", "world, with comma", "42"]);
927 }
928
929 #[test]
930 fn test_split_csv_simple() {
931 let fields = split_csv_line("a,b,c");
932 assert_eq!(fields, vec!["a", "b", "c"]);
933 }
934}