1mod codec;
35mod csv;
36mod influxdb;
37mod options;
38mod otlp;
39mod parse;
40
41use std::fmt::Debug;
42use std::sync::Arc;
43
44use crate::influxdb::InfluxdbRequest;
45use crate::query_handler::PipelineHandlerRef;
46use api::helper::ColumnDataTypeWrapper;
47use api::v1::column_def::options_from_column_schema;
48use api::v1::{
49 Row as GrpcRow, RowInsertRequest, RowInsertRequests, Rows as GrpcRows, SemanticType,
50};
51use async_trait::async_trait;
52use codec::{CopyFormat, RecordParser};
53use common_catalog::consts::{TRACE_TABLE_NAME, TRACE_TABLE_NAME_SESSION_KEY};
54use common_grpc::precision::Precision;
55use common_query::OutputData;
56use common_telemetry::{info, warn};
57use csv::CsvCodec;
58use datafusion::sql::sqlparser::ast::{CopyTarget, Statement as SqlParserStatement};
59use datatypes::data_type::DataType;
60use datatypes::schema::{ColumnSchema, SchemaRef};
61use futures::{Sink, SinkExt, stream};
62use influxdb::InfluxdbOptions;
63use otel_arrow_rust::proto::opentelemetry::collector::metrics::v1::ExportMetricsServiceRequest;
64use otlp::{OtlpOptions, OtlpSignal};
65use pgwire::api::copy::CopyHandler;
66use pgwire::api::results::{CopyResponse, Response, Tag};
67use pgwire::api::{ClientInfo, PgWireConnectionState};
68use pgwire::error::{ErrorInfo, PgWireError, PgWireResult};
69use pgwire::messages::PgWireBackendMessage;
70use pgwire::messages::copy::{CopyData, CopyDone, CopyFail};
71use prost::Message;
72use session::context::QueryContextRef;
73use table::metadata::TableInfo;
74
75use crate::metrics::METRIC_POSTGRES_COPY_IN_ROWS;
76use crate::postgres::PostgresServerHandlerInner;
77use crate::postgres::types::PgErrorCode;
78use crate::postgres::utils::convert_err;
79
80pub(crate) const COPY_IN_FLUSH_ROWS: usize = 32768;
92
93pub(crate) const COPY_IN_MAX_PAYLOAD_BYTES: usize = 64 * 1024 * 1024;
98
99type FormatBuilder = fn(options::CopyOptionSet) -> PgWireResult<CopyFormat>;
101
102const REGISTERED_FORMATS: &[(&str, FormatBuilder)] = &[
107 ("csv", csv::build_csv_codec),
108 ("influxdb", influxdb::build_influxdb_codec),
109 ("influxdb_line", influxdb::build_influxdb_codec),
110 ("otlp", otlp::build_otlp_codec),
111 ("otlp_metrics", otlp::build_otlp_codec),
112 ("text", build_unimplemented_text_codec),
113 ("txt", build_unimplemented_text_codec),
114 ("binary", build_unimplemented_binary_codec),
115];
116
117fn build_unimplemented_text_codec(_options: options::CopyOptionSet) -> PgWireResult<CopyFormat> {
118 Err(unsupported(
119 "the text format is not supported yet; use WITH (FORMAT csv)",
120 ))
121}
122
123fn build_unimplemented_binary_codec(_options: options::CopyOptionSet) -> PgWireResult<CopyFormat> {
124 Err(unsupported(
125 "COPY FROM STDIN WITH (FORMAT binary) is not supported yet",
126 ))
127}
128
129fn resolve_format(options: &mut options::CopyOptionSet) -> PgWireResult<CopyFormat> {
131 let name = options.take_format()?.ok_or_else(|| {
132 unsupported(
133 "COPY FROM STDIN requires an explicit format in this version; use WITH (FORMAT csv)",
134 )
135 })?;
136 let builder = REGISTERED_FORMATS
137 .iter()
138 .find(|(registered, _)| *registered == name)
139 .map(|(_, builder)| *builder)
140 .ok_or_else(|| bad_copy_data(format!("unknown COPY format \"{}\"", name)))?;
141 builder(std::mem::take(options))
142}
143
144#[derive(Debug, Clone)]
146pub(crate) struct CopyFromStdin {
147 table: Vec<String>,
150 columns: Vec<String>,
152 format: CopyFormat,
154}
155
156fn copy_in_error(code: PgErrorCode, message: impl Into<String>) -> PgWireError {
157 PgWireError::UserError(Box::new(code.to_err_info(message.into())))
158}
159
160pub(super) fn unsupported(message: impl Into<String>) -> PgWireError {
161 copy_in_error(PgErrorCode::Ec0A000, message)
162}
163
164fn bad_copy_data(message: impl Into<String>) -> PgWireError {
165 copy_in_error(PgErrorCode::Ec22P04, message)
166}
167
168fn invalid_text_repr(message: impl Into<String>) -> PgWireError {
169 copy_in_error(PgErrorCode::Ec22P02, message)
170}
171
172pub(crate) fn copy_in_must_run_alone_error() -> PgWireError {
173 PgWireError::UserError(Box::new(ErrorInfo::new(
174 "ERROR".to_string(),
175 PgErrorCode::Ec0A000.code(),
176 "COPY FROM STDIN must be executed alone".to_string(),
177 )))
178}
179
180pub(crate) fn copy_from_stdin_from_sql(sql: &str) -> PgWireResult<Option<CopyFromStdin>> {
188 let raw = match parse::scan_copy_from_stdin(sql)? {
189 parse::CopyInScan::NotCopyIn => return Ok(None),
190 parse::CopyInScan::CopyIn(raw) => raw,
191 };
192 let mut option_set = raw.options;
193 let format = resolve_format(&mut option_set)?;
194 Ok(Some(CopyFromStdin {
195 table: raw.table,
196 columns: raw.columns,
197 format,
198 }))
199}
200
201pub(crate) fn is_copy_from_stdin(statement: &SqlParserStatement) -> bool {
205 matches!(
206 statement,
207 SqlParserStatement::Copy {
208 to: false,
209 target: CopyTarget::Stdin,
210 ..
211 }
212 )
213}
214
215fn resolve_table_name(
217 query_ctx: &QueryContextRef,
218 parts: &[String],
219) -> PgWireResult<(String, String, String)> {
220 match parts {
221 [table] => Ok((
222 query_ctx.current_catalog().to_string(),
223 query_ctx.current_schema(),
224 table.clone(),
225 )),
226 [schema, table] => Ok((
227 query_ctx.current_catalog().to_string(),
228 schema.clone(),
229 table.clone(),
230 )),
231 [catalog, schema, table] => Ok((catalog.clone(), schema.clone(), table.clone())),
232 _ => Err(unsupported(format!(
233 "invalid table name for COPY: {}",
234 parts.join(".")
235 ))),
236 }
237}
238
239pub(crate) struct CopyInState {
248 query_ctx: QueryContextRef,
249 payload: Payload,
250 codec_name: &'static str,
252 codec_format_code: i8,
254 rows_written: u64,
255}
256
257enum Payload {
258 Table {
263 grpc_schema: Vec<api::v1::ColumnSchema>,
264 table_name: String,
265 parser: Box<dyn RecordParser + Send>,
266 rows: Vec<GrpcRow>,
268 },
269 Influxdb {
275 data: Vec<u8>,
276 precision: Option<Precision>,
277 },
278 Otlp { data: Vec<u8>, options: OtlpOptions },
282}
283
284impl CopyInState {
285 fn new(
286 codec: &CsvCodec,
287 columns: Vec<String>,
288 table_info: Arc<TableInfo>,
289 query_ctx: QueryContextRef,
290 table_name: String,
291 ) -> PgWireResult<Self> {
292 let schema = table_info.meta.schema.clone();
293 let timestamp_index = schema.timestamp_index();
294
295 let columns = resolve_columns(codec, columns, &schema, &table_info)?;
296 if let Some(ts_index) = timestamp_index {
297 let ts_column = &schema.column_schemas()[ts_index];
298 if !columns.iter().any(|column| column.name == ts_column.name) {
299 return Err(unsupported(format!(
300 "COPY FROM STDIN requires the time index column \"{}\" in the column list",
301 ts_column.name
302 )));
303 }
304 }
305 let parser = codec.create_parser(&columns, query_ctx.timezone().to_string().as_str())?;
306
307 let grpc_schema = columns
308 .iter()
309 .map(|column| build_grpc_column_schema(column, &table_info))
310 .collect::<PgWireResult<Vec<_>>>()?;
311
312 Ok(Self {
313 query_ctx,
314 payload: Payload::Table {
315 grpc_schema,
316 table_name,
317 parser,
318 rows: Vec::with_capacity(COPY_IN_FLUSH_ROWS),
319 },
320 codec_name: "csv",
321 codec_format_code: 0,
322 rows_written: 0,
323 })
324 }
325
326 fn new_influxdb(options: &InfluxdbOptions, query_ctx: QueryContextRef) -> Self {
329 Self {
330 query_ctx,
331 payload: Payload::Influxdb {
332 data: Vec::new(),
333 precision: options.precision,
334 },
335 codec_name: "influxdb",
336 codec_format_code: 0,
337 rows_written: 0,
338 }
339 }
340
341 fn new_otlp(options: &OtlpOptions, query_ctx: QueryContextRef) -> Self {
345 Self {
346 query_ctx,
347 payload: Payload::Otlp {
348 data: Vec::new(),
349 options: options.clone(),
350 },
351 codec_name: "otlp",
352 codec_format_code: 1,
353 rows_written: 0,
354 }
355 }
356
357 pub(crate) fn format_code(&self) -> i8 {
359 self.codec_format_code
360 }
361
362 fn feed(&mut self, data: &[u8]) -> PgWireResult<()> {
365 match &mut self.payload {
366 Payload::Table { parser, rows, .. } => rows.extend(parser.feed(data)?),
367 Payload::Influxdb { data: buffered, .. } | Payload::Otlp { data: buffered, .. } => {
368 if buffered.len() + data.len() > COPY_IN_MAX_PAYLOAD_BYTES {
369 return Err(bad_copy_data(format!(
370 "COPY FROM STDIN payload exceeds the {} limit; split the import into multiple COPY statements",
371 COPY_IN_MAX_PAYLOAD_BYTES
372 )));
373 }
374 buffered.extend_from_slice(data);
375 }
376 }
377 Ok(())
378 }
379
380 fn finish(&mut self) -> PgWireResult<()> {
383 match &mut self.payload {
384 Payload::Table { parser, rows, .. } => rows.extend(parser.finish()?),
385 Payload::Influxdb { .. } | Payload::Otlp { .. } => {}
387 }
388 Ok(())
389 }
390
391 fn should_flush(&self) -> bool {
392 match &self.payload {
393 Payload::Table { rows, .. } => rows.len() >= COPY_IN_FLUSH_ROWS,
394 Payload::Influxdb { .. } | Payload::Otlp { .. } => false,
396 }
397 }
398
399 fn take_requests(&mut self) -> Option<RowInsertRequests> {
401 let Payload::Table {
402 grpc_schema,
403 table_name,
404 rows,
405 ..
406 } = &mut self.payload
407 else {
408 return None;
409 };
410 if rows.is_empty() {
411 return None;
412 }
413 let rows = GrpcRows {
414 schema: grpc_schema.clone(),
415 rows: std::mem::take(rows),
416 };
417 Some(RowInsertRequests {
418 inserts: vec![RowInsertRequest {
419 table_name: table_name.clone(),
420 rows: Some(rows),
421 }],
422 })
423 }
424
425 fn take_otlp(&mut self) -> Option<(OtlpOptions, Vec<u8>)> {
427 let Payload::Otlp { options, data } = &mut self.payload else {
428 return None;
429 };
430 if data.is_empty() {
431 return None;
432 }
433 Some((options.clone(), std::mem::take(data)))
434 }
435
436 fn take_lines(&mut self) -> PgWireResult<Option<InfluxdbRequest>> {
440 let Payload::Influxdb { data, precision } = &mut self.payload else {
441 return Ok(None);
442 };
443 if data.is_empty() {
444 return Ok(None);
445 }
446 let lines = String::from_utf8(std::mem::take(data))
447 .map_err(|e| bad_copy_data(format!("COPY FROM STDIN data is not valid UTF-8: {e}")))?;
448 Ok(Some(InfluxdbRequest {
449 precision: *precision,
450 lines,
451 }))
452 }
453}
454
455fn resolve_columns(
456 codec: &CsvCodec,
457 columns: Vec<String>,
458 schema: &SchemaRef,
459 table_info: &Arc<TableInfo>,
460) -> PgWireResult<Vec<ColumnSchema>> {
461 let table_name = &table_info.name;
462 let selected: Vec<ColumnSchema> = if columns.is_empty() {
463 schema.column_schemas().to_vec()
464 } else {
465 let mut selected = Vec::with_capacity(columns.len());
466 for name in &columns {
467 let column = schema
468 .column_schema_by_name(name)
469 .ok_or_else(|| {
470 copy_in_error(
471 PgErrorCode::Ec42703,
472 format!(
473 "column \"{}\" of relation \"{}\" does not exist",
474 name, table_name
475 ),
476 )
477 })?
478 .clone();
479 if selected.iter().any(|c: &ColumnSchema| &c.name == name) {
480 return Err(bad_copy_data(format!(
481 "column \"{}\" specified more than once",
482 name
483 )));
484 }
485 selected.push(column);
486 }
487 selected
488 };
489
490 for column in &selected {
491 codec.check_column_type(&column.data_type)?;
492 }
493
494 Ok(selected)
495}
496
497fn semantic_type_of(table_info: &TableInfo, column: &ColumnSchema) -> PgWireResult<SemanticType> {
501 let meta = &table_info.meta;
502 if column.is_time_index() {
503 Ok(SemanticType::Timestamp)
504 } else if meta
505 .primary_key_indices
506 .iter()
507 .any(|&index| meta.schema.column_schemas()[index].name == column.name)
508 {
509 Ok(SemanticType::Tag)
510 } else {
511 Ok(SemanticType::Field)
512 }
513}
514
515fn build_grpc_column_schema(
516 column: &ColumnSchema,
517 table_info: &Arc<TableInfo>,
518) -> PgWireResult<api::v1::ColumnSchema> {
519 let (datatype, datatype_extension) = ColumnDataTypeWrapper::try_from(column.data_type.clone())
520 .map_err(|_| {
521 unsupported(format!(
522 "column type \"{}\" is not supported for COPY FROM STDIN yet",
523 column.data_type.name()
524 ))
525 })?
526 .to_parts();
527 Ok(api::v1::ColumnSchema {
528 column_name: column.name.clone(),
529 datatype: datatype as i32,
530 semantic_type: semantic_type_of(table_info, column)? as i32,
531 datatype_extension,
532 options: options_from_column_schema(column),
533 })
534}
535
536impl PostgresServerHandlerInner {
541 pub(crate) async fn begin_copy_in(&self, stmt: CopyFromStdin) -> PgWireResult<Response> {
544 let query_ctx = self.new_query_context();
548
549 let state = match &stmt.format {
552 CopyFormat::Csv(codec) => {
555 if stmt.table.is_empty() {
556 return Err(unsupported(
557 "format csv requires a table: COPY tbl [(cols)] FROM STDIN WITH (FORMAT csv)",
558 ));
559 }
560 let (catalog, schema, table) = resolve_table_name(&query_ctx, &stmt.table)?;
561
562 let Some(table_info) = self
563 .copy_in_handler
564 .copy_in_table(&catalog, &schema, &table, query_ctx.clone())
565 .await
566 .map_err(convert_err)?
567 else {
568 return Err(copy_in_error(
569 PgErrorCode::Ec42P01,
570 format!("relation \"{}\" does not exist", stmt.table.join(".")),
571 ));
572 };
573
574 let query_ctx = adjust_query_context(query_ctx, &catalog, &schema);
578
579 CopyInState::new(codec, stmt.columns, table_info, query_ctx, table)?
580 }
581 CopyFormat::Influxdb(options) => {
586 if !stmt.table.is_empty() {
587 return Err(unsupported(format!(
588 "format influxdb routes each record to its own table; \"COPY {} FROM STDIN\" must not name one",
589 stmt.table.join(".")
590 )));
591 }
592 if self.influxdb_handler.is_none() {
593 return Err(unsupported(
594 "this server is not wired for the influxdb line protocol format",
595 ));
596 }
597 CopyInState::new_influxdb(options, query_ctx)
598 }
599 CopyFormat::Otlp(options) => {
603 if !stmt.table.is_empty() {
604 return Err(unsupported(format!(
605 "format otlp routes each record to its own table; \"COPY {} FROM STDIN\" must not name one",
606 stmt.table.join(".")
607 )));
608 }
609 if self.otlp_handler.is_none() {
610 return Err(unsupported("this server is not wired for the otlp format"));
611 }
612 CopyInState::new_otlp(options, query_ctx)
613 }
614 };
615 let num_columns = match &state.payload {
616 Payload::Table { grpc_schema, .. } => grpc_schema.len(),
617 Payload::Influxdb { .. } | Payload::Otlp { .. } => 0,
619 };
620 let format_code = state.format_code();
621
622 *self.copy_in_state.lock().await = Some(state);
623
624 Ok(Response::CopyIn(CopyResponse::new(
625 format_code,
626 num_columns,
627 stream::empty(),
628 )))
629 }
630
631 async fn flush_copy_in(&self, state: &mut CopyInState) -> PgWireResult<u64> {
634 let query_ctx = state.query_ctx.clone();
635 if let Some((options, bytes)) = state.take_otlp() {
642 let Some(otlp_handler) = &self.otlp_handler else {
643 return Err(unsupported("this server is not wired for the otlp format"));
644 };
645 return self
646 .flush_otlp(otlp_handler, options, bytes, query_ctx)
647 .await;
648 }
649 if let Some(request) = state.take_lines()? {
650 let Some(influxdb_handler) = &self.influxdb_handler else {
651 return Err(unsupported(
652 "this server is not wired for the influxdb line protocol format",
653 ));
654 };
655 let output = influxdb_handler
656 .exec(request, query_ctx)
657 .await
658 .map_err(convert_err)?;
659 return match output.data {
660 OutputData::AffectedRows(rows) => Ok(rows as u64),
661 OutputData::RecordBatches(_) | OutputData::Stream(_) => Ok(0),
662 };
663 }
664 let Some(requests) = state.take_requests() else {
665 return Ok(0);
666 };
667 let output = self
668 .copy_in_handler
669 .copy_in_insert(requests, query_ctx)
670 .await
671 .map_err(convert_err)?;
672 match output.data {
673 OutputData::AffectedRows(rows) => Ok(rows as u64),
674 OutputData::RecordBatches(_) | OutputData::Stream(_) => {
675 warn!("unexpected output from COPY FROM STDIN insert");
676 Ok(0)
677 }
678 }
679 }
680
681 async fn flush_otlp(
688 &self,
689 otlp_handler: &crate::query_handler::OpenTelemetryProtocolHandlerRef,
690 options: OtlpOptions,
691 bytes: Vec<u8>,
692 query_ctx: QueryContextRef,
693 ) -> PgWireResult<u64> {
694 use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest;
695 use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest;
696 use pipeline::{GreptimePipelineParams, PipelineWay, SelectInfo};
697 use session::protocol_ctx::{MetricType, OtlpMetricCtx, ProtocolCtx};
698
699 let pipeline_handler: PipelineHandlerRef = otlp_handler.clone();
702 match options.signal {
703 OtlpSignal::Metrics {
704 promote_all_resource_attrs,
705 resource_attrs,
706 promote_scope_attrs,
707 translation_strategy,
708 } => {
709 let mut ctx = (*query_ctx).clone();
711 ctx.set_protocol_ctx(ProtocolCtx::OtlpMetric(OtlpMetricCtx {
712 promote_all_resource_attrs,
713 resource_attrs,
714 promote_scope_attrs,
715 with_metric_engine: self.otlp_with_metric_engine,
716 resource_info: false,
718 metric_type: MetricType::Init,
719 metric_translation_strategy: translation_strategy,
720 }));
721 let request = ExportMetricsServiceRequest::decode(bytes.as_slice())
722 .map_err(|e| bad_copy_data(format!("invalid OTLP payload: {e}")))?;
723 let outcome = otlp_handler
724 .metrics(
725 request,
726 Arc::new(ctx),
727 Some((self.memory_limiter.clone(), 0)),
728 )
729 .await
730 .map_err(convert_err)?;
731 if outcome.accepted_data_points == 0 && outcome.rejected_data_points > 0 {
735 return Err(all_rejected_error(
736 "data points",
737 outcome.rejected_data_points,
738 outcome.error_message.as_deref(),
739 ));
740 }
741 Ok(outcome.accepted_data_points.max(0) as u64)
743 }
744 OtlpSignal::Traces {
745 table_name,
746 pipeline_name,
747 pipeline_version,
748 pipeline_params,
749 } => {
750 let table_name = table_name.unwrap_or_else(|| TRACE_TABLE_NAME.to_string());
751 let mut ctx = (*query_ctx).clone();
753 ctx.set_extension(TRACE_TABLE_NAME_SESSION_KEY, &table_name);
754 let pipeline = PipelineWay::from_name_and_default(
755 pipeline_name.as_deref(),
756 pipeline_version.as_deref(),
757 None,
758 )
759 .map_err(convert_err)?;
760 let request = ExportTraceServiceRequest::decode(bytes.as_slice())
761 .map_err(|e| bad_copy_data(format!("invalid OTLP payload: {e}")))?;
762 let outcome = otlp_handler
763 .traces(
764 pipeline_handler,
765 request,
766 pipeline,
767 GreptimePipelineParams::from_params(pipeline_params.as_deref()),
768 table_name,
769 Arc::new(ctx),
770 )
771 .await
772 .map_err(convert_err)?;
773 if outcome.accepted_spans == 0 && outcome.rejected_spans > 0 {
775 return Err(all_rejected_error(
776 "spans",
777 outcome.rejected_spans as i64,
778 outcome.error_message.as_deref(),
779 ));
780 }
781 Ok(outcome.accepted_spans as u64)
783 }
784 OtlpSignal::Logs {
785 table_name,
786 pipeline_name,
787 pipeline_version,
788 pipeline_params,
789 extract_keys,
790 } => {
791 let table_name = table_name.unwrap_or_else(|| "opentelemetry_logs".to_string());
792 let select_info = match extract_keys.as_deref() {
794 Some(keys) if !keys.is_empty() => SelectInfo::from(keys.to_string()),
795 _ => Default::default(),
796 };
797 let pipeline = PipelineWay::from_name_and_default(
798 pipeline_name.as_deref(),
799 pipeline_version.as_deref(),
800 Some(PipelineWay::OtlpLogDirect(Box::new(select_info))),
801 )
802 .map_err(convert_err)?;
803 let request = ExportLogsServiceRequest::decode(bytes.as_slice())
804 .map_err(|e| bad_copy_data(format!("invalid OTLP payload: {e}")))?;
805 let outputs = otlp_handler
806 .logs(
807 pipeline_handler,
808 request,
809 pipeline,
810 GreptimePipelineParams::from_params(pipeline_params.as_deref()),
811 table_name,
812 query_ctx,
813 )
814 .await
815 .map_err(convert_err)?;
816 Ok(outputs
818 .iter()
819 .map(|o| match &o.data {
820 OutputData::AffectedRows(rows) => *rows as u64,
821 _ => 0,
822 })
823 .sum())
824 }
825 }
826 }
827}
828
829fn all_rejected_error(kind: &str, rejected: i64, reason: Option<&str>) -> PgWireError {
832 bad_copy_data(match reason {
833 Some(reason) => format!("OTLP ingestion rejected all {rejected} {kind}: {reason}"),
834 None => format!("OTLP ingestion rejected all {rejected} {kind}"),
835 })
836}
837
838fn adjust_query_context(
839 query_ctx: QueryContextRef,
840 catalog: &str,
841 schema: &str,
842) -> QueryContextRef {
843 if query_ctx.current_catalog() == catalog && query_ctx.current_schema() == schema {
844 return query_ctx;
845 }
846 let mut forked = query_ctx.fork();
847 forked.set_current_catalog(catalog);
848 forked.set_current_schema(schema);
849 Arc::new(forked)
850}
851
852#[async_trait]
853impl CopyHandler for PostgresServerHandlerInner {
854 async fn on_copy_data<C>(&self, _client: &mut C, copy_data: CopyData) -> PgWireResult<()>
855 where
856 C: ClientInfo + Sink<PgWireBackendMessage> + Unpin + Send + Sync,
857 C::Error: Debug,
858 PgWireError: From<<C as Sink<PgWireBackendMessage>>::Error>,
859 {
860 let mut guard = self.copy_in_state.lock().await;
861 let Some(mut state) = guard.take() else {
862 return Err(copy_in_error(
863 PgErrorCode::Ec08P01,
864 "CopyData message received outside COPY FROM STDIN",
865 ));
866 };
867 state.feed(©_data.data)?;
872 if state.should_flush() {
873 let rows = self.flush_copy_in(&mut state).await?;
874 state.rows_written += rows;
875 METRIC_POSTGRES_COPY_IN_ROWS.inc_by(rows);
876 }
877 *guard = Some(state);
878 Ok(())
879 }
880
881 async fn on_copy_done<C>(&self, client: &mut C, _done: CopyDone) -> PgWireResult<()>
882 where
883 C: ClientInfo + Sink<PgWireBackendMessage> + Unpin + Send + Sync,
884 C::Error: Debug,
885 PgWireError: From<<C as Sink<PgWireBackendMessage>>::Error>,
886 {
887 let mut guard = self.copy_in_state.lock().await;
888 let Some(mut state) = guard.take() else {
892 return Err(copy_in_error(
893 PgErrorCode::Ec08P01,
894 "CopyDone message received outside COPY FROM STDIN",
895 ));
896 };
897 state.finish()?;
898 let rows = self.flush_copy_in(&mut state).await?;
899 state.rows_written += rows;
900 METRIC_POSTGRES_COPY_IN_ROWS.inc_by(rows);
901
902 let rows_written = state.rows_written;
903 let codec_name = state.codec_name;
904 info!(
905 "PostgreSQL COPY FROM STDIN (format {}) finished: {} rows written",
906 codec_name, rows_written
907 );
908 client
909 .send(PgWireBackendMessage::CommandComplete(
910 Tag::new("COPY").with_rows(rows_written as usize).into(),
911 ))
912 .await?;
913
914 if let PgWireConnectionState::CopyInProgress(true) = client.state() {
921 client.set_state(PgWireConnectionState::AwaitingSync);
922 }
923 Ok(())
924 }
925
926 async fn on_copy_fail<C>(&self, _client: &mut C, fail: CopyFail) -> PgWireError
927 where
928 C: ClientInfo + Sink<PgWireBackendMessage> + Unpin + Send + Sync,
929 C::Error: Debug,
930 PgWireError: From<<C as Sink<PgWireBackendMessage>>::Error>,
931 {
932 self.copy_in_state.lock().await.take();
935 copy_in_error(
936 PgErrorCode::Ec57014,
937 format!("COPY FROM STDIN mode terminated by user: {}", fail.message),
938 )
939 }
940}
941
942#[cfg(test)]
943mod tests {
944 use super::*;
945
946 #[test]
947 fn test_parse_copy_from_stdin_requires_format() {
948 let err = copy_from_stdin_from_sql("COPY t FROM STDIN").unwrap_err();
949 assert!(
950 err.to_string()
951 .contains("requires an explicit format in this version"),
952 "{err}"
953 );
954
955 let copy = copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT csv)")
956 .unwrap()
957 .unwrap();
958 assert_eq!(copy.table, vec!["t"]);
959 assert!(copy.columns.is_empty());
960 assert!(matches!(copy.format, CopyFormat::Csv(_)));
961 }
963
964 #[test]
965 fn test_parse_copy_from_stdin_csv() {
966 let copy = copy_from_stdin_from_sql(
967 "COPY t (a, b) FROM STDIN WITH (FORMAT csv, HEADER, DELIMITER ';')",
968 )
969 .unwrap()
970 .unwrap();
971 assert_eq!(copy.columns, vec!["a", "b"]);
972 assert!(matches!(copy.format, CopyFormat::Csv(_)));
973 }
974
975 #[test]
976 fn test_parse_copy_from_stdin_text_unsupported() {
977 for sql in [
978 "COPY t FROM STDIN WITH (FORMAT text)",
979 "COPY t FROM STDIN WITH (FORMAT txt)",
980 ] {
981 let err = copy_from_stdin_from_sql(sql).unwrap_err();
982 assert!(
983 err.to_string().contains("text format is not supported yet"),
984 "{err}"
985 );
986 }
987 }
988
989 #[test]
990 fn test_parse_copy_from_stdin_binary_unsupported() {
991 let err = copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT binary)").unwrap_err();
992 assert!(err.to_string().contains("not supported"));
993 }
994
995 #[test]
996 fn test_parse_copy_from_stdin_unknown_format() {
997 let err = copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT parquet)").unwrap_err();
998 assert!(err.to_string().contains("unknown COPY format"));
999 }
1000
1001 #[test]
1002 fn test_parse_copy_from_stdin_not_copy_in() {
1003 assert!(
1004 copy_from_stdin_from_sql("COPY (SELECT 1) TO STDOUT")
1005 .unwrap()
1006 .is_none()
1007 );
1008 assert!(
1009 copy_from_stdin_from_sql("COPY t TO '/tmp/x.csv'")
1010 .unwrap()
1011 .is_none()
1012 );
1013 assert!(
1015 copy_from_stdin_from_sql("COPY t FROM '/tmp/x.csv' WITH (FORMAT csv)")
1016 .unwrap()
1017 .is_none()
1018 );
1019 }
1020
1021 #[test]
1022 fn test_redundant_options() {
1023 let err = copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT csv, FORMAT text)")
1024 .unwrap_err();
1025 assert!(err.to_string().contains("redundant FORMAT"));
1026
1027 let err = copy_from_stdin_from_sql("COPY t FROM STDIN WITH (DELIMITER ',', DELIMITER ';')")
1028 .unwrap_err();
1029 assert!(err.to_string().contains("redundant DELIMITER"));
1030 }
1031
1032 #[test]
1033 fn test_format_specific_options_rejected() {
1034 let err =
1037 copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT csv, PRECISION 'ns', FREEZE)")
1038 .unwrap_err();
1039 assert!(
1040 err.to_string().contains("not supported by format csv"),
1041 "{err}"
1042 );
1043 }
1044
1045 #[test]
1046 fn test_multi_statement_rejected() {
1047 let err = copy_from_stdin_from_sql("COPY t FROM STDIN; SELECT 1").unwrap_err();
1048 assert!(err.to_string().contains("alone"));
1049 assert!(
1051 copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT csv);")
1052 .unwrap()
1053 .is_some()
1054 );
1055 assert!(
1056 copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT csv, DELIMITER ';')")
1057 .unwrap()
1058 .is_some()
1059 );
1060 }
1061}