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::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/// A table function takes a row as input and returns a table. It is also known as Set-Returning
39/// Function.
40///
41/// See also [`TableFunction`](risingwave_expr::table_function::TableFunction) trait in expr crate
42/// and [`ProjectSetSelectItem`](risingwave_pb::expr::ProjectSetSelectItem).
43#[derive(Clone, Eq, PartialEq, Hash)]
44pub struct TableFunction {
45    pub args: Vec<ExprImpl>,
46    pub return_type: DataType,
47    pub function_type: TableFunctionType,
48    /// Catalog of user defined table function.
49    pub user_defined: Option<Arc<FunctionCatalog>>,
50}
51
52impl TableFunction {
53    /// Create a `TableFunction` expr with the return type inferred from `func_type` and types of
54    /// `inputs`.
55    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    /// Create a user-defined `TableFunction`.
66    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    /// A special table function which would be transformed into `LogicalFileScan` by `TableFunctionToFileScanRule` in the optimizer.
79    /// select * from `file_scan`('parquet', 's3', region, ak, sk, location)
80    pub fn new_file_scan(mut args: Vec<ExprImpl>) -> RwResult<Self> {
81        let return_type = {
82            // arguments:
83            // file format e.g. parquet
84            // storage type e.g. s3, gcs, azblob
85            // For s3: file_scan('parquet', 's3', s3_region, s3_access_key, s3_secret_key, file_location_or_directory)
86            // For gcs: file_scan('parquet', 'gcs', credential, file_location_or_directory)
87            // For azblob: file_scan('parquet', 'azblob', endpoint, account_name, account_key, file_location)
88            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()), /* for minio, hard code region as not used but needed. */
188                            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                    // if the file location is a directory, we need to remove the last argument and add all files in the directory as arguments
270                    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                            // Boolean types
431                            MySqlColumnType::MYSQL_TYPE_BIT if column.column_length() == 1 => {
432                                DataType::Boolean
433                            }
434
435                            // Numeric types
436                            // NOTE(kwannoel): Although `bool/boolean` is a synonym of TINY(1) in MySQL,
437                            // we treat it as Int16 here. It is better to be straightforward in our conversion.
438                            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                            // Date time types
449                            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                            // String types
460                            MySqlColumnType::MYSQL_TYPE_VARCHAR => DataType::Varchar,
461                            // mysql_async does not have explicit `varbinary` and `binary` types,
462                            // we need to check the `ColumnFlags` to distinguish them.
463                            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                            // JSON types
476                            MySqlColumnType::MYSQL_TYPE_JSON => DataType::Jsonb,
477
478                            // Binary types
479                            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    /// This is a highly specific _internal_ table function meant to scan and aggregate
517    /// `backfill_table_id`, `row_count` for all MVs which are still being created.
518    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    /// Serialize the table function. Returns an error if this will result in an impure table
573    /// function on a retract stream, which may lead to inconsistent results.
574    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    /// Get the name of the table function.
594    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}