1use std::fmt::Debug;
16use std::pin::Pin;
17use std::sync::Arc;
18
19use async_trait::async_trait;
20use common_query::{Output, OutputData};
21use common_recordbatch::RecordBatch;
22use common_recordbatch::error::Result as RecordBatchResult;
23use common_telemetry::{debug, info, tracing};
24use datafusion::sql::sqlparser::ast::{CopyOption, CopyTarget, Statement as SqlParserStatement};
25use datafusion_common::ParamValues;
26use datafusion_expr::LogicalPlan;
27use datafusion_pg_catalog::sql::PostgresCompatibilityParser;
28use datatypes::prelude::ConcreteDataType;
29use datatypes::schema::{Schema, SchemaRef};
30use futures::{Sink, SinkExt, Stream, StreamExt, future, stream};
31use pgwire::api::portal::{Format, Portal};
32use pgwire::api::query::{ExtendedQueryHandler, SimpleQueryHandler};
33use pgwire::api::results::{
34 CopyCsvOptions, CopyEncoder, CopyResponse, CopyTextOptions, DataRowEncoder,
35 DescribePortalResponse, DescribeStatementResponse, FieldInfo, QueryResponse, Response, Tag,
36};
37use pgwire::api::stmt::{QueryParser, StoredStatement};
38use pgwire::api::{ClientInfo, ErrorHandler, Type};
39use pgwire::error::{ErrorInfo, PgWireError, PgWireResult};
40use pgwire::messages::PgWireBackendMessage;
41use pgwire::messages::copy::CopyData;
42use pgwire::messages::data::DataRow;
43use query::dist_analyze_output_schema;
44use query::planner::DfLogicalPlanner;
45use query::query_engine::DescribeResult;
46use session::Session;
47use session::context::QueryContextRef;
48use snafu::ResultExt;
49use sql::dialect::PostgreSqlDialect;
50use sql::parser::{ParseOptions, ParserContext};
51use sql::statements::statement::Statement;
52
53use crate::SqlPlan;
54use crate::error::{DataFusionSnafu, InferParameterTypesSnafu, Result};
55use crate::postgres::types::*;
56use crate::postgres::utils::convert_err;
57use crate::postgres::{PostgresServerHandlerInner, fixtures};
58use crate::query_handler::sql::ServerSqlQueryHandlerRef;
59
60#[async_trait]
61impl SimpleQueryHandler for PostgresServerHandlerInner {
62 #[tracing::instrument(skip_all, fields(protocol = "postgres"))]
63 async fn do_query<C>(&self, client: &mut C, query: &str) -> PgWireResult<Vec<Response>>
64 where
65 C: ClientInfo + Sink<PgWireBackendMessage> + Unpin + Send + Sync,
66 C::Error: Debug,
67 PgWireError: From<<C as Sink<PgWireBackendMessage>>::Error>,
68 {
69 let query_ctx = self.session.new_query_context();
70 let db = query_ctx.get_db_string();
71 let _timer = crate::metrics::METRIC_POSTGRES_QUERY_TIMER
72 .with_label_values(&[crate::metrics::METRIC_POSTGRES_SIMPLE_QUERY, db.as_str()])
73 .start_timer();
74
75 if query.is_empty() {
76 return Ok(vec![Response::EmptyQuery]);
78 }
79
80 let parsed_query = self.query_parser.compatibility_parser.parse(query);
81
82 let query = if let Ok(statements) = &parsed_query {
83 statements
84 .iter()
85 .map(|s| s.to_string())
86 .collect::<Vec<_>>()
87 .join(";")
88 } else {
89 query.to_string()
90 };
91
92 if let Some(resps) = fixtures::process(&query, query_ctx.clone()) {
93 send_warning_opt(client, query_ctx).await?;
94 Ok(resps)
95 } else {
96 let outputs = self.query_handler.do_query(&query, query_ctx.clone()).await;
97
98 let mut results = Vec::with_capacity(outputs.len());
99
100 let statements = parsed_query.ok();
101 for (idx, output) in outputs.into_iter().enumerate() {
102 let copy_format = statements
103 .as_ref()
104 .and_then(|stmts| stmts.get(idx))
105 .and_then(check_copy_to_stdout);
106 let resp = if let Some(format) = ©_format {
107 output_to_copy_response(query_ctx.clone(), output, format)?
108 } else {
109 output_to_query_response(query_ctx.clone(), output, &Format::UnifiedText)?
110 };
111 results.push(resp);
112 }
113
114 send_warning_opt(client, query_ctx).await?;
115 Ok(results)
116 }
117 }
118}
119
120async fn send_warning_opt<C>(client: &mut C, query_context: QueryContextRef) -> PgWireResult<()>
121where
122 C: Sink<PgWireBackendMessage> + Unpin + Send + Sync,
123 C::Error: Debug,
124 PgWireError: From<<C as Sink<PgWireBackendMessage>>::Error>,
125{
126 if let Some(warning) = query_context.warning() {
127 client
128 .feed(PgWireBackendMessage::NoticeResponse(
129 ErrorInfo::new(
130 PgErrorSeverity::Warning.to_string(),
131 PgErrorCode::Ec01000.code(),
132 warning.clone(),
133 )
134 .into(),
135 ))
136 .await?;
137 }
138
139 Ok(())
140}
141
142pub(crate) fn output_to_query_response(
143 query_ctx: QueryContextRef,
144 output: Result<Output>,
145 field_format: &Format,
146) -> PgWireResult<Response> {
147 match output {
148 Ok(o) => match o.data {
149 OutputData::AffectedRows(rows) => {
150 Ok(Response::Execution(Tag::new("OK").with_rows(rows)))
151 }
152 OutputData::Stream(record_stream) => {
153 let schema = record_stream.schema();
154 recordbatches_to_query_response(query_ctx, record_stream, schema, field_format)
155 }
156 OutputData::RecordBatches(recordbatches) => {
157 let schema = recordbatches.schema();
158 recordbatches_to_query_response(
159 query_ctx,
160 recordbatches.as_stream(),
161 schema,
162 field_format,
163 )
164 }
165 },
166 Err(e) => Err(convert_err(e)),
167 }
168}
169
170type RowStream<T> = Pin<Box<dyn Stream<Item = PgWireResult<T>> + Send + Unpin>>;
171
172fn recordbatches_to_query_response<S>(
173 query_ctx: QueryContextRef,
174 recordbatches_stream: S,
175 schema: SchemaRef,
176 field_format: &Format,
177) -> PgWireResult<Response>
178where
179 S: Stream<Item = RecordBatchResult<RecordBatch>> + Send + Unpin + 'static,
180{
181 let format_options = format_options_from_query_ctx(&query_ctx);
182 let pg_schema = Arc::new(
183 schema_to_pg(schema.as_ref(), field_format, Some(format_options)).map_err(convert_err)?,
184 );
185
186 let encoder = DataRowEncoder::new(pg_schema.clone());
187 let row_stream = RecordBatchRowStream::new(
188 query_ctx.clone(),
189 pg_schema.clone(),
190 schema.clone(),
191 recordbatches_stream,
192 encoder,
193 );
194
195 let data_row_stream: RowStream<DataRow> = Box::pin(
196 row_stream
197 .map(move |result| match result {
198 Ok(rows) => Box::pin(stream::iter(rows.into_iter().map(Ok))) as RowStream<DataRow>,
199 Err(e) => Box::pin(stream::once(future::ready(Err(e)))) as RowStream<DataRow>,
200 })
201 .flatten(),
202 );
203
204 Ok(Response::Query(QueryResponse::new(
205 pg_schema,
206 data_row_stream,
207 )))
208}
209
210pub(crate) fn output_to_copy_response(
211 query_ctx: QueryContextRef,
212 output: Result<Output>,
213 format: &str,
214) -> PgWireResult<Response> {
215 match output {
216 Ok(o) => match o.data {
217 OutputData::AffectedRows(_) => Err(PgWireError::UserError(Box::new(ErrorInfo::new(
218 "ERROR".to_string(),
219 "42601".to_string(),
220 "COPY cannot be used with non-query statements".to_string(),
221 )))),
222 OutputData::Stream(record_stream) => {
223 let schema = record_stream.schema();
224 recordbatches_to_copy_response(query_ctx, record_stream, schema, format)
225 }
226 OutputData::RecordBatches(recordbatches) => {
227 let schema = recordbatches.schema();
228 recordbatches_to_copy_response(query_ctx, recordbatches.as_stream(), schema, format)
229 }
230 },
231 Err(e) => Err(convert_err(e)),
232 }
233}
234
235fn recordbatches_to_copy_response<S>(
236 query_ctx: QueryContextRef,
237 recordbatches_stream: S,
238 schema: SchemaRef,
239 format: &str,
240) -> PgWireResult<Response>
241where
242 S: Stream<Item = RecordBatchResult<RecordBatch>> + Send + Unpin + 'static,
243{
244 let format_options = format_options_from_query_ctx(&query_ctx);
245 let pg_fields = schema_to_pg(schema.as_ref(), &Format::UnifiedText, Some(format_options))
246 .map_err(convert_err)?;
247
248 let copy_format = match format.to_lowercase().as_str() {
249 "binary" => 1,
250 _ => 0,
251 };
252
253 let pg_schema = Arc::new(pg_fields);
254 let num_columns = pg_schema.len();
255
256 let copy_encoder = match format.to_lowercase().as_str() {
257 "csv" => CopyEncoder::new_csv(pg_schema.clone(), CopyCsvOptions::default()),
258 "binary" => CopyEncoder::new_binary(pg_schema.clone()),
259 _ => CopyEncoder::new_text(pg_schema.clone(), CopyTextOptions::default()),
260 };
261
262 let row_stream = RecordBatchRowStream::new(
263 query_ctx.clone(),
264 pg_schema.clone(),
265 schema.clone(),
266 recordbatches_stream,
267 copy_encoder,
268 );
269
270 let copy_stream: RowStream<CopyData> = Box::pin(
271 row_stream
272 .map(move |result| match result {
273 Ok(rows) => Box::pin(stream::iter(rows.into_iter().map(Ok))) as RowStream<CopyData>,
274 Err(e) => Box::pin(stream::once(future::ready(Err(e)))) as RowStream<CopyData>,
275 })
276 .flatten(),
277 );
278
279 Ok(Response::CopyOut(CopyResponse::new(
280 copy_format,
281 num_columns,
282 copy_stream,
283 )))
284}
285
286pub struct DefaultQueryParser {
287 query_handler: ServerSqlQueryHandlerRef,
288 session: Arc<Session>,
289 compatibility_parser: PostgresCompatibilityParser,
290}
291
292impl DefaultQueryParser {
293 pub fn new(query_handler: ServerSqlQueryHandlerRef, session: Arc<Session>) -> Self {
294 DefaultQueryParser {
295 query_handler,
296 session,
297 compatibility_parser: PostgresCompatibilityParser::new(),
298 }
299 }
300}
301
302#[derive(Clone, Debug)]
304pub struct PgSqlPlan {
305 pub(crate) plan: SqlPlan,
306 pub(crate) copy_to_stdout_format: Option<String>,
307}
308
309#[async_trait]
310impl QueryParser for DefaultQueryParser {
311 type Statement = PgSqlPlan;
312
313 async fn parse_sql<C>(
314 &self,
315 _client: &C,
316 sql: &str,
317 _types: &[Option<Type>],
318 ) -> PgWireResult<Self::Statement> {
319 crate::metrics::METRIC_POSTGRES_PREPARED_COUNT.inc();
320 let query_ctx = self.session.new_query_context();
321
322 if sql.is_empty() {
324 return Ok(PgSqlPlan {
325 plan: SqlPlan::Empty,
326 copy_to_stdout_format: None,
327 });
328 }
329
330 if fixtures::matches(sql) {
331 return Ok(PgSqlPlan {
332 plan: SqlPlan::Shortcut(sql.to_string()),
333 copy_to_stdout_format: None,
334 });
335 }
336
337 let parsed_statements = self.compatibility_parser.parse(sql);
338 let (sql, copy_to_stdout_format) = if let Ok(mut statements) = parsed_statements {
339 let first_stmt = statements.remove(0);
340 let format = check_copy_to_stdout(&first_stmt);
341 (first_stmt.to_string(), format)
342 } else {
343 (sql.to_string(), None)
346 };
347
348 let mut stmts = ParserContext::create_with_dialect(
349 &sql,
350 &PostgreSqlDialect {},
351 ParseOptions::default(),
352 )
353 .map_err(convert_err)?;
354 if stmts.len() != 1 {
355 Err(PgWireError::UserError(Box::new(ErrorInfo::from(
356 PgErrorCode::Ec42P14,
357 ))))
358 } else {
359 let stmt = stmts.remove(0);
360
361 if let Some(logical_plan) = self
362 .query_handler
363 .do_describe(stmt.clone(), query_ctx)
364 .await
365 .map_err(convert_err)?
366 .map(|DescribeResult { logical_plan }| logical_plan)
367 {
368 Ok(PgSqlPlan {
369 plan: SqlPlan::Plan(logical_plan, stmt),
370 copy_to_stdout_format,
371 })
372 } else {
373 Ok(PgSqlPlan {
374 plan: SqlPlan::Statement(stmt, sql),
375 copy_to_stdout_format,
376 })
377 }
378 }
379 }
380
381 fn get_parameter_types(&self, _stmt: &Self::Statement) -> PgWireResult<Vec<Type>> {
382 Err(PgWireError::ApiError(
385 "get_parameter_types is not expected to be called".into(),
386 ))
387 }
388
389 fn get_result_schema(
390 &self,
391 _stmt: &Self::Statement,
392 _column_format: Option<&Format>,
393 ) -> PgWireResult<Vec<FieldInfo>> {
394 Err(PgWireError::ApiError(
397 "get_result_schema is not expected to be called".into(),
398 ))
399 }
400}
401
402#[async_trait]
403impl ExtendedQueryHandler for PostgresServerHandlerInner {
404 type Statement = PgSqlPlan;
405 type QueryParser = DefaultQueryParser;
406
407 fn query_parser(&self) -> Arc<Self::QueryParser> {
408 self.query_parser.clone()
409 }
410
411 async fn do_query<C>(
412 &self,
413 client: &mut C,
414 portal: &Portal<Self::Statement>,
415 _max_rows: usize,
416 ) -> PgWireResult<Response>
417 where
418 C: ClientInfo + Sink<PgWireBackendMessage> + Unpin + Send + Sync,
419 C::Error: Debug,
420 PgWireError: From<<C as Sink<PgWireBackendMessage>>::Error>,
421 {
422 let query_ctx = self.session.new_query_context();
423 let db = query_ctx.get_db_string();
424 let _timer = crate::metrics::METRIC_POSTGRES_QUERY_TIMER
425 .with_label_values(&[crate::metrics::METRIC_POSTGRES_EXTENDED_QUERY, db.as_str()])
426 .start_timer();
427
428 let pg_sql_plan = &portal.statement.statement;
429 let sql_plan = &pg_sql_plan.plan;
430
431 let output = match sql_plan {
432 SqlPlan::Empty => {
433 return Ok(Response::EmptyQuery);
435 }
436 SqlPlan::Shortcut(query) => {
437 if let Some(mut resps) = fixtures::process(query, query_ctx.clone()) {
438 send_warning_opt(client, query_ctx).await?;
439 return Ok(resps.remove(0));
441 } else {
442 return Ok(Response::EmptyQuery);
444 }
445 }
446 SqlPlan::Plan(plan, stmt) => {
447 let values = parameters_to_scalar_values(plan, portal)?;
448 let plan = plan
449 .clone()
450 .replace_params_with_values(&ParamValues::List(
451 values.into_iter().map(Into::into).collect(),
452 ))
453 .context(DataFusionSnafu)
454 .map_err(convert_err)?;
455 self.query_handler
456 .do_exec_plan(plan, Some(stmt.clone()), query_ctx.clone())
457 .await
458 }
459 SqlPlan::Statement(_stmt, query) => {
460 self.query_handler
466 .do_query(query, query_ctx.clone())
467 .await
468 .remove(0)
469 }
470 };
471
472 send_warning_opt(client, query_ctx.clone()).await?;
473
474 if let Some(format) = &pg_sql_plan.copy_to_stdout_format {
475 output_to_copy_response(query_ctx, output, format)
476 } else {
477 output_to_query_response(query_ctx, output, &portal.result_column_format)
478 }
479 }
480
481 async fn do_describe_statement<C>(
482 &self,
483 _client: &mut C,
484 stmt: &StoredStatement<Self::Statement>,
485 ) -> PgWireResult<DescribeStatementResponse>
486 where
487 C: ClientInfo + Unpin + Send + Sync,
488 {
489 let sql_plan = &stmt.statement.plan;
490 let provided_param_types = &stmt.parameter_types;
492 let server_inferenced_types = if let SqlPlan::Plan(plan, _) = &sql_plan {
493 let param_types = DfLogicalPlanner::get_inferred_parameter_types(plan)
494 .context(InferParameterTypesSnafu)
495 .map_err(convert_err)?
496 .into_iter()
497 .map(|(k, v)| (k, v.map(|v| ConcreteDataType::from_arrow_type(&v))))
498 .collect();
499
500 let types = param_types_to_pg_types(¶m_types).map_err(convert_err)?;
501
502 Some(types)
503 } else {
504 None
505 };
506
507 let param_count = if provided_param_types.is_empty() {
508 server_inferenced_types
509 .as_ref()
510 .map(|types| types.len())
511 .unwrap_or(0)
512 } else {
513 provided_param_types.len()
514 };
515
516 let param_types = (0..param_count)
517 .map(|i| {
518 let client_type = provided_param_types.get(i);
519 match client_type {
521 Some(Some(client_type)) => client_type.clone(),
522 _ => server_inferenced_types
523 .as_ref()
524 .and_then(|types| types.get(i).cloned())
525 .unwrap_or(Type::UNKNOWN),
526 }
527 })
528 .collect::<Vec<_>>();
529
530 let fields = describe_fields(sql_plan, &Format::UnifiedText, &self.session)?;
531
532 Ok(DescribeStatementResponse::new(param_types, fields))
533 }
534
535 async fn do_describe_portal<C>(
536 &self,
537 _client: &mut C,
538 portal: &Portal<Self::Statement>,
539 ) -> PgWireResult<DescribePortalResponse>
540 where
541 C: ClientInfo + Unpin + Send + Sync,
542 {
543 let sql_plan = &portal.statement.statement.plan;
544 let format = &portal.result_column_format;
545
546 let fields = describe_fields(sql_plan, format, &self.session)?;
547
548 Ok(DescribePortalResponse::new(fields))
549 }
550}
551
552fn describe_fields(
553 sql_plan: &SqlPlan,
554 format: &Format,
555 session: &Arc<Session>,
556) -> PgWireResult<Vec<FieldInfo>> {
557 match sql_plan {
558 SqlPlan::Plan(LogicalPlan::Analyze(_), _) => {
564 let schema: Schema =
565 Schema::try_from(dist_analyze_output_schema()).map_err(convert_err)?;
566 schema_to_pg(&schema, format, None).map_err(convert_err)
567 }
568 SqlPlan::Plan(plan, _) if !matches!(plan, LogicalPlan::Dml(_) | LogicalPlan::Ddl(_)) => {
570 let schema: Schema = plan.schema().clone().try_into().map_err(convert_err)?;
571 schema_to_pg(&schema, format, None).map_err(convert_err)
572 }
573 SqlPlan::Statement(
576 Statement::ShowCreateDatabase(_)
577 | Statement::ShowCreateTable(_)
578 | Statement::ShowCreateFlow(_)
579 | Statement::ShowCreateView(_),
580 _,
581 ) => Ok(vec![
582 FieldInfo::new(
583 "name".to_string(),
584 None,
585 None,
586 Type::TEXT,
587 format.format_for(0),
588 ),
589 FieldInfo::new(
590 "create_statement".to_string(),
591 None,
592 None,
593 Type::TEXT,
594 format.format_for(1),
595 ),
596 ]),
597 #[cfg(feature = "enterprise")]
598 SqlPlan::Statement(Statement::ShowCreateTrigger(_), _) => Ok(vec![
599 FieldInfo::new(
600 "name".to_string(),
601 None,
602 None,
603 Type::TEXT,
604 format.format_for(0),
605 ),
606 FieldInfo::new(
607 "create_statement".to_string(),
608 None,
609 None,
610 Type::TEXT,
611 format.format_for(1),
612 ),
613 ]),
614 SqlPlan::Statement(Statement::ShowFlowStatus(_), _) => Ok(vec![
617 FieldInfo::new(
618 "flow_id".to_string(),
619 None,
620 None,
621 Type::INT8, format.format_for(0),
623 ),
624 FieldInfo::new(
625 "flow_name".to_string(),
626 None,
627 None,
628 Type::TEXT,
629 format.format_for(1),
630 ),
631 FieldInfo::new(
632 "start_time".to_string(),
633 None,
634 None,
635 Type::TIMESTAMP,
636 format.format_for(2),
637 ),
638 FieldInfo::new(
639 "last_execution_time".to_string(),
640 None,
641 None,
642 Type::TIMESTAMP,
643 format.format_for(3),
644 ),
645 FieldInfo::new(
646 "uptime_seconds".to_string(),
647 None,
648 None,
649 Type::INT8,
650 format.format_for(4),
651 ),
652 FieldInfo::new(
653 "state_size".to_string(),
654 None,
655 None,
656 Type::NUMERIC,
657 format.format_for(5),
658 ),
659 ]),
660
661 SqlPlan::Statement(
663 Statement::ShowTables(_) | Statement::ShowFlows(_) | Statement::ShowViews(_),
664 _,
665 ) => Ok(vec![FieldInfo::new(
666 "name".to_string(),
667 None,
668 None,
669 Type::TEXT,
670 format.format_for(0),
671 )]),
672 #[cfg(feature = "enterprise")]
673 SqlPlan::Statement(Statement::ShowTriggers(_), _) => Ok(vec![FieldInfo::new(
674 "name".to_string(),
675 None,
676 None,
677 Type::TEXT,
678 format.format_for(0),
679 )]),
680 SqlPlan::Shortcut(query) => {
683 if let Some(mut resp) = fixtures::process(query, session.new_query_context())
685 && let Response::Query(query_response) = resp.remove(0)
686 {
687 Ok((*query_response.row_schema()).clone())
688 } else {
689 Ok(vec![])
691 }
692 }
693 SqlPlan::Statement(Statement::FetchCursor(fetch), _) => {
696 let cursor_name = fetch.cursor_name.to_string();
697 match session.get_cursor(&cursor_name) {
698 Some(cursor) => {
699 schema_to_pg(&cursor.schema(), format, None).map_err(convert_err)
703 }
704 None => {
705 Ok(vec![])
708 }
709 }
710 }
711 _ => {
712 Ok(vec![])
714 }
715 }
716}
717
718impl ErrorHandler for PostgresServerHandlerInner {
719 fn on_error<C>(&self, _client: &C, error: &mut PgWireError)
720 where
721 C: ClientInfo,
722 {
723 match error {
724 PgWireError::IoError(e) => debug!("Postgres client disconnected: {}", e),
725 _ => info!("Postgres interface error: {}", error),
726 }
727 }
728}
729
730fn check_copy_to_stdout(statement: &SqlParserStatement) -> Option<String> {
731 if let SqlParserStatement::Copy {
732 target, options, ..
733 } = statement
734 && matches!(target, CopyTarget::Stdout)
735 {
736 for opt in options {
737 if let CopyOption::Format(format_ident) = opt {
738 return Some(format_ident.value.to_lowercase());
739 }
740 }
741 return Some("txt".to_string());
742 }
743
744 None
745}
746
747#[cfg(test)]
748mod tests {
749 use datafusion_pg_catalog::sql::PostgresCompatibilityParser;
750
751 use super::*;
752
753 fn parse_copy_statement(sql: &str) -> SqlParserStatement {
754 let parser = PostgresCompatibilityParser::new();
755 let statements = parser.parse(sql).unwrap();
756 statements.into_iter().next().unwrap()
757 }
758
759 #[test]
760 fn test_check_copy_out_with_csv_format() {
761 let statement = parse_copy_statement("COPY (SELECT 1) TO STDOUT WITH (FORMAT CSV)");
762 assert_eq!(check_copy_to_stdout(&statement), Some("csv".to_string()));
763 }
764
765 #[test]
766 fn test_check_copy_out_with_txt_format() {
767 let statement = parse_copy_statement("COPY (SELECT 1) TO STDOUT WITH (FORMAT TXT)");
768 assert_eq!(check_copy_to_stdout(&statement), Some("txt".to_string()));
769 }
770
771 #[test]
772 fn test_check_copy_out_with_binary_format() {
773 let statement = parse_copy_statement("COPY (SELECT 1) TO STDOUT WITH (FORMAT BINARY)");
774 assert_eq!(check_copy_to_stdout(&statement), Some("binary".to_string()));
775 }
776
777 #[test]
778 fn test_check_copy_out_without_format() {
779 let statement = parse_copy_statement("COPY (SELECT 1) TO STDOUT");
780 assert_eq!(check_copy_to_stdout(&statement), Some("txt".to_string()));
781 }
782
783 #[test]
784 fn test_check_copy_out_to_file() {
785 let statement =
786 parse_copy_statement("COPY (SELECT 1) TO '/path/to/file.csv' WITH (FORMAT CSV)");
787 assert_eq!(check_copy_to_stdout(&statement), None);
788 }
789
790 #[test]
791 fn test_check_copy_out_case_insensitive() {
792 let statement = parse_copy_statement("COPY (SELECT 1) TO STDOUT WITH (FORMAT csv)");
793 assert_eq!(check_copy_to_stdout(&statement), Some("csv".to_string()));
794
795 let statement = parse_copy_statement("COPY (SELECT 1) TO STDOUT WITH (FORMAT binary)");
796 assert_eq!(check_copy_to_stdout(&statement), Some("binary".to_string()));
797 }
798
799 #[test]
800 fn test_check_copy_out_with_multiple_options() {
801 let statement = parse_copy_statement(
802 "COPY (SELECT 1) TO STDOUT WITH (FORMAT csv, DELIMITER ',', HEADER)",
803 );
804 assert_eq!(check_copy_to_stdout(&statement), Some("csv".to_string()));
805
806 let statement = parse_copy_statement(
807 "COPY (SELECT 1) TO STDOUT WITH (DELIMITER ',', HEADER, FORMAT binary)",
808 );
809 assert_eq!(check_copy_to_stdout(&statement), Some("binary".to_string()));
810 }
811}