1mod codec;
35mod csv;
36mod influxdb;
37mod options;
38mod parse;
39
40use std::fmt::Debug;
41use std::sync::Arc;
42
43use crate::influxdb::InfluxdbRequest;
44use api::helper::ColumnDataTypeWrapper;
45use api::v1::column_def::options_from_column_schema;
46use api::v1::{
47 Row as GrpcRow, RowInsertRequest, RowInsertRequests, Rows as GrpcRows, SemanticType,
48};
49use async_trait::async_trait;
50use codec::{CopyFormat, RecordParser};
51use common_grpc::precision::Precision;
52use common_query::OutputData;
53use common_telemetry::{info, warn};
54use csv::CsvCodec;
55use datafusion::sql::sqlparser::ast::{CopyTarget, Statement as SqlParserStatement};
56use datatypes::data_type::DataType;
57use datatypes::schema::{ColumnSchema, SchemaRef};
58use futures::{Sink, SinkExt, stream};
59use influxdb::InfluxdbOptions;
60use pgwire::api::copy::CopyHandler;
61use pgwire::api::results::{CopyResponse, Response, Tag};
62use pgwire::api::{ClientInfo, PgWireConnectionState};
63use pgwire::error::{ErrorInfo, PgWireError, PgWireResult};
64use pgwire::messages::PgWireBackendMessage;
65use pgwire::messages::copy::{CopyData, CopyDone, CopyFail};
66use session::context::QueryContextRef;
67use table::metadata::TableInfo;
68
69use crate::metrics::METRIC_POSTGRES_COPY_IN_ROWS;
70use crate::postgres::PostgresServerHandlerInner;
71use crate::postgres::types::PgErrorCode;
72use crate::postgres::utils::convert_err;
73
74pub(crate) const COPY_IN_FLUSH_ROWS: usize = 32768;
86
87pub(crate) const COPY_IN_MAX_PAYLOAD_BYTES: usize = 64 * 1024 * 1024;
93
94type FormatBuilder = fn(options::CopyOptionSet) -> PgWireResult<CopyFormat>;
96
97const REGISTERED_FORMATS: &[(&str, FormatBuilder)] = &[
102 ("csv", csv::build_csv_codec),
103 ("influxdb", influxdb::build_influxdb_codec),
104 ("influxdb_line", influxdb::build_influxdb_codec),
105 ("text", build_unimplemented_text_codec),
106 ("txt", build_unimplemented_text_codec),
107 ("binary", build_unimplemented_binary_codec),
108];
109
110fn build_unimplemented_text_codec(_options: options::CopyOptionSet) -> PgWireResult<CopyFormat> {
111 Err(unsupported(
112 "the text format is not supported yet; use WITH (FORMAT csv)",
113 ))
114}
115
116fn build_unimplemented_binary_codec(_options: options::CopyOptionSet) -> PgWireResult<CopyFormat> {
117 Err(unsupported(
118 "COPY FROM STDIN WITH (FORMAT binary) is not supported yet",
119 ))
120}
121
122fn resolve_format(options: &mut options::CopyOptionSet) -> PgWireResult<CopyFormat> {
124 let name = options.take_format()?.ok_or_else(|| {
125 unsupported(
126 "COPY FROM STDIN requires an explicit format in this version; use WITH (FORMAT csv)",
127 )
128 })?;
129 let builder = REGISTERED_FORMATS
130 .iter()
131 .find(|(registered, _)| *registered == name)
132 .map(|(_, builder)| *builder)
133 .ok_or_else(|| bad_copy_data(format!("unknown COPY format \"{}\"", name)))?;
134 builder(std::mem::take(options))
135}
136
137#[derive(Debug, Clone)]
139pub(crate) struct CopyFromStdin {
140 table: Vec<String>,
143 columns: Vec<String>,
145 format: CopyFormat,
147}
148
149fn copy_in_error(code: PgErrorCode, message: impl Into<String>) -> PgWireError {
150 PgWireError::UserError(Box::new(code.to_err_info(message.into())))
151}
152
153pub(super) fn unsupported(message: impl Into<String>) -> PgWireError {
154 copy_in_error(PgErrorCode::Ec0A000, message)
155}
156
157fn bad_copy_data(message: impl Into<String>) -> PgWireError {
158 copy_in_error(PgErrorCode::Ec22P04, message)
159}
160
161fn invalid_text_repr(message: impl Into<String>) -> PgWireError {
162 copy_in_error(PgErrorCode::Ec22P02, message)
163}
164
165pub(crate) fn copy_in_must_run_alone_error() -> PgWireError {
166 PgWireError::UserError(Box::new(ErrorInfo::new(
167 "ERROR".to_string(),
168 PgErrorCode::Ec0A000.code(),
169 "COPY FROM STDIN must be executed alone".to_string(),
170 )))
171}
172
173pub(crate) fn copy_from_stdin_from_sql(sql: &str) -> PgWireResult<Option<CopyFromStdin>> {
181 let raw = match parse::scan_copy_from_stdin(sql)? {
182 parse::CopyInScan::NotCopyIn => return Ok(None),
183 parse::CopyInScan::CopyIn(raw) => raw,
184 };
185 let mut option_set = raw.options;
186 let format = resolve_format(&mut option_set)?;
187 Ok(Some(CopyFromStdin {
188 table: raw.table,
189 columns: raw.columns,
190 format,
191 }))
192}
193
194pub(crate) fn is_copy_from_stdin(statement: &SqlParserStatement) -> bool {
198 matches!(
199 statement,
200 SqlParserStatement::Copy {
201 to: false,
202 target: CopyTarget::Stdin,
203 ..
204 }
205 )
206}
207
208fn resolve_table_name(
210 query_ctx: &QueryContextRef,
211 parts: &[String],
212) -> PgWireResult<(String, String, String)> {
213 match parts {
214 [table] => Ok((
215 query_ctx.current_catalog().to_string(),
216 query_ctx.current_schema(),
217 table.clone(),
218 )),
219 [schema, table] => Ok((
220 query_ctx.current_catalog().to_string(),
221 schema.clone(),
222 table.clone(),
223 )),
224 [catalog, schema, table] => Ok((catalog.clone(), schema.clone(), table.clone())),
225 _ => Err(unsupported(format!(
226 "invalid table name for COPY: {}",
227 parts.join(".")
228 ))),
229 }
230}
231
232pub(crate) struct CopyInState {
241 query_ctx: QueryContextRef,
242 payload: Payload,
243 codec_name: &'static str,
245 codec_format_code: i8,
247 rows_written: u64,
248}
249
250enum Payload {
251 Table {
256 grpc_schema: Vec<api::v1::ColumnSchema>,
257 table_name: String,
258 parser: Box<dyn RecordParser + Send>,
259 rows: Vec<GrpcRow>,
261 },
262 Influxdb {
268 data: Vec<u8>,
269 precision: Option<Precision>,
270 },
271}
272
273impl CopyInState {
274 fn new(
275 codec: &CsvCodec,
276 columns: Vec<String>,
277 table_info: Arc<TableInfo>,
278 query_ctx: QueryContextRef,
279 table_name: String,
280 ) -> PgWireResult<Self> {
281 let schema = table_info.meta.schema.clone();
282 let timestamp_index = schema.timestamp_index();
283
284 let columns = resolve_columns(codec, columns, &schema, &table_info)?;
285 if let Some(ts_index) = timestamp_index {
286 let ts_column = &schema.column_schemas()[ts_index];
287 if !columns.iter().any(|column| column.name == ts_column.name) {
288 return Err(unsupported(format!(
289 "COPY FROM STDIN requires the time index column \"{}\" in the column list",
290 ts_column.name
291 )));
292 }
293 }
294 let parser = codec.create_parser(&columns, query_ctx.timezone().to_string().as_str())?;
295
296 let grpc_schema = columns
297 .iter()
298 .map(|column| build_grpc_column_schema(column, &table_info))
299 .collect::<PgWireResult<Vec<_>>>()?;
300
301 Ok(Self {
302 query_ctx,
303 payload: Payload::Table {
304 grpc_schema,
305 table_name,
306 parser,
307 rows: Vec::with_capacity(COPY_IN_FLUSH_ROWS),
308 },
309 codec_name: "csv",
310 codec_format_code: 0,
311 rows_written: 0,
312 })
313 }
314
315 fn new_influxdb(options: &InfluxdbOptions, query_ctx: QueryContextRef) -> Self {
318 Self {
319 query_ctx,
320 payload: Payload::Influxdb {
321 data: Vec::new(),
322 precision: options.precision,
323 },
324 codec_name: "influxdb",
325 codec_format_code: 0,
326 rows_written: 0,
327 }
328 }
329
330 pub(crate) fn format_code(&self) -> i8 {
332 self.codec_format_code
333 }
334
335 fn feed(&mut self, data: &[u8]) -> PgWireResult<()> {
338 match &mut self.payload {
339 Payload::Table { parser, rows, .. } => rows.extend(parser.feed(data)?),
340 Payload::Influxdb { data: buffered, .. } => {
341 if buffered.len() + data.len() > COPY_IN_MAX_PAYLOAD_BYTES {
342 return Err(bad_copy_data(format!(
343 "COPY FROM STDIN payload exceeds the {} limit; split the import into multiple COPY statements",
344 COPY_IN_MAX_PAYLOAD_BYTES
345 )));
346 }
347 buffered.extend_from_slice(data);
348 }
349 }
350 Ok(())
351 }
352
353 fn finish(&mut self) -> PgWireResult<()> {
356 match &mut self.payload {
357 Payload::Table { parser, rows, .. } => rows.extend(parser.finish()?),
358 Payload::Influxdb { .. } => {}
360 }
361 Ok(())
362 }
363
364 fn should_flush(&self) -> bool {
365 match &self.payload {
366 Payload::Table { rows, .. } => rows.len() >= COPY_IN_FLUSH_ROWS,
367 Payload::Influxdb { .. } => false,
369 }
370 }
371
372 fn take_requests(&mut self) -> Option<RowInsertRequests> {
374 let Payload::Table {
375 grpc_schema,
376 table_name,
377 rows,
378 ..
379 } = &mut self.payload
380 else {
381 return None;
382 };
383 if rows.is_empty() {
384 return None;
385 }
386 let rows = GrpcRows {
387 schema: grpc_schema.clone(),
388 rows: std::mem::take(rows),
389 };
390 Some(RowInsertRequests {
391 inserts: vec![RowInsertRequest {
392 table_name: table_name.clone(),
393 rows: Some(rows),
394 }],
395 })
396 }
397
398 fn take_lines(&mut self) -> PgWireResult<Option<InfluxdbRequest>> {
402 let Payload::Influxdb { data, precision } = &mut self.payload else {
403 return Ok(None);
404 };
405 if data.is_empty() {
406 return Ok(None);
407 }
408 let lines = String::from_utf8(std::mem::take(data))
409 .map_err(|e| bad_copy_data(format!("COPY FROM STDIN data is not valid UTF-8: {e}")))?;
410 Ok(Some(InfluxdbRequest {
411 precision: *precision,
412 lines,
413 }))
414 }
415}
416
417fn resolve_columns(
418 codec: &CsvCodec,
419 columns: Vec<String>,
420 schema: &SchemaRef,
421 table_info: &Arc<TableInfo>,
422) -> PgWireResult<Vec<ColumnSchema>> {
423 let table_name = &table_info.name;
424 let selected: Vec<ColumnSchema> = if columns.is_empty() {
425 schema.column_schemas().to_vec()
426 } else {
427 let mut selected = Vec::with_capacity(columns.len());
428 for name in &columns {
429 let column = schema
430 .column_schema_by_name(name)
431 .ok_or_else(|| {
432 copy_in_error(
433 PgErrorCode::Ec42703,
434 format!(
435 "column \"{}\" of relation \"{}\" does not exist",
436 name, table_name
437 ),
438 )
439 })?
440 .clone();
441 if selected.iter().any(|c: &ColumnSchema| &c.name == name) {
442 return Err(bad_copy_data(format!(
443 "column \"{}\" specified more than once",
444 name
445 )));
446 }
447 selected.push(column);
448 }
449 selected
450 };
451
452 for column in &selected {
453 codec.check_column_type(&column.data_type)?;
454 }
455
456 Ok(selected)
457}
458
459fn semantic_type_of(table_info: &TableInfo, column: &ColumnSchema) -> PgWireResult<SemanticType> {
463 let meta = &table_info.meta;
464 if column.is_time_index() {
465 Ok(SemanticType::Timestamp)
466 } else if meta
467 .primary_key_indices
468 .iter()
469 .any(|&index| meta.schema.column_schemas()[index].name == column.name)
470 {
471 Ok(SemanticType::Tag)
472 } else {
473 Ok(SemanticType::Field)
474 }
475}
476
477fn build_grpc_column_schema(
478 column: &ColumnSchema,
479 table_info: &Arc<TableInfo>,
480) -> PgWireResult<api::v1::ColumnSchema> {
481 let (datatype, datatype_extension) = ColumnDataTypeWrapper::try_from(column.data_type.clone())
482 .map_err(|_| {
483 unsupported(format!(
484 "column type \"{}\" is not supported for COPY FROM STDIN yet",
485 column.data_type.name()
486 ))
487 })?
488 .to_parts();
489 Ok(api::v1::ColumnSchema {
490 column_name: column.name.clone(),
491 datatype: datatype as i32,
492 semantic_type: semantic_type_of(table_info, column)? as i32,
493 datatype_extension,
494 options: options_from_column_schema(column),
495 })
496}
497
498impl PostgresServerHandlerInner {
503 pub(crate) async fn begin_copy_in(&self, stmt: CopyFromStdin) -> PgWireResult<Response> {
506 let query_ctx = self.new_query_context();
510
511 let state = match &stmt.format {
514 CopyFormat::Csv(codec) => {
517 if stmt.table.is_empty() {
518 return Err(unsupported(
519 "format csv requires a table: COPY tbl [(cols)] FROM STDIN WITH (FORMAT csv)",
520 ));
521 }
522 let (catalog, schema, table) = resolve_table_name(&query_ctx, &stmt.table)?;
523
524 let Some(table_info) = self
525 .copy_in_handler
526 .copy_in_table(&catalog, &schema, &table, query_ctx.clone())
527 .await
528 .map_err(convert_err)?
529 else {
530 return Err(copy_in_error(
531 PgErrorCode::Ec42P01,
532 format!("relation \"{}\" does not exist", stmt.table.join(".")),
533 ));
534 };
535
536 let query_ctx = adjust_query_context(query_ctx, &catalog, &schema);
540
541 CopyInState::new(codec, stmt.columns, table_info, query_ctx, table)?
542 }
543 CopyFormat::Influxdb(options) => {
548 if !stmt.table.is_empty() {
549 return Err(unsupported(format!(
550 "format influxdb routes each record to its own table; \"COPY {} FROM STDIN\" must not name one",
551 stmt.table.join(".")
552 )));
553 }
554 if self.influxdb_handler.is_none() {
555 return Err(unsupported(
556 "this server is not wired for the influxdb line protocol format",
557 ));
558 }
559 CopyInState::new_influxdb(options, query_ctx)
560 }
561 };
562 let num_columns = match &state.payload {
563 Payload::Table { grpc_schema, .. } => grpc_schema.len(),
564 Payload::Influxdb { .. } => 0,
566 };
567 let format_code = state.format_code();
568
569 *self.copy_in_state.lock().await = Some(state);
570
571 Ok(Response::CopyIn(CopyResponse::new(
572 format_code,
573 num_columns,
574 stream::empty(),
575 )))
576 }
577
578 async fn flush_copy_in(&self, state: &mut CopyInState) -> PgWireResult<u64> {
581 let query_ctx = state.query_ctx.clone();
582 if let Some(request) = state.take_lines()? {
589 let Some(influxdb_handler) = &self.influxdb_handler else {
590 return Err(unsupported(
591 "this server is not wired for the influxdb line protocol format",
592 ));
593 };
594 let output = influxdb_handler
595 .exec(request, query_ctx)
596 .await
597 .map_err(convert_err)?;
598 return match output.data {
599 OutputData::AffectedRows(rows) => Ok(rows as u64),
600 OutputData::RecordBatches(_) | OutputData::Stream(_) => Ok(0),
601 };
602 }
603 let Some(requests) = state.take_requests() else {
604 return Ok(0);
605 };
606 let output = self
607 .copy_in_handler
608 .copy_in_insert(requests, query_ctx)
609 .await
610 .map_err(convert_err)?;
611 match output.data {
612 OutputData::AffectedRows(rows) => Ok(rows as u64),
613 OutputData::RecordBatches(_) | OutputData::Stream(_) => {
614 warn!("unexpected output from COPY FROM STDIN insert");
615 Ok(0)
616 }
617 }
618 }
619}
620
621fn adjust_query_context(
622 query_ctx: QueryContextRef,
623 catalog: &str,
624 schema: &str,
625) -> QueryContextRef {
626 if query_ctx.current_catalog() == catalog && query_ctx.current_schema() == schema {
627 return query_ctx;
628 }
629 let mut forked = query_ctx.fork();
630 forked.set_current_catalog(catalog);
631 forked.set_current_schema(schema);
632 Arc::new(forked)
633}
634
635#[async_trait]
636impl CopyHandler for PostgresServerHandlerInner {
637 async fn on_copy_data<C>(&self, _client: &mut C, copy_data: CopyData) -> PgWireResult<()>
638 where
639 C: ClientInfo + Sink<PgWireBackendMessage> + Unpin + Send + Sync,
640 C::Error: Debug,
641 PgWireError: From<<C as Sink<PgWireBackendMessage>>::Error>,
642 {
643 let mut guard = self.copy_in_state.lock().await;
644 let Some(mut state) = guard.take() else {
645 return Err(copy_in_error(
646 PgErrorCode::Ec08P01,
647 "CopyData message received outside COPY FROM STDIN",
648 ));
649 };
650 state.feed(©_data.data)?;
655 if state.should_flush() {
656 let rows = self.flush_copy_in(&mut state).await?;
657 state.rows_written += rows;
658 METRIC_POSTGRES_COPY_IN_ROWS.inc_by(rows);
659 }
660 *guard = Some(state);
661 Ok(())
662 }
663
664 async fn on_copy_done<C>(&self, client: &mut C, _done: CopyDone) -> PgWireResult<()>
665 where
666 C: ClientInfo + Sink<PgWireBackendMessage> + Unpin + Send + Sync,
667 C::Error: Debug,
668 PgWireError: From<<C as Sink<PgWireBackendMessage>>::Error>,
669 {
670 let mut guard = self.copy_in_state.lock().await;
671 let Some(mut state) = guard.take() else {
675 return Err(copy_in_error(
676 PgErrorCode::Ec08P01,
677 "CopyDone message received outside COPY FROM STDIN",
678 ));
679 };
680 state.finish()?;
681 let rows = self.flush_copy_in(&mut state).await?;
682 state.rows_written += rows;
683 METRIC_POSTGRES_COPY_IN_ROWS.inc_by(rows);
684
685 let rows_written = state.rows_written;
686 let codec_name = state.codec_name;
687 info!(
688 "PostgreSQL COPY FROM STDIN (format {}) finished: {} rows written",
689 codec_name, rows_written
690 );
691 client
692 .send(PgWireBackendMessage::CommandComplete(
693 Tag::new("COPY").with_rows(rows_written as usize).into(),
694 ))
695 .await?;
696
697 if let PgWireConnectionState::CopyInProgress(true) = client.state() {
704 client.set_state(PgWireConnectionState::AwaitingSync);
705 }
706 Ok(())
707 }
708
709 async fn on_copy_fail<C>(&self, _client: &mut C, fail: CopyFail) -> PgWireError
710 where
711 C: ClientInfo + Sink<PgWireBackendMessage> + Unpin + Send + Sync,
712 C::Error: Debug,
713 PgWireError: From<<C as Sink<PgWireBackendMessage>>::Error>,
714 {
715 self.copy_in_state.lock().await.take();
718 copy_in_error(
719 PgErrorCode::Ec57014,
720 format!("COPY FROM STDIN mode terminated by user: {}", fail.message),
721 )
722 }
723}
724
725#[cfg(test)]
726mod tests {
727 use super::*;
728
729 #[test]
730 fn test_parse_copy_from_stdin_requires_format() {
731 let err = copy_from_stdin_from_sql("COPY t FROM STDIN").unwrap_err();
732 assert!(
733 err.to_string()
734 .contains("requires an explicit format in this version"),
735 "{err}"
736 );
737
738 let copy = copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT csv)")
739 .unwrap()
740 .unwrap();
741 assert_eq!(copy.table, vec!["t"]);
742 assert!(copy.columns.is_empty());
743 assert!(matches!(copy.format, CopyFormat::Csv(_)));
744 }
746
747 #[test]
748 fn test_parse_copy_from_stdin_csv() {
749 let copy = copy_from_stdin_from_sql(
750 "COPY t (a, b) FROM STDIN WITH (FORMAT csv, HEADER, DELIMITER ';')",
751 )
752 .unwrap()
753 .unwrap();
754 assert_eq!(copy.columns, vec!["a", "b"]);
755 assert!(matches!(copy.format, CopyFormat::Csv(_)));
756 }
757
758 #[test]
759 fn test_parse_copy_from_stdin_text_unsupported() {
760 for sql in [
761 "COPY t FROM STDIN WITH (FORMAT text)",
762 "COPY t FROM STDIN WITH (FORMAT txt)",
763 ] {
764 let err = copy_from_stdin_from_sql(sql).unwrap_err();
765 assert!(
766 err.to_string().contains("text format is not supported yet"),
767 "{err}"
768 );
769 }
770 }
771
772 #[test]
773 fn test_parse_copy_from_stdin_binary_unsupported() {
774 let err = copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT binary)").unwrap_err();
775 assert!(err.to_string().contains("not supported"));
776 }
777
778 #[test]
779 fn test_parse_copy_from_stdin_unknown_format() {
780 let err = copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT parquet)").unwrap_err();
781 assert!(err.to_string().contains("unknown COPY format"));
782 }
783
784 #[test]
785 fn test_parse_copy_from_stdin_not_copy_in() {
786 assert!(
787 copy_from_stdin_from_sql("COPY (SELECT 1) TO STDOUT")
788 .unwrap()
789 .is_none()
790 );
791 assert!(
792 copy_from_stdin_from_sql("COPY t TO '/tmp/x.csv'")
793 .unwrap()
794 .is_none()
795 );
796 assert!(
798 copy_from_stdin_from_sql("COPY t FROM '/tmp/x.csv' WITH (FORMAT csv)")
799 .unwrap()
800 .is_none()
801 );
802 }
803
804 #[test]
805 fn test_redundant_options() {
806 let err = copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT csv, FORMAT text)")
807 .unwrap_err();
808 assert!(err.to_string().contains("redundant FORMAT"));
809
810 let err = copy_from_stdin_from_sql("COPY t FROM STDIN WITH (DELIMITER ',', DELIMITER ';')")
811 .unwrap_err();
812 assert!(err.to_string().contains("redundant DELIMITER"));
813 }
814
815 #[test]
816 fn test_format_specific_options_rejected() {
817 let err =
820 copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT csv, PRECISION 'ns', FREEZE)")
821 .unwrap_err();
822 assert!(
823 err.to_string().contains("not supported by format csv"),
824 "{err}"
825 );
826 }
827
828 #[test]
829 fn test_multi_statement_rejected() {
830 let err = copy_from_stdin_from_sql("COPY t FROM STDIN; SELECT 1").unwrap_err();
831 assert!(err.to_string().contains("alone"));
832 assert!(
834 copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT csv);")
835 .unwrap()
836 .is_some()
837 );
838 assert!(
839 copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT csv, DELIMITER ';')")
840 .unwrap()
841 .is_some()
842 );
843 }
844}