Skip to main content

risingwave_frontend/handler/
util.rs

1// Copyright 2022 RisingWave Labs
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 core::str::FromStr;
16use std::collections::HashSet;
17use std::pin::Pin;
18use std::sync::Arc;
19use std::task::{Context, Poll};
20
21use anyhow::Context as _;
22use bytes::{Bytes, BytesMut};
23use futures::Stream;
24use itertools::Itertools;
25use pgwire::pg_field_descriptor::PgFieldDescriptor;
26use pgwire::pg_response::RowSetResult;
27use pgwire::pg_server::BoxedError;
28use pgwire::types::{Format, FormatIterator, Row};
29use pin_project_lite::pin_project;
30use risingwave_common::array::DataChunk;
31use risingwave_common::catalog::Field;
32use risingwave_common::config::FrontendConfig;
33use risingwave_common::id::ObjectId;
34use risingwave_common::row::Row as _;
35use risingwave_common::types::{
36    DataType, Interval, ScalarRefImpl, Timestamptz, write_date_time_tz,
37};
38use risingwave_common::util::epoch::Epoch;
39use risingwave_common::util::iter_util::ZipEqFast;
40use risingwave_connector::sink::elasticsearch_opensearch::elasticsearch::ES_SINK;
41use risingwave_connector::sink::file_sink::fs::FS_SINK;
42use risingwave_connector::source::iceberg::ICEBERG_CONNECTOR;
43use risingwave_connector::source::{BATCH_POSIX_FS_CONNECTOR, KAFKA_CONNECTOR, POSIX_FS_CONNECTOR};
44use risingwave_pb::catalog::connection_params::PbConnectionType;
45use risingwave_sqlparser::ast::{
46    CompatibleFormatEncode, FormatEncodeOptions, ObjectName, Query, Select, SelectItem, SetExpr,
47    TableFactor, TableWithJoins,
48};
49use thiserror_ext::AsReport;
50use tokio::select;
51use tokio::time::{Duration, sleep};
52
53use crate::catalog::root_catalog::SchemaPath;
54use crate::error::ErrorCode::ProtocolError;
55use crate::error::{ErrorCode, Result as RwResult, RwError};
56use crate::session::SessionImpl;
57use crate::{Binder, TableCatalog};
58
59pub fn ensure_local_fs_connector_allowed(session: &SessionImpl, connector: &str) -> RwResult<()> {
60    let is_local_fs_connector = is_local_fs_connector(connector);
61
62    if !is_local_fs_connector
63        || is_local_fs_connector_enabled(session.env().frontend_config(), connector)
64    {
65        return Ok(());
66    }
67
68    Err(RwError::from(ProtocolError(format!(
69        "local filesystem connector '{}' is disabled. Set `frontend.unsafe_enable_local_fs_connector = true` in `risingwave.toml` to enable it.",
70        connector
71    ))))
72}
73
74fn is_local_fs_connector_enabled(frontend_config: &FrontendConfig, connector: &str) -> bool {
75    !is_local_fs_connector(connector) || frontend_config.unsafe_enable_local_fs_connector
76}
77
78fn is_local_fs_connector(connector: &str) -> bool {
79    connector.eq_ignore_ascii_case(POSIX_FS_CONNECTOR)
80        || connector.eq_ignore_ascii_case(BATCH_POSIX_FS_CONNECTOR)
81        || connector.eq_ignore_ascii_case(FS_SINK)
82}
83
84pin_project! {
85    /// Wrapper struct that converts a stream of DataChunk to a stream of RowSet based on formatting
86    /// parameters.
87    ///
88    /// This is essentially `StreamExt::map(self, move |res| res.map(|chunk| to_pg_rows(chunk,
89    /// format)))` but we need a nameable type as part of [`super::PgResponseStream`], but we cannot
90    /// name the type of a closure.
91    pub struct DataChunkToRowSetAdapter<VS>
92    where
93        VS: Stream<Item = Result<DataChunk, BoxedError>>,
94    {
95        #[pin]
96        chunk_stream: VS,
97        column_types: Vec<DataType>,
98        pub formats: Vec<Format>,
99        session_data: StaticSessionData,
100    }
101}
102
103// Static session data frozen at the time of the creation of the stream
104pub struct StaticSessionData {
105    pub timezone: String,
106}
107
108impl<VS> DataChunkToRowSetAdapter<VS>
109where
110    VS: Stream<Item = Result<DataChunk, BoxedError>>,
111{
112    pub fn new(
113        chunk_stream: VS,
114        column_types: Vec<DataType>,
115        formats: Vec<Format>,
116        session: Arc<SessionImpl>,
117    ) -> Self {
118        let session_data = StaticSessionData {
119            timezone: session.config().timezone(),
120        };
121        Self {
122            chunk_stream,
123            column_types,
124            formats,
125            session_data,
126        }
127    }
128}
129
130impl<VS> Stream for DataChunkToRowSetAdapter<VS>
131where
132    VS: Stream<Item = Result<DataChunk, BoxedError>>,
133{
134    type Item = RowSetResult;
135
136    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
137        let mut this = self.project();
138        match this.chunk_stream.as_mut().poll_next(cx) {
139            Poll::Pending => Poll::Pending,
140            Poll::Ready(chunk) => match chunk {
141                Some(chunk_result) => match chunk_result {
142                    Ok(chunk) => Poll::Ready(Some(
143                        to_pg_rows(this.column_types, chunk, this.formats, this.session_data)
144                            .map_err(|err| err.into()),
145                    )),
146                    Err(err) => Poll::Ready(Some(Err(err))),
147                },
148                None => Poll::Ready(None),
149            },
150        }
151    }
152}
153
154/// Format scalars according to postgres convention.
155pub fn pg_value_format(
156    data_type: &DataType,
157    d: ScalarRefImpl<'_>,
158    format: Format,
159    session_data: &StaticSessionData,
160) -> RwResult<Bytes> {
161    // format == false means TEXT format
162    // format == true means BINARY format
163    match format {
164        Format::Text => {
165            if *data_type == DataType::Timestamptz {
166                Ok(timestamptz_to_string_with_session_data(d, session_data))
167            } else {
168                Ok(d.text_format(data_type).into())
169            }
170        }
171        Format::Binary => Ok(d
172            .binary_format(data_type)
173            .context("failed to format binary value")?),
174    }
175}
176
177fn timestamptz_to_string_with_session_data(
178    d: ScalarRefImpl<'_>,
179    session_data: &StaticSessionData,
180) -> Bytes {
181    let tz = d.into_timestamptz();
182    let time_zone = Timestamptz::lookup_time_zone(&session_data.timezone).unwrap();
183    let instant_local = tz.to_datetime_in_zone(time_zone);
184    let mut result_string = BytesMut::new();
185    write_date_time_tz(instant_local, &mut result_string).unwrap();
186    result_string.into()
187}
188
189fn to_pg_rows(
190    column_types: &[DataType],
191    chunk: DataChunk,
192    formats: &[Format],
193    session_data: &StaticSessionData,
194) -> RwResult<Vec<Row>> {
195    assert_eq!(chunk.dimension(), column_types.len());
196    if cfg!(debug_assertions) {
197        let chunk_data_types = chunk.data_types();
198        for (ty1, ty2) in chunk_data_types.iter().zip_eq_fast(column_types) {
199            debug_assert!(
200                ty1.equals_datatype(ty2),
201                "chunk_data_types: {chunk_data_types:?}, column_types: {column_types:?}"
202            )
203        }
204    }
205
206    chunk
207        .rows()
208        .map(|r| {
209            let format_iter = FormatIterator::new(formats, chunk.dimension())
210                .map_err(ErrorCode::InternalError)?;
211            let row = r
212                .iter()
213                .zip_eq_fast(column_types)
214                .zip_eq_fast(format_iter)
215                .map(|((data, t), format)| match data {
216                    Some(data) => Some(pg_value_format(t, data, format, session_data)).transpose(),
217                    None => Ok(None),
218                })
219                .try_collect()?;
220            Ok(Row::new(row))
221        })
222        .try_collect()
223}
224
225/// Convert from [`Field`] to [`PgFieldDescriptor`].
226pub fn to_pg_field(f: &Field) -> PgFieldDescriptor {
227    PgFieldDescriptor::new(
228        f.name.clone(),
229        f.data_type().to_oid(),
230        f.data_type().type_len(),
231    )
232}
233
234#[easy_ext::ext(SourceSchemaCompatExt)]
235impl CompatibleFormatEncode {
236    /// Convert `self` to [`FormatEncodeOptions`] and warn the user if the syntax is deprecated.
237    pub fn into_v2_with_warning(self) -> FormatEncodeOptions {
238        match self {
239            CompatibleFormatEncode::V2(inner) => inner,
240        }
241    }
242}
243
244pub fn gen_query_from_table_name(from_name: ObjectName) -> Query {
245    let table_factor = TableFactor::Table {
246        name: from_name,
247        alias: None,
248        as_of: None,
249    };
250    let from = vec![TableWithJoins {
251        relation: table_factor,
252        joins: vec![],
253    }];
254    let select = Select {
255        from,
256        projection: vec![SelectItem::Wildcard(None)],
257        ..Default::default()
258    };
259    let body = SetExpr::Select(Box::new(select));
260    Query {
261        with: None,
262        body,
263        order_by: vec![],
264        limit: None,
265        offset: None,
266        fetch: None,
267    }
268}
269
270pub fn convert_unix_millis_to_logstore_u64(unix_millis: u64) -> u64 {
271    Epoch::from_unix_millis(unix_millis).0
272}
273
274pub fn convert_logstore_u64_to_unix_millis(logstore_u64: u64) -> u64 {
275    Epoch::from(logstore_u64).as_unix_millis()
276}
277
278pub fn convert_interval_to_u64_seconds(interval: &String) -> RwResult<u64> {
279    let seconds = (Interval::from_str(interval)
280        .map_err(|err| {
281            ErrorCode::InternalError(format!(
282                "Convert interval to u64 error, please check format, error: {:?}",
283                err.to_report_string()
284            ))
285        })?
286        .epoch_in_micros()
287        / 1000000) as u64;
288    Ok(seconds)
289}
290
291pub fn ensure_connection_type_allowed(
292    connection_type: PbConnectionType,
293    allowed_types: &HashSet<PbConnectionType>,
294) -> RwResult<()> {
295    if !allowed_types.contains(&connection_type) {
296        return Err(RwError::from(ProtocolError(format!(
297            "connection type {:?} is not allowed, allowed types: {:?}",
298            connection_type, allowed_types
299        ))));
300    }
301    Ok(())
302}
303
304fn connection_type_to_connector(connection_type: &PbConnectionType) -> &str {
305    match connection_type {
306        PbConnectionType::Kafka => KAFKA_CONNECTOR,
307        PbConnectionType::Iceberg => ICEBERG_CONNECTOR,
308        PbConnectionType::Elasticsearch => ES_SINK,
309        _ => unreachable!(),
310    }
311}
312
313pub fn check_connector_match_connection_type(
314    connector: &str,
315    connection_type: &PbConnectionType,
316) -> RwResult<()> {
317    if !connector.eq(connection_type_to_connector(connection_type)) {
318        return Err(RwError::from(ProtocolError(format!(
319            "connector {} and connection type {:?} are not compatible",
320            connector, connection_type
321        ))));
322    }
323    Ok(())
324}
325
326pub fn get_table_catalog_by_table_name(
327    session: &SessionImpl,
328    table_name: &ObjectName,
329) -> RwResult<(Arc<TableCatalog>, String)> {
330    let db_name = &session.database();
331    let (schema_name, real_table_name) =
332        Binder::resolve_schema_qualified_name(db_name, table_name)?;
333    let search_path = session.config().search_path();
334    let user_name = &session.user_name();
335
336    let schema_path = SchemaPath::new(schema_name.as_deref(), &search_path, user_name);
337    let reader = session.env().catalog_reader().read_guard();
338    match reader.get_created_table_by_name(db_name, schema_path, &real_table_name) {
339        Ok((table, schema_name)) => Ok((table.clone(), schema_name.to_owned())),
340        Err(err) => {
341            if let Some(table) = session
342                .staging_catalog_manager()
343                .get_table(&real_table_name)
344            {
345                // During CREATE TABLE (iceberg engine), the table is only in staging, but we
346                // still need a stable schema name for internal sink planning.
347                let schema_name = reader
348                    .get_schema_by_id(table.database_id, table.schema_id)
349                    .map(|schema| schema.name.clone())?;
350                Ok((Arc::new(table.clone()), schema_name))
351            } else {
352                Err(err.into())
353            }
354        }
355    }
356}
357
358pub fn reject_internal_table_dependency(
359    table: &TableCatalog,
360    statement_name: &str,
361) -> RwResult<()> {
362    if table.is_internal_table() {
363        return Err(RwError::from(ErrorCode::InvalidInputSyntax(format!(
364            "{statement_name} does not support internal table \"{}\"",
365            table.name()
366        ))));
367    }
368    Ok(())
369}
370
371pub fn reject_internal_table_dependencies<'a, I>(
372    session: &SessionImpl,
373    dependent_relations: I,
374    statement_name: &str,
375) -> RwResult<()>
376where
377    I: IntoIterator<Item = &'a ObjectId>,
378{
379    let catalog_reader = session.env().catalog_reader().read_guard();
380    for object_id in dependent_relations {
381        if let Ok(table) = catalog_reader.get_any_table_by_id(object_id.as_table_id()) {
382            reject_internal_table_dependency(table.as_ref(), statement_name)?;
383        }
384    }
385    Ok(())
386}
387
388/// Execute an async operation with a notification if it takes too long.
389/// This is useful for operations that might be delayed due to high barrier latency.
390///
391/// The notification timeout duration is controlled by the `slow_ddl_notification_secs` session variable.
392///
393/// # Arguments
394/// * `operation_fut` - The async operation to execute
395/// * `session` - The session to send notifications to
396/// * `operation_name` - The name of the operation for the notification message (e.g., "DROP TABLE")
397///
398/// # Example
399/// ```ignore
400/// execute_with_long_running_notification(
401///     catalog_writer.drop_table(source_id, table_id, cascade),
402///     &session,
403///     "DROP TABLE",
404/// ).await?;
405/// ```
406#[derive(Clone, Copy)]
407pub enum LongRunningNotificationAction {
408    SuggestRecover,
409    DiagnoseBarrierLatency,
410    MonitorBackfillJob,
411}
412
413impl LongRunningNotificationAction {
414    fn build_message(self, operation_name: &str, notify_timeout_secs: u32) -> String {
415        match self {
416            LongRunningNotificationAction::SuggestRecover => format!(
417                "{} has taken more than {} secs, likely due to high barrier latency.\n\
418                You may trigger cluster recovery to let {} take effect immediately.\n\
419                Run RECOVER in a separate session to trigger recovery.\n\
420                See: https://docs.risingwave.com/sql/commands/sql-recover#recover",
421                operation_name, notify_timeout_secs, operation_name
422            ),
423            LongRunningNotificationAction::DiagnoseBarrierLatency => format!(
424                "{} has taken more than {} secs, likely due to high barrier latency.\n\
425                See: https://docs.risingwave.com/performance/metrics#barrier-monitoring for steps to diagnose high barrier latency.",
426                operation_name, notify_timeout_secs
427            ),
428            LongRunningNotificationAction::MonitorBackfillJob => format!(
429                "{} has taken more than {} secs, barrier latency might be high. Please check barrier latency metrics to confirm.\n\
430                You can also run SHOW JOBS to track the progress of the job.\n\
431                See: https://docs.risingwave.com/performance/metrics#barrier-monitoring and https://docs.risingwave.com/sql/commands/sql-show-jobs",
432                operation_name, notify_timeout_secs
433            ),
434        }
435    }
436}
437
438pub async fn execute_with_long_running_notification<F, T>(
439    operation_fut: F,
440    session: &SessionImpl,
441    operation_name: &str,
442    action: LongRunningNotificationAction,
443) -> RwResult<T>
444where
445    F: std::future::Future<Output = RwResult<T>>,
446{
447    let notify_timeout_secs = session.config().slow_ddl_notification_secs();
448
449    // If timeout is 0, disable notifications and just execute the operation
450    if notify_timeout_secs == 0 {
451        return operation_fut.await;
452    }
453
454    let notify_fut = sleep(Duration::from_secs(notify_timeout_secs as u64));
455    tokio::pin!(operation_fut);
456
457    select! {
458        _ = notify_fut => {
459            session.notice_to_user(action.build_message(operation_name, notify_timeout_secs));
460            operation_fut.await
461        }
462        result = &mut operation_fut => {
463            result
464        }
465    }
466}
467
468#[cfg(test)]
469mod tests {
470    use postgres_types::{ToSql, Type};
471    use risingwave_common::array::*;
472
473    use super::*;
474
475    #[test]
476    fn test_to_pg_field() {
477        let field = Field::with_name(DataType::Int32, "v1");
478        let pg_field = to_pg_field(&field);
479        assert_eq!(pg_field.get_name(), "v1");
480        assert_eq!(pg_field.get_type_oid(), DataType::Int32.to_oid());
481    }
482
483    #[test]
484    fn test_to_pg_rows() {
485        let chunk = DataChunk::from_pretty(
486            "i I f    T
487             1 6 6.01 aaa
488             2 . .    .
489             3 7 7.01 vvv
490             4 . .    .  ",
491        );
492        let static_session = StaticSessionData {
493            timezone: "UTC".into(),
494        };
495        let rows = to_pg_rows(
496            &[
497                DataType::Int32,
498                DataType::Int64,
499                DataType::Float32,
500                DataType::Varchar,
501            ],
502            chunk,
503            &[],
504            &static_session,
505        );
506        let expected: Vec<Vec<Option<Bytes>>> = vec![
507            vec![
508                Some("1".into()),
509                Some("6".into()),
510                Some("6.01".into()),
511                Some("aaa".into()),
512            ],
513            vec![Some("2".into()), None, None, None],
514            vec![
515                Some("3".into()),
516                Some("7".into()),
517                Some("7.01".into()),
518                Some("vvv".into()),
519            ],
520            vec![Some("4".into()), None, None, None],
521        ];
522        let vec = rows
523            .unwrap()
524            .into_iter()
525            .map(|r| r.values().iter().cloned().collect_vec())
526            .collect_vec();
527
528        assert_eq!(vec, expected);
529    }
530
531    #[test]
532    fn test_to_pg_rows_mix_format() {
533        let chunk = DataChunk::from_pretty(
534            "i I f    T
535             1 6 6.01 aaa
536            ",
537        );
538        let static_session = StaticSessionData {
539            timezone: "UTC".into(),
540        };
541        let rows = to_pg_rows(
542            &[
543                DataType::Int32,
544                DataType::Int64,
545                DataType::Float32,
546                DataType::Varchar,
547            ],
548            chunk,
549            &[Format::Binary, Format::Binary, Format::Binary, Format::Text],
550            &static_session,
551        );
552        let mut raw_params = vec![BytesMut::new(); 3];
553        1_i32.to_sql(&Type::ANY, &mut raw_params[0]).unwrap();
554        6_i64.to_sql(&Type::ANY, &mut raw_params[1]).unwrap();
555        6.01_f32.to_sql(&Type::ANY, &mut raw_params[2]).unwrap();
556        let raw_params = raw_params
557            .into_iter()
558            .map(|b| b.freeze())
559            .collect::<Vec<_>>();
560        let expected: Vec<Vec<Option<Bytes>>> = vec![vec![
561            Some(raw_params[0].clone()),
562            Some(raw_params[1].clone()),
563            Some(raw_params[2].clone()),
564            Some("aaa".into()),
565        ]];
566        let vec = rows
567            .unwrap()
568            .into_iter()
569            .map(|r| r.values().iter().cloned().collect_vec())
570            .collect_vec();
571
572        assert_eq!(vec, expected);
573    }
574
575    #[test]
576    fn test_value_format() {
577        use DataType as T;
578        use ScalarRefImpl as S;
579        let static_session = StaticSessionData {
580            timezone: "UTC".into(),
581        };
582
583        let f = |t, d, f| pg_value_format(t, d, f, &static_session).unwrap();
584        assert_eq!(&f(&T::Float32, S::Float32(1_f32.into()), Format::Text), "1");
585        assert_eq!(
586            &f(&T::Float32, S::Float32(f32::NAN.into()), Format::Text),
587            "NaN"
588        );
589        assert_eq!(
590            &f(&T::Float64, S::Float64(f64::NAN.into()), Format::Text),
591            "NaN"
592        );
593        assert_eq!(
594            &f(&T::Float32, S::Float32(f32::INFINITY.into()), Format::Text),
595            "Infinity"
596        );
597        assert_eq!(
598            &f(
599                &T::Float32,
600                S::Float32(f32::NEG_INFINITY.into()),
601                Format::Text
602            ),
603            "-Infinity"
604        );
605        assert_eq!(
606            &f(&T::Float64, S::Float64(f64::INFINITY.into()), Format::Text),
607            "Infinity"
608        );
609        assert_eq!(
610            &f(
611                &T::Float64,
612                S::Float64(f64::NEG_INFINITY.into()),
613                Format::Text
614            ),
615            "-Infinity"
616        );
617        assert_eq!(&f(&T::Boolean, S::Bool(true), Format::Text), "t");
618        assert_eq!(&f(&T::Boolean, S::Bool(false), Format::Text), "f");
619        assert_eq!(
620            &f(
621                &T::Timestamptz,
622                S::Timestamptz(Timestamptz::from_micros(-1).unwrap()),
623                Format::Text
624            ),
625            "1969-12-31 23:59:59.999999+00:00"
626        );
627    }
628
629    #[test]
630    fn test_local_fs_connector_config_gate() {
631        let frontend_config = FrontendConfig::default();
632        let default_enabled = cfg!(debug_assertions);
633        assert_eq!(
634            is_local_fs_connector_enabled(&frontend_config, POSIX_FS_CONNECTOR),
635            default_enabled
636        );
637        assert_eq!(
638            is_local_fs_connector_enabled(&frontend_config, BATCH_POSIX_FS_CONNECTOR),
639            default_enabled
640        );
641        assert_eq!(
642            is_local_fs_connector_enabled(&frontend_config, FS_SINK),
643            default_enabled
644        );
645        assert!(is_local_fs_connector_enabled(
646            &frontend_config,
647            KAFKA_CONNECTOR
648        ));
649
650        let disabled_config = FrontendConfig {
651            unsafe_enable_local_fs_connector: false,
652            ..Default::default()
653        };
654        assert!(!is_local_fs_connector_enabled(
655            &disabled_config,
656            POSIX_FS_CONNECTOR
657        ));
658        assert!(!is_local_fs_connector_enabled(
659            &disabled_config,
660            BATCH_POSIX_FS_CONNECTOR
661        ));
662        assert!(!is_local_fs_connector_enabled(&disabled_config, FS_SINK));
663
664        let enabled_config = FrontendConfig {
665            unsafe_enable_local_fs_connector: true,
666            ..Default::default()
667        };
668        assert!(is_local_fs_connector_enabled(
669            &enabled_config,
670            POSIX_FS_CONNECTOR
671        ));
672        assert!(is_local_fs_connector_enabled(
673            &enabled_config,
674            BATCH_POSIX_FS_CONNECTOR
675        ));
676        assert!(is_local_fs_connector_enabled(&enabled_config, FS_SINK));
677    }
678}