blob: 0ed499da047572309b78c6e5da7ef222853fdc7e [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
//! Runtime configuration, via [`ConfigOptions`]
use arrow_ipc::CompressionType;
#[cfg(feature = "parquet_encryption")]
use crate::encryption::{FileDecryptionProperties, FileEncryptionProperties};
use crate::error::_config_err;
use crate::format::{ExplainAnalyzeLevel, ExplainFormat};
use crate::parsers::CompressionTypeVariant;
use crate::utils::get_available_parallelism;
use crate::{DataFusionError, Result};
#[cfg(feature = "parquet_encryption")]
use hex;
use std::any::Any;
use std::collections::{BTreeMap, HashMap};
use std::error::Error;
use std::fmt::{self, Display};
use std::str::FromStr;
#[cfg(feature = "parquet_encryption")]
use std::sync::Arc;
/// A macro that wraps a configuration struct and automatically derives
/// [`Default`] and [`ConfigField`] for it, allowing it to be used
/// in the [`ConfigOptions`] configuration tree.
///
/// `transform` is used to normalize values before parsing.
///
/// For example,
///
/// ```ignore
/// config_namespace! {
/// /// Amazing config
/// pub struct MyConfig {
/// /// Field 1 doc
/// field1: String, transform = str::to_lowercase, default = "".to_string()
///
/// /// Field 2 doc
/// field2: usize, default = 232
///
/// /// Field 3 doc
/// field3: Option<usize>, default = None
/// }
/// }
/// ```
///
/// Will generate
///
/// ```ignore
/// /// Amazing config
/// #[derive(Debug, Clone)]
/// #[non_exhaustive]
/// pub struct MyConfig {
/// /// Field 1 doc
/// field1: String,
/// /// Field 2 doc
/// field2: usize,
/// /// Field 3 doc
/// field3: Option<usize>,
/// }
/// impl ConfigField for MyConfig {
/// fn set(&mut self, key: &str, value: &str) -> Result<()> {
/// let (key, rem) = key.split_once('.').unwrap_or((key, ""));
/// match key {
/// "field1" => {
/// let value = str::to_lowercase(value);
/// self.field1.set(rem, value.as_ref())
/// },
/// "field2" => self.field2.set(rem, value.as_ref()),
/// "field3" => self.field3.set(rem, value.as_ref()),
/// _ => _internal_err!(
/// "Config value \"{}\" not found on MyConfig",
/// key
/// ),
/// }
/// }
///
/// fn visit<V: Visit>(&self, v: &mut V, key_prefix: &str, _description: &'static str) {
/// let key = format!("{}.field1", key_prefix);
/// let desc = "Field 1 doc";
/// self.field1.visit(v, key.as_str(), desc);
/// let key = format!("{}.field2", key_prefix);
/// let desc = "Field 2 doc";
/// self.field2.visit(v, key.as_str(), desc);
/// let key = format!("{}.field3", key_prefix);
/// let desc = "Field 3 doc";
/// self.field3.visit(v, key.as_str(), desc);
/// }
/// }
///
/// impl Default for MyConfig {
/// fn default() -> Self {
/// Self {
/// field1: "".to_string(),
/// field2: 232,
/// field3: None,
/// }
/// }
/// }
/// ```
///
/// NB: Misplaced commas may result in nonsensical errors
#[macro_export]
macro_rules! config_namespace {
(
$(#[doc = $struct_d:tt])* // Struct-level documentation attributes
$(#[deprecated($($struct_depr:tt)*)])? // Optional struct-level deprecated attribute
$(#[allow($($struct_de:tt)*)])?
$vis:vis struct $struct_name:ident {
$(
$(#[doc = $d:tt])* // Field-level documentation attributes
$(#[deprecated($($field_depr:tt)*)])? // Optional field-level deprecated attribute
$(#[allow($($field_de:tt)*)])?
$field_vis:vis $field_name:ident : $field_type:ty,
$(warn = $warn:expr,)?
$(transform = $transform:expr,)?
default = $default:expr
)*$(,)*
}
) => {
$(#[doc = $struct_d])* // Apply struct documentation
$(#[deprecated($($struct_depr)*)])? // Apply struct deprecation
$(#[allow($($struct_de)*)])?
#[derive(Debug, Clone, PartialEq)]
$vis struct $struct_name {
$(
$(#[doc = $d])* // Apply field documentation
$(#[deprecated($($field_depr)*)])? // Apply field deprecation
$(#[allow($($field_de)*)])?
$field_vis $field_name: $field_type,
)*
}
impl $crate::config::ConfigField for $struct_name {
fn set(&mut self, key: &str, value: &str) -> $crate::error::Result<()> {
let (key, rem) = key.split_once('.').unwrap_or((key, ""));
match key {
$(
stringify!($field_name) => {
// Safely apply deprecated attribute if present
// $(#[allow(deprecated)])?
{
$(let value = $transform(value);)? // Apply transformation if specified
#[allow(deprecated)]
let ret = self.$field_name.set(rem, value.as_ref());
$(if !$warn.is_empty() {
let default: $field_type = $default;
#[allow(deprecated)]
if default != self.$field_name {
log::warn!($warn);
}
})? // Log warning if specified, and the value is not the default
ret
}
},
)*
_ => return $crate::error::_config_err!(
"Config value \"{}\" not found on {}", key, stringify!($struct_name)
)
}
}
fn visit<V: $crate::config::Visit>(&self, v: &mut V, key_prefix: &str, _description: &'static str) {
$(
let key = format!(concat!("{}.", stringify!($field_name)), key_prefix);
let desc = concat!($($d),*).trim();
#[allow(deprecated)]
self.$field_name.visit(v, key.as_str(), desc);
)*
}
}
impl Default for $struct_name {
fn default() -> Self {
#[allow(deprecated)]
Self {
$($field_name: $default),*
}
}
}
}
}
config_namespace! {
/// Options related to catalog and directory scanning
///
/// See also: [`SessionConfig`]
///
/// [`SessionConfig`]: https://docs.rs/datafusion/latest/datafusion/prelude/struct.SessionConfig.html
pub struct CatalogOptions {
/// Whether the default catalog and schema should be created automatically.
pub create_default_catalog_and_schema: bool, default = true
/// The default catalog name - this impacts what SQL queries use if not specified
pub default_catalog: String, default = "datafusion".to_string()
/// The default schema name - this impacts what SQL queries use if not specified
pub default_schema: String, default = "public".to_string()
/// Should DataFusion provide access to `information_schema`
/// virtual tables for displaying schema information
pub information_schema: bool, default = false
/// Location scanned to load tables for `default` schema
pub location: Option<String>, default = None
/// Type of `TableProvider` to use when loading `default` schema
pub format: Option<String>, default = None
/// Default value for `format.has_header` for `CREATE EXTERNAL TABLE`
/// if not specified explicitly in the statement.
pub has_header: bool, default = true
/// Specifies whether newlines in (quoted) CSV values are supported.
///
/// This is the default value for `format.newlines_in_values` for `CREATE EXTERNAL TABLE`
/// if not specified explicitly in the statement.
///
/// Parsing newlines in quoted values may be affected by execution behaviour such as
/// parallel file scanning. Setting this to `true` ensures that newlines in values are
/// parsed successfully, which may reduce performance.
pub newlines_in_values: bool, default = false
}
}
config_namespace! {
/// Options related to SQL parser
///
/// See also: [`SessionConfig`]
///
/// [`SessionConfig`]: https://docs.rs/datafusion/latest/datafusion/prelude/struct.SessionConfig.html
pub struct SqlParserOptions {
/// When set to true, SQL parser will parse float as decimal type
pub parse_float_as_decimal: bool, default = false
/// 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).
/// Note that this option is ignored and will be removed in the future. All case-insensitive values
/// are normalized automatically.
pub enable_options_value_normalization: bool, warn = "`enable_options_value_normalization` is deprecated and ignored", default = false
/// Configure the SQL dialect used by DataFusion's parser; supported values include: Generic,
/// MySQL, PostgreSQL, Hive, SQLite, Snowflake, Redshift, MsSQL, ClickHouse, BigQuery, Ansi, DuckDB and Databricks.
pub dialect: Dialect, default = Dialect::Generic
// no need to lowercase because `sqlparser::dialect_from_str`] is case-insensitive
/// 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.
pub support_varchar_with_length: bool, default = true
/// If true, string types (VARCHAR, CHAR, Text, and String) are mapped to `Utf8View` during SQL planning.
/// If false, they are mapped to `Utf8`.
/// Default is true.
pub map_string_types_to_utf8view: bool, default = true
/// When set to true, the source locations relative to the original SQL
/// query (i.e. [`Span`](https://docs.rs/sqlparser/latest/sqlparser/tokenizer/struct.Span.html)) will be collected
/// and recorded in the logical plan nodes.
pub collect_spans: bool, default = false
/// Specifies the recursion depth limit when parsing complex SQL Queries
pub recursion_limit: usize, default = 50
/// Specifies the default null ordering for query results. There are 4 options:
/// - `nulls_max`: Nulls appear last in ascending order.
/// - `nulls_min`: Nulls appear first in ascending order.
/// - `nulls_first`: Nulls always be first in any order.
/// - `nulls_last`: Nulls always be last in any order.
///
/// By default, `nulls_max` is used to follow Postgres's behavior.
/// postgres rule: <https://www.postgresql.org/docs/current/queries-order.html>
pub default_null_ordering: String, default = "nulls_max".to_string()
}
}
/// This is the SQL dialect used by DataFusion's parser.
/// This mirrors [sqlparser::dialect::Dialect](https://docs.rs/sqlparser/latest/sqlparser/dialect/trait.Dialect.html)
/// trait in order to offer an easier API and avoid adding the `sqlparser` dependency
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum Dialect {
#[default]
Generic,
MySQL,
PostgreSQL,
Hive,
SQLite,
Snowflake,
Redshift,
MsSQL,
ClickHouse,
BigQuery,
Ansi,
DuckDB,
Databricks,
}
impl AsRef<str> for Dialect {
fn as_ref(&self) -> &str {
match self {
Self::Generic => "generic",
Self::MySQL => "mysql",
Self::PostgreSQL => "postgresql",
Self::Hive => "hive",
Self::SQLite => "sqlite",
Self::Snowflake => "snowflake",
Self::Redshift => "redshift",
Self::MsSQL => "mssql",
Self::ClickHouse => "clickhouse",
Self::BigQuery => "bigquery",
Self::Ansi => "ansi",
Self::DuckDB => "duckdb",
Self::Databricks => "databricks",
}
}
}
impl FromStr for Dialect {
type Err = DataFusionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let value = match s.to_ascii_lowercase().as_str() {
"generic" => Self::Generic,
"mysql" => Self::MySQL,
"postgresql" | "postgres" => Self::PostgreSQL,
"hive" => Self::Hive,
"sqlite" => Self::SQLite,
"snowflake" => Self::Snowflake,
"redshift" => Self::Redshift,
"mssql" => Self::MsSQL,
"clickhouse" => Self::ClickHouse,
"bigquery" => Self::BigQuery,
"ansi" => Self::Ansi,
"duckdb" => Self::DuckDB,
"databricks" => Self::Databricks,
other => {
let error_message = format!(
"Invalid Dialect: {other}. Expected one of: Generic, MySQL, PostgreSQL, Hive, SQLite, Snowflake, Redshift, MsSQL, ClickHouse, BigQuery, Ansi, DuckDB, Databricks"
);
return Err(DataFusionError::Configuration(error_message));
}
};
Ok(value)
}
}
impl ConfigField for Dialect {
fn visit<V: Visit>(&self, v: &mut V, key: &str, description: &'static str) {
v.some(key, self, description)
}
fn set(&mut self, _: &str, value: &str) -> Result<()> {
*self = Self::from_str(value)?;
Ok(())
}
}
impl Display for Dialect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let str = self.as_ref();
write!(f, "{str}")
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum SpillCompression {
Zstd,
Lz4Frame,
#[default]
Uncompressed,
}
impl FromStr for SpillCompression {
type Err = DataFusionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"zstd" => Ok(Self::Zstd),
"lz4_frame" => Ok(Self::Lz4Frame),
"uncompressed" | "" => Ok(Self::Uncompressed),
other => Err(DataFusionError::Configuration(format!(
"Invalid Spill file compression type: {other}. Expected one of: zstd, lz4_frame, uncompressed"
))),
}
}
}
impl ConfigField for SpillCompression {
fn visit<V: Visit>(&self, v: &mut V, key: &str, description: &'static str) {
v.some(key, self, description)
}
fn set(&mut self, _: &str, value: &str) -> Result<()> {
*self = SpillCompression::from_str(value)?;
Ok(())
}
}
impl Display for SpillCompression {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let str = match self {
Self::Zstd => "zstd",
Self::Lz4Frame => "lz4_frame",
Self::Uncompressed => "uncompressed",
};
write!(f, "{str}")
}
}
impl From<SpillCompression> for Option<CompressionType> {
fn from(c: SpillCompression) -> Self {
match c {
SpillCompression::Zstd => Some(CompressionType::ZSTD),
SpillCompression::Lz4Frame => Some(CompressionType::LZ4_FRAME),
SpillCompression::Uncompressed => None,
}
}
}
config_namespace! {
/// Options related to query execution
///
/// See also: [`SessionConfig`]
///
/// [`SessionConfig`]: https://docs.rs/datafusion/latest/datafusion/prelude/struct.SessionConfig.html
pub struct ExecutionOptions {
/// Default batch size while creating new batches, it's especially useful for
/// buffer-in-memory batches since creating tiny batches would result in too much
/// metadata memory consumption
pub batch_size: usize, default = 8192
/// When set to true, record batches will be examined between each operator and
/// small batches will be coalesced into larger batches. This is helpful when there
/// are highly selective filters or joins that could produce tiny output batches. The
/// target batch size is determined by the configuration setting
pub coalesce_batches: bool, default = true
/// Should DataFusion collect statistics when first creating a table.
/// Has no effect after the table is created. Applies to the default
/// `ListingTableProvider` in DataFusion. Defaults to true.
pub collect_statistics: bool, default = true
/// Number of partitions for query execution. Increasing partitions can increase
/// concurrency.
///
/// Defaults to the number of CPU cores on the system
pub target_partitions: usize, transform = ExecutionOptions::normalized_parallelism, default = get_available_parallelism()
/// The default time zone
///
/// Some functions, e.g. `now` return timestamps in this time zone
pub time_zone: Option<String>, default = None
/// Parquet options
pub parquet: ParquetOptions, default = Default::default()
/// Fan-out during initial physical planning.
///
/// This is mostly use to plan `UNION` children in parallel.
///
/// Defaults to the number of CPU cores on the system
pub planning_concurrency: usize, transform = ExecutionOptions::normalized_parallelism, default = get_available_parallelism()
/// When set to true, skips verifying that the schema produced by
/// planning the input of `LogicalPlan::Aggregate` exactly matches the
/// schema of the input plan.
///
/// When set to false, if the schema does not match exactly
/// (including nullability and metadata), a planning error will be raised.
///
/// This is used to workaround bugs in the planner that are now caught by
/// the new schema verification step.
pub skip_physical_aggregate_schema_check: bool, default = false
/// Sets the compression codec used when spilling data to disk.
///
/// Since datafusion writes spill files using the Arrow IPC Stream format,
/// only codecs supported by the Arrow IPC Stream Writer are allowed.
/// Valid values are: uncompressed, lz4_frame, zstd.
/// Note: lz4_frame offers faster (de)compression, but typically results in
/// larger spill files. In contrast, zstd achieves
/// higher compression ratios at the cost of slower (de)compression speed.
pub spill_compression: SpillCompression, default = SpillCompression::Uncompressed
/// Specifies the reserved memory for each spillable sort operation to
/// facilitate an in-memory merge.
///
/// When a sort operation spills to disk, the in-memory data must be
/// sorted and merged before being written to a file. This setting reserves
/// a specific amount of memory for that in-memory sort/merge process.
///
/// Note: This setting is irrelevant if the sort operation cannot spill
/// (i.e., if there's no `DiskManager` configured).
pub sort_spill_reservation_bytes: usize, default = 10 * 1024 * 1024
/// When sorting, below what size should data be concatenated
/// and sorted in a single RecordBatch rather than sorted in
/// batches and merged.
pub sort_in_place_threshold_bytes: usize, default = 1024 * 1024
/// Maximum size in bytes for individual spill files before rotating to a new file.
///
/// When operators spill data to disk (e.g., RepartitionExec), they write
/// multiple batches to the same file until this size limit is reached, then rotate
/// to a new file. This reduces syscall overhead compared to one-file-per-batch
/// while preventing files from growing too large.
///
/// A larger value reduces file creation overhead but may hold more disk space.
/// A smaller value creates more files but allows finer-grained space reclamation
/// as files can be deleted once fully consumed.
///
/// Now only `RepartitionExec` supports this spill file rotation feature, other spilling operators
/// may create spill files larger than the limit.
///
/// Default: 128 MB
pub max_spill_file_size_bytes: usize, default = 128 * 1024 * 1024
/// Number of files to read in parallel when inferring schema and statistics
pub meta_fetch_concurrency: usize, default = 32
/// Guarantees a minimum level of output files running in parallel.
/// RecordBatches will be distributed in round robin fashion to each
/// parallel writer. Each writer is closed and a new file opened once
/// soft_max_rows_per_output_file is reached.
pub minimum_parallel_output_files: usize, default = 4
/// Target number of rows in output files when writing multiple.
/// This is a soft max, so it can be exceeded slightly. There also
/// will be one file smaller than the limit if the total
/// number of rows written is not roughly divisible by the soft max
pub soft_max_rows_per_output_file: usize, default = 50000000
/// This is the maximum number of RecordBatches buffered
/// for each output file being worked. Higher values can potentially
/// give faster write performance at the cost of higher peak
/// memory consumption
pub max_buffered_batches_per_output_file: usize, default = 2
/// Should sub directories be ignored when scanning directories for data
/// files. Defaults to true (ignores subdirectories), consistent with
/// Hive. Note that this setting does not affect reading partitioned
/// tables (e.g. `/table/year=2021/month=01/data.parquet`).
pub listing_table_ignore_subdirectory: bool, default = true
/// Should a `ListingTable` created through the `ListingTableFactory` infer table
/// partitions from Hive compliant directories. Defaults to true (partition columns are
/// inferred and will be represented in the table schema).
pub listing_table_factory_infer_partitions: bool, default = true
/// Should DataFusion support recursive CTEs
pub enable_recursive_ctes: bool, default = true
/// Attempt to eliminate sorts by packing & sorting files with non-overlapping
/// statistics into the same file groups.
/// Currently experimental
pub split_file_groups_by_statistics: bool, default = false
/// Should DataFusion keep the columns used for partition_by in the output RecordBatches
pub keep_partition_by_columns: bool, default = false
/// Aggregation ratio (number of distinct groups / number of input rows)
/// threshold for skipping partial aggregation. If the value is greater
/// then partial aggregation will skip aggregation for further input
pub skip_partial_aggregation_probe_ratio_threshold: f64, default = 0.8
/// Number of input rows partial aggregation partition should process, before
/// aggregation ratio check and trying to switch to skipping aggregation mode
pub skip_partial_aggregation_probe_rows_threshold: usize, default = 100_000
/// Should DataFusion use row number estimates at the input to decide
/// whether increasing parallelism is beneficial or not. By default,
/// only exact row numbers (not estimates) are used for this decision.
/// Setting this flag to `true` will likely produce better plans.
/// if the source of statistics is accurate.
/// We plan to make this the default in the future.
pub use_row_number_estimates_to_optimize_partitioning: bool, default = false
/// Should DataFusion enforce batch size in joins or not. By default,
/// DataFusion will not enforce batch size in joins. Enforcing batch size
/// in joins can reduce memory usage when joining large
/// tables with a highly-selective join filter, but is also slightly slower.
pub enforce_batch_size_in_joins: bool, default = false
/// Size (bytes) of data buffer DataFusion uses when writing output files.
/// This affects the size of the data chunks that are uploaded to remote
/// object stores (e.g. AWS S3). If very large (>= 100 GiB) output files are being
/// written, it may be necessary to increase this size to avoid errors from
/// the remote end point.
pub objectstore_writer_buffer_size: usize, default = 10 * 1024 * 1024
}
}
config_namespace! {
/// Options for reading and writing parquet files
///
/// See also: [`SessionConfig`]
///
/// [`SessionConfig`]: https://docs.rs/datafusion/latest/datafusion/prelude/struct.SessionConfig.html
pub struct ParquetOptions {
// The following options affect reading parquet files
/// (reading) If true, reads the Parquet data page level metadata (the
/// Page Index), if present, to reduce the I/O and number of
/// rows decoded.
pub enable_page_index: bool, default = true
/// (reading) If true, the parquet reader attempts to skip entire row groups based
/// on the predicate in the query and the metadata (min/max values) stored in
/// the parquet file
pub pruning: bool, default = true
/// (reading) If true, the parquet reader skip the optional embedded metadata that may be in
/// the file Schema. This setting can help avoid schema conflicts when querying
/// multiple parquet files with schemas containing compatible types but different metadata
pub skip_metadata: bool, default = true
/// (reading) If specified, the parquet reader will try and fetch the last `size_hint`
/// bytes of the parquet file optimistically. If not specified, two reads are required:
/// One read to fetch the 8-byte parquet footer and
/// another to fetch the metadata length encoded in the footer
/// Default setting to 512 KiB, which should be sufficient for most parquet files,
/// it can reduce one I/O operation per parquet file. If the metadata is larger than
/// the hint, two reads will still be performed.
pub metadata_size_hint: Option<usize>, default = Some(512 * 1024)
/// (reading) If true, filter expressions are be applied during the parquet decoding operation to
/// reduce the number of rows decoded. This optimization is sometimes called "late materialization".
pub pushdown_filters: bool, default = false
/// (reading) If true, filter expressions evaluated during the parquet decoding operation
/// will be reordered heuristically to minimize the cost of evaluation. If false,
/// the filters are applied in the same order as written in the query
pub reorder_filters: bool, default = false
/// (reading) If true, parquet reader will read columns of `Utf8/Utf8Large` with `Utf8View`,
/// and `Binary/BinaryLarge` with `BinaryView`.
pub schema_force_view_types: bool, default = true
/// (reading) If true, parquet reader will read columns of
/// `Binary/LargeBinary` with `Utf8`, and `BinaryView` with `Utf8View`.
///
/// Parquet files generated by some legacy writers do not correctly set
/// the UTF8 flag for strings, causing string columns to be loaded as
/// BLOB instead.
pub binary_as_string: bool, default = false
/// (reading) If true, parquet reader will read columns of
/// physical type int96 as originating from a different resolution
/// than nanosecond. This is useful for reading data from systems like Spark
/// which stores microsecond resolution timestamps in an int96 allowing it
/// to write values with a larger date range than 64-bit timestamps with
/// nanosecond resolution.
pub coerce_int96: Option<String>, transform = str::to_lowercase, default = None
/// (reading) Use any available bloom filters when reading parquet files
pub bloom_filter_on_read: bool, default = true
/// (reading) The maximum predicate cache size, in bytes. When
/// `pushdown_filters` is enabled, sets the maximum memory used to cache
/// the results of predicate evaluation between filter evaluation and
/// output generation. Decreasing this value will reduce memory usage,
/// but may increase IO and CPU usage. None means use the default
/// parquet reader setting. 0 means no caching.
pub max_predicate_cache_size: Option<usize>, default = None
// The following options affect writing to parquet files
// and map to parquet::file::properties::WriterProperties
/// (writing) Sets best effort maximum size of data page in bytes
pub data_pagesize_limit: usize, default = 1024 * 1024
/// (writing) Sets write_batch_size in bytes
pub write_batch_size: usize, default = 1024
/// (writing) Sets parquet writer version
/// valid values are "1.0" and "2.0"
pub writer_version: String, default = "1.0".to_string()
/// (writing) Skip encoding the embedded arrow metadata in the KV_meta
///
/// This is analogous to the `ArrowWriterOptions::with_skip_arrow_metadata`.
/// Refer to <https://docs.rs/parquet/53.3.0/parquet/arrow/arrow_writer/struct.ArrowWriterOptions.html#method.with_skip_arrow_metadata>
pub skip_arrow_metadata: bool, default = false
/// (writing) Sets default parquet compression codec.
/// Valid values are: uncompressed, snappy, gzip(level),
/// lzo, brotli(level), lz4, zstd(level), and lz4_raw.
/// These values are not case sensitive. If NULL, uses
/// default parquet writer setting
///
/// Note that this default setting is not the same as
/// the default parquet writer setting.
pub compression: Option<String>, transform = str::to_lowercase, default = Some("zstd(3)".into())
/// (writing) Sets if dictionary encoding is enabled. If NULL, uses
/// default parquet writer setting
pub dictionary_enabled: Option<bool>, default = Some(true)
/// (writing) Sets best effort maximum dictionary page size, in bytes
pub dictionary_page_size_limit: usize, default = 1024 * 1024
/// (writing) Sets if statistics are enabled for any column
/// Valid values are: "none", "chunk", and "page"
/// These values are not case sensitive. If NULL, uses
/// default parquet writer setting
pub statistics_enabled: Option<String>, transform = str::to_lowercase, default = Some("page".into())
/// (writing) Target maximum number of rows in each row group (defaults to 1M
/// rows). Writing larger row groups requires more memory to write, but
/// can get better compression and be faster to read.
pub max_row_group_size: usize, default = 1024 * 1024
/// (writing) Sets "created by" property
pub created_by: String, default = concat!("datafusion version ", env!("CARGO_PKG_VERSION")).into()
/// (writing) Sets column index truncate length
pub column_index_truncate_length: Option<usize>, default = Some(64)
/// (writing) Sets statistics truncate length. If NULL, uses
/// default parquet writer setting
pub statistics_truncate_length: Option<usize>, default = Some(64)
/// (writing) Sets best effort maximum number of rows in data page
pub data_page_row_count_limit: usize, default = 20_000
/// (writing) Sets default encoding for any column.
/// Valid values are: plain, plain_dictionary, rle,
/// bit_packed, delta_binary_packed, delta_length_byte_array,
/// delta_byte_array, rle_dictionary, and byte_stream_split.
/// These values are not case sensitive. If NULL, uses
/// default parquet writer setting
pub encoding: Option<String>, transform = str::to_lowercase, default = None
/// (writing) Write bloom filters for all columns when creating parquet files
pub bloom_filter_on_write: bool, default = false
/// (writing) Sets bloom filter false positive probability. If NULL, uses
/// default parquet writer setting
pub bloom_filter_fpp: Option<f64>, default = None
/// (writing) Sets bloom filter number of distinct values. If NULL, uses
/// default parquet writer setting
pub bloom_filter_ndv: Option<u64>, default = None
/// (writing) Controls whether DataFusion will attempt to speed up writing
/// parquet files by serializing them in parallel. Each column
/// in each row group in each output file are serialized in parallel
/// leveraging a maximum possible core count of n_files*n_row_groups*n_columns.
pub allow_single_file_parallelism: bool, default = true
/// (writing) By default parallel parquet writer is tuned for minimum
/// memory usage in a streaming execution plan. You may see
/// a performance benefit when writing large parquet files
/// by increasing maximum_parallel_row_group_writers and
/// maximum_buffered_record_batches_per_stream if your system
/// has idle cores and can tolerate additional memory usage.
/// Boosting these values is likely worthwhile when
/// writing out already in-memory data, such as from a cached
/// data frame.
pub maximum_parallel_row_group_writers: usize, default = 1
/// (writing) By default parallel parquet writer is tuned for minimum
/// memory usage in a streaming execution plan. You may see
/// a performance benefit when writing large parquet files
/// by increasing maximum_parallel_row_group_writers and
/// maximum_buffered_record_batches_per_stream if your system
/// has idle cores and can tolerate additional memory usage.
/// Boosting these values is likely worthwhile when
/// writing out already in-memory data, such as from a cached
/// data frame.
pub maximum_buffered_record_batches_per_stream: usize, default = 2
}
}
config_namespace! {
/// Options for configuring Parquet Modular Encryption
///
/// To use Parquet encryption, you must enable the `parquet_encryption` feature flag, as it is not activated by default.
pub struct ParquetEncryptionOptions {
/// Optional file decryption properties
pub file_decryption: Option<ConfigFileDecryptionProperties>, default = None
/// Optional file encryption properties
pub file_encryption: Option<ConfigFileEncryptionProperties>, default = None
/// Identifier for the encryption factory to use to create file encryption and decryption properties.
/// Encryption factories can be registered in the runtime environment with
/// `RuntimeEnv::register_parquet_encryption_factory`.
pub factory_id: Option<String>, default = None
/// Any encryption factory specific options
pub factory_options: EncryptionFactoryOptions, default = EncryptionFactoryOptions::default()
}
}
impl ParquetEncryptionOptions {
/// Specify the encryption factory to use for Parquet modular encryption, along with its configuration
pub fn configure_factory(
&mut self,
factory_id: &str,
config: &impl ExtensionOptions,
) {
self.factory_id = Some(factory_id.to_owned());
self.factory_options.options.clear();
for entry in config.entries() {
if let Some(value) = entry.value {
self.factory_options.options.insert(entry.key, value);
}
}
}
}
config_namespace! {
/// Options related to query optimization
///
/// See also: [`SessionConfig`]
///
/// [`SessionConfig`]: https://docs.rs/datafusion/latest/datafusion/prelude/struct.SessionConfig.html
pub struct OptimizerOptions {
/// When set to true, the optimizer will push a limit operation into
/// grouped aggregations which have no aggregate expressions, as a soft limit,
/// emitting groups once the limit is reached, before all rows in the group are read.
pub enable_distinct_aggregation_soft_limit: bool, default = true
/// When set to true, the physical plan optimizer will try to add round robin
/// repartitioning to increase parallelism to leverage more CPU cores
pub enable_round_robin_repartition: bool, default = true
/// When set to true, the optimizer will attempt to perform limit operations
/// during aggregations, if possible
pub enable_topk_aggregation: bool, default = true
/// When set to true, the optimizer will attempt to push limit operations
/// past window functions, if possible
pub enable_window_limits: bool, default = true
/// When set to true, the optimizer will attempt to push down TopK dynamic filters
/// into the file scan phase.
pub enable_topk_dynamic_filter_pushdown: bool, default = true
/// When set to true, the optimizer will attempt to push down Join dynamic filters
/// into the file scan phase.
pub enable_join_dynamic_filter_pushdown: bool, default = true
/// When set to true attempts to push down dynamic filters generated by operators (topk & join) into the file scan phase.
/// For example, for a query such as `SELECT * FROM t ORDER BY timestamp DESC LIMIT 10`, the optimizer
/// will attempt to push down the current top 10 timestamps that the TopK operator references into the file scans.
/// This means that if we already have 10 timestamps in the year 2025
/// any files that only have timestamps in the year 2024 can be skipped / pruned at various stages in the scan.
/// The config will suppress `enable_join_dynamic_filter_pushdown` & `enable_topk_dynamic_filter_pushdown`
/// So if you disable `enable_topk_dynamic_filter_pushdown`, then enable `enable_dynamic_filter_pushdown`, the `enable_topk_dynamic_filter_pushdown` will be overridden.
pub enable_dynamic_filter_pushdown: bool, default = true
/// When set to true, the optimizer will insert filters before a join between
/// a nullable and non-nullable column to filter out nulls on the nullable side. This
/// filter can add additional overhead when the file format does not fully support
/// predicate push down.
pub filter_null_join_keys: bool, default = false
/// Should DataFusion repartition data using the aggregate keys to execute aggregates
/// in parallel using the provided `target_partitions` level
pub repartition_aggregations: bool, default = true
/// Minimum total files size in bytes to perform file scan repartitioning.
pub repartition_file_min_size: usize, default = 10 * 1024 * 1024
/// Should DataFusion repartition data using the join keys to execute joins in parallel
/// using the provided `target_partitions` level
pub repartition_joins: bool, default = true
/// Should DataFusion allow symmetric hash joins for unbounded data sources even when
/// its inputs do not have any ordering or filtering If the flag is not enabled,
/// the SymmetricHashJoin operator will be unable to prune its internal buffers,
/// resulting in certain join types - such as Full, Left, LeftAnti, LeftSemi, Right,
/// RightAnti, and RightSemi - being produced only at the end of the execution.
/// This is not typical in stream processing. Additionally, without proper design for
/// long runner execution, all types of joins may encounter out-of-memory errors.
pub allow_symmetric_joins_without_pruning: bool, default = true
/// When set to `true`, datasource partitions will be repartitioned to achieve maximum parallelism.
/// This applies to both in-memory partitions and FileSource's file groups (1 group is 1 partition).
///
/// For FileSources, only Parquet and CSV formats are currently supported.
///
/// If set to `true` for a FileSource, all files will be repartitioned evenly (i.e., a single large file
/// might be partitioned into smaller chunks) for parallel scanning.
/// If set to `false` for a FileSource, different files will be read in parallel, but repartitioning won't
/// happen within a single file.
///
/// If set to `true` for an in-memory source, all memtable's partitions will have their batches
/// repartitioned evenly to the desired number of `target_partitions`. Repartitioning can change
/// the total number of partitions and batches per partition, but does not slice the initial
/// record tables provided to the MemTable on creation.
pub repartition_file_scans: bool, default = true
/// Should DataFusion repartition data using the partitions keys to execute window
/// functions in parallel using the provided `target_partitions` level
pub repartition_windows: bool, default = true
/// Should DataFusion execute sorts in a per-partition fashion and merge
/// afterwards instead of coalescing first and sorting globally.
/// With this flag is enabled, plans in the form below
///
/// ```text
/// "SortExec: [a@0 ASC]",
/// " CoalescePartitionsExec",
/// " RepartitionExec: partitioning=RoundRobinBatch(8), input_partitions=1",
/// ```
/// would turn into the plan below which performs better in multithreaded environments
///
/// ```text
/// "SortPreservingMergeExec: [a@0 ASC]",
/// " SortExec: [a@0 ASC]",
/// " RepartitionExec: partitioning=RoundRobinBatch(8), input_partitions=1",
/// ```
pub repartition_sorts: bool, default = true
/// When true, DataFusion will opportunistically remove sorts when the data is already sorted,
/// (i.e. setting `preserve_order` to true on `RepartitionExec` and
/// using `SortPreservingMergeExec`)
///
/// When false, DataFusion will maximize plan parallelism using
/// `RepartitionExec` even if this requires subsequently resorting data using a `SortExec`.
pub prefer_existing_sort: bool, default = false
/// When set to true, the logical plan optimizer will produce warning
/// messages if any optimization rules produce errors and then proceed to the next
/// rule. When set to false, any rules that produce errors will cause the query to fail
pub skip_failed_rules: bool, default = false
/// Number of times that the optimizer will attempt to optimize the plan
pub max_passes: usize, default = 3
/// When set to true, the physical plan optimizer will run a top down
/// process to reorder the join keys
pub top_down_join_key_reordering: bool, default = true
/// When set to true, the physical plan optimizer will prefer HashJoin over SortMergeJoin.
/// HashJoin can work more efficiently than SortMergeJoin but consumes more memory
pub prefer_hash_join: bool, default = true
/// When set to true, piecewise merge join is enabled. PiecewiseMergeJoin is currently
/// experimental. Physical planner will opt for PiecewiseMergeJoin when there is only
/// one range filter.
pub enable_piecewise_merge_join: bool, default = false
/// The maximum estimated size in bytes for one input side of a HashJoin
/// will be collected into a single partition
pub hash_join_single_partition_threshold: usize, default = 1024 * 1024
/// The maximum estimated size in rows for one input side of a HashJoin
/// will be collected into a single partition
pub hash_join_single_partition_threshold_rows: usize, default = 1024 * 128
/// The default filter selectivity used by Filter Statistics
/// when an exact selectivity cannot be determined. Valid values are
/// between 0 (no selectivity) and 100 (all rows are selected).
pub default_filter_selectivity: u8, default = 20
/// When set to true, the optimizer will not attempt to convert Union to Interleave
pub prefer_existing_union: bool, default = false
/// When set to true, if the returned type is a view type
/// then the output will be coerced to a non-view.
/// Coerces `Utf8View` to `LargeUtf8`, and `BinaryView` to `LargeBinary`.
pub expand_views_at_output: bool, default = false
}
}
config_namespace! {
/// Options controlling explain output
///
/// See also: [`SessionConfig`]
///
/// [`SessionConfig`]: https://docs.rs/datafusion/latest/datafusion/prelude/struct.SessionConfig.html
pub struct ExplainOptions {
/// When set to true, the explain statement will only print logical plans
pub logical_plan_only: bool, default = false
/// When set to true, the explain statement will only print physical plans
pub physical_plan_only: bool, default = false
/// When set to true, the explain statement will print operator statistics
/// for physical plans
pub show_statistics: bool, default = false
/// When set to true, the explain statement will print the partition sizes
pub show_sizes: bool, default = true
/// When set to true, the explain statement will print schema information
pub show_schema: bool, default = false
/// Display format of explain. Default is "indent".
/// When set to "tree", it will print the plan in a tree-rendered format.
pub format: ExplainFormat, default = ExplainFormat::Indent
/// (format=tree only) Maximum total width of the rendered tree.
/// When set to 0, the tree will have no width limit.
pub tree_maximum_render_width: usize, default = 240
/// Verbosity level for "EXPLAIN ANALYZE". Default is "dev"
/// "summary" shows common metrics for high-level insights.
/// "dev" provides deep operator-level introspection for developers.
pub analyze_level: ExplainAnalyzeLevel, default = ExplainAnalyzeLevel::Dev
}
}
impl ExecutionOptions {
/// Returns the correct parallelism based on the provided `value`.
/// If `value` is `"0"`, returns the default available parallelism, computed with
/// `get_available_parallelism`. Otherwise, returns `value`.
fn normalized_parallelism(value: &str) -> String {
if value.parse::<usize>() == Ok(0) {
get_available_parallelism().to_string()
} else {
value.to_owned()
}
}
}
config_namespace! {
/// Options controlling the format of output when printing record batches
/// Copies [`arrow::util::display::FormatOptions`]
pub struct FormatOptions {
/// If set to `true` any formatting errors will be written to the output
/// instead of being converted into a [`std::fmt::Error`]
pub safe: bool, default = true
/// Format string for nulls
pub null: String, default = "".into()
/// Date format for date arrays
pub date_format: Option<String>, default = Some("%Y-%m-%d".to_string())
/// Format for DateTime arrays
pub datetime_format: Option<String>, default = Some("%Y-%m-%dT%H:%M:%S%.f".to_string())
/// Timestamp format for timestamp arrays
pub timestamp_format: Option<String>, default = Some("%Y-%m-%dT%H:%M:%S%.f".to_string())
/// Timestamp format for timestamp with timezone arrays. When `None`, ISO 8601 format is used.
pub timestamp_tz_format: Option<String>, default = None
/// Time format for time arrays
pub time_format: Option<String>, default = Some("%H:%M:%S%.f".to_string())
/// Duration format. Can be either `"pretty"` or `"ISO8601"`
pub duration_format: String, transform = str::to_lowercase, default = "pretty".into()
/// Show types in visual representation batches
pub types_info: bool, default = false
}
}
impl<'a> TryInto<arrow::util::display::FormatOptions<'a>> for &'a FormatOptions {
type Error = DataFusionError;
fn try_into(self) -> Result<arrow::util::display::FormatOptions<'a>> {
let duration_format = match self.duration_format.as_str() {
"pretty" => arrow::util::display::DurationFormat::Pretty,
"iso8601" => arrow::util::display::DurationFormat::ISO8601,
_ => {
return _config_err!(
"Invalid duration format: {}. Valid values are pretty or iso8601",
self.duration_format
)
}
};
Ok(arrow::util::display::FormatOptions::new()
.with_display_error(self.safe)
.with_null(&self.null)
.with_date_format(self.date_format.as_deref())
.with_datetime_format(self.datetime_format.as_deref())
.with_timestamp_format(self.timestamp_format.as_deref())
.with_timestamp_tz_format(self.timestamp_tz_format.as_deref())
.with_time_format(self.time_format.as_deref())
.with_duration_format(duration_format)
.with_types_info(self.types_info))
}
}
/// A key value pair, with a corresponding description
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct ConfigEntry {
/// A unique string to identify this config value
pub key: String,
/// The value if any
pub value: Option<String>,
/// A description of this configuration entry
pub description: &'static str,
}
/// Configuration options struct, able to store both built-in configuration and custom options
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ConfigOptions {
/// Catalog options
pub catalog: CatalogOptions,
/// Execution options
pub execution: ExecutionOptions,
/// Optimizer options
pub optimizer: OptimizerOptions,
/// SQL parser options
pub sql_parser: SqlParserOptions,
/// Explain options
pub explain: ExplainOptions,
/// Optional extensions registered using [`Extensions::insert`]
pub extensions: Extensions,
/// Formatting options when printing batches
pub format: FormatOptions,
}
impl ConfigField for ConfigOptions {
fn set(&mut self, key: &str, value: &str) -> Result<()> {
// Extensions are handled in the public `ConfigOptions::set`
let (key, rem) = key.split_once('.').unwrap_or((key, ""));
match key {
"catalog" => self.catalog.set(rem, value),
"execution" => self.execution.set(rem, value),
"optimizer" => self.optimizer.set(rem, value),
"explain" => self.explain.set(rem, value),
"sql_parser" => self.sql_parser.set(rem, value),
"format" => self.format.set(rem, value),
_ => _config_err!("Config value \"{key}\" not found on ConfigOptions"),
}
}
fn visit<V: Visit>(&self, v: &mut V, _key_prefix: &str, _description: &'static str) {
self.catalog.visit(v, "datafusion.catalog", "");
self.execution.visit(v, "datafusion.execution", "");
self.optimizer.visit(v, "datafusion.optimizer", "");
self.explain.visit(v, "datafusion.explain", "");
self.sql_parser.visit(v, "datafusion.sql_parser", "");
self.format.visit(v, "datafusion.format", "");
}
}
impl ConfigOptions {
/// Creates a new [`ConfigOptions`] with default values
pub fn new() -> Self {
Self::default()
}
/// Set extensions to provided value
pub fn with_extensions(mut self, extensions: Extensions) -> Self {
self.extensions = extensions;
self
}
/// Set a configuration option
pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
let Some((prefix, key)) = key.split_once('.') else {
return _config_err!("could not find config namespace for key \"{key}\"");
};
if prefix == "datafusion" {
if key == "optimizer.enable_dynamic_filter_pushdown" {
let bool_value = value.parse::<bool>().map_err(|e| {
DataFusionError::Configuration(format!(
"Failed to parse '{value}' as bool: {e}",
))
})?;
{
self.optimizer.enable_dynamic_filter_pushdown = bool_value;
self.optimizer.enable_topk_dynamic_filter_pushdown = bool_value;
self.optimizer.enable_join_dynamic_filter_pushdown = bool_value;
}
return Ok(());
}
return ConfigField::set(self, key, value);
}
let Some(e) = self.extensions.0.get_mut(prefix) else {
return _config_err!("Could not find config namespace \"{prefix}\"");
};
e.0.set(key, value)
}
/// Create new [`ConfigOptions`], taking values from environment variables
/// where possible.
///
/// For example, to configure `datafusion.execution.batch_size`
/// ([`ExecutionOptions::batch_size`]) you would set the
/// `DATAFUSION_EXECUTION_BATCH_SIZE` environment variable.
///
/// The name of the environment variable is the option's key, transformed to
/// uppercase and with periods replaced with underscores.
///
/// Values are parsed according to the [same rules used in casts from
/// Utf8](https://docs.rs/arrow/latest/arrow/compute/kernels/cast/fn.cast.html).
///
/// If the value in the environment variable cannot be cast to the type of
/// the configuration option, the default value will be used instead and a
/// warning emitted. Environment variables are read when this method is
/// called, and are not re-read later.
pub fn from_env() -> Result<Self> {
struct Visitor(Vec<String>);
impl Visit for Visitor {
fn some<V: Display>(&mut self, key: &str, _: V, _: &'static str) {
self.0.push(key.to_string())
}
fn none(&mut self, key: &str, _: &'static str) {
self.0.push(key.to_string())
}
}
// Extract the names of all fields and then look up the corresponding
// environment variables. This isn't hugely efficient but avoids
// ambiguity between `a.b` and `a_b` which would both correspond
// to an environment variable of `A_B`
let mut keys = Visitor(vec![]);
let mut ret = Self::default();
ret.visit(&mut keys, "datafusion", "");
for key in keys.0 {
let env = key.to_uppercase().replace('.', "_");
if let Some(var) = std::env::var_os(env) {
let value = var.to_string_lossy();
log::info!("Set {key} to {value} from the environment variable");
ret.set(&key, value.as_ref())?;
}
}
Ok(ret)
}
/// Create new ConfigOptions struct, taking values from a string hash map.
///
/// Only the built-in configurations will be extracted from the hash map
/// and other key value pairs will be ignored.
pub fn from_string_hash_map(settings: &HashMap<String, String>) -> Result<Self> {
struct Visitor(Vec<String>);
impl Visit for Visitor {
fn some<V: Display>(&mut self, key: &str, _: V, _: &'static str) {
self.0.push(key.to_string())
}
fn none(&mut self, key: &str, _: &'static str) {
self.0.push(key.to_string())
}
}
let mut keys = Visitor(vec![]);
let mut ret = Self::default();
ret.visit(&mut keys, "datafusion", "");
for key in keys.0 {
if let Some(var) = settings.get(&key) {
ret.set(&key, var)?;
}
}
Ok(ret)
}
/// Returns the [`ConfigEntry`] stored within this [`ConfigOptions`]
pub fn entries(&self) -> Vec<ConfigEntry> {
struct Visitor(Vec<ConfigEntry>);
impl Visit for Visitor {
fn some<V: Display>(
&mut self,
key: &str,
value: V,
description: &'static str,
) {
self.0.push(ConfigEntry {
key: key.to_string(),
value: Some(value.to_string()),
description,
})
}
fn none(&mut self, key: &str, description: &'static str) {
self.0.push(ConfigEntry {
key: key.to_string(),
value: None,
description,
})
}
}
let mut v = Visitor(vec![]);
self.visit(&mut v, "datafusion", "");
v.0.extend(self.extensions.0.values().flat_map(|e| e.0.entries()));
v.0
}
/// Generate documentation that can be included in the user guide
pub fn generate_config_markdown() -> String {
use std::fmt::Write as _;
let mut s = Self::default();
// Normalize for display
s.execution.target_partitions = 0;
s.execution.planning_concurrency = 0;
let mut docs = "| key | default | description |\n".to_string();
docs += "|-----|---------|-------------|\n";
let mut entries = s.entries();
entries.sort_unstable_by(|a, b| a.key.cmp(&b.key));
for entry in s.entries() {
let _ = writeln!(
&mut docs,
"| {} | {} | {} |",
entry.key,
entry.value.as_deref().unwrap_or("NULL"),
entry.description
);
}
docs
}
}
/// [`ConfigExtension`] provides a mechanism to store third-party configuration
/// within DataFusion [`ConfigOptions`]
///
/// This mechanism can be used to pass configuration to user defined functions
/// or optimizer passes
///
/// # Example
/// ```
/// use datafusion_common::{
/// config::ConfigExtension, config::ConfigOptions, extensions_options,
/// };
/// // Define a new configuration struct using the `extensions_options` macro
/// extensions_options! {
/// /// My own config options.
/// pub struct MyConfig {
/// /// Should "foo" be replaced by "bar"?
/// pub foo_to_bar: bool, default = true
///
/// /// How many "baz" should be created?
/// pub baz_count: usize, default = 1337
/// }
/// }
///
/// impl ConfigExtension for MyConfig {
/// const PREFIX: &'static str = "my_config";
/// }
///
/// // set up config struct and register extension
/// let mut config = ConfigOptions::default();
/// config.extensions.insert(MyConfig::default());
///
/// // overwrite config default
/// config.set("my_config.baz_count", "42").unwrap();
///
/// // check config state
/// let my_config = config.extensions.get::<MyConfig>().unwrap();
/// assert!(my_config.foo_to_bar,);
/// assert_eq!(my_config.baz_count, 42,);
/// ```
///
/// # Note:
/// Unfortunately associated constants are not currently object-safe, and so this
/// extends the object-safe [`ExtensionOptions`]
pub trait ConfigExtension: ExtensionOptions {
/// Configuration namespace prefix to use
///
/// All values under this will be prefixed with `$PREFIX + "."`
const PREFIX: &'static str;
}
/// An object-safe API for storing arbitrary configuration.
///
/// See [`ConfigExtension`] for user defined configuration
pub trait ExtensionOptions: Send + Sync + fmt::Debug + 'static {
/// Return `self` as [`Any`]
///
/// This is needed until trait upcasting is stabilized
fn as_any(&self) -> &dyn Any;
/// Return `self` as [`Any`]
///
/// This is needed until trait upcasting is stabilized
fn as_any_mut(&mut self) -> &mut dyn Any;
/// Return a deep clone of this [`ExtensionOptions`]
///
/// It is important this does not share mutable state to avoid consistency issues
/// with configuration changing whilst queries are executing
fn cloned(&self) -> Box<dyn ExtensionOptions>;
/// Set the given `key`, `value` pair
fn set(&mut self, key: &str, value: &str) -> Result<()>;
/// Returns the [`ConfigEntry`] stored in this [`ExtensionOptions`]
fn entries(&self) -> Vec<ConfigEntry>;
}
/// A type-safe container for [`ConfigExtension`]
#[derive(Debug, Default, Clone)]
pub struct Extensions(BTreeMap<&'static str, ExtensionBox>);
impl Extensions {
/// Create a new, empty [`Extensions`]
pub fn new() -> Self {
Self(BTreeMap::new())
}
/// Registers a [`ConfigExtension`] with this [`ConfigOptions`]
pub fn insert<T: ConfigExtension>(&mut self, extension: T) {
assert_ne!(T::PREFIX, "datafusion");
let e = ExtensionBox(Box::new(extension));
self.0.insert(T::PREFIX, e);
}
/// Retrieves the extension of the given type if any
pub fn get<T: ConfigExtension>(&self) -> Option<&T> {
self.0.get(T::PREFIX)?.0.as_any().downcast_ref()
}
/// Retrieves the extension of the given type if any
pub fn get_mut<T: ConfigExtension>(&mut self) -> Option<&mut T> {
let e = self.0.get_mut(T::PREFIX)?;
e.0.as_any_mut().downcast_mut()
}
}
#[derive(Debug)]
struct ExtensionBox(Box<dyn ExtensionOptions>);
impl Clone for ExtensionBox {
fn clone(&self) -> Self {
Self(self.0.cloned())
}
}
/// A trait implemented by `config_namespace` and for field types that provides
/// the ability to walk and mutate the configuration tree
pub trait ConfigField {
fn visit<V: Visit>(&self, v: &mut V, key: &str, description: &'static str);
fn set(&mut self, key: &str, value: &str) -> Result<()>;
}
impl<F: ConfigField + Default> ConfigField for Option<F> {
fn visit<V: Visit>(&self, v: &mut V, key: &str, description: &'static str) {
match self {
Some(s) => s.visit(v, key, description),
None => v.none(key, description),
}
}
fn set(&mut self, key: &str, value: &str) -> Result<()> {
self.get_or_insert_with(Default::default).set(key, value)
}
}
/// Default transformation to parse a [`ConfigField`] for a string.
///
/// This uses [`FromStr`] to parse the data.
pub fn default_config_transform<T>(input: &str) -> Result<T>
where
T: FromStr,
<T as FromStr>::Err: Sync + Send + Error + 'static,
{
input.parse().map_err(|e| {
DataFusionError::Context(
format!(
"Error parsing '{}' as {}",
input,
std::any::type_name::<T>()
),
Box::new(DataFusionError::External(Box::new(e))),
)
})
}
/// Macro that generates [`ConfigField`] for a given type.
///
/// # Usage
/// This always requires [`Display`] to be implemented for the given type.
///
/// There are two ways to invoke this macro. The first one uses
/// [`default_config_transform`]/[`FromStr`] to parse the data:
///
/// ```ignore
/// config_field(MyType);
/// ```
///
/// Note that the parsing error MUST implement [`std::error::Error`]!
///
/// Or you can specify how you want to parse an [`str`] into the type:
///
/// ```ignore
/// fn parse_it(s: &str) -> Result<MyType> {
/// ...
/// }
///
/// config_field(
/// MyType,
/// value => parse_it(value)
/// );
/// ```
#[macro_export]
macro_rules! config_field {
($t:ty) => {
config_field!($t, value => $crate::config::default_config_transform(value)?);
};
($t:ty, $arg:ident => $transform:expr) => {
impl $crate::config::ConfigField for $t {
fn visit<V: $crate::config::Visit>(&self, v: &mut V, key: &str, description: &'static str) {
v.some(key, self, description)
}
fn set(&mut self, _: &str, $arg: &str) -> $crate::error::Result<()> {
*self = $transform;
Ok(())
}
}
};
}
config_field!(String);
config_field!(bool, value => default_config_transform(value.to_lowercase().as_str())?);
config_field!(usize);
config_field!(f64);
config_field!(u64);
impl ConfigField for u8 {
fn visit<V: Visit>(&self, v: &mut V, key: &str, description: &'static str) {
v.some(key, self, description)
}
fn set(&mut self, key: &str, value: &str) -> Result<()> {
if value.is_empty() {
return Err(DataFusionError::Configuration(format!(
"Input string for {key} key is empty"
)));
}
// Check if the string is a valid number
if let Ok(num) = value.parse::<u8>() {
// TODO: Let's decide how we treat the numerical strings.
*self = num;
} else {
let bytes = value.as_bytes();
// Check if the first character is ASCII (single byte)
if bytes.len() > 1 || !value.chars().next().unwrap().is_ascii() {
return Err(DataFusionError::Configuration(format!(
"Error parsing {value} as u8. Non-ASCII string provided"
)));
}
*self = bytes[0];
}
Ok(())
}
}
impl ConfigField for CompressionTypeVariant {
fn visit<V: Visit>(&self, v: &mut V, key: &str, description: &'static str) {
v.some(key, self, description)
}
fn set(&mut self, _: &str, value: &str) -> Result<()> {
*self = CompressionTypeVariant::from_str(value)?;
Ok(())
}
}
/// An implementation trait used to recursively walk configuration
pub trait Visit {
fn some<V: Display>(&mut self, key: &str, value: V, description: &'static str);
fn none(&mut self, key: &str, description: &'static str);
}
/// Convenience macro to create [`ExtensionsOptions`].
///
/// The created structure implements the following traits:
///
/// - [`Clone`]
/// - [`Debug`]
/// - [`Default`]
/// - [`ExtensionOptions`]
///
/// # Usage
/// The syntax is:
///
/// ```text
/// extensions_options! {
/// /// Struct docs (optional).
/// [<vis>] struct <StructName> {
/// /// Field docs (optional)
/// [<vis>] <field_name>: <field_type>, default = <default_value>
///
/// ... more fields
/// }
/// }
/// ```
///
/// The placeholders are:
/// - `[<vis>]`: Optional visibility modifier like `pub` or `pub(crate)`.
/// - `<StructName>`: Struct name like `MyStruct`.
/// - `<field_name>`: Field name like `my_field`.
/// - `<field_type>`: Field type like `u8`.
/// - `<default_value>`: Default value matching the field type like `42`.
///
/// # Example
/// See also a full example on the [`ConfigExtension`] documentation
///
/// ```
/// use datafusion_common::extensions_options;
///
/// extensions_options! {
/// /// My own config options.
/// pub struct MyConfig {
/// /// Should "foo" be replaced by "bar"?
/// pub foo_to_bar: bool, default = true
///
/// /// How many "baz" should be created?
/// pub baz_count: usize, default = 1337
/// }
/// }
/// ```
///
///
/// [`Debug`]: std::fmt::Debug
/// [`ExtensionsOptions`]: crate::config::ExtensionOptions
#[macro_export]
macro_rules! extensions_options {
(
$(#[doc = $struct_d:tt])*
$vis:vis struct $struct_name:ident {
$(
$(#[doc = $d:tt])*
$field_vis:vis $field_name:ident : $field_type:ty, default = $default:expr
)*$(,)*
}
) => {
$(#[doc = $struct_d])*
#[derive(Debug, Clone)]
#[non_exhaustive]
$vis struct $struct_name{
$(
$(#[doc = $d])*
$field_vis $field_name : $field_type,
)*
}
impl Default for $struct_name {
fn default() -> Self {
Self {
$($field_name: $default),*
}
}
}
impl $crate::config::ExtensionOptions for $struct_name {
fn as_any(&self) -> &dyn ::std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn ::std::any::Any {
self
}
fn cloned(&self) -> Box<dyn $crate::config::ExtensionOptions> {
Box::new(self.clone())
}
fn set(&mut self, key: &str, value: &str) -> $crate::error::Result<()> {
$crate::config::ConfigField::set(self, key, value)
}
fn entries(&self) -> Vec<$crate::config::ConfigEntry> {
struct Visitor(Vec<$crate::config::ConfigEntry>);
impl $crate::config::Visit for Visitor {
fn some<V: std::fmt::Display>(
&mut self,
key: &str,
value: V,
description: &'static str,
) {
self.0.push($crate::config::ConfigEntry {
key: key.to_string(),
value: Some(value.to_string()),
description,
})
}
fn none(&mut self, key: &str, description: &'static str) {
self.0.push($crate::config::ConfigEntry {
key: key.to_string(),
value: None,
description,
})
}
}
let mut v = Visitor(vec![]);
// The prefix is not used for extensions.
// The description is generated in ConfigField::visit.
// We can just pass empty strings here.
$crate::config::ConfigField::visit(self, &mut v, "", "");
v.0
}
}
impl $crate::config::ConfigField for $struct_name {
fn set(&mut self, key: &str, value: &str) -> $crate::error::Result<()> {
let (key, rem) = key.split_once('.').unwrap_or((key, ""));
match key {
$(
stringify!($field_name) => {
// Safely apply deprecated attribute if present
// $(#[allow(deprecated)])?
{
#[allow(deprecated)]
self.$field_name.set(rem, value.as_ref())
}
},
)*
_ => return $crate::error::_config_err!(
"Config value \"{}\" not found on {}", key, stringify!($struct_name)
)
}
}
fn visit<V: $crate::config::Visit>(&self, v: &mut V, _key_prefix: &str, _description: &'static str) {
$(
let key = stringify!($field_name).to_string();
let desc = concat!($($d),*).trim();
#[allow(deprecated)]
self.$field_name.visit(v, key.as_str(), desc);
)*
}
}
}
}
/// These file types have special built in behavior for configuration.
/// Use TableOptions::Extensions for configuring other file types.
#[derive(Debug, Clone)]
pub enum ConfigFileType {
CSV,
#[cfg(feature = "parquet")]
PARQUET,
JSON,
}
/// Represents the configuration options available for handling different table formats within a data processing application.
/// This struct encompasses options for various file formats including CSV, Parquet, and JSON, allowing for flexible configuration
/// of parsing and writing behaviors specific to each format. Additionally, it supports extending functionality through custom extensions.
#[derive(Debug, Clone, Default)]
pub struct TableOptions {
/// Configuration options for CSV file handling. This includes settings like the delimiter,
/// quote character, and whether the first row is considered as headers.
pub csv: CsvOptions,
/// Configuration options for Parquet file handling. This includes settings for compression,
/// encoding, and other Parquet-specific file characteristics.
pub parquet: TableParquetOptions,
/// Configuration options for JSON file handling.
pub json: JsonOptions,
/// The current file format that the table operations should assume. This option allows
/// for dynamic switching between the supported file types (e.g., CSV, Parquet, JSON).
pub current_format: Option<ConfigFileType>,
/// Optional extensions that can be used to extend or customize the behavior of the table
/// options. Extensions can be registered using `Extensions::insert` and might include
/// custom file handling logic, additional configuration parameters, or other enhancements.
pub extensions: Extensions,
}
impl ConfigField for TableOptions {
/// Visits configuration settings for the current file format, or all formats if none is selected.
///
/// This method adapts the behavior based on whether a file format is currently selected in `current_format`.
/// If a format is selected, it visits only the settings relevant to that format. Otherwise,
/// it visits all available format settings.
fn visit<V: Visit>(&self, v: &mut V, _key_prefix: &str, _description: &'static str) {
if let Some(file_type) = &self.current_format {
match file_type {
#[cfg(feature = "parquet")]
ConfigFileType::PARQUET => self.parquet.visit(v, "format", ""),
ConfigFileType::CSV => self.csv.visit(v, "format", ""),
ConfigFileType::JSON => self.json.visit(v, "format", ""),
}
} else {
self.csv.visit(v, "csv", "");
self.parquet.visit(v, "parquet", "");
self.json.visit(v, "json", "");
}
}
/// Sets a configuration value for a specific key within `TableOptions`.
///
/// This method delegates setting configuration values to the specific file format configurations,
/// based on the current format selected. If no format is selected, it returns an error.
///
/// # Parameters
///
/// * `key`: The configuration key specifying which setting to adjust, prefixed with the format (e.g., "format.delimiter")
/// for CSV format.
/// * `value`: The value to set for the specified configuration key.
///
/// # Returns
///
/// A result indicating success or an error if the key is not recognized, if a format is not specified,
/// or if setting the configuration value fails for the specific format.
fn set(&mut self, key: &str, value: &str) -> Result<()> {
// Extensions are handled in the public `ConfigOptions::set`
let (key, rem) = key.split_once('.').unwrap_or((key, ""));
match key {
"format" => {
let Some(format) = &self.current_format else {
return _config_err!("Specify a format for TableOptions");
};
match format {
#[cfg(feature = "parquet")]
ConfigFileType::PARQUET => self.parquet.set(rem, value),
ConfigFileType::CSV => self.csv.set(rem, value),
ConfigFileType::JSON => self.json.set(rem, value),
}
}
_ => _config_err!("Config value \"{key}\" not found on TableOptions"),
}
}
}
impl TableOptions {
/// Constructs a new instance of `TableOptions` with default settings.
///
/// # Returns
///
/// A new `TableOptions` instance with default configuration values.
pub fn new() -> Self {
Self::default()
}
/// Creates a new `TableOptions` instance initialized with settings from a given session config.
///
/// # Parameters
///
/// * `config`: A reference to the session `ConfigOptions` from which to derive initial settings.
///
/// # Returns
///
/// A new `TableOptions` instance with settings applied from the session config.
pub fn default_from_session_config(config: &ConfigOptions) -> Self {
let initial = TableOptions::default();
initial.combine_with_session_config(config)
}
/// Updates the current `TableOptions` with settings from a given session config.
///
/// # Parameters
///
/// * `config`: A reference to the session `ConfigOptions` whose settings are to be applied.
///
/// # Returns
///
/// A new `TableOptions` instance with updated settings from the session config.
#[must_use = "this method returns a new instance"]
pub fn combine_with_session_config(&self, config: &ConfigOptions) -> Self {
let mut clone = self.clone();
clone.parquet.global = config.execution.parquet.clone();
clone
}
/// Sets the file format for the table.
///
/// # Parameters
///
/// * `format`: The file format to use (e.g., CSV, Parquet).
pub fn set_config_format(&mut self, format: ConfigFileType) {
self.current_format = Some(format);
}
/// Sets the extensions for this `TableOptions` instance.
///
/// # Parameters
///
/// * `extensions`: The `Extensions` instance to set.
///
/// # Returns
///
/// A new `TableOptions` instance with the specified extensions applied.
pub fn with_extensions(mut self, extensions: Extensions) -> Self {
self.extensions = extensions;
self
}
/// Sets a specific configuration option.
///
/// # Parameters
///
/// * `key`: The configuration key (e.g., "format.delimiter").
/// * `value`: The value to set for the specified key.
///
/// # Returns
///
/// A result indicating success or failure in setting the configuration option.
pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
let Some((prefix, _)) = key.split_once('.') else {
return _config_err!("could not find config namespace for key \"{key}\"");
};
if prefix == "format" {
return ConfigField::set(self, key, value);
}
if prefix == "execution" {
return Ok(());
}
let Some(e) = self.extensions.0.get_mut(prefix) else {
return _config_err!("Could not find config namespace \"{prefix}\"");
};
e.0.set(key, value)
}
/// Initializes a new `TableOptions` from a hash map of string settings.
///
/// # Parameters
///
/// * `settings`: A hash map where each key-value pair represents a configuration setting.
///
/// # Returns
///
/// A result containing the new `TableOptions` instance or an error if any setting could not be applied.
pub fn from_string_hash_map(settings: &HashMap<String, String>) -> Result<Self> {
let mut ret = Self::default();
for (k, v) in settings {
ret.set(k, v)?;
}
Ok(ret)
}
/// Modifies the current `TableOptions` instance with settings from a hash map.
///
/// # Parameters
///
/// * `settings`: A hash map where each key-value pair represents a configuration setting.
///
/// # Returns
///
/// A result indicating success or failure in applying the settings.
pub fn alter_with_string_hash_map(
&mut self,
settings: &HashMap<String, String>,
) -> Result<()> {
for (k, v) in settings {
self.set(k, v)?;
}
Ok(())
}
/// Retrieves all configuration entries from this `TableOptions`.
///
/// # Returns
///
/// A vector of `ConfigEntry` instances, representing all the configuration options within this `TableOptions`.
pub fn entries(&self) -> Vec<ConfigEntry> {
struct Visitor(Vec<ConfigEntry>);
impl Visit for Visitor {
fn some<V: Display>(
&mut self,
key: &str,
value: V,
description: &'static str,
) {
self.0.push(ConfigEntry {
key: key.to_string(),
value: Some(value.to_string()),
description,
})
}
fn none(&mut self, key: &str, description: &'static str) {
self.0.push(ConfigEntry {
key: key.to_string(),
value: None,
description,
})
}
}
let mut v = Visitor(vec![]);
self.visit(&mut v, "format", "");
v.0.extend(self.extensions.0.values().flat_map(|e| e.0.entries()));
v.0
}
}
/// Options that control how Parquet files are read, including global options
/// that apply to all columns and optional column-specific overrides
///
/// Closely tied to [`ParquetWriterOptions`](crate::file_options::parquet_writer::ParquetWriterOptions).
/// Properties not included in [`TableParquetOptions`] may not be configurable at the external API
/// (e.g. sorting_columns).
#[derive(Clone, Default, Debug, PartialEq)]
pub struct TableParquetOptions {
/// Global Parquet options that propagates to all columns.
pub global: ParquetOptions,
/// Column specific options. Default usage is parquet.XX::column.
pub column_specific_options: HashMap<String, ParquetColumnOptions>,
/// Additional file-level metadata to include. Inserted into the key_value_metadata
/// for the written [`FileMetaData`](https://docs.rs/parquet/latest/parquet/file/metadata/struct.FileMetaData.html).
///
/// Multiple entries are permitted
/// ```sql
/// OPTIONS (
/// 'format.metadata::key1' '',
/// 'format.metadata::key2' 'value',
/// 'format.metadata::key3' 'value has spaces',
/// 'format.metadata::key4' 'value has special chars :: :',
/// 'format.metadata::key_dupe' 'original will be overwritten',
/// 'format.metadata::key_dupe' 'final'
/// )
/// ```
pub key_value_metadata: HashMap<String, Option<String>>,
/// Options for configuring Parquet modular encryption
///
/// To use Parquet encryption, you must enable the `parquet_encryption` feature flag, as it is not activated by default.
/// See ConfigFileEncryptionProperties and ConfigFileDecryptionProperties in datafusion/common/src/config.rs
/// These can be set via 'format.crypto', for example:
/// ```sql
/// OPTIONS (
/// 'format.crypto.file_encryption.encrypt_footer' 'true',
/// 'format.crypto.file_encryption.footer_key_as_hex' '30313233343536373839303132333435', -- b"0123456789012345" */
/// 'format.crypto.file_encryption.column_key_as_hex::double_field' '31323334353637383930313233343530', -- b"1234567890123450"
/// 'format.crypto.file_encryption.column_key_as_hex::float_field' '31323334353637383930313233343531', -- b"1234567890123451"
/// -- Same for decryption
/// 'format.crypto.file_decryption.footer_key_as_hex' '30313233343536373839303132333435', -- b"0123456789012345"
/// 'format.crypto.file_decryption.column_key_as_hex::double_field' '31323334353637383930313233343530', -- b"1234567890123450"
/// 'format.crypto.file_decryption.column_key_as_hex::float_field' '31323334353637383930313233343531', -- b"1234567890123451"
/// )
/// ```
/// See datafusion-cli/tests/sql/encrypted_parquet.sql for a more complete example.
/// Note that keys must be provided as in hex format since these are binary strings.
pub crypto: ParquetEncryptionOptions,
}
impl TableParquetOptions {
/// Return new default TableParquetOptions
pub fn new() -> Self {
Self::default()
}
/// Set whether the encoding of the arrow metadata should occur
/// during the writing of parquet.
///
/// Default is to encode the arrow schema in the file kv_metadata.
pub fn with_skip_arrow_metadata(self, skip: bool) -> Self {
Self {
global: ParquetOptions {
skip_arrow_metadata: skip,
..self.global
},
..self
}
}
/// Retrieves all configuration entries from this `TableParquetOptions`.
///
/// # Returns
///
/// A vector of `ConfigEntry` instances, representing all the configuration options within this
pub fn entries(self: &TableParquetOptions) -> Vec<ConfigEntry> {
struct Visitor(Vec<ConfigEntry>);
impl Visit for Visitor {
fn some<V: Display>(
&mut self,
key: &str,
value: V,
description: &'static str,
) {
self.0.push(ConfigEntry {
key: key[1..].to_string(),
value: Some(value.to_string()),
description,
})
}
fn none(&mut self, key: &str, description: &'static str) {
self.0.push(ConfigEntry {
key: key[1..].to_string(),
value: None,
description,
})
}
}
let mut v = Visitor(vec![]);
self.visit(&mut v, "", "");
v.0
}
}
impl ConfigField for TableParquetOptions {
fn visit<V: Visit>(&self, v: &mut V, key_prefix: &str, description: &'static str) {
self.global.visit(v, key_prefix, description);
self.column_specific_options
.visit(v, key_prefix, description);
self.crypto
.visit(v, &format!("{key_prefix}.crypto"), description);
}
fn set(&mut self, key: &str, value: &str) -> Result<()> {
// Determine if the key is a global, metadata, or column-specific setting
if key.starts_with("metadata::") {
let k = match key.split("::").collect::<Vec<_>>()[..] {
[_meta] | [_meta, ""] => {
return _config_err!(
"Invalid metadata key provided, missing key in metadata::<key>"
)
}
[_meta, k] => k.into(),
_ => {
return _config_err!(
"Invalid metadata key provided, found too many '::' in \"{key}\""
)
}
};
self.key_value_metadata.insert(k, Some(value.into()));
Ok(())
} else if let Some(crypto_feature) = key.strip_prefix("crypto.") {
self.crypto.set(crypto_feature, value)
} else if key.contains("::") {
self.column_specific_options.set(key, value)
} else {
self.global.set(key, value)
}
}
}
macro_rules! config_namespace_with_hashmap {
(
$(#[doc = $struct_d:tt])*
$(#[deprecated($($struct_depr:tt)*)])? // Optional struct-level deprecated attribute
$vis:vis struct $struct_name:ident {
$(
$(#[doc = $d:tt])*
$(#[deprecated($($field_depr:tt)*)])? // Optional field-level deprecated attribute
$field_vis:vis $field_name:ident : $field_type:ty, $(transform = $transform:expr,)? default = $default:expr
)*$(,)*
}
) => {
$(#[doc = $struct_d])*
$(#[deprecated($($struct_depr)*)])? // Apply struct deprecation
#[derive(Debug, Clone, PartialEq)]
$vis struct $struct_name{
$(
$(#[doc = $d])*
$(#[deprecated($($field_depr)*)])? // Apply field deprecation
$field_vis $field_name : $field_type,
)*
}
impl ConfigField for $struct_name {
fn set(&mut self, key: &str, value: &str) -> Result<()> {
let (key, rem) = key.split_once('.').unwrap_or((key, ""));
match key {
$(
stringify!($field_name) => {
// Handle deprecated fields
#[allow(deprecated)] // Allow deprecated fields
$(let value = $transform(value);)?
self.$field_name.set(rem, value.as_ref())
},
)*
_ => _config_err!(
"Config value \"{}\" not found on {}", key, stringify!($struct_name)
)
}
}
fn visit<V: Visit>(&self, v: &mut V, key_prefix: &str, _description: &'static str) {
$(
let key = format!(concat!("{}.", stringify!($field_name)), key_prefix);
let desc = concat!($($d),*).trim();
// Handle deprecated fields
#[allow(deprecated)]
self.$field_name.visit(v, key.as_str(), desc);
)*
}
}
impl Default for $struct_name {
fn default() -> Self {
#[allow(deprecated)]
Self {
$($field_name: $default),*
}
}
}
impl ConfigField for HashMap<String,$struct_name> {
fn set(&mut self, key: &str, value: &str) -> Result<()> {
let parts: Vec<&str> = key.splitn(2, "::").collect();
match parts.as_slice() {
[inner_key, hashmap_key] => {
// Get or create the struct for the specified key
let inner_value = self
.entry((*hashmap_key).to_owned())
.or_insert_with($struct_name::default);
inner_value.set(inner_key, value)
}
_ => _config_err!("Unrecognized key '{key}'."),
}
}
fn visit<V: Visit>(&self, v: &mut V, key_prefix: &str, _description: &'static str) {
for (column_name, col_options) in self {
$(
let key = format!("{}.{field}::{}", key_prefix, column_name, field = stringify!($field_name));
let desc = concat!($($d),*).trim();
#[allow(deprecated)]
col_options.$field_name.visit(v, key.as_str(), desc);
)*
}
}
}
}
}
config_namespace_with_hashmap! {
/// Options controlling parquet format for individual columns.
///
/// See [`ParquetOptions`] for more details
pub struct ParquetColumnOptions {
/// Sets if bloom filter is enabled for the column path.
pub bloom_filter_enabled: Option<bool>, default = None
/// Sets encoding for the column path.
/// Valid values are: plain, plain_dictionary, rle,
/// bit_packed, delta_binary_packed, delta_length_byte_array,
/// delta_byte_array, rle_dictionary, and byte_stream_split.
/// These values are not case-sensitive. If NULL, uses
/// default parquet options
pub encoding: Option<String>, default = None
/// Sets if dictionary encoding is enabled for the column path. If NULL, uses
/// default parquet options
pub dictionary_enabled: Option<bool>, default = None
/// Sets default parquet compression codec for the column path.
/// Valid values are: uncompressed, snappy, gzip(level),
/// lzo, brotli(level), lz4, zstd(level), and lz4_raw.
/// These values are not case-sensitive. If NULL, uses
/// default parquet options
pub compression: Option<String>, transform = str::to_lowercase, default = None
/// Sets if statistics are enabled for the column
/// Valid values are: "none", "chunk", and "page"
/// These values are not case sensitive. If NULL, uses
/// default parquet options
pub statistics_enabled: Option<String>, default = None
/// Sets bloom filter false positive probability for the column path. If NULL, uses
/// default parquet options
pub bloom_filter_fpp: Option<f64>, default = None
/// Sets bloom filter number of distinct values. If NULL, uses
/// default parquet options
pub bloom_filter_ndv: Option<u64>, default = None
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ConfigFileEncryptionProperties {
/// Should the parquet footer be encrypted
/// default is true
pub encrypt_footer: bool,
/// Key to use for the parquet footer encoded in hex format
pub footer_key_as_hex: String,
/// Metadata information for footer key
pub footer_key_metadata_as_hex: String,
/// HashMap of column names --> (key in hex format, metadata)
pub column_encryption_properties: HashMap<String, ColumnEncryptionProperties>,
/// AAD prefix string uniquely identifies the file and prevents file swapping
pub aad_prefix_as_hex: String,
/// If true, store the AAD prefix in the file
/// default is false
pub store_aad_prefix: bool,
}
// Setup to match EncryptionPropertiesBuilder::new()
impl Default for ConfigFileEncryptionProperties {
fn default() -> Self {
ConfigFileEncryptionProperties {
encrypt_footer: true,
footer_key_as_hex: String::new(),
footer_key_metadata_as_hex: String::new(),
column_encryption_properties: Default::default(),
aad_prefix_as_hex: String::new(),
store_aad_prefix: false,
}
}
}
config_namespace_with_hashmap! {
pub struct ColumnEncryptionProperties {
/// Per column encryption key
pub column_key_as_hex: String, default = "".to_string()
/// Per column encryption key metadata
pub column_metadata_as_hex: Option<String>, default = None
}
}
impl ConfigField for ConfigFileEncryptionProperties {
fn visit<V: Visit>(&self, v: &mut V, key_prefix: &str, _description: &'static str) {
let key = format!("{key_prefix}.encrypt_footer");
let desc = "Encrypt the footer";
self.encrypt_footer.visit(v, key.as_str(), desc);
let key = format!("{key_prefix}.footer_key_as_hex");
let desc = "Key to use for the parquet footer";
self.footer_key_as_hex.visit(v, key.as_str(), desc);
let key = format!("{key_prefix}.footer_key_metadata_as_hex");
let desc = "Metadata to use for the parquet footer";
self.footer_key_metadata_as_hex.visit(v, key.as_str(), desc);
self.column_encryption_properties.visit(v, key_prefix, desc);
let key = format!("{key_prefix}.aad_prefix_as_hex");
let desc = "AAD prefix to use";
self.aad_prefix_as_hex.visit(v, key.as_str(), desc);
let key = format!("{key_prefix}.store_aad_prefix");
let desc = "If true, store the AAD prefix";
self.store_aad_prefix.visit(v, key.as_str(), desc);
self.aad_prefix_as_hex.visit(v, key.as_str(), desc);
}
fn set(&mut self, key: &str, value: &str) -> Result<()> {
// Any hex encoded values must be pre-encoded using
// hex::encode() before calling set.
if key.contains("::") {
// Handle any column specific properties
return self.column_encryption_properties.set(key, value);
};
let (key, rem) = key.split_once('.').unwrap_or((key, ""));
match key {
"encrypt_footer" => self.encrypt_footer.set(rem, value.as_ref()),
"footer_key_as_hex" => self.footer_key_as_hex.set(rem, value.as_ref()),
"footer_key_metadata_as_hex" => {
self.footer_key_metadata_as_hex.set(rem, value.as_ref())
}
"aad_prefix_as_hex" => self.aad_prefix_as_hex.set(rem, value.as_ref()),
"store_aad_prefix" => self.store_aad_prefix.set(rem, value.as_ref()),
_ => _config_err!(
"Config value \"{}\" not found on ConfigFileEncryptionProperties",
key
),
}
}
}
#[cfg(feature = "parquet_encryption")]
impl From<ConfigFileEncryptionProperties> for FileEncryptionProperties {
fn from(val: ConfigFileEncryptionProperties) -> Self {
let mut fep = FileEncryptionProperties::builder(
hex::decode(val.footer_key_as_hex).unwrap(),
)
.with_plaintext_footer(!val.encrypt_footer)
.with_aad_prefix_storage(val.store_aad_prefix);
if !val.footer_key_metadata_as_hex.is_empty() {
fep = fep.with_footer_key_metadata(
hex::decode(&val.footer_key_metadata_as_hex)
.expect("Invalid footer key metadata"),
);
}
for (column_name, encryption_props) in val.column_encryption_properties.iter() {
let encryption_key = hex::decode(&encryption_props.column_key_as_hex)
.expect("Invalid column encryption key");
let key_metadata = encryption_props
.column_metadata_as_hex
.as_ref()
.map(|x| hex::decode(x).expect("Invalid column metadata"));
match key_metadata {
Some(key_metadata) => {
fep = fep.with_column_key_and_metadata(
column_name,
encryption_key,
key_metadata,
);
}
None => {
fep = fep.with_column_key(column_name, encryption_key);
}
}
}
if !val.aad_prefix_as_hex.is_empty() {
let aad_prefix: Vec<u8> =
hex::decode(&val.aad_prefix_as_hex).expect("Invalid AAD prefix");
fep = fep.with_aad_prefix(aad_prefix);
}
Arc::unwrap_or_clone(fep.build().unwrap())
}
}
#[cfg(feature = "parquet_encryption")]
impl From<&Arc<FileEncryptionProperties>> for ConfigFileEncryptionProperties {
fn from(f: &Arc<FileEncryptionProperties>) -> Self {
let (column_names_vec, column_keys_vec, column_metas_vec) = f.column_keys();
let mut column_encryption_properties: HashMap<
String,
ColumnEncryptionProperties,
> = HashMap::new();
for (i, column_name) in column_names_vec.iter().enumerate() {
let column_key_as_hex = hex::encode(&column_keys_vec[i]);
let column_metadata_as_hex: Option<String> =
column_metas_vec.get(i).map(hex::encode);
column_encryption_properties.insert(
column_name.clone(),
ColumnEncryptionProperties {
column_key_as_hex,
column_metadata_as_hex,
},
);
}
let mut aad_prefix: Vec<u8> = Vec::new();
if let Some(prefix) = f.aad_prefix() {
aad_prefix = prefix.clone();
}
ConfigFileEncryptionProperties {
encrypt_footer: f.encrypt_footer(),
footer_key_as_hex: hex::encode(f.footer_key()),
footer_key_metadata_as_hex: f
.footer_key_metadata()
.map(hex::encode)
.unwrap_or_default(),
column_encryption_properties,
aad_prefix_as_hex: hex::encode(aad_prefix),
store_aad_prefix: f.store_aad_prefix(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ConfigFileDecryptionProperties {
/// Binary string to use for the parquet footer encoded in hex format
pub footer_key_as_hex: String,
/// HashMap of column names --> key in hex format
pub column_decryption_properties: HashMap<String, ColumnDecryptionProperties>,
/// AAD prefix string uniquely identifies the file and prevents file swapping
pub aad_prefix_as_hex: String,
/// If true, then verify signature for files with plaintext footers.
/// default = true
pub footer_signature_verification: bool,
}
config_namespace_with_hashmap! {
pub struct ColumnDecryptionProperties {
/// Per column encryption key
pub column_key_as_hex: String, default = "".to_string()
}
}
// Setup to match DecryptionPropertiesBuilder::new()
impl Default for ConfigFileDecryptionProperties {
fn default() -> Self {
ConfigFileDecryptionProperties {
footer_key_as_hex: String::new(),
column_decryption_properties: Default::default(),
aad_prefix_as_hex: String::new(),
footer_signature_verification: true,
}
}
}
impl ConfigField for ConfigFileDecryptionProperties {
fn visit<V: Visit>(&self, v: &mut V, key_prefix: &str, _description: &'static str) {
let key = format!("{key_prefix}.footer_key_as_hex");
let desc = "Key to use for the parquet footer";
self.footer_key_as_hex.visit(v, key.as_str(), desc);
let key = format!("{key_prefix}.aad_prefix_as_hex");
let desc = "AAD prefix to use";
self.aad_prefix_as_hex.visit(v, key.as_str(), desc);
let key = format!("{key_prefix}.footer_signature_verification");
let desc = "If true, verify the footer signature";
self.footer_signature_verification
.visit(v, key.as_str(), desc);
self.column_decryption_properties.visit(v, key_prefix, desc);
}
fn set(&mut self, key: &str, value: &str) -> Result<()> {
// Any hex encoded values must be pre-encoded using
// hex::encode() before calling set.
if key.contains("::") {
// Handle any column specific properties
return self.column_decryption_properties.set(key, value);
};
let (key, rem) = key.split_once('.').unwrap_or((key, ""));
match key {
"footer_key_as_hex" => self.footer_key_as_hex.set(rem, value.as_ref()),
"aad_prefix_as_hex" => self.aad_prefix_as_hex.set(rem, value.as_ref()),
"footer_signature_verification" => {
self.footer_signature_verification.set(rem, value.as_ref())
}
_ => _config_err!(
"Config value \"{}\" not found on ConfigFileEncryptionProperties",
key
),
}
}
}
#[cfg(feature = "parquet_encryption")]
impl From<ConfigFileDecryptionProperties> for FileDecryptionProperties {
fn from(val: ConfigFileDecryptionProperties) -> Self {
let mut column_names: Vec<&str> = Vec::new();
let mut column_keys: Vec<Vec<u8>> = Vec::new();
for (col_name, decryption_properties) in val.column_decryption_properties.iter() {
column_names.push(col_name.as_str());
column_keys.push(
hex::decode(&decryption_properties.column_key_as_hex)
.expect("Invalid column decryption key"),
);
}
let mut fep = FileDecryptionProperties::builder(
hex::decode(val.footer_key_as_hex).expect("Invalid footer key"),
)
.with_column_keys(column_names, column_keys)
.unwrap();
if !val.footer_signature_verification {
fep = fep.disable_footer_signature_verification();
}
if !val.aad_prefix_as_hex.is_empty() {
let aad_prefix =
hex::decode(&val.aad_prefix_as_hex).expect("Invalid AAD prefix");
fep = fep.with_aad_prefix(aad_prefix);
}
Arc::unwrap_or_clone(fep.build().unwrap())
}
}
#[cfg(feature = "parquet_encryption")]
impl From<&Arc<FileDecryptionProperties>> for ConfigFileDecryptionProperties {
fn from(f: &Arc<FileDecryptionProperties>) -> Self {
let (column_names_vec, column_keys_vec) = f.column_keys();
let mut column_decryption_properties: HashMap<
String,
ColumnDecryptionProperties,
> = HashMap::new();
for (i, column_name) in column_names_vec.iter().enumerate() {
let props = ColumnDecryptionProperties {
column_key_as_hex: hex::encode(column_keys_vec[i].clone()),
};
column_decryption_properties.insert(column_name.clone(), props);
}
let mut aad_prefix: Vec<u8> = Vec::new();
if let Some(prefix) = f.aad_prefix() {
aad_prefix = prefix.clone();
}
ConfigFileDecryptionProperties {
footer_key_as_hex: hex::encode(
f.footer_key(None).unwrap_or_default().as_ref(),
),
column_decryption_properties,
aad_prefix_as_hex: hex::encode(aad_prefix),
footer_signature_verification: f.check_plaintext_footer_integrity(),
}
}
}
/// Holds implementation-specific options for an encryption factory
#[derive(Clone, Debug, Default, PartialEq)]
pub struct EncryptionFactoryOptions {
pub options: HashMap<String, String>,
}
impl ConfigField for EncryptionFactoryOptions {
fn visit<V: Visit>(&self, v: &mut V, key: &str, _description: &'static str) {
for (option_key, option_value) in &self.options {
v.some(
&format!("{key}.{option_key}"),
option_value,
"Encryption factory specific option",
);
}
}
fn set(&mut self, key: &str, value: &str) -> Result<()> {
self.options.insert(key.to_owned(), value.to_owned());
Ok(())
}
}
impl EncryptionFactoryOptions {
/// Convert these encryption factory options to an [`ExtensionOptions`] instance.
pub fn to_extension_options<T: ExtensionOptions + Default>(&self) -> Result<T> {
let mut options = T::default();
for (key, value) in &self.options {
options.set(key, value)?;
}
Ok(options)
}
}
config_namespace! {
/// Options controlling CSV format
pub struct CsvOptions {
/// Specifies whether there is a CSV header (i.e. the first line
/// consists of is column names). The value `None` indicates that
/// the configuration should be consulted.
pub has_header: Option<bool>, default = None
pub delimiter: u8, default = b','
pub quote: u8, default = b'"'
pub terminator: Option<u8>, default = None
pub escape: Option<u8>, default = None
pub double_quote: Option<bool>, default = None
/// Specifies whether newlines in (quoted) values are supported.
///
/// Parsing newlines in quoted values may be affected by execution behaviour such as
/// parallel file scanning. Setting this to `true` ensures that newlines in values are
/// parsed successfully, which may reduce performance.
///
/// The default behaviour depends on the `datafusion.catalog.newlines_in_values` setting.
pub newlines_in_values: Option<bool>, default = None
pub compression: CompressionTypeVariant, default = CompressionTypeVariant::UNCOMPRESSED
pub schema_infer_max_rec: Option<usize>, default = None
pub date_format: Option<String>, default = None
pub datetime_format: Option<String>, default = None
pub timestamp_format: Option<String>, default = None
pub timestamp_tz_format: Option<String>, default = None
pub time_format: Option<String>, default = None
// The output format for Nulls in the CSV writer.
pub null_value: Option<String>, default = None
// The input regex for Nulls when loading CSVs.
pub null_regex: Option<String>, default = None
pub comment: Option<u8>, default = None
/// Whether to allow truncated rows when parsing, both within a single file and across files.
///
/// When set to false (default), reading a single CSV file which has rows of different lengths will
/// error; if reading multiple CSV files with different number of columns, it will also fail.
///
/// When set to true, reading a single CSV file with rows of different lengths will pad the truncated
/// rows with null values for the missing columns; if reading multiple CSV files with different number
/// of columns, it creates a union schema containing all columns found across the files, and will
/// pad any files missing columns with null values for their rows.
pub truncated_rows: Option<bool>, default = None
}
}
impl CsvOptions {
/// Set a limit in terms of records to scan to infer the schema
/// - default to `DEFAULT_SCHEMA_INFER_MAX_RECORD`
pub fn with_compression(
mut self,
compression_type_variant: CompressionTypeVariant,
) -> Self {
self.compression = compression_type_variant;
self
}
/// Set a limit in terms of records to scan to infer the schema
/// - default to `DEFAULT_SCHEMA_INFER_MAX_RECORD`
pub fn with_schema_infer_max_rec(mut self, max_rec: usize) -> Self {
self.schema_infer_max_rec = Some(max_rec);
self
}
/// Set true to indicate that the first line is a header.
/// - default to true
pub fn with_has_header(mut self, has_header: bool) -> Self {
self.has_header = Some(has_header);
self
}
/// Returns true if the first line is a header. If format options does not
/// specify whether there is a header, returns `None` (indicating that the
/// configuration should be consulted).
pub fn has_header(&self) -> Option<bool> {
self.has_header
}
/// The character separating values within a row.
/// - default to ','
pub fn with_delimiter(mut self, delimiter: u8) -> Self {
self.delimiter = delimiter;
self
}
/// The quote character in a row.
/// - default to '"'
pub fn with_quote(mut self, quote: u8) -> Self {
self.quote = quote;
self
}
/// The character that terminates a row.
/// - default to None (CRLF)
pub fn with_terminator(mut self, terminator: Option<u8>) -> Self {
self.terminator = terminator;
self
}
/// The escape character in a row.
/// - default is None
pub fn with_escape(mut self, escape: Option<u8>) -> Self {
self.escape = escape;
self
}
/// Set true to indicate that the CSV quotes should be doubled.
/// - default to true
pub fn with_double_quote(mut self, double_quote: bool) -> Self {
self.double_quote = Some(double_quote);
self
}
/// Specifies whether newlines in (quoted) values are supported.
///
/// Parsing newlines in quoted values may be affected by execution behaviour such as
/// parallel file scanning. Setting this to `true` ensures that newlines in values are
/// parsed successfully, which may reduce performance.
///
/// The default behaviour depends on the `datafusion.catalog.newlines_in_values` setting.
pub fn with_newlines_in_values(mut self, newlines_in_values: bool) -> Self {
self.newlines_in_values = Some(newlines_in_values);
self
}
/// Set a `CompressionTypeVariant` of CSV
/// - defaults to `CompressionTypeVariant::UNCOMPRESSED`
pub fn with_file_compression_type(
mut self,
compression: CompressionTypeVariant,
) -> Self {
self.compression = compression;
self
}
/// Whether to allow truncated rows when parsing.
/// By default this is set to false and will error if the CSV rows have different lengths.
/// When set to true then it will allow records with less than the expected number of columns and fill the missing columns with nulls.
/// If the record’s schema is not nullable, then it will still return an error.
pub fn with_truncated_rows(mut self, allow: bool) -> Self {
self.truncated_rows = Some(allow);
self
}
/// The delimiter character.
pub fn delimiter(&self) -> u8 {
self.delimiter
}
/// The quote character.
pub fn quote(&self) -> u8 {
self.quote
}
/// The terminator character.
pub fn terminator(&self) -> Option<u8> {
self.terminator
}
/// The escape character.
pub fn escape(&self) -> Option<u8> {
self.escape
}
}
config_namespace! {
/// Options controlling JSON format
pub struct JsonOptions {
pub compression: CompressionTypeVariant, default = CompressionTypeVariant::UNCOMPRESSED
pub schema_infer_max_rec: Option<usize>, default = None
}
}
pub trait OutputFormatExt: Display {}
#[derive(Debug, Clone, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub enum OutputFormat {
CSV(CsvOptions),
JSON(JsonOptions),
#[cfg(feature = "parquet")]
PARQUET(TableParquetOptions),
AVRO,
ARROW,
}
impl Display for OutputFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let out = match self {
OutputFormat::CSV(_) => "csv",
OutputFormat::JSON(_) => "json",
#[cfg(feature = "parquet")]
OutputFormat::PARQUET(_) => "parquet",
OutputFormat::AVRO => "avro",
OutputFormat::ARROW => "arrow",
};
write!(f, "{out}")
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "parquet")]
use crate::config::TableParquetOptions;
use crate::config::{
ConfigEntry, ConfigExtension, ConfigField, ConfigFileType, ExtensionOptions,
Extensions, TableOptions,
};
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Default, Debug, Clone)]
pub struct TestExtensionConfig {
/// Should "foo" be replaced by "bar"?
pub properties: HashMap<String, String>,
}
impl ExtensionOptions for TestExtensionConfig {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn cloned(&self) -> Box<dyn ExtensionOptions> {
Box::new(self.clone())
}
fn set(&mut self, key: &str, value: &str) -> crate::Result<()> {
let (key, rem) = key.split_once('.').unwrap_or((key, ""));
assert_eq!(key, "test");
self.properties.insert(rem.to_owned(), value.to_owned());
Ok(())
}
fn entries(&self) -> Vec<ConfigEntry> {
self.properties
.iter()
.map(|(k, v)| ConfigEntry {
key: k.into(),
value: Some(v.into()),
description: "",
})
.collect()
}
}
impl ConfigExtension for TestExtensionConfig {
const PREFIX: &'static str = "test";
}
#[test]
fn create_table_config() {
let mut extension = Extensions::new();
extension.insert(TestExtensionConfig::default());
let table_config = TableOptions::new().with_extensions(extension);
let kafka_config = table_config.extensions.get::<TestExtensionConfig>();
assert!(kafka_config.is_some())
}
#[test]
fn alter_test_extension_config() {
let mut extension = Extensions::new();
extension.insert(TestExtensionConfig::default());
let mut table_config = TableOptions::new().with_extensions(extension);
table_config.set_config_format(ConfigFileType::CSV);
table_config.set("format.delimiter", ";").unwrap();
assert_eq!(table_config.csv.delimiter, b';');
table_config.set("test.bootstrap.servers", "asd").unwrap();
let kafka_config = table_config
.extensions
.get::<TestExtensionConfig>()
.unwrap();
assert_eq!(
kafka_config.properties.get("bootstrap.servers").unwrap(),
"asd"
);
}
#[test]
fn csv_u8_table_options() {
let mut table_config = TableOptions::new();
table_config.set_config_format(ConfigFileType::CSV);
table_config.set("format.delimiter", ";").unwrap();
assert_eq!(table_config.csv.delimiter as char, ';');
table_config.set("format.escape", "\"").unwrap();
assert_eq!(table_config.csv.escape.unwrap() as char, '"');
table_config.set("format.escape", "\'").unwrap();
assert_eq!(table_config.csv.escape.unwrap() as char, '\'');
}
#[test]
fn warning_only_not_default() {
use std::sync::atomic::AtomicUsize;
static COUNT: AtomicUsize = AtomicUsize::new(0);
use log::{Level, LevelFilter, Metadata, Record};
struct SimpleLogger;
impl log::Log for SimpleLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= Level::Info
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
fn flush(&self) {}
}
log::set_logger(&SimpleLogger).unwrap();
log::set_max_level(LevelFilter::Info);
let mut sql_parser_options = crate::config::SqlParserOptions::default();
sql_parser_options
.set("enable_options_value_normalization", "false")
.unwrap();
assert_eq!(COUNT.load(std::sync::atomic::Ordering::Relaxed), 0);
sql_parser_options
.set("enable_options_value_normalization", "true")
.unwrap();
assert_eq!(COUNT.load(std::sync::atomic::Ordering::Relaxed), 1);
}
#[cfg(feature = "parquet")]
#[test]
fn parquet_table_options() {
let mut table_config = TableOptions::new();
table_config.set_config_format(ConfigFileType::PARQUET);
table_config
.set("format.bloom_filter_enabled::col1", "true")
.unwrap();
assert_eq!(
table_config.parquet.column_specific_options["col1"].bloom_filter_enabled,
Some(true)
);
}
#[cfg(feature = "parquet_encryption")]
#[test]
fn parquet_table_encryption() {
use crate::config::{
ConfigFileDecryptionProperties, ConfigFileEncryptionProperties,
};
use parquet::encryption::decrypt::FileDecryptionProperties;
use parquet::encryption::encrypt::FileEncryptionProperties;
let footer_key = b"0123456789012345".to_vec(); // 128bit/16
let column_names = vec!["double_field", "float_field"];
let column_keys =
vec![b"1234567890123450".to_vec(), b"1234567890123451".to_vec()];
let file_encryption_properties =
FileEncryptionProperties::builder(footer_key.clone())
.with_column_keys(column_names.clone(), column_keys.clone())
.unwrap()
.build()
.unwrap();
let decryption_properties = FileDecryptionProperties::builder(footer_key.clone())
.with_column_keys(column_names.clone(), column_keys.clone())
.unwrap()
.build()
.unwrap();
// Test round-trip
let config_encrypt =
ConfigFileEncryptionProperties::from(&file_encryption_properties);
let encryption_properties_built =
Arc::new(FileEncryptionProperties::from(config_encrypt.clone()));
assert_eq!(file_encryption_properties, encryption_properties_built);
let config_decrypt = ConfigFileDecryptionProperties::from(&decryption_properties);
let decryption_properties_built =
Arc::new(FileDecryptionProperties::from(config_decrypt.clone()));
assert_eq!(decryption_properties, decryption_properties_built);
///////////////////////////////////////////////////////////////////////////////////
// Test encryption config
// Display original encryption config
// println!("{:#?}", config_encrypt);
let mut table_config = TableOptions::new();
table_config.set_config_format(ConfigFileType::PARQUET);
table_config
.parquet
.set(
"crypto.file_encryption.encrypt_footer",
config_encrypt.encrypt_footer.to_string().as_str(),
)
.unwrap();
table_config
.parquet
.set(
"crypto.file_encryption.footer_key_as_hex",
config_encrypt.footer_key_as_hex.as_str(),
)
.unwrap();
for (i, col_name) in column_names.iter().enumerate() {
let key = format!("crypto.file_encryption.column_key_as_hex::{col_name}");
let value = hex::encode(column_keys[i].clone());
table_config
.parquet
.set(key.as_str(), value.as_str())
.unwrap();
}
// Print matching final encryption config
// println!("{:#?}", table_config.parquet.crypto.file_encryption);
assert_eq!(
table_config.parquet.crypto.file_encryption,
Some(config_encrypt)
);
///////////////////////////////////////////////////////////////////////////////////
// Test decryption config
// Display original decryption config
// println!("{:#?}", config_decrypt);
let mut table_config = TableOptions::new();
table_config.set_config_format(ConfigFileType::PARQUET);
table_config
.parquet
.set(
"crypto.file_decryption.footer_key_as_hex",
config_decrypt.footer_key_as_hex.as_str(),
)
.unwrap();
for (i, col_name) in column_names.iter().enumerate() {
let key = format!("crypto.file_decryption.column_key_as_hex::{col_name}");
let value = hex::encode(column_keys[i].clone());
table_config
.parquet
.set(key.as_str(), value.as_str())
.unwrap();
}
// Print matching final decryption config
// println!("{:#?}", table_config.parquet.crypto.file_decryption);
assert_eq!(
table_config.parquet.crypto.file_decryption,
Some(config_decrypt.clone())
);
// Set config directly
let mut table_config = TableOptions::new();
table_config.set_config_format(ConfigFileType::PARQUET);
table_config.parquet.crypto.file_decryption = Some(config_decrypt.clone());
assert_eq!(
table_config.parquet.crypto.file_decryption,
Some(config_decrypt.clone())
);
}
#[cfg(feature = "parquet_encryption")]
#[test]
fn parquet_encryption_factory_config() {
let mut parquet_options = TableParquetOptions::default();
assert_eq!(parquet_options.crypto.factory_id, None);
assert_eq!(parquet_options.crypto.factory_options.options.len(), 0);
let mut input_config = TestExtensionConfig::default();
input_config
.properties
.insert("key1".to_string(), "value 1".to_string());
input_config
.properties
.insert("key2".to_string(), "value 2".to_string());
parquet_options
.crypto
.configure_factory("example_factory", &input_config);
assert_eq!(
parquet_options.crypto.factory_id,
Some("example_factory".to_string())
);
let factory_options = &parquet_options.crypto.factory_options.options;
assert_eq!(factory_options.len(), 2);
assert_eq!(factory_options.get("key1"), Some(&"value 1".to_string()));
assert_eq!(factory_options.get("key2"), Some(&"value 2".to_string()));
}
#[cfg(feature = "parquet")]
#[test]
fn parquet_table_options_config_entry() {
let mut table_config = TableOptions::new();
table_config.set_config_format(ConfigFileType::PARQUET);
table_config
.set("format.bloom_filter_enabled::col1", "true")
.unwrap();
let entries = table_config.entries();
assert!(entries
.iter()
.any(|item| item.key == "format.bloom_filter_enabled::col1"))
}
#[cfg(feature = "parquet")]
#[test]
fn parquet_table_parquet_options_config_entry() {
let mut table_parquet_options = TableParquetOptions::new();
table_parquet_options
.set(
"crypto.file_encryption.column_key_as_hex::double_field",
"31323334353637383930313233343530",
)
.unwrap();
let entries = table_parquet_options.entries();
assert!(entries
.iter()
.any(|item| item.key
== "crypto.file_encryption.column_key_as_hex::double_field"))
}
#[cfg(feature = "parquet")]
#[test]
fn parquet_table_options_config_metadata_entry() {
let mut table_config = TableOptions::new();
table_config.set_config_format(ConfigFileType::PARQUET);
table_config.set("format.metadata::key1", "").unwrap();
table_config.set("format.metadata::key2", "value2").unwrap();
table_config
.set("format.metadata::key3", "value with spaces ")
.unwrap();
table_config
.set("format.metadata::key4", "value with special chars :: :")
.unwrap();
let parsed_metadata = table_config.parquet.key_value_metadata.clone();
assert_eq!(parsed_metadata.get("should not exist1"), None);
assert_eq!(parsed_metadata.get("key1"), Some(&Some("".into())));
assert_eq!(parsed_metadata.get("key2"), Some(&Some("value2".into())));
assert_eq!(
parsed_metadata.get("key3"),
Some(&Some("value with spaces ".into()))
);
assert_eq!(
parsed_metadata.get("key4"),
Some(&Some("value with special chars :: :".into()))
);
// duplicate keys are overwritten
table_config.set("format.metadata::key_dupe", "A").unwrap();
table_config.set("format.metadata::key_dupe", "B").unwrap();
let parsed_metadata = table_config.parquet.key_value_metadata;
assert_eq!(parsed_metadata.get("key_dupe"), Some(&Some("B".into())));
}
}