Skip to main content

servers/postgres/
handler.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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            // early return if query is empty
77            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) = &copy_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/// A container type of parse result types
303#[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        // do not parse if query is empty or matches rules
323        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            // bypass the error: it can run into error because of different
344            // versions of sqlparser
345            (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        // we have our own implementation of describes in ExtendedQueryHandler
383        // so we don't use these methods
384        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        // we have our own implementation of describes in ExtendedQueryHandler
395        // so we don't use these methods
396        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                // early return if query is empty
434                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                    // if the statement matches our predefined rules, return it early
440                    return Ok(resps.remove(0));
441                } else {
442                    // unreachable logic
443                    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                // We won't replace params from statement manually any more.
461                // Newer version of datafusion can generate plan for SELECT/INSERT/UPDATE/DELETE.
462                // Only CREATE TABLE and others minor statements cannot generate sql plan,
463                // in this case, we assume these statements will not carry parameters
464                // and execute them directly.
465                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        // client provided parameter types, can be empty if client doesn't try to parse statement
491        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(&param_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                // use server type when client provided type is None (oid: 0 or other invalid values)
520                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        // EXPLAIN ANALYZE: at execution time the physical plan is replaced with
559        // GreptimeDB's `DistAnalyzeExec` (see `optimize_physical_plan`), whose
560        // output schema (stage/node/plan) differs from the DataFusion `Analyze`
561        // logical plan schema (plan_type/plan). Describe with the schema the
562        // client will actually receive so the DataRow field count matches.
563        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        // query
569        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        // We can cover only part of show statements
574        // these show create statements will return 2 columns
575        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        // SHOW FLOW STATUS returns six columns; return their descriptions so
615        // prepared/extended-protocol clients receive the correct row description.
616        SqlPlan::Statement(Statement::ShowFlowStatus(_), _) => Ok(vec![
617            FieldInfo::new(
618                "flow_id".to_string(),
619                None,
620                None,
621                Type::INT8, // matches type_gt_to_pg(UInt32) — do not use INT4
622                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        // single column show statements
662        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        // we will not support other show statements for extended query protocol at least for now.
681        // because the return columns is not predictable at this stage
682        SqlPlan::Shortcut(query) => {
683            // test if query caught by fixture
684            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                // fallback to NoData
690                Ok(vec![])
691            }
692        }
693        // FETCH cursor: return the cursor's schema so the RowDescription
694        // matches the DataRow field count sent during Execute.
695        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                    // `cursor.schema()` is the GreptimeDB `SchemaRef` captured
700                    // when the cursor was declared; `schema_to_pg` accepts it
701                    // directly (no DataFusion -> GreptimeDB conversion needed).
702                    schema_to_pg(&cursor.schema(), format, None).map_err(convert_err)
703                }
704                None => {
705                    // Cursor not found (e.g. DECLARE hasn't executed yet in
706                    // an extended-protocol batch). Return NoData as a fallback.
707                    Ok(vec![])
708                }
709            }
710        }
711        _ => {
712            // NoData
713            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}