1use std::sync::Arc;
16
17use anyhow::Context;
18use itertools::Itertools;
19use mysql_async::consts::ColumnType as MySqlColumnType;
20use mysql_async::prelude::*;
21use risingwave_common::array::arrow::IcebergArrowConvert;
22use risingwave_common::types::{DataType, ScalarImpl, StructType};
23use risingwave_connector::connector_common::{PgConnectionConfig, create_pg_client};
24use risingwave_connector::source::iceberg::{
25 FileScanBackend, extract_bucket_and_file_name, get_parquet_fields, list_data_directory,
26 new_azblob_operator, new_gcs_operator, new_s3_operator,
27};
28use risingwave_pb::expr::PbTableFunction;
29pub use risingwave_pb::expr::table_function::PbType as TableFunctionType;
30use tokio_postgres::types::Type as TokioPgType;
31
32use super::{Expr, ExprImpl, ExprRewriter, Literal, RwResult, infer_type};
33use crate::catalog::function_catalog::{FunctionCatalog, FunctionKind};
34use crate::error::ErrorCode::BindError;
35use crate::expr::reject_impure;
36use crate::utils::FRONTEND_RUNTIME;
37
38#[derive(Clone, Eq, PartialEq, Hash)]
44pub struct TableFunction {
45 pub args: Vec<ExprImpl>,
46 pub return_type: DataType,
47 pub function_type: TableFunctionType,
48 pub user_defined: Option<Arc<FunctionCatalog>>,
50}
51
52impl TableFunction {
53 pub fn new(func_type: TableFunctionType, mut args: Vec<ExprImpl>) -> RwResult<Self> {
56 let return_type = infer_type(func_type.into(), &mut args)?;
57 Ok(TableFunction {
58 args,
59 return_type,
60 function_type: func_type,
61 user_defined: None,
62 })
63 }
64
65 pub fn new_user_defined(catalog: Arc<FunctionCatalog>, args: Vec<ExprImpl>) -> Self {
67 let FunctionKind::Table = &catalog.kind else {
68 panic!("not a table function");
69 };
70 TableFunction {
71 args,
72 return_type: catalog.return_type.clone(),
73 function_type: TableFunctionType::UserDefined,
74 user_defined: Some(catalog),
75 }
76 }
77
78 pub fn new_file_scan(mut args: Vec<ExprImpl>) -> RwResult<Self> {
81 let return_type = {
82 let mut eval_args: Vec<String> = vec![];
89 for arg in &args {
90 if arg.return_type() != DataType::Varchar {
91 return Err(BindError(
92 "file_scan function only accepts string arguments".to_owned(),
93 )
94 .into());
95 }
96 match arg.try_fold_const() {
97 Some(Ok(value)) => {
98 if value.is_none() {
99 return Err(BindError(
100 "file_scan function does not accept null arguments".to_owned(),
101 )
102 .into());
103 }
104 match value {
105 Some(ScalarImpl::Utf8(s)) => {
106 eval_args.push(s.to_string());
107 }
108 _ => {
109 return Err(BindError(
110 "file_scan function only accepts string arguments".to_owned(),
111 )
112 .into());
113 }
114 }
115 }
116 Some(Err(err)) => {
117 return Err(err);
118 }
119 None => {
120 return Err(BindError(
121 "file_scan function only accepts constant arguments".to_owned(),
122 )
123 .into());
124 }
125 }
126 }
127
128 if (eval_args.len() != 4 && eval_args.len() != 6)
129 || (eval_args.len() == 4 && !"gcs".eq_ignore_ascii_case(&eval_args[1]))
130 || (eval_args.len() == 6
131 && !"s3".eq_ignore_ascii_case(&eval_args[1])
132 && !"azblob".eq_ignore_ascii_case(&eval_args[1]))
133 {
134 return Err(BindError(
135 "file_scan function supports three backends: s3, gcs, and azblob. Their formats are as follows: \n
136 file_scan('parquet', 's3', s3_region, s3_access_key, s3_secret_key, file_location) \n
137 file_scan('parquet', 'gcs', credential, service_account, file_location) \n
138 file_scan('parquet', 'azblob', endpoint, account_name, account_key, file_location)"
139 .to_owned(),
140 )
141 .into());
142 }
143 if !"parquet".eq_ignore_ascii_case(&eval_args[0]) {
144 return Err(BindError(
145 "file_scan function only accepts 'parquet' as file format".to_owned(),
146 )
147 .into());
148 }
149
150 if !"s3".eq_ignore_ascii_case(&eval_args[1])
151 && !"gcs".eq_ignore_ascii_case(&eval_args[1])
152 && !"azblob".eq_ignore_ascii_case(&eval_args[1])
153 {
154 return Err(BindError(
155 "file_scan function only accepts 's3', 'gcs' or 'azblob' as storage type"
156 .to_owned(),
157 )
158 .into());
159 }
160
161 #[cfg(madsim)]
162 return Err(crate::error::ErrorCode::BindError(
163 "file_scan can't be used in the madsim mode".to_string(),
164 )
165 .into());
166
167 #[cfg(not(madsim))]
168 {
169 let (file_scan_backend, input_file_location) =
170 if "s3".eq_ignore_ascii_case(&eval_args[1]) {
171 (FileScanBackend::S3, eval_args[5].clone())
172 } else if "gcs".eq_ignore_ascii_case(&eval_args[1]) {
173 (FileScanBackend::Gcs, eval_args[3].clone())
174 } else if "azblob".eq_ignore_ascii_case(&eval_args[1]) {
175 (FileScanBackend::Azblob, eval_args[5].clone())
176 } else {
177 unreachable!();
178 };
179 let op = match file_scan_backend {
180 FileScanBackend::S3 => {
181 let (bucket, _) = extract_bucket_and_file_name(
182 &eval_args[5].clone(),
183 &file_scan_backend,
184 )?;
185
186 let (s3_region, s3_endpoint) = match eval_args[2].starts_with("http") {
187 true => ("us-east-1".to_owned(), eval_args[2].clone()), false => (
189 eval_args[2].clone(),
190 format!("https://{}.s3.{}.amazonaws.com", bucket, eval_args[2],),
191 ),
192 };
193 new_s3_operator(
194 s3_region,
195 eval_args[3].clone(),
196 eval_args[4].clone(),
197 bucket,
198 s3_endpoint,
199 )?
200 }
201 FileScanBackend::Gcs => {
202 let (bucket, _) =
203 extract_bucket_and_file_name(&input_file_location, &file_scan_backend)?;
204
205 new_gcs_operator(eval_args[2].clone(), bucket)?
206 }
207 FileScanBackend::Azblob => {
208 let (bucket, _) =
209 extract_bucket_and_file_name(&input_file_location, &file_scan_backend)?;
210
211 new_azblob_operator(
212 eval_args[2].clone(),
213 eval_args[3].clone(),
214 eval_args[4].clone(),
215 bucket,
216 )?
217 }
218 };
219 let files = if input_file_location.ends_with('/') {
220 let files = tokio::task::block_in_place(|| {
221 FRONTEND_RUNTIME.block_on(async {
222 let files = list_data_directory(
223 op.clone(),
224 input_file_location.clone(),
225 &file_scan_backend,
226 )
227 .await?;
228
229 Ok::<Vec<String>, anyhow::Error>(files)
230 })
231 })?;
232 if files.is_empty() {
233 return Err(BindError(
234 "file_scan function only accepts non-empty directory".to_owned(),
235 )
236 .into());
237 }
238
239 Some(files)
240 } else {
241 None
242 };
243 let schema = tokio::task::block_in_place(|| {
244 FRONTEND_RUNTIME.block_on(async {
245 let location = match files.as_ref() {
246 Some(files) => files[0].clone(),
247 None => input_file_location.clone(),
248 };
249 let (_, file_name) =
250 extract_bucket_and_file_name(&location, &file_scan_backend)?;
251
252 let fields = get_parquet_fields(op, file_name).await?;
253
254 let mut rw_types = vec![];
255 for field in &fields {
256 rw_types.push((
257 field.name().clone(),
258 IcebergArrowConvert.type_from_field(field)?,
259 ));
260 }
261
262 Ok::<risingwave_common::types::DataType, anyhow::Error>(DataType::Struct(
263 StructType::new(rw_types),
264 ))
265 })
266 })?;
267
268 if let Some(files) = files {
269 match file_scan_backend {
271 FileScanBackend::S3 => args.remove(5),
272 FileScanBackend::Gcs => args.remove(3),
273 FileScanBackend::Azblob => args.remove(5),
274 };
275 for file in files {
276 args.push(ExprImpl::Literal(Box::new(Literal::new(
277 Some(ScalarImpl::Utf8(file.into())),
278 DataType::Varchar,
279 ))));
280 }
281 }
282
283 schema
284 }
285 };
286
287 Ok(TableFunction {
288 args,
289 return_type,
290 function_type: TableFunctionType::FileScan,
291 user_defined: None,
292 })
293 }
294
295 pub fn new_postgres_query(args: Vec<ExprImpl>) -> RwResult<Self> {
296 let evaled_args = args
297 .iter()
298 .map(expr_impl_to_string_fn)
299 .collect::<RwResult<Vec<_>>>()?;
300
301 #[cfg(madsim)]
302 {
303 return Err(crate::error::ErrorCode::BindError(
304 "postgres_query can't be used in the madsim mode".to_string(),
305 )
306 .into());
307 }
308
309 #[cfg(not(madsim))]
310 {
311 let schema = tokio::task::block_in_place(|| {
312 FRONTEND_RUNTIME.block_on(async {
313 let ssl_mode = evaled_args
314 .get(6)
315 .and_then(|s| s.parse().ok())
316 .unwrap_or_default();
317
318 let ssl_root_cert = evaled_args
319 .get(7)
320 .and_then(|s| if s.is_empty() { None } else { Some(s.clone()) });
321
322 let port = evaled_args[1]
323 .parse::<u16>()
324 .with_context(|| format!("invalid postgres port `{}`", evaled_args[1]))?;
325 let pg_conn = PgConnectionConfig {
326 host: evaled_args[0].clone(),
327 port,
328 user: evaled_args[2].clone(),
329 password: evaled_args[3].clone(),
330 database: evaled_args[4].clone(),
331 ssl_mode,
332 ssl_root_cert,
333 };
334 let client = create_pg_client(&pg_conn, None, None).await?;
335
336 let statement = client.prepare(evaled_args[5].as_str()).await?;
337
338 let mut rw_types = vec![];
339 for column in statement.columns() {
340 let name = column.name().to_owned();
341 let data_type = match *column.type_() {
342 TokioPgType::BOOL => DataType::Boolean,
343 TokioPgType::INT2 => DataType::Int16,
344 TokioPgType::INT4 => DataType::Int32,
345 TokioPgType::INT8 => DataType::Int64,
346 TokioPgType::FLOAT4 => DataType::Float32,
347 TokioPgType::FLOAT8 => DataType::Float64,
348 TokioPgType::NUMERIC => DataType::Decimal,
349 TokioPgType::DATE => DataType::Date,
350 TokioPgType::TIME => DataType::Time,
351 TokioPgType::TIMESTAMP => DataType::Timestamp,
352 TokioPgType::TIMESTAMPTZ => DataType::Timestamptz,
353 TokioPgType::TEXT | TokioPgType::VARCHAR => DataType::Varchar,
354 TokioPgType::INTERVAL => DataType::Interval,
355 TokioPgType::JSONB => DataType::Jsonb,
356 TokioPgType::BYTEA => DataType::Bytea,
357 _ => {
358 return Err(crate::error::ErrorCode::BindError(format!(
359 "unsupported column type: {}",
360 column.type_()
361 ))
362 .into());
363 }
364 };
365 rw_types.push((name, data_type));
366 }
367 Ok::<risingwave_common::types::DataType, anyhow::Error>(DataType::Struct(
368 StructType::new(rw_types),
369 ))
370 })
371 })?;
372
373 Ok(TableFunction {
374 args,
375 return_type: schema,
376 function_type: TableFunctionType::PostgresQuery,
377 user_defined: None,
378 })
379 }
380 }
381
382 pub fn new_mysql_query(args: Vec<ExprImpl>) -> RwResult<Self> {
383 let evaled_args = args
384 .iter()
385 .map(expr_impl_to_string_fn)
386 .collect::<RwResult<Vec<_>>>()?;
387
388 #[cfg(madsim)]
389 {
390 return Err(crate::error::ErrorCode::BindError(
391 "postgres_query can't be used in the madsim mode".to_string(),
392 )
393 .into());
394 }
395
396 #[cfg(not(madsim))]
397 {
398 let schema = tokio::task::block_in_place(|| {
399 FRONTEND_RUNTIME.block_on(async {
400 let database_opts: mysql_async::Opts = {
401 let port = evaled_args[1]
402 .parse::<u16>()
403 .context("failed to parse port")?;
404 mysql_async::OptsBuilder::default()
405 .ip_or_hostname(evaled_args[0].clone())
406 .tcp_port(port)
407 .user(Some(evaled_args[2].clone()))
408 .pass(Some(evaled_args[3].clone()))
409 .db_name(Some(evaled_args[4].clone()))
410 .into()
411 };
412
413 let pool = mysql_async::Pool::new(database_opts);
414 let mut conn = pool
415 .get_conn()
416 .await
417 .context("failed to connect to mysql in binder")?;
418
419 let query = evaled_args[5].clone();
420 let statement = conn
421 .prep(query)
422 .await
423 .context("failed to prepare mysql_query in binder")?;
424
425 let mut rw_types = vec![];
426
427 for column in statement.columns() {
428 let name = column.name_str().to_string();
429 let data_type = match column.column_type() {
430 MySqlColumnType::MYSQL_TYPE_BIT if column.column_length() == 1 => {
432 DataType::Boolean
433 }
434
435 MySqlColumnType::MYSQL_TYPE_TINY => DataType::Int16,
439 MySqlColumnType::MYSQL_TYPE_SHORT => DataType::Int16,
440 MySqlColumnType::MYSQL_TYPE_INT24 => DataType::Int32,
441 MySqlColumnType::MYSQL_TYPE_LONG => DataType::Int32,
442 MySqlColumnType::MYSQL_TYPE_LONGLONG => DataType::Int64,
443 MySqlColumnType::MYSQL_TYPE_FLOAT => DataType::Float32,
444 MySqlColumnType::MYSQL_TYPE_DOUBLE => DataType::Float64,
445 MySqlColumnType::MYSQL_TYPE_NEWDECIMAL => DataType::Decimal,
446 MySqlColumnType::MYSQL_TYPE_DECIMAL => DataType::Decimal,
447
448 MySqlColumnType::MYSQL_TYPE_YEAR => DataType::Int32,
450 MySqlColumnType::MYSQL_TYPE_DATE => DataType::Date,
451 MySqlColumnType::MYSQL_TYPE_NEWDATE => DataType::Date,
452 MySqlColumnType::MYSQL_TYPE_TIME => DataType::Time,
453 MySqlColumnType::MYSQL_TYPE_TIME2 => DataType::Time,
454 MySqlColumnType::MYSQL_TYPE_DATETIME => DataType::Timestamp,
455 MySqlColumnType::MYSQL_TYPE_DATETIME2 => DataType::Timestamp,
456 MySqlColumnType::MYSQL_TYPE_TIMESTAMP => DataType::Timestamptz,
457 MySqlColumnType::MYSQL_TYPE_TIMESTAMP2 => DataType::Timestamptz,
458
459 MySqlColumnType::MYSQL_TYPE_VARCHAR => DataType::Varchar,
461 MySqlColumnType::MYSQL_TYPE_STRING
464 | MySqlColumnType::MYSQL_TYPE_VAR_STRING => {
465 if column
466 .flags()
467 .contains(mysql_common::constants::ColumnFlags::BINARY_FLAG)
468 {
469 DataType::Bytea
470 } else {
471 DataType::Varchar
472 }
473 }
474
475 MySqlColumnType::MYSQL_TYPE_JSON => DataType::Jsonb,
477
478 MySqlColumnType::MYSQL_TYPE_BIT
480 | MySqlColumnType::MYSQL_TYPE_BLOB
481 | MySqlColumnType::MYSQL_TYPE_TINY_BLOB
482 | MySqlColumnType::MYSQL_TYPE_MEDIUM_BLOB
483 | MySqlColumnType::MYSQL_TYPE_LONG_BLOB => DataType::Bytea,
484
485 MySqlColumnType::MYSQL_TYPE_UNKNOWN
486 | MySqlColumnType::MYSQL_TYPE_TYPED_ARRAY
487 | MySqlColumnType::MYSQL_TYPE_ENUM
488 | MySqlColumnType::MYSQL_TYPE_SET
489 | MySqlColumnType::MYSQL_TYPE_GEOMETRY
490 | MySqlColumnType::MYSQL_TYPE_VECTOR
491 | MySqlColumnType::MYSQL_TYPE_NULL => {
492 return Err(crate::error::ErrorCode::BindError(format!(
493 "unsupported column type: {:?}",
494 column.column_type()
495 ))
496 .into());
497 }
498 };
499 rw_types.push((name, data_type));
500 }
501 Ok::<risingwave_common::types::DataType, anyhow::Error>(DataType::Struct(
502 StructType::new(rw_types),
503 ))
504 })
505 })?;
506
507 Ok(TableFunction {
508 args,
509 return_type: schema,
510 function_type: TableFunctionType::MysqlQuery,
511 user_defined: None,
512 })
513 }
514 }
515
516 pub fn new_internal_backfill_progress() -> Self {
519 TableFunction {
520 args: vec![],
521 return_type: DataType::Struct(StructType::new(vec![
522 ("job_id".to_owned(), DataType::Int32),
523 ("fragment_id".to_owned(), DataType::Int32),
524 ("backfill_state_table_id".to_owned(), DataType::Int32),
525 ("current_row_count".to_owned(), DataType::Int64),
526 ("min_epoch".to_owned(), DataType::Int64),
527 ])),
528 function_type: TableFunctionType::InternalBackfillProgress,
529 user_defined: None,
530 }
531 }
532
533 pub fn new_internal_source_backfill_progress() -> Self {
534 TableFunction {
535 args: vec![],
536 return_type: DataType::Struct(StructType::new(vec![
537 ("job_id".to_owned(), DataType::Int32),
538 ("fragment_id".to_owned(), DataType::Int32),
539 ("backfill_state_table_id".to_owned(), DataType::Int32),
540 ("partition_id".to_owned(), DataType::Varchar),
541 ("backfill_progress".to_owned(), DataType::Jsonb),
542 ])),
543 function_type: TableFunctionType::InternalSourceBackfillProgress,
544 user_defined: None,
545 }
546 }
547
548 pub fn new_internal_get_channel_delta_stats(args: Vec<ExprImpl>) -> Self {
549 Self {
550 args,
551 return_type: DataType::Struct(StructType::new(vec![
552 ("upstream_fragment_id".to_owned(), DataType::Int32),
553 ("downstream_fragment_id".to_owned(), DataType::Int32),
554 ("backpressure_rate".to_owned(), DataType::Float64),
555 ("recv_throughput".to_owned(), DataType::Float64),
556 ("send_throughput".to_owned(), DataType::Float64),
557 ])),
558 function_type: TableFunctionType::InternalGetChannelDeltaStats,
559 user_defined: None,
560 }
561 }
562
563 pub fn to_protobuf(&self) -> PbTableFunction {
564 PbTableFunction {
565 function_type: self.function_type as i32,
566 args: self.args.iter().map(|c| c.to_expr_proto()).collect_vec(),
567 return_type: Some(self.return_type.to_protobuf()),
568 udf: self.user_defined.as_ref().map(|c| c.as_ref().into()),
569 }
570 }
571
572 pub fn to_protobuf_checked_pure(&self, retract: bool) -> crate::error::Result<PbTableFunction> {
575 if retract {
576 reject_impure(self.clone(), "table function")?;
577 }
578
579 let args = self
580 .args
581 .iter()
582 .map(|arg| arg.to_expr_proto_checked_pure(retract, "table function argument"))
583 .collect::<crate::error::Result<Vec<_>>>()?;
584
585 Ok(PbTableFunction {
586 function_type: self.function_type as i32,
587 args,
588 return_type: Some(self.return_type.to_protobuf()),
589 udf: self.user_defined.as_ref().map(|c| c.as_ref().into()),
590 })
591 }
592
593 pub fn name(&self) -> String {
595 match self.function_type {
596 TableFunctionType::UserDefined => self.user_defined.as_ref().unwrap().name.clone(),
597 t => t.as_str_name().to_lowercase(),
598 }
599 }
600
601 pub fn rewrite(self, rewriter: &mut impl ExprRewriter) -> Self {
602 Self {
603 args: self
604 .args
605 .into_iter()
606 .map(|e| rewriter.rewrite_expr(e))
607 .collect(),
608 ..self
609 }
610 }
611}
612
613impl std::fmt::Debug for TableFunction {
614 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
615 if f.alternate() {
616 f.debug_struct("FunctionCall")
617 .field("function_type", &self.function_type)
618 .field("return_type", &self.return_type)
619 .field("args", &self.args)
620 .finish()
621 } else {
622 let func_name = format!("{:?}", self.function_type);
623 let mut builder = f.debug_tuple(&func_name);
624 self.args.iter().for_each(|child| {
625 builder.field(child);
626 });
627 builder.finish()
628 }
629 }
630}
631
632impl Expr for TableFunction {
633 fn return_type(&self) -> DataType {
634 self.return_type.clone()
635 }
636
637 fn try_to_expr_proto(&self) -> Result<risingwave_pb::expr::ExprNode, String> {
638 Err("Table function should not be converted to ExprNode".to_owned())
639 }
640}
641
642pub(crate) fn expr_impl_to_string_fn(arg: &ExprImpl) -> RwResult<String> {
643 match arg.try_fold_const() {
644 Some(Ok(value)) => {
645 let Some(scalar) = value else {
646 return Err(BindError(
647 "postgres_query function and mysql_query function do not accept null arguments"
648 .to_owned(),
649 )
650 .into());
651 };
652 Ok(scalar.into_utf8().to_string())
653 }
654 Some(Err(err)) => Err(err),
655 None => Err(BindError(
656 "postgres_query function and mysql_query function only accept constant arguments"
657 .to_owned(),
658 )
659 .into()),
660 }
661}