1use 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 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
103pub 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
154pub fn pg_value_format(
156 data_type: &DataType,
157 d: ScalarRefImpl<'_>,
158 format: Format,
159 session_data: &StaticSessionData,
160) -> RwResult<Bytes> {
161 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
225pub 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 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 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#[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 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}