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