| // 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. | |
| use std::{any::Any, fmt, fmt::Formatter, pin::Pin, sync::Arc}; | |
| use arrow::{datatypes::SchemaRef, error::ArrowError}; | |
| use auron_jni_bridge::{ | |
| conf, | |
| conf::{BooleanConf, IntConf}, | |
| jni_call_static, jni_new_global_ref, jni_new_string, | |
| }; | |
| use bytes::Bytes; | |
| use datafusion::{ | |
| datasource::{ | |
| physical_plan::{FileOpenFuture, FileOpener, FileScanConfig, FileStream}, | |
| schema_adapter::SchemaMapper, | |
| }, | |
| error::Result, | |
| execution::context::TaskContext, | |
| logical_expr::Operator, | |
| physical_expr::{ | |
| EquivalenceProperties, PhysicalExprRef, | |
| expressions::{ | |
| BinaryExpr, Column, InListExpr, IsNotNullExpr, IsNullExpr, Literal, NotExpr, SCAndExpr, | |
| SCOrExpr, | |
| }, | |
| }, | |
| physical_plan::{ | |
| DisplayAs, DisplayFormatType, ExecutionPlan, Partitioning, PlanProperties, | |
| SendableRecordBatchStream, Statistics, | |
| execution_plan::{Boundedness, EmissionType}, | |
| metrics::{Count, ExecutionPlanMetricsSet, MetricBuilder, MetricsSet}, | |
| }, | |
| scalar::ScalarValue, | |
| }; | |
| use datafusion_datasource::PartitionedFile; | |
| use datafusion_ext_commons::{ | |
| arrow::scan_util::splice_partition_columns, df_execution_err, hadoop_fs::FsProvider, | |
| }; | |
| use futures::{FutureExt, StreamExt, future::BoxFuture}; | |
| use futures_util::TryStreamExt; | |
| use once_cell::sync::OnceCell; | |
| use orc_rust::{ | |
| TimestampPrecision, | |
| arrow_reader::ArrowReaderBuilder, | |
| predicate::{Predicate, PredicateValue}, | |
| projection::ProjectionMask, | |
| reader::{AsyncChunkReader, metadata::FileMetadata}, | |
| }; | |
| use crate::{ | |
| common::execution_context::ExecutionContext, | |
| scan::{create_auron_schema_mapper, internal_file_reader::InternalFileReader}, | |
| }; | |
| /// Execution plan for scanning one or more Orc partitions | |
| #[derive(Debug, Clone)] | |
| pub struct OrcExec { | |
| fs_resource_id: String, | |
| base_config: FileScanConfig, | |
| projected_statistics: Statistics, | |
| projected_schema: SchemaRef, | |
| metrics: ExecutionPlanMetricsSet, | |
| predicate: Option<PhysicalExprRef>, | |
| props: OnceCell<Arc<PlanProperties>>, | |
| } | |
| impl OrcExec { | |
| /// Create a new Orc reader execution plan provided file list and | |
| /// schema. | |
| pub fn new( | |
| base_config: FileScanConfig, | |
| fs_resource_id: String, | |
| predicate: Option<PhysicalExprRef>, | |
| ) -> Self { | |
| let metrics = ExecutionPlanMetricsSet::new(); | |
| let projected_schema = base_config.projected_schema().unwrap_or_else(|_| { | |
| base_config | |
| .file_source | |
| .table_schema() | |
| .table_schema() | |
| .clone() | |
| }); | |
| let projected_statistics = base_config.statistics(); | |
| Self { | |
| fs_resource_id, | |
| base_config, | |
| projected_statistics, | |
| projected_schema, | |
| metrics, | |
| predicate, | |
| props: OnceCell::new(), | |
| } | |
| } | |
| } | |
| impl DisplayAs for OrcExec { | |
| fn fmt_as(&self, _t: DisplayFormatType, f: &mut Formatter) -> fmt::Result { | |
| let limit = self.base_config.limit; | |
| let projection = self.base_config.file_source.projection().cloned(); | |
| let file_group = &self.base_config.file_groups; | |
| let pred = &self.predicate; | |
| write!( | |
| f, | |
| "OrcExec: file_group={file_group:?}, limit={limit:?}, projection={projection:?}, predicate={pred:?}" | |
| ) | |
| } | |
| } | |
| impl ExecutionPlan for OrcExec { | |
| fn name(&self) -> &str { | |
| "OrcExec" | |
| } | |
| fn schema(&self) -> SchemaRef { | |
| Arc::clone(&self.projected_schema) | |
| } | |
| fn properties(&self) -> &Arc<PlanProperties> { | |
| self.props.get_or_init(|| { | |
| Arc::new(PlanProperties::new( | |
| EquivalenceProperties::new(self.schema()), | |
| Partitioning::UnknownPartitioning(self.base_config.file_groups.len()), | |
| EmissionType::Both, | |
| Boundedness::Bounded, | |
| )) | |
| }) | |
| } | |
| fn apply_expressions( | |
| &self, | |
| _f: &mut dyn FnMut( | |
| &Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| ) -> Result<datafusion::common::tree_node::TreeNodeRecursion>, | |
| ) -> Result<datafusion::common::tree_node::TreeNodeRecursion> { | |
| Ok(datafusion::common::tree_node::TreeNodeRecursion::Continue) | |
| } | |
| fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> { | |
| vec![] | |
| } | |
| fn with_new_children( | |
| self: Arc<Self>, | |
| _: Vec<Arc<dyn ExecutionPlan>>, | |
| ) -> Result<Arc<dyn ExecutionPlan>> { | |
| Ok(self) | |
| } | |
| fn execute( | |
| &self, | |
| partition: usize, | |
| context: Arc<TaskContext>, | |
| ) -> Result<SendableRecordBatchStream> { | |
| let exec_ctx = ExecutionContext::new(context, partition, self.schema(), &self.metrics); | |
| let io_time = exec_ctx.register_timer_metric("io_time"); | |
| // get fs object from jni bridge resource | |
| let resource_id = jni_new_string!(&self.fs_resource_id)?; | |
| let fs = jni_call_static!(JniBridge.getResource(resource_id.as_obj()) -> JObject)?; | |
| let fs_provider = Arc::new(FsProvider::new(jni_new_global_ref!(fs.as_obj())?, &io_time)); | |
| let projection = match self.base_config.file_column_projection_indices() { | |
| Some(proj) => proj, | |
| None => (0..self.base_config.file_schema().fields().len()).collect(), | |
| }; | |
| let num_file_schema_fields = self.base_config.file_schema().fields().len(); | |
| let full_projection = match self.base_config.file_source.projection() { | |
| Some(proj) => proj.ordered_column_indices(), | |
| None => (0..num_file_schema_fields + self.base_config.table_partition_cols().len()) | |
| .collect(), | |
| }; | |
| let force_positional_evolution = conf::ORC_FORCE_POSITIONAL_EVOLUTION.value()?; | |
| let use_microsecond_precision = conf::ORC_TIMESTAMP_USE_MICROSECOND.value()?; | |
| let is_case_sensitive = conf::ORC_SCHEMA_CASE_SENSITIVE.value()?; | |
| let opener: Arc<dyn FileOpener> = Arc::new(OrcOpener { | |
| projection, | |
| full_projection, | |
| output_schema: self.projected_schema.clone(), | |
| batch_size: conf::ORC_BATCH_SIZE.value()? as usize, | |
| table_schema: self.base_config.file_schema().clone(), | |
| fs_provider, | |
| partition_index: partition, | |
| metrics: self.metrics.clone(), | |
| force_positional_evolution, | |
| use_microsecond_precision, | |
| is_case_sensitive, | |
| predicate: self.predicate.clone(), | |
| }); | |
| let file_stream = Box::pin(FileStream::new( | |
| &self.base_config, | |
| partition, | |
| opener, | |
| exec_ctx.execution_plan_metrics(), | |
| )?); | |
| let timed_stream = execute_orc_scan(file_stream, exec_ctx.clone())?; | |
| Ok(exec_ctx.coalesce_with_default_batch_size(timed_stream)) | |
| } | |
| fn metrics(&self) -> Option<MetricsSet> { | |
| Some(self.metrics.clone_inner()) | |
| } | |
| } | |
| fn execute_orc_scan( | |
| mut stream: Pin<Box<FileStream>>, | |
| exec_ctx: Arc<ExecutionContext>, | |
| ) -> Result<SendableRecordBatchStream> { | |
| Ok(exec_ctx | |
| .clone() | |
| .output_with_sender("OrcScan", move |sender| async move { | |
| sender.exclude_time(exec_ctx.baseline_metrics().elapsed_compute()); | |
| let _timer = exec_ctx.baseline_metrics().elapsed_compute().timer(); | |
| while let Some(batch) = stream.next().await.transpose()? { | |
| sender.send(batch).await; | |
| } | |
| Ok(()) | |
| })) | |
| } | |
| struct OrcOpener { | |
| projection: Vec<usize>, | |
| full_projection: Vec<usize>, | |
| output_schema: SchemaRef, | |
| batch_size: usize, | |
| table_schema: SchemaRef, | |
| fs_provider: Arc<FsProvider>, | |
| partition_index: usize, | |
| metrics: ExecutionPlanMetricsSet, | |
| force_positional_evolution: bool, | |
| use_microsecond_precision: bool, | |
| is_case_sensitive: bool, | |
| predicate: Option<PhysicalExprRef>, | |
| } | |
| impl FileOpener for OrcOpener { | |
| fn open(&self, file_meta: PartitionedFile) -> Result<FileOpenFuture> { | |
| let reader = OrcFileReaderRef { | |
| inner: Arc::new(InternalFileReader::try_new( | |
| self.fs_provider.clone(), | |
| file_meta.object_meta.clone(), | |
| )?), | |
| metrics: OrcFileMetrics::new( | |
| self.partition_index, | |
| file_meta | |
| .object_meta | |
| .location | |
| .filename() | |
| .unwrap_or("__default_filename__"), | |
| &self.metrics.clone(), | |
| ), | |
| }; | |
| let batch_size = self.batch_size; | |
| let projection = self.projection.clone(); | |
| let projected_schema = SchemaRef::from(self.table_schema.project(&projection)?); | |
| let schema_adapter = SchemaAdapter::new( | |
| self.table_schema.clone(), | |
| projected_schema.clone(), | |
| self.force_positional_evolution, | |
| ); | |
| let use_microsecond = self.use_microsecond_precision; | |
| let is_case = self.is_case_sensitive; | |
| let predicate = self.predicate.clone(); | |
| let full_projection = self.full_projection.clone(); | |
| let output_schema = self.output_schema.clone(); | |
| let num_file_schema_fields = self.table_schema.fields().len(); | |
| Ok(Box::pin(async move { | |
| let mut builder = ArrowReaderBuilder::try_new_async(reader) | |
| .await | |
| .or_else(|err| df_execution_err!("create orc reader error: {err}"))?; | |
| if use_microsecond { | |
| builder = builder.with_timestamp_precision(TimestampPrecision::Microsecond); | |
| } | |
| if let Some(range) = file_meta.range.clone() { | |
| let range = range.start as usize..range.end as usize; | |
| builder = builder.with_file_byte_range(range); | |
| } | |
| let (schema_mapping, projection) = | |
| schema_adapter.map_schema(builder.file_metadata(), is_case)?; | |
| let projection_mask = | |
| ProjectionMask::roots(builder.file_metadata().root_data_type(), projection); | |
| builder = builder | |
| .with_batch_size(batch_size) | |
| .with_projection(projection_mask); | |
| if let Some(orc_predicate) = convert_predicate_to_orc(predicate, &projected_schema) { | |
| builder = builder.with_predicate(orc_predicate); | |
| } | |
| let stream = builder.build_async(); | |
| let adapted = stream | |
| .map_err(|e| datafusion::common::DataFusionError::External(Box::new(e))) | |
| .map(move |maybe_batch| { | |
| maybe_batch.and_then(|batch| { | |
| let batch = schema_mapping.map_batch(batch)?; | |
| if file_meta.partition_values.is_empty() { | |
| return Ok(batch); | |
| } | |
| // DataFusion 55 no longer adds partition columns in FileStream. | |
| splice_partition_columns( | |
| batch, | |
| &full_projection, | |
| num_file_schema_fields, | |
| &file_meta.partition_values, | |
| output_schema.clone(), | |
| ) | |
| }) | |
| }); | |
| Ok(adapted.boxed()) | |
| })) | |
| } | |
| } | |
| #[derive(Clone)] | |
| struct OrcFileReaderRef { | |
| inner: Arc<InternalFileReader>, | |
| metrics: OrcFileMetrics, | |
| } | |
| impl AsyncChunkReader for OrcFileReaderRef { | |
| fn len(&mut self) -> BoxFuture<'_, std::io::Result<u64>> { | |
| async move { Ok(self.inner.get_meta().size) }.boxed() | |
| } | |
| fn get_bytes( | |
| &mut self, | |
| offset_from_start: u64, | |
| length: u64, | |
| ) -> BoxFuture<'_, std::io::Result<Bytes>> { | |
| let offset_from_start = offset_from_start; | |
| let length = length; | |
| let range = offset_from_start..(offset_from_start + length); | |
| self.metrics.bytes_scanned.add(length as usize); | |
| async move { self.inner.read_fully(range).map_err(|e| e.into()) }.boxed() | |
| } | |
| } | |
| struct SchemaAdapter { | |
| table_schema: SchemaRef, | |
| projected_schema: SchemaRef, | |
| force_positional_evolution: bool, | |
| } | |
| impl SchemaAdapter { | |
| pub fn new( | |
| table_schema: SchemaRef, | |
| projected_schema: SchemaRef, | |
| force_positional_evolution: bool, | |
| ) -> Self { | |
| Self { | |
| table_schema, | |
| projected_schema, | |
| force_positional_evolution, | |
| } | |
| } | |
| fn map_schema( | |
| &self, | |
| orc_file_meta: &FileMetadata, | |
| is_case_sensitive: bool, | |
| ) -> Result<(Arc<dyn SchemaMapper>, Vec<usize>)> { | |
| let mut projection = Vec::with_capacity(self.projected_schema.fields().len()); | |
| let mut field_mappings = vec![None; self.projected_schema.fields().len()]; | |
| let file_named_columns = orc_file_meta.root_data_type().children(); | |
| if self.force_positional_evolution | |
| || file_named_columns | |
| .iter() | |
| .all(|named_col| named_col.name().starts_with("_col")) | |
| { | |
| let table_schema_fields = self.table_schema.fields(); | |
| assert!( | |
| file_named_columns.len() <= table_schema_fields.len(), | |
| "The given table schema {:?} (length:{}) has fewer {} fields than \ | |
| the actual ORC physical schema {:?} (length:{})", | |
| table_schema_fields, | |
| table_schema_fields.len(), | |
| file_named_columns.len() - table_schema_fields.len(), | |
| file_named_columns, | |
| file_named_columns.len() | |
| ); | |
| for (proj_idx, project_field) in self.projected_schema.fields.iter().enumerate() { | |
| match self.table_schema.fields().find(project_field.name()) { | |
| Some((tbl_idx, _)) => { | |
| if let Some(named_column) = file_named_columns.get(tbl_idx) { | |
| field_mappings[proj_idx] = Some(projection.len()); | |
| projection.push(named_column.data_type().column_index()); | |
| } | |
| } | |
| None => { | |
| return df_execution_err!( | |
| "Cannot find field {} in table schema {:?}", | |
| project_field.name(), | |
| table_schema_fields | |
| ); | |
| } | |
| } | |
| } | |
| } else if is_case_sensitive { | |
| for named_column in file_named_columns { | |
| if let Some((proj_idx, _)) = | |
| self.projected_schema.fields().find(named_column.name()) | |
| { | |
| field_mappings[proj_idx] = Some(projection.len()); | |
| projection.push(named_column.data_type().column_index()); | |
| } | |
| } | |
| } else { | |
| for named_column in file_named_columns { | |
| // Case-insensitive field name matching | |
| let named_column_name_lower = named_column.name().to_lowercase(); | |
| if let Some((proj_idx, _)) = self | |
| .projected_schema | |
| .fields() | |
| .iter() | |
| .enumerate() | |
| .find(|(_, f)| f.name().to_lowercase() == named_column_name_lower) | |
| { | |
| field_mappings[proj_idx] = Some(projection.len()); | |
| projection.push(named_column.data_type().column_index()); | |
| } | |
| } | |
| } | |
| Ok(( | |
| create_auron_schema_mapper(&self.projected_schema, &field_mappings), | |
| projection, | |
| )) | |
| } | |
| } | |
| #[derive(Clone)] | |
| struct OrcFileMetrics { | |
| bytes_scanned: Count, | |
| } | |
| impl OrcFileMetrics { | |
| pub fn new(partition: usize, filename: &str, metrics: &ExecutionPlanMetricsSet) -> Self { | |
| let bytes_scanned = MetricBuilder::new(metrics) | |
| .with_new_label("filename", filename.to_string()) | |
| .counter("bytes_scanned", partition); | |
| Self { bytes_scanned } | |
| } | |
| } | |
| fn convert_predicate_to_orc( | |
| predicate: Option<PhysicalExprRef>, | |
| file_schema: &SchemaRef, | |
| ) -> Option<Predicate> { | |
| let predicate = predicate?; | |
| convert_expr_to_orc(&predicate, file_schema) | |
| } | |
| /// Recursively collect all AND sub-conditions and flatten nested AND | |
| /// structures. | |
| fn collect_and_predicates( | |
| expr: &Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| schema: &SchemaRef, | |
| predicates: &mut Vec<Predicate>, | |
| ) { | |
| // Handle short-circuit AND expression (SCAndExpr) | |
| if let Some(sc_and) = expr.downcast_ref::<SCAndExpr>() { | |
| // Recursively collect AND sub-conditions from both sides | |
| collect_and_predicates(&sc_and.left, schema, predicates); | |
| collect_and_predicates(&sc_and.right, schema, predicates); | |
| return; | |
| } | |
| // Handle BinaryExpr with AND operator | |
| if let Some(binary) = expr.downcast_ref::<BinaryExpr>() | |
| && matches!(binary.op(), Operator::And) | |
| { | |
| // Recursively collect AND sub-conditions from both sides | |
| collect_and_predicates(binary.left(), schema, predicates); | |
| collect_and_predicates(binary.right(), schema, predicates); | |
| return; | |
| } | |
| // Not an AND expression, convert the whole expression | |
| // (could be OR, comparison, IS NULL, etc.) | |
| if let Some(pred) = convert_expr_to_orc(expr, schema) { | |
| predicates.push(pred); | |
| } | |
| } | |
| /// Recursively collect all OR sub-conditions and flatten nested OR | |
| /// structures. | |
| /// | |
| /// Returns `false` if any disjunct cannot be converted. OR pushdown must be | |
| /// all-or-nothing: a pushed predicate is only used to skip row groups whose | |
| /// statistics prove no row can match, so it must be implied by the true filter | |
| /// (`true_filter => pushed`). Dropping a disjunct narrows the OR into a subset | |
| /// of the true filter, which makes the reader skip row groups that actually | |
| /// contain matching rows. (Dropping AND conjuncts only loosens the predicate, | |
| /// so that stays safe.) | |
| fn collect_or_predicates( | |
| expr: &Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| schema: &SchemaRef, | |
| predicates: &mut Vec<Predicate>, | |
| ) -> bool { | |
| // Handle short-circuit OR expression (SCOrExpr) | |
| if let Some(sc_or) = expr.downcast_ref::<SCOrExpr>() { | |
| // Recursively collect OR sub-conditions from both sides | |
| return collect_or_predicates(&sc_or.left, schema, predicates) | |
| && collect_or_predicates(&sc_or.right, schema, predicates); | |
| } | |
| // Handle BinaryExpr with OR operator | |
| if let Some(binary) = expr.downcast_ref::<BinaryExpr>() | |
| && matches!(binary.op(), Operator::Or) | |
| { | |
| // Recursively collect OR sub-conditions from both sides | |
| return collect_or_predicates(binary.left(), schema, predicates) | |
| && collect_or_predicates(binary.right(), schema, predicates); | |
| } | |
| // Not an OR expression, convert the whole expression as a single disjunct | |
| // (could be AND, comparison, IS NULL, etc.). If it cannot be converted, the | |
| // entire OR is unpushable. | |
| match convert_expr_to_orc(expr, schema) { | |
| Some(pred) => { | |
| predicates.push(pred); | |
| true | |
| } | |
| None => false, | |
| } | |
| } | |
| fn convert_expr_to_orc( | |
| expr: &Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| schema: &SchemaRef, | |
| ) -> Option<Predicate> { | |
| // Handle top-level short-circuit AND expression (SCAndExpr) | |
| if let Some(_sc_and) = expr.downcast_ref::<SCAndExpr>() { | |
| let mut predicates = Vec::new(); | |
| collect_and_predicates(expr, schema, &mut predicates); | |
| if predicates.is_empty() { | |
| return None; | |
| } | |
| if predicates.len() == 1 { | |
| return Some( | |
| predicates | |
| .into_iter() | |
| .next() | |
| .expect("Expected non-empty predicates"), | |
| ); | |
| } | |
| return Some(Predicate::and(predicates)); | |
| } | |
| // Handle top-level short-circuit OR expression (SCOrExpr) | |
| if let Some(_sc_or) = expr.downcast_ref::<SCOrExpr>() { | |
| let mut predicates = Vec::new(); | |
| if !collect_or_predicates(expr, schema, &mut predicates) { | |
| // an OR disjunct could not be converted: pushing a narrowed | |
| // predicate would skip row groups holding matching rows | |
| return None; | |
| } | |
| if predicates.is_empty() { | |
| return None; | |
| } | |
| if predicates.len() == 1 { | |
| return Some( | |
| predicates | |
| .into_iter() | |
| .next() | |
| .expect("Expected non-empty predicates"), | |
| ); | |
| } | |
| return Some(Predicate::or(predicates)); | |
| } | |
| // Handle top-level AND expression (BinaryExpr with AND operator) | |
| if let Some(binary) = expr.downcast_ref::<BinaryExpr>() { | |
| if matches!(binary.op(), Operator::And) { | |
| let mut predicates = Vec::new(); | |
| collect_and_predicates(expr, schema, &mut predicates); | |
| if predicates.is_empty() { | |
| return None; | |
| } | |
| if predicates.len() == 1 { | |
| return Some( | |
| predicates | |
| .into_iter() | |
| .next() | |
| .expect("Expected non-empty predicates"), | |
| ); | |
| } | |
| return Some(Predicate::and(predicates)); | |
| } | |
| // Handle top-level OR expression (BinaryExpr with OR operator) | |
| if matches!(binary.op(), Operator::Or) { | |
| let mut predicates = Vec::new(); | |
| if !collect_or_predicates(expr, schema, &mut predicates) { | |
| // an OR disjunct could not be converted: pushing a narrowed | |
| // predicate would skip row groups holding matching rows | |
| return None; | |
| } | |
| if predicates.is_empty() { | |
| return None; | |
| } | |
| if predicates.len() == 1 { | |
| return Some( | |
| predicates | |
| .into_iter() | |
| .next() | |
| .expect("Expected non-empty predicates"), | |
| ); | |
| } | |
| return Some(Predicate::or(predicates)); | |
| } | |
| } | |
| convert_expr_to_orc_internal(expr, schema) | |
| } | |
| /// Internal conversion function for non-AND/OR expressions. | |
| fn convert_expr_to_orc_internal( | |
| expr: &Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| schema: &SchemaRef, | |
| ) -> Option<Predicate> { | |
| // Handle Literal expressions (WHERE true, WHERE false, etc.) | |
| if let Some(lit) = expr.downcast_ref::<Literal>() { | |
| match lit.value() { | |
| ScalarValue::Boolean(Some(true)) => { | |
| // WHERE true - no filtering needed, return None to skip predicate | |
| return None; | |
| } | |
| ScalarValue::Boolean(Some(false)) => { | |
| // WHERE false - need to filter all data | |
| // Create an impossible condition using a schema column if available | |
| // Use: column IS NULL AND column IS NOT NULL (always false) | |
| if let Some(field) = schema.fields().first() { | |
| let col_name = field.name().as_str(); | |
| return Some(Predicate::and(vec![ | |
| Predicate::is_null(col_name), | |
| Predicate::not(Predicate::is_null(col_name)), | |
| ])); | |
| } | |
| // Fallback: no columns in schema, can't create a predicate | |
| // using a synthetic column name to ensure all data is filtered. | |
| let col_name = "__orc_where_false_constant__"; | |
| return Some(Predicate::and(vec![ | |
| Predicate::is_null(col_name), | |
| Predicate::not(Predicate::is_null(col_name)), | |
| ])); | |
| } | |
| _ => { | |
| return None; | |
| } | |
| } | |
| } | |
| // Handle NOT expressions (WHERE NOT condition) | |
| if let Some(not_expr) = expr.downcast_ref::<NotExpr>() { | |
| if let Some(inner_pred) = convert_expr_to_orc(not_expr.arg(), schema) { | |
| return Some(Predicate::not(inner_pred)); | |
| } | |
| return None; | |
| } | |
| // Handle IS NULL expressions | |
| if let Some(is_null) = expr.downcast_ref::<IsNullExpr>() { | |
| if let Some(col) = is_null.arg().downcast_ref::<Column>() { | |
| let col_name = col.name(); | |
| return Some(Predicate::is_null(col_name)); | |
| } | |
| return None; | |
| } | |
| // Handle IS NOT NULL expressions | |
| if let Some(is_not_null) = expr.downcast_ref::<IsNotNullExpr>() { | |
| if let Some(col) = is_not_null.arg().downcast_ref::<Column>() { | |
| let col_name = col.name(); | |
| return Some(Predicate::not(Predicate::is_null(col_name))); | |
| } | |
| return None; | |
| } | |
| // Handle IN expressions (WHERE col IN (val1, val2, ...)) | |
| if let Some(in_list) = expr.downcast_ref::<InListExpr>() { | |
| if let Some(col) = in_list.expr().downcast_ref::<Column>() { | |
| let col_name = col.name(); | |
| // Convert IN to multiple OR conditions: col = val1 OR col = val2 OR ... | |
| let mut predicates = Vec::new(); | |
| for list_expr in in_list.list() { | |
| if let Some(lit) = list_expr.downcast_ref::<Literal>() | |
| && let Some(pred_value) = convert_scalar_value(lit.value()) | |
| { | |
| predicates.push(Predicate::eq(col_name, pred_value)); | |
| } | |
| } | |
| if predicates.is_empty() { | |
| return None; | |
| } | |
| // If negated is true, it represents NOT IN | |
| if in_list.negated() { | |
| return Some(Predicate::not(Predicate::or(predicates))); | |
| } else { | |
| return Some(Predicate::or(predicates)); | |
| } | |
| } | |
| return None; | |
| } | |
| // Handle BinaryExpr (comparison operations) | |
| if let Some(binary) = expr.downcast_ref::<BinaryExpr>() { | |
| let left = binary.left(); | |
| let right = binary.right(); | |
| let op = binary.op(); | |
| // AND/OR are already handled at the outer level, skip here | |
| if matches!(op, Operator::And | Operator::Or) { | |
| return None; | |
| } | |
| if let Some(col) = left.downcast_ref::<Column>() | |
| && let Some(lit) = right.downcast_ref::<Literal>() | |
| { | |
| let col_name = col.name(); | |
| let value = lit.value(); | |
| return build_comparison_predicate(col_name, op, value); | |
| } | |
| if let Some(lit) = left.downcast_ref::<Literal>() | |
| && let Some(col) = right.downcast_ref::<Column>() | |
| { | |
| let col_name = col.name(); | |
| let value = lit.value(); | |
| return build_comparison_predicate_reversed(col_name, op, value); | |
| } | |
| } | |
| None | |
| } | |
| fn build_comparison_predicate( | |
| col_name: &str, | |
| op: &Operator, | |
| value: &ScalarValue, | |
| ) -> Option<Predicate> { | |
| let predicate_value = convert_scalar_value(value)?; | |
| match op { | |
| Operator::Eq => Some(Predicate::eq(col_name, predicate_value)), | |
| Operator::NotEq => Some(Predicate::ne(col_name, predicate_value)), | |
| Operator::Lt => Some(Predicate::lt(col_name, predicate_value)), | |
| Operator::LtEq => Some(Predicate::lte(col_name, predicate_value)), | |
| Operator::Gt => Some(Predicate::gt(col_name, predicate_value)), | |
| Operator::GtEq => Some(Predicate::gte(col_name, predicate_value)), | |
| _ => None, | |
| } | |
| } | |
| fn build_comparison_predicate_reversed( | |
| col_name: &str, | |
| op: &Operator, | |
| value: &ScalarValue, | |
| ) -> Option<Predicate> { | |
| let predicate_value = convert_scalar_value(value)?; | |
| match op { | |
| Operator::Eq => Some(Predicate::eq(col_name, predicate_value)), | |
| Operator::NotEq => Some(Predicate::ne(col_name, predicate_value)), | |
| Operator::Lt => Some(Predicate::gt(col_name, predicate_value)), | |
| Operator::LtEq => Some(Predicate::gte(col_name, predicate_value)), | |
| Operator::Gt => Some(Predicate::lt(col_name, predicate_value)), | |
| Operator::GtEq => Some(Predicate::lte(col_name, predicate_value)), | |
| _ => None, | |
| } | |
| } | |
| fn convert_scalar_value(value: &ScalarValue) -> Option<PredicateValue> { | |
| match value { | |
| ScalarValue::Boolean(v) => Some(PredicateValue::Boolean(*v)), | |
| ScalarValue::Int8(v) => Some(PredicateValue::Int8(*v)), | |
| ScalarValue::Int16(v) => Some(PredicateValue::Int16(*v)), | |
| ScalarValue::Int32(v) => Some(PredicateValue::Int32(*v)), | |
| ScalarValue::Int64(v) => Some(PredicateValue::Int64(*v)), | |
| ScalarValue::Float32(v) => Some(PredicateValue::Float32(*v)), | |
| ScalarValue::Float64(v) => Some(PredicateValue::Float64(*v)), | |
| ScalarValue::Utf8(v) => Some(PredicateValue::Utf8(v.clone())), | |
| ScalarValue::LargeUtf8(v) => Some(PredicateValue::Utf8(v.clone())), | |
| _ => None, | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use std::sync::Arc; | |
| use arrow::datatypes::{DataType, Field, Schema}; | |
| use datafusion::{ | |
| logical_expr::Operator, | |
| physical_expr::expressions::{ | |
| BinaryExpr, Column, IsNotNullExpr, IsNullExpr, Literal, NotExpr, SCAndExpr, SCOrExpr, | |
| in_list, | |
| }, | |
| scalar::ScalarValue, | |
| }; | |
| use super::*; | |
| fn create_test_schema() -> SchemaRef { | |
| Arc::new(Schema::new(vec![ | |
| Field::new("id", DataType::Int32, false), | |
| Field::new("name", DataType::Utf8, true), | |
| Field::new("age", DataType::Int32, true), | |
| Field::new("score", DataType::Float64, true), | |
| ])) | |
| } | |
| #[test] | |
| fn test_literal_true() { | |
| let schema = create_test_schema(); | |
| let expr = Arc::new(Literal::new(ScalarValue::Boolean(Some(true)))); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| // WHERE true should return None (no filtering) | |
| assert!(result.is_none()); | |
| } | |
| #[test] | |
| fn test_literal_false() { | |
| let schema = create_test_schema(); | |
| let expr = Arc::new(Literal::new(ScalarValue::Boolean(Some(false)))); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| // WHERE false should return a predicate that filters all data | |
| assert!(result.is_some()); | |
| } | |
| #[test] | |
| fn test_comparison_eq() { | |
| let schema = create_test_schema(); | |
| let col = Arc::new(Column::new("id", 0)); | |
| let lit = Arc::new(Literal::new(ScalarValue::Int32(Some(42)))); | |
| let expr = Arc::new(BinaryExpr::new(col, Operator::Eq, lit)); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| assert_eq!( | |
| format!("{predicate:?}"), | |
| "Comparison { column: \"id\", op: Equal, value: Int32(Some(42)) }" | |
| ); | |
| } | |
| #[test] | |
| fn test_comparison_ne() { | |
| let schema = create_test_schema(); | |
| let col = Arc::new(Column::new("name", 1)); | |
| let lit = Arc::new(Literal::new(ScalarValue::Utf8(Some("test".to_string())))); | |
| let expr = Arc::new(BinaryExpr::new(col, Operator::NotEq, lit)); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| assert_eq!( | |
| format!("{predicate:?}"), | |
| "Comparison { column: \"name\", op: NotEqual, value: Utf8(Some(\"test\")) }" | |
| ); | |
| } | |
| #[test] | |
| fn test_comparison_lt_gt_lte_gte() { | |
| let schema = create_test_schema(); | |
| // Test LT | |
| let col = Arc::new(Column::new("age", 2)); | |
| let lit = Arc::new(Literal::new(ScalarValue::Int32(Some(30)))); | |
| let expr = Arc::new(BinaryExpr::new(col.clone(), Operator::Lt, lit.clone())); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| // Test GT | |
| let expr = Arc::new(BinaryExpr::new(col.clone(), Operator::Gt, lit.clone())); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| // Test LtEq | |
| let expr = Arc::new(BinaryExpr::new(col.clone(), Operator::LtEq, lit.clone())); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| // Test GtEq | |
| let expr = Arc::new(BinaryExpr::new(col, Operator::GtEq, lit)); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| } | |
| #[test] | |
| fn test_comparison_reversed() { | |
| let schema = create_test_schema(); | |
| // Test all reversed comparison operators | |
| // Format: (operator, expected_debug_string_fragment) | |
| // Symmetric operators (Eq, NotEq) - order doesn't change semantics | |
| let lit = Arc::new(Literal::new(ScalarValue::Int32(Some(42)))); | |
| let col = Arc::new(Column::new("id", 0)); | |
| let expr = Arc::new(BinaryExpr::new(lit.clone(), Operator::Eq, col.clone())); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| // 42 = id → id = 42 | |
| let expr = Arc::new(BinaryExpr::new(lit.clone(), Operator::NotEq, col.clone())); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| // 42 != id → id != 42 | |
| // Asymmetric operators - must be reversed | |
| // Test: 42 < id → id > 42 | |
| let expr = Arc::new(BinaryExpr::new(lit.clone(), Operator::Lt, col.clone())); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| let debug_str = format!("{predicate:?}"); | |
| // Should be GreaterThan, not LessThan | |
| assert!( | |
| debug_str.contains("GreaterThan") || debug_str.contains("Gt"), | |
| "Expected GreaterThan for reversed Lt, got: {debug_str}" | |
| ); | |
| // Test: 42 <= id → id >= 42 | |
| let expr = Arc::new(BinaryExpr::new(lit.clone(), Operator::LtEq, col.clone())); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| let debug_str = format!("{predicate:?}"); | |
| assert!( | |
| debug_str.contains("GreaterThanOrEqual") | |
| || debug_str.contains("GreaterThanEquals") | |
| || debug_str.contains("Gte"), | |
| "Expected GreaterThanOrEqual for reversed LtEq, got: {debug_str}" | |
| ); | |
| // Test: 42 > id → id < 42 | |
| let expr = Arc::new(BinaryExpr::new(lit.clone(), Operator::Gt, col.clone())); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| let debug_str = format!("{predicate:?}"); | |
| assert!( | |
| debug_str.contains("LessThan") || debug_str.contains("Lt"), | |
| "Expected LessThan for reversed Gt, got: {debug_str}" | |
| ); | |
| // Test: 42 >= id → id <= 42 | |
| let expr = Arc::new(BinaryExpr::new(lit, Operator::GtEq, col)); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| let debug_str = format!("{predicate:?}"); | |
| assert!( | |
| debug_str.contains("LessThanOrEqual") | |
| || debug_str.contains("LessThanEquals") | |
| || debug_str.contains("Lte"), | |
| "Expected LessThanOrEqual for reversed GtEq, got: {debug_str}" | |
| ); | |
| } | |
| #[test] | |
| fn test_is_null() { | |
| let schema = create_test_schema(); | |
| let col = Arc::new(Column::new("name", 1)); | |
| let expr = Arc::new(IsNullExpr::new(col)); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| assert_eq!(format!("{predicate:?}"), "IsNull { column: \"name\" }"); | |
| } | |
| #[test] | |
| fn test_is_not_null() { | |
| let schema = create_test_schema(); | |
| let col = Arc::new(Column::new("age", 2)); | |
| let expr = Arc::new(IsNotNullExpr::new(col)); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| assert_eq!(format!("{predicate:?}"), "Not(IsNull { column: \"age\" })"); | |
| } | |
| #[test] | |
| fn test_not_expr() { | |
| let schema = create_test_schema(); | |
| let col = Arc::new(Column::new("id", 0)); | |
| let lit = Arc::new(Literal::new(ScalarValue::Int32(Some(42)))); | |
| let eq_expr = Arc::new(BinaryExpr::new(col, Operator::Eq, lit)); | |
| let not_expr = Arc::new(NotExpr::new(eq_expr)); | |
| let result = convert_predicate_to_orc(Some(not_expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| assert!(format!("{predicate:?}").starts_with("Not(Comparison")); | |
| } | |
| #[test] | |
| fn test_in_list() { | |
| let schema = create_test_schema(); | |
| let col = Arc::new(Column::new("id", 0)); | |
| let values = vec![ | |
| Arc::new(Literal::new(ScalarValue::Int32(Some(1)))) | |
| as Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| Arc::new(Literal::new(ScalarValue::Int32(Some(2)))) | |
| as Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| Arc::new(Literal::new(ScalarValue::Int32(Some(3)))) | |
| as Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| ]; | |
| let expr = in_list(col, values, &false, &schema).expect("valid test in list"); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| // IN list should be converted to OR of equality predicates | |
| assert!(format!("{predicate:?}").starts_with("Or([")); | |
| } | |
| #[test] | |
| fn test_not_in_list() { | |
| let schema = create_test_schema(); | |
| let col = Arc::new(Column::new("name", 1)); | |
| let values = vec![ | |
| Arc::new(Literal::new(ScalarValue::Utf8(Some("foo".to_string())))) | |
| as Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| Arc::new(Literal::new(ScalarValue::Utf8(Some("bar".to_string())))) | |
| as Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| ]; | |
| let expr = in_list(col, values, &true, &schema).expect("valid test in list"); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| // NOT IN should be converted to NOT(OR(...)) | |
| assert!(format!("{predicate:?}").starts_with("Not(Or([")); | |
| } | |
| #[test] | |
| fn test_and_simple() { | |
| let schema = create_test_schema(); | |
| // id = 42 AND age > 18 | |
| let col1 = Arc::new(Column::new("id", 0)); | |
| let lit1 = Arc::new(Literal::new(ScalarValue::Int32(Some(42)))); | |
| let expr1 = Arc::new(BinaryExpr::new(col1, Operator::Eq, lit1)); | |
| let col2 = Arc::new(Column::new("age", 2)); | |
| let lit2 = Arc::new(Literal::new(ScalarValue::Int32(Some(18)))); | |
| let expr2 = Arc::new(BinaryExpr::new(col2, Operator::Gt, lit2)); | |
| let and_expr = Arc::new(BinaryExpr::new(expr1, Operator::And, expr2)); | |
| let result = convert_predicate_to_orc(Some(and_expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| assert!(format!("{predicate:?}").starts_with("And([")); | |
| } | |
| #[test] | |
| fn test_and_nested_flattening() { | |
| let schema = create_test_schema(); | |
| // ((id = 1 AND age = 2) AND name = "foo") should be flattened to And([...]) | |
| let col1 = Arc::new(Column::new("id", 0)); | |
| let lit1 = Arc::new(Literal::new(ScalarValue::Int32(Some(1)))); | |
| let expr1 = Arc::new(BinaryExpr::new(col1, Operator::Eq, lit1)); | |
| let col2 = Arc::new(Column::new("age", 2)); | |
| let lit2 = Arc::new(Literal::new(ScalarValue::Int32(Some(2)))); | |
| let expr2 = Arc::new(BinaryExpr::new(col2, Operator::Eq, lit2)); | |
| let and1 = Arc::new(BinaryExpr::new(expr1, Operator::And, expr2)); | |
| let col3 = Arc::new(Column::new("name", 1)); | |
| let lit3 = Arc::new(Literal::new(ScalarValue::Utf8(Some("foo".to_string())))); | |
| let expr3 = Arc::new(BinaryExpr::new(col3, Operator::Eq, lit3)); | |
| let and2 = Arc::new(BinaryExpr::new(and1, Operator::And, expr3)); | |
| let result = convert_predicate_to_orc(Some(and2), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| let debug_str = format!("{predicate:?}"); | |
| // Should be flattened to And([cond1, cond2, cond3]) | |
| assert!(debug_str.starts_with("And([")); | |
| // Count the number of conditions (should be 3) | |
| let condition_count = debug_str.matches("Comparison").count(); | |
| assert_eq!(condition_count, 3); | |
| } | |
| #[test] | |
| fn test_or_simple() { | |
| let schema = create_test_schema(); | |
| // id = 1 OR id = 2 | |
| let col1 = Arc::new(Column::new("id", 0)); | |
| let lit1 = Arc::new(Literal::new(ScalarValue::Int32(Some(1)))); | |
| let expr1 = Arc::new(BinaryExpr::new(col1.clone(), Operator::Eq, lit1)); | |
| let lit2 = Arc::new(Literal::new(ScalarValue::Int32(Some(2)))); | |
| let expr2 = Arc::new(BinaryExpr::new(col1, Operator::Eq, lit2)); | |
| let or_expr = Arc::new(BinaryExpr::new(expr1, Operator::Or, expr2)); | |
| let result = convert_predicate_to_orc(Some(or_expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| assert!(format!("{predicate:?}").starts_with("Or([")); | |
| } | |
| #[test] | |
| fn test_or_nested_flattening() { | |
| let schema = create_test_schema(); | |
| // ((id = 1 OR age = 2) OR score = 3.0) should be flattened | |
| let col1 = Arc::new(Column::new("id", 0)); | |
| let lit1 = Arc::new(Literal::new(ScalarValue::Int32(Some(1)))); | |
| let expr1 = Arc::new(BinaryExpr::new(col1, Operator::Eq, lit1)); | |
| let col2 = Arc::new(Column::new("age", 2)); | |
| let lit2 = Arc::new(Literal::new(ScalarValue::Int32(Some(2)))); | |
| let expr2 = Arc::new(BinaryExpr::new(col2, Operator::Eq, lit2)); | |
| let or1 = Arc::new(BinaryExpr::new(expr1, Operator::Or, expr2)); | |
| let col3 = Arc::new(Column::new("score", 3)); | |
| let lit3 = Arc::new(Literal::new(ScalarValue::Float64(Some(3.0)))); | |
| let expr3 = Arc::new(BinaryExpr::new(col3, Operator::Eq, lit3)); | |
| let or2 = Arc::new(BinaryExpr::new(or1, Operator::Or, expr3)); | |
| let result = convert_predicate_to_orc(Some(or2), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| let debug_str = format!("{predicate:?}"); | |
| // Should be flattened to Or([cond1, cond2, cond3]) | |
| assert!(debug_str.starts_with("Or([")); | |
| let condition_count = debug_str.matches("Comparison").count(); | |
| assert_eq!(condition_count, 3); | |
| } | |
| #[test] | |
| fn test_or_with_unconvertible_disjunct_not_pushed() { | |
| let schema = create_test_schema(); | |
| // id = 1 OR (id = age) | |
| // The second disjunct compares two columns and cannot be converted. | |
| // The whole OR must NOT push down a narrowed predicate, otherwise the | |
| // reader would skip row groups that only satisfy the dropped disjunct. | |
| let id = Arc::new(Column::new("id", 0)); | |
| let lit1 = Arc::new(Literal::new(ScalarValue::Int32(Some(1)))); | |
| let conv = Arc::new(BinaryExpr::new(id.clone(), Operator::Eq, lit1)); | |
| let age = Arc::new(Column::new("age", 2)); | |
| let unconv = Arc::new(BinaryExpr::new(id, Operator::Eq, age)); | |
| let or_expr = Arc::new(BinaryExpr::new(conv, Operator::Or, unconv)); | |
| let result = convert_predicate_to_orc(Some(or_expr), &schema); | |
| assert!( | |
| result.is_none(), | |
| "OR with an unconvertible disjunct must not push down, got: {result:?}" | |
| ); | |
| } | |
| #[test] | |
| fn test_or_with_unconvertible_and_branch_not_pushed() { | |
| let schema = create_test_schema(); | |
| // (id = age AND age = score) OR (id = 2) | |
| // The first disjunct is an AND of two column-column comparisons, neither | |
| // of which converts, so the AND yields no predicate. That disjunct must | |
| // poison the whole OR rather than being silently dropped. This mirrors | |
| // the production bug where `cast(type)=2 AND cast(gjo)=1` was dropped. | |
| let id = Arc::new(Column::new("id", 0)); | |
| let age = Arc::new(Column::new("age", 2)); | |
| let id_eq_age = Arc::new(BinaryExpr::new(id.clone(), Operator::Eq, age.clone())); | |
| let age_eq_score = Arc::new(BinaryExpr::new( | |
| age, | |
| Operator::Eq, | |
| Arc::new(Column::new("score", 3)), | |
| )); | |
| let and_branch = Arc::new(BinaryExpr::new(id_eq_age, Operator::And, age_eq_score)); | |
| let lit2 = Arc::new(Literal::new(ScalarValue::Int32(Some(2)))); | |
| let id_eq_2 = Arc::new(BinaryExpr::new(id, Operator::Eq, lit2)); | |
| let or_expr = Arc::new(BinaryExpr::new(and_branch, Operator::Or, id_eq_2)); | |
| let result = convert_predicate_to_orc(Some(or_expr), &schema); | |
| assert!( | |
| result.is_none(), | |
| "OR whose disjunct is a fully-unconvertible AND must not push down, got: {result:?}" | |
| ); | |
| } | |
| #[test] | |
| fn test_and_keeps_convertible_conjunct_when_or_unconvertible() { | |
| let schema = create_test_schema(); | |
| // name = "x" AND (id = 1 OR id = age) | |
| // The OR is unconvertible, but it is an AND conjunct. Dropping it only | |
| // loosens the pushed predicate, so the convertible name = "x" conjunct | |
| // must still push down. | |
| let name = Arc::new(Column::new("name", 1)); | |
| let name_lit = Arc::new(Literal::new(ScalarValue::Utf8(Some("x".to_string())))); | |
| let name_eq = Arc::new(BinaryExpr::new(name, Operator::Eq, name_lit)); | |
| let id = Arc::new(Column::new("id", 0)); | |
| let lit1 = Arc::new(Literal::new(ScalarValue::Int32(Some(1)))); | |
| let id_eq_1 = Arc::new(BinaryExpr::new(id.clone(), Operator::Eq, lit1)); | |
| let age = Arc::new(Column::new("age", 2)); | |
| let id_eq_age = Arc::new(BinaryExpr::new(id, Operator::Eq, age)); | |
| let or_expr = Arc::new(BinaryExpr::new(id_eq_1, Operator::Or, id_eq_age)); | |
| let and_expr = Arc::new(BinaryExpr::new(name_eq, Operator::And, or_expr)); | |
| let result = convert_predicate_to_orc(Some(and_expr), &schema); | |
| assert!(result.is_some()); | |
| let debug_str = format!("{:?}", result.expect("Expected valid ORC predicate")); | |
| // Only the name = "x" conjunct survives; the unconvertible OR is dropped. | |
| assert!( | |
| debug_str.contains("\"name\"") && debug_str.contains("Equal"), | |
| "Expected name = \"x\" to push down, got: {debug_str}" | |
| ); | |
| assert!( | |
| !debug_str.contains("Or("), | |
| "Unconvertible OR must not appear in the pushed predicate, got: {debug_str}" | |
| ); | |
| } | |
| #[test] | |
| fn test_sc_or_with_unconvertible_disjunct_not_pushed() { | |
| let schema = create_test_schema(); | |
| // Short-circuit OR: id = 1 OR (id = age) | |
| // Same invariant as the BinaryExpr::Or case, but exercising the | |
| // SCOrExpr path that the fix also updates. | |
| let id = Arc::new(Column::new("id", 0)); | |
| let lit1 = Arc::new(Literal::new(ScalarValue::Int32(Some(1)))); | |
| let conv = Arc::new(BinaryExpr::new(id.clone(), Operator::Eq, lit1)); | |
| let age = Arc::new(Column::new("age", 2)); | |
| let unconv = Arc::new(BinaryExpr::new(id, Operator::Eq, age)); | |
| let sc_or_expr = Arc::new(SCOrExpr::new(conv, unconv)); | |
| let result = convert_predicate_to_orc(Some(sc_or_expr), &schema); | |
| assert!( | |
| result.is_none(), | |
| "SCOrExpr with an unconvertible disjunct must not push down, got: {result:?}" | |
| ); | |
| } | |
| #[test] | |
| fn test_complex_mixed_predicates() { | |
| let schema = create_test_schema(); | |
| // (id = 1 OR id = 2) AND name IS NOT NULL | |
| let col1 = Arc::new(Column::new("id", 0)); | |
| let lit1 = Arc::new(Literal::new(ScalarValue::Int32(Some(1)))); | |
| let expr1 = Arc::new(BinaryExpr::new(col1.clone(), Operator::Eq, lit1)); | |
| let lit2 = Arc::new(Literal::new(ScalarValue::Int32(Some(2)))); | |
| let expr2 = Arc::new(BinaryExpr::new(col1, Operator::Eq, lit2)); | |
| let or_expr = Arc::new(BinaryExpr::new(expr1, Operator::Or, expr2)); | |
| let col2 = Arc::new(Column::new("name", 1)); | |
| let is_not_null = Arc::new(IsNotNullExpr::new(col2)); | |
| let and_expr = Arc::new(BinaryExpr::new(or_expr, Operator::And, is_not_null)); | |
| let result = convert_predicate_to_orc(Some(and_expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| let debug_str = format!("{predicate:?}"); | |
| // Should have And at top level | |
| assert!(debug_str.contains("And"), "Expected And, got: {debug_str}"); | |
| // Should contain OR for the id conditions | |
| assert!(debug_str.contains("Or"), "Expected Or, got: {debug_str}"); | |
| // Should contain the IS NOT NULL condition | |
| assert!( | |
| debug_str.contains("IsNull"), | |
| "Expected IsNull, got: {debug_str}", | |
| ); | |
| } | |
| #[test] | |
| fn test_deeply_nested_and() { | |
| let schema = create_test_schema(); | |
| // Build: (((id = 1 AND age = 2) AND name = "foo") AND score = 3.0) | |
| let col1 = Arc::new(Column::new("id", 0)); | |
| let lit1 = Arc::new(Literal::new(ScalarValue::Int32(Some(1)))); | |
| let expr1 = Arc::new(BinaryExpr::new(col1, Operator::Eq, lit1)); | |
| let col2 = Arc::new(Column::new("age", 2)); | |
| let lit2 = Arc::new(Literal::new(ScalarValue::Int32(Some(2)))); | |
| let expr2 = Arc::new(BinaryExpr::new(col2, Operator::Eq, lit2)); | |
| let and1 = Arc::new(BinaryExpr::new(expr1, Operator::And, expr2)); | |
| let col3 = Arc::new(Column::new("name", 1)); | |
| let lit3 = Arc::new(Literal::new(ScalarValue::Utf8(Some("foo".to_string())))); | |
| let expr3 = Arc::new(BinaryExpr::new(col3, Operator::Eq, lit3)); | |
| let and2 = Arc::new(BinaryExpr::new(and1, Operator::And, expr3)); | |
| let col4 = Arc::new(Column::new("score", 3)); | |
| let lit4 = Arc::new(Literal::new(ScalarValue::Float64(Some(3.0)))); | |
| let expr4 = Arc::new(BinaryExpr::new(col4, Operator::Eq, lit4)); | |
| let and3 = Arc::new(BinaryExpr::new(and2, Operator::And, expr4)); | |
| let result = convert_predicate_to_orc(Some(and3), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| let debug_str = format!("{predicate:?}"); | |
| // Should be flattened to And([cond1, cond2, cond3, cond4]) | |
| assert!(debug_str.starts_with("And([")); | |
| let condition_count = debug_str.matches("Comparison").count(); | |
| assert_eq!(condition_count, 4); | |
| } | |
| #[test] | |
| fn test_all_scalar_types() { | |
| let schema = Arc::new(Schema::new(vec![ | |
| Field::new("col_bool", DataType::Boolean, true), | |
| Field::new("col_i8", DataType::Int8, true), | |
| Field::new("col_i16", DataType::Int16, true), | |
| Field::new("col_i32", DataType::Int32, true), | |
| Field::new("col_i64", DataType::Int64, true), | |
| Field::new("col_f32", DataType::Float32, true), | |
| Field::new("col_f64", DataType::Float64, true), | |
| Field::new("col_utf8", DataType::Utf8, true), | |
| ])); | |
| // Test Boolean | |
| let col = Arc::new(Column::new("col_bool", 0)); | |
| let lit = Arc::new(Literal::new(ScalarValue::Boolean(Some(true)))); | |
| let expr = Arc::new(BinaryExpr::new(col, Operator::Eq, lit)); | |
| assert!(convert_predicate_to_orc(Some(expr), &schema).is_some()); | |
| // Test Int8 | |
| let col = Arc::new(Column::new("col_i8", 1)); | |
| let lit = Arc::new(Literal::new(ScalarValue::Int8(Some(42)))); | |
| let expr = Arc::new(BinaryExpr::new(col, Operator::Eq, lit)); | |
| assert!(convert_predicate_to_orc(Some(expr), &schema).is_some()); | |
| // Test Int16 | |
| let col = Arc::new(Column::new("col_i16", 2)); | |
| let lit = Arc::new(Literal::new(ScalarValue::Int16(Some(1000)))); | |
| let expr = Arc::new(BinaryExpr::new(col, Operator::Eq, lit)); | |
| assert!(convert_predicate_to_orc(Some(expr), &schema).is_some()); | |
| // Test Int32 | |
| let col = Arc::new(Column::new("col_i32", 3)); | |
| let lit = Arc::new(Literal::new(ScalarValue::Int32(Some(100000)))); | |
| let expr = Arc::new(BinaryExpr::new(col, Operator::Eq, lit)); | |
| assert!(convert_predicate_to_orc(Some(expr), &schema).is_some()); | |
| // Test Int64 | |
| let col = Arc::new(Column::new("col_i64", 4)); | |
| let lit = Arc::new(Literal::new(ScalarValue::Int64(Some(1000000000)))); | |
| let expr = Arc::new(BinaryExpr::new(col, Operator::Eq, lit)); | |
| assert!(convert_predicate_to_orc(Some(expr), &schema).is_some()); | |
| // Test Float32 | |
| let col = Arc::new(Column::new("col_f32", 5)); | |
| let lit = Arc::new(Literal::new(ScalarValue::Float32(Some(3.14)))); | |
| let expr = Arc::new(BinaryExpr::new(col, Operator::Eq, lit)); | |
| assert!(convert_predicate_to_orc(Some(expr), &schema).is_some()); | |
| // Test Float64 | |
| let col = Arc::new(Column::new("col_f64", 6)); | |
| let lit = Arc::new(Literal::new(ScalarValue::Float64(Some(2.718)))); | |
| let expr = Arc::new(BinaryExpr::new(col, Operator::Eq, lit)); | |
| assert!(convert_predicate_to_orc(Some(expr), &schema).is_some()); | |
| // Test Utf8 | |
| let col = Arc::new(Column::new("col_utf8", 7)); | |
| let lit = Arc::new(Literal::new(ScalarValue::Utf8(Some("test".to_string())))); | |
| let expr = Arc::new(BinaryExpr::new(col, Operator::Eq, lit)); | |
| assert!(convert_predicate_to_orc(Some(expr), &schema).is_some()); | |
| } | |
| #[test] | |
| fn test_null_literal_in_comparison() { | |
| let schema = create_test_schema(); | |
| // Test: WHERE id = NULL | |
| // Note: In SQL semantics, "col = NULL" always evaluates to NULL (not true or | |
| // false) However, we should still handle it gracefully | |
| let col = Arc::new(Column::new("id", 0)); | |
| let null_lit = Arc::new(Literal::new(ScalarValue::Int32(None))); | |
| let expr = Arc::new(BinaryExpr::new(col.clone(), Operator::Eq, null_lit.clone())); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| // Should convert to an ORC predicate (even though semantically it won't match | |
| // anything) | |
| assert!(result.is_some()); | |
| // Test: WHERE id != NULL | |
| let expr = Arc::new(BinaryExpr::new( | |
| col.clone(), | |
| Operator::NotEq, | |
| null_lit.clone(), | |
| )); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| // Test: WHERE id < NULL | |
| let expr = Arc::new(BinaryExpr::new(col.clone(), Operator::Lt, null_lit.clone())); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| // Test: WHERE id > NULL | |
| let expr = Arc::new(BinaryExpr::new(col.clone(), Operator::Gt, null_lit.clone())); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| // Test: WHERE NULL = id (reversed) | |
| let expr = Arc::new(BinaryExpr::new(null_lit.clone(), Operator::Eq, col.clone())); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| // Test: WHERE NULL < id (reversed) | |
| let expr = Arc::new(BinaryExpr::new(null_lit, Operator::Lt, col)); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| } | |
| #[test] | |
| fn test_null_in_in_list() { | |
| let schema = create_test_schema(); | |
| let col = Arc::new(Column::new("id", 0)); | |
| // Test: WHERE id IN (1, NULL, 3) | |
| // This should handle NULL gracefully | |
| let values = vec![ | |
| Arc::new(Literal::new(ScalarValue::Int32(Some(1)))) | |
| as Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| Arc::new(Literal::new(ScalarValue::Int32(None))) | |
| as Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| Arc::new(Literal::new(ScalarValue::Int32(Some(3)))) | |
| as Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| ]; | |
| let expr = in_list(col.clone(), values, &false, &schema).expect("valid test in list"); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| let debug_str = format!("{predicate:?}"); | |
| // Should be converted to OR of equality predicates | |
| // NULL values should be included (even though they won't match) | |
| assert!(debug_str.starts_with("Or([")); | |
| } | |
| #[test] | |
| fn test_null_in_not_in_list() { | |
| let schema = create_test_schema(); | |
| let col = Arc::new(Column::new("name", 1)); | |
| // Test: WHERE name NOT IN ('foo', NULL, 'bar') | |
| let values = vec![ | |
| Arc::new(Literal::new(ScalarValue::Utf8(Some("foo".to_string())))) | |
| as Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| Arc::new(Literal::new(ScalarValue::Utf8(None))) | |
| as Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| Arc::new(Literal::new(ScalarValue::Utf8(Some("bar".to_string())))) | |
| as Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| ]; | |
| let expr = in_list(col, values, &true, &schema).expect("valid test in list"); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| let debug_str = format!("{predicate:?}"); | |
| // Should be NOT(OR(...)) | |
| assert!(debug_str.starts_with("Not(Or([")); | |
| } | |
| #[test] | |
| fn test_all_null_in_list() { | |
| let schema = create_test_schema(); | |
| let col = Arc::new(Column::new("id", 0)); | |
| // Test: WHERE id IN (NULL, NULL, NULL) | |
| // Edge case: all values are NULL | |
| let values = vec![ | |
| Arc::new(Literal::new(ScalarValue::Int32(None))) | |
| as Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| Arc::new(Literal::new(ScalarValue::Int32(None))) | |
| as Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| Arc::new(Literal::new(ScalarValue::Int32(None))) | |
| as Arc<dyn datafusion::physical_expr::PhysicalExpr>, | |
| ]; | |
| let expr = in_list(col, values, &false, &schema).expect("valid test in list"); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| // Should still produce a predicate (though it won't match anything) | |
| assert!(result.is_some()); | |
| } | |
| #[test] | |
| fn test_null_with_and_predicate() { | |
| let schema = create_test_schema(); | |
| // Test: WHERE id = NULL AND age > 18 | |
| let col1 = Arc::new(Column::new("id", 0)); | |
| let null_lit = Arc::new(Literal::new(ScalarValue::Int32(None))); | |
| let expr1 = Arc::new(BinaryExpr::new(col1, Operator::Eq, null_lit)); | |
| let col2 = Arc::new(Column::new("age", 2)); | |
| let lit2 = Arc::new(Literal::new(ScalarValue::Int32(Some(18)))); | |
| let expr2 = Arc::new(BinaryExpr::new(col2, Operator::Gt, lit2)); | |
| let and_expr = Arc::new(BinaryExpr::new(expr1, Operator::And, expr2)); | |
| let result = convert_predicate_to_orc(Some(and_expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| let debug_str = format!("{predicate:?}"); | |
| // Should be flattened to And([...]) | |
| assert!(debug_str.contains("And")); | |
| } | |
| #[test] | |
| fn test_null_with_or_predicate() { | |
| let schema = create_test_schema(); | |
| // Test: WHERE id = NULL OR age > 18 | |
| let col1 = Arc::new(Column::new("id", 0)); | |
| let null_lit = Arc::new(Literal::new(ScalarValue::Int32(None))); | |
| let expr1 = Arc::new(BinaryExpr::new(col1, Operator::Eq, null_lit)); | |
| let col2 = Arc::new(Column::new("age", 2)); | |
| let lit2 = Arc::new(Literal::new(ScalarValue::Int32(Some(18)))); | |
| let expr2 = Arc::new(BinaryExpr::new(col2, Operator::Gt, lit2)); | |
| let or_expr = Arc::new(BinaryExpr::new(expr1, Operator::Or, expr2)); | |
| let result = convert_predicate_to_orc(Some(or_expr), &schema); | |
| assert!(result.is_some()); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| let debug_str = format!("{predicate:?}"); | |
| // Should be flattened to Or([...]) | |
| assert!(debug_str.contains("Or")); | |
| } | |
| #[test] | |
| fn test_various_null_types() { | |
| let schema = create_test_schema(); | |
| // Test NULL with different data types | |
| let test_cases = vec![ | |
| ("id", ScalarValue::Int32(None)), | |
| ("name", ScalarValue::Utf8(None)), | |
| ("age", ScalarValue::Int32(None)), | |
| ("score", ScalarValue::Float64(None)), | |
| ]; | |
| for (col_name, null_value) in test_cases { | |
| let col = Arc::new(Column::new( | |
| col_name, | |
| schema | |
| .index_of(col_name) | |
| .expect("Column not found in schema"), | |
| )); | |
| let null_lit = Arc::new(Literal::new(null_value)); | |
| let expr = Arc::new(BinaryExpr::new(col, Operator::Eq, null_lit)); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!( | |
| result.is_some(), | |
| "Failed to convert NULL comparison for column: {col_name}" | |
| ); | |
| } | |
| } | |
| #[test] | |
| fn test_null_literal_edge_cases() { | |
| let schema = create_test_schema(); | |
| let col = Arc::new(Column::new("id", 0)); | |
| // Test: WHERE id >= NULL | |
| let null_lit = Arc::new(Literal::new(ScalarValue::Int32(None))); | |
| let expr = Arc::new(BinaryExpr::new( | |
| col.clone(), | |
| Operator::GtEq, | |
| null_lit.clone(), | |
| )); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| // Test: WHERE id <= NULL | |
| let expr = Arc::new(BinaryExpr::new(col, Operator::LtEq, null_lit)); | |
| let result = convert_predicate_to_orc(Some(expr), &schema); | |
| assert!(result.is_some()); | |
| } | |
| #[test] | |
| fn test_where_false_with_empty_schema() { | |
| // Edge case: WHERE false with an empty schema (no columns) | |
| let empty_schema = Arc::new(Schema::empty()); | |
| let expr = Arc::new(Literal::new(ScalarValue::Boolean(Some(false)))); | |
| let result = convert_predicate_to_orc(Some(expr), &empty_schema); | |
| // Should still produce a predicate using a synthetic column name | |
| // to ensure all data is filtered | |
| assert!( | |
| result.is_some(), | |
| "WHERE false should produce a predicate even with empty schema" | |
| ); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| let debug_str = format!("{predicate:?}"); | |
| // Should use the synthetic column name | |
| assert!( | |
| debug_str.contains("__orc_where_false_constant__") || debug_str.contains("And"), | |
| "Expected synthetic column or And predicate, got: {debug_str}" | |
| ); | |
| } | |
| #[test] | |
| fn test_where_true_with_empty_schema() { | |
| // Edge case: WHERE true with an empty schema | |
| let empty_schema = Arc::new(Schema::empty()); | |
| let expr = Arc::new(Literal::new(ScalarValue::Boolean(Some(true)))); | |
| let result = convert_predicate_to_orc(Some(expr), &empty_schema); | |
| // WHERE true should always return None (no filtering) | |
| assert!( | |
| result.is_none(), | |
| "WHERE true should not produce any predicate" | |
| ); | |
| } | |
| #[test] | |
| fn test_short_circuit_and_expr() { | |
| let schema = create_test_schema(); | |
| // Test: SCAndExpr (short-circuit AND) | |
| // Simulating: WHERE id = 42 AND age > 18 | |
| let col1 = Arc::new(Column::new("id", 0)); | |
| let lit1 = Arc::new(Literal::new(ScalarValue::Int32(Some(42)))); | |
| let expr1 = Arc::new(BinaryExpr::new(col1, Operator::Eq, lit1)); | |
| let col2 = Arc::new(Column::new("age", 2)); | |
| let lit2 = Arc::new(Literal::new(ScalarValue::Int32(Some(18)))); | |
| let expr2 = Arc::new(BinaryExpr::new(col2, Operator::Gt, lit2)); | |
| let sc_and_expr = Arc::new(SCAndExpr::new(expr1, expr2)); | |
| let result = convert_predicate_to_orc(Some(sc_and_expr), &schema); | |
| assert!(result.is_some(), "SCAndExpr should convert to predicate"); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| let debug_str = format!("{predicate:?}"); | |
| // Should be flattened to And([...]) | |
| assert!( | |
| debug_str.contains("And"), | |
| "Expected And predicate, got: {debug_str}", | |
| ); | |
| } | |
| #[test] | |
| fn test_short_circuit_or_expr() { | |
| let schema = create_test_schema(); | |
| // Test: SCOrExpr (short-circuit OR) | |
| // Simulating: WHERE id = 1 OR id = 2 | |
| let col1 = Arc::new(Column::new("id", 0)); | |
| let lit1 = Arc::new(Literal::new(ScalarValue::Int32(Some(1)))); | |
| let expr1 = Arc::new(BinaryExpr::new(col1.clone(), Operator::Eq, lit1)); | |
| let lit2 = Arc::new(Literal::new(ScalarValue::Int32(Some(2)))); | |
| let expr2 = Arc::new(BinaryExpr::new(col1, Operator::Eq, lit2)); | |
| let sc_or_expr = Arc::new(SCOrExpr::new(expr1, expr2)); | |
| let result = convert_predicate_to_orc(Some(sc_or_expr), &schema); | |
| assert!(result.is_some(), "SCOrExpr should convert to predicate"); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| let debug_str = format!("{predicate:?}"); | |
| // Should be flattened to Or([...]) | |
| assert!( | |
| debug_str.contains("Or"), | |
| "Expected Or predicate, got: {debug_str}" | |
| ); | |
| } | |
| #[test] | |
| fn test_nested_short_circuit_exprs() { | |
| let schema = create_test_schema(); | |
| // Test: Nested SCAndExpr and SCOrExpr | |
| // Simulating: WHERE (id = 1 OR id = 2) AND age > 18 | |
| let col1 = Arc::new(Column::new("id", 0)); | |
| let lit1 = Arc::new(Literal::new(ScalarValue::Int32(Some(1)))); | |
| let expr1 = Arc::new(BinaryExpr::new(col1.clone(), Operator::Eq, lit1)); | |
| let lit2 = Arc::new(Literal::new(ScalarValue::Int32(Some(2)))); | |
| let expr2 = Arc::new(BinaryExpr::new(col1, Operator::Eq, lit2)); | |
| let sc_or_expr = Arc::new(SCOrExpr::new(expr1, expr2)); | |
| let col2 = Arc::new(Column::new("age", 2)); | |
| let lit3 = Arc::new(Literal::new(ScalarValue::Int32(Some(18)))); | |
| let expr3 = Arc::new(BinaryExpr::new(col2, Operator::Gt, lit3)); | |
| let sc_and_expr = Arc::new(SCAndExpr::new(sc_or_expr, expr3)); | |
| let result = convert_predicate_to_orc(Some(sc_and_expr), &schema); | |
| assert!( | |
| result.is_some(), | |
| "Nested SCAndExpr/SCOrExpr should convert to predicate" | |
| ); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| let debug_str = format!("{predicate:?}"); | |
| // Should have And at top level with Or inside | |
| assert!( | |
| debug_str.contains("And"), | |
| "Expected And at top level, got: {debug_str}" | |
| ); | |
| assert!( | |
| debug_str.contains("Or"), | |
| "Expected Or inside And, got: {debug_str}" | |
| ); | |
| } | |
| #[test] | |
| fn test_mixed_binary_and_short_circuit() { | |
| let schema = create_test_schema(); | |
| // Test: Mix of BinaryExpr (AND) and SCAndExpr | |
| // Simulating: WHERE id = 42 AND (age > 18 AND status = 'ACTIVE') | |
| let col1 = Arc::new(Column::new("id", 0)); | |
| let lit1 = Arc::new(Literal::new(ScalarValue::Int32(Some(42)))); | |
| let expr1 = Arc::new(BinaryExpr::new(col1, Operator::Eq, lit1)); | |
| let col2 = Arc::new(Column::new("age", 2)); | |
| let lit2 = Arc::new(Literal::new(ScalarValue::Int32(Some(18)))); | |
| let expr2 = Arc::new(BinaryExpr::new(col2, Operator::Gt, lit2)); | |
| let col3 = Arc::new(Column::new("name", 1)); | |
| let lit3 = Arc::new(Literal::new(ScalarValue::Utf8(Some("ACTIVE".to_string())))); | |
| let expr3 = Arc::new(BinaryExpr::new(col3, Operator::Eq, lit3)); | |
| let sc_and_inner = Arc::new(SCAndExpr::new(expr2, expr3)); | |
| let binary_and_outer = Arc::new(BinaryExpr::new(expr1, Operator::And, sc_and_inner)); | |
| let result = convert_predicate_to_orc(Some(binary_and_outer), &schema); | |
| assert!( | |
| result.is_some(), | |
| "Mixed BinaryExpr/SCAndExpr should convert to predicate" | |
| ); | |
| let predicate = result.expect("Expected valid ORC predicate"); | |
| let debug_str = format!("{predicate:?}"); | |
| // Should all be flattened to And([...]) | |
| assert!( | |
| debug_str.contains("And"), | |
| "Expected And predicate, got: {debug_str}" | |
| ); | |
| // Should have 3 conditions flattened | |
| let condition_count = debug_str.matches("Comparison").count(); | |
| assert_eq!( | |
| condition_count, 3, | |
| "Expected 3 comparison conditions, got: {condition_count}" | |
| ); | |
| } | |
| } |