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