Skip to main content

risingwave_frontend/expr/
table_function.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 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/// A table function takes a row as input and returns a table. It is also known as Set-Returning
45/// Function.
46///
47/// See also [`TableFunction`](risingwave_expr::table_function::TableFunction) trait in expr crate
48/// and [`ProjectSetSelectItem`](risingwave_pb::expr::ProjectSetSelectItem).
49#[derive(Clone, Eq, PartialEq, Hash)]
50pub struct TableFunction {
51    pub args: Vec<ExprImpl>,
52    pub return_type: DataType,
53    pub function_type: TableFunctionType,
54    /// Catalog of user defined table function.
55    pub user_defined: Option<Arc<FunctionCatalog>>,
56}
57
58impl TableFunction {
59    /// Create a `TableFunction` expr with the return type inferred from `func_type` and types of
60    /// `inputs`.
61    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    /// Create a user-defined `TableFunction`.
72    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    /// A special table function which would be transformed into `LogicalFileScan` by `TableFunctionToFileScanRule` in the optimizer.
85    /// select * from `file_scan`('parquet', 's3', region, ak, sk, location)
86    pub fn new_file_scan(mut args: Vec<ExprImpl>) -> RwResult<Self> {
87        let return_type = {
88            // arguments:
89            // file format e.g. parquet
90            // storage type e.g. s3, gcs, azblob
91            // For s3: file_scan('parquet', 's3', s3_region, s3_access_key, s3_secret_key, file_location_or_directory)
92            // For gcs: file_scan('parquet', 'gcs', credential, file_location_or_directory)
93            // For azblob: file_scan('parquet', 'azblob', endpoint, account_name, account_key, file_location)
94            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()), /* for minio, hard code region as not used but needed. */
194                            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                    // if the file location is a directory, we need to remove the last argument and add all files in the directory as arguments
276                    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                            // Boolean types
527                            MySqlColumnType::MYSQL_TYPE_BIT if column.column_length() == 1 => {
528                                DataType::Boolean
529                            }
530
531                            // Numeric types
532                            // NOTE(kwannoel): Although `bool/boolean` is a synonym of TINY(1) in MySQL,
533                            // we treat it as Int16 here. It is better to be straightforward in our conversion.
534                            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                            // Date time types
545                            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                            // String types
556                            MySqlColumnType::MYSQL_TYPE_VARCHAR => DataType::Varchar,
557                            // mysql_async does not have explicit `varbinary` and `binary` types,
558                            // we need to check the `ColumnFlags` to distinguish them.
559                            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                            // JSON types
572                            MySqlColumnType::MYSQL_TYPE_JSON => DataType::Jsonb,
573
574                            // Binary types
575                            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    /// This is a highly specific _internal_ table function meant to scan and aggregate
613    /// `backfill_table_id`, `row_count` for all MVs which are still being created.
614    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    /// Serialize the table function. Returns an error if this will result in an impure table
669    /// function on a retract stream, which may lead to inconsistent results.
670    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    /// Get the name of the table function.
690    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}