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 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, copy_in, fixtures};
60use crate::query_handler::sql::ServerSqlQueryHandlerRef;
61
62impl PostgresServerHandlerInner {
63    /// Creates a query context carrying this connection's batching
64    /// selection, so write paths can route through the pending-rows
65    /// batcher when it is enabled.
66    pub(super) fn new_query_context(&self) -> QueryContextRef {
67        let mut ctx = self.session.new_query_context();
68        Arc::make_mut(&mut ctx).set_batching_enabled(self.batching_enabled);
69        ctx
70    }
71}
72
73#[async_trait]
74impl SimpleQueryHandler for PostgresServerHandlerInner {
75    #[tracing::instrument(skip_all, fields(protocol = "postgres"))]
76    async fn do_query<C>(&self, client: &mut C, query: &str) -> PgWireResult<Vec<Response>>
77    where
78        C: ClientInfo + Sink<PgWireBackendMessage> + Unpin + Send + Sync,
79        C::Error: Debug,
80        PgWireError: From<<C as Sink<PgWireBackendMessage>>::Error>,
81    {
82        let query_ctx = self.new_query_context();
83        let db = query_ctx.get_db_string();
84        let _timer = crate::metrics::METRIC_POSTGRES_QUERY_TIMER
85            .with_label_values(&[crate::metrics::METRIC_POSTGRES_SIMPLE_QUERY, db.as_str()])
86            .start_timer();
87
88        if query.is_empty() {
89            // early return if query is empty
90            return Ok(vec![Response::EmptyQuery]);
91        }
92
93        // A leading COPY FROM STDIN takes over the connection for the copy
94        // sub-protocol; it is parsed textually so the WITH clause may carry
95        // options beyond sqlparser's COPY grammar. It must run alone.
96        if let Some(copy_from_stdin) = copy_in::copy_from_stdin_from_sql(query)? {
97            let response = self.begin_copy_in(copy_from_stdin).await?;
98            send_warning_opt(client, query_ctx).await?;
99            return Ok(vec![response]);
100        }
101
102        let parsed_query = self.query_parser.compatibility_parser.parse(query);
103
104        // A COPY FROM STDIN trailing other statements must be rejected;
105        // the textual scanner above only sees statement-leading COPYs.
106        if let Ok(statements) = &parsed_query
107            && statements.iter().any(copy_in::is_copy_from_stdin)
108        {
109            return Err(copy_in::copy_in_must_run_alone_error());
110        }
111
112        let query = if let Ok(statements) = &parsed_query {
113            // Comments, whitespace and empty statements also require EmptyQueryResponse.
114            if statements.is_empty() {
115                return Ok(vec![Response::EmptyQuery]);
116            }
117            statements
118                .iter()
119                .map(statement_to_query)
120                .collect::<Vec<_>>()
121                .join(";")
122        } else {
123            query.to_string()
124        };
125
126        if let Some(resps) = fixtures::process(&query, query_ctx.clone()) {
127            send_warning_opt(client, query_ctx).await?;
128            Ok(resps)
129        } else {
130            let outputs = self.query_handler.do_query(&query, query_ctx.clone()).await;
131
132            let mut results = Vec::with_capacity(outputs.len());
133
134            let statements = parsed_query.ok();
135            for (idx, output) in outputs.into_iter().enumerate() {
136                let copy_format = statements
137                    .as_ref()
138                    .and_then(|stmts| stmts.get(idx))
139                    .and_then(check_copy_to_stdout);
140                let resp = if let Some(format) = &copy_format {
141                    output_to_copy_response(query_ctx.clone(), output, format)?
142                } else {
143                    output_to_query_response(query_ctx.clone(), output, &Format::UnifiedText)?
144                };
145                results.push(resp);
146            }
147
148            send_warning_opt(client, query_ctx).await?;
149            Ok(results)
150        }
151    }
152}
153
154fn statement_to_query(statement: &SqlParserStatement) -> String {
155    if let SqlParserStatement::ShowVariable { variable } = statement
156        && variable.len() > 1
157        && (variable[0].value.eq_ignore_ascii_case("query")
158            || variable[0].value.eq_ignore_ascii_case("datafusion"))
159    {
160        return format!(
161            "SHOW {}",
162            variable
163                .iter()
164                .map(ToString::to_string)
165                .collect::<Vec<_>>()
166                .join(".")
167        );
168    }
169    statement.to_string()
170}
171
172async fn send_warning_opt<C>(client: &mut C, query_context: QueryContextRef) -> PgWireResult<()>
173where
174    C: Sink<PgWireBackendMessage> + Unpin + Send + Sync,
175    C::Error: Debug,
176    PgWireError: From<<C as Sink<PgWireBackendMessage>>::Error>,
177{
178    if let Some(warning) = query_context.warning() {
179        client
180            .feed(PgWireBackendMessage::NoticeResponse(
181                ErrorInfo::new(
182                    PgErrorSeverity::Warning.to_string(),
183                    PgErrorCode::Ec01000.code(),
184                    warning.clone(),
185                )
186                .into(),
187            ))
188            .await?;
189    }
190
191    Ok(())
192}
193
194pub(crate) fn output_to_query_response(
195    query_ctx: QueryContextRef,
196    output: Result<Output>,
197    field_format: &Format,
198) -> PgWireResult<Response> {
199    match output {
200        Ok(o) => match o.data {
201            OutputData::AffectedRows(rows) => {
202                Ok(Response::Execution(Tag::new("OK").with_rows(rows)))
203            }
204            OutputData::Stream(record_stream) => {
205                let schema = record_stream.schema();
206                recordbatches_to_query_response(query_ctx, record_stream, schema, field_format)
207            }
208            OutputData::RecordBatches(recordbatches) => {
209                let schema = recordbatches.schema();
210                recordbatches_to_query_response(
211                    query_ctx,
212                    recordbatches.as_stream(),
213                    schema,
214                    field_format,
215                )
216            }
217        },
218        Err(e) => Err(convert_err(e)),
219    }
220}
221
222type RowStream<T> = Pin<Box<dyn Stream<Item = PgWireResult<T>> + Send + Unpin>>;
223
224fn recordbatches_to_query_response<S>(
225    query_ctx: QueryContextRef,
226    recordbatches_stream: S,
227    schema: SchemaRef,
228    field_format: &Format,
229) -> PgWireResult<Response>
230where
231    S: Stream<Item = RecordBatchResult<RecordBatch>> + Send + Unpin + 'static,
232{
233    let format_options = format_options_from_query_ctx(&query_ctx);
234    let pg_schema = Arc::new(
235        schema_to_pg(schema.as_ref(), field_format, Some(format_options)).map_err(convert_err)?,
236    );
237
238    let encoder = DataRowEncoder::new(pg_schema.clone());
239    let row_stream = RecordBatchRowStream::new(
240        query_ctx.clone(),
241        pg_schema.clone(),
242        schema.clone(),
243        recordbatches_stream,
244        encoder,
245    );
246
247    let data_row_stream: RowStream<DataRow> = Box::pin(
248        row_stream
249            .map(move |result| match result {
250                Ok(rows) => Box::pin(stream::iter(rows.into_iter().map(Ok))) as RowStream<DataRow>,
251                Err(e) => Box::pin(stream::once(future::ready(Err(e)))) as RowStream<DataRow>,
252            })
253            .flatten(),
254    );
255
256    Ok(Response::Query(QueryResponse::new(
257        pg_schema,
258        data_row_stream,
259    )))
260}
261
262pub(crate) fn output_to_copy_response(
263    query_ctx: QueryContextRef,
264    output: Result<Output>,
265    format: &str,
266) -> PgWireResult<Response> {
267    match output {
268        Ok(o) => match o.data {
269            OutputData::AffectedRows(_) => Err(PgWireError::UserError(Box::new(ErrorInfo::new(
270                "ERROR".to_string(),
271                "42601".to_string(),
272                "COPY cannot be used with non-query statements".to_string(),
273            )))),
274            OutputData::Stream(record_stream) => {
275                let schema = record_stream.schema();
276                recordbatches_to_copy_response(query_ctx, record_stream, schema, format)
277            }
278            OutputData::RecordBatches(recordbatches) => {
279                let schema = recordbatches.schema();
280                recordbatches_to_copy_response(query_ctx, recordbatches.as_stream(), schema, format)
281            }
282        },
283        Err(e) => Err(convert_err(e)),
284    }
285}
286
287fn recordbatches_to_copy_response<S>(
288    query_ctx: QueryContextRef,
289    recordbatches_stream: S,
290    schema: SchemaRef,
291    format: &str,
292) -> PgWireResult<Response>
293where
294    S: Stream<Item = RecordBatchResult<RecordBatch>> + Send + Unpin + 'static,
295{
296    let format_options = format_options_from_query_ctx(&query_ctx);
297    let pg_fields = schema_to_pg(schema.as_ref(), &Format::UnifiedText, Some(format_options))
298        .map_err(convert_err)?;
299
300    let copy_format = match format.to_lowercase().as_str() {
301        "binary" => 1,
302        _ => 0,
303    };
304
305    // Render timestamps with an explicit session-timezone offset so the
306    // exported text carries the instant unambiguously: the timezone-aware
307    // COPY IN decoder honors embedded offsets, keeping COPY OUT -> COPY IN
308    // round trips exact in non-UTC sessions (matching SQL INSERT parity).
309    // This touches both halves of the encoder: the arrow schema drives
310    // value conversion into a tz-aware DateTime, and the pg field type
311    // selects the offset-bearing text format.
312    let arrow_schema_override = copy_out_schema_with_timezone(&schema, &query_ctx);
313    let pg_fields = if arrow_schema_override.is_some() {
314        pg_fields
315            .into_iter()
316            .map(|mut field| {
317                if field.datatype() == &Type::TIMESTAMP {
318                    field = FieldInfo::new(
319                        field.name().to_string(),
320                        None,
321                        None,
322                        Type::TIMESTAMPTZ,
323                        field.format(),
324                    )
325                    .with_type_size(field.type_size())
326                    .with_type_modifier(field.type_modifier());
327                }
328                field
329            })
330            .collect::<Vec<_>>()
331    } else {
332        pg_fields
333    };
334
335    let pg_schema = Arc::new(pg_fields);
336    let num_columns = pg_schema.len();
337
338    let copy_encoder = match format.to_lowercase().as_str() {
339        "csv" => CopyEncoder::new_csv(pg_schema.clone(), CopyCsvOptions::default()),
340        "binary" => CopyEncoder::new_binary(pg_schema.clone()),
341        _ => CopyEncoder::new_text(pg_schema.clone(), CopyTextOptions::default()),
342    };
343
344    let row_stream = RecordBatchRowStream::new(
345        query_ctx.clone(),
346        pg_schema.clone(),
347        schema.clone(),
348        recordbatches_stream,
349        copy_encoder,
350    )
351    .with_arrow_schema_override(arrow_schema_override);
352
353    let copy_stream: RowStream<CopyData> = Box::pin(
354        row_stream
355            .map(move |result| match result {
356                Ok(rows) => Box::pin(stream::iter(rows.into_iter().map(Ok))) as RowStream<CopyData>,
357                Err(e) => Box::pin(stream::once(future::ready(Err(e)))) as RowStream<CopyData>,
358            })
359            .flatten(),
360    );
361
362    Ok(Response::CopyOut(CopyResponse::new(
363        copy_format,
364        num_columns,
365        copy_stream,
366    )))
367}
368
369pub struct DefaultQueryParser {
370    query_handler: ServerSqlQueryHandlerRef,
371    session: Arc<Session>,
372    compatibility_parser: PostgresCompatibilityParser,
373}
374
375impl DefaultQueryParser {
376    pub fn new(query_handler: ServerSqlQueryHandlerRef, session: Arc<Session>) -> Self {
377        DefaultQueryParser {
378            query_handler,
379            session,
380            compatibility_parser: PostgresCompatibilityParser::new(),
381        }
382    }
383}
384
385/// A container type of parse result types
386#[derive(Clone, Debug)]
387pub struct PgSqlPlan {
388    pub(crate) plan: SqlPlan,
389    pub(crate) copy_to_stdout_format: Option<String>,
390    pub(crate) copy_from_stdin: Option<copy_in::CopyFromStdin>,
391}
392
393#[async_trait]
394impl QueryParser for DefaultQueryParser {
395    type Statement = PgSqlPlan;
396
397    async fn parse_sql<C>(
398        &self,
399        _client: &C,
400        sql: &str,
401        _types: &[Option<Type>],
402    ) -> PgWireResult<Option<Self::Statement>> {
403        crate::metrics::METRIC_POSTGRES_PREPARED_COUNT.inc();
404        let query_ctx = self.session.new_query_context();
405
406        // do not parse if query is empty or matches rules
407        if sql.is_empty() {
408            return Ok(None);
409        }
410
411        if fixtures::matches(sql) {
412            return Ok(Some(PgSqlPlan {
413                plan: SqlPlan::Shortcut(sql.to_string()),
414                copy_to_stdout_format: None,
415                copy_from_stdin: None,
416            }));
417        }
418
419        // A COPY FROM STDIN is parsed textually — not through sqlparser —
420        // so the WITH clause may carry options beyond sqlparser's COPY
421        // grammar; the extended query handler starts the copy sub-protocol
422        // when the statement is executed.
423        if let Some(copy_from_stdin) = copy_in::copy_from_stdin_from_sql(sql)? {
424            return Ok(Some(PgSqlPlan {
425                plan: SqlPlan::Shortcut(sql.to_string()),
426                copy_to_stdout_format: None,
427                copy_from_stdin: Some(copy_from_stdin),
428            }));
429        }
430
431        let parsed_statements = self.compatibility_parser.parse(sql);
432        let (sql, copy_to_stdout_format) = if let Ok(mut statements) = parsed_statements {
433            if statements.is_empty() {
434                return Ok(None);
435            }
436            let first_stmt = statements.remove(0);
437            let format = check_copy_to_stdout(&first_stmt);
438            (first_stmt.to_string(), format)
439        } else {
440            // bypass the error: it can run into error because of different
441            // versions of sqlparser
442            (sql.to_string(), None)
443        };
444
445        let mut stmts = ParserContext::create_with_dialect(
446            &sql,
447            &PostgreSqlDialect {},
448            ParseOptions::default(),
449        )
450        .map_err(convert_err)?;
451        if stmts.len() != 1 {
452            Err(PgWireError::UserError(Box::new(ErrorInfo::from(
453                PgErrorCode::Ec42P14,
454            ))))
455        } else {
456            let stmt = stmts.remove(0);
457
458            if let Some(logical_plan) = self
459                .query_handler
460                .do_describe(stmt.clone(), query_ctx)
461                .await
462                .map_err(convert_err)?
463                .map(|DescribeResult { logical_plan }| logical_plan)
464            {
465                Ok(Some(PgSqlPlan {
466                    plan: SqlPlan::Plan(logical_plan, stmt),
467                    copy_to_stdout_format,
468                    copy_from_stdin: None,
469                }))
470            } else {
471                Ok(Some(PgSqlPlan {
472                    plan: SqlPlan::Statement(stmt, sql),
473                    copy_to_stdout_format,
474                    copy_from_stdin: None,
475                }))
476            }
477        }
478    }
479
480    fn get_parameter_types(&self, _stmt: &Self::Statement) -> PgWireResult<Vec<Type>> {
481        // we have our own implementation of describes in ExtendedQueryHandler
482        // so we don't use these methods
483        Err(PgWireError::ApiError(
484            "get_parameter_types is not expected to be called".into(),
485        ))
486    }
487
488    fn get_result_schema(
489        &self,
490        _stmt: &Self::Statement,
491        _column_format: Option<&Format>,
492    ) -> PgWireResult<Vec<FieldInfo>> {
493        // we have our own implementation of describes in ExtendedQueryHandler
494        // so we don't use these methods
495        Err(PgWireError::ApiError(
496            "get_result_schema is not expected to be called".into(),
497        ))
498    }
499}
500
501#[async_trait]
502impl ExtendedQueryHandler for PostgresServerHandlerInner {
503    type Statement = PgSqlPlan;
504    type QueryParser = DefaultQueryParser;
505
506    fn query_parser(&self) -> Arc<Self::QueryParser> {
507        self.query_parser.clone()
508    }
509
510    async fn do_query<C>(
511        &self,
512        client: &mut C,
513        portal: &Portal<Self::Statement>,
514        _max_rows: usize,
515    ) -> PgWireResult<Response>
516    where
517        C: ClientInfo + Sink<PgWireBackendMessage> + Unpin + Send + Sync,
518        C::Error: Debug,
519        PgWireError: From<<C as Sink<PgWireBackendMessage>>::Error>,
520    {
521        let query_ctx = self.new_query_context();
522        let db = query_ctx.get_db_string();
523        let _timer = crate::metrics::METRIC_POSTGRES_QUERY_TIMER
524            .with_label_values(&[crate::metrics::METRIC_POSTGRES_EXTENDED_QUERY, db.as_str()])
525            .start_timer();
526
527        let pg_sql_plan = &portal.statement.statement;
528        let sql_plan = &pg_sql_plan.plan;
529
530        // A COPY FROM STDIN takes over the connection for the copy
531        // sub-protocol.
532        if let Some(copy_from_stdin) = &pg_sql_plan.copy_from_stdin {
533            let response = self.begin_copy_in(copy_from_stdin.clone()).await?;
534            send_warning_opt(client, query_ctx).await?;
535            return Ok(response);
536        }
537
538        let output = match sql_plan {
539            SqlPlan::Empty => {
540                // early return if query is empty
541                return Ok(Response::EmptyQuery);
542            }
543            SqlPlan::Shortcut(query) => {
544                if let Some(mut resps) = fixtures::process(query, query_ctx.clone()) {
545                    send_warning_opt(client, query_ctx).await?;
546                    // if the statement matches our predefined rules, return it early
547                    return Ok(resps.remove(0));
548                } else {
549                    // unreachable logic
550                    return Ok(Response::EmptyQuery);
551                }
552            }
553            SqlPlan::Plan(plan, stmt) => {
554                let values = parameters_to_scalar_values(plan, portal)?;
555                let plan = plan
556                    .clone()
557                    .replace_params_with_values(&ParamValues::List(
558                        values.into_iter().map(Into::into).collect(),
559                    ))
560                    .context(DataFusionSnafu)
561                    .map_err(convert_err)?;
562                self.query_handler
563                    .do_exec_plan(plan, Some(stmt.clone()), query_ctx.clone())
564                    .await
565            }
566            SqlPlan::Statement(_stmt, query) => {
567                // We won't replace params from statement manually any more.
568                // Newer version of datafusion can generate plan for SELECT/INSERT/UPDATE/DELETE.
569                // Only CREATE TABLE and others minor statements cannot generate sql plan,
570                // in this case, we assume these statements will not carry parameters
571                // and execute them directly.
572                self.query_handler
573                    .do_query(query, query_ctx.clone())
574                    .await
575                    .remove(0)
576            }
577        };
578
579        send_warning_opt(client, query_ctx.clone()).await?;
580
581        if let Some(format) = &pg_sql_plan.copy_to_stdout_format {
582            output_to_copy_response(query_ctx, output, format)
583        } else {
584            output_to_query_response(query_ctx, output, &portal.result_column_format)
585        }
586    }
587
588    async fn do_describe_statement<C>(
589        &self,
590        _client: &mut C,
591        stmt: &StoredStatement<Self::Statement>,
592    ) -> PgWireResult<DescribeStatementResponse>
593    where
594        C: ClientInfo + Unpin + Send + Sync,
595    {
596        let sql_plan = &stmt.statement.plan;
597        // client provided parameter types, can be empty if client doesn't try to parse statement
598        let provided_param_types = &stmt.parameter_types;
599        let server_inferenced_types = if let SqlPlan::Plan(plan, _) = &sql_plan {
600            let param_types = DfLogicalPlanner::get_inferred_parameter_types(plan)
601                .context(InferParameterTypesSnafu)
602                .map_err(convert_err)?
603                .into_iter()
604                .map(|(k, v)| (k, v.map(|v| ConcreteDataType::from_arrow_type(&v))))
605                .collect();
606
607            let types = param_types_to_pg_types(&param_types).map_err(convert_err)?;
608
609            Some(types)
610        } else {
611            None
612        };
613
614        let param_count = if provided_param_types.is_empty() {
615            server_inferenced_types
616                .as_ref()
617                .map(|types| types.len())
618                .unwrap_or(0)
619        } else {
620            provided_param_types.len()
621        };
622
623        let param_types = (0..param_count)
624            .map(|i| {
625                let client_type = provided_param_types.get(i);
626                // use server type when client provided type is None (oid: 0 or other invalid values)
627                match client_type {
628                    Some(Some(client_type)) => client_type.clone(),
629                    _ => server_inferenced_types
630                        .as_ref()
631                        .and_then(|types| types.get(i).cloned())
632                        .unwrap_or(Type::UNKNOWN),
633                }
634            })
635            .collect::<Vec<_>>();
636
637        let fields = describe_fields(sql_plan, &Format::UnifiedText, &self.session)?;
638
639        Ok(DescribeStatementResponse::new(param_types, fields))
640    }
641
642    async fn do_describe_portal<C>(
643        &self,
644        _client: &mut C,
645        portal: &Portal<Self::Statement>,
646    ) -> PgWireResult<DescribePortalResponse>
647    where
648        C: ClientInfo + Unpin + Send + Sync,
649    {
650        let sql_plan = &portal.statement.statement.plan;
651        let format = &portal.result_column_format;
652
653        let fields = describe_fields(sql_plan, format, &self.session)?;
654
655        Ok(DescribePortalResponse::new(fields))
656    }
657}
658
659fn describe_fields(
660    sql_plan: &SqlPlan,
661    format: &Format,
662    session: &Arc<Session>,
663) -> PgWireResult<Vec<FieldInfo>> {
664    match sql_plan {
665        // Execution swaps in DistAnalyzeExec (stage/node/plan), whose schema
666        // differs from the logical `Analyze` plan's (plan_type/plan).
667        SqlPlan::Plan(LogicalPlan::Analyze(_), _) => {
668            let schema: Schema =
669                Schema::try_from(dist_analyze_output_schema()).map_err(convert_err)?;
670            schema_to_pg(&schema, format, None).map_err(convert_err)
671        }
672        // query
673        SqlPlan::Plan(plan, _) if !matches!(plan, LogicalPlan::Dml(_) | LogicalPlan::Ddl(_)) => {
674            let schema: Schema = plan.schema().clone().try_into().map_err(convert_err)?;
675            schema_to_pg(&schema, format, None).map_err(convert_err)
676        }
677        // We can cover only part of show statements
678        // these show create statements will return 2 columns
679        SqlPlan::Statement(
680            Statement::ShowCreateDatabase(_)
681            | Statement::ShowCreateTable(_)
682            | Statement::ShowCreateFlow(_)
683            | Statement::ShowCreateView(_),
684            _,
685        ) => Ok(vec![
686            FieldInfo::new(
687                "name".to_string(),
688                None,
689                None,
690                Type::TEXT,
691                format.format_for(0),
692            ),
693            FieldInfo::new(
694                "create_statement".to_string(),
695                None,
696                None,
697                Type::TEXT,
698                format.format_for(1),
699            ),
700        ]),
701        #[cfg(feature = "enterprise")]
702        SqlPlan::Statement(Statement::ShowCreateTrigger(_), _) => Ok(vec![
703            FieldInfo::new(
704                "name".to_string(),
705                None,
706                None,
707                Type::TEXT,
708                format.format_for(0),
709            ),
710            FieldInfo::new(
711                "create_statement".to_string(),
712                None,
713                None,
714                Type::TEXT,
715                format.format_for(1),
716            ),
717        ]),
718        // SHOW FLOW STATUS returns six columns; return their descriptions so
719        // prepared/extended-protocol clients receive the correct row description.
720        SqlPlan::Statement(Statement::ShowFlowStatus(_), _) => Ok(vec![
721            FieldInfo::new(
722                "flow_id".to_string(),
723                None,
724                None,
725                Type::INT8, // matches type_gt_to_pg(UInt32) — do not use INT4
726                format.format_for(0),
727            ),
728            FieldInfo::new(
729                "flow_name".to_string(),
730                None,
731                None,
732                Type::TEXT,
733                format.format_for(1),
734            ),
735            FieldInfo::new(
736                "start_time".to_string(),
737                None,
738                None,
739                Type::TIMESTAMP,
740                format.format_for(2),
741            ),
742            FieldInfo::new(
743                "last_execution_time".to_string(),
744                None,
745                None,
746                Type::TIMESTAMP,
747                format.format_for(3),
748            ),
749            FieldInfo::new(
750                "uptime_seconds".to_string(),
751                None,
752                None,
753                Type::INT8,
754                format.format_for(4),
755            ),
756            FieldInfo::new(
757                "state_size".to_string(),
758                None,
759                None,
760                Type::NUMERIC,
761                format.format_for(5),
762            ),
763        ]),
764
765        #[cfg(feature = "enterprise")]
766        SqlPlan::Statement(Statement::ShowTriggers(_), _) => Ok(vec![FieldInfo::new(
767            "name".to_string(),
768            None,
769            None,
770            Type::TEXT,
771            format.format_for(0),
772        )]),
773        // we will not support other show statements for extended query protocol at least for now.
774        // because the return columns is not predictable at this stage
775        SqlPlan::Shortcut(query) => {
776            // test if query caught by fixture
777            if let Some(mut resp) = fixtures::process(query, session.new_query_context())
778                && let Response::Query(query_response) = resp.remove(0)
779            {
780                Ok((*query_response.row_schema()).clone())
781            } else {
782                // fallback to NoData
783                Ok(vec![])
784            }
785        }
786        // Single column named after the variable (see `query::sql::show_variable`).
787        SqlPlan::Statement(Statement::ShowVariables(show), _) => Ok(vec![FieldInfo::new(
788            show.variable.to_string().to_uppercase(),
789            None,
790            None,
791            Type::TEXT,
792            format.format_for(0),
793        )]),
794        // Mirrors `query::sql::show_status` (currently always empty).
795        SqlPlan::Statement(Statement::ShowStatus(_), _) => Ok(vec![
796            FieldInfo::new(
797                "Variable_name".to_string(),
798                None,
799                None,
800                Type::TEXT,
801                format.format_for(0),
802            ),
803            FieldInfo::new(
804                "Value".to_string(),
805                None,
806                None,
807                Type::TEXT,
808                format.format_for(1),
809            ),
810        ]),
811        SqlPlan::Statement(Statement::ShowSearchPath(_), _) => Ok(vec![FieldInfo::new(
812            "search_path".to_string(),
813            None,
814            None,
815            Type::TEXT,
816            format.format_for(0),
817        )]),
818        // Mirrors `query::sql::describe_table`.
819        SqlPlan::Statement(Statement::DescribeTable(_), _) => {
820            schema_to_pg(&DESCRIBE_TABLE_OUTPUT_SCHEMA, format, None).map_err(convert_err)
821        }
822        // Single column typed with the function's return type (see
823        // `operator::statement::admin_output_schema`).
824        SqlPlan::Statement(Statement::Admin(admin), _) => {
825            let query_ctx = session.new_query_context();
826            match admin_output_schema(admin, &query_ctx) {
827                Some(schema) => schema_to_pg(&schema, format, None).map_err(convert_err),
828                // Unresolvable; execution will surface the error.
829                None => Ok(vec![]),
830            }
831        }
832        // Describe from the declared cursor's schema.
833        SqlPlan::Statement(Statement::FetchCursor(fetch), _) => {
834            let cursor_name = fetch.cursor_name.to_string();
835            match session.get_cursor(&cursor_name) {
836                Some(cursor) => schema_to_pg(&cursor.schema(), format, None).map_err(convert_err),
837                // Cursor not declared yet; execution will error.
838                None => Ok(vec![]),
839            }
840        }
841        _ => {
842            // NoData
843            Ok(vec![])
844        }
845    }
846}
847
848impl ErrorHandler for PostgresServerHandlerInner {
849    fn on_error<C>(&self, _client: &C, error: &mut PgWireError)
850    where
851        C: ClientInfo,
852    {
853        match error {
854            PgWireError::IoError(e) => debug!("Postgres client disconnected: {}", e),
855            _ => info!("Postgres interface error: {}", error),
856        }
857    }
858}
859
860/// Rewrites timestamp fields of the table's arrow schema to carry the
861/// session timezone, so COPY OUT renders them with an explicit offset
862/// (`2024-01-01 08:00:00+08:00`). Returns `None` when there is nothing
863/// to rewrite (no naive timestamps or a UTC session leaving output as-is
864/// is fine either way — offsets are always unambiguous on re-import).
865fn copy_out_schema_with_timezone(
866    schema: &SchemaRef,
867    query_ctx: &QueryContextRef,
868) -> Option<Arc<arrow::datatypes::Schema>> {
869    let timezone = query_ctx.timezone().to_string();
870    let arrow_schema = schema.arrow_schema();
871    let mut changed = false;
872    let fields = arrow_schema
873        .fields()
874        .iter()
875        .map(|field| match field.data_type() {
876            arrow::datatypes::DataType::Timestamp(unit, None) => {
877                changed = true;
878                field
879                    .as_ref()
880                    .clone()
881                    .with_data_type(arrow::datatypes::DataType::Timestamp(
882                        *unit,
883                        Some(Arc::from(timezone.as_str())),
884                    ))
885            }
886            _ => field.as_ref().clone(),
887        })
888        .collect::<Vec<_>>();
889    changed.then(|| Arc::new(arrow::datatypes::Schema::new(fields)))
890}
891
892fn check_copy_to_stdout(statement: &SqlParserStatement) -> Option<String> {
893    if let SqlParserStatement::Copy {
894        target, options, ..
895    } = statement
896        && matches!(target, CopyTarget::Stdout)
897    {
898        for opt in options {
899            if let CopyOption::Format(format_ident) = opt {
900                return Some(format_ident.value.to_lowercase());
901            }
902        }
903        return Some("txt".to_string());
904    }
905
906    None
907}
908
909#[cfg(test)]
910mod tests {
911    use datafusion_pg_catalog::sql::PostgresCompatibilityParser;
912
913    use super::*;
914
915    fn parse_copy_statement(sql: &str) -> SqlParserStatement {
916        let parser = PostgresCompatibilityParser::new();
917        let statements = parser.parse(sql).unwrap();
918        statements.into_iter().next().unwrap()
919    }
920
921    #[test]
922    fn test_check_copy_out_with_csv_format() {
923        let statement = parse_copy_statement("COPY (SELECT 1) TO STDOUT WITH (FORMAT CSV)");
924        assert_eq!(check_copy_to_stdout(&statement), Some("csv".to_string()));
925    }
926
927    #[test]
928    fn test_check_copy_out_with_txt_format() {
929        let statement = parse_copy_statement("COPY (SELECT 1) TO STDOUT WITH (FORMAT TXT)");
930        assert_eq!(check_copy_to_stdout(&statement), Some("txt".to_string()));
931    }
932
933    #[test]
934    fn test_check_copy_out_with_binary_format() {
935        let statement = parse_copy_statement("COPY (SELECT 1) TO STDOUT WITH (FORMAT BINARY)");
936        assert_eq!(check_copy_to_stdout(&statement), Some("binary".to_string()));
937    }
938
939    #[test]
940    fn test_check_copy_out_without_format() {
941        let statement = parse_copy_statement("COPY (SELECT 1) TO STDOUT");
942        assert_eq!(check_copy_to_stdout(&statement), Some("txt".to_string()));
943    }
944
945    #[test]
946    fn test_check_copy_out_to_file() {
947        let statement =
948            parse_copy_statement("COPY (SELECT 1) TO '/path/to/file.csv' WITH (FORMAT CSV)");
949        assert_eq!(check_copy_to_stdout(&statement), None);
950    }
951
952    #[test]
953    fn test_check_copy_out_case_insensitive() {
954        let statement = parse_copy_statement("COPY (SELECT 1) TO STDOUT WITH (FORMAT csv)");
955        assert_eq!(check_copy_to_stdout(&statement), Some("csv".to_string()));
956
957        let statement = parse_copy_statement("COPY (SELECT 1) TO STDOUT WITH (FORMAT binary)");
958        assert_eq!(check_copy_to_stdout(&statement), Some("binary".to_string()));
959    }
960
961    #[test]
962    fn test_check_copy_out_with_multiple_options() {
963        let statement = parse_copy_statement(
964            "COPY (SELECT 1) TO STDOUT WITH (FORMAT csv, DELIMITER ',', HEADER)",
965        );
966        assert_eq!(check_copy_to_stdout(&statement), Some("csv".to_string()));
967
968        let statement = parse_copy_statement(
969            "COPY (SELECT 1) TO STDOUT WITH (DELIMITER ',', HEADER, FORMAT binary)",
970        );
971        assert_eq!(check_copy_to_stdout(&statement), Some("binary".to_string()));
972    }
973}