1mod codec;
36mod csv;
37mod options;
38mod parse;
39
40use std::fmt::Debug;
41use std::sync::Arc;
42
43use api::helper::ColumnDataTypeWrapper;
44use api::v1::column_def::options_from_column_schema;
45use api::v1::{
46 Row as GrpcRow, RowInsertRequest, RowInsertRequests, Rows as GrpcRows, SemanticType,
47};
48use async_trait::async_trait;
49use codec::{CopyInCodec, RecordParser};
50use common_query::OutputData;
51use common_telemetry::{info, warn};
52use datafusion::sql::sqlparser::ast::{CopyTarget, Statement as SqlParserStatement};
53use datatypes::data_type::DataType;
54use datatypes::schema::{ColumnSchema, SchemaRef};
55use futures::{Sink, SinkExt, stream};
56use pgwire::api::copy::CopyHandler;
57use pgwire::api::results::{CopyResponse, Response, Tag};
58use pgwire::api::{ClientInfo, PgWireConnectionState};
59use pgwire::error::{ErrorInfo, PgWireError, PgWireResult};
60use pgwire::messages::PgWireBackendMessage;
61use pgwire::messages::copy::{CopyData, CopyDone, CopyFail};
62use session::context::QueryContextRef;
63use table::metadata::TableInfo;
64
65use crate::metrics::METRIC_POSTGRES_COPY_IN_ROWS;
66use crate::postgres::PostgresServerHandlerInner;
67use crate::postgres::types::PgErrorCode;
68use crate::postgres::utils::convert_err;
69
70pub(crate) const COPY_IN_FLUSH_ROWS: usize = 32768;
82
83type CodecBuilder = fn(options::CopyOptionSet) -> PgWireResult<Arc<dyn CopyInCodec>>;
85
86const REGISTERED_FORMATS: &[(&str, CodecBuilder)] = &[
91 ("csv", csv::build_csv_codec),
92 ("text", build_unimplemented_text_codec),
93 ("txt", build_unimplemented_text_codec),
94 ("binary", build_unimplemented_binary_codec),
95];
96
97fn build_unimplemented_text_codec(
98 _options: options::CopyOptionSet,
99) -> PgWireResult<Arc<dyn CopyInCodec>> {
100 Err(unsupported(
101 "the text format is not supported yet; use WITH (FORMAT csv)",
102 ))
103}
104
105fn build_unimplemented_binary_codec(
106 _options: options::CopyOptionSet,
107) -> PgWireResult<Arc<dyn CopyInCodec>> {
108 Err(unsupported(
109 "COPY FROM STDIN WITH (FORMAT binary) is not supported yet",
110 ))
111}
112
113fn resolve_codec(options: &mut options::CopyOptionSet) -> PgWireResult<Arc<dyn CopyInCodec>> {
115 let name = options.take_format()?.ok_or_else(|| {
116 unsupported(
117 "COPY FROM STDIN requires an explicit format in this version; use WITH (FORMAT csv)",
118 )
119 })?;
120 let builder = REGISTERED_FORMATS
121 .iter()
122 .find(|(registered, _)| *registered == name)
123 .map(|(_, builder)| *builder)
124 .ok_or_else(|| bad_copy_data(format!("unknown COPY format \"{}\"", name)))?;
125 builder(std::mem::take(options))
126}
127
128#[derive(Debug, Clone)]
130pub(crate) struct CopyFromStdin {
131 table: Vec<String>,
134 columns: Vec<String>,
136 codec: Arc<dyn CopyInCodec>,
138}
139
140fn copy_in_error(code: PgErrorCode, message: impl Into<String>) -> PgWireError {
141 PgWireError::UserError(Box::new(code.to_err_info(message.into())))
142}
143
144fn unsupported(message: impl Into<String>) -> PgWireError {
145 copy_in_error(PgErrorCode::Ec0A000, message)
146}
147
148fn bad_copy_data(message: impl Into<String>) -> PgWireError {
149 copy_in_error(PgErrorCode::Ec22P04, message)
150}
151
152fn invalid_text_repr(message: impl Into<String>) -> PgWireError {
153 copy_in_error(PgErrorCode::Ec22P02, message)
154}
155
156pub(crate) fn copy_in_must_run_alone_error() -> PgWireError {
157 PgWireError::UserError(Box::new(ErrorInfo::new(
158 "ERROR".to_string(),
159 PgErrorCode::Ec0A000.code(),
160 "COPY FROM STDIN must be executed alone".to_string(),
161 )))
162}
163
164pub(crate) fn copy_from_stdin_from_sql(sql: &str) -> PgWireResult<Option<CopyFromStdin>> {
172 let raw = match parse::scan_copy_from_stdin(sql)? {
173 parse::CopyInScan::NotCopyIn => return Ok(None),
174 parse::CopyInScan::CopyIn(raw) => raw,
175 };
176 let mut option_set = raw.options;
177 let codec = resolve_codec(&mut option_set)?;
178 Ok(Some(CopyFromStdin {
179 table: raw.table,
180 columns: raw.columns,
181 codec,
182 }))
183}
184
185pub(crate) fn is_copy_from_stdin(statement: &SqlParserStatement) -> bool {
189 matches!(
190 statement,
191 SqlParserStatement::Copy {
192 to: false,
193 target: CopyTarget::Stdin,
194 ..
195 }
196 )
197}
198
199fn resolve_table_name(
201 query_ctx: &QueryContextRef,
202 parts: &[String],
203) -> PgWireResult<(String, String, String)> {
204 match parts {
205 [table] => Ok((
206 query_ctx.current_catalog().to_string(),
207 query_ctx.current_schema(),
208 table.clone(),
209 )),
210 [schema, table] => Ok((
211 query_ctx.current_catalog().to_string(),
212 schema.clone(),
213 table.clone(),
214 )),
215 [catalog, schema, table] => Ok((catalog.clone(), schema.clone(), table.clone())),
216 _ => Err(unsupported(format!(
217 "invalid table name for COPY: {}",
218 parts.join(".")
219 ))),
220 }
221}
222
223pub(crate) struct CopyInState {
225 query_ctx: QueryContextRef,
226 grpc_schema: Vec<api::v1::ColumnSchema>,
230 table_name: String,
231 parser: Box<dyn RecordParser + Send>,
232 codec_name: &'static str,
234 codec_format_code: i8,
236 rows: Vec<GrpcRow>,
238 rows_written: u64,
239}
240
241impl CopyInState {
242 fn new(
243 codec: Arc<dyn CopyInCodec>,
244 columns: Vec<String>,
245 table_info: Arc<TableInfo>,
246 query_ctx: QueryContextRef,
247 table_name: String,
248 ) -> PgWireResult<Self> {
249 let schema = table_info.meta.schema.clone();
250 let timestamp_index = schema.timestamp_index();
251
252 let columns = resolve_columns(codec.as_ref(), columns, &schema, &table_info)?;
253 if let Some(ts_index) = timestamp_index {
254 let ts_column = &schema.column_schemas()[ts_index];
255 if !columns.iter().any(|column| column.name == ts_column.name) {
256 return Err(unsupported(format!(
257 "COPY FROM STDIN requires the time index column \"{}\" in the column list",
258 ts_column.name
259 )));
260 }
261 }
262 let parser = codec.create_parser(&columns, query_ctx.timezone().to_string().as_str())?;
263
264 let grpc_schema = columns
265 .iter()
266 .map(|column| build_grpc_column_schema(column, &table_info))
267 .collect::<PgWireResult<Vec<_>>>()?;
268
269 Ok(Self {
270 query_ctx,
271 grpc_schema,
272 table_name,
273 parser,
274 codec_name: codec.name(),
275 codec_format_code: codec.format_code(),
276 rows: Vec::with_capacity(COPY_IN_FLUSH_ROWS),
277 rows_written: 0,
278 })
279 }
280
281 pub(crate) fn format_code(&self) -> i8 {
283 self.codec_format_code
284 }
285
286 fn feed(&mut self, data: &[u8]) -> PgWireResult<()> {
288 self.rows.extend(self.parser.feed(data)?);
289 Ok(())
290 }
291
292 fn finish(&mut self) -> PgWireResult<()> {
295 self.rows.extend(self.parser.finish()?);
296 Ok(())
297 }
298
299 fn should_flush(&self) -> bool {
300 self.rows.len() >= COPY_IN_FLUSH_ROWS
301 }
302
303 fn take_requests(&mut self) -> Option<RowInsertRequests> {
305 if self.rows.is_empty() {
306 return None;
307 }
308 let rows = GrpcRows {
309 schema: self.grpc_schema.clone(),
310 rows: std::mem::take(&mut self.rows),
311 };
312 Some(RowInsertRequests {
313 inserts: vec![RowInsertRequest {
314 table_name: self.table_name.clone(),
315 rows: Some(rows),
316 }],
317 })
318 }
319}
320
321fn resolve_columns(
322 codec: &dyn CopyInCodec,
323 columns: Vec<String>,
324 schema: &SchemaRef,
325 table_info: &Arc<TableInfo>,
326) -> PgWireResult<Vec<ColumnSchema>> {
327 let table_name = &table_info.name;
328 let selected: Vec<ColumnSchema> = if columns.is_empty() {
329 schema.column_schemas().to_vec()
330 } else {
331 let mut selected = Vec::with_capacity(columns.len());
332 for name in &columns {
333 let column = schema
334 .column_schema_by_name(name)
335 .ok_or_else(|| {
336 copy_in_error(
337 PgErrorCode::Ec42703,
338 format!(
339 "column \"{}\" of relation \"{}\" does not exist",
340 name, table_name
341 ),
342 )
343 })?
344 .clone();
345 if selected.iter().any(|c: &ColumnSchema| &c.name == name) {
346 return Err(bad_copy_data(format!(
347 "column \"{}\" specified more than once",
348 name
349 )));
350 }
351 selected.push(column);
352 }
353 selected
354 };
355
356 for column in &selected {
357 codec.check_column_type(&column.data_type)?;
358 }
359
360 Ok(selected)
361}
362
363fn semantic_type_of(table_info: &TableInfo, column: &ColumnSchema) -> PgWireResult<SemanticType> {
367 let meta = &table_info.meta;
368 if column.is_time_index() {
369 Ok(SemanticType::Timestamp)
370 } else if meta
371 .primary_key_indices
372 .iter()
373 .any(|&index| meta.schema.column_schemas()[index].name == column.name)
374 {
375 Ok(SemanticType::Tag)
376 } else {
377 Ok(SemanticType::Field)
378 }
379}
380
381fn build_grpc_column_schema(
382 column: &ColumnSchema,
383 table_info: &Arc<TableInfo>,
384) -> PgWireResult<api::v1::ColumnSchema> {
385 let (datatype, datatype_extension) = ColumnDataTypeWrapper::try_from(column.data_type.clone())
386 .map_err(|_| {
387 unsupported(format!(
388 "column type \"{}\" is not supported for COPY FROM STDIN yet",
389 column.data_type.name()
390 ))
391 })?
392 .to_parts();
393 Ok(api::v1::ColumnSchema {
394 column_name: column.name.clone(),
395 datatype: datatype as i32,
396 semantic_type: semantic_type_of(table_info, column)? as i32,
397 datatype_extension,
398 options: options_from_column_schema(column),
399 })
400}
401
402impl PostgresServerHandlerInner {
407 pub(crate) async fn begin_copy_in(&self, stmt: CopyFromStdin) -> PgWireResult<Response> {
410 let query_ctx = self.new_query_context();
414 let (catalog, schema, table) = resolve_table_name(&query_ctx, &stmt.table)?;
415
416 let Some(table_info) = self
417 .copy_in_handler
418 .copy_in_table(&catalog, &schema, &table, query_ctx.clone())
419 .await
420 .map_err(convert_err)?
421 else {
422 return Err(copy_in_error(
423 PgErrorCode::Ec42P01,
424 format!("relation \"{}\" does not exist", stmt.table.join(".")),
425 ));
426 };
427
428 let query_ctx = adjust_query_context(query_ctx, &catalog, &schema);
432
433 let state = CopyInState::new(stmt.codec, stmt.columns, table_info, query_ctx, table)?;
434 let num_columns = state.grpc_schema.len();
435 let format_code = state.format_code();
436
437 *self.copy_in_state.lock().await = Some(state);
438
439 Ok(Response::CopyIn(CopyResponse::new(
440 format_code,
441 num_columns,
442 stream::empty(),
443 )))
444 }
445
446 async fn flush_copy_in(&self, state: &mut CopyInState) -> PgWireResult<u64> {
449 let Some(requests) = state.take_requests() else {
450 return Ok(0);
451 };
452 let query_ctx = state.query_ctx.clone();
453 let output = self
454 .copy_in_handler
455 .copy_in_insert(requests, query_ctx)
456 .await
457 .map_err(convert_err)?;
458 match output.data {
459 OutputData::AffectedRows(rows) => Ok(rows as u64),
460 OutputData::RecordBatches(_) | OutputData::Stream(_) => {
461 warn!("unexpected output from COPY FROM STDIN insert");
462 Ok(0)
463 }
464 }
465 }
466}
467
468fn adjust_query_context(
469 query_ctx: QueryContextRef,
470 catalog: &str,
471 schema: &str,
472) -> QueryContextRef {
473 if query_ctx.current_catalog() == catalog && query_ctx.current_schema() == schema {
474 return query_ctx;
475 }
476 let mut forked = query_ctx.fork();
477 forked.set_current_catalog(catalog);
478 forked.set_current_schema(schema);
479 Arc::new(forked)
480}
481
482#[async_trait]
483impl CopyHandler for PostgresServerHandlerInner {
484 async fn on_copy_data<C>(&self, _client: &mut C, copy_data: CopyData) -> PgWireResult<()>
485 where
486 C: ClientInfo + Sink<PgWireBackendMessage> + Unpin + Send + Sync,
487 C::Error: Debug,
488 PgWireError: From<<C as Sink<PgWireBackendMessage>>::Error>,
489 {
490 let mut guard = self.copy_in_state.lock().await;
491 let Some(mut state) = guard.take() else {
492 return Err(copy_in_error(
493 PgErrorCode::Ec08P01,
494 "CopyData message received outside COPY FROM STDIN",
495 ));
496 };
497 state.feed(©_data.data)?;
502 if state.should_flush() {
503 let rows = self.flush_copy_in(&mut state).await?;
504 state.rows_written += rows;
505 METRIC_POSTGRES_COPY_IN_ROWS.inc_by(rows);
506 }
507 *guard = Some(state);
508 Ok(())
509 }
510
511 async fn on_copy_done<C>(&self, client: &mut C, _done: CopyDone) -> PgWireResult<()>
512 where
513 C: ClientInfo + Sink<PgWireBackendMessage> + Unpin + Send + Sync,
514 C::Error: Debug,
515 PgWireError: From<<C as Sink<PgWireBackendMessage>>::Error>,
516 {
517 let mut guard = self.copy_in_state.lock().await;
518 let Some(mut state) = guard.take() else {
522 return Err(copy_in_error(
523 PgErrorCode::Ec08P01,
524 "CopyDone message received outside COPY FROM STDIN",
525 ));
526 };
527 state.finish()?;
528 let rows = self.flush_copy_in(&mut state).await?;
529 state.rows_written += rows;
530 METRIC_POSTGRES_COPY_IN_ROWS.inc_by(rows);
531
532 let rows_written = state.rows_written;
533 let codec_name = state.codec_name;
534 info!(
535 "PostgreSQL COPY FROM STDIN (format {}) finished: {} rows written",
536 codec_name, rows_written
537 );
538 client
539 .send(PgWireBackendMessage::CommandComplete(
540 Tag::new("COPY").with_rows(rows_written as usize).into(),
541 ))
542 .await?;
543
544 if let PgWireConnectionState::CopyInProgress(true) = client.state() {
551 client.set_state(PgWireConnectionState::AwaitingSync);
552 }
553 Ok(())
554 }
555
556 async fn on_copy_fail<C>(&self, _client: &mut C, fail: CopyFail) -> PgWireError
557 where
558 C: ClientInfo + Sink<PgWireBackendMessage> + Unpin + Send + Sync,
559 C::Error: Debug,
560 PgWireError: From<<C as Sink<PgWireBackendMessage>>::Error>,
561 {
562 self.copy_in_state.lock().await.take();
565 copy_in_error(
566 PgErrorCode::Ec57014,
567 format!("COPY FROM STDIN mode terminated by user: {}", fail.message),
568 )
569 }
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575
576 #[test]
577 fn test_parse_copy_from_stdin_requires_format() {
578 let err = copy_from_stdin_from_sql("COPY t FROM STDIN").unwrap_err();
579 assert!(
580 err.to_string()
581 .contains("requires an explicit format in this version"),
582 "{err}"
583 );
584
585 let copy = copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT csv)")
586 .unwrap()
587 .unwrap();
588 assert_eq!(copy.table, vec!["t"]);
589 assert!(copy.columns.is_empty());
590 assert_eq!(copy.codec.name(), "csv");
591 assert_eq!(copy.codec.format_code(), 0);
592 }
593
594 #[test]
595 fn test_parse_copy_from_stdin_csv() {
596 let copy = copy_from_stdin_from_sql(
597 "COPY t (a, b) FROM STDIN WITH (FORMAT csv, HEADER, DELIMITER ';')",
598 )
599 .unwrap()
600 .unwrap();
601 assert_eq!(copy.columns, vec!["a", "b"]);
602 assert_eq!(copy.codec.name(), "csv");
603 }
604
605 #[test]
606 fn test_parse_copy_from_stdin_text_unsupported() {
607 for sql in [
608 "COPY t FROM STDIN WITH (FORMAT text)",
609 "COPY t FROM STDIN WITH (FORMAT txt)",
610 ] {
611 let err = copy_from_stdin_from_sql(sql).unwrap_err();
612 assert!(
613 err.to_string().contains("text format is not supported yet"),
614 "{err}"
615 );
616 }
617 }
618
619 #[test]
620 fn test_parse_copy_from_stdin_binary_unsupported() {
621 let err = copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT binary)").unwrap_err();
622 assert!(err.to_string().contains("not supported"));
623 }
624
625 #[test]
626 fn test_parse_copy_from_stdin_unknown_format() {
627 let err = copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT parquet)").unwrap_err();
628 assert!(err.to_string().contains("unknown COPY format"));
629 }
630
631 #[test]
632 fn test_parse_copy_from_stdin_not_copy_in() {
633 assert!(
634 copy_from_stdin_from_sql("COPY (SELECT 1) TO STDOUT")
635 .unwrap()
636 .is_none()
637 );
638 assert!(
639 copy_from_stdin_from_sql("COPY t TO '/tmp/x.csv'")
640 .unwrap()
641 .is_none()
642 );
643 assert!(
645 copy_from_stdin_from_sql("COPY t FROM '/tmp/x.csv' WITH (FORMAT csv)")
646 .unwrap()
647 .is_none()
648 );
649 }
650
651 #[test]
652 fn test_redundant_options() {
653 let err = copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT csv, FORMAT text)")
654 .unwrap_err();
655 assert!(err.to_string().contains("redundant FORMAT"));
656
657 let err = copy_from_stdin_from_sql("COPY t FROM STDIN WITH (DELIMITER ',', DELIMITER ';')")
658 .unwrap_err();
659 assert!(err.to_string().contains("redundant DELIMITER"));
660 }
661
662 #[test]
663 fn test_format_specific_options_rejected() {
664 let err =
667 copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT csv, PRECISION 'ns', FREEZE)")
668 .unwrap_err();
669 assert!(
670 err.to_string().contains("not supported by format csv"),
671 "{err}"
672 );
673 }
674
675 #[test]
676 fn test_multi_statement_rejected() {
677 let err = copy_from_stdin_from_sql("COPY t FROM STDIN; SELECT 1").unwrap_err();
678 assert!(err.to_string().contains("alone"));
679 assert!(
681 copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT csv);")
682 .unwrap()
683 .is_some()
684 );
685 assert!(
686 copy_from_stdin_from_sql("COPY t FROM STDIN WITH (FORMAT csv, DELIMITER ';')")
687 .unwrap()
688 .is_some()
689 );
690 }
691}