1use std::sync::Arc;
32
33use api::helper::vectors_to_rows;
34use arrow::csv::ReaderBuilder;
35use arrow::csv::reader::Decoder as CsvDecoder;
36use arrow_schema::{ArrowError, DataType as ArrowDataType, Field, Schema as ArrowSchema};
37use datatypes::data_type::DataType;
38use datatypes::prelude::ConcreteDataType;
39use datatypes::schema::{ColumnSchema, Schema as GreptimeSchema};
40use datatypes::vectors::{BooleanVector, Helper, StringVector, Vector};
41use pgwire::error::{PgWireError, PgWireResult};
42
43use crate::postgres::copy_in::codec::{CopyFormat, ParsedRow, RecordParser};
44use crate::postgres::copy_in::options::CopyOptionSet;
45use crate::postgres::copy_in::{COPY_IN_FLUSH_ROWS, bad_copy_data, invalid_text_repr, unsupported};
46
47#[derive(Debug, Clone)]
49pub(super) struct CsvCodec {
50 delimiter: u8,
51 quote: u8,
52 escape: u8,
53 header: bool,
54}
55
56pub(super) fn build_csv_codec(mut options: CopyOptionSet) -> PgWireResult<CopyFormat> {
58 let delimiter = options.take_single_byte("DELIMITER")?.unwrap_or(b',');
59 let quote = options.take_single_byte("QUOTE")?.unwrap_or(b'"');
60 let escape = options.take_single_byte("ESCAPE")?.unwrap_or(quote);
63 if let Some(null) = options.take_string("NULL")?
67 && !null.is_empty()
68 {
69 return Err(unsupported(
70 "custom NULL string is not supported by format csv; an empty field is NULL",
71 ));
72 }
73 let header = options.take_flag("HEADER")?.unwrap_or(false);
74 options.reject_leftovers("csv")?;
75 if delimiter == quote {
76 return Err(bad_copy_data("DELIMITER must not be equal to QUOTE"));
77 }
78 Ok(CopyFormat::Csv(CsvCodec {
79 delimiter,
80 quote,
81 escape,
82 header,
83 }))
84}
85
86impl CsvCodec {
87 pub(super) fn check_column_type(&self, data_type: &ConcreteDataType) -> PgWireResult<()> {
89 let supported = matches!(
90 data_type,
91 ConcreteDataType::Boolean(_)
92 | ConcreteDataType::Int8(_)
93 | ConcreteDataType::Int16(_)
94 | ConcreteDataType::Int32(_)
95 | ConcreteDataType::Int64(_)
96 | ConcreteDataType::UInt8(_)
97 | ConcreteDataType::UInt16(_)
98 | ConcreteDataType::UInt32(_)
99 | ConcreteDataType::UInt64(_)
100 | ConcreteDataType::Float32(_)
101 | ConcreteDataType::Float64(_)
102 | ConcreteDataType::String(_)
103 | ConcreteDataType::Date(_)
104 | ConcreteDataType::Timestamp(_)
105 | ConcreteDataType::Decimal128(_)
106 );
107 if !supported {
108 return Err(unsupported(format!(
109 "column type \"{}\" is not supported by format csv",
110 data_type.name()
111 )));
112 }
113 Ok(())
114 }
115
116 pub(super) fn create_parser(
117 &self,
118 columns: &[ColumnSchema],
119 timezone: &str,
120 ) -> PgWireResult<Box<dyn RecordParser + Send>> {
121 let boolean_columns: Vec<usize> = columns
131 .iter()
132 .enumerate()
133 .filter(|(_, c)| matches!(c.data_type, ConcreteDataType::Boolean(_)))
134 .map(|(i, _)| i)
135 .collect();
136 let arrow_schema = GreptimeSchema::try_new(columns.to_vec())
137 .map_err(|e| unsupported(format!("invalid columns for format csv: {e}")))?
138 .arrow_schema()
139 .clone();
140 let timezone = timezone.to_string();
141 let fields = arrow_schema
142 .fields()
143 .iter()
144 .zip(columns)
145 .map(|(field, column)| match &column.data_type {
146 ConcreteDataType::Boolean(_) => {
147 Field::new(field.name(), ArrowDataType::Utf8, field.is_nullable())
148 }
149 ConcreteDataType::Timestamp(_) => match field.data_type() {
150 ArrowDataType::Timestamp(unit, None) => Field::new(
151 field.name(),
152 ArrowDataType::Timestamp(*unit, Some(Arc::from(timezone.as_str()))),
153 field.is_nullable(),
154 ),
155 _ => field.as_ref().clone(),
156 },
157 _ => field.as_ref().clone(),
158 })
159 .collect::<Vec<_>>();
160 let mut builder = ReaderBuilder::new(Arc::new(ArrowSchema::new(fields)))
161 .with_header(self.header)
162 .with_delimiter(self.delimiter)
163 .with_quote(self.quote)
164 .with_batch_size(COPY_IN_FLUSH_ROWS);
165 if self.escape != self.quote {
168 builder = builder.with_escape(self.escape);
169 }
170 Ok(Box::new(CsvRecordParser {
171 decoder: builder.build_decoder(),
172 boolean_columns,
173 }))
174 }
175}
176
177struct CsvRecordParser {
179 decoder: CsvDecoder,
180 boolean_columns: Vec<usize>,
183}
184
185impl RecordParser for CsvRecordParser {
186 fn feed(&mut self, data: &[u8]) -> PgWireResult<Vec<ParsedRow>> {
187 let mut buf = data;
188 let mut rows = Vec::new();
189 while !buf.is_empty() {
190 let consumed = self.decoder.decode(buf).map_err(decode_error)?;
191 if consumed == 0 {
192 while let Some(batch) = self.decoder.flush().map_err(decode_error)? {
193 rows.extend(self.flush_batch(&batch)?);
194 }
195 } else {
196 buf = &buf[consumed..];
197 }
198 }
199 Ok(rows)
200 }
201
202 fn finish(&mut self) -> PgWireResult<Vec<ParsedRow>> {
203 self.decoder.decode(&[]).map_err(decode_error)?;
206 let mut rows = Vec::new();
207 while let Some(batch) = self.decoder.flush().map_err(decode_error)? {
208 rows.extend(self.flush_batch(&batch)?);
209 }
210 Ok(rows)
211 }
212}
213
214impl CsvRecordParser {
215 fn flush_batch(
216 &mut self,
217 batch: &arrow::record_batch::RecordBatch,
218 ) -> PgWireResult<Vec<ParsedRow>> {
219 let mut vectors = Helper::try_into_vectors(batch.columns())
220 .map_err(|e| bad_copy_data(format!("failed to convert decoded CSV batch: {e}")))?;
221 for &index in &self.boolean_columns {
222 let Some(vector) = vectors[index].as_any().downcast_ref::<StringVector>() else {
223 return Err(bad_copy_data(
224 "expected boolean column to decode as a string vector",
225 ));
226 };
227 let mut values = Vec::with_capacity(batch.num_rows());
228 for row_index in 0..batch.num_rows() {
229 values.push(match vector.get(row_index) {
230 datatypes::value::Value::Null => None,
231 datatypes::value::Value::String(s) => Some(parse_pg_bool(s.as_utf8())?),
232 other => {
233 return Err(bad_copy_data(format!(
234 "expected string values in a boolean column, got {other:?}"
235 )));
236 }
237 });
238 }
239 vectors[index] = Arc::new(BooleanVector::from(values));
240 }
241 Ok(vectors_to_rows(vectors.iter(), batch.num_rows()))
242 }
243}
244
245fn parse_pg_bool(input: &str) -> PgWireResult<bool> {
251 match input.trim().to_ascii_lowercase().as_str() {
252 "true" | "t" | "yes" | "y" | "on" | "1" => Ok(true),
253 "false" | "f" | "no" | "n" | "off" | "0" => Ok(false),
254 _ => Err(invalid_text_repr(format!(
255 "invalid input syntax for type boolean: \"{input}\""
256 ))),
257 }
258}
259
260fn decode_error(error: ArrowError) -> PgWireError {
261 match &error {
262 ArrowError::CsvError(message) => {
263 bad_copy_data(format!("failed to parse CSV data: {message}"))
264 }
265 ArrowError::ParseError(message) | ArrowError::CastError(message) => {
266 invalid_text_repr(format!("failed to parse CSV data: {message}"))
267 }
268 _ => bad_copy_data(format!("failed to parse CSV data: {error}")),
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use datatypes::schema::ColumnSchema;
275
276 use super::*;
277 use crate::postgres::copy_in::options::CopyOptionValue;
278
279 fn test_columns() -> Vec<ColumnSchema> {
280 vec![
281 ColumnSchema::new("host", ConcreteDataType::string_datatype(), true),
282 ColumnSchema::new("val", ConcreteDataType::float64_datatype(), true),
283 ColumnSchema::new(
284 "ts",
285 ConcreteDataType::timestamp_millisecond_datatype(),
286 false,
287 ),
288 ]
289 }
290
291 fn build_codec(set: CopyOptionSet) -> CsvCodec {
292 match build_csv_codec(set).unwrap() {
293 CopyFormat::Csv(codec) => codec,
294 other => panic!("expected csv format, got {other:?}"),
295 }
296 }
297
298 fn parser() -> Box<dyn RecordParser + Send> {
299 build_codec(CopyOptionSet::default())
300 .create_parser(&test_columns(), "UTC")
301 .unwrap()
302 }
303
304 fn string_at(row: &ParsedRow, index: usize) -> Option<String> {
305 row.values[index].value_data.as_ref().map(|v| match v {
306 api::v1::value::ValueData::StringValue(s) => s.clone(),
307 other => panic!("expected string, got {other:?}"),
308 })
309 }
310
311 fn rows_with(mut set: CopyOptionSet) -> Box<dyn RecordParser + Send> {
312 set.take_format().unwrap();
313 build_codec(set)
314 .create_parser(&test_columns(), "UTC")
315 .unwrap()
316 }
317
318 fn feed_all(parser: &mut Box<dyn RecordParser + Send>, chunks: &[&[u8]]) -> Vec<ParsedRow> {
323 let mut rows = Vec::new();
324 for chunk in chunks {
325 rows.extend(parser.feed(chunk).unwrap());
326 }
327 rows.extend(parser.finish().unwrap());
328 rows
329 }
330
331 #[test]
332 fn test_build_defaults() {
333 let codec = build_codec(CopyOptionSet::default());
334 assert!(matches!(codec, CsvCodec { .. }));
335 }
336
337 #[test]
338 fn test_build_rejects_bad_options() {
339 let mut set = CopyOptionSet::default();
340 set.insert("delimiter".into(), CopyOptionValue::String("\"".into()))
341 .unwrap();
342 let err = build_csv_codec(set).unwrap_err();
343 assert!(
344 err.to_string()
345 .contains("DELIMITER must not be equal to QUOTE")
346 );
347
348 let mut set = CopyOptionSet::default();
349 set.insert("null".into(), CopyOptionValue::String("NULL".into()))
350 .unwrap();
351 assert!(
352 build_csv_codec(set)
353 .unwrap_err()
354 .to_string()
355 .contains("custom NULL string")
356 );
357
358 let mut set = CopyOptionSet::default();
359 set.take_format().unwrap();
360 set.insert("precision".into(), CopyOptionValue::Ident("ns".into()))
361 .unwrap();
362 assert!(
363 build_csv_codec(set)
364 .unwrap_err()
365 .to_string()
366 .contains("not supported by format csv")
367 );
368 }
369
370 #[test]
371 fn test_check_column_type() {
372 let codec = build_codec(CopyOptionSet::default());
373 assert!(
374 codec
375 .check_column_type(&ConcreteDataType::json_datatype())
376 .is_err()
377 );
378 assert!(
379 codec
380 .check_column_type(&ConcreteDataType::binary_datatype())
381 .is_err()
382 );
383 assert!(
384 codec
385 .check_column_type(&ConcreteDataType::string_datatype())
386 .is_ok()
387 );
388 }
389
390 #[test]
391 fn test_csv_parse_records() {
392 let mut parser = parser();
393 let rows = feed_all(
394 &mut parser,
395 &[b"host1,1.5,2023-11-14 22:13:20\n\
396 ,2.5,2023-11-14 22:13:21\n\
397 \"quoted, comma\",3.5,2023-11-14 22:13:22\n"],
398 );
399 assert_eq!(rows.len(), 3);
400 assert_eq!(string_at(&rows[0], 0), Some("host1".into()));
401 assert_eq!(string_at(&rows[1], 0), None);
403 assert_eq!(string_at(&rows[2], 0), Some("quoted, comma".into()));
404 }
405
406 #[test]
407 fn test_csv_escaped_quotes() {
408 let mut parser = parser();
409 let rows = feed_all(
410 &mut parser,
411 &[b"\"say \"\"hi\"\"\",1.0,2023-11-14 22:13:20\n"],
412 );
413 assert_eq!(string_at(&rows[0], 0), Some("say \"hi\"".into()));
414 }
415
416 #[test]
417 fn test_csv_custom_escape() {
418 let mut set = CopyOptionSet::default();
419 set.insert("escape".into(), CopyOptionValue::String("\\".into()))
420 .unwrap();
421 let mut parser = rows_with(set);
422 let rows = feed_all(&mut parser, &[b"\"a\\\"b\",1.0,2023-11-14 22:13:20\n"]);
423 assert_eq!(string_at(&rows[0], 0), Some("a\"b".into()));
424 }
425
426 #[test]
427 fn test_csv_quoted_newline_and_chunk_boundaries() {
428 let mut parser = parser();
429 assert!(parser.feed(b"\"multi").unwrap().is_empty());
433 assert!(
434 parser
435 .feed(b"line\nfield\",1.0,2023-11-14 22:13:20")
436 .unwrap()
437 .is_empty()
438 );
439 assert!(
440 parser
441 .feed(b"\r\nhost2,2.0,2023-11-14 22:13:21\n")
442 .unwrap()
443 .is_empty()
444 );
445 let rows = parser.finish().unwrap();
446 assert_eq!(rows.len(), 2);
447 assert_eq!(string_at(&rows[0], 0), Some("multiline\nfield".into()));
448 assert_eq!(string_at(&rows[1], 0), Some("host2".into()));
449 }
450
451 #[test]
452 fn test_csv_header() {
453 let mut set = CopyOptionSet::default();
454 set.insert("header".into(), CopyOptionValue::Flag).unwrap();
455 let mut parser = rows_with(set);
456 let rows = feed_all(
457 &mut parser,
458 &[b"host,val,ts\nhost1,1.0,2023-11-14 22:13:20\n"],
459 );
460 assert_eq!(rows.len(), 1);
461 assert_eq!(string_at(&rows[0], 0), Some("host1".into()));
462 }
463
464 #[test]
465 fn test_csv_trailing_record_without_newline() {
466 let mut parser = parser();
467 let fed = parser
468 .feed(b"host1,1.0,2023-11-14 22:13:20\nhost2,2.0,2023-11-14 22:13:21")
469 .unwrap();
470 assert!(fed.is_empty());
473 let rows = parser.finish().unwrap();
474 assert_eq!(rows.len(), 2);
475 assert_eq!(string_at(&rows[0], 0), Some("host1".into()));
476 assert_eq!(string_at(&rows[1], 0), Some("host2".into()));
477 }
478
479 #[test]
480 fn test_csv_finish_at_record_boundary() {
481 let mut parser = parser();
482 assert!(
485 parser
486 .feed(b"host1,1.0,2023-11-14 22:13:20\n")
487 .unwrap()
488 .is_empty()
489 );
490 let rows = parser.finish().unwrap();
491 assert_eq!(rows.len(), 1);
492 assert_eq!(string_at(&rows[0], 0), Some("host1".into()));
493 }
494
495 #[test]
502 fn test_csv_chunked_feeds_are_alignment_safe() {
503 let sweep_columns = vec![
505 ColumnSchema::new("i", ConcreteDataType::int32_datatype(), true),
506 ColumnSchema::new(
507 "ts",
508 ConcreteDataType::timestamp_millisecond_datatype(),
509 false,
510 ),
511 ];
512 let total = 2 * COPY_IN_FLUSH_ROWS + 137;
515 let payload: Vec<u8> = (0..total)
516 .map(|i| {
517 format!(
518 "{:06},2024-01-01 00:00:{:02}.{:03}\n",
519 i,
520 (i / 1000) % 60,
521 i % 1000
522 )
523 })
524 .collect::<String>()
525 .into_bytes();
526
527 let check = |chunk: usize| {
528 let mut parser = build_codec(CopyOptionSet::default())
529 .create_parser(&sweep_columns, "UTC")
530 .unwrap();
531 let mut ids: Vec<i32> = Vec::new();
532 for c in payload.chunks(chunk) {
533 for row in parser.feed(c).unwrap() {
534 ids.push(match row.values[0].value_data.as_ref().unwrap() {
535 api::v1::value::ValueData::I32Value(v) => *v,
536 other => panic!("expected i32, got {other:?}"),
537 });
538 }
539 }
540 for row in parser.finish().unwrap() {
541 ids.push(match row.values[0].value_data.as_ref().unwrap() {
542 api::v1::value::ValueData::I32Value(v) => *v,
543 other => panic!("expected i32, got {other:?}"),
544 });
545 }
546 assert_eq!(ids.len(), total, "chunk={chunk}");
547 for (i, v) in ids.iter().enumerate() {
548 assert_eq!(*v, i as i32, "chunk={chunk} row {i} corrupted");
549 }
550 };
551
552 for chunk in [1usize, 2, 3, 7, 8, 13, 31, 64, 97, 128, 1024] {
555 check(chunk);
556 }
557 for chunk in [4096usize, 8192, 65531] {
560 check(chunk);
561 let mut shifted = payload.clone();
562 shifted.drain(..37);
563 let mut parser = build_codec(CopyOptionSet::default())
564 .create_parser(&sweep_columns, "UTC")
565 .unwrap();
566 let mut count = 0usize;
567 for c in shifted.chunks(chunk) {
568 count += parser.feed(c).unwrap().len();
569 }
570 count += parser.finish().unwrap().len();
571 assert_eq!(count, total - 1, "chunk={chunk} shifted");
572 }
573 }
574
575 #[test]
576 fn test_csv_boolean_spellings() {
577 let columns = vec![
580 ColumnSchema::new("flag", ConcreteDataType::boolean_datatype(), true),
581 ColumnSchema::new(
582 "ts",
583 ConcreteDataType::timestamp_millisecond_datatype(),
584 false,
585 ),
586 ];
587 let mut parser = build_codec(CopyOptionSet::default())
588 .create_parser(&columns, "UTC")
589 .unwrap();
590 let rows = feed_all(
591 &mut parser,
592 &[b"t,2024-01-01 00:00:00\n\
593 f,2024-01-01 00:00:01\n\
594 TRUE,2024-01-01 00:00:02\n\
595 no,2024-01-01 00:00:03\n\
596 on,2024-01-01 00:00:04\n\
597 0,2024-01-01 00:00:05\n\
598 ,2024-01-01 00:00:06\n"],
599 );
600 let expected = [
601 Some(true),
602 Some(false),
603 Some(true),
604 Some(false),
605 Some(true),
606 Some(false),
607 None,
608 ];
609 for (row, want) in rows.iter().zip(expected) {
610 assert_eq!(
611 row.values[0].value_data,
612 want.map(api::v1::value::ValueData::BoolValue),
613 "{:?}",
614 row.values[0].value_data
615 );
616 }
617 }
618
619 #[test]
620 fn test_csv_boolean_rejects_garbage() {
621 let columns = vec![
622 ColumnSchema::new("flag", ConcreteDataType::boolean_datatype(), true),
623 ColumnSchema::new(
624 "ts",
625 ConcreteDataType::timestamp_millisecond_datatype(),
626 false,
627 ),
628 ];
629 let mut parser = build_codec(CopyOptionSet::default())
630 .create_parser(&columns, "UTC")
631 .unwrap();
632 parser.feed(b"maybe,2024-01-01 00:00:00\n").unwrap();
633 let err = match parser.finish() {
634 Ok(_) => panic!("expected parse error"),
635 Err(e) => e,
636 };
637 assert!(
638 err.to_string()
639 .contains("invalid input syntax for type boolean"),
640 "{err}"
641 );
642 }
643
644 #[test]
645 fn test_csv_timestamp_session_timezone() {
646 let columns = vec![
649 ColumnSchema::new("host", ConcreteDataType::string_datatype(), true),
650 ColumnSchema::new(
651 "ts",
652 ConcreteDataType::timestamp_millisecond_datatype(),
653 false,
654 ),
655 ];
656 let mut parser = build_codec(CopyOptionSet::default())
657 .create_parser(&columns, "Asia/Shanghai")
658 .unwrap();
659 let rows = feed_all(
660 &mut parser,
661 &[b"host1,2024-01-01 00:00:00\n\
662 host2,2024-01-01T00:00:00+08:00\n\
663 host3,2024-01-01T00:00:00Z\n"],
664 );
665 let millis = |row: &ParsedRow| match row.values[1].value_data.as_ref().unwrap() {
666 api::v1::value::ValueData::TimestampMillisecondValue(v) => *v,
667 other => panic!("expected ts, got {other:?}"),
668 };
669 assert_eq!(millis(&rows[0]), 1704038400000);
671 assert_eq!(millis(&rows[1]), 1704038400000);
673 assert_eq!(millis(&rows[2]), 1704067200000);
675 }
676
677 #[test]
678 fn test_csv_errors() {
679 let mut p = parser();
680 let err = match p.feed(b"host1,1.0\n") {
681 Ok(_) => panic!("expected parse error"),
682 Err(e) => e,
683 };
684 assert!(
685 err.to_string().contains("incorrect number of fields"),
686 "{err}"
687 );
688
689 let mut p = parser();
692 p.feed(b"host1,not_a_double,2023-11-14 22:13:20\n").unwrap();
693 let err = match p.finish() {
694 Ok(_) => panic!("expected parse error"),
695 Err(e) => e,
696 };
697 assert!(err.to_string().contains("failed to parse"), "{err}");
698 }
699}