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