mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
perf: upgrade datafusion to 54 (#12665)
- [x] ~add sort when to many groups, we can do this, but for now do not do this~ - [x] remove the unsafe for memory pool - [x] add approx_distinct improve, already fork, will add in next pr - [x] fork vortex to add cast fix, and export `.exclude_schemes([IntDictScheme.id()])` - [x] ~enable_physical_uncorrelated_scalar_subquery test~
This commit is contained in:
Generated
+318
-245
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -319,10 +319,10 @@ cityhasher = { version = "0.1", default-features = false }
|
||||
collapse = "0.1.2"
|
||||
cron = "0.15"
|
||||
dashmap = { version = "6.1", features = ["serde"] }
|
||||
datafusion = "53"
|
||||
datafusion-proto = "53"
|
||||
datafusion-functions-aggregate-common = "53"
|
||||
datafusion-functions-json = "0.53"
|
||||
datafusion = "54"
|
||||
datafusion-proto = "54"
|
||||
datafusion-functions-aggregate-common = "54"
|
||||
datafusion-functions-json = "0.54"
|
||||
expect-test = "1.4"
|
||||
arrow = { version = "58", features = ["ipc_compression", "prettyprint"] }
|
||||
arrow-flight = "58"
|
||||
@@ -330,7 +330,7 @@ arrow-json = "58"
|
||||
arrow-schema = { version = "58", features = ["serde"] }
|
||||
parquet = { version = "58", features = ["arrow", "async", "object_store"] }
|
||||
object_store = { version = "0.13.2", features = ["aws", "azure", "gcp"] }
|
||||
sqlparser = { version = "0.61", features = ["serde", "visitor"] }
|
||||
sqlparser = { version = "0.62", features = ["serde", "visitor"] }
|
||||
dotenv_config = "0.2"
|
||||
dotenvy = "0.15"
|
||||
env_logger = "0.11"
|
||||
|
||||
@@ -90,7 +90,7 @@ vrl.workspace = true
|
||||
tonic.workspace = true
|
||||
tonic-prost.workspace = true
|
||||
tracing-opentelemetry.workspace = true
|
||||
vortex = { git = "https://github.com/openobserve/vortex", rev = "13423ca204f81dc1b20ca040da2aeb386220a312", features = [
|
||||
vortex = { git = "https://github.com/openobserve/vortex", branch = "0.75.0-fix", features = [
|
||||
"tokio",
|
||||
], optional = true }
|
||||
|
||||
|
||||
@@ -431,7 +431,9 @@ mod tests {
|
||||
assert!(result.is_err());
|
||||
|
||||
// Test range where start > end (this should not cause overflow)
|
||||
let result = get_file_contents(file_path.to_str().unwrap(), Some(5..3));
|
||||
let start = 5;
|
||||
let end = 3;
|
||||
let result = get_file_contents(file_path.to_str().unwrap(), Some(start..end));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,10 @@ use std::{
|
||||
};
|
||||
|
||||
#[cfg(feature = "vortex")]
|
||||
use arrow::{array::StructArray, datatypes::DataType};
|
||||
use arrow::{
|
||||
array::StructArray,
|
||||
datatypes::{DataType, Field},
|
||||
};
|
||||
use arrow::{error::ArrowError, record_batch::RecordBatch};
|
||||
use arrow_schema::Schema;
|
||||
#[cfg(feature = "vortex")]
|
||||
@@ -36,7 +39,7 @@ use parquet::{
|
||||
#[cfg(feature = "vortex")]
|
||||
use vortex::{
|
||||
VortexSessionDefault,
|
||||
array::{ArrayRef, arrow::IntoArrowArray},
|
||||
array::{ArrayRef, VortexSessionExecute, arrow::ArrowSessionExt},
|
||||
buffer::Buffer,
|
||||
file::OpenOptionsSessionExt,
|
||||
io::session::RuntimeSessionExt,
|
||||
@@ -144,11 +147,15 @@ pub type RecordBatchStream = Pin<Box<dyn Stream<Item = Result<RecordBatch, Arrow
|
||||
/// Convert a single vortex [`ArrayRef`] to an Arrow [`RecordBatch`].
|
||||
#[cfg(feature = "vortex")]
|
||||
pub fn vortex_array_to_record_batch(
|
||||
session: &VortexSession,
|
||||
array: ArrayRef,
|
||||
data_type: &DataType,
|
||||
) -> Result<RecordBatch, ArrowError> {
|
||||
let array = array
|
||||
.into_arrow(data_type)
|
||||
let mut ctx = session.create_execution_ctx();
|
||||
let target = Field::new("", data_type.clone(), array.dtype().is_nullable());
|
||||
let array = session
|
||||
.arrow()
|
||||
.execute_arrow(array, Some(&target), &mut ctx)
|
||||
.map_err(|e| ArrowError::ExternalError(Box::new(e)))?;
|
||||
let struct_array = array
|
||||
.as_any()
|
||||
@@ -183,9 +190,12 @@ pub async fn get_recordbatch_reader_from_bytes(
|
||||
|
||||
let stream = vortex_stream.then(move |result| {
|
||||
let arrow_data_type = arrow_data_type.clone();
|
||||
let session = session.clone();
|
||||
async move {
|
||||
match result {
|
||||
Ok(array) => vortex_array_to_record_batch(array, &arrow_data_type),
|
||||
Ok(array) => {
|
||||
vortex_array_to_record_batch(&session, array, &arrow_data_type)
|
||||
}
|
||||
Err(e) => Err(ArrowError::ExternalError(Box::new(e))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ impl<'n> TreeNodeVisitor<'n> for RemoteScanVisitor {
|
||||
fn f_up(&mut self, node: &'n Self::Node) -> Result<TreeNodeRecursion> {
|
||||
let name = node.name();
|
||||
if name == "RemoteScanExec" {
|
||||
let remote_scan_exec = node.as_any().downcast_ref::<RemoteScanExec>().unwrap();
|
||||
let remote_scan_exec = node.downcast_ref::<RemoteScanExec>().unwrap();
|
||||
self.scan_stats = Some(remote_scan_exec.scan_stats());
|
||||
Ok(TreeNodeRecursion::Stop)
|
||||
} else {
|
||||
@@ -86,7 +86,7 @@ impl<'n> TreeNodeVisitor<'n> for MetricsVisitor {
|
||||
fn f_up(&mut self, node: &'n Self::Node) -> Result<TreeNodeRecursion> {
|
||||
let name = node.name();
|
||||
if name == "RemoteScanExec" {
|
||||
let remote_scan_exec = node.as_any().downcast_ref::<RemoteScanExec>().unwrap();
|
||||
let remote_scan_exec = node.downcast_ref::<RemoteScanExec>().unwrap();
|
||||
self.metrics.push(remote_scan_exec.cluster_metrics());
|
||||
}
|
||||
Ok(TreeNodeRecursion::Continue)
|
||||
@@ -114,7 +114,7 @@ impl<'n> TreeNodeVisitor<'n> for PartialErrVisitor {
|
||||
|
||||
fn f_up(&mut self, node: &'n Self::Node) -> Result<TreeNodeRecursion> {
|
||||
if node.name() == "RemoteScanExec" {
|
||||
let remote_scan_exec = node.as_any().downcast_ref::<RemoteScanExec>().unwrap();
|
||||
let remote_scan_exec = node.downcast_ref::<RemoteScanExec>().unwrap();
|
||||
self.partial_err = Some(remote_scan_exec.partial_err());
|
||||
Ok(TreeNodeRecursion::Stop)
|
||||
} else {
|
||||
@@ -145,7 +145,7 @@ impl<'n> TreeNodeVisitor<'n> for PeakMemoryVisitor {
|
||||
fn f_up(&mut self, node: &'n Self::Node) -> Result<TreeNodeRecursion> {
|
||||
let name = node.name();
|
||||
if name == "RemoteScanExec" {
|
||||
let remote_scan_exec = node.as_any().downcast_ref::<RemoteScanExec>().unwrap();
|
||||
let remote_scan_exec = node.downcast_ref::<RemoteScanExec>().unwrap();
|
||||
self.peak_memory = Some(remote_scan_exec.peak_memory());
|
||||
}
|
||||
Ok(TreeNodeRecursion::Continue)
|
||||
@@ -154,40 +154,14 @@ impl<'n> TreeNodeVisitor<'n> for PeakMemoryVisitor {
|
||||
|
||||
/// Get the peak memory from the SessionContext's memory pool.
|
||||
/// This should be called after the query execution to get the peak memory usage.
|
||||
///
|
||||
/// # Safety
|
||||
/// This function uses unsafe code to downcast Arc<dyn MemoryPool> to Arc<PeakMemoryPool>.
|
||||
/// This is sound because:
|
||||
/// 1. The memory pool is always created as PeakMemoryPool in datafusion context creation (see
|
||||
/// exec.rs)
|
||||
/// 2. Arc::into_raw/from_raw maintain proper Arc semantics
|
||||
/// 3. The type name is checked at runtime to verify the cast is valid
|
||||
pub fn get_peak_memory_from_ctx(ctx: &SessionContext) -> Arc<AtomicUsize> {
|
||||
let memory_pool = ctx.runtime_env().memory_pool.clone();
|
||||
|
||||
// Runtime type check: verify this is actually a PeakMemoryPool
|
||||
let type_name = format!("{:?}", memory_pool);
|
||||
if !type_name.contains("PeakMemoryPool") {
|
||||
log::warn!(
|
||||
"Memory pool is not PeakMemoryPool (type: {}), returning 0",
|
||||
type_name
|
||||
);
|
||||
return Arc::new(AtomicUsize::new(0));
|
||||
}
|
||||
|
||||
// SAFETY: We've verified the concrete type is PeakMemoryPool via runtime check above.
|
||||
// The memory pool is created as PeakMemoryPool in datafusion context creation.
|
||||
// We use Arc::into_raw and Arc::from_raw to maintain proper reference counting.
|
||||
unsafe {
|
||||
let raw_ptr = Arc::into_raw(memory_pool);
|
||||
let peak_pool_ptr = raw_ptr as *const PeakMemoryPool;
|
||||
let peak_memory = (*peak_pool_ptr).peak_memory.clone();
|
||||
|
||||
// Reconstruct the Arc to prevent memory leak
|
||||
let _ = Arc::from_raw(raw_ptr);
|
||||
|
||||
peak_memory
|
||||
}
|
||||
memory_pool
|
||||
.downcast_ref::<PeakMemoryPool>()
|
||||
.unwrap()
|
||||
.peak_memory
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -40,7 +40,7 @@ pub(crate) fn try_decode(
|
||||
}
|
||||
|
||||
pub(crate) fn try_encode(node: Arc<dyn ExecutionPlan>, buf: &mut Vec<u8>) -> Result<()> {
|
||||
let Some(node) = node.as_any().downcast_ref::<AggregateTopkExec>() else {
|
||||
let Some(node) = node.downcast_ref::<AggregateTopkExec>() else {
|
||||
return internal_err!("Not supported");
|
||||
};
|
||||
let plan_node = cluster_rpc::AggregateTopkExecNode {
|
||||
@@ -120,8 +120,8 @@ mod tests {
|
||||
let ctx = datafusion::prelude::SessionContext::new();
|
||||
let plan2 =
|
||||
physical_plan_from_bytes_with_extension_codec(&plan_bytes, &ctx.task_ctx(), &proto)?;
|
||||
let plan2 = plan2.as_any().downcast_ref::<AggregateTopkExec>().unwrap();
|
||||
let plan = plan.as_any().downcast_ref::<AggregateTopkExec>().unwrap();
|
||||
let plan2 = plan2.downcast_ref::<AggregateTopkExec>().unwrap();
|
||||
let plan = plan.downcast_ref::<AggregateTopkExec>().unwrap();
|
||||
|
||||
// check
|
||||
assert_eq!(plan.limit(), plan2.limit());
|
||||
|
||||
@@ -57,14 +57,11 @@ pub(super) fn try_decode(
|
||||
}
|
||||
|
||||
pub(super) fn try_encode(node: Arc<dyn ExecutionPlan>, buf: &mut Vec<u8>) -> Result<()> {
|
||||
let exec = node
|
||||
.as_any()
|
||||
.downcast_ref::<DeduplicationExec>()
|
||||
.ok_or_else(|| {
|
||||
datafusion::error::DataFusionError::Internal(
|
||||
"Failed to downcast to DeduplicationExec".to_string(),
|
||||
)
|
||||
})?;
|
||||
let exec = node.downcast_ref::<DeduplicationExec>().ok_or_else(|| {
|
||||
datafusion::error::DataFusionError::Internal(
|
||||
"Failed to downcast to DeduplicationExec".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Encode deduplication columns as PhysicalExprNodes
|
||||
let mut deduplication_columns_proto = Vec::new();
|
||||
|
||||
@@ -62,7 +62,7 @@ pub(crate) fn try_decode(
|
||||
}
|
||||
|
||||
pub(crate) fn try_encode(node: Arc<dyn ExecutionPlan>, buf: &mut Vec<u8>) -> Result<()> {
|
||||
let Some(node) = node.as_any().downcast_ref::<NewEmptyExec>() else {
|
||||
let Some(node) = node.downcast_ref::<NewEmptyExec>() else {
|
||||
return internal_err!("Not supported");
|
||||
};
|
||||
let extension_codec = DefaultLogicalExtensionCodec {};
|
||||
@@ -119,8 +119,8 @@ mod tests {
|
||||
let ctx = datafusion::prelude::SessionContext::new();
|
||||
let plan2 =
|
||||
physical_plan_from_bytes_with_extension_codec(&plan_bytes, &ctx.task_ctx(), &proto)?;
|
||||
let plan2 = plan2.as_any().downcast_ref::<NewEmptyExec>().unwrap();
|
||||
let plan = plan.as_any().downcast_ref::<NewEmptyExec>().unwrap();
|
||||
let plan2 = plan2.downcast_ref::<NewEmptyExec>().unwrap();
|
||||
let plan = plan.downcast_ref::<NewEmptyExec>().unwrap();
|
||||
|
||||
// check
|
||||
assert_eq!(plan.name(), plan2.name());
|
||||
|
||||
@@ -42,7 +42,7 @@ pub(crate) fn try_decode(
|
||||
}
|
||||
|
||||
pub(crate) fn try_encode(node: Arc<dyn ExecutionPlan>, buf: &mut Vec<u8>) -> Result<()> {
|
||||
let Some(node) = node.as_any().downcast_ref::<EnrichmentExec>() else {
|
||||
let Some(node) = node.downcast_ref::<EnrichmentExec>() else {
|
||||
return internal_err!("Not supported");
|
||||
};
|
||||
let plan_node = cluster_rpc::EnrichmentExecNode {
|
||||
@@ -92,8 +92,8 @@ mod tests {
|
||||
let ctx = datafusion::prelude::SessionContext::new();
|
||||
let plan2 =
|
||||
physical_plan_from_bytes_with_extension_codec(&plan_bytes, &ctx.task_ctx(), &proto)?;
|
||||
let plan2 = plan2.as_any().downcast_ref::<EnrichmentExec>().unwrap();
|
||||
let plan = plan.as_any().downcast_ref::<EnrichmentExec>().unwrap();
|
||||
let plan2 = plan2.downcast_ref::<EnrichmentExec>().unwrap();
|
||||
let plan = plan.downcast_ref::<EnrichmentExec>().unwrap();
|
||||
|
||||
// check
|
||||
assert_eq!(plan.trace_id(), plan2.trace_id());
|
||||
|
||||
@@ -88,25 +88,25 @@ impl PhysicalExtensionCodec for PhysicalPlanNodePhysicalExtensionCodec {
|
||||
|
||||
fn try_encode(&self, node: Arc<dyn ExecutionPlan>, buf: &mut Vec<u8>) -> Result<()> {
|
||||
#[cfg(feature = "enterprise")]
|
||||
if node.as_any().downcast_ref::<NewEmptyExec>().is_some() {
|
||||
if node.downcast_ref::<NewEmptyExec>().is_some() {
|
||||
super::empty_exec::try_encode(node, buf)
|
||||
} else if node.as_any().downcast_ref::<DeduplicationExec>().is_some() {
|
||||
} else if node.downcast_ref::<DeduplicationExec>().is_some() {
|
||||
super::deduplication_exec::try_encode(node, buf)
|
||||
} else if node.as_any().downcast_ref::<AggregateTopkExec>().is_some() {
|
||||
} else if node.downcast_ref::<AggregateTopkExec>().is_some() {
|
||||
super::aggregate_topk_exec::try_encode(node, buf)
|
||||
} else if node.as_any().downcast_ref::<StreamingAggsExec>().is_some() {
|
||||
} else if node.downcast_ref::<StreamingAggsExec>().is_some() {
|
||||
super::streaming_aggs_exec::try_encode(node, buf)
|
||||
} else if node.as_any().downcast_ref::<TmpExec>().is_some() {
|
||||
} else if node.downcast_ref::<TmpExec>().is_some() {
|
||||
super::tmp_exec::try_encode(node, buf)
|
||||
} else if node.as_any().downcast_ref::<EnrichmentExec>().is_some() {
|
||||
} else if node.downcast_ref::<EnrichmentExec>().is_some() {
|
||||
super::enrichment_exec::try_encode(node, buf)
|
||||
} else {
|
||||
internal_err!("Not supported")
|
||||
}
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
if node.as_any().downcast_ref::<NewEmptyExec>().is_some() {
|
||||
if node.downcast_ref::<NewEmptyExec>().is_some() {
|
||||
super::empty_exec::try_encode(node, buf)
|
||||
} else if node.as_any().downcast_ref::<DeduplicationExec>().is_some() {
|
||||
} else if node.downcast_ref::<DeduplicationExec>().is_some() {
|
||||
super::deduplication_exec::try_encode(node, buf)
|
||||
} else {
|
||||
internal_err!("Not supported")
|
||||
@@ -144,8 +144,8 @@ mod tests {
|
||||
let ctx = datafusion::prelude::SessionContext::new();
|
||||
let plan2 =
|
||||
physical_plan_from_bytes_with_extension_codec(&plan_bytes, &ctx.task_ctx(), &proto)?;
|
||||
let plan2 = plan2.as_any().downcast_ref::<NewEmptyExec>().unwrap();
|
||||
let plan = plan.as_any().downcast_ref::<NewEmptyExec>().unwrap();
|
||||
let plan2 = plan2.downcast_ref::<NewEmptyExec>().unwrap();
|
||||
let plan = plan.downcast_ref::<NewEmptyExec>().unwrap();
|
||||
|
||||
// check
|
||||
assert_eq!(plan.name(), plan2.name());
|
||||
|
||||
@@ -36,7 +36,7 @@ pub(crate) fn try_decode(
|
||||
};
|
||||
let extension_codec = super::get_physical_extension_codec();
|
||||
let aggregate_plan = aggregate_plan.try_into_physical_plan(ctx, &extension_codec)?;
|
||||
let Some(aggregate_plan) = aggregate_plan.as_any().downcast_ref::<AggregateExec>() else {
|
||||
let Some(aggregate_plan) = aggregate_plan.downcast_ref::<AggregateExec>() else {
|
||||
return internal_err!("aggregate_plan is not an AggregateExec");
|
||||
};
|
||||
let aggregate_plan = Arc::new(aggregate_plan.clone());
|
||||
@@ -61,7 +61,7 @@ pub(crate) fn try_decode(
|
||||
}
|
||||
|
||||
pub(crate) fn try_encode(node: Arc<dyn ExecutionPlan>, buf: &mut Vec<u8>) -> Result<()> {
|
||||
let Some(node) = node.as_any().downcast_ref::<StreamingAggsExec>() else {
|
||||
let Some(node) = node.downcast_ref::<StreamingAggsExec>() else {
|
||||
return internal_err!("Not supported");
|
||||
};
|
||||
|
||||
@@ -169,8 +169,8 @@ mod tests {
|
||||
let ctx = datafusion::prelude::SessionContext::new();
|
||||
let plan2 =
|
||||
physical_plan_from_bytes_with_extension_codec(&plan_bytes, &ctx.task_ctx(), &proto)?;
|
||||
let plan2 = plan2.as_any().downcast_ref::<StreamingAggsExec>().unwrap();
|
||||
let plan = plan.as_any().downcast_ref::<StreamingAggsExec>().unwrap();
|
||||
let plan2 = plan2.downcast_ref::<StreamingAggsExec>().unwrap();
|
||||
let plan = plan.downcast_ref::<StreamingAggsExec>().unwrap();
|
||||
|
||||
// check
|
||||
assert_eq!(plan.id(), plan2.id());
|
||||
@@ -255,8 +255,8 @@ mod tests {
|
||||
ctx.register_udf(STR_MATCH_UDF.clone());
|
||||
let plan2 =
|
||||
physical_plan_from_bytes_with_extension_codec(&plan_bytes, &ctx.task_ctx(), &proto)?;
|
||||
let plan2 = plan2.as_any().downcast_ref::<StreamingAggsExec>().unwrap();
|
||||
let plan = plan.as_any().downcast_ref::<StreamingAggsExec>().unwrap();
|
||||
let plan2 = plan2.downcast_ref::<StreamingAggsExec>().unwrap();
|
||||
let plan = plan.downcast_ref::<StreamingAggsExec>().unwrap();
|
||||
|
||||
// check
|
||||
assert_eq!(plan.id(), plan2.id());
|
||||
@@ -268,14 +268,11 @@ mod tests {
|
||||
|
||||
// Verify that the aggregate plan contains a FilterExec
|
||||
let agg_plan_decoded = plan2.aggregate_plan();
|
||||
let agg_exec = agg_plan_decoded
|
||||
.as_any()
|
||||
.downcast_ref::<AggregateExec>()
|
||||
.unwrap();
|
||||
let agg_exec = agg_plan_decoded.as_ref();
|
||||
let input_plan = agg_exec.input();
|
||||
|
||||
// The input should be a FilterExec
|
||||
assert!(input_plan.as_any().downcast_ref::<FilterExec>().is_some());
|
||||
assert!(input_plan.downcast_ref::<FilterExec>().is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ pub(crate) fn try_decode(
|
||||
}
|
||||
|
||||
pub(crate) fn try_encode(node: Arc<dyn ExecutionPlan>, buf: &mut Vec<u8>) -> Result<()> {
|
||||
let Some(node) = node.as_any().downcast_ref::<TmpExec>() else {
|
||||
let Some(node) = node.downcast_ref::<TmpExec>() else {
|
||||
return internal_err!("Not supported");
|
||||
};
|
||||
let plan_node = cluster_rpc::TmpExecNode {
|
||||
@@ -111,8 +111,8 @@ mod tests {
|
||||
let ctx = datafusion::prelude::SessionContext::new();
|
||||
let plan2 =
|
||||
physical_plan_from_bytes_with_extension_codec(&plan_bytes, &ctx.task_ctx(), &proto)?;
|
||||
let plan2 = plan2.as_any().downcast_ref::<TmpExec>().unwrap();
|
||||
let plan = plan.as_any().downcast_ref::<TmpExec>().unwrap();
|
||||
let plan2 = plan2.downcast_ref::<TmpExec>().unwrap();
|
||||
let plan = plan.downcast_ref::<TmpExec>().unwrap();
|
||||
|
||||
// check
|
||||
assert_eq!(plan.trace_id(), plan2.trace_id());
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{any::Any, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow::{
|
||||
array::{Int32Builder, StringBuilder},
|
||||
@@ -104,11 +104,6 @@ impl ExecutionPlan for DistributeAnalyzeExec {
|
||||
"DistributeAnalyzeExec"
|
||||
}
|
||||
|
||||
/// Return a reference to Any that can be used for downcasting
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn properties(&self) -> &Arc<PlanProperties> {
|
||||
&self.cache
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{any::Any, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_schema::SortOptions;
|
||||
use config::TIMESTAMP_COL_NAME;
|
||||
@@ -196,11 +196,6 @@ impl ExecutionPlan for NewEmptyExec {
|
||||
"NewEmptyExec"
|
||||
}
|
||||
|
||||
/// Return a reference to Any that can be used for downcasting
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn properties(&self) -> &Arc<PlanProperties> {
|
||||
&self.cache
|
||||
}
|
||||
@@ -314,12 +309,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_as_any_downcasts() {
|
||||
use datafusion::physical_plan::ExecutionPlan;
|
||||
|
||||
let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
|
||||
let exec = NewEmptyExec::new("t", schema.clone(), None, &[], None, false, schema);
|
||||
let any = exec.as_any();
|
||||
assert!(any.downcast_ref::<NewEmptyExec>().is_some());
|
||||
let exec_plan: &dyn ExecutionPlan = &exec;
|
||||
assert!(exec_plan.downcast_ref::<NewEmptyExec>().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{any::Any, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use config::utils::record_batch_ext::convert_json_to_record_batch;
|
||||
use datafusion::{
|
||||
@@ -93,11 +93,6 @@ impl ExecutionPlan for EnrichExec {
|
||||
"EnrichExec"
|
||||
}
|
||||
|
||||
/// Return a reference to Any that can be used for downcasting
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn properties(&self) -> &Arc<PlanProperties> {
|
||||
&self.cache
|
||||
}
|
||||
@@ -222,12 +217,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_as_any_downcasts() {
|
||||
use datafusion::physical_plan::ExecutionPlan;
|
||||
|
||||
let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
|
||||
let exec = EnrichExec::new("org", "stream", schema);
|
||||
let any = exec.as_any();
|
||||
assert!(any.downcast_ref::<EnrichExec>().is_some());
|
||||
let exec_plan: &dyn ExecutionPlan = &exec;
|
||||
assert!(exec_plan.downcast_ref::<EnrichExec>().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{any::Any, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow::{
|
||||
array::{ArrayRef, RecordBatch},
|
||||
@@ -146,10 +146,6 @@ impl ExecutionPlan for EnrichmentExec {
|
||||
"EnrichmentExec"
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn properties(&self) -> &Arc<PlanProperties> {
|
||||
&self.cache
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ impl NewEmptyExecVisitor {
|
||||
self.plan
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.as_any()
|
||||
.downcast_ref::<NewEmptyExec>()
|
||||
.unwrap()
|
||||
}
|
||||
@@ -107,7 +106,7 @@ impl TreeNodeRewriter for ReplaceTableScanExec {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{any::Any, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use datafusion::physical_plan::{ExecutionPlan, PlanProperties};
|
||||
|
||||
@@ -128,9 +127,6 @@ mod tests {
|
||||
fn name(&self) -> &'static str {
|
||||
"NewEmptyExec"
|
||||
}
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
fn properties(&self) -> &Arc<PlanProperties> {
|
||||
panic!("not needed")
|
||||
}
|
||||
|
||||
@@ -13,10 +13,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
any::Any,
|
||||
sync::{Arc, atomic::AtomicUsize},
|
||||
};
|
||||
use std::sync::{Arc, atomic::AtomicUsize};
|
||||
|
||||
use arrow_schema::SchemaRef;
|
||||
use config::{
|
||||
@@ -207,11 +204,6 @@ impl ExecutionPlan for RemoteScanExec {
|
||||
"RemoteScanExec"
|
||||
}
|
||||
|
||||
/// Return a reference to Any that can be used for downcasting
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn schema(&self) -> SchemaRef {
|
||||
Arc::clone(&self.input.schema())
|
||||
}
|
||||
@@ -493,7 +485,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_remote_scan_exec_as_any() {
|
||||
let exec = make_remote_exec();
|
||||
assert!(exec.as_any().downcast_ref::<RemoteScanExec>().is_some());
|
||||
let exec_plan: &dyn ExecutionPlan = &exec;
|
||||
assert!(exec_plan.downcast_ref::<RemoteScanExec>().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -75,7 +75,7 @@ impl TreeNodeRewriter for TantivyOptimizeRewriter {
|
||||
|
||||
fn f_up(&mut self, node: Arc<dyn ExecutionPlan>) -> Result<Transformed<Self::Node>> {
|
||||
if node.name() == "AggregateExec" {
|
||||
let aggregate = node.as_any().downcast_ref::<AggregateExec>().unwrap();
|
||||
let aggregate = node.downcast_ref::<AggregateExec>().unwrap();
|
||||
if *aggregate.mode() == AggregateMode::Partial {
|
||||
let tantivy_exec = Arc::new(TantivyOptimizeExec::new(
|
||||
self.query.clone(),
|
||||
|
||||
@@ -60,7 +60,10 @@ use super::{
|
||||
storage::file_list, udf::transform_udf::get_all_transform,
|
||||
};
|
||||
use crate::service::search::{
|
||||
datafusion::table_provider::{listing_adapter::ListingTableAdapter, uniontable::NewUnionTable},
|
||||
datafusion::{
|
||||
storage::file_statistics_cache,
|
||||
table_provider::{listing_adapter::ListingTableAdapter, uniontable::NewUnionTable},
|
||||
},
|
||||
index::IndexCondition,
|
||||
};
|
||||
|
||||
@@ -107,6 +110,29 @@ pub fn create_session_config(
|
||||
.execution
|
||||
.skip_physical_aggregate_schema_check = true;
|
||||
|
||||
// DataFusion 54 executes uncorrelated scalar subqueries physically via
|
||||
// `ScalarSubqueryExec`/`ScalarSubqueryExpr` instead of rewriting them into joins.
|
||||
// `ScalarSubqueryExpr` can only be (de)serialized inside its surrounding
|
||||
// `ScalarSubqueryExec`, which breaks our distributed plan splitting across the Flight
|
||||
// boundary. Disable the physical path so `ScalarSubqueryToJoin` decorrelates them into
|
||||
// joins again, keeping the serialized follower plans valid.
|
||||
config
|
||||
.options_mut()
|
||||
.optimizer
|
||||
.enable_physical_uncorrelated_scalar_subquery = false;
|
||||
|
||||
// DataFusion 54 builds a runtime `DynamicFilterPhysicalExpr` from a `HashJoinExec`'s
|
||||
// build-side join keys and pushes it into the probe-side scan. That runtime state can't
|
||||
// cross our distributed RemoteScan/Flight boundary, and after our custom join rewrites
|
||||
// (`swap_inputs` + broadcast/enrichment join) the filter ends up referencing build-side
|
||||
// columns by index against the projected probe-side batch, producing
|
||||
// "PhysicalExpr Column references column ... but input schema only has N columns" at
|
||||
// execution time. Disable join dynamic filter pushdown to keep the split plans valid.
|
||||
config
|
||||
.options_mut()
|
||||
.optimizer
|
||||
.enable_join_dynamic_filter_pushdown = false;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
@@ -126,9 +152,9 @@ pub async fn create_runtime_env(trace_id: &str, memory_limit: usize) -> Result<R
|
||||
RuntimeEnvBuilder::new().with_object_store_registry(Arc::new(object_store_registry));
|
||||
if cfg.limit.datafusion_file_stat_cache_max_size > 0 {
|
||||
let cache_config = CacheManagerConfig::default();
|
||||
let cache_config = cache_config.with_files_statistics_cache(Some(
|
||||
super::storage::file_statistics_cache::GLOBAL_CACHE.clone(),
|
||||
));
|
||||
let cache_config = cache_config
|
||||
.with_file_statistics_cache(Some(file_statistics_cache::GLOBAL_CACHE.clone()))
|
||||
.with_file_statistics_cache_limit(cfg.limit.datafusion_file_stat_cache_max_size);
|
||||
builder = builder.with_cache_manager(cache_config);
|
||||
}
|
||||
|
||||
@@ -614,6 +640,14 @@ mod tests {
|
||||
assert_eq!(config.options().sql_parser.dialect, Dialect::PostgreSQL);
|
||||
assert!(!config.options().execution.listing_table_ignore_subdirectory);
|
||||
assert!(config.information_schema());
|
||||
// Join dynamic filter pushdown must stay disabled: its runtime filter can't cross our
|
||||
// distributed RemoteScan/Flight boundary and breaks our custom join rewrites.
|
||||
assert!(
|
||||
!config
|
||||
.options()
|
||||
.optimizer
|
||||
.enable_join_dynamic_filter_pushdown
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -375,15 +375,14 @@ mod tests {
|
||||
|
||||
let expected = vec![
|
||||
"HashJoinExec: mode=CollectLeft, join_type=Inner, on=[(prod_id@1, prod_id@0)], projection=[usr_id@0, prod_id@1]",
|
||||
" ProjectionExec: expr=[usr_id@1 as usr_id, prod_id@0 as prod_id]",
|
||||
" HashJoinExec: mode=CollectLeft, join_type=Inner, on=[(usr_id@0, usr_id@0)], projection=[prod_id@1, usr_id@2]",
|
||||
" ProjectionExec: expr=[usr_id@0 as usr_id, prod_id@1 as prod_id]",
|
||||
" DeduplicationExec: columns: [usr_id@0]",
|
||||
" SortExec: expr=[usr_id@0 DESC NULLS LAST, _timestamp@2 DESC NULLS LAST], preserve_partitioning=[false]",
|
||||
" SortPreservingMergeExec: [_timestamp@2 DESC NULLS LAST], fetch=50000",
|
||||
" SortExec: TopK(fetch=50000), expr=[_timestamp@2 DESC NULLS LAST], preserve_partitioning=[true]",
|
||||
" DataSourceExec: partitions=2, partition_sizes=[1, 1]",
|
||||
" DataSourceExec: partitions=1, partition_sizes=[1]",
|
||||
" HashJoinExec: mode=CollectLeft, join_type=Inner, on=[(usr_id@0, usr_id@0)], projection=[usr_id@2, prod_id@1]",
|
||||
" ProjectionExec: expr=[usr_id@0 as usr_id, prod_id@1 as prod_id]",
|
||||
" DeduplicationExec: columns: [usr_id@0]",
|
||||
" SortExec: expr=[usr_id@0 DESC NULLS LAST, _timestamp@2 DESC NULLS LAST], preserve_partitioning=[false]",
|
||||
" SortPreservingMergeExec: [_timestamp@2 DESC NULLS LAST], fetch=50000",
|
||||
" SortExec: TopK(fetch=50000), expr=[_timestamp@2 DESC NULLS LAST], preserve_partitioning=[true]",
|
||||
" DataSourceExec: partitions=2, partition_sizes=[1, 1]",
|
||||
" DataSourceExec: partitions=1, partition_sizes=[1]",
|
||||
" ProjectionExec: expr=[prod_id@0 as prod_id]",
|
||||
" DeduplicationExec: columns: [prod_id@0]",
|
||||
" SortExec: expr=[prod_id@0 DESC NULLS LAST, _timestamp@1 DESC NULLS LAST], preserve_partitioning=[false]",
|
||||
|
||||
@@ -29,7 +29,7 @@ use crate::service::search::datafusion::distributed_plan::{
|
||||
|
||||
// replace the AnalyzeExec to DistributeAnalyzeExec
|
||||
pub fn optimize_distribute_analyze(plan: Arc<dyn ExecutionPlan>) -> Result<Arc<dyn ExecutionPlan>> {
|
||||
if let Some(analyze) = plan.as_any().downcast_ref::<AnalyzeExec>() {
|
||||
if let Some(analyze) = plan.downcast_ref::<AnalyzeExec>() {
|
||||
let distribute_analyze = Arc::new(DistributeAnalyzeExec::new(
|
||||
analyze.verbose(),
|
||||
analyze.show_statistics(),
|
||||
@@ -55,7 +55,7 @@ impl TreeNodeRewriter for AnalyzeRewrite {
|
||||
type Node = Arc<dyn ExecutionPlan>;
|
||||
|
||||
fn f_up(&mut self, node: Arc<dyn ExecutionPlan>) -> Result<Transformed<Self::Node>> {
|
||||
if let Some(remote) = node.as_any().downcast_ref::<RemoteScanExec>() {
|
||||
if let Some(remote) = node.downcast_ref::<RemoteScanExec>() {
|
||||
let remote = Arc::new(remote.clone().set_analyze()) as Arc<dyn ExecutionPlan>;
|
||||
return Ok(Transformed::yes(remote));
|
||||
}
|
||||
@@ -84,6 +84,7 @@ mod tests {
|
||||
false,
|
||||
false,
|
||||
vec![],
|
||||
None,
|
||||
Arc::new(EmptyExec::new(get_schema())),
|
||||
get_schema(),
|
||||
));
|
||||
@@ -102,7 +103,7 @@ mod tests {
|
||||
) as Arc<dyn ExecutionPlan>;
|
||||
let mut rewriter = AnalyzeRewrite::new();
|
||||
let plan = plan.rewrite(&mut rewriter).unwrap().data;
|
||||
let remote_scan = plan.as_any().downcast_ref::<RemoteScanExec>().unwrap();
|
||||
let remote_scan = plan.downcast_ref::<RemoteScanExec>().unwrap();
|
||||
assert!(remote_scan.analyze());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ impl TreeNodeRewriter for EnrichmentExecRewriter {
|
||||
type Node = Arc<dyn ExecutionPlan>;
|
||||
|
||||
fn f_up(&mut self, node: Arc<dyn ExecutionPlan>) -> Result<Transformed<Self::Node>> {
|
||||
if let Some(new_empty) = node.as_any().downcast_ref::<NewEmptyExec>() {
|
||||
if let Some(new_empty) = node.downcast_ref::<NewEmptyExec>() {
|
||||
let table_name = TableReference::parse_str(new_empty.name());
|
||||
if matches!(
|
||||
table_name.schema(),
|
||||
@@ -140,7 +140,7 @@ pub fn should_use_enrichment_broadcast_join(plan: &Arc<dyn ExecutionPlan>) -> bo
|
||||
let mut result = false;
|
||||
let _ = plan.apply(|node| {
|
||||
Ok(if node.name() == "HashJoinExec" {
|
||||
if let Some(hash_join) = node.as_any().downcast_ref::<HashJoinExec>() {
|
||||
if let Some(hash_join) = node.downcast_ref::<HashJoinExec>() {
|
||||
result = is_enrichment_table(hash_join.left())
|
||||
&& is_valid_enrich_broadcast_right(hash_join.right());
|
||||
}
|
||||
@@ -157,7 +157,7 @@ pub fn is_enrichment_table(plan: &Arc<dyn ExecutionPlan>) -> bool {
|
||||
let mut found_enrichment = false;
|
||||
let _ = plan.apply(|node| {
|
||||
Ok(
|
||||
if let Some(new_empty) = node.as_any().downcast_ref::<NewEmptyExec>() {
|
||||
if let Some(new_empty) = node.downcast_ref::<NewEmptyExec>() {
|
||||
let table_name = TableReference::parse_str(new_empty.name());
|
||||
if matches!(
|
||||
table_name.schema(),
|
||||
@@ -180,7 +180,7 @@ fn is_valid_enrich_broadcast_right(plan: &Arc<dyn ExecutionPlan>) -> bool {
|
||||
let mut is_valid = true;
|
||||
let _ = plan.apply(|node| {
|
||||
Ok(
|
||||
if let Some(new_empty) = node.as_any().downcast_ref::<NewEmptyExec>() {
|
||||
if let Some(new_empty) = node.downcast_ref::<NewEmptyExec>() {
|
||||
let table_name = TableReference::parse_str(new_empty.name());
|
||||
if matches!(
|
||||
table_name.schema(),
|
||||
|
||||
@@ -177,7 +177,7 @@ impl TreeNodeRewriter for IndexOptimizer {
|
||||
type Node = Arc<dyn ExecutionPlan>;
|
||||
|
||||
fn f_up(&mut self, node: Self::Node) -> Result<Transformed<Self::Node>> {
|
||||
if let Some(filter) = node.as_any().downcast_ref::<FilterExec>() {
|
||||
if let Some(filter) = node.downcast_ref::<FilterExec>() {
|
||||
self.has_filter = true;
|
||||
let mut index_conditions = IndexCondition::new();
|
||||
let mut other_conditions = Vec::new();
|
||||
@@ -254,7 +254,7 @@ fn construct_filter_exec(
|
||||
|
||||
// Check if the expression is valid for the index.
|
||||
fn is_expr_valid_for_index(expr: &Arc<dyn PhysicalExpr>, index_fields: &HashSet<String>) -> bool {
|
||||
if let Some(expr) = expr.as_any().downcast_ref::<BinaryExpr>() {
|
||||
if let Some(expr) = expr.downcast_ref::<BinaryExpr>() {
|
||||
match expr.op() {
|
||||
Operator::Eq | Operator::NotEq => {
|
||||
let column = if is_value(expr.left()) && is_column(expr.right()) {
|
||||
@@ -275,7 +275,7 @@ fn is_expr_valid_for_index(expr: &Arc<dyn PhysicalExpr>, index_fields: &HashSet<
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
} else if let Some(expr) = expr.as_any().downcast_ref::<InListExpr>() {
|
||||
} else if let Some(expr) = expr.downcast_ref::<InListExpr>() {
|
||||
if !is_column(expr.expr()) || !index_fields.contains(get_column_name(expr.expr())) {
|
||||
return false;
|
||||
}
|
||||
@@ -285,7 +285,7 @@ fn is_expr_valid_for_index(expr: &Arc<dyn PhysicalExpr>, index_fields: &HashSet<
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if let Some(expr) = expr.as_any().downcast_ref::<ScalarFunctionExpr>() {
|
||||
} else if let Some(expr) = expr.downcast_ref::<ScalarFunctionExpr>() {
|
||||
let name = expr.name();
|
||||
return match name {
|
||||
MATCH_ALL_UDF_NAME => {
|
||||
@@ -303,7 +303,7 @@ fn is_expr_valid_for_index(expr: &Arc<dyn PhysicalExpr>, index_fields: &HashSet<
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
} else if let Some(expr) = expr.as_any().downcast_ref::<NotExpr>() {
|
||||
} else if let Some(expr) = expr.downcast_ref::<NotExpr>() {
|
||||
return is_expr_valid_for_index(expr.arg(), index_fields);
|
||||
} else {
|
||||
return false;
|
||||
|
||||
@@ -69,7 +69,7 @@ impl<'n> TreeNodeVisitor<'n> for SimpleCountVisitor {
|
||||
type Node = Arc<dyn ExecutionPlan>;
|
||||
|
||||
fn f_down(&mut self, node: &'n Self::Node) -> Result<TreeNodeRecursion> {
|
||||
if let Some(aggregate) = node.as_any().downcast_ref::<AggregateExec>() {
|
||||
if let Some(aggregate) = node.downcast_ref::<AggregateExec>() {
|
||||
if aggregate.group_expr().is_empty()
|
||||
&& aggregate.aggr_expr().len() == 1
|
||||
&& is_count_rows_aggregate(&aggregate.aggr_expr()[0])
|
||||
|
||||
+5
-5
@@ -82,7 +82,7 @@ impl<'n> TreeNodeVisitor<'n> for SimpleDistinctVisitor {
|
||||
type Node = Arc<dyn ExecutionPlan>;
|
||||
|
||||
fn f_down(&mut self, node: &'n Self::Node) -> Result<TreeNodeRecursion> {
|
||||
if let Some(sort_merge) = node.as_any().downcast_ref::<SortPreservingMergeExec>() {
|
||||
if let Some(sort_merge) = node.downcast_ref::<SortPreservingMergeExec>() {
|
||||
if let Some(fetch) = sort_merge.fetch()
|
||||
&& fetch > 0
|
||||
&& sort_merge.expr().len() == 1
|
||||
@@ -99,7 +99,7 @@ impl<'n> TreeNodeVisitor<'n> for SimpleDistinctVisitor {
|
||||
// if not simple distinct, stop visiting
|
||||
self.simple_distinct = None;
|
||||
return Ok(TreeNodeRecursion::Stop);
|
||||
} else if let Some(aggregate) = node.as_any().downcast_ref::<AggregateExec>() {
|
||||
} else if let Some(aggregate) = node.downcast_ref::<AggregateExec>() {
|
||||
// check if the group by is simple distinct,
|
||||
// only one group by field, no aggregate function
|
||||
if aggregate.group_expr().expr().len() == 1
|
||||
@@ -122,7 +122,7 @@ impl<'n> TreeNodeVisitor<'n> for SimpleDistinctVisitor {
|
||||
// if not simple distinct, stop visiting
|
||||
self.simple_distinct = None;
|
||||
return Ok(TreeNodeRecursion::Stop);
|
||||
} else if let Some(projection) = node.as_any().downcast_ref::<ProjectionExec>() {
|
||||
} else if let Some(projection) = node.downcast_ref::<ProjectionExec>() {
|
||||
// Check ProjectionExec for the structure: [index_field]
|
||||
let exprs = projection.expr();
|
||||
if exprs.len() == 1 {
|
||||
@@ -133,7 +133,7 @@ impl<'n> TreeNodeVisitor<'n> for SimpleDistinctVisitor {
|
||||
// If projection doesn't have exactly 2 expressions, stop visiting
|
||||
self.simple_distinct = None;
|
||||
return Ok(TreeNodeRecursion::Stop);
|
||||
} else if let Some(filter) = node.as_any().downcast_ref::<FilterExec>() {
|
||||
} else if let Some(filter) = node.downcast_ref::<FilterExec>() {
|
||||
let predicate = filter.predicate();
|
||||
let exprs = split_conjunction(predicate);
|
||||
if exprs.len() == 2 && is_only_timestamp_filter(&exprs) {
|
||||
@@ -159,7 +159,7 @@ impl<'n> TreeNodeVisitor<'n> for SimpleDistinctVisitor {
|
||||
}
|
||||
|
||||
fn is_simple_str_match(expr: &Arc<dyn PhysicalExpr>) -> Option<String> {
|
||||
if let Some(func) = expr.as_any().downcast_ref::<ScalarFunctionExpr>()
|
||||
if let Some(func) = expr.downcast_ref::<ScalarFunctionExpr>()
|
||||
&& func.fun().name().to_lowercase() == "str_match"
|
||||
&& func.args().len() == 2
|
||||
{
|
||||
|
||||
+9
-9
@@ -86,7 +86,7 @@ impl<'n> TreeNodeVisitor<'n> for SimpleHistogramVisitor {
|
||||
type Node = Arc<dyn ExecutionPlan>;
|
||||
|
||||
fn f_down(&mut self, node: &'n Self::Node) -> Result<TreeNodeRecursion> {
|
||||
if let Some(aggregate) = node.as_any().downcast_ref::<AggregateExec>() {
|
||||
if let Some(aggregate) = node.downcast_ref::<AggregateExec>() {
|
||||
// Check if the AggregateExec matches SimpleHistogram pattern
|
||||
if aggregate.group_expr().expr().len() == 1
|
||||
&& aggregate.aggr_expr().len() == 1
|
||||
@@ -118,7 +118,7 @@ impl<'n> TreeNodeVisitor<'n> for SimpleHistogramVisitor {
|
||||
// If AggregateExec doesn't match SimpleHistogram pattern, stop visiting
|
||||
self.simple_histogram = None;
|
||||
return Ok(TreeNodeRecursion::Stop);
|
||||
} else if let Some(projection) = node.as_any().downcast_ref::<ProjectionExec>() {
|
||||
} else if let Some(projection) = node.downcast_ref::<ProjectionExec>() {
|
||||
// Check ProjectionExec for the structure: [histogram(_timestamp), count(*)]
|
||||
let exprs = projection.expr();
|
||||
if exprs.len() == 2 {
|
||||
@@ -139,7 +139,7 @@ impl<'n> TreeNodeVisitor<'n> for SimpleHistogramVisitor {
|
||||
}
|
||||
|
||||
fn get_data_bin(expr: &Arc<dyn PhysicalExpr>) -> Option<&ScalarFunctionExpr> {
|
||||
if let Some(func) = expr.as_any().downcast_ref::<ScalarFunctionExpr>()
|
||||
if let Some(func) = expr.downcast_ref::<ScalarFunctionExpr>()
|
||||
&& func.fun().name().to_lowercase() == "date_bin"
|
||||
{
|
||||
Some(func)
|
||||
@@ -150,7 +150,7 @@ fn get_data_bin(expr: &Arc<dyn PhysicalExpr>) -> Option<&ScalarFunctionExpr> {
|
||||
|
||||
// unit: microseconds
|
||||
fn get_histogram_interval(expr: &Arc<dyn PhysicalExpr>) -> Option<u64> {
|
||||
let interval = expr.as_any().downcast_ref::<Literal>()?.value();
|
||||
let interval = expr.downcast_ref::<Literal>()?.value();
|
||||
match interval {
|
||||
ScalarValue::IntervalMonthDayNano(Some(interval)) => {
|
||||
// convert interval to nanoseconds
|
||||
@@ -168,16 +168,16 @@ fn get_histogram_interval(expr: &Arc<dyn PhysicalExpr>) -> Option<u64> {
|
||||
/// `to_timestamp_micros(_timestamp + offset)` — the shape histogram() with a timezone
|
||||
/// rewrites to — yields the offset. None when the source is not the timestamp column.
|
||||
fn get_timestamp_offset(expr: &Arc<dyn PhysicalExpr>) -> Option<i64> {
|
||||
let func = expr.as_any().downcast_ref::<ScalarFunctionExpr>()?;
|
||||
let func = expr.downcast_ref::<ScalarFunctionExpr>()?;
|
||||
let arg = func.args().first()?;
|
||||
if get_column_name(arg) == TIMESTAMP_COL_NAME {
|
||||
return Some(0);
|
||||
}
|
||||
let bin = arg.as_any().downcast_ref::<BinaryExpr>()?;
|
||||
let bin = arg.downcast_ref::<BinaryExpr>()?;
|
||||
if *bin.op() != Operator::Plus || get_column_name(bin.left()) != TIMESTAMP_COL_NAME {
|
||||
return None;
|
||||
}
|
||||
match bin.right().as_any().downcast_ref::<Literal>()?.value() {
|
||||
match bin.right().downcast_ref::<Literal>()?.value() {
|
||||
ScalarValue::Int64(Some(ts_offset)) => Some(*ts_offset),
|
||||
_ => None,
|
||||
}
|
||||
@@ -239,7 +239,7 @@ impl<'n> TreeNodeVisitor<'n> for SimpleMultiHistogramVisitor {
|
||||
type Node = Arc<dyn ExecutionPlan>;
|
||||
|
||||
fn f_down(&mut self, node: &'n Self::Node) -> Result<TreeNodeRecursion> {
|
||||
if let Some(aggregate) = node.as_any().downcast_ref::<AggregateExec>() {
|
||||
if let Some(aggregate) = node.downcast_ref::<AggregateExec>() {
|
||||
// Exactly 2 group-by expressions (histogram + breakdown) and 1 aggregate (count(*))
|
||||
if aggregate.group_expr().expr().len() == 2
|
||||
&& aggregate.aggr_expr().len() == 1
|
||||
@@ -285,7 +285,7 @@ impl<'n> TreeNodeVisitor<'n> for SimpleMultiHistogramVisitor {
|
||||
// If AggregateExec doesn't match, stop visiting
|
||||
self.simple_multi_histogram = None;
|
||||
return Ok(TreeNodeRecursion::Stop);
|
||||
} else if let Some(projection) = node.as_any().downcast_ref::<ProjectionExec>() {
|
||||
} else if let Some(projection) = node.downcast_ref::<ProjectionExec>() {
|
||||
// Projection should have 3 expressions: timestamp, breakdown, count
|
||||
let exprs = projection.expr();
|
||||
if exprs.len() == 3 {
|
||||
|
||||
@@ -132,11 +132,7 @@ impl TreeNodeRewriter for FollowerIndexOptimizer {
|
||||
return Ok(Transformed::new(plan, false, TreeNodeRecursion::Stop));
|
||||
}
|
||||
|
||||
if plan
|
||||
.as_any()
|
||||
.downcast_ref::<SortPreservingMergeExec>()
|
||||
.is_some()
|
||||
{
|
||||
if plan.downcast_ref::<SortPreservingMergeExec>().is_some() {
|
||||
// Check for SimpleSelect
|
||||
if let Some(index_optimize_mode) = is_simple_select(Arc::clone(&plan)) {
|
||||
*self.index_optimizer_mode.lock() = Some(index_optimize_mode);
|
||||
@@ -158,7 +154,7 @@ impl TreeNodeRewriter for FollowerIndexOptimizer {
|
||||
}
|
||||
}
|
||||
return Ok(Transformed::new(plan, false, TreeNodeRecursion::Continue));
|
||||
} else if plan.as_any().downcast_ref::<AggregateExec>().is_some() {
|
||||
} else if plan.downcast_ref::<AggregateExec>().is_some() {
|
||||
// Check for SimpleCount
|
||||
if let Some(index_optimize_mode) = is_simple_count(Arc::clone(&plan)) {
|
||||
*self.index_optimizer_mode.lock() = Some(index_optimize_mode);
|
||||
@@ -242,11 +238,7 @@ impl TreeNodeRewriter for LeaderIndexOptimizer {
|
||||
return Ok(Transformed::new(plan, false, TreeNodeRecursion::Stop));
|
||||
}
|
||||
|
||||
if plan
|
||||
.as_any()
|
||||
.downcast_ref::<SortPreservingMergeExec>()
|
||||
.is_some()
|
||||
{
|
||||
if plan.downcast_ref::<SortPreservingMergeExec>().is_some() {
|
||||
// Get the index fields of the underlying table
|
||||
let mut visitor = TableNameVisitor::new();
|
||||
plan.visit(&mut visitor)?;
|
||||
@@ -298,7 +290,7 @@ impl TreeNodeRewriter for IndexOptimizerRewrite {
|
||||
type Node = Arc<dyn ExecutionPlan>;
|
||||
|
||||
fn f_up(&mut self, node: Arc<dyn ExecutionPlan>) -> Result<Transformed<Self::Node>> {
|
||||
if let Some(remote) = node.as_any().downcast_ref::<RemoteScanExec>() {
|
||||
if let Some(remote) = node.downcast_ref::<RemoteScanExec>() {
|
||||
let remote = Arc::new(
|
||||
remote
|
||||
.clone()
|
||||
@@ -327,7 +319,7 @@ impl<'n> TreeNodeVisitor<'n> for TableNameVisitor {
|
||||
fn f_up(&mut self, node: &'n Self::Node) -> Result<TreeNodeRecursion> {
|
||||
let name = node.name();
|
||||
if name == "NewEmptyExec" {
|
||||
let table = node.as_any().downcast_ref::<NewEmptyExec>().unwrap();
|
||||
let table = node.downcast_ref::<NewEmptyExec>().unwrap();
|
||||
self.table_name = Some(TableReference::from(table.name()));
|
||||
Ok(TreeNodeRecursion::Stop)
|
||||
} else {
|
||||
@@ -402,11 +394,7 @@ mod tests {
|
||||
let result = plan.rewrite(&mut rewriter).unwrap();
|
||||
assert!(result.transformed, "plan should be marked as transformed");
|
||||
assert_eq!(result.data.name(), "RemoteScanExec");
|
||||
let remote_scan = result
|
||||
.data
|
||||
.as_any()
|
||||
.downcast_ref::<RemoteScanExec>()
|
||||
.unwrap();
|
||||
let remote_scan = result.data.downcast_ref::<RemoteScanExec>().unwrap();
|
||||
assert_eq!(remote_scan.index_optimize_mode(), Some(mode));
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ impl<'n> TreeNodeVisitor<'n> for SimpleSelectVisitor {
|
||||
type Node = Arc<dyn ExecutionPlan>;
|
||||
|
||||
fn f_down(&mut self, node: &'n Self::Node) -> Result<TreeNodeRecursion> {
|
||||
if let Some(sort_merge) = node.as_any().downcast_ref::<SortPreservingMergeExec>() {
|
||||
if let Some(sort_merge) = node.downcast_ref::<SortPreservingMergeExec>() {
|
||||
if let Some(fetch) = sort_merge.fetch()
|
||||
&& fetch > 0
|
||||
&& sort_merge.expr().len() == 1
|
||||
|
||||
@@ -89,7 +89,7 @@ impl<'n> TreeNodeVisitor<'n> for SimpleTopnVisitor {
|
||||
type Node = Arc<dyn ExecutionPlan>;
|
||||
|
||||
fn f_down(&mut self, node: &'n Self::Node) -> Result<TreeNodeRecursion> {
|
||||
if let Some(sort_merge) = node.as_any().downcast_ref::<SortPreservingMergeExec>() {
|
||||
if let Some(sort_merge) = node.downcast_ref::<SortPreservingMergeExec>() {
|
||||
// need a fetch limit, a primary sort plus up to one secondary sort per group field
|
||||
let fetch = sort_merge.fetch().unwrap_or(0);
|
||||
let sort_exprs = sort_merge.expr();
|
||||
@@ -120,7 +120,7 @@ impl<'n> TreeNodeVisitor<'n> for SimpleTopnVisitor {
|
||||
|
||||
// fields are filled in at the AggregateExec
|
||||
self.simple_topn = Some((vec![], fetch, !first_sort.options.descending));
|
||||
} else if let Some(projection) = node.as_any().downcast_ref::<ProjectionExec>() {
|
||||
} else if let Some(projection) = node.downcast_ref::<ProjectionExec>() {
|
||||
// expect [field1, .., fieldN, count(*)]; the exact length is validated against
|
||||
// the group by at the AggregateExec
|
||||
let exprs = projection.expr();
|
||||
@@ -141,7 +141,7 @@ impl<'n> TreeNodeVisitor<'n> for SimpleTopnVisitor {
|
||||
{
|
||||
return self.reject();
|
||||
}
|
||||
} else if let Some(aggregate) = node.as_any().downcast_ref::<AggregateExec>() {
|
||||
} else if let Some(aggregate) = node.downcast_ref::<AggregateExec>() {
|
||||
let group_len = aggregate.group_expr().expr().len();
|
||||
if !(1..=MAX_SIMPLE_TOPN_FIELDS).contains(&group_len)
|
||||
|| aggregate.aggr_expr().len() != 1
|
||||
|
||||
@@ -83,7 +83,7 @@ pub(crate) mod tests {
|
||||
let mut visitor = AggregateVisitor::new();
|
||||
let _ = plan.visit(&mut visitor);
|
||||
let data = visitor.get_data();
|
||||
data.map(|v| v.as_any().downcast_ref::<AggregateExec>().unwrap().clone())
|
||||
data.map(|v| v.downcast_ref::<AggregateExec>().unwrap().clone())
|
||||
}
|
||||
|
||||
struct AggregateVisitor {
|
||||
@@ -111,7 +111,7 @@ pub(crate) mod tests {
|
||||
|
||||
fn f_up(&mut self, node: &'n Self::Node) -> Result<TreeNodeRecursion> {
|
||||
if node.name() == "AggregateExec" {
|
||||
let agg = node.as_any().downcast_ref::<AggregateExec>().unwrap();
|
||||
let agg = node.downcast_ref::<AggregateExec>().unwrap();
|
||||
if *agg.mode() == AggregateMode::Partial {
|
||||
self.data = Some(node.clone());
|
||||
Ok(TreeNodeRecursion::Stop)
|
||||
@@ -155,7 +155,7 @@ pub(crate) mod tests {
|
||||
|
||||
fn f_up(&mut self, node: &'n Self::Node) -> Result<TreeNodeRecursion> {
|
||||
if node.name() == "RemoteScanExec" {
|
||||
let remote_scan = node.as_any().downcast_ref::<RemoteScanExec>().unwrap();
|
||||
let remote_scan = node.downcast_ref::<RemoteScanExec>().unwrap();
|
||||
self.data.push(Arc::new(remote_scan.clone()));
|
||||
}
|
||||
Ok(TreeNodeRecursion::Continue)
|
||||
|
||||
@@ -62,7 +62,7 @@ impl PhysicalOptimizerRule for JoinReorderRule {
|
||||
}
|
||||
|
||||
fn swap_join_order(plan: Arc<dyn ExecutionPlan>) -> Result<Transformed<Arc<dyn ExecutionPlan>>> {
|
||||
if let Some(hash_join) = plan.as_any().downcast_ref::<HashJoinExec>() {
|
||||
if let Some(hash_join) = plan.downcast_ref::<HashJoinExec>() {
|
||||
let left = hash_join.left();
|
||||
let right = hash_join.right();
|
||||
|
||||
|
||||
@@ -273,7 +273,7 @@ impl TreeNodeRewriter for RemoteScanRewriter {
|
||||
// For sort, we should add a SortPreservingMergeExec
|
||||
if child.name() == "SortExec" {
|
||||
let table_name = visitor.table_name.clone().unwrap();
|
||||
let sort = child.as_any().downcast_ref::<SortExec>().unwrap();
|
||||
let sort = child.downcast_ref::<SortExec>().unwrap();
|
||||
let sort_merge = Arc::new(
|
||||
SortPreservingMergeExec::new(
|
||||
LexOrdering::new(sort.expr().to_vec()).unwrap(),
|
||||
@@ -338,7 +338,7 @@ fn wrap_partial_reduce(
|
||||
if !partial_reduce_enabled {
|
||||
return Ok(input);
|
||||
}
|
||||
let Some(agg) = input.as_any().downcast_ref::<AggregateExec>() else {
|
||||
let Some(agg) = input.downcast_ref::<AggregateExec>() else {
|
||||
return Ok(input);
|
||||
};
|
||||
if *agg.mode() != AggregateMode::Partial {
|
||||
@@ -429,7 +429,7 @@ impl<'n> TreeNodeVisitor<'n> for TableNameVisitor {
|
||||
self.has_remote_scan = true;
|
||||
Ok(TreeNodeRecursion::Stop)
|
||||
} else if name == "NewEmptyExec" {
|
||||
let table = node.as_any().downcast_ref::<NewEmptyExec>().unwrap();
|
||||
let table = node.downcast_ref::<NewEmptyExec>().unwrap();
|
||||
self.table_name = Some(TableReference::from(table.name()));
|
||||
Ok(TreeNodeRecursion::Continue)
|
||||
} else {
|
||||
@@ -546,7 +546,7 @@ mod tests {
|
||||
)?;
|
||||
let input: Arc<dyn ExecutionPlan> = Arc::new(agg);
|
||||
let result = wrap_partial_reduce(true, input)?;
|
||||
let result_agg = result.as_any().downcast_ref::<AggregateExec>().unwrap();
|
||||
let result_agg = result.downcast_ref::<AggregateExec>().unwrap();
|
||||
assert_eq!(*result_agg.mode(), AggregateMode::Single);
|
||||
Ok(())
|
||||
}
|
||||
@@ -566,7 +566,7 @@ mod tests {
|
||||
)?;
|
||||
let input: Arc<dyn ExecutionPlan> = Arc::new(agg);
|
||||
let result = wrap_partial_reduce(true, input)?;
|
||||
let result_agg = result.as_any().downcast_ref::<AggregateExec>().unwrap();
|
||||
let result_agg = result.downcast_ref::<AggregateExec>().unwrap();
|
||||
assert_eq!(*result_agg.mode(), AggregateMode::PartialReduce);
|
||||
assert!(result_agg.group_expr().expr().is_empty());
|
||||
Ok(())
|
||||
@@ -592,13 +592,12 @@ mod tests {
|
||||
)?;
|
||||
let input: Arc<dyn ExecutionPlan> = Arc::new(agg);
|
||||
let result = wrap_partial_reduce(true, input)?;
|
||||
let result_agg = result.as_any().downcast_ref::<AggregateExec>().unwrap();
|
||||
let result_agg = result.downcast_ref::<AggregateExec>().unwrap();
|
||||
assert_eq!(*result_agg.mode(), AggregateMode::PartialReduce);
|
||||
assert_eq!(result_agg.group_expr().expr().len(), 1);
|
||||
let (expr, name) = &result_agg.group_expr().expr()[0];
|
||||
assert_eq!(name, "a");
|
||||
let col_expr = expr
|
||||
.as_any()
|
||||
.downcast_ref::<PhysicalColumn>()
|
||||
.expect("group expr should be a Column reference into partial output schema");
|
||||
assert_eq!(col_expr.name(), "a");
|
||||
@@ -638,7 +637,7 @@ mod tests {
|
||||
)?;
|
||||
let input: Arc<dyn ExecutionPlan> = Arc::new(agg);
|
||||
let result = wrap_partial_reduce(true, input)?;
|
||||
let result_agg = result.as_any().downcast_ref::<AggregateExec>().unwrap();
|
||||
let result_agg = result.downcast_ref::<AggregateExec>().unwrap();
|
||||
assert_eq!(*result_agg.mode(), AggregateMode::PartialReduce);
|
||||
// The critical invariant: input_schema must be the original scan schema so that
|
||||
// aggregate expression argument types (e.g. _timestamp:Int64) are reconstructed
|
||||
@@ -694,7 +693,7 @@ mod tests {
|
||||
)?;
|
||||
let input: Arc<dyn ExecutionPlan> = Arc::new(agg);
|
||||
let result = wrap_partial_reduce(true, input)?;
|
||||
let result_agg = result.as_any().downcast_ref::<AggregateExec>().unwrap();
|
||||
let result_agg = result.downcast_ref::<AggregateExec>().unwrap();
|
||||
assert_eq!(*result_agg.mode(), AggregateMode::PartialReduce);
|
||||
|
||||
let null_exprs = result_agg.group_expr().null_expr();
|
||||
@@ -702,7 +701,7 @@ mod tests {
|
||||
// Both must still be Literal(NULL), NOT remapped to Column refs.
|
||||
for (expr, _) in null_exprs {
|
||||
assert!(
|
||||
expr.as_any().is::<Literal>(),
|
||||
expr.is::<Literal>(),
|
||||
"null_expr must remain a Literal, not be replaced with a Column ref"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ impl TreeNodeRewriter for PlanRewriter {
|
||||
type Node = Arc<dyn ExecutionPlan>;
|
||||
|
||||
fn f_up(&mut self, plan: Self::Node) -> Result<Transformed<Self::Node>> {
|
||||
if let Some(filter) = plan.as_any().downcast_ref::<FilterExec>() {
|
||||
if let Some(filter) = plan.downcast_ref::<FilterExec>() {
|
||||
let predicate = filter.predicate();
|
||||
|
||||
// 1. Check if the predicate contains any match_all functions
|
||||
@@ -160,7 +160,7 @@ impl TreeNodeRewriter for ColumnIndexRewriter {
|
||||
|
||||
fn f_up(&mut self, expr: Self::Node) -> Result<Transformed<Self::Node>> {
|
||||
// Check if this is a Column expression
|
||||
if let Some(column) = expr.as_any().downcast_ref::<Column>() {
|
||||
if let Some(column) = expr.downcast_ref::<Column>() {
|
||||
let field_name = column.name();
|
||||
// Find the new index in the new schema
|
||||
if let Ok(new_index) = self.new_schema.index_of(field_name) {
|
||||
@@ -210,12 +210,9 @@ fn rewrite_match_all_physical(
|
||||
schema: SchemaRef,
|
||||
fields: &[(String, DataType)],
|
||||
) -> Result<Arc<dyn PhysicalExpr>> {
|
||||
let scalar_fn = expr
|
||||
.as_any()
|
||||
.downcast_ref::<ScalarFunctionExpr>()
|
||||
.ok_or_else(|| {
|
||||
DataFusionError::Internal("Expected ScalarFunctionExpr for match_all".to_string())
|
||||
})?;
|
||||
let scalar_fn = expr.downcast_ref::<ScalarFunctionExpr>().ok_or_else(|| {
|
||||
DataFusionError::Internal("Expected ScalarFunctionExpr for match_all".to_string())
|
||||
})?;
|
||||
|
||||
let name = scalar_fn.name();
|
||||
let args = scalar_fn.args();
|
||||
@@ -398,7 +395,7 @@ fn has_match_all_function(expr: &Arc<dyn PhysicalExpr>) -> bool {
|
||||
|
||||
// check if the expr is match_all function
|
||||
fn is_match_all_physical(expr: &Arc<dyn PhysicalExpr>) -> bool {
|
||||
if let Some(scalar_fn) = expr.as_any().downcast_ref::<ScalarFunctionExpr>() {
|
||||
if let Some(scalar_fn) = expr.downcast_ref::<ScalarFunctionExpr>() {
|
||||
let name = scalar_fn.name();
|
||||
name.to_lowercase() == MATCH_ALL_UDF_NAME
|
||||
|| name == FUZZY_MATCH_ALL_UDF_NAME
|
||||
@@ -428,7 +425,7 @@ impl TreeNodeRewriter for AddFstFieldsToProjection {
|
||||
type Node = Arc<dyn ExecutionPlan>;
|
||||
|
||||
fn f_up(&mut self, plan: Self::Node) -> Result<Transformed<Self::Node>> {
|
||||
if let Some(empty_exec) = plan.as_any().downcast_ref::<NewEmptyExec>() {
|
||||
if let Some(empty_exec) = plan.downcast_ref::<NewEmptyExec>() {
|
||||
let schema = empty_exec.full_schema();
|
||||
// used for read field from parquet file, because match_all function do not include
|
||||
// the field in argument, so when we rewrite match_all function, we need to add
|
||||
@@ -658,13 +655,13 @@ mod tests {
|
||||
|
||||
// Verify that all column indices in FilterExec match the input schema
|
||||
let _ = physical_plan.apply(&mut |node: &Arc<dyn ExecutionPlan>| -> Result<TreeNodeRecursion> {
|
||||
if let Some(filter) = node.as_any().downcast_ref::<FilterExec>() {
|
||||
if let Some(filter) = node.downcast_ref::<FilterExec>() {
|
||||
let input_schema = filter.input().schema();
|
||||
let predicate = filter.predicate();
|
||||
|
||||
// Traverse the predicate expression tree to find all Column references
|
||||
let _ = predicate.apply(&mut |expr: &Arc<dyn PhysicalExpr>| -> Result<TreeNodeRecursion> {
|
||||
if let Some(column) = expr.as_any().downcast_ref::<Column>() {
|
||||
if let Some(column) = expr.downcast_ref::<Column>() {
|
||||
let column_name = column.name();
|
||||
let column_index = column.index();
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn is_aggregate_exec(plan: &Arc<dyn ExecutionPlan>) -> bool {
|
||||
}
|
||||
|
||||
pub fn extract_string_literal(expr: &Arc<dyn PhysicalExpr>) -> Result<String> {
|
||||
if let Some(literal) = expr.as_any().downcast_ref::<Literal>() {
|
||||
if let Some(literal) = expr.downcast_ref::<Literal>() {
|
||||
match literal.value() {
|
||||
ScalarValue::Utf8(Some(s)) => Ok(s.clone()),
|
||||
ScalarValue::Utf8View(Some(s)) => Ok(s.to_string()),
|
||||
@@ -56,7 +56,7 @@ pub fn extract_string_literal(expr: &Arc<dyn PhysicalExpr>) -> Result<String> {
|
||||
}
|
||||
|
||||
pub fn extract_column(expr: &Arc<dyn PhysicalExpr>) -> Result<Column> {
|
||||
if let Some(column) = expr.as_any().downcast_ref::<Column>() {
|
||||
if let Some(column) = expr.downcast_ref::<Column>() {
|
||||
Ok(column.clone())
|
||||
} else {
|
||||
Err(DataFusionError::Internal(
|
||||
@@ -66,7 +66,7 @@ pub fn extract_column(expr: &Arc<dyn PhysicalExpr>) -> Result<Column> {
|
||||
}
|
||||
|
||||
pub fn extract_int64_literal(expr: &Arc<dyn PhysicalExpr>) -> Result<i64> {
|
||||
if let Some(literal) = expr.as_any().downcast_ref::<Literal>() {
|
||||
if let Some(literal) = expr.downcast_ref::<Literal>() {
|
||||
match literal.value() {
|
||||
ScalarValue::Int64(Some(s)) => Ok(*s),
|
||||
_ => Err(DataFusionError::Internal(format!(
|
||||
@@ -100,9 +100,9 @@ fn disjunction_opt(
|
||||
}
|
||||
|
||||
pub fn is_column(expr: &Arc<dyn PhysicalExpr>) -> bool {
|
||||
if expr.as_any().downcast_ref::<Column>().is_some() {
|
||||
if expr.downcast_ref::<Column>().is_some() {
|
||||
true
|
||||
} else if let Some(expr) = expr.as_any().downcast_ref::<CastExpr>() {
|
||||
} else if let Some(expr) = expr.downcast_ref::<CastExpr>() {
|
||||
is_column(expr.expr())
|
||||
} else {
|
||||
false
|
||||
@@ -110,9 +110,9 @@ pub fn is_column(expr: &Arc<dyn PhysicalExpr>) -> bool {
|
||||
}
|
||||
|
||||
pub fn get_column_name(expr: &Arc<dyn PhysicalExpr>) -> &str {
|
||||
if let Some(expr) = expr.as_any().downcast_ref::<Column>() {
|
||||
if let Some(expr) = expr.downcast_ref::<Column>() {
|
||||
expr.name()
|
||||
} else if let Some(expr) = expr.as_any().downcast_ref::<CastExpr>() {
|
||||
} else if let Some(expr) = expr.downcast_ref::<CastExpr>() {
|
||||
get_column_name(expr.expr())
|
||||
} else {
|
||||
UNKNOWN_NAME
|
||||
@@ -133,7 +133,7 @@ pub fn is_count_rows_aggregate(expr: &AggregateFunctionExpr) -> bool {
|
||||
}
|
||||
|
||||
pub fn is_value(expr: &Arc<dyn PhysicalExpr>) -> bool {
|
||||
expr.as_any().downcast_ref::<Literal>().is_some()
|
||||
expr.downcast_ref::<Literal>().is_some()
|
||||
}
|
||||
|
||||
pub fn is_only_timestamp_filter(expr: &[&Arc<dyn PhysicalExpr>]) -> bool {
|
||||
@@ -141,7 +141,7 @@ pub fn is_only_timestamp_filter(expr: &[&Arc<dyn PhysicalExpr>]) -> bool {
|
||||
}
|
||||
|
||||
fn is_timestamp_filter(expr: &Arc<dyn PhysicalExpr>) -> bool {
|
||||
if let Some(expr) = expr.as_any().downcast_ref::<BinaryExpr>() {
|
||||
if let Some(expr) = expr.downcast_ref::<BinaryExpr>() {
|
||||
match expr.op() {
|
||||
Operator::Gt | Operator::GtEq | Operator::Lt | Operator::LtEq => {
|
||||
let column = if is_value(expr.left()) && is_column(expr.right()) {
|
||||
@@ -286,9 +286,7 @@ mod tests {
|
||||
fn test_disjunction_empty_returns_true_literal() {
|
||||
let result = disjunction(vec![]);
|
||||
// empty → lit(true)
|
||||
let lit = result
|
||||
.as_any()
|
||||
.downcast_ref::<datafusion::physical_plan::expressions::Literal>();
|
||||
let lit = result.downcast_ref::<datafusion::physical_plan::expressions::Literal>();
|
||||
assert!(lit.is_some());
|
||||
}
|
||||
|
||||
|
||||
@@ -287,15 +287,8 @@ impl TreeNodeRewriter for SortByTime {
|
||||
fn generate_table_source_with_sorted_by_time(
|
||||
table_source: Arc<dyn TableSource>,
|
||||
) -> Arc<dyn TableSource> {
|
||||
let source: &DefaultTableSource = table_source
|
||||
.as_any()
|
||||
.downcast_ref::<DefaultTableSource>()
|
||||
.unwrap();
|
||||
if let Some(table_provider) = source
|
||||
.table_provider
|
||||
.as_any()
|
||||
.downcast_ref::<NewEmptyTable>()
|
||||
{
|
||||
let source: &DefaultTableSource = table_source.downcast_ref::<DefaultTableSource>().unwrap();
|
||||
if let Some(table_provider) = source.table_provider.downcast_ref::<NewEmptyTable>() {
|
||||
let mut new_table_provider = (*table_provider).clone();
|
||||
new_table_provider.sorted_by_time = true;
|
||||
let new_source = DefaultTableSource::new(Arc::new(new_table_provider));
|
||||
|
||||
@@ -13,9 +13,12 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
use std::{
|
||||
fmt,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use datafusion::execution::memory_pool::{
|
||||
@@ -51,6 +54,10 @@ impl PeakMemoryPool {
|
||||
}
|
||||
|
||||
impl MemoryPool for PeakMemoryPool {
|
||||
fn name(&self) -> &str {
|
||||
"PeakMemoryPool"
|
||||
}
|
||||
|
||||
fn register(&self, consumer: &MemoryConsumer) {
|
||||
self.inner.register(consumer);
|
||||
}
|
||||
@@ -91,6 +98,12 @@ impl MemoryPool for PeakMemoryPool {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PeakMemoryPool {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}({})", self.name(), self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PeakMemoryPool {
|
||||
fn drop(&mut self) {
|
||||
let peak = self.peak_memory.load(Ordering::Relaxed);
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
any::Any,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
@@ -111,10 +110,6 @@ impl ExecutionPlan for DeduplicationExec {
|
||||
"DeduplicationExec"
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn properties(&self) -> &Arc<PlanProperties> {
|
||||
&self.cache
|
||||
}
|
||||
@@ -161,7 +156,7 @@ impl ExecutionPlan for DeduplicationExec {
|
||||
)))
|
||||
}
|
||||
|
||||
fn partition_statistics(&self, partition: Option<usize>) -> Result<Statistics> {
|
||||
fn partition_statistics(&self, partition: Option<usize>) -> Result<Arc<Statistics>> {
|
||||
self.input.partition_statistics(partition)
|
||||
}
|
||||
|
||||
@@ -455,7 +450,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_deduplication_exec_as_any() {
|
||||
let exec = make_exec();
|
||||
assert!(exec.as_any().downcast_ref::<DeduplicationExec>().is_some());
|
||||
let exec_plan: &dyn ExecutionPlan = &exec;
|
||||
assert!(exec_plan.downcast_ref::<DeduplicationExec>().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{any::Any, collections::HashSet, sync::Arc};
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
|
||||
use arrow::array::{
|
||||
Array, Int64Array, TimestampMicrosecondArray, TimestampNanosecondArray, UInt64Array,
|
||||
@@ -114,10 +114,6 @@ impl ExecutionPlan for TantivyOptimizeExec {
|
||||
"TantivyOptimizeExec"
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn properties(&self) -> &Arc<PlanProperties> {
|
||||
&self.cache
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ use std::{
|
||||
use config::metrics;
|
||||
use dashmap::DashMap;
|
||||
use datafusion::{
|
||||
common::Statistics,
|
||||
common::{Statistics, TableReference},
|
||||
execution::cache::{
|
||||
CacheAccessor,
|
||||
cache_manager::{CachedFileMetadata, FileStatisticsCacheEntry},
|
||||
CacheAccessor, TableScopedPath,
|
||||
cache_manager::{self, CachedFileMetadata, FileStatisticsCacheEntry},
|
||||
},
|
||||
};
|
||||
use object_store::{ObjectMeta, path::Path};
|
||||
@@ -145,10 +145,10 @@ impl Default for FileStatisticsCache {
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheAccessor<Path, CachedFileMetadata> for FileStatisticsCache {
|
||||
impl CacheAccessor<TableScopedPath, CachedFileMetadata> for FileStatisticsCache {
|
||||
/// Get cached metadata for file location.
|
||||
fn get(&self, k: &Path) -> Option<CachedFileMetadata> {
|
||||
let k = self.format_key(k);
|
||||
fn get(&self, k: &TableScopedPath) -> Option<CachedFileMetadata> {
|
||||
let k = self.format_key(&k.path);
|
||||
match self.statistics.get(&k) {
|
||||
Some(s) => {
|
||||
metrics::QUERY_PARQUET_METADATA_CACHE_HITS_TOTAL
|
||||
@@ -171,8 +171,8 @@ impl CacheAccessor<Path, CachedFileMetadata> for FileStatisticsCache {
|
||||
}
|
||||
|
||||
/// Save collected file statistics
|
||||
fn put(&self, k: &Path, value: CachedFileMetadata) -> Option<CachedFileMetadata> {
|
||||
let k = self.format_key(k);
|
||||
fn put(&self, k: &TableScopedPath, value: CachedFileMetadata) -> Option<CachedFileMetadata> {
|
||||
let k = self.format_key(&k.path);
|
||||
let entry_size = Self::estimate_entry_size(&k, &value.meta, &value.statistics);
|
||||
|
||||
let old = self.statistics.insert(
|
||||
@@ -201,8 +201,8 @@ impl CacheAccessor<Path, CachedFileMetadata> for FileStatisticsCache {
|
||||
old.map(|(meta, stats, _)| CachedFileMetadata::new(meta, stats, None))
|
||||
}
|
||||
|
||||
fn remove(&self, k: &Path) -> Option<CachedFileMetadata> {
|
||||
let k = self.format_key(k);
|
||||
fn remove(&self, k: &TableScopedPath) -> Option<CachedFileMetadata> {
|
||||
let k = self.format_key(&k.path);
|
||||
self.statistics.remove(&k).map(|(_, (meta, stats, size))| {
|
||||
self.current_memory
|
||||
.fetch_sub(size as i64, Ordering::Relaxed);
|
||||
@@ -210,8 +210,8 @@ impl CacheAccessor<Path, CachedFileMetadata> for FileStatisticsCache {
|
||||
})
|
||||
}
|
||||
|
||||
fn contains_key(&self, k: &Path) -> bool {
|
||||
let k = self.format_key(k);
|
||||
fn contains_key(&self, k: &TableScopedPath) -> bool {
|
||||
let k = self.format_key(&k.path);
|
||||
self.statistics.contains_key(&k)
|
||||
}
|
||||
|
||||
@@ -224,17 +224,34 @@ impl CacheAccessor<Path, CachedFileMetadata> for FileStatisticsCache {
|
||||
self.cacher.lock().clear();
|
||||
self.current_memory.store(0, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
"FileStatisticsCache".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl datafusion::execution::cache::cache_manager::FileStatisticsCache for FileStatisticsCache {
|
||||
fn list_entries(&self) -> HashMap<Path, FileStatisticsCacheEntry> {
|
||||
let mut entries = HashMap::<Path, FileStatisticsCacheEntry>::new();
|
||||
impl cache_manager::FileStatisticsCache for FileStatisticsCache {
|
||||
fn cache_limit(&self) -> usize {
|
||||
config::get_config()
|
||||
.limit
|
||||
.datafusion_file_stat_cache_max_size
|
||||
}
|
||||
|
||||
fn update_cache_limit(&self, _limit: usize) {
|
||||
// No-op: this is a process-wide, long-lived global cache whose eviction
|
||||
// budget is owned solely by `put()` via `datafusion_file_stat_cache_max_size`.
|
||||
// DataFusion calls this once per `CacheManager::try_new` (i.e. per session /
|
||||
// per query) with its own per-session limit;
|
||||
}
|
||||
|
||||
fn list_entries(&self) -> HashMap<TableScopedPath, FileStatisticsCacheEntry> {
|
||||
let mut entries = HashMap::<TableScopedPath, FileStatisticsCacheEntry>::new();
|
||||
|
||||
for entry in &self.statistics {
|
||||
let path = Path::from(entry.key().as_str());
|
||||
let path = TableScopedPath {
|
||||
table: None,
|
||||
path: Path::from(entry.key().as_str()),
|
||||
};
|
||||
let (object_meta, stats, _) = entry.value();
|
||||
entries.insert(
|
||||
path,
|
||||
@@ -251,86 +268,102 @@ impl datafusion::execution::cache::cache_manager::FileStatisticsCache for FileSt
|
||||
|
||||
entries
|
||||
}
|
||||
|
||||
fn drop_table_entries(
|
||||
&self,
|
||||
_table_ref: &Option<TableReference>,
|
||||
) -> datafusion::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::DateTime;
|
||||
use chrono::{DateTime, Utc};
|
||||
use datafusion::arrow::datatypes::{DataType, Field, Schema, TimeUnit};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_file_statistics_cache() {
|
||||
let meta = ObjectMeta {
|
||||
location: Path::from("test"),
|
||||
last_modified: DateTime::parse_from_rfc3339("2022-09-27T22:36:00+02:00")
|
||||
.unwrap()
|
||||
.into(),
|
||||
size: 1024,
|
||||
/// Wrap a [`Path`] into the table-scoped key the cache API expects.
|
||||
fn key(location: &Path) -> TableScopedPath {
|
||||
TableScopedPath {
|
||||
table: None,
|
||||
path: location.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse an RFC3339 timestamp, panicking on malformed input (test-only).
|
||||
fn ts(s: &str) -> DateTime<Utc> {
|
||||
DateTime::parse_from_rfc3339(s).unwrap().into()
|
||||
}
|
||||
|
||||
/// Minimal [`ObjectMeta`] for a location, fixed `last_modified`, no etag/version.
|
||||
fn object_meta(location: &str, size: u64) -> ObjectMeta {
|
||||
ObjectMeta {
|
||||
location: Path::from(location),
|
||||
last_modified: ts("2024-01-15T00:00:00+00:00"),
|
||||
size,
|
||||
e_tag: None,
|
||||
version: None,
|
||||
};
|
||||
let cache = FileStatisticsCache::default();
|
||||
assert!(cache.get(&meta.location).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
let stats: Arc<Statistics> = Statistics::new_unknown(&Schema::new(vec![Field::new(
|
||||
/// Unknown statistics for the given fields, ready to cache.
|
||||
fn unknown_stats(fields: Vec<Field>) -> Arc<Statistics> {
|
||||
Statistics::new_unknown(&Schema::new(fields)).into()
|
||||
}
|
||||
|
||||
/// Insert an entry into the cache under its own location.
|
||||
fn put(cache: &FileStatisticsCache, meta: ObjectMeta, stats: Arc<Statistics>) {
|
||||
cache.put(
|
||||
&key(&meta.location),
|
||||
CachedFileMetadata::new(meta, stats, None),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_statistics_cache() {
|
||||
let meta = object_meta("test", 1024);
|
||||
let cache = FileStatisticsCache::default();
|
||||
assert!(cache.get(&key(&meta.location)).is_none());
|
||||
|
||||
let stats = unknown_stats(vec![Field::new(
|
||||
"test_column",
|
||||
DataType::Timestamp(TimeUnit::Second, None),
|
||||
false,
|
||||
)]))
|
||||
.into();
|
||||
cache.put(
|
||||
&meta.location,
|
||||
CachedFileMetadata::new(meta.clone(), stats, None),
|
||||
);
|
||||
let cached = cache.get(&meta.location);
|
||||
assert!(cached.is_some());
|
||||
assert!(cached.unwrap().is_valid_for(&meta));
|
||||
)]);
|
||||
put(&cache, meta.clone(), stats);
|
||||
|
||||
// file size changed
|
||||
let mut meta2 = meta.clone();
|
||||
meta2.size = 2048;
|
||||
let cached = cache.get(&meta2.location);
|
||||
assert!(cached.is_some());
|
||||
assert!(!cached.unwrap().is_valid_for(&meta2));
|
||||
// exact match is valid
|
||||
let cached = cache.get(&key(&meta.location)).expect("entry present");
|
||||
assert!(cached.is_valid_for(&meta));
|
||||
|
||||
// file last_modified changed
|
||||
let mut meta2 = meta.clone();
|
||||
meta2.last_modified = DateTime::parse_from_rfc3339("2022-09-27T22:40:00+02:00")
|
||||
.unwrap()
|
||||
.into();
|
||||
let cached = cache.get(&meta2.location);
|
||||
assert!(cached.is_some());
|
||||
assert!(!cached.unwrap().is_valid_for(&meta2));
|
||||
// same location but file size changed -> cached but stale
|
||||
let mut changed = meta.clone();
|
||||
changed.size = 2048;
|
||||
let cached = cache.get(&key(&changed.location)).expect("entry present");
|
||||
assert!(!cached.is_valid_for(&changed));
|
||||
|
||||
// different file
|
||||
let mut meta2 = meta;
|
||||
meta2.location = Path::from("test2");
|
||||
assert!(cache.get(&meta2.location).is_none());
|
||||
// same location but last_modified changed -> cached but stale
|
||||
let mut changed = meta.clone();
|
||||
changed.last_modified = ts("2024-01-15T01:00:00+00:00");
|
||||
let cached = cache.get(&key(&changed.location)).expect("entry present");
|
||||
assert!(!cached.is_valid_for(&changed));
|
||||
|
||||
// different location -> miss
|
||||
assert!(cache.get(&key(&Path::from("test2"))).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_size_calculation() {
|
||||
let cache = FileStatisticsCache::new();
|
||||
assert_eq!(cache.memory_size(), 0, "empty cache tracks zero memory");
|
||||
|
||||
// Empty cache tracks zero memory
|
||||
let empty_size = cache.memory_size();
|
||||
assert_eq!(empty_size, 0);
|
||||
|
||||
// Add some entries
|
||||
for i in 0..10 {
|
||||
let meta = ObjectMeta {
|
||||
location: Path::from(format!("test_file_{i}")),
|
||||
last_modified: DateTime::parse_from_rfc3339("2022-09-27T22:36:00+02:00")
|
||||
.unwrap()
|
||||
.into(),
|
||||
size: 1024 * (i + 1),
|
||||
e_tag: Some(format!("etag_{i}")),
|
||||
version: Some(format!("v{i}")),
|
||||
};
|
||||
|
||||
let schema = Schema::new(vec![
|
||||
for i in 0..10u64 {
|
||||
let mut meta = object_meta(&format!("test_file_{i}"), 1024 * (i + 1));
|
||||
meta.e_tag = Some(format!("etag_{i}"));
|
||||
meta.version = Some(format!("v{i}"));
|
||||
let stats = unknown_stats(vec![
|
||||
Field::new("column1", DataType::Utf8, false),
|
||||
Field::new("column2", DataType::Int64, true),
|
||||
Field::new(
|
||||
@@ -339,84 +372,55 @@ mod tests {
|
||||
false,
|
||||
),
|
||||
]);
|
||||
|
||||
cache.put(
|
||||
&meta.location,
|
||||
CachedFileMetadata::new(
|
||||
meta.clone(),
|
||||
Statistics::new_unknown(&schema).into(),
|
||||
None,
|
||||
),
|
||||
);
|
||||
put(&cache, meta, stats);
|
||||
}
|
||||
|
||||
// Size should increase after adding entries
|
||||
let filled_size = cache.memory_size();
|
||||
assert!(filled_size > empty_size);
|
||||
|
||||
println!(
|
||||
"Cache with {} entries uses {} bytes",
|
||||
cache.len(),
|
||||
filled_size,
|
||||
assert!(
|
||||
cache.memory_size() > 0,
|
||||
"memory tracked after inserting entries"
|
||||
);
|
||||
assert_eq!(cache.len(), 10);
|
||||
|
||||
// After clearing, tracked memory must return to zero
|
||||
cache.clear();
|
||||
assert_eq!(cache.memory_size(), 0);
|
||||
assert_eq!(cache.memory_size(), 0, "cleared cache returns to zero");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_name() {
|
||||
let cache = FileStatisticsCache::new();
|
||||
assert_eq!(cache.name(), "FileStatisticsCache");
|
||||
assert_eq!(FileStatisticsCache::new().name(), "FileStatisticsCache");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_contains_key_and_remove() {
|
||||
let cache = FileStatisticsCache::new();
|
||||
let path = Path::from("test_file");
|
||||
let meta = ObjectMeta {
|
||||
location: path.clone(),
|
||||
last_modified: DateTime::parse_from_rfc3339("2024-01-15T00:00:00+00:00")
|
||||
.unwrap()
|
||||
.into(),
|
||||
size: 512,
|
||||
e_tag: None,
|
||||
version: None,
|
||||
};
|
||||
let schema = Schema::new(vec![Field::new("col", DataType::Utf8, false)]);
|
||||
let stats: Arc<Statistics> = Statistics::new_unknown(&schema).into();
|
||||
let meta = object_meta("test_file", 512);
|
||||
let k = key(&meta.location);
|
||||
|
||||
assert!(!cache.contains_key(&path));
|
||||
assert!(!cache.contains_key(&k));
|
||||
|
||||
cache.put(&path, CachedFileMetadata::new(meta.clone(), stats, None));
|
||||
assert!(cache.contains_key(&path));
|
||||
put(
|
||||
&cache,
|
||||
meta,
|
||||
unknown_stats(vec![Field::new("col", DataType::Utf8, false)]),
|
||||
);
|
||||
assert!(cache.contains_key(&k));
|
||||
assert_eq!(cache.len(), 1);
|
||||
|
||||
let removed = cache.remove(&path);
|
||||
assert!(removed.is_some());
|
||||
assert!(!cache.contains_key(&path));
|
||||
assert!(cache.remove(&k).is_some());
|
||||
assert!(!cache.contains_key(&k));
|
||||
assert_eq!(cache.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_clear() {
|
||||
let cache = FileStatisticsCache::new();
|
||||
let schema = Schema::new(vec![Field::new("col", DataType::Int64, false)]);
|
||||
|
||||
for i in 0..3 {
|
||||
let path = Path::from(format!("file_{i}"));
|
||||
let meta = ObjectMeta {
|
||||
location: path.clone(),
|
||||
last_modified: DateTime::parse_from_rfc3339("2024-01-15T00:00:00+00:00")
|
||||
.unwrap()
|
||||
.into(),
|
||||
size: 100 * (i + 1),
|
||||
e_tag: None,
|
||||
version: None,
|
||||
};
|
||||
let stats: Arc<Statistics> = Statistics::new_unknown(&schema).into();
|
||||
cache.put(&path, CachedFileMetadata::new(meta, stats, None));
|
||||
for i in 0..3u64 {
|
||||
let meta = object_meta(&format!("file_{i}"), 100 * (i + 1));
|
||||
put(
|
||||
&cache,
|
||||
meta,
|
||||
unknown_stats(vec![Field::new("col", DataType::Int64, false)]),
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(cache.len(), 3);
|
||||
@@ -428,34 +432,25 @@ mod tests {
|
||||
fn test_list_entries() {
|
||||
use datafusion::execution::cache::cache_manager::FileStatisticsCache as FscTrait;
|
||||
|
||||
let cache = super::FileStatisticsCache::new();
|
||||
let path = Path::from("list_test");
|
||||
let meta = ObjectMeta {
|
||||
location: path.clone(),
|
||||
last_modified: DateTime::parse_from_rfc3339("2024-01-15T00:00:00+00:00")
|
||||
.unwrap()
|
||||
.into(),
|
||||
size: 256,
|
||||
e_tag: None,
|
||||
version: None,
|
||||
};
|
||||
let schema = Schema::new(vec![Field::new("col", DataType::Utf8, false)]);
|
||||
let stats: Arc<Statistics> = Statistics::new_unknown(&schema).into();
|
||||
let cache = FileStatisticsCache::new();
|
||||
let meta = object_meta("list_test", 256);
|
||||
let k = key(&meta.location);
|
||||
|
||||
let entries_before = FscTrait::list_entries(&cache);
|
||||
assert!(entries_before.is_empty());
|
||||
assert!(FscTrait::list_entries(&cache).is_empty());
|
||||
|
||||
cache.put(&path, CachedFileMetadata::new(meta, stats, None));
|
||||
put(
|
||||
&cache,
|
||||
meta,
|
||||
unknown_stats(vec![Field::new("col", DataType::Utf8, false)]),
|
||||
);
|
||||
let entries = FscTrait::list_entries(&cache);
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert!(entries.contains_key(&path));
|
||||
assert!(entries.contains_key(&k));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_remove_nonexistent_returns_none() {
|
||||
let cache = FileStatisticsCache::new();
|
||||
let path = Path::from("does_not_exist");
|
||||
let removed = cache.remove(&path);
|
||||
assert!(removed.is_none());
|
||||
assert!(cache.remove(&key(&Path::from("does_not_exist"))).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
any::Any,
|
||||
collections::HashMap,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
@@ -40,10 +39,6 @@ impl StreamTypeProvider {
|
||||
|
||||
#[async_trait]
|
||||
impl SchemaProvider for StreamTypeProvider {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn table_names(&self) -> Vec<String> {
|
||||
let tables = self.tables.read().unwrap();
|
||||
tables.keys().cloned().collect::<Vec<_>>()
|
||||
@@ -104,9 +99,6 @@ mod tests {
|
||||
|
||||
#[async_trait]
|
||||
impl TableProvider for DummyTable {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
fn schema(&self) -> SchemaRef {
|
||||
self.0.clone()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{any::Any, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use datafusion::{
|
||||
@@ -64,10 +64,6 @@ impl NewEmptyTable {
|
||||
|
||||
#[async_trait]
|
||||
impl TableProvider for NewEmptyTable {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn schema(&self) -> SchemaRef {
|
||||
self.schema.clone()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{any::Any, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use config::TIMESTAMP_COL_NAME;
|
||||
@@ -67,10 +67,6 @@ impl EnrichTable {
|
||||
|
||||
#[async_trait]
|
||||
impl TableProvider for EnrichTable {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn schema(&self) -> SchemaRef {
|
||||
self.schema.clone()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{any::Any, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_schema::{SchemaRef, SortOptions};
|
||||
use config::{TIMESTAMP_COL_NAME, get_config};
|
||||
@@ -74,10 +74,6 @@ impl ListingTableAdapter {
|
||||
|
||||
#[async_trait]
|
||||
impl TableProvider for ListingTableAdapter {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn schema(&self) -> SchemaRef {
|
||||
Arc::clone(&self.listing_table.schema())
|
||||
}
|
||||
@@ -188,10 +184,9 @@ fn handler_tantivy_index(
|
||||
order_by_time_desc: bool,
|
||||
target_partitions: usize,
|
||||
) -> Arc<dyn ExecutionPlan> {
|
||||
if let Some(data_source_exec) = plan.as_any().downcast_ref::<DataSourceExec>()
|
||||
if let Some(data_source_exec) = plan.downcast_ref::<DataSourceExec>()
|
||||
&& let Some(config) = data_source_exec
|
||||
.data_source()
|
||||
.as_any()
|
||||
.downcast_ref::<FileScanConfig>()
|
||||
{
|
||||
let mut file_groups = config.file_groups.clone();
|
||||
@@ -250,7 +245,7 @@ fn handler_tantivy_index(
|
||||
.into_iter()
|
||||
.map(|mut file| {
|
||||
if let Some(access_plan) = generate_access_plan(&file) {
|
||||
file = file.with_extensions(access_plan);
|
||||
file = file.with_extension(access_plan);
|
||||
}
|
||||
file
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{any::Any, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow::array::RecordBatch;
|
||||
use arrow_schema::{DataType, SchemaRef, SortOptions};
|
||||
@@ -120,10 +120,6 @@ mod tests {
|
||||
|
||||
#[async_trait]
|
||||
impl TableProvider for NewMemTable {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn schema(&self) -> SchemaRef {
|
||||
self.mem_table.schema()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{any::Any, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_schema::SchemaRef;
|
||||
use async_trait::async_trait;
|
||||
@@ -40,10 +40,6 @@ impl NewUnionTable {
|
||||
|
||||
#[async_trait]
|
||||
impl TableProvider for NewUnionTable {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn schema(&self) -> SchemaRef {
|
||||
Arc::clone(&self.schema)
|
||||
}
|
||||
|
||||
@@ -82,10 +82,6 @@ impl Default for SummaryPercentile {
|
||||
}
|
||||
|
||||
impl AggregateUDFImpl for SummaryPercentile {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
SUMMARY_PERCENTILE
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{any::Any, sync::LazyLock as Lazy};
|
||||
use std::sync::LazyLock as Lazy;
|
||||
|
||||
use arrow_schema::TimeUnit;
|
||||
use datafusion::{
|
||||
@@ -47,10 +47,6 @@ impl CastToTimestampUdf {
|
||||
}
|
||||
|
||||
impl ScalarUDFImpl for CastToTimestampUdf {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
CAST_TO_TIMESTAMP_UDF_NAME
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{any::Any, sync::LazyLock as Lazy};
|
||||
use std::sync::LazyLock as Lazy;
|
||||
|
||||
use arrow::datatypes::{DataType, DataType::Timestamp, TimeUnit::Microsecond};
|
||||
use datafusion::{
|
||||
@@ -40,10 +40,6 @@ impl HistogramUdf {
|
||||
}
|
||||
|
||||
impl ScalarUDFImpl for HistogramUdf {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
HISTOGRAM_UDF_NAME
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{any::Any, sync::LazyLock as Lazy};
|
||||
use std::sync::LazyLock as Lazy;
|
||||
|
||||
use arrow::datatypes::DataType;
|
||||
use datafusion::{
|
||||
@@ -44,10 +44,6 @@ impl MatchAllUdf {
|
||||
}
|
||||
|
||||
impl ScalarUDFImpl for MatchAllUdf {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
MATCH_ALL_UDF_NAME
|
||||
}
|
||||
@@ -87,10 +83,6 @@ impl FuzzyMatchAllUdf {
|
||||
}
|
||||
|
||||
impl ScalarUDFImpl for FuzzyMatchAllUdf {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
FUZZY_MATCH_ALL_UDF_NAME
|
||||
}
|
||||
|
||||
@@ -13,10 +13,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
any::Any,
|
||||
sync::{Arc, LazyLock as Lazy},
|
||||
};
|
||||
use std::sync::{Arc, LazyLock as Lazy};
|
||||
|
||||
use arrow::array::{Array, ArrayRef, GenericStringBuilder, ListBuilder, OffsetSizeTrait};
|
||||
use arrow_schema::Field;
|
||||
@@ -87,10 +84,6 @@ impl RegexpMatchesFunc {
|
||||
}
|
||||
|
||||
impl ScalarUDFImpl for RegexpMatchesFunc {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
REGEX_MATCHES_UDF_NAME
|
||||
}
|
||||
|
||||
@@ -247,10 +247,6 @@ impl RegxpMatchToFields {
|
||||
}
|
||||
|
||||
impl ScalarUDFImpl for RegxpMatchToFields {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"regexp_match_to_fields"
|
||||
}
|
||||
|
||||
@@ -13,10 +13,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
any::Any,
|
||||
sync::{Arc, LazyLock as Lazy},
|
||||
};
|
||||
use std::sync::{Arc, LazyLock as Lazy};
|
||||
|
||||
use datafusion::{
|
||||
arrow::{
|
||||
@@ -54,10 +51,6 @@ impl StrMatchUdf {
|
||||
}
|
||||
|
||||
impl ScalarUDFImpl for StrMatchUdf {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
super::STR_MATCH_UDF_NAME
|
||||
}
|
||||
@@ -95,10 +88,6 @@ impl StrMatchIgnoreCaseUdf {
|
||||
}
|
||||
|
||||
impl ScalarUDFImpl for StrMatchIgnoreCaseUdf {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
super::STR_MATCH_UDF_IGNORE_CASE_NAME
|
||||
}
|
||||
|
||||
+10
-10
@@ -267,7 +267,7 @@ impl Condition {
|
||||
}
|
||||
|
||||
pub fn from_physical_expr(expr: &Arc<dyn PhysicalExpr>) -> Self {
|
||||
if let Some(expr) = expr.as_any().downcast_ref::<BinaryExpr>() {
|
||||
if let Some(expr) = expr.downcast_ref::<BinaryExpr>() {
|
||||
match expr.op() {
|
||||
Operator::Eq | Operator::NotEq => {
|
||||
let (field, value) = if is_physical_value(expr.left())
|
||||
@@ -302,11 +302,11 @@ impl Condition {
|
||||
),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else if let Some(expr) = expr.as_any().downcast_ref::<InListExpr>() {
|
||||
} else if let Some(expr) = expr.downcast_ref::<InListExpr>() {
|
||||
let field = get_physical_column_name(expr.expr()).to_string();
|
||||
let values = expr.list().iter().map(get_physical_value).collect();
|
||||
Condition::In(field, values, expr.negated())
|
||||
} else if let Some(expr) = expr.as_any().downcast_ref::<ScalarFunctionExpr>() {
|
||||
} else if let Some(expr) = expr.downcast_ref::<ScalarFunctionExpr>() {
|
||||
let name = expr.name();
|
||||
match name {
|
||||
MATCH_ALL_UDF_NAME => Condition::MatchAll(get_physical_value(&expr.args()[0])),
|
||||
@@ -327,7 +327,7 @@ impl Condition {
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else if let Some(expr) = expr.as_any().downcast_ref::<NotExpr>() {
|
||||
} else if let Some(expr) = expr.downcast_ref::<NotExpr>() {
|
||||
Condition::Not(Box::new(Condition::from_physical_expr(expr.arg())))
|
||||
} else {
|
||||
unreachable!()
|
||||
@@ -705,9 +705,9 @@ impl Condition {
|
||||
|
||||
// TODO: duplication with datafusion/optimizer/physical_optimizer/utils.rs
|
||||
fn is_physical_column(expr: &Arc<dyn PhysicalExpr>) -> bool {
|
||||
if expr.as_any().downcast_ref::<Column>().is_some() {
|
||||
if expr.downcast_ref::<Column>().is_some() {
|
||||
true
|
||||
} else if let Some(expr) = expr.as_any().downcast_ref::<CastExpr>() {
|
||||
} else if let Some(expr) = expr.downcast_ref::<CastExpr>() {
|
||||
is_physical_column(expr.expr())
|
||||
} else {
|
||||
false
|
||||
@@ -716,9 +716,9 @@ fn is_physical_column(expr: &Arc<dyn PhysicalExpr>) -> bool {
|
||||
|
||||
// TODO: duplication with datafusion/optimizer/physical_optimizer/utils.rs
|
||||
fn get_physical_column_name(expr: &Arc<dyn PhysicalExpr>) -> &str {
|
||||
if let Some(expr) = expr.as_any().downcast_ref::<Column>() {
|
||||
if let Some(expr) = expr.downcast_ref::<Column>() {
|
||||
expr.name()
|
||||
} else if let Some(expr) = expr.as_any().downcast_ref::<CastExpr>() {
|
||||
} else if let Some(expr) = expr.downcast_ref::<CastExpr>() {
|
||||
get_physical_column_name(expr.expr())
|
||||
} else {
|
||||
UNKNOWN_NAME
|
||||
@@ -726,11 +726,11 @@ fn get_physical_column_name(expr: &Arc<dyn PhysicalExpr>) -> &str {
|
||||
}
|
||||
|
||||
fn is_physical_value(expr: &Arc<dyn PhysicalExpr>) -> bool {
|
||||
expr.as_any().downcast_ref::<Literal>().is_some()
|
||||
expr.downcast_ref::<Literal>().is_some()
|
||||
}
|
||||
|
||||
fn get_physical_value(expr: &Arc<dyn PhysicalExpr>) -> String {
|
||||
if let Some(literal) = expr.as_any().downcast_ref::<Literal>() {
|
||||
if let Some(literal) = expr.downcast_ref::<Literal>() {
|
||||
match literal.value() {
|
||||
ScalarValue::Boolean(Some(b)) => b.to_string(),
|
||||
ScalarValue::Int64(Some(i)) => i.to_string(),
|
||||
|
||||
@@ -89,7 +89,7 @@ impl VisitorMut for TrackTotalHitsVisitor {
|
||||
value_table_mode: None,
|
||||
exclude: None,
|
||||
flavor: SelectFlavor::Standard,
|
||||
optimizer_hint: None,
|
||||
optimizer_hints: vec![],
|
||||
select_modifiers: None,
|
||||
})));
|
||||
*query = Query {
|
||||
@@ -177,7 +177,7 @@ impl VisitorMut for TrackTotalHitsVisitor {
|
||||
value_table_mode: None,
|
||||
exclude: None,
|
||||
flavor: SelectFlavor::Standard,
|
||||
optimizer_hint: None,
|
||||
optimizer_hints: vec![],
|
||||
select_modifiers: None,
|
||||
})));
|
||||
*query = Query {
|
||||
|
||||
@@ -73,7 +73,7 @@ impl<'n> TreeNodeVisitor<'n> for GroupByFieldVisitor {
|
||||
fn f_up(&mut self, node: &'n Self::Node) -> Result<TreeNodeRecursion> {
|
||||
let name = node.name();
|
||||
if name == "AggregateExec" {
|
||||
let aggregate = node.as_any().downcast_ref::<AggregateExec>().unwrap();
|
||||
let aggregate = node.downcast_ref::<AggregateExec>().unwrap();
|
||||
let group_by = aggregate.group_expr();
|
||||
let fields = group_by
|
||||
.expr()
|
||||
|
||||
@@ -78,7 +78,7 @@ impl ExecutionPlanVisitor for ScanStatsVisitor {
|
||||
type Error = datafusion::common::DataFusionError;
|
||||
|
||||
fn pre_visit(&mut self, plan: &dyn ExecutionPlan) -> Result<bool, Self::Error> {
|
||||
let mayby_remote_scan_exec = plan.as_any().downcast_ref::<RemoteScanExec>();
|
||||
let mayby_remote_scan_exec = plan.downcast_ref::<RemoteScanExec>();
|
||||
if let Some(remote_scan_exec) = mayby_remote_scan_exec {
|
||||
{
|
||||
let guard = remote_scan_exec.scan_stats.lock();
|
||||
|
||||
@@ -49,9 +49,10 @@ pub(super) async fn scan_vortex_async(
|
||||
|
||||
let stream = scan.into_array_stream()?.then(move |result| {
|
||||
let data_type = data_type.clone();
|
||||
let session = session.clone();
|
||||
async move {
|
||||
match result {
|
||||
Ok(array) => vortex_array_to_record_batch(array, &data_type),
|
||||
Ok(array) => vortex_array_to_record_batch(&session, array, &data_type),
|
||||
Err(e) => Err(ArrowError::ExternalError(Box::new(e))),
|
||||
}
|
||||
}
|
||||
@@ -78,7 +79,7 @@ pub(super) fn scan_vortex_row_range(
|
||||
let iter: Box<dyn Iterator<Item = Result<RecordBatch, ArrowError>> + 'static> = Box::new(
|
||||
scan.into_array_iter(&runtime)?
|
||||
.map(move |result| match result {
|
||||
Ok(array) => vortex_array_to_record_batch(array, &data_type),
|
||||
Ok(array) => vortex_array_to_record_batch(&session, array, &data_type),
|
||||
Err(e) => Err(ArrowError::ExternalError(Box::new(e))),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -61,9 +61,13 @@ class TestNewFieldAddition:
|
||||
ingest(client, s, batch2)
|
||||
flush_and_wait(client, s, expected=10)
|
||||
|
||||
for field in ("field_a", "field_b"):
|
||||
n = count_records(client, s, where=f"{field} IS NOT NULL AND {field} != ''")
|
||||
assert n == 10, f"{field} should appear in all 10 rows, got {n}"
|
||||
# field_a is a string — IS NOT NULL AND != '' is a valid emptiness check.
|
||||
n = count_records(client, s, where="field_a IS NOT NULL AND field_a != ''")
|
||||
assert n == 10, f"field_a should appear in all 10 rows, got {n}"
|
||||
# field_b is int — comparing to '' is a numeric/string type mismatch; use
|
||||
# the actual value (1 in every row) instead.
|
||||
n = count_records(client, s, where="field_b = 1")
|
||||
assert n == 10, f"field_b should appear in all 10 rows, got {n}"
|
||||
|
||||
# Use value-specific checks: IS NOT NULL AND != '' is unreliable for
|
||||
# numeric/boolean fields (e.g. field_d=0, field_e=false may look empty).
|
||||
|
||||
Reference in New Issue
Block a user