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