diff --git a/datafusion/common/src/config.rs b/datafusion/common/src/config.rs index 1f20bd255027..c3082546b497 100644 --- a/datafusion/common/src/config.rs +++ b/datafusion/common/src/config.rs @@ -210,6 +210,9 @@ config_namespace! { /// When set to true, SQL parser will normalize ident (convert ident to lowercase when not quoted) pub enable_ident_normalization: bool, default = true + /// When set to true, SQL parser will normalize options value (convert value to lowercase) + pub enable_options_value_normalization: bool, default = true + /// Configure the SQL dialect used by DataFusion's parser; supported values include: Generic, /// MySQL, PostgreSQL, Hive, SQLite, Snowflake, Redshift, MsSQL, ClickHouse, BigQuery, and Ansi. pub dialect: String, default = "generic".to_string() diff --git a/datafusion/core/src/execution/session_state.rs b/datafusion/core/src/execution/session_state.rs index 515888519fce..cc2b44cf1933 100644 --- a/datafusion/core/src/execution/session_state.rs +++ b/datafusion/core/src/execution/session_state.rs @@ -512,6 +512,8 @@ impl SessionState { ParserOptions { parse_float_as_decimal: sql_parser_options.parse_float_as_decimal, enable_ident_normalization: sql_parser_options.enable_ident_normalization, + enable_options_value_normalization: sql_parser_options + .enable_options_value_normalization, support_varchar_with_length: sql_parser_options.support_varchar_with_length, } } diff --git a/datafusion/sql/src/cte.rs b/datafusion/sql/src/cte.rs index 3dfe00e3c5e0..4c380f0b37a3 100644 --- a/datafusion/sql/src/cte.rs +++ b/datafusion/sql/src/cte.rs @@ -38,7 +38,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { // Process CTEs from top to bottom for cte in with.cte_tables { // A `WITH` block can't use the same name more than once - let cte_name = self.normalizer.normalize(cte.alias.name.clone()); + let cte_name = self.ident_normalizer.normalize(cte.alias.name.clone()); if planner_context.contains_cte(&cte_name) { return plan_err!( "WITH query name {cte_name:?} specified more than once" diff --git a/datafusion/sql/src/expr/identifier.rs b/datafusion/sql/src/expr/identifier.rs index 9b8356701a40..049600799f3c 100644 --- a/datafusion/sql/src/expr/identifier.rs +++ b/datafusion/sql/src/expr/identifier.rs @@ -50,7 +50,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { // interpret names with '.' as if they were // compound identifiers, but this is not a compound // identifier. (e.g. it is "foo.bar" not foo.bar) - let normalize_ident = self.normalizer.normalize(id); + let normalize_ident = self.ident_normalizer.normalize(id); // Check for qualified field with unqualified name if let Ok((qualifier, _)) = @@ -96,7 +96,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { if ids[0].value.starts_with('@') { let var_names: Vec<_> = ids .into_iter() - .map(|id| self.normalizer.normalize(id)) + .map(|id| self.ident_normalizer.normalize(id)) .collect(); let ty = self .context_provider @@ -110,7 +110,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { } else { let ids = ids .into_iter() - .map(|id| self.normalizer.normalize(id)) + .map(|id| self.ident_normalizer.normalize(id)) .collect::>(); // Currently not supporting more than one nested level diff --git a/datafusion/sql/src/planner.rs b/datafusion/sql/src/planner.rs index 901a2ad38d8c..bf7c3fe0be4f 100644 --- a/datafusion/sql/src/planner.rs +++ b/datafusion/sql/src/planner.rs @@ -24,10 +24,10 @@ use arrow_schema::*; use datafusion_common::{ field_not_found, internal_err, plan_datafusion_err, DFSchemaRef, SchemaError, }; -use sqlparser::ast::TimezoneInfo; use sqlparser::ast::{ArrayElemTypeDef, ExactNumberInfo}; use sqlparser::ast::{ColumnDef as SQLColumnDef, ColumnOption}; use sqlparser::ast::{DataType as SQLDataType, Ident, ObjectName, TableAlias}; +use sqlparser::ast::{TimezoneInfo, Value}; use datafusion_common::TableReference; use datafusion_common::{ @@ -38,8 +38,7 @@ use datafusion_expr::logical_plan::{LogicalPlan, LogicalPlanBuilder}; use datafusion_expr::utils::find_column_exprs; use datafusion_expr::{col, Expr}; -use crate::utils::make_decimal_type; - +use crate::utils::{make_decimal_type, value_to_string}; pub use datafusion_expr::planner::ContextProvider; /// SQL parser options @@ -48,6 +47,7 @@ pub struct ParserOptions { pub parse_float_as_decimal: bool, pub enable_ident_normalization: bool, pub support_varchar_with_length: bool, + pub enable_options_value_normalization: bool, } impl Default for ParserOptions { @@ -56,6 +56,7 @@ impl Default for ParserOptions { parse_float_as_decimal: false, enable_ident_normalization: true, support_varchar_with_length: true, + enable_options_value_normalization: true, } } } @@ -86,6 +87,32 @@ impl IdentNormalizer { } } +/// Value Normalizer +#[derive(Debug)] +pub struct ValueNormalizer { + normalize: bool, +} + +impl Default for ValueNormalizer { + fn default() -> Self { + Self { normalize: true } + } +} + +impl ValueNormalizer { + pub fn new(normalize: bool) -> Self { + Self { normalize } + } + + pub fn normalize(&self, value: Value) -> Option { + match (value_to_string(&value), self.normalize) { + (Some(s), true) => Some(s.to_ascii_lowercase()), + (Some(s), false) => Some(s), + (None, _) => None, + } + } +} + /// Struct to store the states used by the Planner. The Planner will leverage the states to resolve /// CTEs, Views, subqueries and PREPARE statements. The states include /// Common Table Expression (CTE) provided with WITH clause and @@ -184,7 +211,8 @@ impl PlannerContext { pub struct SqlToRel<'a, S: ContextProvider> { pub(crate) context_provider: &'a S, pub(crate) options: ParserOptions, - pub(crate) normalizer: IdentNormalizer, + pub(crate) ident_normalizer: IdentNormalizer, + pub(crate) value_normalizer: ValueNormalizer, } impl<'a, S: ContextProvider> SqlToRel<'a, S> { @@ -195,12 +223,14 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { /// Create a new query planner pub fn new_with_options(context_provider: &'a S, options: ParserOptions) -> Self { - let normalize = options.enable_ident_normalization; + let ident_normalize = options.enable_ident_normalization; + let options_value_normalize = options.enable_options_value_normalization; SqlToRel { context_provider, options, - normalizer: IdentNormalizer::new(normalize), + ident_normalizer: IdentNormalizer::new(ident_normalize), + value_normalizer: ValueNormalizer::new(options_value_normalize), } } @@ -214,7 +244,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { .iter() .any(|x| x.option == ColumnOption::NotNull); fields.push(Field::new( - self.normalizer.normalize(column.name), + self.ident_normalizer.normalize(column.name), data_type, !not_nullable, )); @@ -252,8 +282,10 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { let default_expr = self .sql_to_expr(default_sql_expr.clone(), &empty_schema, planner_context) .map_err(error_desc)?; - column_defaults - .push((self.normalizer.normalize(column.name.clone()), default_expr)); + column_defaults.push(( + self.ident_normalizer.normalize(column.name.clone()), + default_expr, + )); } } Ok(column_defaults) @@ -268,7 +300,9 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { let plan = self.apply_expr_alias(plan, alias.columns)?; LogicalPlanBuilder::from(plan) - .alias(TableReference::bare(self.normalizer.normalize(alias.name)))? + .alias(TableReference::bare( + self.ident_normalizer.normalize(alias.name), + ))? .build() } @@ -289,7 +323,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { let fields = plan.schema().fields().clone(); LogicalPlanBuilder::from(plan) .project(fields.iter().zip(idents.into_iter()).map(|(field, ident)| { - col(field.name()).alias(self.normalizer.normalize(ident)) + col(field.name()).alias(self.ident_normalizer.normalize(ident)) }))? .build() } @@ -415,7 +449,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { None => Ident::new(format!("c{idx}")) }; Ok(Arc::new(Field::new( - self.normalizer.normalize(field_name), + self.ident_normalizer.normalize(field_name), data_type, true, ))) diff --git a/datafusion/sql/src/relation/join.rs b/datafusion/sql/src/relation/join.rs index ee2e35b550f6..fb1d00b7e48a 100644 --- a/datafusion/sql/src/relation/join.rs +++ b/datafusion/sql/src/relation/join.rs @@ -115,7 +115,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { JoinConstraint::Using(idents) => { let keys: Vec = idents .into_iter() - .map(|x| Column::from_name(self.normalizer.normalize(x))) + .map(|x| Column::from_name(self.ident_normalizer.normalize(x))) .collect(); LogicalPlanBuilder::from(left) .join_using(right, join_type, keys)? diff --git a/datafusion/sql/src/select.rs b/datafusion/sql/src/select.rs index 9b105117af15..4de3952dc7ea 100644 --- a/datafusion/sql/src/select.rs +++ b/datafusion/sql/src/select.rs @@ -576,7 +576,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { &[&[plan.schema()]], &plan.using_columns()?, )?; - let name = self.normalizer.normalize(alias); + let name = self.ident_normalizer.normalize(alias); // avoiding adding an alias if the column name is the same. let expr = match &col { Expr::Column(column) if column.name.eq(&name) => col, diff --git a/datafusion/sql/src/statement.rs b/datafusion/sql/src/statement.rs index 67107bae0202..218ff54a1a09 100644 --- a/datafusion/sql/src/statement.rs +++ b/datafusion/sql/src/statement.rs @@ -66,30 +66,6 @@ fn ident_to_string(ident: &Ident) -> String { normalize_ident(ident.to_owned()) } -fn value_to_string(value: &Value) -> Option { - match value { - Value::SingleQuotedString(s) => Some(s.to_string()), - Value::DollarQuotedString(s) => Some(s.to_string()), - Value::Number(_, _) | Value::Boolean(_) => Some(value.to_string()), - Value::DoubleQuotedString(_) - | Value::EscapedStringLiteral(_) - | Value::NationalStringLiteral(_) - | Value::SingleQuotedByteStringLiteral(_) - | Value::DoubleQuotedByteStringLiteral(_) - | Value::TripleSingleQuotedString(_) - | Value::TripleDoubleQuotedString(_) - | Value::TripleSingleQuotedByteStringLiteral(_) - | Value::TripleDoubleQuotedByteStringLiteral(_) - | Value::SingleQuotedRawStringLiteral(_) - | Value::DoubleQuotedRawStringLiteral(_) - | Value::TripleSingleQuotedRawStringLiteral(_) - | Value::TripleDoubleQuotedRawStringLiteral(_) - | Value::HexStringLiteral(_) - | Value::Null - | Value::Placeholder(_) => None, - } -} - fn object_name_to_string(object_name: &ObjectName) -> String { object_name .0 @@ -881,25 +857,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { } }; - let mut options = HashMap::new(); - for (key, value) in statement.options { - let value_string = match value_to_string(&value) { - None => { - return plan_err!("Unsupported Value in COPY statement {}", value); - } - Some(v) => v, - }; - - if !(&key.contains('.')) { - // If config does not belong to any namespace, assume it is - // a format option and apply the format prefix for backwards - // compatibility. - let renamed_key = format!("format.{}", key); - options.insert(renamed_key.to_lowercase(), value_string.to_lowercase()); - } else { - options.insert(key.to_lowercase(), value_string.to_lowercase()); - } - } + let options_map = self.parse_options_map(statement.options, true)?; let maybe_file_type = if let Some(stored_as) = &statement.stored_as { if let Ok(ext_file_type) = self.context_provider.get_file_type(stored_as) { @@ -946,7 +904,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { output_url: statement.target, file_type, partition_by, - options, + options: options_map, })) } @@ -1007,29 +965,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { let inline_constraints = calc_inline_constraints_from_columns(&columns); all_constraints.extend(inline_constraints); - let mut options_map = HashMap::::new(); - for (key, value) in options { - if options_map.contains_key(&key) { - return plan_err!("Option {key} is specified multiple times"); - } - - let Some(value_string) = value_to_string(&value) else { - return plan_err!( - "Unsupported Value in CREATE EXTERNAL TABLE statement {}", - value - ); - }; - - if !(&key.contains('.')) { - // If a config does not belong to any namespace, we assume it is - // a format option and apply the format prefix for backwards - // compatibility. - let renamed_key = format!("format.{}", key.to_lowercase()); - options_map.insert(renamed_key, value_string.to_lowercase()); - } else { - options_map.insert(key.to_lowercase(), value_string.to_lowercase()); - } - } + let options_map = self.parse_options_map(options, false)?; let compression = options_map .get("format.compression") @@ -1081,6 +1017,36 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { ))) } + fn parse_options_map( + &self, + options: Vec<(String, Value)>, + allow_duplicates: bool, + ) -> Result> { + let mut options_map = HashMap::new(); + for (key, value) in options { + if !allow_duplicates && options_map.contains_key(&key) { + return plan_err!("Option {key} is specified multiple times"); + } + + let Some(value_string) = self.value_normalizer.normalize(value.clone()) + else { + return plan_err!("Unsupported Value {}", value); + }; + + if !(&key.contains('.')) { + // If config does not belong to any namespace, assume it is + // a format option and apply the format prefix for backwards + // compatibility. + let renamed_key = format!("format.{}", key); + options_map.insert(renamed_key.to_lowercase(), value_string); + } else { + options_map.insert(key.to_lowercase(), value_string); + } + } + + Ok(options_map) + } + /// Generate a plan for EXPLAIN ... that will print out a plan /// /// Note this is the sqlparser explain statement, not the @@ -1204,7 +1170,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { // parse value string from Expr let value_string = match &value[0] { SQLExpr::Identifier(i) => ident_to_string(i), - SQLExpr::Value(v) => match value_to_string(v) { + SQLExpr::Value(v) => match crate::utils::value_to_string(v) { None => { return plan_err!("Unsupported Value {}", value[0]); } @@ -1365,8 +1331,8 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { None => { // If the target table has an alias, use it to qualify the column name if let Some(alias) = &table_alias { - Expr::Column(Column::new( - Some(self.normalizer.normalize(alias.name.clone())), + datafusion_expr::Expr::Column(Column::new( + Some(self.ident_normalizer.normalize(alias.name.clone())), field.name(), )) } else { @@ -1421,7 +1387,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { let mut value_indices = vec![None; table_schema.fields().len()]; let fields = columns .into_iter() - .map(|c| self.normalizer.normalize(c)) + .map(|c| self.ident_normalizer.normalize(c)) .enumerate() .map(|(i, c)| { let column_index = table_schema diff --git a/datafusion/sql/src/utils.rs b/datafusion/sql/src/utils.rs index 483b8093a033..a9a782902ac9 100644 --- a/datafusion/sql/src/utils.rs +++ b/datafusion/sql/src/utils.rs @@ -32,7 +32,7 @@ use datafusion_expr::builder::get_unnested_columns; use datafusion_expr::expr::{Alias, GroupingSet, Unnest, WindowFunction}; use datafusion_expr::utils::{expr_as_column_expr, find_column_exprs}; use datafusion_expr::{expr_vec_fmt, Expr, ExprSchemable, LogicalPlan}; -use sqlparser::ast::Ident; +use sqlparser::ast::{Ident, Value}; /// Make a best-effort attempt at resolving all columns in the expression tree pub(crate) fn resolve_columns(expr: &Expr, plan: &LogicalPlan) -> Result { @@ -263,6 +263,30 @@ pub(crate) fn normalize_ident(id: Ident) -> String { } } +pub(crate) fn value_to_string(value: &Value) -> Option { + match value { + Value::SingleQuotedString(s) => Some(s.to_string()), + Value::DollarQuotedString(s) => Some(s.to_string()), + Value::Number(_, _) | Value::Boolean(_) => Some(value.to_string()), + Value::DoubleQuotedString(_) + | Value::EscapedStringLiteral(_) + | Value::NationalStringLiteral(_) + | Value::SingleQuotedByteStringLiteral(_) + | Value::DoubleQuotedByteStringLiteral(_) + | Value::TripleSingleQuotedString(_) + | Value::TripleDoubleQuotedString(_) + | Value::TripleSingleQuotedByteStringLiteral(_) + | Value::TripleDoubleQuotedByteStringLiteral(_) + | Value::SingleQuotedRawStringLiteral(_) + | Value::DoubleQuotedRawStringLiteral(_) + | Value::TripleSingleQuotedRawStringLiteral(_) + | Value::TripleDoubleQuotedRawStringLiteral(_) + | Value::HexStringLiteral(_) + | Value::Null + | Value::Placeholder(_) => None, + } +} + /// The context is we want to rewrite unnest() into InnerProjection->Unnest->OuterProjection /// Given an expression which contains unnest expr as one of its children, /// Try transform depends on unnest type diff --git a/datafusion/sql/tests/sql_integration.rs b/datafusion/sql/tests/sql_integration.rs index 511f97c4750e..40a58827b388 100644 --- a/datafusion/sql/tests/sql_integration.rs +++ b/datafusion/sql/tests/sql_integration.rs @@ -28,9 +28,11 @@ use datafusion_common::{ assert_contains, DataFusionError, ParamValues, Result, ScalarValue, }; use datafusion_expr::{ + dml::CopyTo, logical_plan::{LogicalPlan, Prepare}, test::function_stub::sum_udaf, - ColumnarValue, ScalarUDF, ScalarUDFImpl, Signature, Volatility, + ColumnarValue, CreateExternalTable, DdlStatement, ScalarUDF, ScalarUDFImpl, + Signature, Volatility, }; use datafusion_functions::{string, unicode}; use datafusion_sql::{ @@ -87,6 +89,7 @@ fn parse_decimals() { parse_float_as_decimal: true, enable_ident_normalization: false, support_varchar_with_length: false, + enable_options_value_normalization: false, }, ); } @@ -141,6 +144,7 @@ fn parse_ident_normalization() { parse_float_as_decimal: false, enable_ident_normalization, support_varchar_with_length: false, + enable_options_value_normalization: false, }, ); if plan.is_ok() { @@ -151,6 +155,70 @@ fn parse_ident_normalization() { } } +#[test] +fn test_parse_options_value_normalization() { + let test_data = [ + ( + "CREATE EXTERNAL TABLE test OPTIONS ('location' 'LoCaTiOn') STORED AS PARQUET LOCATION 'fake_location'", + "CreateExternalTable: Bare { table: \"test\" }", + HashMap::from([("format.location", "LoCaTiOn")]), + false, + ), + ( + "CREATE EXTERNAL TABLE test OPTIONS ('location' 'LoCaTiOn') STORED AS PARQUET LOCATION 'fake_location'", + "CreateExternalTable: Bare { table: \"test\" }", + HashMap::from([("format.location", "location")]), + true, + ), + ( + "COPY test TO 'fake_location' STORED AS PARQUET OPTIONS ('location' 'LoCaTiOn')", + "CopyTo: format=csv output_url=fake_location options: (format.location LoCaTiOn)\n TableScan: test", + HashMap::from([("format.location", "LoCaTiOn")]), + false, + ), + ( + "COPY test TO 'fake_location' STORED AS PARQUET OPTIONS ('location' 'LoCaTiOn')", + "CopyTo: format=csv output_url=fake_location options: (format.location location)\n TableScan: test", + HashMap::from([("format.location", "location")]), + true, + ), + ]; + + for (sql, expected_plan, expected_options, enable_options_value_normalization) in + test_data + { + let plan = logical_plan_with_options( + sql, + ParserOptions { + parse_float_as_decimal: false, + enable_ident_normalization: false, + support_varchar_with_length: false, + enable_options_value_normalization, + }, + ); + if let Ok(plan) = plan { + assert_eq!(expected_plan, format!("{plan:?}")); + + match plan { + LogicalPlan::Ddl(DdlStatement::CreateExternalTable( + CreateExternalTable { options, .. }, + )) + | LogicalPlan::Copy(CopyTo { options, .. }) => { + expected_options.iter().for_each(|(k, v)| { + assert_eq!(Some(&v.to_string()), options.get(*k)); + }); + } + _ => panic!( + "Expected Ddl(CreateExternalTable) or Copy(CopyTo) but got {:?}", + plan + ), + } + } else { + assert_eq!(expected_plan, plan.unwrap_err().strip_backtrace()); + } + } +} + #[test] fn select_no_relation() { quick_test( diff --git a/datafusion/sqllogictest/test_files/information_schema.slt b/datafusion/sqllogictest/test_files/information_schema.slt index ddacf1cc6a79..431060a1f6f8 100644 --- a/datafusion/sqllogictest/test_files/information_schema.slt +++ b/datafusion/sqllogictest/test_files/information_schema.slt @@ -239,6 +239,7 @@ datafusion.optimizer.skip_failed_rules false datafusion.optimizer.top_down_join_key_reordering true datafusion.sql_parser.dialect generic datafusion.sql_parser.enable_ident_normalization true +datafusion.sql_parser.enable_options_value_normalization true datafusion.sql_parser.parse_float_as_decimal false datafusion.sql_parser.support_varchar_with_length true @@ -324,6 +325,7 @@ datafusion.optimizer.skip_failed_rules false When set to true, the logical plan datafusion.optimizer.top_down_join_key_reordering true When set to true, the physical plan optimizer will run a top down process to reorder the join keys datafusion.sql_parser.dialect generic Configure the SQL dialect used by DataFusion's parser; supported values include: Generic, MySQL, PostgreSQL, Hive, SQLite, Snowflake, Redshift, MsSQL, ClickHouse, BigQuery, and Ansi. datafusion.sql_parser.enable_ident_normalization true When set to true, SQL parser will normalize ident (convert ident to lowercase when not quoted) +datafusion.sql_parser.enable_options_value_normalization true When set to true, SQL parser will normalize options value (convert value to lowercase) datafusion.sql_parser.parse_float_as_decimal false When set to true, SQL parser will parse float as decimal type datafusion.sql_parser.support_varchar_with_length true If true, permit lengths for `VARCHAR` such as `VARCHAR(20)`, but ignore the length. If false, error if a `VARCHAR` with a length is specified. The Arrow type system does not have a notion of maximum string length and thus DataFusion can not enforce such limits. diff --git a/docs/source/user-guide/configs.md b/docs/source/user-guide/configs.md index e4b849cd28bb..e992361755d3 100644 --- a/docs/source/user-guide/configs.md +++ b/docs/source/user-guide/configs.md @@ -115,5 +115,6 @@ Environment variables are read during `SessionConfig` initialisation so they mus | datafusion.explain.show_schema | false | When set to true, the explain statement will print schema information | | datafusion.sql_parser.parse_float_as_decimal | false | When set to true, SQL parser will parse float as decimal type | | datafusion.sql_parser.enable_ident_normalization | true | When set to true, SQL parser will normalize ident (convert ident to lowercase when not quoted) | +| datafusion.sql_parser.enable_options_value_normalization | true | When set to true, SQL parser will normalize options value (convert value to lowercase) | | datafusion.sql_parser.dialect | generic | Configure the SQL dialect used by DataFusion's parser; supported values include: Generic, MySQL, PostgreSQL, Hive, SQLite, Snowflake, Redshift, MsSQL, ClickHouse, BigQuery, and Ansi. | | datafusion.sql_parser.support_varchar_with_length | true | If true, permit lengths for `VARCHAR` such as `VARCHAR(20)`, but ignore the length. If false, error if a `VARCHAR` with a length is specified. The Arrow type system does not have a notion of maximum string length and thus DataFusion can not enforce such limits. |