diff --git a/Cargo.lock b/Cargo.lock index fac9e97c5f8b9..d3d3cdb04a6cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8166,6 +8166,9 @@ name = "redact" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b97c0a6319ae55341eb213c8ef97002630a3a5bd6f287f0124d077121d3f2a5" +dependencies = [ + "serde", +] [[package]] name = "redis" @@ -8996,6 +8999,7 @@ dependencies = [ "pulsar", "quote", "rand", + "redact", "redis", "regex", "reqwest", diff --git a/src/connector/Cargo.toml b/src/connector/Cargo.toml index 3f469e5ad65ae..3fec1c5248398 100644 --- a/src/connector/Cargo.toml +++ b/src/connector/Cargo.toml @@ -102,6 +102,7 @@ rdkafka = { workspace = true, features = [ "gssapi", "zstd", ] } +redact = { version = "0.1.5", features = ["serde"] } redis = { version = "0.24.0", features = ["aio","tokio-comp","async-std-comp"] } regex = "1.4" reqwest = { version = "0.11", features = ["json"] } diff --git a/src/connector/src/common.rs b/src/connector/src/common.rs index df61dc157b40b..ac49bb71c9439 100644 --- a/src/connector/src/common.rs +++ b/src/connector/src/common.rs @@ -56,6 +56,8 @@ use aws_credential_types::provider::SharedCredentialsProvider; use aws_types::region::Region; use aws_types::SdkConfig; +use crate::source::SecretString; + /// A flatten config map for aws auth. #[derive(Deserialize, Debug, Clone, WithOptions)] pub struct AwsAuthProps { @@ -63,7 +65,7 @@ pub struct AwsAuthProps { #[serde(alias = "endpoint_url")] pub endpoint: Option, pub access_key: Option, - pub secret_key: Option, + pub secret_key: Option, pub session_token: Option, pub arn: Option, /// This field was added for kinesis. Not sure if it's useful for other connectors. @@ -95,7 +97,7 @@ impl AwsAuthProps { Ok(SharedCredentialsProvider::new( aws_credential_types::Credentials::from_keys( self.access_key.as_ref().unwrap(), - self.secret_key.as_ref().unwrap(), + self.secret_key.as_ref().unwrap().expose_secret(), self.session_token.clone(), ), )) @@ -453,7 +455,7 @@ pub struct KinesisCommon { rename = "aws.credentials.secret_access_key", alias = "kinesis.credentials.secret" )] - pub credentials_secret_access_key: Option, + pub credentials_secret_access_key: Option, #[serde( rename = "aws.credentials.session_token", alias = "kinesis.credentials.session_token" diff --git a/src/connector/src/source/common.rs b/src/connector/src/source/common.rs index 11cbfce5d97f5..0668e667c5b99 100644 --- a/src/connector/src/source/common.rs +++ b/src/connector/src/source/common.rs @@ -15,9 +15,11 @@ use futures::{Stream, StreamExt, TryStreamExt}; use futures_async_stream::try_stream; use risingwave_common::error::RwError; +use serde::{Deserialize, Serialize, Serializer}; use crate::parser::ParserConfig; use crate::source::{SourceContextRef, SourceMessage, SplitReader, StreamChunkWithState}; +use crate::with_options::WithOptions; pub(crate) trait CommonSplitReader: SplitReader + 'static { fn into_data_stream( @@ -74,3 +76,31 @@ pub(crate) async fn into_chunk_stream( yield msg_batch?; } } + +#[derive(Clone, Debug, Deserialize, PartialEq, with_options::WithOptions)] +pub struct SecretString { + inner: redact::Secret, +} + +impl Serialize for SecretString { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + format!("{:?}", self.inner).serialize(serializer) + } +} + +impl WithOptions for redact::Secret {} + +impl SecretString { + pub fn expose_secret(&self) -> &String { + self.inner.expose_secret() + } + + pub fn new(s: impl Into) -> Self { + Self { + inner: redact::Secret::new(s.into()), + } + } +} diff --git a/src/connector/src/source/filesystem/mod.rs b/src/connector/src/source/filesystem/mod.rs index bdd50678cd6e1..867bd24db4303 100644 --- a/src/connector/src/source/filesystem/mod.rs +++ b/src/connector/src/source/filesystem/mod.rs @@ -18,5 +18,5 @@ pub mod file_common; pub mod nd_streaming; pub use file_common::{FsPage, FsPageItem, FsSplit, OpendalFsSplit}; pub mod opendal_source; -mod s3; +pub mod s3; pub mod s3_v2; diff --git a/src/connector/src/source/filesystem/opendal_source/s3_source.rs b/src/connector/src/source/filesystem/opendal_source/s3_source.rs index ef18ffa4b8fec..5b2751b7bb3a6 100644 --- a/src/connector/src/source/filesystem/opendal_source/s3_source.rs +++ b/src/connector/src/source/filesystem/opendal_source/s3_source.rs @@ -49,7 +49,7 @@ impl OpendalEnumerator { } if let Some(secret) = s3_properties.secret { - builder.secret_access_key(&secret); + builder.secret_access_key(secret.expose_secret()); } else { tracing::error!( "secret access key of aws s3 is not set, bucket {}", diff --git a/src/connector/src/source/filesystem/s3/mod.rs b/src/connector/src/source/filesystem/s3/mod.rs index 7c611b874cbc4..907ba65fd4afc 100644 --- a/src/connector/src/source/filesystem/s3/mod.rs +++ b/src/connector/src/source/filesystem/s3/mod.rs @@ -17,17 +17,17 @@ use std::collections::HashMap; pub use enumerator::S3SplitEnumerator; mod source; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; pub use source::S3FileReader; use crate::common::AwsAuthProps; use crate::source::filesystem::FsSplit; -use crate::source::{SourceProperties, UnknownFields}; +use crate::source::{SecretString, SourceProperties, UnknownFields}; pub const S3_CONNECTOR: &str = "s3"; /// These are supported by both `s3` and `s3_v2` (opendal) sources. -#[derive(Clone, Debug, Deserialize, PartialEq, with_options::WithOptions)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, with_options::WithOptions)] pub struct S3PropertiesCommon { #[serde(rename = "s3.region_name")] pub region_name: String, @@ -38,7 +38,7 @@ pub struct S3PropertiesCommon { #[serde(rename = "s3.credentials.access", default)] pub access: Option, #[serde(rename = "s3.credentials.secret", default)] - pub secret: Option, + pub secret: Option, #[serde(rename = "s3.endpoint_url")] pub endpoint_url: Option, } diff --git a/src/connector/src/source/mod.rs b/src/connector/src/source/mod.rs index 6cdc7d30e277a..a4e24e82fddab 100644 --- a/src/connector/src/source/mod.rs +++ b/src/connector/src/source/mod.rs @@ -25,6 +25,7 @@ pub mod nats; pub mod nexmark; pub mod pulsar; pub use base::{UPSTREAM_SOURCE_KEY, *}; +pub use common::SecretString; pub(crate) use common::*; pub use google_pubsub::GOOGLE_PUBSUB_CONNECTOR; pub use kafka::KAFKA_CONNECTOR; diff --git a/src/connector/src/source/pulsar/source/reader.rs b/src/connector/src/source/pulsar/source/reader.rs index 8d80487a7da8b..488230ff753f5 100644 --- a/src/connector/src/source/pulsar/source/reader.rs +++ b/src/connector/src/source/pulsar/source/reader.rs @@ -444,7 +444,7 @@ impl PulsarIcebergReader { if let Some(secret_key) = &self.props.aws_auth_props.secret_key { iceberg_configs.insert( "iceberg.table.io.secret_access_key".to_string(), - secret_key.to_string(), + secret_key.expose_secret().to_string(), ); } diff --git a/src/frontend/src/handler/util.rs b/src/frontend/src/handler/util.rs index c225ccec33dc2..4b31a4fe7ded6 100644 --- a/src/frontend/src/handler/util.rs +++ b/src/frontend/src/handler/util.rs @@ -286,6 +286,9 @@ mod tests { use postgres_types::{ToSql, Type}; use risingwave_common::array::*; use risingwave_common::types::Timestamptz; + use risingwave_connector::source::filesystem::s3::S3PropertiesCommon; + use risingwave_connector::source::SecretString; + use risingwave_sqlparser::ast::{Ident, ObjectName, SqlOption, Value}; use super::*; @@ -441,4 +444,55 @@ mod tests { "1969-12-31 23:59:59.999999+00:00" ); } + + fn to_object_name(s: &str) -> ObjectName { + ObjectName(vec![Ident::new_unchecked(s)]) + } + + #[test] + fn test_redact() { + use risingwave_sqlparser::ast::utils::SqlOptionVecSerializer; + use serde::Serialize; + + let p = S3PropertiesCommon { + region_name: "region".to_string(), + bucket_name: "bucket".to_string(), + match_pattern: Some("pattern".into()), + access: None, + secret: Some(SecretString::new("123")), + endpoint_url: None, + }; + let mut s = SqlOptionVecSerializer::default(); + p.serialize(&mut s).unwrap(); + let sql_options: Vec = s.into(); + assert_eq!( + sql_options, + vec![ + SqlOption { + name: to_object_name("s3.region_name"), + value: Value::SingleQuotedString("region".into()) + }, + SqlOption { + name: to_object_name("s3.bucket_name"), + value: Value::SingleQuotedString("bucket".into()) + }, + SqlOption { + name: to_object_name("match_pattern"), + value: Value::SingleQuotedString("pattern".into()) + }, + SqlOption { + name: to_object_name("s3.credentials.access"), + value: Value::Null + }, + SqlOption { + name: to_object_name("s3.credentials.secret"), + value: Value::SingleQuotedString("[REDACTED alloc::string::String]".into()) + }, + SqlOption { + name: to_object_name("s3.endpoint_url"), + value: Value::Null + }, + ] + ); + } } diff --git a/src/sqlparser/Cargo.toml b/src/sqlparser/Cargo.toml index 2bb58461302b3..0cda8621545f6 100644 --- a/src/sqlparser/Cargo.toml +++ b/src/sqlparser/Cargo.toml @@ -26,7 +26,7 @@ normal = ["workspace-hack"] [dependencies] itertools = "0.12" -serde = { version = "1.0", features = ["derive"], optional = true } +serde = { version = "1.0", features = ["derive"] } tracing = "0.1" [target.'cfg(not(madsim))'.dependencies] diff --git a/src/sqlparser/src/ast/mod.rs b/src/sqlparser/src/ast/mod.rs index eef14722ee841..45d44fc2b7f2f 100644 --- a/src/sqlparser/src/ast/mod.rs +++ b/src/sqlparser/src/ast/mod.rs @@ -17,6 +17,7 @@ mod legacy_source; mod operator; mod query; mod statement; +pub mod utils; mod value; #[cfg(not(feature = "std"))] diff --git a/src/sqlparser/src/ast/utils.rs b/src/sqlparser/src/ast/utils.rs new file mode 100644 index 0000000000000..774f6d9edccf8 --- /dev/null +++ b/src/sqlparser/src/ast/utils.rs @@ -0,0 +1,574 @@ +// Copyright 2024 RisingWave Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use core::fmt::{Debug, Display, Formatter}; + +use serde::ser::{Impossible, StdError}; +use serde::{ser, Serialize}; + +use crate::ast::{Ident, ObjectName, SqlOption, Value}; + +#[derive(Debug)] +pub enum Error { + Message(String), + NotSupported(String), +} + +impl ser::Error for Error { + fn custom(msg: T) -> Self + where + T: Display, + { + Error::Message(msg.to_string()) + } +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + Error::Message(msg) => f.write_str(msg), + Error::NotSupported(msg) => f.write_str(msg), + } + } +} + +impl StdError for Error {} + +#[derive(Default)] +struct ValueSerializer {} + +impl serde::Serializer for ValueSerializer { + type Error = Error; + type Ok = Value; + type SerializeMap = Impossible; + type SerializeSeq = Impossible; + type SerializeStruct = Impossible; + type SerializeStructVariant = Impossible; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + + fn serialize_bool(self, v: bool) -> Result { + Ok(Value::Boolean(v)) + } + + fn serialize_i8(self, v: i8) -> Result { + Ok(Value::Number(v.to_string())) + } + + fn serialize_i16(self, v: i16) -> Result { + Ok(Value::Number(v.to_string())) + } + + fn serialize_i32(self, v: i32) -> Result { + Ok(Value::Number(v.to_string())) + } + + fn serialize_i64(self, v: i64) -> Result { + Ok(Value::Number(v.to_string())) + } + + fn serialize_u8(self, v: u8) -> Result { + Ok(Value::Number(v.to_string())) + } + + fn serialize_u16(self, v: u16) -> Result { + Ok(Value::Number(v.to_string())) + } + + fn serialize_u32(self, v: u32) -> Result { + Ok(Value::Number(v.to_string())) + } + + fn serialize_u64(self, v: u64) -> Result { + Ok(Value::Number(v.to_string())) + } + + fn serialize_f32(self, v: f32) -> Result { + Ok(Value::Number(v.to_string())) + } + + fn serialize_f64(self, v: f64) -> Result { + Ok(Value::Number(v.to_string())) + } + + fn serialize_char(self, v: char) -> Result { + Ok(Value::SingleQuotedString(v.to_string())) + } + + fn serialize_str(self, v: &str) -> Result { + Ok(Value::SingleQuotedString(v.to_string())) + } + + fn serialize_bytes(self, _v: &[u8]) -> Result { + Err(Error::NotSupported("serialize_bytes".into())) + } + + fn serialize_none(self) -> Result { + Ok(Value::Null) + } + + fn serialize_some(self, value: &T) -> Result + where + T: Serialize, + { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + Err(Error::NotSupported("serialize_unit".into())) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + Err(Error::NotSupported("serialize_unit_struct".into())) + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + Err(Error::NotSupported("serialize_unit_variant".into())) + } + + fn serialize_newtype_struct( + self, + _name: &'static str, + _value: &T, + ) -> Result + where + T: Serialize, + { + Err(Error::NotSupported("serialize_newtype_struct".into())) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: Serialize, + { + Err(Error::NotSupported("serialize_newtype_variant".into())) + } + + fn serialize_seq(self, _len: Option) -> Result { + Err(Error::NotSupported("serialize_seq".into())) + } + + fn serialize_tuple(self, _len: usize) -> Result { + Err(Error::NotSupported("serialize_tuple".into())) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(Error::NotSupported("serialize_tuple_struct".into())) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(Error::NotSupported("serialize_tuple_variant".into())) + } + + fn serialize_map(self, _len: Option) -> Result { + Err(Error::NotSupported("serialize_map".into())) + } + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(Error::NotSupported("serialize_struct".into())) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(Error::NotSupported("serialize_struct_variant".into())) + } +} + +#[derive(Default)] +pub struct SqlOptionVecSerializer { + output: Vec, + last_name: Option, +} + +impl From for Vec { + fn from(value: SqlOptionVecSerializer) -> Self { + value.output + } +} + +impl<'a> ser::SerializeMap for &'a mut SqlOptionVecSerializer { + type Error = Error; + type Ok = (); + + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + assert!(self.last_name.take().is_none()); + let Value::SingleQuotedString(name) = key.serialize(ValueSerializer::default())? else { + return Err(Error::Message("expect key of string type".into())); + }; + self.last_name = Some(ObjectName(vec![Ident::new_unchecked(name)])); + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + let name = self + .last_name + .take() + .ok_or_else(|| Error::Message("expect name".into()))?; + let value = value.serialize(ValueSerializer::default())?; + self.output.push(SqlOption { name, value }); + Ok(()) + } + + fn end(self) -> Result { + Ok(()) + } +} + +impl<'a> ser::SerializeStruct for &'a mut SqlOptionVecSerializer { + type Error = Error; + type Ok = (); + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + let value_serializer = ValueSerializer::default(); + let sql_option = SqlOption { + name: ObjectName(vec![Ident::new_unchecked(key)]), + value: value.serialize(value_serializer)?, + }; + self.output.push(sql_option); + Ok(()) + } + + fn end(self) -> Result { + Ok(()) + } +} + +impl<'a> serde::Serializer for &'a mut SqlOptionVecSerializer { + type Error = Error; + type Ok = (); + type SerializeMap = Self; + type SerializeSeq = Impossible<(), Error>; + type SerializeStruct = Self; + type SerializeStructVariant = Impossible<(), Error>; + type SerializeTuple = Impossible<(), Error>; + type SerializeTupleStruct = Impossible<(), Error>; + type SerializeTupleVariant = Impossible<(), Error>; + + fn serialize_bool(self, _v: bool) -> Result { + Err(Error::NotSupported("serialize_bool".into())) + } + + fn serialize_i8(self, _v: i8) -> Result { + Err(Error::NotSupported("serialize_i8".into())) + } + + fn serialize_i16(self, _v: i16) -> Result { + Err(Error::NotSupported("serialize_i16".into())) + } + + fn serialize_i32(self, _v: i32) -> Result { + Err(Error::NotSupported("serialize_i32".into())) + } + + fn serialize_i64(self, _v: i64) -> Result { + Err(Error::NotSupported("serialize_i64".into())) + } + + fn serialize_u8(self, _v: u8) -> Result { + Err(Error::NotSupported("serialize_u8".into())) + } + + fn serialize_u16(self, _v: u16) -> Result { + Err(Error::NotSupported("serialize_u16".into())) + } + + fn serialize_u32(self, _v: u32) -> Result { + Err(Error::NotSupported("serialize_u32".into())) + } + + fn serialize_u64(self, _v: u64) -> Result { + Err(Error::NotSupported("serialize_u64".into())) + } + + fn serialize_f32(self, _v: f32) -> Result { + Err(Error::NotSupported("serialize_f32".into())) + } + + fn serialize_f64(self, _v: f64) -> Result { + Err(Error::NotSupported("serialize_f64".into())) + } + + fn serialize_char(self, _v: char) -> Result { + Err(Error::NotSupported("serialize_char".into())) + } + + fn serialize_str(self, _v: &str) -> Result { + Err(Error::NotSupported("serialize_str".into())) + } + + fn serialize_bytes(self, _v: &[u8]) -> Result { + Err(Error::NotSupported("serialize_bytes".into())) + } + + fn serialize_none(self) -> Result { + Ok(()) + } + + fn serialize_some(self, value: &T) -> Result + where + T: Serialize, + { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + Err(Error::NotSupported("serialize_unit".into())) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + Err(Error::NotSupported("serialize_unit_struct".into())) + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + Err(Error::NotSupported("serialize_unit_variant".into())) + } + + fn serialize_newtype_struct( + self, + _name: &'static str, + value: &T, + ) -> Result + where + T: Serialize, + { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: Serialize, + { + Err(Error::NotSupported("serialize_newtype_variant".into())) + } + + fn serialize_seq(self, _len: Option) -> Result { + Err(Error::NotSupported("serialize_seq".into())) + } + + fn serialize_tuple(self, _len: usize) -> Result { + Err(Error::NotSupported("serialize_tuple".into())) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(Error::NotSupported("serialize_tuple_struct".into())) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(Error::NotSupported("serialize_tuple_variant".into())) + } + + fn serialize_map(self, _len: Option) -> Result { + Ok(self) + } + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Ok(self) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(Error::NotSupported("serialize_struct_variant".into())) + } +} + +#[cfg(test)] +mod tests { + use serde::Serialize; + + use crate::ast::utils::SqlOptionVecSerializer; + use crate::ast::{Ident, ObjectName, SqlOption, Value}; + + fn to_object_name(s: &str) -> ObjectName { + ObjectName(vec![Ident::new_unchecked(s)]) + } + + #[test] + fn test_serializer_basic() { + #[derive(Serialize)] + struct Foo { + a: String, + #[serde(rename = "foo.2.b")] + b: u32, + c: Option, + d: Option, + } + + let mut serializer = SqlOptionVecSerializer::default(); + let foo = Foo { + a: "v_a".to_string(), + b: 2, + c: Some(1.5f32), + d: None, + }; + foo.serialize(&mut serializer).unwrap(); + let sql_option: Vec = serializer.into(); + assert_eq!( + sql_option, + vec![ + SqlOption { + name: to_object_name("a"), + value: Value::SingleQuotedString("v_a".into()) + }, + SqlOption { + name: to_object_name("foo.2.b"), + value: Value::Number("2".into()) + }, + SqlOption { + name: to_object_name("c"), + value: Value::Number("1.5".into()) + }, + SqlOption { + name: to_object_name("d"), + value: Value::Null + }, + ] + ); + } + + #[test] + fn test_serializer_flatten() { + #[derive(Serialize)] + struct Foo { + #[serde(flatten)] + x: Option, + #[serde(flatten)] + y: F1, + } + #[derive(Serialize)] + struct Bar { + y: u32, + #[serde(flatten)] + a: Option, + #[serde(flatten)] + b: F2, + } + #[derive(Serialize)] + struct F1 { + f1_a: u32, + } + + #[derive(Serialize)] + struct F2 { + f2_a: u32, + } + + let mut serializer = SqlOptionVecSerializer::default(); + let foo = Foo { + x: Some(Bar { + y: 1, + a: Some(F1 { f1_a: 5 }), + b: F2 { f2_a: 10 }, + }), + y: F1 { f1_a: 100 }, + }; + foo.serialize(&mut serializer).unwrap(); + let sql_option: Vec = serializer.into(); + // duplicated name `f1_a` is allowed + assert_eq!( + sql_option, + vec![ + SqlOption { + name: to_object_name("y"), + value: Value::Number("1".into()) + }, + SqlOption { + name: to_object_name("f1_a"), + value: Value::Number("5".into()) + }, + SqlOption { + name: to_object_name("f2_a"), + value: Value::Number("10".into()) + }, + SqlOption { + name: to_object_name("f1_a"), + value: Value::Number("100".into()) + }, + ] + ); + } +}